让自己习惯C++
条款01: 视C++为一个语言联邦
C++主要的4个次语言
- C Blocks; statements; preprocessor; built-in data types; arrays; pointers; …
- Object-Oriented C++ Classes; encapsulation(封装); inheritance; polymorphism virtual; …
- Template C++ Generic programming
- STL Containers; iterators; algorithms; function objects; …
每个层次应该有自己的最佳实践。例如对于C层次,传入函数最佳的实践应该是传入值(pass-by-value),而不是指针(pass-by-reference),而对于C with classes层次,则以传递引用为最佳的实践。
条款02: 尽量以const
, enum
, inline
替代 #define
宏常量用全局的const
或者enum
来替换
当你在一个类内声明某变量,但你的编译器不允许在声明时赋值初始化,同时接下来的某个语句却需要用到这个变量的具体数值,例如:
int noPlayer;
int scores[noPlayer];
此时编译器便会报错,需要在编译期间noPlayer
有具体数值,这个时候就需要使用如下技巧:
enum {noPlayer = 5};
int scores[noPlayer];
这样编译器就能顺利通过,因为enum
可以被当成int
类型来使用 但注意enum
类型在内存中没有实体,无法取得enum
类型的地址,因此这个方法更相当于取一个本地的#define
数值
宏函数用inline
修饰的函数来替换
template<typename T>
inline void callWithMax(const T &a, const T &b)
{
a > b ? a : b;
}
//
// #define CALL_WITH_MAX(a, b) ((a) > (b) ? (a) : (b))
int main()
{
int a = 5, b = 0;
// a被累加几次取决于被拿来跟谁比较
// CALL_WITH_MAX(++a, b); // a被累加2次
// CALL_WITH_MAX(++a, b + 10); // a被累加1次
callWithMax(++a, b);
callWithMax(++a, b + 10);
return 0;
}
条款03: 尽可能使用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;
void f1(const Widget* pw);
void f2(Widget const* pw); // f1 == f2
const vector<int>::iterator iter; // iter == T* const
vector<int>::const_iterator citer; // citer == const T* (定义某迭代器指向一个常数)
令函数返回常量值往往可以降低客户错误而造成的意外,而又不至于放弃安全性和高效性。
class Rational{....};
Rational operator*(const Rational& lhs, const Rational& rhs){...}
Rational a,b,c;
if(a * b = c){......} //error
// 开头的 const 可以预防无意义的赋值动作(也就是防止将“==”写成“=”造成的错误)
const Rational operator*(const Rational& lhs, const Rational& rhs){...}
给成员函数使用const
关键字是非常重要的,它可以让接口更加直观,直接告诉用户这个函数是不是只读(Read only),会不会改变某变量。更重要的是,用const
修饰的对象只能调用const
修饰的成员函数,因为不被const
修饰的成员函数可能会修改其他成员数据,打破const
关键字的限制。因此,需要同时声明有const
和没有const
的成员函数,例如:
const char& operator[](size_t pos) const;
// 函数前const:普通函数或成员函数(非静态成员函数)前均可加const修饰,表示函数的返回值为const,不可修改。
// 函数后加const:只有类的非静态成员函数后可以加const修饰,表示该类的this指针为const类型,不能改变类的成员变量的值,即成员变量为read only
char& operator[](size_t pos);
下面代码中length()
函数要做某些错误检测,因此可能会修改成员数据。即使真正的功能核心只是返回字符长度,编译器依然认为你可能会修改某些成员数据而报错。
class Text{
public:
std::sizt_t length() const;
private:
char* pText;
std::size_t length;
bool lengthValid;
....
};
std::size_t Text::length() const{
if(!lengthValid){ //做某些错误检测
length = std::strlen(pText); //error! 在const成员函数中
lengthValid = true; //不能赋值给textLength
} //和lengthIsValid
return length;
}
逻辑常量性(Logical constness),即允许某些数据被修改,只要这些改动不会反映在外,例如,以上问题可以用mutable
关键字来解决。
mutable std::size_t length;
mutable bool lengthValid;
在定义常量与非常量成员函数时,避免代码重复
代码复制一遍,既不显得美观,又增加了代码维护的难度和编译时间。因此,我们可以使用非常量的函数来调用常量函数。(如果使用相反的方法,用const函数来调用non-const函数,就可能会有未知结果,因为这样相当于non-const函数接触到了const对象的数据,就可能导致常量数据被改变。)
const char& operator[](std::size_t pos) const{....}
char& operator[](std::size_t pos){
return
const_cast<char&>( //const_cast去掉const关键字,并转换为char&
static_cast<const Text&>(*this)[position]; //给当前变量加上const关键字,才可以调用const操作符
);
}
为了避免无限递归调用当前非常量的操作符,我们需要将(*this)
转换为const Text&
类型才能保证安全调用const
的操作符,最后去掉const
关键字再将其返回,巧妙避免了代码的大段复制。
条款04: 确定对象被使用前已被初始化
⚠️assignment和initialization的区别
对象的成员变量的初始化动作发生在进入构造函数本体之前。
class A {...};
class B {
private:
int x;
int y;
const std::list<double>& num;
public:
B(const int& _x, const int& _y, const std::list<double>& num;){
x = _x; // assignments (NOT initializations)
y = _y;
num = _num;
}
}
所以最好使用member initialization list替换赋值动作,结果相同但通常效率更高:
B::B(const int& _x, const int& _y, const std::list<double>& num;)
:x(_x),
y(_y),
num(_num)
{}
构造函数是可以被重载(overload)的,还需要一个没有参数输入的默认构造函数,可以定义:
B::B():x(0), y(0), num() {}
//num()调用了std::list<double>类型的默认构造函数
在定义引用(reference)和常量(const)时,不将其初始化会导致编译器报错
const int a; //error! 需要初始化!
int& b; //error! 需要初始化!
const int a = 3; //编译通过
int c = 3;
int& b = c; //编译通过!
C+成员初始化次序:
- base classes早于其derived classes被初始化;
- class的成员变量总是以其声明次序被初始化;
class myClass{
private:
int a;
int b;
int c;
public:
myClass(int _a, int _b, int _c);
};
//注意,即使初始化列表是c->b->a的顺序,真正的初始化顺序还是按照a->b->c
myClass::myClass(int _a, int _b, int _c): c(_c), a(_a), b(_b) {}
static: 寿命从被构造出来直到程序结束为止;
local static: 函数内生命的static对象;
non-local static: 其余static对象。
程序结束时static对象会被自动销毁,也就是他们的析构函数会在main()结束时被自动调用。
编译单元(translation unit): 可以让编译器生成代码的基本单元,一般一个源代码文件就是一个编译单元。
非本地静态对象(non-local static object): 静态对象可以是在全局范围定义的变量,在名空间范围定义的变量,函数范围内定义为static的变量,类的范围内定义为static的变量,而除了函数中的静态对象是本地的,其他都是非本地的。
C++对定义于不同编译单元内的non-local static对象的初始化次序无明确定义。
此外注意,静态对象存在于程序的开始到结束,所以它不是基于堆(heap)或者栈(stack)的。初始化的静态对象存在于.data中,未初始化的则存在于.bss中。
现在有一种特殊情况,尤其是在大型项目中比较普遍:在两个编译单元中,分别包含至少一个非本地静态对象,当这些对象发生互动时,它们的初始化顺序是不确定的,所以直接使用这些变量,就会给程序的运行带来风险。那如何初始化非本地静态对象?
解决方法: 将非本地的静态变量变为本地静态变量
使用一个函数,只用来定义一个本地静态变量并返回它的引用。因为C++规定在本地范围(函数范围)内定义某静态对象时,当此函数被调用,该静态变量一定会被初始化。
class Server{...};
Server& server(){ //将直接的声明改为一个函数
static Server server;
return server;
}
class Client{...};
Client::client(){ //客户端构造函数通过函数访问服务器数据
number = server().number;
}
Client& client(){ //同样将客户端的声明改为一个函数
static Client client; //定义并初始化local static对象
return client;//返回一个reference指向上述对象
}