之前,在编写C++的一些代码的时候,在类的构造、析构、赋值函数部分,偶尔会出现编译器做出和我初想不一样的决策。后来,看了<深度探究C++对象模型 >这本书后,有的疑问或多或少得到了解决。所以在此,想写一些笔记来记录这些疑问的地方,可能后面关于对象模型还会关于其他方面的文章。不过这个系列文章都主要是以探讨为核心,以后在编程中遇到的相关问题也会不定期在这系列文章继续补充。有的疑问的解答可能只是经过本人的探究得到的猜测,如果有错误,还望指出,谢谢! |
在看这篇文章之前,先看如下几个问题: |
1.任何class如果没有用户定义的default constructor或者copy constructor,编译器就会合成一个出来? 2.编译器合成出来的default constructor会显式设定 “class 内每一个datamember” 的默认值? 3.编译器合成出来的copy constructor的拷贝是memberwise copy 还是 bitwise copy? |
又或者是如下几段代码: |
//Test类,下面的样例代码都会使用该类。
#include <iostream>
using namespace std;
class Test
{
public:
Test() { cout << "默认" << endl; }
Test(int a) { cout << "带参" << endl; }
~Test() { cout << "析构" << endl; }
Test(const Test&) { cout << "拷贝构造" << endl; }
Test& operator=(const Test&) {
cout << "赋值" << endl;
return *this;
}
};
1.如下两句代码分别会构造几个object? |
int main()
{
Test test1 = 1; //第一句
Test test2 = Test(1); //第二句
return 0;
}
2.如下代码中fun1和fun2的区别? |
Test fun1(){
Test a(1);
return a;
}
Test fun2(){
return Test(1);
}
int main(){
{ Test test1 = fun1(); }
cout << "--------------分隔符---------------" << endl;
{ Test test2 = fun2(); }
return 0;
}
3.如下代码两行代码的区别? |
int main(){
Test test1;
Test test2();
return 0;
}
4.如下代码两行代码的区别? |
Test fun(){
return Test();
}
int main(){
Test test1(fun());
Test test2(Test());
return 0;
}
在看本篇文章之前,你可以尝试在编译器中编译运行以上代码,看看编译器对以上代码做出的解释、决策跟你所预期的是否一样。如果你对以上问题留有困惑,那么你的问题应该在本篇文章的后续内容都能得到解答。 |
首先,我先要对第一个问题进行探讨:
“任何class如果没有用户定义的default constructor或者copy constructor,编译器就会合成一个出来?” 在之前对C++的学习中,可能有些朋友(包括本人)会有以上这个概念的认识。然而事实上,这个结论是错误的。 要真正地理解编译器会在何时自主合成一个Constructor,我们首先需要了解Constructor的相关功能: |
1. 初始化member
2. 初始化base obj
3. 初始化vptr(如果存在)
4. 初始化virtual base obj相关信息(如果存在)
什么时候编译器会自主合成Constructor呢?我们就要分清楚什么是程序需要,什么是编译器需要。 很显然,对于编译器需要的,一定是被合成出来的constructor函数要担负起编译器需要执行的相关功能。 在<深度探究C++对象模型 >一书中说到,对于以下四种情况,编译器会默认合成contructor: |
1. 带有 Constructor 的 Member Class Object (无论是用户提供还是编译器合成的),该类对应合成相应的Constructor。
2. 带有 Constructor 的 Base Class (无论是用户提供还是编译器合成的),该类对应合成相应的Constructor。
3. 带有一个Virtual Function (包括父类中存在 Virtual Function )
4. 带有一个Virtual Base Class ( 包括父类中存在Virtual Base Class)
对于1.如果一个类不含有用户定义的 Default Constructor 且不含合成的 Constructor 函数,那么对其 member 是不进行初始化的,而如果member中含有其他 class object 递归同样操作。如果存在1所属情况,那么就必须由编译器合成一个默认构造函数来调用其 member class object 的 constructor。那么 copy constructor 同理。 对于2.其实和上一个的原因差不多,都是因为需要调用base object 的 constructor ,所以编译器合成了对应的 constructor 函数 。 对于3.在这里不过多着墨于vptr(虚表指针)的介绍,如果对此不清楚的可以在这里先理解为一个拥有虚函数class 所特有的指针。而这个指针需要在构造对象之初就要完成对该指针的初始化,所以编译器合成了 default and copy constructor 。 对于4.虚基类,我们知道,对于虚基类,从多个途径派生出来的虚基类在子类有且只有一份,那么在其派生链中间类对于虚基类需要固定其实际偏移量。这句话可能不容易理解,嗯,你可以这样想,对于一个对象取其成员,其实实际上是获取该成员的偏移量,再根据自身的地址得到,然而如果对于一个中间类A,其reference或者pointer可能代表的是其任何子类。既然A*或者A&可以代表的实际类型有很多可能,如果使得在这些类中对于虚基类拥有同一偏移量呢?所以,在这里,引入了跟vptr一样的实现方法,用指针指向虚基类。那么所以编译器需要合成 default and copy constructor 用于初始化该指针。 其实以上只是对原因的简单阐述,如果有想深入了解编译器的小动作,可以自行阅读<深度探究C++对象模型 >一书 第二章。 同理我们可以知道,对于问题二: “编译器合成出来的default constructor会显式设定 “class 内每一个datamember” 的默认值?” 答案肯定是No,编译器合成出来的default constructor只会对上述4中情况中所涉及到的内容进行初始化。 |
既然以上的问题解决了,那么我们试图在这个问题的基础上继续衍生出新的问题: 如果在需要合成constructor的情况下,用户提供了相应的constructor呢?编译器会做出如何的行为呢? 既然我们知道,在上述四种情况中,编译器需要constructor函数来对其某些属性进行必不可少的初始化。所以当用户提供了constructor之后,编译器会在user code之前插入这些初始化代码 ,所以在如下代码中: |
class A
{
public:
A() {
num = 0;
}
string str;
int num;
};
实际上编译器加入了如下代码: |
//c++伪码
class A
{
public:
A() {
str.string::string(); //对member class object constructor的调用
num = 0;
}
string str;
int num;
};
所以如果要对str进行非默认构造,请使用构造列表,否则如果在构造函数内部初始化就出现如下情况: |
//c++伪码
class A
{
public:
A() {
//这里实际上是先使用默认构造函数构造一个str,然后在使用"abc"构造一个临时string对象,然后调用拷贝构造给str。
str = "abc";
num = 0;
}
string str;
int num;
};
接着第三个问题: “编译器合成出来的copy constructor的拷贝是memberwise copy 还是 bitwise copy?” 我们先来了解一下什么是Memberwise Copy , Bitwise Copy。 |
1. Bitwise Copy(位逐次拷贝):对于一个对象在其所在内存按照一位一位的依次拷贝。特点:速度块。
2. Memberwise Copy(成员逐次拷贝):对一个对象所有成员进行拷贝(拷贝方式要视成员和copy constructor实现而定),特点:控制力强。
了解了两个copy方式的区别之后,我们再来回顾一下,copy constructor 合成出现的原因。 1.需要对member class object 的 copy constructor进行调用。 2.需要对base class 的 copy constructor 进行调用。 3.需要对vptr进行初始化。 4.需要对指向 virtual base class 的指针进行初始化。 我们可以看出来,对于1,2,我们肯定不能把合成的copy constructor 视位Bitwise Copy。对于vptr呢?我们知道,派生类对象可以赋值给基类对象,虽然这是会发生sliced(截断),但是从语法和用法上是可取的。如果在这个情况下,合成的copy constructor按照Bitwise Copy。那么很明显,派生类的vptr被赋值给了基类的vptr。一旦使用该对象的指针进行对虚函数的调用就会Boom!!!!。同理,对于第4点也一一样。 所以我们知道对于编译器合成的copy constructor 采用的是Memberwise Copy。这里值得注意的是:对于基本类型:int,char,double..等等还是使用的Bitwise Copy。而其他member class object 要参照其类内部copy constructor实现而定。 |
原文:http://blog.csdn.net/y1196645376/article/details/54619923