一、拷贝构造函数
功能:使用一个已经存在的对象来初始化一个新的同一类型的对象
声明:只有一个参数并且参数为该类对象的引用
注意:如果类中没有说明拷贝构造函数,则系统自动生成一个缺省复制构造函数,作为该类的公有成员
下面有一个String的例子,说明拷贝构造函数的使用:
#ifndef _STRING_H_
#define _STRING_H_
class String
{
public:String(char* str="");~String();String(const String& other);operator=(const String& other);void Display();
private:char* AllocAndCpy(char* str);char* str_;};
#endif // _STRING_H_
cpp文件:
#include "String.h"
#include <cstring>
#include <iostream>
using namespace std;String::String(char* str/* = */)
{str_ = AllocAndCpy(str);
}String::~String()
{delete[] str_;
}String::String(const String& other)
{str_ = AllocAndCpy(other.str_);
}String& String::operator =(const String &other)
{if (this == &other)return *this;delete[] str_;str_ = AllocAndCpy(other.str_);return *this;
}char* String::AllocAndCpy(char* str)
{int len = strlen(str) + 1;char* tmp = new char[len];memset(tmp, 0, len);strcpy(tmp, str);return tmp;
}void String::Display()
{cout<<str_<<endl;
}
二、拷贝构造函数调用的几种情况
- 函数的形参是类的对象,调用函数时,进行形参与实参结合时使用。这时要在内存新建立一个局部对象,并把实参拷贝到新的对象中。理所当然也调用拷贝构造函数。如:String a;String b(a);
- 函数的返回值是类对象,函数执行完成返回调用者时使用。理由也是要建立一个临时对象中,再返回调用者。为什么不直接用要返回的局部对象呢?因为局部对象在离开建立它的函数时就消亡了,不可能在返回调用函数后继续生存,所以在处理这种情况时,编译系统会在调用函数的表达式中创建一个无名临时对象,该临时对象的生存周期只在函数调用处的表达式中。所谓return对象,实际上是调用拷贝构造函数把该对象的值拷入临时对象。如果返回的是变量,处理过程类似,只是不调用构造函数。
例子如下:
String func(String a)
{a = "bbb";return a;
}
int main()
{String s1("aaaa");String s2(s1); // 初始化调用拷贝构造函数,我们提供了深拷贝构造String s3 = s2; //上面是等价的初始化操作,我们提供深拷贝String s4 = func(s3);//这个语句调用两次构造函数1、用实参s3初始化形参a 2、用返回值初始化临时对象时s4.Display();return 0;
}
三、深拷贝和浅拷贝
上面的例子中,我们做的都是深拷贝,因为我们提供自己的构造函数,并在构造函数里分配内存。如果我们没有提供自己的构造函数,系统默认会为我们生成自己的构造函数,这个构造函数是浅拷贝的,对于不涉及到显式内存分配的类来说,默认的构造函数是没有问题的,但是对于有显式内容分配的类,比如我们这个String例子,就会出现问题。为什么会出现问题呢?我们来看一下它们的内存模型:
1.浅拷贝
图画的比较挫,将就着看吧。由图可以看出,s1和s2指向的是同一块内存区域,在s1,s2出了作用域之后,会调用各自的构造函数,造成内存的重复释放,引发程序错误。
2.例子里实现的深拷贝
深拷贝的时候会创建对象自己的内存空间,再把原来对象的内容拷贝进去,这样拷贝和被拷贝的就是两个不同的对象。
四、拷贝构造函数和"="操作符的区别
上面的例子中我们已经实现了拷贝构造函数和=操作符,我们在这里说下两者的区别。首先拷贝构造函数用于对象的初始化,在这之前被初始化对象没有任何内容。但是调用=操作符的时候,左边的对象可能已经有值了,我们不能像构造函数那样直接将对象里的指针直接指向新分配的空间,这样指针原来指向的内存空间就没有指向,造成内存泄露。我们必须先把原来的空间释放,在调用拷贝构造函数。还有一种例外的情况,就是=操作符两边是相同的对象,此时我们直接返回对象本身即可。例子如下:
int main()
{String s1("aaaa");String s2(s1); // 初始化调用拷贝构造函数String s3 = s2; //上面是等价的初始化操作String s4 = func(s3);String s5;s5.Display();s5 = s2; // 调用等号运算符s5 = s5; // 系统提供的默认等号运算符实施的是浅拷贝 s3.str_ = s2.str_;// s3.operator=(s2);return 0;
}
五、禁止对象拷贝
要让对象是独一无二的,我们要禁止拷贝,方法是将拷贝构造函数与=运算符声明为私有,并且不提供它们的实现。也可以写一个类noncopy,私有化拷贝构造函数和=运算符。然后让我们需要禁止拷贝的类继承类noncopy即可。
#include <iostream>
using namespace std;
class Noncopy
{
public:Noncopy(){}~Noncopy(){}
private:Noncopy(const Noncopy &a);const Noncopy& operator=( const Noncopy& );
};class Student : public Noncopy
{
public:void setAge(int age) { age_ = age; }int getAge() { return age_; }
private:int age_;
};
int main()
{Student a;a.setAge(21);cout<<a.getAge()<<endl;Student b(a);//出错,不能拷贝return 0;
}