C++类构造函数初始化列表
C++——类继承以及类初始化顺序

C++——类继承以及类初始化顺序对于类以及类继承, ⼏个主要的问题:1) 继承⽅式: public/protected/private继承.这是c++搞的, 实际上继承⽅式是⼀种允许⼦类控制的思想. ⼦类通过public继承, 可以把基类真实还原, ⽽private继承则完全把基类屏蔽掉.这种屏蔽是相对于对象层⽽⾔的, 就是说⼦类的对象完全看不到基类的⽅法, 如果继承⽅式是private的话, 即使⽅法在基类中为public的⽅法.但继承⽅式并不影响垂直⽅向的访问特性, 那就是⼦类的函数对基类的成员访问是不受继承⽅式的影响的.⽐较(java): java是简化的, 其实可认为是c++中的public继承. 实在没必要搞private/protected继承, 因为如果想控制,就直接在基类控制就好了.2) 对象初始化顺序: c++搞了个成员初始化列表, 并确明确区分初时化跟赋值的区别. c++对象的初始化顺序是:(a) 基类初始化(b) 对象成员初时化(c) 构造函数的赋值语句举例:假设 class C : public A, public B {D d;//}则初始化的顺序是A, B, D, C的构造函数.这⾥基类的初始化顺序是按照声明的顺序, 成员对象也是按照声明的顺序. 因此 c(int i, int j) : B(i), A(j) {} //这⾥成员初始化列表的顺序是不起作⽤的;析构函数的顺序则刚好是调过来, 构造/析构顺序可看作是⼀种栈的顺序;⽐较(java): java中初始化赋值是⼀回事. ⽽且对基类的构造函数调⽤必须显⽰声明, 按照你⾃⼰写的顺序.对成员对象, 也叫由你初始化.没有什么系统安排的顺序问题, 让你感觉很舒服;3) 多继承问题: c++⽀持多继承, 会导致"根"不唯⼀. ⽽java则没有该问题;此外c++没有统⼀的root object, java所有对象都存在Object类使得很多东西很⽅便. ⽐如公共的seriall, persistent等等.4) 继承中的重载: c++中, 派⽣类会继承所有基类的成员函数, 但构造函数, 析构函数除外.这意味着如果B 继承A, A(int i)是基类构造函数, 则⽆法B b(i)定义对象. 除⾮B也定义同样的构造函数.c++的理由是, 假如派⽣类定义了新成员, 则基类初始化函数⽆法初始化派⽣类的所有新增成员.⽐较(java): java中则不管, 就算有新增对象基类函数没有考虑到, ⼤不了就是null, 或者你⾃⼰有缺省值. 也是合理的.5) 继承中的同名覆盖和⼆义性: 同名覆盖的意思是说, 当派⽣类跟基类有完全⼀样的成员变量或者函数的时候, 派⽣类的会覆盖基类的.类似于同名的局部变量覆盖全局变量⼀样. 但被覆盖的基类成员还是可以访问的.如B继承A, A, B都有成员变量a,则B b, b.a为访问B的a, b.A::a 则为访问基类中的a. 这对于成员函数也成⽴.但需要注意的是, 同名函数必须要完全⼀样才能覆盖. int func(int j)跟int func(long j)其实是不⼀样的. 如果基类,派⽣类有这两个函数, 则不会同名覆盖.最重要的是, 两者也不构成重载函数. 因此假如A有函数int func(int j), B有函数int func(long j). 则B的对象b.func(int)调⽤为错误的. 因为B中的func跟它根本就不构成重载.同名覆盖导致的问题是⼆义性. 假如C->B=>A, 这⾥c继承B, B继承A. 假如A, B都有同样的成员fun, 则C的对象c.fun存在⼆义性. 它到底是指A 的还是B的fun呢?解决办法是⽤域限定符号c.A::fun来引⽤A的fun.另外⼀个导致⼆义性的是多重继承. 假设B1, B2都继承⾃B, D则继承B1, B2. 那么D有两个B⽽产⽣⼆义性.这种情况的解决办法是⽤虚基类. class B1 : virtual public B, class B2:virtual public B, D则为class D : public B1, public B2. 这样D中的成员只包含⼀份B的成员使得不会产⽣⼆义性.⽐较(java). java中是直接覆盖. 不给机会这么复杂, 还要保存基类同名的东西. 同名的就直接覆盖, 没有同名的就直接继承.虚基类的加⼊, 也影响到类的初始化顺序. 原则是每个派⽣类的成员化初始化列表都必须包含对虚基类的初始化.最终初始化的时候, 只有真正实例化对象的类的调⽤会起作⽤. 其它类的对虚基类的调⽤都是被忽略的. 这可以保证虚基类只会被初始化⼀次.c++没有显式接⼝的概念, 我觉得是c++语⾔的败点. 这也是导致c++要⽀持组件级的重⽤⾮常⿇烦. 虽然没有显式的接⼝, 但c++中的纯虚函数以及抽象类的⽀持, 事实上是等同于接⼝设施的. 当⼀个类中, 所有成员函数都是纯虚函数, 则该类其实就是接⼝.java c++接⼝类(所有成员函数都是纯虚函数)抽象类类(部分函数是虚函数)对象类对象类C++构造函数调⽤顺序1. 如果类⾥⾯有成员类,成员类的构造函数优先被调⽤;2. 创建派⽣类的对象,基类的构造函数优先被调⽤(也优先于派⽣类⾥的成员类);3. 基类构造函数如果有多个基类,则构造函数的调⽤顺序是某类在类派⽣表中出现的顺序⽽不是它们在成员初始化表中的顺序;4. 成员类对象构造函数如果有多个成员类对象,则构造函数的调⽤顺序是对象在类中被声明的顺序⽽不是它们出现在成员初始化表中的顺序;5. 派⽣类构造函数,作为⼀般规则派⽣类构造函数应该不能直接向⼀个基类数据成员赋值⽽是把值传递给适当的基类构造函数,否则两个类的实现变成紧耦合的(tightly coupled)将更加难于正确地修改或扩展基类的实现。
C++类的定义和对象

C++类的定义和对象C++类的定义和对象类的成员变量称为类的属性(Property),将类的成员函数称为类的⽅法(Method)。
在⾯向对象的编程语⾔中,经常把函数(Function)称为⽅法(Method)。
类的定义class Student{public://成员变量char *name;int age;float score;void say(){cout<<name<<age<<score<<endl;}};类只是⼀个模板(Template),编译后不占⽤内存空间.class C++ 中新增的关键字,⽤来定义类。
成员变量和成员函数,它们统称为类的成员(Member)创建对象:Student Lilei; //创建对象Student是类名,liLei是对象名。
和使⽤基本类型定义变量的形式类似, 从这个⾓度考虑,我们可以把 Student 看做⼀种新的数据类型,把 liLei 看做⼀个变量。
在创建对象时,class 关键字可要可不要class Student LiLei; //正确Student LiLei; //同样正确还可以创建对象数组:Student allStu[100];使⽤对象指针:Student stu;//pStu 是⼀个指针,它指向 Student 类型的数据,通过 Student 创建出来的对象Student *pStu = &stu;在堆上创建对象,这个时候就需要使⽤前⾯讲到的new关键字Student *pStu = new Student;使⽤ new 在堆上创建出来的对象是匿名的,没法直接使⽤,必须要⽤⼀个指针指向它,再借助指针来访问它的成员变量或成员函数.对象指针后,可以通过箭头->来访问对象的成员变量和成员函数;两种创建对象的⽅式:⼀种是在栈上创建,形式和定义普通变量类似;另外⼀种是在堆上使⽤ new 关键字创建,必须要⽤⼀个指针指向它,读者要记得 delete 掉不再使⽤的对象C++类的成员变量和成员函数在定义类的时候不能对成员变量赋值,因为类只是⼀种数据类型或者说是⼀种模板,本⾝不占⽤内存空间,⽽变量的值则需要内存来存储.类的成员函数也和普通函数⼀样,都有返回值和参数列表,它与⼀般函数的区别是:成员函数是⼀个类的成员,出现在类体中,它的作⽤范围由类来决定;⽽普通函数是独⽴的,作⽤范围是全局的,或位于某个命名空间内.只在类体中声明函数,⽽将函数定义放在类体外⾯,如下图所⽰:class Student{public://成员变量;char *name;int age;float score;//声明成员函数;void say();};//定义成员函数void Student::say(){cout<<name<<"的年龄是"<<age<<",成绩是"<<score<<endl;}类体内成员函数默认为内联函数; inline 关键词是多余的;类体外定义 inline 函数的⽅式,必须将类的定义和成员函数的定义都放在同⼀个头(源)⽂件中.C++类成员的访问权限以及类的封装成员访问限定符publicprivateprotected只能修饰类的成员,不能修饰类,C++中的类没有共有私有之分类的内部(定义类的代码内部):⽆论成员被声明为 public、protected 还是 private, 都是可以互相访问的,没有访问权限的限制.在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员。
如何处理C++构造函数中的错误——兼谈不同语言的错误处理_转

如何处理C++构造函数中的错误——兼谈不同语⾔的错误处理_转⽤C++写代码的时候总是避免不了处理错误,⼀般来说有两种⽅式,通过函数的返回值或者抛出异常。
C语⾔的错误处理⼀律是通过函数的返回值来判断的,⼀般是返回0、NULL或者-1表⽰错误,或者直接返回错误代码,具体是哪种⽅式没有统⼀的规定,各种API也各有各的偏好。
譬如fopen函数,当成功时返回⽂件指针,失败时返回NULL,⽽POSIX标准的open函数则在成功时返回0或者正数,失败时返回-1,然后需要再通过全局变量errno来判断具体错误是什么,配套的还有⼀系列perror、strerror这样的函数。
C++的错误处理⽅式C++号称向下兼容C语⾔,于是就将C语⾔通过返回值的错误处理⽅式也搬了进来。
但C++最⼤的不同是引⼊了异常机制,可以⽤throw产⽣⼀个异常,并通过try和catch来捕获。
于是就混乱了,到底是什么时候使⽤返回值表⽰错误,什么时候使⽤异常呢?⾸先简单谈论⼀下异常和返回值的特点。
异常的优点1. 错误信息丰富,便于获得错误现场2. 代码相对简短,不需要判断每个函数的返回值异常的缺点1. 使控制流变得复杂,难以追踪2. 开销相对较⼤返回值的优点1. 性能开销相对⼩2. 避免定义异常类返回值的缺点1. 程序员经常「忘记」处理错误返回值2. 每个可能产⽣错误的函数在调⽤后都需要判断是否有错误3. 与「真正的」返回值混⽤,需要规定⼀个错误代码(通常是0、-1或NULL)使⽤异常还是返回值我的观点是,⽤异常来表⽰真正的、⽽且不太可能发⽣的错误。
所谓不太可能发⽣的错误,指的是真正难以预料,但发⽣了却⼜不得不单独处理的,譬如内存耗尽、读⽂件发⽣故障。
⽽在⼀个字符串中查找⼀个⼦串,如果没有找到显然应该是⽤⼀个特殊的返回值(如-1),⽽不应该抛出⼀个异常。
⼀句话来概况就是不要⽤异常代替正常的控制流,只有当程序真的「不正常」的时候,才使⽤异常。
反过来说,当程序真正发⽣错误了,⼀定要使⽤异常⽽不是返回⼀个错误代码,因为错误代码总是倾向于被忽略。
c++中冒号(:)的用法

c++中冒号(:)的⽤法(1)表⽰机构内位域的定义(即该变量占⼏个bit空间)typedef struct _XXX{unsigned char a:4;unsigned char c;} ; XXX(2)构造函数后⾯的冒号起分割作⽤,是类给成员变量赋值的⽅法,初始化列表,更适⽤于成员变量的常量const型。
struct _XXX{_XXX() : y(0xc0) {}};(3) public:和private:后⾯的冒号,表⽰后⾯定义的所有成员都是公有或私有的,直到下⼀个"public:”或"private:”出现为⽌。
"private:"为默认处理。
(4)类名冒号后⾯的是⽤来定义类的继承。
class 派⽣类名 : 继承⽅式基类名{派⽣类的成员};继承⽅式:public、private和protected,默认处理是public。
2、类构造函数(Constructor)的初始化列表先说下什么叫构造函数吧(是不是啰嗦了?C++的⼈应该都知道了吧,还是以防万⼀⼀下)。
所谓构造函数,就是与类同名的函数,它与普通函数的区别在于,它没有返回类型。
在构造函数后⾯紧跟着冒号加初始化列表,各初始化变量之间以逗号(,)隔开。
下⾯举个例⼦。
class myClass{public :myClass();// 构造函数,⽆返回类型,可以有参数列表,这⾥省去~myClass();// 析构函数int a;const int b;}myClass::myClass():a(1),b(1)// 初始化列表{}上⾯的例⼦展⽰了冒号的这个⽤法,下⾯对这个⽤法进⾏⼏点说明:1)初始化列表的作⽤相当于在构造函数内进⾏相应成员变量的赋值,但两者是有差别的。
在初始化列表中是对变量进⾏初始化,⽽在构造函数内是进⾏赋值操作。
两都的差别在对于像const类型数据的操作上表现得尤为明显。
我们知道,const类型的变量必须在定义时进⾏初始化,⽽不能对const型的变量进⾏赋值,因此const类型的成员变量只能(⽽且必须)在初始化列表中进⾏初始化,即下⾯的代码将会出错:myClass::myClass(){a = 1;// 没错,效果相当于在初始化列表中进⾏初始化b = 1;// 出错,const变量不能进⾏赋值操作;}2)初始化的顺序与成员变量声名的顺序相同。
c++中冒号和双冒号的用法

unsigned char a:4;
unsigned char c;
} ; XXX
(2)构造函数后面的冒号起分割作用,是类给成员变量赋值的方法,初始化列表,更适用于成员变量的常量const型。
struct _XXX{
_XXX() : y(0xc0) {}
在运算符等级中属于最高级的!
在你的问题中,似乎说的只是命名空间作用域符。
using namespace 命名空间名(如,abc);
表示在以下程序代码中所使用的标示符(如果此标示符在abc中定义)是abc中的,包括类型名(类),变量名,函数名,对象名。。。
using abc::标示符(i);
7、汇编指令模板
这个我也不懂,不班门弄斧了,可以参考一下:/articles/2006/43/1144846933898_1.html
改天学习一下。
(1)表示机构内位域的定义(即该变量占几个bit空间)
typedef struct _XXX{
(2)using abc::;万一你的程序中也用到了一个函数(函数名与abc中的这个函数同名),那么系统也不能判断你使用的是abc中的那个函数,还是本程序中的那个函数;
最安全的办法(当然也是最繁琐的)
就是,每当你用到一个变量(函数...)时,你都要明确他的来历(即属于哪个命名空间)除非它没有命名空间
struct bs
{
int a:8;
int b:2;
int c:6;
}data;
说明data为bs变量,共占两个字节。其中位域a占8位,位域b占2位,位域c占6位。对于位域的定义尚有以下几点说明:
1. 一个位域必须存储在同一个字节中,不能跨两个字节。如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。也可以有意使某位域从下一单元开始。例如:
C类构造函数初始化列表

类构造函数初始化列表初始化列表地定义在使用编程地过程当中,常常需要对类成员进行初始化,通常地方法有两种:一种是构造函数内对类地成员赋值,一种则是使用初始化列表地构造函数显式地初始化类地成员.构造函数初始化列表以一个冒号开始,接着是以逗号分隔地数据成员列表,每个数据成员后面跟一个放在括号中地初始化式.例如:{: ;;构造函数初始化列表(): ()() {}构造函数内部赋值(){;;}}; 从技术上说,用初始化列表来初始化类成员比较好,但是在大多数情况下,两者实际上没有什么区别.第二种语法被称为成员初始化列表,之所以要使用这种语法有两个原因:一个原因是必须这么做,另一个原因是出于效率考虑初始化列表地必要性初始化和赋值对内置类型地成员没有什么大地区别,像上面地任一个构造函数都可以.但在一些情况下,初始化列表可以做到构造函数做不到地事情:、类里面有类型地成员,它是不能被赋值地,所以需要在初始化列表里面初始化它;、引用类型地成员(也就是名字成员,它作为一个现有名字地别名),也是需要在初始化列表里面初始化地,目地是为了生成了一个其名字成员在类外可以被修改而在内部是只读地对象;、需要调用基类地构造函数,且此基类构造函数是有参数地;、类里面有其他类类型地成员,且这个“其他类”地构造函数是有参数地.举个例子:设想你有一个类成员,它本身是一个类或者结构,而且只有一个带一个参数地构造函数. { : ( ) { ... } }; 因为有一个显式声明地构造函数,编译器不产生一个缺省构造函数(不带参数),所以没有一个整数就无法创建地一个实例. * ; 出错!! * (); 如果是另一个类地成员,你怎样初始化它呢?答案是你必须使用成员初始化列表. { ; : (); };必须使用初始化列表来初始化成员() : () {……} 没有其它办法将参数传递给.情况和其实一样地道理.如果成员是一个常量对象或者引用也是一样.根据地规则,常量对象和引用不能被赋值,它们只能被初始化. 初始化列表与构造函数赋值地效率比较首先把数据成员按类型分类并分情况说明:.内置数据类型,复合类型(指针,引用)在成员初始化列表和构造函数体内进行,两者在性能和结果上都是一样地.用户定义类型(类类型)两者在结果上相同,但是性能上存在很大地差别.因为编译器总是确保所有成员对象在构造函数体执行之前初始化,所以对于用户自定义类型(类),在初始化列表中只会调用类地构造函数,在构造函数体中赋值就会先调用一次类地构造函数,然后再调用一次类地赋值操作符函数.显然后者在性能上有所损失,特别对于构造函数和赋值操作符都需要分配内存空间地情况,使用初始化列表,就可以避免不必要地多次内存分配.举个例子:假定你有一个类具有一个类型地成员,你想把它初始化为" .".你有两种选择:、使用构造函数赋值(){使用赋值操作符(); (" .");} 、使用初始化列表() : ((" .")) {} 编译器总是确保所有成员对象在构造函数体执行之前被初始化,因此在第一个例子中编译地代码将调用来初始化,这在控制到达赋值语句前完成.在第二个例子中编译器产生一个对:: ()地调用并将" ."传递给这个函数.结果是在第一个例子中调用了两个函数(构造函数和赋值操作符),而在第二个例子中只调用了一个函数.在地例子里这是无所谓地,因为缺省构造函数是内联地,只是在需要时为字符串分配内存(即,当你实际赋值时).但是,一般而言,重复地函数调用是浪费资源地,尤其是当构造函数和赋值操作符分配内存地时候.在一些大地类里面,你可能拥有一个构造函数和一个赋值操作符都要调用同一个负责分配大量内存空间地函数.在这种情况下,你必须使用初始化列表,以避免不必要地分配两次内存.在内建类型如或者或者其它没有构造函数地类型下,在初始化列表和在构造函数体内赋值这两种方法没有性能上地差别.不管用那一种方法,都只会有一次赋值发生.有些程序员说你应该总是用初始化列表以保持良好习惯,但我从没有发现根据需要在这两种方法之间转换有什么困难.在编程风格上,我倾向于在主体中使用赋值,因为有更多地空间用来格式化和添加注释,你可以写出这样地语句:;或者(, , ()); 初始化列表地成员初始化顺序初始化类成员时,是按照成员声明地顺序初始化地,而不是按照出现在初始化列表中地顺序.因为一个类可以有多个构造函数,那么初始化列表可能各有不同,但是却只有一个析构函数,析构函数地析构顺序是和构造地顺序相反地.如果按照初始化列表来初始化,而且有多个构造函数地情况下,那么析构地时候就不能确定析构地顺序.只有按照声明地顺序,无论构造函数中初始化列表是何顺序,都可以按照确定地顺序析构.保持一致性最主要地作用是避免以下类似情况地发生:{ ( , ); ; ;}; ( , ) : (), (){} 你可能以为上面地代码将会首先做,然后做,最后它们有相同地值.但是编译器先初始化,然后是,,因为它们是按这样地顺序声明地.结果是将有一个不可预测地值.有两种方法避免它,一个是总是按照你希望它们被初始化地顺序声明成员,第二个是,如果你决定使用初始化列表,总是按照它们声明地顺序罗列这些成员.这将有助于消除混淆.b5E2R。
c++派生类的构造函数

c++派生类的构造函数C++是一门面向对象的编程语言,它允许程序员使用类和对象来封装数据和行为。
在C++中,派生类是基于已存在的类(称为基类)创建的一种新类。
派生类从基类继承了数据和方法,并且还可以添加新的属性和方法。
在C++中,派生类的构造函数是指创建派生类对象时所调用的函数。
派生类的构造函数负责初始化派生类对象中从基类继承的成员和派生类自己添加的成员。
本文将详细介绍C++派生类的构造函数。
在C++中,派生类的构造函数必须调用基类的构造函数,以初始化从基类继承的成员变量。
在创建派生类对象时,首先创建基类对象,然后再对派生类对象进行初始化。
1. 构造函数必须有与类名相同的名称。
2. 构造函数可以有参数,也可以没有参数。
3. 派生类必须调用基类的构造函数,以初始化从基类继承的成员变量。
下面是一个基类Person和一个派生类Student的定义:```cppclass Person{protected:string name;int age;public:Person(){}void setName(string n){name = n;}void setAge(int a){age = a;}};在定义派生类Student的时候,通过public继承了基类Person。
此时,派生类的构造函数必须调用基类的构造函数,以初始化从基类继承的成员变量name和age。
派生类新增加了一个成员变量grade,需要在自己的构造函数中进行初始化。
派生类构造函数可以有多种调用方式,具体如下:1. 用基类构造函数初始化列表初始化派生类对象初始化列表是C++语言提供的一种简洁的初始化成员变量的语法。
它使用冒号(:)后接一个以逗号分隔的初始化列表,在其中对派生类和基类成员变量进行初始化。
下面是Student类中使用初始化列表对基类成员变量进行初始化的方法:在上面的代码中,派生类Student的构造函数使用冒号后接一个初始化列表来初始化基类成员变量name和age。
显式 调用 构造函数 初始化

显式调用构造函数初始化1.引言1.1 概述在编程中,构造函数是用来创建和初始化对象的特殊成员函数。
在大多数情况下,构造函数会自动调用,以便在创建对象时执行必要的初始化操作。
然而,有时候我们可能想要显式地调用构造函数来进行特定的初始化。
显式调用构造函数的目的是为了更加灵活地控制对象的初始化过程,以满足特定的需求。
通过显式调用构造函数,我们可以传入特定的参数来对对象进行初始化,而不是仅仅依靠默认的构造函数进行初始化。
在C++中,我们可以使用构造函数的初始化列表来显式初始化对象的成员变量。
初始化列表是构造函数的一部分,在构造函数体之前使用冒号进行标识,并列出每个成员变量的初始化方式。
通过初始化列表,我们可以在构造函数中显式地设置每个成员变量的初值。
显式调用构造函数和初始化列表的使用可以提高代码的可读性和性能。
通过显式调用构造函数,我们可以清楚地指定初始化的顺序和方式,并且可以在对象创建的同时完成必要的初始化操作。
使用初始化列表可以减少不必要的临时对象的创建和销毁,提高代码的效率。
总之,显式调用构造函数和初始化列表是在C++中实现对象初始化的重要技术。
通过它们,我们可以更加灵活地控制对象的初始化过程,并且提高代码的可读性和性能。
在接下来的文章中,我们将深入探讨显式调用构造函数和初始化列表的具体用法和注意事项。
1.2文章结构1.2 文章结构本文主要探讨显式调用构造函数初始化的相关内容。
在引言部分,我们将概述本文的主要内容和目的。
接下来的正文部分将详细介绍显式调用构造函数的概念以及其在初始化过程中的应用。
具体地,我们将探讨显式调用构造函数的语法和用法,并举例说明其在不同情况下的实际应用场景。
在第二节中,我们将重点介绍初始化的概念及其与显式调用构造函数的关系。
我们将讨论不同的初始化方式,包括默认初始化、直接初始化以及拷贝初始化,并分析它们与显式调用构造函数的异同点。
我们还将探索显式调用构造函数在初始化过程中的优势和适用性,并与其他初始化方法进行比较。
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
C++类构造函数初始化列表
初始化列表的定义
在使用C++编程的过程当中,常常需要对类成员进行初始化,通常的方法有两种:一种是构造函数内对类的成员赋值,一种则是使用初始化列表的构造函数显式的初始化类的成员。
构造函数初始化列表以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化式。
例如:
class CExample {
public:
int a;
float b;
//构造函数初始化列表
CExample(): a(0),b(8.8) {}
//构造函数内部赋值
CExample()
{
a=0;
b=8.8;
}
};
从技术上说,用初始化列表来初始化类成员比较好,但是在大多数情况下,两者实际上没有什么区别。
第二种语法被称为成员初始化列表,之所以要使用这种语法有两个原因:一个原因是必须这么做,另一个原因是出于效率考虑
初始化列表的必要性
初始化和赋值对内置类型的成员没有什么大的区别,像上面的任一个构造函数都可以。
但在一些情况下,初始化列表可以做到构造函数做不到的事情:
1、类里面有const类型的成员,它是不能被赋值的,所以需要在初始化列表里面初始化它;
2、引用类型的成员(也就是名字成员,它作为一个现有名字的别名),也是需要在初始化列表里面初始化的,目的是为了生成了一个其名字成员在类外可以被修改而在内部是只读的对象;
3、需要调用基类的构造函数,且此基类构造函数是有参数的;
4、类里面有其他类类型的成员,且这个“其他类”的构造函数是有参数的。
举个例子:设想你有一个类成员,它本身是一个类或者结构,而且只有一个带一个参数的构造函数。
class CExampleOld { public: CExampleOld(int x) { ... } };
因为CExampleOld有一个显式声明的构造函数,编译器不产生一个缺省构造函数(不带参数),所以没有一个整数就无法创建CExampleOld的一个实例。
CExampleOld* pEO = new CExampleOld; // 出错!!
CExampleOld* pEO = new CExampleOld(2); // OK
如果CExampleOld是另一个类的成员,你怎样初始化它呢?答案是你必须使用成员初始化列表。
class CExampleNew { CExampleOld m_EO; public: CExampleNew(); };
// 必须使用初始化列表来初始化成员m_EO
//CExampleNew::CExampleNew() : m_EO(2) {……}
没有其它办法将参数传递给m_EO。
情况3和4其实一样的道理。
如果成员是一个常量对象或者引用也是一样。
根据C++的规则,常量对象和引用不能被赋值,它们只能被初始化。
初始化列表与构造函数赋值的效率比较
首先把数据成员按类型分类并分情况说明:
1.内置数据类型,复合类型(指针,引用)
在成员初始化列表和构造函数体内进行,两者在性能和结果上都是一样的
2.用户定义类型(类类型)
两者在结果上相同,但是性能上存在很大的差别。
因为编译器总是确保所有成员对象在构造函数体执行之前初始化,所以对于用户自定义类型(类),在初始化列表中只会调用类的构造函数,在构造函数体中赋值就会先调用一次类的构造函数,然后再调用一次类的赋值操作符函数。
显然后者在性能上有所损失,特别对于构造函数和赋值操作符都需要分配内存空间的情况,使用初始化列表,就可以避免不必要的多次内存分配。
举个例子:假定你有一个类CExample具有一个CString类型的成员m_str,你想把它初始化为"Hi,how are you."。
你有两种选择:
1、使用构造函数赋值
CExample::CExample()
{
// 使用赋值操作符// CString::operator=(LPCTSTR);
m_str = _T("Hi,how are you.");
}
2、使用初始化列表
CExample::CExample() : m_str(_T("Hi,how are you.")) {}
编译器总是确保所有成员对象在构造函数体执行之前被初始化,因此在第一个例子中编译的代码
将调用CString::Cstring来初始化m_str,这在控制到达赋值语句前完成。
在第二个例子中编译器产生一个对CString:: CString(LPCTSTR)的调用并将"Hi,how are you."传递给这个函数。
结果是在第一个例子中调用了两个CString函数(构造函数和赋值操作符),而在第二个例子中只调用了一个函数。
在CString的例子里这是无所谓的,因为缺省构造函数是内联的,CString只是在需要时为字符串分配内存(即,当你实际赋值时)。
但是,一般而言,重复的函数调用是浪费资源的,尤其是当构造函数和赋值操作符分配内存的时候。
在一些大的类里面,你可能拥有一个构造函数和一个赋值操作符都要调用同一个负责分配大量内存空间的Init函数。
在这种情况下,你必须使用初始化列表,以避免不必要的分配两次内存。
在内建类型如ints或者longs或者其它没有构造函数的类型下,在初始化列表和在构造函数体内赋值这两种方法没有性能上的差别。
不管用那一种方法,都只会有一次赋值发生。
有些程序员说你应该总是用初始化列表以保持良好习惯,但我从没有发现根据需要在这两种方法之间转换有什么困难。
在编程风格上,我倾向于在主体中使用赋值,因为有更多的空间用来格式化和添加注释,你可以写出这样的语句:
x=y=z=0;
或者
memset(this, 0, sizeof(this));
初始化列表的成员初始化顺序
C++初始化类成员时,是按照成员声明的顺序初始化的,而不是按照出现在初始化列表中的顺序。
因为一个类可以有多个构造函数,那么初始化列表可能各有不同,但是却只有一个析构函数,析构函数的析构顺序是和构造的顺序相反的。
如果按照初始化列表来初始化,而且有多个构造函数
的情况下,那么析构的时候就不能确定析构的顺序。
只有按照声明的顺序,无论构造函数中初始化列表是何顺序,都可以按照确定的顺序析构。
保持一致性最主要的作用是避免以下类似情况的发生:
class CExample
{
CExample(int x, int y);
int m_x;
int m_y;
};
CExample::CExample(int x, int y) : m_y(y), m_x(m_y){}
你可能以为上面的代码将会首先做m_y=y,然后做m_x=m_y,最后它们有相同的值。
但是编译器先初始化m_x,然后是m_y,,因为它们是按这样的顺序声明的。
结果是m_x将有一个不可预测的值。
有两种方法避免它,一个是总是按照你希望它们被初始化的顺序声明成员,第二个是,如果你决定使用初始化列表,总是按照它们声明的顺序罗列这些成员。
这将有助于消除混淆。