Effective C++

explicit声明

class B
{
public: 
    explicit B(int x = 0, bool b = true) //默认构造函数,要么没有参数,要么所有参数都有默认值
    { 
        m_x = x; 
    }
public:
    int m_x;
};

void doSomething(B bObject)
{
    std::cout << bObject.m_x << endl;
}
int main()
{
    doSomething(B());  //ok   0
    doSomething(B(12)); //ok   12
    //doSomething(12);  //error 
    system("pause");
    return 0;
}

通过在构造函数前加explict,用以避免隐式类型转换。

条目1:把C++看成多种语言的联合体

 C++是一门多范型编程语言,可以将C++看成是一个由若干门语言组成的联合体:

  • C
  • 面向对象的C++
  • 包含模板的C++
  • STL

条目2:多用const、enum、inline,少用#define

#define ASPECT_RATIO 1.653替换为定义一个常量

const double AspectRatio=1.653;

对于类内部的常量,为了限制常量的份数超过一份,必须将该常量声明为static成员。

class GamePlayer
{
private:
    static const int NumTurns = 5; //常量声明
    int scores[NumTurns];
};
const int GamePlayer::NumTurns; //常量定义

类内部的为常量声明,而非定义。类内部的静态常量如果是整形(整数、字符型、布尔型)则不需要定义。只要你不需要得到它们的地址,你可以声明它们、调用它们而不需要定义。

对于简单的常量,多用const对象或者枚举类型数据,少用#define

多用内联函数来代替带参宏

条目3:尽可能使用const

char greeting[] = "Hello";
char *p1 = greeting;  //非const指针,非const数据
const char *p2 = greeting; //非const指针,const数据
char *const p3 = greeting; //const指针,非const数据
const char *const p4 = greeting; //const指针,const数据

const成员函数

const成员函数可以被具有相同参数列表的非const成员函数重载:

 1 class TextBlock
 2 {
 3 private: 
 4     std::string text;
 5 public:
 6     TextBlock(std::string s)
 7     {
 8         text = s;
 9     }
10     char& operator[](std::size_t position)
11     {
12         return text[position]; //用于非const对象
13     }
14     const char& operator[](std::size_t position) const
15     {
16         std::cout << "const func" << endl;
17         return text[position]; //用于const对象
18     }
19 };
20 
21 int main()
22 {
23     TextBlock tb("Hello");
24     std::cout << tb[0] << endl;
25     const TextBlock ctb("World");
26     std::cout << ctb[0] << endl;
27     return 0;
28 }
View Code

关于const成员函数的两种说法:

  • 按位恒定:当且仅当一个成员函数对其所在对象的所有数据成员(static数据成员除外)都不做改动时,才需要将成员函数声明为const.但是,如果据成员是指针,则const成员函数并不能保证不修改指针指向的对象,编译器不会把这种修改检测为错误。
  • 逻辑恒定:如果某个对象调用了一个const成员函数,则这个成员函数可以对对象作出内部改动,但仅仅是客户端无法察觉的方式进行。

条目4:确保对象在使用前得到初始化

读取未初始化的数据将导致未定义行为。在一些语言平台中,通常情况下读取未初始化的数据仅仅是使你的程序无法运行罢了。更典型的情况是,这样的读取操作可能会得到内存中某些位置上的半随机的数据,这些数据将会“污染”需要赋值的对象,最终,程序的行为将变得十分令人费解,你也会陷入烦人的除错工作中。

解决这类不确定的问题的最好途径是:总是在使用对象之前进行初始化。对于内置类型的非成员对象,需要手动完成这一工作。注意赋值和初始化的区别:

C++约定:一个对象的数据成员要在进入构造函数内部之前得到初始化。在进入ABEntry构造函数内部之前,这些数据成员的默认构造函数应该自动得到调用。注意对于numTimesConsulted成员不成立,因为其为内置类型,对其而言,在被赋值之前,无法确保其得到了初始化。

更好的方法是使用初始化表,这样效率会更高:

 C++对象中数据成员的初始化顺序为其在类中声明的顺序,而不是成员初始化列表中的顺序。为了使读者不至于陷入困惑,应保证初始化表中的顺序与声明时的顺序保持一致。

条目5:要清楚C++后台为你书写和调用了什么函数

对于一个类来说,如果不自己手动声明一个复制构造函数、赋值运算符、析构函数,编译器会自动声明这些函数,没有声明构造函数的话,编译器也会为你声明一个默认构造函数。所有这些函数都是public和inline的。

条目6:要显式禁止编译器为你生成不必要的函数

通常情况下,如果你希望一个类不支持某种特定的功能,你需要做的仅仅是不去声明那个函数。然而这一策略对复制构造函数和拷贝赋值运算符就失效了,这是因为,即使你不做声明,而一旦有人尝试调用这些函数,编译器就会为你自动声明它们(参见条目 5)。解决问题的关键是,所有编译器生成的函数都是公共的。为了防止编译器生成这些函数,将复制构造函数和赋值运算符声明为私有的。通过显式声明一个函数,你就可以防止编译器去自动生成这个函数,同时,通过将函数声明为private的,你便可以防止人们去调用它。同时为了防止其他成员函数或者友元函数访问这些private函数,可将这些private成员函数只声明而不进行定义。

class HomeForSale
{
public:
    HomeForSale() {}
private:
    HomeForSale(const HomeForSale&);//只有声明,无定义
    HomeForSale& operator=(const HomeForSale&); //只有声明,无定义
};
View Code

条目7:要把多态基类的析构函数声明为虚函数

C++有明确的规则:如果希望通过一个基类类型的指针来删除一个派生类对象,并且基类的析构函数为非虚析构函数,则结果是未定义的。典型的后果是,运行中派生类中新派生出的部分将得不到销毁,基类部分会被销毁掉,这样就产生了一个古怪的“部分销毁”的现象。

排除这一问题的方法很简单:为基类提供一个虚拟的析构函数,这样删除一个派生类对象,程序就可以精确地按照需要进行了,这个对象都会得到销毁。任何有虚函数的类几乎都需要一个虚析构函数,如果一个类不包含虚函数,则通常情况下意味着它将不作为基类使用。当一个类不作为基类使用时,将它的析构函数声明为虚函数不是个好主意

应该为多态基类声明虚析构函数。一旦一个类包含虚函数,它就应该包含一个虚析构函数。

如果一个类不用作基类或者不需具有多态性,便不应该为它声明虚析构函数。

条目8:防止因异常中止析构函数

 条目9:永远不要在构造或者析构的过程中调用虚函数

创建一个派生类的对象时,基类的构造函数优先于派生类的构造函数运行,在基类构造函数运行的时候,派生类的数据成员还未得到初始化。对于一个派生类的对象来说,在其进行基类部分构造工作的时候,这一对象的类型就是基类的。不仅仅虚函数会解析为基类的,而且 C++中“使用运行时类型信息”的部分(比如 dynamic_cast(参见条目 27)和typeid)也会将其看作基类类型的对象。

对于析构过程可以应用同样的推理方式。一旦派生类的析构函数运行完毕,对象中派生类的那一部分数据成员将取得未定义的值,所以 C++会认为它们不再存在。在进入基类的析构函数时,这个对象将成为一个基类对象,C++的所有部分——包括虚函数、dynamic_cast 等等——都会这样对待该对象。

条目10:让赋值运算符返回一个指向*this的引用

int x, y, z;
x = y = z = 15; //一连串的赋值操作

这种实现的本质是:赋值时,返回一个指向运算符左边对象的引用。当为你的类实现赋值运算符时,应遵守这一惯例,这一惯例对所有的赋值运算符同样适用。

 1 class Widget
 2 {
 3 public:
 4     Widget() {}
 5     Widget& operator=(const Widget& rhs)
 6     {
 7         //other code
 8         return *this; //返回运算符左边的对象
 9     }
10     Widget& operator+=(const Widget& rhs)
11     {
12         //other code
13         return *this; //返回运算符左边的对象
14     }
15 };
View Code

条目11:在operator=中要处理自赋值问题

Widget& operator=(const Widget& rhs)
    {
        if (this == &rhs) return;
        this->a = rhs.a;
        return *this;
    }

条目12 要复制整个对象,不要遗漏任一部分

当没有手动定义拷贝成员函数时(拷贝构造函数和拷贝赋值运算符),编译器将自动生成拷贝函数,且自动生成的拷贝函数可以精确地按你所期望的方式运行,当前正在赋值的所有对象都会得到复制。然而当自己声明拷贝函数时,如果拷贝函数内只进行部分复制,编译器不会给出任何警告和错误。通过继承,这一问题可以带来更加严重却隐蔽的危害。

 1 void logCall(const std::string& msg)
 2 {
 3     ///
 4 }
 5 class Customer
 6 {
 7 public:
 8     Customer():name("unknown"){}
 9     Customer(const std::string& s) :name(s) {}
10     Customer(const Customer& rhs):name(rhs.name)
11     {
12         logCall("Customer copy constructor");
13     }
14     Customer& operator=(const Customer& rhs)
15     {
16         logCall("Customer copy assignment operator");
17         name = rhs.name;
18         return *this;
19     }
20 private:
21     std::string name;
22 };
23 ////////////////////////////////////////
24 class PriorityCustomer :public Customer
25 {
26 public:
27     PriorityCustomer() :priority(0) {}
28     PriorityCustomer(const PriorityCustomer& rhs);
29     PriorityCustomer& operator=(const PriorityCustomer& rhs);
30 private:
31     int priority;
32 };
33 PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs) :priority(rhs.priority)
34 {
35     logCall("PriorityCustomer copy constructor");
36 }
37 PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs)
38 {
39     logCall("Customer copy assignment operator");
40     priority = rhs.priority;
41     return *this;
42 }
View Code

拷贝时,PriorityCustomer对象从基类继承而来的成员始终没有得到复制。一旦你亲自为一个继承类编写了拷贝函数,你必须同时留心其基类的部分。当然这些部分通常情况下是私有的,所以你无法直接访问它们。取而代之的是,派生类的拷贝函数必须调用这些私有数据在基类中相关的函数。

PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
    :Customer(rhs),  //调用基类的拷贝构造函数
    priority(rhs.priority)
{
    logCall("PriorityCustomer copy constructor");
}
PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
    logCall("Customer copy assignment operator");
    Customer::operator=(rhs); //为基类部分赋值
    priority = rhs.priority;
    return *this;
}
View Code

 条目13:要使用对象来管理资源

为了确保createInvestment()所返回的资源总能得到释放,我们需要将这类资源放置在一个对象中,以来C++对默认析构函数的自动调用来确保资源及时得到释放。标准库中的auto_ptr是类似于指针的对象(智能指针),其析构函数可以自动对其所指内容执行delete。

由于当一个 auto_ptr 被销毁时,它将自动删除其所指向的内容,所以永远不存在多个 auto_ptr 指向同一个对象的情况,这一点很重要。如果存在的话,这个对象就会被多次删除,这样你的程序就会立即陷入未定义行为。为了防止此类问题发生,auto_ptr 有一个不同寻常的特性:如果你复制它们(通过拷贝构造函数或者拷贝赋值运算符),它们就会被重设为 null,然后资源的所有权将由复制出的指针独占

引用计数智能指针是auto_ptr的替代品,它可以跟踪有多少个对象指向了一个特定的资源,同时在没有指针再指向这一资源时,智能指针会自动删除该资源。可以看出引用计数智能指针的行为和垃圾回收器相似。

auto_ptr和tr1::shared_ptr在析构函数中使用的是delete语句,而不是delete[]。这就意味着对于动态分配的数组使用auto_ptr和tr1::shared_ptr不是一个好主意。但是遗憾的是,这样的代码会通过编译。

条目14:要注意资源管理类中的复制行为

条目15:要为资源管理类提供对原始资源的访问权

条目16:互相关联的new和delete要使用相同的形式

使用new语句时,将会分配内存和为这段内存调用一个或者多个构造函数;当使用delete语句时,将会为分配的内存调用一个或多个析构函数,释放内存。

std::string *stringPtr1 = new std::string;
std::string *stringPtr2 = new std::string[100];
delete stringPtr1; 
delete[] stringPtr2;

对stringPtr1调用delete[],或者对stringPtr2调用delete,都会导致未定义的行为。这里的规则很简单:如果你在一个 new 语句中使用了[],那么你必须在相关的delete 语句中也使用[]。如果你在一个 new 语句中没有使用[],那么在相关的delete 语句中也不应使用[]。

条目17:用智能指针存储由new创建的对象时要使用独立的语句

processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());

在编译器能够生成对processWidget的调用前,必须对传入的参数进行处理,编译器必须自动生成代码来解决下面三件事情:1.调用priority(),2.执行new Widget,3.调用tri::shared_ptr的构造函数。C++编译器对这三项任务完成的顺序要求的很宽松,调用priority()可能出现在第2步,如果调用priority抛出异常的话,则第一步new Widget返回的指针将会消失,这种情况下,processWidget()可能会造成资源泄露。这是因为:在资源被创建(通过 new Widget)以后和将这个资源转交给一个资源管理对象之前的这段时间内,有产生异常的可能。防止这类问题发生的办法很简单:使用单独的语句,创建 Widget 并将其存入一个智能指针,然后将这个智能指针传递给 processWidget。

 条目18:要让接口易于正确使用,而不易被误用

优秀的接口应该易于正确使用,而不易误用。你应该力争让你所有的接口都具备这一特征。
 增加易用性的方法包括:让接口保持一致性,让代码与内建数据类型保持行为上的兼容性。
 防止错误发生的方法包括:创建新的数据类型,严格限定类型的操作,约束对象的值,主动管理资源以消除客户的资源管理职责。

 条目19:要像设计类型一样设计class

 

条目20:传参时要多用“引用常量”,少用传值

默认情况下,C++为函数传入和传出对象是采用传值方式的(这是从C语言继承而来的特征)。除非你明确使用其他方法,否则函数的形参总是通过复制实参的副本来创建,而且,函数的调用者得到的也是函数返回值的副本。这些副本是由对象的拷贝构造函数创建的。

bool validateStudent(const Student& s);

通过引用传参也可以避免“截断问题”。当一个派生类的对象以一个基类对象的形式传递(传值方式)时,基类的拷贝构造函数就会被调用,此时,这一对象的独有特征——使它区别于基类对象的特征会被“截掉”。剩下的只是一个简单的基类对象,这并不奇怪,因为它是由基类构造函数创建的。通过传递常量引用,可以避免截断问题。

C++编译器中,引用通常是以指针的形式实现的,所以通过引用传递实质是传递一个指针。传递一个内置数据类型的对象,传值会比传递引用更为高效,迭代器和 STL 中的函数对象也是如此,这是因为它们设计的初衷就是能够更适于传值,这是 C++的惯例。

条目21:在必须返回一个对象时,不要尝试返回一个引用

这个函数会返回一个指向result的引用,但result为一个局部对象,局部对象在函数退出时会被销毁。事实上,任何返回局部对象的引用的函数都是灾难性的(任何返回指向局部对象的指针的函数也是灾难性的)

条目22:将数据成员声明为私有的

protected并不会带来比public更高的封装性

条目23:多用非成员非友元函数,少用成员函数

多用非成员非友元函数,少用成员函数。这样做可以增强封装性,以及包装的灵活性和功能的扩展性。

条目24:当函数所有参数都需要进行类型转换时,要将其声明为非成员函数

条目25:最好不要让swap抛出异常

 1 #include <iostream>
 2 #include <string>
 3 #include <vector>
 4 using namespace std;
 5 
 6 class WidgetImpl
 7 {
 8 public:
 9     WidgetImpl() :a(1), b(2), c(3), d(4) {}
10     WidgetImpl(int _a, int _b, int _c, int _d) :a(_a), b(_b), c(_c), d(_d) {}
11 private:
12     int a, b, c, d;
13 };
14 class Widget
15 {
16 public:
17     Widget(WidgetImpl* pp) :pImpl(pp) {}
18     Widget(const Widget& rhs)
19     {
20         *pImpl = *rhs.pImpl;
21     }
22     Widget& operator=(const Widget& rhs)
23     {
24         if (this == &rhs) return *this;
25         *pImpl = *rhs.pImpl;
26         return *this;
27     }
28     void swap(Widget& other)
29     {
30         using std::swap;
31         swap(pImpl, other.pImpl);//交换两个Widget,只需交换其pImpl指针
32     }
33 private: 
34     WidgetImpl *pImpl;
35 };
36 //////////////////
37 namespace std
38 {
39     //将Widget的std::swap特化
40     template<>
41     void swap<Widget>(Widget& a, Widget& b)
42     {
43         cout << "swap widget..." << endl;
44         a.swap(b);
45     }
46 }
47 
48 
49 
50 int main()
51 {
52     WidgetImpl imp1(1, 2, 3, 4);
53     WidgetImpl imp2(4, 5, 6, 7);
54     Widget wd1(&imp1);
55     Widget wd2(&imp2);
56     swap(wd1, wd2);
57     system("pause");
58 }
View Code

 如果默认的 swap 实现并不够高效(大多数情况下意味着你的类或模板正在运用 pimpl 惯用法),请按下面步骤进行:

1. 提供一个公用的 swap 成员函数,让它可以高效的交换你的类型的两个对象的值。理由将在后面列出,这个函数永远不要抛出异常。
2. 在你的类或模板的同一个名字空间中提供一个非成员的 swap。让它调用你的swap 成员函数。
3. 如果你正在编写一个类(而不是类模板),要为你的类提供一个 std::swap的特化版本。同样让它调用你的 swap 成员函数。
最后,如果你正在调用 swap,要确保使用一条 using 声明来使 std::swap 对你的函数可见,然后在调用 swap 时,不要做出任何名字空间的限制。

条目26:定义变量的时机越晚越好

条目27:尽量少用转型操作

 

 const_cast用于脱去对象的恒定性

dynamic_cast 主要用于进行“安全的向下转型”

static_cast 可以用于强制隐式转换

 

条目28:不要返回指向对象内部部件的“句柄”

  条目29:力求代码做到“异常安全”

 抛出异常时,异常安全的代码应该能够做到:不泄漏资源,不能让数据结构遭到破坏。

条目30:深入探究内联函数

inline是对编译器的一次请求,而不是一条命令,这种请求可以显式提出,也可以隐式提出,隐式提出的途径是在类定义的内部定义函数,显式方法为在函数定义前加inline关键字。

inline是对编译器的请求,但编译器可能会忽略它,大多数编译器如果认为当前的函数过于复杂(比如包含循环或递归的函数),或者这个函数是虚函数(即使是最平常的虚函数调用),就会拒绝将其内联。

 条目31:尽量减少文件间的编译依赖

接口类实现方法

//Person.h
#pragma once
#include <string>
#include <memory>
class Person
{
public:
    virtual ~Person() {};
    virtual std::string name() const= 0;
    virtual std::string birthDate() const = 0;
    virtual std::string address() const = 0;
public:
    static std::shared_ptr<Person>
        create(const std::string& _name,
            const std::string& _birth,
            const std::string& _addr);
};

//RealPerson.h
#pragma once
#include "Person.h"
class RealPerson : public Person {
public:
    RealPerson(const std::string& name, const std::string& birthday,
        const std::string& addr)
        : theName(name), theBirthDate(birthday), theAddress(addr)
    {}
    virtual ~RealPerson() {}
    std::string name() const;  // 这里省略了这些函数的具体实现,
    std::string birthDate() const;  // 但是很容易想象它们是什么样子。
    std::string address() const;
private:
    std::string theName;
    std::string theBirthDate;
    std::string theAddress;
};

//RealPerson.cpp
#include "RealPerson.h"

std::string RealPerson::name() const
{
    return theName;
}
std::string RealPerson::birthDate() const
{
    return theBirthDate;
}
std::string RealPerson::address() const
{
    return theAddress;
}
std::shared_ptr<Person> Person::create(const std::string& _name, const std::string& _birth, const std::string& _addr)
{
    return std::shared_ptr<Person>(new RealPerson(_name, _birth, _addr));
}

//test.cpp
#include <iostream>
#include <string>
#include <vector>
#include "Person.h"
using namespace std;

int main()
{
    std::shared_ptr<Person> pp(Person::create("aaa", "2019", "beijing"));
    cout << pp->name() << " " << pp->birthDate() << " " << pp->address() << endl;
    system("pause");
}
View Code

 句柄类实现方法

//PersonImpl.h
#pragma once
#include <string>
class PersonImpl 
{
public:
    PersonImpl(const std::string& name, const std::string& birthday,
        const std::string& addr)
        : theName(name), theBirthDate(birthday), theAddress(addr)
    {}
    ~PersonImpl() {}
    std::string name() const;  // 这里省略了这些函数的具体实现,
    std::string birthDate() const;  // 但是很容易想象它们是什么样子。
    std::string address() const;
private:
    std::string theName;
    std::string theBirthDate;
    std::string theAddress;
};

//PersonImpl.cpp
#include "PersonImpl.h"

std::string PersonImpl::name() const
{
    return theName;
}
std::string PersonImpl::birthDate() const
{
    return theBirthDate;
}
std::string PersonImpl::address() const
{
    return theAddress;
}

//Person.h
#pragma once
#include "PersonImpl.h"
#include <memory>
class Person
{
public:
    Person(const std::string& name, const std::string& birthday,
        const std::string& addr);
    std::string name() const;
    std::string birthDate() const;
    std::string address() const;
private:
    std::shared_ptr<PersonImpl> pImpl; //指向实现的指针
};

#include "Person.h"
#include "PersonImpl.h"
Person::Person(const std::string& name, const std::string& birthday, const std::string& addr)
    :pImpl(new PersonImpl(name,birthday,addr))
{
}
std::string Person::name() const
{
    return pImpl->name();
}
std::string Person::birthDate() const
{
    return pImpl->birthDate();
}
std::string Person::address() const
{
    return pImpl->address();
}

//test.cpp
#include <iostream>
#include <string>
#include <vector>
#include "Person.h"
using namespace std;

int main()
{
    Person pp("aaa", "2019", "beijing");
    cout << pp.name() << " " << pp.birthDate() << " " << pp.address() << endl;
    system("pause"); 
}
View Code

 条目32:确保公共继承以“A是一个B”形式进行

公共继承意味着“A 是一个 B”关系。对于基类成立的一切都应该适用于派生类,因为派生类的对象就是一个基类对象。

条目33:避免隐藏继承而来的名字

当我们在一个派生类的成员函数中企图引用基类的某些内容(比如成员函数、typedef、或者数据成员等等)时,编译器能够找出我们所引用的内容,因为派生类所继承的内容在基类中都做过声明。这里真正的工作方式实际上是:派生类的作用域嵌套在基类的作用域中。

即使同一函数在基类和派生类中的参数表不同,基类中该函数依然会被隐藏,而且这一结论不会因函数是否为虚函数而改变。

派生类中的名字会将基类中的名字隐藏起来。在公共继承体系下,这是我们永远不希望见到的。

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

声明纯虚函数的目的是让派生类仅仅继承函数接口。为纯虚函数提供一个定义并没有被C++所禁止,但是在调用这种函数时,需要加上类名。

声明简单虚函数(非纯虚函数)的目的是让派生类继承函数接口的同时,继承一个默认的具体实现。

声明一个非虚函数的目的是让派生类继承这一函数接口,同时强制继承其固定的具体实现。

条目35:虚函数的替代方案

 条目36:避免对派生的非虚函数进行重定义

class B
{
public:
    void mf()
    {
        cout << "B mf()" << endl;
    }
};
class D:public B
{
public:
    void mf()
    {
        cout << "D mf()" << endl;
    }
};



int main()
{
    D x;
    B *pb = &x;
    D *pd = &x;
    pb->mf(); //B mf()
    pd->mf(); //D mf()
    system("pause"); 
}
View Code

条目37:避免对函数中继承得来的默认参数值进行重定义

可以继承的函数可以分为两种:虚拟的和非虚拟的。然而,由于重定义一个派生的非虚函数始终是一个错误(参见条目 36),因此我们可以放心地将此处的讨论范围缩小至以下情况:继承一个含有默认参数值的虚函数。

虚函数是动态绑定的,而默认参数值是静态绑定的

一个对象的静态类型就是在对其进行声明时赋予它的类型;一个对象的动态类型是通过它当前引用的对象的类型决定的,动态类型表明了它应具有怎么样的行为。

Shape *pc = new Circle;

pc的静态类型为Shape*,动态类型为Circle*

 虚函数是动态绑定的,意味着,对于一个特定函数的调用,其调用对象的动态类型将决定调用这一函数的哪个版本。

class Shape
{
public:
    enum ShapeColor{Red,Green,Blue};
    
    virtual void draw(ShapeColor color = Red) const = 0;
};
class Rectangle :public Shape
{
public:
    virtual void draw(ShapeColor color = Green) const
    {
        cout << "use color:" << color << endl;
    }
};

int main()
{
    Shape* pr = new Rectangle();
    pr->draw(); //use color:0
    system("pause"); 
}
View Code

条目38:使用组合来表示“A拥有一个B”、“A以B的形式实现”

class Address
{};
class PhoneNumber
{};
class Person
{
private:
    std::string name;
    Address address;
    PhoneNumber voiceNumber;
    PhoneNumber faxNumber;
};
View Code

条目39:审慎使用私有继承

如果类之间的层次关系是私有继承的话,那么编译器一般不会将派生类对象(比如 Student)直接转换为一个基类对象(比如 Person)。

派生类中继承自私有基类的成员也将成为私有成员,即使他们在基类中用 public 或 protected 修饰也是如此。

私有继承意味着“A 以 B 的形式实现”。通常它的优先级要低于组合,但是当派生类需要访问基类中受保护的成员,或者需要重定义派生的虚函数时,私有继
承还是有其存在的合理性的。

条目40:审慎使用多重继承

 条目41:理解隐式接口和编译时多态

类的接口是显式的,基于函数签名。多态通过虚函数产生于运行时。模板参数的接口是隐式的,基于逻辑表达式。

条目42:理解typename的双重含义

在声明模板参数时,class 和 typename 可以互换。使用 typename 来指定嵌套从属类型名字。

条目43:了解如何访问模板化基类内的名字

要在派生类模板中调用基类模板内部的名字,可以通过“this->”前缀,通过using 声明,或者通过一个显式的基类限定来实现。

条目44:将参数无关的代码从模板中分离

类模板的成员函数只有在被使用时才会被隐式初始化

条目45:使用成员函数模板来接纳“所有兼容类型”

条目46:在需要进行类型转换时要将非成员函数定义在模板内部

在编写一个类模板时,如果需要提供一个与该模板相关的、对所有参数支持隐式类型转换的函数,要在类模板内部将其定义为友元。

template<typename T>
class Rational {
public:
    Rational(const T& numerator = 0,const T& denominator = 1)
        :num(numerator), den(denominator)
    {
    }
    const T numerator() const
    {
        return num;
    }
    const T denominator() const
    {
        return den;
    }
    friend const Rational operator*(const Rational& lhs,const Rational& rhs)
    {
        Rational<T> result(lhs.numerator()*rhs.numerator(), lhs.denominator()*rhs.denominator());
        return result;
    }
private:
    T num;
    T den;
};
int main()
{
    Rational<int> a(1,2);
    Rational<int> result = a*2;//隐式类型转换
    return 0;
}
View Code

条目47:使用traits类来描述类型的相关信息

条目48:留意模板元编程

 

posted @ 2018-10-23 14:34  summer91  阅读(434)  评论(0编辑  收藏  举报