Effective C++读书笔记~01 让自己习惯C++

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

View C++ as a federation of languages.

如何理解C++?

将C++视为一个由相关语言组成的联邦而非单一语言。主要的4个次语言(sublanguage):

  • C, C++的基础
  • Object-Oriented C++, C with Classes:构造函数、析构函数,封装,继承,多态,虚函数等。
  • Template C++,C++泛型编程部分
  • STL,template程序库,对容器、迭代器、算法、函数对象的规约有极佳的紧密配合与协调。

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

[======]

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

Prefer consts, enums, and inlines to #defines.

另一种理解:“编译器替换预处理器”

const等类型可以有编译器进行检查,而#define对编译器是透明的,无法利用编译器检查。

两种替换#define特殊情况:

1)定义常量指针(constant pointers)
如果是用char *定义常量指针,用于替换#define,必须写const两次。

const char *const authorName = "Scott Meyers";

如果是用string(通常比char *要合适)

const std::string authorName("Scott Meyers");

2)class专属常量
为了将常量的作用域(scope)限制于class内,必须让它成为class的一个成员(member)。而为确保此常量至多只有一份实体,必须让它成为static成员:

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

const int GamePlayer::NumTerns; // NumTerns定义
// 为什么没有赋值?

上面定义NumTerns时,为什么没赋值?
因为经常把定义的式子放到实现文件(.cpp, .cxx)中,由于class常量已经在声明时获得初值(5),因此定义时不可再设初值。
当然也可以把初值放在定义中,而声明中不指定初值。

PS:#define无法创建一个class专属常量,因为宏一旦被定义,在其后的编译过程中有效(除非被#undef)

enum hack

1)替换static const
如果编译器必须在编译器就知道数组大小,static const方式无法做到,可以改成使用enum

class GamePlayer {
private:
    enum{ NumTurns = 5 }; // enum hack: NumTurns成为常量5的一个记号名称
    int scores[NumTurns]; // 使用该常量
};

2)实用主义
很多代码用了它,必须认识。也是模板元编程的基础技术。

用template inline替换宏函数

可以获得宏带来的效率以及一般函数的所有可预料行为和类型安全(type safety)
比如,用template inline函数callWithMax,替换宏函数,以a,b中较大者调用f。

// 不安全的宏函数
// 以a和b的较大者调用f
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))

// 兼顾安全与效率的template inline函数
template<typename T>
inline void callWithMax(cosnt T& a, const T& b)
{
    f(a > b? a : b);
}

PS:
1)对于单纯常量,最好以const 对象或enum替换#define;
2)对于形似函数的宏(macro),最好改用inline函数替换#define;

[======]

条款03:尽可能使用const

Use const whenever possible.

顶层const:代表指针变量自身无法修改;
底层const:代表指针所指对象无法修改。

char* const p = "hello"; // 顶层const, const pointer, non-const data
const char *p = "hello"; // 底层const, non-const pointer, cosnt data

const与迭代器

迭代器的作用像T指针。
如果希望迭代器本身无法改变,而指向的内容可以改变,可以在迭代器前面加上const,即T
const。
如果希望迭代器本身可以改变,而指向的内容不可改变,可以使用const_iterator,即const T*。

std::vector<int> vec;
...
const std::vector<int>::iterator iter = vec.begin(); // <=> T* const
*iter = 10; // OK
++iter; // error: iter 是const

std::vector<int>::const_iterator cIter = vec.begin(); // <=> const T*
*cIter = 10; // error: *cIter是const
++cIter; // OK

const成员函数

const成员函数不允许更改对象的任何变量(static除外)。目的在于确认该成员函数可作用于const对象身上。const成员函数很重要,原因:
1)使class接口比较容易被理解。因为很容易得知,哪个函数可以改动对象内容,而哪个函数不行。
2)使“操作const对象”成为可能。对编写高效代码是关键:以pass by reference-to-const方式传递对象(条款20)。??

2个成员函数,如果只是常量性(constness)不同,可以被重载。non-const对象调用non-const function for non-const object, const 对象调用const function for const object。

class TextBlock
{
public:
       TextBlock(const char* s) : text(s) {}
       const char& operator[] (size_t position) const /* operator[] for const 对象 */
       {
              return text[position];
       }
       char& operator[] (size_t position) /* operator[] for non-const 对象 */
       {
              return text[position];
       }
private:
       string text;
};

non-const与const成员函数,在使用上的差异:

TextBlock tb("hello"); // non-const 对象
tb[0] = 'x'; // OK: 写一个non-const TextBlock
cout << tb[0] << endl; // OK: 调用non-const TextBlock::operator[]

const TextBlock ctb("hello"); // const 对象
ctb[0] = 'y';    // error: 写一个const TextBlock
cout << ctb[0] << endl; // OK: 调用const TextBlock::operator[]

注意:要修改string text,operator[] 返回值必须是引用类型(char &)或指针类型,不能是值。如果是值, 那么调用者修改的是tb[0]副本,而不是tb[0]本身。
比如,operator[] 返回char,下面句子无法通过编译:

tb[0] = 'x';

成员函数如果是const意味着什么?

这里有2个流行概念:bitwise constness(又称physical constness),logical constness。

1)bitwise constness
成员函数只有在不更改对象任何成员变量(static除外)时,才能说是const。i.e. 它不更改对象内的任何一个bit。编译器只需要寻找成员变量的赋值动作即可。bitwise constness是C++对常量性(constness)的定义,因此const成员函数不能更改对象内任何non-static成员变量。
但是,bitwise测试存在缺陷:如果类对象持有的是指针,bitwise测试能确保const成员函数不修改指针本身,但无法确保指针指向的内容不被修改。

class CTextBlock
{
public:
       CTextBlock(const char* s) { pText = new char[strlen(s) + 1]; strcpy(pText,  s); }
       ~CTextBlock() { delete[] pText; }
       char& operator[] (size_t position) const /* bitwise const声明, 确保operator[] 不修改pText */
       {
              return pText[position];
       }
       ...
private:
       char *pText;
};

// 以下内容能通过编译器
// 可以通过operator[]返回的指针, 修改pText指向的内容
const CTextBlock cctb("Hello"); // 声明一个常量对象
char *pc = &cctb[0];  // 调用const operator[]取得一个指针, 指向cctb第0个数据
*pc = 'J'; // 更新了cctb[0]内容

2)logical constness
由于bitwise constness并不能完全避免在const成员函数内修改处理对象内的bits,logical constness主张认为一个const成员函数可以修改它所处理的对象内的某些bits,但只有在客户端侦测不出的情况下才得如此。
例如,CTextBlock class有可能高速缓存(cache)文本区块的长度,以便应付查询:

class CTextBlock
{
public:
       // ...
       size_t length() const;
private:
       char *pText;
       size_t textLength;   // 最近一次计算的文本区块长度
       bool lengthIsValid;  // 目前的长度是否有效
};

size_t CTextBlock::length() const
{
       if (!lengthIsValid) {
              textLength = strlen(pText); // 错误:不能在const成员函数内修改对象属性textLength和lengthIsValid
              lengthIsValid = true;
       }
       return textLength;
}
// ...

上面length()的实现不是bitwise const,因为textLength和lengthIsValid都可能被修改,因此在用length()取得pText字符串最新长度时,需要重新计算、更新textLength和lengthIsValid的值。
而要在const成员函数内修改对象属性,可以用C++与const相关的摆动场:mutable(可变的)。mutable释放掉non-static成员变量的bitwise constness约束:

class CTextBlock
{
public:
       // ...
       size_t length() const;
private:
       char *pText;
       mutable size_t textLength;   // mutable表示这些成员变量可能总是会被更改, 即使是const成员函数内
       mutable bool lengthIsValid; 
};

size_t CTextBlock::length() const
{
       if (!lengthIsValid) {
              textLength = strlen(pText); // OK
              lengthIsValid = true; // OK
       }
       return textLength;
}
// ...

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

虽然上面的例子中,用mutable可以解决在const成员函数中修改对象属性,但不能解决代码重复的问题。比如,const 成员函数和non-const成员函数,operator[] 都可能有边界检验、日志记录数据访问、检验数据完整性这相同的3步。

class TextBlock
{
public:
       TextBlock(const char* s) : text(s) {}
       const char operator[] (size_t position) const
       {
              // 边界检验 (bounds checking)
              // 日志记录数据访问(log access data)
              // 检验数据完整性(verify data integrity)

              return text[position];
       }
       char operator[] (size_t position)
       {
              // 边界检验 (bounds checking)
              // 日志记录数据访问(log access data)
              // 检验数据完整性(verify data integrity)

              return text[position];
       }
private:
       string text;
};

能否把这些重复的代码,利用起来呢?编写一次,使用2次。
答案是可以的,可以将常量性转除(casting away constness):利用non-const函数调用cast函数,首先需要转型,然后去掉const修饰:

class TextBlock
{
public:
       TextBlock(const char* s) : text(s) {}
       const char& operator[] (size_t position) const
       {
              // 边界检验 (bounds checking)
              // 日志记录数据访问(log access data)
              // 检验数据完整性(verify data integrity)

              return text[position];
       }
       char& operator[] (size_t position)
       {
              // 边界检验 (bounds checking)
              // 日志记录数据访问(log access data)
              // 检验数据完整性(verify data integrity)

              return const_cast<char &>(static_cast<const  TextBlock&>(*this)[position]);
       }
private:
       string text;
};

这里有2个转型动作:
1)让non-const operator[]调用其兄弟const operator[],通过static_cast将*this由原始类型TextBlock& 安全转型为const TextBlock&;
2)再从const operator[]的返回值中,通过const_cast移除const。该步当然也可以选择用C风格的强制类型转换,不过并不推荐;

为避免重复,为什么只能是令non-const版本调用const的operator[],而不能是const版本调用non-const的?
因为const成员函数承诺绝不改变其对象的逻辑状态(logical state),non-const成员函数没有这样的承诺。如果在const函数中调用了non-const函数,就是冒了这样的风险。而non-const函数中调用const函数却不会带来风险。

小结

  • 将某些东西声明为const,可以帮助编译器检查出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数;
  • 编译器强制实施bitwise constness(const成员函数要求不能修改任何对象属性),编写代码时应该使用“概念上的常量性”(conceptual constness)(用mutable在const成员函数修改对象属性);
  • 当const和non-const成员函数有着实质等价实现时,可以令non-const版本调用const版本避免代码重复。

[======]

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

Make sure thath objects are initialized before they're used.

对于内置型对象

内置类型对象x,在某些语境下保证被初始化为0,不过其他语境并不能保证为0。例如,

// 作为全局变量
int x; // 0

// 作为自动变量
void fun() {
    int x; // x不能保证为0
}

// 作为类对象变量的成员
class Point {
    int x, y; 
};

Point p; // 全局变量p确保x为0

最佳解决办法:手工在使用对象之前现将其初始化。

int x = 0; // 对int手工初始化
const char* text = "A C-style string"; // 对指针手工初始化

double d;
std::cin >> d; // 以读取input stream方式完成初始化

对于类对象

使用构造函数(constructor):确保构造函数都将对象的每个成员初始化。
需要注意:在构造函数中使用member initialization list(成员初值列),才是初始化成员;而构造函数体内使用赋值操作,是赋值(assignment)而非初始化(initialization)。

class PhoneNumber
{
public:
       string number;
};
class ABEntry
{
public:
       // case1 构造函数体内对class成员进行赋值
       ABEntry(const string& name, const string& address, const list<PhoneNumber>&  phone)
       {
              /* 这些都是赋值(assignment), 而非初始化 */
              theName = name; // 如果是类对象, 赋值(=)操作会调用拷贝构造函数; 如果是基本类型, 则直接赋值
              theAddress = address;
              thePhone = phone;
              numTiemsConsulted = 0;
       }

       //  case2 使用member initialization list构造ABEntry对象
       ABEntry(const string& name, const string& address, const list<PhoneNumber>&  phone)
              : theName(name), // 调用theName的default构造函数, 从而构造成员对象
              theAddress(address),
              thePhone(phone),
              numTimesConsulted(0)
       {}
private:
       string theName;
       string theAddress;
       list<PhoneNumber> thePhone;
       int numTimesConsulted;
};

1)case1 构造函数体内对class成员进行赋值,实际上在进入构造函数体之前,会调用构造函数对各对象成员进行初始化。构造函数体内是赋值操作,而非初始化。
2)case2 是推荐的做法,在进入构造函数前,就进行了成员初始化。

因此,case1 相当于下面的代码,可以看出对成员相当于调用了2次赋值操作:

...
// case1 构造函数体内对class成员进行赋值
ABEntry(const string& name, const string& address, const list<PhoneNumber>&  phone)
	: theName(), // 构造成员对象
	theAddress(),
	thePhone(),
	numTimesConsulted(0)
{
	/* 这些都是赋值(assignment), 而非初始化 */
	theName = name; // 如果是类对象, 赋值(=)操作会调用拷贝构造函数; 如果是基本类型, 则直接赋值
	theAddress = address;
	thePhone = phone;
	numTiemsConsulted = 0;
}
...

注意:如果成员过多,或者无需初值,可以在成员初值列中忽略该成员,这样它就没有初值。

关于成员初始化次序:
member initialization list中对象成员初始化次序,以防漏掉,或者导致检阅者迷惑,建议与声明的顺序保持一致。

不同编译单元内定义的non-local static对象的初始化次序

static对象,其寿命从被构造出来直到程序结束为止。因此stack和heap-based对象都不是static对象。static对象包括:global对象、定义于namespace作用域内的对象,在class内、函数内、file作用域内被声明为static的对象。函数内的static对象称为local static对象,其他static对象都称为non-local static对象。
编译单元(transitation unit):指产出单一目标文件(sigle object file)的那些源码。通常是单一源码文件(.c, .cpp, .cxx)加上所包含的头文件(#include files)。

问题:如果编译单元A内的某个non-local static对象的初始化动作,使用了另一个编译单元B内的某个non-local static对象,A初始化时,B可能没有被初始化。而C++对定义于不同编译单元内的non-local static对象的初始化次序,并没有明确的规定(事实上也很难做到)。

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

extern FileSystem tfs; // 声明global 对象tfs, 属于non-local static对象

// Directory.h
class Directory {
public:
    Director(params);
    ...
};
extern Directory tempDir;

// Directory.cpp
Director::Director(params)
{
    ...
    size_t disks = tfs.numDisks();// 错误:使用tfs对象, 但C++无法保证位于编译单元FileSystem.o中的FileSystem tfs已经初始化完成
}

如何解决这个问题?
可以将B的non-local static对象通过函数包装起来,替换为local static对象,A只需要通过这个包装函数访问B的local static对象即可。类似于单例模式。



// FileSystem.h
class FileSystem { ... };
// reference-returning函数, 返回local static对象引用
FileSystem& tfs() // 用于替换tfs对象, 将non-local static对象tfs替换为local static对象, 并返回其reference
{
    static FileSystem fs; // 这里会调用FileSystem构造函数, 确保fs初始化完成
    return fs;
}
// Directory.h
class Directory { ... };
// Directory.cpp
Directory::Directory(params)
{
    ...
    size_t disks = tfs().numDisks();
    ...
}
// reference-returning函数, 返回local static对象引用
Directory& tempDir() // 用于替换tempDir对象
{
    static Directory td;
    return td;
}

如果这种reference-returning函数,被频繁调用,还可以声明为inline函数。

缺点:
由于函数内含static对象(带有状态),在多线程系统中带有不确定性。解决办法是在程序单线程启动阶段,手工调用所有reference-returning函数,可消除与初始化有关的“竞争条件(race conditions)”。

注意:
绝对要避免这种尴尬状况:对象A初始化依赖于对象B,而对象B的初始化又要依赖于A。

小结

  • 为内置型对象进行手工初始化,因为C++不保证初始化它们;
  • 构造函数最好使用成员初值列表(member initialization list),而不要在构造函数本地内用赋值操作(assignment)。初值列列出的成员变量,其排列次序应该和它们在class中声明的次序相同;
  • 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-static对象;

[======]

posted @ 2021-11-15 22:47  明明1109  阅读(133)  评论(0编辑  收藏  举报