好的,这是一份关于《Effective C++》第一部分"让自己习惯C++"的读书总结要点:
第一部分:让自己习惯C++
条款01:视C++为一个语言联邦
- C++ 是多范式编程语言,可视为由多个"子语言"组成的联邦:
- C:C++ 的基础,包含基础语法、数据类型、预处理等。
- 面向对象 C++:类、封装、继承、多态、虚函数等。
- 模板 C++:泛型编程,模板元编程。
- STL:标准模板库,包含容器、迭代器、算法和函数对象。
- 要点:在不同"子语言"下切换时,可能需要遵循不同的编程规则。理解这些子语言及其交互是高效使用 C++ 的关键。
条款02:尽量以 const, enum, inline 替换 #define
-
问题 :宏 (
#define) 在预处理阶段进行简单文本替换,缺乏类型检查和作用域概念,可能导致难以理解的错误。 -
替代方案 :
-
常量定义 :用
const定义常量(全局或类内)。cppconst double Pi = 3.14159; // 优于 #define Pi 3.14159 -
类内常量 :对于类专属常量,可声明为
static const。cppclass Widget { private: static const int MinSize = 5; // 常量声明 }; -
枚举值 :当需要在类内定义一组相关整数常量时,可用
enum("the enum hack")。cppclass Widget { private: enum { MinSize = 5 }; // 编译器通常不分配内存 }; -
函数宏 :用
inline函数替换函数宏,以获得类型安全、参数计算、作用域和访问控制。cpptemplate<typename T> inline T max(const T& a, const T& b) { // 优于 #define max(a, b) ((a) > (b) ? (a) : (b)) return a > b ? a : b; }
-
-
要点 :优先使用语言层面的
const,enum,inline而非预处理器宏,以获得更好的安全性、可维护性和调试便利性。
条款03:尽可能使用 const
const的作用:表达语义约束("不该被改动"),编译器强制执行。- 应用场景 :
-
修饰变量/对象 :声明常量,防止修改。
cppconst int DaysInWeek = 7; const std::string authorName("Scott Meyers"); -
修饰指针 :
const char* p:指向常量的指针(指针可变,指向的内容不可变)。char* const p:常量指针(指针不可变,指向的内容可变)。const char* const p:指向常量的常量指针(指针和内容都不可变)。
-
修饰迭代器 :
std::vector<int>::const_iterator cit:指向常量的迭代器(类似const T*)。const std::vector<int>::iterator it:常量迭代器(类似T* const)。
-
修饰函数参数和返回值:防止传入的参数在函数内被修改,或防止函数返回值被意外修改(较少用)。
-
修饰成员函数 :
- 目的 :表明该成员函数不会修改对象的非静态成员变量(
mutable成员除外)。 - 语法 :在成员函数声明的参数列表后加上
const。
cppclass TextBlock { public: const char& operator[](std::size_t position) const { // 用于 const 对象 return text[position]; } char& operator[](std::size_t position) { // 用于 non-const 对象 return text[position]; } private: std::string text; };- 意义 :
const对象只能调用const成员函数。通过提供const和 non-const版本的重载成员函数,可以在const正确性(避免错误修改)和非const对象的使用便利性(允许修改)之间取得平衡。
- 目的 :表明该成员函数不会修改对象的非静态成员变量(
-
mutable:用于修饰成员变量,即使在const成员函数中,mutable成员也可以被修改。
-
- 要点 :大胆使用
const,它有助于编译器检测错误,增强代码可读性(表达设计意图),并支持const对象的使用。理解const在指针、迭代器和成员函数中的应用至关重要。
条款04:确定对象被使用前已先被初始化
-
问题:未初始化的对象(特别是内置类型)的值是未定义的,使用它们会导致未定义行为(UB)。
-
初始化方法 :
-
手动初始化内置类型 :在定义时赋值。
cppint x = 0; // 手动初始化 -
构造函数初始化列表 :类成员变量的初始化应通过构造函数初始化列表进行,而不是在构造函数体内赋值。
cppclass PhoneNumber { /* ... */ }; class ABEntry { public: ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones); private: std::string theName; std::string theAddress; std::list<PhoneNumber> thePhones; int numTimesConsulted; }; ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones) : theName(name), // 初始化列表 theAddress(address), thePhones(phones), numTimesConsulted(0) // 内置类型也要初始化! { // 构造函数体(此时成员已初始化完毕,这里是赋值) }- 优点 :效率更高(避免先调用默认构造函数再赋值的开销),尤其对于
const和引用成员(必须在初始化列表中初始化)。
- 优点 :效率更高(避免先调用默认构造函数再赋值的开销),尤其对于
-
类内成员初始化 (C++11) :在类定义中直接为成员变量指定初始值。
cppclass ABEntry { private: std::string theName = ""; std::string theAddress = ""; std::list<PhoneNumber> thePhones{}; int numTimesConsulted = 0; };- 优点:代码更清晰,减少构造函数初始化列表的负担,避免遗漏初始化。
-
static对象初始化 :- 函数内的
static对象 (local static):在第一次调用该函数时初始化(线程安全需考虑)。 - 非函数内的
static对象 (non-local static) :在main开始前初始化,顺序不确定(不同编译单元间)。 - 问题:不同编译单元中 non-local static 对象的初始化顺序不确定。如果某个 non-local static 对象依赖另一个 non-local static 对象,而后者尚未初始化,会导致问题。
- 解决方案:将 non-local static 对象替换为函数内的 local static 对象(单例模式常用)。
cppclass FileSystem { /* ... */ }; FileSystem& tfs() { // 用函数代替直接访问 static FileSystem fs; // local static 对象 return fs; } - 函数内的
-
-
要点 :永远确保所有对象在使用前已被初始化。对于类成员,优先使用构造函数初始化列表或类内成员初始化。对于
static对象,注意初始化顺序问题,用 local static 对象替代 non-local static 对象可以解决跨编译单元的初始化依赖问题。
总结第一部分: 第一部分奠定了编写良好 C++ 代码的基础习惯:理解 C++ 的多范式特性、避免宏、善用 const 保证正确性、以及始终确保对象的正确初始化。这些习惯能有效避免许多常见错误,提高代码的健壮性和可维护性。