类和对象深层回顾:(内含面试题)拷贝构造函数,传值返回和传引用返回区别
🎬 胖咕噜的稞达鸭:个人主页


本文的学习你将深入理解:
问题一:构造函数的本质是什么,为什么需要构造函数?
问题二:拷贝构造函数和构造函数的区别,已经有构造函数了,为什么还需要拷贝构造函数?
面试题插入:问题三:传值返回和传引用返回的区别,优劣分析?
通过拷贝构造函数的特点理解:
- 特点一:拷贝构造函数是构造函数的一个重载
怎么理解:
问题一:构造函数的本质是什么,为什么需要构造函数?
构造函数的本质就是替代我们之前实现stack和Date类中的Init功能,构造函数自动调用就可以替代Init.
比如说红黑树中迭代器,list容器的迭代器要实现遍历,红黑树的一个节点和它的左右子树需要有联系才能实现 “ 迭代器 operator++()“”迭代器 operator --() ",这就需要对迭代器定义构造函数,同理list迭代器要遍历,但是链表的节点是不连续的,不像数组那样可以靠下标来访问下一个位置的元素。
问题二:拷贝构造函数和构造函数的区别,已经有构造函数了,为什么还需要拷贝构造函数?
拷贝构造函数的作用就是用已有的对象初始化新的对象,同时解决了默认拷贝带来的问题,
默认构造是有缺陷的,编译器【对于自定义类型成员变量会调用它的拷贝构造】,【对于内置类型自动生成默认拷贝构造,但默认实现是 “浅拷贝”(逐字节复制,值拷贝)】。而当类包含堆内存、文件句柄、网络连接等 “需要手动管理的资源” 时,浅拷贝会导致严重问题:
资源重复释放:两个对象的指针指向同一块堆内存,析构时会两次调用 delete,导致程序崩溃;
野指针:一个对象修改了堆内存中的数据,另一个对象会受影响;一个对象析构释放内存后,另一个对象的指针变成野指针。
此时必须手动实现拷贝构造函数,通过== “深拷贝”(不仅复制指针,还复制指针指向的资源)==解决这些问题,保证对象拷贝的安全性。
重载的核心要求是 “参数列表不同”(参数类型、数量或顺序不同)。
class Date {
public:// 重载1:无参构造Date() : _year(0), _month(0), _day(0) {}// 重载2:带参构造Date(int year, int month, int day) : _year(year), _month(month), _day(day) {}// 重载3:拷贝构造(构造函数的特殊重载)Date(const Date& d) : _year(d._year), _month(d._month), _day(d._day) {}
private:int _year, _month, _day;
};
- 特点二:拷贝构造函数是构造函数的一种特殊重载,具体体现在拷贝构造函数的第一个参数必须是类类型对象的引用
(Date(const Date& d)
如果使用Date(Date d)就会报错,如下图,所以不可以传值方式写一个拷贝构造的参数,会形成无穷递归调用。

3. 特点三:如果一个类实现了析构并且释放资源,那么就需要显示写拷贝构造,否则不需要。
全是内置类型没有指向什么资源的,编译器自动生成的拷贝构造就可以实现拷贝,不需要显示写拷贝构造;
有内置类型但是指向了一个资源,或者内部有内存的申请自动生成的拷贝构造(浅拷贝,值拷贝)不符合我们的需要,只拷贝了值,没有拷贝指向的资源,所以要显示实现拷贝构造。
一个自定义类型,里面都是内置类型,而且没有指向资源,那么编译器自动生成的拷贝构造就可以实现拷贝,不需要显示实现拷贝构造。
- 特点四:传值返回和传引用返回的区别(传值返回产生一个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名(引用),没有产生拷贝。)
面试题插入:问题三:传值返回和传引用返回的区别?
| 返回类型 | 传值返回 | 传引用返回 |
|---|---|---|
| 产生拷贝了吗 | 产生一个临时对象调用拷贝构造贝 | 引用返回,返回的是返回对象的别名(引用),没有产生拷贝 |
| 受生命周期影响 | 临时对象不受原函数生命周期影响 | 如果返回对象是局部域的局部对象,受生命周期影响,函数结束销毁,此时的引用就是野指针野引用 |
| 适用于哪种场景 | 内置类型没有指向资源 | 返回静态对象、全局对象或堆上持久存在的对象 |
这里给一道题:传值返回和传引用返回认识的巩固
假设有一个类,它的构造和析构函数如下:
class Buffer {
private:char* data; // 指向堆内存的指针size_t size;
public:Buffer(size_t s) : size(s) {data = new char[size];cout << "Buffer 构造,分配 " << size << " 字节" << endl;}~Buffer() {delete[] data;cout << "Buffer 析构,释放 " << size << " 字节" << endl;}// 为了简化,省略拷贝构造、赋值运算符等
};
现在用两个成员函数来调用,并指出有什么问题?
// 函数1:传值返回
Buffer createBuffer1(size_t s) {Buffer buf(s);return buf;
}// 函数2:传引用返回
Buffer& createBuffer2(size_t s) {Buffer buf(s);return buf;
}int main() {// 场景1Buffer b1 = createBuffer1(1024);// 场景2Buffer& b2 = createBuffer2(2048);return 0;
}
这里我们转入visual stdio来编译一下:

可以编译出结果,但是程序是有问题的,
传值返回+重复析构问题的解决:
此时函数1传值返回,默认是浅拷贝,产生一个临时对象调用拷贝构造拷贝,函数在构造的时候没有指向额外的资源,所以浅拷贝一个一个字节进行拷贝的时候可以完成拷贝,也就可以打印出图中的结果,如果指向额外的资源,
默认的浅拷贝后的buffer,临时对象,b1的data会同时指向一块空间,会发生 “三次析构(buf→临时对象→b1)、三次delete[]”。
第一次析构是buffer的析构,第二次析构是临时对象的析构,重复析构,第三次析构是(b1)析构,重复析构问题。
所以要有一个深拷贝构造函数,确保拷贝时复制堆内存资源,而非仅复制指针。
Buffer(const Buffer& other) :size(other.size){data = new char[size];memcpy(data, other.data, size);cout << "Buffer 深拷贝,构造" << size << "字节" << endl;}

传引用返回+野引用 问题的解决:
此时函数2传引用返回,返回的是返回对象的别名(引用),没有法生拷贝,但是此时的buf(s)是函数2的局部域的局部对象,出了作用域就会销毁,此时的引用就像一个野引用,函数析构的时候会析构掉buf,里面就全是野指针,返回会返回野指针。所以为了避免出现这个问题,将返回对象设置为不可以被析构的,在Buffer前加上static。静态全局对象,函数结束后不析构。
Buffer& createBuffer2(size_t s) {static Buffer buf(s);// 静态对象,函数结束后不析构return buf;
}

这里我们再用内置类型来演示一下传值返回+重复析构问题:
重点观察stack类型的输出打印:
#include<iostream>using namespace std;
typedef int STDataType;
class stack
{
private:STDataType* _a;size_t _capacity;size_t _top;
public:stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (_a == nullptr) { perror("malloc failed"); return; }_capacity = n;_top = 0;}~stack(){cout << "~stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}};
class Myqueue
{
public:
private:stack pushst;stack popst;
};int main()
{stack st1;stack st2 = st1;
}

所以要加上深拷贝:
stack(const stack& other)
{_a = (STDataType*)malloc(sizeof(STDataType) * other._capacity);if (_a == nullptr) { perror("malloc failed"); return; }memcpy(_a, other._a, sizeof(STDataType) * other._top);_top = other._top;_capacity = other._capacity;
}
other._capacity 是被拷贝对象(other)的容量,保证新栈的容量与原栈一致。
sizeof(STDataType) * other._capacity 计算所需内存总字节数(STDataType 是栈中元素的类型,如 int)。
malloc 动态分配堆内存,并强制转换为 STDataType* 类型(与 _a 的指针类型匹配)。
目的:通过新开辟内存,避免与 other 共享同一块堆空间(深拷贝的核心)。
memcpy(_a, other._a, sizeof(STDataType) * other._top);将other._a中的元素拷贝到_a中,仅仅拷贝有效个元素,other._top*sizeof(STDataType), 实现元素的深拷贝。


#include<iostream>using namespace std;
typedef int STDataType;
class stack
{
private:STDataType* _a;size_t _capacity;size_t _top;
public:stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (_a == nullptr) { perror("malloc failed"); return; }_capacity = n;_top = 0;}stack(const stack& other){_a = (STDataType*)malloc(sizeof(STDataType) * other._capacity);if (_a == nullptr) { perror("malloc failed"); return; }memcpy(_a, other._a, sizeof(STDataType) * other._top);_top = other._top;_capacity = other._capacity;}~stack(){cout << "~stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}};
class Myqueue
{
public:
private:stack pushst;stack popst;
};int main()
{Myqueue my1;Myqueue my2 = my1;stack st1;stack st2 = st1;
}

总结:传值返回传引用返回的区别分析:
| 返回方式 | 说明 | 优势(使用场景) | 劣势(不适用场景) |
|---|---|---|---|
| 传值返回 | 传值返回产生一个临时对象调用拷贝构造 | 返回的是临时对象,不受生命周期限制 | 传值返回是浅拷贝,仅复制指针,内部有资源申请,会引发重复析构,需要深拷贝 |
| 传引用返回 | 返回对象的别名(引用) | 不产生拷贝 | 受到局部域生命周期影响,函数结束后会对象销毁,析构后返回对象里面都是野引用,需要加static |
最后来做一道题:题目:
设已经有A,B,C,D4个类的定义,程序中A,B,C,D析构函数调用顺序为?( )
C c;int main(){A a;B b;static D d;return 0;}
1、类的析构函数调用一般按照构造函数调用的相反顺序进行调用,但是要注意static对象的存在, 因为static改变了对象的生存作用域,需要等待程序结束时才会析构释放对象
2、全局对象先于局部对象进行构造
3、局部对象按照出现的顺序进行构造,无论是否为static
4、所以构造的顺序为 c a b d
5、析构的顺序按照构造的相反顺序析构,只需注意static改变对象的生存作用域之后,会放在局部对象之后进行析构
6、因此析构顺序为B A D C
先定义的后析构。

