C++面向对象编程核心思想:封装、继承、多态与RAII内存管理机制详解
C++以兼容 C(过程式编程)为基础,实现了面向对象编程的编程范式。与面向对象编程相关的三个核心思想分别是封装,继承以及多态。

C++不存在 GC(Garbage Collection)这样的语法机制。对于动态内存的管理,包括如何避免泄露,特别是在引发异常时的情况。RAII 即 Resource Acquisition Is Initialization,它指的是利用类的封装以及对象的自动构造和自动析构的语法机制,从而实现对资源的有效管理,也就是对资源的初始化与释放。
Object-oriented programming 的核心思想在于把类型组织成层次结构,这样就能在代码中直接体现它们之间的关系。继承是一种纵向的“is-a”关系。在继承的层次关系里,利用虚函数来达成多态。同时,在虚函数的语法机制中也实现了较为简单的 RTTI(运行时类型识别)。
RAII 指的是资源获取即初始化。 它是一种 C++ 编程技术。 用于管理资源的获取和释放。 通常与对象的构造和析构函数相关联。 可以确保资源在对象生命周期结束时被正确释放。 避免资源泄漏等问题。 提高程序的可靠性和安全性。
RAII 是 C++语言用于管理资源的一种语法机制,比如堆内存、文件句柄、网络连接套接字等,并且能避免资源泄漏。C++确保在任何情况下,已构造的对象最终都会被销毁,也就是说它的析构函数最终会被调用。简单来说,RAII 的做法是运用一个对象。在这个对象构造的时候,它会获取资源。在对象的生命期内,能够控制对资源的访问,让资源始终保持有效。最后,在对象析构的时候,会释放资源。
C++社群在给技术赋予缩略语和名字方面有着长久且值得自豪的传统。RAII就是其中的一个例子,它的名字既古怪又神秘。RAII 的意思是“资源获取即初始化”,而不是某些人所认为的“初始化即资源获取”。顺便说一句,如果你想要搞怪,那就彻底地怪下去,不然是达不到效果的。
RAII 是一项较为简单的技术,它借助 C++对象生命期的概念来对程序的资源进行控制。RAII 的基本技术原理并不复杂。倘若想要对某个重要资源进行追踪,那就创建一个对象,并且让资源的生命期与对象的生命期相互关联。这样一来,就能够运用 C++复杂且老练的对象管理设施来对资源进行管理。最简单的 RAII 形式是创建一个对象,这个对象的构造函数会获取一份资源,并且其析构函数会释放这份资源。
class Resource {} ;
class ResourceHandle {
public:
创建一个显式的 ResourceHandle,其参数为一个 Resource 类型的指针 aResource
: r_(aResource) {} //获取资源
~ResourceHandle ()
释放资源。
Resource *get()
返回 r_ 。 // 这是在进行访问资源的操作。
private:
ResourceHandle 是一个函数或类的构造函数,它接受一个 const ResourceHandle &类型的参数。
将一个 ResourceHandle 的引用赋值给另一个 ResourceHandle 。
Resource *r_;
} ;
RTTI 指的是运行时类型识别。它是一种在程序运行期间能够确定对象的实际类型的机制。通过 RTTI,程序可以在运行时获取对象的类型信息,并根据这些信息执行相应的操作。例如,可以使用 RTTI 来判断一个对象是否属于某个特定的类,或者获取对象的虚函数表等信息。
看如下范例:
Human 的指针 phuman 指向了一个 Men 类的对象。
Human 的引用 &r 被绑定到派生类对象上。
变量声明时的类型就是这里的静态类型,在编译时静态类型是已知的,比如上面代码中的 phuman 和 r,它们的静态类型分别是 Human 类型指针和 Human 类型引用。
基类的指针或引用指向派生类对象时,会出现一种只有在运行时(执行到相应代码时)才能确定的类型,这种类型称为动态类型。在 C++中,只有当基类存在虚函数,且基类的指针或引用指向派生类对象产生多态时,才会出现动态类型。若不是多态的上下文,那么静态类型与动态类型应该始终是一致的。
在 C++ 的环境里,头文件包含着类的定义,也就是说头文件包含着有关类的结构信息。但是,这些信息仅被编译器使用。在编译完成后,这些信息并未留存下来。因此,在执行时期,无法获取到对象的类信息,诸如类名称、数据成员名称与类型、函数名称与类型等。例如,存在两个类 Figure 和 Circle,它们之间存在继承关系。
若有如下指令﹕
Figure *p;
p = new Circle();
Figure &q = *p;
执行时,p 指向一个对象。然而,当想要得知此对象的类信息时,就会遇到困难。同样地,想要得知 q 所参考对象的类信息,也无法获取到。RTTI(运行时类型识别)的作用就是要解决这个困难,也就是说,在执行过程中,当你想要知道指针所指向或参考到的对象类型时,该对象有能力告诉你。应用场合不同时,所需支持的 RTTI 范围也不同。其中最单纯的 RTTI 包含:
I 类识别包含类名称或者 ID。
继承关系支持执行时期的“往下变换类型”,也就是动态变换类型。
在对象数据库存取上﹐还需要下述RTTI﹕
对象结构包含属性的类型,对象结构包含属性的名称,对象结构包含属性的位置(位置可以是 position 或 offset)。
成员函数表包含着函数的相关信息,包括函数的类型、名称以及参数类型等。
它的目的是帮助对象进行 I/O 操作以及实现持久化。同时,它还能提供调试相关的信息等。
若按照 Bjarne Stroustrup 的建议,C++ 还应当包含更完备的 RTTI 。
I 能得知类所实例化的各对象 。
II 能参考到函数的源代码。
能获取有关类的在线说明。
其实这些信息在 C++ 编译完成时会被丢弃。现在只是期望找到一个办法,能够将这些信息保留到执行期间。然而,如果要提供完整的 RTTI,就会使 C++ 的复杂度大幅提升。
在C++中,最单纯的RTTI使用了两个运算符:
动态转换(dynamic_cast)能够将父类的指针或者引用稳妥地转换为子类的指针或者引用。
typeid:返回指针或者引用所指对象的实际类型。
特别需要注意的是,上述两个运算符需能正常按期望工作,且父类中至少要有一个虚函数,否则这两个运算符工作的结果很可能与预期不同。因为只有存在虚函数,这两个运算符才会使用指针或引用所指对象的类型(new 时的类型)。
Java 能够生成字节码文件,接着由 JVM(Java 虚拟机)进行加载运行,字节码文件中包含类的信息。
在 C++ 程序里,如果一个类含有虚函数,那么这个类就会有一个虚函数表(Virtual Function Table,简称 VFT)。为了实现 RTTI,C++ 会在 VFT 中附加一个指针,这个指针指向一个 typeinfo 对象,而这个 typeinfo 对象里面包含着 RTTI 资料。
该类所实例化出的各个对象,它们都含有一个指针指向 VFT 表。所以,各个对象都能够取出 typeinfo 对象,从而获得 RTTI。例如,
Figure *f1 = new Square();
Figure *f2 = new Square();
获取到指向 f2 的指针所指向的对象的类型信息,这个类型信息被赋值给了 const typeinfo 类型的变量 ty,该类型信息是通过 typeid 操作符获取的。
其中﹐typeid(*f2) 的动作是﹕
I 取得f2所指之对象。
从对象中取出指向 VFT 的指针,然后通过这个指针来获取 VFT 表。
从表中找出指向 typeinfo 对象的指针,然后通过这个指针来获取 typeinfo 对象。
这种 typeinfo 对象含有 RTTI 。通过 f1 这个指针可以取得 typeinfo 对象,通过 f2 这个指针也可以取得 typeinfo 对象,所以 typeid(*f2) 等于 typeid(*f1) 。
C++的多态中,如果父类存在一个虚函数,并且子类对这个虚函数进行了覆盖,那么,在父类指针指向子类对象时,被调用的虚函数就是子类中的虚函数。就如同下面实例里的 vt()函数那样。
可以通过 RTTI 来达成相同的功能,就像下面实例里的 vt2RTTI()函数那样。
如果子类存在一个父类所没有的普通成员函数(非虚函数),那么即便父类指针指向了该子类对象,也无法通过父类指针来调用子类中的这个普通成员函数。那么,倘若想要用父类指针调用子类中的这个普通成员函数,该怎么做呢?
我将这个普通成员函数改写成了虚函数,因为在父类中只要是虚函数,在子类中自然就会是虚函数。然而,如果子类中每增加一个新成员函数,都需要在父类中增加同等的虚函数,这样的解决方案实在让人不太满意。
替代虚函数方案的办法是 RTTI,通过使用 dynamic_cast 进行类型转换。例如下面实例中的 doSomething()函数:
#include
#include
#include
using namespace std;
class Movable
{
public:
virtual void move()=0;
};
class Bus : public Movable
{
public:
virtual void move()
{
cout << "Bus -- move" << endl;
}
void carry()
{
cout << "Bus -- carry" << endl;
}
};
class Tank :public Movable
{
public:
virtual void move()
{
cout << "Tank -- move" << endl;
}
void fire()
{
cout << "Tank -- fire" << endl;
}
};
void vt2RTTI(Movable *obj)
{
如果 obj 的类型是 Bus 的类型,那么即使去掉 move()的虚函数声明,也无法进行 RTTI 操作。
{
Bus *bus = dynamic_cast(obj);//注意这两个语句
bus->move();
}
如果 obj 的类型是 Tank 的类型
{
Tank *tank=dynamic_cast(obj);//注意这两个语句
tank->move();
}
}
void vt(Movable *obj)
{
obj->move();
}
执行某些操作(可移动对象 *obj)
{
if(typeid(*obj) == typeid(Bus)) // 如果去掉move()的虚函数声明也无法RTTI
{
Bus *bus = dynamic_cast(obj);//注意这两个语句
bus->carry();
}
if(typeid(*obj) == typeid(Tank))
{
Tank *tank=dynamic_cast(obj);//注意这两个语句
tank->fire();
}
}
int main(void)
{
Bus b;
Tank t;
cout<
vt()和 vt2RTTI()都能实现多态化。使用复选方法的 vt2RTTI()常常会导致违反“开放╱封闭原则”。而使用虚函数方法的 vt()则能够符合这个原则。
当 Movable 类体系派生出子类时,vt2RTTI()函数的内容需要增加一个 if 指令。这样就违反了“开放╱封闭原则”。
想一想,如果 C++ 没有提供 RTTI,那么程序员就没有别的选择,必须使用虚函数来支持 move() 函数的多态,并且要使用 vt() 函数。
void vt(Movable *obj)
{
obj->move();
}
这样一来,Movable 类体系能够随时派生出新的类,并且无需对 vt()函数进行修正。也就是说,Movable 体系拥有一个稳定的接口,vt()函数使用这个接口,从而使得 vt()函数也保持稳定,不会随着 Movable 类体系的扩充而发生变动。这体现了它封闭的一面。然而,这个稳定的接口并没有限制 Movable 体系的成长,这又体现了它开放的一面。因为符合“开放╱封闭”原则,所以软件的结构会更有弹性,并且更容易随着环境的变化而不断成长。
ref
的RTTI观念、使用场合及实现来源.html
-End-