【C++对象模型】构造函数语意学之二 拷贝构造函数

时间:2021-12-20 08:57:42

关于默认拷贝构造函数,有一点和默认构造函数类似,就是编译器只有在【需要的时候】才去合成默认的拷贝构造函数。

在什么时候才是【需要的时候】呢?

也就是类不展现【bitwise copy semantics】时,即不展现【逐位次拷贝】时,才会合成默认拷贝构造函数。

所谓的【逐位次拷贝】,也就是简单的赋值,不管类内的数据成员是int还是char*指针,都是简单的赋值,这叫【逐位次拷贝】。

那什么请下不展现【逐位次拷贝】呢?

有四种情况:

①类中有一个类对象成员,而该类对象成员声明了一个默认拷贝构造函数(不管这个默认拷贝构造函数是显式声明的还是编译器合成的)

②类继承自一个基类,而该基类有一个默认拷贝构造函数(不管这个默认拷贝构造函数是显式声明的还是编译器合成的)

③类声明了一个或多个虚函数时

④类派生自一个继承链,其中有一个或多个虚基类时

------------------------------

时隔几日,我又回来重新要修改一下这篇博客,原谅我这一生不羁放纵爱*,今天在群里看到了一个哥们问问题,大概是问

CObj obj1 = obj2; 是否是调用拷贝构造函数而不是调用重载的operator=,我回答了一下:当有拷贝构造函数的时候是不会调用operator=()的; 我的言外之意是:如果程序员没有手动定义拷贝构造或者编译器没有为该类合成默认的拷贝构造(没有上面4种情况),则就会去调用operator=运算符(虽然我明知道上面语句是定义,是从无到有的过程),其过程是首先调用CObj类的默认构造函数初始化obj1,然后再调用operator=()为其赋值。嗯,我是这么想的,而且也觉得这也应该是对的,于是刚才回来试了一下代码。

原来的类定义如下:

class CTest
{
public:
CTest(int a, int b);
virtual ~CTest();
CTest(const CTest& obj);
CTest& operator=(const CTest& obj); protected:
int m_nValue1;
int m_nValue2;
}; CTest::CTest(int a, int b) : m_nValue1(a), m_nValue2(b){cout << "构造函数被调用\r\n";}
CTest::~CTest(){}
CTest::CTest(const CTest& obj)
{
cout << "拷贝构造函数被调用\r\n";
this->m_nValue1 = obj.m_nValue1;
this->m_nValue2 = obj.m_nValue2;
} CTest& CTest::operator=(const CTest& obj)
{
cout << "重载赋值运算符\r\n";
this->m_nValue1 = obj.m_nValue1;
this->m_nValue2 = obj.m_nValue2; return *this;
}

可以看到,为该类定义了一个virtual析构函数,一个默认拷贝构造函数,按照这样的类定义,有如下使用代码

CTest get(CTest obj)
{
CTest obj2 = obj; return obj2;
}
int _tmain(int argc, _TCHAR* argv[])
{
CTest obj(, ); CTest obj2 = get(obj); return ;
}

有一个get函数,该函数使用一个CTest类对象作为参数,返回一个CTest类对象,在main函数中,首先定义了一个obj对象,然后使用obj作为参数调用get函数,将返回值赋值给obj2,

【C++对象模型】构造函数语意学之二  拷贝构造函数

其执行情况如上图:

①构造函数被调用一次, CTest obj(10, 20);

②拷贝构造函数第一次,obj作为实参向get函数传参时

③拷贝构造函数第二次,get函数内部CTest obj2 = obj;

④拷贝构造函数第三次,返回之前,在get函数内部,使用get函数内部的obj2作为参数,调用main函数里的obj2的拷贝构造函数。

第四种情况这种情况是经过编译器优化后的。

上面的执行情况符合预期,下面我们对该类的定义进行一下修改

去掉默认拷贝构造函数,同时要将析构函数改为非virtual的,因为如果不改,则编译器会为该类合成一个默认的拷贝构造函数(【不展现逐位拷贝】的第③中情况);

class CTest
{
public:
CTest(int a, int b);
//virtual ~CTest();
~CTest();
CTest& operator=(const CTest& obj); protected:
int m_nValue1;
int m_nValue2;
}; CTest::CTest(int a, int b) : m_nValue1(a), m_nValue2(b){cout << "构造函数被调用\r\n";}
CTest::~CTest(){} CTest& CTest::operator=(const CTest& obj)
{
cout << "重载赋值运算符\r\n";
this->m_nValue1 = obj.m_nValue1;
this->m_nValue2 = obj.m_nValue2; return *this;
}

使用代码不变

CTest get(CTest obj)
{
CTest obj2 = obj; return obj2;
}
int _tmain(int argc, _TCHAR* argv[])
{
CTest obj(, ); CTest obj2 = get(obj); return ;
}

执行情况如下

【C++对象模型】构造函数语意学之二  拷贝构造函数

可以看到执行结果:只调用了一次构造函数,这次调用跟前面的一样,都是main函数中定义obj时调用的,现在单步调试一下过程中的变量值

【C++对象模型】构造函数语意学之二  拷贝构造函数

【C++对象模型】构造函数语意学之二  拷贝构造函数

现在,我们再次修改一下代码,仅仅是把类的析构函数改为virtual的,仍然没有手动定义拷贝构造函数

【C++对象模型】构造函数语意学之二  拷贝构造函数

【C++对象模型】构造函数语意学之二  拷贝构造函数

可以看到,形参和实参的虚函数表指针的值是相同的,C++对象模型里说到过(《深度探索C++对象模型》p55),这种情况是bitwise copy,此时是安全的。