C语言面的向对象编程(OOP)

如果使用过C++、C#、Java语言,一定知道面向对象编程,这些语言对面向对象编程的支持是语言级别的。C语言在语言级别不支持面向对象,那可以实现面向对象吗?其实面向对象是一种思想,而不是一种语言,很多初学者很容易把这个概念搞错!

面向对象编程(OOP)有三大特性:封装、继承、多态,这里以实现矩形与圆形计算面积为例来说明,C++代码如下:

#include <iostream>
using namespace std;typedef enum {Black,Red,White,Yellow,
}Color;class CShape {
private:const char* m_name;Color color;public:CShape(const char* name) {m_name = name;color = Black;cout << "CShape Ctor:" << name << endl;}virtual int GetArea() = 0;virtual ~CShape() {cout << "CShape Dtor" << endl;}
};class CRect : public CShape {
private:int _w, _h;
public:CRect(int w, int h) : CShape("Rect"), _w(w), _h(h) {cout << "CRect Ctor" << endl;}virtual ~CRect() {cout << "CRect Dtor" << endl;}virtual int GetArea() {cout << "CRect GetArea" << endl;return _w * _h;}
};class CCircle : public CShape {
private:int _r;
public:CCircle(int r): CShape("Circle"), _r(r) {cout << "CCircle Ctor" << endl;}virtual ~CCircle() {cout << "CCircle Dtor" << endl;}virtual int GetArea() {cout << "CCircle GetArea" << endl;return 3.14 * _r * _r;}
};extern "C" void testCC() {cout << "CRect Size:" << sizeof(CRect) << endl;cout << "CCircle Size:" << sizeof(CCircle) << endl;CRect* r = new CRect(10, 20);int a1 = r->GetArea();CCircle* c = new CCircle(10);int a2 = c->GetArea();delete r;delete c;
}

先定义了一个形状类CShape,类中定义了一个构造函数CShape、一个虚析构函数~CShape和一个纯虚函数GetArea,矩形以及圆形都继承CShape并各自实现了自己的构造函数、析构函数和计算面积的GetArea函数,调用GetArea函数会调用到各自的实现。

分别看一下CShape、CRect以及CCircle的内存布局:

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
可以看到CShape占24字节,CRect以及CCircle都占32字节。

C++很容易就在语言级别实现了面向对象编程,C语言在语言级别无法支持面向对象,就需要手动模拟。

先来看看Shape的定义,它与C++的CShape类等效:

typedef struct _Shape Shape;
typedef int (*ShapeGetArea)(Shape*);
typedef void (*ShapeDtor)(Shape*);// 定义Shape的虚函数,以实现多态
typedef struct {ShapeGetArea GetArea; // 计算面积ShapeDtor Dtor; // 析构函数
} vtShape;typedef enum {Black,Red,White,Yellow,
}Color;struct _Shape {const vtShape* vtb; // 指向虚函数表// 公有变量char* name;Color color;
};// Shape 的构造函数
void shapeCtor(Shape* p, const char* name) {printf("shapeCtor:%s\n", name);p->name = strdup(name);
}
// Shape 的析构函数
void shapeDtor(Shape* p) {printf("shapeDtor:%s\n", p->name);free(p->name);
}

为什么Shape中使用一个vtShape指针,而不是把计算面积的函数指针直接放在Shape中? vtShape指针指向的是虚函数表,在代码中矩形与圆形的虚函数表各自只有唯一的一份,这样不管构建了多少实例,每个实例都只有一个指向虚函数表的指针,节约了内存空间。 这样就与C++的类的实现完全一致,可以看一下内存布局:

在这里插入图片描述

再来看矩形与圆形的头文件定义:

typedef struct {Shape; // 继承Shape
}Rect;typedef struct {Shape; // 继承Shape
}Circle;Rect* newRect(int w, int h);
Circle* newCircle(int r);

矩形的实现:


typedef struct {Rect; // 这里继承头文件中公开的Rect定义// 下面定义私有变量int w, h;
}realRect;// 计算矩形面积
static int RectArea(realRect* s) {printf("Rect GetArea\n");return s->w * s->h;
}// 矩形的析构函数
static void RectRelease(realRect* s) {if (s) {printf("Rect Dtor:%s\n", s->name);shapeDtor((Shape*)s);free(s);}
}// 矩形的虚函数表
// 虚函数表只有唯一的一份,这样不管构建了多少实例,
// 每个实例都只有一个指向虚函数表的指针,节约了内存空间
static const vtShape vtRect = {.GetArea = (ShapeGetArea)RectArea,.Dtor = (ShapeDtor)RectRelease,
};Rect* newRect(int w, int h) {// 以realRect大小分配内存realRect* p = calloc(1, sizeof(realRect));if (NULL == p)return NULL;// 调用基类的构造函数shapeCtor((Shape*)p, "Rect");// 设置虚函数表p->vtb = &vtRect;p->h = h;p->w = w;printf("Rect Ctor\n");printf("Rect Size:%zd\n", sizeof(realRect));return (Rect*)p;
}

圆形的实现:

typedef struct {Circle; // 这里继承头文件中公开的Circle定义// 下面定义私有变量int r;
}realCircle;// 计算圆形面积
static int CircleArea(realCircle* s) {printf("Circle GetArea\n");return (int)(3.14 * s->r * s->r);
}// 圆形的析构函数
static void CircleRelease(realCircle* s) {if (s) {printf("Circle Dtor:%s\n", s->name);shapeDtor((Shape*)s);free(s);}
}// 圆形的虚函数表
// 虚函数表只有唯一的一份,这样不管构建了多少实例,
// 每个实例都只有一个指向虚函数表的指针,节约了内存空间
static const vtShape vtCircle = {.GetArea = (ShapeGetArea)CircleArea,.Dtor = (ShapeDtor)CircleRelease,
};Circle* newCircle(int r) {realCircle* p = calloc(1, sizeof(realCircle));if (NULL == p)return NULL;shapeCtor((Shape*)p, "Circle");p->vtb = &vtCircle;p->r = r;printf("Circle Ctor\n");printf("Circle Size:%zd\n", sizeof(realCircle));return (Circle*)p;
}

定义了好了后,就可以使用它们来计算面积了,示例代码:

Rect* r = newRect(10, 20);
Circle* c = newCircle(10);
r->color = Red;
c->color = Yellow;
const vtShape* rt = r->vtb;
const vtShape* ct = c->vtb;
int a1 = rt->GetArea((Shape*)r);
int a2 = ct->GetArea((Shape*)c);
rt->Dtor((Shape*)r);
ct->Dtor((Shape*)c);

完整代码:

shape.h

#pragma oncetypedef struct _Shape Shape;
typedef int (*ShapeGetArea)(Shape*);
typedef void (*ShapeDtor)(Shape*);// 定义Shape的虚函数,以实现多态
typedef struct {ShapeGetArea GetArea;ShapeDtor Dtor;
} vtShape;typedef enum {Black,Red,White,Yellow,
}Color;struct _Shape {const vtShape* vtb; // 指向虚函数表char* name;Color color;
};// Shape 的构造函数
void shapeCtor(Shape* shape, const char* name);
// Shape 的析构函数
void shapeDtor(Shape* shape);typedef struct {Shape; // 继承Shape
}Rect;typedef struct {Shape; // 继承Shape
}Circle;Rect* newRect(int w, int h);
Circle* newCircle(int r);

shape.c

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include "shape.h"void shapeCtor(Shape* p, const char* name) {printf("shapeCtor:%s\n", name);p->name = strdup(name);
}void shapeDtor(Shape* p) {printf("shapeDtor:%s\n", p->name);free(p->name);
}typedef struct {Rect; // 这里继承头文件中公开的Rect定义// 下面定义私有变量int w, h;
}realRect;// 计算矩形面积
static int RectArea(realRect* s) {printf("Rect GetArea\n");return s->w * s->h;
}// 矩形的析构函数
static void RectRelease(realRect* s) {if (s) {printf("Rect Dtor:%s\n", s->name);shapeDtor((Shape*)s);free(s);}
}// 矩形的虚函数表
// 虚函数表只有唯一的一份,这样不管构建了多少实例,
// 每个实例都只有一个指向虚函数表的指针,节约了内存空间
static const vtShape vtRect = {.GetArea = (ShapeGetArea)RectArea,.Dtor = (ShapeDtor)RectRelease,
};Rect* newRect(int w, int h) {// 以realRect大小分配内存realRect* p = calloc(1, sizeof(realRect));if (NULL == p)return NULL;// 调用基类的构造函数shapeCtor((Shape*)p, "Rect");// 设置虚函数表p->vtb = &vtRect;p->h = h;p->w = w;printf("Rect Ctor\n");printf("Rect Size:%zd\n", sizeof(realRect));return (Rect*)p;
}typedef struct {Circle; // 这里继承头文件中公开的Circle定义// 下面定义私有变量int r;
}realCircle;// 计算圆形面积
static int CircleArea(realCircle* s) {printf("Circle GetArea\n");return (int)(3.14 * s->r * s->r);
}// 圆形的析构函数
static void CircleRelease(realCircle* s) {if (s) {printf("Circle Dtor:%s\n", s->name);shapeDtor((Shape*)s);free(s);}
}// 圆形的虚函数表
// 虚函数表只有唯一的一份,这样不管构建了多少实例,
// 每个实例都只有一个指向虚函数表的指针,节约了内存空间
static const vtShape vtCircle = {.GetArea = (ShapeGetArea)CircleArea,.Dtor = (ShapeDtor)CircleRelease,
};Circle* newCircle(int r) {realCircle* p = calloc(1, sizeof(realCircle));if (NULL == p)return NULL;shapeCtor((Shape*)p, "Circle");p->vtb = &vtCircle;p->r = r;printf("Circle Ctor\n");printf("Circle Size:%zd\n", sizeof(realCircle));return (Circle*)p;
}

shape.cc

#include <iostream>
using namespace std;typedef enum {Black,Red,White,Yellow,
}Color;class CShape {
private:const char* m_name;Color color;public:CShape(const char* name) {m_name = name;color = Black;cout << "CShape Ctor:" << name << endl;}virtual int GetArea() = 0;virtual ~CShape() {cout << "CShape Dtor" << endl;}
};class CRect : public CShape {
private:int _w, _h;
public:CRect(int w, int h) : CShape("Rect"), _w(w), _h(h) {cout << "CRect Ctor" << endl;}virtual ~CRect() {cout << "CRect Dtor" << endl;}virtual int GetArea() {cout << "CRect GetArea" << endl;return _w * _h;}
};class CCircle : public CShape {
private:int _r;
public:CCircle(int r): CShape("Circle"), _r(r) {cout << "CCircle Ctor" << endl;}virtual ~CCircle() {cout << "CCircle Dtor" << endl;}virtual int GetArea() {cout << "CCircle GetArea" << endl;return 3.14 * _r * _r;}
};extern "C" void testCC() {cout << "CRect Size:" << sizeof(CRect) << endl;cout << "CCircle Size:" << sizeof(CCircle) << endl;CRect* r = new CRect(10, 20);int a1 = r->GetArea();CCircle* c = new CCircle(10);int a2 = c->GetArea();delete r;delete c;
}

main.c

#include "shape.h"void testCC();int main(){testCC();printf("\n\n");Rect* r = newRect(10, 20);Circle* c = newCircle(10);r->color = Red;c->color = Yellow;const vtShape* rt = r->vtb;const vtShape* ct = c->vtb;int a1 = rt->GetArea((Shape*)r);int a2 = ct->GetArea((Shape*)c);rt->Dtor((Shape*)r);ct->Dtor((Shape*)c);return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.25.0)
project(cobj VERSION 0.1.0)if(CMAKE_C_COMPILER_ID MATCHES "Clang")
add_compile_options(-fms-extensions-Wno-microsoft-anon-tag
)
endif()add_executable(${PROJECT_NAME} ${SRC} shape.c shape.cc main.c)

运行结果:

在这里插入图片描述

这就是C实现面向对象的原理,如果有兴趣的读者可以看看glib中的gobject,不过要看懂它的代码,掌握原理是非常重要的!

如对你有帮助,欢迎点赞收藏!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/891511.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用C#构建一个论文总结AI Agent

前言 我觉得将日常生活中一些简单重复的任务交给AI Agent&#xff0c;是学习构建AI Agent应用一个很不错的开始。本次分享我以日常生活中一个总结论文的简单任务出发进行说明&#xff0c;希望对大家了解AI Agent有所帮助。任务可以是多种多样的&#xff0c;真的帮助自己提升了…

vs 2022 中xml 粘贴为Class 中,序列化出来的xml 的使用

上图是visual studio 2022 中使用的粘贴功能的菜单位置 在生成的xml 中&#xff0c;有些是类似如下类型的 [System.Serializable] [System.Xml.Serialization.XmlType] public class Item {private bool isVisibleField;private bool isVisibleFieldSpecified;[System.Xml.Se…

代际超越:方太冰箱勾勒“蛙跳模型”轮廓

文&#xff1a;互联网江湖 作者&#xff1a;志刚 每一代人&#xff0c;都有其独特的新需求、新创造和新使命。 曾经的手机领域&#xff0c;苹果以其革命性创新颠覆了诺基亚的塞班系统&#xff0c;惊艳了整个行业。而如今&#xff0c;华为凭借其三折叠和自主研发的鸿蒙系统&am…

spring-boot启动源码分析(二)之SpringApplicationRunListener

在上一篇《spring-boot启动源码分析&#xff08;一&#xff09;之SpringApplication实例构造》后&#xff0c;继续看了一个月的Spring boot启动源码&#xff0c;初步把流程看完了&#xff0c;接下来会不断输出总结&#xff0c;以巩固这段时间的学习。同时也希望能帮到同样感兴趣…

Linux-Redis哨兵搭建

环境资源准备 主机名IP端口号角色vm1192.168.64.156379/26379mastervm2192.168.64.166379/26379slavevm3192.168.64.176379/26379slave 6379为redis服务暴露端口号、26379为sentinel暴露端口号。 安装Redis # 包文件下载 wget https://github.com/redis/redis/archive/7.2.2…

MySQL 03 章——基本的SELECT语句

一、SQL概述 &#xff08;1&#xff09;SQL背景知识 SQL&#xff08;Structured Query Language&#xff0c;结构化查询语言&#xff09;是使用关系模型的数据库应用语言&#xff0c;与数据直接打交道不同的数据库管理系统生产厂商都支持SQL语句&#xff0c;但都有特有内容 …

[羊城杯 2024]1z_misc

得到FL4G.zip和天机不可泄露.txt文件&#xff0c;其中压缩包需要解压密码&#xff1a; 二十八星宿&#xff1a; 东方苍龙七宿&#xff1a;角、亢、氐、房、心、尾、箕 南方朱雀七宿&#xff1a;鬼、井、柳、星、张、翼、轸 西方白虎七宿&#xff1a;奎、娄、胃、昴、毕、觜、…

QT----------多媒体

实现思路 多媒体模块功能概述&#xff1a; QT 的多媒体模块提供了丰富的功能&#xff0c;包括音频播放、录制、视频播放和摄像头操作等。 播放音频&#xff1a; 使用 QMediaPlayer 播放完整的音频文件。使用 QSoundEffect 播放简短的音效文件。 录制音频&#xff1a; 使用 QMe…

云计算学习架构篇之HTTP协议、Nginx常用模块与Nginx服务实战

一.HTTP协议讲解 1.1rsync服务重构 bash 部署服务端: 1.安装服务 [rootbackup ~]# yum -y install rsync 2.配置服务 [rootbackup ~]# vim /etc/rsyncd.conf uid rsync gid rsync port 873 fake super yes use chroot no max connections 200 timeout 600 ignore erro…

《Vue3实战教程》42:Vue3TypeScript 与组合式 API

如果您有疑问&#xff0c;请观看视频教程《Vue3实战教程》 TypeScript 与组合式 API​ 这一章假设你已经阅读了搭配 TypeScript 使用 Vue 的概览。 为组件的 props 标注类型​ 使用 <script setup>​ 当使用 <script setup> 时&#xff0c;defineProps() 宏函数支…

Diffusion Transformer(DiT)——将扩散过程中的U-Net换成ViT:近频繁用于视频生成与机器人动作预测(含清华PAD详解)

前言 本文最开始属于此文《视频生成Sora的全面解析&#xff1a;从AI绘画、ViT到ViViT、TECO、DiT、VDT、NaViT等》 但考虑到DiT除了广泛应用于视频生成领域中&#xff0c;在机器人动作预测也被运用的越来越多&#xff0c;加之DiT确实是一个比较大的创新&#xff0c;影响力大&…

2024年12月 Scratch 图形化(二级)真题解析#中国电子学会#全国青少年软件编程等级考试

Scratch图形化等级考试(1~4级)全部真题・点这里 一、单选题(共25题,共50分) 第 1 题 小猫初始位置和方向如下图所示,下面哪个选项能让小猫吃到老鼠?( ) A. B. C.

LLaMA详解

LLaMA 进化史 大规模语言模型(Large Language Model, LLM)的快速发展正在以前所未有的速度推动人工智能(AI)技术的进步。 作为这一领域的先行者, Meta在其LLaMA(Large Language Model Meta AI)系列模型上取得了一系列重大突破。 近日, Meta官方正式宣布推出LLaMA-3, 作为继LL…

SpringMVC(六)拦截器

目录 1.什么是拦截器 2.拦截器和过滤器有哪些区别 3.拦截器方法 4.单个拦截器的执行流程 5.使用拦截器实现用户登录权限验证&#xff08;实例&#xff09; 1.先在html目录下写一个login.html文件 2.在controller包下写一个LoginController文件 3.加拦截器 1.创建一个conf…

推理加速:投机采样经典方法

一 SpecInfer 基于模型 SpecInfer&#xff08;[2305.09781] SpecInfer: Accelerating Generative Large Language Model Serving with Tree-based Speculative Inference and Verification&#xff09; SpecInfer 投机采样利用多个小型模型&#xff08;SSM&#xff09;快速生…

最好用的图文识别OCR -- PaddleOCR(1) 快速集成

最近在项目中遇到了 OCR 的需求&#xff0c;希望能够实现高效而准确的文字识别。由于预算限制&#xff0c;我并未选择商业付费方案&#xff0c;而是优先尝试了开源工具。一开始&#xff0c;我测试了 GOT-OCR2.0&#xff0c;但由于我的 Mac 配置较低&#xff0c;不支持 GPU 运算…

FFmpeg:详细安装教程与环境配置指南

FFmpeg 部署完整教程 在本篇博客中&#xff0c;我们将详细介绍如何下载并安装 FFmpeg&#xff0c;并将其添加到系统的环境变量中&#xff0c;以便在终端或命令行工具中直接调用。无论你是新手还是有一定基础的用户&#xff0c;这篇教程都能帮助你轻松完成 FFmpeg 的部署。 一、…

Spring SpEL表达式由浅入深

标题 前言概述功能使用字面值对象属性和方法变量引用#this 和 #root变量获取类的类型调用对象(类)的方法调用类构造器类型转换运算符赋值运算符条件(关系)表达式三元表达式Elvis 操作符逻辑运算instanceof 和 正则表达式的匹配操作符 安全导航操作员数组集合(Array 、List、Map…

“AI人工智能软件开发公司:创新技术,引领未来

大家好&#xff01;今天我们来聊聊一个充满未来感的话题——AI人工智能软件开发公司。这个公司&#xff0c;用大白话说&#xff0c;就是专门研究和开发人工智能软件的地方&#xff0c;它们用最新的技术帮我们解决问题&#xff0c;让生活和工作变得更智能、更便捷。听起来是不是…

常见中间件漏洞复现

1.tomcat 1.1 CVE-2017-12615(put上传) 当在Tomcat的conf&#xff08;配置目录下&#xff09;/web.xml配置文件中添加readonly设置为false时&#xff0c;将导致该漏洞产 ⽣&#xff0c;&#xff08;需要允许put请求&#xff09; , 攻击者可以利⽤PUT方法通过精心构造的数据包…