[10.1] 构造函数是干嘛的?
构造函数是用来在杂乱的内存中构建对象的。
构造函数就像是"初始化函数"。它们可以把一系列的无规则的位转化为一个活生生的对象。最低程度地,它们初始化了内部使用的空间。它们也会为对象分配资源(内存,信号量,套接口等)
"ctor"是构造函数的缩写
[10.2] List x与List x()有什么不同吗?
这两个太不同了!
假设List是类的名字,那么f()是申明了一个本地的叫x的List对象。
void f()
{
List x; //本地的一个叫x的List的对象
}
但是函数g()定义了一个叫x()的函数,它会返回一个List对象:
void g()
{
List x(); //一个返回List对象的名为x的函数
}
[10.3] 我怎么才能从另一个构造函数里面调用另一个构造函数来进行初始化呢?
这样做是不行的。
原因是:如果你调用了另一个构造函数,编译器会初始化一个临时的对象,而不初始化这个对象。你可以用默认参数来初始化两个构造函数,或者你可以共享在私有init()成员函数的公共的代码。
[10.4] 对Fred类的默认的构造函数总是Fred::Fred()吗?
不是的,默认的构造函数是一个可以在没有参数的情况下被调用的构造函数。这样没有参数的构造函数就一定是一个默认的构造函数:
class Fred {
public:
Fred(); //默认的构造函数:可以被在没有参数的情况下被调用。
....
};
但是有参数的默认构造函数是可能出现的,只要它们有默认的初始参数值:
class Fred {
public:
Fred(int i=3,int j=5); //默认构造函数:可以在没有参数的情况下被调用。
//...
};
[10.5] 当我构建一个Fred的对象数组的时候我会调用哪个构造函数呢?
它会调用Fred的默认构造函数(例外情况会在下面被提出来).
这种情况下没有办法调用另一个构造函数。如果类Fred没有默认构造函数的话,构建一个Fred对象数组会在编译期出现一个错误。
class Fred {
public:
Fred(int i,int j);
//...假设在Fred类里面没有默认构造函数
}
int main()
{
Fred a[10]; //错误:没有默认构造函数
Fred* p=new Fred[10]; //错误:没有默认构造函数
}
然而如果你构造一个std::vector的对象组时还不是定义一个数组(你本就应该这样做,因为数据通常对于你的程序来说就是个魔鬼)你就不用在Fred类里面有一个默认的构造函数了,因为你你可以std::vector一个Fred对象用来初始化实例:
#include
int main()
{
std::vector 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)初始化,你可以使用任何参数进行初始化,甚至参数或是其他的变量。关键在于这样的做法是可行的,但是没有std::vector合适。记住:数组是魔鬼,除非有实在没有办法的情况才使用数组,其他的情况下用std::vector代替
[10.6] 我的构造函数是应该用"初始化列表"呢还是"赋值"呢?
构造函数应该初始化在初始化列表中的所有的成员对象。
例如,构造函数用初始化列表:Fred::Fred() : x_(whatever) {}来初始化成员对象x_。从性能方面考虑的话,注意whatever表示式都不能自动地让一个独立的对象被构建出来并拷贝到x_中。如果与whatever的值类型是一样的,那么就可以被直接从x_的内部被构造出来。
与以下的用赋值的构造函数不同,Fred::Fred() 。这种情况下表达式whatever会造成一个分离的,临时的对象,这个临时的对象会被传递到x_对象的赋值运算符中,然后在;处对它进行析构。这样做是没有效率的。
有另外一个没有效率的来源:在赋值的情况中,对象的默认构造函数会分配一些类的内存或打开一些默认的文件。这样的工作在如果whatever表达式或赋值运算符造成对象关闭,以至文件被关闭,并释放内存的情况下,就成了完全无用的了。(例如,当默认构造函数没能分配足够大的内存池或打开了错误的文件的时候)
[10.7] 可以在构造函数中使用this指针吗?
一些人觉得不应该在构造函数中使用this指针,因为this对象还没有被完全初始化出来,但是如果你用的很仔细的话,可以使用在构造函数中使用this指针.
当你在构造函数的构造体内的时候,因为所有的基类的对象和成员对象已经被构造出来了,这样使用this指针就很容易理解的。然而甚至在这种情况下你也得非常小心,例如:如果你在这个对象上调用一个虚函数(或调用其他的会调用虚函数的函数时),你就可能得不到你想得到的东西。
但是当你非常小心不会触及任何没有被初始化的成员对象或基类子对象的时候,你是被允许在构造函数的初始化列表中使用this指针的。这就要求你要对构造函数中会发生的事情的顺序了解的很透彻。最保险的做法是把this指针中的值保存起来,在之后使用this指针
[10.8] 什么叫"命名的构造函数用语"(Named Constructor Idiom)?
这是一个对类的使用都提供了更多直觉的或是更保险的构造方法的技术
问题是你的构造函数总是与类的名字相同,这样区分一个类的不同的构造函数就只能通过参数列表了。但是如果有很多个构造函数,那么这些区别看起来太小了,很容易会引起错误。
通过命名的构造函数用语,你可以自己定义类里面的所有的构造函数:你可以写一个公共的静态的方法来返回一个对象。这些静态的方法就是所谓的"命名的构造函数"。总的来说,每一种构造对象的方法都有一种这样的静态方法。
例如,假设我们在构建一个表示在X-Y空间的Point类。有两种方法来指出一个二维空间的坐标:方坐标(X+Y),极坐标(半径+角度)。(不用担心如果偿不理解这些东西,关键不是坐标系统中的特例,关键是构造一个Point对象有多种方法).不幸的是对于这两个坐标繁育的参数是相同的:两个浮点.这就会造成构造函数加载的错误:
class Point {
public:
Point(float x, float y); // 坐标系
Point(float r, float a); // 极坐标
// 错误: 无法选择构造函数
};
int main()
{
Point p = Point(5.7, 1.2); // 错误:无法选择构造函数
}
一种解决这个问题的办法是用命名的构造函数用语:
#include // 得到sin()和cos()函数的使用权
class Point {
public:
static Point rectangular(float x, float y); // 坐标系
static Point polar(float radius, float angle); // 极坐标
//这些静态函数就是所谓的"命名的构造函数"
// ...
private:
Point(float x, float y); // 坐标
float x_, y_;
};
inline Point::Point(float x, float y)
: x_(x), y_(y) { }
inline Point Point::rectangular(float x, float y)
{ return Point(x, y); }
inline Point Point::polar(float radius, float angle)
{ return Point(radius*cos(angle), radius*sin(angle)); }
这样Point类的使用都就不会在构造一个坐标系的时候被迷惑:
int main()
{
Point p1 = Point::rectangular(5.7, 1.2); // 用二维坐标系初始化
Point p2 = Point::polar(5.7, 1.2); // 用极坐标系进行初始化
}
如果你希望Point有继承类的话,请确定构造函数是在protected区中。
命名的构造函数可被用来确保你的对象可以用new来创建.
[10.9] 为什么我不能在我的构造函数的初始化列表中初始化静态的成员数据?
因不你必须在外部定义静态的数据成员.
Fred.h:
class Fred {
public:
Fred();
// ...
private:
int i_;
static int j_;
};
Fred.cpp(或Fred.c什么的)
Fred::Fred()
: i_(10) // OK,你可这样初始化成员数据
j_(42) //错误,你不能这样初始化静态数据
{
// ...
}
// 你必须这样初始化静态数据
int Fred::j_ = 42;
[10.10] 为什么有静态数据成员的类会有联接器错误?
因为静态的数据成员会仅在一个编译单元被定义。如果你不这样做,你可能会得到一个"undefined external"连接错误。例如:
// Fred.h
class Fred {
public:
// ...
private:
static int j_; // 定义了静态数据成员Fred::j_
// ...
};
连接器会告诉你("Fred::j_ is not defined")除非你在一个源文件里定义了Fred::j_
// Fred.cpp
#include "Fred.h"
int Fred::j_ = some_expression_evaluating_to_an_int;
//如果你希望对静态整型用外部的0值
// int Fred::j_;
通常的定义类Fred的静态数据成员的地方是在Fred.cpp或是Fred.c这样的文件里面.
[10.11] 什么是"静态初始化顺序失败"?
一种会毁掉你项目的极细微的方法
静态的初始化顺序失败是一个非常细微的也是一个非常普遍的误解。不幸的是,它是非常难以发现的,它是在main()开始之前就发生的错误
简言之,建议你有两个静态的对象x和y,它们在两个源文件里面他离开,假设为x.cpp和y.cpp。假定构造y对象的构造函数调用了x对象的一些方法
就是这样的简单的一个问题。
这种悲剧让你的项目有一半的机率失败。如果x.cpp编译单元刚巧先被初始化了,一切都是正常的。但是如果y.cpp编译单元先被初始化了,那么y的构造函数会在x的构造函数之前运行,那么就出问题了。比如说y的构造函数会调用一个x对象的方法,而x对象还没有被构造呢。
我听说麦当劳正在雇新员工呢。享受你这份递汉堡的新工作吧,呵呵
注意:静态初始化顺序失败不会发生在像int或char*这样的原有类型上。例如如果你构造了一个静态的浮点对象的话,就永远不会有静态初始化顺序失败问题。静态初始化顺序失败只有在你的静态的或全局的对象有构造函数的情况下有这样的问题。
[10.12] 如何防止静态初始化顺序失败呢?
使用“在首次使用的时候构造”,这样会意味着把你的静态对象包裹到函数之内。
例如,假设你有两个类,Fred和Barney。有一个全局的Fred对象称为x,和一个全局的Barney对象称为y。Barney的构造函数调用x对象的goBowling()函数。文件x.cpp定义了x对象:
// x.cpp
#include "Fred.hpp"
Fred x;
文件y.cpp定义了y对象:
// y.cpp
#include "Barney.hpp"
Barney y;
为了完整性,Barney构造函数就如下:
// Barney.cpp
#include "Barney.hpp"
Barney::Barney()
{
// ...
x.goBowling();
// ...
}
就像上述的那样,如果y在x之前被构造,那么灾难就会发生了,因为它们在不同的文件中,这种情况发生的机率有50%。
对于这个问题有多个解决方法,但是一个简单而完备的解决方法就用一个全局的函数x()来代替全局的Fred对象x,它会返回一个Fred对象的引用。
// x.cpp
#include "Fred.hpp"
Fred& x()
{
static Fred* ans = new Fred();
return *ans;
}
因为静态对象在且仅在控制流第一次经过它们定义的时候被构造,上面的new Fred()一句就只会调用一次。每一次后续的调用都只会返回同样的Fred对象。这样你需要做的只是把x替换为x()就可以了。
// Barney.cpp
#include "Barney.hpp"
Barney::Barney()
{
// ...
x().goBowling();
// ...
}
这个被叫做在第一次使用时构造原则,因为它做到的就是让全局Fred对象只在第一次调用的时候被构造。
这个方法的接下来的问题就是Fred对象永远不会被析构掉。以后的FAQ里面解决了这个问题,但是会引起“静态逆初始化顺序失败"问题。
注意:你不需要对像int或char*这样的内建类型做这样的操作。例如如果你创建了一个静态的或全局的浮点对象,就没有必要把它包裹为一个函数。仅会出问题的时机只有当你的静态的或全局的对象有一个构造函数
[10.13] 对于表态数据成员如何防止"静态初始化顺序失败"问题?
仅仅使用上述的技术就可以解决,但是这次是用一个静态的成员,而不是一个全局的函数。
假设有一个类X有一个静态Fred对象:
// X.hpp
class X {
public:
// ...
private:
static Fred x_;
};
自然地,这个静态的成员是被分别地初始化的。
// X.cpp
#include "X.hpp"
Fred X::x_;
自然Fred对象会被在X的方法中使用一次或多次:
void X::someMethod()
{
x_.goBowling();
}
但是现在的危险的情况是如果一个人某时某地在某些方面Fred对象初始化之前通过某种方法调用了这个方法。例如:如果另一个人构建了一个静态的X对象并在静态初始化的时候 调用了someMethod()方法,那么你就得看编译器能不能显灵了,看它是会在X::x_构造之前调用someMethod()还是在之后。(注意ANSI/ISO C++编译协会正在研究这个问题,但是编译器还没能完全都解决这个问题,以后需要关注这个问题。)
在任何情况下,把X::x_改为一个静态的成员函数总是轻便的而且是安全的.
// X.hpp
class X {
public:
// ...
private:
static Fred& x();
};
这样自然地这个静态的成员就会被分别地被初始化。
// X.cpp
#include "X.hpp"
Fred& X::x()
{
static Fred* ans = new Fred();
return *ans;
}
这样你只要把x_代替为x()就可以了:
void X::someMethod()
{
x().goBowling();
}
如果你性能的要求很高,而且你对在调用X::someMethod()多调用一个函数很在意的话,你可以建立一个Fred&型的静态变量。静态本地只初始化一次(控制流第一次经过定义),这样X::x()只会被调用一次:第一次X::someMethod()被调用的时候。
void X::someMethod()
{
static Fred& x = X::x();
x.goBowling();
}
注意:你不需要对像int或char*这样的内建类型做这样的操作。例如如果你创建了一个静态的或全局的浮点对象,就没有必要把它包裹为一个函数。仅会出问题的时机只有当你的静态的或全局的对象有一个构造函数
[10.14] 当构造函数出错的时候我怎么办?
抛出一个异常.见[17.2]