【笔记】C++的150个建议,第四章

2019-12-28  本文已影响0人  NeilXu

目录
第一部分 语法篇

  1. 第一章 从C继承而来
  2. 第二章 从C到C++
  3. 第三章 内存管理
  4. 第四章 类

第四章 重中之重的类

建议36:class与struct的区别

C++中的struct可以包含函数、可以继承。

  1. 大括号初始化
    (1)struct如果没有定义构造函数,就能用大括号初始化;否则,不能。
    (2)class只能在所有成员函数为public,且无自定义构造函数时时,才可以用大括号初始化。
  2. 默认访问权限:structpublic的,classprivate
  3. 继承方式:struct默认是public继承

建议37:编译器对类悄悄做的事

类的3个重要组成部分:一个或多个构造函数、一个析构函数、一个拷贝赋值运算符。
当定义一个类时,编译器会隐式地生成这些方法。可以通过deletedefault告诉编译器是否自动生成这些函数。

class A
{
    A() = default;              // 需要生成
    A (const A&) = delete;      // 禁止生成
    virtual ~A() = default;     // 需要生成
    A & operator = (const A&) = delete;  // 禁止生成
}

注意:默认的拷贝构造函数可能会出现浅拷贝的问题。
对于一个空类,为了能够实现它的实例化,编译器会强制使其大小由0变成1.

建议38:首选初始化列表实现类成员的初始化

class A
{
private:
    A operator=(const A& rhs);
};
class B
{
public:
    B();
    ~B();
private:
    A mA;
};
B::B()
    : mA(A())
{
    mA = A();    // Error
}

如何拒绝对象的复制操作

namespace noncopyable_
{
    class noncopyable
    {
    protected:
        noncopyable(){}
        ~noncopyable(){}
    private:
        noncopyable(const noncopyable& );
        const noncopyable& operator=(const noncopyable& );
    }
}
class CStudent: private boost::noncopyable
{};

建议40:自定义拷贝构造函数

class B: public A
{
    B& operator=(const B& b){
        if(this == &b){
            return *this;
        }
        A::operator=(b);    // 拷贝基类部分的数据。
        // copy B ...
        return *this;
    }
}

建议41:谨防因构造函数抛出异常而引发的问题

构造函数抛出异常会引起对象的部分构造,因为不能自动调用析构函数,在异常发生之前分配的资源将得不到及时的清理,进而造成内存泄露问题。所以,如果对象中涉及了资源分配,一定要对构造之中可能抛出的异常做谨慎而细致的处理。

建议42:多态基类的析构函数应该为虚

  1. 只要类中包含一个虚函数,就要将析构函数声明为虚。

当通过基类的指针delete派生类对象时,如果基类的析构函数不是虚函数,那么C++不会调用整个析构链。只是调用基类的析构函数,出现“部分析构”的问题。

  1. 如果一个类的析构不被设计为基类,那么就不应该把析构函数设置为虚。因为会多余地产生“虚函数表”。

另外,标准库中的string、complex,以及STL容器,虚构函数非虚,不能被继承。

建议43:构造函数不能为虚

构造前,内存还没分配,无法找到虚函数表。

建议44:避免在构造/析构函数中调用虚函数

class Base {
  public: Base() {
    cout << "Base constructor\n";
    Init();
  }
  virtual void Init() {
    cout << "Base::Init " << endl;

  }
};
class Derived: public Base {
  public: Derived(): Base() {
    cout << "Derived constructor" << endl;
  }
  virtual void Init() {
    cout << "Derived::Init " << endl;
  }
};
int main() {
  Derived d;
  return 0;
}
// output:
Base constructor
Base::Init
Derived constructor

上例中,调用虚函数,是为了实现多态,即用基类实例去调用派生类的虚函数。
但是,由于在派生类被正确地构造出来之前,调用派生类的徐成员函数是没有意义的。因为构造派生类之前,需要构造基类。因此调用基类的虚函数时,派生类的实例还不存在。
所以,基类的虚函数指向的还是自己。成员函数,包括虚成员函数,都可以在构造、析构的过程中被调用。

构造顺序:基类;派生类
析构顺序:派生类;基类

  1. 如果在构造函数或析构函数中调用了一个类的虚函数,那它们就变成了普通函数,失去了多态的能力。
    换句话说,对象不能在生与死的过程中表现出『多态』。

C++标准规范:

当一个虚函数被构造函数或析构函数直接或间接地调用时,调用对象就是正在构造或者析构的那个对象。
其调用的函数是定义于自身类或者其基类的函数,而不是其派生类或者最底派生类的其他基类的重写函数。

建议45:谨慎使用默认参数的构造函数

不合理地使用默认参数,将会导致重载函数的二义性。

CTimer(string name, int hour=0)
{
}
CTimer(string name)
{
    int hour = 11;
}
// 当使用如下方式构造时,将会产生二义性
CTimer cTimer("a");

建议46:重载

Overloading、Overriding与Hiding

  1. 重载,Overloading。同一作用域的不同函数使用相同的函数名,但是函数参数的个数或类型不同。
  2. 重写,Overriding。派生类中对基类中的虚函数重新实现,即函数名和参数都一样,只是函数的实现不一样。
  3. 隐藏,Hiding。派生类中的函数屏蔽基类中相同名字的非虚函数。


    重载、重写、隐藏
class Printer{
private:
    int mPrivData;
public:
    int mPubData;
    void Print(int data)    {cout << data << endl;};
    void Print(float data)  {cout << data << endl;};
    void Print(const char* pStr, size_t sz) {cout << pStr << endl;};
    void SetPrivData(int data)  {mPrivData = data;};
};

class StringPrinter: public Printer{
public:
    //using Printer::Print;           // 需要将基类的Print函数声明引入到派生类
    void Print(const string& str)   {cout << str << endl;};
};

int main(int argc, char* argv[]){
    StringPrinter stringPrinter;
    stringPrinter.mPivData = 10;        // 编译错误
    stringPrinter.mPubData = 10;        // 没问题

    stringPrinter.SetPrivData(2019);    // 没问题
    stringPrinter.Print(2019);          // 编译错误

    return 0;
}

建议47:重载赋值操作符

重载派生类的赋值运算符

ColorString & ColorString::operator=(const ColorString& rhs)
// 返回自身的引用
{
    if(this == &rhs){    // 检查自赋值
        return *this;
    }
    CString::operator=(rhs);  // 赋值基类的部分
    // 如果成员变量有指针,需申请内存,再复制
    m_dColor = rhs.m_dColor;
    return *this;
}

建议48:运算符重载

一般来说,

  1. 对于双目运算符,重载为友元函数。能够接受左参数和右参数的隐式转换。
  2. 对于单目运算符,重载为成员函数。只能允许右参数的隐式转换。

建议49:有些运算符应该成对实现

建议50:重载自增、自减运算符

T& operator++();            // ++前缀
const T& operator++(int);   // ++后缀
T& operator--();            // --前缀
const T& operator--(int);   // --后缀

建议51:不要重载operator&&、operator||以及operator,

  1. 重载operator&&、operator||会破坏短路求值特性
  2. 重载operator,时很难实现逗号运算符的『整个表达式的结果是最右边表达式的值』特性

建议52:使用inline函数提高效率

内联函数既有宏定义的效率,又保留了函数的作用域特性。
内联是一个编译时行为,因此内联函数一般定义在头文件中。
在类内部定义的函数体的函数,默认为内联函数。一般Get、Set方法会这样定义。

使用内联函数,需注意以下几点:

建议53:慎用私有继承

建议54:慎用多重继承

多重继承很难维护,所以不推荐。

建议55:提防对象切片

多态的实现必须依赖指针或引用,否则会出现对象切片(Object Slicing)的问题。

如果不使用指针或引用的话(或类中没有虚拟机制),就会出现对象的向上强制转换。强制换换后,对象仅保留基类那一部分。
对象切片往往发生在派生类对象被赋值给基类对象。

class Bird
{
public:
    Bird(const string& name): mName(name){}
    virtual void Feature(){
        cout << mName << "can fly."<< endl;
    }
protected:
    string mName;
}
class Parrot: public Bird
{
public:
    Parrot(const string& name, const string& food)
      : Bird(name), mFood(food){}
    virtual void Feature(){
        cout << mName << "can fly. and like to eat " << food << endl;
    }
private:
    string mFood;
}
void Helper(Bird bird)
{
    bird.Feature();
}
int main()
{
    Bird bird1("Crow");
    Helper(bird1);      // output: Crow can fly
    Bird bird2("Polly", "millet");
    Helper(bird2);      // output: Polly can fly
}

输出并不想多态那样,bird2调用ParrotFeature()方法。
当派生类对象被强制转成基类对象时,发生对象切片现象,如下图:

对象切片

建议56:在正确的场合使用恰当的特性

建议57:将数据成员声明为private

建议58:明晰对象构造、析构的顺序

建议59:main函数调用前程序的操作

最简单的方式:调用一个全局对象的构造函数。因为全局对象在main()开始之前进行构造。

C语言中,全局变量的初始化处于编译期
C++中,会先调用Startup,完成函数库初始化、进程信息设立、I/O stream产生,以及对static对象的初始化。

上一篇 下一篇

猜你喜欢

热点阅读