文章目录
条款01:视C++为一个语言联邦
C++发展到现在是个多重范型编程语言(multiparadigm programming language),一个同时支持过程形式(procedural)、面向对象形式(object-oriented)、函数形式(function)、泛型形式(generic)、元编程形式(metaprogramming)的语言,在其次语言中(sublanguage)中,从一个次语言移往另一个次语言中,守则可能会改变。幸运的是其次语言总共只有四个:
- C 说到底C++仍旧以C语言为基础区块(blocks)、语句(statements)、预处理器(preprocessor)、内置数据类型(build-in data types)、数组(arrays)、指整(pointers)。许多时候C++对问题的解法起始不过就是较高级的C解法(例如条款2谈到到预处理器之外的选择,条款13对象管理资源)
- Object-Oriented C++ C with Classes classes(包括构造函数和析构函数),封装(encapsulation),继承(inheritance),多态(polymorphism),virtual 函数(动态绑定)等等,是面向对象设计之古典守则在C++上最直接的实施
- Tamplatr C++ C++泛型编程(generic programming)部分,大多程序员经验最少的部分。由于Tamplate 威力巨大,它们带来崭新的编程范型(programming paradigm),也就是所谓的template metaprogramming (TMP ,模板元编程)
- STL 是个template 程序库,对容器(containers)、迭代器(iterators)、算法(algorithms)以及函数对象(function objects)的规约有极佳的紧密配合与协调,template 及程序库也可由其他想法建立出来。
记住这四个次语言,当你从某个次语言切换到另一个,导致高效的编程守则要求你改变策略时,不必惊讶。例如内置类型(C-like)类型而言pass-by-value 通常比pass-by-reference 高效,但当你从C part of C++ 移往Object-Oriented C++ 由于用户自定义(user-defined)构造函数和析构函数的存在pass-by-reference-const 往往更好
- 高效的编程守则视状况而变化,取决于你用C++的哪一部分
条款02:尽量以const,enum,inline替换#define
const
也可说“宁可以编译器替换预处理器”,因为或许#define不被视为语言的一部分
#define ASPECT _RATIO 1.653
这里的记号名称也许未被编译器看见,也许编译器在处理源码前它就被移走了。可能其并没有进入记号表(symbol table)内,解决方案就是用const 来替换上述宏(#define)
对浮点量而言,常量可能常量的使用比#define 产生的代码量更少,同时预处理器盲目改变宏名称替换为数字可能会导致目标码(object-code)产生多次改用常量就不会出现这种情况
两种特殊情况:
- 定义和常量指针(constant pointers)。由于常量定义式通常在头文件内(以便被不同的源码含入)有必要将指针声明为const pointer(注意是常量指针而不是所指对象)
- class 专属常量为了将常量的作用域(scope)限制于class 内,你必须让它成为class 成员 (member);为了确保常量至多只有一份,必须让它成为static 成员
class GamePlayer{
private:
static const int NumTurns = 5;
int score[NumTurns];
...
}
注意: 看到的NumTurns 是声明式而非定义式,通常C++要求你对所使用的的任何东西提供一个定义式,但如果它是个class 专属常量又是static 且为整数类型(integral type ,例如ints,chars,bools)则需要特殊处理,只要不取其地址就可以声明并使用无需提供定义式。但如果取其地址或编译器一定要看到一个定义式就必须额外提供定义式:
const int GamePlayer::NumTurn;
在声明时已经为它设置初值所以定义就无需再设初值。
旧编译器或许不支持上述语法,它们不允许static 成员在static 声明式上获得初值,此外所谓的“in-class 初值设定”也许只对整数常量进行,如果编译器不知处可以将初值放在定义式。
enum
几乎在任何时候都唯一要做的事。唯一例外是当你在编译期间需要一个class 常量值,例如上述score[NumTurn] 数组(编译器坚持必须在编译期间知道数组的大小)。万一编译器不允许“static 整数型 class 常量”完成“in class 初始值设定”,可改用所谓的“the enum hack” 补偿做法。理论基础是:“一个属于枚举类型(enumerated type)的数值可权充ins被使用”,于是GamePlay定义如下:
class GamePlay{
private:
enum{ NumTurn = 5;}
int scores[Numturn];
}
认识enum的数个理由:
- enum hack的行为某方面说比较像#define 而不是const ,有时这就是想要的。例如取一个const 的地址是合法的,但取一个enum 是不合法的,而取一个#define的地址通常也不合法。如果不想让别人获得一个pointer 或reference 指向你的某个整数的常量,enum可以帮助你实现这个约束。enums 和#define 一样绝对不会导致非必要的内存分配
- 事实上”enum hack“ 是 “template metaprogramming” (模板元编程)的基础技术
预处理器
为宏中的所有实参加上小括号,否则某些人在表达式中调用这个宏会遭遇麻烦
请记住
- 对于单纯常量,最好以const 对象或enums 替换#defines。
- 对于形似函数的宏(macros),最好改用inline 函数替换#defines。
条款03:尽可能使用const
1、判断const指针还是指向const对象
如果关键字const 出现在星号左边,则表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在两边,表示被指物和指针两者都是常量
对于被指物是常量,有些程序员会将关键字const 写在类型之前,有些人会写在类型之后、星号之前。这两种写法意义相同,如下:
void f1 (const Wight* pw);
void f2 (Wight const * pw);
2、STL迭代器中的const
STL迭代器系以指针为根据塑膜出来,所以迭代器类似于T* 指针,声明迭代器为const 像声明指针为const 一样(即声明一个T* const 指针),则表示这个迭代器不能指向不同的东西,但它所指的东西的值可以改,
p18
如果希望迭代器指向的东西不可改动(即希望STL 模拟一个const T*的指针)需要用的是const_iterator
if(a * b = c)... //其实是想做一个比较的动作
以上代码会隐式的转换为bool类型,如果a 和b 都是内置类型,这样的代码直截了当就是不合法,“良好的用户自定义类型”的特征是他们会避免无端的与内置类型不兼容,将operator* 的回传值声明为const 会预防那个“没必要的动作”
**warming:**除非你有改动的参数或local 对象,否则请将它们声明为const,多打6个字符,像类似“ == 写成 = ”的情况就可以避免
3、const成员函数
- 改善C++ 程序效率的根本方法是以 pass by reference-to-const 方式传递对象
- 如果函数的返回类型是个内置类型,那么改动函数返回值从来就不合法。纵使合法C++ 以by value 传值这一事实,事实上是改变其副本,不是其自身,这不会是我们想要的行为。(事实上by value 的返回会先生成一个临时对象,再把临时对象返回)
4、成员函数如果是const意味着什么(bitwise const 和logical const)
bitwise const又称为physical const
bitwise const
在该阵营的人相信,成员函数只有在不更改对象的任何成员变量(static 除外,静态的类似于全局变量)时才可以说是const 。也就是说它不更改对象内任何一个bit 。好处是容易侦测异常点:编译器只需要寻找成员变量的赋值动作即可,bitwise 是C++ 对常量性(constness)的定义,因此const 成员函数不可以改变对象内任何non-static 成员变量
虽然许多成员函数不十足具备const 性质却能通过bitwise 测试不会引发编译错误,具体地说,一个更改了“指针所指物”的成员函数虽然不能算是const ,但如果只有指针(而非所指物)隶属于对象,不会引发编译错误,可能有点绕那么请看以下例子:
class CTextBlock{
public:
...
char& operator[](std::size_t position) const //bitwise 声明
{ return pText[postion]; }
private:
char* pText;
}
在这里operator[] 实现代码不会改变pText 。于是编译器认为其是bitwise const ,那么接下来看下面:
const CTextBlock cctb("Hello"); //声明一个常量对象
char* pc = &cctb[0]; //调用operator[]函数取得指针指向cctb数据
*pc = 'j'; //cctb现在改变了原有的内容
在以上代码中不该会有任何错误,创建了const 对象且调用了const 成员函数,但最终还是改变了它的值。
logical constness
这种情况就导出了所谓的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)
{
textLength = std::strlen(pText);//错误:不可修改
lengthIsValid = true;//错误:不可修改
}
return textLength;
}
length 的实现当然不是bitwise const ,因为textlength 和lengthIsValid 都可能被修改。编译器不同意,因为其坚持bitwise const
解决编译器坚持bitwise const
解决办法:利用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 的成员变量也可能会被更改
mutable bool lengthIsValid;
};
std::size_t CTextBlock::length() const //已添加const
{
if (!lengthIsValid) {
textLength = std::strlen(pText);//编译通过,可以进行赋值
lengthIsValid = true; //也可以这样
}
return textLength;
}
5、在const 和non-const 成员函数中避免重复(常量性转除)
先看如下例子:假设TextBlock (和CTextBlock )内的operator[ ] 不单只是返回一个reference 指向某字符,也执行边界检验(bounds checking)、志记数据访问(log access data),甚至检验数据完整性(verify data integrity)等等。把这些都放入两个成员函数中const operator[ ] 和 non-const operator[ ] 两个成员函数中(不管inline )其中的代码重复,以及伴随的编译时间,维护等等,想想都令人头疼。
其中也可提取重复代码移动到另一个成员函数中(通常是private )再去调用这个函数,但是还是产生重复的代码,例如:函数调用、两次return 等等。
class TextBlock {
public:
const char& operator[](std::size_t postion) const {
//.. //边界检验(bounds checking)
//.. //志记数据访问(log access data)
//.. //检验数据完整性(verify data integrity)
return text[postion];
}
char& operator[](std::size_t postion) {
//.. //边界检验(bounds checking)
//.. //志记数据访问(log access data)
//.. //检验数据完整性(verify data integrity)
return text[postion];
}
private:
std::string text;
};
真正应该做的是operator[ ] 的机能一次并使用它两次。也就是说,你必须令其中一个调用另一个。这促使我们使用常量性转除(casting away constness),一般情况下不建议转型(casting),本次使用的是安全的转型,毕竟代码重复也不那么令人愉快。
class TextBlock {
public:
const char& operator[](std::size_t postion) const {
//.. //边界检验(bounds checking)
//.. //志记数据访问(log access data)
//.. //检验数据完整性(verify data integrity)
return text[postion];
}
char& operator[](std::size_t postion) {
//将op[]返回值的const 转除为*this 加上const 调用const operator[]
const_cast<char&>(static_cast<const CTextBlock>(*this)[postion]);
return text[postion];
}
private:
std::string text;
};
虽然代码并不怎么美丽,但有了“避免代码重复的效果”,其运用了const operator 实现了non-const 的效果,运用const 成员函数实现出其non_const 孪生兄弟是值得了解的。
同时反向做法,在const operator 中调用non-const 并非是我们应该做的事。const 成员函数承诺绝不改变其对象的逻辑状态(logical state),non-const 没有这般的承诺。如果在const 函数中调用了non-const 函数那么,就会冒风险:你曾经承诺不改动的那个对象被改动了。
在const 成员函数里调用non-const 函数是一种错误的行为
- 可将某些东西声明为const 可以帮助编译器侦测错误用法
- 编译器强制实施bitwise const ,但我们在编写程序时应该使用概念上的常量性(conceptual constness)
- 当const 和non-const 成员函数有着实质等价的实现时,令non-const 版本调用const 版本可以避免代码重复