最近在学C++,记录自己的所得所悟。
在C++中,当用一个对象去初始化另外一个对象,或者用实参初始化形参,或者从函数返回值的时候,通常都会调用复制构造函数,当把类的一个对象赋值给另外一个对象时,通常会调用类的赋值运算符函数。
1. 初始化与赋值的区别
个人理解:初始化是在构造对象的时候,给对象赋初值,而赋值是将一个对象赋值给已有的另外一个对象,覆盖掉目标对象的内容。
class Person { public: Person() = default; Person(const string& n, int a):name(n), age(a) {} private: string name = ""; int age = 0; }; Person p1; //定义一个Person的对象p1,使用类内初始值进行初始化 Person p2(p1) //定义Person的对象P2,并使用p1去初始化p2,将调用默认复制构造函数 Person p3 = p1; //p3 为新对象,这是初始化,不是赋值,将调用默认复制构造函数 p3 = p2; //p3为已经存在的对象,所以这是赋值不是初始化!
2. 复制构造函数
如果在设计类的时候,没有显式为类定义复制构造函数,那么编译器会为我们生成一个默认的复制构造函数,默认的构造函数执行的是对应数据成员的复制。比如Person类没有显式定义复制构造函数,编译器会为我们生成一个默认的复制构造函数,比如:
Person p1("Megatron", 1000); //p1.name = "Megatron", p1.age = 1000 Person p2(p1); //相当于p2.name(p1.name), p2.age(p1.age), //name和age成员为Person类的私有成员,不能使用p1.name去访问p1的name成员,这么写只是为了方便
相当于构造了一个对象p2,p2成员的值等于p1对应成员的值。
与构造函数类似,当我们自己定义了一个复制构造函数的时候,编译器就不再为我们提供默认版本的复制构造函数。
编译器提供的默认复制构造函数在有的情况下能满足我们的需求,但是如果成员变量中有指针成员的时候,就需要特别注意,编译器提供的往往不能满足我们的需求,需要我们自己进行定义。
class Myarray { public: Myarray()=default; //默认构造函数 Myarray(int length) : size(length), ptr(new int[size]) {} //构造函数 ~Myarray() { delete[] ptr; } private: int size = 0; int* ptr = nullptr; }; int main() { { Myarray arr1(10); //创建一个int类型的动态数组,数组长度为10 Myarray arr2(arr1); //创建一个动态数组arr2,并用arr1去初始化arr2 //此时arr1.size == arr2.size, arr1.ptr == arr2.ptr }//离开内层作用域,arr1和arr2将调用析构函数,对同一块内存空间执行两次delete[] *** 作,程序崩溃 return 0; }
比如上面的一个简易的动态数组类Myarray,里面有一个int* 成员ptr,执行默认的复制构造函数后,arr1的指针成员ptr与arr2的ptr指向同一块内存空间。当对象离开作用域时,调用析构函数,由于两个对象的指针指向同一块内存空间,调用第一个对象的析构函数时,程序正常运行,把指针指向的内存空间释放掉,但是在执行第二个对象的析构函数时,将会对已经delete掉的内存空间再次执行delete *** 作,将导致程序崩溃,这就是所谓的浅拷贝。
所以类中有指针成员时,往往需要执行深拷贝 *** 作,需要自己定义一个拷贝构造函数,拷贝构造函的声明通常如下:
classname(const classname& obj)
注意,形参列表必须是本类对象的引用,如果是classname(const classname obj),将采用值传递的方式传递参数,那么在用实参去初始化形参的时候,将会调用类的复制构造函数,进入死循环!使用const引用是因为不管是const对象还是非const对象都能传进去。对于上面简易的动态数组类,定义一个如下的复制构造函数
Myarray:Myarray(const Myarray& arr) : size(arr.size) { ptr = new int[size]; //重新申请一块同样大小的内存空间 //逐个对象复制 for(int index = 0; index != size; ++index){ ptr[index] = arr.ptr[index]; } } Myarray arr1(10); Myarray arr2(arr1);//调用复制构造函数,arr2和arr1的ptr成员指向不同的内存空间 //对象离开作用域后,不会对同一块内存空间析构两次
2. 析构函数
在对象离开其作用域时,析构函数将会自动调用,用来执行清理工作,比如释放内存,关闭文件,断开数据库连接等等。对在heap(堆)上开辟了动态内存的类(通常含有指针成员),需要在析构函数中手动释放掉开辟的内存空间,将其返还给 *** 作系统,否则申请的这块内存无法得到重复利用,导致内存泄漏。
需要注意的是,如果构造函数中使用new动态分配内存,那么在析构函数中则使用delete释放内存;如果使用new[]分配内存,则使用delete[]释放内存。
3. 拷贝赋值运算符
如果没有为类显式重载赋值运算符,那么编译器也会为我们提供默认的赋值运算符,执行的是把右侧对象的成员赋值给左侧对象的对应成员,如果类内没有指针成员时,默认的赋值运算符能满足我们的需求。但是如果内中有指针成员时,默认的赋值运算符会带来两个问题,比如:
Myarray arr1(10); Myarray arr2(10); arr1 = arr2;//赋值 *** 作,其实就是arr1.size = arr2.size, arr1.ptr = arr2.ptr
(1)与默认的复制构造函数相似,默认的赋值运算符将执行指针成员的拷贝,是浅拷贝,在析构的时候也会出现对同一块内存释放两次而导致程序崩溃;
(2)内存泄漏:在赋值之前,arr1对象的ptr成员指向了一块动态内存,是属于arr1自己持有的资源,默认的赋值 *** 作后,左侧对象arr1的指针成员ptr被赋予了新地址,arr1的ptr指针将指向新的内存空间,导致原来指向的内存空间现在没有指针指向了,这一块内存将无法归还给 *** 作系统,造成内存泄漏!
所以,在类含有指针成员时,必须为类定义一个拷贝赋值运算符,就是重载= *** 作符,通常的声明如下:
classname& operator= (const classname& obj);
比如对简易的动态数组类Myarray,可以定义如下的拷贝赋值运算符:
Myarray::Myarray& operator= (const Myarray& arr) { if(this == &arr) //检测赋值是否为自我赋值,如果为自我赋值,直接返回 return *this; delete[] ptr; //将=左侧对象的ptr成员指向的内存空间先释放掉 ptr = new int[arr.size];//重新为=左侧对象申请内存空间 //执行深拷贝 for(int i = 0; i != size; ++i){ ptr[i] = arr.ptr[i]; } }
4. 移动构造函数
C++11增加了移动语义,从而有了所谓的移动构造函数,移动构造函数的参数是右值引用。顾名思义,C++的左值指的是能放在赋值运算符“=”左侧,能用取地址运算符&获取其地址的对象,比如一个变量,引用都是左值;而右值指的是只能出现在赋值运算符的右侧,不能用取地址运算符&获取其地址的对象,比如临时对象,字面量,表达式,函数的返回值等都是右值。比如声明一个如下的函数:
Myarray func(int size) { ... return Myarray(size); }
函数的返回类型为Myarray类型,在程序从函数中将对象返回给调用func的函数时,将会调用复制构造函数,执行一次深拷贝,由于Myarray(size)是一个没有名字的临时对象,是一个即将消亡的右值,可以定义如下的移动构造函数来避免深拷贝
Myarray::Myarray (Myarray&& arr) : size(arr.size), ptr(arr.ptr) { // && 表示右值引用 //必须将arr的ptr指针置为空指针,否则一个构造出来的对象的ptr与临时对象的ptr指向同一块内存 arr.ptr = nullptr; //这一步必不可少 }
上面的移动构造函数将临时对象arr所持有的资源转移出去了,避免了深拷贝。同时,将一个即将消亡的临时对象arr的ptr成员设置为nullptr,析构的时候,delete[] 一个nullptr不会有任何问题。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)