拷贝构造函数的特殊调用方式+陷阱
- 方式三:在函数中把类作为形参
- 方式四:在函数中把类作为返回值类型+陷阱
阅读之前注意:
本文阅读建议用时:36min
本文阅读结构如下表:
项目 | 下属项目 | 测试用例数量 |
---|---|---|
方式三:在函数中把类作为形参 | 无 | 1 |
方式四:在函数中把类作为返回值类型+陷阱 | 无 | 1 |
方式三:在函数中把类作为形参
在上一篇博客中,我们有说到拷贝构造函数的两种调用方式,那是最常用的调用方式。但在一些特殊情况下,我们也会用到拷贝构造函数,比如说在函数中把类作为形参或者是把类作为返回值类型。
现在我们就来看看这两种情形下的拷贝构造函数的调用。
首先是在函数中把类作为形参
参考以下代码:
#define _CRT_SECURE_NO_WARNINGS
#include "iostream"
using namespace std;
#include<string.h>
class Test
{
public:
Test();//默认构造函数
~Test();//析构函数
Test(Test &obj);//拷贝构造函数
public:
void setBuf(const char *str)
{
strcpy(buf, str);
}
void setA(int a)
{
m_a = a;
}
int getA()
{
return m_a;
}
char *getBuf()
{
return buf;
}
void print()
{
printf("m_a=%d,buf=%s\n", m_a, buf);
}
private:
int m_a;
char buf[32];
};
Test::Test()
{
m_a = 0;
strcpy(buf, "just do it");
cout << "默认构造..." << endl;
}
Test::~Test()
{
cout << "析构..." << endl;
}
Test::Test(Test &obj)
{
m_a = obj.getA();
strcpy(buf, obj.getBuf());
cout << "拷贝构造..." << endl;
}
void fun4(Test P)//形参类型是类,实参传给形参,执行的是拷贝构造
{
cout << "p.x=" << P.getA() << " p.buf=" << P.getBuf() << endl;
}
void objPlay2()
{
Test t1;
t1.setA(12);
t1.setBuf("nice");
t1.print();
Test t2 = t1;//拷贝构造函数调用方式一
t2.print();
Test t3(t1);//拷贝构造函数调用方式二
t3.print();
fun4(t1);//拷贝构造函数调用方式三
}
void main()
{
objPlay2();//搭建一个舞台,为了完整展现对象的生命周期
system("pause");
}
这一段代码和上篇博客的相比,仅仅是添加了一个fun4()的函数,其形式参数是一个类,在代码中我把这种拷贝构造调用注释为了方式三。在实际的测试中,可以去掉方式一和二,单独观察方式三的行为,就会知道在把实参传给形参的时候,执行的不是通常的赋值而是执行拷贝构造函数来传值的。1
方式四:在函数中把类作为返回值类型+陷阱
这一段中我们会知道拷贝构造函数的调用方式四,同时也会遇到拷贝构造过程中的陷阱,准确说来,应该是拷贝构造后的陷阱,这一过程,理解拷贝构造的具体行为是很重要的。
参考以下代码:
#define _CRT_SECURE_NO_WARNINGS
#include"iostream"
using namespace std;
#include<string.h>
class position
{
public:
position();
position(int a, char *strsrc);
~position();
position(position &obj);
public:
void print()
{
cout << "m_a=" << m_a << "buf=" << buf << endl;
}
public:
int getA()
{
return m_a;
}
char *getBuf()
{
return buf;
}
private:
int m_a;
char buf[32];
};
position::position()
{
m_a = 0;
strcpy(buf, "zero");
cout << "默认构造" << endl;
}
position::position(int a,char *strsrc)
{
m_a = a;
strcpy(buf, strsrc);
cout << "有参构造..." << endl;
}
position::position(position &obj)
{
m_a = obj.getA();
strcpy(buf, obj.getBuf());
cout << "拷贝构造" << endl;
}
position::~position()
{
cout << "析构..." << endl;
}
position fun()
{
position A(11,"chimpanzee");
return A;//返回的时候,构建一个匿名的中间对象,之后析构掉A对象
//匿名的中间对象的生命周期到函数外的赋值后为止
//但如果返回后是赋给引用,那么其生命周期将和引用一致
//因为引用是变量的别名而已,所以相当于这个匿名对象一直存在到其引用的生命周期结束
//或者返回后是用于初始化一个对象,即函数是在类的定义同时初始化的语句“类 变量=fun();”当中,
//那么匿名对象将直接成为那个对象
}
void objPlay4()
{
//position B=fun();拷贝构造函数的正确调用方式四
position B;
B = fun();//这里隐藏着问题,=操作执行的是浅拷贝,内存方面有问题,涉及到内存释放时会出错
B.print();
}
void main()
{
objPlay4();
system("pause");
}
在这段代码中,我注释掉了正确的调用方式。也就是说,你拷贝运行这段代码绝对会出错。
重点在于对那个匿名的中间对象的理解。
在fun()的注释中我也说得很清楚,如果一个函数的返回值类型是类的话,返回的时候就会拷贝构造一个匿名的中间对象,如果返回后是赋给一个引用或者说是用于一个类的类型的变量的初始化,那么中间对象将直接成为那个对象,其生命周期将会持续。
对于引用而言其实也是初始化,因为引用要求在定义的时候就必须初始化,也就是说返回后同样是用于类的初始化,其生命周期将持续。
就结论而言,非常简单:如果返回后有一个对象(未初始化)去接这个中间对象,中间对象将直接成为那个对象,其生命周期持续;如果返回后仅仅是一个简单的=操作,那么这个中间对象将会析构,其生命周期结束。
至于这里隐藏的陷阱,也就是内存的问题了,因为=执行的是浅拷贝,那么B变量里面的指针改变了内存空间的指向,那么原来的内存就会泄露(没有释放掉),而且由于新的浅拷贝过来的指针指向的是那个匿名中间对象的内存空间,但中间对象因为没有未初始化的对象去接已经析构了,那么从函数objPlay4()返回的时候就会出错,毕竟同一片内存是不能释放两次的。1
如果本文对你有帮助,不如请我一罐可乐吧 🍼