1. 让自己习惯C++
条款1:视C++为一个语言联邦
今天的C++已经是多个多重范型编程语言,一个同时支持过程形式、面向对象形式、函数形式、泛型形式、元编程形式的语言。
条款2:尽量以const、enum、inline替换#define
1)#define 修饰的记号,在预处理的时候,已经全部被替换成了某个数值,如果出错,错误信息可能会提到这个数值,而不会提到这个记号。在纠错方面很花时间,因为其他程序员不知道这个数值代表什么。我们可以用 const 和 enum 解决这个问题。
#define ASPECT_RATION 1.653 --> const double Aspect_ration = 1.653
作为一个常量语言,AspectRation肯定会被编译器看到
2)无法利用#define创建一个class专属常量,因为#define并不注重作用域。一旦宏被定义,它在其后的编译过程中有效。这意味着#define不仅不能用来定义class专属常量,也不能够提供任何封装性。
2)通过使用内联函数模板,可以获得宏的所有效率,以及普通函数的所有可预测行为和类型安全。
#define MY_COMPARE(a, b) f((a) > (b) ? (a) : (b))
//这是一个三目运算符,如果 a > b,则返回 a,否则返回 b
//宏中的每一个实参都必须加上小括号
//调用:
int a = 5, b = 0;
MY_COMPARE(++a, b);//1
MY_COMPARE(++a, b + 10);//2
/*
1式中,++a => a = 6 => 6 > b = 0 => return ++a;
a 的值竟然增加了两次!
*/
//定义 inline:
#define MY_MAX(a, b) (a) > (b) ? (a) : (b)
template<class T>
inline int MY_COMPARE(const T&a, const T&b)
{
a > b ? a : b;
}
//inline 将函数调用变成函数本体
//传入的是 ++a 的值
- 对于简单常量,首选const对象或枚举,而不是#define
- 对于类似函数的宏,优先选择内联函数
条款03:尽可能使用 const
const对指针的使用
const出现在 * 左侧:表示被指物是常量(所指向的内容不能改变)
const出现在 * 右侧:指针本身是常量 (指针不能再重新指向其他内容)
char greeting[] = "hello";
char * p =greeting; //non-const指针、non-const数据
const char* p = greeting;||char const* p = greeting; //non-const指针,const数据
char* const p = greeting; //const指针,non-const数据
const char* const p =greeting; //const指针,const数据
const对迭代器的使用
const vector<int>::iterator iter = vec.begin();
*iter = 10; //正确
++iter; //错误,iter本身是const
vector<int>:: const_iterator iter=vec.begin();
*iter=10; //错误
++iter;//正确
在成员函数上使用const
- 使类的接口意图更明确。知道哪些函数可以修改一个对象,哪些不能。
- 使得使用const对象成为可能
两个成员函数如果只是常量性不同,可以被重载。
class TextBlock {
public:
TextBlock(string s) {
text = s;
}
const char& operator[](size_t position) const {
cout << "const版本:";
return text[position];
}
char& operator[](size_t position) {
return text[position];
}
private:
string text;
};
int main() {
TextBlock tb("hello");
cout << tb[0] << endl;
const TextBlock ctb("world");
cout << ctb[0] << endl;
}
结果:
h
const版本:w
-----------------------------------------------------------------
void print(const TextBlock& ctb){
cout<<ctb[0]<<endl; //不论传入的实参是否为const,最终都会调用const
}
int main() {
TextBlock tb("hello");
const TextBlock ctb("world");
print(tb);
print(ctb);
}
结果:
const版本:h
const版本:w
--------------------------------------------------------------------
tb[0] = 'x'; //正确,写一个non-const TextBlock
ctb[0] = 'x'; //错误,写一个const TextBlock
编译器对待const对象的态度通常是 bitwise constness,而我们在编写程序时通常采用 logical constness,这就意味着,在确保客户端不会察觉的情况下,我们认为const对象中的某些成员变量应当是允许被改变的,使用关键字mutable来标记这些成员变量:
C++中mutable是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中。
C++中如果类的成语函数不会改变对象的状态,那么这个成员函数一般会声明成const的。但是有些时候,我们需要在const的函数里面修改一些跟类状态无关的数据成员,那么这个数据成员应该被mutable来修饰。
class CTextBlock {
public:
std::size_t Length() const;
private:
char* pText;
mutable std::size_t textLength;
mutable bool lengthIsValid;
};
std::size_t CTextBlock::Length() const {
if (!lengthIsValid) {
textLength = std::strlen(pText); // 可以修改mutable成员变量
lengthIsValid = true; // 可以修改mutable成员变量
}
return textLength;
}
在重载const和non-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&>(static_cast<const TextBlock&>(*this)[position]);
}
private:
std::string text;
};
需要注意的是,反向做法:令const版本调用non-const版本以避免重复——并不被建议,一般而言const版本的限制比non-const版本的限制更多,因此这样做会带来风险。
- 将某些东西声明为const可帮助编译器侦测出错误用法。const可施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
- 编译器强制实行bitwise constness,但你别写程序应该使用概念上的常量性。
- 当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。
条款4:确保对象在使用之前被初始化
永远在使用对象之前将其初始化。对于无任何成员内的内置类型,必须手工完成
int x = 0;
const char* text = "A C-style string";
double d;
cin>>d;
对于内置类型以外的任何其他东西,初始化则由构造函数完成。确保每一个构造函数都将对象的每一个成员初始化。
构造函数比较佳的写法是,使用所谓的member initialization list替换赋值动作
class ABEntry {
public:
ABEntry(const string& name, const string& address, const list<PhoneNumber>& phones);
private:
string theName;
string theAddress;
list< PhoneNumber> thePhones;
int numTimesConsulted;
};
这个版本的构造函数首先调用default构造函数设初值,然后立即再对它们赋予新值。default构造函数的一切因此浪费了
ABEntry::ABEntry(const string& name, const string& address, const list<PhoneNumber>& phones) {
//这些都是赋值而非初始化
theName = name;
theAddress = address;
thePhones = phones;
numTimesConsulted = 0;
}
最好写成以下形式即member initialization list替换赋值动作,效率更高。
ABEntry::ABEntry(const string& name, const string& address, const list<PhoneNumber>& phones):
theName(name),
theAddress(address),
thePhones(phones),
numTimesConsulted(0){}
成员初始化列表也可以留空用来执行默认构造函数:
ABEntry::ABEntry()
: theName(),
theAddress(),
thePhones(),
numTimesConsulted(0) {}
静态对象的初始化
函数内的static对象称为local static对象,其他static对象称为non-local static对象。static对象的析构函数会在main()结束时自动调用。
C++ 对于定义于不同编译单元内的全局静态对象的初始化相对次序并无明确定义,因此,以下代码可能会出现使用未初始化静态对象的情况:
// File 1
extern FileSystem tfs;
// File 2
class Directory {
public:
Directory() {
FileSystem disk = tfs;
}
};
Directory tempDir;
在上面这个例子中,你无法确保位于不同编译单元内的tfs一定在tempDir之前初始化完成。
这个问题的一个有效解决方案是采用 Meyers’ singleton,将全局静态对象转化为局部静态对象:
FileSystem& tfs() {
static FileSystem fs;
return fs;
}
Directory& tempDir() {
static Directory td;
return td;
}
- 为内置型对象进行手工初始化,因为C++不保证初始化它们
- 构造函数最好使用成员初始列,而不要在构造函数本体内使用赋值操作。初值列列出的成员变量,其排列次序应该和他们在class中的声明次序相同。
- 为免除“跨编译单元值初始化次序”问题,就以local static 替换 non-local static对象