Effective C++第一版大约在1990年,第二版1997年,第三版2005年。
第四版变了名字《Effective Modern C++》,因为c++11、c++14变更较大,c++17、c++23对应的似乎还没有。
其他参考书:《Effective STL》、More Effective C++。
以下为概要学习笔记,如果太长就分篇:
软件的目标:正确、时间空间高效、稳定、易理解维护、易扩展、可移植、与底层和上层匹配…
X t =t1; // 调用的是拷贝构造函数,并不是operator=拷贝赋值函数。
拷贝构造函数在传值参数时需要。void api(X t);
个人习惯是为类写好所有范式函数,避免遇到模板编译不通过、非预期行为这种问题。
ctor 构造函数。dtor 析构函数。
lhs left-hand side , rhs right-hand side
a*b
operator*(a,b);
const X operator*(const X &lhs,const X &rhs);//二元操作符定义范式
c++出现的早期,没有multithreaded多线程概念,所以早期c++实际天然地不支持多线程。
TR1(Technical Report 1)是类似Boost的一个库。注意这本书是2005年的,有些库已经过时了。
条款01 :c++是一个多语言联邦。
主要次语言概念:c(procedural过程)、class(object-oriented面向对象)、template(generic泛型metaprogramming元编程)、函数形式functional、stl库。不同范畴的规则范式建议各有区别,关键看本质。
stl迭代器、函数对象,本质是指针。
内置类型传值比较好,有些是cpu指令集所支持的,例如c语言里面的复数。
自定义类型,较大对象建议传引用、本质是指针。
条款02 :尽量用const、enum、inline替代#define
单纯的常量,尽量用const、enum。类似函数的宏改用inline。
本质是面向编译器编程,减少对预处理器的依赖。
编译器有更多检查,更安全。预处理器简单粗暴,缺乏安全性。
#define被预处理器替换后,缺乏符号表,记号式调试器无法体现,不容易看出问题。
#define PI 3.14159265358979323846264338327650288419716939937510
const double PI=3.14159265358979323846264338327650288419716939937510; //推荐
const char* const s="PI";
const std::string s("PI"); //推荐
class X{
enum{kI=42};//用枚举可以定义常量
//__LINE__也可用于定义常量
private:
static const int i=42; //h中,新版cpp编译器支持h中给出整数初值,浮点数不行
static const double d;
};
const int X::i;//cpp中
const int X::d=4.2;
#define Max(a,b) (a)>(b)?(a):(b) //废弃
template<typename T>inline const T& max(const T& a,const T& b){return a>b?a:b;} //推荐
用__LINE__和函数内无名enum推算常量,用于定义固定长度数组
关键点在于:利用编译器在编译期,通过函数内无名enum和__LINE__自动获得函数内变量总量。
enum hack是模板元编程template metaprogramming的基础。
条款03 :尽量用const
本质是面向编译器编程,概念清晰一致。
告诉编译器更多的程序概念细节,方便优化、减少错误。
注意新的关键字,mutable可变的,与const对应。
const函数中可以修改mutable成员。
class X{
mutable std::size_t i;
std::size_t length()const{ //const函数
i = std::strlen(p); //修改了mutable成员
return i;
}
};
另外,const本质是编译期的事情,告诉编译器如何更好地处理代码。
所以就有剔除const修饰的语法支持
const T& operator[](std::size_t i)const{return v[i];}
T& operator[](std::size_t i){
return
const_cast<T&>( //剔除const修饰
static_cast<const TT&>(*this) //加上const修饰,使得自动调用const operator[]
[i] //调用const operator[]
);
}
const_cast用于修改值的const或volatile属性,删除const或者添加const属性,实际主要是用于一种情况:删除const属性。
static_cast用于在编译时可以确定的类型转换,支持派生类到基类,不支持基类到派生类。
dynamic_cast用于需要运行时信息来确定是否可以执行的类型转换,支持基类到派生类。
c风格的转换也是可以的,但是没有cpp的安全,毕竟cpp的信息更多更明确。
非const函数可以调用const函数,安全级别低的调高的。反之则不合适,概念上就出现问题了。
条款04 :确定对象被使用前已经被初始化
int i = 0; // 初始化使得概念明确化,避免不确定的初始化值引起bug。
c array不保证初始化,cpp vector保证了初始化,存在运行期成本的差别,各有取舍。
如果是超大内存,这事又另当别论。
cpp class每个构造函数都保证初始化每个成员,且用初始化列表,而不是赋值。
基类先初始化,派生类在后面初始化。
成员按照定义顺序初始化,int size;与T v[];定义时就要注意顺序。
static对象会在main函数结束后析构。
用函数内的局部static变量控制它们的初始化时序;如果有多线程隐患,那就在初始阶段单线程调用一遍。
条款05 :了解C++默默编写并调用哪些函数(Know what functions C++ silently writes and calls)
编译期自动补全的函数只有在被使用时才会被创建。
类似模板函数的实例化,没有用过的类型版本是不会被创建的。
析构函数默认是非虚版本,原因是降低成本(虚函数表的空间占用、运行时开销)。
如果含有引用成员、const成员等,就没有默认的成员拷贝,涉及引用不可变等问题。
class X{}; //你看到的版本
class X{ //实际的版本,编译期自动补全的版本
public:
inline X(){} //默认构造函数。X x;触发。如果定义了其他版本,就不会有默认版本。
inline X(const X &t){} //拷贝构造函数。X x(t);触发。
inline ~X(){} //析构函数,非虚。X x;触发。
//virtual ~X(){} //虚析构函数。当基类有虚析构函数时才会是这种版本。
inline X&operator=()(const X &t){} //拷贝赋值,x=t;触发
};
个人认为这一条的本质是:编译器会尽量帮着做所有可能做的事情,安全高效省事。
条款06 :若不想使用编译器自动生成的函数,就该明确拒绝。(Explicitly disallow the use of compiler-generated functions you do not want)
核心是:准确表达概念,不应有歧义。
以下形成Uncopyable模式。同时给出新旧两种方案。
class Uncopyable{
protected: //防止X被直接使用,此处protected可以使得X成为一种模式,仅仅是赋予派生类不可拷贝的模式。
Uncopyable() {}
~Uncopyable() {}
private: //通过私有函数阻止拷贝构造和赋值拷贝,旧方案,实际对于friend类存在隐患。
Uncopyable(const Uncopyable&);
Uncopyable& operator= (const Uncopyable&);
protected: //以下为c++11的语法新方案,不受访问级别影响
Uncopyable() = default; // 使用=default来明确表示使用默认的构造行为
~Uncopyable() = default;
Uncopyable(const Uncopyable&) = delete;// 显式地删除拷贝构造函数和拷贝赋值运算符
Uncopyable& operator= (const Uncopyable&) = delete;
};
private:只有同一个类的成员函数可以访问此成员。
protected:同一个类的成员函数和其子类(派生类)可以访问此成员。
public:任何函数都可以访问此成员。
条款07 :为多态基类声明virtual析构函数(Declare destructors virtual in polymorphic base classes)
delete基类或者派生类指针时,如果没有虚析构函数,只会调用当前delete语句中的类型析构,否则就从实际new出来的析构开始调用(查虚函数表)。
template <class _CharT, class _Traits, class _Alloc>
class basic_string : private _String_base<_CharT,_Alloc>
std中的string、basic_string、_String_base都没有虚函数。因为std string的设计初衷里面就没有多态polymorphically。
polymorphic(带多态性质的)base classes的设计目的是为了用来”通过base class接口处理derived class对象”,但std中的_String_base并不是。
vptr(virtual table pointer)指向一个由函数指针构成的数组,称为vtbl(virtual table)。每一个带有virtual函数的class都有一个相应的vtbl。当对象调用某一virtual函数,实际被调用的函数取决于该对象的vptr所指的那个vtbl----编译器在其中寻找适当的函数指针。个人认为是在new的时候决定的vptr值。
另外,对于小对象,也尽量不要用虚函数,否则内存消耗增大50%甚至100%,对于海量对象就是一个灾难。
TODO:std中无虚函数的基类模式,设计思想。估计是只会对派生类进行简单new delete,不涉及内存泄漏。这种级别的源码往往要求编译器也配合语法支持,甚至msvc还有自己的语法。核心是语义的表达和编译支持。
条款08 :别让异常逃离析构函数(Prevent exceptions from leaving destructors)
cpp编译期并不禁止析构函数吐出异常,但不建议,因为array的析构异常会导致内存泄漏。
解决方法:在析构函数中try catch,处理异常,std::abort()或输出错误日志。
或者将可能出现异常的行为交由用户决断、调用,用户可单独调用的函数处理可能有异常的事务,析构函数只做最后的防御。
class DBConn {
public:
void close(){//供客户使用的新函数
db.close( );closed = true;
}
~DBConn(){
if (!closed){
try{
db.close();//关闭连接(如果客户不那么做的话)
}catch (...) {//如果关闭动作失败
//制作运转记录,记下对 close 的调用失败.
//记录下来并结束程序,或吞下异常。
}
}
}
private:
DBConnection db;
bool closed;
};
条款09 :绝不在构造和析构过程中调用virtual函数(Never call virtual functions during construction or destruction)
因为对象构造析构的时序问题,构造和析构过程中调用virtual函数可能会导致访问未初始化或者已经释放的对象。
另外,base class构造期间,虚函数不是虚函数,甚至类型信息也是基类信息。
另外,间接调用也不行。
条款10 :令operator=返回一个reference to *this(Have assignment operators return a reference to *this)
不遵循return *this也可以,只是不符合约定俗成。
class X{
public:
X& operator= (const X& rhs){return *this;} // 返回类型是个reference,指向当前对象
X& operator+= (const X& rhs){return *this;} // 这个协议适用于+=、-=、*=等等
X& operator= (int rhs){return *this;} // 此函数也适用,即使此一操作符的参数类型不符协定
};
条款11 :在operator=中处理”自我赋值”(Handle assignment to self in operator=)
防止bug的一种编码范式。实际并不一定只有operator=存在这种潜在的bug,注意考虑这种特例。
Widget& Widget::operator=(const widget& rhs){
if (this == &rhs) return *this;//证同测试(identity test) : 如果是自我赋值,就不做任何事
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
条款12 :复制对象时勿忘其每一个成分(Copy all parts of an object)
除了当前类的,还需要关注基类的,BaseClass::operator=(rhs);
条款13 :以对象管理资源(Use objects to manage resources)
资源:堆栈内存、文件、互斥锁、UI中的字体笔刷、数据库链接、网络sockets等等。
把资源放进对象内,利用C++的”析构函数自动调用机制”确保资源被释放。
auto_ptr是个”类指针(pointer-like)对象”,也就是所谓的”智能指针”,其析构函数自动对其所指对象调用delete。一定要注意别让多个auto_ptr同时指向同一对象,如果真是那样,对象会被删除一次以上。
在C++11中auto_ptr已经被废弃,用unique_ptr替代。auto_ptr不支持部分std容器。
shared_ptr允许多个指针指向同一个对象,但存在环状引用问题。
unique_ptr则"独占"所指向的对象。”所有权”仅能够通过标准库的move函数来转移。unique_ptr是一个删除了拷贝构造函数、保留了移动构造函数的指针封装类型。
template <class _Ty, class _Dx /* = default_delete<_Ty> */>
class unique_ptr{
public:
...
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
...
};
RAII(Resource Acquisition Is Initialization,资源取得时机便是初始化时机)对象,它们在构造函数中获得资源并在析构函数中释放资源.
”引用计数型智慧指针”(reference-counting smart pointer, RCSP)。所谓RCSP也是个智能指针,持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。类似于C++11中的shared_ptr。
条款14 :在资源管理类中小心copying行为(Think carefully about copying behavior in resource-managing classes)
(1).复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。(2).普遍而常见的RAII class copying行为是:抑制copying、施行引用计数法(reference counting)。不过其它行为也都可能被实现。
这一条也是内存管理的建议,比较基础和常规。
#include <iostream>
#include <memory>
class ResourceManager {
private:
int* resource;
public:
ResourceManager() : resource(new int(5)) {}
~ResourceManager() { delete resource; }
ResourceManager(const ResourceManager& other) : resource(new int(*other.resource)) {}
ResourceManager& operator=(const ResourceManager& other) {
if (this != &other) {
delete resource;
resource = new int(*other.resource);
}
return *this;
}
// 示例中的RAII类的另一种选择是禁止复制
// ResourceManager(const ResourceManager& other) = delete;
// ResourceManager& operator=(const ResourceManager& other) = delete;
};
条款15 :在资源管理类中提供对原始资源的访问(Provide access to raw resources in resource-managing classes)
请记住:(1).APIs往往要求访问原始资源(raw resources),所以每一个RAII class应该提供一个”取得其所管理之资源”的办法。(2).对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较安全,但隐式转换对客户比较方便。
#include <iostream>
#include <cstring>
#include <memory>
class StringResource {
private:
std::unique_ptr<char[]> str;
public:
StringResource(const char* s) : str(new char[strlen(s) + 1]) {
strcpy(str.get(), s);
}
const char* getRawString() const {return str.get();}
operator const char* () const {return str.get();}
};
条款16 :成对使用new和delete时要采用相同形式(Use the same form in corresponding uses of new and delete)
这个的原因在之前的文章有讲过,https://blog.csdn.net/weixin_43172531/article/details/133872314
Effective C++ 2.0 版,条款5:对应的new和delete要采用相同的形式
本质是new[]时,在指针前面有16byte用于管理数组大小,编译期生成了不同的代码和数据结构。
条款17 :以独立语句将new对象置入智能指针(Store newed objects in smart pointers in standalone statements)
以独立语句将new对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。
class Widget17 {};
int priority() { return 0; }
void processWidget(std::shared_ptr<Widget17> pw, int priority) {}
int test_item_17(){
// 执行new Widget17; 调用priority; 调用std::shared_ptr构造函数,它们的执行顺序不确定
processWidget(std::shared_ptr<Widget17>(new Widget17), priority()); // 可能泄露资源
std::shared_ptr<Widget17> pw(new Widget17); // 在单独语句内以智能指针存储newed所得对象
processWidget(pw, priority()); // 这个调用动作绝不至于造成泄露
return 0;
}
条款18 :让接口容易被正确使用,不易被误用(Make interface easy to use correctly and hard to use incorrectly)
(1).好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。(2).”促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
(3).”阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
(4).std::shared_ptr支持定制型删除器(custom deleter)。这可防范DLL问题(动态链接库里面new外面delete),可被用来自动解除互斥锁(mutex)等等。
class CustomString {
public:
// 可以通过复制构造函数和赋值操作符对接口进行一致性设计
CustomString(const char* str);
CustomString(const CustomString& other);
CustomString& operator=(const CustomString& other);
// 与内置类型兼容的显示转换
explicit operator const char*() const;
};
void processString(const CustomString& str);
class NonCopyableResource {
public:
NonCopyableResource();
NonCopyableResource(const NonCopyableResource&) = delete; // 删除复制构造函数
NonCopyableResource& operator=(const NonCopyableResource&) = delete; // 删除赋值操作符
};
void closeFile(FILE* file) {
// 使用std::shared_ptr并传入lambda作为定制型删除器
std::shared_ptr<FILE> filePtr(file, [](FILE* f) { fclose(f); });
// 无需手动关闭文件,离开作用域后自动调用fclose
}
条款19 :设计class犹如设计type(Treat class design as type design)
class Shape {
public:
virtual double area() const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
public:
double area() const override { return 3.14 * radius_ * radius_; }
private:
double radius_;
};
class Square : public Shape {
public:
double area() const override { return side_ * side_; }
private:
double side_;
};
class Email {
public:
explicit Email(const std::string& address) : address_(address) {}
std::string getAddress() const { return address_; }
private:
std::string address_;
};
void sendEmail(const Email& email) {
// 发送电子邮件
}
条款20 :以pass-by-reference-to-const替换pass-by-value(Prefer pass-by-reference-to-const to pass-by-value)
如果你有个对象属于内置类型(例如int),pass by value往往比pass by reference的效率高些。
请记住:(1).尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题(slicing problem)。(2).以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对它们而言,pass-by-value往往比较适当。
切割问题(slicing problem)通常出现在传递派生类对象的基类接口时,使用值传递的情况下。
//Shape是基类,派生类对象传进来时变成了基类对象,派生类的信息被切割了。
void drawShape(Shape s) {
s.draw();
}
条款21 :必须返回对象时,别妄想返回其reference(Don’t try to return a reference when you must return an object)
绝不要返回pointer或reference指向一个local stack对象,本地栈对象。
或返回reference指向一个heap-allocated对象,堆上申请的对象。
或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。
实际还有一个原因:返回引用可能会被滥用,导致值被莫名其妙地改变。虽然可以提高效率,但可能要根据具体情况谨慎使用。
条款22 :将成员变量声明为private(Declare data members private)
(1).切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。
(2).protected并不比public更具封装性。
条款23 :宁以non-member、non-friend替换member函数(Prefer non-member non-friend functions to member functions)
宁可拿non-member non-friend函数替换member函数。这样做可以增加封装性、包裹弹性(packaging flexibility)和机能扩充性。
class Point {
public:
float x, y;
};
// 非成员非友元函数
void offset(Point& point, float offsetX, float offsetY) {
point.x += offsetX;
point.y += offsetY;
}
类似std的几十个头文件,目标是分开定义,最小化编译。
条款24 :若所有参数皆需类型转换,请为此采用non-member函数(Declare non-member functions when type conversions should apply to all parameters)
无论何时如果你可以避免friend函数就该避免。
如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member。
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;
};
// Operator overloads implemented as non-member functions
const Rational operator*(const Rational& lhs, const Rational& rhs) {
return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
int test(){
Rational24 oneEighth(1, 8);
Rational24 oneHalf(1, 2);
Rational24 result = oneHalf * oneEighth;
result = result * oneEighth;
result = oneHalf * 2; // 隐式类型转换(implicit type conversion)
result = 2 * oneHalf; // 也可以隐式类型转换
return 0;
}
条款25 :考虑写出一个不抛异常的swap函数(Consider support for a non-throwing swap)
缺省情况下swap动作可由标准程序库提供的swap算法完成。
(1).当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。(2).如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于classes(而非template),也请特化std::swap。(3).调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何”命名空间资格修饰”。(4).为”用户定义类型”进行std template全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。
class WidgetImpl { // 针对Widget数据而设计的class
private:
int a, b, c; // 可能有许多数据,意味复制时间很长
std::vector<double> v;
};
class Widget { // 这个class使用pimpl(pointer to implementation)
public:
Widget(const Widget& rhs) {}
Widget& operator= (const Widget& rhs) {
*pImpl = *(rhs.pImpl);
return *this;
}
void swap(Widget& other) {
using std::swap;
swap(pImpl, other.pImpl);
}
private:
WidgetImpl* pImpl;
};
namespace std {
template<>
void swap<effective_cplusplus_::Widget25>(effective_cplusplus_::Widget25& a, effective_cplusplus_::Widget25& b)
{
a.swap(b);
}
}
条款26 :尽可能延后变量定义式的出现时间(Postpone variable definitions as long as possible)
减少构造析构的调用,毕竟有可能执行不到那个定义的位置。可增加程序的清晰度并改善程序效率。
条款27 :尽量少做转型动作(Minimize casting)
const_cast通常被用来将对象的常量性移除(cast away the constness)。它也是唯一有此能力的C+±style转型操作符。
dynamic_cast主要用来执行”安全向下转型”(safe downcasting),也就是用来决定某对象是否归属继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作。
reinterpret_cast意图执行低级转型,实际动作(及结果)可能取决于编译器,这也就表示它不可移植。例如将一个pointer to int转型为一个int。
static_cast用来强迫隐式转换(implicit conversions),例如将non-const对象转为const对象,或将int转为double等等。它也可以用来执行上述多种转换的反向转换,例如将void*指针转为typed指针,将pointer-to-base转为pointer-to-derived。但它无法将const转为non-const,这个只有const_cast才办得到。
(1).如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast。如果有个设计需要转型动作,试着发展无须转型的替代设计。(2).如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内。(3). 宁可使用C+±style(新式)转型,不要使用旧式转型(C风格转型)。前者很容易辨识出来,而且也比较有着分门别类的职掌。
使用explicit构造函数时,需要使用c风格转换。
条款28 :避免返回handles指向对象内部成分(Avoid returning “handles” to object internals)
避免返回handles(包括reference、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生”虚吊号码牌”(dangling handles)的可能性降至最低。
dangling handles实例:外部指针指向内部对象,内部对象失效时,外部指针成为野指针。
实际性能优先时,这一条也不是不能破例:
#include <cstdint> // for uint8_t
#include <cstddef> // for size_t
class file_view{
public:
typedef uint8_t* uint8_pointer;
typedef uint8_pointer& uint8_pointer_ref;
enum : uint8_t{
kNewLineFlag_0x0D = 0x0D,
kNewLineFlag_0x0A = 0x0A,
};
public:
file_view(uint8_t* const file_pointer, const size_t file_size)
: file_pointer_(file_pointer), file_size_(file_size), size_parsed_(0)
{}
file_view() = delete;
~file_view() = default;
inline bool get_line(
uint8_pointer_ref p_start,
uint8_pointer_ref p_end,
const uint8_t new_line_flag = kNewLineFlag_0x0A){
if (size_parsed_ >= file_size_){
p_start = p_end = nullptr;
return false;
}
uint8_t* p_cur = file_pointer_ + size_parsed_;
while (size_parsed_ < file_size_ && kNewLineFlag_0x0D == *p_cur){
++p_cur;
++size_parsed_;
}
while (size_parsed_ < file_size_ && *p_cur == new_line_flag){
++p_cur;
++size_parsed_;
}
p_start = p_cur;
while (size_parsed_ < file_size_ && *p_cur != new_line_flag){
++p_cur;
++size_parsed_;
}
p_end = p_cur;
return true;
}
private:
uint8_t* const file_pointer_; // 文件指针
const size_t file_size_; // 文件总大小
size_t size_parsed_; // 已解析的size
};
条款29 :为”异常安全”而努力是值得的(Strive for exception-safe code)
“异常安全”有两个条件:(1).不泄漏任何资源。(2).不允许数据败坏。
异常安全函数(Exception-safe functions)提供以下三个保证之一:
(1).基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态(例如所有的class约束条件都继续获得满足)。然而程序的现实状态(exact state)恐怕不可预料。
(2).强烈保证:如果异常被抛出,程序状态不改变。调用这样的函数需有这样的认知:如果函数成功,就是完全成功;如果函数失败,程序会回复到”调用函数之前”的状态。
(3).不抛掷(nothrow)保证:承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。作用于内置类型(例如int,指针等等)身上的所有操作都提供nothrow保证。这是异常安全码中一个必不可少的关键基础材料。
异常安全码(Exception-safe code)必须提供上述三种保证之一。如果它不这样做,它就不具备异常安全性。
有个一般化的设计策略很典型地会导致强烈保证,这个策略被称为copy and swap。原则很简单:为你打算修改的对象(原件)做出一份副本,然后在那副本身上做一切必要修改。若有任何修改动作抛出异常,原对象仍保持未改变状态。待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换(swap)。但一般而言它并不保证整个函数有强烈的异常安全性。
(1).异常安全函数(Exception-safe function)即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。(2).”强烈保证”往往能够以copy-and-swap实现出来,但”强烈保证”并非对所有函数都可实现或具备现实意义。(3).函数提供的”异常安全保证”通常最高只等于其所调用之各个函数的”异常安全保证”中的最弱者。
#include <iostream>
#include <cstring>
class MyString {
public:
MyString() : data(nullptr), length(0) {}
MyString(const char* str) : data(nullptr), length(0) {
if (str) {
length = std::strlen(str);
data = new char[length + 1];
std::strcpy(data, str);
}
}
MyString(const MyString& other) : MyString() {
MyString temp(other.data); // Use the constructor to create a copy
swap(temp); // Swap the content of this and temp
}
MyString(MyString&& other) noexcept : data(nullptr), length(0) {
swap(other); // Swap the content with the temporary object
}
~MyString() {
delete[] data;
}
MyString& operator=(MyString other) {
swap(other); // Swap the content with the copy
return *this;
}
// Swap function
void swap(MyString& other) noexcept {
std::swap(data, other.data);
std::swap(length, other.length);
}
const char* c_str() const {
return data;
}
private:
char* data;
std::size_t length;
};
int main() {
MyString str1("Hello");
MyString str2("World");
// Copy and Swap used in assignment
str1 = str2;
std::cout << "str1: " << str1.c_str() << std::endl; // Output: "str1: World"
return 0;
}
条款30 :透彻了解inlining的里里外外(Understand the ins and outs of inlining)
(1).将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级(binary upgradability)更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。(2).不要只因为function templates出现在头文件,就将它们声明为inline。较小模板函数会被编译期自动inline,交由编译器自行判断。
单文件时,所有函数inline可以简单解决编译问题。
inline的函数不易被debug。
代码inline导致过大,换页更频繁,也会导致速度下降,不是所有inline都能提速。
条款31 :将文件间的编译依存关系降至最低(Minimize compilation dependencies between files)
(1). 支持”编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是handle classes和interface classes。(2). 程序库头文件应该以”完全且仅有声明式”(full and declaration-only forms)的形式存在。这种做法不论是否涉及templates都适用。
减少编译成本,低耦合问题。
尽量做到:少一行就编译失败,多一行就浪费,每行代码都有底层逻辑在支撑。
条款32 :确定你的public继承塑模出is-a关系(Make sure public inheritance models “is-a”)
”public继承”意味is-a。适用于base classes身上的每一件事情一定也适用于derived classes身上,因为每一个derived class对象也都是一个base class对象。
条款33 :避免遮挡继承而来的名称(Avoid hiding inherited names)
(1). derived classes内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。
(2).为了让被遮掩的名称再见天日,可使用using声明式或转交函数(forwarding functions)。
条款34 :区分接口继承和实现继承(Differentiate between inheritance of interface and inheritance of implementation)
(1).接口继承和实现继承不同。在public继承之下,derived classes总是继承base class的接口。
(2). pure virtual函数只具体指定接口继承。
(3). 简朴的(非纯)impure virtual函数具体指定接口继承及缺省实现继承。
(4). non-virtual函数具体指定接口继承以及强制性实现继承。
条款35 :考虑virtual函数以外的其它选择(Consider alternatives to virtual functions)
(1). virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。
(2). 将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员。
(3). std::function对象的行为就像一般函数指针。这样的对象可接纳”与给定之目标签名式(target signature)兼容”的所有可调用物(callable entities)。
class Base {
public:
void operation() {
// Non-virtual interface
doOperation();
}
private:
virtual void doOperation() = 0;
};
class Derived : public Base {
private:
void doOperation() override {
// Implementation for Derived
}
};
class Strategy {
public:
virtual void execute() = 0;
};
class ConcreteStrategyA : public Strategy {
public:
void execute() override {
// Implementation for Strategy A
}
};
class ConcreteStrategyB : public Strategy {
public:
void execute() override {
// Implementation for Strategy B
}
};
class Context {
private:
Strategy* strategy;
public:
void setStrategy(Strategy* s) {
strategy = s;
}
void executeStrategy() {
strategy->execute();
}
};
class MyClass {
private:
int privateData;
public:
friend void externalFunction(MyClass& obj);
};
void externalFunction(MyClass& obj) {
int data = obj.privateData;
}
#include <functional>
class Base {
public:
std::function<void()> operation;//本质就是函数指针
};
class Derived : public Base {
public:
Derived() {
operation = []() {//lambda 表达式本质是函数指针,或者有inline,具体看编译期
// Implementation for Derived
};
}
};
条款36 : 绝不重新定义继承而来的non-virtual函数(Never redefine an inherited non-virtual function)
non-virtual函数都是静态绑定(statically bound).
virtual函数确是动态绑定(dynamically bound)。
条款37 : 绝不重新定义继承而来的缺省参数值(Never redefine a function’s inherited default parameter value)
virtual函数系动态绑定(dynamically bound),
而缺省参数值确是静态绑定(statically bound)。
静态绑定又名前期绑定,early binding;动态绑定又名后期绑定,late binding。
对象的所谓静态类型(static type),就是它在程序中被声明时所采用的类型。
对象的所谓动态类型(dynamic type)则是指”目前所指对象的类型”。
也就是说,动态类型可以表现出一个对象将会有什么行为。virtual函数系动态绑定而来,意思是调用一个virtual函数时,究竟调用哪一份函数实现代码,取决于发出调用的那个对象的动态类型。
条款38 : 通过复合塑模出has-a或”根据某物实现出”(Model “has-a” or “is-implemented-in-terms-of” through composition)
(1). 复合(composition)的意义和public继承完全不同。
(2). 在应用域(application domain),复合意味has-a(有一个)。
在实现域(implementation domain),复合意味is-implemented-in-terms-of(根据某物实现出)。
条款39 : 明智而审慎地使用private继承(Use private inheritance judiciously)
如果classes之间的继承关系是private,编译器不会自动将一个derived class对象转换为一个base class对象。这和public继承的情况不同。
由private base class继承而来的所有成员,在derived class中都会变成private属性,纵使它们在base class中原本是protected或public属性。
private继承意味implemented-in-terms-of(根据某物实现出)。如果你让class D以private形式继承class B,你的用意是为了采用class B内已经备妥的某些特性,不是因为B对象和D对象存在有任何观念上的关系。
private继承纯粹只是一种实现技术。private继承意味只有实现部分被继承,接口部分应略去。如果D以private形式继承B,意思是D对象根据B对象实现而得,再没有其它意涵了。
尽可能使用复合,必要时才使用private继承。
EBO(empty base optimization, 空白基类最优化),EBO一般只在单一继承(而非多重继承)下才可行。
(1). private继承意味is-implemented-in-terms-of(根据某物实现出)。它通常比复合(composition)的级别低。但是当derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。(2).和复合(composition)不同,private继承可以造成empty base最优化。这对致力于”对象尺寸最小化”的程序库开发者而言,可能很重要。
条款40 : 明智而审慎地使用多重继承(Use multiple inheritance judiciously)
(1).多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承的需要。
(2).virtual继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果virtual base classes不带任何数据,将是最具实用价值的情况。
(3).多重继承的确有正当用途。其中一个情节涉及”public继承某个Interface class”和”private继承某个协助实现的class”的两相组合。
条款41 :了解隐式接口和编译期多态(Understand implicit interfaces and compile-time polymorphism)
(1).classes和templates都支持接口(interface)和多态(polymorphism)。
(2).对classes而言接口是显式的(explicit),以函数签名为中心。多态则是通过virtual函数发生于运行期。
(3).对template参数而言,接口是隐式的(implicit),奠基于有效表达式。多态则是通过template具现化和函数重载解析(function overloading resolution)发生于编译期。
条款42 :了解typename的双重意义(Understand the two meanings of typename)
(1).声明template参数时,前缀关键字class和typename可互换。
(2).请使用关键字typename标识嵌套从属类型名称;但不得在base class lists(基类列)或member initialization list(成员初值列)内以它作为base class修饰符。
条款43 :学习处理模板化基类内的名称(Know how to access names in templatized base classes)
可在derived class templates内通过”this->”指涉base class templates内的成员名称,或藉由一个明白写出的”base class资格修饰符”完成。
在 C++ 中,模板是在编译时实例化的,而不是在运行时。当你定义一个模板化类时,编译器并不知道具体的模板参数类型,因此无法确定成员名称是否存在,以及如何访问它们。
资格修饰符 (Qualification): 使用 this-> 或 Base:: 等资格修饰符来明确指示你正在访问的成员属于哪个类。这种方式可以让编译器知道你的意图,从而正确解析成员名称。
依赖名字查找 (Dependent Name Lookup): 当模板类内部引用成员名称时,编译器将这些名称视为依赖于模板参数的名称。这些名称的解析会推迟到实例化阶段,当编译器获得了具体的模板参数类型信息时才进行。
由于 C++ 的模板机制的复杂性,编译器在模板实例化时必须考虑不同的上下文和可能的参数类型,因此成员名称不会自动可见。为了确保模板代码的正确性和可移植性,需要显式指定名称的所属类或使用资格修饰符。
虽然这可能会增加一些额外的编写代码的复杂性,但它也提供了更大的灵活性,因为它允许你在不同的上下文中使用相同的模板类,并且可以根据实际需要访问不同的成员。
条款44 :将与参数无关的代码抽离templates(Factor parameter-independent code out of templates)
(1).Templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。
(2).因非类型模板参数(non-type template parameters)而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数。例如整数作为函数参数比较合适。
(3).因类型参数(type parameters)而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述(binary representations)的具现类型(instantiation types)共享实现码。
感觉要通过多看std、boost等优秀代码去找到感觉,也需要知道编译器是怎么去编译的。
条款45 :运用成员函数模板接受所有兼容类型(Use member function templates to accept “all compatible types”)
template<typename T>
class SmartPtr {
public:
// 需要支持U* T*转换
template<typename U>
SmartPtr(const SmartPtr<U>& other) : heldPtr(other.get()) {}
T* get() const { return heldPtr; }
private:
T* heldPtr;
};
条款46 :需要类型转换时请为模板定义非成员函数(Define non-member functions inside templates when type conversions are desired)
#include <iostream>
template <typename T>
class XXX {
public:
XXX (T value) : data(value) {}
T getData() const {
return data;
}
private:
T data;
};
// 非成员函数用于类型转换
template<typename T>
const XXX<T> doMultiply(const XXX <T>& lhs, const XXX <T>& rhs){
return XXX <T>(lhs.getData() * rhs.getData());
}