原文地址:《Effective C++》55条款详读以及示例代码 - 知乎 (zhihu.com)

《Effective C++》是由C++权威Scott Meyers所著,旨在帮助读者编写更加高效、健壮、可维护的C++代码。该书共包含55个条款,涵盖了C++中的各个方面,包括对象创建和销毁、资源管理、继承和多态、模板和泛型编程、异常安全、并发等。

以下是该书中的一些重要条款以及示例代码:

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

C++中存在多个语言子集,如C、面向对象C++、泛型C++等。不同的子集有不同的语法和语义,因此需要视作不同的语言。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
// C语言风格代码
#include <stdio.h>
int main() {
printf("Hello, world!\n");
return 0;
}

// 面向对象C++风格代码
#include <iostream>
int main() {
std::cout << "Hello, world!" << std::endl;
return 0;
}

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

#define存在一些问题,如缺乏作用域、不能进行类型检查、可能被宏定义的东西误解等。因此,建议使用const、enum、inline等关键字替代#define。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
// 使用#define定义常量
#define PI 3.14159

// 使用const定义常量
const double pi = 3.14159;

// 使用enum定义常量
enum { kPi = 3 };

// 使用inline函数替代宏定义
inline double Pi() { return 3.14159; }

条款3:尽可能使用const

const可以防止变量被修改,有助于保证代码的可读性和可维护性。同时,const还可以帮助编译器进行优化,提高程序的效率。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
// 不使用const
void Print(char* str) {
str[0] = 'H';
str[1] = 'i';
std::cout << str << std::endl;
}

// 使用const
void Print(const char* str) {
std::cout << str << std::endl;
}

条款4:确保对象在使用前已被初始化

C++中存在多种初始化对象的方法,如初始化列表、构造函数、默认构造函数等。使用这些方法可以确保对象在使用前被初始化。

示例代码:

1
2
3
4
5
6
7
8
9
10
class MyClass {
public:
MyClass() : data_(0) {} // 使用初始化列表初始化data_
private:
int data_;
};

void func() {
MyClass obj; // 调用默认构造函数初始化obj
}

条款5:了解C++默认编写并调用哪些函数

在 C++ 中,编译器会默认编写一些函数,包括默认构造函数、析构函数、拷贝构造函数和赋值运算符。这些函数被称为“默认函数”,当程序需要这些函数时,编译器会自动生成。

需要注意的是,如果类的成员变量包含指针等动态资源,需要自行编写拷贝构造函数和赋值运算符,否则会出现浅拷贝问题。

以下是示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class MyClass {
public:
MyClass(); //默认构造函数
~MyClass(); //析构函数
MyClass(const MyClass& other); //拷贝构造函数
MyClass& operator=(const MyClass& other); //赋值运算符

private:
int* ptr;
};

MyClass::MyClass() {
ptr = new int;
}

MyClass::~MyClass() {
delete ptr;
}

MyClass::MyClass(const MyClass& other) {
ptr = new int(*other.ptr);
}

MyClass& MyClass::operator=(const MyClass& other) {
if (this != &other) {
*ptr = *other.ptr;
}
return *this;
}

条款6:尽量少地进行显示类型转换

C++中存在多种类型转换方式,如static_cast、dynamic_cast、reinterpret_cast等。尽量少地进行显示类型转换可以提高代码的可读性和可维护性,并减少类型错误的可能性。

示例代码:

1
2
3
4
5
6
7
// 不进行显示类型转换
double d = 3.14159;
int i = d; // 编译器会发出警告

// 使用static_cast进行类型转换
double d = 3.14159;
int i = static_cast<int>(d); // 显示进行类型转换

条款7:避免隐式类型转换

隐式类型转换会降低代码的可读性和可维护性,并且可能导致类型错误。因此,尽量避免隐式类型转换。

示例代码:

1
2
3
4
5
6
7
8
9
// 避免隐式类型转换
void func(int i) {
// ...
}

int main() {
short s = 1;
func(s); // 必须进行显式类型转换
}

条款8:优先使用nullptr而不是0或NULL

C++11引入了nullptr关键字,它能够避免与整数类型混淆,提高代码的可读性和可维护性。

示例代码:

1
2
3
4
5
6
7
8
// 使用0进行指针初始化
int* p = 0;

// 使用NULL进行指针初始化
int* p = NULL;

// 使用nullptr进行指针初始化
int* p = nullptr;

条款9:优先使用const_iterator而不是iterator

C++中的STL容器提供了两种迭代器类型,分别是const_iterator和iterator。const_iterator可以避免容器中的元素被修改,因此应该优先使用。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
std::vector<int> vec{1, 2, 3};

// 使用iterator遍历容器
for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {
*it = 0; // 可以修改容器中的元素
}

// 使用const_iterator遍历容器
for (std::vector<int>::const_iterator it = vec.begin(); it != vec.end(); ++it) {
// *it = 0; // 不能修改容器中的元素
}

条款10:优先使用std::vector而不是内置数组

std::vector比内置数组更加灵活、安全,且具有更好的可维护性。同时,std::vector还提供了多种方便的操作函数。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
// 使用内置数组
int arr[3] = {1, 2, 3};
for (int i = 0; i < 3; ++i) {
std::cout << arr[i] << std::endl;
}

// 使用std::vector
std::vector<int> vec{1, 2, 3};
for (auto i : vec) {
std::cout << i << std::endl;
}

条款11:优先使用delete关键字而不是free函数

C++中的delete关键字比free函数更加安全,因为它可以自动处理对象的析构函数,避免资源泄漏和未定义行为。

示例代码:

1
2
3
4
5
6
7
// 使用free函数释放内存
int* p = (int*)malloc(sizeof(int));
free(p);

// 使用delete关键字释放内存
int* p = new int;
delete p;

条款12:禁用编译器自动生成的函数

在一些情况下,编译器会自动生成一些函数,如拷贝构造函数、赋值操作符等。但是,这些函数的默认行为可能不是我们期望的,因此应该禁用它们的自动生成。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
// 禁用编译器自动生成的拷贝构造函数
class Foo {
public:
Foo(const Foo&) = delete;
};

// 禁用编译器自动生成的赋值操作符
class Bar {
public:
Bar& operator=(const Bar&) = delete;
};

条款13:使用对象的引用而不是对象的指针

使用对象的引用可以避免指针操作中的空指针和野指针等问题,并且更加直观和方便。

示例代码:

1
2
3
4
5
6
7
8
9
// 使用对象的指针
void func(Foo* foo) {
foo->doSomething();
}

// 使用对象的引用
void func(Foo& foo) {
foo.doSomething();
}

条款14:避免使用与内存相关的函数

与内存相关的函数,如memcpy、memset等,容易引发内存泄漏和未定义行为,应该避免使用。可以使用C++11中的类型转换函数、循环等方式替代。

示例代码:

1
2
3
4
5
6
7
8
// 使用memcpy进行内存复制
int arr1[3] = {1, 2, 3};
int arr2[3];
memcpy(arr2, arr1, sizeof(arr1));

// 使用类型转换函数进行复制
std::vector<int> vec1{1, 2, 3};
std::vector<int> vec2(vec1.begin(), vec1.end());

条款15:优先使用const成员函数

const成员函数可以避免对象被修改,提高代码的可读性和可维护性。同时,const成员函数还可以被const对象调用。

示例代码:

1
2
3
4
5
6
7
8
9
class Foo {
public:
void doSomething() const { // 声明为const成员函数
// ...
}
};

const Foo foo;
foo.doSomething(); // 可以调用const成员函数

条款16:使用成员函数模板提高代码的复用性

成员函数模板可以在一个类中定义多个具有相同函数名称但参数类型不同的函数,提高代码的复用性和灵活性。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
class Foo {
public:
template<typename T>
void doSomething(T t) { // 声明为成员函数模板
// ...
}
};

Foo foo;
foo.doSomething(1); // 调用doSomething<int>
foo.doSomething("hello"); // 调用doSomething<const char*>

条款17:理解特种成员函数的生成规则

特种成员函数,如默认构造函数、析构函数、拷贝构造函数、移动构造函数等,会根据一定的规则自动生成。理解这些规则可以帮助我们避免一些不必要的代码。

示例代码:

1
2
3
4
5
6
7
class Foo {
public:
Foo() = default; // 显式声明默认构造函数
~Foo() = default; // 显式声明析构函数
Foo(const Foo&) = delete; // 禁用拷贝构造函数
Foo(Foo&&) = default; // 显式声明移动构造函数
};

条款18:使用std::unique_ptr管理动态资源

std::unique_ptr是一种智能指针,可以自动管理动态资源的生命周期,避免内存泄漏和野指针等问题。

示例代码:

1
2
3
4
5
6
7
8
9
// 使用new创建动态对象
std::unique_ptr<Foo> p(new Foo);

// 使用std::make_unique创建动态对象
std::unique_ptr<Foo> p = std::make_unique<Foo>();

// 转移动态对象的所有权
std::unique_ptr<Foo> p1(new Foo);
std::unique_ptr<Foo> p2 = std::move(p1);

条款19:使用std::shared_ptr管理共享资源

std::shared_ptr是一种智能指针,可以自动管理共享资源的生命周期,避免内存泄漏和野指针等问题。

示例代码:

1
2
3
4
5
6
7
8
9
10
// 使用std::make_shared创建共享对象
std::shared_ptr<Foo> p = std::make_shared<Foo>();

// 复制共享指针
std::shared_ptr<Foo> p1 = std::make_shared<Foo>();
std::shared_ptr<Foo> p2 = p1;

// 分离共享指针
std::shared_ptr<Foo> p1 = std::make_shared<Foo>();
Foo* ptr = p1.get();

条款20:优先使用std::make_unique和std::make_shared

std::make_unique和std::make_shared是C++11中新增的函数模板,可以方便地创建智能指针和共享指针,避免手动管理new和delete。

以下是示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <memory>
#include <iostream>

struct MyClass {
int val;
MyClass(int v) : val(v) {}
void printVal() const {
std::cout << "Val: " << val << std::endl;
}
};

int main() {
// 使用 std::make_unique 创建 unique_ptr
auto ptr1 = std::make_unique<MyClass>(42);
ptr1->printVal(); // 输出 "Val: 42"

// 使用 std::make_shared 创建 shared_ptr
auto ptr2 = std::make_shared<MyClass>(100);
ptr2->printVal(); // 输出 "Val: 100"

// shared_ptr 可以拷贝构造
auto ptr3 = ptr2;
ptr3->printVal(); // 输出 "Val: 100"

return 0;
}

在上面的代码中,我们使用了std::make_unique和std::make_shared来创建智能指针,这样就不需要手动进行内存分配和释放。同时,由于std::make_unique和std::make_shared是C++11新增的特性,它们在处理异常时也更加安全。

条款21:使用std::move移动对象而非拷贝

移动语义可以避免不必要的拷贝,提高代码效率。在移动一个对象时,可以使用std::move函数将对象的所有权转移。

示例代码:

1
2
3
4
5
6
7
8
9
class Foo {
public:
Foo() = default;
Foo(const Foo&) { std::cout << "copy ctor\n"; }
Foo(Foo&&) { std::cout << "move ctor\n"; }
};

Foo foo;
Foo foo2 = std::move(foo); // 调用移动构造函数

条款22:使用std::forward完美转发

完美转发可以保留函数参数的左右值属性,提高代码灵活性。在实现完美转发时,可以使用std::forward函数。

示例代码:

1
2
3
4
5
6
template<typename T>
void doSomething(T&& t) { // 声明为通用引用
std::cout << "doSomething\n";
// 调用其他函数,使用std::forward进行完美转发
otherFunction(std::forward<T>(t));
}

条款23:理解std::move和std::forward的区别

std::move和std::forward都可以进行对象所有权的转移,但二者的语义不同。std::move只是简单地将对象的所有权转移,而std::forward可以保留对象的左右值属性。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Foo {
public:
Foo() = default;
Foo(const Foo&) { std::cout << "copy ctor\n"; }
Foo(Foo&&) { std::cout << "move ctor\n"; }
};

void doSomething(Foo&& foo) { // 右值引用
Foo foo2 = std::move(foo); // 调用移动构造函数
}

template<typename T>
void doSomething(T&& t) { // 通用引用
Foo foo = std::forward<T>(t); // 根据t的左右值属性调用拷贝构造函数或移动构造函数
}

条款24:优先使用不抛异常的函数

在设计函数时,应该尽量避免抛出异常,以提高代码的健壮性和可维护性。

示例代码:

1
2
3
4
5
6
class Foo {
public:
void doSomething() noexcept { // 声明为不抛异常函数
// ...
}
};

条款25:考虑使用std::optional代替可能不存在的对象

std::optional是一种包装器类型,可以表示一个可能不存在的对象。使用std::optional可以简化代码,并且避免指针和引用可能存在的空值问题。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <optional>
#include <iostream>

std::optional<int> getInt() {
return 42;
}

int main() {
std::optional<int> i = getInt();
if (i.has_value()) {
std::cout << *i << std::endl;
} else {
std::cout << "no value" << std::endl;
}
return 0;
}

条款26:尽量使用const、enum、inline替换#define

在定义常量、枚举值和内联函数时,应该尽量使用const、enum和inline关键字,而不是使用#define宏定义。这样可以避免宏定义可能导致的问题,如不必要的重复定义和作用域问题。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用const定义常量
const int MaxValue = 100;

// 使用enum定义枚举值
enum class Color {
Red,
Green,
Blue
};

// 使用内联函数
inline int add(int a, int b) {
return a + b;
}

条款27:尽量避免使用全局变量和静态变量

在编写代码时,应该尽量避免使用全局变量和静态变量,因为它们可能会导致不必要的耦合和副作用,从而使代码难以理解和维护。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
// 不要使用全局变量
int globalValue = 0;

void doSomething() {
globalValue++;
}

// 不要使用静态变量
void doSomething() {
static int staticValue = 0;
staticValue++;
}

条款28:避免使用递增、递减和解引用运算符的后置形式

在使用递增、递减和解引用运算符时,应该尽量避免使用后置形式,因为后置形式可能会导致不必要的性能损失。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Foo {
public:
int& operator[](int index) {
return data_[index];
}

private:
int data_[10];
};

Foo foo;

int a = foo[0]++; // 不要使用后置形式
int b = foo[0]; // a和b的值不同,会导致混淆和错误

条款29:了解隐式类型转换的风险

在进行类型转换时,应该尽量避免隐式类型转换,因为隐式类型转换可能会导致类型不一致和数据丢失等问题。

示例代码:

1
2
3
4
int a = 42;
double b = a; // 隐式类型转换,可能会导致数据丢失

int c = b; // 隐式类型转换,可能会导致类型不一致

条款30:透彻了解inlining的里里外外

函数的inline指令可以告诉编译器,将函数的定义插入到调用该函数的代码中,从而避免了函数调用的开销。inline可以提高程序的执行效率,但是也有一些需要注意的问题。

首先,inline并不是一定会让程序运行更快,因为inline代码的执行速度可能会受到代码大小的影响。如果函数体非常大,插入到调用点可能会导致代码膨胀,使得程序的效率反而降低。

其次,inline函数的定义必须在所有使用该函数的位置之前进行定义。如果使用了一个还没有定义的inline函数,会导致编译错误。

最后,inline函数的定义通常应该放在头文件中。这是因为头文件通常会被多个源文件包含,如果inline函数的定义在源文件中,就会导致函数的多次定义,从而引发链接错误。

下面是一个示例代码,演示了inline函数的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Rational {
public:
Rational(int numerator = 0, int denominator = 1)
: n(numerator), d(denominator) {}
int numerator() const { return n; }
int denominator() const { return d; }
// ...
private:
int n, d;
};

inline const Rational operator*(const Rational& lhs, const Rational& rhs) {
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}

void f(const Rational& r);

int main() {
Rational oneEighth(1, 8), oneHalf(1, 2);

f(oneEighth * oneHalf);

return 0;
}

在这个例子中,定义了一个Rational类,支持有理数的基本操作。然后定义了一个inline的operator函数,实现了有理数的乘法运算。在主函数中,定义了两个Rational对象,oneEighth和oneHalf,然后将它们相乘,并传递给了一个函数f。由于operator被声明为inline,编译器会将其定义插入到调用点,从而避免了函数调用的开销。

条款31:避免多重继承的复杂性

在使用多重继承时,应该尽量避免多重继承的复杂性,包括菱形继承和虚继承等。因为多重继承可能会导致不必要的复杂性和歧义,从而使代码难以理解和维护。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
示例代码:

class A {
public:
void doSomething() {}
};

class B : public A {
public:
void doSomething() {}
};

class C : public A {
public:
void doSomething() {}
};

class D : public B, public C {};

D d;
d.doSomething(); // 编译错误,需要指定调用B::doSomething()还是C::doSomething()

条款32:确定你的public继承塑模出is-a关系

在使用public继承时,应该确定所定义的继承关系确实是一种”is-a”关系,而不是其他类型的关系,如”has-a”关系和”like-a”关系。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Shape {
public:
virtual void draw() = 0;
};

class Rectangle : public Shape {
public:
void draw() override {}
};

class Car {
public:
void drive() {}
};

class SportsCar : public Car, public Shape {};

SportsCar car;
car.drive(); // 合法,但不符合"is-a"关系

条款33:避免遮掩继承而来的名称

在派生类中,应该避免遮掩继承而来的名称,包括变量、函数和类型等。因为遮掩继承而来的名称可能会导致混淆和错误,从而降低代码的可读性和健壮性。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
class Base {
public:
void doSomething() {}
};

class Derived : public Base {
public:
void doSomething(int value) {} // 遮掩了Base::doSomething()的名称
};

Derived d;
d.doSomething(); // 编译错误,需要指定参数

条款34:区分接口继承和实现继承

在使用继承时,应该区分接口继承和实现继承,即通过纯虚函数实现接口继承,通过非虚函数和数据成员实现实现继承。这样可以使代码更加清晰和可维护。

示例代码:

1
2
3
4
5
6
7
8
9
10
class Shape {
public:
virtual ~Shape() {}
virtual void draw() = 0; // 接口继承
};

class Rectangle : public Shape {
public:
void draw() override {}
int getArea() { return

条款35:考虑virtual的替代方案

在使用虚函数时,应该考虑virtual的替代方案,包括将虚函数变成非虚函数、使用模板和函数对象等。这样可以提高程序的性能和灵活性。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Shape {
public:
virtual ~Shape() {}
virtual void draw() = 0;
};

class Rectangle : public Shape {
public:
void draw() override {}
};

void drawShapes(const vector<Shape*>& shapes) {
for (const auto& shape : shapes) {
shape->draw();
}
}

// 替代方案1:将虚函数变成非虚函数
class NonVirtualShape {
public:
void draw() {}
};

class NonVirtualRectangle : public NonVirtualShape {};

// 替代方案2:使用模板
template<typename ShapeType>
void drawShapes(const vector<ShapeType*>& shapes) {
for (const auto& shape : shapes) {
shape->draw();
}
}

// 替代方案3:使用函数对象
class DrawShape {
public:
virtual ~DrawShape() {}
virtual void operator()(const Shape& shape) const {
shape.draw();
}
};

void drawShapes(const vector<Shape>& shapes, const DrawShape& drawer) {
for (const auto& shape : shapes) {
drawer(shape);
}
}

条款36:绝不重新定义继承而来的non-virtual函数

在派生类中,不应该重新定义继承而来的non-virtual函数,因为这样会导致调用父类版本的函数的时候出现错误。如果需要修改函数的行为,应该将函数变成virtual函数。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base {
public:
void doSomething() {}
};

class Derived : public Base {
public:
void doSomething() override {} // 不应该重新定义继承而来的non-virtual函数
};

Derived d;
Base* b = &d;
b->doSomething(); // 调用Base版本的函数,而不是Derived版本的函数

条款37:绝不重新定义继承而来的缺省参数值

在派生类中,不应该重新定义继承而来的缺省参数值,因为这样会导致编译器选择错误的函数。如果需要修改参数的缺省值,应该使用using声明式和默认参数。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
class Base {
public:
void doSomething(int value = 0) {}
};

class Derived : public Base {
public:
void doSomething(int value) override {} // 不应该重新定义继承而来的缺省参数值
};

Derived d;
d.doSomething(); // 编译错误,需要指定参数

条款38:通过复合塑模出has-a或”is-implemented-in-terms-of”关系

在设计类的关系时,应该通过复合来塑模出has-a或”is-implemented-in-terms-of”关系,而不是通过继承来实现这种关系。因为复合更加灵活和可控。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Engine {};

class Car {
public:
Car(const Engine& engine) : engine_(engine) {}

private:
Engine engine_;
};

class Widget {};

class ComplexWidget {
public:
ComplexWidget(const Widget& widget) : widget_(widget) {}

private:
Widget widget_;
};

条款39:明智而审慎地使用private继承

在使用继承时,应该明智而审慎地使用private继承。private继承意味着派生类只是从基类继承实现,而不会继承接口。如果需要继承接口,应该使用public继承。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Base {
public:
void doSomething() {}
};

// private继承
class Derived : private Base {
public:
void doSomething() {
Base::doSomething(); // 可以调用基类的函数
}
};

// public继承
class PublicDerived : public Base {
public:
void doSomething() override {
Base::doSomething(); // 可以调用基类的函数
}
};

条款40:明白何时该通过pass-by-reference-to-const来替换pass-by-value

在函数调用时,如果传递的参数是对象,应该考虑使用pass-by-reference-to-const来替换pass-by-value,这样可以提高程序的性能。但是,如果对象的大小不确定或者是内置类型,则应该使用pass-by-value。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Widget {
public:
Widget(const Widget& rhs) {} // 复制构造函数

// ...
};

// pass-by-value
void processWidget(Widget w) {}

// pass-by-reference-to-const
void processWidget(const Widget& w) {}

int main() {
Widget w;
processWidget(w); // pass-by-value,会调用复制构造函数
processWidget(w); // pass-by-reference-to-const,不会调用复制构造函数
}

条款41:了解隐式接口和编译期多态

在C++中,接口通常是通过类的public成员函数来实现的。但是,这种接口是显式的,因为它需要在类中明确定义。另一方面,隐式接口是通过非成员函数和模板参数来实现的,它是在使用类的过程中自动定义的。

编译期多态是通过函数重载和模板来实现的,它是在编译期决定调用哪个函数或模板实例的过程。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Widget {
public:
void doSomething(); // 显式接口

// ...
};

// 隐式接口
template<typename T>
void doSomethingElse(T& obj) {
obj.doSomething();
}

// 编译期多态
void doSomething(int x) {}
void doSomething(double x) {}

template<typename T>
void doSomething(T x) {}

int main() {
Widget w;
doSomethingElse(w); // 隐式接口

doSomething(1); // 编译期多态,调用doSomething(int)
doSomething(3.14); // 编译期多态,调用doSomething(double)
doSomething(w); // 编译期多态,调用doSomething(Widget)
}

条款42:了解typename的双重意义

在C++中,typename有双重意义。在模板定义中,typename表示一个类型,但在其他上下文中,它表示一个关键字。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template<typename T>
class Widget {
public:
typedef typename T::value_type value_type; // typename表示类型

// ...
};

template<typename Iterator>
void doSomething(Iterator iter) {
typename Iterator::value_type temp; // typename表示关键字

// ...
}

int main() {
Widget<std::vector<int>> w;
std::vector<int>::value_type x; // typename表示类型
}

条款43:学习处理模板化基类内的名称

当使用模板化基类时,需要学习如何处理名称。有时候,需要使用typename或this->来帮助编译器找到正确的名称。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template<typename T>
class Base {
public:
void doSomething() {}
};

template<typename T>
class Derived : public Base<T> {
public:
void process() {
doSomething(); // 会被解析为Derived<T>::doSomething,但是没有这个函数
this->doSomething(); // 正确的方式,会被解析为Base<T>::doSomething
}
};

int main() {
Derived<int> d;
d.process();
}

条款44:在类层次结构中正确处理“赋值兼容性”问题

在设计类层次结构时,通常需要考虑“赋值兼容性”问题,即基类对象能否通过赋值运算符进行派生类对象的赋值操作。为了保证赋值兼容性,需要遵循以下规则:

  1. 如果基类定义了赋值运算符,派生类需要显式调用基类的赋值运算符,以确保基类部分得到正确的赋值。
  2. 派生类需要为其新增加的成员定义自己的赋值运算符。

以下是示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Base {
public:
Base& operator=(const Base& rhs) {
//...
return *this;
}
};

class Derived : public Base {
public:
Derived& operator=(const Derived& rhs) {
//调用基类赋值运算符,确保基类部分正确赋值
Base::operator=(rhs);

//处理派生类新增加的成员
//...

return *this;
}
};

需要注意的是,在实现派生类赋值运算符时,需要调用基类的赋值运算符,并确保基类部分得到正确的赋值。否则,如果基类部分被忽略,就可能导致基类部分出现未定义的行为。

条款45:运用成员函数模板接受所有兼容类型

这个条款建议在设计类时,尽可能使用成员函数模板来实现通用性。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyClass
{
public:
template <typename T>
void foo(T arg)
{
// 处理 arg 的代码
}
};

// 调用 MyClass::foo() 的示例代码
MyClass obj;
obj.foo(42); // 实例化 MyClass::foo<int>(int)
obj.foo("hello"); // 实例化 MyClass::foo<const char*>(const char*)

在这个例子中,MyClass中的foo函数使用了成员函数模板,它能够接受任何类型的参数。调用时,编译器会根据参数的类型实例化模板函数,从而达到通用的效果。

条款46:需要类型转换时请使用explicit

这个条款建议对于单参数构造函数,应该使用explicit关键字,以避免隐式类型转换。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyClass
{
public:
explicit MyClass(int value)
{
// 构造函数的代码
}
};

void foo(MyClass obj)
{
// 函数的代码
}

// 调用 foo() 的示例代码
foo(42); // 错误:无法隐式转换 int 到 MyClass
foo(MyClass(42)); // 正确

在这个例子中,MyClass中的构造函数使用了explicit关键字,以避免将int类型隐式转换为MyClass类型。这样做可以防止一些潜在的错误,提高代码的安全性。

条款47:请注意非类型模板参数的区别

这个条款讨论了非类型模板参数(例如整数常量)和类型模板参数的区别,以及它们在模板中的使用方式。

示例代码:

1
2
3
4
5
6
7
8
template <int N>
struct Factorial
{
enum { value = N * Factorial<N-1>::value };
};

template <>
struct Factorial

在上一个示例代码中,Factorial是一个使用非类型模板参数的模板类,计算一个整数的阶乘。每一个实例都有一个名为value的枚举常量,表示计算出的阶乘值。

在示例代码中,Factorial<4>::value表示计算4的阶乘的结果,其值为24。在这个例子中,非类型模板参数被使用在了计算中。

条款48:认识template元编程

这个条款讨论了template元编程的概念,即在编译时使用模板来生成代码。这种技术可以用于编写高效的、类型安全的代码,但也需要开发人员具备一定的模板编程技能。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template <int N>
struct Fibonacci
{
enum { value = Fibonacci<N-1>::value + Fibonacci<N-2>::value };
};

template <>
struct Fibonacci<0>
{
enum { value = 0 };
};

template <>
struct Fibonacci<1>
{
enum { value = 1 };
};

// 调用 Fibonacci::value 的示例代码
std::cout << Fibonacci<10>::value << std::endl; // 输出 55

在这个例子中,Fibonacci是一个使用非类型模板参数的模板类,用于计算斐波那契数列。每一个实例都有一个名为value的枚举常量,表示计算出的斐波那契数列的值。

在示例代码中,Fibonacci<10>::value表示计算斐波那契数列中第10个数的值,其值为55。这个例子中展示了模板元编程的能力,通过在编译时展开模板,生成了高效的斐波那契数列计算代码。

条款49:理解new-handler的行为

这个条款讨论了new-handler的概念,它是一个函数指针,指向在分配内存时无法满足要求时调用的函数。在使用new操作符进行内存分配时,如果无法分配所需大小的内存,则会调用new-handler函数。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
void outOfMemory()
{
std::cerr << "Unable to satisfy request for memory\n";
std::abort();
}

int main()
{
std::set_new_handler(outOfMemory);
int* pBigDataArray = new int[1000000000000000000];
delete[] pBigDataArray;
return 0;
}

在这个例子中,我们定义了一个outOfMemory函数作为new-handler,并在main函数中使用set_new_handler函数将其注册为当前的new-handler。然后我们试图分配一个非常大的int数组,这个数组大小远远超出了系统可用内存的限制。由于无法分配所需大小的内存,new操作符将调用我们之前设置的new-handler函数outOfMemory,输出一条错误消息并中止程序。

条款50:了解new和delete的合理替换时机

这个条款讨论了new和delete的使用场景,以及何时可以使用其他的内存分配方式来代替它们。使用new和delete时,应该遵循一些最佳实践,例如避免使用异常、在分配大量内存时使用malloc/free等等。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void* operator new(std::size_t size)
{
if (void* mem = std::malloc(size))
return mem;
else
throw std::bad_alloc();
}

void operator delete(void* mem) noexcept
{
std::free(mem);
}

int main()
{
int* pBigDataArray = new int[1000000000000000000];
delete[] pBigDataArray;
return 0;
}

在这个例子中,我们重载了全局的new和delete操作符,使用了malloc和free函数来分配和释放内存。通过这种方式,我们可以在使用new和delete时避免抛出异常,从而提高程序的效率。同时,在分配大量内存时使用malloc/free也可以获得更好的性能。

条款51:编写new和delete时需固守常规

这个条款讨论了在编写new和delete时需要遵循的一些最佳实践,例如在分配内存时使用nothrow、遵循new和delete的匹配规则等等。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Widget {
public:
void* operator new(std::size_t size) throw(std::bad_alloc)
{
if (void* mem = std::malloc(size))
return mem;
else
throw std::bad_alloc();
}

void operator delete(void* mem) noexcept
{
std::free(mem);
}

void* operator new(std::size_t size, const std::nothrow_t& nt) noexcept
{
void* mem = nullptr;
try {
mem = operator new(size);
}
catch (...) {}
return mem;
}

void operator delete(void* mem, const std::nothrow_t& nt) noexcept
{
operator delete(mem);
}

Widget() {}
virtual ~Widget() {}
};

int main()
{
Widget* pWidget = new (std::nothrow) Widget();
delete pWidget;
return 0;
}

在这个例子中,我们定义了一个Widget类,并重载了它的new和delete操作符。我们还定义了一个nothrow版本的new操作符,它会在无法分配所需大小的内存时返回一个空指针,而不是抛出一个bad_alloc异常。在main函数中,我们使用nothrow版本的new操作符来分配一个Widget对象,并使用delete操作符来释放它

条款52:写出typename的正确形式

这个条款讨论了在使用typename关键字时需要遵循的一些规则,例如typename的正确位置、typename与template关键字的结合使用等等。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<typename IterT>
void workWithIterator(IterT iter)
{
typedef typename std::iterator_traits<IterT>::value_type value_type;
value_type temp(*iter);
//...
}

int main()
{
std::vector<int> vec;
workWithIterator(vec.begin());
return 0;
}

在这个例子中,我们定义了一个workWithIterator函数,它接受一个迭代器作为参数,并使用std::iterator_traits模板类来获取迭代器的value_type类型。由于value_type是一个依赖于模板参数的类型,所以在使用typename关键字时必须将其放在typename关键字之前。在main函数中,我们使用workWithIterator函数来处理一个std::vector的迭代器。

条款53:避免过度使用多重继承

这个条款讨论了多重继承的一些问题,例如菱形继承、虚基类等等,以及如何避免多重继承带来的一些困扰。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Animal {
public:
virtual void eat() = 0;
virtual ~Animal() {}
};

class FlyingAnimal : public virtual Animal {
public:
virtual void fly() = 0;
virtual ~FlyingAnimal() {}
};

class SwimmingAnimal : public virtual Animal {
public:
virtual void swim() = 0;
virtual ~SwimmingAnimal() {}
};

class Bird : public FlyingAnimal {
public:
virtual void eat() override {}
virtual void fly() override {}
virtual ~Bird() {}
};

class Fish : public SwimmingAnimal {
public:
virtual void eat() override {}
virtual void swim() override {}
virtual ~Fish() {}
};

class Duck : public FlyingAnimal, public SwimmingAnimal {
public:
virtual void eat() override {}
virtual void fly() override {}
virtual void swim() override {}
virtual ~Duck() {}
};

int main()
{
Duck d;
d.eat();
d.fly();
d.swim();
return 0;
}

在这个例子中,我们定义了一些动物类和飞行类、游泳类,并使用虚基类Animal来避免菱形继承带来的问题。我们还定义了一些具体的动物类,例如Bird、Fish和Duck,并使用多重继承来组合不同的功能。在main函数中,我们创建了一个Duck对象,并调用它的eat、fly和swim函数。

条款54:让自己熟悉STL的组件

这个条款讨论了STL的一些常用组件,例如容器、算法、迭代器、函数对象等等,并提出了在使用STL时需要注意的一些问题,例如避免将STL容器内的迭代器作为指针使用、使用STL算法时避免过多的拷贝操作等等。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>

class Widget {
public:
Widget(int i) : m_i(i) {}
int getI() const { return m_i; }
private:
int m_i;
};

int main()
{
std::vector<Widget> vec;
vec.push_back(Widget(1));
vec.push_back(Widget(2));
vec.push_back(Widget(3));

// 使用STL算法和函数对象
std::transform(vec.begin(), vec.end(), std::ostream_iterator<int>(std::cout, " "),
[](const Widget& w) { return w.getI(); });

return 0;
}

在这个例子中,我们使用了STL的vector容器来存储Widget对象,并使用transform算法和lambda表达式来将vector容器内的Widget对象的值转换为int类型,并输出到标准输出流中。

条款55:明智而审慎地使用异常

这个条款讨论了异常的使用问题,包括何时使用异常、如何设计异常类、如何处理异常等等。作者提出了一些异常使用的准则,例如:只在非异常情况下使用异常、使用标准库异常类、不在析构函数中抛出异常等等。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <stdexcept>

void func1() {
throw std::runtime_error("error in func1");
}

void func2() {
try {
func1();
} catch(const std::exception& e) {
std::cerr << "Exception caught: " << e.what() << '\n';
}
}

int main()
{
try {
func2();
} catch(const std::exception& e) {
std::cerr << "Exception caught: " << e.what() << '\n';
}
return 0;
}

在这个例子中,我们定义了两个函数func1和func2,其中func1抛出一个std::runtime_error异常,而func2调用func1并捕获异常。在main函数中,我们调用func2,并在主函数中捕获异常。在捕获异常时,我们使用了const std::exception&类型的引用来捕获任何类型的异常,并输出了异常信息。

附加补充内容Ⅰ:

使用 placement new 运算符时需要特别小心,并且需要自己编写对应的 placement delete 运算符来正确释放内存,避免内存泄漏或者其他的问题。这个条款主要包含以下几个方面的内容:

  1. 理解 placement new 运算符:placement new 运算符是一种定位 new 运算符,它的作用是在已经分配的内存块上面构造对象。placement new 运算符的语法如下:
  2. void* operator``new(std::size_t size, void* ptr)``noexcept;
    其中,size 表示要分配的内存块的大小,ptr 表示指向已经分配的内存块的指针。
  3. 注意内存块的大小:在使用 placement new 运算符时,必须确保已经分配的内存块的大小足够容纳要构造的对象。如果内存块的大小不足,就会导致未定义行为。
  4. 编写 placement delete 运算符:在使用 placement new 运算符时,必须手动调用对应的 placement delete 运算符来销毁对象和释放内存。placement delete 运算符的语法如下:
  5. void``operator``delete(void* ptr, void* ptr2)``noexcept;
    其中,ptr 表示要销毁的对象的指针,ptr2 表示已经分配的内存块的指针。
  6. 注意 placement new 和普通 new 的区别:在使用 placement new 运算符时,构造函数不会被自动调用,而且如果构造函数抛出异常,程序也不会自动调用析构函数。

下面是一个使用 placement new 的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <cstdlib>

class MyClass {
public:
MyClass() {
std::cout << "Constructing MyClass\n";
}
~MyClass() {
std::cout << "Destructing MyClass\n";
}
};

int main() {
// 使用 malloc 分配一块内存,大小为 sizeof(MyClass)
void* mem = std::malloc(sizeof(MyClass));

// 在已分配的内存上构造对象
MyClass* p = new(mem) MyClass();

// 销毁对象并释放内存
p->~MyClass();
operator delete(p, mem);

// 释放已分配的内存
std::free(mem);

return 0;
}

在这个示例代码中,我们使用 std::malloc 函数分配一块内存,大小为 sizeof(MyClass)。然后,我们使用 placement new 运算符在已分配的内存上构造一个 MyClass 对象,并调用析构函数来销毁对象。最后,我们使用 operator delete 运算符释放内存。需要注意的是,我们在调用 operator delete 运算符时需要传入已分配的内存块的指针作为第二个参数,以确保释放的内存块与分配的内存块匹配。

需要注意的是,当我们在使用 placement new 运算符时,需要手动调用构造函数和析构函数。在这个示例代码中,我们手动调用了析构函数和 operator delete 运算符来销毁对象和释放内存。这种方式需要程序员手动管理内存,容易出错,因此在使用时需要格外小心。