1.面向对象编程的核心思想是数据抽象、继承和动态绑定。
派生类
2.C++中,基类将类型相关的函数(即派生类可能需要改变的函数)与派生类不做改变直接继承的函数区分对待。对于基类希望它的派生类各自定义适合自身版本的函数,基类需要将这些函数声明为虚函数(virtual function)。
3.派生类必须通过使用类派生列表明确指定它从哪个基础派生而来,类派生列表即类名后面跟冒号,然后是基类列表,每个基类前面可以有访问说明符。派生类必须在其内部对所有重新定义的虚函数进行声明。声明方式可以在函数前加关键字virtual,或者使用C++11中的在后面添加override。但派生类不需要桥头所有继承而来的虚函数,如果派生类没有覆盖其基类中的某个虚函数,则该虚函数的行为类似其他普通成员,派生类会直接继承其在基类中的版本。eg:
1 | class Quote { |
4.使用基类的引用(或指针)调用一个虚函数时将发生运行时动态绑定。即当某个函数形参是基类的引用时,可以为其传递一个基类的引用,也可以为其传递一个派生类的引用,在该函数内部调用虚函数时,将在运行时根据传递的是基类还是派生类来决定调用的是基类的虚函数还是派生类的虚函数。
5.基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作。
6.任何构造函数之外的非静态函数都可以是虚函数,关键字virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义。如果基类把一个函数声明为虚函数,则该函数在派生类中也将隐式地成为虚函数。当使用指针或引用调用虚函数时,该调用将被动态绑定。成员函数如果没被声明为虚函数,则其解析过程将发生在编译时而非运行时。
9.编译器可以隐式地执行派生类到基类的转换,所以之所有可以将基类的指针或引用绑定到了派生类,实际上是发生了动态转换。eg:
1 | Quote item; //基类 |
10.每个类控制它自己的成员初始化。尽管派生类对象中含有从基类继承而来的成员,但派生类并不能直接初始化这些成员。派生类必须使用基类的构造函数来初始化它的基类部分。eg:
1 | //Bulk_quote构造函数 |
初始化过程:首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。
11.如果基类定义了一个静态成员,则整个继承体系中只存在该成员的唯一定义。不论从基类派生出多少个派生类,对于每个静态成员来说都只存在唯一实例。也就是说调用时派生类和派生类对象可以直接调用基类中定义的静态成员。
12.派生类的声明不包含派生列表(class Bulk_quote;)。但被用作基类的类必须定义而非仅仅声明。
13.派生类也可以被继承。如果D为基类,D1继承自D,D2继承自D1,则D是D1的直接基类(direct base),同时是D2的间接基类(indirect base)。但类不能继承自它自己
14.C++11中可以在类名后面跟关键字final来指定某个类不能被继承。
15.派生类向基类的类型转换中存在静态类型与动态类型的问题。静态类型(static type)是指在编译时就已知其类型,它是变量声明时的类型或表达式生成的类型,如果表达式即不是指针也不是引用,则它的动态类型与静态类型永远一致。动态类型(dynamic type)是指变量或表达式在内存中的对象的类型,动态类型只到运行时才可知。例如派生类的对象可以赋给基类的指针或引用,此时基类的指针或引用在声明时便是静态类型,但在运行时却是动态类型,因为此时该指针或引用即可以接受一个派生类对象,也可以接受一个基类对象。所以只有基类的指针或引用的静态类与动态类型才有可能不一致。
16.不存在从基类向派生类的隐式类型转换,因为每个派生类对象中都包含一个基类部分,而基类对象中并不一定包含派生类的部分,当将一个基类的指针或引用绑定到派生类时,实际上是绑定到该派生类对象的基类部分上。派生类向基类的自动类型转换只针对指针或引用 有效。
虚函数
通过虚函数可以实现通过基类的引用来访问派生类中通过基类继承而来的虚函数。其中一个用处就是可以定义一个基类的变量,这个变量即可以指向基类对象,又可以指向派生类对象,那么通过该变量也就即可以访问基类成员又可以访问派生类的成员。
1.必须为每一个虚函数都提供定义,因为基类的引用或指针调用一个虚成员函数时会执行动态绑定,直到运行时才能知道到底调用了哪个版本的虚函数,因为在运行时用户可以根据需要来确定给虚函数提供的参数是基类的对象还是派生类的对象。
2.OOP的核心思想是多态性(polymorphism)。其含义是多种形式,具有继承关系的多个类型称为多态性,因为我们能使用这些类型的“多种形式”而无须在意它们之间的差异。例如基类的引用或指针可以动态绑定到派生类,从而通过虚函数来执行派生类的相应函数,在该情况下该引用或指针所绑定的对象可能是一个基类的对象也可能是一个派生类的对象,相应的执行的虚函数也就有可能是基类的版本,也有可能是派生类的版本。引用或指针的静态类型与动态类型不同这一事实是C++支持多态性的根本所在。
3.一个函数一旦被声明成了虚函数,则在有派生类中它都是虚函数。如果在派生类的覆盖了某个继承而来的虚函数,而它的形参类型和函数返回值必须与被它覆盖的基类函数完全一致。返回值的一个例外是当类的虚函数返回类型时类本身的指针或引用时,此时基类返回的类型肯定是基类的,派生类自然也可以返回派生类自己的。注意此处与函数重载是完全不一样的,重载只发生在同一个作用域之内。当然派生类中可以声明与基类形参列表不同但同名的函数,但此时该函数将与基类函数是相互独立的,且编译器不会报错,解决这个问题需要使用override关键字。
4.C++11中可以在派生类的虚函数中使用override关键字用来表明该函数将会覆盖基类中的虚函数,如果此时形参列表不致,编译将报错。
举例:
1 |
|
5.当把某个函数声明为final时,则之后任何尝试覆盖该函数的操作都将引发编译错误。final和override说明符出现在形参列表(包括任何const或引用修饰符)以及尾置返回类型之后。
6.如果在虚函数中使用默认实参,则通过基类的引用或指针调用函数时,不管派生类中定义的默认实参值是什么,都将使用基类的默认实参。所以基类和派生类的默认最好一致。
7.可以通过使用作用域运算符来强制回避虚函数的动态绑定。如:
1 | //强行调用基类中定义的函数版本而不管baseP的动态类型是什么 |
8.如果一个派生类的虚函数需要调用它的基类版本,而没有使用作用域运算符明确指定,则在动行时该调用将被解析为对派生类版本自身的调用 ,从而导致无限递归。eg:
1 | class A { |
9.关于虚函数表/虚表(vtable),当类中包含有虚成员函数时,在用该类实例化对象时,对象的第一个成员将是一个指向虚函数表的指针,gdb打印显示为_vptr。该虚函数表记录运行过程中实际应该调用的所有虚函数的入口地址。eg:
1 |
|
使用gdb调试时,分别打印dd和aa,发现dd的第一个成员是一个指向虚表的指针。
1 | (gdb) info locals |
10.虚析构函数的意义主要是为了防止内存泄漏。因为当一个基类的指针指向派生类的对象时,如果基类的析构函数不是虚函数,则通过该指针来delete派生类对象,系统将只执行基类的析构函数。因为只有通过虚函数实现的多态才能使用基类的指针访问派生类的成员。所以为了避免由于这种情况引起的无法正常析构派生类,往往将析构函数声明为虚函数,这样,执行delete时,系统将先执行派生类对象的析构函数,再执行基类的析构函数。
抽象基类-纯虚函数
可以通过将类定义成抽象基类来禁止用户创建该类的对象,通过在函数内部定义纯虚函数即可将一个类定义成抽象基类,无法创建抽象基类的对象。
1.纯虚函数(pure virtual)无须定义,只需要在函数体的位置(即声明语句的分号之前)添加=0
即可将一个虚函数声明为纯虚函数。=0
只能出现的类内部的虚函数声明语句处,当然这个虚函数可以是从基类继承而来的,此时可以不写函数声明前面的virtual关键字。如果在基类中定义纯虚函数,则必须带上virtual关键字。纯虚函数也可以有定义,但定义必须在类的外部,即不能在函数内部为一个=0的函数提供函数体。eg:
1 | virtual double net_price(std::size_t n) const = 0; |
2.含有纯虚函数的类是抽象基类(abstract base class),抽象基类负责定义接口,后续的其他类可以覆盖该接口提供具体实现。如果一个派生自抽象基类的派生类不给纯虚函数提供定义,则该派生类仍将是抽象基类。
3.派生类的构造函数只初始化它的直接基类,即不管继承多少次,派生类的构造函数都只负责初始化它自己新定义的成员,基类的成员将由基类的构造函数来初始化。eg:
1 | class A { |
4.抽象类的指针可以指向它的所有派生类对象,并调用派生类中的虚函数。
访问控制
- public成员: 可以被该类中的函数、友元函数、子类中的函数、类的对象访问
- protected成员: 可以被该类中的函数、友元函数、子类中的函数访问
- private成员: 可以被该类中的函数、友元函数访问
protected成员对于类的用户来说与私有成员一样,无法被类的用户访问,但对于派生类的成员和友元来说是可访问的。但派生类的成员或友元只能通过派生类对象来访问基类的protected成员,而派生类对于基类对象中的受保护成员没有任何访问权限,也就是说派生类的成员和友元只能访问派生类对象中的基类部分的受保护成员,因为派生类是继承而类的受保护成员。eg:
1 | class B { |
注意:访问权限是针对类外部对类对象数据的访问权限
如:
1 | class Bass { |
继承权限
1.某个类对于它继承而来的成员的访问权限受两个因素影响:一是基类中该成员的访问说明符,二是派生类的派生列表中的访问说明符
SOF上的解答:Difference between private, public, and protected inheritance
继承权限主要影响派生类中继承自基类的成员的被访问权限(即这些继承而来的成员是对否派生类的用户可见),而派生访问说明符对于派生类的成员(及友元)能否访问其直接基类的成员没有影响,对基类成员的访问权限只与基类中的访问说明符有关。根据基类的访问说明符可以有public、protected、private三种情况,子类中相应的权限有:
- public: 基类中的public、protected成员在子类中依然不变,当然private成员子类没有权限访问(这也是不指定继承权限时的默认情况)
- protected:基类中的public、protected成员在子类中都将变成protected。
- private: 基类中的三种访问权限的成员在子类中都将变成private。
2.如果派生是公有的,则基类的公有成员也是派生接口的组成部分,这样就可以将公有派生类型的对象绑定到基类的引用或指针上,即实现动态绑定。
也就是说只有当派生是公有的时候,对于用户代码来说才能使用派生类向基类的类型转换。如果派生方式是私有的或者是受保护的,则用户代码不能使用该转换。
3.不论派生类以什么方式继承自基类,派生类的成员函数和友元都能使用派生类向基类的转换。
4.类的设计建议:基类应该将其接口成员声明为公有的,同时将属于其实现的部分分成两组:一组可供派生类访问,即将该组声明为受保护的,这样派生类就能在实现自己的功能时使用基类的这些受保护的操作和数据;另一组只能由基类及基类的友元访问,即将该组声明为私有的。
5.友元与继承权限:
友元关系不能传递,同样友元关系也不能继承,基类的友元在访问派生类的成员时不具有特殊性,类似的,派生类的友元也不能随意访问基类的成员。但基类友的元可以访问内嵌在其派生类对象中的情况(因为每个派生类都包含一个完整的基类,那么基类的友元就可以通过派生类的对象来访问基类的成员,当然该基类成员也是派生类的成员)。
6.可以使用using指令来修改派生类继承的个别名字的访问级别。通过在类的内部使用using声明语句,可以将该类的直接或间接基类中的任何可访问成员(例如,非私有成员)标记出来,从而改变派生类中该基类成员的被访问权限。using声明语句中名字的访问权限由该using声明语句之前的访问说明符来决定。即,如果一条using声明语句出现在类的private部分,则该名字只能被类的成员和友元访问;相应的可以将其放在public和protected部分从而改变其被访问权限。
派生类只能为那些它可以访问的名字提供using声明。eg:
1 | class B { |
7.默认的继承保护级别由定义派生类所用的关键字来决定。默认情况下使用class关键字定义的派生类当省略继承权限控制关键字时是私有继承,而使用struct关键字定义的派生类是公有继承。eg:
1 | class B {/*...*/ }; |
使用struct还是class定义类之间的唯一差别就是默认成员访问说明符及默认派生访问说明符。
继承中的类作用域
1.当存在继承关系时,派生类的作用域嵌套到其基类的作用域之内。所以如果一个名字在派生类中无法正确解析,编译器将在外层的基类作用域内查找。
2.一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的,即使静态类型与动态类型可能不一致。当发生动态绑定时,由于形参使用的是基类的引用或指针,在使用该引用或指针的函数内部调用虚函数时将发生动态绑定,也就是在运行时才会解析该调用是使用派生类的虚函数还是基类的虚函数(注意此时派生类中的虚函数是继承自基类的)。但如果将一个基类的指针绑定到一个派生类,然后直接使用该指针调用派生类中新定义的成员将是非法的,因为此时实际上是产生了派生类向基类的隐式类型转换,所以在该指针所指的对象中只有基类的成员,并不存在派生类的成员。
3.派生类的成员将隐藏同名的基类成员,所以除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字。
4.可以使用域运算符::
通过基类来调用被隐藏的虚函数。
5.成员函数无论是否是虚函数都能被重载,派生类可以覆盖重载函数的0个或多个实例,如果派生类希望基类中所有的重载版本对于它来说都是可见的,那么它就需要覆盖所有的版本,或者一个也不覆盖。由于using
声明语句可以指定一个名字,所以可以使用一条基类成员函数的using声明语句来把该函数的所有重载实例添加到派生类作用域中,此时派生类只需要定义其特有的函数就可以了。注意using声明作用的成员必须是派生类可访问的。
继承体系中的拷贝控制
1.如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为,所以为了能够动态分配继承体系中的对象需要把函数声明为虚函数。虚函数将阻止合成默认的移动操作,即使通过=default的形式使用了合成的版本,编译器也不会为这个类合成移动操作。
2.在默认情况下,基类默认构造函数初始化派生类对象的基类部分,如果我们想拷贝或移动基类部分,则必须在派生类的构造函数初始值列表中显式地使用基类的拷贝或移动构造函数。同样,派生类的赋值运算符也必须显式地为其基类部分赋值。但派生类的析构函数只负责销毁由派生类自己分配 的资源,对象的基类部分将由基类的析构函数隐式销毁。eg:
1 | class B {/*...*/}; |
3.C++11中派生类能够重用直接基类定义的构造函数,相当于一种特殊的继承。类不能继承默认、拷贝和移动构造函数,如果派生类没有直接定义这些构造函数,编译器将为派生类合成它们。派生类继承基类构造函数的方式是提供一条注明了直接基类类名的using声明语句。eg:
1 | class Bulk_quote : public Dis_quote { |
如果派生类含有自己的数据成员,继承构造函数之后,这些成员将被默认初始化。
和普通成员的using声明不一样,构造函数的using声明不会改变该构造函数的访问级别。且using声明语句不能指定explicit和constexpr,如果基类的构造函数是explicit或者constexpr,则继承的构造函数也拥有相同的属性。
4.如果派生类定义的构造函数与基类的构造函数具有相同的参数列表,则该构造函数将不会被继承,定义在派生类中的构造函数将替换继承而来的构造函数。
容器与继承
1.当派生类对象被赋值给基类对象时,其中的派生类部分将被“切掉”,因此容器和存在继承关系的类型无法兼容。
2.可以在容器中放置基类的指针从而实现动态绑定,更好的选择是智能指针。eg:
1 | vector<Quote> basket_q; |
3.C++面向对象编程的一个悖论是无法直接使用对象进行面向对象编程,而是必须使用指针和引用,由于指针会增加程序的复杂性,所以最好是定义一些辅助的类的来处理指针问题。
4.如果使用G++编译时出现undefined reference to "vtable..."
需要检查是不是有某个类的虚函数没有提供定义,C++要求所有非纯虚函数(=0)的虚函数都必须有定义,也就是要有函数体,或者使用=default指定。
5.可以采用模拟虚拷贝的方式来实现在容器中所存储指针的动态绑定。即为基类定义一个虚函数,该虚函数返回*this的拷贝,其派生类重新定义该虚函数返回自己的拷贝。然后定义一个存放该基类静态类型指针的容器,则该容器中的指针便可动态绑定到基类及其派生类的对象。eg:
1 | //基类 |
文本查询再探
相关总结后补