一、让自己习惯C++
条款01:视 C++ 为一个语言联邦
C++由几个重要的次语言构成
C语言: 区块,语句,预处理器,数组,指针等等。
类: class,封装,继承,多态…(动态绑定等等)
模板: 涉及泛型编程,内置数种可供套用的函数或者类。
STL: STL是个模板库,主要涉及容器,算法和迭代器
在不同情况下使用适合的部分,可以使 C++ 实现高效编程
条款02:尽量以 const enum inline 替换 #define(宁可以编译器替换预处理器)
1、#define 修饰的记号,在预处理的时候,已经全部被替换成了某个数值,如果出错,错误信息可能会提到这个数值,而不会提到这个记号。在纠错方面很花时间,因为其他程序员不知道这个数值代表什么。我们可以用 const 和 enum 解决这个问题。
//enum hack 补偿做法: enum 枚举量{para1 = value1, para2 = value2,......} //将一个枚举类型的数值当作 int 类型使用 //和 #define 很像,都不能取地址,但它没有 #define 的缺点
2、#define 不能定义类的常量,因为被 #define 定义的常量可以被全局访问,它不能提供任何封装性。
3、#define 修饰的宏书写繁琐且容易出错,inline 函数可以避免这种情况:
#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 的值 int main() { int a = 2; int b = 2; MY_COMPARE(++a, b); cout << a << endl; //此时 a = 3 MY_MAX(++a, b); cout << a << endl; //此时 a = 5,因为 define 展开式 a 会执行两次 ++ system("pause"); return 0; }
条款03:尽可能使用 const
const 允许我们指定一个语义约束,使某个值应该保持不变。
1、const 修饰变量,指针,函数,函数返回值等,可以使程序减少错误,或者更容易检测错误:
- 指针常量: int* const p;//指针地址不可变,指针指向值可变
- 常量指针: const int* p;//指针指向值不可变,指针地址可变
- 常量指针常量: const int* const p;//都不可变
const 修饰迭代器:
- iterator 相当于 T* const //指针常量
- const_iterator 相当于 const T* //常量指针
const 修饰函数返回值:
const int max(int a, int b) { return a > b ? a : b; } int c = 6; max(a,b) = c; //将 c 的值赋给 max(a, b) 是没有意义的,const 防止这种操作的发生 if(max(a,b)=c) //很可能是少打了一个 = 号但编译器不会报错,但是加上 const 后就可以报错
2、const 修饰成员函数
如果两个成员函数只是常量性不同(其他相同)则可以发生重载:
- const 类对象调用 const 成员函数
- non-const 类对象调用普通成员函数
bitwise:
const 成员函数不能改变(除 static)成员变量的值,因为常函数里 this 指针指向的值不可改变。同理,const 对象不可以调用 non-const 函数,因为函数有能力更改成员属性的值。
但是若成员变量是一个指针,仅仅改变指针指向的值却不改变指针地址(地址是 this 指向的值),则不算是 const 函数,但能够通过 bitwise 测试。
使用 mutable 可以消除 non-static 成员变量的 bitwise constness 约束。
class person { public: person(int a) { m_id = a; } int& func() const { m_id = 888; } mutable int m_id; }; int main() { const person p(666); p.func(); cout << p.m_id << endl; system("pause"); return 0; }
3、当 const 和 non-const 成员函数有实质的等价实现时,利用两次转型,令 non-const 调用 const 可以避免代码重复。
const char& operator[](int pos) const { //... //... return name[pos]; } char& operator[](int pos) { return const_cast<char&>//移除第一次转型添加的 const ( static_cast<const classname>(*this)[pos] //把 classname 类型数据转换为 const classname //使得能够调用 const operator[] ); }
条款04:确定对象被使用前已先被初始化
1、内置数据类型:
int a = 0; double b = 0; char* c = "A C-style string";
2、自定义数据类型:
使用成员初值列替换有参构造,且次序和 class 声明次序相同:
class person { public: person(int age, int id, string name)//这是一个有参构造 { this->m_age = age; this->m_id = id; this->m_name = name; } int m_age; int m_id; string m_name; }; //这是成员初始列 class person { public: person(int age, int id, string name): m_age(age), m_id(id), m_name(name) {} int m_age; int m_id; string m_name; }; /* 成员初始列格式: classname(parameter1, parameter2,...): member1(value1), member2(value2), ... membern(value) {} */
有参构造是先执行默认构造,再给成员变量赋值。这有一个坏处,const 修饰的量不可以被赋值,但是可以初始化。而且其效率是不如初始化列表的,因为有参构造里的参数会先调用默认构造函数进行初始化,然后再通过传进来的参数进行赋值操作,而初始化列表则只进行了一次拷贝构造。
使用成员初值列则省去了默认构造部分。成员初值列在不传参的情况下,可以看成默认构造。
int m_size; char m_array; //这两个成员变量在初始化的时候是有次序的,m_array 的大小是由 m_size 指定的 //所以必须先初始化 m_size //实际上,成员初值列中变量的次序可以和声明次序不同 //但为防止这类错误出现,成员初值列的次序应当和声明次序相同
3、使用 local-static 对象替换 non-local-static 对象:
如果调用顺序不对,例如下面在类定义之前创建对象以及在类定义之后创建对象得到的结果是不一样的。
所以我们可以使用 local-static 对象来替换 non-local-static 对象,具体操作如下。
C++ 保证,local-static 对象在包含它的函数被调用期间(或者说首次遇到这个对象),会被初始化。也就是说,如果用返回引用的函数访问这个对象,就没有调用未初始化对象的困扰。