zoukankan      html  css  js  c++  java
  • 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:留意模板元编程

  • 相关阅读:
    03、使用字符串
    加载selenium2Library失败---robotframework环境搭建(site-packages下无selenium2library文件夹)
    python无法启动火狐浏览器且报错“selenium.common.exceptions.WebDriverException: Message: Unable to find a matching set of capabilities”
    移动H5前端性能优化指南
    appium+python 启动一个app步骤
    Appium_Python_Client介绍
    python自动化---各类发送邮件方法及其可能的错误
    python自动化--批量执行测试之生成报告
    揭秘webdriver实现原理【转】
    selenium 三种断言以及异常类型
  • 原文地址:https://www.cnblogs.com/larry-xia/p/9836470.html
Copyright © 2011-2022 走看看