函数
3.1 函数基础¶
3.1.1 函数三大属性¶
函数的三大属性:输入参数、返回值、函数名,编译器看到此三大属性便会判定为函数。
- 函数名 :地址标签
- 输入参数 :可以是多个
- 返回值 :最多一个
- 为什么说函数名本质上是一个地址标签?
测试程序:
#include <stdio.h>
void func(void)
{
printf("[INFO] hello world\n");
}
int main(void)
{
func();
return 0;
}
查看反汇编码:
func
标签指向的地址 0x1149 ,可以通过<func>
访问 0x1149 地址下的代码。
# 反汇编码
0000000000001149 <func>:
1149: f3 0f 1e fa endbr64
114d: 55 push %rbp
114e: 48 89 e5 mov %rsp,%rbp
1151: 48 8d 3d ac 0e 00 00 lea 0xeac(%rip),%rdi # 2004 <_IO_stdin_used+0x4>
1158: e8 f3 fe ff ff callq 1050 <puts@plt>
115d: 90 nop
115e: 5d pop %rbp
115f: c3 retq
0000000000001160 <main>:
1160: f3 0f 1e fa endbr64
1164: 55 push %rbp
1165: 48 89 e5 mov %rsp,%rbp
1168: e8 dc ff ff ff callq 1149 <func>
116d: b8 00 00 00 00 mov $0x0,%eax
1172: 5d pop %rbp
1173: c3 retq
1174: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
117b: 00 00 00
117e: 66 90 xchg %ax,%ax
3.1.2 参数传递的实质¶
函数的参数输入和参数返回的过程实质上是: 按值传递,进行内存拷贝 。
例如下面这个函数:
#include <stdio.h>
int func(int a)
{
a++;
return a;
}
int main(void)
{
int num = 10;
int retval;
retval = func(num);
printf("[INFO] %d\n", retval);
return 0;
}
func
函数的参数传递过程如下图:
- 将实参
num
传入函数func
后,函数会在栈上为形参分配新内存,用形参变量a
表示,并将实参的值 逐字节复制到形参的内存空间 。 - 将形数
a
的值返回时,将形参a
的值复制给实参retval
的内存空间。
3.1.3 按值传递¶
我们已经知道函数是按值传递,进行内存拷贝; 可以对数据进行隔离和保护 。下面通过一个例子,更深刻的理解按值传递的特性。
测试程序:
#include <stdio.h>
// 指针函数
int *func(int a)
{
int *p;
p = &a;
return p;
}
int main(void)
{
int num = 10;
int *retval;
retval = func(num);
printf("[INFO] %d\n", *retval);
return 0;
}
输出结果:
输出分析:
- 虽然程序并未报错,但是
func
函数的使用是非法的。 - 函数执行完
p
和a
作为局部变量均被销毁,此时p
指向的内存是非法的。
如下图所示:
3.1.4 按地址传递¶
按值传递可以的对数据进行很好的保护,但同时限制了一些操作,比如:
- 结构体的内存拷贝会有很大的内存开销。
- 按值传递做不到修改原始数据。
- 函数需要返回多个参数。
以上这些问题均可以使用指针 按地址传递 解决。
- 经典函数,交换两个变量的值。
#include <stdio.h>
void func(int *a, int *b)
{
// 临时变量,记录a/b的值
int temp;
temp = *a;
*a = *b;
*b = temp;
}
int main(void)
{
int num_1 = 10;
int num_2 = 15;
func(&num_1, &num_2);
printf("[INFO] num_1:%d num_2:%d\n", num_1, num_2);
return 0;
}
输出结果:
- 指针做输出参数
#include <stdio.h>
/**
* @brief 加法运算函数
*
* @params sum 输出参数,存放运算结果
* @params a 输入参数
* @params b 输入参数
*
* @retval 正常返回0,错误返回-1
* */
int func(int *sum, int a, int b)
{
// 若是有错误返回-1
if ((a + b) == 0) {
return -1;
}
*sum = a + b + 10;
// 函数正常结束,返回0
return 0;
}
int main(void)
{
int num_1 = 10;
int num_2 = 15;
int sum = 0;
func(&sum, num_1, num_2);
printf("[INFO] num_1:%d num_2:%d sum:%d\n", num_1, num_2, sum);
return 0;
}
输出结果:
输出分析:
- 在嵌入式开发中,将返回值做运行结果判断,指针做输出参数是一种十分常用的手段。
- 修改指针指向的地址(使用二级指针)
#include <stdio.h>
void func(int **p, int *b)
{
*p = b;
}
int main(void)
{
int a = 10;
int b = 23;
printf("[INFO] %p %p\n", &a, &b);
int *ptr = &a;
printf("[INFO] %p\n", ptr);
func(&ptr, &b);
printf("[INFO] %p\n", ptr);
return 0;
}
输出结果:
- 连续空间传递
- 对于非字符空间:要传入可操作的内存空间大小。
- 对于字符空间:字符会以
\0
为结束符,可以不传入可操作大小。
#include <stdio.h>
void func_1(const char *str)
{
int i = 0;
while (str[i] != '\0') {
printf("%c", str[i]);
i++;
}
printf("\n");
}
void func_2(int *num, int len)
{
for (int i = 0 ; i < len ; i++) {
printf("%d", num[i]);
}
printf("\n");
}
int main(void)
{
char *s = "hello world";
int n[] = { 1, 2, 3, 4, 6 };
func_1(s);
func_2(n, 5);
return 0;
}
输出结果:
3.2 C 与面向对象¶
3.2.1 C 与封装¶
- C++版
#include <iostream>
#include <cstring>
class Person
{
public:
// 构造函数
Person(int age, const char *name);
// 成员函数
void get_age(int *age);
void get_name(char *name);
protected:
int p_age;
char p_name[20];
};
Person::Person(int age, const char *name)
{
p_age = age;
strcpy(p_name, name);
}
void Person::get_age(int *age)
{
*age = p_age;
}
void Person::get_name(char *name)
{
strcpy(name, p_name);
}
int main(void)
{
char name[20];
int age;
Person p(18, "lzh");
p.get_age(&age);
p.get_name(name);
std::cout << "name:" << name << " age:" << age << std::endl;
return 0;
}
- C 语言版
#include <stdio.h>
#include <string.h>
typedef struct Person {
int p_age;
char p_name[20];
void (*get_age)(struct Person *p, int *age);
void (*get_name)(struct Person *p, char *name);
} Person;
void get_age(Person *p, int *age)
{
*age = p->p_age;
}
void get_name(Person *p, char *name)
{
strcpy(name, p->p_name);
}
Person p1 = {
.p_age = 18,
.p_name = "lzh",
.get_age = get_age,
.get_name = get_name,
};
int main(void)
{
int age;
char name[20];
Person *person = &p1;
person->get_age(person, &age);
person->get_name(person, name);
printf("age:%d name:%s\n", age, name);
return 0;
}
3.2.2 C 与继承¶
- C++版
#include <iostream>
/* 基类 */
class Shape
{
public:
void set_values(int w, int h);
protected:
int width; // 宽度
int height; // 高度
};
/* 编写成员函数 */
void Shape::set_values(int w, int h)
{
width = w;
height = h;
}
/* 派生类,继承基类 */
class Rectangle : public Shape
{
public:
int get_area(void)
{
return width * height;
}
};
int main(void)
{
// 类的初始化
Rectangle rtg;
// 调用成员函数
rtg.set_values(10, 10);
int area = rtg.get_area();
std::cout << "矩阵的面积:" << area << std::endl;
return 0;
}
- C 语言版
#include <stdio.h>
/* 模拟基类
*
* 使用typedef struct 后面必须加上别名
* 才能在结构体中使用本结构体作为函数指针的参数
* */
typedef struct Shape {
int width;
int height;
// 函数指针
void (*set_values)(struct Shape *s, int w, int h);
} Shape;
/* 编写成员函数 */
void set_values(Shape *s, int w, int h)
{
s->width = w;
s->height = h;
}
/* 派生类,模拟继承 */
typedef struct Rectangle {
Shape shape;
// 函数指针
int (*get_area)(struct Shape *s);
} Rectangle;
/* 编写成员函数 */
int get_area(Shape *s)
{
return (s->width * s->height);
}
int main(void)
{
// 模拟类的初始化
Rectangle rtg = {
// 模拟基类的成员函数
.shape.set_values = set_values,
// 模拟派生类的成员函数
.get_area = get_area,
};
// 调用成员函数
rtg.shape.set_values(&rtg.shape, 10, 10);
int area = rtg.get_area(&rtg.shape);
printf("矩形的面积:%d\n", area);
return 0;
}
3.2.3 C 与多态¶
- C++版
#include <iostream>
#include <cstring>
class Person
{
public:
// 构造函数
Person(const char *name, int age)
{
strcpy(p_name, name);
p_age = age;
}
// 成员函数
virtual void get_name(char *name)
{
strcpy(name, p_name);
}
protected:
int p_age;
char p_name[20];
};
class Usa_Person : public Person
{
public:
Usa_Person(const char *name, int age) : Person(name, age)
{
}
void get_name(char *name)
{
sprintf(name, "%s-usa", p_name);
}
};
class China_Person : public Person
{
public:
China_Person(const char *name, int age) : Person(name, age)
{
}
void get_name(char *name)
{
sprintf(name, "%s-cn", p_name);
}
};
void get_person_name(Person *p, char *name)
{
p->get_name(name);
printf("%s\n", name);
}
int main(void)
{
char name[20];
Usa_Person usa_p("lzh", 18);
China_Person cn_p("lzh", 18);
get_person_name(&usa_p, name);
get_person_name(&cn_p, name);
}
- C 语言版
#include <stdio.h>
#include <string.h>
/* 基类 */
typedef struct Person {
int p_age;
char p_name[20];
void (*get_name)(struct Person *p, char *name);
} Person;
void get_name(Person *p, char *name)
{
strcpy(name, p->p_name);
}
/* 派生类 */
typedef struct Usa_Person {
Person p;
} Usa_person;
void get_usa_name(Person *p, char *name)
{
sprintf(name, "%s-usa", p->p_name);
}
/* 派生类 */
typedef struct China_Person {
Person p;
} China_Person;
void get_china_name(Person *p, char *name)
{
sprintf(name, "%s-cn", p->p_name);
}
/* 初始化 */
Usa_person usa_p = {
.p = {
.p_age = 18,
.p_name = "lzh",
.get_name = get_usa_name,
}
};
China_Person cn_p = {
.p = {
.p_age = 18,
.p_name = "lzh",
.get_name = get_china_name,
}
};
void get_person_name(Person *p, char *name)
{
p->get_name(p, name);
printf("%s\n", name);
}
int main(void)
{
char name[20];
get_person_name(&usa_p.p, name);
get_person_name(&cn_p.p, name);
}
3.3 C 与重载¶
3.3.1 可变参数函数¶
实现参数可变的核心机制:
va_list
:用于访问可变参数的变量类型va_start
:初始化参数列表va_arg
:获取参数列表中下一个参数va_end
:清理操作,必须调用
#include <stdio.h>
#include <stdarg.h>
double average(int num, ...)
{
if (num <= 0) { return 0.0; }
// 声明参数列表
va_list args;
// 初始化参数列表
va_start(args, num);
double sum = 0.0;
for (int i = 0 ; i < num ; i++) {
// 获取参数列表的下一个double参数,进行求和运算
sum += va_arg(args, double);
}
// 清理操作
va_end(args);
return sum/num;
}
// 简易版printf实现
void my_printf(const char *format, ...) {
va_list args;
va_start(args, format);
// 遍历格式字符串
for (const char *p = format; *p != '\0'; p++) {
if (*p != '%') {
putchar(*p);
continue;
}
// 处理格式符
p++; // 跳过%符号
switch (*p) {
case 'd': {
int i = va_arg(args, int);
printf("%d", i);
break;
}
case 'f': {
double d = va_arg(args, double);
printf("%.2f", d); // 限制两位小数
break;
}
case 'c': {
// 注意:char会被提升为int
int c = va_arg(args, int);
putchar(c);
break;
}
case 's': {
char *s = va_arg(args, char*);
printf("%s", s);
break;
}
case 'X':
case 'x': {
unsigned int u = va_arg(args, unsigned int);
printf("%X", u);
break;
}
case '%': {
putchar('%');
break;
}
default: {
putchar('%');
putchar(*p);
break;
}
}
}
va_end(args);
}
int main(void)
{
double aver = average(3, 20.0, 30.0, 50.0);
printf("%f\n", aver);
return 0;
}
3.3.2 回调函数¶
- C++版
#include <iostream>
#include <cstring>
class Swap
{
public:
void exchange(int *a, int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
void exchange(double *a, double *b)
{
double temp = *a;
*a = *b;
*b = temp;
}
};
int main(void)
{
double a = 12.1;
double b = 13.6;
int c = 10;
int d = 12;
printf("[INFO] %f %f %d %d\n", a, b, c, d);
Swap s;
s.exchange(&a, &b);
s.exchange(&c, &d);
printf("[INFO] %f %f %d %d\n", a, b, c, d);
}
- C 语言版
#include <stdio.h>
void exchange_int(void *a, void *b)
{
int temp = *(int *)a;
*(int *)a = *(int *)b;
*(int *)b = temp;
}
void exchange_double(void *a, void *b)
{
double temp = *(double *)a;
*(double *)a = *(double *)b;
*(double *)b = temp;
}
// 声明一个函数指针
typedef void (*exchange)(void *a, void *b);
void swap(exchange ex, void *a, void *b)
{
ex(a, b);
}
int main(void)
{
double a = 12.1;
double b = 13.6;
int c = 10;
int d = 12;
printf("[INFO] %f %f %d %d\n", a, b, c, d);
swap(exchange_double, &a, &b);
swap(exchange_int, &c, &d);
printf("[INFO] %f %f %d %d\n", a, b, c, d);
}
3.3.3 弱连接函数¶
弱连接函数在嵌入式中很常用,例如:STM32 的 HAL 库中,定时器回调函数均是采用弱连接函数,可以供开发者重写符合业务的回调函数。
下面举一个实际使用案例:
在我做 OpenHarmony 的 MQTT 开发时,我单独将 MQTT 驱动封装为一个 mqtt_task.c
文件,但是遇到一个问题:
我的话题订阅需要一个回调函数,但是回调函数中需要执行特定的业务,且又其他函数需要绑定回调函数。我希望自己写的驱动可以适配各种业务需求,那么我的就不能将回调函数写死,同时不能直接在驱动文件修改回调函数,这样会使得驱动文件代码臃肿。因此,我将回调函数写成 弱连接函数 ,这样我只需要为特定业务单独重写这个函数即可,无需修改驱动文件。
- 驱动文件的回调函数如下:
/**
* @brief 订阅话题的任务函数,用于处理具体业务
*
* 这是一个虚函数,可以在其他文件重写,注意参数一致
*
* @param payload 订阅话题下发的json格式数据
*
*/
__attribute__((weak)) void SubscribeTopicCallback(char* payload)
{
printf("[INFO] CallBack\r\n");
}
/* 主题订阅回调函数 */
void SubscribeTopicHander(MessageData *data)
{
// (MQTTClient.h)
// printf("[INFO] Message Size:%d\r\n", (int)data->message->payloadlen);
printf("[INFO] Message arrived on topic:\r\n[INFO] Topic: %.*s\r\n",
(int)data->topicName->lenstring.len, (char *)data->topicName->lenstring.data);
printf("[INFO] payload: %.*s\r\n", (int)data->message->payloadlen, (char *)data->message->payload);
SubscribeTopicCallback((char *)data->message->payload);
}
- 业务文件的回调函数如下:
/**
* @brief 重写订阅话题的业务函数
*
* @param payload 订阅话题下发的json格式数据
*
*/
void SubscribeTopicCallback(char* payload)
{
/* 处理订阅的主题内容 */
cJSON *root = cJSON_Parse((char *)payload);
if (!root)
{
printf("[ERROR] JSON parse error\r\n");
return;
}
// 1. 提取 object_device_id
cJSON *obj_id = cJSON_GetObjectItem(root, "object_device_id");
if (cJSON_IsString(obj_id))
{
printf("[INFO] Device ID: %s\r\n", obj_id->valuestring);
}
// 2. 处理 services 数组
cJSON *services_arr = cJSON_GetObjectItem(root, "services");
if (cJSON_IsArray(services_arr) && cJSON_GetArraySize(services_arr) > 0)
{
// 解析 services 数组第一个参数(多参数时需要套 for 循环)
cJSON *first_entry = cJSON_GetArrayItem(services_arr, 0);
// 2.1 提取 service_id
cJSON *service_id = cJSON_GetObjectItem(first_entry, "service_id");
if (cJSON_IsString(service_id))
{
printf("[INFO] Service ID: %s\r\n", service_id->valuestring);
}
// 2.2 处理 properties 属性
cJSON *properties = cJSON_GetObjectItem(first_entry, "properties");
// 处理 lock 属性
if (properties)
{
cJSON *lock = cJSON_GetObjectItem(properties, "lock");
if (cJSON_IsBool(lock))
{
// 日志输出
printf("[INFO] Desired lock status (bool): %s\r\n", lock->valueint ? "True" : "False");
// 记录智能锁状态
bool lock_status = lock->valueint;
// 发布事件标志
osEventFlagsSet(g_smartLockEvent, (1 << (int)lock_status));
}
}
}
cJSON_Delete(root);
}
3.4 SOLID 设计原则¶
SOLID 原则是面向对象编程的五大核心设计原则,由 Robert C. Martin(又称 Uncle Bob)提出。这些原则共同构成了创建可维护、可扩展、可重用软件的基础框架。
3.4.1 单一责任原则¶
核心思想 :一个 class
应该只做一件事,一 个 class
应该只有一个变化的理由。
- 每个类/模块只负责一个功能领域。
- 实现“高内聚,低耦合”,相同的功能要高度汇聚到一个类,关系不大的功能要进行分离解耦。
3.4.2 开闭原则¶
核心思想 :class
应该对扩展开放对修改关闭。
- 通过添加新代码(而非修改旧代码)来扩展功能。
- 使用抽象(接口、抽象类)定义稳定接口。
- 使用多态实现行为变化。
3.4.3 里氏替换原则¶
核心思想 :子类应该能够完全替换其基类。
- 任何传入基类的方法传入子类,方法不应有异常。
3.4.4 接口隔离原则¶
核心原则 :接口应该具备独立性,不应该强制实现他们不需要的函数。
- 创建专注的小接口而不是通用的大接口。
- 为不同的客户端需求提供特定接口。
3.4.5 依赖倒置原则¶
核心思想 :高层模块不应依赖低层模块,两者都应依赖抽象接口。
- 依赖抽象接口,而不是特定的类或函数。
- 依赖抽象接口,而不是特定的硬件。