C++
为一个语言联邦const
,enum
,inline
替换#define
"宁可以编译器替换预处理器",使用#define
那么在编译器之前预处理器已经完成了替换,记号名称没有进入记号表(symbol table
),所以在调试或者编译错误的时候会莫名其妙。以常量替换宏是个好主意:const double AspectRatio = 1.653
替换#define ASPECT_RATIO 1.653
。
如果你不想让别人获得一个指针或者引用指向你的某个整数常量,enum
可以实现这个约束。
-#define CALL_WITH_MAX(a,b) f((a)>(b) ? (a):(b))
这种长相的宏,看着就头疼。可使用template inline
替换:
template<typename T>
inline void callWithMax(const T &a, const T &b){
f(a > b ? a : b)
}
对于单纯常量,最好以
const
对象或enum
替换#define
。
对于形似函数的宏,最好使用inline
函数替换#defines
。
const
const
修饰左侧的对象,如果关键字const
出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。
STL
迭代器是以指针为根据塑模出来的。所以迭代器的作用像是个T *
指针。声明迭代器为const
就像声明指针为const
一样(T * const
),表示这个迭代器不能指向不同的东西(位置),但是它指向的东西是可以改变的。如果你希望迭代器所指的东西不能改变(const T *
),需要使用const_iterator
。这两者是有差别的,比较如下:
vector<int> vec;
const vector<int>::iterator iter = vec.begin(); // T* const
*iter = 10; //没问题
++iter; //错误,不能改变指针的值
vector<int>::const_iterator cIter = vec.begin(); // const T*
*cIter = 10; //错误,值不能改变
++cIter; //没问题
const
最有威力的地方是面对函数声明时的应用。可以和返回值、参数函数自身产生关联。
const Rational operator*(const Rational &lhs, const Rational &rhs);
可以避免if(a * b = c)
的发生。
const
成员函数 将const
实施于成员函数的目的,是为了确认该成员函数可作用于const
对象身上。有两个原因:(1)得知那个函数可以改动对象哪个不能,这很重要。(2)这使得操作const
对象成为可能。根据参数的不同属性调用不同的版本。
const char & operator[](size_t position) const
{ ...; ...; ...; return text[position]; }
char & operator[](size_t position )
{ ...; ...; ...; return text[position]; }
void print(const TextBlock & ctb)
{
cout << ctb[0]; //会根据ctb的常量属性调用不同的函数
}
两个成员函数如果只是常量性(函数声明参数列表后有无
const
)不同,可以被重载。
mutable
释放掉non-static
成员变量的不可改变约束,这些变量可能总是会被改变,即使在const
成员函数内。因此,在const
成员函数内可以改变这些值,不会引起错误。对哪些变量使用mutable
是要有很好的估计的,因为它能突破很多限制。
在上面的代码中,两个版本的operator[]
除了返回值和常量属性不同外别无区别,存在着冗余和代码膨胀。有两个方法做的更好:(1)提取公共部分作为工具函数。(2)转型。显然,第一种方法仍不能根本上较少代码膨胀。
char & operator[](size_t position )
{
//转型是安全的
return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
}
我们做了两次转型,第一次是用来为*this
添加const
(这使接下来调用operator[]
时得以调用const
版本,避免对自己的递归调用),第二次是从const operator[]
的返回值中移除const
。
这样,我们就能使用常量版本实现非常量版本,实现‘避免代码重复’。反过来是不行的,因为不能在常量版本中改变值。
除了内置类型,初始化的责任落在构造函数身上。规则很简单:确保每一个构造函数都对对象的每一个成员初始化。
注意的是:区分构造函数初始化和赋值的不同。在构造函数大括号中写的是赋值,因为初始化发生在进入大括号之前。因此使用初始化列表才是真正的初始化。虽然它们的结果相同,但是效率更高(赋值版本有发生一次构造一次赋值)。
总是使用初始值列表。
在初始时列表中,有着固定的初始化顺序:基类早于派生类,类的成员变量以生命顺序初始化(即使在列表中以不同次序出现)。