写在前面:
本文主要介绍了拷贝构造函数和赋值运算符的区别,简单的分析了深拷贝和浅拷贝的问题,以及在什么时候调用拷贝构造函数、什么情况下调用赋值运算符。
————————————————————————————————————————————
在默认情况下(用户没有定义,但是也没有显式的删除),编译器会自动的隐式生成一个拷贝构造函数和赋值运算符。但如果用户将拷贝构造函数和赋值运算符定义成私有的(private),则用户不能使用拷贝构造和对象赋值。可以使用delete来指定不生成拷贝构造函数和赋值运算符,这样的对象就不能通过值传递,也不能进行赋值运算。
class Person
{
public:
Person(const Person& p) = delete;
Person& operator=(const Person& p) = delete;
private:
int age;
string name;
};
上面的定义的类Person显式的删除了拷贝构造函数和赋值运算符,在需要调用拷贝构造函数或者赋值运算符的地方,会提示无法调用该函数,它是已删除的函数。
注意:拷贝构造函数必须以引用的方式传递参数。这是因为以值传递的方式传递给一个函数的时候,会调用拷贝构造函数生成函数的实参。如果拷贝构造函数的参数仍然是以值的方式,就会无限循环的调用下去,直到函数的栈溢出。
拷贝构造函数和赋值运算符的行为比较相似,都是将一个对象的值复制给另一个对象;但是其结果却有些不同,拷贝构造函数使用传入对象的值生成一个新的对象的实例,而赋值运算符是将对象的值复制给一个已经存在的实例。这种区别从两者的名字也可以很轻易的分辨出来,拷贝构造函数也是一种构造函数,那么它的功能就是创建一个新的对象实例;赋值运算符是执行某种运算,将一个对象的值复制给另一个对象(已经存在的)。调用的是拷贝构造函数还是赋值运算符,主要是看是否有新的对象实例产生。如果产生了新的对象实例,那调用的就是拷贝构造函数;如果没有,那就是对已有的对象赋值,调用的是赋值运算符。
1.什么时候调用拷贝构造函数?
(1)对象作为函数的参数,以值传递的方式传给函数。
(2)对象作为函数的返回值,以值的方式从函数返回
(3)使用一个对象给另一个对象初始化。
假设bird是一个Fly对象,则下面四种声明都将调用拷贝构造函数:
Fly look(bird);
Fly str = bird;
Fly dump = Fly(bird);
Fly * pfly = new Fly(bird);
其中中间的两种声明可能会使用拷贝构造函数直接创建 str 和 dump,也可能使用拷贝构造函数生成一个临时对象,然后将临时对象的内容赋给 dump和 bird,取决于具体的实现。最后一种声明使用 bird初始化一个匿名对象,并将新对象的地址赋给 pfly指针。
当程序生成了对象副本时,编译器都将使用拷贝构造函数。具体地说,当函数按值传递对象或函数返回对象时,都将使用拷贝构造函数。记住,按值传递意味着创建原始变量的一个副本。编译器生成临时对象时,也将使用拷贝构造函数。
因为按值传递对象将调用拷贝构造函数,所以应该按引用传递对象。这样可以节省调用构造函数的时间以及存储新对象的空间。
2.默认拷贝构造函数的功能
默认的拷贝构造函数逐个复制非静态成员(成员复制也成为浅拷贝),复制的是成员的值。
下面语句:
Fly bird = animal;
与下面的代码等效:
Fly bird;
bird.str = animal.str;
bird.len = animal.len;
上述代码是会出现浅拷贝的错误的,原因在于默认拷贝构造函数是按值进行复制的。
bird.str = animal.str;
这里复制的不是字符串,而是一个指向字符串的指针。也就是说,将 bird初始化为 animal后,得到的是两个指向同一个字符串的指针。当析构函数被调用时,这将引发问题。析构函数释放 str 指针指向的内存,因此释放 bird 的效果如下:
delete []bird.str;
但是它被赋值为animal.str,而animal.str指向的正是上述字符串,但上述字符串所指向的内存已经被释放,然后程序释放animal如下:
delete []animal.str
但是animal.str指向的内存已经被bird的析构函数释放,这将导致不确定的、可能有害的后果,试图释放内存两次可能导致程序异常终止,这通常是内存管理不善的表现。
3.定义一个显式拷贝构造函数以解决问题
解决类设计中这种问题的方法是进行深度复制(deep copy),也就是说,拷贝构造函数应当复制字符串并将副本的地址赋给str成员,而不仅仅是复制字符串的地址。这样每个对象都有自己的字符串,而不会试图去释放已经被释放的字符串。
可以这样编写String的复制构造函数:
Fly::Fly(const Fly & st)
{
len = st.len;
str = new char[len + 1];
std::strcpy(str, st.str);
}
必须定义拷贝构造函数的原因在于:
一些类成员是使用new初始化的、指向数据的指针,而不是数据本身。
小结:
如果类中包含了使用new初始化的指针成员,应当定义一个拷贝构造函数,以复制指向的数据,而不是指针,这被称为深度复制(即深拷贝)。复制的另一种形式(成员复制或浅复制)只是复制指针值。浅拷贝仅浅浅地复制指针信息,而不会深入“挖掘”以复制指针引用的结构。
C++允许类对象赋值,这是通过自动为类重载赋值运算符实现的。这种运算符的原型如下:
School_name & School_name::operator=(const Shool_name &);
它接受并返回一个指向类对象的引用,Fly类的赋值运算符的原型如下:
Fly & Fly::operator=(const Fly &);
1.赋值运算符的功能及何时使用它
将已有的对象赋给另一个对象时,将使用重载的赋值运算符:
Fly head("hello");
Fly knot;
knot = head;
初始化对象时,并不一定会使用赋值运算符:
Fly p = knot;
这里,p是一个新创建的对象,被初始化为knot的值,因此使用拷贝构造函数。然而,正如前面所指出的,实现时也可能分两步来处理这条语句:使用拷贝构造函数创建一个临时对象,然后通过赋值将临时对象的值复制到新对象中。这就是说:初始化时总是会调用拷贝构造函数,而使用=运算符也可能调用赋值运算符。
与拷贝构造函数相似,赋值运算符的隐式实现也对成员进行逐个复制,即浅拷贝。如果成员本身就是类对象,则程序将使用为这个类定义的赋值运算符来复制该成员,但静态数据成员不受影响。
2.解决赋值的问题
(1)对于由于默认赋值运算符不合适而导致的问题,解决办法是提供赋值运算符(进行深拷贝)定义。其实现与拷贝构造函数相似,但也有一些差别。
(2)由于目标对象可能引用了以前分配的数据,所以使用函数detele[]来释放这些数据。
(3)函数应当避免将对象赋给自身;否则,给对象重新赋值前,释放内存操作可能删除对象的内容。
(4)函数返回一个指向调用对象的引用。
按照如下编写赋值运算符:
Fly & Fly::operator=(const Fly & st)
{
if(this == &st)
return *this;
detele []str;
len = st.len;
str = new char[len + 1];
std::strcpy(str, st.str);
return *this;
}
代码首先检查自我复制,这是通过查看赋值操作符右边的地址(&st)是否与接收对象(this)的地址相同来完成的,如果相同,程序将返回*this,然后结束。
如果地址不同,函数将释放str指向的内存,这是因为稍后将把一个新字符串的地址赋给str。如果不首先使用delete操作符,则上述字符串将保留在内存中。由于程序程序不再包含指向字符串的指针,因此这些内存被浪费掉。
接下来的操作与拷贝构造函数相似,即为新字符串分配足够的内存空间,然后复制字符串。
上述操作完成后,程序将返回*this并结束。
3.具体的说,该方法应完成这些操作:
(1)检查自我赋值情况
(2)释放成员指针以前指向的内存
(3)复制数据而不仅仅是数据的地址
(4)返回一个指向调用对象的引用
4.注意new和delete的使用(记得总结。。。)