[笔记]Effective C++改善程序与设计的55个具体做法_第一章 让自己习惯C++

条款 01:视 C++ 为一个语言联邦

C++ 并非单一范式的编程语言,而是由四种不同的 “次语言” 组成的联邦,每种次语言都有其独特的编程范式和规范:

  1. C 语言
  • 以过程式编程为核心,包含基本语法、指针、数组、结构体和预处理器等内容。

  • 规范:注重内存管理、手动资源释放,使用 malloc/free 进行内存操作。

  1. 面向对象的 C++
  • 涵盖类、继承、多态、虚函数等面向对象的特性。

  • 规范:提倡使用构造函数和析构函数进行资源管理,遵循 RAII(资源获取即初始化)原则。

  1. 模板 C++(泛型编程)
  • 以模板元编程(TMP)为核心,支持编写通用的、类型安全的代码。

  • 规范:利用模板实现编译时计算,避免运行时开销。

  1. STL(标准模板库)
  • 包含容器(如 vector、map)、算法(如 sort、find)和迭代器等组件。

  • 规范:采用统一的接口设计,遵循 “算法与数据结构分离” 的原则。

关键要点

  • 当从一个次语言切换到另一个次语言时,编程规范可能会发生变化(例如,在 STL 中应优先使用迭代器而非原始指针)。

  • 优秀的 C++ 代码需要在不同的范式之间灵活切换,以达到最佳的性能和可维护性。

条款 02:尽量以 const、enum、inline 替换 #define

预处理器(如 #define)在编译前会进行文本替换,这可能会引发一些难以调试的问题。我们应尽量使用编译器机制来替代预处理器。

1. 用 const 替代宏常量

// 不良做法
#define ASPECT_RATIO 1.653

// 推荐做法
const double AspectRatio = 1.653;  // 具有类型检查,并且可在调试时查看变量名

优势

  • 宏定义的名称在编译时可能会被移除,导致调试困难;而 const 变量会保留在符号表中。

  • 宏不具备作用域的概念,可能会引发命名冲突;而 const 变量可以被限制在类、文件或命名空间的作用域内。

在C++11中海引入了constexpr,它与const的差异点:

  • const:其作用是声明一个运行时常量,也就是在程序运行时,这个值被初始化之后就不能再改变了。它主要用于对变量进行修饰,以此来保证变量的值不会被修改。
  • constexpr:它声明的是编译时常量,意味着这个值在编译阶段就已经确定了。constexpr能够修饰变量、函数以及构造函数,借助它可以在编译时就完成计算,进而提高程序的运行效率。

2. 类内的专属常量

class GamePlayer {
private:
    static const int NumTurns = 5;  // 常量声明(编译期常量)
    int scores[NumTurns];           // 合法:NumTurns 在编译时已知
};

// 如果需要在类外定义(如获取变量地址)
const int GamePlayer::NumTurns;     // 定义(无需重复赋值)

若编译器不支持上述语法(例如旧版本的编译器),可以使用 enum hack

class GamePlayer {
private:
    enum { NumTurns = 5 };          // enum hack,效果类似宏,但具有类型安全性
    int scores[NumTurns];           // 同样合法
};

3. 用 inline 函数替代形似函数的宏

// 不良做法
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))  // 存在优先级和副作用问题

// 推荐做法
template<typename T>
inline void callWithMax(const T& a, const T& b) {
    f(a > b ? a : b);  // 类型安全,且不会出现宏的副作用
}

示例对比

int a = 5, b = 0;
CALL_WITH_MAX(++a, b);      // a 被递增两次
CALL_WITH_MAX(++a, b+10);   // a 被递增一次

关键要点

  • 优先使用 const 和 enum 替代宏常量,以获得类型检查和作用域控制。

  • 优先使用 inline 函数替代形似函数的宏,以避免副作用和调试困难的问题。

条款 03:尽可能使用 const

const 是 C++ 中一个非常强大的工具,它能够在编译时强制实现语义约束,提高代码的安全性。

1. 指针与常量的结合

char greeting[] = "Hello";

char* p = greeting;             // 非 const 指针,非 const 数据
const char* p = greeting;       // 非 const 指针,指向 const 数据(数据不可变)
char* const p = greeting;       // const 指针,指向非 const 数据(指针不可变)
const char* const p = greeting; // const 指针,指向 const 数据

记忆方法

  • const 在星号左边:数据是常量(*p 不可变)。

  • const 在星号右边:指针是常量(p 不可变)。

2. STL 迭代器与 const

std::vector<int> vec = {1, 2, 3};

// 迭代器本身是常量(类似 T* const)
std::vector<int>::iterator const iter = vec.begin();
*iter = 10;  // 合法:修改数据
++iter;     // 非法:迭代器不可变

// 迭代器指向常量数据(类似 const T*)
const std::vector<int>::iterator cIter = vec.begin();  // 错误!容易混淆的语法
std::vector<int>::const_iterator cIter = vec.begin();  // 正确写法
*cIter = 10;  // 非法:数据不可变
++cIter;     // 合法:迭代器可变

3. const 成员函数

常量成员函数:承诺不会修改对象的逻辑状态。

class TextBlock {
public:
    const char& operator[](std::size_t position) const {  // const 版本
        return text[position];
    }

    char& operator[](std::size_t position) {  // 非 const 版本
        return text[position];
    }

private:
    std::string text;
};

// 使用示例
const TextBlock ctb("Hello");
ctb[0] = 'X';           // 非法:调用 const 版本,返回 const char&

TextBlock tb("World");
tb[0] = 'X';            // 合法:调用非 const 版本,返回 char&

关键要点

  • 常量成员函数可以被重载,这使得 const 对象和非 const 对象可以调用不同版本的函数。

  • 使用 mutable 关键字可以让成员变量在 const 成员函数中被修改(例如用于缓存或锁机制)。

条款 04:确定对象被使用前已先被初始化

1. 区分初始化和赋值

// 赋值(先默认构造,再赋值)
class Person {
public:
    Person(const std::string& name, int age) {
        theName = name;  // 赋值,而非初始化
        theAge = age;    // 赋值,而非初始化
    }
private:
    std::string theName;
    int theAge;
};

// 初始化(使用成员初始化列表)
class Person {
public:
    Person(const std::string& name, int age) 
        : theName(name),      // 初始化
          theAge(age) {}      // 初始化
};

为什么优先使用初始化列表?

  • 对于 std::string 等类类型,初始化列表会直接调用拷贝构造函数,而赋值操作会先调用默认构造函数,再调用赋值运算符,效率较低。

  • 对于 const 成员变量和引用成员变量,必须使用初始化列表进行初始化。

2. 成员变量的初始化顺序

成员变量的初始化顺序由它们在类中的声明顺序决定,与初始化列表中的顺序无关。

class X {
private:
    int a;
    int b;
public:
    X(int value) : b(value), a(b) {}  // 错误:a 先被初始化,此时 b 尚未初始化
};

3. 局部静态对象的初始化问题

在多线程环境下,局部静态对象的初始化可能会出现竞态条件。C++11 解决了这个问题:

// C++11 保证线程安全的单例实现
class Logger {
public:
    static Logger& instance() {
        static Logger logger;  // 线程安全的初始化
        return logger;
    }
private:
    Logger() = default;
};

关键要点

  • 对于内置类型(如 int、double),在使用前手动初始化,因为它们不会进行默认初始化。

  • 使用成员初始化列表替代赋值操作,提高效率。

  • 注意成员变量的初始化顺序与声明顺序一致。

总结

第一章强调了 C++ 的多范式特性以及一些基础的编程准则:

  • 灵活运用不同的次语言范式,以适应具体的应用场景。

  • 减少对预处理器的依赖,充分利用编译器的类型系统。

  • 借助 const 增强代码的安全性和可读性。

  • 重视对象初始化,避免未定义行为。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇