您的位置:首页 > 编程语言 > C语言/C++

1 让自己习惯C++

2017-11-24 12:33 113 查看

条款 01:视C++为一个语言联邦

  今天的C++已经是一个多重范型编程语言,一个同时支持过程形式(procedural)、面向对象形式(object-oriented)、函数形式(functional)、泛型形式(generic)、元编程形式(metaprogramming)的语言。

  为了理解C++,你必须人认识其主要的次语言,幸运的是总共只有四个:

  C:说到底C++仍是以C为基础。区块(blocks)、语句(statements)、预处理器(preprocessor)、内置数据类型(built-in data types)、数组(arrays)、指针(pointers)等统统来自C。但当以C++内的C成分工作时,高效编程守则映照出C语言的局限:没有模板(templates)、没有异常(exceptions),没有重载(overloading)……

  Object-Oriented C++:这部分就是C with Classes所述求的:classes(包括构造函数和析构函数),封装(encapsulation)、继承(inheritance)、多态(polymorphism)、virtual函数(动态绑定)……等等。这一部分是面向对象设计值古典守则在C++上的最直接实施。

  Template C++:这是C++的泛型编程部分(generic programming),也是大多数程序员经验最少的部分。Template相关考虑与设计已经弥漫整个C++,良好编程守则中“惟template适用”的特殊条款并不罕见。实际上由于template威力强大,它们带来崭新的编程泛型(programming paradigm),也就是所谓的 template meraprogramming(TMP,模板元编程)。

  STL:是个template程序库,但它是非常特殊的一个。它对容器(containers)、迭代器(iterators)、算法(algorithms)以及函数对象(function objects)的规约有极佳的紧密配合与协调,然而templates及程序库也可以其他想法建置出来。STL有自己特殊的办事方式,当你伙同STL一起工作,你必须遵守它的规约。

  记住这四个次语言,当你从某个次语言切换到另一个,导致高效编程守则要去你改变策略时,不要感到惊讶。

  请记住

  C++高效编程守则视状况而变化,取决于你使用C++那一部分。

条款 02:尽量以const、enum、inline替换 #define

  这个条款或许改为“宁可以编译器替换预处理器”比较好,因为或许 #define 不被视为语言的一部分。那正是它的问题所在。

  可以用常量代替宏定义:#define ASPECT_RATIO 1.653可以用const double AspectRatio = 1.653 代替。当以常量替换#define,有两种特殊情况值得说说。第一是定义常量指针(constant pointers)。由于常量定义式通常被放在头文件内(以便不同的源码含入),因此有必要将指针(而不只是指针所指之物)声明为const。

  例如若要在头文件内定义一个常量的char*based字符串,必须写成const两次:const char * const authorName = “Scott Meyers”;但是值得提醒的是,string对象通常比其前辈char*-based合宜,所以上述的authorName往往定义成这样更好些:

  const std::string authorName(“Scott Meyers”);

  第二个值得注意的是class专属常量。为了将常量的作用于(scope)限制于class内,你必须让它成为class的一个成员(member);而为确保此常量之多只有一份实体,必须让他成为一个static成员:

class GamePlayer{
private:
static const int NumTurns = 5;//常量声明式
int scores[NumTurns];//使用该常量
}


  你说看到的是NumTurns的声明式而非定义式。只要不取它们的地址,你可以声明并使用它们而无须提供定义式。但如果你取某个class专属常量的地址,你就必须另外提供定义式如下:

  const int GamePlayer::NumTurns;//NumTurns的定义

  请把这个式子放进一个实现文件而非头文件。由于class常量已在声明时获得初值,因此定义时不可以再设初值。

  另外,无法利用#define创建一个class专属常量,因为#define并不重视作用域,一旦宏被定义,它就在其后的编译过程中有效(除非在某处被#undef)。

  此处所谓的“in-class”初值设定也只允许对整数常量进行。如果你的编译器不支持上述语法,可以将初值放在定义式。唯一例外是当你在class编译期间需要一个class常量值,例如在数组声明中式中。这时候万一你的编译器不支持“in class初值设定”,可改用所谓的“the enum hack”补偿做法。其理论基础是“一个属于枚举类型的数值可权充ints 被使用”。于是GamePlayer可定义如下:

class GamePlayer{
private:
enum{NumTurns = 5};
int scores[NumTurns];
};


  基于数个理由enum hack值得我们认识。第一,enum hack的行为某方面说比较像#define而不像const,有时候这正是你想要的。例如取一个const的地址是合法的,但取一个enum的地址就不合法,而取一个#define的地址通常也不合法。如果你不想让别人获得一个pointer后reference指向你的某个整数常量,enum可以帮助你实现这个约束。Enums和#define一样绝不会导致非必要的内存分配。

  认识enum hack的第二个理由纯粹是为了实用主义。事实上“enum hack”是template metaprogramming(模板元编程)的基础技术。

  另一个常见的#define误用情况是以它实现宏(macros)。宏看起来像函数,但不会招致函数调用带来的额外开销。

#define CALL_WITH_MAX(a,b) f((a) > (b) ? (a) : (b))
int a = 5, b = 0;
CALL_WITH_MAX(++a, b);//a被累加二次
CALL_WITH_MAX(++a, b+10); //a被累加一次


在这里,调用f之前,a的递增次数竟然取决于“它被拿来和谁比较”。

  幸运的是,你可以获得宏带来的效率以及一般函数的所有可预料行为和类型安全性(type safety)——只要你写出template inline函数。

template<typename T>
inline void callWithMax(const T& a, const T& b)
{
f(a > b ? a : b);
}


  这个template产出一整群函数,每个函数都接受两个同型对象,并以其中较大者调用f。这里不需要在函数体重为参数加上括号,也不需要关心参数被核算多次等等。此外callWithMax是个真正的函数,它遵守作用于和访问规则。例如你绝对可以写出一个“class 内的private inline函数”。一般而言,宏无法完成此事。

  有了const、enum和inline,对预处理的需求降低了,但并非完全消除。#include仍然是必需品,而#ifdef/#ifndef也继续扮演控制编译的重要角色。

  请记住

  对于单纯常量,最好以const对象或enum替换#define。

  对于形似函数的宏,最好改用inline函数替换#define。

条款 03:尽可能用const

  关键字const多才多艺。你可以用它在class外部修饰global或namespace作用域中的常量,或修饰文件、函数、或区块作用域中被声明为static的对象。你也可以使用它修饰class内部的static和non-stati成员变量。面对指针,你也可以指出指针本身、指针所指物,或两者是const;

char greeting[] = "Hello";
char *p = greeting;     //non-const pointer, non-const data
const char* p = greeting; //non-const pointer, const data
char* const p = greeting;//const pointer, non-const data
const char* const p = greeting;//const pointer, const data


  如果被指物是常量,有些程序员会将关键字const写在类型之前,有些人会把它写在类型之后、星号之前。两种写法的意义相同。

void f1(const Widget* pw);
void f2(Widget const* pw);


  STL迭代器系以指针为根据塑模出来,所以迭代器的作用就像个T*指针。声明迭代器为const就像声明指针为const一样(即声明一个T const指针),表示这个迭代器不得指向不同的东西,但它所指的东西的值是可以改动的。如果你希望迭代器所指的东西不可被改动(即希望STL模拟一个cosnt T 指针),你需要的是const_iterator:

std::vector<int> vec;
const std::vector<int>::iterator iter= vec.begin();//iter的作用像个T * const
std::vector<int>::const_iterator cIter = vec.begin();//cIter的作用像个const T*


  const最具威力的用法是面对函数声明时的应用。在一个函数声明式内,const可以和函数返回值、各参数、函数自身(如果是成员函数)产生关联。

  令函数返回一个常量值,往往可以降低因客户错误而造成的意外,而又不至于放弃安全性和高效性。

  至于const参数,就像local const对象一样,应该在必要使用它们的时候使用它们。除非你有需要改动参数或local对象,否则请将它们生米用为const。

const成员函数

  是为了确认该成员函数可作用于const对象身上。这一类成员函数之所以重要,基于两个理由。第一,它们使class接口比较容易理解。这是因为,得知哪个函数可以改动对象内容而哪个函数不行,很是重要。第二,它们使“操作const对象”成为可能。这对编写高效代码是个关键,因为如条款20所言,改善C++程序效率的一个根本办法是以pass by reference-to-const方式传递对象,而此技术的前提是,有const成员函数可用来处理取得的const对象。

  两个成员函数如果只是常量性不同,可以被重载。这实在是一个重要的cosnt特性。

class TextBlock{
public:
const char& operator[](std::size_t position)const
{return text[position];}
char& operator[](std::size_t position)
{return text[position];}
private:
std::string text;
};


TextBlock的operator[]可被这么使用

TextBlock tb("Hello");
std::cout << tb[0];//调用non-const
const TextBlock ctb("World");
std::cout << ctb[0];//调用const


  附带一提,真实程序中const对象大多用于passed by pointer-to-const或passed by reference-to-const的传递结果。

void print(const TextBlock* ctb)
{
std::cout << ctb[0];//调用const TextBlock::operator[]
}


  成员函数如果是const意味什么?这有两个流行概念:bitwise constness(又称physical constness)和logical constness。

  bitwise const阵营的人相信,成员函数只有在不更改对象之任何成员变量(static除外)时才可以说是const。也就是它不更改对象内的任何一个bit。这种论点的好处就是很容易侦测违反点:编译器只需寻找成员变量的赋值动作即可。bitwise constness正是C++对常量性的定义,因此,const成员函数不可以更改对象内任何non-static成员变量。

  不幸的是许多成员函数虽然不十足具备const性质却能通过bitwise测试。更具体地说,一个更改了“指针所指物”的成员函数虽然不能算是const,但如果只有指针(而非其所指物)隶属于对象,那么称此函数为bitwise const不会引发编译器异议。这导致反直观结果。假设我们有一个TextBlock-like class,它将数据存储为char*而不是string,因为它需要和一个不认识string对象的C API 沟通:

class CTexBlock{
public:
char& operator[](std::size_t position)const//bitwise const 声明 但其实不当
{return pText[position];}
private:
char* pText;


这个class不适当地将其operator[]声明为const成员函数,而改函数却返回一个reference指向对象内部值。假设不管这个事实,请注意,operator[]实现代码并不更改pText。于是编译器很开心地为operator[]产出目标码。它是bitwise const,所有编译器都这么认定。但是看看:

const CTextBlock cctb("Hello");//声明一个常量对象
char *pc = &cctb[0];//调用const operator[]取得一个指针指向cctb的数据
*pc = 'J';//cctb现在有了‘Jello’这样的内容


  这种情况导出所谓的logical constness。这一派拥护者主张,一个const成员函数可以修改它所处理的对象内的某些bits,但只有在客户端侦测不出的情况下才得如此。例如你的CTextBlock class有可能高速缓存(cache)文本区块的长度以便应付询问:

class  CTextBlock{
public:
std::size_t length() const;
private:
char* pText;
std::size_t textLength;//最近一次计算的文本区块长度
bool lengthIsValid;//目前的长度是否有效
};
std::size_t CTextBlock::length() const
{
if (!lengthIsValid){
textLength = std::strlen(pText);//错误!在const成员函数内不能赋值给textLength和LengthIsValid
lengthIsValid = true;
}
return textLength;
}


  length的实现当然不是bitwise const,因为textLength和lengthIsValid都可能被修改。这两笔数据被修改对const CTextBlock对象而言虽然可接受,但编译器不同意。它们坚持bitwise constness。怎么办?

  解决办法很简单:利用C++的一个与 const相关的摆动场:mutable(可变的)。mutable释放掉non-static成员变量的bitwise constness约束:

class  CTextBlock{
public:
std::size_t length() const;
private:
char* pText;
mutable std::size_t textLength;//这些成员变量可能总是会被更改,即使在const成员函数内
mutable bool lengthIsValid;//目前的长度是否有效
};
std::size_t CTextBlock::length() const
{
if (!lengthIsValid){
textLength = std::strlen(pText);//现在可以这样
lengthIsValid = true;//也可以这样
}
return textLength;
}


在const和non-const成员函数中避免重复

  对于“bitwise-constness 非我所欲的问题,mutable是个解决的办法,但它不能解决所有的const相关难题。举个例子,假设TextBlock(和CTextBlock)内的operator[]不单只是返回一个reference指向某字符,也执行边界检验(bounds checking)、志记访问信息(logged access info)、甚至可能进行数据完善性检验。把所有这些同时放进const和non-const operator[]中,导致这样的怪物(暂且不管那将会成为一个”长度颇为可议“的隐喻式inline函数——见条款30)

class TextBlock{
public:
const char& operator[](std::size_t position) const
{
……//边界检验
……//志记数据访问
……//检验数据完整性
return text[position];
}
char& operator[](std::size_t position)
{
……//边界检验
……//志记数据访问
……//检验数据完整性
return text[position];
}
private:
std::string text;
};


  你能说出其中发生的代码重复以及伴随的编译时间、维护、代码膨胀等令人头痛的问题吗?当然啦,将边界检验……等所有代码移到另一个成员函数(往往是个private)并令两个版本的operator[]调用它,是可能的,但你还是重复了一些代码,例如函数调用、两次return语句等等。

  你真正该做的是实现operator[]的机能一次并使用它两次。也就是说,你必须令其中一个调用另一个。这促使我们将常量性转除。

  就一般守则而言,转型是一个糟糕的想法,我将贡献一整个条框来谈这码事(条款27),告诉你不要那么做。然而代码重复也不是什么令人愉快的经验。本例中const operator[]完全做掉了non-const版本该做的一切,唯一的不同是其返回类型多了一个const资格修饰。这种情况下如果将返回值const转除是安全的,因为不论谁调用non-const operator[]都一定首先有个non-const operator对象,否则不能够调用non-const函数。所以令non-const operator[]调用其const兄弟是一个避免代码重复的安全做法——即使过程中需要一个转型动作。下面是代码,稍后有更详细的解释:

class TextBlock{
public:
const char& operator[](std::size_t position) const//一如既往
{
……
return text[position];
}
char& operator[](std::size_t position)//现在只调用const op[]
{
return
const_cast<char&>(//将op[]返回值的const转除
static_cast<const TextBlock&>(*this)//为*this加上const
[position]//调用const op[]
);
}
};


  如你所见,这份代码有两个转型动作,而不是一个。我们打算让non-const operator[]调用其const兄弟,但是non-const operator[]内部若只是单纯调用operator[],会递归掉用户自己。为了避免无穷递归,我们必须明确指出调用的时const operator[],但C++缺乏直接的语法可以那么做。因此这里将*this从原始类型TextBlock&转型为constTextBook&。是的,我们使用转型操作为它加上const!所以这里共有两次转型:第一次用来为 *this添加const(这次接下来调用operator[]时得以调用const版本),第二次则是从const operator[]的返回值中移除const。

  添加const的那一次转型强迫进行了一次安全转型(将non-const对象转为const对象),所以我们使用static_cast。移除const的那个动作只可以藉由const_cast完成,没有其他选择(就技术而言其实使有的:一个C-style转型也行得通,但一如在条款27 所说,那种转型很少时正确的抉择。如果你不熟悉static_cast或const_cast,条款27提供了一份概要)。

  至于其他动作,由于本例调用的时操作符,所以语法有一点点奇特,恐怕无法赢得选美大赛,但却有我们渴望的“避免代码重复”效果,因为它运用const operator[]实现出non-const版本。为了达到那个目标而写出如此难看的语法是否值得,只有你能决定,但“运用const成员函数实现出其non-const孪生兄弟”的技术是值得了解的。

  更值得了解的是,反向做法——令const版本调用non-const版本以避免重复——并不是你该做的事。记住,const成员函数承诺绝不改变其对象的逻辑状态,non-const成员函数却没有这般承诺。如果在const函数内调用non-const函数,就是冒了这样的风险:你曾经承诺不改动的那个对象被改动了。这就是为什么“const 成员函数调用non-const成员函数”是一种错误行为:因为对象有可能因此被改动。实际上若要令这样的代码通过编译,你必须使用一个const_cast将*this身上的const性质解放掉。反向调用(也就是我们先前使用的那个)才是安全的:non-const成员函数本来就可以对其对象做任何动作,所以在其中调用一个const成员函数并不会带来风险。这就是为什么本例以static_cast作用于*this的原因:这里并不存在const相关风险。

  本条款一开始就提醒你:const是个奇妙且非比寻常的东西。在指针和迭代器身上:在指针、迭代器及reference指涉的对象身上;在函数参数和返回类型身上;在local变量身上;在成员函数身上,林林总总总不一而足。const是个威力强大的助手。尽可能使用它。

  请记住:

  1.将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。

  2.编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”

  3.当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。

  

条款04:确定对象被使用前已被先初始化

  读取未初始化的值会导致不明确的行为。现在,我们终于有了一些规则,描述“对象的初始化动作何时一定发生,何时不一定发生”。不幸的是这些规则很复杂,我认为对记忆力而言是太繁复了些。

  通常如果你使用C part of C++而且初始化可能导致运行期成本,那么就不保证发生初始化。一旦进入non-C part of C++,规则有些变化。这就很好的地解释了为什么array(来自C part of C++)不保证其内容被初始化,而vector(来自STL part of C++)却有此保证。

  表面上这似乎是个无法决定的状态,而最佳处理办法就是:永远在使用对象之前先将它初始化。对于无任何成员的内置类型,你必须手工完成此事。

  对于内置类型以外的任何其他东西,初始化责任落在构造函数身上。规则很简单:确保每一个构造函数都将对象的每一个成员初始化。

  这个规则很容易奉行,重要的是别混淆了赋值(assignment)和初始化(initialization)。考虑一个用来表现通讯薄的class,其构造函如下:

class PhoneNumber{...};
class ABEntry{
public:
ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones);
private:
std::string theName;
std::string theAddress;
std::list<PhoneNumber> thePhones;
int numTimesConsulted;
};
ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)
{
theName = name;//这些都是赋值而非初始化
theAddress = address;
thePhones = phones;
numTimesConsulted = 0;
}


  这会导致ABEntry对象带有你期望的值,但不是最佳做法。C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。在ABEntry构造函数内,theName,theAddress和thePhones都不是被初始化,而是被赋值。初始化发生时间更早,发生于这些成员的default构造函数被自动调用之时(比进入ABENntry构造函数本体的时间更早)。但这对numTimesConsulted不为真,因为它属于内置类型,不保证一定在你所看到的那个赋值动作的时间点之前获得初值。

  ABEntry构造函数的一个较佳写法是,使用所谓的member initialization list(成员初值列)替换赋值动作:

ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)
:   thename(name),//现在这些都是初始化
theAddress(address),
thePhones(phones),
numTimesConsulted(0)
{}


  这个构造函数和上一个的最终结构相同,但通常效率较高。基于赋值的那个版本首先调用default构造函数为theName,theAddress和thePhones设初值,然后立刻再对它们赋予新值。default构造函数的一切作为因此浪费了。成员初值列的做法避免了这一问题,因为初值列中针对各个成员变量而设的实参,被拿去作为各成员变量之构造函数的实参。本例中的中thename以name为初值进行copy构造,theAddress以address为初值进行copy构造,thePhones以pones为初值进行copy构造。

  对大多数类型而言,比起先调用default构造函数然后在调用copy assignment操作符,单只调用一次copy构造函数时比较高效的,有时甚至高效的多。对于内置型对象如numTimesConsulted,其初始化和赋值的成本相同,但为了一致性最好也通过成员初值列来初始化。同样道理,甚至当你想要default构造一个成员变量,你都可以使用成员初值列,只要指定无物作为初始化实参即可。假设ABEntry有一个无参数构造函数,可将它实现如下:

ABEntry::ABEntry()
: theName(),//调用theName的default构造函数
theAddress(),//为theAddress做类似动作
thePhones(),//为thePhones做类似动作;
numTimesConsulted(0)//记得将numTimesConsulted显示初始化为0
{}


  由于编译器会为用户自定义类型之成员变量自动调用default构造函数——如果那些成员变量在“成员初值列”中没有被指定初值的话,因而引发某些程序员过度夸张地采用以上写法。那是可理解的,但请立下一个规则,规定总是在初值列中列出所有成员变量,以免还的记住哪些成员变量(如果它们在初值列中被遗漏的话)可以无需初值。举个例子,由于numTimesConsulted属于内置类型,如果成员初值列(member initialization list)遗漏了它,它就没有初值,因而可能开启“不明确行为”的潘多拉盒子。

  有些情况下即使面对的成员变量属于内置类型(那么其初始化与赋值的成本相同),也一定得使用处置列。是的,如果成员变量时const或reference,它们就一定需要初值,不能被初值。为避免需要记住成员变量何时必须在成员初值列中初始化,何时不需要,最简单的做法就是:总是使用成员初值列。这样做有时候绝对必要,且又往往比赋值更高效。

  许多class拥有多个构造函数,每个构造函数有自己的成员初值列。如果这种class存在许多成员变量和/或base class,多份成员初值列的存在就会导致不受欢迎的重复(在初值列内)和无聊的工作(对程序员而言)。这种情况下可以合理地在初值列中遗漏那些“赋值表现像初始化一样好”的成员变量,改用它们的赋值操作,并将那些赋值操作移往某个函数(通常是private),供所有构造函数调用。这种做法在“成员变量的初值系由文件或数据库读入“时特别有用。然而,比起经由赋值操作完成的”伪初始化“,通过成员初值列完成的”真正初始化“通常更加可取。

  C++有着十分固定的“成员初始化次序”,次序总是相同:base class更早于其derived class被初始化(见条款12),而class的成员变量总是以其声明次序被初始化。回头看看ABEntry,其theName成员变量永远最先被初始化,然后是theAddress,再来是thePhones,最后是numTimesConsulted。即使它们在成员初值列中以不同的次序出现(很不幸那是合法的),也不会有任何影响。为避免你或你的检阅者迷惑,并避免某些可能存在的晦涩错误,当你在成员初值列中条列各个成员时,最好总是以声明次序为次序。

  一旦你已经很小心地将“内置型成员变量”明确地加以初始化,而且也确保你的构造函数运用“成员初值列”初始化base class和成员变量,那就只剩唯一一件事需要操心,就是“不同编译单元内定义之non-local static对象的初始化次序。

  让我们一点一点地探钻这一长串词组。

  所谓static对象,其寿命从被构造出来直到程序结束为止,因此stack和heap-based对象都被排除。这种对象包括global对象、定义于namespace作用域内的对象、在class内、在函数内、以及在file作用域内被声明为static的对象。函数内的static对象称为local static对象(因为它们对函数而言是local),其他static对象称为non-local static 对象。程序结束时static对象会被自动销毁,也就是它们的析构函数会在main()结束时被自动调用。

  所谓编译单元是指产出单一目标文件的那些源码。基本上它是单一源码加上其含入的头文件。

  现在看,我们关心的问题涉及至少两个源码文件,每一个内含至少一个non-local static 对象(也就是说该对象是global或位于namespace作用域内,抑或在class内或file作用域内被声明为static)。真正的问题是:如果某编译单元内的某个non-local static对象,它所用到的这个对象可能尚未被初始化,因为C++对”定义于不同编译单元内的non-local static对象“的初始化次序并无明确定义。

  实例可以帮助理解。假设你有一个FileSystem class,它让互联网上的文件看起来好像位于本机。由于这个class使世界看起来像单一文件系统,你可能会产出一个特殊对象,位于global或namespace作用域内,象征单一文件系统:

class FileSystem{//来自你的程序库
public:
std::size_t numDisks() const;//众多成员函数之一
};
extern FileSystem tfs;//预备给客户使用的对象;tfs代表“the file system”


  FileSystem对象绝不是一个稀松平常无关痛痒的对象,因此你的客户如果在theFileSystem对象构造完成前就使用它,会得到惨重的灾情。

  现在假设某些客户建立了一个class用以处理文件系统内的目录。很自然他们的class会用上theFileSystem对象:

class Directory{//由程序库用户建立
public:
Directory(params);
};
Directory::Directory(params0
{
std::size_t disks = tfs.numDisks();//使用tfs对象
}


  进一步假设,这些客户决定创建一个Directory对象,用来放置临时文件:Directory tempDir(params);//为临时文件而做出的目录

  现在,初始化次序的重要性显现出来了:除非tfs在tempDir之前被初始化,否则tempDir的构造函数会用到尚未初始化的tfs。但tfs和tempDir是不同的人在不同的时间于不同的源码文件建立起来的,它们是定义于不同编译单元的non-local static对象。如果能够确定tfs会在tempDir之前先被初始化

  你无法确定。再说一次,C++对”定义于不同的编译单元内的non-local static对象“的初始化相对次序并无明确定义。这是由原因的:决定它们的初始化次序相当困难,根本无解。在其最常见形式,也就是多个编译单元内的non-local static对象经由“模板隐式具现化”形成),不但不可能决定正确的初始化次序,甚至往往不值得寻找“可决定正确次序”的特殊情况。

  幸运的是一个小小的设计便可完全消除这个问题。唯一需要做的是:将每个non-local static对象搬到自己专属函数内(该对象在此函数内被声明为static)。这些函数返回一个reference指向它所含的对象。然后用户调用这些函数,而不直接指涉这些对象。换句话说,non-local static对象被localstatic对象替换了。DesignPatterns,这是singleton模式的一个常见实现手法。

  这个手法的基础在于:C++保证,函数内的local static对象会在“该函数被调用期间”“首次遇上该对象之定义式”时被初始化。所以如果你以“函数调用”(返回一个reference指向local static对象)替换“直接访问non-local static对象”,你就获得了保证,保证你所获得的那个reference将指向一个历经初始化的对象。更棒的是,如果你从未调用non-local static对象的”仿真函数“,就绝不会引发构造和析构成本:真正的non-local static对象可没者等便宜!

  以此技术施行于tfs和tempDir身上,结果如下:

class FileSystem{};//同前
FileSystem& tfs()//这个函数用来替换tfs对象;它在FileSystem class中可能是个static
{
static FileSystem fs;//定义并初始化一个local static对象
return fs;//返回一个reference指向上述对象
}
class Directory{};//同前
Directory::Directory(params)//同前,但原本的reference to tfs
{                                          //现在改为ths()
std::size_t disks = tfs().numDisks();
}
Directory& tempDir()//这个函数用来替换tempDir对象
{                  //它在Directory class中可能是个static
static Directory td;//定义并初始化local static 对象
return td;//返回一个reference指向上述对象
}


  这么修改之后,这个系统程序的客户完全像以前一样地用它,唯一不同的是他们现在使用tfs()和tempDir()而不再是tfs和tempDir。也就是他们使用函数返回“指向static对象”的references,而不再使用static对象自身。

  这种结构下reference-returning函数往往十分单纯:第一行定义并初始化一个local static对象,第二行返回它。这样的单纯性使它们成为绝佳的inlining候选人,尤其如果它们被频繁调用的话(见条款30)。但是从另一个角度看,这些函数“内含static对象”的事实使它们在多线程系统中带有不确定性。再说一次,任何一种non-const static对象,不论它是local或non-local,在多线程环境下“等待某事发生”都会有麻烦。处理这个麻烦的一种做法是:在程序的单线程启动阶段手工调用所有reference-returning函数,这可消除与初始化有关的“竞速形势”。

  当然啦,运用reference-returning函数防止“初始化次序问题”,前提是其中有着一个对对象而言合理的初始化次序。如果你有一个系统,其中对象A必须在对象B之前先初始化,但A的初始化能否成功却又受制于B是否已初始化,这时候你就用麻烦了。坦白说你自锁自受。只要避开如此病态的境况,此处描述的办法应该可以提供你良好的服务,至少在单线程程序中。

  既然这样,为避免在对象初始化之前过早地使用它们,你需要做三件事:第一,手工初始化内置型non-member对象。第二,使用成员初值列对付对象的所有成分。最后,在“初始化次序不确定性”(这对不用编译单元所定义的non-local static对象是一种折磨)氛围下加强你的设计。

请记住

  为内置型对象进行手工初始化,因为C++不保证初始化它们

  构造函数最好使用成员初值列,而不要在构造函数本体内使用赋值操作。初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同。

  为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  c++