- 1.视 C++ 为一个语言联邦
- 2.尽量以 const, enum, inline 替换 #define
- 3.尽可能使用 const
- 4.确定对象被使用前先已被初始化
- 5. 了解 C++ 默认编写并调用哪些函数
1. 视 C++ 为一个语言联邦
- C++高效编程守则视情况而定,取决于你使用 C++ 的哪一个部分。
分为四个部分, 如下:
- C
C 基础。 - Object-Oriented C++
C++ 面向对象编程基础, 内含类的封装、继承、多态等等。 - Template C++
C++ 泛型编程,获取经验最少的一部分。 - STL
STL 为一个 template 程序库, 内含容器、迭代器、算法以及函数对象等丰富内容。
2. 尽量以 const, enum, inline 替换 #define
- 对于单纯常量,最好以 const 对象或 enum 替换 #define
- 对于宏函数,最好改用 inline 函数替换 #define
-
const 可以进行类型检查,相比于 #define 更安全。
-
#define 无法为一个类定义专属常量,一旦被定义,无视作用域。
-
如果你不想让别人获取变量指针或引用,使用enum避免非必要的内存分配和间接篡改。
-
定义 #define 宏函数已出现未知错误, 例如:
template<typename T> void f(const T& max) { std::cout << "调用f(max)" << std::endl; } #define CALL_WITH_MAX(a, b) f( (a) > (b) ? (a) : (b) ) int a = 5, b = 0; CALL_WITH_MAX(++a, b); //a 被累加两次
这种情况可以通过 template inline 函数解决
template<typename T> inline void CallWithMax(const T& a, const T& b) { f( a > b ? a : b); //T 为内置类型,否则 T 类型重载运算符 > 才能编译通过 } struct Grade { int score; //分数 std::string name; //学科 bool operator>(const Test& rhs) { return this->score > rhs.score; } }
3. 尽可能使用 const
- 将某些东西声明为 const 可帮助编译器侦测除错误用法。 const 可被施加于任何作用域的对象、函数参数、函数返回类型、成员函数本体(1、2、3.)。
- 编译器强制实施 bitwise constness, 但你编写程序时应该使用 ”概念上的常量“ (conceptual constness),通过mutable打破 bitwise constness 的约束(4.)。
- 当 const 和 non-const 成员函数有着实质等价的实现时, 令 non-const 版本调用 const 版本可避免代码重复(5.)。
-
理解 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 //non-const pointer, non-const data的两种表现形式 const char* p = NULL; char const * p = NULL; //针对于迭代器 std::vector<int> vec; std::vector<int>::iterator iter = vec.begin(); // non-const iterator, non-const data const std::vector<int>::iterator iter = vec.begin(); // const iterator, non-const data *iter = 10; // ok ++iter; // error std::vector<int>::const_iterator iter = vec.begin(); //non-const iterator, const data *iter = 10; //error ++iter; //ok
-
函数中使用 const
// 令函数返回一个常量值, 往往可以降低因客户错误而造成的意外, 又不至于放弃安全性和高效性 class Rational { Rational(){} ~Rational(){} }; const Rational operator*(const Rational& lhs, const Rational& rhs); Rational a,b,c; (a*b) = c; //在 a * b 返回后调用operator= //如果在无意中如下这么做,会令你得不到预期的结果 if( a * b = c) //若 a * b = c 为非0,则条件也会成立,导致问题产生。若 operator* 返回值声明为 const ,编译器会阻止该表达式,解决此种问题 ;//TODO 条件成立,执行相应逻辑
-
const 成员函数
// const 用于成员函数的目的,为了确认该成员函数可作用与 const 对象身上。 const 函数带来两个好处: // 1、他们使 class 接口更容易被理解, 通过声明 const 函数, 可以得知哪个函数可以改动对象内容哪个函数不行。 // 2、他们使 ”操作 const 对象“ 成为可能。通过 const 重载函数, 使 const 对象相应的 const 函数 class TextBlock { public: TextBlock(std::string str) : text(str) { } const char& operator[](std::size_t position) const //const 函数 { return text[position];}//non-const 函数, 若返回 char 则编译不通过,报错为error C2440: 'initializing' : cannot convert from 'const char [2]' to 'int []', 即使 by value 返回对象能编译通过,也无法改变对象本身, 只是改变临时的副本。(见条款20) char& operator[](std::size_t position) { return text[position];} private: std::string text; }; TextBlock tb("Hello"); tb[0]; //调用 non-const 函数 tb[0] = '1'; //ok const TextBlock ctb("world"); ctb[0]; //调用 const 函数 ctb[0] = 'c'; //error //该用途在为 const 参数时, 更加真实。 void print(const TextBlock& ctb) { std::cout << ctb[0] << std::endl; //调用 const 函数 }
-
理解 bitwise constness(又称 physical constness ) 和 logical constness
-
bitwise const: 成员函数只有在不更改对象之任何成员变量(static除外)时才可以说是 const。也就是不能更改对象内的任何一个 bit。
// 许多函数虽然不足具备 const 性质却能通过 bitwise 测试。 class CTextBlock { public: CTextBlock(std::string str) : text(str) { } char& operator[](std::size_t position) const //bitwise const 声明,但其实不恰当 { return text[position];} private: char* pText; }; //上面的 operator[] 函数表面上满足了 bitwise const, 但是结果不尽人意 const CTextBlock cctb("hello"); char* pc = &cctb[0]; *pc = 'J'; //通过指针间接修改了对象内的内容,违反了 bitwise const 定义,引出了 logical constness
-
logical constness:一个 const 成员函数可以修改它所处理的对象内的某些 bits, 但只有在客户端侦测不出的情况下才得如此。
class CTextBlock { public: CTextBlock(std::string str) : text(str) { } std::size_t length() const { if(!lengthIsValid) { textLength = std::strlen(pText); //错误!在 const 成员函数中不能赋值给textLength和lengthIsValid lengthIsValid = true; } } private: char* pText; std::size_t textLength; bool lengthIsValid; }; //利用 C++ 的一个 const 相关的摆动场:mutable(可变的)。mutable 释放掉 non-static 成员变量的 bitwise constness 约束。 class CTextBlock { public: CTextBlock(std::string str) : text(str) { } std::size_t length() const { if(!lengthIsValid) { textLength = std::strlen(pText); //可正常访问 lengthIsValid = true; } } private: char* pText; mutable std::size_t textLength; mutable bool lengthIsValid; };
-
-
在 const 和 non-const 函数中避免重复
// const 函数和 non-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; }; // 非 const 函数使用转型调用 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; }; //理解两次转型 1、static_const 转型使 *this 从 non-const 到 const ,为了调用 operator[] const 版函数 2、const_cast 从const operator[]的返回值中移除 const //不建议 const 函数调用 non-const 函数避免代码重复 1、const 成员函数绝不改变其对象的逻辑状态, non-const 函数没有这般承诺。如果在 const 函数中调用 non-const 函数,就是冒风险: non-const 函数可能会改变对象内容,违反了 const 函数本身的职责。
4.确定对象被使用前先已被初始化
- 为内置型对象进行手工初始化,因为 C++ 不保证初始化他们。
- 构造函数最好使用成员初始值(member initialization list),而不要在构造函数本体内使用赋值操作。 初值列列出的成员变量, 其排列次序应该和他们在 class 中的生命次序相同。
- 为免除 ”跨编译单元之初始化次序“ 问题,请以 local static 对象替换 non-local static 对象。
-
区分赋值和初始化
class PhoneNumber{}; , //版本1 class ABEntry { public: ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones) { //如下均为赋值而非初始化 m_name_ = name; m_address_ = address; m_phones = phones; numTimesConsultes = 0; } private: std::string m_name_; std::string m_address_; std::list<PhoneNumber> m_phones_; int numTimesConsulted; }; //版本2 C++ 对象的成员变量初始化动作发生在进入构造函数之前。 使用成员初值列替换赋值动作: class ABEntry { public: ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones) : m_name_(name), //使用对应的拷贝构造函数进行初始化 m_address_(address), m_phones_(phones), numTimesConsulted(0) { } private: std::string m_name_; std::string m_address_; std::list<PhoneNumber> m_phones_; int numTimesConsulted; }; //版本1和版本2最终结果相同, 但通常版本2效率比版本1要高。因为版本1首先为m_name_, m_address_和m_phones设置初值,然后再进行赋值。而版本2只需要进行一次拷贝构造函数。 //结论:对于内置类型,其初始化和赋值成本相同,对于其他大多数类型而言,一次拷贝构造函数比先调用默认构造函数再调用赋值操作符高效。 //对于成员变量是 const 和 references 时,他们必须设置初值不能赋值, 因为会导致与预期不一样的结果。
5. 了解 C++ 默认编写并调用哪些函数
-
编译器可以暗自为 class 创建 default 构造函数、copy 构造函数、copy assignment 操作符, 以及析构函数。
//创建空类 class Empty { }; //空类等价于如下类 class Empty { public: Empty(){} //默认生成无参构造函数 ~Empty(){} //默认生成析构函数 Empty(const Empty& rhs){} //默认生成拷贝构造函数 Empty& operator=(const Empty& rhs){ return *this;} //默认生成赋值操作符 }; Empty e; //调用默认构造函数 Empty e1(e); //调用拷贝构造函数 Empty e1 = e; //调用拷贝构造函数 e2 = e1; // 调用赋值操作符 //1、若类中声明了有参构造函数, 则编译器不帮助生成默认构造函数, 不影响生成拷贝构造和赋值操作符。 template <typename T> class NamedObjected { public: NamedObjected(std::string& name, const T& value) : name_value(name), object_value(value) { } private: std::string& name_value; const T object_value; }; std::string s; NamedObjected<int> e(s, 1); //ok NamedObjected<int> e2(e); //ok e2 = e; //error //2、 如果类中含 const 或 引用成员变量,则编译器拒绝自动生成赋值操作符,因为违反了引用和 const 的不可变性。 //3、如果父类将赋值操作符声明为 private, 那么编译器也将拒绝为其派生类生成一个赋值操作符, 因为派生类的赋值操作符可以处理父类的赋值操作符(见条款12)。