C++面向对象编程核心思想:封装、继承、多态与RAII内存管理机制详解

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

C++面向对象编程核心思想:封装、继承、多态与RAII内存管理机制详解

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-

你可能想看:
« 上一篇
下一篇 »

相关推荐

FreeBasic:强大免费开源的64位basic编译器及应用优势

2025年05月10日

82阅读

脊柱侧弯矫正方法大盘点:运动康复、支具矫正等多种方式

2025年05月09日

87阅读

如何在winForm标题栏去掉标题文字且任务栏图标正常显示

2025年05月09日

86阅读

十一黄金周促销广告词大全,国庆购物狂欢等你来

2025年05月09日

78阅读

关闭快速编辑模式及插入模式,设置控制台指定文字颜色举例

2025年05月09日

63阅读

黄金搭档低糖高钙饮品包装及成分功效全解析,适合人群要知晓

2025年05月09日

91阅读