C语言面向对象编程(二):继承详解
在 C 语言面向对象编程(一)里说到继承,这里再详细说一下。
C++ 中的继承,从派生类与基类的关系来看(出于对比 C 与 C++,只说公有继承):
- 派生类内部可以直接使用基类的 public 、protected 成员(包括变量和函数)
- 使用派生类的对象,可以像访问派生类自己的成员一样访问基类的成员
- 对于被派生类覆盖的基类的非虚函数,在派生类中可以通过基类名和域作用符(::)来访问
- 当使用基类指针调用虚函数时,会调用指针指向的实际对象实现的函数,如果该对象未重载该虚函数,则沿继承层次,逐级回溯,直到找到一个实现
上面的几个特点,我们在 C 语言中能否全部实现呢?我觉得可以实现类似的特性,但在使用方法上会有些区别。后面我们一个一个来说,在此之前呢,先说继承的基本实现。
先看 C 语言中通过“包含”模拟实现继承的简单代码框架:
- struct base{
- int a;
- };
- struct derived{
- struct base parent;
- int b;
- };
- struct derived_2{
- struct derived parent;
- int b;
- };
struct base{int a;
};struct derived{struct base parent;int b;
};struct derived_2{struct derived parent;int b;
};
上面的示例只有数据成员,函数成员其实是个指针,可以看作数据成员。 C 中的 struct 没有访问控制,默认都是公有访问(与 java 不同)。
下面是带成员函数的结构体:
- struct base {
- int a;
- void (*func1)(struct base *_this);
- };
- struct derived {
- struct base parent;
- int b;
- void (*func2)(struct derived* _this;
- };
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 文件中实现的函数:
- 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");
- }
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 函数族来分配内存块,我们没有机会来自动初始化结构体的成员,只能自己增加一个函数。如下面这样(略去头文件中的声明语句):
- struct base * new_base()
- {
- struct base * b = malloc(sizeof(struct base));
- b->a = 0;
- b->func1 = base_func1;
- return b;
- }
struct base * new_base()
{struct base * b = malloc(sizeof(struct base));b->a = 0;b->func1 = base_func1;return b;
}
好的,构造函数有了。通过 new_base() 调用返回的结构体指针,已经可以像类实例一样使用了:
- struct base * b1 = new_base();
- b1->func1(b1);
struct base * b1 = new_base();
b1->func1(b1);
到这里我们已经知道如何在 C 语言中实现一个基本的“类”了。接下来一一来看前面提到的几点。
第一点,派生类内部可以直接使用基类的 public 、protected 成员(包括变量和函数)。具体到上面的例子,我们可以在 derived_func2 中访问基类 base 的成员 a 和 func1 ,没有任何问题,只不过是显式通过 derived 的第一个成员 parent 来访问:
- static void derived_func2(struct derived *_this)
- {
- printf("this is derived::func2, base::a = %d\n", _this->parent.a);
- _this->parent.func1(&_this->parent);
- }
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 来访问基类的成员(通过指针强制转换的话可以直接访问)。代码如下:
- struct derived d;
- printf("base::a = %d\n",d.parent.a);
- struct derived *p = new_derived();
- ((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 中,严格来讲,实际上不存在覆盖这种情况。即便定义了完全一样的函数指针,也没有关系,因为“包含”这种方式,已经从根本上分割了“基类”和“派生类”的成员,它们不在一个街区,不会冲突。
下面是一个所谓覆盖的例子:
- 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);
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语言面向对象编程(二):继承详解相关推荐
- C# 面向对象编程【多态详解】
C# 面向对象编程[多态详解] 文章目录 C# 面向对象编程[多态详解] 1. 里氏转换 2. 多态 2.1 虚方法 3.2 抽象类 3.3 接口 1. 里氏转换 1).子类可以赋值给父类 2).如果 ...
- 【职坐标】java面向对象三大特性(二)——继承详解
[职坐标]java面向对象三大特性(二)--继承详解 a) 什么是继承? i. 多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承那个类即可 b) ...
- 大二c语言期末考试题库及详解答案,大学C语言期末考试练习题(带详解答案)...
<大学C语言期末考试练习题(带详解答案)>由会员分享,可在线阅读,更多相关<大学C语言期末考试练习题(带详解答案)(55页珍藏版)>请在金锄头文库上搜索. 1.一. 单项选择题 ...
- C语言面向对象编程(四):面向接口编程
Java 中有 interface 关键字,C++ 中有抽象类或纯虚类可以与 interface 比拟,C 语言中也可以实现类似的特性. 在面试 Java 程序员时我经常问的一个问题是:接口和抽象类有 ...
- Java并发编程最佳实例详解系列
Java并发编程最佳实例详解系列: Java并发编程(一)线程定义.状态和属性 Java并发编程(一)线程定义.状态和属性 线程是指程序在执行过程中,能够执行程序代码的一个执行单元.在java语言中, ...
- RxJS 系列之二 - Observable 详解
查看新版教程,请访问前端修仙之路 RxJS 系列目录 RxJS 系列之一 - Functional Programming 简介 RxJS 系列之二 - Observable 详解 (本文) RxJS ...
- 我所偏爱的 C 语言面向对象编程范式
我所偏爱的 C 语言面向对象编程范式 面向对象编程不是银弹.大部分场合,我对面向对象的使用非常谨慎,能不用则不用.相关的讨论就不展开了. 但是,某些场合下,采用面向对象的确是比较好的方案.比如 UI ...
- mcem r语言代码_R语言面向对象编程:S3和R6
R语言面向对象编程:S3和R6 2017-06-10 0 R语言面向对象编程:S3和R6 一.基于S3的面向对象编程 基于S3的面向对象编程是一种基于泛型函数(generic function)的实现 ...
- java的匿名函数_JAVA语言中的匿名函数详解
本文主要向大家介绍了JAVA语言中的匿名函数详解,通过具体的内容向大家展示,希望对大家学习JAVA语言有所帮助. 一.使用匿名内部类 匿名内部类由于没有名字,所以它的创建方式有点儿奇怪.创建格式如下: ...
最新文章
- 边缘计算Edage Computing
- 前端文章精选- 收藏集 - 掘金
- 关于uint8_t/uint16_t/uint32_t/uint_fast16_t
- Indy中判断邮件来源
- 面试项目 java-服务端 18h58
- 微信小程序webview内页面分享
- Python 父类调用子类方法
- 锐捷网关交换机开启dhcp服务
- 谷歌浏览器屏蔽自动更新浏览器提示版本太旧
- Thoughts On To The Moon
- 鱼塘钓鱼 (贪心+枚举)
- Java学习day096 并发(六)(线程安全的集合:高效的映射、集和队列、映射条目的原子更新、对并发散列映射的批操作、并发集视图、写数组的拷贝、并行数组算法、较早的线程安全集合)
- 铁矿石需求量matlab代码,铁矿石期货市场分析
- 移动开发中的仿真器(Emulator)与模拟器(Simulator)
- JS 生成永不重复的随机序列号
- 春江花月夜 唐 张若虚
- mysql 白天范围_急求mysql 统计一个时间范围内的白天与夜上的数据方法
- mysql字符集修改无效,Mysql字符集的修改及查看问题_MySQL
- Prometheus监控实战系列十七:探针监控
- winxp计算机语言改为英语,如何让英文版WinXP系统支持中文文字显示