C++第六节课 - 拷贝构造函数
一、复习构造函数
一般情况下,构造函数都需要我们自己去写!
但是有两种情况自己可以不用去写构造函数:
- 内置类型成员都有缺省值,且初始化符合我们的要求;
- 全是自定义类型的构造,且这些类型都定义默认构造;
且对于不同的编译器来说,有的可能也会对内置类型的成员变量进行初始化!
下面分别是在VS2013和VS2019中的结果:
二、复习析构函数
return之后调用析构函数!
注意点:假如当前有一个成员变量如下:
这里的_a[100]需不需要使用析构函数来释放?
答案是:不需要,析构函数用于释放动态申请的资源,例如下面所示,对于静态的资源(在栈上),不需要我们去手动的释放,出了作用域会自动销毁!(如果定义的是全局对象或者静态对象->不在堆上不需要自己手动释放的,都不需要用析构函数来释放!这些程序结束后会自动销毁!)
结论:什么时候需要写析构函数,什么时候不写析构函数?
- 一般情况下,有动态申请资源,就需要显示写析构函数释放资源;
- 没有动态申请的资源,不需要写析构;
- 需要释放资源的成员都是自定义类型,不需要写析构;
例如下面两种不需要构建析构函数:
三、拷贝构造函数
补充知识点:
C++规定:
- 内置类型可以直接拷贝;
- 自定义类型必须调用拷贝构造函数实现拷贝;
- 执行func(Date d)这一函数,实际上是先去Date这个类中执行拷贝构造函数,再运行函数体内的代码;
不仅仅是在传参上面,正常的赋值也是这样的;
假设这里不传入引用:如果我们传入d1,因为d1是自定义类型,传入d1需要调用拷贝构造函数,而调用拷贝构造函数需要传入d1......此时引起无限循环!
拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,
因为会引发无穷递归调用。
因此,传入引用或者指针都可以!
但是一般我们传引用,因为引用比指针用着更加方便;
此时this就是d2,d在这里就是d1(d是d1的别名)!
拷贝构造函数传引用调用的时候一般最好加上const!
加上const之后,此时d的值不能别修改,如果我们意外将数据传反了,编译器会主动提示,方便我们检查! (权限可以缩小,不能方法!)
注意点:权限的private的变量只是在类外的实例化对象不能使用,但是在类内还是能调用的!
例如这里的312行的_year和320的_year是同一个_year吗?
答案:不是的!320的_year只是变量的声明,312中的_year是this的year!
一个是d2的_year,一个是d1的year!
若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按
字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
- 内置类型成员完成值拷贝/浅拷贝;(类似于memcpy,一个字节一个字节的值拷贝过去)
- 自定义类型成员会调用他的拷贝构造;
例如下面:
将d1进行初始化赋值,d2会按照字节拷贝d1;
这里可以发现,日期类可以不写,默认生成的拷贝构造函数就够用;
但是有的情况下我们必须要自己写拷贝构造函数:
如果使用默认的拷贝构造函数,当st1销毁的时候,会调用析构函数来清理,其实st2也会调用析构函数来清理:会造成同一块空间被清理两次!
且对于这种来说,后定义的会先析构:st2会先进行析构!
因此,此时就需要自己实现深拷贝!(关于深拷贝我们后面再讲)
且如果此时st1.push(x)在st2中也有! 两者产生影响!
自己实现的析构函数完成深度拷贝的代码如下:
总结:为什么有的时候不能使用浅拷贝?
- 会析构两次,导致报错;
- 对一个对象进行修改会影响另一个!
这里Data和Queue都不需要我们自己写:(分别对应左边两条准则),但是stack需要自己完成实现!
思考下面这种情况:
void func(Date& d)
{}
void func(Stack st)
{}
int main()
{
Date d1;
func(d1);
Stack st1;
func(st1);
}
- 这里d是d1的别名,通过引用直接对d1进行操作,比较方便;
- 如果stack这里已经实现深拷贝,如果Stack进行传值调用,要进行拷贝构造,如果stack过大,开辟的空间会非常大;
- 如果此时传递的是对象的引用,则不会调用拷贝构造函数。函数将直接操作原始对象。
如果返回的对象出作用域不会被销毁,我们此时可以返回引用(不会调用拷贝构造函数);
如果返回的对象出作用域后被销毁,我们此时使用带引用的返回值!
如果你从一个函数返回一个对象的引用,而该对象在函数返回后超出了作用域,那么返回的引用将指向一个已经被销毁的对象。这种情况会导致未定义行为。
1. 按值返回
- 拷贝构造:当函数返回一个类类型的对象时,通常会调用拷贝构造函数来创建返回值的副本。这意味着返回的对象是原始对象的一个拷贝,函数结束后,原始对象的生命周期不会受到影响。
- 示例:
class MyClass {
public:
int value;
MyClass(int v) : value(v) {}
MyClass(const MyClass &obj) : value(obj.value) { // 拷贝构造函数
// 其他初始化
}
};
MyClass createObject() {
MyClass obj(10); // 局部对象
return obj; // 返回对象的副本
}
int main() {
MyClass myObj = createObject(); // 调用拷贝构造函数
// myObj.value 现在是 10
}
2. 返回值优化 (RVO)
- 返回值优化:现代 C++ 编译器通常会应用返回值优化(Return Value Optimization, RVO),以避免不必要的拷贝。在这种情况下,编译器会直接在调用者的上下文中构造返回对象,从而省略拷贝构造的调用。
- 示例:
MyClass createObject() { return MyClass(10); // 可能会直接在调用者的上下文中构造对象 }
但是注意:引用的返回值不能是出了函数域就销毁的!如果是这种情况只能使用传值返回!
上面一种可以使用引用返回,下面一种不可以!
总结:拷贝构造函数典型调用场景:
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象