浅复制和深复制
假如类内声明了指针或引用等数据结构,那么在其进行构造新变量的时候就需要额外注意这些数据。
浅复制和深复制的诞生原因大部分是因为类地动态内存分配。
我们想让类成员在程序运行时决定需要多少分配内存,所以我们定义一个成员指针,在其构造函数是使用new运算符动态分配内存。
但请考虑下面的情况。
myString stringA(stringB);
stringA调用复制构造函数将stringB作为参数传递,如果你没有实现复制构造函数,那么编译器会自动生成一个默认复制构造函数。
默认构造函数仅仅只负责将值复制,也就是说它仅仅只是吧stringB中字符串的地址复制到stringA中。这意味着stringA和stringB中存储的字符串是同一个字符串,在程序结束时,stringA率先调用析构函数删除该字符串,这个时候stringB的字符串内容就指向一个已经被释放的地址,这会发生危险。
这个问题在函数pass by value时候极为常见。
以下是复制构造函数调用的四种情况
-
myString ditto(motto);
-
myString metoo=motto;
-
myString also=myString(motto);
-
myString* pms=new myString(motto);
浅复制的另一情况是重载赋值运算符。
同样,如果你没有认真处理重载的运算符,也会发生这样的错误。
假如你没有定义重载赋值运算符
myString stringA;
stringA=stringB;
编译器同样会自动生成重载的赋值运算符,它和复制构造函数一样仅仅只是复制值。
在编写自己的赋值运算符需要记住几点。
-
查看赋值的对象是否是自身,也就是
if(this==&object) return *this;
-
返回常对象参数的引用,这是为了实现连续赋值。
typename& operator=(const typename& object);
-
内部需要delete之前的旧数据
总之,为了防止在创建新对象和赋值时候出现问题,对于哪些只存储值的对象,比如int,double等,可以直接赋值。但是对于指针引用等需要地址的数据,需要释放掉之前的数据,然后重新申请空间,将数据复制过去才行。这就是deep copy
静态成员函数
可以定义静态成员函数,但是静态函数成员不属于任何一个创造的变量,所以无法通过成员运算符调用。
要想正确调用静态成员函数,需要使用域解析运算符。
而且,因为静态成员函数不属于任何变量,所以它也无法访问任何的私有数据成员,只能处理静态成员变量。
定位new
假设我们频繁的使用new来申请空间,内存中很有可能存在简短的狭窄内存,每次申请空间都需要查找足够的内存空间分配,这回带来一些开销。事实上,c++提供一种定位new操作让内存在实现设定好的空间中申请。
new (address) typename(initialization);
由于address已经事先分配好了,所以定位new运算符只负责构建对象,即调用构造函数,而不负责申请空间。
通过定位new运算符,可以在栈上申请空间,也可以在堆上申请空间。
但是有几点需要注意。
- delete能与new配用,但是不能和定位new配用。
char* buffer= new char[512];
A* testA=new (buffer) A;
delete[] buffer;
首先,delete[] buffer不会调用A的析构函数,虽然事实上A的内存空间的确被释放掉了。也许你会想使用delete来删掉A的内存,但是仔细思考,new和delete系统动态的管理目前已经分配的空间,之前申请了512字节的空间被记录下来,之后定位new不申请空间,所以系统还是记录512字节已经被分配。但是由于delete和定位new不配用,如果这个时候调用delete A,那么系统就会减去之前记录的大小也就是512,清空了原有已经分配的内存大小,造成不可预估的后果。
所以delete A实质上是清空了buffer,虽然事实上必须使用delete[]才可以。
- 但是不调用A的析构函数依旧是个问题,所以我们需要显式的调用testA的析构函数。
testA->~A();
这是少数需要显式调用析构函数的情况。