前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >C语言面向对象

C语言面向对象

作者头像
WuShF
发布于 2023-02-27 03:01:20
发布于 2023-02-27 03:01:20
1.5K00
代码可运行
举报
文章被收录于专栏:笔记分享笔记分享
运行总次数:0
代码可运行

我们在编写程序时,通常采用以下步骤:

  1. 将问题的解法分解成若干步骤
  2. 使用函数分别实现这些步骤
  3. 依次调用这些函数

这种编程风格的被称作面向过程。除了面向过程之外,还有一种被称作面向对象的编程风格被广泛使用。面向对象采用基于对象的概念建立模型,对现实世界进行模拟,从而完成对问题的解决。 C语言的语法并不直接支持面向对象风格的编程。但是,我们可以通过额外的代码,让C语言实现一些面向对象特性。在这一节当中,我们将探究什么是面向对象,以及怎样用C语言来实现它。 单纯理论上的讨论可能比较难以理解,为了能够让我们的讨论能够落地到实际中,我们选取学校为场景,展开对面向对象风格编程的讨论。 一般而言面向对象风格的编程具有以下3大特性:

  • 封装
  • 继承
  • 多态

我们将以这3个特性为线索,讨论C语言如何面向对象编程。

封装

我们来看看学校里面最重要的主体是什么?是学生。学生肯定拥有很多属性,比如学生的学号、姓名、性别、考试分数等等。自然地,我们会声明一个结构体用于表示学生。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct student {
	int id; // 学号
	char name[20]; // 姓名
	int gender; // 性别
	int mark; // 成绩
};

学生的学号由 入学年份 、 班级 、 序号 拼接构成。 例如,某一个同学的是 2022 年入学的 123 班的 26 号学生。那么,它的学号为 202212326 。 为了方便设置学号,我们有一个 makeStudentId 函数,参数为 入学年份 、 班级 、 序号 ,它将这些数据拼接成字符串,再将字符串转换为整型数据,最后将这个整型数据作为学生的 id 并返回。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int makeStudentId(int year, int classNum, int serialNum)
{
	char buffer[20];
	sprintf(buffer, "%d%d%d", year, classNum, serialNum);
	int id = atoi(buffer);
	return id;
}

sprintfprintf函数类似,printf函数会将占位符"%d%d%d"替换为其后的参数,将结果打印到控制台上。而sprintf不会将结果打印在控制台上,而是将结果存放在第一个参数buffer所指示的字符数组当中。 函数atoi能将buffer指示的字符串转换为整型并返回结果。 性别在结构体中存储为整型数值,0代表女生、1代表男生。而显示时,我们希望0显示为1显示为。因此,还需要有一对用于操作性别的函数。在函数命名中,使用numGender代表使用整型表示的性别。strGender代表使用字符串表示的性别。 我们将定义两个函数: numGenderToStrGender表示,将整型表示的性别转换为字符串表示的性别。 strGenderToNumGender表示,将字符串表示的性别转换为整型表示的性别。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const char* numGenderToStrGender(int numGender)
{
	if (numGender == 0)
	{
		return "女";
	}
	else if (numGender == 1)
	{
		return "男";
	}
	return "未知";
}
int strGenderToNumGender(const char* strGender)
{
	int numGender;
	if (strcmp("男", strGender) == 0)
	{
		numGender = 1;
	}
	else if (strcmp("女", strGender) == 0)
	{
		numGender = 0;
	}
	else
	{
		numGender = -1;
	}
	return numGender;
}

我们将使用以下方式,调用这个结构体和这3个函数。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int main()
{
	struct student stu;
	// 设置数值
	// 学号:202212326
	// 姓名:小明
	// 性别: 男
	// 成绩:98
	stu.id = makeStudentId(2022, 123, 26);
	strcpy(stu.name, "小明");
	stu.gender = strGenderToNumGender("男");
	stu.mark = 98;
	// 打印这些数值
	printf("学号:%d\n", stu.id);
	printf("姓名:%s\n", stu.name);
	const char* gender = numGenderToStrGender(stu.gender);
	printf("性别:%s\n", gender);
	printf("分数:%d\n", stu.mark);
	return 0;
}

现在,我们使用面向过程风格写了3个函数和一个结构体,并且调用了这些函数,将函数返回的结果赋值给了结构体。接下来,让我们以面向对象风格来重新审视这段代码。 在面向对象风格中,结构体被看做数据(data),而操作数据的函数称作方法(method)。目前函数和数据是分离的,函数并不直接操作数据,我们需要拿到函数返回的结果,再将其赋值给数据。面向对象风格编程的第一大特性——封装,它希望方法直接操作数据,并且将数据和方法结合在一起,它们构成一个整体。而这个整体被称作对象。 此外,还有一个方法命名上的规则。一般来说,获取数据的方法会被命名为getXXX,设置数据的方法会被命名为setXXX。 我们对这3个函数做如下修改:

  1. 将函数的第一个参数设置为struct student *,让函数直接操作student结构体。
  2. 修改函数名,获取数据的方法命名为getXXX,设置数据的方法命名为setXXX
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void setStudentId(struct student* s, int year, int classNum, int serialNum)
{
	char buffer[20];
	sprintf(buffer, "%d%d%d", year, classNum, serialNum);
	int id = atoi(buffer);
	s->id = id;
}
const char* getGender(struct student* s)
{
	if (s->gender == 0)
	{
		return "女";
	}
	else if (s->gender == 1)
	{
		return "男";
	}
	return "未知";
}
void setGender(struct student* s, const char* strGender)
{
	int numGender;
	if (strcmp("男", strGender) == 0)
	{
		numGender = 1;
	}
	else if (strcmp("女", strGender) == 0)
	{
		numGender = 0;
	}
	else
	{
		numGender = -1;
	}
	s->gender = numGender;
}

现在,我们用修改后的函数,直接操作student结构。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int main()
{
	struct student stu;
	// 学号:202212326
	// 姓名:小明
	// 性别: 男
	// 分数:98
	setStudentId(&stu, 2022, 123, 26);
	strcpy(stu.name, "小明");
	setGender(&stu, "男");
	stu.mark = 98;
	// 打印这些数值
	printf("学号:%d\n", stu.id);
	printf("姓名:%s\n", stu.name);
	const char* gender = getGender(&stu);
	printf("性别:%s\n", gender);
	printf("分数:%d\n", stu.mark);
}

目前,函数可以直接操作数据了。但是,函数和数据依然是两个独立的部分。我们要将函数和数据结合到一起,这样,这个整体就能被称作对象,函数可以称作属于这个对象的方法。 大多数面向对象语言都提供了以下的格式调用一个对象的方法。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
对象.方法(对象指针,参数1,参数2, 参数3...)

接下来,我们举几个这种格式的例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
stu.setGender(&stu, "男");

以上代码中,对象为stu,方法为setGender。通过对象 + 点 + 方法的形式,可以调用属于对象stusetGender方法。在方法的参数中传入性别。这样,方法会把性别转换为整形,并设置到对象stu的数据当中。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const char* gender = stu.getGender(&stu);

以上代码中,对象为stu,方法为getGender。通过对象 + 点 + 方法的形式,可以调用属于对象stugetGender方法。getGender方法从对象数据中获取整形表示的性别,并返回性别对应的字符串。 在C语言中,若要实现对象 + 点 + 方法的形式,我们可以借助于函数指针。 在结构中,声明这3个函数的函数指针。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct student {
	void (*setStudentId)(struct student* s, int year, int classNum, int serialNum);
	const char* (*getGender)(struct student* s);
	void (*setGender)(struct student* s, const char* strGender);
	int id; // 学号
	char name[20]; // 姓名
	int gender; // 性别
	int mark; // 分数
};

为了让函数指针有正确的指向,我们需要通过一个initStudent函数,为结构体初始化。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void initStudent(struct student* s)
{
	s->setStudentId = setStudentId;
	s->getGender = getGender;
	s->setGender = setGender;
}

现在,我们可以使用对象 + 点 + 方法的形式,调用对象的方法了。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct student stu;
// 初始化student
initStudent(&stu);
// 学号:202212326
// 姓名:小明
// 性别: 男
// 分数:98
stu.setStudentId(&stu, 2022, 123, 26);
strcpy(stu.name, "小明");
stu.setGender(&stu, "男");
stu.mark = 98;
// 打印这些数值
printf("学号:%d\n", stu.id);
printf("姓名:%s\n", stu.name);
const char* gender = stu.getGender(&stu);
printf("性别:%s\n", gender);
printf("分数:%d\n", stu.mark);

这里有一个需要注意的地方,结构体声明后,结构体内的函数指针是无效的。必须先调用initStudent函数,将其设置正确的指向,才能使用这些函数指针。否则,将有可能导致程序崩溃。 为了让方法修改或访问对象,方法的参数中必须要有对象的指针。实现的形式中,第一个参数就是被操作对象指针。其它语言中,被操作对象指针是隐式传递的。不需要你在传参时写明参数,它会自动传入函数。例如,C++中会自动将一个名为this的对象指针作为方法的参数。而C语言中,无法做到自动将对象的指针传入方法,所以我们需要手动写上需要操作的对象的指针。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// C++的写法
stu.setGender("男");

// C语言的写法
stu.setGender(&stu, "男");

继承

除了学生之外,学校里面还需要有老师,老师也具有很多属性。例如:

  • 工号
  • 姓名
  • 性别
  • 任课科目

声明一个结构体用于表示老师。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct teacher {
	int id; // 工号
	char name[20]; // 姓名
	int gender; // 性别
	char subject[20]; // 任课科目
};

比较一下学生和老师的结构体,看看它们之间有什么共同之处与不同之处。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct teacher {
	int id; // 工号
	char name[20]; // 姓名
	int gender; // 性别
	char subject[20]; // 任课科目
};
struct student {
	int id; // 学号
	char name[20]; // 姓名
	int gender; // 性别
	int mark; // 分数
};

共同之处如下:

  • 编号
  • 姓名
  • 性别

不同之处:

  • 学生有考试分数
  • 老师有任课科目

我们可以把两个结构体中的共同之处抽象出来,让它共同之处成为一个新的结构。这个结构体具有老师和学生的共性,而老师与学生它们都是人,可以把这个结构体命名为person

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct person {
	int id; // 编号
	char name[20]; // 姓名
	int gender; // 性别
};

接下来,我们可以让老师和学生结构包含这个person对象。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct teacher {
	struct person super;
	char subject[20]; // 任课科目
};
struct student {
	struct person super;
	int mark; // 分数
};

让我们比较一下原有代码与现有代码。

原有代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 原有代码
struct teacher {
	int id; // 工号
	char name[20]; // 姓名
	int gender; // 性别
	char subject[20]; // 任课科目
};
struct student {
	int id; // 学号
	char name[20]; // 姓名
	int gender; // 性别
	int mark; // 分数
};

现有代码

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 现有代码
struct person {
	int id; // 编号
	char name[20]; // 姓名
	int gender; // 性别
};
struct teacher {
	struct person super;
	char subject[20]; // 任课科目
};
struct student {
	struct person super;
	int mark; // 分数
};

原有代码中,老师和学生结构体中,均有idnamegender三个变量。现有代码中,将这3个变量抽象成结构体person。这样一来,有两个好处:

  1. 减少重复代码
  2. 代码层次更清晰

由于studentteacher拥有person的一切,因此,我们可以说,studentteacher继承personpersonstudentteacher父对象studentteacherperson子对象

刚刚我们只讨论了数据,现在我们结合上方法一起讨论。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct person {
	int id; // 编号
	char name[20]; // 姓名
	int gender; // 性别
};
struct teacher {
	struct person super;
	char subject[20]; // 任课科目
};
struct student {
	struct person super;
	int mark; // 分数
	void (*setStudentId)(struct student* s, int year, int classNum, int serialNum);
	const char* (*getGender)(struct student* s);
	void (*setGender)(struct student* s, const char* strGender);
};

之前我们为student写了3个方法

  • 设置性别
  • 获取性别
  • 设置学号

其中,性别相关的方法也属于共性的方法。可以把这两个函数指针移动到person对象里面去,注意,要把方法的第一个参数struct student *修改为struct person *。移动后,子对象studentteacher均可以使用这一对性别相关的方法。而设置学号的方法,为student独有的方法,因此保持不变,依然将其放置在student对象内。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct person {
	int id; // 编号
	char name[20]; // 姓名
	int gender; // 性别
	// 设置性别
	void (*setGender)(struct person* s, const char* strGender);
	// 获取性别
	const char* (*getGender)(struct person* s);
};
struct teacher {
	struct person super;
	char subject[20]; // 任课科目
};
struct student {
	struct person super;
	int mark; // 分数
	// 设置学号
	void (*setStudentId)(struct student* s, int year, int classNum, int serialNum);
};

对应上面的更改,函数getGendersetGender的第一个参数也要由struct student *修改为struct person *

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const char* getGender(struct person* p)
{
	if (p->gender == 0)
	{
		return "女";
	}
	else if (p->gender == 1)
	{
		return "男";
	}
	return "未知";
}
void setGender(struct person* p, const char* strGender)
{
	int numGender;
	if (strcmp("男", strGender) == 0)
	{
		numGender = 1;
	}
	else if (strcmp("女", strGender) == 0)
	{
		numGender = 0;
	}
	else
	{
		numGender = -1;
	}
	p->gender = numGender;
}

此外,setStudentId函数中,id成员,不在student中,而是在student中的person中。这里也要对应的修改一下。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void setStudentId(struct student* s, int year, int classNum, int serialNum)
{
	char buffer[20];
	sprintf(buffer, "%d%d%d", year, classNum, serialNum);
	int id = atoi(buffer);
	s->super.id = id; // 由s->id = id 修改为 s->super.id = id
}

还有,别忘了给结构初始化函数指针。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void initPerson(struct person* p)
{
	p->getGender = getGender;
	p->setGender = setGender;
}
void initStudent(struct student* s)
{
	initPerson(&(s->super));
	s->setStudentId = setStudentId;
}
void initTeacher(struct teacher* t)
{
	initPerson(&(t->super));
}

下面我们即可使用这些对象了。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct student stu;
// 初始化student
initStudent(&stu);
// 学号:202212326
// 姓名:小明
// 性别: 男
// 分数:98
stu.setStudentId(&stu, 2022, 123, 26);
strcpy(stu.super.name, "小明");
stu.super.setGender(&stu.super, "男");
stu.mark = 98;
// 打印这些数值
printf("学号:%d\n", stu.super.id);
printf("姓名:%s\n", stu.super.name);
const char* gender = stu.super.getGender(&stu.super);
printf("性别:%s\n", gender);
printf("分数:%d\n", stu.mark);
putchar('\n');
struct teacher t;
// 初始化teacher
initTeacher(&t);
// 工号:12345
// 姓名:林老师
// 性别: 男
// 科目:C语言
t.super.id = 12345;
strcpy(t.super.name, "林老师");
t.super.setGender(&t.super, "男");
strcpy(t.subject, "C语言");
// 打印这些数值
printf("学号:%d\n", t.super.id);
printf("姓名:%s\n", t.super.name);
gender = t.super.getGender(&t.super);
printf("性别:%s\n", gender);
printf("科目:%s\n", t.subject);

多态

我们以绘制各种图形为背景,展开对多态这一特性的讨论。

绘制图形

现在,我们有3种图形,它们分别为:

  • 矩形
  • 圆形
  • 三角形

我们把这3种图形均看做对象,这些图形对象,分别需要有哪些属性呢?

  • 矩形:左上角坐标、右下角坐标
  • 圆形:圆心x坐标、圆心y坐标、半径
  • 三角形:三个顶点坐标

现在,我们用代码分别实现这几个对象。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct Rect {
	int left;
	int top;
	int right;
	int bottom;
};
struct Circle {
	int x;
	int y;
	int r;
};
struct Triangle {
	POINT p1;
	POINT p2;
	POINT p3;
};

为了能够在屏幕上绘制这些图形,每个图形都设置一个名为draw的方法。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct Rect {
	void (*draw)(struct Rect*);
	int left;
	int top;
	int right;
	int bottom;
};
struct Circle {
	void (*draw)(struct Circle*);
	int x;
	int y;
	int r;
};
struct Triangle {
	void (*draw)(struct Triangle*);
	POINT p1;
	POINT p2;
	POINT p3;
};

分别实现3个不同的绘制函数。

绘制矩形:

调用 easyx 中的 rectangle 函数,传入左上角坐标与右下角坐标。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void drawRect(struct Rect* r)
{
	rectangle(r->left, r->top, r->right, r->bottom);
}

绘制圆形:

调用 easyx 中的 circle 函数,传入圆心坐标与半径。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void drawCircle(struct Circle* c)
{
	circle(c->x, c->y, c->r);
}

绘制三角形:

调用 easyx 中的 line 函数,分别绘制点 p1 到 p2 的线段, p2 到 p3 的线段,以及 p3 到 p1 的线段。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void drawTriangle(struct Triangle* t)
{
	line(t->p1.x, t->p1.y, t->p2.x, t->p2.y);
	line(t->p2.x, t->p2.y, t->p3.x, t->p3.y);
	line(t->p3.x, t->p3.y, t->p1.x, t->p1.y);
}

下面,分别写3个初始化函数,用于给对象中的函数指针draw进行赋值。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void initRect(struct Rect* r)
{
	r->draw = drawRect;
}
void initCircle(struct Circle* r)
{
	r->draw = drawCircle;
}
void initTriangle(struct Triangle* r)
{
	r->draw = drawTriangle;
}

现在,准备工作都做好了,我们开始绘制这些图形吧。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int main()
{
	initgraph(800, 600);
	setaspectratio(1, -1);
	setorigin(400, 300);
	setbkcolor(WHITE);
	setlinecolor(BLACK);
	cleardevice();
	struct Rect r = { -200, 200, 200, 0 };
	struct Circle c = { 0, 0, 100 };
	struct Triangle t = { {0, 200}, {-200, 0}, {200, 0} };
	initRect(&r);
	initCircle(&c);
	initTriangle(&t);
	r.draw(&r);
	c.draw(&c);
	t.draw(&t);
	getchar();
	closegraph();
	return 0;
}

创建一个800 * 600的绘图窗体,设置x轴正方向为从左到右,y轴正方向为从下到上。将原点坐标从窗体左上角更改为窗体中心。设置背景颜色为白色,描边颜色为黑色,并使用背景色刷新整个窗体。下面分别声明矩形、圆形、三角形三个对象,并将需要的属性初始化。之后,三个对象分别调用各自的init函数,为对象内的函数指针赋值。完成准备工作后,即可使用对象 + 点 + 方法的形式,调用各自的draw方法绘制图形了。

多态

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct Rect {
	void (*draw)(struct Rect*);
	int left;
	int top;
	int right;
	int bottom;
};
struct Circle {
	void (*draw)(struct Circle*);
	int x;
	int y;
	int r;
};
struct Triangle {
	void (*draw)(struct Triangle*);
	POINT p1;
	POINT p2;
	POINT p3;
};

我们仔细观察这3个对象,看看它们分别有什么共性?可以发现,这3个对象,它们都有一个draw方法。那么,我们可以将draw这个方法抽象出来,单独放置到一个对象当中。由于这三个对象都是形状。我们可以把单独抽象出来的对象,命名为shapeshape对象中的draw方法,应当是一个共性的方法,所以,它的参数应当设置为struct Shape *

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct Shape {
	void (*draw)(struct Shape*);
};

接下来,让RectCircleTriangle三个对象分别都包含Shape对象。这样,它们就都能使用draw这个方法了。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct Rect {
	struct Shape super;
	int left;
	int top;
	int right;
	int bottom;
};
struct Circle {
	struct Shape super;
	int x;
	int y;
	int r;
};
struct Triangle {
	struct Shape super;
	POINT p1;
	POINT p2;
	POINT p3;
};

这里有一个需要注意的地方,父对象与子对象的内存排布必须重合。 例如:下图中,上面的两个对象内存排布可以重合。而下面的两个对象的内存排布无法重合。

像下面一样的声明Rect是正确的。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 正确
struct Rect {
	struct Shape super;
	int left;
	int top;
	int right;
	int bottom;
};

而下面一样的声明Rect是错误的。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 错误
struct Rect {
	int left;
	int top;
	int right;
	int bottom;
	struct Shape super;
};

接着,我们需要修改各对象的初始化函数。将原有的r->draw改为r->super.draw

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void initRect(struct Rect* r)
{
	r->super.draw = drawRect;
}
void initCircle(struct Circle* c)
{
	c->super.draw = drawCircle;
}
void initTriangle(struct Triangle* t)
{
	t->super.draw = drawTriangle;
}

注意,这里还有一个问题,函数内赋值运算符左边的函数指针r->super.draw的类型为void (*)(struct Shape*),参数为struct Shape*。而赋值运算符右边的函数指针类型分别为:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void (*)(struct Rect*)
void (*)(struct Circle*)
void (*)(struct Triangle*)

函数指针参数类型不一致,无法进行赋值。我们可以把右边的函数指针强制类型转换为void (*)(struct Shape*)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void initRect(struct Rect* r)
{
	r->super.draw = (void (*)(struct Shape*))drawRect;
}
void initCircle(struct Circle* c)
{
	c->super.draw = (void (*)(struct Shape*))drawCircle;
}
void initTriangle(struct Triangle* t)
{
	t->super.draw = (void (*)(struct Shape*))drawTriangle;
}

我们考虑一下怎样来使用这些对象。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct Rect r = { {}, - 200, 200, 200, 0 };
struct Circle c = { {}, 0, 0, 100 };
struct Triangle t = { {}, {0, 200}, {-200, 0}, {200, 0} };

首先,声明RectCircleTriangle这3个对象,并使用初始化列表将其初始化。注意,由于它们的第一个成员为super,所以,这里使用空列表{},将super成员初始化为零。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
initRect(&r);
initCircle(&c);
initTriangle(&t);

让三个对象分别调用各自的初始化函数,给各自对象super成员中的draw设置为各自对应的绘图函数。

  • r.super.draw设置为drawRect
  • c.super.draw设置为drawCircle
  • t.super.draw设置为drawRTriangle
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct Shape* arrShape[3] = {
(struct Shape*)&r, (struct Shape*)&c, (struct Shape*)&t };

声明一个元素类型为struct Shape *的数组,元素个数为3。分别用r的指针,c的指针,t的指针初始化。注意,这里也需要进行强制类型转换,否则初始化列表里面的指针类型和数组元素的指针类型不一致。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
for (int i = 0; i < 3; i++)
{
	arrShape[i]->draw(arrShape[i]);
}

到了关键的一步,使用循环,依次调用draw函数。由于3次循环中的draw函数分别为各个图形各自的绘图函数。所以,虽然统一调用的是draw,但是,却可以执行它们各自的绘图函数。至此,不同实现的方法,在此得到统一。

总结实现多态的步骤

  1. 抽离出各个对象中共有的方法draw,将其单独放置在一个对象Shape内。
  2. 各个对象均继承于Shape对象。
  3. 将各个子对象中的draw方法,设置为各自的实现方法。
  4. 声明一个Shape对象的指针,并将其赋值为一个子对象的指针。
  5. 通过上述对象指针,调用方法共有方法draw,执行的是第三步中设置的方法。
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-02-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
Java集合框架知识整理
Java集合框架主要由Collection和Map两个根接口及其子接口、实现类组成。
Abalone
2022/07/14
6380
Java集合框架知识整理
Java集合详解【面试+工作】
在说集合前我们不得不说一下数组 数组的作用: 存放一组相同的数据类型(基本或对象)的数据,从而实现对数据的管理 优势:可以快速的通过下标对数组元素进行访问,效率高 劣势:容量实现定义好了,不能随着需求变化而扩容 因此出现了更好的集合框架 一、数组和集合的比较 数组不是面向对象的,存在明显的缺陷,集合弥补了数组的缺点,比数组更灵活更实用,而且不同的集合框架类可适用不同场合。如下: 1:数组能存放基本数据类型和对象,而集合类存放的都是对象的引用,而非对象本身! 2:数组容易固定无法动态改变,集合类容量动态改变。
Java帮帮
2018/03/15
2K0
Java集合详解【面试+工作】
java集合详解和集合面试题目
数组(可以存储基本数据类型)是用来存现对象的一种容器,但是数组的长度固定,不适合在对象数量未知的情况下使用。
全栈程序员站长
2022/09/05
6810
Java集合面试题&知识点总结(上篇)
解答:Java 集合类呢主要是指 java.Util包 下的集合容器。主要包含三种:List、Set、Map,其中 List、Set 主要继承自 Collection 接口,然后它们三个又都依赖了 Iterator 迭代器;
栗筝i
2023/10/29
2790
死磕 java集合之TreeSet源码分析
TreeSet底层是采用TreeMap实现的一种Set,所以它是有序的,同样也是非线程安全的。
彤哥
2019/07/08
4640
死磕 java集合之TreeSet源码分析
Java集合框架详解(全)
集合可以看作是一种容器,用来存储对象信息。所有集合类都位于java.util包下,但支持多线程的集合类位于java.util.concurrent包下。
硕人其颀
2020/06/02
1.1K0
Java集合面试题[通俗易懂]
大家好,又见面了,我是你们的朋友全栈君。 Java集合面试题 Java 集合框架的基础接口有哪些? Collection ,为集合层级的根接口。一个集合代表一组对象,这些对象即为它的元素。Java 平台不提供这个接口任何直接的实现。 Set ,是一个不能包含重复元素的集合。这个接口对数学集合抽象进行建模,被用来代表集合,就如一副牌。 List ,是一个有序集合,可以包含重复元素。你可以通过它的索引来访问任何元素。List 更像长度动态变换的数组。 Map ,是一个将 key 映射到 value 的对
全栈程序员站长
2022/07/23
5730
老哥,您看我这篇Java集合,还有机会评优吗?
集合在我们日常开发使用的次数数不胜数,ArrayList/LinkedList/HashMap/HashSet······信手拈来,抬手就拿来用,在 IDE 上龙飞凤舞,但是作为一名合格的优雅的程序猿,仅仅了解怎么使用API是远远不够的,如果在调用API时,知道它内部发生了什么事情,就像开了透视外挂一样,洞穿一切,这种感觉才真的爽,而且这样就不是集合提供什么功能给我们使用,而是我们选择使用它的什么功能了。
cxuan
2020/08/07
5900
老哥,您看我这篇Java集合,还有机会评优吗?
杰哥教你面试之一百问系列:java集合
集合是我们在java中经常会用到的东西,熟悉了集合我们就熟悉了java。当面试官在Java面试中涉及到Java集合的问题时,通常会涉及到集合的概念、类型、常见操作、性能等方面的内容。
程序那些事
2023/09/12
2730
深入浅出的分析 Set集合
原文链接:https://blog.csdn.net/javageektech/article/details/103077788
chenchenchen
2019/11/26
5300
Java 集合常见知识点&面试题总结(上),2022 最新版!
Java 集合, 也叫作容器,主要是由两大接口派生而来:一个是 Collection接口,主要用于存放单一元素;另一个是 Map 接口,主要用于存放键值对。对于Collection 接口,下面又有三个主要的子接口:List、Set 和 Queue。
Guide哥
2022/11/07
3390
Java 集合常见知识点&面试题总结(上),2022 最新版!
Java集合解惑
本文取自工匠若水的qq群里的Java基础题目,把里面有关Java集合放在一起。 全文github地址
1025645
2018/08/23
6910
Java集合分类以及各自特点
常用的就是ArrayList,LinkedList,HashSet,LinkedHashSet,TreeSet,HashMap,LinkedHashMap,TreeMap; 数组和集合的区别 区别1: 数组可以存储基本数据类型/引用数据类型 基本数据类型存的是值 引用数据类型存的是地址 数组在创建的时候 就会定义存储的数据类型 也就是只能存储一种数据类型 集合只能存储引用数据类型(对象) 集合中也可以存储基本数据类型(装箱)最终存储的还是 Object 如果没有泛型限定 默认存储的都是 Object类型的数据 也就是任意类型 区别2 数组长度是固定的,不能自动增长 集合是长度可变的,根据元素的多少来决定长度
全栈程序员站长
2022/09/01
5400
40个Java集合面试问题和答案
每种编程语言中都有集合,最初的Java版本包含几种集合类:Vector、Stack、HashTable和Array。随着集合的广泛使用,Java1.2提出了囊括所有集合接口、实现和算法的集合框架。在保证线程安全的情况下使用泛型和并发集合类,Java已经经历了很久。它还包括在Java并发包中,阻塞接口以及它们的实现。集合框架的部分优点如下:
田维常
2019/07/16
8220
大厂必问的Java集合面试题
本文已经收录到github仓库,此仓库用于分享互联网大厂高频面试题、Java核心知识总结,包括Java基础、并发、MySQL、Springboot、MyBatis、Redis、RabbitMQ等等,面试必备!欢迎大家star!
程序员大彬
2022/01/06
1.5K0
大厂必问的Java集合面试题
Java学习笔记——集合
存储对象可以使用数组 (基本数据类型 & 引用数据类型) 和集合 (引用数据类型),用数组存储对象的弊端有:一旦创建,其长度不可变;数组中真正存储的对象个数不可知,除非自定义类。使用集合可以解决这些问题。
梦飞
2022/06/23
2740
Java学习笔记——集合
深入探索Java集合框架
Java集合框架位于java.util包中,是Java编程语言的核心部分。它定义了几种类型的集合,包括列表(List)、集合(Set)、队列(Queue)、双端队列(Deque)以及映射(Map)。这些集合类型通过统一的接口和抽象类来实现,从而提供了对数据的一致视图。
公众号:码到三十五
2024/03/19
2350
Java 集合框架(7)---- Set 相关类解析
在上篇文章中,我们将剩下的常见的 Map 接口下的相关具体类做了一个解析,还有一些相关的类将会在下一篇文章中做一个总结,这篇我们来看看 Set 接口的相关类。老规矩,还是继续看一下 Set 接口下继承关系图:
指点
2019/01/18
5180
Java 集合框架(7)---- Set 相关类解析
Java--集合类之Collection与Map
上一篇:Java--集合类之Vector、BitSet、Stack、Hashtable 集合(Collection):一组单独的元素,通常应用了某种规则。在这里,一个 List(列表)必须按特定的顺序容纳元素,而一个Set(集)不可包含任何重复的元素。相反,“包”(Bag)的概念未在新的集合库中实现,因为“列表”已提供了类似的功能。 映射(Map):一系列“键-值”对(这已在散列表身上得到了充分的体现)。从表面看,这似乎应该成为一个“键-值”对的“集合”,但假若试图按那种方式实现它,就会发现实现过程相当笨拙
SuperHeroes
2018/05/22
9450
JavaSE(八)集合之Set
今天这一篇把之前没有搞懂的TreeSet中的比较搞得非常的清楚,也懂得了它的底层实现。希望博友提意见! 一、Set接口 1.1、Set集合概述   Set集合:它类似于一个罐子,程序可以依次把多个对象 “丢进” Set 集合,而 Set 集合通常不能记住元素的添加的顺序,也就是说Set 集合是无序的。         Set 集合与 Colleaction 基本相同,没有提供额外的方法,实际上 Set 就是 Collection,只是行为略有不同(Set 不允许包含重复元素)。 1.2、Set类型集合特点
用户1195962
2018/01/18
1K0
JavaSE(八)集合之Set
相关推荐
Java集合框架知识整理
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档