C++类和对象(中):https://developer.aliyun.com/article/1459441
同名函数
当子类实现父类同名成员函数时候,父类的所有同名函数将会被子类屏蔽。如果用户必须调用父类的同名函数则加上作用域即可。
#include <iostream> #include <string> #include <iostream> #include <algorithm> #include <fstream> using namespace std; class Base { public: int getNum() { return num; } int getNum(int a) { return num; } private: int num{10}; }; class Derive : public Base { public: int getNum() { return num; } private: int num{20}; }; int main(int argc, char* argv[]) { Derive d; cout << d.getNum() << endl; cout << d.Base::getNum() << endl; cout << d.Base::getNum(1) << endl; return 0; }
C++中基类不是所有的函数都能被继承到派生类中,构造函数和析构函数用来处理对象的创建和析构操作,构造和析构函数只知道对他们的特定层次的对象做什么,也就是说构造函数和析构函数不能被继承,必须为每一个特定的派生类分别创建,另外operator=也不能被继承,因为他完成类似于拷贝构造的行为,也就是说尽管我们知道基类如何由=右边的对象初始化左边的对象的所以后成员,但是这个并不意味着这对其派生类依然有效。在继承过程中,如果没有重写这些函数,则编译器会给这些函数生成默认函数。
静态同名成员和函数
静态成员函数和非静态成员函数的共同点:
- 都可以被继承到派生类中
- 如果重新定义一个静态成员函数,则所有基类的其他重载函数都会被屏蔽
- 如果我们改变基类中的一个函数的特征,所以使用还函数名的基类模板都会被隐藏
#include <iostream> #include <string> #include <iostream> #include <algorithm> #include <fstream> using namespace std; class Base { public: static int getNum() { return num; } static int getNum(int a) { return num; } private: static int num; }; int Base::num{10}; class Derive : public Base { public: static int getNum() { return num; } private: static int num; }; int Derive::num{20}; int main(int argc, char* argv[]) { Derive d; cout << d.getNum() << endl; cout << d.Base::getNum() << endl; cout << d.Base::getNum(1) << endl; return 0; }
多继承
在C++中,我们可以从一个类中继承,也可以同时从多个类中继承,这就是多继承。但是由于多继承是非常受争议的,从多个类继承可能会因为函数、变量等同名导致较多的歧义。
多继承会有二义性问题,如果两个基类中有同名的函数或者是变量,那么通过派生类对象去访问这个函数或者是变量的时候就不能明确到底调用那个版本的函数
#include <iostream> #include <string> #include <iostream> #include <algorithm> #include <fstream> using namespace std; class Base { public: int a{10}; }; class Base2 { public: int a{20}; }; class Derive : public Base, public Base2 { public: }; int main(int argc, char* argv[]) { Derive d; cout << d.Base::a << endl; cout << d.Base2::a << endl; return 0; }
菱形继承
两个派生类继承同一个基类而又有某个类同时继承这两个派生类,这种继承称为菱形继承或者钻石型继承。
因为继承的数据存在两份,因此调用数据的时候会存在二义性问题。这可以加作用域访问对应的父类的a,可以解决二义性,但是继承了两份数据。
对于菱形继承带来的问题,C++提供了一种方式,采用虚基类。
class Base { public: int a{10}; }; class Base2 : public Base { public: }; class Base1 : public Base { public: }; // Derive 中有两个a class Derive : virtual public Base1, virtual public Base2 { public: };
虚继承
在继承方式前面加virtual修饰,叫做虚继承。使用虚继承,在菱形继承的时候不存在菱形继承的问题。
class Derive : virtual 继承方式 Base { // ...... };
#include <iostream> #include <string> #include <iostream> #include <algorithm> #include <fstream> using namespace std; class Base { public: int a{10}; }; class Base2 : virtual public Base { public: }; class Base1 : virtual public Base { public: }; class Derive : public Base1, public Base2 { public: }; int main(int argc, char* argv[]) { Derive d; cout << d.a << endl; return 0; }
vbptr:虚基类指针,虚基类指针是指向虚基类表vbtable
vbtable:虚基类表中存放的是数据的偏移量。
总结: 使用虚继承产生vbptr 和 vbtable 的目的是为了保证不管多少个继承,虚基类的数据只有一份。
当使用虚继承的时候,虚基类是被共享的,也就是在继承体系中,无论被继承多少次,对象内存模型中之后出现一个虚基类的字对象(这和多继承是完全不同的)。即使共享虚基类,但是必须以后一个类来完成基类的初始化(因为所有的对象都必须完成初始化,那怕是默认的),同时还不能够进行重复的初始化。
那么虚基类由谁来初始化呢?C++标准中选择在每次继承子类中必须书写初始化语句(因为每一次继承子类都会用来定义对象),但是虚基类的初始化是由最后的子类完成,其他的初始化语句都不会调用。
多态
多态是面向对象程序设计中数据抽象和继承之外的第三个基本特征。多态性提供接口与具体实现之间的另一层隔离,从而将what和how分离开来。多态性改善了代码的可读性和组织性,同时也使创建的程序具有可扩展性,项目不仅在最初创建时期可以去扩展,而且当项目在需要新的功能时也能扩展。C++支持编译时多态(静态多态)和运行时多态(动态多态),运算符重载和函数重载就是编译时多态,而派生类和虚函数就是实现运行时多态。静态多态和动态多态的区别就是函数地址是早绑定(静态联编)和晚绑定(动态联编)。如果函数的调用在编译阶段就可以确定调用地址,并且产生函数代码,就是静态多态,就是说地址是早绑定的。而如果函数的调用地址不能在编译期间确定,而需要在运行时候才能决定,就是运行时多态, 属于晚绑定。
#include <iostream> #include <string> #include <iostream> #include <algorithm> #include <fstream> using namespace std; class Base { public: virtual void print() { cout << "Base" << endl; } }; class Derive : public Base { public: void print() override { cout << "Derive" << endl; } }; int main(int argc, char* argv[]) { Base *d = new Derive(); d->print(); return 0; }
备注: 如果不适用virtual main函数中的print调用的会是父类的方法,而不是派生类的方法
C++动态多态主要就是通过虚函数来实现的,虚函数允许子类重新定义父类成员函数,而子类重新定义父类虚函数的做法称为覆盖或者称为重写(override)。对于特定的函数进行动态绑定,C++要求在基类中声明这个函数的时候使用virtual关键字修饰,动态绑定也就对virtual函数生效。为创建一个需要动态绑定的虚成员函数,可以简单的在这个函数声明前加上virtual关键字。如果一个函数在基类中被声明为virtual关键字,那么在所有派生类中它都是virtual的,在派生类中virtual函数的重定义称为重写(override)。
注意: virtual关键字只能修饰成员函数和析构函数,构造函数不能为虚函数。
虚函数
虚函数在类中会产生一个虚函数指针,虚函数指针指向虚函数表;
vfptr : 虚函数指针,指向虚函数表
vftable : 虚函数表,存放的是虚函数的入口地址
总结:
- 如果类中不涉及到继承,则函数指针偏移量指向自身的函数。
- 如果涉及到继承,派生类会继承基类的虚函数指针和虚函数表,编译器会将虚函数表中的函数入口地址更新为子类的函数地址。如果使用基类指针或者引用访问虚函数的时候会间接的调用子类的虚函数。
应用
一般使用基类指针或者引用作为函数参数:
#include <iostream> using namespace std; class Base { public: virtual void print() { cout << "Base" << endl; } }; class Derive1 : public Base { public: void print() override { cout << "Derive1" << endl; } }; class Derive2 : public Base { public: void print() override { cout << "Derive2" << endl; } }; void func(Base &base) { base.print(); } int main(int argc, char* argv[]) { Derive1 d1; Derive2 d2; func(d2); func(d1); return 0; }
虚析构
虚析构函数是为了解决基类指针指向派生类对象,并且基类的指针删除派生类的对象。
先看下面的例子:
#include <iostream> using namespace std; class Base { public: ~Base() { cout << "~Base" << endl; } virtual void print() { cout << "Base" << endl; } }; class Derive : public Base { public: ~Derive() { cout << "~Derive" << endl; } void print() override { cout << "Derive" << endl; } }; int main(int argc, char* argv[]) { Base *b = new Derive(); delete b; return 0; }
我们看到子类并没有被析构。
如果我们把析构函数改成虚函数,则发现都可以调用,代码如下:
#include <iostream> using namespace std; class Base { public: virtual ~Base() { cout << "~Base" << endl; } virtual void print() { cout << "Base" << endl; } }; class Derive : public Base { public: ~Derive() { cout << "~Derive" << endl; } void print() override { cout << "Derive" << endl; } }; int main(int argc, char* argv[]) { Base *b = new Derive(); delete b; return 0; }
建议: 如果类会被继承,把析构函数写成虚函数
纯虚函数和抽象类
在设计类时候,尝尝希望基类仅仅作为其派生类的一个接口。也就是说,仅想对基类进行向上转换,使用他的接口,而不希望用户在实际使用的时候创建一个基类对象。同时创建一个纯虚函数允许接口中放置成员函数,而不一定会要提供一段可能对这个函数毫无意义的代码。
纯虚函数格式是
virtual void function() = 0;
如果一个类中有纯虚函数,那么这个类就是抽象类
抽象类不能实例化对象;virtual void function() = 0告诉编译器在vftable中为函数保留一个位置,但是这个特定的位置不放地址。
建立公共接口的目的是为了将公共的操作抽象出来,可以通过一个公共接口来操作一组类,且这个公共接口不需要实现(或者是不需要完全实现)。可以创建一个公共的类。
#include <iostream> using namespace std; class Base { public: virtual ~Base() = default; virtual void print1() = 0; virtual void print2() = 0; virtual void print3() = 0; virtual void print() { print1(); print2(); print3(); } }; class Derive1 : public Base { public: void print1() override { cout << "print1 "; } void print2() override { cout << "print2 "; } void print3() override { cout << "print2 Derive1" << endl; } }; class Derive2 : public Base { public: void print1() override { cout << "print1 "; } void print2() override { cout << "print2 "; } void print3() override { cout << "print2 Derive2" << endl; } }; int main(int argc, char* argv[]) { Base *b1 = new Derive1(); Base *b2 = new Derive2(); b1->print(); b2->print(); delete b1; delete b2; return 0; }
纯虚函数和多继承
多继承带来了一些争议,但是接口继承可以说是一种毫无争议的运用。绝大多数面向对象的语言都不支持多继承,但是绝大数面向对象语言都支持接口的概念。C++中没有接口的概念,但是可以通过纯虚函数实现接口。
接口中只有函数原型定义,没有任何数据定义。
多重继承接口不会带来二义性和复杂性问题。接口类只是一个功能声明,并不是功能实现,子类需要根据功能说明定义功能实现。
注意: 除了析构函数外,其他的都要声明成纯虚函数
纯虚析构函数
纯虚析构在C++中是合法的,但是在使用的时候有一个额外的限制,必须为纯虚析构提供一个函数体。那么问题是,如果给纯虚析构提供函数体的情况下,怎么还能称为纯虚析构函数呢?纯虚析构函数和非纯虚析构函数之间的唯一不同之处在于纯虚析构函数使得基类是抽象类,不能创建对象。
#include <iostream> using namespace std; class Base { public: virtual ~Base() = 0; }; Base::~Base() {} class Derive1 : public Base { public: }; int main(int argc, char* argv[]) { // Base b; Base *b1 = new Derive1(); return 0; }
虚函数和纯虚函数、虚析构和纯虚析构
- 虚函数:由virtual修饰的有函数体的函数。
- 纯虚函数:virtual修饰,函数尾部是=0,没有函数体,所在类为抽象类。
- 虚析构:修饰类中的析构函数。
- 纯虚析构:析构函数后面加=0;必须在类外实现析构函数体。
重载、重写、重定义
重载:
- 同一个作用域
- 参数个数,参数顺序,参数类型不同
- 与函数返回值没有关系
- const也可以作为重载条件
int func(){} int func(int a){} int func(int a, int b){}
重定义 - 隐藏父类函数
- 有继承
- 子类重新定义父类的同名成员(非虚函数)
class Base { public: void func(){} void func(int, int){} }; class Derive : public Base { public: void func(){} }
重写 - 覆盖父类函数
- 有继承
- 子类重写父类中的虚函数
- 函数的返回值,函数的名字,函数的参数,必须和基类中的虚函数一致
class Base { public: virtual void func(){} virtual void func(int, int){} }; class Derive : public Base { public: void func() override{} void func(int, int) override{} }