整理码字不易,养成好习惯,点赞关注,你的支持就是我写下去的动力,谢谢老板。
本文为C++第三篇后续接着这篇文章写,大家可以持续关注,前四篇在主页
4.7 继承和派生
4.7.1 继承概述
4.7.1.1 为什么需要继承
网页类 class IndexPage{ public: //网页头部 void Header(){ cout << "网页头部!" << endl; } //网页左侧菜单 void LeftNavigation(){ cout << "左侧导航菜单!" << endl; } //网页主体部分 void MainBody(){ cout << "首页网页主题内容!" << endl; } //网页底部 void Footer(){ cout << "网页底部!" << endl; } private: string mTitle; //网页标题 };
#if 0 //如果不使用继承,那么定义新闻页类,需要重新写一遍已经有的代码 class NewsPage{ public: //网页头部 void Header(){ cout << "网页头部!" << endl; } //网页左侧菜单 void LeftNavigation(){ cout << "左侧导航菜单!" << endl; } //网页主体部分 void MainBody(){ cout << "新闻网页主体内容!" << endl; } //网页底部 void Footer(){ cout << "网页底部!" << endl; } private: string mTitle; //网页标题 };
void test(){ NewsPage* newspage = new NewsPage; newspage->Header(); newspage->MainBody(); newspage->LeftNavigation(); newspage->Footer(); } #else //使用继承,可以复用已有的代码,新闻业除了主体部分不一样,其他都是一样的 class NewsPage : public IndexPage{ public: //网页主体部分 void MainBody(){ cout << "新闻网页主主体内容!" << endl; } }; void test(){ NewsPage* newspage = new NewsPage; newspage->Header(); newspage->MainBody(); newspage->LeftNavigation(); newspage->Footer(); } #endif int main(){
test();
return EXIT_SUCCESS; } |
4.7.1.2 继承基本概念
c++最重要的特征是代码重用,通过继承机制可以利用已有的数据类型来定义新的数据类型,新的类不仅拥有旧类的成员,还拥有新定义的成员。
一个B类继承于A类,或称从类A派生类B。这样的话,类A成为基类(父类), 类B成为派生类(子类)。
派生类中的成员,包含两大部分:
- 一类是从基类继承过来的,一类是自己增加的成员。
- 从基类继承过过来的表现其共性,而新增的成员体现了其个性。
4.7.1.3 派生类定义
派生类定义格式:
Class 派生类名 : 继承方式 基类名{ //派生类新增的数据成员和成员函数 } |
三种继承方式:
-
- public : 公有继承
- private : 私有继承
- protected : 保护继承
从继承源上分:
- 单继承:指每个派生类只直接继承了一个基类的特征
- 多继承:指多个基类派生出一个派生类的继承关系,多继承的派生类直接继承了不止一个基类的特征
4.7.2 派生类访问控制
派生类继承基类,派生类拥有基类中全部成员变量和成员方法(除了构造和析构之外的成员方法),但是在派生类中,继承的成员并不一定能直接访问,不同的继承方式会导致不同的访问权限。
派生类的访问权限规则如下:
//基类 class A{ public: int mA; protected: int mB; private: int mC; };
//1. 公有(public)继承 class B : public A{ public: void PrintB(){ cout << mA << endl; //可访问基类public属性 cout << mB << endl; //可访问基类protected属性 //cout << mC << endl; //不可访问基类private属性 } }; class SubB : public B{ void PrintSubB(){ cout << mA << endl; //可访问基类public属性 cout << mB << endl; //可访问基类protected属性 //cout << mC << endl; //不可访问基类private属性 } }; void test01(){
B b; cout << b.mA << endl; //可访问基类public属性 //cout << b.mB << endl; //不可访问基类protected属性 //cout << b.mC << endl; //不可访问基类private属性 }
//2. 私有(private)继承 class C : private A{ public: void PrintC(){ cout << mA << endl; //可访问基类public属性 cout << mB << endl; //可访问基类protected属性 //cout << mC << endl; //不可访问基类private属性 } }; class SubC : public C{ void PrintSubC(){ //cout << mA << endl; //不可访问基类public属性 //cout << mB << endl; //不可访问基类protected属性 //cout << mC << endl; //不可访问基类private属性 } }; void test02(){ C c; //cout << c.mA << endl; //不可访问基类public属性 //cout << c.mB << endl; //不可访问基类protected属性 //cout << c.mC << endl; //不可访问基类private属性 } //3. 保护(protected)继承 class D : protected A{ public: void PrintD(){ cout << mA << endl; //可访问基类public属性 cout << mB << endl; //可访问基类protected属性 //cout << mC << endl; //不可访问基类private属性 } }; class SubD : public D{ void PrintD(){ cout << mA << endl; //可访问基类public属性 cout << mB << endl; //可访问基类protected属性 //cout << mC << endl; //不可访问基类private属性 } }; void test03(){ D d; //cout << d.mA << endl; //不可访问基类public属性 //cout << d.mB << endl; //不可访问基类protected属性 //cout << d.mC << endl; //不可访问基类private属性 } |
4.7.3 继承中的构造和析构
4.7.3.1 继承中的对象模型
在C++编译器的内部可以理解为结构体,子类是由父类成员叠加子类新成员而成:
class Aclass{ public: int mA; int mB; }; class Bclass : public Aclass{ public: int mC; }; class Cclass : public Bclass{ public: int mD; }; void test(){ cout << "A size:" << sizeof(Aclass) << endl; cout << "B size:" << sizeof(Bclass) << endl; cout << "C size:" << sizeof(Cclass) << endl; } |
4.7.3.2 对象构造和析构的调用原则
- 继承中的构造和析构
- 子类对象在创建时会首先调用父类的构造函数
- 父类构造函数执行完毕后,才会调用子类的构造函数
- 当父类构造函数有参数时,需要在子类初始化列表(参数列表)中显示调用父类构造函数
- 析构函数调用顺序和构造函数相反
class A{ public: A(){ cout << "A类构造函数!" << endl; } ~A(){ cout << "A类析构函数!" << endl; } };
class B : public A{ public: B(){ cout << "B类构造函数!" << endl; } ~B(){ cout << "B类析构函数!" << endl; } };
class C : public B{ public: C(){ cout << "C类构造函数!" << endl; } ~C(){ cout << "C类析构函数!" << endl; } };
void test(){ C c; } |
- 继承与组合混搭的构造和析构
class D{ public: D(){ cout << "D类构造函数!" << endl; } ~D(){ cout << "D类析构函数!" << endl; } }; class A{ public: A(){ cout << "A类构造函数!" << endl; } ~A(){ cout << "A类析构函数!" << endl; } }; class B : public A{ public: B(){ cout << "B类构造函数!" << endl; } ~B(){ cout << "B类析构函数!" << endl; } }; class C : public B{ public: C(){ cout << "C类构造函数!" << endl; } ~C(){ cout << "C类析构函数!" << endl; } public: D c; }; void test(){ C c; } |
4.7.3 继承中同名成员的处理方法
- 当子类成员和父类成员同名时,子类依然从父类继承同名成员
- 如果子类有成员和父类同名,子类访问其成员默认访问子类的成员(本作用域,就近原则)
- 在子类通过作用域::进行同名成员区分(在派生类中使用基类的同名成员,显示使用类名限定符)
class Base{ public: Base():mParam(0){} void Print(){ cout << mParam << endl; } public: int mParam; };
class Derived : public Base{ public: Derived():mParam(10){} void Print(){ //在派生类中使用和基类的同名成员,显示使用类名限定符 cout << Base::mParam << endl; cout << mParam << endl; } //返回基类重名成员 int& getBaseParam(){ return Base::mParam; } public: int mParam; };
int main(){
Derived derived; //派生类和基类成员属性重名,子类访问成员默认是子类成员 cout << derived.mParam << endl; //10 derived.Print(); //类外如何获得基类重名成员属性 derived.getBaseParam() = 100; cout << "Base:mParam:" << derived.getBaseParam() << endl;
return EXIT_SUCCESS; } |
注意: 如果重新定义了基类中的重载函数,将会发生什么?
class Base{ public: void func1(){ cout << "Base::void func1()" << endl; }; void func1(int param){ cout << "Base::void func1(int param)" << endl; } void myfunc(){ cout << "Base::void myfunc()" << endl; } }; class Derived1 : public Base{ public: void myfunc(){ cout << "Derived1::void myfunc()" << endl; } }; class Derived2 : public Base{ public: //改变成员函数的参数列表 void func1(int param1, int param2){ cout << "Derived2::void func1(int param1,int param2)" << endl; }; }; class Derived3 : public Base{ public: //改变成员函数的返回值 int func1(int param){ cout << "Derived3::int func1(int param)" << endl; return 0; } }; int main(){
Derived1 derived1; derived1.func1(); derived1.func1(20); derived1.myfunc(); cout << "-------------" << endl; Derived2 derived2; //derived2.func1(); //func1被隐藏 //derived2.func1(20); //func2被隐藏 derived2.func1(10,20); //重载func1之后,基类的函数被隐藏 derived2.myfunc(); cout << "-------------" << endl; Derived3 derived3; //derived3.func1(); 没有重新定义的重载版本被隐藏 derived3.func1(20); derived3.myfunc();
return EXIT_SUCCESS; } |
- Derive1 重定义了Base类的myfunc函数,derive1可访问func1及其重载版本的函数。
- Derive2通过改变函数参数列表的方式重新定义了基类的func1函数,则从基类中继承来的其他重载版本被隐藏,不可访问
- Derive3通过改变函数返回类型的方式重新定义了基类的func1函数,则从基类继承来的没有重新定义的重载版本的函数将被隐藏。
任何时候重新定义基类中的一个重载函数,在新类中所有的其他版本将被自动隐藏. |
4.7.4 非自动继承的函数
不是所有的函数都能自动从基类继承到派生类中。构造函数和析构函数用来处理对象的创建和析构操作,构造和析构函数只知道对它们的特定层次的对象做什么,也就是说构造函数和析构函数不能被继承,必须为每一个特定的派生类分别创建。
另外operator=也不能被继承,因为它完成类似构造函数的行为。也就是说尽管我们知道如何由=右边的对象如何初始化=左边的对象的所有成员,但是这个并不意味着对其派生类依然有效。
在继承的过程中,如果没有创建这些函数,编译器会自动生成它们。
4.7.5 继承中的静态成员特性
静态成员函数和非静态成员函数的共同点:
- 他们都可以被继承到派生类中。
- 如果重新定义一个静态成员函数,所有在基类中的其他重载函数会被隐藏。
- 如果我们改变基类中一个函数的特征,所有使用该函数名的基类版本都会被隐藏。
静态成员函数不能是虚函数(virtual function).
class Base{ public: static int getNum(){ return sNum; } static int getNum(int param){ return sNum + param; } public: static int sNum; }; int Base::sNum = 10;
class Derived : public Base{ public: static int sNum; //基类静态成员属性将被隐藏 #if 0 //重定义一个函数,基类中重载的函数被隐藏 static int getNum(int param1, int param2){ return sNum + param1 + param2; } #else //改变基类函数的某个特征,返回值或者参数个数,将会隐藏基类重载的函数 static void getNum(int param1, int param2){ cout << sNum + param1 + param2 << endl; } #endif }; int Derived::sNum = 20; |
4.7.6 多继承
4.7.6.1 多继承概念
我们可以从一个类继承,我们也可以能同时从多个类继承,这就是多继承。但是由于多继承是非常受争议的,从多个类继承可能会导致函数、变量等同名导致较多的歧义。
class Base1{ public: void func1(){ cout << "Base1::func1" << endl; } }; class Base2{ public: void func1(){ cout << "Base2::func1" << endl; } void func2(){ cout << "Base2::func2" << endl; } }; //派生类继承Base1、Base2 class Derived : public Base1, public Base2{}; int main(){
Derived derived; //func1是从Base1继承来的还是从Base2继承来的? //derived.func1(); derived.func2();
//解决歧义:显示指定调用那个基类的func1 derived.Base1::func1(); derived.Base2::func1();
return EXIT_SUCCESS; } |
多继承会带来一些二义性的问题, 如果两个基类中有同名的函数或者变量,那么通过派生类对象去访问这个函数或变量时就不能明确到底调用从基类1继承的版本还是从基类2继承的版本?
解决方法就是显示指定调用那个基类的版本。
4.7.6.2 菱形继承和虚继承
两个派生类继承同一个基类而又有某个类同时继承者两个派生类,这种继承被称为菱形继承,或者钻石型继承。
这种继承所带来的问题:
- 羊继承了动物的数据和函数,鸵同样继承了动物的数据和函数,当草泥马调用函数或者数据时,就会产生二义性。
- 草泥马继承自动物的函数和数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
class BigBase{ public: BigBase(){ mParam = 0; } void func(){ cout << "BigBase::func" << endl; } public: int mParam; };
class Base1 : public BigBase{}; class Base2 : public BigBase{}; class Derived : public Base1, public Base2{};
int main(){
Derived derived; //1. 对“func”的访问不明确 //derived.func(); //cout << derived.mParam << endl; cout << "derived.Base1::mParam:" << derived.Base1::mParam << endl; cout << "derived.Base2::mParam:" << derived.Base2::mParam << endl;
//2. 重复继承 cout << "Derived size:" << sizeof(Derived) << endl; //8
return EXIT_SUCCESS; } |
上述问题如何解决?对于调用二义性,那么可通过指定调用那个基类的方式来解决,那么重复继承怎么解决?
对于这种菱形继承所带来的两个问题,c++为我们提供了一种方式,采用虚基类。那么我们采用虚基类方式将代码修改如下:
class BigBase{ public: BigBase(){ mParam = 0; } void func(){ cout << "BigBase::func" << endl; } public: int mParam; };
class Base1 : virtual public BigBase{}; class Base2 : virtual public BigBase{}; class Derived : public Base1, public Base2{};
int main(){
Derived derived; //二义性问题解决 derived.func(); cout << derived.mParam << endl; //输出结果:12 cout << "Derived size:" << sizeof(Derived) << endl;
return EXIT_SUCCESS; } |
以上程序Base1 ,Base2采用虚继承方式继承BigBase,那么BigBase被称为虚基类。
通过虚继承解决了菱形继承所带来的二义性问题。
但是虚基类是如何解决二义性的呢?并且derived大小为12字节,这是怎么回事?
4.7.6.3 虚继承实现原理
class BigBase{ public: BigBase(){ mParam = 0; } void func(){ cout << "BigBase::func" << endl; } public: int mParam; }; #if 0 //虚继承 class Base1 : virtual public BigBase{}; class Base2 : virtual public BigBase{}; #else //普通继承 class Base1 : public BigBase{}; class Base2 : public BigBase{}; #endif class Derived : public Base1, public Base2{}; |
通过内存图,我们发现普通继承和虚继承的对象内存图是不一样的。我们也可以猜测到编译器肯定对我们编写的程序做了一些手脚。
- BigBase 菱形最顶层的类,内存布局图没有发生改变。
- Base1和Base2通过虚继承的方式派生自BigBase,这两个对象的布局图中可以看出编译器为我们的对象中增加了一个vbptr (virtual base pointer),vbptr指向了一张表,这张表保存了当前的虚指针相对于虚基类的首地址的偏移量。
- Derived派生于Base1和Base2,继承了两个基类的vbptr指针,并调整了vbptr与虚基类的首地址的偏移量。
由此可知编译器帮我们做了一些幕后工作,使得这种菱形问题在继承时候能只继承一份数据,并且也解决了二义性的问题。现在模型就变成了Base1和 Base2 Derived三个类对象共享了一份BigBase数据。
当使用虚继承时,虚基类是被共享的,也就是在继承体系中无论被继承多少次,对象内存模型中均只会出现一个虚基类的子对象(这和多继承是完全不同的)。即使共享虚基类,但是必须要有一个类来完成基类的初始化(因为所有的对象都必须被初始化,哪怕是默认的),同时还不能够重复进行初始化,那到底谁应该负责完成初始化呢?C++标准中选择在每一次继承子类中都必须书写初始化语句(因为每一次继承子类可能都会用来定义对象),但是虚基类的初始化是由最后的子类完成,其他的初始化语句都不会调用。
class BigBase{ public: BigBase(int x){mParam = x;} void func(){cout << "BigBase::func" << endl;} public: int mParam; }; class Base1 : virtual public BigBase{ public: Base1() :BigBase(10){} //不调用BigBase构造 }; class Base2 : virtual public BigBase{ public: Base2() :BigBase(10){} //不调用BigBase构造 };
class Derived : public Base1, public Base2{ public: Derived() :BigBase(10){} //调用BigBase构造 }; //每一次继承子类中都必须书写初始化语句 int main(){ Derived derived; return EXIT_SUCCESS; } |
注意:
虚继承只能解决具备公共祖先的多继承所带来的二义性问题,不能解决没有公共祖先的多继承的. |
工程开发中真正意义上的多继承是几乎不被使用,因为多重继承带来的代码复杂性远多于其带来的便利,多重继承对代码维护性上的影响是灾难性的,在设计方法上,任何多继承都可以用单继承代替。
4.8 多态
4.8.1 多态基本概念
多态是面向对象程序设计语言中数据抽象和继承之外的第三个基本特征。
多态性(polymorphism)提供接口与具体实现之间的另一层隔离,从而将”what”和”how”分离开来。多态性改善了代码的可读性和组织性,同时也使创建的程序具有可扩展性,项目不仅在最初创建时期可以扩展,而且当项目在需要有新的功能时也能扩展。
c++支持编译时多态(静态多态)和运行时多态(动态多态),运算符重载和函数重载就是编译时多态,而派生类和虚函数实现运行时多态。
静态多态和动态多态的区别就是函数地址是早绑定(静态联编)还是晚绑定(动态联编)。如果函数的调用,在编译阶段就可以确定函数的调用地址,并产生代码,就是静态多态(编译时多态),就是说地址是早绑定的。而如果函数的调用地址不能编译不能在编译期间确定,而需要在运行时才能决定,这这就属于晚绑定(动态多态,运行时多态)。
//计算器 class Caculator{ public: void setA(int a){ this->mA = a; } void setB(int b){ this->mB = b; } void setOperator(string oper){ this->mOperator = oper; } int getResult(){
if (this->mOperator == "+"){ return mA + mB; } else if (this->mOperator == "-"){ return mA - mB; } else if (this->mOperator == "*"){ return mA * mB; } else if (this->mOperator == "/"){ return mA / mB; } } private: int mA; int mB; string mOperator; };
//这种程序不利于扩展,维护困难,如果修改功能或者扩展功能需要在源代码基础上修改 //面向对象程序设计一个基本原则:开闭原则(对修改关闭,对扩展开放)
//抽象基类 class AbstractCaculator{ public: void setA(int a){ this->mA = a; } virtual void setB(int b){ this->mB = b; } virtual int getResult() = 0; protected: int mA; int mB; string mOperator; };
//加法计算器 class PlusCaculator : public AbstractCaculator{ public: virtual int getResult(){ return mA + mB; } };
//减法计算器 class MinusCaculator : public AbstractCaculator{ public: virtual int getResult(){ return mA - mB; } };
//乘法计算器 class MultipliesCaculator : public AbstractCaculator{ public: virtual int getResult(){ return mA * mB; } };
void DoBussiness(AbstractCaculator* caculator){ int a = 10; int b = 20; caculator->setA(a); caculator->setB(b); cout << "计算结果:" << caculator->getResult() << endl; delete caculator; } |
4.8.2 向上类型转换及问题
4.8.2.1 问题抛出
对象可以作为自己的类或者作为它的基类的对象来使用。还能通过基类的地址来操作它。取一个对象的地址(指针或引用),并将其作为基类的地址来处理,这种称为向上类型转换。
也就是说:父类引用或指针可以指向子类对象,通过父类指针或引用来操作子类对象。
class Animal{ public: void speak(){ cout << "动物在唱歌..." << endl; } };
class Dog : public Animal{ public: void speak(){ cout << "小狗在唱歌..." << endl; } };
void DoBussiness(Animal& animal){ animal.speak(); }
void test(){ Dog dog; DoBussiness(dog); } |
运行结果: 动物在唱歌 问题抛出: 我们给DoBussiness传入的对象是dog,而不是animal对象,输出的结果应该是Dog::speak。 |
4.8.2.1 问题解决思路
解决这个问题,我们需要了解下绑定(捆绑,binding)概念。
把函数体与函数调用相联系称为绑定(捆绑,binding) |
当绑定在程序运行之前(由编译器和连接器)完成时,称为早绑定(early binding).C语言中只有一种函数调用方式,就是早绑定。
上面的问题就是由于早绑定引起的,因为编译器在只有Animal地址时并不知道要调用的正确函数。编译是根据指向对象的指针或引用的类型来选择函数调用。这个时候由于DoBussiness的参数类型是Animal&,编译器确定了应该调用的speak是Animal::speak的,而不是真正传入的对象Dog::speak。
解决方法就是迟绑定(迟捆绑,动态绑定,运行时绑定,late binding),意味着绑定要根据对象的实际类型,发生在运行。
C++语言要实现这种动态绑定,必须有某种机制来确定运行时对象的类型并调用合适的成员函数。对于一种编译语言,编译器并不知道实际的对象类型(编译器并不知道Animal类型的指针或引用指向的实际的对象类型)。
4.8.2.1 问题解决方案(虚函数,vitual function)
C++动态多态性是通过虚函数来实现的,虚函数允许子类(派生类)重新定义父类(基类)成员函数,而子类(派生类)重新定义父类(基类)虚函数的做法称为覆盖(override),或者称为重写。
对于特定的函数进行动态绑定,c++要求在基类中声明这个函数的时候使用virtual关键字,动态绑定也就对virtual函数起作用.
- 为创建一个需要动态绑定的虚成员函数,可以简单在这个函数声明前面加上virtual关键字,定义时候不需要.
- 如果一个函数在基类中被声明为virtual,那么在所有派生类中它都是virtual的.
- 在派生类中virtual函数的重定义称为重写(override).
- Virtual关键字只能修饰成员函数.
- 构造函数不能为虚函数
注意: 仅需要在基类中声明一个函数为virtual.调用所有匹配基类声明行为的派生类函数都将使用虚机制。虽然可以在派生类声明前使用关键字virtual(这也是无害的),但这个样会使得程序显得冗余和杂乱。(我建议写上) |
class Animal{ public: virtual void speak(){ cout << "动物在唱歌..." << endl; } }; class Dog : public Animal{ public: virtual void speak(){ cout << "小狗在唱歌..." << endl; } }; void DoBussiness(Animal& animal){ animal.speak(); } void test(){ Dog dog; DoBussiness(dog); } |
4.8.3 C++如何实现动态绑定
动态绑定什么时候发生?所有的工作都是由编译器在幕后完成。当我们告诉通过创建一个virtual函数来告诉编译器要进行动态绑定,那么编译器就会根据动态绑定机制来实现我们的要求, 不会再执行早绑定。
问题:C++的动态捆绑机制是怎么样的?
首先,我们看看编译器如何处理虚函数。当编译器发现我们的类中有虚函数的时候,编译器会创建一张虚函数表,把虚函数的函数入口地址放到虚函数表中,并且在类中秘密增加一个指针,这个指针就是vpointer(缩写vptr),这个指针是指向对象的虚函数表。在多态调用的时候,根据vptr指针,找到虚函数表来实现动态绑定。
验证对象中的虚指针: |
class A{ public: virtual void func1(){} virtual void func2(){} };
//B类为空,那么大小应该是1字节,实际情况是这样吗? class B : public A{};
void test(){ cout << "A size:" << sizeof(A) << endl; cout << "B size:" << sizeof(B) << endl; } |
在编译阶段,编译器秘密增加了一个vptr指针,但是此时vptr指针并没有初始化指向虚函数表(vtable),什么时候vptr才会指向虚函数表?在对象构建的时候,也就是在对象初始化调用构造函数的时候。编译器首先默认会在我们所编写的每一个构造函数中,增加一些vptr指针初始化的代码。如果没有提供构造函数,编译器会提供默认的构造函数,那么就会在默认构造函数里做此项工作,初始化vptr指针,使之指向本对象的虚函数表。
起初,子类继承基类,子类继承了基类的vptr指针,这个vptr指针是指向基类虚函数表,当子类调用构造函数,使得子类的vptr指针指向了子类的虚函数表。
- 当子类无重写基类虚函数时:
过程分析: Animal* animal = new Dog; animal->fun1(); 当程序执行到这里,会去animal指向的空间中寻找vptr指针,通过vptr指针找到func1函数,此时由于子类并没有重写也就是覆盖基类的func1函数,所以调用func1时,仍然调用的是基类的func1. 执行结果: 我是基类的func1 测试结论: 无重写基类的虚函数,无意义 |
- 当子类重写基类虚函数时:
过程分析: Animal* animal = new Dog; animal->fun1(); 当程序执行到这里,会去animal指向的空间中寻找vptr指针,通过vptr指针找到func1函数,由于子类重写基类的func1函数,所以调用func1时,调用的是子类的func1. 执行结果: 我是子类的func1 测试结论: 无重写基类的虚函数,无意义 |
多态的成立条件:
a) 返回值,函数名字,函数参数,必须和父类完全一致(析构函数除外) b) 子类中virtual关键字可写可不写,建议写
|
4.8.4 抽象基类和纯虚函数(pure virtual function)
在设计时,常常希望基类仅仅作为其派生类的一个接口。这就是说,仅想对基类进行向上类型转换,使用它的接口,而不希望用户实际的创建一个基类的对象。同时创建一个纯虚函数允许接口中放置成员原函数,而不一定要提供一段可能对这个函数毫无意义的代码。
做到这点,可以在基类中加入至少一个纯虚函数(pure virtual function),使得基类称为抽象类(abstract class).
- 纯虚函数使用关键字virtual,并在其后面加上=0。如果试图去实例化一个抽象类,编译器则会阻止这种操作。
- 当继承一个抽象类的时候,必须实现所有的纯虚函数,否则由抽象类派生的类也是一个抽象类。
- Virtual void fun() = 0;告诉编译器在vtable中为函数保留一个位置,但在这个特定位置不放地址。
建立公共接口目的是为了将子类公共的操作抽象出来,可以通过一个公共接口来操纵一组类,且这个公共接口不需要事先(或者不需要完全实现)。可以创建一个公共类. |
案例: 模板方法模式
//抽象制作饮品 class AbstractDrinking{ public: //烧水 virtual void Boil() = 0; //冲泡 virtual void Brew() = 0; //倒入杯中 virtual void PourInCup() = 0; //加入辅料 virtual void PutSomething() = 0; //规定流程 void MakeDrink(){ Boil(); Brew(); PourInCup(); PutSomething(); } };
//制作咖啡 class Coffee : public AbstractDrinking{ public: //烧水 virtual void Boil(){ cout << "煮农夫山泉!" << endl; } //冲泡 virtual void Brew(){ cout << "冲泡咖啡!" << endl; } //倒入杯中 virtual void PourInCup(){ cout << "将咖啡倒入杯中!" << endl; } //加入辅料 virtual void PutSomething(){ cout << "加入牛奶!" << endl; } };
//制作茶水 class Tea : public AbstractDrinking{ public: //烧水 virtual void Boil(){ cout << "煮自来水!" << endl; } //冲泡 virtual void Brew(){ cout << "冲泡茶叶!" << endl; } //倒入杯中 virtual void PourInCup(){ cout << "将茶水倒入杯中!" << endl; } //加入辅料 virtual void PutSomething(){ cout << "加入食盐!" << endl; } };
//业务函数 void DoBussiness(AbstractDrinking* drink){ drink->MakeDrink(); delete drink; }
void test(){ DoBussiness(new Coffee); cout << "--------------" << endl; DoBussiness(new Tea); } |
4.8.5 纯虚函数和多继承
多继承带来了一些争议,但是接口继承可以说一种毫无争议的运用了。
绝大数面向对象语言都不支持多继承,但是绝大数面向对象对象语言都支持接口的概念,c++中没有接口的概念,但是可以通过纯虚函数实现接口。
接口类中只有函数原型定义,没有任何数据定义。 |
多重继承接口不会带来二义性和复杂性问题。接口类只是一个功能声明,并不是功能实现,子类需要根据功能说明定义功能实现。
注意:除了析构函数外,其他声明都是纯虚函数。
4.8.6 虚析构函数
4.8.6.1 虚析构函数作用
虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。
class People{ public: People(){ cout << "构造函数 People!" << endl; } virtual void showName() = 0; virtual ~People(){ cout << "析构函数 People!" << endl; } };
class Worker : public People{ public: Worker(){ cout << "构造函数 Worker!" << endl; pName = new char[10]; } virtual void showName(){ cout << "打印子类的名字!" << endl; } ~Worker(){ cout << "析构函数 Worker!" << endl; if (pName != NULL){ delete pName; } } private: char* pName; };
void test(){
People* people = new Worker; people->~People(); } |
4.8.6.2 纯虚析构函数
纯虚析构函数在c++中是合法的,但是在使用的时候有一个额外的限制:必须为纯虚析构函数提供一个函数体。
那么问题是:如果给虚析构函数提供函数体了,那怎么还能称作纯虚析构函数呢?
纯虚析构函数和非纯析构函数之间唯一的不同之处在于纯虚析构函数使得基类是抽象类,不能创建基类的对象。
//非纯虚析构函数 class A{ public: virtual ~A(); };
A::~A(){}
//纯析构函数 class B{ public: virtual ~B() = 0; };
B::~B(){}
void test(){ A a; //A类不是抽象类,可以实例化对象 B b; //B类是抽象类,不可以实例化对象 } |
如果类的目的不是为了实现多态,作为基类来使用,就不要声明虚析构函数,反之,则应该为类声明虚析构函数。 |
4.8.7 重写 重载 重定义
- 重载,同一作用域的同名函数
- 同一个作用域
- 参数个数,参数顺序,参数类型不同
- 和函数返回值,没有关系
- const也可以作为重载条件 //do(const Teacher& t){} do(Teacher& t)
- 重定义(隐藏)
- 有继承
- 子类(派生类)重新定义父类(基类)的同名成员(非virtual函数)
- 重写(覆盖)
- 有继承
- 子类(派生类)重写父类(基类)的virtual函数
- 函数返回值,函数名字,函数参数,必须和基类中的虚函数一致
class A{ public: //同一作用域下,func1函数重载 void func1(){} void func1(int a){} void func1(int a,int b){} void func2(){} virtual void func3(){} };
class B : public A{ public: //重定义基类的func2,隐藏了基类的func2方法 void func2(){} //重写基类的func3函数,也可以覆盖基类func3 virtual void func3(){} }; |
4.8.8 指向类成员的指针
4.8.8.1 指向成员变量的指针
- 定义格式
<数据类型> <类名>::*<指针名> 例如: int A::*pPram; |
-
- 赋值/初始化
<数据类型> <类名>::*<指针名> = &<类名>::<非静态数据成员> 例如: int A::*pParam = &A::param; |
- 解引用
<类对象名>.*<非静态数据成员指针> <类对象指针>->*<非静态数据成员指针> 例如: A a; a.*pParam; a->*pParam; |
class A{ public: A(int param){ mParam = param; } public: int mParam; };
void test(){ A a1(100); A* a2 = new A(200); int* p1 = &a1.mParam; int A::*p2 = &A::mParam;
cout << "*p1:" << *p1 << endl; cout << "a1.*p2:" << a1.*p2 << endl; cout << "a2->*p2:" << a2->*p2 << endl; } |
4.8.8.2 指向成员函数的指针
- 定义格式
<返回类型> (<类名>::*<指针名>)(<参数列表>) 例如: void (A::*pFunc)(int,int); |
-
- 赋值/初始化
<返回类型>(<类名>::*<指针名>)(<参数列表>) = &<类名>::<非静态数据函数> 例如: void (A::pFunc)(int,int) = &A::func; |
- 解引用
(<类对象名>.*<非静态成员函数>)(<参数列表>) (<类对象指针>->*<非静态成员函数>)(<参数列表>) 例如: A a; (a.*pFunc)(10,20); (a->*pFunc)(10,20); |
class A{ public: int func(int a,int b){ return a + b; } };
void test(){ A a1; A* a2 = new A;
//初始化成员函数指针 int(A::*pFunc)(int, int) = &A::func; //指针解引用 cout << "(a1.*pFunc)(10,20):" << (a1.*pFunc)(10, 20) << endl; cout << "(a2->*pFunc)(10,20):" << (a2->*pFunc)(10, 20) << endl; } |
4.8.8.3 指向静态成员的指针
- 指向类静态数据成员的指针
指向静态数据成员的指针的定义和使用与普通指针相同,在定义时无须和类相关联,在使用时也无须和具体的对象相关联。 - 指向类静态成员函数的指针
指向静态成员函数的指针和普通指针相同,在定义时无须和类相关联,在使用时也无须和具体的对象相关联·
class A{ public: static void dis(){ cout << data << endl; } static int data; };
int A::data = 100;
void test(){ int *p = &A::data; cout << *p << endl; void(*pfunc)() = &A::dis; pfunc(); } |