date
icon
password
博客链接
Pin
Pin
Hide-in-Web
Hide-in-Web
网址
type
slug
tags
category
bottom
bottom
Hide-in-Config
Hide-in-Config
comment
status
summary
💡
本篇笔记只用于记录在嵌入式中常用的 C 语言代码(内容和代码示例大部分都来自黑马老师的文档),由于过去曾经有过 C++ 基础,因此只对重要的内容进行摘录。未来如果有时间和精力,会将 C 语言再从头学一遍,并整理成一篇完整的笔记。
常识
标识符
命名规则
与 C++ 的标识符规则一致:
  • 大小写字母(A-Z、a-z)
  • 数字(0-9)
  • 下划线(_)
并且,数字不可作为开头。
同时,标识符不能够使用系统的关键字,C++ 对标识符的大小写是敏感的。
系统关键字主要有:
关键字
关键字
关键字
关键字
auto
break
case
char
const
continue
default
do
double
else
enum
extern
float
for
goto
if
int
long
register
return
short
signed
sizeof
static
struct
switch
typedef
union
unsigned
void
volatile
while
命名规范
大驼峰法:单词首字母均大写;
小驼峰法:第一个单词全小写,其余单词首字母全大写;
下划线命名法:单词之间以下划线分割。
一般,变量、常量、字符、指针采用的是小驼峰命名法;函数、结构体采用的是下划线命名法;
字符串和转义字符
字符本质上就是 ASCII 码数字,因此 char ch = 'A';uint8_t ch = 65; 是相同的概念。
printf 语句可以输出字符,由于字符本质是数字,所以可以通过占位符:
  • %d 来输出其 ASCII 数字
  • %c 来输出其字符本身
转义字符:
  • \n:换行符
  • \t:制表符
  • \\:反斜杠
  • \':单引号
  • \":双引号
  • \0:空字符(用于字符串的结束标志)
运算符
算术运算符:
运算符
术语
示例
结果
+
10 + 5
15
-
10 - 5
5
*
10 * 5
50
/
10 / 5
2
%
取模(取余)
10 % 3
1
++
前自增
a=2; b=++a;
a=3; b=3;
++
后自增
a=2; b=a++;
a=3; b=2;
--
前自减
a=2; b=--a;
a=1; b=1;
--
后自减
a=2; b=a--;
a=1; b=2;
赋值运算符:
运算符
术语
示例
结果
=
赋值
a=2; b=3;
a=2; b=3;
+=
加等于
a=0; a+=2; 等同于 a = a + 2;
a=2;
-=
减等于
a=5; a-=3; 等同于 a = a - 3;
a=2;
*=
乘等于
a=2; a*=2; 等同于 a = a * 2;
a=4;
/=
除等于
a=4; a/=2; 等同于 a = a / 2;
a=2;
%=
模等于
a=3; a%=2; 等同于 a = a % 2;
a=1;
比较运算符:
运算符
术语
示例
结果
=
赋值
a=2; b=3;
a=2; b=3;
+=
加等于
a=0; a+=2; 等同于 a = a + 2;
a=2;
-=
减等于
a=5; a-=3; 等同于 a = a - 3;
a=2;
*=
乘等于
a=2; a*=2; 等同于 a = a * 2;
a=4;
/=
除等于
a=4; a/=2; 等同于 a = a / 2;
a=2;
%=
模等于
a=3; a%=2; 等同于 a = a % 2;
a=1;
逻辑运算符:
运算符
术语
示例
结果
!
!a
如果a为假,则!a为真; 如果a为真,则!a为假。
&&
a && b
如果a和b都为真,则结果为真,否则为假。
||
a || b
如果a和b有一个为真,则结果为真,二者都为假时,结果为假。
位运算符:
运算符
术语
示例
结果
&
按位与运算
011 & 101
2个都为1才为1,结果为001
|
按位或运算
011 | 101
有1个为1就为1,结果为111
^
按位异或运算
011 ^ 101
不同的为1,结果为110
~
取反运算
~011
100
<<
左移运算
1010 << 1
10100
>>
右移运算
1010 >> 1
0101
常用控制语句
分支语句
单条件:
三目运算符:
条件 ? 表达式1 : 表达式2
三目运算符
a>b?a:b;
如果a>b,整体为结果a,否则整体结果为b
多条件:
case 语句:
循环语句
while 循环:
do-while() 循环:
for() 循环:
库文件
程序中包含头文件有两种形式:
  1. #include <xxx.h>
  1. #include "xxx.h"
第一种形式会提示系统去指定的文件目录中查找头文件,一般用于包含 gcc 的系统头文件(标准库);
第二种形式会提示系统先在当前目录下寻找头文件,如果找不到再到系统头文件目录中去寻找,一般是用户自定义的文件。
Linux 系统的头文件目录有两个,分别为:
  1. /usr/local/include
  1. /usr/include
Windows 的头文件目录比较多而且复杂,大家如果感兴趣可以上网搜搜。一般我们都会将头文件和库文件存放在自己指定的目录中,在使用 gcc 编译的时候指定就行,具体的方式可以参考我的 gcc 笔记:

一般在 C 语言的开头都要引入标准库文件(可以使用一些基础函数,比如 printf(...)):
如果要使用 C99 新标准,在引用前面 <stdio.h> 库文件的同时还要引入下面的库文件:
C 语言中主函数的写法
变量定义(C 90 和 C99)
什么是 C90 标准,什么是 C99 标准?
C90 标准(ANSI C / ISO C89):
  • 正式名称:ISO/IEC 9899:1990(ANSI 于 1989 年通过,也称 C89)。
  • 背景:首个官方国际标准,旨在统一当时混乱的 C 语言实现(如 K&R C)。
  • 关键特性
    • 规范了语法和标准库(如 stdio.hstdlib.h)。
    • 要求函数原型声明(如 int func(int a);)。
    • 支持 const 和 volatile 关键字。
    • 变量声明必须在作用域开头(如函数或代码块开始处)。
    • 仅支持 /* ... */ 注释,无单行注释。
  • 遗留问题:允许隐式函数声明(未声明直接调用函数可能导致警告)。

C99 标准(ISO C99):
  • 正式名称:ISO/IEC 9899:1999(1999 年通过的新标准,应用更为广泛)。
  • 背景:在 C90 基础上引入现代特性,增强灵活性和功能。
  • 关键改进
      1. 变量声明位置自由:可在代码中间声明(如 for (int i=0; ...))。
      1. 单行注释:支持 // 注释。
      1. 新数据类型
          • long long(64 位整数)、_Bool(布尔类型,需 <stdbool.h>)。
          • 复数支持(_Complex 和 <complex.h>)。
      1. 变长数组(VLA):数组长度可用运行时变量定义(如 int arr[n];)。
      1. 复合字面量:直接构造匿名结构或数组(如 (int[]){1, 2, 3})。
      1. inline 函数:通过 inline 关键字提示编译器内联优化。
      1. 预处理增强:支持 #pragma 和 __func__ 宏(获取当前函数名)。
      1. 更严格的语法:禁止隐式函数声明,要求 main 函数显式返回类型(如 int main())。
(C90 标准)变量定义(旧标准)
数据类型
占用 bit
范围
char
8
0~255 或 -128~127 (取决于编译器实现) 课程所用 MinGW64,对 char 的实现是:有符号 即:-128~127
unsigned char
8
0~255
signed char
8
-128~127
short
16
-32768~32767
unsigned short
16
0~65535
int
32
-2147483648 ~ 2147483647
unsigned int
32
0~4294967295
long
32(Windows) 或 64(Mac、Linux)
32bit 有符号范围 或 64bit 有符号范围
long long
64
-2^63 ~ 2^63-1
unsigned long long
64
0 ~ 2^64 - 1
浮点数:
数据类型
占用 bit
精度
float
32
6-7 位有效数字
double
64
15-16 位有效数字
long double
80~128
18-19 位或更高 有效数字
(C99 标准)变量定义(整型数据的定义)
使用新标准的变量定义需要在代码文件开头加上:
数据类型
占用 bit
范围
int8_t
8
-128~127
int16_t
16
-32768~32767
int32_t
32
-2147483648 ~ 2147483647
int64_t
64
-2^63 ~ 2^63-1
uint8_t
8
0~255
uint16_t
16
0~65535
uint32_t
32
0~4294967295
uint64_t
64
0 ~ 2^64 - 1
bool (需要 #include <stdbool.h>
8
0 或 1 ; 0 表示 false 假,1 表示 true 真
交互相关指令
printf 打印输出(占位符)
printf() 语句中的更多占位符:
常用:
打印格式
对应数据类型
含义
%c
char
字符型,输入的数字按照ASCII码相应转换为对应的字符
%d
int
接受整数值并将它表示为有符号的十进制整数
%f
float
单精度浮点数
%s
char *
字符串。输出字符串中的字符直至字符串中的空字符(字符串以'\0‘结尾,这个'\0'即空字符)
%p
void *
以16进制形式输出指针
%m.nf
float
%f 相比,能够实现精度控制。m 表示输出数据的总宽度(整数位数+小数点+小数位数)
%.nf
float
很多时候,m 数据总宽度是根据我们的输入由系统自动判定的,我们只需要控制小数位数即可,因此 m 可以省略不写。
其他占位符:
%lf
double
双精度浮点数
%hd
short int
短整数
%hu
unsigned short
无符号短整数
%u
unsigned int
无符号10进制整数
%ld
long
接受长整数值并将它表示为有符号的十进制整数
%e,%E
double
科学计数法表示的数,此处"e"的大小写代表在输出时用的"e"的大小写
%o
unsigned int
无符号8进制整数
%x,%X
unsigned int
无符号16进制整数,x对应的是abcdef,X对应的是ABCDEF
getchar()scanf() 读取输入
getchar() 函数用于从标准输入(通常是键盘)读取一个字符。它返回一个int类型的值,但通常被赋值给char类型的变量。
scanf() 函数是 C 语言中用于格式化输入的函数,可以用来读取字符、整数、浮点数等。
输入语法: scanf("格式化字符串", &变量1, &变量2,...);
scanf() 中的 & 表示的是取地址符,scanf 需要知道变量的地址才能够将读取到的内容赋值给变量。

scanf() 的占位符与 printf 基本一致,占位符如下:
占位符
对应数据类型
输入示例
宽度限制说明
注意事项
%d
int
123
可指定宽度(如 %4d 最多读4位数字)
自动跳过前导空白符,读取到非数字为止。
%i
int(自动进制解析)
0x1A(十六进制)
同 %d
可识别十进制、八进制(0开头)、十六进制(0x开头)。
%u
unsigned int
456
同 %d
不接受负号输入。
%f
float
3.14
可指定精度(如 %6f
输入可为整数或浮点数(如 123 或 123.45)。
%lf
double
3.1415926
同 %f
注意scanf中必须用 %lf,而 printf 中 double 用 %f
%c
char
A
可指定宽度(如 %3c 读3字符)
不会跳过空白符!需谨慎(如空格、换行符会被读取)。
%s
char[](字符串)
Hello
必须指定宽度(如 %9s 最多读9字符)
自动跳过前导空白符,读到空白符停止。不检查缓冲区溢出,需手动限制长度。
%o
unsigned int(八进制)
077
同 %d
输入需为八进制数字(可带或不带前导0)。
%x / %X
unsigned int(十六进制)
0x1F / 1F
同 %d
不区分大小写,可省略前导0x
%e / %E
float / double(科学计数法)
6.02e23
同 %f
指数符号 e 或 E 均可。
%g / %G
float / double(自动选择格式)
6.02e23 或 123.45
同 %f
自动根据数值大小选择普通或科学计数法。
%p
void*(指针地址)
0x7ffd1234
输入需为指针格式(如 %p 通常匹配 printf 输出的地址格式)。
%n
int*(记录已读字符数)
-
不消耗输入,用于统计已读取的字符数(如 scanf("%d%n", &num, &count))。
%[]
char[](自定义字符集)
abc123
必须指定宽度(如 %10[0-9]
%[a-z]:只接收小写字母- %[^0-9]:排除数字的任意字符。
%%
匹配字面量 %
%
两种特殊的关键字
const
const 关键字修饰的变量会成为常量,是不可更改的。

const 修饰普通变量
定义方式是在普通变量定义前加上 const 关键字:
const 修饰指针变量
主要有两种方式:
  • const int *p 这样类型的表示指针指向的变量的值是不能修改的,但指向的变量可以更改,通过修改指向的变量可以间接修改值;
  • int * const p 这样类型的表示指针指向的内存区域是不能更改的,但是指向的变量的值可以更改。
记忆方法为,从左往右看,const 如果修饰的是 *p 这个整体(将 * 看作是取值操作),则表示值为常量;如果修饰的只有 p 那么表示地址为常量。
示例:
volatile
volatile 关键词是一个类型修饰符,与 const 类似。用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程(多线程共享变量)等。
一般来说,编译器会对代码进行优化,比如下面这段代码:
在开始时,我们将 i 赋值为 10,并将其赋值给 a,此时 a 的值就为 10。在之后运行了很多代码后(其中并没有代码对 i 进行修改),将 i 的值再次赋值给 b
如果我们没有声明 ivolatile 类型,那么,由于在 int a = iint b = i 之间没有对 i 的值进行修改,编译器就会认为 i 的值没有变化,因此将值 10 赋给 b
但如果声明了 ivolatile 类型,情况就会不同,编译器不再对代码进行优化,系统会重新从 i 所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。这样,当我们操作硬件设备时,如果通过硬件设备对 i 的值进行了修改,即便代码内部没有对 i 进行修改,操作系统也能够正确读取出 i 的值。

修饰指针:
const 关键字一样,volatile 也能够对指针类型变量进行修饰,而且也分为两种类型:
  1. volatile int *p
  1. int* volatile p
第一个是修饰指针所指向的变量为 volatile 类型(最常用的方式),即指针指向的内容是易变的,比如:
第二个是修饰指针本身的地址为 volatile 类型,即指针本身的地址是易变的(这种情况非常少见)。
宏定义与预处理指令
除了使用 const 关键字定义常量以外,还可以使用宏定义的方式定义。
宏定义
定义的基本语法为:#define 名 值
📌
定义时需要注意两点:① 语句结尾不带分号;② 宏定义的名字最好全部大写
宏定义的本质是变量替换,如果宏定义中的值为表达式,那么也是先进行替换后再对替换后的表达式整体进行计算。
预处理指令(条件编译)
条件编译语法:
其中,条件表达式必须是预处理器可计算的常量表达式(仅支持宏、字面量、整数运算,不支持变量),如果条件表示式的值为 0,则直接跳转到 #endif 语句之后的指令。在条件表达式中,可以使用 && 表示逻辑与,|| 表示逻辑或。
判断值是否非 0:
判断宏是否定义:
多条件编译语法:
条件编译与 if 表达式一样,也可以多条件执行。
如何表示不同进制数
十进制
以正常数字1-9开头,如 15
八进制
以数字0开头,如 017
十六进制
以0x或0X开头,如 0xf
二进制
以0b或0B开头,如 0b1111
类型转换
隐性转换:
在运算过程中,编译系统会进行隐性的类型转换。隐性转换由占用内存少的向占用内存多的类型进行转换。
notion image
强制类型转换(Explicit Type Casting)
强制类型转换是一种显式地将变量或表达式从一种数据类型转换为另一种数据类型的操作。它允许程序员在代码中主动指定类型转换,覆盖编译器的默认类型推断规则(隐性转换)。
强制类型转换的方式为 (目标类型) 表达式 ,比如:
函数
函数定义
示例:
多文件编程
xxx.h 的作用
把函数声明放在头文件 xxx.h 中,在主函数中包含相应头文件即可调用头文件中声明的函数;
在头文件对应的xxx.c中,实现xxx.h声明的函数:
notion image
当一个项目比较大时,往往都是分文件,这时候有可能不小心把同一个头文件 include 多次,或者头文件嵌套包含。
避免头文件被多次 include
为了避免同一个文件被 include 多次,C/C++中有两种方式。
第一种方式:
第二种方式:
头文件包含关系(<>"" 的不同)
  • <> 表示系统直接按系统指定的目录检索
  • "" 表示系统先在 "" 指定的路径(没写路径代表当前路径)查找头文件,如果找不到,再按系统指定的目录检索
extern 关键字
extern主要用于声明外部变量或函数,当我们将一个变量或函数声明为extern时,那么就表示该变量或函数是在其他地方定义的,我们只是在当前文件中引用它。
 
 
函数地址的三种表示方法
函数声明
一般来说,要调用一个函数,该函数就必须在调用函数的代码前进行定义。但有时为了让我们的主函数放在前面,我们会选择在调用前声明函数,再在代码最后进行定义。
函数的声明方式为: 返回值 函数名(形参列表); ,其中形参列表可以写上类型名和变量名,也可以只写出各个形参的类型名称。
比如, int add(int a, int b);void func(int, char);
指针
介绍
指针指向的是内存地址,而指针变量用于存储内存地址,通过指针变量可以直接操作内存空间,如修改内存、传递地址等。
指针变量与其他变量一样,也有不同的类型,如 intchar 等。不同类型的指针变量分配的地址空间是不同的,如 int 型的指针变量的地址空间为 4 字节。
取地址符 & 和解引用符 *
& (取地址符):
  • 如果是 a & b是按位与;
  • 如果是 &a 则是取地址符,表示取变量的内存地址;
*指针符号 解引用 符号):
  • 用在变量声明中,表示此变量为指针变量,即 指针符号
  • 用在指针变量上,表示根据其记录的地址,直接操作内存中的,即 解引用符号
sizeof() 运算符
用于计算变量占用的内存空间大小,如果该变量为指针变量,则在 64 位的系统中,固定为 8 字节,在 32 位的系统中,固定为 4 字节。
野指针和空指针
只有取出变量或函数的地址赋给指针才有意义,其他任意数值赋值给指针变量没有意义,这样的指针会成为野指针,指向的区域为未知。
野指针本身并不会有什么问题,但如果操作野指针指向的内存区域就会出现问题。

如果一个指针暂时不需要指向什么内存区域,可以将其赋值为 NULL ,比如 int *p = NULL .
多级指针
在 C 语言中, 一级指针和二级指针是最为常用的,所谓的二级指针,就是用于存放一级指针的地址的指针变量;而更多级的指针,以此类推,比如三级指针用于存放二级指针的地址。
示例:
常量指针
使用 const 关键字进行修饰。
  • const int *p 这样类型的表示指针指向的变量的值是不能修改的,但指向的变量可以更改,通过修改指向的变量可以间接修改值;
  • int * const p 这样类型的表示指针指向的内存区域是不能更改的,但是指向的变量的值可以更改。
记忆方法为,从左往右看,const 如果修饰的是 *p 这个整体(将 * 看作是取值操作),则表示值为常量;如果修饰的只有 p 那么表示地址为常量。
示例:
指针作为函数参数
普通变量作为函数参数的示例
 
函数指针
与过往学习的整型指针类似,函数指针也是指针,只不过这个指针是函数类型的指针,指向的是函数。
函数指针的表示方法为: 返回值 (*函数指针变量)(形参列表);
其中返回值表示的是函数的返回值,函数指针变量表示的是该指针的名称,形参列表为函数的形参列表。
我个人觉得,之所以要带上返回值和形参列表,是因为两个函数在函数名相同且形参相同时,若返回值不同就表示不同的函数,同样地,若函数名相同且返回值相同,若形参列表不同就表示不同的函数。
malloc() 申请堆内存
malloc() 函数可以用于动态分配堆内存,堆内存与函数内存不同,函数内存是栈内存,是系统自动创建与销毁的,而堆内存是用户自行创建的,需要手动销毁。
当进行指针函数的调用时,需动态分配内存给用于返回的指针,否则在退出指针函数后,原先指针的内容可能会被覆盖或被系统回收,造成输出乱码。
malloc() 函数创建堆内存的方法很简单,在括号中填上内存大小就行(malloc(内存大小) ),比如 malloc(sizeof(int)) 会动态分配 int 长度的内存。一般将其与强制类型转换配合使用,malloc 默认返回 void* 类型(通用指针)。,如 (int*)malloc(sizeof(int)) 将动态堆内存强制转换为 int 类型内存区域,并将其首地址返回。
如果后续要释放该区域的内存,使用 free(指针) 即可。
指针函数
指针函数的主体是函数,前面的修饰词“指针”表示的含义是该函数的返回值为指针,类似的说法有整型函数,空函数等,它们的返回值分别为整型变量与 void
要注意,上述代码里指针函数中返回的指针变量指向的内存区域必须是堆内存,这样在退出函数后,该指针变量指向的内存区域仍然会存在。
如果代码如下面所示,输出很可能会乱码:
函数指针和指针函数的声明
指针函数的声明如下:
函数指针的声明如下:
回调函数
函数指针变量做函数参数,这个函数指针变量指向的函数就是回调函数。
回调函数可以增加函数的通用性,在不改变原函数的前提下,增加新功能。
数组
数组的定义与初始化
  • 数组是 C 语言中的一种数据结构,用于存储一组具有相同数据类型的数据。
  • 数组中的每个元素可以通过一个索引(下标)来访问,索引从 0 开始,最大值为数组长度减 1。
数组定义的方式如下:
初始化:
在定义数组的同时进行赋值,称为初始化。
全局数组若不初始化,编译器将其初始化为零局部数组若不初始化,内容为随机值
数组首地址
数组名是一个地址的常量,代表数组中首元素的地址,即 arr == &arr[0]
数组长度的计算
指针操纵数组元素
数组名字是数组的首元素地址,但它是一个常量, *[] 效果一样,都是操作指针所指向的内存。
数组名做函数参数
指针数组
本质为数组,定义方式为: int* p[n]
指针数组是用于存放指针的,数组中每个元素都是指针类型。
数组指针
本质上是指针。定义方式为 int (*p)[n]
数组指针的定义与指针数组特别类似,但数组指针是指向数组首地址的指针,存放的是数组的首地址。
二者的定义可以通过这样的方式来记忆:看是否有括号。括号具有最高优先级,当使用括号包裹 *p 时,表示 p 是一个指针;如果定义时类型名与 * 连在一起,如 int* p[],就表示 p 是一个数组,存放的类型为 int* ,即整形指针。
字符指针
  • 字符指针可直接赋值为字符串,保存的实际上是字符串的首地址;
  • 字符串指针所指向的内存不能修改,指针变量本身可以修改,相当于是加上了 const 关键字。
字符数组与字符串
  • C语言中没有字符串这种数据类型,可以通过 char 类型的数组来替代;
  • 数字 0 (和字符 '\0' 等价) 结尾的 char 数组就是一个字符串,字符串是一种特殊的 char 的数组;
  • 如果 char 数组不以 0 结尾(最后一个字符不是 0),那么 char 只是一个字符数组。
字符串的读取与输出
gets(str);fgets(str, n, stdin); 读取;printf("%s\n", str); 输出。
字符串的常用函数
在使用前需要加上头文件引用: #include <string.h>
计算字符串的长度: strlen(s)
计算指定指定字符串 s 的长度,不包含字符串结束符 '\0'
复制字符串:strcpy(dest, src)
src 所指向的字符串复制到 dest 所指向的空间中,'\0' 也会拷贝过去。
如果复制失败会返回 NULL
 
拼接字符串: strcat(dest, src)
src 字符串连接到 dest 的尾部,'\0'也会追加过去。
如果拼接失败会返回 NULL
字符串比较: strcmp(s1, s2)
比较 s1 和 s2 的大小,比较的是字符 ASCII 码大小。
依次比较 s1 和 s2 的每一个字符,如果相等则返回 0,如果前者更大,则返回正数,如果后者更大则返回负数。
如果本篇笔记对你有用,能否『请我吃根棒棒糖🍭 』🤠…
Loading...