一般构造器、析构器、拷贝构造器会被称之为构造函数、析构函数与复制构造函数,但是其与函数还是有一些区别,所以我们以“器”来称呼之。
(1)、构造器简析:
constructor 构造器 * 与类名相同A(),(A为类名),无返回值,生成对象时系统自行调用,相当于初始化 * 可以有参数,能够重载、以及设置默认参数 * 如果不自定义任何构造器,则系统会有一个默认的无参构造器生成,若自定义了一个构造器,则系统不再生成构造器 * 注意:无参数构造器与有参数但是有默认参数的构造器,定义对象时可能会产生二义性 * 所以重载与默认参数不要在同一个函数名中同时出现
(2)、析构器简析:
destructor 析构器 * 格式:~A(),无参数、无返回值、用于对象销毁时的内存处理工作(对象消失时,自动被调用) * 所谓消失,指的是跳出其作用空间,且以后不会再使用 * 若无自定义析构器,则系统默认生成一个析构器 * 对于系统自行析构的,先创建的后析构、后创建的先析构 * 由于析构器无参数,所以不存在重载的问题
(3)、拷贝构造器:
copy constructor 拷贝构造器 * 格式:A(const A &); * 若不自定义,则采用系统默认的拷贝构造器(类似于构造器) * 系统提供的拷贝构造器,默认是一个等位拷贝,即江湖上传闻的“浅拷贝”,浅拷贝可能会导致内存重析构(double free),但是内存重析构在有些系统上可能不会表现出来(如Windows)。 * 在有些情况(对象含有堆空间的时候),要自定义实现拷贝构造器 * 拷贝构造是一个从无到有的过程(用一个已有的对象,完成另一个对象从无到有并初始化的过程),而构造是一个从有到初始化的过程
eg:(注意一下几种写法的区别) string a("China");//构造 string b = a;//拷贝构造 string c(a);//拷贝构造 string d;//构造 d = a;//赋值运算符重载所谓内存重析构,其实就是同一块堆内存被释放了两次,第一次释放没有什么问题,但是第二次属于free(NULL);的操作,这种内存重析构(重复析构)存在逻辑性的致命错误。 而浅拷贝由于只存在一份堆空间,却存在两个对象拥有指向该空间的指针,所以析构时,两个指针指向的空间都会被回收,但是第二次回收,指针NULL无指向、无效。但深拷贝析构时,各自对象有各自的堆内存,就不会存在这种重析构出错的情况(如下图)。(浅拷贝存在的这种问题在有的平台上或许不会变现出来)
以实例(string类的拷贝构造器)说明拷贝构造器的浅拷贝与内存重析构:
/*mystring.h*/ #ifndef MYSTRING_H #define MYSTRING_H #include <stdio.h> #include <string.h> class MyString { public: MyString(const char *p = NULL);//构造器 ~MyString();//析构器 MyString(const MyString & another);//拷贝构造器 char * c_str();//返回堆中字符串的首地址 private: char * _str; }; #endif /*mystring.cpp*/ #include "mystring.h" MyString::MyString(const char *p) { if(p == NULL){ _str = new char[1];/*定义成数组,与else对应,方便析构*/ *_str = '\0'; }else{ int len = strlen(p); _str = new char[len]; strcpy(_str, p); } } MyString::~MyString() { delete []_str; } MyString::MyString(const MyString & another) { #if 0 _str = another._str;//浅拷贝,与系统默认的一样 #endif #if 1 /*深拷贝*/ int len = strlen(another._str); _str = new char[len]; _str = another._str; #endif } char * MyString::c_str(){ return _str; } /*main.cpp*/ #include <iostream> #include "mystring.h" using namespace std; int main() { string s1; string s2("string s2"); string s3(s2); cout<<"s1:"<<s1.c_str()<<endl; cout<<"s2:"<<s2.c_str()<<endl; cout<<"s3:"<<s3.c_str()<<endl; cout<<"\n*****************************\n"<<endl; MyString S1;//由默认参数构造器完成 MyString S2("String S2");//由非默认参数参构造器完成 MyString S3(S2);//由拷贝构造器实现,而不是有参数的构造器实现 /*由于<<没有重载,所以暂时用c_str()来输出字符串数组*/ cout<<"S1:"<<s1.c_str()<<endl; cout<<"S2:"<<s2.c_str()<<endl; cout<<"S3:"<<s3.c_str()<<endl; return 0; }我们先采用浅拷贝的方式来编译运行(运行时出错提示:double free):
再将浅拷贝注释掉,用深拷贝来编译运行(结果正常):
其实每一个类中,系统不但默认存在以上三种“器”,还存在默认的赋值运算符重载:
(默认)赋值运算符重载: * 格式:A& operator=(A&); * 系统/编译器提供默认重载, * 默认赋值运算符重载也是一种等位赋值(浅赋值),若是自定义,编译器不再提供 * 浅复制不但会导致重析构,还会导致自身内存泄漏; * 对于自赋值需要特别注意。
为什么说它是穿着马甲的浅拷贝?又为什么说默认赋值运算符重载会导致内存重析构与内存泄漏呢?我们画张图来分析:
/*系统默认的类似于如下所示*/ MyString& MyString::operator=(const MyString & another) { this->_str = another._str;//只是复制了字符串的地址 return *this; }首先默认的赋值运算符重载是浅拷贝才会导致内存泄露,其次对象析构时会产生重析构。对于赋值运算符重载,在对象含有堆空间的申请与释放时,也需要自行定义,不能够使用系统默认的。对于我们MyString的例子来说,其非默认实现如下:
MyString& MyString::operator=(const MyString & another) { if(this == &another) return *this;//自赋值,不会删除自身空间,直接返回,排除自赋值逻辑错误 delete []this->_str;//先删除自身,否则会导致内存泄露 int len = strlen(another._str);//求赋值对象的长度 this->_str = new char[len+1];//根据赋值对象为被赋值对象创建新的空间,防止重析构。 strcpy(this->_str, another._str);//复制 return *this;//返回this以实现连等:S1 = S2 = S3 = ...; }上面所说自赋值逻辑错误即:如果S1 = S1;那么默认按(先删除自身->再求长度->创建新空间->复制)的步骤走肯定是不对的,因为第一步删除S1后,后面的步骤丢失掉了依赖,即所谓逻辑出错。
即使是浅拷贝这种致命的逻辑错误,在Windows下也可能不会有异常:
但是对于这种潜在的隐患,我们一定要注意,在对象有堆空间时,一定要用深拷贝,而不能用浅拷贝。对于C++的在这种浅拷贝的问题是因为char*引起来的,也就是为了兼容C而引起的。在纯C++语法编写的程序中,由于不会用到char数组、char*等,就不会有浅拷贝深拷贝的问题了。