读书笔记《深度探索c++对象模型》(2) - 构造函数(默认构造函数、拷贝构造函数)
一、C++ 默认构造函数 1.关键字explict的引入为解决单个参数的构造函数被隐式当作一个转换运算符的窘迫,此关键字要求在单个参数的构造函数时需显式调用构造函数,而不是当作转换运算;故除非的确需要用到类包装器的转换类时,否则一般情况下单个参数的构造函数建议用explict修饰。 2.编译器会在必要的时候生成默认构造的构造函数,什么情况下是必要的时候,另外生成的默认构造函数是否为合理的内容实现呢? 必要的时候:仅仅为编译器需要,而不是程序员需要,故而有时候根据程序员需要需自行实现自己的构造函数实现内容; 是否为合理的内容:编译器生成的构造函数实现可能是没有显而易见的用处,因不会初始化其他不需要默认构造函数的成员变量(如整数、指针、普通数组等),故有时程序员还得自行实现构造函数及内容实现; 注:全局变量在内存中由程序运行时初始化清0,而本地栈变量或堆上变量则未被初始化为0,而是上次内存使用后的垃圾值。 3.有用的默认构造函数(由编译器在必要的时候合成,若程序员已提供,则编译器不再合成): 1)一个类中的成员变量或多个变量带有默认构造函数时,则编译器会合成一个有用的默认构造函数,以调用成员变量的默认构造函数来初始化该成员变量;但此时因仅满足编译器的需要,该类中的其他成员(普通的,没有构造函数的)则不会被合成的构造函数处理,故而若程序员需要需自行实现默认构造函数,此时编译器会在该构造函数中插入对有构造函数的成员变量初始化调用的代码片段,另外当有多个成员变量均有默认构造函数时,则依次按照声明的顺序来调用各成员变量的默认构造函数。此外,若是程序员定义了多个子类的构造函数,则每个构造函数都会在必要时被编译器插入相应的调用成员变量的默认构造函数。 2)一个类中的基类或多个基类含有默认构造函数,则该类也会被编译器合成以继承顺序调用基类的默认构造函数。若程序员有提供自己的多个构造函数时,则编译器会给每个构造函数插入必要的调用基类构造函数的代码片段。此外,若此时除了基类有默认构造函数外,该类中还有带有默认构造函数的成员变量,则除了插入调用基类的默认构造函数外还会在调用基类构造函数后插入调用成员变量的默认构造函数的代码片段。 3)一个带有虚函数的类(含类声明的虚函数、由继承而来的虚函数、多个基类中其中有一个或多个的虚基类,此3种情况时),也会合成默认构造函数;此编译器合成构造函数的操作需要的实现内容为:编译器产生一个虚函数表(内放置类的虚函数地址)、每个类对象中的一个额外vptr,其指向类的虚函数表。因为需要处理虚表,编译器需要在默认构造函数中插入一些设置vptr和构建虚表的操作(也包括父类中的默认构造函数,必要时还会为父类提供默认构造函数以设置父类对应的vptr和虚表)。 4)一个带有虚基类的类,虽不同编译器实现虚基类的做法不同,但均需要处理好需基类的初始化问题,故编译器会合成默认构造函数;另外,若程序员提供多个构造函数,则编译器需安插入必要的处理虚基类的初始化代码片段。 二、C++拷贝构造函数 1.拷贝构造函数的需要,一般会出现在以下几种情况中:1) 用一个对象初始化赋值给另一个对象2) 作为函数实参参数传递 3) 作为函数的返回值对象。 2.默认的类成员初始化 当一个类中没有提供显式的拷贝构造函数时,当用类对象初始化另一个相同类对象时,则对类中的每个自身声明或继承来的数据成员(一般为普通的类型,如指针、整数、普通数组等)依次由赋值类对象成员拷贝即可(类似于位逐次拷贝语义),而对于类中的为类的成员变量,则以递归的方式继续实现以上的初始化过程;整个过程,由编译器产生的,即编译器会在必要的时候合成默认拷贝构造函数。 3.位逐次拷贝,此时不需要提供拷贝构造函数,直接位拷贝赋值;其中之一的是,若类中的成员提供了显式的拷贝构造函数,则该类需要合成一个初始化调用该成员的拷贝构造函数,其他的普通成员也会插入被初始化赋值的代码。 4.不会展现位逐次拷贝的情况: 1)类中的成员变量有一个或多个为含有拷贝构造函数时(无论该拷贝构造函数为程序员提供或编译器合成的) 2)类的基类提供了拷贝构造函数(无论是程序员提供或被编译器合成的) 3)类中声明了一个或多个虚函数时 4)类中的基类或多个基类为虚基类时 对于1) ,2)两种情况时,类中的成员变量或基类的成员变量的初始化代码会被插入到合成的拷贝构造函数中。3),4)则因虚函数表和vptr指针的重新设定不可以按位逐次赋值(主要体现在用子类来赋值给基类对象时,vptr需要被修改,不能直接赋值)。 5.重新设定虚表的指针,同上所述,为了解决用子类来赋值给基类对象时,vptr需要被修改,不能直接赋值,即不能仅依靠按位逐次拷贝的机制) 6.处理虚基类对象,同样的对于此类情况同以上5,需要编译器合成拷贝构造函数,以安插一些代码设定虚基类的指针或offset偏移的初值,而其他的成员可执行必要的成员初始化操作或其他内存相关的工作,故而也不能仅依靠按位逐次拷贝,尤其是以子类来赋值给基类对象时。 7.拷贝构造函数是否有必要?一般若是比较平坦(Pain)的类成员时的类,应不需要显式的提供拷贝构造函数,按位逐次拷贝可以提供更为安全且快速的初始化操作;再不济,也可以用memcpy或memset等可以提供优雅有效率的初始化操作(注意:memcpy和memset等使用的场合,如含有虚函数、虚基类、特殊的成员变量(如string)等不可使用)。 8.构造函数中的类成员的初始化在函数体内或在初始化列表中均可,但有几种情况例外。 必须用初始化列表来初始化时: 1)初始化引用成员对象时 2)初始化const 成员对象时; 3)调用一个含有多个参数的基类构造函数时; 4)调用一个含有多个参数的类成员对象时; 初始化列表本身在编译器编译时会被重新构建构造函数,以适当的顺序(一般按照成员声明次序决定而不是初始化列表的顺序)将初始化列表中的操作转移安插到构造函数体中(一般会被安插到函数体的程序员编写的代码之前)。 因此原因,若初始化列表中的程序员认为的初始化依赖顺序可能被编译器插入构造函数时打乱,可能导致错误依赖或是依赖一个未被初始化的成员对象等,进而导致意想不到情况的发生(而这个BUG有时候很难被发现)。 另外,初始化列表中的成员初始化若用成员函数来初始化,则依赖于该函数对该类对象的依赖程度,若依赖了其他未被初始化的成员变量,则可能导致意想不到的结果,故而需要谨慎调用类成员函数来初始化。 同样的,在初始化列表中若用继承而来的成员函数来初始化基类对象,也同样如此。