前言
C++语言律师成长史的第一篇文章,献给《Effective C++》咯。其实年前就开始写这个了,断断续续到今天才写完。You大学上有个叫Bo Qian的up主专门推出了系列视频:Advanced C++。感兴趣的朋友可以去看看,里面讲解了这本书的前些条款。
另外分享这位知乎朋友的文章: 一篇文章学完 Effective C++:条款 & 实践
作者专精图形学方向,C++的功底远胜于我这个新人。
这篇文章的例子理解之后可以当个小字典用(本身就是一些编译器层面的准则),遇到了相应的问题就看一下,直到足够熟悉。
第一章:让自己习惯C++
条款01:视C++为一个语言联邦
四个次语言:C、Object-Oriented C++、Template C++、 STL
条款02:尽量以const,enum,inline替换 #define
const:
#define MAX_SIZE 100
使用这个替代:
const int MaxSize = 100;
enum:
#define COLOR_RED 1
#define COLOR_GREEN 2
#define COLOR_BLUE 3
使用这个替代:
enum Color {
Red = 1,
Green = 2,
Blue = 3
};
inline:
#define SQUARE(x) ((x) * (x))
使用这个替代:
inline int square(int x) {
return x * x;
}
条款03:尽可能使用const:
1. 对象和基本类型:
const int MaxValue = 100; // 常量
const std::string greeting = "Hello, World!"; // 常量字符串
int main() {
const double Pi = 3.14159; // 圆周率,不应被修改
// Pi = 3.14; // 编译错误:不能修改const变量
}
2. 函数参数:
使用const可以防止函数内部意外改变输入参数:
void printVector(const std::vector<int>& vec) {
for (int value : vec) {
std::cout << value << " ";
}
// vec.push_back(123); // 编译错误:不能修改const引用指向的内容
}
3. 函数返回值:
返回 const 值可以防止返回的值被错误地修改:
const std::string& getStatus() {
static std::string status = "Working";
return status;
// 返回常量引用,防止调用者修改status
}
4. 成员函数:
在成员函数后使用const表示该函数不会修改对象的状态:
class Calculator {
public:
Calculator(int value) : value(value) {}
int getValue() const { // 常量成员函数
return value;
}
private:
int value;
};
int main() {
const Calculator calc(5);
std::cout << calc.getValue(); // 正确,getValue是const函数
// calc.setValue(10); // 编译错误:calc是const对象,不能调用非const函数
}
条款04:确定对象被使用前已先被初始化
这个规则很容易奉行,重要的是别混淆了赋值(assignment)和初始化(initialization)。
class MyClass {
private:
int* pointer;
public:
MyClass() : pointer(nullptr) { // 使用初始化列表将指针初始化为nullptr
}
void allocateMemory() {
pointer = new int(42); // 分配内存并初始化
}
void usePointer() {
if (pointer != nullptr) {
// 安全地使用pointer
std::cout << *pointer << std::endl;
}
}
~MyClass() {
delete pointer; // 释放内存
}
};
第二章:构造/析构/赋值运算
条款05:了解C++默默编写并调用哪些函数
default构造函数、copy构造函数、copy assignment操作符、以及析构函数。
如果我们写下 class Example {}, 相当于写下了:
class Example {
public:
Example(){ ... }
Example(const Example& example){ ... }
~Example(){ ... }
Example& operator=(const Example& example){ ... }
};
然后看这个例子:
class Example {
public:
int* ptr;
Example(int value) {
ptr = new int(value);
}
};
int main() {
Example ex1(10);
Example ex2 = ex1; // 使用编译器生成的复制构造函数
return 0;
}
- 默认构造函数:如果没有定义任何构造函数,编译器会生成一个默认构造函数。但是,如果已经定义了至少一个构造函数(就像在这个例子中一样),编译器就不会生成默认构造函数。
- 复制构造函数:当使用
Example ex2 = ex1;时,编译器会使用自动生成的复制构造函数,这只会进行成员的浅复制。在这个例子中,ex2.ptr和ex1.ptr将指向同一个内存地址。 - 复制赋值运算符:当你将一个对象赋值给另一个已存在的对象时(如
ex2 = ex1;),编译器会使用自动生成的复制赋值运算符,这里同样进行浅复制。 - 析构函数:编译器会生成一个默认的析构函数。但是,在这个例子中,默认析构函数不会释放
ptr指向的内存,这可能会导致内存泄漏。
补充:浅复制和深复制的区别:
浅复制仅复制对象的各个成员的值,而不复制其指向的资源。这意味着,如果对象含有指向动态分配内存的指针,浅复制将复制指针的值(即内存地址),而不是指针指向的内存块的内容。这导致复制后的对象和原始对象共享同一块内存。
深复制不仅复制对象的各个成员的值,还复制其指向的资源。当对象包含指向动态分配内存的指针时,深复制会在堆上分配新的内存块,并复制原始内存块的内容到新内存块。这样,复制后的对象将拥有一份原始资源的独立副本。
条款06:若不想使用编译器自动生成的函数,就该明确拒绝
class NonCopyable {
public:
NonCopyable() = default; // 使用默认构造函数
// 明确拒绝复制构造函数和赋值运算符
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
// 允许移动构造函数和移动赋值运算符
NonCopyable(NonCopyable&&) = default;
NonCopyable& operator=(NonCopyable&&) = default;
};
条款07:为多态基类声明virtual析构函数
#include<iostream>
using namespace std;
class Base {
public:
virtual ~Base() {cout<<"Base"<<endl;} // 虚析构函数
};
class Derived : public Base {
public:
Derived() { }
~Derived() { cout<<"Derive"<<endl; } // 自定义析构函数来释放资源
};
int main() {
Base* b = new Derived();
delete b; // 这里会先调用 Derived 的析构函数,然后是 Base 的析构函数
}
运行上面这个代码会输出Derive 和 Base。如果 Base 的析构函数不是虚的,那么 delete b 将不会调用 Derived 的析构函数,从而导致动态分配的内存没有被释放,只输出 Base。
条款08:别让异常逃离析构函数
如果析构函数中抛出了异常,并且没有在析构函数内部捕获处理,这个异常就会逃离析构函数:
class Resource {
public:
Resource() {/* 资源分配 */}
~Resource() {
// 假设在释放资源时发生了错误,并抛出了异常
if(/* 某些错误条件 */) {
throw std::runtime_error("Failed to release resource");
}
}
};
void function() {
Resource res;
// 进行一些操作
// 函数结束时,res对象会被销毁,调用析构函数
}
好的做法是:
~Resource() {
try {
// 尝试释放资源
} catch(...) {
// 处理异常,但不再抛出
// 可以记录日志、清理状态等
}
}
条款09:绝不在构造和析构过程中调用virtual函数
-
构造过程中调用虚拟函数:
- 当构造一个派生类对象时,基类的构造函数首先被调用。
- 在基类构造函数执行期间,对象类型被视为基类类型,而不是派生类类型。
- 因此,如果基类构造函数调用了任何虚拟函数,实际上调用的是基类中的版本,即使派生类已经覆盖了这些虚拟函数。
-
析构过程中调用虚拟函数:
- 析构函数的调用顺序与构造函数相反,先调用派生类的析构函数,然后是基类的析构函数。
- 当进入基类的析构函数时,派生类部分已经被销毁。
- 在基类的析构函数中调用虚拟函数,同样会调用基类的实现,因为此时派生类的部分已不复存在。
#include<iostream>
using namespace std;
class Base {
public:
Base() { virtualFunc(); }
virtual ~Base() { virtualFunc(); }
virtual void virtualFunc() { std::cout << "Base version of virtualFunc\n"; }
};
class Derived : public Base {
public:
Derived() { virtualFunc(); }
virtual ~Derived() { virtualFunc(); }
void virtualFunc() override { std::cout << "Derived version of virtualFunc\n"; }
};
int main() {
Derived d;
}
当对象d被创建时,以下是调用序列:
Base::Base()构造函数被调用。Base::virtualFunc()被调用(注意,这里不会调用Derived::virtualFunc(),即使我们期望它这样做)。Derived::Derived()构造函数被调用。Derived::virtualFunc()被调用。
当对象d被销毁时,以下是调用序列:
Derived::~Derived()析构函数被调用。Derived::virtualFunc()被调用。Base::~Base()析构函数被调用。Base::virtualFunc()被调用(这里不会调用Derived::virtualFunc(),因为此时派生类部分已经被销毁)。
输出结果是:
Base version of virtualFunc
Derived version of virtualFunc
Derived version of virtualFunc
Base version of virtualFunc
由于这种机制,如果虚拟函数的行为依赖于派生类的状态或成员,那么在构造和析构过程中调用这些虚拟函数可能不会按照预期工作,因为派生类的状态在构造函数中可能还未初始化,而在析构函数中可能已经被销毁。
条款10:令operator= 返回一个reference to *this
class MyClass {
int value;
public:
MyClass(int v) : value(v) {}
// 重载赋值运算符
MyClass& operator=(const MyClass& other) {
if (this != &other) { // 检查自赋值
value = other.value;
}
return *this; // 返回当前对象的引用
}
int getValue() const { return value; }
};
int main() {
MyClass a(5), b(10);
MyClass c(15);
// 连续赋值
a = b = c;
// a, b 现在都与 c 有相同的值
std::cout << "a: " << a.getValue() << ", b: " << b.getValue() << std::endl;
return 0;
}
重点就在于return *this。
条款11:在operator= 中处理“自我赋值”
class MyClass {
private:
int* data;
public:
MyClass(int value) {
data = new int(value);
}
~MyClass() {
delete data;
}
MyClass& operator=(const MyClass& other) {
if (this != &other) {
delete data; // 释放原来的资源
data = new int(*other.data); // 分配新的资源
}
return *this;
}
};
条款通过比较 this 指针(指向左侧对象)和 &other (指向右侧对象)来实现。
条款12:复制对象时勿忘其每一个成分
class Example {
private:
int ordinaryValue;
int* dynamicValue;
public:
// 构造函数
Example(int ordinary, int dynamic) : ordinaryValue(ordinary) {
dynamicValue = new int(dynamic);
}
// 析构函数
~Example() {
delete dynamicValue;
}
// 复制构造函数
Example(const Example& other) {
ordinaryValue = other.ordinaryValue;
// 忘记复制dynamicValue
}
// 赋值运算符
Example& operator=(const Example& other) {
if (this != &other) {
ordinaryValue = other.ordinaryValue;
// 忘记复制dynamicValue
}
return *this;
}
};
在这个例子中,复制构造函数和赋值运算符都忽略了 dynamicValue 成员。这意味着当一个 Example 对象被复制时,新对象的 dynamicValue 将保持未初始化状态,出现悬挂指针或者野指针的情况。
悬挂指针是指向已释放内存的指针。当内存(如堆上的对象)被释放或删除(例如使用
delete),而有指针仍然指向那块内存时,这个指针就成为悬挂指针。尽管内存已被释放,但指针仍然保留着原来的地址,此时的指针是悬挂的。使用悬挂指针会导致未定义行为,因为指针指向的内存可能被操作系统回收或重新分配给其他进程。
野指针是指未初始化的指针或者指向随机内存地址的指针。由于它们没有被初始化为一个确定的值,所以可能指向任意的内存地址。这类指针的危险在于,它们可能指向程序的任何部分(包括敏感数据或操作系统的关键部分),从而导致程序崩溃、数据损坏或安全问题。野指针通常是由于指针声明后没有被立即赋予一个明确的地址引起的。
第三章:资源管理
条款13:以对象管理资源
#include <iostream>
#include <fstream>
#include <string>
class FileWrapper {
private:
std::fstream file;
public:
// 构造函数:打开文件
FileWrapper(const std::string& filename) {
file.open(filename, std::ios::in | std::ios::out | std::ios::app);
if (!file.is_open()) {
throw std::runtime_error("Unable to open file");
}
}
// 向文件写入数据
void write(const std::string& data) {
if (file.is_open()) {
file << data;
std::cout << "Data written to file: " << data << std::endl;
} else {
std::cout << "File not open." << std::endl;
}
}
// 读取文件数据
std::string read() {
std::string data;
if (file.is_open()) {
file >> data;
}
return data;
}
// 析构函数:关闭文件
~FileWrapper() {
if (file.is_open()) {
file.close();
}
}
};
int main() {
try {
FileWrapper fw("F:\algorithms\example.txt");
fw.write("Hello, world!");
// 当main函数结束时,fw的析构函数会被调用,文件自动关闭
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
}
return 0;
}
RAII的核心思想是:
- 资源获取:在对象构造时获取资源。在
FileWrapper类中,这通过在构造函数中打开文件来实现。 - 资源释放:在对象析构时释放资源。在
FileWrapper类中,这通过在析构函数中关闭文件来实现。
条款14:在资源管理类中小心copying行为
class FileWrapper {
private:
std::fstream file;
public:
FileWrapper(const std::string& filename) {
file.open(filename, std::ios::in | std::ios::out | std::ios::app);
if (!file.is_open()) {
throw std::runtime_error("Unable to open file");
}
}
// 析构函数
~FileWrapper() {
if (file.is_open()) {
file.close();
}
}
// 复制构造函数
FileWrapper(const FileWrapper& other) = delete; // 禁止复制
// 赋值运算符
FileWrapper& operator=(const FileWrapper& other) = delete; // 禁止赋值
};
比如我们复制 std::fstream 对象可能导致两个对象共享同一个文件句柄,在对象被销毁的时候会引起问题,因为同一个文件句柄可能被多次关闭。
条款15:在资源管理类中提供对原始资源的访问
class FileWrapper {
FILE* file;
public:
// 构造函数打开文件
FileWrapper(const char* filename, const char* mode) {
file = fopen(filename, mode);
}
// 析构函数关闭文件
~FileWrapper() {
if (file) {
fclose(file);
}
}
// 获取原始 FILE* 的方法
FILE* get() const {
return file;
}
// 禁止复制和赋值
FileWrapper(const FileWrapper&) = delete;
FileWrapper& operator=(const FileWrapper&) = delete;
};
FileWrapper 类封装了原始的资源,即 FILE* file。这个指针是对一个打开的文件的引用,是被管理的资源。
通过 get() 成员函数,FileWrapper 类允许外部代码访问其内部封装的原始资源(FILE* file)。这意味着如果需要,外部代码可以直接使用原始的 FILE* 来进行文件操作。
条款16:成对使用new和delete时要采取相同形式
错误例子:
std::string* example = new std::string[100];
...
delete example;
正确做法:
std::string* example = new std::string[100];
...
delete [] example;
条款17:以独立语句将newed对象置于智能指针
正确的做法:
std::shared_ptr<MyClass> createMyClass() {
MyClass* rawPtr = new MyClass(); // 独立的语句
std::shared_ptr<MyClass> smartPtr(rawPtr);
return smartPtr;
}
错误的做法:
std::shared_ptr<MyClass> createMyClass() {
return std::shared_ptr<MyClass>(new MyClass());
}
第四章:设计与声明
条款18:让接口容易被正确使用,不易被误用
意味着设计者应该尽量减少用户犯错的机会,同时使得正确的使用方式变得直观和简单。比如:当函数有许多参数时,可以考虑使用参数对象来简化接口:
不推荐的做法(参数列表过长):
void createWindow(int x, int y, int width, int height, bool isVisible, bool isResizable);
推荐的做法:使用参数对象
class WindowSettings {
public:
WindowSettings() : x(0), y(0), width(800), height(600), isVisible(true), isResizable(true) {}
// 提供设置方法
WindowSettings& setPosition(int x, int y) { this->x = x; this->y = y; return *this; }
WindowSettings& setSize(int width, int height) { this->width = width; this->height = height; return *this; }
WindowSettings& setVisible(bool isVisible) { this->isVisible = isVisible; return *this; }
WindowSettings& setResizable(bool isResizable) { this->isResizable = isResizable; return *this; }
// ... 其他设置方法
private:
int x, y;
int width, height;
bool isVisible;
bool isResizable;
// ... 其他设置
};
void createWindow(const WindowSettings& settings);
条款19:设计 class 犹如设计 type
考虑行为、接口、数据封装、继承、构造和析构、赋值等等多个方面:
class Date {
private:
int day, month, year;
public:
Date(int d, int m, int y) : day(d), month(m), year(y) { // 构造函数
// 这里可以添加对日期合法性的验证
}
void setDay(int d) {
day = d;
}
void setMonth(int m) {
month = m;
}
void setYear(int y) {
year = y;
}
int getDay() const { return day; }
int getMonth() const { return month; }
int getYear() const { return year; }
bool isValidDate() const {
// 验证日期的有效性
return true; // 简化的例子
}
void print() const {
std::cout << day << "/" << month << "/" << year << std::endl;
}
};
条款20: 宁以 pass-by-reference-to-cast 替换 pass-by-value
假如有一个类:
class LargeObject {
// 假设这个类非常大,并且拷贝代价很高
public:
LargeObject() { /* ... */ }
LargeObject(const LargeObject&) { /* 复制构造函数,可能很昂贵 */ }
// ...
};
按值传递:
void processByValue(LargeObject obj) {
// 处理 obj
// 这里会调用 LargeObject 的复制构造函数
}
按引用传递:
void processByValue(const LargeObject& obj){
// 处理 obj
// 这里不会发生复制
}
条款21:必须返回对象时,别妄想返回其reference
下面是错误做法:
class Matrix{
public:
Matrix(int rows, int cols):rows(rows),cols(cols),data(rows*cols,0){}
const Matrix& add(const Matrix& other) const{
static Matrix result(rows,cols);
for(int i=0;i<rows*cols;i++){
result.data[i] =this->data[i] + other.data[i];
}
return result;
}
private:
int rows, cols;
std::vector<int> data;
};
这里add 函数返回了一个局部静态 Matrix 对象的引用,这虽然避免了悬挂引用的问题,但是局部静态对象在多次调用间共享,这有可能导致一些非预期现象,尤其在多线程环境当中。
正确做法是:
class Matrix{
public:
Matrix(int rows, int cols):rows(rows),cols(cols),data(rows*cols,0){}
Matrix add(const Matrix& other) const{
static Matrix result(rows,cols);
for(int i=0;i<rows*cols;i++){
result.data[i] =this->data[i] + other.data[i];
}
return result;
}
private:
int rows, cols;
std::vector<int> data;
};
条款21:将成员变量声明为private
class Car {
private:
int speed; // 私有成员,控制对速度的访问
int fuel; // 私有成员,控制对燃料的访问
public:
Car() : speed(0), fuel(0) {} // 构造函数
// 设置速度的方法,包含安全检查
void setSpeed(int s) {
if (s >= 0 && s <= 200) {
speed = s;
} else {
// 报告错误或者进行其他处理
}
}
// 获取当前速度
int getSpeed() const {
return speed;
}
// 类似地,为燃料添加设置器和获取器
void setFuel(int f) {
if (f >= 0 && f <= 100) {
fuel = f;
} else {
// 报告错误或者进行其他处理
}
}
int getFuel() const {
return fuel;
}
};
为成员变量提供set、get接口
条款22:宁以non-member、non-friend替换member函数
// 基类或接口,提供一个用于计算面积的方法
class Shape {
public:
virtual double area() const = 0;
virtual ~Shape() {}
};
class Rectangle : public Shape {
private:
int width, height;
public:
Rectangle(int w, int h) : width(w), height(h) {}
virtual double area() const override {
return width * height;
}
};
class Circle : public Shape {
private:
int radius;
public:
Circle(int r) : radius(r) {}
virtual double area() const override {
return 3.14159 * radius * radius;
}
};
// 模板非成员函数,比较两个形状的面积
template <typename T>
bool compareArea(const T& shape1, const T& shape2) {
return shape1.area() < shape2.area();
}
条款24:若所有参数皆需类型转换,请为此采用non-member函数
先看错误例子:
class Vector2D {
public:
double x, y;
Vector2D(double x, double y) : x(x), y(y) {}
// 作为成员函数的向量与标量的乘法
Vector2D operator*(double scalar) const {
return Vector2D(x * scalar, y * scalar);
}
};
int main() {
Vector2D v(1.0, 2.0);
double scalar = 3.0;
Vector2D result1 = v * scalar; // 正确,向量乘以标量
// Vector2D result2 = scalar * v; // 编译错误,因为double没有定义operator*来接受Vector2D
}
要解决这个问题,我们可以将乘法操作符实现为一个非成员函数,这样就可以对两个参数都进行隐式类型转换。
class Vector2D {
public:
double x, y;
Vector2D(double x = 0.0, double y = 0.0) : x(x), y(y) {}
// 可以添加其他成员函数,如向量加法、长度计算等
};
// 向量与标量的乘法 - 非成员函数
Vector2D operator*(const Vector2D& v, double scalar) {
return Vector2D(v.x * scalar, v.y * scalar);
}
// 标量与向量的乘法 - 非成员函数,提供对称的操作
Vector2D operator*(double scalar, const Vector2D& v) {
return Vector2D(v.x * scalar, v.y * scalar);
}
条款25:考虑写出一个不抛异常的swap函数
#include <algorithm> // 用于 std::swap
class Widget {
private:
int *data;
size_t size;
public:
Widget(size_t sz) : size(sz), data(new int[sz]) { /* 初始化数据 */ }
~Widget() { delete[] data; }
// 拷贝构造函数和拷贝赋值操作符
Widget(const Widget& other) : size(other.size), data(new int[other.size]) {
std::copy(other.data, other.data + size, data);
}
Widget& operator=(Widget other) {
swap(*this, other);
return *this;
}
// 自定义 swap 函数
friend void swap(Widget& first, Widget& second) noexcept {
using std::swap;
swap(first.size, second.size);
swap(first.data, second.data);
}
// 其他成员函数...
};
第五章:实现
条款26:尽可能延后变量定义式的出现时间
不好的做法:
void someFunction() {
int expensiveComputationResult; // 早期定义,但还未使用
if (someCondition()) {
expensiveComputationResult = expensiveComputation();
// 使用 expensiveComputationResult
}
// 其他代码,但不使用 expensiveComputationResult
}
好的做法:
void someFunction() {
if (someCondition()) {
int expensiveComputationResult = expensiveComputation(); // 定义并初始化
// 使用 expensiveComputationResult
}
// 其他代码
}
条款27:尽量少做转型动作
C++提供了四种新式转型:const_cast<T>(expression)、dynamic_cast(expression)、reinterpret_cast<T>(expression)、static_cast<T>(expression)
看下面这个滥用转型的例子:
class Base {
// 基类成员
};
class Derived : public Base {
public:
void doSomething() {
// 特定于 Derived 类的行为
}
};
void process(Base *base) {
Derived *derived = dynamic_cast<Derived*>(base);
if (derived) {
derived->doSomething();
}
// 其他操作...
}
这里用转型,反而没有好好利用多态的性质,违反了“封装原则”,还增加了性能开销。
正确做法是:
class Base {
public:
virtual void doSomething() {
// 基类的默认行为
}
// ...其他成员...
};
class Derived : public Base {
public:
void doSomething() override {
// 特定于 Derived 类的行为
}
};
void process(Base *base) {
base->doSomething();
// 其他操作...
}
条款28:避免返回handles指向对象内部成分
不好的方式:
class MyClass {
private:
int value;
public:
MyClass(int val) : value(val) {}
// 返回内部成员的引用
int& getValue() { return value; }
};
int main() {
MyClass myObject(5);
int& valRef = myObject.getValue();
valRef = 10; // 直接修改对象内部状态
}
这个例子里,getValue成员函数返回了对私有成员value的引用。这允许外部代码通过引用直接修改value,破坏了封装性。
好的做法:
class MyClass {
private:
int value;
public:
MyClass(int val) : value(val) {}
// 返回内部成员的副本
int getValue() const { return value; }
// 提供修改内部状态的函数
void setValue(int val) { value = val; }
};
int main() {
MyClass myObject(5);
int val = myObject.getValue(); // 安全地获取value的副本
myObject.setValue(10); // 通过控制的方式修改内部状态
}
条款29:为“异常安全”而努力是值得的
异常安全函数(Exception-safe functions)即使发生异常也不会泄露资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
“强烈保证”往往能够以 copy-and-swao实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。
函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。
下面这个是强异常安全保证的例子:
MyClass& operator=(const MyClass& other) {
if (this != &other) {
std::vector<int> tempData = other.data; // 复制,可能抛出异常
data.swap(tempData); // 不抛出异常的操作
}
return *this;
}
这个例子里,如果std::vector<int> tempData = other.data复制失败,这个异常会被抛出,并且不会影响this->data的值;下一行是使用std::vector的swap,不会抛出异常。
条款30:透彻了解 inlining 的里里外外
内联的优点
- 提高性能:内联减少了函数调用的开销,特别是对于小型且频繁调用的函数。
- 代码优化:编译器可以对内联函数的上下文进行更好的优化。
内联的缺点和风险
- 代码膨胀:如果一个内联函数在多处被调用,它的代码会在每个调用处复制一遍,可能导致可执行文件体积增大。
- 滥用风险:不是所有的函数都适合内联。对于复杂或不常调用的函数,内联可能不会提高性能。
适合inline的情况:
inline int square(int x) {
return x * x;
}
for (int i = 0; i < 100; ++i) {
int result = square(i); // 内联可能提高性能
}
不适合inline的情况:
inline void complexOperation() {
// 复杂的操作,比如读写文件
}
complexOperation(); // 不常调用或复杂的函数,内联可能不提高性能
条款31:将文件间的编译依存关系降至最低
这里放写的ROS2的例子:
这个例子里有几个不同类型的demo:action_demo、interfaces_demo、node_demo、service_demo、topic_demo,每个demo便是单独编译都需要一定的时间,如果一个demo时需要依赖于另一个demo,耗费时间是非常大的,所以需要尽可能降低编译依存关系。
第六章:继承与面向对象设计
条款32:确定你的public继承塑模出 is-a 关系
class Bird {
public:
void fly() {
// 鸟类通常会飞
cout << "Flying" << endl;
}
void eat() {
// 鸟类的进食行为
cout << "Eating" << endl;
}
};
class Penguin : public Bird {
public:
// 覆盖基类的fly方法,因为企鹅不会飞
void fly() override {
cout << "Cannot fly" << endl;
}
// 添加企鹅特有的行为,比如游泳
void swim() {
cout << "Swimming" << endl;
}
};
条款33: 避免遮掩继承而来的名称
先看下面这个例子:
#include <iostream>
using namespace std;
class Base {
public:
void doSomething() {
cout << "Base::doSomething()" << endl;
}
};
class Derived : public Base {
public:
void doSomething(int x) {
cout << "Derived::doSomething(int)" << endl;
}
};
int main() {
Derived d;
d.doSomething(); // 错误!Base 类中的 doSomething() 被遮掩
d.doSomething(5); // 正确调用 Derived 类的 doSomething(int)
}
发生错误的原因是,调用d.doSomething()的时候,编译器优先在派生类的作用域里查找doSomething(),结果派生类里这个函数的参数不一样,所以报错。
解决方案是在派生类里使用using声明:
class Derived : public Base {
public:
using Base::doSomething; // 引入 Base 类的 doSomething
void doSomething(int x) {
cout << "Derived::doSomething(int)" << endl;
}
};
条款34: 区分接口继承和实现继承
接口继承是指派生类继承基类的接口(即方法的声明,而不是方法的具体实现)。这种类型的继承使得派生类必须提供一套特定的公共接口。在C++中,这通常是通过纯虚函数(Pure Virtual Functions)实现的。
实现继承是指派生类继承基类的具体实现。派生类继承了基类的方法和属性,并可以使用或重写这些方法。实现继承允许派生类重用基类的代码。
#include <iostream>
using namespace std;
// 基类
class Shape {
public:
// 接口继承(纯虚函数)
virtual void draw() const = 0; // 必须在派生类中实现
// 实现继承(具体实现)
virtual void move(int deltaX, int deltaY) {
cout << "Shape moved." << endl;
// 具体的移动逻辑
}
};
// 派生类
class Circle : public Shape {
public:
// 重写draw(),提供具体实现
void draw() const override {
cout << "Circle drawn." << endl;
}
// 使用基类的move()实现,或者重写它(可选)
};
int main() {
Circle c;
c.draw(); // 使用 Circle 的 draw 实现
c.move(5, 10); // 使用 Shape 的 move 实现
}
条款35:考虑virtual函数以外的其他选择
NVI (Non-Virtual Interface) 方法
class Base {
public:
void doWork() {
// ... do some general work ...
// 调用虚拟函数
doWorkImpl();
}
protected:
virtual void doWorkImpl() = 0; // 纯虚函数
};
class Derived : public Base {
protected:
void doWorkImpl() override {
// ... 实现特定的工作 ...
}
};
Strategy 设计模式
class Strategy {
public:
virtual ~Strategy() {}
virtual void execute() const = 0;
};
class ConcreteStrategyA : public Strategy {
public:
void execute() const override {
// ... 实现算法A ...
}
};
class ConcreteStrategyB : public Strategy {
public:
void execute() const override {
// ... 实现算法B ...
}
};
class Context {
private:
Strategy* strategy;
public:
Context(Strategy* strategy = nullptr) : strategy(strategy) {}
void setStrategy(Strategy* strategy) {
delete this->strategy;
this->strategy = strategy;
}
void doSomething() {
// ...
if (this->strategy)
this->strategy->execute();
// ...
}
};
std::function 对象
std::function 是一个函数包装器,它可以存储、复制和调用任何可调用对象,如函数指针、lambda表达式等。
#include <iostream>
void exampleFunction(int x) {
std::cout << "Value: " << x << std::endl;
}
int main() {
std::function<void(int)> func = exampleFunction;
func(10); // 调用exampleFunction
}
条款36: 绝不重新定义继承而来的non-virtual函数
在C++中,如果一个派生类重新定义了基类中的非虚函数,这不会产生多态行为,而是会隐藏基类中的那个函数。这可能会导致混淆和错误,因为调用的函数取决于调用对象的类型,而不是对象实际指向的类型。
#include <iostream>
class Base {
public:
void nonVirtualFunc() {
std::cout << "Base nonVirtualFunc" << std::endl;
}
};
class Derived : public Base {
public:
void nonVirtualFunc() {
std::cout << "Derived nonVirtualFunc" << std::endl;
}
};
int main() {
Base *b = new Derived();
b->nonVirtualFunc();
Derived *d = new Derived();
d->nonVirtualFunc();
delete b;
delete d;
return 0;
}
这个会输出,输出结果取决于调用对象的类型,所以没有表现出多态性
Base nonVirtualFunc
Derived nonVirtualFunc
条款37: 绝不重新定义继承而来的缺省参数值
在C++中,继承时不应该重新定义虚函数的默认参数值。这是因为虚函数的默认参数值是静态绑定的,而虚函数本身是动态绑定的。这意味着虚函数使用的默认参数值取决于调用该函数的对象的静态类型,而不是其动态类型,这可能导致混淆和错误。
#include <iostream>
class Base {
public:
virtual void func(int x = 10) {
std::cout << "Base func with x = " << x << std::endl;
}
};
class Derived : public Base {
public:
virtual void func(int x = 20) override {
std::cout << "Derived func with x = " << x << std::endl;
}
};
int main() {
Base *b = new Derived();
b->func(); // 预期调用哪个函数和使用哪个默认参数?
Derived *d = new Derived();
d->func(); // 预期调用哪个函数和使用哪个默认参数?
delete b;
delete d;
return 0;
}
条款38: 通过复合塑模出 has-a 或 “根据某物实现出”
假设我们有一个 Timer 类,它负责计时,和一个 Stopwatch 类,它是一种特殊的计时器,具有启动和停止功能。使用组合来实现 Stopwatch 比继承 Timer 类更合适,因为 Timer是Stopwatch 的一部分,而不是Stopwatch 的一种类型。
class Timer {
public:
void start() {
// 启动计时器逻辑
}
void stop() {
// 停止计时器逻辑
}
};
class Stopwatch {
private:
Timer timer; // 使用组合
public:
void start() {
timer.start();
}
void stop() {
timer.stop();
}
};
条款 39:明智而审慎地使用private继承
之前说到public继承的关系是 is-a ,而 Private 继承意味着is-implemented-in-terms-of(基于某物实现出)。
class List {
public:
void insert(int value); // 插入元素
void remove(int value); // 移除元素
int size() const; // 返回列表大小
};
class Stack : private List {
public:
void push(int value) {
insert(value); // 使用List的insert函数
}
void pop() {
// 假设top存储了栈顶元素的值
remove(top); // 使用List的remove函数
}
int top() const {
// 返回栈顶元素
// ... 实现细节 ...
}
int size() const {
return List::size(); // 使用List的size函数
}
};
条款 40:明智而审慎地使用多重继承
先看下面这个例子:
class Printer {
public:
void print() {
// 执行打印操作
}
};
class Scanner {
public:
void scan() {
// 执行扫描操作
}
};
class MultiFunctionPrinter : public Printer, public Scanner {
public:
void copy() {
scan(); // 首先使用Scanner的扫描功能
print(); // 然后使用Printer的打印功能来复制
}
};
然后补充一下虚继承的例子:
class Base {
public:
Base() { /* ... */ }
void baseFunction() { /* ... */ }
};
class Derived1 : virtual public Base {
public:
Derived1() { /* ... */ }
};
class Derived2 : virtual public Base {
public:
Derived2() { /* ... */ }
};
class Final : public Derived1, public Derived2 {
public:
Final() { /* ... */ }
};
- 虚继承:
Derived1和Derived2都是通过virtual关键字从Base虚继承而来。这意味着无论Final类通过多少个中间类间接继承Base,都只有一个Base类的实例。 - 构造函数:在虚继承中,最底层的派生类(在这个例子中是
Final)负责构造其虚基类(Base)。这是因为只有最底层的派生类知道所有虚基类的构造路径。 - 资源共享:由于只有一个
Base实例,所有从Base继承来的成员都是共享的。这解决了非虚继承时可能出现的重复继承问题。
这解决了菱形继承带来的资源冗余和数据不一致性问题。然而,这也带来了性能和复杂性的成本,因为对虚基类的访问需要额外的间接跳转,而且构造顺序更加复杂。因此,虚继承应该谨慎使用,只在解决菱形继承问题时才考虑。
第七章:模板与泛型编程
条款41:了解隐式接口和编译器多态
#include <iostream>
// 函数模板,用于比较两个值
template <typename T>
bool compare(const T& a, const T& b) {
return a < b;
}
int main() {
std::cout << compare(5, 10) << std::endl; // 使用int类型
std::cout << compare(3.5, 2.1) << std::endl; // 使用double类型
}
在这个示例中,compare 是一个函数模板,它定义了一个隐式接口:任何可以使用 < 运算符进行比较的类型 T。当你用不同的类型调用 compare 时,编译器为每个类型生成一个特定的 compare 函数版本。因此,compare(5, 10) 生成的是两个 int 类型参数的 compare 函数,而 compare(3.5, 2.1) 生成的是两个 double 类型参数的 compare 函数。这种在编译期确定使用哪个函数版本的能力就是编译期多态的体现。
条款42:了解typename的双重意义
第一个是最常见的用途,用来指示模板参数,这个地方typename 和 class是等价的
template <typename T>
class MyClass {
T data;
public:
MyClass(T d) : data(d) {}
T getData() const { return data; }
};
第二个是指示依赖类型的名字:
template <typename T>
class MyClass {
public:
typename T::SubType *ptr; // 使用typename指明SubType是一个类型
};
当编译器开始解析template MyClass的时候,C++有这样一个规则:如果解析器在template中遭遇一个嵌套从属名称,它便假设这名称不是个类型,除非你告诉它是。什么意思?
想象一下,如果我们不用typename指明SubType是一个类型,编译器认为T::SubType不是个类型,而是个常量,那T::SubType *ptr这个是不是就变成了一个相乘的动作?但我们实际上想做的是声明一个指针,于是错误就发生了。
条款43: 学习处理模板化基类内的名称
定义一个基类:
template <typename T>
class Base {
public:
T value;
void baseFunc() {
}
};
首先看下面这个反面例子:
template <typename T>
class Derived : public Base<T> {
public:
void derivedFunc() {
value = T(); // 错误:编译器无法解析
baseFunc(); // 错误:编译器无法解析
}
};
在这个例子中,Derived::derivedFunc 直接尝试访问 value 和 baseFunc。但由于它们是从基类模板 Base<T> 继承来的依赖名称,在编译时编译器无法确定它们是否有效或者它们的类型是什么。
正确做法是这样的:
template <typename T>
class Derived : public Base<T> {
public:
void derivedFunc() {
this->value = T(); // 正确
this->baseFunc(); // 正确
// 或者
Base<T>::value = T(); // 正确
Base<T>::baseFunc(); // 正确
}
};
或者
template <typename T>
class Derived : public Base<T> {
public:
using Base<T>::value; // 将Base的value引入Derived的作用域
using Base<T>::baseFunc; // 将Base的baseFunc引入Derived的作用域
void derivedFunc() {
value = T(); // 现在可以直接访问
baseFunc(); // 现在可以直接访问
}
};
条款44:将与参数无关的代码抽离模板
1. 避免模板参数引起的代码膨胀
template <int Size>
class Array {
public:
int arr[Size];
int getSize() const { return Size; }
// 其他与Size相关的功能...
};
// 使用
Array<10> a;
Array<20> b;
改进后的例子:
class ArrayBase {
public:
int getSize() const;
// 其他不依赖于Size的通用功能...
};
template <int Size>
class Array : public ArrayBase {
public:
int arr[Size];
// 特定于Size的功能...
};
2. 用函数参数或成员变量替换非类型模板参数
class Array {
int size;
public:
int* arr;
Array(int s) : size(s), arr(new int[s]) {}
int getSize() const { return size; }
// 其他功能...
};
// 使用
Array a(10);
Array b(20);
在这里,Array 类不再是模板。它使用一个普通的构造函数参数 size 来指定数组大小,避免了模板实例化带来的代码膨胀。
3. 让具有相同二进制表述的类型共享实现
class ArrayBase {
// 类型无关的基类
protected:
void* arr;
int size;
public:
int getSize() const { return size; }
// 其他通用功能...
};
template <typename T>
class Array : public ArrayBase {
public:
Array(int s) {
size = s;
arr = new T[s];
}
T& operator[](int index) {
return static_cast<T*>(arr)[index];
}
// 其他特定于T的功能...
};
// 使用
Array<int> a(10);
Array<float> b(10);
条款45:运用成员函数模板接受所有兼容类型
未使用成员函数模板的类:
class MyClass {
public:
void assign(int value) {
// ...
}
void assign(double value) {
// ...
}
};
使用成员函数模板:
class MyClass {
public:
template <typename T>
void assign(T value) {
// 处理value,可能需要转换类型或其他逻辑
}
};
// 使用
MyClass myObject;
myObject.assign(123); // 使用 int 类型
myObject.assign(45.67); // 使用 double 类型
在这个改进的例子中,MyClass 只需要一个 assign 成员函数模板,它可以接受任何类型的参数。这样就大大减少了需要写的代码量,并且增加了类的通用性。
条款46:需要类型转换时请为模板定义非成员函数
使用成员函数的实现:
template <typename T>
class MyClass {
public:
// 成员函数实现类型转换
template <typename U>
MyClass<U> toType() const {
return MyClass<U>(/* 转换逻辑 */);
}
};
// 使用
MyClass<int> intObj;
MyClass<double> doubleObj = intObj.toType<double>();
使用非成员函数(友元函数)的实现:
template <typename T>
class MyClass;
// 非成员函数实现类型转换
template <typename T, typename U>
MyClass<U> toType(const MyClass<T>& obj) {
return MyClass<U>(/* 载换逻辑 */);
}
template <typename T>
class MyClass {
// 声明友元函数
template <typename X, typename Y>
friend MyClass<Y> toType(const MyClass<X>& obj);
public:
// 其他成员 ...
};
// 使用
MyClass<int> intObj;
MyClass<double> doubleObj = toType<int, double>(intObj);
toType 函数被声明为 MyClass 的友元。这意味着 toType 函数可以访问 MyClass 的所有成员,包括私有和受保护的成员。这在需要对 MyClass 的不同模板实例进行操作时特别有用,特别是当这些操作需要访问对象的私有状态时。
条款47:请使用traits classes表现类型信息
定义一个特性类,用于确定类型是否为指针:
template <typename T>
struct is_pointer {
static const bool value = false;
};
template <typename T>
struct is_pointer<T*> {
static const bool value = true;
};
然后可以这样使用:
template <typename T>
void process(T t) {
if (is_pointer<T>::value) {
// T是指针类型的特定处理
} else {
// T不是指针类型的特定处理
}
}
C++17后更好的方法:
template <typename T>
void process(T t) {
if constexpr (is_pointer<T>::value) {
// T是指针类型的特定处理
} else {
// T不是指针类型的特定处理
}
}
if constexpr 允许编译器在编译时做出决策,避免了运行时的条件判断,提高了程序的效率。
条款48:认识template元编程
模板元编程允许程序员编写在编译时执行的代码,从而生成更有效率的运行时代码。
示例:编译时计算阶乘
在传统的运行时编程中,阶乘函数通常如下实现:
unsigned long long factorial(int n) {
return (n <= 1) ? 1 : n * factorial(n - 1);
}
模板元编程实现:
template <unsigned int n>
struct Factorial {
static const unsigned long long value = n * Factorial<n - 1>::value;
};
template <>
struct Factorial<0> {
static const unsigned long long value = 1;
};
在C++模板编程中,当我们对一个模板进行特化时,如果这个特化是针对特定的、已知的模板参数值,那么我们在声明特化时就不再需要提供模板参数列表。
这里对于基本情况 Factorial<0>,我们提供了一个特化定义,确保递归有一个终止条件。
使用模板:
unsigned long long fact5 = Factorial<5>::value; // 编译时计算 5的阶乘
unsigned long long fact10 = Factorial<10>::value; // 编译时计算 10的阶乘
在这里,阶乘的计算发生在编译时,而不是运行时。
第八章:定制new和delete
条款49:了解new-handler的行为
#include <new>
#include <iostream>
// 自定义 new-handler
void myNewHandler() {
std::cerr << "自定义内存分配失败处理程序被调用" << std::endl;
// 这里可以实现内存释放、日志记录或其他恢复策略
// 如果无法处理内存分配失败,则可以抛出异常或终止程序
std::abort();
}
int main() {
std::set_new_handler(myNewHandler);
try {
// 尝试分配大量内存
int* p = new int[1000000000000];
} catch (const std::bad_alloc& e) {
std::cerr << "捕获到异常: " << e.what() << '\n';
}
return 0;
}
new-handler 的行为
new-handler被调用时机是在new表达式无法满足内存分配请求且在抛出std::bad_alloc异常之前。- 如果
new-handler能够成功解决内存问题(比如通过释放一些预先分配的内存),new会再次尝试分配内存。 - 如果
new-handler无法解决问题,它通常应该使程序失败,例如通过抛出异常或调用std::abort()。 - 默认的
new-handler会直接抛出std::bad_alloc异常。如果设置了自定义的new-handler,则默认行为会被覆盖。
条款50:了解new和delete的合理替换时机
首先要知道,每个C++对象默认都有与之关联的 new 和 delete 操作。这些是C++中内置的操作符,用于动态内存分配和释放。
class MyClass {
// ...
};
int main() {
MyClass* obj = new MyClass; // 调用默认的 new
delete obj; // 调用默认的 delete
return 0;
}
假设我们正在开发一个需要频繁分配和释放大量小对象的应用程序。在这种情况下,使用标准的 new 和 delete 可能会导致内存碎片和性能下降。我们可以通过内存池来优化这个过程。
自定义 new 和 delete:
class MyClass {
public:
// 重载 MyClass 的 new 和 delete
void* operator new(size_t size) {
// 从内存池中分配内存
return MemoryPool::allocate(size);
}
void operator delete(void* memory, size_t size) {
// 将内存归还给内存池
MemoryPool::deallocate(memory, size);
}
};
条款51:编写new和delete时需固守常规
1. 对齐内存
确保自定义的 new 操作符正确地对齐内存,以便它可以用于任何类型的对象。内存对齐是一种优化,可提高内存访问速度,也是某些硬件平台的要求。
2. 异常处理
当内存分配失败时,标准的 new 会抛出 std::bad_alloc 异常。自定义 new 应该保持这一行为,除非有特别的理由去改变它。
3. 考虑 new 的无异常版本
C++ 提供了 new(std::nothrow) 变体,它在内存分配失败时不抛出异常,而是返回空指针。如果重载 new,考虑实现这一行为的版本。
4. delete 应能处理空指针
标准的 delete 操作符能够安全地处理空指针(什么都不做)。确保自定义 delete 也能做到这一点。
5. 处理 new[] 和 delete[]
如果重载了 new,考虑也重载 new[] 和对应的 delete[]。这些操作符用于数组分配和释放。
#include <new>
#include <iostream>
#include <cstdlib>
void* operator new(size_t size) {
void* memory = std::malloc(size);
if (memory == nullptr) {
throw std::bad_alloc();
}
std::cout << "自定义 new 分配了 " << size << " 字节\n";
return memory;
}
void operator delete(void* memory) noexcept {
std::free(memory);
std::cout << "自定义 delete 释放了内存\n";
}
int main() {
try {
int* p = new int;
delete p;
} catch (const std::bad_alloc& e) {
std::cerr << "内存分配失败: " << e.what() << '\n';
}
return 0;
}
条款52:写了placement new也要写placement delete
定义一个资源类:
class Resource {
public:
Resource() {
std::cout << "Acquiring resource.\n";
// 假设这里进行了资源分配,比如分配内存或打开文件
}
~Resource() {
std::cout << "Releasing resource.\n";
// 清理资源,比如释放内存或关闭文件
}
};
使用 Placement New 和 Placement Delete
现在,我们在预分配的内存上使用placement new创建 Resource 实例,并定义相应的placement delete来处理可能的异常。
void* operator new(size_t size, void* where) {
return where;
}
void operator delete(void* memory, void* where) noexcept {
std::cout << "Placement delete called.\n";
// 这里不需要释放内存,但可以执行其他清理操作
reinterpret_cast<Resource*>(where)->~Resource();
}
int main() {
char buffer[sizeof(Resource)]; // 分配足够大的内存
try {
Resource* myResource = new (buffer) Resource(); // 在buffer的位置构造对象
throw std::runtime_error("Exception after constructing Resource");
} catch (const std::runtime_error& e) {
std::cout << "Exception caught: " << e.what() << "\n";
}
}
在这个例子中,如果在使用placement new构造 Resource 对象后发生异常(在这里是人为抛出的),placement delete将被调用。在这种情况下,placement delete调用了 Resource 的析构函数,以确保分配的资源被正确释放,即使对象的构造未能成功完成。
第九章:杂项讨论
条款53:不要忽视编译器的警告
- 严肃对待编译器发出的警告信息。努力在你的编译器的最高(最严苛)警告级别下争取“无任何警告”的荣誉。
- 不要过度依赖编译器的警告能力,因为不同的编译器对待事情的态度不同。一旦移植到另一个编译器上,你原本依赖的警告信息可能会消失。
条款54:让自己熟悉包括TR1在内的标准程序库
今天TR1已经完全融入C++标准库了,不需要额外学习。
条款55:让自己熟悉Boost
Boost是一个社群,也是一个网站。致力于免费、源码开放、同僚复审的C++程序库开发。Boost在C++标准化过程中扮演深具影响力的角色。