改善程序设计的55个具体做法

记录看《Effective C++》的进程

===========================

一、让自己习惯C++

1. 视C++为一个语言联邦

四个次语言

  • C语言基础
  • object-oriented C++
  • Templated C++
  • STL 标准模版库

2. 尽可能的使用const,enum,inline 代替 #define

3. 尽可能使用 const

4. 确定对象被使用前已先被初始化

需要确保每一个构造函数都将对象的每一个成员初始化成功。C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。为了避免先调用default构造函数再赋值,较佳的做法是使用 member initialization list (成员初值列)。

复制代码
#include<list>

class PhoneNumber {};

class ABENtry {
public:
    ABENtry(const std::string& name, const std::string& address,
            const std::list<PhoneNumber>& phones);
private:
    std::string theName;
    std::string theAddress;
    std::list<PhoneNumber> thePhones;
    int numTimesConsulted;
};

// 先调用default构造函数,再赋值
ABENtry::ABENtry(const std::string& name, const std::string& address,
            const std::list<PhoneNumber>& phones) 
    {
        theName = name;
        theAddress = address;
        thePhones = phones;
        numTimesConsulted = 0;
    }
// 成员初值列, 效率更高
ABENtry::ABENtry(const std::string& name, const std::string& address,
            const std::list<PhoneNumber>& phones)
        :theName(name), theAddress(address), thePhones(phones), numTimesConsulted(0)
        {}
复制代码

规定总是在初值列中列出所有的成员变量,以免还得记住哪些成员变量无需初值。因此,最简单的做法是:总是使用成员初值列。

C++有着十分固定的 "成员初始化次序": base class 总是更早于其 derived classes 被初始化(见条款12), 而 class 的成员变量总是以其声明次序被初始化。

以 "成员初值列" 初始化对象以后,还需要注意的是: 不同编译单元内定义之 non-local static 对象的初始化相对次序并没有明确定义。 解决方案是,将每个 non-local static 对象搬到自己的专属函数内,并且该对象在此函数内被声明为 static, 即这些函数返回的是一个 reference 指向在函数内定义的 non-local static 对象。这就是 Singleton 模式的一个常见实现手法。

复制代码
class FileSystem {
public:
    std::size_t numDIsks() const;
};

FileSystem& tfs() {
    static FileSystem fs; //定义并且初始化一个 local static 对象,返回一个 reference
    return fs;
}
// extern Filesystem tfs; // 被 tfs() 替换

//以下都在另一个文件中
class Dictionary {};

Dictionary::Dictionary(params) {
    std::size_t disks = tfs().numDisks();
};

Dictionary& tempDir() {
    static Dictionary td; //定义并且初始化一个 local static 对象,返回一个 reference
    return td;
}
// Dictionary tempDir(params); // 被 temDir() 替换
复制代码

这种结构下的 reference-returning 十分单纯,是inlining函数的绝佳选择。

总结: 

  • 为内置型对象进行手工初始化,因为 C++ 不保证初始化它们。
  • 构造函数最好使用成员初值列 (member initilization list), 而不要在构造函数本体内适用赋值操作。初值列列出的成员变量,其排列次序应该和他们在class中的声明次序相同1.
  • 为了免除 "跨编译单元之初始化次序"问题,请以 local static 对象替换 non-local static 对象。

 

二、构造/析构/赋值运算

5. 了解C++默默编写并调用哪些函数

  • 构造函数
  • 析构函数
  • copy 构造函数
  • copy 赋值函数

6. 若不想使用编译器自动生成的函数,就该明确拒绝

  • 为了驳回编译器自动(暗自)提供的技能,可将相应的成员函数声明为private并且不予以实现;或者使用 Uncopyable/noncopyable(Boost 提供的)作为 base class.
  • class Uncopyable {
        protected:
            Uncopyable() {};
            ~Uncopyable() {};
        private:  //将copying函数设置为private,来阻止copying
            Uncopyable(const Uncopyable&);
            Uncopyable& operator=(const Uncopyable&);
    }

7. 为多态基类声明 virtual 析构函数

如果某个 base class 带有多态的性质,即我们设计这个 base class 的目的是为了通过 base class 接口处理 derived class 对象,此时需要给 base class 定义一个 virtual析构函数函数,否则会导致派生类的资源不能够及时被释放等等一系列“灾难”。

但是并非所有的 base class 都是为了多态用途,例如 STL 和 string。

8. 别让异常逃离析构函数

如果析构函数中抛出异常,容易造成“不明确行为”或者“过早的结束程序”,可能会导致内存泄露或资源浪费等行为。

有两种方法来避免:

  • 如果析构函数抛出异常,就结束程序,通常通过调用 std::abort() 完成;
  • 如果析构函数抛出异常,吞下这个异常。

更好的策略是给提供客户一个接口(例如一个普通函数),让客户能对某个操作函数运行期间抛出的异常作出反应。

9. 绝不在构造或者析构过程中调用 virtual 函数

在构造或者析构期间,不要调用 virtual 函数,因为此类调用不会下降至 derived class (比起当前执行构造和析构函数的那层)

tips_1: 如果一个 class 有多个构造函数,并且每一个构造函数都需要执行某一个相同的工作,那么避免代码重复,可以将共同的初始代码放进一个初始化函数当中,如命名为(init())。

tips_2: 除了使用成员初值列来给予 base class 所需参数,也可以通过一个辅助函数创建一个值传 给base clas 的构造函数(比较方便,也增加了可读性),并且令此辅助函数为 static,防止此函数指向“初期未成熟之 derived class 对象内尚未初始化的变量” 。

10. 令 operator= 返回一个 reference to *this

对于 “=”、“+=”, “-=”、“*=” 、“\=” 的运算符重载, 需要返回一个 *this 的引用,以此来实现“连锁赋值”,即 x=y=z=15. 

这是一份协议,不是强制性的,但是内置类型以及标准库提供的类型如 string、complex、vector、tr1::shared_ptr 都遵循了这份协议。

 11. 在 operator= 中处理“自我赋值”

一般而言,如果某段代码操作 pointers 或者 references 而他们被用来 “指向多个相同类型的对象”, 就需要考虑这些对象是否为同一个。

法1: 通过 “证同测试”,即 identity test:

if (this == &rhs) return *this;

但是这种方法不能保证“异常安全性”,例如 new 操作导致异常。

因此还需要保证在复制 ptr 所指向的对象之前,千万别删除 ptr。

法2: copy and swap 技术

通过一个 swap(class_a &rhs) 函数,交换 *this 和 rhs 的数据。并且 swap 函数也可以是 “pass by value”, 但是牺牲了一些清晰性,与此同时,将 “copying动作” 从函数本体转移至 “函数参数构造阶段” 却可以让编译器有时候生成更高效的代码。

复制代码
class Widget {
    ...
    void swap(Widget& rhs);
    ...
};

Widget& Widget::operator=(const Widget& rhs)
{
    Widget temp(rhs); // 调用 copy 构造函数
    swap(temp);
    return *this;
};
复制代码

总结: 确保当对象自我复制时,operator= 有良好的行为,其中技术包括 比较“来源对象”和“目标对象” 的地址、精心周到的语句顺序(即要保证在复制 ptr 所指向的对象之前,千万别删除 ptr) 以及 copy and  swap 技术。

12. 复制对象时勿忘其每一个成分

Copying 函数 指的是 copy 构造函数 以及 copy assignment 函数。

考虑一个 base class Customer

复制代码
void logCall(const std::string& funName); // 全局函数用于输出相关信息

class Customer { public: ... Customer(const Customer& rhs); // 拷贝构造函数 Customer& operator= (const Customer& rhs); // 拷贝赋值函数 ... private: std::string name; }; Customer::Customer(const Customer& rhs) : name(rhs.name) { logCall("Customer copy constructor"); } Customer& Customer::operator=(const Customer& rhs) { logCall("Customer copy assignment operator"); name = rhs.name; return *this; }
复制代码

如果需要为这个 base class 写 derived class, 并且为 derived class 撰写 copying 函数, 那么必须很小心的复制其 base class 的部分:

复制代码
class PriorityCustomer: public Customer {
public:
     ....
     PriorityCustomer(const PriorityCustomer& rhs);
     PriorityCustomer& operator= (const PriorityCustomer& rhs);  
    ....

private:
    int  priority;   
};

PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs) : Customer(rhs), // 调用 base class 的 copy 构造函数 
   priority(rhs.priority) 
{
    logCall("PriorityCustomer  copy constructor");
}

PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs) {

    logCall("PriorityCustomer  copy assignment operator");
    Customer::operator=(rhs); // 对 base class 成分进行赋值动作
}
复制代码

注意; 不要试图为了代码的简洁性,对 copy 构造函数 和 copy assignment 函数 相互调用。一种消除重复代码的方法是 建立一个新的成员函数(往往是 private 并且可以命名为 init )。

总结: Copying 函数 应当确保复制 “对象内的所有成员函数”以及所有 base class 的成分。

 

三、资源管理

13. 以对象管理资源

在我们动态分配内存的时候(例如使用 new 操作符),有时候或许因为维护等原因,导致 delete 操作符未能成功被执行。因此,利用 C++ 里的 “析构函数自动调用机制”,我们可以利用对象来管理资源,也即 Resource Acquisition Is Initialization (RAII)。

简单的 auto_ptr 例子

复制代码
class Investment:  // Base class

{
  ...
}

Investment* createInvestment(); // Factory函数

void f() {
  std::auto_ptr
<Investment> pInv(createInvestment());   ... }
复制代码

auto_ptr 有个不寻常的性质:若通过 copying 构造函数 或者 copy assignment 操作符复制它们,他们会变成null,即复制所得的指针获得了资源的唯一拥有权。

替代方案是采用 share_ptr,即 “引用计数型智能指针”(reference-counting smart pointer, RCSP). RSCP 提供的行为类似垃圾回收。但是,RCSP 无法打破环状引用,例如两个其实已经没被使用的对象彼此互指,因此好像还处在“被使用”的状态。

如果有多个智能指针指向同一个对象,引用计数会增加,直至计数为0,对象将自动销毁。

void f() {
  ...
  std::tr1::share_ptr<Investment> pInv1(createInvestment());
  std::tr1::share_ptr<Investment> pInv2(pInv1); // pInv1和pInv2指向同一个对象
  pInv1 = pInv2; 
}

还有unique_ptr 以及 weak_ptr 可参考 四种智能指针的使用

14. 在资源管理类中小心 copying 行为

当一个 RAII 对被复制,有两种选择:1)禁止复制,例如将copying操作声明为private(参考条款6); 2)对底层资源使用"引用计数法"(Reference-count), 例如使用 std::tr1::share_ptr<>.

Ps: 对于 shared_ptr,它允许我们指定所谓的 "删除器" (deleter),即一个函数或者函数对象。

复制代码
#include<mutex>

void lock(Mutex* pm);
void unlock(Mutex* pm);

class Lock {
    public: 
    explicit Lock(Mutex* pm): mutexPtr(pm, unlock) { // 指定 deleter
        lock(mutexPtr.get());
    }
    private:
        std::tr1::shared_ptr<Mutex> mutexPtr;
}
复制代码
  • 我们可以选择: 1) 复制底部资源,即深拷贝,例如 char* 指针指向分配的内存,即一个指向字符串对象 2)转移底部资源所有权,例如 auto_ptr。
  • 当我们复制 RAII 对象时,必须一并复制它所管理的资源,所以资源的copying行为决定了RAII对象的copying行为。
  • 普遍的RAII class cpying 行为是: 抑制 copying、施行引用计数法

15. 在资源管理类中提供对原始资源的访问

如果在 RAII对象的基础上,需要直接访问其原始资源,有 显式转换 和 隐式转换 两种方法。

复制代码
#include<memory>
class Investment { public: bool isTaxFree(); }; Investment* createInvestment(); // Factory 函数 int daysHeld(const Investment* pi); //返回投资天数 std::shared_ptr<Investment> pInv(createInvestment()); //int days = daysHeld(pInv); //编译错误 //显式转换 int days = daysHeld(pInv.get()); // get()成员函数返回 T*,即Investment*; //隐式转换 std::shared_ptr<Investment> p1(createInvestment()); bool tax1 = !(p1->isTaxFree()); //重载 “->” 运算符, 返回 T& std::auto_ptr<Investment> p2(createInvestment()); bool tax2 = !(*p2).isTaxFree(); //重载 “*”, 返回 T*
复制代码
  • 每一个 RAII class 应该提供一个 "取得其所管理之资源"的办法;
  • 对原始资源的访问可能经由显式转换或者隐式转换,一般而言显式转换比较安全,但隐式转换对客户更方便。

16. 成对使用 new 和 delete 时要采取相同的形式

当你使用 new (也就是通过 new 动态生成一个对象),有两件事情发生。第一,内存被分配出来(通过名为 operator new 的函数)。第二,针对此内存会有一个或者更多的构造函数被调用。当你使用 delete,也有两件事情发生: 针对此内存会有一个或者更多的的析构函数被调用,然后内存才被释放(通过名为 operator delete 的函数,见条款51)。 delete 的最大问题在于,即将被删除的内存中有多少个对象,那么就决定了有多少个析构函数将会被调用。

std::string* ps1 = new std::string;
std::string* ps2 = new std::string[10];
...
delete ps1;
delete [] ps2;
  • new 和 delete 以及 new [] 和 delete [] 应该成对使用。

17. 以独立语句将 newed 的对象置入智能指针

为了避免由于其他函数调用异常导致 new 所返回的指针遗失,使用分离语句,即先在单独语句内以智能指针存储 newed 所得对象,再将智能指针传入其他函数当中。

int priority();
void processWidget(std::shared_ptr<Widget> pw, int priority)

std::shared_ptr<Widget> pw(new Widget); //在单独语句哪存储newed对象
processWidget(pw, priority()); //这个调用动作不会造成泄漏

 

四、设计与声明

18. 让接口容易被正确使用,不易被误用 

  • 尽量令你的 types 行为与内置的 types 行为一致
  • 软件接口的设计原则是先发制人,即定制化你的函数返回类型,如 std::share_ptr

如条款14所说,share_ptr允许当智能指针被建立起来时就指定一个资源释放函数(所谓删除器,"deleter") 绑定于智能指针身上(auto_ptr 就没有这种能耐)。

share_ptr 提供的某个构造函数接受两个实参,一个是被管理指针,另一个是引用次数为 0 时将被调用的 “删除器”。 如果我们要实现一个 createInvertment 函数并且使它返回一个 share_ptr 且夹带着 getRidofInvestment 函数作为删除器,则

std::shared_ptr<Investment> createInvestment() {
    std::shared_ptr<Investment> retVel(static_cast<Investment* > (0), 
                                        getRidOfInvestment);
    retVel = ... // 让其指向正确的对象
    return retVal;                        
}

19. 设计 class 犹如设计 type

  • 新 type 的对象应该如何被创建和销毁?
  • 对象的初始化和对象的赋值该有什么样的差别?
  • 新 type 的对象如果被 pass by value,意味着什么?
  • 什么是新 type 的 "合法值"?
  • 你的新 type 需要配合那个继承图系 (inheritance graph)?
  • 你的新 type 需要什么样的转换?
  • 什么样的操作符和函数对此新 type 的合理的?
  • 什么样的标准函数应该驳回?
  • 谁该取用新 type 的成员?
  • 什么是新 type 的 "未声明接口"?
  • 你的新 type 有多么一般化?
  • 你真的需要一个新 type 吗?

 

20. 宁以 pass-by-reference-to-const 替换 pass-by-value

  • 尽量以 pass-by-reference-to-const 替换 pass-by-value,前者通常比较搞笑,并可避免切割问题 (slicing problem)。
  • 以上规则不适用于内置类型(如int、string等),以及 STL 迭代器和函数对象。对他们而言,pass-by-value 往往比较适当。

对于用户自定义的类型,pass-by-reference-to-const 的效率会远高于pass-by-value,因为pass-by-value 其实是在对传递实参的副本操作,pass-by-reference-to-const 省去了构造函数和析构函数的时间。

复制代码
class Person {  // 基类
public:    
    Person();
    virtual ~Person();
private:
    std::string name;
    std::string address;
};

class Student: public Person { // 派生类
public:
    Student();
    virtual ~Student();
private:
    std::string schoolName;
    std::string schoolAddress;
};

Student plato;
// bool validateStudent(Student s); // pass by value
// bool validateStudent(const Student& s); // pass by reference
bool platoIsOK = validateStudent(plato);
复制代码

21. 必须返回对象时,别妄想返回其 reference

记住,所谓的 reference 只是一个名称,代表某个既有的对象,任何时候看到一个 reference 的声明式,你都应该立刻问自己: 它的另一个名称是什么?

函数创建对象的途径有二: 在 stack 空间 或者在 heap 空间。

但是,在函数内部,如果在 stack 或者 heap 上创建新的对象并返回此对象,此对象在离开函数返回后就回消失,如果欲返回其 reference,则此 reference 将指向无明确定义的内存空间。

因此此时,返回新建的对象即可,即返回 value。

复制代码
class Rational {
public:
    Rational(int numetator = 0, int denominator = 1);
private:
    int n, d;
    friend const Rational // 以 by-value 返回计算结果
        operator* (const Rational& lhs, const Rational& rhs);
}

inline const Rational operator* (const Rational& lhs, const Rational& rhs) {
    return Rational(lhs.n * rhs.n, lhs.d * rhs.d); // 返回一个新对象,右值
}
复制代码
  • 不要返回 pointer 或 reference 指向一个 local stack 对象,或返回 reference 指向一个 heap-allocated 对象,或返回 pointer 或 reference 指向一个 local static 对象那个而有可能同时需要多个这样的对象。条款 4 已经为 "在单线程环境中合理返回 reference 指向一个 local static 对象" 提供了一份设计实例 (Singleton 设计模式)。

22. 将成员变量声明为 private

  • 切记将成员变量声明为 private。这可以赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供 class 作者以充分的实现弹性。
  • protected 并不比 public 更具有封装性。

23.宁以 non-member non-friend 函数 替换 member 函数

就封装性而言,愈多的东西被封装,我们改变那些东西的能力也就愈大,即它使我们能够改变食物而只影响有限的客户。

如果以计算能够访问到对象内的数据的函数数量作为一种对数据封装性的粗糙量测,那么 non-member non-friend 函数 并不增加 "能够访问 class 内之 private成分" 的函数数量,而 member 函数会增加。

在 C++, 比较自然的做法是让 clearBrowser 成为一个 non-member non-friend 函数并且位于 WebBrowser 所在的同一个 namespace 内:

复制代码
namespace WebBrowserStuff{

class WebBrowser {
public:
    void clearCache();
    void clearHistory();
    void removeCookies();
    void clearEverything(); // member函数,此函数实现上面三个功能
};

// non-member non-friend 函数
void clearBrowser(WebBrowser& wb) {
    wb.clearCache();
    wb.clearHistory();
    wb.removeCookies();
}

}; /* namespace WebBrowserStuff */
复制代码

对于同一个 namespace 内,具有不同功能的函数和类,可以分开再不同的头文件中声明并且定义,就像 C++ 标准库所做的那样,在 std命名空间内有<vector>、<memory> 等相关技能分布在不同的头文件中。

总结: 宁以 non-member non-friend 函数 替换 member 函数, 这样可以增加封装性(能够访问 class 的 private 成分的函数的数量更少)、包裹弹性(packiging flexibility) 和 机能扩充性(在同一个 namespace 内声明和定义不同的功能函数或者类)。

24. 若所有参数皆需类型转换,请为此采用 non-member 函数

如果你需要为某个函数的所有参数(包括被 this 指针所指的那个隐喻参数) 进行类型转换,那么这个函数必须是个 non-member。

复制代码
class Rational { //有理数类
public:
    Rational(int numetator = 0, int denominator = 1);
    int numerator() const;
    int denominator() const;

    const Rational operator* (const Rational& rhs) const; // 成员函数
private:
    int n, d;
};

Rational oneEight(1, 8);
Rational oneHalf(1, 2);
Rational result_1 = oneEight * oneHalf;
Rational result_2 = oneEight * 2; // 编译通过,发生了隐式转换,即 implicit type conversion
Rational result_3 = 2 * oneHalf; // 只有用下面的 non-member 函数才能通过编译

// non-member 函数
const Rational operator* (const Rational& lhs,
                          const Rational& rhs)
    {
        return Rational(lhs.numerator() * rhs.numerator(),
                        rhs.denominator() * rhs.denominator());
    };
复制代码

25. 考虑写出一个不抛出异常的 swap 函数

标准程序库中,即 std namespace 中的 swap 函数如下:

复制代码
namespace std // 标准程序库里的 swap 函数
{
    template<typename T>
    void swap(T& a, T& b) {
        T tmp(a); // copy 构造函数
        a = b; // copy 赋值函数
        b = tmp; // copy 赋值函数
    }  
}
复制代码

如果某个类的实现是基于 “以指针指向某个对象,其内部含有真正的数据”,这就是所谓的 "pimpl" 手法 (pointer to implementation)。 如下例子: 

复制代码
class WidgetImpl {
public:
    ...
private:
    int a, b, c;
    std::vector<double> v; 
}

class Widget { // 此 class 采用 pimpl 手法
public:
    Widget(const Widget& rhs);
    Widget& operator= (const Widget& rhs) {
        ...
        *pImpl = *(rhs.pImpl);
    }
private:
    WidgetImpl* pImpl;
}
复制代码

如果采用 std::swap 置换两个 Widget 对象,本应当只置换两个 pImpl 指针,但是 std::swap 还会置换其指向的 WidgetImpl 对象。解决办法讨论如下:

全特化(total template specialization)版本如下:

namespace std {
    template<> // 表示全特化
    void swap<Widget> (Widget& a, Widget& b) {
        swap(a.pImpl, b.pImpl); // 企图访问 private 的指针,无法通过编译
    }
}

正确方法是声明一个特化一个 non-member 的 swap 函数调用 public member swap 函数:

复制代码
class Widget {
public:
    ...
    void swap(Widget& other) {
        using std::swap;
        swap(pImpl, other.pImpl); //置换的是指针
    }
}

namespace std {  // 修订后的 swap 的特化版本
    template<>
    void swap<Widget> (Widget& a, Widget& b) {
        a.swap(b); // Widget 的 member 函数
    }
}
复制代码

如果是针对 class templates 而非 class, 由于我们不能在 std 空间内对 function template 进行偏特化(partially specialize), 因为C++只允许对 clas template 进行偏特化,因此正确的做法是,声明一个 non-member swap 让它调用 member swap,但是不再将那个 non-member swap 声明为 std::swap 的特化版本或者重载版本。为了简化期间,假设 Widget 的所有相关机能都被置于命名空间 WidgetStuff 内,即:

复制代码
namespace WidgetStuff {
    ...
    template<typename T>
    class Widget { //同前,内含swap member函数
        ...
        void swap(Widget& other) {
            using std::swap;
            swap(pImpl, othet.pImpl);
        }
    }; 

    template<typename T> //non-member 函数
    void swap(Widget<T>& a, Widget<T>& b)
        {
            a.swap(b);
        }
}
复制代码

C++ 的名称查找法则(name lookup rules) 确保将找到 global 作用域或者 T 所在之命名空间内的任何T专属的 swap。

最后需要注意的是,成员版的 swap 函数 绝不可以抛出异常。

总结:

  • 当 std::swap 对你的类型效率不高时,提供一个 swap 成员函数,并确定这个函数不抛出异常。
  • 如果你提供一个 member swap,也该提供一个 non-member swap 用来调用前者。对于 classes (而非 templates), 也请特化 std::swap。
  • 调用 swap 时应针对 std::swap 使用 using 声明式,然后调用 swap 并且不带任何 "命名空间资格修饰"。
  • 为 "用户定义类型" 进行 std templates 全特化是好的,但是千万不要尝试在 std 内加入某些对 std 而言全新的东西。

 

五、实现

26. 尽可能延后变量定义式的出现时间

因为所定义的变量可能未被完全使用甚至真的没有被使用过(由于异常等原因),因此,尽可能的延后变量定义式的出现,这样可以避免构造函数和析构函数的成本。

条款4 解释了为什么 "通过 default 构造函数构造出一个对象然后对它赋值" 比 "直接在构造时指定初值" 效率差。因此,本条款的真正意义是,不只应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试这份定义指导能够给它初值实参为止。如此,不仅能够避免构造(和析构)非必要对象,还可以避免无意义的 default 构造行为。

 

复制代码
void encrypt(std::string& s); // 对 s 进行适当加密

std::string encryptPassword(const std::string& password) {

    //... 检查密码长度是否合乎标准,否则 return log_error

    /* 直到能够给到初值实参,才定义并出示话变量 */
    std::string encrypted(password); // 通过 copy 构造函数初始化
    encrypt(encrypted); // 加密
    return encrypted; // 返回加密后的密码
}
复制代码

如果变量只在循环当中使用,最好采用如下第二种形式:

复制代码
/* 先定义, 调用 default 构造函数,再赋值
    即 1 次 default 构造 + 1 次 析构 + n 次赋值 */
Widget w;
for (int i = 0; i < n; ++i) {
    w = i;
    ...
}

/* 每次循环都调用一次 构造函数 和 析构函数 
    即 n 次构造 + n 次析构 */
for (int i = 0; i < n; ++i) {
    Widget w(i);
    ...
}
复制代码

除非: 1) 你知道赋值成本比 "构造+析构" 成本低; 2)你正在处理代码中效率高度敏感的部分。

27. 尽量少做转型动作

通常有三种不同的形式的转型动作:

  1. C 风格的转型动作: (T) expression //将 expression 转型为 T
  2. 函数风格的转型动作: T(expression) //将 expression 转型为 T
  3. 新式转型
  • const_cast<T>(expression)
    • 通常被用来将对象的常量性转除(cast away the constness),  它也是唯一有此能力的 C++-style 转型操作符。  
  • dynamic_cast<T>(expression)
    • 主要迎来执行 "安全向下转型"(safe downcasting), 也就是用来决定某对象是否归属于继承体系中的某个类型。它是唯一无法用旧式语法执行的动作,也是唯一啃呢个耗费重大运行成本的转型动作。
    • dynamic_cast 的许多实现版本执行速度相当慢。例如至少有一个很普遍的实现版本基于 "class 名称之字符串比较",其意味着需要调用 strcmp 用以比较 class 名称。 因此,除了对一般性保持机敏和猜疑,更应该在注重效率的代码中对 dynamic_cast 保持机敏与猜疑。
    • 之所以需要 dynamic_cast, 通常是因为你想在一个你认定为 derived class 对象身上执行 derived class 操作函数,但是你手上却只有一个 "指向 base" 的 pointer 或 reference,你只能靠它们在处理对象。当然,有两个一般性的做法可以避免这个问题:
      • 使用类型安全容器: 使用容器并在其中存储直接指向 derived class 对象的指针(通常是智能指针), 如此便消除了 "通过base class 接口处理对象" 的需要。例如针对不同的派生类,采用 vector<shared_ptr<derived_T> > 存储derived class 的指针。
      • 将 virtual 函数往继承体系上方移动: 这种方法可以让你通过 base class 接口处理 "所有可能之各种派生类",那就是在 base class 内提供 virtual 函数做你想对各个 derived class 做的事情。意思就是在 base class 中定义相应的 virtual function,并在相应的 derived class 中 重写 virtual 函数。  
  • reinterpret_cast<T>(expression)
    • 意图执行低级转型,实际动作(及结果)可能取决于编译器,这也就表示它不可移植。例如,讲一个 pointer to int 转型为 int。这一类转型在低级代码以外很少见。本书只使用一次,那是在讨论如何针对原始内存写出一个调试用的分配器 (degugging allocator), 见条款 50。
  • static_cast<T>(expression)
    • 用来强迫隐式转换 (implicit conversions), 例如将 non-const 对象转为 const 对象 (如条款 3 所为),或者将 int 转换为 double等等。它也可以用来执行上述多种转换的反向转换,例如,将 void* 转换为 typed 指针,将 pointer-to-base 转换为 pointer-to-derived。但是,它无法将 const 转换为 non-const --- 只有 const_cast 才办得到。

新式转型比前两种旧式的更受欢迎,原因是: 1) 它们很容易在代码中被识别出来(人工识别或者使用工具如 grep) 2)各转型动作的目标愈窄化,编译器愈可能诊断出错误的运用。

任何一个类型转换 (不论是通过转型操作而进行的显式转换,或通过编译器完成的隐式转换) 往往真的令编译器编译出运行期间执行的码。

总结:

如果可以,尽量避免转型,特别是在注重效率的代码中避免 dynamic_cast。 如果有个设计需要转型动作,试着发展无需转型的替代设计。

如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需要将转型放进他们自己的代码内。

宁可使用 C++ 风格的新式转型,不要使用旧式转型。前者可以很容易被辨识出来,而且也比较有着分门别类的执掌。

28. 避免返回 handles 指向对象内部成分

Handles 指的是 references、指针 和 迭代器。如果类中的某个成员函数返回 references 指向 private 内部数据,那么调用者是可以通过这些 references 更改内部数据的。

复制代码
class Point {  // 代表 “点” 的 class
public:
    Point(int x, int y);
    void setX(int newVal);
    void setY(int newVal); 
private:
    int pa;
    int pb;
};

struct RectData {  //存储代表矩形的两个“点”
    Point ulhc;
    Point lrhc;
};

class Rectangle {
private:
    std::shared_ptr<RectData> pData; // 一个指向 struct 的智能指针
public:
    Rectangle(Point& p1, Point& p2);
    Point& upperLeft() const {return pData->ulhc;} // 返回 handles 指向内部数据
    Point& lowerRight() const {return pData->lrhc;} // 返回 handles 指向内部数据
};

// 举例 Point point_1(
0, 0); Point point_2(100, 100); const Rectangle rec(point_1, point_2); rec.upperLeft().setX(50); // 由于 成员函数 upperLeft() 返回 引用,因此此行代码可以更改Point的内部数据 /* 正确做法 */ const Point& upperLeft() const {return pData->ulhc;} // 返回 const handles, 确保其不被改变 const Point& lowerRight() const {return pData->lrhc;} // 返回 const handles, 确保其不被改变
复制代码

因此,如果函数 "返回一个 handle 代表对象的内部private成分" 总是危险的。关键在于,如果一个 handle 被函数传出去了,那么你就是暴露在 "handle 比其所指的对象更长寿"的风险下。

当然,这不意味着你不可以让成员函数返回 handle,例如对于 运算符重载 "operator[]" 就是返回 references 指向容器内的数据。

总结: 避免返回 handles (包括 references、指针、迭代器) 指向对象内部。遵守这个条约可以增加封装性,帮助 const 成员函数的行为像个 const,并将发生 "虚吊 handles" (handles 指向一个已经被销毁即不存在的对象)的可能性降至最低。

29. 为 "异常安全" 而努力是值得的

当异常被抛出时,带有异常安全性的函数会: 1) 不泄漏任何资源 2)不允许数据败坏。

1)解决资源泄漏的问题,可以通过 条款13 所讨论的如何以对象管理资源解决 (智能指针),例如 条款14 中也导入了 Lock clss 作为一种确保 "互斥器被及时释放" 的方法。

复制代码
class Lock {
    public: 
    explicit Lock(Mutex* pm): mutexPtr(pm, unlock) {
        lock(mutexPtr.get());
    }
    private:
        std::shared_ptr<Mutex> mutexPtr;
}

class PrettyMenu {
public:
    void changeBackground(std::istream& imgSrc); // 改变背景图像
private:
    Mutex mutex; // 互斥器
    Image* bgImage; // 当前的背景
    int imageChanges; // 背景改变的次数
};

void PrettyMenu::changeBackground(std::istream& imgSrc) {
    lock(&mutex);  // 取得互斥器
    delete bgImage;
    ++imageChanges;
    bgImage = new Image(imgSrc); // 安装新的背景
    unlock(&mutex);  // 释放互斥器
}

// 解决资源泄漏问题
void PrettyMenu::changeBackground(std::istream& imgSrc) {
    Lock ml(&mutex); // 采用 shared_ptr 管理资源,获得互斥器并确保他单它稍后被释放
    delete bgImage;
    ++imageChanges;
    bgImage = new Image(imgSrc);
}
复制代码

2)在解决数据败坏问题之前,先看几个定义 "保证性" 选项的术语:

  • 基本承诺: 如果异常被抛出,程序内的任何事物仍然保持在有效状态下,然而程序的现实(exact state)状态可能不可预料。
  • 强烈保证: 如果异常抛出,程序状态不改变,即在调用一个提供强烈保证的函数后,程序状态只有两种可能: 如预期般地达到函数成功执行后的状态,或者回到函数被调用前的状态。
  • 不抛掷(nothrow)保证: 承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。

首先改变 PrettyMenu 的 bgImage 成员变量的类型为智能指针,再者重新排列 changeBackground 的语句次序:

复制代码
class PrettyMenu {
    ...
    std::shared_ptr<Image> bgImage; // 用智能指针管理
    ...
};

void PrettyMenu::changeBackground(std::istream& imgSrc) {
    Lock ml(&mutex);
    bgImage.reset(new Image(imgSrc));
    ++imageChanges; // 将 "++" 操作放在 reset 新的背景之后
};
复制代码

这样的操作几乎足够让 changeBackground 提供强烈的异常安全保证(因为 参数 imgSrc 可能导致 Image 的构造函数抛出异常) 。

有一个一般化的设计策略可以很典型地导致强烈保证,叫做 copy and swap。原则很简单: 为你打算修改的对象(原件)作出一个副本,然后在那副本身上作一切必要的修改。若任何修改动作导致抛出异常,原对象仍保持未改变的状态。待所有改变成功后,再将修改过的那个副本和原对象在一个不抛出任何异常的操作中置换 swap (此处可参考 条款25)。

具体实现上通常是将所有 "隶属对象的数据" 从原对象放进另一个对象内,然后赋予原对象一个指针,指向那个所谓的实现对象(implementation object,即副本),这种手法被称为 pimpl idiom,具体可参考 条款31。 对 PrettyMenu 而言,典型写法如下:

复制代码
class Image {};

// 采用 struct 而不用 class 的原因是 PrettyMenu 的数据封装性已经
// 由于 "pImpl 是 private" 而获得了保证
struct PMImpl { 
    std::shared_ptr<Image> bgImage;
    int imageChanges;
}

class PrettyMenu {
    ...
private:
    Mutex mutex;
    std::shared_ptr<PMImpl> pImpl;
};

void PrettyMenu::changeBackground(std::istream& imgSrc) {
    using std::swap;
    Lock mk(&mutex);
    std::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));  // 新建一个指针,指向副本,即实现对象

    pNew->bgImage.reset(new Image(imgSrc)); // 修改副本
    ++pNew->imageChanges;

    swap(pImpl, pNew); // 置换数据,释放mutex
}
复制代码

但是, copy and swap 会导致效率上的损失,因此 "强烈保证" 并非在任何时刻都显得实际。如果系统内有一个(唯有一个) 函数不具备异常安全性,那么整个系统就不具备异常安全性,因为调用那个不具备异常安全性的函数有可能导致资源泄漏或者数据结构败坏。

建议: 然而许多老旧的 C++ 代码 并不具备安全性。因此,当你撰写新代码或者修改旧码时,首先是 "以对象管理资源",那可以阻止资源泄漏。然后是挑选三个 "异常安全保证" 中的某一个实施于你所写的每一个函数身上,并且应该挑选 "现实可实施" 条件下的最强烈等级。

总结:

  • 异常安全函数 (Exception-safe functions) 即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证: 基本型、强烈型、不抛异常型。
  • "强烈保证" 往往能够以 copy and swap 实现出来,但是 "强烈保证" 并非对所有函数都可实现或者具备现实意义。
  • 函数提供的 "异常安全保证" 通常只等于其所调用之各个函数的 "异常安全保证" 中的最弱者。

30. 透彻了解 inlining 的里里外外

inline 函数背后的整体观念是,将 "对此函数的每一个调用" 都以函数本体替换之,当然,这样做可能增加你的目标码(object code)。但是,过度热衷于 inlining 会造成程序体积太大,即 inline 造成的代码膨胀也会导致额外的换页行为(paging),降低指令高速缓存装置的击中率(instruction cache hit rate)。

换个角度说,如果 inline 函数的本体很小,编译器针对 "函数本体" 所产出的码可能比针对 "函数调用" 所产出的码更小。

记住,inline 只是对编译器的一个申请,不是强制命令。可以明确提出(在其定义式前加上 inline 关键字),也可以隐喻提出(将函数定义在 class 定义式内)。

复制代码
class Person {
public:
    int age() const {return theAge;} // 一个隐喻的 inline 申请
private:
    int theAge;
};

template<typename T>
inline const T& std::max(const T& a, const T& b) { // inline 关键字 明确申请
    return a < b ? b : a;
}
复制代码

Note: Inlining 在大多数 C++ 程序中是编译期行为,且一个表面看似inline 函数是否真的是 inline 取决于你的建置环境,主要取决于编译器

实际上,构造函数和析构函数往往是 inlining 的糟糕候选人。因为 C++ 对于 "对象被创建和被销毁时发生了什么事" 做了各式各样的保证,所以一个看似为空的构造函数,可能实际上做了很多的构造对象的工作,析构函数亦是如此。

程序设计者必须评估 "将函数声明为 inline" 的冲击: inline 函数无法随着程序库的升级而升级。换句话说如果 某个函数 f() 是程序库内的一个 inline 函数,客户将 "f 函数本体" 编进其程序中,一旦程序设计者决定改变 f,那么所有用到 f 的客户端程序都必须重新编译。然后如果 f 是 non-inline 函数,一旦它有任何修改,客户端只需要重新连接就好,远比重新编译的负担少很多。如果程序采取动态链接,升级版的函数甚至可以不知不觉的被应用程序吸纳。

总结:

  • 将大多数 inlining 限制在小型、被频繁调用的函数上。这可以使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
  • 不要只因为 function templates 出现在头文件,就将它们声明为 inline。

31. 将文件间的编译依存关系降至最低

编译器必须在编译期间指导对象的大小,而编译器获得这项信息的唯一办法就是询问 class 的定义式。

对此,我们可以采用 pimpl idiom 设计方法,即把 Person 分割为两个 classes,一个只提供接口,一个负责实现该接口:

复制代码
#include <string>
#include <memory>

class PersonImpl; // Person 实现类的前置声明
class Date; // 前置声明
class Adress; // 前置声明
class Person {
public:
    Person(const std::string& name, const Date& birthday, const Adress& addr);
    std::string name() const;
    std::string birthDate() const;
    std::string address() const;
private:
    std::shared_ptr<PersonImpl> pImpl; // 智能指针,指向实现物
};
复制代码

这个分离的关键在于 "以声明的依存性" 替换 "定义的依存性",这正是编译依存性最小化的本质。

这个简单的设计策略是:

  • 如果使用 object references 或 object pointers 可以完成任务,就不要使用 objects。
  • 如果能够,尽量以 class 声明式 替换 class 定义式。
  • 为声明式和定义式提供不同的头文件。

像 Person 这样使用 pimpl idiom 的 classes, 往往被称为 Handle classes

第一个办法是将它们的所有函数转交给相应的实现类 (implementation classes) 并且由后者完成实际工作。

复制代码
#include "Person.h" // 我们正在实现 Person class, 必须 #inlude 其定义式
#include "PersonImpl.h" // 同理,并且 PersonImpl 和 Person 有完全相同的成员函数,接口完全相同

Person::Person(const std::string& name, const Date& birthday, const Adress& addr)
                : pImpl(new PersonImpl(name, birthday, addr))
                {}
std::string Person::name() const {
    return pImpl->name();
}
复制代码

第二个办法是 Interface class,即令 Person 成为一种特殊的 abstract base class (抽象基类)。抽象基类的目的是详细的一一描述 derived class 的接口,因此它通常不带成员变量,也没有构造函数名只有一个virtual 析构函数以及一组 pure virtual 函数,用于描述所有的接口。

class Person {
public:
    virtual ~Person(); // 虚析构函数
    virtual std::string name() const = 0;   // 纯虚函数
    virtual std::string birthDate() const = 0;
    virtual std::string address() const = 0;
}

Interface class 的客户通常调用一个特殊函数,它将扮演着 "真正将被具现化" 的那个 derived class 的构造函数的角色,常被称为 factory 函数,即工厂函数,它们返回指针(or 智能指针),指向动态分配所得对象,而该对象支持 Interface class 的接口。

复制代码
class Person {
pulic:
    static std::shared_ptr<Person> 
        create(const std::string& name, 
               const Date& birthday,
               const Adress& addr);
}

// RealPerson 是 Person 的一个具象的 derived class
std::shared_ptr<Person> Person::create(const std::string& name, 
                                       const Date& birthday,
                                       const Adress& addr) {
    return 
        std::shared_ptr<Person>(new RealPerson(name, birthday, addr));
                                       }
复制代码

对于 Handle classes 你将蒙受因动态内存分配(及其之后的释放动作)而来的额外开销,以及遭遇 bad_alloc 异常的可能性;对于 Interface class 你必须为每次含漱调用付出一个间接跳跃成本,当然呢还需要考虑 vptr 和 vtable。

总结:

  • 支持 "编译依存性最小化" 的一般构想是: 相依于声明式,不要相依于定义式。基于此构想的两个手段是 Handle classes 和 Interface classes。
  • 程序库头文件应该以 "完全且仅有声明式" (Fully and declaration-only forms) 的形式存在。这种做法不论是否涉及templates都适用。 

六、继承于面对对象设计

32.确定你的 public 继承塑膜出 is-a 关系

  • "public" 继承意味着 is-a. 适用于 base classes 身上的每一件事情也一定适用于 derived class 身上,因为每一个 derived class 对象也是一个 base class 对象。

33.避免遮掩继承而来的名称

  • derived class 内的名称会遮掩 base classes 内部的名称。在public继承下从来没有人希望如此。
  • 为了让被遮掩的名称重见天日,可使用 using 声明式或转交函数(forwarding functions).

34.区分接口继承和实现继承

  • 接口继承和实现继承不同。在public继承之下,derived class 总是继承 base class 的接口。
  • pure virtual 函数只具体指定接口继承。
  • 简朴的(非纯) impure virual 函数具体指定接口继承及缺省实现继承。
  • non-vitual 函数具体指定接口继承以及强制性实现继承。

35. 考虑 virtual 函数以外的其他选择

  • virtual 函数的替代方案包括 NVI(non-virtual interface)手法以及 Strategy 设计模式的多种形式。 NVI手法自身是一个特殊形式的 Template Method 设计模式。
    • 使用NVI手法(non-virtual interface), 它以public non-virtual 成员函数包裹较低访问性(private或protected)的virtual函数。
    • 将virtual函数替换为“函数指针成员变量”,这是 Strategy 设计模式的一种分解表现形式。
    • 以 tr1::function 成员变量替换 virtual 函数,因而允许使用任务可调用物搭配一个兼容于需求的签名式。
    • 将继承体系内的virtual函数替换为另一个继承体系内的virtual函数,这是Strategy设计模式的传统表现形式。
  • 将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问 class 的 non-public 成员。
  • tr1::function 对象的行为就像一般的行为指针。这样的对象可接纳“与给定之目标签名式(target signature)兼容"的所以可调用物(callable entity).

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

绝对不要重新定义继承而来的non-virtual函数

37. 绝不重新定义继承而来的缺省参数

绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数--你唯一应该覆写的东西,却是动态绑定。

38. 通过复合塑膜出 has-a 或  "根据某物实现出(is-implemented-of-terms-of)"

  • 复合composition的意义和public继承完全不同。
  • 在应用域,复合意味着has-a。在实现域,复合意味着is-implemented-of-terms-of。

39. 明智而审慎的使用private继承 

  • private继承意味着 is-implemented-in-terms-of (根据某物实现出)。它通常比复合composition的级别低。但是当derived class 需要访问 protected base class 的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。
  • 和复合不同,私有继承可以造成 empty base 最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。

40. 明智而审慎地使用多重继承

  • 多重继承比单一继承复杂,它可能导致新的歧义性,以及对virtual即成的需要。
  • virtual继承只会增加大小,速度,初始化(及赋值)复杂度等等成本。如果virtual base class 不带任何数据,将是最具实用价值的情况。
  • 多重继承的确有正当用途。其中一个情节涉及“public 继承某个Interface class”和“private 继承某个协助实现的class”的两相组合。

七. 模版与泛型编程

41. 了解隐式接口和编译器多态

  • classes 和 templates 都支持接口interfaces和多态polymorphism。
  • 对classes而言,接口是显式的,以函数签名为中心。多态则是通过virtual函数发生于运行期。
  • 对于template参数而言,接口是隐式的,奠基于有效表达式。多态则是通过template具现化和函数重载解析 function overloading resolution 发生于编译期。

42. 了解typename的双重意义

  • 声明template参数时,前缀关键字 class 和 typename 可互换。
  • 请使用关键字 typename 标识 嵌套从属类型名称;但不得在 base class lists 或者 member initialization list 内以它作为 base class 修饰符。

43. 学习处理模版化基类的名称

可在 derived class template 内通过 “this->”指涉 base class templates 内的成员名称,或藉由一个明白写出的 “base class 资格修饰符”

44. 将与参数无关的代码抽离 templates

  • templates 生成多个 classes 和多个函数,所以任何 template 代码都不改与某个造成膨胀的 template 参数产生相依关系。
  • 因非类型模版参数 non-type template parameters 而造成的代码膨胀,往往可消除,做法是以函数参数或 class 成员变量替换 templates 参数。
  • 因类型参数 type parameters 而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述 binary representations 的具现类型 instantiation types 共享实现码。

45. 运用成员函数模版接受所以兼容类型

  • 请使用 member function templates 生成 “可接受所以兼容类型”的函数。
  • 如果你声明 member templates 用于“泛化copy构造”或“泛化assignment操作”,你还是需要声明正常的copy构造函数和copy assignment操作符。

46. 需要类型转换时请为模版定义非成员函数

  • 当我们编写一个 class template,而它所提供之 “与此template相关的” 函数支持 “所有参数之隐式类型转换”时,请将那些函数定义为 “class template 内部的 friend 函数”

47. 请使用 traits classes 表现类型信息

  • Traits classes 使得“类型相关信息”在编译期可用。它们以 templates 和 “templates特化”实现。
  • 整合重载技术 overloading 后,traits classes 有可能在编译期对类型执行 if...else测试。

48. 认识 template 元编程

  • TMP template metaprogramming 可将工作由运行期移往编译期:因而得以实现早期错误侦测和更高的执行效率。
  • TMP 可被用来生成“基于政策选择组合”的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。

八. 定制 new 和 delete

49. 了解 new-handler 行为

  • set_new_handler 允许客户指定一个函数,在内存分配无法获得满足时倍调用。
  • Nothrow new 是一个颇为局限的工具,因为它只适用于内村分配:后继的构造函数调用还是可能抛出异常。

50. 了解 new 和 delete 的合理替换时机

有许多理由需要写个自定的new 和 delete,包括改善效能,对heap运行错误进行调试,收集heap使用信息。

51. 编写new 和 delete时需固守常规

  • operator new 应该内含一个无限循环,并在其中尝试分配内存,如果它无法满足内存需求,就应该调用 new-handler。它也应该有能力处理0 bytes 申请。 Class 专属版本则还应该处理 “比正确大小更大的(错误)申请”。
  • operator delete 应该在收到 null 指针时不做任何事。Class 专属版本则还应该处理 “比正确大小更大的(错误)申请”。

52. 写了 placement new 也要写 placement delete

  • 当你写一个 placement  operator new, 请确定页写出了对于的 placement  operator delete。如果没有这样做,你的程序可能会发生隐微而时断时续的内存泄漏。
  • 当你声明了 placement new 和 placement delete,请确定不要无意识地遮掩了他们的正常版本。

九. 杂项讨论

53. 不要轻忽编译器的警告

  • 严肃对待编译器发出的警告信息。努力在你的编译器的最高(最严苛)警告级别下争取“无任何警告”的荣誉。
  • 不要过度依赖编译器的报警信息,因为不同编译器对待事情的态度并不相同。一旦移植到另一个编译器上,你原本倚赖的警告信息可能消失。

54. 让自己熟悉包括 TR1 在内的标准程序库

  • C++标准程序库的主要机能由 STL,iostream,locales组成。并包含 C99 标准程序库。
  • TR1 添加了智能指针,例如 tr1::shared_ptr,一般化指针 tr1::function,hash-based 容器,正则表达式以及另外10个组件的支持。
  • TR1自身只是一份规范,为获得TR1提供的好处,你需要一份实物,一个好的实物来源是 Boost。

55. 让自己熟悉Boost

posted @   算是一个初学者  阅读(127)  评论(0编辑  收藏  举报
编辑推荐:
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架
点击右上角即可分享
微信分享提示