条款02 尽量以const,enum,inline替换#define
“宁可以编译器替换预处理器”比较好
预处理器:最常见为C预处理器,采用"#"为行首的指示,根据用户定义的规则,进行简单的词法单元替换。典型的词法预处理器产生宏替换,包含其他文件的文本,并且条件性地编译或者包含文件。
当你做出
#define ASPECT_RATIO 1.653
意味着所有的ASPECT_RATIO 都被替换为1.653,也意味着记号名称ASPECT_RATIO也许从未被编译器看见,于是记号名称ASPECT_RATIO有可能没进入记号表(symbol table)内。当你运用此常量但获得一个编译错误时,可能会有困惑,这个1.653是一个什么玩意?如果ASPECT_RATIO被定义在一个非你所写的头文件内,你肯定以及它来自何处毫无概念
解决之道:
const double AspectRatio = 1.653; //大写名称通常用于宏,因此这里改变名称写法
以常量替换#define,有一种特殊情况值得讨论:
一、class专属常量。
为了将常量的作用域(scope)限制于class内,你必须让它成为class的一个专员(member);而为确保此常量至多只有一份实体,你必须让它成为一个static成员:
class GamePlayer{
private:
static const int NumTurns = 5; //而这只是个声明式不是定义式
int scores[NumTurns];
...
};
通常C++要求你对你所使用的任何东西提供一个定义式,但如果它是个class专属常量又是static且为整数类型(ints, chars, bools),则可以只声明并使用它们而无须提供定义式(in-class初值设定)。
但如果你取某个class专属常量的地址,或你的编译器(不正确地)坚持要看到一个定义式,你就必须另外提供定义式:
const int GamePlayer::NumTurns; //由于已在声明时获得初值,因此定义时无须再设
然而我们无法利用#define创建一个class常量。
万一你的编译器不允许“static整数型class常量”完成“in-class初值设定”,可改用“the enum hack”补偿法
“一个属于枚举类型(enumerated type)的数值可权充ints被使用”:
class GamePlayer{
private:
enum { NumTurns = 655; };
int scores[NumTurns];
...
};
如果你不想让别人获得一个pointer或reference指向你的某个整数常量,enum可以帮助你实现这个约束。
另一个常见的#define误用情况是以它实现宏(macros),宏看起来像函数,但不会招致函数调用(function call)带来的额外开销。
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b)) //a或b较大那个作为参数调用f函数
int a = 5, b= 0;
CALL_WITH_MAX(++a, b); //a被累加二次
CALL_WITH_MAX(++a, b+10); //a被累加一次
调用f之前,a的递增次数竟然被取决于“它被来和谁比较”!
可用模板(template inline)函数解决:
template<typename T>
inline void callWithMax(const T& a, const T& b)
{
f(a > b ? a : b);
}
这个template产出一整群函数,T为任意类型。
callWithMax是个真正的函数,它遵守作用域和访问规则。
总结:
- 对于单纯常量,最好以const对象或enums替换#define
- 对于形似函数的宏(macros),最好改用inline函数替换#defines
条款03 尽可能使用const (use const whenever possible)
const多才多艺,你可以用它在classes外部修饰global或namespace作用域中的常量,或修饰文件、函数、或区块作用域中被声明为static的对象。
你也可以用它修饰classes内部的static和non-static成员变量。面对指针,你也可以指出指针自身、指针所有物,或两者都(或都不)是const。
const语法随变化多端,但并不莫测高深。
char greeting[] = "Hello";
如果const出现在型号左边,表示被指物是常量:
const char* p =greeting;
如果出现在星号右边,表示指针自身是常量:
char* const p = greeting;
如果出现在星号两边,表示被指物和指针两者都是常量 :
const char* const p =greeting;
STL迭代器系以指针根据塑模出来,所以迭代器的作用像个T* 指针。
声明迭代器为const就像声明指针为const一样(即声明一个T * const指针)
std::vector<int> vec;
const std::vector<int>::iterator iter =vec.begin(); //iter的作用像个T* const
*iter = 10; //没问题,改变iter所指物
++iter; //错误!iter是const
如果你希望迭代器所指的东西不可被改动(即希望STL模拟一个const T*指针),你需要的是const_iterator:
std::vector<int>::const_iterator cIter = vec.begin();
*cIter = 10; //错误!*cIter是const
++cIter; //没问题,改变cIter
const最具威力的用法是面对函数声明时的应用。
令函数返回一个常量值,往往可以降低因客户错误而造成的意外,而又不至于放弃安全性和高效性。
class Rational {...};
const Rational operator* (const Rational& lhs, const Rational& rhs);
这样做可以防止客户实现这样的暴行:
Rational a,b,c;
...
(a * b) = c;
这有可能不是刻意为之,但会有很多人无意识那么做,只因为单纯的打字错误。
const成员函数
将const实施于成员函数的目的,是为了确认该成员函数可作用于const对象身上。
一个容易忽视的事实是:
两个成员函数如果只是常量性(constness)不同,可以被重载,这是一个重要的C++特性。
真正程序中const对象大多用于passed by pointer-to-const或passed by reference-to-const的传递结果
void print(const TextBlock& ctb)
{
std::out<<ctb[0];
}
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;
}
只要重载operator[]并对不同版本给予不同的返回类型,就可以让const和non-const TextBlocks 获得不同的处理。
TextBlock tb("Hello");
std::cout<<tb[0];
tb[0]='x';
const TextBlock ctb("world");
std::cout<<ctb[0];
ctb[0]='x'; //错误!写一个const TextBlock
对于编译器来说,const成员函数不更改对象内的任何一个bit,这是毋容置疑的。
但事实上存在着const成员修改对象内某些bit的需求,即在一个成员函数里,希望改变const对象的某些值的同时不改变某些值。
例如一个CTextBlocks class有可能需要知道告诉缓存(cache)文本区块的长度以便应付询问:
class CTextBlocks{
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()函数去改变const对象的 textLength变量和lengthIsValid,这两个数据的被修改对const CTextBlock对象是可以接受的(符合需求),但编译器不同意。
利用C++关键字mutable(可变的),让const对象里的某些需要改变的值得以在const成员函数里面改变。
class CTextBlocks{
public:
...
std::size_t length() const;
private:
char* pText;
mutable std::size_t textLength; //这些成员函数变量可能总是会被更改,即使在const成员函数内
mutable bool lengthIsValid;
}
我的理解是,对于编译器来说没有任何函数能够改变const对象的任何一个bit,但实际来需求来说会有一些记录变量或者状态变量的变化需要记录下来(例如计数),所以mutable关键字就是解决这部分的。
还有一个const难题就是,往往很多const和non-const的重载成员函数代码是几乎一致的,若某对const和non-const成员函数代码量太大,这将造成不必要空间和时间上的浪费。
例如一个operator[]需要实现返回一个reference指向某字符、执行边界检验、日志访问信息、数据完整性检验,这将是一对怪物。
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;
}
将这些相同的所有代码移到另一个成员函数并令两个版本的operator[]调用它,是可以的,但还是重复了一些代码,例如函数调用、两次return语句等。
真正该做的是实现编写operator[]的代码一次并使用它两次。即必须令其中一个调用另一个。
我们打算让non-const operator[]调用其const兄弟
class TextBlock{
public:
...
const char& operator[](std::size_t position)const
{
...
return text[position];
}
char& operator[](std::size_t position)
{
return
const_cast<char&>( //将const operator[]返回值的const移除
static_cast<const TextBlock>(*this) //为*this加上const
[position] //调用const operator[]
);
}
}
...
}
上述代码中有两个转型动作,一是将*this从其原始类型TextBlock& 转型为const TextBlock&,我们使用转型操作让它加上来了const!
在这我们使用了static_cast。
二是则是从const operator[]的返回值中移除const,在这我们使用了const_cast。
虽然语法上有点奇怪,不够美观,但避免了代码重复。为了到达这个目标而写出如此难看的代码是否值得,只能视情况而定。
而令const版本调用non-const版本这显然是不合理的。
总结:
-
将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体
-
编译器強制实施bitwise constness(const成员函数不更改对象内的任何一个bit),但你编写程序时应该使用“概念上的常量性”(conceptual constness)
-
当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复
于03/24/2019.