打卡第五集-C语言基础
C语言 USB-CDC串口之printf函数的实现
1.打开USB库中的PRINTF_HID宏定义(去掉//)
2.理解PRINTF的函数原型的定义
#define printf printf_hid
int printf_hid (const char *fmt, ...);
参数fmt -- 是格式控制字符串,包含了两种类型的对象:普通字符和转换说明 。
普通字符:在输出时,普通字符将原样不动地复制到标准输出。
printf("8051U深度入门到32位51大型实战视频\r\n");
转换说明:不直接输出,用于控制 printf 中参数的转换和打印。每个转换说明都由一个百分号字符(%)开始,以转换说明符结束,从而说明输出数据的类型、宽度、精度等。
printf("8051U深度入门到32位51大型实战视频,%s\\r\\n","加油");
转换说明简介:
1.类型:根据不同的 fmt 字符串,函数可能需要一系列的附加参数,每个
参数包含了一个要被插入的值,替换了 fmt 参数中指定的每个 % 标签。
关于附加参数,既可以是变量,也可以是常量。
2.位置:printf()函数的普通字符和转换说明放在" "双引号内,附加参数
放在双引号外,每个附加参数之间用逗号隔开。
3.数量:printf() 的附加参数与转换说明符是⼀⼀对应关系,如果有 n 个转
换说明符, printf() 的参数就应该有 n + 1 个。如果参数个数少于对应的转
换说明符,printf() 可能会输出内存中的任意值。


C语言的基本数据类型(也称为内置类型)是构建程序的基础。它们分为整型、浮点型、字符型和void 型四类。下面分别介绍。
1. 整型(Integer Types)
整型用于存储整数,分为有符号(signed)和无符号(unsigned)两种。默认情况下 int、short、long 都是有符号的。
| 类型 |
常用大小(字节) |
取值范围(典型) |
格式说明符 |
char |
1 |
-128~ 127 或 0 ~ 255(取决于符号性) |
%c, %hhd |
signed char |
1 |
-128~ 127 |
%hhd |
unsigned char |
1 |
0~ 255 |
%hhu |
short(或 short int) |
2 |
-32,768~ 32,767 |
%hd |
unsigned short |
2 |
0~ 65,535 |
%hu |
int |
4(常用) |
-2,147,483,648~ 2,147,483,647 |
%d |
unsigned int |
4 |
0~ 4,294,967,295 |
%u |
long(或 long int) |
4 或 8(视平台) |
至少 -2,147,483,647~ 2,147,483,647 |
%ld |
unsigned long |
4 或 8 |
至少 0~ 4,294,967,295 |
%lu |
long long(C99起) |
8 |
-9.22×10¹⁸~ 9.22×10¹⁸ |
%lld |
unsigned long long |
8 |
0~ 1.84×10¹⁹ |
%llu |
注意:实际字节数由编译器和平台决定。可以用 sizeof(类型) 获取,或用 <limits.h> 中的宏(如 INT_MAX)得到准确范围。
2. 浮点型(Floating-Point Types)
用于存储带小数部分的数值。
| 类型 |
常用大小(字节) |
精度 |
格式说明符 |
float |
4 |
约 7 位十进制 |
%f |
double |
8 |
约 15 位十进制 |
%lf(输入)/ %f(输出) |
long double |
8、12 或 16 |
依平台而定 |
%Lf |
float 通常用于节省内存,但精度较低。
double 是默认的浮点类型,足够大多数场景。
long double 提供更高精度,但可能因平台不同而表现不同。
3. 字符型(Character Type)
char 本质上是整型,用于存储单个字符(ASCII 或 UTF-8 等编码)。字符常量用单引号括起,如 'A'。
- 字符在内存中以整数形式存储(例如
'A' 对应 65)。
- 可以用
%c 输出字符,或用 %d 输出其 ASCII 码。
c
char ch = 'A';
printf("%c = %d\n", ch, ch); // 输出: A = 65
4. void 类型
void 表示“无类型”,主要用于:
- 函数无返回值:
void func(void);
- 通用指针:
void * 可指向任意类型的数据。
5. 类型修饰符(Modifiers)
- 符号修饰:
signed / unsigned 决定是否有符号。
- 长度修饰:
short、long、long long 改变数据长度。
可以组合使用:unsigned long long int(int 可省略)。
6. 如何选择数据类型
- 整数:优先用
int(通常是最快的自然宽度)。若需大范围,用 long long;若需节省内存或用于数组索引,用 short 或 unsigned。
- 浮点数:一般用
double,除非内存紧张或确定不需要高精度才用 float。
- 字符:用
char 处理文本,用 unsigned char 处理原始字节(如二进制数据)。
7. 示例:使用基本数据类型
c
#include <stdio.h>
#include <limits.h>
int main() {
// 整型
int a = 42;
unsigned int b = 100u;
long long big = 123456789012345LL;
// 浮点
double pi = 3.1415926535;
// 字符
char grade = 'A';
// 打印大小
printf("int 大小: %zu 字节\n", sizeof(int));
printf("long long 大小: %zu 字节\n", sizeof(long long));
printf("double 大小: %zu 字节\n", sizeof(double));
// 使用 limits.h 中的常量
printf("int 最大值: %d\n", INT_MAX);
return 0;
}
8. 进制与数据类型的关联(承前)
- 整型常量可以用十进制、十六进制(
0x 前缀)或八进制(0 前缀)书写。
- 在格式化输出时,
printf 的 %x 以十六进制显示整数的内存表示(对负数显示补码)。
c
int num = 0x2A; // 十六进制字面量,赋值给 int
printf("%d\n", num); // 输出 42
printf("%x\n", num); // 输出 2a
C语言的运算符
1. 算术运算符
用于数值计算。
| 运算符 |
含义 |
示例 |
+ |
加法 |
a + b |
- |
减法 |
a - b |
* |
乘法 |
a * b |
/ |
除法 |
a / b |
% |
取模(求余) |
a % b |
注意:
- 两个整数相除,结果为整数(向零截断)。例如
5 / 2 得 2。
- 取模运算要求操作数均为整数,结果的符号与被除数相同:
-5 % 2 得 -1。
2. 关系运算符
用于比较大小,结果为 1(真)或 0(假)。
| 运算符 |
含义 |
示例 |
== |
等于 |
a == b |
!= |
不等于 |
a != b |
< |
小于 |
a < b |
> |
大于 |
a > b |
<= |
小于等于 |
a <= b |
>= |
大于等于 |
a >= b |
常见错误:将 == 误写为 =(赋值),例如 if (x = 5) 会永远为真(因为赋值表达式结果为 5,非零)。
3. 逻辑运算符
用于组合多个条件,结果也为 1(真)或 0(假)。
| 运算符 |
含义 |
示例 |
&& |
逻辑与 |
a && b |
|| |
逻辑或 |
a || b |
! |
逻辑非 |
!a |
短路求值:
&& 若左操作数为假,不再计算右操作数。
|| 若左操作数为真,不再计算右操作数。
c
int a = 0;
if (a && (++a > 0)) { } // 右侧不会执行,a 保持 0
4. 位运算符
直接操作整数在内存中的二进制位。
| 运算符 |
含义 |
示例 |
& |
按位与 |
a & b |
| |
按位或 |
a | b |
^ |
按位异或 |
a ^ b |
~ |
按位取反 |
~a |
<< |
左移 |
a << n |
>> |
右移 |
a >> n |
注意:
- 移位运算符对负数进行右移时,行为由编译器实现决定(通常为算术右移,即补符号位)。
- 左移
n 位等价于乘以 2^n(不溢出时),右移等价于除以 2^n 的整数部分。
5. 赋值运算符
| 运算符 |
含义 |
示例 |
= |
简单赋值 |
a = b |
+= |
加后赋值 |
a += b → a = a + b |
-= |
减后赋值 |
类似 |
*= |
乘后赋值 |
|
/= |
除后赋值 |
|
%= |
取模后赋值 |
|
&= |
按位与后赋值 |
|
|= |
按位或后赋值 |
|
^= |
按位异或后赋值 |
|
<<= |
左移后赋值 |
|
>>= |
右移后赋值 |
|
赋值表达式的结果是赋值后左操作数的值,且具有右结合性,因此可以连续赋值:a = b = c = 0;。
6. 自增自减运算符
| 运算符 |
含义 |
示例 |
++ |
自增(加1) |
i++(后缀)、++i(前缀) |
-- |
自减(减1) |
i--、--i |
前缀与后缀的区别:
- 前缀:先运算,后使用值。
- 后缀:先使用值,后运算。
c
int i = 5, j;
j = ++i; // i 先变为 6,再赋值给 j,所以 i=6, j=6
i = 5;
j = i++; // 先赋值 j=5,再 i 自增为 6,所以 i=6, j=5
7. 条件(三目)运算符
表达式1 ? 表达式2 : 表达式3
- 先计算
表达式1,若为真,则执行 表达式2 并返回其值,否则执行 表达式3 并返回其值。
c
int max = (a > b) ? a : b;
8. 逗号运算符
依次计算多个表达式,整个表达式的值是最后一个表达式的值。
c
int a = (1, 2, 3); // a 得到 3
常用于 for 循环中:
c
for (i=0, j=10; i<j; i++, j--) { ... }
9. sizeof 运算符
以字节为单位返回类型或对象的大小。结果为 size_t 类型(无符号整型)。
c
size_t size = sizeof(int); // 通常是 4
int arr[10];
size_t arr_size = sizeof(arr); // 整个数组占用的字节数
注意:sizeof 是运算符,不是函数,括号仅对类型必需。
10. 指针运算符
| 运算符 |
含义 |
示例 |
& |
取地址 |
&var |
* |
解引用(间接访问) |
*ptr |
c
int x = 10;
int *p = &x; // p 指向 x
*p = 20; // 通过指针修改 x
11. 类型转换运算符
C 语言支持隐式类型转换(自动提升)和显式强制转换。
- 隐式转换:混合运算时,小类型向大类型提升;有符号和无符号混合时可能产生意外结果。
- 显式转换:
(类型)表达式
c
double d = 3.14;
int i = (int)d; // i = 3
12. 运算符优先级与结合性
- 优先级:例如
* 和 / 高于 + 和 -,() 最高。
- 结合性:多数二元运算符为左结合(从左到右计算),赋值、条件、单目运算符为右结合(从右到左)。
常见陷阱:
c
if (x & 1 == 0) // 错误:`==` 优先级高于 `&`,实际为 x & (1==0) → x & 0 → 0
正确写法:if ((x & 1) == 0)
为避免错误,建议不确定优先级时使用括号。
13. 总结表(常用运算符优先级,从高到低)
| 优先级 |
运算符 |
结合性 |
| 1 |
() [] -> . |
左→右 |
| 2 |
++ -- ! ~ + - * & (类型) sizeof |
右→左 |
| 3 |
* / % |
左→右 |
| 4 |
+ - |
左→右 |
| 5 |
<< >> |
左→右 |
| 6 |
< <= > >= |
左→右 |
| 7 |
== != |
左→右 |
| 8 |
& |
左→右 |
| 9 |
^ |
左→右 |
| 10 |
| |
左→右 |
| 11 |
&& |
左→右 |
| 12 |
|| |
左→右 |
| 13 |
?: |
右→左 |
| 14 |
= += -= *= 等 |
右→左 |
| 15 |
, |
左→右 |
三种常用进制的表示法
在代码中,通过前缀来区分:
- 二进制:前缀
0b 或 0B
- 例如:
0b1010 (等于十进制的10)
- 注:部分老旧编译器可能不支持,但在主流单片机IDE(如Keil、Arduino、STM32CubeIDE)中均支持。
- 十六进制:前缀
0x
- 例如:
0x0A (等于十进制的10)
- 这是单片机中最常用的写法,因为它和内存地址、寄存器位一一对应。
- 十进制:无前缀,直接写数字
2. 为什么要用十六进制(最实用)
在单片机中,一个字节由8位二进制组成。如果用二进制写 11001011,看着很累;用十进制写 203,看不出每一位是0还是1。
十六进制的优势: 1位十六进制数 = 4位二进制数。
因此,1个字节(8位) 可以用 2位十六进制数 完美表示,非常直观:
0b10101010 (二进制,看着眼花) →→ 0xAA (十六进制,简洁)
0x37 →→ 拆开:高4位是 3 (0011),低4位是 7 (0111),组合成 00110111。
3. 常用换算与位运算
在单片机编程中,你很少需要手动计算“十进制转二进制”的数学题,更多的是通过位运算直接操作寄存器。
- 设置某一位为1:
REG |= (1 << 2); (将第2位设为1,其余不变)
- 清除某一位为0:
REG &= ~(1 << 3); (将第3位清0)
- 取反某一位:
REG ^= (1 << 5);
记住常见的十六进制掩码:
0x01 = 0b0000 0001
0x0F = 0b0000 1111
0xFF = 0b1111 1111 (即255,常用于清除一个字节或全设置为高电平)
4. 总结
- 看前缀识进制:
0x 是十六进,0b 是二进,没前缀是十进。
- 写寄存器用十六进:因为
0xFF 比 255 更能直接反映8个引脚的高低电平状态。
- 移位操作代替乘法:
(1 << n) 比 pow(2, n) 快得多,且直接对应位位置。