&&&每个类只有一个析构函数和一个赋值函数,但可以有多个构造函数(包含一个拷贝构造函数,其他称为普通构造函数)。对于任一的一个类A,如果不想编写上述的函数,C++编译器就会自动产生四个缺省的函数。例如:
A(void); //缺省的无参构造函数 A(const A &a); //缺省的拷贝构造函数 ~A(void); //缺省的析构函数 A& operator=(const A& a); //缺省的赋值函数
那么既然能自动生成函数,那么为什么还要自己编写?
原因:如果使用了默认的构造和析构函数,那么就等于放弃了,变量自主初始化和自主清理的功能。另外默认的拷贝构造函数和赋值函数,都是采用位拷贝,而非针对特定的值进行拷贝。这样的两个函数很容易出现错误。
例如:经典的笔试面试题String类的赋值和拷贝构造函数。
Class String{
public:
String(const char *str=NULL);
String(const String &other);
~String(void); //析构函数
String& operator=(const String &other); //赋值函数
private:
char *m_data;
};
相关知识:
构造函数的初始化表:构造函数有一个特殊的初始化方式叫“初始化表达式表”,这个初始化表位于函数的形参表之后,在函数体之前。表明这个初始化表在函数体内的任何代码执行之前执行。使用构造函数的初始化表有几点需要特别注意:
a)如果类存在继承关系,派生类必须在其初始化表里调用基类的构造函数
Class A{
A(int x);
};
Class B:public A{
B(int x,int y);
};
B::B(int x,int y):A(x){} //在初始化表里调用A的构造函数
b)类的const常量只能在初始化表里被初始化
c)类的数据成员的初始化可以采用初始化表或函数体内赋值两种方式,这两种方式的效率不完全相同。对于非内部数据类型应该采用初始化表初始化,而内部数据类型两种初始化的效率相近。
构造函数与析构函数的次序:
构造函数从类层次的最根处开始,在每一层中首先调用基类的构造函数,然后调用成员对象的构造函数。析构函数则严格按照与构造函数相反的次序执行。
另外成员对象的初始化次序完全不受它们在初始化表中的次序的影响,只由成员对象在类中声明的次序决定。
经典例子String类的构造函数与析构函数:
String::String(const char *str){
if(str==NULL){
m_data=new char[1];
*m_data='\0';
}else{
int len=strlen(str);
m_data=new char[len+1];
strcpy(m_data,str);
}
}
String::~String(){
delete[] m_data;
}
相关知识:
对于拷贝构造函数和赋值函数:因为并非所有的对象都会使用到这两个函数,所有它们能很容易被轻视。如果不主动的编写拷贝和赋值函数,编译器将会以“位拷贝”的方式生成默认的函数。假如类包含指针变量,那么这两个默认生成的函数就很容易出错。一String类的两个对象a,b为例子,令a.m_data指向“Hello”,b.m_data指向“World”,现在将a赋值给b,那么就意味着b.m_data=a.m_data.将造成如下的错误:
a):b.m_data原来指向的内存没有被释放造成内存泄露
b):b.m_data和a.m_data指向同一内存,a或者b任何一方变得都会影响到另一方
c):调用析构函数时候,m_data被释放两次
典型例子String类的拷贝构造函数和赋值函数
String::String(const String &other){
int length=strlen(other.m_data);
m_data=new char[length+1];
strcpy(m_data,other.m_data);
}
String &String::operator =(const String &other){
if(this==&other)
return *this;
delete []m_data;
int len=strlen(other.m_data);
m_data=new char[len+1];
strcpy(m_data,other.m_data);
return *this;
}
知识点:
类String拷贝构造函数与普通构造函数的区别是:在函数入口处无需与NULL进行比较,这是因为引用不可能是空,但是指针可以为NULL
类Sting的赋值函数比构造函数要复杂得多,赋值函数要分成四步实现:
a):检查自己赋值
b):释放原有的内存
c):分配新的内存资源,并复制字符串
d):返回对象的引用,目的为了可以连续赋值
&&&如何在派生类中实现类的基本函数
基类的构造函数、析构函数、赋值函数都不能被派生类继承。如果类之间存在继承关系,在编写基本函数的时候应该注意:
a):派生类的构造函数应该在其初始化表里调用基类的构造函数
b):基类与派生类的析构函数应该为虚函数
例如:
Class Base{
public:
virtual ~Base(){cout<<"~Base"<<endl;}
};
class Derived:public Base{
public:
virtual ~Derived(){cout<<"~Derived"<<endl;}
};
main(){
Base *pB=new Derived;
delete pB;
}
这时候输出的结果: ~Derived ~Base
如果析构函数吧设为虚函数,那么输出~Base
c):在编写派生类的赋值函数时,注意不要忘记对基类的数据成员重新赋值
例如:
Class Base{
public:
Base &operator=(const Base& other);
private:
int m_i,m_j,m_k;
};
class Derived:public Base{
public:
Derived& operator=(const Derived &other);
private:
int m_x,m_y,m_z;
};
Derived& Derived::operator=(const Derived &other){
if(this==&other)
return *this;
Base::operator=(other); //对基类的成员重新赋值
m_x=other.m_x;
m_y=other.m_y;
m_z=other.m_z;
return *this;
}