一、我的问题是关于初始化C++类成员的。我见过许多这样的代码:
CSomeClass::CSomeClass()
{
x=0;
y=1;
}
而在别的什么地方则写成下面的样子:
CSomeClass::CSomeClass() : x(0), y(1)
{
}
我的一些程序员朋友说第二种方法比较好,但他们都不知道为什么是这样。你能告诉我这两种类成员初始化方法的区别吗?
回答
从技术上说,你的程序员朋友是对的,但是在大多数情况下,两者实际上没有区别。有两个原因使得我们选择第二种语法,它被称为成员初始化列表:一个原因是必 须的,另一个只是出于效率考虑。
让我们先看一下第一个原因——必要性。设想你有一个类成员,它本身是一个类或者结构,而且只有一个带一个参数的构造函数。
class CMember {
public:
CMember(int x) { ... }
};
因为Cmember有一个显式声明的构造函数,编译器不产生一个缺省构造函数(不带参数),所以没有一个整数就无法创建Cmember的一个实例。
CMember* pm = new CMember; // Error!!
CMember* pm = new CMember(2); // OK
如果Cmember是另一个类的成员,你怎样初始化它呢?你必须使用成员初始化列表。
class CMyClass {
CMember m_member;
public:
CMyClass();
};
//必须使用成员初始化列表
CMyClass::CMyClass() : m_member(2)
{
...
}
没有其它办法将参数传递给m_member,如果成员是一个常量对象或者引用也是一样。根据C++的规则,常量对象和引用不能被赋值,它们只能被初始化。
第二个原因是出于效率考虑,当成员类具有一个缺省的构造函数和一个赋值操作符时。MFC的Cstring提供了一个完美的例子。假定你有一个类 CmyClass具有一个Cstring类型的成员m_str,你想把它初始化为 "yada yada. "。你有两种选择:
CMyClass::CMyClass() {
// 使用赋值操作符
// CString::operator=(LPCTSTR);
m_str = _T( "yada yada ");
}
//使用类成员列表
// and constructor CString::CString(LPCTSTR)
CMyClass::CMyClass() : m_str(_T( "yada yada "))
{
}
在 它们之间有什么不同吗?是的。编译器总是确保所有成员对象在构造函数体执行之前初始化,因此在第一个例子中编译的代码将调用CString:: Cstring来初始化m_str,这在控制到达赋值语句前完成。在第二个例子中编译器产生一个对CString:: CString(LPCTSTR)的调用并将 "yada yada "传递给这个函数。结果是在第一个例子中调用了两个Cstring函数(构造函数和赋值操作符),而在第二个例子中只调用了一个函数。在 Cstring的例子里这是无所谓的,因为缺省构造函数是内联的,Cstring只是在需要时为字符串分配内存(即,当你实际赋值时)。但是,一般而言, 重复的函数调用是浪费资源的,尤其是当构造函数和赋值操作符分配内存的时候。在一些大的类里面,你可能拥有一个构造函数和一个赋值操作符都要调用同一个负 责分配大量内存空间的Init函数。在这种情况下,你必须使用初始化列表,以避免不要的分配两次内存。在内部类型如ints或者longs或者其它没有构 造函数的类型下,在初始化列表和在构造函数体内赋值这两种方法没有性能上的差别。不管用那一种方法,都只会有一次赋值发生。有些程序员说你应该总是用初始 化列表以保持良好习惯,但我从没有发现根据需要在这两种方法之间转换有什么困难。在编程风格上,我倾向于在主体中使用赋值,因为有更多的空间用来格式化和 添加注释,你可以写出这样的语句:x=y=z=0;
或者memset(this,0,sizeof(this));
注意第二个片断绝对是非面向对象的。
当我考虑初始化列表的问题时,有一个奇怪的特性我应该警告你,它是关于C++初始化类成员的,它们是按照声 明的顺序初始化的,而不是按照出现在初始化列表中的顺序。
class CMyClass {
CMyClass(int x, int y);
int m_x;
int m_y;
};
CMyClass::CMyClass(int i) : m_y(i), m_x(m_y)
{
}
你 可能以为上面的代码将会首先做m_y=I,然后做m_x=m_y,最后它们有相同的值。但是编译器先初始化m_x,然后是m_y,,因为它们是按这样的顺 序声明的。结果是m_x将有一个不可预测的值。我的例子设计来说明这一点,然而这种bug会更加自然的出现。有两种方法避免它,一个是总是按照你希望它们 被初始化的顺序声明成员,第二个是,如果你决定使用初始化列表,总是按照它们声明的顺序罗列这些成员。这将有助于消除混淆。
二、很多的人对中构造函数寝初始化很多的困惑,对冒号后初始化不是太明白,总搞不清楚它们之间的区别,我想把我对这个问题的理解和看法和大家讨论讨 论。
在程序中定义变量并初始化的机制中,有两种形式,一个是我们传统的初始化的形式,即赋值运算符赋值,还有一种是括号赋值,如:
int a=10;
char b='r';\\赋值运算符赋值
int a(10);\
char b('r');\\括号赋值
以上定义并初始化的形式是正确的,可以通过编译,但括号赋值只能在变量定义并初始化中,不能用在变量定义后再赋值,这是和赋值运算符赋值的不同之处,如:
(1)
int a; \\先定义一个变量
......
a=10; \\根据需要赋值
(2)
int b; \\先定义一个变量
......
b(10); \\和(1)一样根据需要赋值
(1)是可以用通过编译,定义一个变量a但并没有初始化,在需要变量a的时候,通过赋值运算符把10赋给a,而在(2)中,是通过括号把10赋值给b,但 编译系统认为
这是一个函数的调用,函数名为b,10为实际参数,所以编译错误。因此,括号赋值只用在定义变量并初始化中。
现在我们来看构造函数中冒号初始化和函数初始化的问题,类构造函数的作用是创建一个类的对象时,调用它来构造这个类对象的数据成员,一要给出此数据成员分 配内存空间,二是要给函数数据成员初始化,构造数据成员是按数据成员在类中声明的顺序进行构造。
冒号初始化与函数体初始化的区别在于:
冒号初始化是给数据成员分配内存空间时就进行初始化,就是说分配一个数据成员只要冒号后有此数据成员的赋值表达式(此表达式必须是括号赋值表达式),那么 分配了内存空间后在进入函数体之前给数据成员赋值,就是说初始化这个数据成员此时函数体还未执行。
对于在函数中初始化,是在所有的数据成员被分配内存空间后才进行的。
这样是有好处的,有的数据成员需要在构造函数调入之后函数体执行之前就进行初始化如引用数据成员,常量数据成员和对象数据成员,看下面的一段程序:
class student
{public :
student ()
.
.
.
protected:
const int a;
int &b;
}
student ::student (int i,int j)
{
a=i;
b=j;
}
在Student类中有两个数据成员,一个是常量数据成员,一个是引用数据成员,并且在构造函数中初始化了这两个数据成员,但是这并不能通过编译,因为常 量初始化时必须赋值,它的值是不能再改变的,与常量一样引用初始化也需要赋值,定义了引用后,它就和引用的目标维系在了一起,也是不能再被赋值的。所以C
++":"后初始化的机制,使引用和常量数据成员变为可能的,Student类的构造函数应为:
student ::student(int i,int j):a(i),b(j){}
在下面的程序:
class teach
{
public :
teach(char *p="name",int a=0)
.
.
.
protected:
char name[30];
int age;
}
teach ::teach(char*p,int a)
{
strcopy(name ,p);
age=a;
cout>>name>>endl;
}
class student
{
public:
student (char *p="name");
.
.
.
protected;
char name[30];
teach teacher;
};
student::student(char *p)
{
strcopy(name,p);
cont>>name>>endl;
}
在上面的程序中通不过编译,编译系统会告诉你teacher这个类对象缺默认构造函数,因为在teach 类中没有定义默认的构造函数。那么带参数的构造函数怎么进行构造呢?通过我们前面提到的冒号赋值。那它的构造函数应该是:
student::student(char *p,char *pl,int ag):teacher(pl,ag)
{
strcopy(name,p);
cont>>name>>endl;
}
就是说在没有默认构造函数的时候,如果一个类对象是另一个类的数据成员,那么初始化这个数 据成员,就应该放到冒号后面。这样可以带参数。在类的定义中,如:
protected;
char name[30];
teach teacher
类对象是不能带参数的,因为它只是声明。
所以在C++中就增加了这种机制,这是面向对象编程所必须的。不知道我讲明白没有。如不明白请查阅有关资料。