Effective C++
-
视 C++ 为一个语言联邦(C、Object-Oriented C++、Template C++、STL)
-
宁可以编译器替换预处理器(尽量以
const
、enum
、inline
替换#define
)编译器可以进行类型检查,避免预处理宏可能导致的类型错误。而且比预处理宏更具有可读性和可维护性,方便调试和错误定位,并且编译器对译器可以对
const
、enum
、inline
等方式定义的常量进行优化,例如可以将常量直接嵌入到代码中,从而提高代码的执行效率。 -
尽可能使用 const
在 C++ 编程中,尽可能使用
const
可以提高代码的可读性、可维护性和安全性,同时也可以带来一些性能优势。以下是一些常见的情况,可以考虑使用const
:- 常量声明:
const double PI = 3.1415926;
- 函数参数:如果函数不会修改参数的值,应该将参数声明为
const
,这可以帮助编译器进行更好的优化,并防止意外修改参数的值。例如:
void printMessage(const std::string& message);
3.迭代器和指针:如果指针或迭代器指向的对象不会被修改,应该将其声明为指向常量的指针或迭代器。例如:
const int* ptr; const_iterator it;
- 成员函数中的成员变量:在成员函数中,如果成员变量不会被修改,应该将其声明为
const
,以提高代码的可读性和安全性。例如:
class MyClass { public:int getValue() const {return value;} private:int value; };
-
确定对象被使用前已先被初始化(构造时赋值(copy 构造函数)比 default 构造后赋值(copy assignment)效率高)
"default 构造后赋值(copy assignment)"指的是使用默认构造函数创建对象后,再通过赋值运算符(copy assignment operator)将另一个对象的值赋给该对象。
class MyClass { public:int value;// 默认构造函数MyClass() : value(0) {}// 赋值运算符(copy assignment operator)MyClass& operator=(const MyClass& other) {if (this != &other) { // 检查是否是自我赋值value = other.value;}return *this;} };int main() {MyClass obj1; // 使用默认构造函数创建对象 obj1MyClass obj2; // 使用默认构造函数创建对象 obj2obj2 = obj1; // 使用赋值运算符将 obj1 的值赋给 obj2return 0; }
-
了解 C++ 默默编写并调用哪些函数(编译器暗自为 class 创建 default 构造函数、copy 构造函数、copy assignment 操作符、析构函数)
-
若不想使用编译器自动生成的函数,就应该明确拒绝(将不想使用的成员函数声明为 private,并且不予实现)
-
为多态基类声明 virtual 析构函数(如果 class 带有任何 virtual 函数,它就应该拥有一个 virtual 析构函数)
-
别让异常逃离析构函数(析构函数应该吞下不传播异常,或者结束程序,而不是吐出异常;如果要处理异常应该在非析构的普通函数处理)
-
绝不在构造和析构过程中调用 virtual 函数(因为这类调用从不下降至 derived class)
-
令
operator=
返回一个reference to *this
(用于连锁赋值)这句话的意思是在重载赋值运算符(
operator=
)时,让它返回一个对当前对象的引用,通常是*this
。这样做的目的是为了支持连锁赋值操作,即可以通过连续地对同一对象进行赋值操作。考虑下面的示例:
class MyClass { public:int value;// 重载赋值运算符MyClass& operator=(const MyClass& other) {if (this != &other) {value = other.value;}return *this; // 返回对当前对象的引用} };int main() {MyClass obj1, obj2, obj3;// 连锁赋值操作obj1 = obj2 = obj3;return 0; }
在这个例子中,
operator=
被重载为返回对当前对象的引用*this
。因此,连锁赋值obj1 = obj2 = obj3
的执行顺序是从右向左,首先obj2 = obj3
被执行,然后返回对obj2
的引用,接着obj1 = obj2
被执行,并返回对obj1
的引用。这样就实现了连锁赋值操作。如果
operator=
没有返回引用,则无法进行连锁赋值操作,因为每次赋值操作都会返回一个新的对象,而不是对原始对象的引用。因此,重载赋值运算符时通常会让它返回一个对当前对象的引用,以支持连锁赋值操作。 -
在
operator=
中处理 “自我赋值”// 重载赋值运算符 MyClass& operator=(const MyClass& other) {// 检查自我赋值if (this != &other) {// 删除旧资源delete data;// 分配新资源并复制数据data = new int(*other.data);}return *this; }
-
赋值对象时应确保复制 “对象内的所有成员变量” 及 “所有 base class 成分”(调用基类复制构造函数)
#include <iostream>// 基类 class Base { public:int baseValue;// 基类构造函数Base(int value) : baseValue(value) {}// 基类复制构造函数Base(const Base& other) : baseValue(other.baseValue) {}// 基类赋值运算符重载Base& operator=(const Base& other) {if (this != &other) {baseValue = other.baseValue;}return *this;} };// 派生类 class Derived : public Base { public:int derivedValue;// 派生类构造函数Derived(int base, int derived) : Base(base), derivedValue(derived) {}// 派生类复制构造函数Derived(const Derived& other) : Base(other), derivedValue(other.derivedValue) {}// 派生类赋值运算符重载Derived& operator=(const Derived& other) {if (this != &other) {Base::operator=(other); // 调用基类赋值运算符重载derivedValue = other.derivedValue;}return *this;} };int main() {// 创建对象Derived obj1(10, 20);Derived obj2(30, 40);// 赋值对象obj1 = obj2;// 输出赋值后的对象状态std::cout << "obj1: BaseValue = " << obj1.baseValue << ", DerivedValue = " << obj1.derivedValue << std::endl;return 0; }
-
以对象管理资源(资源在构造函数获得,在析构函数释放,建议使用智能指针,资源取得时机便是初始化时机(Resource Acquisition Is Initialization,RAII))
-
在资源管理类中小心 copying 行为(普遍的 RAII class copying 行为是:抑制 copying、引用计数、深度拷贝、转移底部资源拥有权(类似 auto_ptr))
-
在资源管理类中提供对原始资源(raw resources)的访问(对原始资源的访问可能经过显式转换或隐式转换,一般而言显示转换比较安全,隐式转换对客户比较方便)
-
成对使用 new 和 delete 时要采取相同形式(
new
中使用[]
则delete []
,new
中不使用[]
则delete
) -
以独立语句将 newed 对象存储于(置入)智能指针(如果不这样做,可能会因为编译器优化,导致难以察觉的资源泄漏)
-
让接口容易被正确使用,不易被误用(促进正常使用的办法:接口的一致性、内置类型的行为兼容;阻止误用的办法:建立新类型,限制类型上的操作,约束对象值、消除客户的资源管理责任)
-
设计 class 犹如设计 type,需要考虑对象创建、销毁、初始化、赋值、值传递、合法值、继承关系、转换、一般化等等。
-
宁以 pass-by-reference-to-const (传递常量引用)替换 pass-by-value (传值)(前者通常更高效、避免切割问题(slicing problem),但不适用于内置类型、STL迭代器、函数对象)
-
必须返回对象时,别妄想返回其 reference(绝不返回 pointer 或 reference 指向一个 local stack 对象,或返回 reference 指向一个 heap-allocated 对象,或返回 pointer 或 reference 指向一个 local static 对象而有可能同时需要多个这样的对象。)
返回引用存在一些潜在的问题和风险:
- 引用指向局部栈对象:如果函数返回一个指向局部栈对象的引用,在函数执行完毕后,该对象的内存空间会被释放,引用就变成了悬空引用(dangling reference),使用该引用会导致未定义的行为。
- 引用指向堆内存中的对象:如果函数返回一个指向堆内存中对象的引用,并且调用者不知道要负责释放该对象的内存,可能会导致内存泄漏或者二次释放内存的问题。
- 引用指向局部静态对象:如果函数返回一个指向局部静态对象的引用,并且函数可能被多次调用,可能会导致多个引用指向同一个对象,破坏程序的逻辑和数据一致性。
因此,当函数需要返回一个对象时,应该考虑返回对象本身,而不是返回对象的引用。如果需要返回一个对象的引用,应该确保引用指向的对象的生命周期足够长,以避免悬空引用或内存泄漏等问题。
-
将成员变量声明为 private(为了封装、一致性、对其读写精确控制等)
-
宁以 non-member、non-friend 替换 member 函数(可增加封装性、包裹弹性(packaging flexibility)、机能扩充性)
这句话的意思是在设计类的成员函数时,应该优先考虑使用非成员函数(non-member function)而不是成员函数(member function),尤其是当这个函数不需要访问类的私有成员时。使用非成员函数可以增加封装性、包裹弹性和功能扩充性。
让我们详细解释一下这些概念:
- 增加封装性:将相关函数定义为类的成员函数会暴露类的内部实现细节,降低了类的封装性。而将这些函数定义为非成员函数可以将实现与类的接口分离开来,提高了类的封装性,使得类的实现细节更加私有和隐藏。
- 增加包裹弹性:非成员函数可以定义在类的外部,因此可以灵活地将它们放置在不同的命名空间或文件中,提高了代码的组织和维护的灵活性。而成员函数则必须放置在类的定义内部,导致类的定义变得更加臃肿和复杂。
- 增加功能扩充性:非成员函数可以与多个类进行交互,因此具有更好的功能扩展性。当需要在多个类之间共享某个函数时,将这个函数定义为非成员函数可以避免代码的重复定义,提高了代码的复用性和可维护性。
举个例子,假设我们有一个表示二维点的类
Point
,我们需要计算两个点之间的距离。这个函数可以定义为成员函数,也可以定义为非成员函数。如果定义为非成员函数,它可以更灵活地与其他类进行交互,提高了功能扩展性和代码的可维护性。// 非成员函数版本 class Point { public:int x, y;Point(int x, int y) : x(x), y(y) {} };double distance(const Point& p1, const Point& p2) {return sqrt(pow(p1.x - p2.x, 2) + pow(p1.y - p2.y, 2)); }int main() {Point p1(1, 1), p2(4, 5);std::cout << "Distance: " << distance(p1, p2) << std::endl;return 0; }
在这个例子中,
distance()
函数被定义为Point
类的非成员函数,它不需要访问Point
类的私有成员,因此更适合作为非成员函数。这样可以提高代码的封装性、包裹弹性和功能扩展性。 -
若所有参数(包括被this指针所指的那个隐喻参数)皆须要类型转换,请为此采用 non-member 函数
-
考虑写一个不抛异常的 swap 函数
一个不抛异常的
swap
函数应该尽可能地简单、高效,并且不包含任何可能抛出异常的操作。通常情况下,我们可以使用移动语义来实现一个不抛异常的swap
函数,因为移动操作是不抛异常的(移动语义本身不会抛出异常的原因在于它是基于资源的转移,而不是复制。在移动语义中,资源(如内存、文件句柄等)的所有权从一个对象转移到另一个对象,而不涉及资源的复制或分配。因此,移动操作不会引发任何可能导致异常的动态内存分配或其他资源分配/释放操作。)。下面是一个示例实现:
#include <utility>template<typename T> void my_swap(T& a, T& b) noexcept {T temp = std::move(a); // 使用移动语义,避免抛出异常a = std::move(b); // 使用移动语义,避免抛出异常b = std::move(temp); // 使用移动语义,避免抛出异常 }int main() {int a = 1, b = 2;my_swap(a, b);return 0; }
在这个示例中,
my_swap
函数接受两个参数a
和b
,它们都是可移动类型(例如:std::vector
、std::string
等),函数使用移动语义来交换它们的值,从而实现swap
的功能。由于移动操作是不抛出异常的,因此整个my_swap
函数也是不抛异常的。 -
尽可能延后变量定义式的出现时间(尽可能将变量的定义延迟到其首次使用的地方。这样做可以增加程序的清晰度,并且有时候还可以改善程序的效率。)
-
尽量少做转型动作(旧式:
(T)expression
、T(expression)
;新式:const_cast<T>(expression)
、dynamic_cast<T>(expression)
、reinterpret_cast<T>(expression)
、static_cast<T>(expression)
、;尽量避免转型、注重效率避免 dynamic_casts、尽量设计成无需转型、可把转型封装成函数、宁可用新式转型) -
避免使用 handles(包括 引用、指针、迭代器)指向对象内部(以增加封装性、使 const 成员函数的行为更像 const、降低 “虚吊号码牌”(dangling handles,如悬空指针等)的可能性)
-
为 “异常安全” 而努力是值得的(异常安全函数(Exception-safe functions)即使发生异常也不会泄露资源或允许任何数据结构败坏,分为三种可能的保证:基本型、强列型、不抛异常型)
- 强异常安全(strong exception safety):即使在函数执行过程中抛出了异常,函数的执行状态和所有资源都保持不变,程序的状态不会发生任何变化。这是最高级别的异常安全保证,也是最为理想的情况。
- 基本异常安全(basic exception safety):即使在函数执行过程中抛出了异常,函数执行前后的状态仍然保持一致,但可能会有一些资源泄漏或数据损坏的问题,需要在异常处理代码中进行清理操作。
- 无异常安全(no exception safety):函数在执行过程中不具备任何异常安全保证,可能会导致资源泄漏、数据损坏或程序崩溃等问题。
-
透彻了解 inlining 的里里外外(inlining 在大多数 C++ 程序中是编译期的行为;inline 函数是否真正 inline,取决于编译器;大部分编译器拒绝太过复杂(如带有循环或递归)的函数 inlining,而所有对 virtual 函数的调用(除非是最平淡无奇的)也都会使 inlining 落空;inline 造成的代码膨胀可能带来效率损失;inline 函数无法随着程序库的升级而升级)
-
将文件间的编译依存关系降至最低(如果使用 object references 或 object pointers 可以完成任务,就不要使用 objects;如果能够,尽量以 class 声明式替换 class 定义式;为声明式和定义式提供不同的头文件)
-
确定你的 public 继承塑模出 is-a(是一种)关系(适用于 base classes 身上的每一件事情一定适用于 derived classes 身上,因为每一个 derived class 对象也都是一个 base class 对象)
-
避免遮掩继承而来的名字(可使用 using 声明式或转交函数(forwarding functions)来让被遮掩的名字再见天日)
这句话指的是在子类(派生类)中避免使用与父类(基类)相同名称的成员或函数,以避免隐藏(遮掩)父类中相同名称的成员或函数。如果在子类中定义了与父类相同名称的成员或函数,那么子类中的成员或函数会覆盖(遮掩)父类中的同名成员或函数,导致父类中的同名成员或函数无法直接访问。
为了避免这种情况,可以采取以下几种方法:
- 使用 using 声明式:在子类中使用 using 声明式,将父类中的同名成员或函数引入到子类的作用域中,使其重新可见。这样可以明确指定要使用的成员或函数来自于父类。
- 转交函数(Forwarding functions):如果子类中需要调用父类中同名的函数,可以在子类中定义一个转交函数,将调用转发给父类中的同名函数。这样可以避免直接遮掩父类中的同名函数。
通过避免遮掩继承而来的名字,可以提高代码的可读性和可维护性,避免潜在的命名冲突和错误。这对于维护大型项目和实现复杂的继承关系特别重要。
-
区分接口继承和实现继承(在 public 继承之下,derived classes 总是继承 base class 的接口;pure virtual 函数只具体指定接口继承;非纯 impure virtual 函数具体指定接口继承及缺省实现继承;non-virtual 函数具体指定接口继承以及强制性实现继承)
-
考虑 virtual 函数以外的其他选择(如 Template Method 设计模式的 non-virtual interface(NVI)手法,将 virtual 函数替换为 “函数指针成员变量”,以
tr1::function
成员变量替换 virtual 函数,将继承体系内的 virtual 函数替换为另一个继承体系内的 virtual 函数) -
绝不重新定义继承而来的 non-virtual 函数
-
绝不重新定义继承而来的缺省参数值,因为缺省参数值是静态绑定(statically bound),而 virtual 函数却是动态绑定(dynamically bound)
-
通过复合塑模 has-a(有一个)或 “根据某物实现出”(在应用域(application domain),复合意味 has-a(有一个);在实现域(implementation domain),复合意味着 is-implemented-in-terms-of(根据某物实现出))
-
明智而审慎地使用 private 继承(private 继承意味着 is-implemented-in-terms-of(根据某物实现出),尽可能使用复合,当 derived class 需要访问 protected base class 的成员,或需要重新定义继承而来的时候 virtual 函数,或需要 empty base 最优化时,才使用 private 继承)
-
明智而审慎地使用多重继承(多继承比单一继承复杂,可能导致新的歧义性,以及对 virtual 继承的需要,但确有正当用途,如 “public 继承某个 interface class” 和 “private 继承某个协助实现的 class”;virtual 继承可解决多继承下菱形继承的二义性问题,但会增加大小、速度、初始化及赋值的复杂度等等成本)
-
了解隐式接口和编译期多态(class 和 templates 都支持接口(interfaces)和多态(polymorphism);class 的接口是以签名为中心的显式的(explicit),多态则是通过 virtual 函数发生于运行期;template 的接口是奠基于有效表达式的隐式的(implicit),多态则是通过 template 具现化和函数重载解析(function overloading resolution)发生于编译期)
-
了解 typename 的双重意义(声明 template 类型参数是,前缀关键字 class 和 typename 的意义完全相同;请使用关键字 typename 标识嵌套从属类型名称,但不得在基类列(base class lists)或成员初值列(member initialization list)内以它作为 base class 修饰符)
这句话指出了在声明模板类型参数时关键字class
和typename
的等价性,以及在不同的上下文中如何使用它们。
在声明模板类型参数时,class
和 typename
是完全等效的。例如:
template <class T>
class MyClass1 {// ...
};template <typename T>
class MyClass2 {// ...
};
在上面的代码中,MyClass1
和 MyClass2
是等效的,它们都声明了一个模板类,其中类型参数使用了不同的关键字。
然而,当我们在模板类中使用类型参数作为嵌套从属类型名称时,必须使用关键字 typename
。例如:
template <typename T>
class MyClass {
public:// 使用 typename 标识嵌套从属类型名称typename T::NestedType member;
};
这里的 NestedType
是 T
类型的一个嵌套类型,我们使用 typename
来标识它。
然而,在模板类的基类列表或成员初始化列表中,不能使用 typename
作为基类修饰符。例如:
template <typename T>
class Base {// ...
};template <typename T>
class MyClass : public typename Base<T>::NestedType { // 错误,不能使用 typename 作为基类修饰符// ...
};
在上面的代码中,Base<T>::NestedType
被错误地用作基类,而应该在模板类的成员中使用 typename
来标识嵌套从属类型名称。
-
学习处理模板化基类内的名称(可在 derived class templates 内通过
this->
指涉 base class templates 内的成员名称,或藉由一个明白写出的 “base class 资格修饰符” 完成) -
将与参数无关的代码抽离 templates(因类型模板参数(non-type template parameters)而造成代码膨胀往往可以通过函数参数或 class 成员变量替换 template 参数来消除;因类型参数(type parameters)而造成的代码膨胀往往可以通过让带有完全相同二进制表述(binary representations)的实现类型(instantiation types)共享实现码)
-
运用成员函数模板接受所有兼容类型(请使用成员函数模板(member function templates)生成 “可接受所有兼容类型” 的函数;声明 member templates 用于 “泛化 copy 构造” 或 “泛化 assignment 操作” 时还需要声明正常的 copy 构造函数和 copy assignment 操作符)
这句话意味着使用成员函数模板(member function templates)来创建能够接受所有兼容类型的函数。通常,成员函数模板可以用于编写能够处理多种类型的通用代码。当你需要实现泛化的复制构造函数或复制赋值操作符时,通常需要声明成员模板(member templates),但同时也需要声明正常的复制构造函数和复制赋值操作符,以确保代码的完整性和兼容性。
举例来说,假设你有一个类
MyClass
,你想要为它编写一个泛化的复制构造函数和复制赋值操作符,可以使用成员函数模板来实现:class MyClass { public:template<typename T>MyClass(const T& other) {// 泛化的复制构造函数实现}template<typename T>MyClass& operator=(const T& other) {if (this != &other) {// 泛化的复制赋值操作符实现}return *this;}// 正常的复制构造函数MyClass(const MyClass& other) {// 实现}// 正常的复制赋值操作符MyClass& operator=(const MyClass& other) {if (this != &other) {// 实现}return *this;} };
这样,你就可以使用成员函数模板来创建能够接受各种类型的函数,并保留了正常的复制构造函数和复制赋值操作符,以确保代码的完整性和兼容性。
-
需要类型转换时请为模板定义非成员函数(当我们编写一个 class template,而它所提供之 “与此 template 相关的” 函数支持 “所有参数之隐式类型转换” 时,请将那些函数定义为 “class template 内部的 friend 函数”)
-
请使用 traits classes 表现类型信息(traits classes 通过 templates 和 “templates 特化” 使得 “类型相关信息” 在编译期可用,通过重载技术(overloading)实现在编译期对类型执行 if…else 测试)
举个简单的例子来说明使用traits classes的方法。
假设我们有一个需求:我们想要编写一个函数
printSize
,它可以打印出任意类型的大小。我们可以使用traits classes来实现这个功能,让我们看看具体的代码:#include <iostream>// 定义一个 traits class 来获取类型的大小信息 template <typename T> struct TypeSize {static constexpr int size = sizeof(T); };// 辅助函数,用于打印类型的大小 template <typename T> void printSize() {std::cout << "Size of type T is: " << TypeSize<T>::size << std::endl; }int main() {// 打印 int 类型的大小printSize<int>();// 打印 double 类型的大小printSize<double>();// 打印 char 类型的大小printSize<char>();return 0; }
在这个例子中,我们定义了一个traits class
TypeSize
,它包含一个静态成员size
,用于获取类型的大小。然后,我们编写了一个辅助函数printSize
,它使用traits class来获取类型的大小,并将其打印出来。通过这个例子,我们可以看到traits classes的使用方法:通过定义一个traits class来获取类型相关的信息,然后在其他地方使用该traits class来实现类型相关的操作。traits classes提供了一种灵活且类型安全的方式来处理类型相关的信息。
-
认识 template 元编程(模板元编程(TMP,template metaprogramming)可将工作由运行期移往编译期,因此得以实现早期错误侦测和更高的执行效率;TMP 可被用来生成 “给予政策选择组合”(based on combinations of policy choices)的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码)
#include <iostream>// 模板元编程计算斐波那契数列中的第N个数 template <int N> struct Fibonacci {static const int value = Fibonacci<N - 1>::value + Fibonacci<N - 2>::value; };// 基础情况,斐波那契数列的定义 template <> struct Fibonacci<0> {static const int value = 0; };template <> struct Fibonacci<1> {static const int value = 1; };int main() {// 计算斐波那契数列中的第10个数const int result = Fibonacci<10>::value;std::cout << "Fibonacci<10> = " << result << std::endl;return 0; }
-
了解 new-handler 的行为(set_new_handler 允许客户指定一个在内存分配无法获得满足时被调用的函数;nothrow new 是一个颇具局限的工具,因为它只适用于内存分配(operator new),后继的构造函数调用还是可能抛出异常)
new-handler
是一个函数指针,用于处理动态内存分配失败的情况。在C++中,当使用new
运算符分配内存失败时(例如内存耗尽),默认情况下会抛出std::bad_alloc
异常。然而,程序员可以通过设置new-handler
来定义自己的内存分配失败处理函数,以便更好地控制程序的行为。一旦内存分配失败,C++ 运行时系统将检查是否已设置了
new-handler
。如果设置了,它将调用指定的处理函数,而不是抛出异常。这样,程序员可以在new-handler
中执行一些操作,如释放一些不必要的内存、记录日志、尝试释放其他资源等。通过使用自定义的
new-handler
,程序员可以更灵活地处理内存分配失败的情况,而不是简单地抛出异常,从而提高程序的健壮性和可靠性。以下是一个简单的示例,演示了如何设置和使用
new-handler
:#include <iostream> #include <cstdlib> // 包含了 std::set_new_handler// 自定义的 new-handler 函数 void myNewHandler() {std::cout << "Custom new-handler called. Memory allocation failed!" << std::endl;std::exit(1); // 退出程序 }int main() {// 设置自定义的 new-handlerstd::set_new_handler(myNewHandler);// 尝试分配大量内存,会触发内存分配失败int* ptr = new int[1000000000000];// 如果内存分配失败,并且没有抛出异常,则会调用自定义的 new-handler 函数return 0; }
在这个例子中,我们首先使用
std::set_new_handler
函数设置了自定义的new-handler
,然后尝试分配一个非常大的内存块,会导致内存分配失败。此时,程序会调用自定义的new-handler
函数来处理内存分配失败的情况。- nothrow new:使用
nothrow new
进行内存分配时,如果内存不足,它不会抛出异常,而是返回一个空指针。这意味着你可以通过检查返回的指针是否为空来判断内存分配是否成功,而无需处理异常。 - 构造函数调用可能抛出异常:即使使用了
nothrow new
进行内存分配,但在后续的构造函数调用过程中,仍然有可能抛出异常。这是因为构造函数内部的代码可能会执行一些可能导致异常的操作,如动态内存分配、文件IO等。如果构造函数抛出异常,那么对象的构造将失败,导致内存泄漏和未定义行为。
- nothrow new:使用
-
了解 new 和 delete 的合理替换时机(为了检测运用错误、收集动态分配内存之使用统计信息、增加分配和归还速度、降低缺省内存管理器带来的空间额外开销、弥补缺省分配器中的非最佳齐位、将相关对象成簇集中、获得非传统的行为)
合理替换
new
和delete
的时机通常取决于你的应用程序的特定需求和性能考虑。然而,有一些一般性的建议可以帮助你确定何时应该考虑替换它们:- 性能优化:如果你的应用程序中频繁地使用
new
和delete
来分配和释放内存,并且对性能要求很高,你可能需要考虑替换为自定义的内存管理机制,如内存池或对象池。这样可以减少内存分配和释放的开销,并且可以更好地管理内存碎片化。 - 定制的内存分配策略:如果你需要特定的内存分配策略,例如按大小分配内存块、使用特定的内存对齐方式等,你可能需要替换
new
和delete
。你可以通过重载全局的operator new
和operator delete
来实现自定义的内存分配策略。 - 跟踪内存泄漏和调试:在调试过程中,你可能需要跟踪内存分配和释放的情况,以检测内存泄漏等问题。你可以通过重载
new
和delete
运算符来插入跟踪代码,从而更好地调试和分析内存管理问题。 - 特定平台或环境的需求:在某些特定的平台或环境中,可能存在对内存管理的特殊要求或限制。例如,嵌入式系统可能有特定的内存管理策略,需要使用定制的内存分配方案。在这种情况下,你可能需要替换
new
和delete
来满足特定的需求。
总的来说,合理替换
new
和delete
的时机取决于你的应用程序的具体需求和性能考虑。在做出决定之前,最好仔细评估你的应用程序的内存管理需求,并根据实际情况进行选择。 - 性能优化:如果你的应用程序中频繁地使用
-
编写 new 和 delete 时需固守常规(operator new 应该内涵一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就应该调用 new-handler,它也应该有能力处理 0 bytes 申请,class 专属版本则还应该处理 “比正确大小更大的(错误)申请”;operator delete 应该在收到 null 指针时不做任何事,class 专属版本则还应该处理 “比正确大小更大的(错误)申请”)
-
写了 placement new 也要写 placement delete(当你写一个 placement operator new,请确定也写出了对应的 placement operator delete,否则可能会发生隐微而时断时续的内存泄漏;当你声明 placement new 和 placement delete,请确定不要无意识(非故意)地遮掩了它们地正常版本)
可以使用
delete[] buffer
来释放placement new
分配的内存块。这个操作将调用buffer
对应类型的析构函数,并释放内存块。然而,需要注意的是,当你使用
placement new
在已分配的内存块中构造对象时,你必须手动调用对象的析构函数,而不是使用delete
或delete[]
。因为delete
或delete[]
会尝试调用对象的析构函数并释放内存,但这会导致 undefined behavior,因为对象是在自定义的内存块中构造的,而不是在使用new
或new[]
分配的标准内存块中。 -
不要轻忽编译器的警告
-
让自己熟悉包括 TR1 在内的标准程序库(TR1,C++ Technical Report 1,C++11 标准的草稿文件)
-
让自己熟悉 Boost(准标准库)
More Effective c++
-
仔细区别 pointers 和 references(当你知道你需要指向某个东西,而且绝不会改变指向其他东西,或是当你实现一个操作符而其语法需求无法由 pointers 达成,你就应该选择 references;任何其他时候,请采用 pointers)
-
最好使用 C++ 转型操作符(
static_cast
、const_cast
、dynamic_cast
、reinterpret_cast
) -
绝不要以多态(polymorphically)方式处理数组(多态(polymorphism)和指针算术不能混用;数组对象几乎总是会涉及指针的算术运算,所以数组和多态不要混用)
-
非必要不提供 default constructor(避免对象中的字段被无意义地初始化)
-
对定制的 “类型转换函数” 保持警觉(单自变量 constructors 可通过简易法(explicit 关键字)或代理类(proxy classes)来避免编译器误用;隐式类型转换操作符可改为显式的 member function 来避免非预期行为)
-
区别 increment/decrement 操作符的前置(prefix)和后置(postfix)形式(前置式累加后取出,返回一个 reference;后置式取出后累加,返回一个 const 对象;处理用户定制类型时,应该尽可能使用前置式 increment;后置式的实现应以其前置式兄弟为基础)
以下是一个简单的示例代码,演示了如何定义和使用前置和后置形式的递增/递减运算符:
#include <iostream>class Counter { private:int count;public:Counter(int initialCount) : count(initialCount) {}// 前置形式的递增运算符Counter& operator++() {++count;return *this;}// 后置形式的递增运算符Counter operator++(int) {Counter temp(*this);++count;return temp;}// 前置形式的递减运算符Counter& operator--() {--count;return *this;}// 后置形式的递减运算符Counter operator--(int) {Counter temp(*this);--count;return temp;}// 打印当前计数值void printCount() const {std::cout << "Current count: " << count << std::endl;} };int main() {Counter c(0);// 使用前置形式的递增运算符++c;c.printCount(); // 输出: Current count: 1// 使用后置形式的递增运算符Counter d = c++;d.printCount(); // 输出: Current count: 1c.printCount(); // 输出: Current count: 2// 使用前置形式的递减运算符--c;c.printCount(); // 输出: Current count: 1// 使用后置形式的递减运算符d = c--;d.printCount(); // 输出: Current count: 1c.printCount(); // 输出: Current count: 0return 0; }
在这个示例中,
Counter
类表示一个计数器,包含了前置和后置形式的递增/递减运算符的定义。在main
函数中,我们演示了如何使用这些运算符来增加或减少计数,并打印出当前的计数值。 -
千万不要重载
&&
,||
和,
操作符(&&
与||
的重载会用 “函数调用语义” 取代 “骤死式语义”;,
的重载导致不能保证左侧表达式一定比右侧表达式更早被评估) -
了解各种不同意义的 new 和 delete(
new operator
、operator new
、placement new
、operator new[]
;delete operator
、operator delete
、destructor
、operator delete[]
) -
利用 destructors 避免泄漏资源(在 destructors 释放资源可以避免异常时的资源泄漏)
-
在 constructors 内阻止资源泄漏(由于 C++ 只会析构已构造完成的对象,因此在构造函数可以使用 try…catch 或者 auto_ptr(以及与之相似的 classes) 处理异常时资源泄露问题)
-
禁止异常流出 destructors 之外(原因:一、避免 terminate 函数在 exception 传播过程的栈展开(stack-unwinding)机制种被调用;二、协助确保 destructors 完成其应该完成的所有事情)
-
了解 “抛出一个 exception” 与 “传递一个参数” 或 “调用一个虚函数” 之间的差异(第一,exception objects 总是会被复制(by pointer 除外),如果以 by value 方式捕捉甚至被复制两次,而传递给函数参数的对象则不一定得复制;第二,“被抛出成为 exceptions” 的对象,其被允许的类型转换动作比 “被传递到函数去” 的对象少;第三,catch 子句以其 “出现于源代码的顺序” 被编译器检验对比,其中第一个匹配成功者便执行,而调用一个虚函数,被选中执行的是那个 “与对象类型最佳吻合” 的函数)
-
以 by reference 方式捕获 exceptions(可避免对象删除问题、exception objects 的切割问题,可保留捕捉标准 exceptions 的能力,可约束 exception object 需要复制的次数)
-
明智运用 exception specifications(exception specifications 对 “函数希望抛出什么样的 exceptions” 提供了卓越的说明;也有一些缺点,包括编译器只对它们做局部性检验而很容易不经意地违反,与可能会妨碍更上层的 exception 处理函数处理未预期的 exceptions)
异常规格是 C++ 中的一种特性,用于指定函数可能抛出的异常类型。在函数声明或定义中,可以使用异常规格来说明函数可能抛出的异常类型,以便调用者了解函数的行为。异常规格的一般形式如下:
return_type function_name(parameters) throw(exception_list);
其中:
return_type
是函数的返回类型。function_name
是函数的名称。parameters
是函数的参数列表。throw(exception_list)
指定了函数可能抛出的异常类型列表。
exception_list
是一个以逗号分隔的异常类型列表,每个异常类型可以是标准异常类型、用户自定义的异常类型或...
(表示函数可以抛出任何类型的异常)。有两种形式的异常规格:
-
动态异常规格:指定函数可能抛出的异常类型。例如:
void myFunction() throw(MyException, std::runtime_error);
这表示
myFunction
可能会抛出MyException
或std::runtime_error
类型的异常。 -
空异常规格:指定函数不会抛出任何异常。例如:
void myFunction() throw();
这表示
myFunction
不会抛出任何异常。
需要注意的是,异常规格在 C++11 中已被弃用,并在 C++17 中被移除。这是因为异常规格并不能提供强大的异常安全性保证,而且很难与现代 C++ 特性(如移动语义和模板)兼容。相反,现代 C++ 中更推荐使用异常安全性的最佳实践和异常处理技术来管理异常。
-
了解异常处理的成本(粗略估计,如果使用 try 语句块,代码大约整体膨胀 5%-10%,执行速度亦大约下降这个数;因此请将你对 try 语句块和 exception specifications 的使用限制于非用不可的地点,并且在真正异常的情况下才抛出 exceptions)
-
谨记 80-20 法则(软件的整体性能几乎总是由其构成要素(代码)的一小部分决定的,可使用程序分析器(program profiler)识别出消耗资源的代码)
-
考虑使用 lazy evaluation(缓式评估)(可应用于:Reference Counting(引用计数)来避免非必要的对象复制、区分 operator[] 的读和写动作来做不同的事情、Lazy Fetching(缓式取出)来避免非必要的数据库读取动作、Lazy Expression Evaluation(表达式缓评估)来避免非必要的数值计算动作)
这段话提到了一种编程技术,即 lazy evaluation(缓式评估),它的核心思想是将计算推迟到真正需要的时候再执行,而不是立即执行。这样可以节省计算资源,并提高程序的性能和效率。
在不同的场景下,可以应用 lazy evaluation 来实现不同的优化:
- Reference Counting(引用计数):在引用计数技术中,通过延迟对象复制的操作来避免不必要的对象复制。只有在对象被修改时才执行实际的复制操作,而在只读操作中共享相同的对象引用。
- 区分 operator[] 的读和写动作:对于类似于数组或映射的数据结构,可以区分读取和写入操作。如果只是读取数据,可以延迟执行,直到真正需要读取数据时才进行。这样可以避免在不需要的情况下执行不必要的读取操作。
- Lazy Fetching(缓式取出):在需要从外部数据源(如数据库)获取数据时,可以延迟实际的数据库读取操作,直到数据真正被需要时才执行。这样可以减少不必要的数据库访问,提高程序的性能。
- Lazy Expression Evaluation(表达式缓评估):在进行数值计算时,可以延迟实际的计算操作,直到计算结果被需要时才执行。这样可以避免在不需要的情况下执行不必要的数值计算,提高程序的效率。
总的来说,lazy evaluation 是一种重要的优化技术,可以在需要时才执行计算,从而提高程序的性能和效率,减少不必要的资源消耗。
理解上述优化技术后,我们可以通过代码示例来演示它们的应用:
- Reference Counting(引用计数):
#include <iostream> #include <memory>class MyClass { public:MyClass(int data) : mData(data) {}// 虚析构函数确保通过基类指针删除派生类对象时,正确地释放资源virtual ~MyClass() {}// 某些复杂操作可能会改变对象状态,需要复制void modifyData() {std::cout << "Modifying data" << std::endl;// 这里执行修改操作...}int getData() const {return mData;}private:int mData;// 其他数据成员... };int main() {std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(10);std::shared_ptr<MyClass> ptr2 = ptr1; // 共享指针,不会引起额外的复制std::cout << "Data in ptr1: " << ptr1->getData() << std::endl;std::cout << "Data in ptr2: " << ptr2->getData() << std::endl;// 对象被修改,需要复制ptr1->modifyData();std::cout << "Data in ptr1 after modification: " << ptr1->getData() << std::endl;std::cout << "Data in ptr2 after modification: " << ptr2->getData() << std::endl;return 0; }
- 区分 operator[] 的读和写动作:
#include <iostream> #include <vector>class MyArray { public:// 重载 operator[],支持读取和写入操作int& operator[](int index) {std::cout << "Writing to index " << index << std::endl;// 这里执行写入操作...return mData[index];}const int& operator[](int index) const {std::cout << "Reading from index " << index << std::endl;// 这里执行读取操作...return mData[index];}private:std::vector<int> mData; };int main() {MyArray arr;arr[0] = 10; // 写入操作int value = arr[0]; // 读取操作return 0; }
- Lazy Fetching(缓式取出):
#include <iostream>class Database { public:int getData() {std::cout << "Fetching data from database" << std::endl;// 这里执行实际的数据库读取操作...return 42; // 假设这里返回从数据库中获取的数据} };int main() {Database db;// 数据不会立即从数据库中读取,直到真正需要时才执行读取操作int data = db.getData();return 0; }
- Lazy Expression Evaluation(表达式缓评估):
#include <iostream>int heavyCalculation(int x, int y) {std::cout << "Performing heavy calculation" << std::endl;// 这里执行复杂的计算操作...return x + y; // 假设这里返回计算结果 }int main() {int x = 5;int y = 10;// 计算结果不会立即生成,直到真正需要时才执行计算操作int result = heavyCalculation(x, y);return 0; }
以上示例演示了如何在代码中应用 lazy evaluation 技术来避免不必要的计算或操作,从而提高程序的效率。
-
分期摊还预期的计算成本(当你必须支持某些运算而其结构几乎总是被需要,或其结果常常被多次需要的时候,over-eager evaluation(超急评估)可以改善程序效率)
举个简单的例子,假设有一个复杂的数值计算过程,但是其结果并不总是立即需要,而是在后续的操作中可能被多次使用。在这种情况下,可以延迟计算过程,直到真正需要计算结果时才进行。这样可以避免在不需要结果的情况下进行不必要的计算,从而提高程序的效率。