我的C++primer长征之路:面向对象程序设计

面向对象的程序设计

核心思想:数据抽象、继承和动态绑定。

引用和指针的静态类型和动态类型不总是相同,这一点是C++支持多态的根本。

动态绑定只有通过指针或者引用调用虚函数时才会发生。

  • 基类和派生类

class Quote{
    
    
    public:
    Quote() = default;
    Quote(const std::string &book, double sales_price) : bookNo(book), price(sales_price) {
    
    }
    std::string isbn() const {
    
    return bookNo;}

    //返回给定数量的书籍的销售总额,派生类负责改写并实现不同的计算方法
    virtual double net_price(std::size_t n) const {
    
    
        return n * sales_price;
    }
    virtual ~Quote() = default;   //对析构函数进行动态绑定,基类通常都应该定义一个虚析构函数。

    private:
    std::string bookNo;
    protected:
    double price = 0;    
};

//定义派生类

class Bulk_Quote : public Quote{
    
    
    public:
    Bulk_Quote() = default;
    Bulk_Quote(const std::string&, double, std::size_t, double);

    //覆盖基类的net_price函数
    double net_price(std::size_t) const override;

    private:
    std::size_t min_qty = 0;
    double discount;
};

关于virtual虚函数,构造函数之外的任何非静态函数都可以是虚函数,包括析构函数,virtual只能出现在类内部声明语句而不能出现在类外部定义语句中。如果派生类没有覆盖基类中的某个虚函数,那么派生类就会直接继承基类中的版本。override显式地注明该函数覆盖了它继承的虚函数,放在常成员函数const、引用成员函数的引用限定符(&,&&)之后。

  • 为什么派生类的对象能当成基类对象来使用呢??
  • 因为派生类对象中含有与其基类对应的组成成分,所以可以将基类的指针或者引用绑定到派生类对象上。

派生类初始化过程:

//先初始化基类Quote部分,等到Quote构造函数函数体执行完后再初始化min_qty,discount这两顺序由在类体中声明的顺序决定。最后再执行空函数体。
Bulk_Quote(const std::string& book, double p, std::size_t qty, double disc) : Quote(book, p), min_qty(qty), discount(dis) {
    
     } 

继承中的静态成员

若基类存在静态成员,在整个继承体系中只存在该成员的唯一实例。

防止继承

final关键字

class Last final : Base {
    
     //final放在派生列表之前
    ///
}

类型转换与继承

基类的指针或者引用的静态类型可能与动态类型不一致,也就是基类指针引用可以指向派生类对象。但是基类的变量的静态类型和动态类型必须是一致的 。

不存在基类向派生类的隐式转换。可以有显式转换dynamic_cast和static_cast。

Quote base;
Bulk_Quote* d1 = &base; //错误,不能将基类转换成派生类
Bulk_Quote& d2 = base;  //错误,同理

//即使一个基类指针已经指向了派生类对象,也不能实现基类向派生类的转换
Bulk_Quote bulk;
Quote *item = &bulk;
Bulk_Quote *bp = item;  //错误

类对象之间不存在类型转换。

当用派生类对象初始化或者赋值基类对象时,只有派生类对象中的基类部分会被拷贝移动或者赋值,派生类的其他部分会被忽略掉。

Bulk_Quote bulk;
Quote item(bulk); //调用的是Quote::Quote(const Quote&)构造函数。
item = bulk;      //调用的是Quote::operator = (const Quote&)

虚函数

所有的虚函数必须有定义。

virtual关键字:指明派生类中的虚函数。

final关键字:指明后续的任何覆盖该函数的行为都是不允许的。

这两个关键字应出现在形参列表及尾置返回类型之后。

一般情况下,基类和派生类中的虚函数的默认实参最好保持一致。

虚函数表的内容没有???

回避虚函数的机制

可以使用位作用域运算符::指定执行虚函数的某个版本。

double undis = Base->Quote::net_price(42);  //指定调用基类的net_price

什么时候需要回避虚函数机制呢??

通常是在派生类虚函数需要调用它所覆盖掉的基类版本虚函数时。
如果一个派生类虚函数需要调用它的基类版本,但是没有使用作用域运算符,则会在运行时被解析为对派生类版本虚函数的自身调用,从而导致无限递归。

抽象基类

纯虚函数

纯虚函数无需定义。

class Disc_quote : public Quote{
    
    
    public:
    Disc_quote() = default;
    Disc_quote(const std::string& book, double price, std::size_t qty, double disc) :
                Quote(book, price), quantity(qty), discount(disc) {
    
    }
    //纯虚函数,=0只能出现在类内部声明处
    double net_price(std::size_t) const = 0;  

    protected:
    std::size_t quantity = 0;
    double discount = 0.0;
};

含有纯虚函数的类叫做抽象类。抽象类不能直接定义对象,需要通过继承覆盖纯虚函数才行。

派生类只初始化它的直接基类。

class Bulk_quote : public Disc_quote{
    
    
    public:
    Bulk_quote() = default;
    Bulk_quote(const std::string& book, double price, std::size_t qty, double disc) :
                    Disc_quote(book, price, qty, disc);
    //覆盖抽象基类的版本
    double net_price(std::size_t) const override;
};
//此时的继承关系是:Quote->Disc_quote->Bulk_quote。
Bulk_quote bq;

此时定义一个Bulk_quote对象bq的过程是:

  • Bulk_quote构造函数将实参传递给Disc_quote的构造函数,Disc_quote的构造函数将实参再传递给Quote的构造函数。
  • Quote构造函数先初始化bookNo和price成员,然后执行Quote构造函数的函数体。
  • 执行完Quote构造函数体之后,运行Disc_quote的构造函数并初始化quantity和discount成员,之后执行Disc_quote构造函数体。
  • 最后执行Bulk_quote的构造函数。

访问控制和继承

保护成员

  • 与私有成员类似,类对象不能访问受保护成员。
  • 与共有成员类似,派生类的成员和友元可以访问到基类的受保护成员。
  • 但是派生类的成员或者友元只能通过派生类对象来访问基类受保护的成员,不能通过基类对象来访问受保护成员。
  • 同时,派生类成员不能访问基类的private成员。

共有、私有和受保护继承

派生访问说明符的作用是:控制派生类用户对基类成员的访问权限

派生类访问说明符对于派生类的成员和友元能否访问基类的成员没有影响。

  1. public继承

    不改变基类成员的访问属性。

    派生类内能访问基类的public、protected成员。

    派生类对象能访问基类的public成员。

  2. protected继承

    基类中的public、protected成员在派生类中的访问属性变成protected。

    派生类内能访问基类的public、protected成员。

    派生类对象不能直接访问基类的所有成员,相当于基类的public成员变成了派生类的protected成员。

    派生类的派生类内可以访问到基类的public、protected成员。

  3. private继承

    基类中的public和protected成员在派生类中的访问属性变成private。

    派生类的派生类不能访问到基类的所有成员。

友元与继承

友元关系不能传递,也不能继承。

改变个别成员的可访问性

使用using改变访问属性。

class Base{
    
    
    public:
    std::size_t size() const {
    
    return n}

    protected:
    std::size_t n;
};

class Derived : private Base{
    
    
    public:
    using Base::size;  //将private继承而来的size()访问属性变成public,原先是private的

    protected:
    using Base::n;    //将private继承而来的n访问属性变成public,原先是private的

};

默认的继承保护级别

class默认是私有继承,struct默认是共有继承。

class和struct的唯一区别就是成员访问说明符和派生访问说明符的默认属性

继承中的类作用域

派生类的作用域嵌套与基类的作用域之内。派生类的成员会隐藏基类的同名成员,即使形参列表不一样也会隐藏,因为C++中名字查找先于类型检查,也就是先检查成员名字,如果有相同名字就隐藏,还未轮到类型检查。这也就是为什么基类和派生类的虚函数必须要有相同的形参列表。

如果要使用基类的同名成员,可以使用作用域运算符::来调用被隐藏的成员。

构造函数与拷贝控制

虚析构函数

基类通常应该定义一个虚析构函数。为什么呢??

因为当基类指针指向派生类对象时,delete基类指针,如果析构函数不是虚函数,那么调用的是基类的析构函数(会产生未定义行为),但实际上应该执行的是派生类的析构函数,因为指针指向的是一个派生类的对象。所以析构函数需要是虚函数才会在运行时确定该执行基类的析构函数还是派生类的析构函数。 只要基类的析构函数是虚函数,就能保证delete基类指针时执行的是正确的析构函数。

如果一个类定义了析构函数,即使是以=default形式定义的,编译器也不会为这个类合成一定操作。

如果一个类定义了拷贝构造函数,编译器就不会合成移动构造函数。

如果需要使用移动操作,那么必须从基类开始逐一定义移动操作。

//例如Quote中定义了虚析构函数,那么就没有合成的移动操作,必须自己定义移动操作。
class Quote{
    
    
    public:
    Quote() = default;
    Quote(const Quote&) = default;   //显式定义拷贝控制函数,逐一拷贝成员
    Quote(Quote&&) = default;        //显式定义移动构造函数,逐一移动成员
    Quote& operator = (const Quote&) = default;  //显式定义拷贝赋值运算符
    Quote& operator = (Quote&&) = default;    //显式定义移动赋值运算符

    virtual ~Quote() = default;
};

定义派生类的拷贝或移动构造函数

若要正确地定义派生类的拷贝和移动构造函数,那么必须在派生类的构造函数初始化列表中显式地调用拷贝或移动构造函数。

class Base{
    
    /*...*/};  //基类中定义有了拷贝构造函数和移动构造函数

class D : public Base{
    
    
    public:
    D(const D& d) : Base(d) , ..., {
    
    }  //显式调用基类拷贝构造函数来拷贝基类中的成员。
    //显式调用基类移动构造函数来移动基类中的成员
    D(D&& d) : Base(std::move(d)) , ..., {
    
    }

    //如果是这样的话,派生类的基类部分将会使用默认构造函数,派生类自己的部分使用拷贝构造函数
    D(const D& d) : /*初始化派生类数据成员*/ {
    
    }
};

默认情况下,派生类使用基类的默认构造函数初始化派生类对象的基类部分,如果想要拷贝或移动派生类,必须在派生类的拷贝或移动构造函数的初始化列表中显式地调用基类的拷贝或移动构造函数。

派生类的赋值运算符

派生类的赋值运算符必须显式地为派生类的基类部分赋值。

D& D::operator = (const D& rhs){
    
    
    Base::operator=(rhs);   //显式地为基类赋值

    //为派生类部分赋值
    return *this;
}

派生类析构函数

派生类析构函数只负责销毁派生类分配的内存。

对象销毁顺序与生成顺序相反:先执行派生类的析构函数,再执行基类的析构函数,直到继承体系得顶端。

在构造函数和析构函数中调用虚函数得情况

如果构造函数或析构函数调用了某个虚函数,那么执行得是与构造函数或者析构函数所属类型相对应的虚函数版本。

继承的构造函数

实际上基类的构造函数是不能被继承的,所以准确来说构造函数并不是继承而来的。在派生类中调用基类的构造函数的方法是使用using语句。

class Bulk_quote : public Disc_quote{
    
    
    public:
    using Disc_quote::Disc_quote;  //使Disc_quote构造函数在派生类中可见,这里是基类全部的构造函数,不止某一个
};

一些特点:

基类的构造函数含有默认实参,那么派生类将获得多个构造函数,每个构造函数分别省略掉一个默认形参。
默认、拷贝和移动构造函数不会被继承。这些构造函数按照正常规则被合成。继承而来的构造函数不会被视为用户定义的构造函数,也就是说一个类如果含有继承来的构造函数,那么它也将拥有一个合成的默认构造函数。

容器与继承

用容器可以存放继承体系中不同的对象,但是存的应该是基类的指针,而不是具体的对象。

vector<shared_ptr<Quote>> basket;   //存放指针的容器
basket.push_back(make_shared<Quote>("0-000-1", 50));
basket.push_back(make_shared<Bulk_quote>("0-000-2", 50, 10, .25));
//调用的是Quote的版本,因为basket存的元素类型是Quote
cout << basket.back()->net_price(15) << endl;

例子:定义basket类

class Basket{
    
    
    public:
    void add_item(const std::shared_ptr<Quote> &sale){
    
    
        item.insert(sale);
    }
    double total_receipt(std::ostream&) const;

    private:
    //用于multiset中的元素排序
    static bool compare(const std::shared_ptr<Quote> &lhs, const std::shared_ptr<Quote> &rhs){
    
    
        return lhs->isbn() < rhs->isbn();
    }
    std::multiset<std::shared_ptr<Quote>>, decltype(compare)*> items{
    
    compare};
};

猜你喜欢

转载自blog.csdn.net/weixin_40313940/article/details/107601052