Skip to content

宏和关键字

2.1 宏

2.1.1 宏函数

测试程序:

C
include <stdio.h>

#define FUNC_1(a, b) a*b
#define FUNC_2(a, b) (a*b)
#define FUNC_3(a, b) a = a * b; a++
#define FUNC_4(a, b) { a = a * b; a++; }
#define FUNC_5(a, b) do { a = a * b; a++; } while(0)

int main(void)
{
    int a = 10;
    // 1.期望值为100
    a = FUNC_1(9+1, 10);
    printf("[INFO] %d\r\n", a);

    // 2.期望值100
    a = 10;
    a = FUNC_2(a, 10);
    printf("[INFO] %d\r\n", a);

    // 3.期望值101
    a = 10;
    FUNC_3(a, 10);
    printf("[INFO] %d\r\n", a);

    // 4.期望值10
    a = 10;
    if (0)
        FUNC_3(a, 10);
    printf("[INFO] %d\r\n", a);

    // 5.期望值111(编译会出错,故注释)
    /*if (0) 
        FUNC_4(a, 10);
    else
        FUNC_4(a, 11);
    printf("[INFO] %d\r\n", a);
    */

    // 6.期望值111
    a = 10;
    if (0) {    
        FUNC_4(a, 10);
    }
    else {
        FUNC_4(a, 11);
    }
    printf("[INFO] %d\r\n", a);

    // 7.期望值111
    a = 10;
    if (0)
        FUNC_5(a, 10);
    else
        FUNC_5(a, 11);
    printf("[INFO] %d\r\n", a);

    return 0;
}

输出结果:

Text Only
[INFO] 19
[INFO] 100
[INFO] 101
[INFO] 11
[INFO] 111
[INFO] 111

问题解析

需要同代码注释的序号

① 期望值为100,输出值为19

原因在于,宏仅是文本的展开,查看预编译文件,如下图。

② 期望值为100,输出值为100

③ 期望值为101,输出值为101

④ 期望值为10,输出值为11

程序未套大括号,导致if仅阻碍了第一句宏代码,后一句被执行。

⑤ 期望值为111,编译出错

FUNC_4可以解决FUNC_3在④中的问题,但当ifelse一起使用时会出现新的问题。原因在于FUNC_4会使得if提前结束,预编译程序,如下图:

⑥和⑦是两种可以解决⑤的问题的方法。

但是,⑥依赖外层 {} 保证语法正确性,通过预编译文件可以发现, ⑦更符合语法标准 ,也是一种常见的写法。

2.1.2 调试宏

调试宏用于结合printf函数实现规范性的日志打印,常见的操作符和内置宏如下:

  • #:字符串化操作符,宏参数转换为字符串常量。
  • ##:标记连接符,将两个宏参数连接成一个新的标识符。
  • __FUNCTION__:调用宏的函数名,是一个字符串。
  • __LINE__:调用宏的所在行号,是一个数字。
  • __FILE__:调用宏的所在文件名,是一个字符串。

字符化

这个功能在调试、日志记录等场景中非常有用,可以将变量名或值直接转换为字符串形式。

测试程序:

C
#include <stdio.h>

#define LOG_INFO(info) printf("[%s] %s\r\n", #info, info)

int main(void)
{
    char INFO[] = "hello world"; 
    LOG_INFO(INFO);
    return 0;
}

输出结果:

Text Only
[INFO] hello world

连接符

这种特性常用于生成代码中的变量名、函数名、枚举值等场景,尤其在需要泛型编程或代码复用时非常有用。

测试程序:

C
#include <stdio.h>

#define LOG_INFO(info) printf("[INFO] %s is %d.\r\n", #info, info)

/* 1.生存规则变量,并通过宏调用 */
#define VAR_GENERATOR(num) int var_##num
#define VAR(num) var_##num

/* 2.泛型函数生成 */
#define ATTRIBUTE_FUNCTION(type, name) type get_##name(void) { return name; }

/* 3.枚举与字符串映射 */
#define COLORS(name) Color_##name

typedef enum {
    COLORS(Red),
    COLORS(Green),
    COLORS(Blue),
} Colors;


int main(void)
{
    // 1.生存规则变量,并通过宏调用
    VAR_GENERATOR(1) = 10;
    VAR_GENERATOR(2) = 100; 
    LOG_INFO(VAR(1));
    LOG_INFO(VAR(2));
    LOG_INFO(var_1);
    LOG_INFO(var_2);
    // 2.泛型函数生成
    int age = 18;
    ATTRIBUTE_FUNCTION(int, age);
    LOG_INFO(get_age());
    // 3.枚举与字符串映射
    LOG_INFO(Color_Red);
    LOG_INFO(Color_Green);
    LOG_INFO(Color_Blue);

    return 0;
}

输出结果:

Text Only
[INFO] VAR(1) is 10.
[INFO] VAR(2) is 100.
[INFO] var_1 is 10.
[INFO] var_2 is 100.
[INFO] get_age() is 18.
[INFO] Color_Red is 0.
[INFO] Color_Green is 1.
[INFO] Color_Blue is 2.

内置宏

修改文件中的日志宏:

C
#define LOG_INFO(info) printf("[INFO] [%s : %s : %d]--> %s is %d.\r\n",\
                                __FILE__,\
                                __FUNCTION__,\
                                __LINE__,\
                                #info, info)

修改后的输出结果:

Text Only
[INFO] [main.c : main : 31]--> VAR(1) is 10.
[INFO] [main.c : main : 32]--> VAR(2) is 100.
[INFO] [main.c : main : 33]--> var_1 is 10.
[INFO] [main.c : main : 34]--> var_2 is 100.
[INFO] [main.c : main : 38]--> get_age() is 18.
[INFO] [main.c : main : 40]--> Color_Red is 0.
[INFO] [main.c : main : 41]--> Color_Green is 1.
[INFO] [main.c : main : 42]--> Color_Blue is 2.

进一步修改文件中的日志宏,修改为类似printf函数:

C
#include <stdio.h>

#define LOG_INFO(info, ...) printf("[INFO] [%s : %s : %d]--> "info"\r\n",\
                                __FILE__,\
                                __FUNCTION__,\
                                __LINE__,\
                                ##__VA_ARGS__)

/* 1.生存规则变量,并通过宏调用 */
#define VAR_GENERATOR(num) int var_##num
#define VAR(num) var_##num

/* 2.泛型函数生成 */
#define ATTRIBUTE_FUNCTION(type, name) type get_##name(void) { return name; }

/* 3.枚举与字符串映射 */
#define COLORS(name) Color_##name

typedef enum {
    COLORS(Red),
    COLORS(Green),
    COLORS(Blue),
} Colors;


int main(void)
{
    // 1.生存规则变量,并通过宏调用
    VAR_GENERATOR(1) = 10;
    VAR_GENERATOR(2) = 100; 
    LOG_INFO("%d", VAR(1));
    LOG_INFO("%d", VAR(2));
    LOG_INFO("%d", var_1);
    LOG_INFO("%d", var_2);
    // 2.泛型函数生成
    int age = 18;
    ATTRIBUTE_FUNCTION(int, age);
    LOG_INFO("%d", get_age());
    // 3.枚举与字符串映射
    LOG_INFO("%d", Color_Red);
    LOG_INFO("%d", Color_Green);
    LOG_INFO("%d", Color_Blue);

    return 0;
}

修改后的输出结果:

Text Only
[INFO] [main.c : main : 31]--> 10
[INFO] [main.c : main : 32]--> 100
[INFO] [main.c : main : 33]--> 10
[INFO] [main.c : main : 34]--> 100
[INFO] [main.c : main : 38]--> 18
[INFO] [main.c : main : 40]--> 0
[INFO] [main.c : main : 41]--> 1
[INFO] [main.c : main : 42]--> 2

2.2 sizeof 关键字

2.2.1 sizeof 使用

C/C++ 语言中,sizeof 是一个关键字 ,同时也是 运算符 ,用于在编译时计算对象或类型的字节大小。

测试程序:

C
#include <stdio.h>


int main(void)
{
    int a = 10;
    char b = 'A';
    printf("[INFO] %ld\r\n", sizeof(a));
    printf("[INFO] %ld\r\n", sizeof(b);
           return 0;
           }

输出结果:

Text Only
[INFO] 4
[INFO] 1

2.2.2 sizeof 是函数?

为什么说sizeof是一个关键字,而非内置的函数呢?

  • 查看反汇编程序便很容易明白。

测试程序:

C
#include <stdio.h>

int my_sizeof(int a)
{
    return sizeof(a);
}

int a = 10; 
int b = 0;
int c = 0;

int main(void)
{
    b = sizeof(a);
    c = my_sizeof(a);
    printf("[INFO] %d\r\n", b);
    printf("[INFO] %d\r\n", c);
    return 0;
}

反汇编:

Bash
gcc keywords.c -o keywords.s -S

如下图所示,调用sizeof是直接把类型的字节大小计算出来,而封装成一个函数会先调用函数,因此不难看出没有调用sizeof为函数的汇编,因此它不是一个内置函数。

2.3 数据类型关键字

2.3.1 char

最小内存空间的数据类型,也是最适合操作硬件的数据类型

操作内存的最小单元是 8 bit,也就是 1 byte,通常使用 char 代指 1 byte 内存大小。

char 除了可以表示一定范围的数字,还可以表示单个字符,通过ASCII码表进行符号映射,完整表格见附录。

测试程序:

C
#include <stdio.h>

char a = 64;

int main(void)
{   
    printf("[INFO] 数字:%d ASCII字符:%c\r\n", a, a);
    return 0;   
}

输出结果:

Text Only
[INFO] 数字:64 ASCII字符:@

2.3.2 int

最适合CPU的数据类型,大小和编译器有关

在系统的一个周期内所能处理的最大单元就是int,所以对于 32 位的CPU,int就是 32 位,4 字节;对于 64 位的CPU,int就是 64 位,8 个字节。

short/long用于减少/增加int的取值范围和内存空间,使用short/long可以省略int

unsigned/signed用于规定变量是无符号/有符号,其中signed可以省略。

例如:unsigned char取值范围是 [0, 255],而signed char/char取值范围是 [-128, 127]。

测试程序:

C
#include <stdio.h>

int a = 25;
short b = 25;
long c = 25;
unsigned char d = 128;
signed char e = 128;

int main(void)
{   
    printf("[INFO] int: %ldbyte short: %ldbyte long: %ldbyte\r\n", sizeof(a), sizeof(b), sizeof(c));
    printf("[INFO] unsigned: %d signed: %d\r\n", d, e);
    return 0;   
}

输出结果:

Text Only
[INFO] int: 4byte short: 2byte long: 8byte
[INFO] unsigned: 128 signed: -128

2.3.3 float/double

float/double分别是单精度和双精度浮点数,float是 4 字节,double是 8 字节。在嵌入式中,浮点数的计算慢于整型,若精度要求不高可优先使用整型;然而,一些先进的嵌入式设备已经内置FPU,可以用于加速浮点数运算。

Danger

注意unsigned/signed不能用于修饰浮点数。

2.3.4 void

void在 C 语言中表示空,不能用于定义变量,多用于表示函数无返回值、无参数;void*则多用于内存操作。

  • 表示函数无返回值/无参数
C
void function(void) {
    printf("hello world");
}
  • 表示参数未使用,解决编译器警告
C
#include <stdio.h>

int main(void)
{
    int a = 10;
    (void)a;
}
  • void*:通用指针
C
#include <stdio.h>

int main(void)
{
    int a = 10;
    float b = 3.14f;
    // void指针需要显式数据类型转换后使用
    void* ptr;
    ptr = &a;
    printf("[INFO] %d %p\r\n", *(int*)ptr, ptr);
    ptr = &b;   
    printf("[INFO] %f %p\r\n", *(float*)ptr, ptr);
}
  • void*:内存空间
C
// Linux中memcpy函数的声明,使用 void* 表示操作的内存空间 
void *memcpy(void *dest, const void *src, size_t n);

测试程序:

C
#include <stdio.h>

int main(void)
{
    int var_a = 0;
    int var_b = 0x785621;
    memcpy(&var_a, &var_b, 1);
    printf("[INFO] 0x%X\r\n", var_a);
}

输出结果:

Text Only
[INFO] 0x21

x86架构的计算机使用的是小端存储数据,即数据的最低有效字节(LSB)被存储在内存的最低地址处,而最高有效字节(MSB)则存储在较高的地址处。

因此,memcpy(&var_a, &var_b, 1)会将var_b所在内存低地址开始的一个字节单位数据,拷贝到var_a所在内存,如下图所示。

Danger

void*这里涉及到了指针和函数,也就引出一个问题:函数指针和指针函数,详见附录。

2.4 自定义类型关键字

2.4.1 struct

C语言通过 struct 关键字表示 结构体 数据类型,内存表现为 累加且字节对齐

C
typedef struct{
    char a;
    int b;
} Struct_A;

上面的结构体的内存表现如下图所示,左侧为字节对齐表现,以CPU单周期最大处理单位为准,方便CPU连续的四个字节读取数据。

测试程序:

C
#include <stdio.h>

typedef struct{
    char a;
    int b;
} Struct_A;

int main(void)
{
    Struct_A struct_a = {
        .a = 'c',
        .b = 8,
    };
    printf("[INFO] size_a: %ld size_b: %ld size_struct_a: %ld\r\n", 
            sizeof(struct_a.a), sizeof(struct_a.b), sizeof(struct_a));
}

输出结果:

Text Only
[INFO] size_a: 1 size_b: 4 size_struct_a: 8

结构体有一个内存调整机制,若相邻成员的总大小未超过单周期最大处理单元,编译器会将相邻的小成员尽可能合并到同一对齐单元内。

C
// 可以触发
typedef struct{
    char a;
    char c;
    int b;
} Struct_B;

// 不能触发
typedef struct{
    char a;
    int b;
    char c;
} Struct_C;

测试程序:

C
#include <stdio.h>

typedef struct{
    char a;
    int b;
} Struct_A;

typedef struct{
    char a;
    char c;
    int b;
} Struct_B;

typedef struct{
    char a;
    int b;
    char c;
} Struct_C;

int main(void)
{
    Struct_A struct_a = {
        .a = 'c',
        .b = 8,
    };
    printf("[INFO] size_a: %ld size_b: %ld size_struct_a: %ld\r\n", 
            sizeof(struct_a.a), sizeof(struct_a.b), sizeof(struct_a));
    printf("[INFO] size_a: %ld size_b: %ld size_c: %ld size_struct_b: %ld\r\n",
            sizeof(struct_b.a), sizeof(struct_b.b), sizeof(struct_b.c), sizeof(struct_b));
    printf("[INFO] size_a: %ld size_b: %ld size_c: %ld size_struct_c: %ld\r\n",
            sizeof(struct_c.a), sizeof(struct_c.b), sizeof(struct_c.c), sizeof(struct_c));
}

输出结果:

Text Only
[INFO] size_a: 1 size_b: 4 size_struct_a: 8
[INFO] size_a: 1 size_b: 4 size_c: 1 size_struct_b: 8
[INFO] size_a: 1 size_b: 4 size_c: 1 size_struct_c: 12

2.4.2 union

C语言通过 union 关键字表示 联合体 数据类型。内存表现为 共享同一份内存(以最大数据类型为分配空间)和内存首地址

测试程序:

C
#include <stdio.h>

typedef union {
    int a;
    char b;
} Union_A;

int main(void)
{
    Union_A union_a = {
        .a = 0x10,
    };
    Union_A union_b = {
        .a = 0x2310,
    };
    printf("[INFO] a: 0x%x b: 0x%x\r\n", union_a.a, union_a.b);
    printf("[INFO] a: 0x%x b: 0x%x\r\n", union_b.a, union_b.b);
    return 0;
}

输出结果:

Text Only
[INFO] a: 0x10 b: 0x10
[INFO] a: 0x2310 b: 0x10

结构体Union_A的成员共用一份 4 字节的内存,但是成员 b 仅占用 1 字节。因此当赋值a = 0x2310时,读取成员 b 仅为起始地址起一个字节的数据,如下图所示。

image-20250525003742741

2.4.3 enum

C语言通过enum关键字表示 **枚举 **数据类型。内存表现为 **根据定义值的大小默认选择整数常量大小(int),如果超出int大小,编译器会选择更大的整型类型 **,比如long,所以内存大小不是固定的。

C
#include <stdio.h>

enum Colors_1{
    COLORS_RED,
    COLORS_GREEN,
    COLORS_BLUE,
};

enum Colors_2{
    COLORS_YELLOW = 0x123456789,
    COLORS_BLACK,
    COLORS_WHITE,
};

int main(void)
{
    printf("[INFO] Colors_1: COLORS_RED = %d COLORS_GREEN = %d COLORS_BLUE = %d\r\n", 
          COLORS_RED, COLORS_GREEN, COLORS_BLUE);
    printf("[INFO] Colors_1 size: COLORS_RED = %ld COLORS_GREEN = %ld COLORS_BLUE = %ld Colors_1 = %ld\r\n", 
          sizeof(COLORS_RED), sizeof(COLORS_GREEN), sizeof(COLORS_BLUE), sizeof(enum Colors_1));
    printf("[INFO] Colors_2: COLORS_YELLOW = 0x%x COLORS_BLACK = 0x%x COLORS_WHITE = 0x%x\r\n", 
          COLORS_YELLOW, COLORS_BLACK, COLORS_WHITE);
    printf("[INFO] Colors_2 size: COLORS_YELLOW = %ld COLORS_BLACK = %ld COLORS_WHITE = %ld Colors_1 = %ld\r\n", 
          sizeof(COLORS_YELLOW), sizeof(COLORS_BLACK), sizeof(COLORS_WHITE), sizeof(enum Colors_2));
    return 0;
}

输出结果:

Text Only
[INFO] Colors_1: COLORS_RED = 0 COLORS_GREEN = 1 COLORS_BLUE = 2
[INFO] Colors_1 size: COLORS_RED = 4 COLORS_GREEN = 4 COLORS_BLUE = 4 Colors_1 = 4
[INFO] Colors_2: COLORS_YELLOW = 0x23456789 COLORS_BLACK = 0x2345678a COLORS_WHITE = 0x2345678b
[INFO] Colors_2 size: COLORS_YELLOW = 8 COLORS_BLACK = 8 COLORS_WHITE = 8 Colors_1 = 8

2.5 修饰关键字

2.5.1 register

register是 C 语言中的一个 建议性 的关键字,请求编译器将变量存储在寄存器中,适用于需要高频访问的变量。

需要注意的是,即使我们加上register修饰变量,但是:

  1. 编译器会尽力去把变量存储在寄存器中, 不一定做得到
  2. 即使编译器成功将变量放入寄存器,它不会让我们使用<font style="color:rgb(64, 64, 64);">&</font>访问这个变量。

2.5.2 static

C 语言中的static关键字有两个核心作用:

  1. 规定static修饰的变量的内存在全局静态区域分配,进而延长局部变量的生命周期。
  2. 限定函数或变量的作用域为当前文件。

Note

养成一个良好的编码习惯:对于只在当前文件使用的函数,都加上static修饰。

2.5.3 extern

C 语言中的extern关键字用于跨文件访问变量或函数。

C
// 1.c :定义一个全局变量
#include <stdio.h>

int a = 10;
C
// 2.c :通过extern访问 1.c 的全局变量
#include <stdio.h>

extern int a;

int main(void)
{
    printf("[INFO] %d\r\n", a);
    return 0;
}

在嵌入式 RTOS 开发中,如果多文件都用extern关键字声明同一个全局变量容易出现冲突和混乱。特别是当多个任务同时读写这个变量时,可能会发生数据错乱。

此时,我们应该将需要共享的变量/硬件资源封装成统一接口,并使用互斥锁保护这些资源,下面是一个我项目开发中的一个实例:

C
// 共享变量 g_odomQueue
static QueueHandle_t g_odomQueue;

// 封装写队列
void CanMid_WriteOdom(Odom* data) {
    xQueueSend(g_odomQueue, data, 0);
}

// 封装读队列
void CanMid_ReadOdom(Odom* data) {
    xQueueReceive(g_odomQueue, data, portMAX_DELAY);
}

无论是写队列还是读队列操作,均需要使用g_odomQueue全局变量,而且可能是多个任务调用。因此封装统一的函数接口供多任务调用(队列本身就是一个同步机制,因此无需互斥锁保护)。

2.5.4 const

C 语言中的const关键字用于声明 只读变量 ,即变量初始化后不能修改。

虽然原则上const修饰的变量不能修改,但 C 语言的编译器拦不住通过地址间接修改值的方法。

  • 声明const变量
C
const <数据类型> <变量名称>;  <=>  <数据类型> const <变量名称>;
  • const修饰指针
C
#include <stdio.h>

int value = 10;
int other = 30;

/* 数据类型不看,对比const靠近 * 或是变量,判断哪部分不能修改  */
const int * a = &value; // 等价于 const * a,指针指向的值,即 (*a) 不能修改。
int const * b = &value; // 同上
int * const c = &value; // 等价于 * const c,指针变量,即 c 不能修改。
const int * const d = &value; // 等价于 const * const c,指针指向的值和指针变量均不能修改。

int main(void)
{
    /* 1. const int * a 和 int const * b */
    printf("%p\r\n", a);
    // *a = 20;         // 错误,不能修改指针指向的值
    a = &other;         // 正确,可以修改指针指向的地址
    printf("%p\r\n", a);

    /* 2. int * const c */
    printf("%d\r\n", (*c));
    *c = 20;            // 正确,可以修改指针指向的值
    // c = &other;      // 错误,不能修改指针指向的地址
    printf("%d\r\n", (*c));

    /* 3. const int * const d */
    printf("%d\r\n", (*d));
    printf("%p\r\n", d);
    // *d = 30;         // 错误,不能修改指针指向的值
    // d = &other;      // 错误,不能修改指针指向的地址

    return 0;
}
  • constdefine宏的区别
    • 编译器处理方式:define宏是在预处理阶段展开;const变量是编译运行阶段使使用。
    • 安全检查:define宏不做任何类型检査,仅是文本的展开;const变量编译阶段会执行类型检查。
    • 内存位置:define宏在代码段;const常量可以在静态全局数据段、栈中。

2.5.5 volatile

C 语言中的volatile是一个反编译器优化的关键字,告诉编译器:" 不要优化这个变量的访问,它可能随时变化! "

  • 为什么需要 volatile

编译器默认会进行代码优化,例如:

  • 将频繁访问的变量存储在寄存器中,减少内存访问次数。
  • 若代码中连续多次读取同一变量,编译器可能合并为一次读取。

这些优化会导致一些问题,原因在于:

  • 其他线程可能修改共享变量。
  • 外设寄存器的值可能随时被硬件改变。
  • 信号处理函数可能异步修改变量。

附录

ASCII码表

十进制 十六进制 字符/缩写 描述
0 0x00 NUL Null (空字符)
1 0x01 SOH Start of Heading (标题开始)
2 0x02 STX Start of Text (文本开始)
3 0x03 ETX End of Text (文本结束)
4 0x04 EOT End of Transmission (传输结束)
5 0x05 ENQ Enquiry (询问)
6 0x06 ACK Acknowledge (确认)
7 0x07 BEL Bell (响铃)
8 0x08 BS Backspace (退格)
9 0x09 HT Horizontal Tab (水平制表符)
10 0x0A LF Line Feed (换行)
11 0x0B VT Vertical Tab (垂直制表符)
12 0x0C FF Form Feed (换页)
13 0x0D CR Carriage Return (回车)
14 0x0E SO Shift Out (移出)
15 0x0F SI Shift In (移入)
16 0x10 DLE Data Link Escape (数据链路转义)
17 0x11 DC1 Device Control 1 (设备控制1)
18 0x12 DC2 Device Control 2 (设备控制2)
19 0x13 DC3 Device Control 3 (设备控制3)
20 0x14 DC4 Device Control 4 (设备控制4)
21 0x15 NAK Negative Acknowledge (否定确认)
22 0x16 SYN Synchronous Idle (同步空闲)
23 0x17 ETB End of Transmission Block (传输块结束)
24 0x18 CAN Cancel (取消)
25 0x19 EM End of Medium (介质结束)
26 0x1A SUB Substitute (替换)
27 0x1B ESC Escape (转义)
28 0x1C FS File Separator (文件分隔符)
29 0x1D GS Group Separator (组分隔符)
30 0x1E RS Record Separator (记录分隔符)
31 0x1F US Unit Separator (单元分隔符)
32 0x20 Space (空格)
33 0x21 ! Exclamation mark (感叹号)
34 0x22 " Double quote (双引号)
35 0x23 # Hash (井号)
36 0x24 $ Dollar sign (美元符号)
37 0x25 % Percent sign (百分号)
38 0x26 & Ampersand (和号)
39 0x27 ' Single quote (单引号)
40 0x28 ( Left parenthesis (左括号)
41 0x29 ) Right parenthesis (右括号)
42 0x2A * Asterisk (星号)
43 0x2B + Plus (加号)
44 0x2C , Comma (逗号)
45 0x2D - Hyphen (连字符)
46 0x2E . Period (句号)
47 0x2F / Slash (斜杠)
48 0x30 0 Zero
49 0x31 1 One
50 0x32 2 Two
51 0x33 3 Three
52 0x34 4 Four
53 0x35 5 Five
54 0x36 6 Six
55 0x37 7 Seven
56 0x38 8 Eight
57 0x39 9 Nine
58 0x3A : Colon (冒号)
59 0x3B ; Semicolon (分号)
60 0x3C < Less than (小于号)
61 0x3D = Equals sign (等号)
62 0x3E > Greater than (大于号)
63 0x3F ? Question mark (问号)
64 0x40 @ At sign (at符号)
65 0x41 A Uppercase A
66 0x42 B Uppercase B
... ... ... ... (字母表继续到Z和z)
91 0x5B [ Left square bracket (左方括号)
92 0x5C \ Backslash (反斜杠)
93 0x5D ] Right square bracket (右方括号)
94 0x5E ^ Caret (脱字符)
95 0x5F _ Underscore (下划线)
96 0x60 ` Grave accent (重音符)
97 0x61 a Lowercase a
98 0x62 b Lowercase b
... ... ... ... (字母表继续到z)
123 0x7B { Left curly brace (左花括号)
124 0x7C
125 0x7D } Right curly brace (右花括号)
126 0x7E ~ Tilde (波浪号)
127 0x7F DEL Delete (删除)
128+ 0x80+ (扩展ASCII) 因系统/编码不同而异的符号

printf 格式化

格式符 用途 示例 输出示例
%d 十进制整数(有符号) printf("%d", 42); 42
%u 十进制整数(无符号) printf("%u", 255); 255
%o 八进制整数 printf("%o", 64); 100
%x 十六进制整数(小写字母) printf("%x", 255); ff
%X 十六进制整数(大写字母) printf("%X", 255); FF
%c 单个字符 printf("%c", 'A'); A
%s 字符串 printf("%s", "Hello"); Hello
%f 浮点数(默认6位小数) printf("%f", 3.14); 3.140000
%e 科学计数法(小写e) printf("%e", 1000.0); 1.000000e+03
%E 科学计数法(大写E) printf("%E", 1000.0); 1.000000E+03
%g 自动选择 %f%e(更紧凑) printf("%g", 0.0001); 0.0001
%p 指针地址 printf("%p", &x); 0x7ffd34a1b2c

大小端

  • 小端存储数据,即数据低位字节(LSB)被存储在内存的低地址处,而高位字节(MSB)则存储内存的高地址处。 - 常见于x86架构、ARM架构。
  • 大端存储数据,即数据低位字节(LSB)被存储在内存的高地址处,而高位字节(MSB)则存储内存的低地址处。 - 常用于网络协议、某些硬件平台。

函数指针与指针函数

C
/* 指针函数:返回值为指针类型的函数
 * 返回void*,需要在接收返回值前显示声明转换的数据类型
 * 需手动释放内存
 */ 
void *Callback(void *user_data);
/* 函数指针:指向函数的指针变量,存储函数的入口地址。
 * 函数本身无内容,可以指向一个具体函数,这个函数需要返回值为void,参数为void*
 * Callback = my_callback;  √
 * Callback = &my_callback; √
 */
void (*Callback)(void *user_data);

函数指针的RTOS和Linux较为常用,下面是一个简单的函数指针示例:

C
#include <stdio.h>

/* 声明一个名为Callback的函数指针 */
typedef void (*Callback)(void *user_data);

/* 定义一个用户参数 */
typedef struct {
    char* name;
    int age;
} UserData;

/* 定义一个具体的回调函数 */
void my_callback(void *data)
{
    // 显式转换指针数据类型
    UserData *dt = (UserData*)data;
     printf("[INFO] name:%s age:%d\r\n", dt->name, dt->age);
}

/* 定义一个回调函数注册函数 */
void register_callback(Callback cb, void *cb_data)
{
    cb(cb_data);
}

int main(void)
{
    UserData dt = {
        .name = "Tim",
        .age = 22,
    };
    register_callback(my_callback, &dt);

    return 0;
}

输出结果:

Text Only
[INFO] name:Tim age:22