在  C 语言面向对象编程(一)里说到继承,这里再详细说一下。

C++ 中的继承,从派生类与基类的关系来看(出于对比 C 与 C++,只说公有继承):

  • 派生类内部可以直接使用基类的 public 、protected 成员(包括变量和函数)
  • 使用派生类的对象,可以像访问派生类自己的成员一样访问基类的成员
  • 对于被派生类覆盖的基类的非虚函数,在派生类中可以通过基类名和域作用符(::)来访问
  • 当使用基类指针调用虚函数时,会调用指针指向的实际对象实现的函数,如果该对象未重载该虚函数,则沿继承层次,逐级回溯,直到找到一个实现

上面的几个特点,我们在 C 语言中能否全部实现呢?我觉得可以实现类似的特性,但在使用方法上会有些区别。后面我们一个一个来说,在此之前呢,先说继承的基本实现。

先看 C 语言中通过“包含”模拟实现继承的简单代码框架:

[cpp] view plaincopy print?
  1. struct base{
  2. int a;
  3. };
  4. struct derived{
  5. struct base parent;
  6. int b;
  7. };
  8. struct derived_2{
  9. struct derived parent;
  10. int b;
  11. };
struct base{int a;
};struct derived{struct base parent;int b;
};struct derived_2{struct derived parent;int b;
};

上面的示例只有数据成员,函数成员其实是个指针,可以看作数据成员。 C 中的 struct 没有访问控制,默认都是公有访问(与 java 不同)。

下面是带成员函数的结构体:

[cpp] view plaincopy print?
  1. struct base {
  2. int a;
  3. void (*func1)(struct base *_this);
  4. };
  5. struct derived {
  6. struct base parent;
  7. int b;
  8. void (*func2)(struct derived* _this;
  9. };
struct base {int a;void (*func1)(struct base *_this);
};struct derived {struct base parent;int b;void (*func2)(struct derived* _this;
};

为了像 C++ 中一样通过类实例来访问成员函数,必须将结构体内的函数指针的第一个参数定义为自身的指针,在调用时传入函数指针所属的结构体实例。这是因为 C 语言中不存在像 C++ 中那样的 this 指针,如果我们不显式地通过参数提供,那么在函数内部就无法访问结构体实例的其它成员。

下面是在 c 文件中实现的函数:

[cpp] view plaincopy print?
  1. static void base_func1(struct base *_this)
  2. {
  3. printf("this is base::func1\n");
  4. }
  5. static void derived_func2(struct derived *_this)
  6. {
  7. printf("this is derived::func2\n");
  8. }
static void base_func1(struct base *_this)
{printf("this is base::func1\n");
}
static void derived_func2(struct derived *_this)
{printf("this is derived::func2\n");
}

C++ 的 new 操作符会调用构造函数,对类实例进行初始化。 C 语言中只有 malloc 函数族来分配内存块,我们没有机会来自动初始化结构体的成员,只能自己增加一个函数。如下面这样(略去头文件中的声明语句):

[cpp] view plaincopy print?
  1. struct base * new_base()
  2. {
  3. struct base * b = malloc(sizeof(struct base));
  4. b->a = 0;
  5. b->func1 = base_func1;
  6. return b;
  7. }
struct base * new_base()
{struct base * b = malloc(sizeof(struct base));b->a = 0;b->func1 = base_func1;return b;
}

好的,构造函数有了。通过 new_base() 调用返回的结构体指针,已经可以像类实例一样使用了:

[cpp] view plaincopy print?
  1. struct base * b1 = new_base();
  2. b1->func1(b1);
struct base * b1 = new_base();
b1->func1(b1);

到这里我们已经知道如何在 C 语言中实现一个基本的“类”了。接下来一一来看前面提到的几点。

第一点,派生类内部可以直接使用基类的 public 、protected 成员(包括变量和函数)。具体到上面的例子,我们可以在 derived_func2 中访问基类 base 的成员 a 和 func1 ,没有任何问题,只不过是显式通过 derived 的第一个成员 parent 来访问:

[cpp] view plaincopy print?
  1. static void derived_func2(struct derived *_this)
  2. {
  3. printf("this is derived::func2, base::a = %d\n", _this->parent.a);
  4. _this->parent.func1(&_this->parent);
  5. }
static void derived_func2(struct derived *_this)
{printf("this is derived::func2, base::a = %d\n", _this->parent.a);_this->parent.func1(&_this->parent);
}

第二点,使用派生类的对象,可以像访问派生类自己的成员一样访问基类的成员。这个有点变化,还是只能通过派生类实例的第一个成员 parent 来访问基类的成员(通过指针强制转换的话可以直接访问)。代码如下:

[cpp] view plaincopy print?
  1. struct derived d;
  2. printf("base::a = %d\n",d.parent.a);
  3. struct derived *p = new_derived();
  4. ((struct base *)p)->func1(p);
struct derived d;
printf("base::a = %d\n",d.parent.a);struct derived *p = new_derived();
((struct base *)p)->func1(p);

第三点,对于被派生类覆盖的基类的非虚函数,在派生类中可以通过基类名和域作用符(::)来访问。其实通过前两点,我们已经熟悉了在 C 中访问“基类”成员的方法,总是要通过“派生类”包含的放在结构体第一个位置的基类类型的成员变量来访问。所以在 C 中,严格来讲,实际上不存在覆盖这种情况。即便定义了完全一样的函数指针,也没有关系,因为“包含”这种方式,已经从根本上分割了“基类”和“派生类”的成员,它们不在一个街区,不会冲突。

下面是一个所谓覆盖的例子:

[cpp] view plaincopy print?
  1. struct base{
  2. int a;
  3. int (*func)(struct base * b);
  4. };
  5. struct derived {
  6. struct base b;
  7. int (*func)(struct derived *d);
  8. };
  9. /* usage */
  10. struct derived * d = new_derived();
  11. d->func(d);
  12. d->b.func((struct base*)d);
struct base{int a;int (*func)(struct base * b);
};struct derived {struct base b;int (*func)(struct derived *d);
};/* usage */
struct derived * d = new_derived();
d->func(d);
d->b.func((struct base*)d);

如上面的代码所示,不存在名字覆盖问题。

第四点,虚函数。虚函数是 C++ 里面最有意义的一个特性,是多态的基础,要想讲明白比较困难,我们接下来专门写一篇文章讲述如何在 C 中实现类似虚函数的效果,实现多态。

C语言面向对象编程(二):继承详解相关推荐

  1. C# 面向对象编程【多态详解】

    C# 面向对象编程[多态详解] 文章目录 C# 面向对象编程[多态详解] 1. 里氏转换 2. 多态 2.1 虚方法 3.2 抽象类 3.3 接口 1. 里氏转换 1).子类可以赋值给父类 2).如果 ...

  2. 【职坐标】java面向对象三大特性(二)——继承详解

    [职坐标]java面向对象三大特性(二)--继承详解 a) 什么是继承? i. 多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承那个类即可 b) ...

  3. 大二c语言期末考试题库及详解答案,大学C语言期末考试练习题(带详解答案)...

    <大学C语言期末考试练习题(带详解答案)>由会员分享,可在线阅读,更多相关<大学C语言期末考试练习题(带详解答案)(55页珍藏版)>请在金锄头文库上搜索. 1.一. 单项选择题 ...

  4. C语言面向对象编程(四):面向接口编程

    Java 中有 interface 关键字,C++ 中有抽象类或纯虚类可以与 interface 比拟,C 语言中也可以实现类似的特性. 在面试 Java 程序员时我经常问的一个问题是:接口和抽象类有 ...

  5. Java并发编程最佳实例详解系列

    Java并发编程最佳实例详解系列: Java并发编程(一)线程定义.状态和属性 Java并发编程(一)线程定义.状态和属性 线程是指程序在执行过程中,能够执行程序代码的一个执行单元.在java语言中, ...

  6. RxJS 系列之二 - Observable 详解

    查看新版教程,请访问前端修仙之路 RxJS 系列目录 RxJS 系列之一 - Functional Programming 简介 RxJS 系列之二 - Observable 详解 (本文) RxJS ...

  7. 我所偏爱的 C 语言面向对象编程范式

    我所偏爱的 C 语言面向对象编程范式 面向对象编程不是银弹.大部分场合,我对面向对象的使用非常谨慎,能不用则不用.相关的讨论就不展开了. 但是,某些场合下,采用面向对象的确是比较好的方案.比如 UI ...

  8. mcem r语言代码_R语言面向对象编程:S3和R6

    R语言面向对象编程:S3和R6 2017-06-10 0 R语言面向对象编程:S3和R6 一.基于S3的面向对象编程 基于S3的面向对象编程是一种基于泛型函数(generic function)的实现 ...

  9. java的匿名函数_JAVA语言中的匿名函数详解

    本文主要向大家介绍了JAVA语言中的匿名函数详解,通过具体的内容向大家展示,希望对大家学习JAVA语言有所帮助. 一.使用匿名内部类 匿名内部类由于没有名字,所以它的创建方式有点儿奇怪.创建格式如下: ...

最新文章

  1. 边缘计算Edage Computing
  2. 前端文章精选- 收藏集 - 掘金
  3. 关于uint8_t/uint16_t/uint32_t/uint_fast16_t
  4. Indy中判断邮件来源
  5. 面试项目 java-服务端 18h58
  6. 微信小程序webview内页面分享
  7. Python 父类调用子类方法
  8. 锐捷网关交换机开启dhcp服务
  9. 谷歌浏览器屏蔽自动更新浏览器提示版本太旧
  10. Thoughts On To The Moon
  11. 鱼塘钓鱼 (贪心+枚举)
  12. Java学习day096 并发(六)(线程安全的集合:高效的映射、集和队列、映射条目的原子更新、对并发散列映射的批操作、并发集视图、写数组的拷贝、并行数组算法、较早的线程安全集合)
  13. 铁矿石需求量matlab代码,铁矿石期货市场分析
  14. 移动开发中的仿真器(Emulator)与模拟器(Simulator)
  15. JS 生成永不重复的随机序列号
  16. 春江花月夜 唐 张若虚
  17. mysql 白天范围_急求mysql 统计一个时间范围内的白天与夜上的数据方法
  18. mysql字符集修改无效,Mysql字符集的修改及查看问题_MySQL
  19. Prometheus监控实战系列十七:探针监控
  20. winxp计算机语言改为英语,如何让英文版WinXP系统支持中文文字显示

热门文章

  1. java 单引号的字符串类型_Java程序以字符串形式显示双引号和单引号
  2. thinkcmfx漏洞太大_ThinkCMF5 代码执行漏洞及后续有关思考
  3. poj2912(种类并查集+枚举)
  4. 8、web入门回顾/ Http
  5. uva 156 Ananagrams
  6. 为什么说语言是思维的最有效的工具
  7. java serializable深入了解
  8. 【转载】c语言中的可变参数编程
  9. (译)Objective-C的动态特性
  10. HDOJ 3255 Farming(扫描线 + 线段树 体积的并)