拷贝构造函数 和 赋值运算符(C++)

写在前面:

本文主要介绍了拷贝构造函数和赋值运算符的区别,简单的分析了深拷贝和浅拷贝的问题,以及在什么时候调用拷贝构造函数、什么情况下调用赋值运算符。
————————————————————————————————————————————

一、 拷贝构造函数和赋值运算符

在默认情况下(用户没有定义,但是也没有显式的删除),编译器会自动的隐式生成一个拷贝构造函数和赋值运算符。但如果用户将拷贝构造函数和赋值运算符定义成私有的(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的使用(记得总结。。。)

你可能感兴趣的:(c++)