C语言学习笔记—C中的继承(方法1)

C语言学习笔记—C中的继承(方法1)

0、简述

在此之前,我们一起学习了如何用C语言进行OOP,并了解了组合和聚合的概念。在这里我们继续讨论对象及其对应类之间的关系,涵盖继承和多态。

我们已经知道类之间可能存在的关系,C中继承和多态很大程度上依赖于前两章解释的理论基础。我们已经解释了组合和聚合关系,该文将继续讨论继承关系,以及一些其他主题。

以下是我们接下来将解释的主题:

第一个主题是继承关系:本章将介绍在C语言中实现继承关系的各种方法,并对它们进行比较。

第二个主题是多态:在各类之间具有继承关系的情况下,多态允许我们在子类中拥有关于相同行为函数的不同版本。我们将一起学习在C语言中拥有多态函数的方法,这将是我们理解C++如何提供多态的第一步。

让我们从继承关系开始讨论。

一、继承

在上一篇笔记中,我们讨论了“拥有”关系,最终得到了组合和聚合关系。在该笔记中,我们将讨论“是”或“是一个”关系

继承关系是“是”的关系。

继承关系也可以称为扩展关系,因为它只向已存在的对象或类中添加额外的属性和行为。

在某些情况下,一个对象需要具有与另一个对象相同的属性。换句话说,新对象是对老对象的扩展。

例如,一个学生具有一个人的所有属性,但也可能有额外的属性,如下:

上述例子清楚地说明了,student_t 扩展了 person_t,有了新的属性:student_number 和 student_GPA,这是学生的特有属性。

前面指出,继承(或扩展)关系与组合、聚合那么“拥有”关系不同,它是一种“是”的关系。因此,对于上述的例子,我们可以说“一个学生是一个人”,这在教育软件领域似乎是正确的。只要在一个领域中存在“是”的关系时,他就可能是继承关系。在前面的例子中,person_t通常称为基类型或父类型。student_t通常称为子类型或继承的子类型。

二、继承的本质

如果要深入挖掘并了解继承关系到底是什么,就会发现它本质上确实是一个组合关系。我们可以说一个学生具有人的特性,我们可以假设在Student类的属性结构中存在一个私有的person对象。也就是说,继承关系可以等价于一对一的组合关系。

因此,还可以这样写:

这样的语法在C语言中完全有效,实际上,使用结构变量(而不是指针)进行结构嵌套的设置方法功能很强大。它允许在新的结构中加入一个结构变量,这个新结构实际是对原有结构的扩展。

在前面结构类型的设置中,需要将person_t类型的字段作为第一个字段,这样指向student_t类型的指针可以很容易地转换为指向person_t类型的指针,它们都可以指向内存中的相同地址。这种情况叫做向上转换。注意:结构变量不具有此性能。

看如下演示:

可以看到,我们期望 s_ptr 和 p_ptr 指针都指向内存中相同的地址。注意,每次运行时显示的地址可能不同,但关键是两个指针指向的是相同的地址。这意味着 student_t 类型的结构变量在内存分布上确实继承了 person_t 结构。这也意味着,我们可以使用指向 student 对象的指针来使用Person 类的行为函数。

换句话说:Person类的行为函数可以被 Student 对象重用!

请注意,下面的这段代码是错误的,不能编译:

声明 person 字段时产生了错误,因为不能由不完整的类型创建变量。记住,结构的前向声明会得到一个不完整的类型声明。只能有不完整类型的指针,而不能有不完整的变量。之前已经看到,对不完整类型,甚至无法为其分配堆内存。

这是什么意思呢?这意味着,如果打算使用嵌套结构变量来实现继承,则 student_t 结构应该看到 person_t 结构的实际定义,根据我们对封装的了解,person_t 应该是私有的,对任何其他类都不可见。

因此,有两种方法来实现继承关系:

1、使子类能够访问基类的私有实现。

2、使子类只能访问基类的公共接口。

2.1 C语言中实现继承的第一种方法

我们将在下例中举例演示第一种方法,它们都用一些行为函数表示了相同的类:Student 和 Person。在 main 函数中构造了这些类的对象,并在一个简单的场景中运行。

其中Student类需要能访问实际的Person类属性结构的私有定义。

Person类的头文件:

详看上述构造函数,它接受创建person对象所需的所有值:人名、性别、年龄。可以看到,属性结构person_t是不完整的,因此 Student 类不能使用该头文件来建立继承关系,因为person_t只有声明没有定义。

另一方面,该头文件不能包含 person_t 属性结构的实际定义,因为该头文件将被代码的其他部分使用,这部分代码不应该知道Person内部的任何情形。那么,这该怎么办呢?我们希望逻辑的某个部分知道一个结构定义,而其他部分不能知道。这就是私有头文件的切入点。

私有头文件是一个普通的头文件,它应该被实际需要它的特定代码部分或特点类所包含和使用。对于上述代码,person_t 的实际定义应该就是私有头文件中的一部分。看如下私有头文件的示例:

可以看到,它只包含了person_t结构的定义,仅此而已。这是Person类中应该保持私有的一部分,但它需要对Student类公开。

这时,需要这个定义来定义 student_t 属性结构。下面的代码展示了Person类属性的私有实现源文件:

Person类的源文件:

#include

#include

#include"person_private.h" //person_t的定义 在这个私有头文件中

person_t* person_new(void)

{

return (person_t*)malloc(sizeof(person_t));

}

void person_ctor(person_t *person, const char *name, const char *gender, unsigned int age)

{

strcpy(person->name, name);

strcpy(person->gender, gender);

person->age = age;

}

void person_dtor(person_t* person)

{

//什么也不用做

}

void person_get_name(person_t* person, char *buffer)

{

strcpy(buffer, person->name);

}

void person_get_gender(person_t* person, char *buffer)

{

strcpy(buffer, person->gender);

}

unsigned int person_get_age(person_t* person)

{

return person->age;

}

与之前的所有例子一样,这里对Person类定义除了加了私有头文件的这一概念,其他没有什么特别的。

Student类的头文件:

可以看到,Student类的构造函数接受与Person类的构造函数相似的参数。这是因为student对象实际上包含了person对象,它需要这些值来填充其组合的person对象。这意味着student构造函数需要为student对象中的person部分设置属性。

注意:我们只为Student类增加了两个额外拓展的行为函数,因为我们可以对student对象使用Person类的行为函数。

Student类的源文件:

#include

#include

#include"person.h"

#include"person_private.h"

typedef struct

{

person_t person; //这里继承person类的所有属性,并且由于采用结构嵌套,还可以使用它的所有行为函数。

char *student_number;

unsigned int GPA;

}student_t;

student_t* student_new(void)

{

return (student_t*)malloc(sizeof(student_t));

}

void student_ctor(student_t *student, const char *name,

const char *gender, unsigned int age,

const char *student_number, float GPA)

{

//调用基类的构造函数

person_ctor( (struct person_t*)student, name, gender, age );

student->student_number = (char*)malloc(sizeof(char)*16);

strcpy(student->student_number, student_number);

student->GPA = GPA;

}

void student_dtor(student_t* student)

{

free(student->student_number);

person_dtor((struct person*)student);

}

void student_get_Number(student_t *student, char *buffer)

{

strcpy(buffer, student->student_number);

}

unsigned int student_get_GPA(student_t *student)

{

return student->GPA;

}

上述代码包含有关继承关系的最重要的代码。首先,我们需要包含Person类的私有头文件,因为在定义student_t结构类型时,需要使用person_t类型来定义第一个字段。而且,因为这个字段是一个实际的变量而不是一个指针,它要求 person_t 是预先定义好的。

注意,这个变量必须是结构中的第一个字段。否则,就不可能使用Person类的行为函数。

同样,在Student类的构造函数中,调用了基类的构造函数来初始化基类对象的属性。看看在将指向 student_t 类型的指针传递给 person_ctor 函数时,是如何将 student_t 指针转换为 person_t 指针的。之所以可以这样做,是因为 person 字段是 student_t 的第一个成员。

同样,在Student类的析构函数中,也调用了基类的析构函数。这种析构应该首先发生在子级,然后是基级,和构造顺序相反。

下面这个主逻辑源文件中,它将使用Student类并创建一个 Student类型的对象:

在这个主要场景中,我们已经包含了Person和Student两个类的公共接口(不是私有头文件),但是只创建了一个student对象。可以看到,student对象继承了其内部person对象的所有属性,可以通过person类的行为函数读取这些属性。

main函数运行结果:

相关数据

华为手机连接车载蓝牙的步骤与注意事项
日博365官网手机版

华为手机连接车载蓝牙的步骤与注意事项

⌛ 08-10 👁️ 8447
如何设计一个本地缓存
office365无法登录账号

如何设计一个本地缓存

⌛ 10-28 👁️ 2270