01 视C++为一个语言联邦
- 一开始C++只是C加上OOP特性,但随着C++成熟就不再只是C with classes,如今C++是一个多重范型编程语言,同时支持过程形式、面向对象形式、函数形式、泛型形式、元编程形式,因此应当将C++视为一个由相关语言组成的联邦而非单一语言,在其某个次语言中,各种守则倾向简单易懂
- C++有四个次语言
- C:C++的基础,区块、语句、预处理器、内置类型、数组、指针都来源于C,C的局限在于没有模板、没有异常、没有重载......
- Object-Oriented C++:C with Classes所诉求的部分,主要涉及概念有类(构造函数、析构函数)、封装、继承、多态、虚函数(动态绑定)......
- Template C++:C++的泛型编程部分,大多数程序员经验最少的部分,它带来了崭新的编程范性,即template metaprogramming(TMP模板元编程)
- STL:STL是个template程序库,对容器、迭代器、算法、函数对象的规约有极佳的紧密配合与协调
02 尽量以const,enum,inline替换#define
#define ASPECT_RATIO 1.653
- 编译器看不到ASPECT_RATIO,#define在预处理期间替换字符,此时如果用此常量获得一个错误信息,提到的是1.653而非ASPECT_RATIO,你会不知道这个1.653是怎么来的从而浪费时间去追踪它,改用const就不会出现这样的情况
const double Aspectratio = 1.653; // 大写名称通常用于宏,这里改写法
- 以常量替换#define时有两种情况,一是定义const pointer,常量定义式通常放在头文件里,所以有必要将指针(而不是指针所指之物)声明为const,例如定义一个char*字符串必须const两次
const char* const authorName = “Scott”;
- 不过string对象通常比char*合适,所以上述定义这样写更好
const std::string authorName("Scott");
- 第二种情况是class中的常量,为确保此常量至多只有一个实体,必须让它成为static成员
class Gameplayer {
private:
static const int NumTurns = 5; // 常量声明式
int scores[NumTurns]; // 使用此常量
...
};
// 上述是声明式而非定义式,如果编译器要看到一个定义式,这样写
const int Gameplayer::NumTurns; // 不用赋值,因为声明时已经有了初值
// 如果编译器较老不允许static成员在声明式上获得初值,可以将初值放在定义式
class CostEstimate {
private:
static const double FudgeFactor; // 常量声明位于头文件内
```
};
const double CostEstima::FudgeFactor = 1.35; // 位于实现文件内
- 如果编译期间需要class常量值,如上述数组声明式中,可以用"the enum hack"补偿做法
class GamePlayer {
private:
enum { NumTurns = 5; }
int scores[NumTurns];
...
};
- 取enum地址是不合法的,如果不想让别人获得一个pointer或reference指向你的某个int常量,enum可以实现此约束。enum被许多代码用到,"enum hack"是TMP的基础技术
- 宏看起来像函数而没有函数的额外开销,但容易造成奇怪的错误
// 对a和b的较大值调用f
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
int a = 5, b = 0;
CALL_WITH_MAX(++a, b); // a累加2次
CALL_WITH_MAX(++a, b + 10); // a累加1次
- 所以不必纠结于#define,用template inline函数可以同时获得宏的效率和一般函数的可预料行为及类型安全性
template <typename T>
inline void callWithMax(const T& a, const T& b)
{
f(a > b ? a : b);
}
- const、enum、inline可以降低对预处理器的需求,但#include仍然是必需品,#ifdef/ifndef也扮演着控制编译的重要角色。对于单纯常量,用const或enum替换#define,对形似函数的宏则用inline替换
03 尽可能使用const
void f1(const Widget* pw);
void f2(Widget const* pw);
- 如果希望迭代器类似于const pointer则声明为const,如果希望迭代器类似pointer to const则使用const_iterator
std::vector<int> v{1, 2};
const std::vector<int>::iterator it = v.begin();
*it = 10; // 正确
++it; // 错误
std::vector<int>::const_iterator it2 = v.begin(); // 相当于v.begin()
*it2 = 10; // 错误
++it2; // 正确
- const最大的作用是函数声明返回const,这样可以减少因客户错误造成的意外,而又不至于放弃安全性和高效性,比如有理数的operator*声明式
class Rational { ... };
const Rational operator*(const Rational& lhs, const Rational& rhs);
// 为什么要用const,往下看
Rational a, b, c;
...
(a*b) = c;
// 在a * b的结果上再赋值,虽然是很明显的错但很容易无意识造成,例如
// if(a * b = c)
- 函数形参为pointer to const或reference to const也能造成重载,在类中,把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);
lengthValid = true;
}
return textLength;
}
- 在const和non-const成员函数中避免代码重复,做法是先写const版本,然后在non-const版本中对其调用,返回类型再用const_cast去const
// 注意形参类型是指针或引用,const才构成重载
const string& shorterString(const string& s1, const string& s2)
{
return s1.size() <= s2.size() ? s1 : s2; // return的是常量
// 注意,不要返回局部对象的引用或指针!
}
string& shorterString(string &s1, string &s2)
{
auto& r = shorterString(const_cast<const string&>(s1),
const_cast<const string&>(s2));
return const_cast<string&>(r);
}
shorterString(s1, s2);
// 如果s1或s2中至少有一个常量,则只调用第一个函数
// 如果都是非常量则调用第二个,第二个再调用第一个
// 先把非常量转为常量形参,再把返回的常量转为非常量
04 确定对象被使用前已被初始化
- 在使用对象之前将其初始化,对于内置类型手动完成,对于其他初始化责任落在构造函数身上
- 不要混淆赋值和初始化
class A{
public:
A(const std::string& s, const std::list<int> n);
private:
std::string name;
std::list<int> number;
int x;
};
A::A(const std::string& s, const std::list<int> l)
{
name = s; // 这些都是赋值而非初始化
number = n;
x = 0;
}
- C++规定,初始化发生在进入构造函数体之前,上述只是赋值,初始化发生在默认构造函数被自动调用之时,但对内置类型x来说并不保证在赋值前获得初值。构造函数较好的写法是用成员初值列,这样省去了调用默认构造函数的过程,效率更高。对内置类型来说,初始化和赋值的成本相同,但为了一致性最好也通过成员初值列来初始化
A::A(const std::string& s, const std::list<int> l)
:name(s),
number(n),
x(0)
{}
- 如果想通过default构造成员变量也可以用成员初值列
A::A()
:name(),
number(),
x(0)
{}
- static对象包括global对象、namespace内的对象、类,函数,file作用域内被声明为static的对象,函数内的static称为local static对象(因为它们对函数而言是local),其他static称为non-local static对象,static对象在main函数结束时销毁
- 编译单元:产出单一目标文件的源码
- 成员初始化次序是固定的,但对不同编译单元的non-local static对象初始化次序没有明确定义,如一个编译单元的某个non-local static对象初始化用了另一编译单元的某个non-local static对象,此时后者可能还未初始化。解决此问题的一个小设计是,将每个non-local static对象写到一个inline函数内,该对象在此函数内声明为static,函数返回一个指向该对象的引用,用户调用这些函数而不直接指涉这些对象,这是Singleton模式的一个常见实现手法,但从另一角度看,这些函数内含static对象会使它们在多线程中带有不确定性
class Singleton {
public:
static Singleton& Instance()
{
static Singleton instance;
return instance;
}
private:
Singleton();
~Singleton();
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
};
网友评论