目录:
拷贝构造函数
例:浅拷贝 / 深拷贝实例 | 拷贝构造函数的调用时机: | 例:拷贝构造函数的3种调用时机实例 |
---|
常见问题
1. 左值和右值 | 2. 拷贝构造函数的引用 | 3. 拷贝构造函数的const |
---|
C++ 中经常会使用一个变量初始化另一个变量,如
int x = 1;
int y = x;
我们希望这样的操作也能作用于自定义类类型,如
Point pt1(1, 2);
Point pt2 = pt1;
这两组操作是不是一致的呢?第一组好说,而第二组只是将类型换成了 Point 类型,执行 Point pt2 = pt1;语句时, pt1 对象已经存在,而 pt2 对象还不存在,所以也是这句创建了 pt2 对象,既然涉及到对象的创建,就必然需要调用构造函数,而这里会调用的就是 复制构造函数,又称为 拷贝构造函数。
当我们进行测试时,会发现我们不需要显式给出拷贝构造函数,就可以执行第二组测试。这是因为如果类中没有显式定义拷贝构造函数时,编译器会自动提供一个缺省的拷贝构造函数。其原型是:
类名::类名(const 类名&);
那缺省(默认)的拷贝构造函数是如何实现的呢?很简单,我们来实现一下 Point 类的拷贝构造函数:
Point::Point(const Point & rhs)
: _ix(rhs._ix)
, _iy(rhs._iy)
{}
由于 Point 的成员比较简单,缺省的拷贝构造函数已经可以满足需求了,所以可以不显式定义。接下 来,我们把目光转向 Computer 类,如果 Computer 类使用缺省拷贝构造函数,会发生什么问题呢?我们先来看看 缺省的拷贝构造函数的实现
Computer::Computer(const Computer & rhs)
: _brand(rhs._brand)
, _price(rhs._price)
{}
//执行构造初始化
Computer pc1("Huawei Matebook14", 5699);
Computer pc2 = pc1;
从上面的定义来看, pc1 与 pc2 对象的数据成员 _brand 都会指向同一个堆空间的字符串,这种只拷贝指针的地址的方式,我们称为浅拷贝。当两个对象被销毁时,就会造成 double free 的问题。显然, 缺省拷贝构造函数不再满足需求,此时需要显式定义拷贝构造函数:
Computer::Computer(const Computer & rhs)
: _brand(new char[strlen(rhs._brand) + 1]())
, _price(rhs._price)
{
strcpy(_brand, rhs._brand);
}
这种拷贝指针所指空间内容的方式,我们称为深拷贝。因为两个对象都拥有各自的独立堆空间字符串, 一个对象销毁时就不会影响另一个对象。
#include
#include
#include
#include
using std::cout;
using std::endl;
class Student
{
public:
//构造函数
Student(const char * name, int score)
: _name(new char[strlen(name) + 1]())
, _score(score)
{
strcpy(_name, name);
cout << "Student" << endl;
}
//拷贝构造函数-浅拷贝
//Student(const Student & rhs)
//: _name(rhs._name)
//, _score(rhs._score)
//{
// cout << "Student copy" << endl;
//}
//拷贝构造函数-深拷贝
Student(const Student & rhs)
: _name(new char[strlen(rhs._name) + 1]())
, _score(rhs._score)
{
strcpy(_name, rhs._name);
cout << "Student copy" << endl;
}
//参数score修改
void setScore(int score)
{
_score = score;
}
//参数name修改
void setName(const char * name)
{
strcpy(_name, name);
}
//打印函数
void print() const
{
cout << "name:" << _name << endl
<< "score:" << _score << endl;
}
//析构函数
~Student()
{
delete [] _name;
cout << "~Student" << endl;
}
private:
char * _name;
int _score;
};
void func()
{
Student stu1("Lones", 30);
cout << "stu1" << endl;
stu1.print();
cout << endl << "stu2" << endl;
Student stu2 = stu1;
stu2.print();
cout << endl;
stu2.setScore(20);
stu2.setName("pp");
cout << "stu1" << endl;
stu1.print();
cout << endl << "stu2" << endl;
stu2.print();
cout << endl;
}
int main()
{
func();
return 0;
}
运行结果(浅拷贝和深拷贝):
因为stu2是stu1的拷贝,可看见对于stu2的name的修改,浅拷贝共用一块内存,所以stu1也发生改变;深拷贝是各自的内存,所以stu2对name的修改,并没有修改stu1。
浅拷贝
调用一次构造函数,调用两次析构函数,两个对象的指针成员所指同一块内存,这会导致指针被分配一次内存,但是程序结束时该内存却被释放了两次,会导致崩溃!由于编译系统在我们没有自己定义拷贝构造函数时,会在拷贝对象时调用默认拷贝构造函数,进行的是浅拷贝!即对指针name拷贝后会出现两个指针指向同一个内存空间。
深拷贝
在对含有指针成员的对象进行拷贝时,必须要自己定义拷贝构造函数,使拷贝后的对象指针成员有自己的内存空间,即进行深拷贝,这样就避免了内存泄漏发生。
浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间;
深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。
#include
#include
#include
#include
using std::cout;
using std::endl;
class Student
{
// ...
};
//时机1:已经存在的对象赋值给另一个新对象
void func()
{
Student stu1("Lones", 30);
cout << "stu1" << endl;
stu1.print();
cout << endl << "stu2" << endl;
Student stu2 = stu1;
stu2.print();
cout << endl;
stu2.setScore(20);
stu2.setName("pp");
cout << "stu1" << endl;
stu1.print();
cout << endl << "stu2" << endl;
stu2.print();
cout << endl;
}
//时机2:当实参和形参(函数参数)都是对象,进行实参与形参的结合
void func2_ret(Student stu)
{
stu.print();
}
void func2()
{
Student stu1("Jack", 66);
func2_ret(stu1);
}
//时机3:当函数的返回值是对象,函数调用完成返回时(执行return)
Student func3_ret()
{
Student stu1("Jason", 44);
stu1.print();
return stu1;
}
void func3()
{
Student stu2 = func3_ret();
}
void func4()
{
const Student &rhs = func3_ret();
}
int main()
{
func();
cout << endl << endl;
func2();
cout << endl << endl;
func3();
cout << endl << endl;
func4();
cout << endl << endl;
return 0;
}
时机1,时机2都会调用拷贝构造函数,注意的是时机3,在直接使用g++编译时,不会显示调用拷贝构造函数,这是因为测试右值,临时对象的构造,编译器会自动优化导致没有打印。编译时在末尾添加 -fno-elide-constructors
关闭自动优化,就会正常打印该显示的拷贝构造函数
我们详细看一下func3,func4
func3调用 两次 拷贝构造函数 — return stu1;
返回的是一个值,编译器首先会创建一个临时变量拷贝下这个stu1(匿名对象), 这时调用了拷贝构造函数(变量或者对象,若结果是一个值就直接返回,若结果是一个对象则会产生一个匿名对象,将会调用拷贝构造函数将需要返回的对象赋值给匿名对象,然后把匿名对象返回出去,func3_ret()
函数执行完毕原来需要返回的对象就会被析构掉,匿名对象才是真的返回值),这是第一次拷贝调用;再是函数中stu2
接收func3_ret();
的返回值(复制),这是第二次拷贝调用
func4调用 一次 拷贝构造函数 — return stu1;
进行了一次拷贝调用,然后在接收返回值时,使用了左值引用来接收func3_ret();
的返回值,因为是引用所以没有调用拷贝。
左值: 可以取地址
右值: 一般是 临时对象 或 字面值常量 ,不可以取地址,生命周期只有本行,在内存没有存储
通常以下三种情况会产生临时对象:
1,以值的方式给函数传参;
2,类型转换;
3,函数需要返回一个对象时;
调用拷贝构造函数去掉引用符号,形参是对象,无穷递归调用拷贝构造函数,没有出口,最后栈溢出。
拷贝构造函数
Student(const Student rhs)
函数里
Student p2 = p; //调用拷贝构造函数
传参(值传递,复制)时 const Student rhs = p; //又调用拷贝构造函数
传参(值传递,复制)时 const Student rhs1 = rhs; //又调用拷贝构造函数
传参(值传递,复制)时 const Student rhs2 = rhs1; //又调用拷贝构造函数
…
无限递归调用
const是不可以去掉的,如果const去掉,当传递一个临时对象(右值),无法调用拷贝构造函数,而且会报错。