第二篇笔记拖了好长时间,因为我自身也是一知半解。所幸在查阅许多资料后,略有所得,故与诸君共享。这篇可能理解起来比较晦涩,我尽可能做好铺垫。
若存在错误 请指正 万分感想
铺垫:
1.
Default memberwise initialization and bitwise copy 概念的区分和理解。说实话,一开始我也不明白,后来翻阅了下资料,知道涉及到深浅拷贝。
default memberwise initialization 同 user_defined initialization 对应。前者是从编译器的角度来说,后者自然是程序员角度来说。
这个地方的初始化行为我是这样理解的:初始化只是目标,需要一定的方式才能进行,至于具体的方式那么就看底下的内容。
2.
bitwise copy 同 memberwise copy 对应。两种不同的拷贝方式,分别是浅拷贝和深拷贝。通常编译器为了效率会选择bitwise 拷贝。
我在看深度探索C++模型一书中,在拷贝构造函数那一块是十分混乱的,书中有时提及memberwise initialization ,但是后面又讲了bitwise copy ,令我一度不能完成此章节的学习,博文也就拖到了今天。这两个概念在类的对象初始化和赋值的时候经常会一起出现。
但是只要把握住上面那句话,初始化只是目标,而拷贝却是具体的实现方式。
从整体对象的角度来说:类中对象的初始化和赋值操作通常是按成员进行的,这个地方不是指memberwise copy.
这个地方描述的按成员的意思是对每一个成员进行某种操作。
具体到每一个数据成员,编译器通常采用的是bitwise copy 的方式实现。
如果用过memcpy函数的话,那么这个地方的bitwise copy 就是如此实现的。
示例:
#include <iostream>
using namespace std;
class Base{
private:
int a;
int b;
int* ptr;
public:
Base(){
a = 0;
b = 0;
ptr = nullptr;
}
Base(int a_a, int b_b, int* p_p){
a = a_a;
b = b_b;
ptr = p_p;
}
};
int main(){
int tmp = 33333;
int* ptmp = &tmp;
Base A(100, 25,ptmp);
Base B = A; //这个时候的A 和 B 的ptr 指针指向哪里?
system("pause");
return 0;
<span style="color:#330099;">}</span>
用图片解释下:(详细情况可以参看下维基百科)
我就解释下右边这个P指针都指向一个一个位置时候的情况,那么当一个对象被析构掉的时候,那么另一个指针怎么办?
这个时候你就需要按照左边图片的方式来解决问题。
示例:
#include <iostream>
using namespace std;
class Base{
private:
int a;
int b;
int* ptr;
public:
Base(){
ptr = new int(100);
}
Base(int a_a, int b_b, int* p_p){
a = a_a;
b = b_b;
ptr = p_p;
}
/*Base(const Base& p){
a = p.a; b = p.b;
ptr = p.ptr;//这个地方会发生什么?直接进行地址的赋值。
}*/
~Base(){
delete ptr;
}
Base(const Base& p){
a = p.a; b = p.b;
ptr = new int; //这个地方分配空间了.
if (!ptr){
cout << "!!" << endl;
}
*ptr = *(p.ptr);
}
friend ostream& operator<<(ostream& os, const Base& p){
os << p.a << " " << p.b << " " << *(p.ptr) << endl;
return os;
}
};
int main(){
Base b1;
Base b2( b1); //这个时候的b1 和 b2 的ptr 指针指向哪里?
cout << b1 << endl;
cout << "----------" << endl;
cout << b2 << endl;
system("pause");
return 0;
}
铺垫知识就到这里了,详情可以翻阅MSDN,大牛博客等资料。
重点是要记住一句话,初始化是目标,实现目标的方式却是进行拷贝。拷贝的方式就是由你和编译器一起决定的。
关于深浅拷贝的详细知识点我乎总结的,但是现在却不是合适的时候,重点是要理解浅拷贝为什么出问题,以及如何解决这个问题。核心是对内存的操作。
正题:
三个问题:
1.
拷贝构造函数是解决什么问题的?一定要明确了,是用对象给对象进行初始化或者赋值的时候才会产生。
一开始我在读书的时候经常会混乱,读着读着竟然想到了默认构造函数,后来我一想,是我自己根本没搞清楚拷贝构造函数的产生是干什么,以及何时会遇到这种问题。
这个问题的产生是源于对象到对象的初始化。
2.
是否对象到对象的初始化一定要用到构造函数呢?答案是否定的。
3.
成员初始化只是目标,而不是具体的实现方式,实现方式你可以通过bitwise copy ,拷贝构造函数。
三种情况:
1.用拷贝构造函数初始化对象(这个地方我可没说是同类对象之间)
2.按值传递对象
3.按值返回一个对象的副本。
具体的你可以参见前面的临时对象相关博文。
大部分情况一下,以同类对象进行初始化,会调用相应的拷贝构造函数(前提是你已经显示定义了一个,如果你不显式的定义一个,那么编译器会合成嘛?)
Default memberwise initialization
首先回忆上面的话,这个是目标,不然你可能会像我一样进入一个怪圈。
按照书中所描述,若类中没用显式的拷贝构造函数,当用同类对象进行初始化的时候,是按照default memberwise initialization 的方式完成初始化。
看好了,这个地方只是简单的initialization,并不是memberwise copy(深拷贝行为)。但是若类中竟然包含了一个member class object 时,那么对这个会对类中包含的成员对象进行递归并且是按照default memberwise initialization 的方式完成初始化。
示例:
#include <iostream>
using namespace std;
class String{
private:
int len;
char* str;
public:
//没用显式的拷贝构造函数。
String(){
str = new char(100);
}
String(char* p){
strcpy(str, p);
}
};
int main(){
String noun("book");
String verb = noun;
/*内部的操作类似:
verb.len=noun.len;
verb.str=noun.str
暂时回避浅拷贝的问题*/
}
如果类里面是含有对象成员的例子我就不举了。大致的操作是类似的。
上面我已经提到了,这个只是我们的目标而已,具体的实现方式是如何实现的呢?
是编译器合成的拷贝构造函数实现的嘛?
这里的理解可以参考默认构造函数的合成:C++标准是如此解释的,只有在必要的时候才会合成。关键是还是要理解什么时候是必要的,这个把握住了,拷贝构造函数的合成时机也就能把握住了。
合成的构造函数是trival 或者是nontrival 是依据一个标准进行划分的。
Bitwise copy semantics
当用同类对象进行初始化的时候,如果类中展现出了bitwise copy semantics ,那么是不需要合成拷贝构造函数的,仅仅通过bitwise copy 就可以实现对象成员之间的初始化工作,也就是上面提到的default memberwise initialization 行为。
示例:
#include"word.h"
Word noun("bool");
void foo(){
Word verb = noun;
}
这个地方的verb对象明显是根据noun对象进行初始化的,那么编译器是否需要合成一个拷贝构造函数呢?具体的需要看一下头文件里面到底是什么东西。
//这个地方就展现出了bitwise copy 的意思。
//也就是暗示编译器你简单的浅拷贝就行了,
//至于这个地方存在指针会导致浅拷贝出现问题
//编译器才不会管。
class Word{
private:
int cnt;
char* str;
public:
Word(const char*);
~Word();
};
可以观察到私有的数据成员都是基本的数据类型,这个时候简单的浅拷贝是可以解决问题的,暂时回避指针的问题。
下面看一下暗示编译器没有浅拷贝的意思的例子:
//这个例子就没有展现出bitwise copy 的意思。
#include <string>
class Word{
private:
string str;
int cnt;
public:
Word(const string&);
~Word();
};
//而在string 的定义中存在一个拷贝构造函数。
class string{
public:
string(const string&); //我显式的定义了一个拷贝构造函数。
string(const char*);
};
//一个被合成出来的拷贝构造函数可能是如下的(伪码):
inline Word::Word(const Word& wd){
str.string::string(wd.str);//这个地方就调用了string 的拷贝狗仔函数。
cnt = wd.cnt;
}
不知道你们看到这个地方是否与上面的default memberwise initialization 那一节对比一下,是否产生了疑惑。你看,这个地方我的类中内含了一个成员对象str,你上面不是讲了嘛,是按照default memberwise initialization 方式初始化的嘛?一开始我没有把这句话理解成一个目标,而是理解成一种实现方式了,导致我后面看一点,就冲突一点。
四种特殊情况:
1.内含对象成员:
也就是上面例子中str,并且内含的成员对象是带有拷贝构造函数的(不论你的拷贝构造函数是如何来的,譬如拷贝啊,编译器合成)。
2.继承的情况:
基类是有拷贝构造函数的(同上面类似,不关注你拷贝构造函数的来源渠道)
3.存在虚函数。
4.有虚基类
相关解释:
一和二的解释:
对于1和2 两种情况,很好理解的。可以对比下上一篇博文的情况进行理解。
因为我内含对象成员或者是我有父类,并且他们都有拷贝构造函数的,那么我当我想用同类的对象进行初始化的时候,那么我干嘛不调用他们的拷贝构造函数,万无一失嘛。
从编译器的角度来说,编译器是干嘛的,帮助程序员的,既然存在了拷贝构造函数了,你就要按照程序员的要求做事情。
所以这两种情况下,编译器检查到你有拷贝构造函数了,那么编译器就默认你是想用他们的,所以编译器直接合成一个拷贝构造函数以方便调用他们的拷贝构造函数。
不然你不合成,拷贝构造函数就在那里放着,你不调用一辈子也用不上。
所以我的理解就是这里的拷贝构造函数被合成两个目的:
一是满足程序员的需求,因为编译器认为你是想要让它用拷贝构造函数的;
二就是为了合理的调用内含成员的拷贝构造函数,总不能凭空就出现了内含成员对象的拷贝构造函数吧。
这些都是我自己现阶段的理解,仅供参考。
其次比较重要的一点就是不要被上面的东西绕进去,你要明白这个问题的产生是由于类对象之间的初始化或者赋值,不然谈何构造函数呢?
第三的解释:
根源我们都知道的,是类对象的之间的赋值造成的,那么你是否只是简单的认为同类对象的直接的复杂,如果是父类对象和子类对象之间的初始化会怎么样呢?尤其是当存在虚函数的情况。
回忆下,虚函数出现的时候,类的行为变化:
1.是产生虚表(虚函数的地址表,virtual function table ,也称vtbl)
2.是对象里面都出现了一个指向虚表的指针(vptr)
那么当我们用子类的成员给父类成员进行初始化时会发生什么有趣的事情呢?
示例:
class ZooAnimal{
public:
ZooAnimal();
virtual ~ZooAnimal();
virtual void animate();
virtual void draw();
private:
//所需要的数据
};
class Bear :public ZooAnimal{
public:
Bear();
void animate();//虚函数,virtual 可以省略。
void draw();//同上
virtual void dance();//自己又定义了一个虚函数。
private:
//所需要的数据.
};
可以看到,Bear 继承了ZooAnimal 类,并且都没用定义拷贝构造函数。那么当用同类
对象进行初始化的时候,会发生什么?
如:Bear yogi;
Bear winne=yogi; //当你用同类对象进行初始化的时候,会发生什么呢?
用图片解释一
下:
这个地方排除了数据成员中有指针的情况。,可以看到虽然vptr指针进行浅拷贝,但是行为却是符合我们的要求的,因为vptr指针必须是指向一个地方的。也就是说同类对象成员直接的初始化操作是可以同过简单bitwise copy 完成的,不需要合成一个拷贝构造函数的。
但是当你用子类对象给父类对象进行初始化的时候会发生什么?
如:ZooAnimal franny=yogi; //看过前面文章的人应该是知道会发生切割行为的。这个时候的vptr会怎么样?
先看一下编译器不干涉的情况:
我画图解释下:
在第一篇文章中说到内存模型,那当你用子类对象给父类对象初始化的时候,那么会发生切割,继承的可能就是subobject那一部分,但是那里面的指针指向的虚表是谁的呢?如果编译器不干预的话,那么肯定是指向子类的虚表的,也就是图中的Bear。
但是让他指向了Bear的虚表根本就不符合我们的意愿啊,我这里可不是多态啊。
那么编译器又会默认为们是不想这么做的,所以它要发威了,我帮你重新设定一下虚表的指针的指向位置,那么可能出现这样的情况:
可以看到虚线的情况是我们不想看见的,但是在编译器不干预的情况下,却又会发生这种事情,所以编译器只能出马了。帮父类强制矫正它的vptr指针的指向位置。
也就是说,合成出来的拷贝构造函数会显式的设定对象的vptr,让它指向ZooAnimal的虚表,而不是简单的bitwise copy.
第四的解释:
还是重复一下拷贝构造函数产生的根源性目的,用类对象进行初始化。
当存在虚基类的时候,拷贝构造函数被合成是因为要正确处理virtual base class subobject 的位置(这个地方会涉及到虚基类的内存模型,我现在理解的不好,我会在后面的虚拟继承一节中总结下虚基类的问题),所以这个地方我只是一句话带过,感兴趣的可以去看看虚基类的知识,涉及到偏移位置。
总结:
上述的四种情况中,类已经不在保持bitwise copy 的含义,并且还没有声明一个拷贝构造函数,并且你还想用对象初始化对象,那么编译器就有必要出手了,合成一个拷贝构造函数帮助你
后话:
这篇文章着实花了不少时间,写的着实头晕,如果我犯错了,请指出。如果感觉博文字体太小不便于阅读,可以放大到133%。体验不错。
参考文献:
<<深度探索C++对象模型>>
CSDN 博文:前面的铺垫知识是这位大大给我普及的。
地址:点击打开链接
End
下面的可能会接着记笔记,应该是记录程序转化语意学。