Effective C++ (一) : 让自己习惯C++

xiaoxiao2021-02-28  9

让自己习惯C++

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

C++的主要的四个次语言:

-C。C++仍是以C为基础:区块(blocks),语句(statements),预处理器(preprocessor),内置数据类型(built-in data types),数组(array),指针(pointers)

-Object-Oriented C++。这部分是C with Classes所述求的:classes(包括构造函数和析构函数),封装(encapsulation),继承(inheritance),多态(polymorphism),virtual函数(动态绑定)....等等

-Template C++。这是C++的泛型编程部分。

-STL。STL是个template程序库。它对容器,迭代器,算法以及函数对象的规约有极佳的紧密配合与协调。

 

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

#define ASPECT_RATIO 1.653

记号ASPECT_RATIO也许从未被编译器看见;也许在编译器开始处理源码之前它就被预处理器移走了,记号名称ASPECT_RATIO可能没进入记号表。解决之道是以一个常量替换上述的宏(#define):

const double AspectRatio = 1.653;                //大写名称通常用于宏定义,这里改变名称写法。

 

作为语言常量,AspectRatio肯定会被编译器看到,会进入记号表。

 

以常量替换#define两种特殊情况: 1.定义常量指针,有必要将指针const。若要定义一个常量字符串,必须写const两次:

   

 

1

const char* const authorName = "amoscykl";

   上述的authorName往往如下定义比较好:

 

 

1

const std::string authorName("amoscykl");

 

2.定义class专属常量:将常量的作用域限制于class内,必须让它成为class的一个成员。为了确保此常量唯一,必须让它成为一个static成员:

 

1

2

3

4

5

class GamePlayer {

private:

static const int NumTurns = 5; //常量声明式

int scores[NumTurns]; //使用常量

}

 

无法利用#define创建一个class专属常量。一但宏被定义,它就在其后的编译过程中有效。这就意味着#define不仅不能够用来定义class专属常量,也不能够提供任何封装性,所以没有private #define 这样的东西。const成员变量是可以被封装的。

 

若编译器不允许"static整数型class常量” 完成"in class初值设定“,可改用"the enum hack”补偿做法。

即枚举类型的数值可充int使用:

 

1

2

3

4

5

class GamePlayer {

private:

enum { NumTurns = 5 }; //"the enum hack"-令NumTurns成为5的记号名称

int scores[NumTurns];

}

 

 

对比宏定义和模板函数:

 

1

#define MAX(a,b) f((a) > (b) ? (a) : (b)) //当写出这种宏,一定要给所有实参加上小括号。

当调用:

 

1

2

3

int a =5, b = 0;

MAX(++a,b); //a被累加二次

MAX(++a,b+10); //a被累加一次

调用f之前,a的递增次数取决于和谁比较! 调用此宏很可能遇到麻烦!

 

模板函数:

 

1

2

3

4

5

template<typename T>

inline void MAX(const T& a, const T& b)

{

f (a > b ? a : b); //遵循pass by reference-to-const规则 (根据动态绑定的实参调用)

}

这个template产出一整群函数,每个函数都接受两个同型对象。这里不需要为参数加上括号,也不需要操心参数被核算多次...等等

 

总结: -对于单纯常量,最好以const对象或enums替换#define

-对于形似函数的宏(macros),最好改用inline函数替换#define

 

条款03:尽可能使用const

 

-若关键字出现在 * 左边,表示被指物是常量(但可以通过其它途径改变被值对象的值,不能通过此指针改变)

 若关键字出现在 * 右边,表示指针自身是常量;

 若出现在两边,表示被植物和指针两者都是常量

 

 

1

2

3

//若被指物是常量,则两种写法意义相同。

void f1(const Widget* pw); //f1获得一个指针,指向一个常量的Widget对象

void f2(Widget const * pw); //同上

 

迭代器的作用就像个T* 指针。

声明迭代器为const就像声明指针为const一样(T* const指针),表示迭代器不可变(但迭代器指向的对象的值可以改动)。

若希望迭代器指向的值不可变。则需要const_iterator:

 

1

2

3

4

5

6

7

8

9

10

std::vector<int> vec;

...

const std::vector<int>::iterator iter = //iter的作用像个T* const

vec.begin();

*iter = 10; //没问题,该变iter所指物

++iter; //错误! iter是const

std::vector<int>::const_iterator cIter = //cIter的作用像个const T*

vec.begin();

*cIter = 10; //错误! *cIter是const

++cIter; //没问题,改变cIter;

 

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

有理数的operator* 声明式:

 

1

2

class Rational { ... };

const Rational operator* (const Rational& lhs,const Rational& rhs); //第一个const表示返回一个const

 

若不返回一个const,考虑以下代码:

 

1

2

3

Rational a,b,c;

...

if (a * b = c); //若把做比较的运算符==打成了=

无意间把==打成了=,就会有无意义的赋值操作。

若返回值是一个const,则赋值操作是错误的,可以预防这样的错误。

 

const成员函数: 两个成员函数如果只是常量性不同,可以被重载。----C++重要特性

考虑以下class:

 

1

2

3

4

5

6

7

8

9

10

11

class TextBlock {

public:

...

/* 以下两个重载返回类型都是引用!!!敲黑板*/

const char& operator[](std::size_t position) const //operator[]for const对象

{ return text[position]; }

char& operator[](std::size_t postion) //operator[]for no-const对象

{ return text[position];

private:

std::string text;

};

    

TextBlock的operator[]可被这么使用:

 

1

2

3

4

5

6

7

TextBlock tb("Hello");

tb[0] = 'X'; //正确——写一个no-const TextBlock

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

const TextBlock ctb("world!");

ctb[0] = 'X'; //错误——写一个const TextBlock

std::cout << ctb[0]; //调用const TextBlock::operator

 

 

1

2

3

4

void print(const TextBlock& ctb) //此函数ctb是const

{

std::cout << ctb[0]; //调用const TextBlock::operator[]

}

 

/* 成员函数const补充:哲学 (逃 */

哲学的两个流派:bitwise constness 和 logical constness;

bitwise constness阵营主张:const成员函数不可以更改对象内任何非static成员变量

logical constness阵营主张: const成员函数可以修改处理的对象内的某些bits;

 

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

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。

虽然修改对const CTextBlock对象而言可以接受,但编译器不同意(编译器是bitwise constness阵营的),怎么办怎么办??

 

解决方案:用mutable释放掉non-static成员变量的bitwise constness约束(把非static对象开除出bitwise constness阵营)

修改7、8行代码为:

 

1

2

mutable std::size_t textLength;

mutable bool lengthIsValid;

现在这些成员可以被更改,即使在const成员函数里

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

 

const是个奇妙且非比寻常的东西。在指针和迭代器身上;在指针、迭代器及reference指涉的对象身上;在函数参数和返回类型身上;

在local变量身上;在成员函数身上,林林总总不一而足。const是个威力强大的助手。尽可能使用它。你会对你的作为感到高兴~

 

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

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

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

 

 

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

对于构造函数:确保每一个构造函数都将对象的每一个成员初始化。

//区分赋值和初始化

 

例如以下构造函数:

 

1

2

3

4

5

6

7

8

9

AB::AB(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)

{

//以下几个左值都是类的成员

theName = name;

theAddress = address;

thePhones = phones;

numTimesConsulted = 0;

//敲黑板!!以上都是赋值,不是初始化!!

}

这会导致AB对象带有你期望的值,不是最佳做法!

 

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

 

1

2

3

AB::AB(const std::string& name, const std::string& address; const std::list<PhoneNumber>& phones)

:theName(name), theAddress(address), thePhones(phones), numTimesConsulted(0) {}

//通过copy构造初始化

这个构造函数和上一个的最终结果相同,但通常效率较高。

 

默认构造函数也可以使用成员初值列。

 

1

AB::AB() : theName(), theAddress(), thePhones(), numTimesConsulted(0) {}

    //没有指定初值则自动调用default构造函数

 

重要规则:规定总是在初值列中列出所有成员变量。以免还得记住哪些成员变量可以无需初值。

如果成员初值列遗漏某个成员,它就没有初值,因此可能开启”不明确行为“的潘多拉盒子~

 

若成员变量是const或reference,就一定需要初值,不能被赋值!

所以最简单的做法就是:总是使用成员初值列。这样做有时候绝对必要,且又往往比赋值更高效。

 

C++有着固定的”成员初始化次序“:

base classes基类总是比derived classes继承类更早被初始化。

class的成员变量总是以其声明次序被初始化。

例如:

 

1

2

3

4

5

6

7

8

9

10

class AB {

public:

AB::AB(const std::string& name, const std::string& address; const std::list<PhoneNumber>& phones)

:theName(name), theAddress(address), thePhones(phones), numTimesConsulted(0) {}

private:

std::list<PhoneNumber> thePhones;

std::string theAddress;

int numTimesConsulted;

std::string theName;

}

即使在构造函数成员初值列中出现的次序和private的声明次序不同,初始化次序依旧和声明次序相同(即thePhones最先初始化)

为避免你或读者被迷惑,并避免某些可能存在的晦涩错误,当在成员初值列中条列各个成员时,最好总是以其声明次序为次序!

 

 

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

static对象,其寿命从被构造出来直到程序结束为止。

这种对象包括:global对象,定义于namespace作用域内的对象,在class内,在函数内,以及在file作用域内被声明为static的对象。

函数内的static对象称为local static对象(对函数而言是local),其它对象称为non-local static对象。

static对象在程序结束时自动销毁,它们的析构函数会在main()结束时被自动调用。

 

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

 

真正的问题是:如果某编译单元内的某个non-local static对象的初始化动作使用了另一个编译单元内的某个non-local static对象,

它所用到的这个对象可能尚未被初始化,因为C++对定义在不同编译单元内的non-local static对象的初始化次序并无明确定义。

 

实例:

 

1

2

3

4

5

6

7

8

class FileSystem { //来自你的程序库

public:

...

std::size_t numDisks() const; //众多成员函数之一

...

};

extern FileSystem tfs; //预备给客服使用的对象

//tfs代表"the file system"

 

 

1

2

3

4

5

6

7

8

9

10

class Directory {

public:

Directory( params );

...

};

Directory::Directory( params )

{

...

std::size_t disks = tfs.numDisks(); //使用tfs对象

}

然后执行:

 

1

Directory tempDir( params ); //为临时文件而做出的目录

 

现在,初始化次序的重要性显示出来了:除非tfs在tempDir之前先被初始化,否则tempDir的构造函数会用到尚未初始化的tfs。

 

如何确定tfs在tempDir之前先被初始化?

唯一需要做的是:

将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。

这些函数返回一个reference指向它所含的对象。然后用户调用这些函数,而不直接指涉这些对象。

即:non-local static 被 local static对象替换了。

 

这种方法基于:C++保证,函数内的local static对象会在 “函数被调用期间" 或 "首次遇上该对象之定义式” 时被初始化!

 

所以以"函数调用“(返回一个reference指向local static对象) 替换”直接访问non-local static对象".

这样获得的reference将指向一个历经初始化的对象。

 

所以,经过此技术施行:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

class FileSystem { //来自你的程序库

public:

...

std::size_t numDisks() const; //众多成员函数之一

...

};

FileSystem& tfs() //这个函数用来替换tfs对象:它在FileSystem class中可能是个static.

{

static FileSystem fs; //定义并初始化一个local static对象

return fs; //返回一个reference指向上诉对象。

}

class Directory {

public:

Directory( params );

...

};

Directory::Directory( params )

{

...

std::size_t disks = tfs().numDisks(); //使用tfs()对象

}

Directory& tempDir() //这个函数用来替换tempDir对象,在Directory class中可能是个static

{

static Directory td; //定义并初始化local static对象

return td; //返回一个reference指向上述对象

}

现在这个程序调用就没有问题了,唯一不同的是现在使用tfs()和tempDir()而不再是tfs和tempDir.

也就是说使用函数返回的"指向static对象"的reference,而不再使用static对象本身。

 

注意:任何一种non-const static对象,不论它是local或non-local,在多线程环境下“等待某事发生”都会有麻烦!

处理麻烦的一种做法:在程序的单线程启动阶段手工调用所有reference-returning函数。

 

/*    补充,内置类型:包括算术类型和空类型在内的基本数据类型。

       算术类型包括:字符型,整型,bool型,浮点型                   */

 

为避免在对象初始化之前过早地使用它们,需要做三件事:

第一,手工初始化内置型non-member对象。

第二,使用成员初值列对付对象的所有成分。

第三,在“初始化次序不确定性”氛围下加强设计!

 

 

总结:

-对内置型对象进行手工初始化,因为C++不保证初始化它们。(避免出现不确定情况)

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

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

 

转载请注明原文地址: https://www.6miu.com/read-2000057.html

最新回复(0)