《c++ primer》 第15章 面向对象程序设计 学习笔记

第 15 章 面向对象程序设计


毫无疑问重要的一章。


1. oop 概述

面向对象程序设计的核心思想是:数据抽象(类),继承和动态绑定。

使用继承:可以定义相似的类型并对其相似关系建模。

使用动态绑定:可以在一定成都上忽略相似类型的区别,而以统一的方式使用他们的对象。

<1.继承

公有继承(public)、私有继承(private)、保护继承(protected)是常用的三种继承方式。

1. 公有继承(public)

公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的,不能被这个派生类的子类所访问。

2. 私有继承(private)

私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问。

3. 保护继承(protected)

保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的。


通过继承联系在一起的类构成一种层次关系,根部是一个基类,其他类直接或间接继从基类继承而来。

继承得到的类叫做派生类。基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。

某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明为虚函数

派生类必须通过派生类列表来指明是那个基类的派生类以及访问权限。

派生类必须在其内部对所有重新定义的虚函数进行声明。

c++11 新标准允许派生类显式注明它将使用哪个成员函数改写基类的虚函数,具体措施是在该函数的形参列表之后增加一个override(覆盖)关键字。

<2.动态绑定

有时我们需要定义一个函数来处理基类和派生类,比如输出函数,根据实际传入的参数来决定到底输出那个类的数据。

函数的运行版本由实参决定,既在运行时选择函数的版本,所以动态绑定有时被称为运行时绑定。

注意:在c++中,我们使用基类的引用或指针调用一个虚函数时会发生动态绑定。


2. 定义基类和派生类

基类通常都应该定义一个虚构函数,即使虚构函数不执行任何实际操作也是如此。

派生类可以继承基类的成员,派生类需要对这些操作提供自己的新定义以覆盖从基类继承而来的就定义。

任何构造函数之外的非静态函数都可以是虚函数,关键字virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义。

c++是静态类型的语言,其解析过程发生在编译期间,但对于虚函数是在运行期间动态绑定。

受保护的成员:对于想让派生类访问而其他用户禁止访问的成员我们可以声明为protected.

<1.定义派生类

派生类必须通过使用派生类列表明确指出它是从哪个基类继承而来的。

派生类需要对其继承而来的成员函数中需要覆盖的那些进行重新声明。

派生类经常覆盖它继承的虚函数。如果派生类没有覆盖其基类中的某个虚函数,则该虚函数的行为类似于其他的普通成员,派生类会直接继承其在基类中的版本。

派生类可以访问基类的public和protected成员

声明派生类不需要指名基类。


<<1.派生类对象及派生类向基类的类型转换

一个派生类对象包含多个组成部分:一个含有派生类自己定义的(非静态)成员的子对象,以及一个与该派生类继承的基类对应的子对象。

在一个对象中,派生类继承基类的部分和派生类的部分不一定是连续存储的。

因为派生类对象中包含与其基类对应的组成部分,所以我们能把派生类的对象当成基类对象来使用,我们也能将基类的指针或引用绑定到派生类对象的基类部分

这种称为派生类到基类的类型转换,编译器会隐式的执行转换。

!!注意

派生类内部继承基类的虚函数我们必须定义而不只能声明

base a;      //基类

derive b;    //派生类  error

仅仅声明不定义就报了下面的错误

错误是没有定义bulk_quote在vtable中。说明仅仅声明就没有被vtable包含进去。

v table是虚函数表, 在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证真实反应实际的函数。这样,在有虚函数的类的实例中这个

表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用

的函数。


<<2.派生类构造函数

虽然派生类从基类继承了部分成员,但是并不能直接初始化这些成员。派生类必须使用基类的构造函数类初始化这些成员

每个类控制它自己的成员的初始化过程

派生类首先初始化基类的部分,然后按照声明的顺序依次初始化派生类部分。

注意:
每个类负责定义自己各自的接口,要想与类的对象交互必须使用该类的接口,即使这个对象是派生类的基类部分也是如此。

派生类不能直接初始化基类的成员也就是遵循了上面的准则。


<<3.继承与静态成员

如果基类定义了一个静态成员,则在整个继承体系中只存在该成语的唯一定义,不论从基类中派生出多少个派生类,对于每个静态成员来说都只存在唯一的实例

因为静态成员在全局区(静态区)。


<<4.被用作基类的类

如果我们想将某个类用作基类,则该类必须已经定义而非仅仅声明。

一个类是基类,同时它也可以是一个派生类。

直接基类:直接继承基类

间接基类:继承基类的派生类

最终的派生类将包含它的直接基类的子对象以及每个简介基类的子对象。


<<5.防止继承的发生

有时我们会希望定义这样一种类,我们不希望其他类继承它,或者不想考虑它是否适合作为一个基类。

c++11提供了一种防止继承的方法就是在类名后面跟一个关键字final


例子:书店买书打折例子

#include <iostream>
#include <string>

class quote
{
    friend std::ostream& operator<<(std::ostream&os, quote &qt);
    friend std::ostream& print_total(std::ostream &os, quote &qt, std::size_t n);
    public:
        quote() = default;
        quote(const std::string &book, double sales_price):
            bookNo(book), price(sales_price) { }
        //赋值运算符返回值不能定义为const quote&   因为可能存在(s1 = s2) = s3; 这种情况若是const就会错误。
        quote& operator=(const quote &qt);
        std::string isbn()const { return bookNo; }
        virtual double net_price(std::size_t n)const   //虚函数
        {
            return n * price;
        }
        virtual ~quote() = default;                    //虚析构函数       

    private:
        std::string bookNo;

    protected:                                         //派生类可访问的但是禁止其他用户访问
        double price = 0.0;
};

class bulk_quote final: public quote                   //派生类列表,final指名此类不希望被其他类继承
{
    public:
        bulk_quote() = default;
        bulk_quote(const std::string &book, double sales_price, std::size_t ms, double dis):
            quote(book, sales_price), min_sold(ms), discount(dis) { }
        double net_price(std::size_t n)const override;
        ~bulk_quote() = default;

    private:
        std::size_t min_sold = 0;
        double discount = 0.0;
};

class bulk2_quote : public quote
{
    public:
        bulk2_quote() = default;
        bulk2_quote(const std::string &book, double sales_price, std::size_t ms, double dis):
            quote(book, sales_price), max_sold(ms), discount(dis) { }
        double net_price(std::size_t n)const override;
        ~bulk2_quote() = default;

    private:
        std::size_t max_sold = 0;
        double discount = 0.0;
};

double bulk2_quote::net_price(std::size_t n)const
{
    if(n <= max_sold)
        return n * price * (1-discount);
    else
        return max_sold * price * (1-discount) + (n-max_sold) * price;
}

double bulk_quote::net_price(std::size_t n)const
{
    if(n >= min_sold)
        return n * price * (1-discount);
    else
        return n * price;
}

std::ostream& print_total(std::ostream &os, quote &qt, std::size_t n)
{
    auto ret = qt.net_price(n);
    os << "Isbn:" << qt.isbn() << " price:" << qt.price 
              << " sold:" << n << " revenue:" << ret << '\n';
    return os;
}

std::ostream& operator<<(std::ostream &os, quote &qt)
{
    os << "Isbn:" << qt.isbn() << " price:" << qt.price << '\n';
    return os;
}

int main()
{
    quote item("herry pory", 128.0);             //基类
    quote *p = &item;
    bulk_quote bulk("hello boy", 114, 8, 0.5);   //销售方式1
    bulk2_quote bulk2("hello boy2", 114, 8, 0.5);//销售方式2
    p = &bulk;
    quote &q = bulk;
    print_total(std::cout, item, 10);
    print_total(std::cout, bulk, 10);
    print_total(std::cout, bulk2,10);

    return 0;
}


<<6.类型转换与继承

注意:理解基类和派生类之间的类型转换是理解c++语言面向对象编程的关键所在。

存在继承关系的类:我们可以将基类的指针或引用绑定到派生类上(是类型匹配绑定的一个例外)。含义是当我们使用该指针或引用时,实际我们并不知道绑定对象的真实类型,可能是基类也可能是派生类。

智能指针类也支持这样的转换。我们可以在基类的智能指针存储派生类对象

(1).静态类型与动态类型(多态之一)

静态类型是在编译时就已知的,它是变量声明时的类型或表达式生成的类型。

动态类型则是变量或表达式表示的内存中的对象的类型,动态类型直到运行时才可知。

比如上面的类中,print_total( )传递的参数我们也不知道是基类还是派生类,这样print_total里面的net_price也就不知道是哪个版本,直到动态运行时才能知道。

但是如果表达式既不是指针也不是引用,则它的动态类型永远和静态类型一致

测试了下,在不使用指针或引用的情况下参数是什么类型,运行的就是什么类型的版本。上面的例子,operator<<参数从quote &改为quote在输出的时候都是基类quote版本

我在构造函数里面添加了信息,因为值传参要拷贝,结果果然即使传参是派生类,值拷贝调用的也是基类的构造函数,那么当然使用的是基类版本。现在可以理解如果表达式既不是指针或者引用 ,则它的动态类型永远和静态类型一致这句话了。

不存在基类向派生类的隐式转换

派生类继承基类的所有,但是也可以通过虚函数来重新定义一些成员,在虚函数表中,重新定义就会覆盖(override)掉基类版本,所以基类并不完全是派生类的一部分,所以不存在从基类向派生类的自动类型转换。假如合法我们则可能访问到不存在的成员。

简单的来说就是基类有一个范围,派生类是基类的扩展,比基类的范围大,大的包含小的,大的能变成小的,小的却变不成大的。

编译器在编译时无法确定某个特定的转换在运行时是否安全,因为编译器只能通过检查指针或引用的静态类型来推断该转换是否合法。

如果我们要自己转换可以使用dynamic_cast请求类型转换,该转换的安全检查在运行时执行,如果我们知道某个类型是安全的那么可是使用static_cast来转换。

在拷贝赋值运算符和拷贝构造函数传值的时候传的也是引用,所以派生类也可以通过这两个来初始化或赋值基类,但是拷贝过去的只是基类存在的对象,其余部分被切割掉了。

当我们用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝,移动或赋值,它的派生类部分将会被忽略掉。

关键概念:存在继承关系的类型之间的转换规则

1.从派生类向基类的类型转换只针对指针或引用有效。

2.基类向派生类不存在隐式类型转换

3.和任何其他成员一样,派生类向基类的类型转换也可能会由于访问受限而变得不可行。

举个例子:

一个函数的参数为istream,但是我们传递参数的时候确可以传递istringstream和ifstream,因为sstream和fstream都是继承iostream。iostream 是基类,sstream 和fstream是派生类。所以可以通过引用或指针来传递。



3.虚函数重点

标记了重点,因为都知道c++是oop的一门语言,那么oop的核心思想就是多态性,而我认为c++的多态性其中之一就是虚函数实现的。

前面已经提到了虚函数,现在具体来说说。

<1.我们必须定义而不只能声明虚函数。

为什么必须定义呢,我们知道基类的引用或指针作为参数时可以传参基类或派生类,当我们使用引用或指针来传参绑定对象然后调用虚函数时实际上是动态绑定,

此时我们不知道到底运行时调用的是哪个类型的虚函数,但是我们必须确保每个虚函数在被调用的时候必须可以使用。

必须清楚的一点是动态类型必须是引用或指针调用时才会发生。

c++ primer上面说:引用或指针的静态类型与动态类型不同这一事实是c++语言支持多态性的根本所在。

好好理解下。

<2.一个函数在基类中被声明为虚函数,那么在派生类override覆盖基类的这个函数时可以不加virtual关键字,因为一个函数一旦被声明为虚函数时,那么它在所有

派生类中也是虚函数。

一个派生类的函数如果覆盖了基类的虚函数,那么这个函数的参数和返回类型必须和虚函数一致。

自己测试了下,当派生类覆盖基类的函数形参或者返回类型与基类的虚函数不一致时,这个函数被编译器认为是派生类重新自己定义的新的函数,而不被认为是

覆盖掉基类的虚函数。如果我们在实际变成中发生这种错误是很难发现的,我在测试时是在派生类的函数中添加了override关键字发现的,这也是c++11所添加它的原因。

但存在一个例外,就是返回类型如果是类本身的指针或引用时,这个规则无效。就是基类可以返回base*或base&,派生类可以返回dervie*或derive&

结论:基类中的虚函数在派生类中隐含的也是一个虚函数,当派生类覆盖了某个虚函数时,该函数在基类总的形参必须与派生类中的形参严格匹配

<3.final和override说明符

c++11新标准中我们可以使用override(覆盖)关键字来说明派生类中的虚函数。这么作的好处是使得程序员的意图更加清晰的同时让编译器可以为我们发现一些错误,

后者在编程时间中显的更加重要,如果我们使用override标记了某个函数,但该函数并没有覆盖已存在的虚函数,此时编译器将报错。

final的作用同上。可以跟在类后面说明不许被继承或者跟在虚函数后面说明不许后继的类继承。

final和override说明符出现在形参列表(包括任何const或引用修饰符)以及尾置返回类型之后。

<4.虚函数和默认实参

如果我们在基类的虚函数中存在默认实参,那么当我们通过引用或指针来调用时使用的是基类的默认实参,即使调用的是派生类的虚函数也是如此。

所以在定义默认实参的时候,基类和派生类的默认实参最好一致。

<5.回避虚函数机制

在某些情况下我们不需要动态绑定,而是强迫执行虚函数的某个特定版本。通过作用域操作符可以实现这个目的

通常情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数机制。

有种情况,基类的虚函数是抽象出来所有继承层次都要完成的操作,派生类只是在基类的虚函数操作中在添加一些额外的操作,那么我们就可以在通过作用域操作符

在派生类的虚函数中调用基类版本,但是必须要通过作用域操作符,不然就会引起无限递归。


课后题15.11 为上面定义的quote类体系增加一个名为debug的虚函数,令其分别显示每个类的数据成员。

写时开始定义每个类的debug函数然后输出数据成员,但是派生类是不能直接输出基类的private成员的,虽然它继承了。只能通过作用域操作符来调用基类的debug函数

然后在输出派生类新增加的数据成员。


4.抽象基类

概念

比如在我们上面的买书例子,基类是没有任何折扣的例子,派生类有两个,是两种不同的折扣策略,但是可以发现每种折扣策略都是一个discount和num,那么我们

可以新定义一个类disc_quote来继承quote基类,它含有两个数据成员,一个是discount折扣,一个是num数量,我们可以让本来继承quote的派生类来继承新定义的

disc_quote类,这样就不用每个派生类都定义一遍属于自己的discount和num成员。

<1.纯虚函数

新定义的disc_quote是一个打折通用的基类,而不是具体的折扣策略,所以我们不希望用户来定义一个disc_count,而是定义具体的折扣策略。

我们可以将disc_quote中的net_price定义为一个纯虚函数来实现。这样可以清晰的告诉用户当前这个net_price没有实际意义的。和普通虚函数不一样。

一个纯虚函数无需定义,我们通过在函数体的位置后面加上=0 即可。就可以将一个虚函数变为纯虚函数。其中=0只能出现在类内部的虚函数声明语句处

注意:我们也可以为虚函数提供定义,不过函数体必须定义在类的外部,也就是说我们不能在类的内部为一个=0的函数提供函数体。

<2.含有纯虚函数的类是抽象基类

含有(未经覆盖直接继承)纯虚函数的类是抽象基类,抽象基类负责定义接口,而后续的其他类可以覆盖接口,我们不能直接创建一个抽象基类的对象。

我们可以定义抽象基类的派生类对象,前提是覆盖了纯虚函数的接口。

注意:我们不能创建抽象基类的对象。

<3.派生类构造函数只初始化它的直接基类

基类quote----->抽象基类disc_quote----->派生类bulk_quote

-->表示继承关系

那么我们定义bulk_quote的构造函数时只能初始化disc_quote,在由disc_quote的构造函数来初始化quote的成员。

关键概念重构:

重构负责重新设计类的体系以便将操作和/或数据从一个类移动到另一个类中,对于面向对象的应用程序来说,重构是一种很普遍的现象。


练习,定义自己的disc_quote和bulk_quote

#include <iostream>
#include <string>

class quote
{
    public:
        quote() = default;
        quote(std::string &book, double p):
            bookNo(book), price(p) { }
        virtual double net_price(std::size_t sz)const     //虚函数
        { return sz * price; }
    
    protected:                                            //因为派生类也需要有一份这样的成员所以定义为protected
        double price = 0.0;
    
    private:
        std::string bookNo = "";
};

class disc_quote : public quote                            //抽象基类,抽象出所有折扣的类
{
    public:
        disc_quote() = default;
        disc_quote(std::string &book, double p, double disc, std::size_t num):   //构造函数初始化quote类时必须调用quote类的构造函数
            quote(book, p), discount(disc), limit_num(num) { }
        double net_price(std::size_t sz)const = 0;

    protected:                             //定义成保护的成员,因为要被派生类继承。且每个派生类都有自己独一无二的成员
        double discount = 0.0;           //表示折扣
        std::size_t limit_num = 0;       //表示限定数量
};

class bulk_quote : public disc_quote                        //继承抽象基类,有一种具体的折扣策略
{
    public:
        bulk_quote() = default;
        bulk_quote(std::string &book, double p, double disc, std::size_t num):
            disc_quote(book, p, disc, num) { }
        double net_price(std::size_t sz)const override   //不加const错误,不是同一个函数了
        {
            if(sz > limit_num)
                return (sz-limit_num) * price * discount + limit_num * price;
            else
                return sz * price;
        }

    private:
};

int main()
{
    quote q;
    //disc_quote disc;                   //error
}
我们不能定义抽象基类,也就是disc_quote

如果定义了报错


note:为实现的纯虚函数method net_price 在 disc_quote里面

网上一个回答纯虚函数我觉得挺好:

纯虚函数是为你的程序制定一种标准,即只要你继承了我,就必须按照我和标准来,实现我所有的方法,否则你也是虚拟的
可以好好理解下。

举个很简单的例子,每个人出门上班的方式不同,所以可以抽象出来一个抽象基类就是人们的出行方式,标准就是出行。

张三步行上班

李四骑自行车上班

王五开车上班

他们都是派生出行方式的那个抽象基类,但是都有一个出行的标准,不过工具不一样而已,和前面的打折例子类似。

注意:包含纯虚函数的类是抽象基类,我们不能实例化抽象基类,如果派生类没有覆盖掉纯虚函数,那么派生类也是抽象类,也不能实例化,

也就是说如果我们在派生类中实例化抽象基类,必须要覆盖纯虚函数。



5.访问控制和继承

<1.受保护的成员

一个类使用protected关键字来说明哪些它希望与派生类 分享但那是不想被其他公共访问使用的成员。

和私有成员类似,受保护的成员对于类的用户来说是不可访问的。

和共有成员类似,受保护的成员对于派生类的成员和友元来说是可访问的。

派生类的成员或友元只能通过派生类来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问权限。

注意:派生类的成员和友元只能访问派生类对象中的基类部分的受保护成员,对于普通的基类对象中的成员不具有特殊的访问权限。

<2.共有,私有和受保护继承

某个类继承基类的成员访问权限受到两方面限制,一方面是基类内部该成员的访问权限,另一方面是派生类的继承方式

在前面也说了

公有继承(public)、私有继承(private)、保护继承(protected)是常用的三种继承方式。

1. 公有继承(public)

公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的,不能被这个派生类的子类所访问。

2. 私有继承(private)

私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问。

3. 保护继承(protected)

保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的。

派生类访问说明符目的是控制派生类用户对基类的访问权限。

<3.派生类向基类转换的可访问性

1只有当derive类共有的继承base类时,用户代码才能使用派生类向基类的转换,如果derive继承base的方式是受保护的或者私有的,则用户代码不能使用该转换

2不论derive以什么方式继承base类,derive的成员函数和友元都能使用派生类向基类的转换,派生类向其直接基类的类型转换对于派生类的成员和友元来说是永久可访问的。

3如果derive继承base的方式是共有的或者受保护的,则derive的派生类的成员和友元可以使用derive向base的类型转换,反之,如果derive继承base的方式是私有的,则

不能使用


<4.友元与继承

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

基类的友元在访问派生类成员时不具有特殊性,派生类的友元也不能随便访问基类的成员。

当一个类将另一个类声明为友元时,这种友元关系只对做出声明的类有效,对于原来的那个类来说,其友元的基类或者派生类不具有特殊的访问能力。简单来说就是友元关系

不能被继承。


<5.改变个别成员的可访问性

通过在类的内部使用using 声明语句,我们可以将该基类的直接或间接基类中的任何可访问成员标记出来,using 声明语句中名字的访问权限由它所在的访问说明符来决定

如果一条using 声明语句出现在类的private部分,则该名字只能由类的成员和友元来访问。

如果一条using 声明语句出现在类的protect部分,则该名字能由类的成员和友元和派生类来访问。

如果一条using 声明语句出现在类的public部分,则该名字能由类的所有用户访问。

注意:派生类只能为它可以访问的名字提供using 声明。


<5.默认的继承保护级别

class定义的派生类默认是private继承

struct 定义的派生类默认是 public 继承


6.继承着中的类作用域

每个类定义自己的作用域,在这个作用域内我们定义类的成员,当存在继承关系时,派生类的作用域嵌套在其基类作用域之内。如果一个名字在自己的作用域内无法解析

那么就会在基类中查找。

<1.在编译时进行名字查找

一个对象,引用或指针的静态类型,决定了该对象的哪些成员是可见的。即使静态类型与动态类型可能不一致(当使用基类的引用或指针时会发生这种情况),

但是我们能使用哪些成员仍然是由静态类型决定的。

<2.名字冲突和继承

和其他作用域一样,派生类也能重用定义在其直接基类或间接基类中的名字,此时定义在内层作用域(既派生类)的名字将隐藏定义在外层作用域(既基类)的名字

简单来说就是派生类隐藏基类的同名成员。但是即使基类和派生类函数的参数列表不一样,也会被隐藏。 

但是我们可以通过作用域符来使用一个被隐藏的基类成员。

注意:除了覆盖掉继承而来的虚函数,派生类最好不要重用其他定义在基类的名字。

和其他作用域一样,如果派生类的成员(既内层作用域)与基类(既外层作用域)的某个成员名字相同,则派生类将在其作用域内隐藏该基类成员,即使形参列表不同,

基类成员也仍然会被隐藏。可以理解为什么基类和派生类的虚函数必须有相同的列表。

调用非虚函数时不会发生动态绑定,实际调用的函数由指针的静态类型来决定。


<3.覆盖重载的函数

如果我们在派生类中定义与基类同名的函数,会覆盖基类中所有同名的函数,如果我们仅仅想覆盖基类中的部分函数,我们可以使用using 声明,将基类的同名函数作用域

全部添加到派生类作用域中,此时派生类作用域中也有了这些函数,我们在定义我们想覆盖的函数的版本就可以了。

#include <iostream>

class base
{
    public:
        virtual int fun1() { std::cout << "base" << std::endl; return 10;}
};

class D1 : public base
{
    public:
        int fun1(int i) { std::cout << "D1" << std::endl; return i; }
};

class D2 : public D1
{
    public:
        int fun1(int i) { std::cout << "D2" << std::endl; return i; }
};

int main()
{
    base b;
    D1 d1;
    D2 d2;
    base *p1 = &b, *p2 = &d1, *p3 = &d2;
    p1->fun1();
    p2->fun1(1);
    //p3->fun1(1);
}

《c++ primer》 第15章 面向对象程序设计 学习笔记_第1张图片

p2->fun1(1)报错了,参数不对,绑定的指针是base的,也就是说我们希望动态绑定,但是在D1中int fun1(int i)就把基类的fun1( )虚函数覆盖了,此时编译器解析为静态绑定

那么base指针指向D1调用fun1(1)肯定是不对的,去掉参数或者把指针改为D1才对。


覆盖和隐藏

覆盖就是覆盖掉了,访问不了了,隐藏是必须通过域操作符才能访问,一般是虚函数被重写是覆盖,其他的是隐藏

覆盖:

必须是基类的虚函数。派生类为实现多态覆盖掉基类的虚函数

必须是虚函数

且函数之间参数列表必须相同

隐藏:(来自百度)

初学者很容易把函数的隐藏与函数的覆盖重载相混淆我们看下面两种函数隐藏的情况

1派生类的函数与基类的函数完全相同函数名和参数列表都相同只是基类的函数没有使用virtual关键字此时基类的函数将被隐藏而不是覆盖

2派生类的函数与基类的函数同名但参数列表不同在这种情况下不管基类的函数声明是否有virtual关键字基类的函数都将被隐藏注意这种情况与函数重载的区别重载发生在同一个类中。


7.构造函数与拷贝控制

1.虚析构函数

<1.继承关系对基类拷贝控制最直接的影响是基类通常应该定义一个虚析构函数。这样我们就能动态分配继承体系中的对象了。

因为动态绑定实现多态,如果我们仅仅定义普通版本的析构函数可能释放的并不是我们想要释放的对象。

<2.我们通过在基类中将析构函数定义为虚函数以确保执行正确的析构函数版本。只要基类的析构函数是虚函数,就能确保当我们delete基类指针时将运行正确的

析构函数版本。

注意:如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为。

<3.之前我们说过一个重要的准则,如果一个类需要析构函数那么它也同样需要拷贝和赋值操作,基类的虚析构函数不遵循这个准则,是个例外。

<4.如果一个类定义了析构函数,即使它通过=default的形式使用了合成版本,编译器也不会为这个类合成移动操作。

总之:虚析构函数能够确保delete基类指针时将运行正确的析构函数版本。


2.合成拷贝控制与继承

简单的记忆就是基类永远会影响派生类,因为派生类继承基类,如果基类拷贝控制部分操作有问题,那么派生类也会有。

比如:基类的拷贝构造定义为删除的,那么派生类也必定为删除的,因为派生类中的拷贝构造必须要调用基类的拷贝构造来初始化基类部分,基类定义为删除的,说明

派生类不能构造基类部分,那么派生类也就为删除的了。


3.派生类的拷贝控制成员

派生类构造函数在其初始化阶段中不但要初始化派生类自己的成员,还要负责初始化派生类对象的基类部分,因此,派生类的拷贝和移动构造函数在拷贝和移动自有

成员的同时,也要拷贝和移动基类部分的成员,类似的派生类赋值运算符也必须为其基类部分的成员赋值。

和构造函数及赋值运算符不同的是,析构函数只负责销毁派生类自己分配的资源。

当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内的整个对象。


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

在默认情况下,基类默认构造函数初始化派生类对象的基类部分,如果我们向拷贝或移动基类部分,则必须在派生类的构造函数初始值列表显示的使用基类的拷贝或移动

构造函数。


5.派生类赋值运算符

和拷贝和移动构造函数一样,派生类赋值运算符也必须显示的为其基类部分赋值。


6.派生类析构函数

对象的基类部分是隐式销毁的,和构造函数及赋值运算符不同,派生类析构函数只负责销毁由派生类自己分配的资源。


7.在构造函数和析构函数中调用虚函数

简单理解。这个虚函数可能会访问派生类的成员,毕竟,如果它不需要访问派生类的成员的话,则派生类直接使用基类的虚函数版本就可以了,然而,执行基类

构造函数的时候,它要用到派生类成员尚未初始化,如果我们允许这样的访问,我们的程序可能会崩溃。

注意:如果构造函数或析构函数调用了某个虚函数,则我们应该执行与构造函数或析构函数所属类型相对应的虚函数版本。


附上一篇关于第7条的博客,博主说的很详细了,据说面试问过。

http://anwj336.blog.163.com/blog/static/8941520920106791516915/


8.继承的构造函数

在c++11 新标准中,派生类能够重用直接基类定义的构造函数。

派生类继承基类构造函数的方式是提供一条注明了基类名的using声明语句。

using声明语句将令编译器产生代码,对于基类的每个构造函数,编译器都生成与之对应的派生类构造函数。





8.容器与继承

当我们使用容器存放继承体系中的对象时,通常必须采用简介存储的方式,因为不允许在容器中保存不同类型的元素,所以我们不能把具有继承关系的多种类型

的对象直接存放在容器中。

注意:当派生类对象被赋值给基类对象时,其中的派生类部分将被切掉,因此容器和存在继承关系的类型无法兼容。

在容器中放置(智能)指针而非对象

当我们希望在容器中存放具有继承关系的对象时,我们实际上存放的通常是基类的指针,更好的是存放智能指针,和往常一样,这些指针所指对象的动态类型可能是

基类类型,也可能是派生类类型。

正如我们可以把派生类的指针转换为基类的指针一样

我们也可以把派生类的智能指针转换为基类的智能指针。

std::vector<std::shared<quote>>basket;


这章的内容基本就完了,还剩下两个例子,单独写个博客吧。

这章无疑是非常重要的一章,内容也非常多,经常看经常理解oop吧~


你可能感兴趣的:(C++,面向对象,面向对象编程,C++11)