前言
上个章节讲述了构造函数和析构函数,本节将讲解拷贝构造函数和赋值运算符重载等知识。
1.拷贝构造函数
1.1函数定义
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是一个特殊的构造函数。
1.2函数特点
1. 拷贝构造函数是 构造函数的一个重载。
2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。
3. C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
4. 若未显式定义拷贝构造,编译器会生成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。
5. 传值返回会产生一个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名(引用),没有产生拷贝。但是如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个野引用,类似一个野指针一样。传引用返回可以减少拷贝,但是一定要确保返回对象,在当前函数结束后还在,才能用引用返回。
代码解释
#include <iostream> using namespace std; class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //拷贝构造函数 Date(const Date& d) { _year = d._year; _month = d._month; _day = d._day; } //指针构造函数 Date(Date* d) { _year = d->_year; _month = d->_month; _day = d->_day; } void print(){ cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; }; void Func1(Date d) { cout << &d << endl; d.print(); } void Func3( Date& d) {//const Date&d时,public中的void print()改为void print()const // 这里的 d 是对原始 Date 对象的引用,不会拷贝 cout << &d << endl; // 输出d对象的地址 d.print(); // 调用d的print函数 } Date& Func2() { Date tmp(2024, 7, 25); tmp.print(); return tmp; } int main() { Date d(2024, 7, 24); /* // 这⾥可以完成拷⻉,但是不是拷⻉构造,只是⼀个普通的构造 Date d1(&d); d1.print(); //这样写才是拷⻉构造,通过同类型的对象初始化构造,⽽不是指针 Date d2(d); d2.print(); Date d3 = d; d3.print(); */ Func1(d);//按值传递 Func3(d);//传引用 // Func2返回了⼀个局部对象tmp的引⽤作为返回值 // Func2函数结束,tmp对象就销毁了,相当于了⼀个野引⽤ Date ret = Func2(); ret.print(); return 0; }
补充:
在C++中,当通过值传递自定义类型的对象时,确实会调用拷贝构造函数进行对象的拷贝。这种方式在某些情况下可能会导致性能的下降,尤其是当对象较大或者拷贝构造函数比较复杂时。
传值与传引用的区别
1. 传值:
- 当函数参数以值的方式传递时,编译器会创建该对象的副本。
- 调用拷贝构造函数,这可能涉及内存分配和数据复制,这是一个相对较重的操作。
- 如果 `Date` 对象比较大,频繁的拷贝可能会影响性能。
void Func1(Date d) { // 这里的 d 是 Date 对象的副本 }
2. 传引用:
- 通过引用传递参数时,不会创建副本,而是直接使用原始对象。
- 这样可以避免拷贝构造函数的调用,从而提高性能。
- 还可以对传入的对象进行修改(如果传递的是非常量引用)。
void Func1(const Date& d) { // 这里的 d 是对原始 Date 对象的引用,不会拷贝 d.print(); }
故在实际编程中,如果不需要在函数内部修改传入的对象,并且想要提高性能,使用引用作为函数参数是一种推荐的做法。这种方式可以避免不必要的对象拷贝,特别是在处理较大或复杂对象时。不过,如果你确实需要在函数中对参数进行修改,使用非常量引用可以让你直接操作原始对象。
6. 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,所以不需要我们显⽰实现拷贝构造。像Stack这样的类,虽然也都是内置类型, 但是_a指向了资源,编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求,所以需要 我们自己实现深拷贝(对指向的资源也进行拷贝)。像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的拷贝构造会调用Stack的拷贝构造,也不需要我们显示实现
MyQueue的拷贝构造。这里还有一个小技巧,如果一个类显示实现了析构并释放资源,那么他就
需要显示写拷贝构造,否则就不需要。
#include<iostream> using namespace std; typedef int Datatype; class Stack { public: Stack(int n = 4) { _a = (Datatype*)malloc(sizeof(Datatype) * n); if (nullptr == _a) { perror("malloc申请空间失败"); return; } _capacity = n; _top = 0; } Stack(const Stack& st) { // 需要对_a指向资源创建同样⼤的资源再拷⻉值 _a = (Datatype*)malloc(sizeof(Datatype) * st._capacity); if (nullptr == _a) { perror("malloc申请空间失败!!!"); return; } memcpy(_a, st._a, sizeof(Datatype) * st._top); _top = st._top; _capacity = st._capacity; } void Push(Datatype x) { if (_top == _capacity) { int newcapacity = _capacity * 2; Datatype* tmp = (Datatype*)realloc(_a, newcapacity * sizeof(Datatype)); if (tmp == NULL) { perror("realloc fail"); return; } _a = tmp; _capacity = newcapacity; } _a[_top++] = x; } ~Stack() { cout << "~Stack()构析函数调用" << endl; free(_a); _a = nullptr; _top = _capacity = 0; } private: Datatype* _a; size_t _capacity; size_t _top; }; // 两个Stack实现队列 class MyQueue { public: private: Stack pushst; Stack popst; }; int main() { Stack st1; st1.Push(1); st1.Push(2); // Stack不显⽰实现拷⻉构造,⽤⾃动⽣成的拷⻉构造完成浅拷⻉ // 会导致st1和st2⾥⾯的_a指针指向同⼀块资源,析构时会析构两次,程序崩溃 Stack st2 = st1; MyQueue mq1; // MyQueue⾃动⽣成的拷⻉构造,会⾃动调⽤Stack拷⻉构造完成pushst/popst // 的拷⻉,只要Stack拷⻉构造⾃⼰实现了深拷⻉,他就没问题 MyQueue mq2 = mq1; return 0; }
C++之类与对象(3)(下):https://developer.aliyun.com/article/1624937