Skip to content

函数

3.1 函数基础

3.1.1 函数三大属性

函数的三大属性:输入参数、返回值、函数名,编译器看到此三大属性便会判定为函数。

  • 函数名 :地址标签
  • 输入参数 :可以是多个
  • 返回值 :最多一个
C
type func_name(type arg_1, type arg2, ...)
{
    ...
}

  • 为什么说函数名本质上是一个地址标签?

测试程序:

C
#include <stdio.h>

void func(void)
{
    printf("[INFO] hello world\n");
}

int main(void)
{
    func();
    return 0;
}

查看反汇编码:

Bash
objdump -d  main > bin.s

func标签指向的地址 0x1149 ,可以通过<func>访问 0x1149 地址下的代码。

Bash
# 反汇编码
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 参数传递的实质

函数的参数输入和参数返回的过程实质上是: 按值传递,进行内存拷贝

例如下面这个函数:

C
#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 按值传递

我们已经知道函数是按值传递,进行内存拷贝; 可以对数据进行隔离和保护 。下面通过一个例子,更深刻的理解按值传递的特性。

测试程序:

C
#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;
}

输出结果:

Text Only
[INFO] 10

输出分析:

  • 虽然程序并未报错,但是 func 函数的使用是非法的。
  • 函数执行完 pa 作为局部变量均被销毁,此时 p 指向的内存是非法的。

如下图所示:

画板

3.1.4 按地址传递

按值传递可以的对数据进行很好的保护,但同时限制了一些操作,比如:

  • 结构体的内存拷贝会有很大的内存开销。
  • 按值传递做不到修改原始数据。
  • 函数需要返回多个参数。

以上这些问题均可以使用指针 按地址传递 解决。


  • 经典函数,交换两个变量的值。
C
#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;
}

输出结果:

Text Only
INFO] num_1:15 num_2:10

  • 指针做输出参数
C
#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;
}

输出结果:

Text Only
[INFO] num_1:10 num_2:15 sum:35

输出分析:

  • 在嵌入式开发中,将返回值做运行结果判断,指针做输出参数是一种十分常用的手段。

  • 修改指针指向的地址(使用二级指针)
C
#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;
}

输出结果:

Text Only
[INFO] 0x7ffead05c458 0x7ffead05c45c
[INFO] 0x7ffead05c458
[INFO] 0x7ffead05c45c

  • 连续空间传递 - 对于非字符空间:要传入可操作的内存空间大小。 - 对于字符空间:字符会以 \0 为结束符,可以不传入可操作大小。
C
#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;
}

输出结果:

Text Only
hello world
12346

3.2 C 与面向对象

3.2.1 C 与封装

  • 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 语言版
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++版
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 语言版
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++版
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 语言版
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:清理操作,必须调用
C
#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++版
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 语言版
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文件,但是遇到一个问题:

我的话题订阅需要一个回调函数,但是回调函数中需要执行特定的业务,且又其他函数需要绑定回调函数。我希望自己写的驱动可以适配各种业务需求,那么我的就不能将回调函数写死,同时不能直接在驱动文件修改回调函数,这样会使得驱动文件代码臃肿。因此,我将回调函数写成 弱连接函数 ,这样我只需要为特定业务单独重写这个函数即可,无需修改驱动文件。

  • 驱动文件的回调函数如下:
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);
}
  • 业务文件的回调函数如下:
C
/**
 * @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 依赖倒置原则

核心思想 :高层模块不应依赖低层模块,两者都应依赖抽象接口。

  • 依赖抽象接口,而不是特定的类或函数。
  • 依赖抽象接口,而不是特定的硬件。