C++自动提供以下成员函数:
1.默认构造函数,如果没有定义构造函数。
2.复制构造函数,如果没有定义。
3.赋值操作符,如果没有定义。
4.默认析构函数,如果没有定义。
5.地址操作符,如果没有定义。
一.默认构造函数
如果没有定义构造函数,编译器将提供ClassName::ClassName(){ }这样的空构造函数。这样的函数也可以显式的定义。需要注意的是所有参数都有默认值的构造函数也是默认构造函数。而默认构造函数只能有一个,也就是说不能出
现:
ClassName::ClassName(){ }
ClassName::ClassName(int n=0){ }
这样的定义。
如果希望再创建对象时显示地对它进行初始化,或需要创建对象数组时,则必须显示的定义默认构造函数。
二.复制构造函数
复制构造函数用于将一个对象复制到新创建的对象中。也就是说,它用于初始化过程中,而不是常规的赋值过程中。
原形通常如下:
ClassName(const Classname &);
(1)何时调用复制构造函数
新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。
例如,假设motto是一个ClassName对象,则下面4种声明都将调用复制构造函数:
//calls ClassName(const ClassName&)
ClassName ditto(motto);
ClassName metoo = motto;
ClassName also = ClassName(motto);
ClassName * pClassName = new ClassName(motto);
每当程序生成了对象副本时,编译器都将使用复制构造函数。具体地说,当函数按值传递对象或函数返回对象时,都将使用复制构造函数。因为按值传递意味着创建原始变量的一个副本。编译器生成临时对象时,也将使用复制构造函数。
例如,将3个ob对象相加时,编译器可能生成临时的ob对象来保存中间结果。
(2)复制构造函数的功能
默认的复制构造函数逐个复制非静态成员(成员复制也称为浅复制),复制的是成员的值。
如果成员本身就是类对象,则将使用这个类的复制构造函数来复制成员对象。
静态成员不受影响,因为它们属于整个类,而不是各个对象。
(3)默认复制构造函数的潜藏BUG
由于默认复制构造函数的浅复制规则,将引发潜藏错误。
例如,一个类ClassName,定义了一个以字符指针为形参的构造函数,函数中用new动态分配内存,并用名为char *str的成员指向该内存(str = new char[100])。也就是说,这个类用来封装一个字符数组,但不使用数组,而是使用
字符串指针。同时该类定义了析构函数,函数中使用delete[] str来释放字符串数组。
现在定义一个ClassName的对象ob,ClassName ob("string"),
现在有一个函数T(ClassName dob){}。接收一个ClassName实参,按值传送。
当调用函数时T(ob),这时会创建一个实参的临时副本dob。也就是复制ob到dob,这时将调用复制构造函数,它将逐个复制成员变量。相当于dob.str = ob.str。注意,这里复制仅仅是复制了指针,而字符串"string"在内存中仍旧只
有一个!这时两个对象的成员dob.str和ob.str同时指向"string"。
当函数T()结束时,它将自动调用ClassName类的析构函数来处理临时对象dob,delete[] str,释放dob中的字符数组。最终造成ob.str指向的字符串也被释放。
(4)使用显示复制构造函数来解决问题
解决类设计中这种问题的方法是进行深度复制,也就是说复制构造函数应当复制字符串并将副本的地址赋值给str成员,而不仅仅是赋值字符串地址。那么调用析构函数时都将释放不同的字符串。
注意: 如果类中包含了使用new初始化的指针成员,应当定义一个复制构造函数,来复制指向的数据,而不是指针,这被称为深度复制。复制的另一种形式(成员复制或浅复制)只是复制指针值。浅复制仅复制指针信息,而不会复制指针引用的结构。
三.赋值操作符
C++允许类对象赋值,这是通过自动为类重载赋值操作符实现的。这种操作符的原型如下:
ClassName & ClassName::operator= (const ClassName &);
它接收并返回一个指向类对象的引用。
(1)何时使用赋值操作符
将已有的对象赋值给另一个对象时,将使用重载的赋值操作符:
ClassName ob("string");
ClassName dob;
dob = ob;
同时,在初始化对象时,并不一定会使用赋值操作符。
ClassName dob = ob;
这里dob是一个新创建的对象,被初始化为ob的值,因此使用复制构造函数。不过实现时也可能分两步来处理这条语句:使用复制构造函数创建一个临时对象,然后通过赋值将临时对象的值复制到新对象中。这就是说,初始化总是会调用复制构造函数,而使用=操作符也可能调用赋值操作符。
(2)赋值操作符的功能
与赋值构造函数相似,赋值操作符的隐式实现也对成员进行逐个复制。如果成员本身就是类对象,则程序将使用为这个类定义的赋值操作符来复制该成员,但静态数据成员不受影响。
(3)赋值的潜藏BUG与解决赋值问题
赋值操作符也会出现默认复制构造函数同样的问题:数据受损。
解决办法是提供赋值操作符(进行深度复制)定义。其实现与复制构造函数相似,但有一些差别。
1.由于目标对象可能引用了以前分配的数据,所以函数应使用delete[]来释放这些数据。
2.函数应当避免将对象赋值给自身;否则,给对象重新复制之前,释放内存操作可能删除对象的内容。
3.函数返回一个指向调用对象的引用。
通过返回一个对象,函数可能想常规赋值操作那样,连续进行赋值,即如果S0、S1、S2都是ClassName对象,则可以
S0 = S1 = S2; //S0.operator= (S1.operator= (S2));
下面的代码说明如何为ClassName类编写赋值操作符:
ClassName & ClassName::operator= (const ClassName & dob)
{
if (this == &dob)
return *this; //如果是对象赋值给自身,则直接返回该对象的引用。
delete[] str; //删除当前函数调用对象所使用的动态内存空间。
str = new char [......];
std::strcpy(str,dob.str); //为当前函数调用对象分配新的内存空间,并复制新值。
return *this
}
四、五
默认析构函数不执行任何操作。
隐式地址操作符返回调用对象的地址(即this指针的值)。