目录
- 多态
- 构造函数和析构函数存在多态吗?
- 虚函数表
- 虚析构函数
- 纯虚函数和抽象类
- 运行时多态和编译时多态的区别
- 继承
- 设计实例
- 指针对象和普通对象的区别
- 正确初始化派生类方式
- 继承和赋值的兼容规则
- protected 和 private 继承
- 基类与派生类的指针强制转换
- 如何用C实现C++的三大特性
- C语言实现封装性
- C语言实现继承
- C语言实现多态
多态
在类的定义中,前面有 virtual 关键字的成员函数称为虚函数;
virtual 关键字只用在类定义里的函数声明中,写函数体时不用。
「派生类的指针」可以赋给「基类指针」;
通过基类指针调用基类和派生类中的同名「虚函数」时:
若该指针指向一个基类的对象,那么被调用是 基类的虚函数;
若该指针指向一个派生类的对象,那么被调用 的是派生类的虚函数。
调用哪个虚函数,取决于指针对象指向哪种类型的对象。
派生类的对象可以赋给基类「引用」
通过基类引用调用基类和派生类中的同名「虚函数」时:
若该引用引用的是一个基类的对象,那么被调 用是基类的虚函数;
若该引用引用的是一个派生类的对象,那么被 调用的是派生类的虚函数。
调用哪个虚函数,取决于引用的对象是哪种类型的对象。
在面向对象的程序设计中使用「多态」,能够增强程序的可扩充性,即程序需要修改或增加功能的时候,需要改动和增加的代码较少。
this 指针的作用就是指向成员函数所作用的对象, 所以非静态成员函数中可以直接使用 this 来代表指向该函数作用的对象的指针。
pBase 指针对象指向的是派生类对象,派生类里没有 fun1 成员函数,所以就会调用基类的 fun1 成员函数,在Base::fun1() 成员函数体里执行 this->fun2() 时,实际上指向的是派生类对象的 fun2 成员函数。
构造函数和析构函数存在多态吗?
在构造函数和析构函数中调用「虚函数」,不是多态。
一般不建议在构造函数或者析构函数中调用虚函数,因为在构造函数和析构函数中调用虚函数不会呈现多态性。
原因是啥呢?你想啊,在构造基类调用基类的构造函数时,派生类的部分还没有构造,怎么可能能用虚函数实现动态绑定派生生类对象呢,所以构造B基类部分的时候,调用的基类的函数bar;
对于foo函数不是虚函数不会有动态绑定,所以调用的基类部分;
对于第三个bar调用,是虚函数,实现动态绑定,所以调用的是派生类部分。
同样的道理,当调用继承层次中某一层次的类的析构函数时,往往意味着其派生类部分已经析构掉,所以也不会呈现出多态。
编译时即可确定,调用的函数是自己的类或基类中定义的函数,不会等到运行时才决定调用自己的还是派生类的函数。
虚函数表
每一个有「虚函数」的类(或有虚函数的类的派生类)都有一个「虚函数表」,该类的任何对象中都放着虚函数表的指针。「虚函数表」中列出了该类的「虚函数」地址。
可以发现有虚函数的类,多出了 8 个字节,在 64 位机子上指针类型大小正好是 8 个字节,多出来的 8 个字节就是用来放「虚函数表」的地址。
多态的函数调用语句被编译成一系列根据基类指针所指向的(或基类引用所引用的)对象中存放的虚函数表的地址,在虚函数表中查找虚函数地址,并调用虚函数的指令。
虚函数表的指针」指向的是「虚函数表」,「虚函数表」里存放的是类里的「虚函数」地址,那么在调用过程中,就能实现多态的特性。
虚析构函数
析构函数是在删除对象或退出程序的时候,自动调用的函数,其目的是做一些资源释放。
那么在多态的情景下,通过基类的指针删除派生类对象时,通常情况下只调用基类的析构函数,这就会存在派生类对象的析构函数没有调用到,存在资源泄露的情况。
解决办法:把基类的析构函数声明为virtual
派生类的析构函数可以 virtual 不进行声明;
通过基类的指针删除派生类对象时,首先调用派生类的析构函数,然后调用基类的析构函数,还是遵循「先构造,后虚构」的规则。
所以要养成好习惯:
一个类如果定义了虚函数,则应该将析构函数也定义成虚函数;
或者,一个类打算作为基类使用,也应该将析构函数定义成虚函数。
注意:构造函数不能定义成虚构造函数
在构造基类调用基类的构造函数时,派生类的部分还没有构造,怎么可能能用虚函数实现动态绑定派生生类对象呢,所以构造B基类部分的时候,调用的基类的函数bar;
纯虚函数和抽象类
纯虚函数:没有函数体的虚函数。
包含纯虚函数的类叫抽象类
抽象类只能作为基类来派生新类使用,不能创建抽象类的对象
抽象类的指针和引用可以指向由抽象类派生出来的类的对象
运行时多态和编译时多态的区别
编译时的多态,是指参数列表的不同, 来区分不同的函数, 在编译后, 就自动变成两个不同的函数。
运行时多态:用到的是后期绑定的技术, 在程序运行前不知道,会调用那个方法, 而到运行时, 通过运算程序,动态的算出被调用的地址. 运行时多态,也就是动态绑定,是指在执行期间(而非编译期间)判断所引用对象的实际类型,根据实际类型判断并调用相应的属性和方法
继承
派生类是通过对基类进行修改和扩充得到的,在派生类中,可以扩充新的成员变量和成员函数。
派生类拥有基类的全部成员函数和成员变量,不论是private、protected、public。需要注意的是:在派生类的各个成员函数中,不能访问基类的 private 成员。
在派生类对象中,包含着基类对象,而且基类对象的存储位置位于派生类对象新增的成员变量之前,相当于基类对象是头部。派生类对象的大小 = 基类对象成员变量的大小 + 派生类对象自己的成员变量的大小
设计实例
假设要写一个小区养狗管理系统:
需要写一个「主人」类。
需要些一个「狗」类。
假定狗只有一个主人,但是一个主人可以最多有 10 条狗,应该如何设计和使用「主人」类 和「狗」类呢?我们先看看下面几个例子。
为狗类设一个主人类的对象指针;
为主人类设一个狗类的对象指针数组。
class CDog;
class CMaster // 主人类
{CDog * pDogs[10]; // 狗类的对象指针数组
};class CDog // 狗类
{CMaster * pm; // 主人类的对象指针
};
因为相当于狗和主人是独立的,然后通过指针的作用,使得狗是可以指向一个主人,主人也可以同时指向属于自己的 10 个狗,这样会更灵活。
指针对象和普通对象的区别
如果不用指针对象,生成 A 对象的同时也会构造 B 对象。用指针就不会这样,效率和内存都是有好处的。
class Car
{Engine engine; // 成员对象Wing * wing; // 成员指针对象
};
定义一辆汽车,所有的汽车都有 engine,但不一定都有 wing 这样对于没有 wing 的汽车,wing 只占一个指针,判断起来也很方便。
空间上讲,用指针可以节省空间,免于构造 B 对象,而是只在对象中开辟了一个指针,而不是开辟了一个对象 B 的大小。
效率上讲,使用指针适合复用。对象 B 不但 A 对象能访问,其他需要用它的对象也可以使用。
指针对象可以使用多态的特性,基类的指针可以指向派生链的任意一个派生类。
指针对象,需要用它的时候,才需要去实例化它,但是在不使用的时候,需要手动回收指针对象的资源。
正确初始化派生类方式
class Bug {
private :int nLegs; int nColor;
public:int nType;Bug (int legs, int color);void PrintBug (){ };
};Bug::Bug( int legs, int color)
{nLegs = legs;nColor = color;
}
class FlyBug : public Bug // FlyBug 是Bug 的派生类
{int nWings;
public:FlyBug( int legs,int color, int wings);
};
正确的FlyBug 构造函数:
通过调用基类构造函数来初始化基类,在执行一个派生类的构造函数 之前,总是先执行基类的构造函数。所以派生类析构时,会先执行派生类析构函数,再执行基类析构函数。
// 正确的FlyBug 构造函数:
FlyBug::FlyBug ( int legs, int color, int wings):Bug( legs, color)
{nWings = wings;
}
继承和赋值的兼容规则
派生类的对象可以赋值给基类对象
派生类对象可以初始化基类引用
派生类对象的地址可以赋值给基类指针
如果派生方式是 private 或 protected,则上述三条不可行。
protected 和 private 继承
protected 继承时,基类的 public 成员和 protected 成员成为派生类的 protected 成员;
private 继承时,基类的 public 成员成为派生类的 private 成员,基类的 protected 成员成 为派生类的不可访问成员;
派生方式是 private 或 protected,则是无法像 public 派生承方式一样把派生类对象赋值、引用、指针给基类对象。
基类与派生类的指针强制转换
public 派生方式的情况下,派生类对象的指针可以直接赋值给基类指针:
Base *ptrBase = & objDerived;
ptrBase 指向的是一个 Derived 派生类(子类)的对象。
*ptrBase 可以看作一个 Base 基类的对象,访问它的 public 成员直接通过 ptrBase 即可,但不能通过 ptrBase 访问 。objDerived 对象中属于 Derived 派生类而不属于基类的成员。
通过强制指针类型转换,可以把 ptrBase 转换成 Derived 类的指针。
Base * ptrBase = &objDerived;
Derived *ptrDerived = ( Derived * ) ptrBase;
如何用C实现C++的三大特性
C语言实现封装性
构体AchievePackage中有成员变量_a和两个函数指针,在InitStruct函数中被赋予两个函数的地址(函数名即为其地址,也可为&fun1,得到的值一样),故在此处InitStruct函数相当于该结构体的构造函数,既可以初始化其成员变量_a的值,也在对象定义的同时为其函数指针赋值(需显示调用)
C语言实现继承
两个类如果存在继承关系,其子类必定具有父类的相关属性(即变量)和方法(即函数)。
用“组合”去实现一下C语言中的继承:
结构体嵌套:
C语言实现多态
多态是通过父类的指针或引用,调用了一个在父类中是virtual类型的函数,实现动态绑定机制。
若想使用父类的指针/引用调用子类的函数,需要在父类中将其声明为虚函数(virtual),且必须与子类中的函数参数列表相同,返回值也相同。
C++的多态是通过覆盖实现的,即父类的函数被子类覆盖了!
父类的该函数为虚函数,告诉父类的指针/引用,你调用这个函数的时候必须看一看你绑定的对象到底是哪个类的对象,然后去那个类里调用该函数!
//C语言模拟C++的继承与多态typedef void (*FUN)(); //定义一个函数指针来实现对成员函数的继承struct _A //父类
{FUN _fun; //由于C语言中结构体不能包含函数,故只能用函数指针在外面实现int _a;
};struct _B //子类
{_A _a_; //在子类中定义一个基类的对象即可实现对父类的继承int _b;
};void _fA() //父类的同名函数
{printf("_A:_fun()\n");
}
void _fB() //子类的同名函数
{printf("_B:_fun()\n");
}
下面是测试代码:
//C语言模拟继承与多态的测试
_A _a; //定义一个父类对象_a
_B _b; //定义一个子类对象_b
_a._fun = _fA; //父类的对象调用父类的同名函数
_b._a_._fun = _fB; //子类的对象调用子类的同名函数_A* p2 = &_a; //定义一个父类指针指向父类的对象
p2->_fun(); //调用父类的同名函数
p2 = (_A*)&_b; //让父类指针指向子类的对象,由于类型不匹配所以要进行强转
p2->_fun(); //调用子类的同名函数
效果: