C/C++面向对象编程之继承

目录:

C/C++面向对象编程之封装
C/C++面向对象编程之继承
C/C++面向对象编程之多态

1、什么是继承与派生

继承(Inheritance) 可以理解为一个类从另一个类获取成员变量和成员函数的过程。例如类 B 继承于类 A,那么 B 就拥有 A 的成员变量和成员函数。
派生(Derive) 和继承是一个概念,只是站的角度不同。继承是儿子接收父亲的产业,派生是父亲把产业传承给儿子。
被继承的类称为父类或基类,继承的类称为子类或派生类。“子类”和“父类”通常放在一起称呼,“基类”和“派生类”通常放在一起称呼。
派生类除了拥有基类的成员,还可以定义自己的新成员,以增强类的功能。
以下是两种典型的使用继承的场景:

  1. 当你创建的新类与现有的类相似,只是多出若干成员变量或成员函数时,可以使用继承,这样不但会减少代码量,而且新类会拥有基类的所有功能。
  2. 当你需要创建多个类,它们拥有很多相似的成员变量或成员函数时,也可以使用继承。可以将这些类的共同成员提取出来,定义为基类,然后从基类继承,既可以节省代码,也方便后续修改成员

2、继承时的对象内存模型

以C++为例:
没有继承时对象内存的分布情况。这时的内存模型很简单,成员变量和成员函数会分开存储:

  • 对象的内存中只包含成员变量,存储在栈区或堆区(使用 new 创建对象);
  • 成员函数与对象内存分离,存储在代码区。

有继承关系时,派生类的内存模型可以看成是基类成员变量和新增成员变量的总和,而所有成员函数仍然存储在另外一个区域——代码区,由所有对象共享。
请看下面的代码:

#include <cstdio>
//基类A
class A{
public:
    A(int a, int b);
public:
    void display();
protected:
    int m_a;
    int m_b;
};
A::A(int a, int b): m_a(a), m_b(b){}
void A::display(){
    printf("m_a=%d, m_b=%d\n", m_a, m_b);
}

//派生类B
class B: public A{
public:
    B(int a, int b, int c);
    void display();
private:
    int m_c;
};
B::B(int a, int b, int c): A(a, b), m_c(c){ }
void B::display(){
    printf("m_a=%d, m_b=%d, m_c=%d\n", m_a, m_b, m_c);
}

int main(){
    A obj_a(99, 10);
    B obj_b(84, 23, 95);
    obj_a.display();
    obj_b.display();
	obj_b.A::display();//访问基类的同名函数
    return 0;
}

/*输出:
m_a=99, m_b=10
m_a=84, m_b=23, m_c=95
m_a=84, m_b=23
*/

注意到派生类中B的成员和基类A中的成员有重名函数display(),这在C++里叫名字遮蔽,所谓遮蔽,就是在派生类中使用该成员(包括在定义派生类时使用,也包括通过派生类对象访问该成员)时,实际上使用的是派生类新增的成员,而不是从基类继承来的。
派生类的对象如果访问一个名字在派生类的成员内无法找到,编译器会智能的继续到基类作用域中查找该名字的定义;如果派生类的对象想访问基类的同名名字,需要加上类名和域解析符。
内存模型:
obj_a 是基类对象,obj_b 是派生类对象。假设 obj_a 的起始地址为 0X1000,那么它的内存分布如下图所示:
在这里插入图片描述
假设 obj_b 的起始地址为 0X1100,那么它的内存分布如下图所示:

在这里插入图片描述
在派生类的对象模型中,会包含所有基类的成员变量。这种设计方案的优点是访问效率高,能够在派生类对象中直接访问基类变量,无需经过好几层间接计算;所有成员函数仍然存储在另外一个区域——代码区,由所有对象共享。
以C语言为例:
在C语言的世界里里结构体不能包含函数体,每个功能函数的名字必须不同,所以自然没有像C++那样函数重载的功能和继承时的名字遮蔽问题。C语言里结构体只能包含变量,在派生类的对象模型中,会包含所有基类的成员变量。
C语言没有C++那样智能,C语言如果想访问基类成员,必须加上基类的对象名。
请看下面的代码:

#include <stdio.h>
//基类A
struct A{
    int m_a;
    int m_b;
    void (*display)(struct A *a);
};
void a_display(struct A *a){
    printf("m_a=%d, m_b=%d\n", a->m_a, a->m_b);
}
//派生类B
struct B{
	struct A parent;
    int m_c;
    void (*display)(struct B *b);
};
void b_display(struct B *b){
    printf("m_a=%d, m_b=%d, m_c=%d\n", b->parent.m_a, b->parent.m_b, b->m_c);
}

int main(){
    struct A obj_a={99, 10,a_display};
    struct B obj_b={99, 10, a_display,95,b_display};
    obj_a.display(&obj_a);
    obj_b.display(&obj_b);
	obj_b.parent.display(&obj_a);
    return 0;
}
/*输出:
m_a=99, m_b=10
m_a=99, m_b=10, m_c=95
m_a=99, m_b=10
*/

C语言的内存模型比较简单,去除掉C++的函数体,就是C语言的内存模型。

3、C语言运用继承的思想

在C语言中,可以利用“结构在内存中的布局与结构的声明具有一致的顺序”这一事实实现继承。利用继承的思想,C语言很容易就能实现较为复杂的代码。
RT-Thread 采用内核对象管理系统来访问 / 管理所有内核对象,内核对象包含了内核中绝大部分设施,这些内核对象可以是静态分配的静态对象,也可以是从系统内存堆中分配的动态对象。
RT-Thread 内核对象包括:线程,信号量,互斥量,事件,邮箱,消息队列和定时器,内存池,设备驱动等。对象容器中包含了每类内核对象的信息,包括对象类型,大小等。对象容器给每类内核对象分配了一个链表,所有的内核对象都被链接到该链表上,如图 RT-Thread 的内核对象容器及链表如下图所示:
在这里插入图片描述
从面向对象的观点,可以认为每一种具体对象是抽象对象的派生,继承了基本对象的属性并在此基础上扩展了与自己相关的属性。下图则显示了 RT-Thread 中各类内核对象的派生和继承关系:
在这里插入图片描述
通过这种内核对象的设计方式,RT-Thread 做到了不依赖于具体的内存分配方式,系统的灵活性得到极大的提高。
RT-Thread的内核架构请看下边的文章:
https://blog.csdn.net/sinat_31039061/article/details/104121771

参考资料:http://c.biancheng.net/cplus/

联系作者:
欢迎关注本人公众号:

在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章