C++基础学习七
多态
多态基本概念
多态是面向对象程序设计语言中数据抽象和继承之外的第三个基本特征。
多态性(polymorphism)提供接口与具体实现之间的另一层隔离,从而将”what”和”how”分离开来。多态性改善了代码的可读性和组织性,同时也使创建的程序具有可扩展性,项目不仅在最初创建时期可以扩展,而且当项目在需要有新的功能时也能扩展。
c++支持编译时多态(静态多态)和运行时多态(动态多态),运算符重载和函数重载就是编译时多态,而派生类和虚函数实现运行时多态。
静态多态和动态多态的区别就是函数地址是早绑定(静态联编)还是晚绑定(动态联编)。如果函数的调用,在编译阶段就可以确定函数的调用地址,并产生代码,就是静态多态(编译时多态),就是说地址是早绑定的。而如果函数的调用地址不能编译不能在编译期间确定,而需要在运行时才能决定,这这就属于晚绑定(动态多态,运行时多态)。
向上类型转换及问题
对象可以作为自己的类或者作为它的基类的对象来使用。还能通过基类的地址来操作它。取一个对象的地址(指针或引用),并将其作为基类的地址来处理,这种称为向上类型转换。
也就是说:父类引用或指针可以指向子类对象,通过父类指针或引用来操作子类对象。
- 问题抛出
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。
- 问题解决思路
解决这个问题,我们需要了解下绑定(捆绑,binding)概念。
把函数体与函数调用相联系称为绑定(捆绑,binding)
当绑定在程序运行之前(由编译器和连接器)完成时,称为早绑定(early binding).C语言中只有一种函数调用方式,就是早绑定。
上面的问题就是由于早绑定引起的,因为编译器在只有Animal地址时并不知道要调用的正确函数。编译是根据指向对象的指针或引用的类型来选择函数调用。这个时候由于DoBussiness的参数类型是Animal&,编译器确定了应该调用的speak是Animal::speak的,而不是真正传入的对象Dog::speak。
解决方法就是迟绑定(迟捆绑,动态绑定,运行时绑定,late binding),意味着绑定要根据对象的实际类型,发生在运行。
C++语言要实现这种动态绑定,必须有某种机制来确定运行时对象的类型并调用合适的成员函数。对于一种编译语言,编译器并不知道实际的对象类型(编译器并不知道Animal类型的指针或引用指向的实际的对象类型)。
问题解决方案(虚函数,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(){ //virtual 建议写上可读性强 ,可以省略不写
cout << "小狗在唱歌..." << endl;
}
};
void DoBussiness(Animal& animal){
animal.speak();
}
void test(){
Dog dog;
DoBussiness(dog);
}
C++如何实现动态绑定
我们看看编译器如何处理虚函数。当编译器发现我们的类中有虚函数的时候,编译器会创建一张虚函数表,把虚函数的函数入口地址放到虚函数表中,并且在类中秘密增加一个指针,这个指针就是vpointer(缩写vptr),这个指针是指向对象的虚函数表。在多态调用的时候,根据vptr指针,找到虚函数表来实现动态绑定。
class A{
public:
virtual void func1(){} //不加virtual对象大小为1 ,加了后变成了函数指针 ,
virtual void func2(){}
};
//B类为空,那么大小应该是1字节,实际情况是这样吗?
class B : public A{};
void test(){
cout << "A size:" << sizeof(A) << endl;// 4+4 字节 vfptr指针
cout << "B size:" << sizeof(B) << endl;
}
在编译阶段,编译器秘密增加了一个vptr指针,但是此时vptr指针并没有初始化指向虚函数表(vtable),什么时候vptr才会指向虚函数表?在对象构建的时候,也就是在对象初始化调用构造函数的时候。编译器首先默认会在我们所编写的每一个构造函数中,增加一些vptr指针初始化的代码。如果没有提供构造函数,编译器会提供默认的构造函数,那么就会在默认构造函数里做此项工作,初始化vptr指针,使之指向本对象的虚函数表。
多态的成立条件:
- 有继承
- 子类重写父类虚函数函数
a) 返回值,函数名字,函数参数,必须和父类完全一致(析构函数除外)
b) 子类中virtual关键字可写可不写,建议写 - 类型兼容,父类指针,父类引用 指向 子类对象
抽象基类和纯虚函数(pure virtual function)
- 纯虚函数使用关键字virtual,并在其后面加上=0。如果试图去实例化一个抽象类,编译器则会阻止这种操作。
- 当继承一个抽象类的时候,必须实现所有的纯虚函数,否则由抽象类派生的类也是一个抽象类。
- Virtual void fun() = 0;告诉编译器在vtable中为函数保留一个位置,但在这个特定位置不放地址。
建立公共接口目的是为了将子类公共的操作抽象出来,可以通过一个公共接口来操纵一组类,且这个公共接口不需要事先(或者不需要完全实现)。可以创建一个公共类.
纯虚函数和多继承
多继承带来了一些争议,但是接口继承可以说一种毫无争议的运用了。
绝大数面向对象语言都不支持多继承,但是绝大数面向对象对象语言都支持接口的概念,c++中没有接口的概念,但是可以通过纯虚函数实现接口。
接口类中只有函数原型定义,没有任何数据定义。
多重继承接口不会带来二义性和复杂性问题。接口类只是一个功能声明,并不是功能实现,子类需要根据功能说明定义功能实现。
注意:除了析构函数外,其他声明都是纯虚函数。
虚析构函数
虚析构函数作用
虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。
因为子类析构函数不会调用,导致释放不干净 ,所以要用虚析构
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();
}
纯虚析构函数
纯虚析构函数和非纯析构函数之间唯一的不同之处在于纯虚析构函数使得基类是抽象类,不能创建基类的对象
//非纯虚析构函数
class A{
public:
virtual ~A();
};
A::~A(){}
//纯析构函数 ,也属于抽象类,不能实例化了
class B{
public:
virtual ~B() = 0;//类内声明 ,类外实现
};
B::~B(){}//类外实现
void test(){
A a; //A类不是抽象类,可以实例化对象
B b; //B类是抽象类,不可以实例化对象
}
如果类的目的不是为了实现多态,作为基类来使用,就不要声明虚析构函数,反之,则应该为类声明虚析构函数
重写 重载 重定义
- 重载,同一作用域的同名函数
1.同一个作用域
2.参数个数,参数顺序,参数类型不同
3.和函数返回值,没有关系
4.const也可以作为重载条件 //do(const Teacher& t){} do(Teacher& t) - 重定义(隐藏)
1.有继承
2.子类(派生类)重新定义父类(基类)的同名成员(非virtual函数) - 重写(覆盖)
1.有继承
2.子类(派生类)重写父类(基类)的virtual函数
3.函数返回值,函数名字,函数参数,必须和基类中的虚函数一致
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(){}
};
指向类成员的指针
指向成员变量的指针
定义格式
<数据类型> <类名>::*<指针名>
例如: 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;
}
指向静态成员的指针
- 指向类静态数据成员的指针
指向静态数据成员的指针的定义和使用与普通指针相同,在定义时无须和类相关联,在使用时也无须和具体的对象相关联。 - 指向类静态成员函数的指针
指向静态成员函数的指针和普通指针相同,在定义时无须和类相关联,在使用时也无须和具体的对象相关联·
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();
}