深入理解构造和析构函数
一、 概述
构造函数和析构函数是当你刚接触C++的时候就会碰到的两个概念,也是C++语法中较难掌握的两个概念。但是它们又是学习C++必须掌握的,可以说没有理解构造和析构函数,你的C++就还没有入门。
本文拟对构造函数、析构函数进行系统的介绍,使得那些对这两个概念有初步认识的人能有更进一步的理解。
二、 构造函数做什么?
构造函数从无到有创建对象。
构造函数就象“初始化函数”。它将一连串的随意的内存位变成活的对象。至少它要初始化对象内部所使用的域。它还可以分配资源(内存、文件、信号、套接字等)
"ctor" 是构造函数(constructor)典型的缩写。
三、 List x; 和 List x();有区别吗?
有非常大的区别!
假设List 是某个类的名称。那么函数f() 中声明了一个局部的 List对象,名称为 x:
void f()
{
List x; // Local object named x (of class List)
// ...
}
但是函数 g() 中声明了一个名称为x()的函数,它返回一个 List:
void g()
{
List x(); // Function named x (that returns a List)
// ...
}
四、 Fred 类的默认构造函数总是Fred::Fred()吗?
不。“默认构造函数”是能够被无参数调用的构造函数。因此,一个不带参数的构造函数当然是默认构造函数:
class Fred {
public:
Fred(); // 默认构造函数: 能够被无参数调用
// ...
};
然而,如果参数被提供了默认值,那么带参数的默认构造函数也是可能的:
class Fred {
public:
Fred(int i=3, int j=5); // 默认构造函数: 能够被无参数调用
// ...
};
五、 当建立一个 Fred 对象数组时,哪个构造函数将被调用?
Fred 的默认构造函数(以下讨论除外)。
你无法告诉编译器调用不同的构造函数(以下讨论除外)。如果你的Fred类没有默认构造函数,那么试图创建一个Fred对象数组将会导致编译时出错。
class Fred {
public:
Fred(int i, int j);
// ... 假设 Fred 类没有默认构造函数 ...
};
int main()
{
Fred a[10]; // 错误:Fred 类没有默认构造函数
Fred* p = new Fred[10]; // 错误:Fred 类没有默认构造函数
}
然而,如果你正在创建一个标准的std::vector<Fred>,而不是 Fred对象数组(既然数组是有害的,那么你可能应该这么做),则在 Fred 类中不需要默认构造函数。因为你能够给std::vector一个用来初始化元素的Fred 对象:
#include <vector>
int main()
{
std::vector<Fred> a(10, Fred(5,7));
// 在std::vector 中的 10 个 Fred对象将使用 Fred(5,7) 来初始化
// ...
}
虽然应该使用std::vector而不是数组,但有有应该使用数组的时候,那样的话,有“数组的显式初始化”语法。它看上去是这样的:
class Fred {
public:
Fred(int i, int j);
// ... 假设Fred类没有默认构造函数...
};
int main()
{
Fred a[10] = {
Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7),
Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7)
};
// 10 个 Fred对象将使用 Fred(5,7) 来初始化.
// ...
}
当然你不必每个项都做Fred(5,7)—你可以放任何你想要的数字,甚至是参数或其他变量。重点是,这种语法是(a)可行的,但(b)不如std::vector语法漂亮。记住这个:数组是有害的—除非由于编译原因而使用数组,否则应该用std::vector 取代。
六、 构造函数应该用“初始化列表”还是“赋值”?
初始化列表。事实上,构造函数应该在初始化列表中初始化所有成员对象。
例如,构造函数用初始化列表Fred::Fred() : x_(whatever) { }来初始化成员对象 x_。这样做最普通的好处是提高性能。如,whatever表达式和成员变量 x_ 相同,whatever表达式的结果直接由内部的x_来构造——编译器不会产生对象的两个拷贝。即使类型不同,使用初始化列表时编译器通常也能够做得比使用赋值更好。
建立构造函数的另一种(错误的)方法是通过赋值,如:Fred::Fred() { x_ = whatever; }。在这种情况下,whatever表达式导致一个分离的,临时的对象被建立,并且该临时对象被传递给x_对象的赋值操作。然后该临时对象会在 ;处被析构。这样是效率低下的。
这好像还不是太坏,但这里还有一个在构造函数中使用赋值的效率低下之源:成员对象会被以默认构造函数完整的构造,例如,可能分配一些缺省数量的内存或打开一些缺省的文件。但如果 whatever表达式和/或赋值操作导致对象关闭那个文件和/或释放那块内存,这些工作是做无用功(举例来说,如默认构造函数没有分配一个足够大的内存池或它打开了错误的文件)。
结论:其他条件相等的情况下,使用初始化列表的代码会快于使用赋值的代码。
注意:如果x_的类型是诸如int或者char* 或者float之类的内建类型,那么性能是没有区别的。但即使在这些情况下,我个人的偏好是为了对称,仍然使用初始化列表而不是赋值来设置这些数据成员。
七、 可以在构造函数中使用 this 指针吗?
某些人认为不应该在构造函数中使用this指针,因为这时this对象还没有完全形成。然后,只要你小心,是可以在构造函数(在函数体甚至在初始化列表中)使用this的。
以下是始终可行的:构造函数的函数体(或构造函数所调用的函数)能可靠地访问基类中声明的数据成员和/或构造函数所属类声明的数据成员。这是因为所有这些数据成员被保证在构造函数函数体开始执行时已经被完整的建立。
以下是始终不可行的:构造函数的函数体(或构造函数所调用的函数)不能向下调用被派生类重定义的虚函数。如果你的目的是得到派生类重定义的函数,那么你将无功而返。注意,无论你如何调用虚成员函数:显式使用this指针(如,this->method()),隐式的使用this指针(如,method()),或甚至在this对象上调用其他函数来调用该虚成员函数,你都不会得到派生类的重写函数。这是底线:即使调用者正在构建一个派生类的对象,在基类的构造函数执行期间,对象还不是一个派生类的对象。
以下是有时可行的:如果传递 this 对象的任何一个数据成员给另一个数据成员的初始化程序,你必须确保该数据成员已经被初始化。好消息是你能使用一些不依赖于你所使用的编译器的显著的语言规则,来确定那个数据成员是否已经(或者还没有)被初始化。坏消息是你必须知道这些语言规则(例如,基类子对象首先被初始化(如果有多重和/或虚继承,则查询这个次序!),然后类中定义的数据成员根据在类中声明的次序被初始化)。如果你不知道这些规则,则不要从this对象传递任何数据成员(不论是否显式的使用了this关键字)给任何其他数据成员的初始化程序!如果你知道这些规则,则需要小心。
八、 为何不能在构造函数的初始化列表中初始化静态成员数据?
因为必须显式定义类的静态数据成员。
Fred.h:
class Fred {
public:
Fred();
// ...
private:
int i_;
static int j_;
};
Fred.cpp (或 Fred.C 或其他):
Fred::Fred()
: i_(10) // 正确:能够(而且应该)这样初始化成员数据
, j_(42) // 错误:不能象这样初始化静态成员数据
{
// ...
}
// 必须这样定义静态数据成员:
int Fred::j_ = 42;
九、 如何处理构造函数的失败?
抛出一个异常。
构造函数没有返回类型,所以返回错误代码是不可能的。因此抛出异常是标记构造函数失败的最好方法。
如果你没有或者不愿意使用异常,这里有一种方法。如果构造函数失败了,构造函数可以把对象带入一种“僵尸”状态。你可以通过设置一个内部状态位使对象就象死了一样,即使从技术上来说,它仍然活着。然后加入一个查询(“检察员”)成员函数,以便类的用户能够通过检查这个“僵尸位”来确定对象是真的活着还是已经成为僵尸(也就是一个“活着的死对象”)。你也许想有另一个成员函数来检查这个僵尸位,并且当对象并不是真正活着的时候,执行一个 no-op(或者是更令人讨厌的如 abort())。这样做真的不漂亮,但是如果你不能(或者不想)使用异常的话,这是最好的方法了。
十、 编译器在什么情况下会自动创建“构造函数”和“拷贝构造函数”,还有其他一些函数?创建出来的是一些什么样的函数,是不是缺省的构造函数啊?还是其他的?
在没有为类定义任何构造函数时,编译器会为它生成一个“默认的构造函数”;在没有为类定义拷贝初始化构造函数和析构函数时,编译器也会为它生成一个。
编译器生成的默认构造函数和析构函数其实是什么事情也不做,也就是空函数。而编译器生成的拷贝初始化构造函数则完成“浅拷贝”,也就是逐成员拷贝。
十一、 有点奇怪,拷贝构造函数为什么能操作类的私有成员哪?比如说A(A &a)这个函数里面可以访问对象a里面的私有成员.
访问控制关键字private限制的是类的成员不可以在类作用域以外被直接访问,包括水平和垂直两个方向。而在类内则不受这个限制,也就是说一个类的所有成员函数包括构造函数和析构函数都是可以访问同类对象的私有成员的。
十二、 关于函数返回值与拷贝构造函数的困惑:
书上说函数的返回值是按值传递的,如果返回一个对象应该调用拷贝构函
classObject()
{
public:
Object( const Object &arg )
{
cout<< "cpy constructor\n";
}
}
Objectfun( Object arg )
{
returnarg;
}
然后在主函数中
Object obj1;
Object obj2 = fun( obj1 );
为什么结果显示只调用两次拷贝构函? fun中形参与实参结合的时候以及返回结果的时候都会调用拷贝构造函数,然后对obj2的初始化又会调用一次,感觉应该是三次才对啊?这是不是由编译器优化引起的啊?
答:函数fun()的形参为值传递形式,在函数被调用时形参被拷贝初始化为实参的值,这里有一次拷贝初始化。当函数返回的时候,return语句指定的表达式的值被用于初始化调用者中的对象obj2,这里是第二次拷贝初始化。不存在编译优化的问题。
十三、 在《C++编程思想》中14.8.2中介绍:对于在构造函数中调用一个虚函数的情况,被调用的只是这个函数的本地版本,就是说,虚机制在构造函数中不工作请问这句话怎么理解?
答: 假设你有一个为股票交易建模的类层次结构,例如买单,卖单,等等。为该类交易建立审计系统是非常重要的,这样的话,每当创建一个交易对象,在审计登录项上就生成一个适当的入口项。这看上去不失为一种解决该问题的合理方法:
class Transaction {// 所有交易的基类
public:
Transaction();
virtual void logTransaction() const = 0;//建立依赖于具体交易类型的登录项
...
};
Transaction::Transaction() //实现基类的构造函数
{
...
logTransaction(); //最后,登录该交易
}
class BuyTransaction: public Transaction {
// 派生类
public:
virtual void logTransaction() const; //怎样实现这种类型交易的登录?
...
};
class SellTransaction: public Transaction {
//派生类
public:
virtual void logTransaction() const; //怎样实现这种类型交易的登录?
...
};
现在,请分析执行下列代码调用时所发生的事情:
BuyTransaction b;
很明显,一个BuyTransaction类构造器被调用。但是,首先调用的是Transaction类的构造器-派生类对象的基类部分是在派生类部分之前被构造的。Transaction构造器的最后一行调用了虚函数logTransaction,但是奇怪的事情正是在此发生的。被调用函数logTransaction的版本是Transaction中的那个,而不是BuyTransaction中的那个-即使现在产生的对象的类型是BuyTransaction,情况也是如此。在基类的构造过程中,虚函数调用从不会被传递到派生类中。代之的是,派生类对象表现出来的行为好象其本身就是基类型。不规范地说,在基类的构造过程中,虚函数并没有被"构造"。
对上面这种看上去有点违背直觉的行为可以用一个理由来解释-因为基类构造器是在派生类之前执行的,所以在基类构造器运行的时候派生类的数据成员还没有被初始化。如果在基类的构造过程中对虚函数的调用传递到了派生类,派生类对象当然可以参照引用局部的数据成员,但是这些数据成员其时尚未被初始化。这将会导致无休止的未定义行为和彻夜的代码调试。沿类层次往下调用尚未初始化的对象的某些部分本来就是危险的,所以C++干脆不让你这样做。
十四、 关于C++构造函数的问题:
class CLS
{
public:
int m_i;
CLS( int i ) {
cout<<"construct with argue\n";
m_i=i;
}
CLS()
{
cout<<"construct\n";
CLS(0);
}
};
int main(intargc, char* argv[])
{
CLS obj;
cout << obj.m_i << endl;
return 0;
}
输出:
construct
constructwith argue
-85893460(随机的,没有初始化的m_i值)
在C++中,一个构造函数不能调用另外一个构造函数来给成员变量赋值,但这里为什么"construct with argue"这条打印语句被执行了呢,按我的理解,CLS(int)还是被CLS()调用了,但m_i的值没有被改变,但我弄不懂打印语句被执行,赋值语句为什么没被执行呢?.
答:如果你在一个构造函数中调用了另一个构造函数,编译器将初始化一个临时局部对象,而不是初始化this对象。你可以通过一个默认参数或在一个私有成员函数 init() 中共享它们的公共代码来使两个构造函数结合起来。
十五、 拷贝构造函数???
#include<iostream.h>
#include<string.h>
class student
{
public:
student(char*pname="no name",int ssid=0)
{
strcpy(name,pname);
cout<<"constructingnew student"<<pname<<endl;
}
student(student&s)//STUDENT
{
cout<<"constuctingcopy of"<<s.name<<endl;
strcpy(name,"copyof");
strcat(name,s.name);
id=s.id;
}
~student()
{
cout<<"destructing"<<name<<endl;
}
protected:
charname[40];
int id;
};
void fn(student s)//为什么调用这里的时候不执行{}里面的却要执行STUDENT处
{
cout<<"infunction fn()"<<endl;
}
void main()
{
studentrandy("randy",1234);
cout<<"callingfn()\n";
fn(randy);
cout<<"returnedfrom fn()\n";
}
答:这就是按值传递,他要另外构造一个东西出来好不改变原来的那个对象的值。而要是改成void fn(const student& s)引用传递的话,就省了构造了也就不会出现你说的问题了。
十六、 有关运算符重载和拷贝构造函数的问题:
class Foo
{
public:
Foo(int x=0)
{
Foo::x=x;
cout<<"constructor"<<endl;
}
Foo(constFoo& ob)
{
x=ob.x;
cout<<"copyconstructor"<<endl;
}
pt()
{
cout<<x;
}
const Foooperator=(const Foo ob)
{
x=ob.x;
cout<<"operator="<<endl;
return*this;
}
private:
int x;
};
int main()
{
Foo x(1),y;
y=x;
y.pt();
}
输出结果:
constructor
constructor
copy constructor
operator =
copy constructor
1
我想不通的是为什么会有第二次拷贝构造函数的调用,是不是产生了一个无名对象并用*this对其初始化?
答:1) 传值返回啊,会先copy 个副本的:
const Foo::Foo operator = (const Foo ob)
{
x = ob.x;
cout<<"operator="<<endl;
return *this;//保存进一个临时对象,会调用一次拷贝构造函数!
}
2) 赋值操作符返回引用就不会调用第二次拷贝构造了:
const Foo::Foo &operator=(const Foo ob)
{
x=ob.x;
cout<<"operator="<<endl;
return *this;
}
十七、 关于拷贝构造函数的原型:
class A
{
public:
.......
A(A &other){ ....... }
.......
};
为什么构造函数的参数是引用型A &other。如果把构造函数的 &去掉 会有什么结果?
答:拷贝构造函数的参数不用引用,那么必将导致无限递归。很多编译器会报错。
十八、 关于构造函数实现类型转换
#include<iostream.h>
class B
{
int i;
public:
B(){cout<<"调用构造函数B()!\n";}
};
class A
{
int i;
public:
A()
{
cout<<"调用构造函数A()!\n";
}
A(inta)
{
i=a;
cout<<"i="<<i<<'\t'<<"调用构造函数A(int)!\n";
}
A(By,int a=10)
{
i=a;
cout<<"i="<<i<<'\t'<<"调用构造函数A(B)!\n";
}
};
void main()
{
A a1(10);
A a2=20;//不懂
a2=50;//不懂
B b;
A a3=b;//不懂
a3=b;//不懂
}
答:A a2=20; // 调用构造函数A(inta),这里的=不是赋值而是初始化
a2=50; // 右边的50被转化成一个A对象,调用了A(int); 相当于:
// A temp(50);
// a2 = temp;
B b;
A a3=b; // 调用构造函数A(B, 10); 因为int有默认值
a3 = b; // 右边用构造函数调用了A(B, 10)生成了一个临时对象
十九、 类的默认的位拷贝构造函数为什么会调用其成员A的拷贝构造函数?
#include <iostream>
using namespace std;
class A
{
public:
A(){cout<<"Aconstructor"<<endl;}
A(constA& a) {cout<<"A copy constructor"<<endl;}
};
class B
{
public:
A aa;
B(){cout<<"Bconstructor"<<endl;}
//B(constB& a) {cout<<"B copy constructor"<<endl;} (*)
};
int main( void )
{
B b;
Bb2=b;
getchar();
return1;
}
/*输出:不屏蔽(*)行
A constructor
B constructor
A constructor
B copy constructor
屏蔽(*)行
A constructor
B constructor
A copy constructor
*/
屏蔽(*)行时就使用了默认的拷贝构造函数,即位拷贝。既然是逐位拷贝,那么B默认的位拷贝构造函数为什么会调用B成员A的拷贝构造函数,而不仅仅是位拷贝?
答:按C++规定,如果没有在构造函数中显式指定成员初始化列表,则隐含地指定了调用成员子对象和基类子对象默认构造函数的成员初始化列表。因此,当不屏蔽(*)行时,你定义的类B的拷贝构造函数:
B(constB& a) {cout<<"B copy constructor"<<endl;}
等价于:
B(constB& a):aa() {cout<<"B copyconstructor"<<endl;}
这样在构造对象b2时,其子对象的默认构造函数被调用,然后才是b2的拷贝初始化构造函数体被调用。
另一方面,在屏蔽(*)行时也就是没有为类B定义拷贝构造函数时,编译器会为它生成一个拷贝构造函数,这个函数完成类B成员的逐成员拷贝,其定义为:
B(constB& a):aa(a.aa) {}
这样,在生成对象b2的时候,首先会调用A的拷贝构造函数来生成成员子对象,于是出现了相应的打印。
二十、 是不是产生任何对象都必须调用构造函数?
例:
class AA
{
int i;
int j;
};
答:<<insidethe c++ object model>>:
default constructors...在需要的时候被编译器产生出来”,需要有两种:1。编译器需要 2。程序需要,程序员的责任。
有四种情况,会导致"变压器必须为未声明construtor之classes合成一个defaultconstructor",四种情况是:
1."带有Default Constructor"的Merber ClassObject
2."带有Default Constructor"的Base Class
3."带有Virtual Function"的Class
4."带有Virtual Base Class"的Class
C++ Stardand 把那些合成无称为implicit nontrivial default construtors。被合成出来的constructor只是满足编译器(非程序)的需要。
至于不存在那四种情况而又没有声明如何constructor的classes, 我们说它们拥有的是implicit trivial dafault construcors,它们实际上并不会被合成出来.
C++新手一般有两个常见的误解:
1。任何class如果没有定义default constructor,就会被合成出一个来。
2。编译器合成出来的defaultconstructor会明确设定"class内每一个data member默认值"
如你所见,没有一个是真的。