C语言中的面向对象思想

1.静态数组管理多个结构体变量

对于c语言当一个结构体要创建多个变量时,若我们分开管理就会比较难以管理,但是我们可以通过结构体数组(对象数组)的形式对其进行管理。我们看下面这段程序:

cpp 复制代码
#include <stdio.h>
#include <stdint.h>

typedef struct {
	uint8_t port; //引脚接口      0:GPIOA  1:GPIOB
	uint8_t pin;  //引脚标号      0-15
	uint8_t mode; //输入输出模式  0:输入   1:输出
	uint8_t state;//引脚状态      0:低电平 1:高电平
}GPIO_Pin_t;

GPIO_Pin_t gpio_conf[8] = {
	//{port ,pin ,mode ,state}GPIO参数配置列表
	  {0    ,0   ,0    ,0},
	  {0    ,1   ,0    ,0},
	  {0    ,2   ,1    ,0},
	  {0    ,3   ,1    ,0},
	  {1    ,0   ,0    ,0},
	  {1    ,1   ,0    ,0},
	  {1    ,2   ,1    ,0},
	  {1    ,3   ,1    ,0}
};

void GPIO_Write_Pin(uint8_t index, uint8_t state)
{
	if (index < 0 | index >= 8) return;
	gpio_conf[index].state = state;
}

void GPIO_init(void)
{
	for (int i = 0; i < 8; i++)
	{
		if (1 == gpio_conf[i].mode)
		{
			GPIO_Write_Pin(i,1);
		}
	}
}

int main(void)
{
	GPIO_init();    //批量处理
	return 0;
}

可见当我们需要管理多个gpio时我们就可以通过一个数组的方式将其管理在一个数组内,并且通过对数组的遍历我们还可以批量处理数据,方便了我们的管理。

2.手写一个类似hal库的串口设备库

在我们常使用的hal库中也有着c语言面向对象的思想的体现,下面我们来实现一个串口类。我们先来看.h文件:

cpp 复制代码
#ifndef   USART_H
#define   USART_H

#include "stm32f4xx.h"
#include <stdint.h>

//前向声明
typedef struct USART_Device USART_Device;

//串口类定义
struct USART_Device{
	USART_TypeDef* hw;         //硬件基地址
	uint32_t baurate;          //波特率
	uint8_t tx_buffer[256];    //发送buffer
	uint8_t rx_buffer[256];    //接收buffer

	void (*init) (USART_Device* self);                                   //串口初始化
	void (*send) (USART_Device* self, const char* data, uint32_t len);   //发送数据
	int  (*recv) (USART_Device* self, char* buffer, uint32_t max_len);   //接收数据
	void (*deint)(USART_Device* self);                                   //销毁创建的对象

};



#endif // USART_H

在这个文件中我们定义出了串口类,类中包含了属性和方法。下面我们来看.c文件中一些方法的具体实现:

cpp 复制代码
#include "usart.h"
#include <string.h>

static void usart_init(USART_Device* self)
{
	//时钟配置
	if (self->hw == USART1)
	{
		RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
	}

	//波特率配置
	uint32_t usartdiv = 42000000 / (16 * self->baurate);
	self->hw->BRR = usartdiv;

	//串口使能
	self->hw->CR1 = USART_CR1_UE |    //使能串口
		            USART_CR1_TE |    //使能发送
		            USART_CR1_RE;     //使能接收

	//清空缓存区
	menset(self->tx_buffer, 0, 256);
	menset(self->rx_buffer, 0, 256);
}

static void usart_send(USART_Device* self, const char* data, uint32_t len)
{
	for (int i = 0; i < len; i++)
	{
		//发送寄存器为空
		while (!(self->hw->SR & UART_SR_TXE));
		//写入
		self->hw->DR = data[i];
		//发送完成
		while (!(self->hw->SR & UART_SR_TC));
	}
}

USART_Device* usart_create(USART_TypeDef* hw, uint32_t baurate)
{
	//分配内存
	USART_Device* dev = (USART_Device*)malloc(sizeof(USART_Device));
	if (dev == NULL) return NULL;

	//初始化成员
	dev->hw = hw;
	dev->baurate = baurate;
	menset(dev->tx_buffer, 0, 256);
	menset(dev->rx_buffer, 0, 256);

	//绑定方法
	dev->init  = usart_init;
	dev->send  = usart_send;
	dev->recv  = usart_recv;
	dev->deint = usart_deint();

	return dev;
}

可见上面的程序与标准的hal库一样都实现了对象化的处理,这也是c语言中面向对象思想的体现。我们来看看main函数的实现:

cpp 复制代码
#include "usart.h"

int main(void)
{
	USART_Device* uart1 = usart_create(USART1, 115200);
	USART_Device* uart2 = usart_create(USART2, 9600);

	uart1->init(uart1);
	uart2->init(uart2);

	uart1->send(uart1, "hello usart1", 12);
	uart2->send(uart2, "hello usart2", 12);

	return 0;
}

可见在mian函数中我们创建了两个串口对象,并分别使用同一个接口实现了对不同对象的初始化和发送数据,可见对象化的思想使我们的程序更加清晰明了啦。

3.解决对象内存分配使用malloc不高效的问题

我们知道当我们每创建一个对象时使用malloc,需要向系统申请一块空间,每次使用完毕后还需要使用free来进行销毁,这就会导致程序不够高效。对此我们可以通过使用对象池来解决这个问题,首先我们要定义出一个对象池,程序如下:

cpp 复制代码
typedef struct {
	int is_free;    //0:有对象使用,1:没有对象使用
	char obj[256];  //对象使用空间
}Object;

typedef struct {
	Object* pool;  //对象池地址
	int pool_size; //池容量
	int pool_count;//池使用量
};

对象池定义好后我们需要对其进行操作,程序如下:

cpp 复制代码
Objectpool* pool_init(int size)
{
	//创建管理池
	Objectpool* p = (Objectpool*)malloc(sizeof(Objectpool));
	if (p == NULL)
	{
		return NULL;
	}

	//创建池
	p->pool = (Object*)malloc(size * sizeof(Object));
	if (p->pool == NULL)
	{
		free(p);
		return NULL;
	}

	//管理池参数初始化
	p->pool_size = size;
	p->pool_count = 0;

	//池参数初始化
	for (int i = 0; i < size; i++)
	{
		p->pool[i].is_free = 1;
	}
}

Object* object_alloc(Objectpool* p)  //向对象池中获取存储对象的空间
{
	if (p == NULL)
	{
		return NULL;
	}
	for (int i = 0; i < p->pool_size; i++)
	{
		if (p->pool[i].is_free == 1)
		{
			p->pool[i].is_free = 0; //标志位置为0,表示已被使用
			p->pool_count++;        //表示内存时使用量加1
			return &p->pool[i];
		}
	}
	return NULL;
}

void object_free(Objectpool* p, Object* obj) //将对象池中的对象空间还回对象池
{
	if (p == NULL | obj == NULL)
	{
		return;
	}
	if (obj >= p->pool && obj < p->pool + p->pool_size)//通过和p指向的内存池地址相比较,确保该对象空间是从该对象池中申请的
	{
		obj->is_free = 1;
		p->pool_count--;            //表示内存使用量减1
	}
}

void pool_destory(Objectpool* p)
{
	if (p == NULL)
	{
		return;
	}
	free(p->pool);
	free(p);
}

我们分别实现了对象池的初始化,向对象池中获取一个对象空间,将从对象池中获取的对象空间还回对应对象池和销毁对象池的基本操作。下面我们在main函数中使用一下该对象池。

cpp 复制代码
int main(void)
{
	//创建对象池
	Objectpool* p = pool_init(7);
	if (p == NULL)
	{
		printf("对象池分配失败\n");
		return -1;
	}
	//创建对象,并从对象池中获取存储空间
	Object* obj1 = object_alloc(p);
	Object* obj2 = object_alloc(p);
	Object* obj3 = object_alloc(p);
	if (obj1 && obj2 && obj3)
	{
		printf("成功分配三个对象,对象池已用%d/7\n", p->pool_count);
	}
	//释放对象,将从对象池中获取的空间还回对象池
	object_free(p, obj3);
	printf("成功释放一个对象,对象池已用%d/7\n", p->pool_count);
	//再创建对象,从对象池获取存储空间
	Object* obj4 = object_alloc(p);
	if (obj4 != NULL)
	{
		printf("成功再次分配一个对象,对象已用%d/7\n", p->pool_count);
	}
	//对象池使用完毕,销毁对象池
	pool_destory(p);

	return 0;
}

我们在主函数中分别进行上述操作,验证对象池的操作程序是否正确。运行后结构如下:

可见对象的操作没有问题。

4.单例模式的使用

当我们对同一个硬件进行初始化时,若不使用单列模式的话,每一个实例的对象都相当于是用的不同的硬件。当一个硬件以及被其中一个对象初始化并且开始运行时,其他的对象并不会知道该硬件的状态,然后就可能导致该硬件执行到一般就又被初始化了,或者之间的混乱使用导致出现问题。对此我们就可以使用单例模式来解决。我们看下面的程序:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

typedef struct {
	int port;
	int pageCount;
}Pinter;

Pinter* get_pinter(void)
{
	static Pinter pinter;
	static int init_flag = 0;
	if (!init_flag)
	{
		pinter.port = 0x378;
		pinter.pageCount = 5;
		init_flag = 1;
	}
	return &pinter;
}

int main(void)
{
	Pinter* p1 = get_pinter();
	Pinter* p2 = get_pinter();
	if (p1 == p2)
		printf("p1=p2\n");
	else
		printf("p1!=p2\n");
	return 0;
}

我们通过static关键字的使用就实现了单例模式,因为static修饰后变量只会被初始化一次,不会出现多次初始化的问题,其中pinter也只有一个,函数返回的都是同一个pinter即可以实现了其中信息的共享也不会因为多个对象一起操作同一个硬件出现问题。

5.浅拷贝与深拷贝问题

浅拷贝和深拷贝对于基本类型都是直接复制值,但是对于引用类型或者指针浅拷贝只是将指针的值复制过去了,并没有将指针指向的内容复制,深拷贝则是重新开辟一个内存然后将指针指向的内容进行复制。我们来看看下面这段程序:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
	char* name;
	int   age;
}Person;

Person shallow_copy(Person* src)
{
	Person p;
	p.name = src->name;
	p.age = src->age;
	return p;
}

Person deep_copy(Person* src)
{
	Person p;
	p.age = src->age;
	//创建一块新的内存
	p.name = (char*)malloc(sizeof(src->name));
	//复制内容
	strcpy(p.name, src->name);
	return p;
}


int main(void)
{
	Person p1;
	p1.age = 18;
	p1.name = (char*)malloc(10);
	strcpy(p1.name, "张三");
	Person p2 = shallow_copy(&p1);
	Person p3 = deep_copy(&p1);

	strcpy(p1.name,"李四");
	printf("p1.name:%s\n", p1.name);
	printf("p2.name:%s\n", p2.name);
	printf("p3.name:%s\n", p3.name);

	strcpy(p2.name, "王五");
	printf("p1.name:%s\n", p1.name);
	printf("p2.name:%s\n", p2.name);
	printf("p3.name:%s\n", p3.name);

	free(p1.name);
	free(p3.name);

	return 0;
}

可见我们分别对p2和p3实现了浅拷贝和深拷贝,运行后结果如下:

可见p2和p1的name指向的是同一块内存,所以不管是通过p1来修改该内存下的内容还是p2来修改其中的内容两者都会被修改。故浅拷贝容易造成内容不安全,指针悬空,重复释放等问题,所以我们一般当有指针或引用赋值操作时都使用深拷贝,但是在基本变量复制,或者不会对数据内容进行修改等情况下也可以使用浅拷贝,因为虽然深拷贝是实现了数据的安全性和解决了指针悬空等问题,但是也消耗了更多的空间和时间。

相关推荐
lionliu05193 小时前
执行上下文 (Execution Context)
开发语言·前端·javascript
nbsaas-boot3 小时前
JWT 与 Session 的实用场景分析:从架构边界到工程落地
java·开发语言·架构
Tim_103 小时前
【C++入门】03、C++整型
java·开发语言·jvm
花月C4 小时前
基于Redis的BitMap数据结构实现签到业务
数据结构·数据库·redis
fpcc4 小时前
跟我学C++中级篇——循环展开的分析
c++·优化
盼哥PyAI实验室4 小时前
Python编码处理:解决12306项目的中文乱码问题
开发语言·python
一杯美式 no sugar4 小时前
数据结构——单向无头不循环链表
c语言·数据结构·链表
ss2734 小时前
阻塞队列:三组核心方法全对比
java·数据结构·算法
li星野4 小时前
打工人日报#20251215
单片机·嵌入式硬件