一、多重继承的二义性问题
举例:
#include <iostream>
using namespace std;
class BaseA {
public: void fun() { cout << "A.fun" << endl; }
};
class BaseB {
public:
void fun() { cout << "B.fun" << endl; }
void tun() { cout << "B.tun" << endl; }
};
class Derived :public BaseA, public BaseB {
public:
void tun() { cout << "D.tun" << endl; }
void hun() { fun(); } //此处调用出现二义性,编译无法通过
};
int main() {
Derived d, * p = &d; d.hun();
return 0;
}
类名限定
void hun() { BaseA::fun(); } //改写上述代码的第14行,用BaseA类名限定调用的函数
d.BaseB::fun(); //派生类对象调用基类同名函数时,使用类名限定
p->BaseB::fun(); //派生类指针调用基类同名函数时,使用类名限定
名字支配规则
如果存在两个或多个包含关系的作用域,外层声明了一个名字,而内层没有再次声明相同的名字,则外层名字在内层可见;如果在内层声明了相同的名字,则外层名字在内层不可见——隐藏(屏蔽)规则。
在类的派生层次结构中,基类的成员和派生类新增的成员都具有类作用域,二者的作用域不同:基类在外层,派生类在内层。如果派生类声明了一个和基类成员同名的新成员,则派生类的新成员就会屏蔽基类的同名成员,直接使用成员名只能访问到派生类新增的成员。(如需使用从基类继承的成员,应当使用基类名限定)
#include <iostream>
using namespace std;
class Base {
public:
void fun() { cout << "A.fun" << endl; }
Base(int x = 1, int y = 2) :x(x), y(y) {}
int x, y;
};
class Derived :public Base{
public:
void tun() { cout << "D.tun" << endl; }
void fun() { cout << "D.fun" << endl; }
Derived(int x = 0) :x(x) {}
int x;
};
int main() {
Derived d, * p = &d;
d.fun(); //输出的结果为 D.fun
cout << p->x << " " << p->y << " " << p->Base::x << endl; //输出为 0 2 1
d.Base::fun(); //输出为 A.fun
}
二、虚基类
虚基类的使用目的:在继承间接基类时只保留一份成员。
声明虚基类需要在派生类定义时,指定继承方式的时候声明,只需要在访问标号(public、protected、private继承方式)前加上virtual关键字。注意:为了保证虚基类在派生类中只继承依次,应当在该基类的所有直接派生类中声明为虚基类,否则仍会出现多次继承。
派生类不仅要负责对直接基类进行初始化,还要负责对虚基类初始化;若多重继承中没有虚基类,则派生类只需要对间接基类进行初始化,而对基类的初始化由各个间接基类完成(会因此产生多个基类的副本保存在各个间接基类中)。
#include <iostream>
using namespace std;
class Base {
public:
Base(int n) { nv = n; cout << "Member of Base" << endl; }
void fun() { cout << "fun of Base" << endl; }
private:
int nv;
};
class A:virtual public Base { //声明Base为虚基类,作为间接基类都需要使用virtual关键字
public:
A(int a) :Base(a) { cout << "Member of A" << endl; }
private:
int na;
};
class B :virtual public Base { //声明Base为虚基类,作为间接基类都需要使用virtual关键字
public:
B(int b) :Base(b) { cout << "Member of B" << endl; }
private:
int nb;
};
class Derived :public A, public B {
public:
Derived(int n) :Base(n), A(n), B(n) { cout << "Member of Derived" << endl; }
//派生类的构造函数初始化列表,先调用基类Base的构造函数,再依次调用间接基类A、B的构造函数
//由于虚基类Base中没有默认构造函数(允许无参构造),所以从Base类继承的所有派生类的构造函数初始化表中都需要显式调用基类(包括间接基类)的构造函数,完成初始化
private:
int nd;
};
int main() {
Derived de(3); de.fun();//不会产生二义性
return 0;
}
关于虚基类的说明:
- 一个类在一个类族中既可以被用作虚基类,也可以被用作非虚基类;
- 如果虚基类中没有默认构造函数(或参数全部为默认参数的),则在派生类中必须显式声明构造函数,并在初始化列表中列出对虚基类构造函数的调用;
- 在一个成员初始化列表中同时出现对虚基类和非虚基类构造函数的调用时,虚基类的构造函数先于非虚基类的构造函数执行。
三、虚函数
虚函数概念:被virtual关键字修饰的成员函数,即为虚函数,其作用就是实现多态性。
虚函数使用说明:
-
虚函数只能是类中的成员函数,且不能是静态的;
-
virtual关键字只能在类体中使用,即便虚函数的实现在类体外定义,也不能带上virtual关键字;
-
当在派生类中定义了一个与基类虚函数同名的成员函数时,只要该函数的参数个数、类型、顺序以及返回类型与基类中的完全一致,则派生类的这个成员函数无论是否使用virtual关键字,它都将自动成为虚函数;
-
利用虚函数,可以在基类和派生类中使用相同的函数名定义函数的不同实现,达到「一个接口,多种方式」的目的。当基类指针或引用对虚函数进行访问时,系统将根据运行时指针(或引用)所指向(或引用)的实际对象来确定调用的虚函数版本;
-
使用虚函数并不一定产生多态性,也不一定使用动态联编。如:在调用中对虚函数使用类名限定,可以强制C++对该函数使用静态联编。
-
在派生类中,当一个指向基类成员函数的指针指向一个虚函数,并且通过指向对象的基类指针(或引用)访问这个虚函数时,仍将发生多态性。
#include <iostream> using namespace std; class Base { public: virtual void print() { cout << "Base-print" << endl; } }; class Derived :public Base { public: void print() { cout << "Derived-print" << endl; } }; void display(Base* p, void(Base::* pf)()) { (p->*pf)(); } int main() { Derived d; Base b; display(&d, &Base::print); //输出Derived-print display(&b, &Base::print); //输出Base-print return 0; }
-
使用虚函数,系统要增加一定的空间开销存储虚函数表,但是系统在进行动态联编时的时间开销时很少的,因此,虚函数实现的多态性是高效的。
虚函数实现多态的条件(同时满足)
- 类之间的继承关系满足赋值兼容规则;
- 改写了同名的虚函数,但函数形参、返回类型要保持一致;
- 根据赋值兼容规则使用指针(或引用);
- 使用基类指针(或引用)访问虚函数;
- 把指针(或引用)作为函数参数,这个函数不一定是类的成员函数,可以是普通函数,并且可以重载。
虚析构函数
派生类的对象从内存中撤销时一般先调用派生类的析构函数,然后再调用基类的析构函数。但如果用new运算符建立了派生类对象,且定义了一个基类的指针指向这个对象,那么当用delete运算符撤销对象时,系统会只执行基类的析构函数,而不执行派生类的析构函数,因而也无法对派生类对象进行真正的撤销清理工作。
如果希望delete关键字作用于基类指针时,也执行派生类的析构函数,则需要将基类的析构函数声明为虚函数。
如果将基类的析构函数声明为虚函数,则由该基类所派生的所有派生类的析构函数也都自动成为虚函数,即使派生类的析构函数与基类的析构函数名字不相同。
C++支持虚析构函数,但是不支持虚构造函数,即构造函数不能声明为虚函数!
纯虚函数
许多情况下,不能在基类中为虚函数给出一个有意义的定义,这时可以将它说明为纯虚函数,将具体定义留给派生类去做。纯虚函数的定义形式为:virtual 返回类型 函数名(形式参数列表) = 0;
包含有纯虚函数的类称为抽象类,一个抽象类只能作为基类来派生新类,因此又称为抽象基类,抽象类不能定义对象(实体)。
四、多态性
多态的含义:指同一操作作用于不同的对象时产生不同的结果。
-
重载多态——函数重载、运算符重载
-
强制多态——也称类型转换
C++的基本数据类型之间转换规则:
char→short→int→unsigned→long→unsigned→float→double→long double
可以在表达式中使用3中强制类型转换表达式:
static_cast<T>(E)
或T(E)
或(T)E
其中E代表运算表达式(获得一个值),T代表一个类型标识符。强制多态使得类型检查复杂化,尤其在允许重载的情况下,会导致无法消解的二义性。 -
类型参数化多态——模板(函数模板、类模板)
-
包含多态——使用虚函数
至少含有一个虚函数的类称为多态类,虚函数使得程序能够以动态联编的方式达到执行结果的多态化。这种多态使用的背景是:派生类继承基类的所有操作,或者说,基类的操作能被用于操作派生类的对象,当基类的操作不能适应派生类时,派生类就需要重载基类的操作;其表现为C++允许用基类的指针接收派生类的地址或使用基类的引用绑定派生类的对象。
静态联编和动态联编
联编:将模块或者函数合并在一起生成可执行代码的处理过程,同时对每个模块或者函数分配内存地址,并且对外部访问也分配正确的内存地址。
静态联编:在编译阶段就将函数实现和函数调用绑定。静态联编在编译阶段就必须了解所有函数的或模块执行所需要的信息,它对函数的选择是基于指向对象的指针(或者引用)的类型。C语言中,所有的联编都是静态联编,C++中一般情况下的联编也是静态联编。
动态联编:在程序运行的时候才进行函数实现和函数调用的绑定称之为动态联编(dynamic binding)
#include <iostream>
#define PI 3.14159265
using namespace std;
class Point {
public:
Point(double x = 0, double y = 0) :x(x), y(y) {}
double area_static() { return 0; } //不是虚函数,只会在编译期绑定,形成静态联编
virtual double area_dynamic() { return 0; } //用虚函数声明,则编译时只做赋值兼容的合法性检查,而不做绑定
private:
double x, y;
};
class Circle :public Point {
public:
Circle(double r = 1.0) :r(r) {} //由于基类中的构造函数非必须显式传参,所以系统会自动调用基类带默认参数的构造函数
Circle(double x, double y, double r=1.0) :Point(x, y), r(r) {} //重载一个可传坐标点、半径值参数的构造函数
double area_static() { return PI * r * r; } //静态联编
double area_dynamic() { return PI * r * r; } //动态联编(仍为虚函数),为使可读性更好,可在不缺省virutal关键字
private:
double r;
};
int main() {
Point o(2.5, 2.5); Circle c(2.5, 2.5, 1);
Point* po = &o, * pc = &c, & y_c = c;
//下面五个全部为静态联编,无论指针指向的是基类还是派生类,由于指针类型为基类类型,且调用的不是虚函数,则统一绑定为基类中的函数
cout << "Point area =" << o.area_static() << endl; //值为0
cout << "Circle area=" << c.area_static() << endl; //值为3.14159
cout << "the o area from po:" << po->area_static() << endl; //值为0
cout << "the c area from pc:" << pc->area_static() << endl; //值为0
cout << "the c area from cite y_c:" << y_c.area_static() << endl; //值为0
//下面三个为动态联编,有指针(或引用)、虚函数,则所调用的虚函数会在运行时通过vptr指针找到虚函数表 ,根据指针指向的实际对象(而非指针类型)来判定调用谁的函数
cout << "the o area from po:" << po->area_dynamic() << endl; //值为0
cout << "the c area from pc:" << pc->area_dynamic() << endl; //值为3.14159
cout << "the c area from cite y_c:" << y_c.area_dynamic() << endl; //值为3.14159
//强制使用静态联编
cout << "the c area calculated by Point::area_():" << pc->Point::area_dynamic() << endl; //值为0
return 0;
}
动态联编与虚函数
-
当调用虚函数时,先通过vptr指针(编译虚函数时,编译器会为类自动生成一个指向虚函数表的vptr指针)找到虚函数表,然后再找出虚函数的真正地址,再调用它
-
派生类能继承基类的虚函数表,而且只要是和基类同名(参数也相同)的成员函数,无论是否使用virtual声明,它们都自动成为虚函数。如果派生类没有改写继承基类的虚函数,则函数指针调用基类的虚函数;如果派生类改写了基类的虚函数,编译器将重新为派生类的虚函数建立地址,函数指针会调用改写后的虚函数。