标签: effective C++
Item1 : 视C++为一个语言联邦
- C
- Object-Oriented C++
- Template C++
- STL
C++可视为4个次级语言的组合,当你从次级语言中切换,高效编程守则会要求你改变策略。
比如:
在基于C的设计中,内置类型通常pass-by-value比pass-by-reference高效;
而在O-O设计中,由于用户自定义构造函数、析构函数存在,使用 pass-by-reference-const更高效;
运用Template C++时更是如此,此时你甚至不知道所处理对象的类型;
一旦跨入STL,pass-by-value又变得适用,因为STL的迭代器和函数对象是在C指针上塑造的。
Item2 : 尽量以const, enum, inline 替换 #define
#define
将标识符定义为宏,预处理阶段完成文本宏替换。由预处理器完成而不是编译器,这个条款意味着:宁愿以编译器替换预处理器
不利于调试
#define ASPECT_RATIO 1.653
ASPECT_RATIO = 1.5;
老编译器不会记录宏替换信息,如果ASPECT_RATIO编译出错,错误信息提示会是1.653而让你迷惑,当然,现代编译器错误信息会记录宏替换信息,告诉你错误信息关于ASPECT_RATIO
容易出错
#define MAX(a,b) a>b?a:b
3 * MAX(5,6) //结果是5而不是18 --> 3 * 5>6?5:6
#define DIVIDE(a,b) a/b
DIVIDE(3+5,2) //结果是5而不是4 --> 3+5/2
//正确行为
#define MAX2(a, b) ((a) > (b) ? (a) : (b))
然而,除了需要加括号,#define
还有别的问题
#define MAX(a, b) (a) > (b) ? (a) : (b)
MAX(i++,j) //i自增次数取决于j,调用者知道吗?
enum hack
当我们需要使用class专属常量,enum hack可以作为static const
的替代方案(int)。
class A
{
private:
static const double value;
};
const double A::value = 1.1;
class B
{
private:
enum { value = 5};
int scores[value];
};
此外,取 const
地址合法,而取 enum
地址不合法,取 #define
的地址也不合法,从这点上看,enum
更像 #define
#define TIME 5
enum {TIME2 = 5};
const int TIME3 = 5;
static const int TIME4 = 5;
总结:
- 对于单纯常量,最好以
const
对象或enums
替换#define
- 对于形似函数的宏,最好用inline函数替换
#define
Item3 : 尽可能的使用const
const
作为一种约束,能区分是否可以改变对象,防止客户错误使用你的代码。
常量的声明
指针的声明:
char greeting[] = "Hello";
char* p = greeting; //non-const pointer, non-const data
const char* p = greeting; //non-const pointer, const data
char* const p = greeting; //const pointer, non-const data
const char* const p = greeting; //const pointer, const data
const
出现在 *
的左边则被指向的对象是常量,出现在 *
右边则指针本身是常量。
const
出现在类型的左边和右边则是等效的
void f1(const Widget* pw);
void f2(Widget const * pw);
STL迭代器以指针为根基模塑而来,迭代器的作用就像T* 指针,如果希望迭代器所指data不可变,需要 const_iterator
std::vector<int> vec;
//iter like T* const
const std::vector<int>::iterator iter = vec.begin();
*iter = 10; //ok, change what iter point to
++iter; //error, iter is const
//cIter like const T*
std::vector<int>::const_iterator cIter = vec.begin();
*cIter = 10; //error, *cIter is const
++cIter; //ok, change cIter
对于返回值,声明为 const
也能阻止意外的发生
//对两个有理数的乘积不需要接受赋值
const Rational operator*(const Rational& lhs, const Rational& rhs);
//避免用户使用出现这样的暴力行为,编译器会阻止
Rational a, b, c;
if (a * b = c) {
}
对指针、迭代器、参数、返回值 都可以尽量声明为 const
const 成员函数
const
实施于成员函数,有个明显的好处: 使class接口容易理解,得知哪个函数可以改动对象,哪个不可以。
成员方法添加常量限定符属于函数重载,因此在类外实现需要加 const
关键字。
常量对象只能调用常量方法,非常量对象优先调用非常量方法,如不存在非常量方法会调用同名常量方法。
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;
};
TextBlock tb("hello");
const TextBlock ctb("world");
std::cout << tb[0]; //ok read non-const
std::cout << ctb[0]; //ok read const
tb[0] = 'x'; //ok write non-const
ctb[0] = 'x'; //error wirte const
比特常量(bitwise const):成员函数只有在不更改对象之任何成员变量( staic
除外)时才可以说是 const
。也即是说,const
成员函数不能修改非常量数据成员。
比特常量是C++对常量性的定义,然而一个满足比特常量性的方法,表现的却可能不像 const
,它确实满足bitwise特性,但是它结果是反直观的:它改变指针所指的值
class TextBlock{
char* text;
public:
char& operator[](int pos) const{
return text[pos];
}
};
const TextBlock tb;
char *p = &tb[1];
*p = 'a';
这种错误是不该有的,因为创建一个常量对象,而且只对它调用 const
成员函数,却改变了它的值(尽管对象的bit没有改变,但所指的值变了)
这种情况产生了:
- 对非常量静态数据成员(non-const data)无法修改
- 对指针成员(non-const data pointer)所指数据却可以修改
于是导出另一种观点,const
成员函数可以修改它所处理的对象内某些bits,但是只有在客户端侦测不出的情况下才可以如此。这观点叫逻辑常量(logical constness)
常量成员函数直接修改数据成员C++编译器不会同意,这时候使用 mutable
释放 non-static
成员变量的bitwise-constness
class CTextBlock {
public:
std::size_t length() const;
private:
char *pText;
mutable std::size_t textLength; // these data members may
mutable bool lengthIsValid; // always be modified
};
std::size_t CTextBlock::length() const{
if (!lengthIsValid) {
textLength = std::strlen(pText);
lengthIsValid = true;
}
return textLength;
}
在const和non-const成员函数种避免重复
通常我们会成对的定义常量成员函数和普通成员函数,可能这两个函数仅仅是返回值的不同,这样写两个相同的逻辑并不是我们想要的。const
成员函数并不允许调用 non-const
成员函数,因此只能反之
const char& operator[](size_t pos) const{
...
}
char& operator[](size_t pos){
return const_cast<char&>(
static_cast<const TextBlock&>(*this)
[pos]
);
}
我们要调用 const
方法,因此需要将普通对象转为 const
对象,当然使用 static_cast<>
返回需要去掉 const
属性,调用 const_cast
注意返回类型是引用,类型转化成什么是需要考虑的
Item4 : 确定对象被使用前已先被初始化
读取未初始化的变量会导致不明确的行为,因此在变量使用前将其初始化是必要的,在构造对象时初始化是一个好习惯。
- C++的C部分不保证内容初始化 array,而STL部分却能保证std::vector初始化。
- 了解定义时默认值是有必要的,比如全局变量、局部变量未初始化值。
- 最佳的处理方法是:在对象使用前初始化,对于内置类型,手动初始化,对于其它,初始化靠构造函数,请在构造函数初始化每一个变量。
- 构造函数初始化是在初始化列表进行的,而不是构造函数内部进行赋值。
const
成员变量、引用类型成员变量初始化在初始化列表进行。- 同时,应该知道的是初始化顺序,以免出现意料之外的结果,类的初始化顺序和成员声明顺序一致,而不是初始化列表顺序,而基类又总在派生类前初始化。
最佳实践:以local static 替代 non-local-static
//file1.cc
extern int i = 1;
//file2.cc
int j = i + 1;
实际上无法保证哪个先进行了初始化,方法是写到函数中,以 local static
替换
class Ex
{
public:
static int getI() {
static int i = 2;
return i;
}
private:
};
//file2.cc
int j = Ex::getI() + 1;
这类似单例模式的实现方式:懒汉单例——局部静态变量
注:该方法是线程安全的
class Singleton
{
public:
~Singleton() {}
Singleton(const Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
static Singleton& Singleton::getInstance()
{
static Singleton instance;
return instance;
}
private:
Singleton() {}
};