1. 构造函数
1.1. 缺省构造函数
缺省构造函数指不带任何参数的构造函数。
1.1.1. 使用方法
class CA{
public:
CA() { mi = 0 ; }
int getMi(){ return mi ; }
private:
int mi;
};
int main(){
CA oa;
CA* poa = new CA; // 或者 new CA();
int i;
i = oa.getMi();
i = poa->getMi();
delete poa;
return 0;
}
1.1.2. 隐蔽性
缺省构造函数是非常重要的构造函数,重要性体现在"缺省"2个字上面。它是编译器在缺省情况下所使用的构造函数。缺省情况是指在编译过程中需要使用构造函数而程序中又没有提供任何构造函数的情况,在这种情况下,编译器默默的为CA创建了一个不带参数的构造函数,此构造函数没有做任何的初始化工作,只是为了编译程序的需要而产生。
" 创建没有任何构造函数的类的对象时候
例如:
class CA{
public:
int getMi(){ return mi ; }
private:
int mi;
};
int main(){
CA oa;
int i = oa.getMi();
return 0;
}
本例中,类CA没有提供任何构造函数,但是在main函数中还是可以生成一个CA的对象出来。
但是oa对象的成员变量mi的值是不确定的,因为并没有做任何初始化工作。
" 从没有任何构造函数的类派生出子类的时候
class CBase{
public:
int getMi(){ return mi ; }
private:
int mi;
};
class CDerived : public CBase {
public:
int getMj(){ return mj ; }
private:
int mj;
};
int main(){
CDerived oderived;
int i = oderived.getMi();
int j = oderived.getMj();
return 0;
}
在编译过程中,类CDerived的构造函数CDerived( int j)会调用基类CBase的缺省构造函数。
" 一个类中含有一个以另外一个类为类型的成员变量
class CA{
public:
int getMi(){ return mi ; }
private:
int mi;
};
class CB{
public:
int getMj(){ return mj ; }
private:
CA moa;
int mj;
};
1.1.3. 将缺省构造函数申明为private
从上面所说知道,编译器会在需要的时候生成缺省构造函数。但有时候,我们想禁止这样做,不想客户程序通过使用构造函数来创建类的对象,通过将缺省构造函数显式的申明为private可以做到这点。
典型的一个应用是GOF4的23个设计模式中的单例模式(Singleton)
1.2. 带参数构造函数
前面的例子中,类CA的缺省构造函数只能静态的初始化成员变量mi,当需要客户程序动态的初始化mi的时候,就需要用到带参数构造函数。
1.2.1. 使用方法
class CA{
public:
CA( int i) { mi = i ; }
int getMi(){ return mi ; }
private:
int mi;
};
int main(){
CA oa( 8 );
CA* poa = new CA( 8 );
int i;
i = oa.getMi();
i = poa->getMi();
delete poa;
return 0;
}
1.2.2. 需要注意的地方
如果一个类只声明了带参数的构造函数,而没有提供缺省构造函数,那么就不能通过使用缺省构造函数来创建该类的对象。
如上例的CA类,不能这样来创建CA的对象:
CA oa;
CA* poa = new CA();
在编译的过程中编译器将提示错误: 没有构造函数CA()。
上面这点是比较明显的,也很容易更正,但是,如果在有继承的情况下,情况就变得比较隐蔽一些。
考虑下面的例子:
#include <iostream>
class CBase{
public:
CBase( int i ){ mi = i ; }
int getMi(){ return mi ; }
private:
int mi;
};
class CDerived : public CBase {
public:
CDerived( int j ){ mj = j ; }
int getMj(){ return mj ; }
private:
int mj;
};
int main(){
CDerived oderived( 8 );
return 0;
}
在编译的过程中编译器将提示错误: 没有构造函数CBase ()。你不要奇怪说我main函数中并没有使用CBase的缺省构造函数。呵呵,编译器替你做了这个。
1.3. 基类和子类构造函数的调用顺序
1.3.1. 调用顺序
当生成子类的对象时候,首先会调用基类的缺省构造函数(如果未指定要调用的基类构造函数),然后再调用子类的指定构造函数。
下面例子可以说明这点:
#include <iostream>
class CBase{
public:
CBase(){ std::cout << "base constructor" << endl; }
int getMi(){ return mi ; }
private:
int mi;
};
class CDerived : public CBase {
public:
CDerived(){ std::cout << "derived constructor" << endl; }
int getMj(){ return mj ; }
private:
int mj;
};
int main(){
CDerived oderived;
return 0;
}
程序执行后打印的结果:
base constructor
derived constructor
1.3.2. 为什么子类构造函数要调用基类的构造函数
这是为了保证子类能够正确的初始化所有的成员变量。
注意前面的例子,类CBase有一个private成员变量mi, 这就表示只有类CBase自己可以初始化这个变量。然而,CBase的子类也从CBase继承了这个变量,并且还从CBase继承了getMi()。但是,CBase的子类如何初始化mi呢?
解决方案就是在CBase子类的构造函数中调用CBase的构造函数。
#include <iostream>
class CBase{
public:
CBase(){ mi = 10 ; }
int getMi(){ return mi ; }
private:
int mi;
};
class CDerived : public CBase {
public:
CDerived() { mj = 20 ; }
int getMj(){ return mj ; }
private:
int mj;
};
int main(){
CDerived oderived ;
int i = oderived.getMi();
int j = oderived.getMj();
std::cout<< "mi= " << i << ", mj= " << j <<endl;
return 0;
}
程序执行后打印的结果:
mi= 10, mj= 20
从结果可以看到,CDerived 的构造函数调用了CBase缺省构造函数。
这个调用过程是由编译器自动完成的。
但是,编译器只会自动调用基类的缺省构造函数,如果程序需要在子类的构造函数中指定要调用的基类构造函数,就必须由程序员自己来做了。看看下面的例子:
#include <iostream>
class CBase{
public:
CBase( int i ){ mi = i ; }
int getMi(){ return mi ; }
private:
int mi;
};
class CDerived : public CBase {
public:
CDerived( int i, int j ) : CBase(i) { mj = j ; }
int getMj(){ return mj ; }
private:
int mj;
};
int main(){
CDerived oderived( 10, 20 ) ;
int i = oderived.getMi();
int j = oderived.getMj();
std::cout<< "mi= " << i << ", mj= " << j <<endl;
return 0;
}
程序执行后打印的结果:
mi= 10, mj= 20
看看CDerived( int i, int j ) : CBase(i) { mj = j ; },CDerived( int i, int j )调用了CBase(i)来初始化mi。
1.4. 构造函数的initiallist(初始化列表)
看看上面的例子中CDerived( int i, int j ) : CBase(i) { mj = j ; },CDerived( int i, int j )是怎样调用CBase(i)。使用的是initiallist。
initiallist的一个重要作用就是在子类中调用基类的构造函数。需要注意的是,在JAVA中实现方法不一样,JAVA中是在构造函数体内使用super关键字来实现的,以上面的例子为例,在JAVA中的实现方法是:
class CBase{
public:
CBase( int i ){ mi = i ; }
int getMi(){ return mi ; }
private:
int mi;
}
class CDerived extends CBase {
public:
CDerived( int i, int j ) { super(i); mj = j ; }
int getMj(){ return mj ; }
private:
int mj;
}
JAVA对使用super有一个限制: super语句必须是构造函数的第一条语句。
1.4.1. 哪些情况下也必须要用initiallist
除了上面所说的在子类中调用基类的构造函数时需要使用initiallist外,还有另外一些情况下也需要使用initiallist:
" 一个类中含有一个以另外一个类为类型的成员变量,而又不能使用该类的缺省构造函数时
class CA{
public:
CA( int i) { mi = i ; }
int getMi(){ return mi ; }
private:
int mi;
};
class CB{
public:
CB(int i, int j ) { mj = j ; }
int getMj(){ return mj ; }
private:
CA moa;
int mj;
};
int main(){
CB ob( 10, 20 );
return 0;
}
此程序编译的时候会提示错误:没有定义CA类的缺省构造函数。
解决方法:
把CB(int i, int j ) { mj = j ; } 改为CB(int i, int j ): moa(i) { mj = j ; }
" 一个类中含有const成员变量
class CA{
public:
CA( int i) { mi = i; ; mi2 = 20; }
int getMi(){ return mi }
int getMi2(){ return mi2 ; }
private:
int mi;
const int mi2;
};
int main(){
CA oa( 10 );
int i, i2;
i = oa.getMi();
i2 = oa.getMi2();
return 0;
}
上面这个程序在编译的时候会提示错误: 不能给const成员变量mi2赋值。const成员变量只能初始化而不能赋值。语句mi2 = 20;虽然是在构造函数中,但它还是一条赋值语句。
解决方案:
改CA( int i) { mi = i; ; mi2 = 20; } 为 CA( int i) : mi2( 20 ) { mi = i; }。
" 一个类中含有引用成员变量
class CA{
public:
CA( int i, int i2 ) : mi2( i2 ){ mi = i; }
int getMi(){ return mi ; }
int getMi2(){ return mi2 ; }
private:
int mi;
int& mi2;
};
int main(){
int i = 10 , i2 = 20;
CA oa( i, i2 );
i = oa.getMi();
i2 = oa.getMi2();
return 0;
}
1.4.2. 需要注意的地方
每个成员在成员初始化表中只能出现一次。且初始化的顺序不是由名字在初始化表中的顺序决定而是由成员在类中被声明的顺序决定的。例如:
class CA{
public:
CA( int i ) : mi(i), mi2( mi + 1 ){ ; }
int getMi(){ return mi ; }
int getMi2(){ return mi2 ; }
private:
int mi2;
int mi;
};
int main(){
CA oa( 10 );
int i, i2;
i = oa.getMi();
i2 = oa.getMi2();
return 0;
}
在此例中, mi2声明在mi之前。然而,在initiallist中,先初始化mi,而mi2的初始化有依赖于mi,结果是,mi正确的初始化,而mi2是不能正确的初始化的,其值是不确定的。
1.5. copy构造函数
copy构造函数的重要性类似于缺省构造函数,体现在它的隐藏性。
以一个非常简单的,非常常见的,而又让很多初学者倍感郁闷的bug:
class CA{
public:
CA(){ pmi = new int( 10 ) ; }
virtual ~CA(){delete pmi; }
private:
int* pmi;
};
void fun( CA oa){
return;
}
int main(){
CA oa;
fun(oa);
return 0;
}
这个非常简单的程序一运行就会coredump。
2个解决方法:
1. 把void fun( CA oa) 改为void fun( CA &oa)
2. 在CA中增加如下的copy构造函数:
CA( const CA &oa) { pmi = new int( *oa.pmi ) ; }
1.5.1. 隐藏性体现
1.5.1.1. 函数参数体现
如上的例子,函数void fun( CA oa),以CA类为参数,而且是传值方式,这样,编译器会在函数体内插入一个CA类的临时对象,此对象使用CA的copy构造函数来初始化,因为CA没有提供copy构造函数,所以编译器采用默认的copy构造函数,即bitcopy方式,这样,内部临时对象的pmi和参数对象oa的pmi的值是一样的,即指向同一个地址。如此,在函数fun执行完毕之后,编译器会要求调用这个对象的析构函数,把这个临时对象的pmi 变量delete掉。然后,在main函数执行完之后,参数对象oa也会被执行它的析构函数,把oa的pmi 变量delete掉。然而,因为2个pmi是指向同一个地址,只能delete一次,所以,在第二次delete的时候就出错了。
解决方案1,把参数改为CA &oa,即按引用传递参数,这样,就避免了编译器创建临时对象。但是,这个方法治标不治本,因为在其他的时候其他的人来使用CA类的时候,很可能再犯这个错误。
解决方案2彻底解决了这个隐藏的错误。定义CA的copy构造函数,重新创建pmi,使得2个pmi不指向同一个地址,从而避免了这个错误。
1.5.1.2. 函数返回值体现
道理和使用函数参数一样。看下面的例子:
class CA{
public:
CA(){ pmi = new int( 10 ) ; }
virtual ~CA(){delete pmi; }
private:
int* pmi;
};
CA fun(){
CA oa;
return oa;
}
int main(){
fun();
return 0;
}
同样的,这个程序一运行就会崩溃。
原因也是: 如果一个函数的返回值是一个类的对象,那么在return出这个函数是,会调用这个类的copy构造函数来为要返回的对象初始化。解决方法有3个: 第一个就是返回对象的引用而不是返回对象值(但是这个方法有很大的缺陷,参看effective c++)。另一个方法就是提供自定义的copy构造函数。
第三个方法是:
把函数fun改为
CA fun(){
return CA();
}
这样做,避免了return的时候产生临时对象,从而不会使程序出错。
1.5.2. 与assignment 的关系
1.区分2者
CA oa;
CA ob;
CA oc = oa; // 使用了copy 构造函数
ob = oa; //使用了assignment
2. effective c++中的建议: 当你实现了类的copy构造函数,也应该实现该类的assignment
2. 析构函数
析构函数比较简单,只提醒一点: [effective c++] Item 14: Make sure base classes have virtual destructors
即:保证基类的virtual的析构函数是virtual的。
详细信息参看effective c++。
#include <iostream>
class CBase{
public:
CBase(){ std::cout << "base constructor" << endl; }
CBase(){ std::cout << "base destructor" << endl; }
int getMi(){ return mi ; }
private:
int mi;
};
class CDerived : public CBase {
public:
CDerived(){ std::cout << "derived constructor" << endl; }
~CDerived(){ std::cout << "derived destructor" << endl; }
int getMj(){ return mj ; }
private:
int mj;
};
int main(){
CBase* poderived = new CDerived() ;
delete poderived;
return 0;
}
执行结果,打印:
base constructor
derived constructor
base destructor
为什么运行了derived constructor而没有运行derived destructor?那么在derived constructor中分配的资源就没有被释放掉。
把CBase的析构函数改为
virtual ~CBase(){ std::cout << "base destructor" << endl; }
执行结果,打印:
base constructor
derived constructor
derived destructor
base destructor