C++学习笔记:拷贝构造函数、深拷贝与浅拷贝
1.拷贝构造函数的引入
1.1 用对象来初始化对象
简单变量定义时,可以直接初始化,也可以用另一个同类型变量来初始化。
举个栗子:
//直接初始化,定义的时候直接给定一个初始值
int a = 4;
//间接初始化,用另一个同类型的变量来初始化
int x=5;
int y=x;
用class
来定义对象时,可以直接初始化,也可以用另一个对象来初始化(class
是类型,定义出来的对象类似于变量)。
举个栗子:
//方式一:直接初始化
xx::person tom("tom",15,true);
//方式二:间接初始化,用另一个对象来初始化新定义的对象、
xx::person p2(tom);//写法1
//x::person p2=tom;//写法2
std::cout<<p2.name<<" "<<p2.age<<" "<<p2.male<<std::endl;
//输出地址
std::cout<<&tom<<std::endl;
std::cout<<&p2<<std::endl;
输出:
tom 15 1
0x7ffd8d6a6310
0x7ffd8d6a6350
可以看出,p2
是新对象与tom
地址不同,并且内部的属性已经被用tom
来初始化了。
1.2 为什么可以
普通变量的直接初始化是变量在被分配内存之后直接用初始化值去填充赋值完成初始化;变量用另一个变量来初始化是给变量分配了内存后执行了一个内存复制操作来完成的初始化。
对象的直接初始化是对象在分配内存之后调用了相应的构造函数来完成的初始化;对象的用另一个对象来初始化,是对象在分配之后调用了相应的拷贝构造函数来完成初始化
1.3 什么是拷贝构造函数
拷贝构造函数是构造函数的一种,符合构造函数的一般性规则(没有返回值,函数名同类名等),与构造函数一样,C++
也提供了默认构造函数(这就是上面代码中我们啥都没写,但是依然可以通过两种写法来间接初始化对象的原因)。拷贝构造函数的引入是为了让对象在初始化时能够像简单变量一样的可以被直接用=
来赋值。
![](/qrcode.jpg)
拷贝构造函数不需要重载,拷贝构造函数只接受一个参数,就是该类型对象的const
引用,所以它的参数列表固定为const classname& xx
。拷贝构造函数很合适用初始化列表来实现。
举个栗子:
namespace xx{
//声明类
class person
{
public:
//属性
std::string name;
int age;
bool male;
//默认拷贝构造函数
person(const person&pn);
};
};
//一般赋值初始化
xx::person::person(const person&pn)
{
this->name=pn.name;
this->age=pn.age;
this->male=pn.male;
}
//使用初始化列表
xx::person::person(const person&pn):name(pn.name),age(pn.age),male(pn.male)
{
}
2.浅拷贝与深拷贝
拷贝构造函数有两种构造方式,深拷贝和浅拷贝。
2.1 浅拷贝的缺陷
上面讲的只有普通成员变量初始化的拷贝构造函数就是浅拷贝。
普通成员变量是成员占用的内存就在对象本身上,比如类内的
int age
等。特殊成员变量是指针指向的需要动态分配内存的特殊成员。
namespace xx{
class person
{
public:
//普通成员变量
std::string name;
int age;
bool male;
//特殊成员变量
int* p_int;
person(const person&pn);
void print(void);
};
};
如果不显式提供拷贝构造函数,C ++
会自动提供一个全部普通成员被浅拷贝的默认拷贝构造函数,浅拷贝在遇到有动态内存分配时就会出问题。
举个栗子:
#include"person.hpp"
xx::person::person(std::string name,int age,bool male){
this->name=name;
this->age=age;
this->male=male;
this->p_int = new int(55);//分配了一个值为55的int
}
xx::person::~person(){
std::cout<<"默认析构函数"<<std::endl;
delete this->p_int;;
}
//默认拷贝构造函数
xx::person::person(const person&pn):name(pn.name),age(pn.age),male(pn.male),p_int(pn.p_int)
{
}
void xx::person::print(void){
std::cout<<this->name<<" "<<this->age<<" "<<this->male<<" "<<*this->p_int<<std::endl;
}
主程序:
int main(int argc,char**argv)
{
std::string name="tom";
person tom(name,15,1);
person p2(tom);
tom.print();
p2.print();
return 0;
}
输出:
tom 15 1 55
tom 15 1 55
默认析构函数
*** Error in `./person': munmap_chunk(): invalid pointer: 0x00000000004013c0 ***
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(+0x777f5)[0x7f3812b787f5]
/lib/x86_64-linux-gnu/libc.so.6(cfree+0x1a8)[0x7f3812b856e8]
./person[0x401248]
./person[0x400ff1]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7f3812b21840]
./person[0x400e39]
======= Memory map: ========
00400000-00402000 r-xp 00000000 00:2f 19 /mnt/hgfs/winshare/per/person
00601000-00602000 r--p 00001000 00:2f 19 /mnt/hgfs/winshare/per/person
00602000-00603000 rw-p 00002000 00:2f 19 /mnt/hgfs/winshare/per/person
00ecd000-00eff000 rw-p 00000000 00:00 0 [heap]
7f38127f8000-7f3812900000 r-xp 00000000 08:01 964007 /lib/x86_64-linux-gnu/libm-2.23.so
...
7ffeb79c9000-7ffeb79cb000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
Aborted (core dumped)
从输出结果可以看出,tom
的每个属性是被赋值到p2
中去的,并且print()
函数也正确的执行了,但是后面跟了一大堆错误日志。
只打印出一次"默认析构函数"
可以看出实际上这种错误在发生在main()
函数快要结束的时候,因为tom
和p2
都是分配在栈上的,所以在main()
快要结束的时候会自动执行这两个对象的析构函数。
tom.p_int
指向的空间是其本身构造函数中new
出来的,而p2._pint
是由执行拷贝构造函数时tom.p_int
赋值而来的,使得这两个指针指向了同一个空间;在执行tom
的析构函数时,这块地址被释放了,这就导致执行p2
的析构函数时,再次释放这块地址出现了错误。问题的本质在于new
了1次,但是析构函数中却delete
了2次.
2.2 如何解决这个问题
解决方法就是不要用默认的拷贝构造函数,自己显式提供一个拷贝构造函数,并且在其内部再次分配动态内存:
xx::person::person(const person&pn):name(pn.name),age(pn.age),male(pn.male))
{
this->p_int = new int(*pn.p_int);
}
正常运行:
tom 15 1 55
tom 15 1 55
默认析构函数
默认析构函数
这就叫深拷贝,深的意思就是不止给指针变量本身分配内存,也给指针指向的空间再分配内存(如果有需要还要复制内存内的值);一般如果不需要深拷贝,根本就不用显式提供拷贝构造函数,所以提供了的基本都是需要深拷贝的;拷贝构造函数不需要额外的析构函数来对应,用的还是原来的析构函数。
浅拷贝和深拷贝的问题不是C++
特有的,Java
,Python
等语言也会遇到,只是语言给封起来了,而C++
需要类作者自己精心处理。从编程语言学角度讲,本质上是值语义value symatics
和引用语义reference symatics
的差别。