宏和关键字
2.1 宏¶
2.1.1 宏函数¶
测试程序:
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;
}
输出结果:
问题解析
需要同代码注释的序号
① 期望值为100,输出值为19
原因在于,宏仅是文本的展开,查看预编译文件,如下图。
② 期望值为100,输出值为100
√
③ 期望值为101,输出值为101
√
④ 期望值为10,输出值为11
程序未套大括号,导致if
仅阻碍了第一句宏代码,后一句被执行。
⑤ 期望值为111,编译出错
FUNC_4
可以解决FUNC_3
在④中的问题,但当if
和else
一起使用时会出现新的问题。原因在于FUNC_4
会使得if
提前结束,预编译程序,如下图:
⑥和⑦是两种可以解决⑤的问题的方法。
但是,⑥依赖外层 {}
保证语法正确性,通过预编译文件可以发现, ⑦更符合语法标准 ,也是一种常见的写法。
2.1.2 调试宏¶
调试宏用于结合printf
函数实现规范性的日志打印,常见的操作符和内置宏如下:
#
:字符串化操作符,宏参数转换为字符串常量。##
:标记连接符,将两个宏参数连接成一个新的标识符。__FUNCTION__
:调用宏的函数名,是一个字符串。__LINE__
:调用宏的所在行号,是一个数字。__FILE__
:调用宏的所在文件名,是一个字符串。
字符化
这个功能在调试、日志记录等场景中非常有用,可以将变量名或值直接转换为字符串形式。
测试程序:
#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;
}
输出结果:
连接符
这种特性常用于生成代码中的变量名、函数名、枚举值等场景,尤其在需要泛型编程或代码复用时非常有用。
测试程序:
#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;
}
输出结果:
[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.
内置宏
修改文件中的日志宏:
#define LOG_INFO(info) printf("[INFO] [%s : %s : %d]--> %s is %d.\r\n",\
__FILE__,\
__FUNCTION__,\
__LINE__,\
#info, info)
修改后的输出结果:
[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
函数:
#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;
}
修改后的输出结果:
[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
是一个关键字 ,同时也是 运算符 ,用于在编译时计算对象或类型的字节大小。
测试程序:
#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;
}
输出结果:
2.2.2 sizeof 是函数?¶
为什么说sizeof
是一个关键字,而非内置的函数呢?
- 查看反汇编程序便很容易明白。
测试程序:
#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;
}
反汇编:
如下图所示,调用sizeof
是直接把类型的字节大小计算出来,而封装成一个函数会先调用函数,因此不难看出没有调用sizeof
为函数的汇编,因此它不是一个内置函数。
2.3 数据类型关键字¶
2.3.1 char¶
最小内存空间的数据类型,也是最适合操作硬件的数据类型
操作内存的最小单元是 8 bit,也就是 1 byte,通常使用 char
代指 1 byte 内存大小。
char
除了可以表示一定范围的数字,还可以表示单个字符,通过ASCII码表进行符号映射,完整表格见附录。
测试程序:
#include <stdio.h>
char a = 64;
int main(void)
{
printf("[INFO] 数字:%d ASCII字符:%c\r\n", a, a);
return 0;
}
输出结果:
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]。
测试程序:
#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;
}
输出结果:
2.3.3 float/double¶
float/double
分别是单精度和双精度浮点数,float
是 4 字节,double
是 8 字节。在嵌入式中,浮点数的计算慢于整型,若精度要求不高可优先使用整型;然而,一些先进的嵌入式设备已经内置FPU
,可以用于加速浮点数运算。
Danger
注意unsigned/signed
不能用于修饰浮点数。
2.3.4 void¶
void
在 C 语言中表示空,不能用于定义变量,多用于表示函数无返回值、无参数;void*
则多用于内存操作。
- 表示函数无返回值/无参数
- 表示参数未使用,解决编译器警告
void*
:通用指针
#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*
:内存空间
测试程序:
#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);
}
输出结果:
x86
架构的计算机使用的是小端存储数据,即数据的最低有效字节(LSB)被存储在内存的最低地址处,而最高有效字节(MSB)则存储在较高的地址处。
因此,memcpy(&var_a, &var_b, 1)
会将var_b
所在内存低地址开始的一个字节单位数据,拷贝到var_a
所在内存,如下图所示。
Danger
在void*
这里涉及到了指针和函数,也就引出一个问题:函数指针和指针函数,详见附录。
2.4 自定义类型关键字¶
2.4.1 struct¶
C语言通过 struct
关键字表示 结构体 数据类型,内存表现为 累加且字节对齐 。
上面的结构体的内存表现如下图所示,左侧为字节对齐表现,以CPU单周期最大处理单位为准,方便CPU连续的四个字节读取数据。
测试程序:
#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));
}
输出结果:
结构体有一个内存调整机制,若相邻成员的总大小未超过单周期最大处理单元,编译器会将相邻的小成员尽可能合并到同一对齐单元内。
// 可以触发
typedef struct{
char a;
char c;
int b;
} Struct_B;
// 不能触发
typedef struct{
char a;
int b;
char c;
} Struct_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));
}
输出结果:
[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
关键字表示 联合体 数据类型。内存表现为 共享同一份内存(以最大数据类型为分配空间)和内存首地址 。
测试程序:
#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;
}
输出结果:
结构体Union_A
的成员共用一份 4 字节的内存,但是成员 b 仅占用 1 字节。因此当赋值a = 0x2310
时,读取成员 b 仅为起始地址起一个字节的数据,如下图所示。
2.4.3 enum¶
C语言通过enum
关键字表示 **枚举 **数据类型。内存表现为 **根据定义值的大小默认选择整数常量大小(int),如果超出int大小,编译器会选择更大的整型类型 **,比如long
,所以内存大小不是固定的。
#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;
}
输出结果:
[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
修饰变量,但是:
- 编译器会尽力去把变量存储在寄存器中, 不一定做得到 。
- 即使编译器成功将变量放入寄存器,它不会让我们使用
<font style="color:rgb(64, 64, 64);">&</font>
访问这个变量。
2.5.2 static¶
C 语言中的static
关键字有两个核心作用:
- 规定
static
修饰的变量的内存在全局静态区域分配,进而延长局部变量的生命周期。 - 限定函数或变量的作用域为当前文件。
Note
养成一个良好的编码习惯:对于只在当前文件使用的函数,都加上static
修饰。
2.5.3 extern¶
C 语言中的extern
关键字用于跨文件访问变量或函数。
// 2.c :通过extern访问 1.c 的全局变量
#include <stdio.h>
extern int a;
int main(void)
{
printf("[INFO] %d\r\n", a);
return 0;
}
在嵌入式 RTOS 开发中,如果多文件都用extern
关键字声明同一个全局变量,容易出现冲突和混乱。特别是当多个任务同时读写这个变量时,可能会发生数据错乱。
此时,我们应该将需要共享的变量/硬件资源封装成统一接口,并使用互斥锁保护这些资源,下面是一个我项目开发中的一个实例:
// 共享变量 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
变量
const
修饰指针
#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;
}
const
和define
宏的区别- 编译器处理方式:
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)则存储内存的低地址处。 - 常用于网络协议、某些硬件平台。
函数指针与指针函数¶
/* 指针函数:返回值为指针类型的函数
* 返回void*,需要在接收返回值前显示声明转换的数据类型
* 需手动释放内存
*/
void *Callback(void *user_data);
/* 函数指针:指向函数的指针变量,存储函数的入口地址。
* 函数本身无内容,可以指向一个具体函数,这个函数需要返回值为void,参数为void*
* Callback = my_callback; √
* Callback = &my_callback; √
*/
void (*Callback)(void *user_data);
函数指针的RTOS和Linux较为常用,下面是一个简单的函数指针示例:
#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;
}
输出结果: