Effective C++ 55个具体做法 (Meyers) 1. 让自己习惯C++ 摘录

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

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

理解C++语言的最简单的方法是将C++视为一个由相关语言组成的联邦而非单一语言。在某个次语言中,各种守则与通例都倾向于简单、直观易懂、并且容易记住。C++最主要的次语言,总共只有四个:

        C语言。说到底C++仍然是以C为基础的。区块、语句、预处理器、内置数据类型、数组、指针等统统来自C。当你以C++内的C成分工作时,高效编程守则则映射出C语言的局限:没有模板、异常、重载......

        Object-Oriented C++。class、封装、继承、多态、虚函数......。

        Template C++。这是C++泛型编程部分。实际上由于template威力强大,他们带来崭新的编程泛型,也就是所谓的模板元编程。

        STL。STL是个template程序库,它是非常特殊的一个。它对容器(container)、迭代器(iterator)、算法(algorithms)以及函数对象(function objects)的规约有极佳的紧密配合和协调,然而template及程序库也可以其他想法建置出来。

记住这四个次语言,当你从某个次语言切换到另一个,导致高效编程守则则要求你改变策略时,不要感到惊讶。例如对于内置类型(C-like),pass by value通常比pass by reference高效。当移到Objected-Oriented C++,由于用户自定义构造函数和析构函数存在,pass-by-reference-to-const往往更好。运用Template C++时尤其如此。一旦跨入STL你就会知道,迭代器和函数对象都是在C指针之上塑造出来的,所以对于STL的迭代器和函数对象而言,旧式的C pass-by-value守则则再次适用。

请记住:

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

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

当你做出下列事情时:

#define ASPECT_RATIO 1.653

记号名称ASPECT_RATIO也许从未被编译器看见;也许在编译器开始处理源码之前它就被预处理器移走了。于是该记号名称有可能没进入记号表(symbol table)内。当你运用此常量获得错误信息时,可能会带来困惑,因为这个错误信息也许会提到1.653,而不是ASPECT_RATIO。

解决之道是以一个常量替换上述的宏;

const double AspectRatio = 1.653;

此外,对于浮点常量(floating point constant)而言,使用常量可能比使用#define导致较小量的代码。

当我们以常量替换#define,有两种特殊情况值得说明。第一是定义常量指针(constant pointers)。由于常量定义式通常放在头文件内,因此有必要将指针(而不是指针所指之物)声明为const。例如,若要在头文件内定义一个常量的(不变的)char* based字符串,你必须写const两次:

const char* const authorName = "Scott Meyer";

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

class GamePlayer
{
private:
    static const int NUMTurns = 5;
    int scores[NumTurns];
...
};
const int GamePlayer::NumTurns;

上述NumTurns请放入一个实现文件而非头文件。由于class常量已在声明时获得初值,因此定义时不可以在设置初值。

顺带一提,请注意,我们无法利用#define创建一个class专属常量,因为#defien并不重视作用域。一旦宏被定义,它就在其后的编译过程中有效(除非在某处被#undef)。这也就意味#define不仅不能够用来定义class专属常量,也不能够提供任何封装性。

旧式编译器不允许static成员在其声明式上获得初值。此外,in-class初值设定也只允许对整数常量进行。如果你的编译器不支持上述语法,你也可以将初值放在定义式:

// 头文件
class CostEstimate
{
private:
    static const double FudgeFactor;
...
};


// 实现文件
const double CostEstimate::FudgeFactor = 1.35;

万一你的编译器(错误地)不允许“”static整数型class常量“”完成“in-class初值设定”,可改用“the enum hack”补偿做法。其理论基础是:“一个属于枚举类型(enumerated type)”的数值可权当int型被使用。于是,GamePlayer定义如下;

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

enum hack的行为某方面说比较像#define而不像const。例如,取一个const的地址是合法的,但取一个enum的地址就不合法,而取一个#define的地址通常也不合法。

如果你不想让别人活得一个pointer或reference指向你的某个整型常量,enum可以帮助你实现这个约束。

认识enum hack的第二个理由纯粹是为了实用主义。许多代码用了它。

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

#define CLAA_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被累加一次 

幸运的是,你可以使用template inline函数来获得宏的效率已经一般函数的所有可预料行为和类型安全性(type safety)。

有了const、enums、inline,我们对预处理器(特别是#define)的需要降低了,但并非完全消除。#include任然是必需品,而#ifndef/define也继续扮演控制编译的重要角色。

请记住;

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

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

条款03:尽可能使用const

const允许你指定一个语义约束(也就是指定一个不该被改动的对象),而编译器会强制实施这项约束。

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

char greet[] = "hello";
char* p = greet;                  // non-const pointer, non-const data
const char* p = greet;            // non-const pointer, const data
char* const p = greet;            // const pointer, non-const data
const char* const p = greet;      // const pointer, const data

const关键字出现在星号左侧,则表示被指物是常量;如果出现在星号右侧,则表示指针自身为常量。如果出现在两侧都出现,则表示被指物和指针都是常量。

在STL中,声明迭代器为const就像声明指针为const一样(即声明一个T* const指针),表示这个迭代器不得指向不同的东西,但它所指的东西的值是可以改变的。

如果你希望迭代器指向的东西不可被改变(即希望STL模拟一个const T*指针),你需要的是const_iterator。

const std::vector::iterator iter = vec.begin();    // iter的作用像个T* const
*iter = 10;                                             // Ok
++iter;                                                 // 错误,iter是个const

std::vector::const_iterator cIter = vec.begin();   // iter的作用像个const T*
*cIter = 10;                                            // 错误,*cIter是const
++cIter;                                                // Ok

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

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

而一个“良好的用户自定义类型”的特征是它们避免无端地与内置类型不兼容。

const成员函数:

将const实施于成员函数的目的,是为了确认该成员函数可作用于const对象身上。基于以下两个原因:

        第一,它们使class接口比较容易被理解。这是因为得知那个函数可以改动对象内容而那个函数不行。

        第二,它们使“操作const”对象成为可能。

改善C++程序效率的一个根本办法是以pass by valreference-to-const 方式传递对象。

许多人漠视一个事实:两个成员函数如果只是constness不同,可以被重载。

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 tb("Hello");

std::cout << tb[0];             // 调用non-const TextBlock::operator[];

const TextBlock ctb("World");   
std::cout << ctb[0];            // 调用const TextBlock::operator[];

tb[0] = 0;  // OK 修改一个non-const TextBlock
ctb[0] = 0; // wrong 修改一个const TextBlock 此错误只因为operator[]的返回类型以致

如果函数的返回类型是个内置类型,那么改动函数返回值从来就不合法。纵使合法,C++以vy value返回对象这一事实意味着被改动的其实是tb.text[0]的一个副本,不是tb.text[0]自身,那不会是你想要的行为。

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

bitwise const阵营的人相信,成员函数只有在不更改对象之任何成员变量(static除外)时才可以说是const。

不幸的是许多成员函数虽然不十足具备const性质却能通过bitwise测试。更具体地说,一个更改了“指针所指物”地成员函数虽然不能算是const,但如果只有指针(而非其所指物)隶属于对象,那么称此函数为bitwise const不会引发编译器异议。

class CTextBlock
{
public:
    // ...
    char& operator[] (std::size_t position) const
    {  return text[position];  } 


private:
    char* ptext;
};

const CTextBlock cctb("Hello"); // 声明一个常量对象
char* pc = &cctb[0];            // 调用const operator[]取得一个指针, 指向cctb地数据

*pc = 'J';                      // cctb现在有了“Jello”这样地内容

这个class不适当地将其operator[]声明为const成员函数,而该函数返回一个reference指向对象内部值。上述代码导出了所谓地logical constness。这一派拥护者认为,一个const成员函数可以修改它所处理的对象内的某些bits,但只有在客户端侦测不出的情况下才得如此。

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)
    {
        textLengh = std::strlen(pText);    // wrong, 在const成员函数内,不能赋值给textLength
        lengthIsValid = true;              // wrong,在const成员函数内,不能赋值lengthIsValid
    }
    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;
    mutable bool lengthIsValid;
};

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

读取未初始化的值会导致不明确的行为。

更可能的情况是读入一些半随机的bits,污染了正在进行读取动作的那个对象,最终导致不可预知的程序行为,以及许多令人不愉快的调试。

int x = 0;                                // 对int进行手工初始化
const char* text = "A C-style string";    // 对指针进行手工初始化
double d;
std::cin >> d;

重要是别混淆了赋值(assignment)和初始化(initialization)。

class PhoneNumber {...};

clas ABEntry
{
public: 
    ABEntry(const std::string name, const std::string address, const std::list phones);

private:
    std::string theName;
    std::string theAddress;
    std::list thePhones;
    int numTimeConsulted;
};

ABEntry::ABEntry(const std::string name, const std::string address, const std::list phones)
{
    theName = name;            // 这些都是赋值assignment
    theAddress = address;      // 而非初始化initialization
    thePhones = phones;
    numTimeConsulted = 0;
}

C++ 规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。若是成员变量是内置类型,不保证一定在你所看到的那个赋值动作的时间点之前获得初值。

ABEntry::ABEntry(const std::string name, const std::string address, const std::list phones) :theName(name), theAddress(address), thePhones(phones), numTimeConsulted(0)
{

}// 都是初始化initialization

基于赋值的那个版本首先调用默认构造函数为 theName, theAddress ,thePhones,numTimeConsulted设初值,然后立刻再对它们赋予新值。默认构造函数的一切作为因此都浪费了。

对大多数类型而言,比起先调用默认构造函数然后在调用copy assignment操作符,单只调用一次copy构造函数是比较高效的。

对于内置类型对象如numTimeConsulted,其初始化和赋值的成本相同,但为了一致性最好也通过成员初值列来初始化。

同样道理,甚至当你想要default构造一个成员变量,你都可以只用成员初值列,只要指定nothing作为初始化实参即可。

ABEntry::ABEntry() :theName(), theAddress(), thePhones(), numTimeConsulted(0)
{

}

由于编译器会为用户自定义类型(user-defined types)之成员变量自动调用default构造函数--如果那些成员变量在”成员初值列”中没有被指定初值的话,因而引发某些程序员过度夸张地采用以上写法。

规定总是在初值列中列出所有成员变量,以避免还得记住那些成员变量(如果他们在初值列中被遗漏的话)可以无需初值。

如果成员变量是const和references,它们就一定需要初值,不能被赋值。最简单的做法:总是使用成员初始列。

如果class内存在成员变量或base class,多份成员初值列的存在就会导致不受欢迎的重复(在初值列中)和无聊的工作。这种情况下可以合理地在初值列汇总遗漏那些“赋值表现像初始化一样好”的成员变量,改用它们的赋值操作,并将那些赋值操作移往某个函数(通常是private),供所有构造函数使用。这种做法在“成员变量的初值系由文件或数据库读入”时特别有用。

C++有着十分固定的“成员初始化次序”。base class更早于derived class被初始化,而class的成员变量总是以其声明次序被初始化。

为避免某些存在的隐晦错误,当你的成员初值列中条列各个成员时,最好总是以其声明次序为次序。

最后,不同编译单元内定义的non-local static对象的初始化顺序。

所谓static对象,包括global对象、定义与namespace作用域内的对象、在class内、在函数内、以及在file作用域内被声明为static对象。

函数内的static对象成为local static对象(因为它们相对函数来说是local),其他static对象称为non-local static对象。程序结束时static对象会被自动销毁,也就是它们的析构函数会在main结束时被调用。

真正头疼的问题是:如果某个编译单元内的non-local static对象的初始化动作使用了另一个编译单元的non-local static对象,它所用到这个对象可能尚未初始化。因为C++对“定义于不同编译单元内的non-local static对象”的初始化次序并无明确定义。

// file1
class FileSystem
{
public:
    ...
    std::size_t numDisks() const;
    ...
};

extern FileSystem tfs;


// file2
class Directory
{
public:
    Directory(params);
    ...
};

Directory::Directory(params)
{
    ...
    std::size_t disks = tfs.numDisks();
    ...
}

Directory tempDir(params);

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

C++对“定义于不同编译单元内的non-local static对象”的初始化次序并无明确定义。原因在于:决定它们的初始化次序相当困难,非常困难,根本无解。

幸运的是一个小的设计就可完全解决问题:将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个references指向它所含的对象。换句话说,non-local static对象被local static对象替换了。Singleton模式的一个常见的实现手法。

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

class FileSynstem {...};

FileSynstem& tfs()
{
    static FileSystem fs;
    return fs;
}

class Directory {...};
Directory::Directory(params)
{
    ...
    std::size_t disks = tfs().numDisks();
    ...
}

Directort& tempDir()
{
    static Directory td;
    return td;
}

唯一的不同是他们现在使用tfs()和tempDir()而不是tfs和tempDir。也就是说他们使用函数返回指向static对象的reference,而不是static对象自身。

这种结构下的reference-returning函数往往十分单纯:第一行定义并初始化一个local static对象,第二行返回它。非常适合inline。

从另一个角度来看,这些函数“内含static对象”的事实使它们在多线程系统中带有不确定性。处理这一麻烦的做法是:在程序的单线程启动阶段手工调用所有reference-returning函数,这可消除与初始化有关的race-condition。

运用reference-returning函数防止“初始化次序问题”,前提是其中有一个对对象而言合理的初始化次序。

为了避免在对象初始化之前过早地使用它们,你需要做三件事情。第一,手工初始化内置类型non-member对象。第二,使用成员初值列对付对象的所有成分。最后,在“初始化次序不确定性”氛围下加强你的设计。

请记住:

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

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

        为免除跨编译单元之初始化次序问题,请以local static对象替换non-local static对象;

你可能感兴趣的:(Effective,C++,55,c++,开发语言,后端)