从0到1的硬件学习01 C语言语法基础
本篇是 C 语言语法基础的速览,从一个最简单的 C 程序出发,依次介绍基本数据类型、运算符、分支与循环、函数、数组、结构体、枚举和 typedef 等内容,为后续嵌入式开发打基础。
第一个C程序
先来看一个最简单的C程序:
1 |
|
程序输出Hello C!,虽然只有几行代码,但是已经包含几个C语言最基本的概念了。
#include <stdio.h>是什么
这一行#include <stdio.h>表示
将标准输入输出相关的声明引入当前源文件。
这里使用到的printf()就是标准库提供的输出函数。可以简单理解为stdio.h告诉编译器printf、scanf函数是什么。
理解int main(void)
int main(void)可以拆成:
int-> 返回类型main-> 函数名称(void)-> 参数列表
int 表示这个函数最终返回一个整数,比如return 0;这里返回0就是一个int。通常约定0表示程序正常结束,非0通常表示发生错误。
main这是程序的入口函数,程序运行以后,会从main()开始执行。
void它是一个类型关键字,在这里表示这个函数不接收任何参数,所以int main(void)完整的解读就是定义一个名为main的函数,它不接收参数,并返回一个int。另外void还可以表示”没有返回值”,比如:
1 | void hello(void) |
这里出现了两个void,前一个void表示不返回任何值,后一个void表示不接收参数。可以理解为输入无,输出无。而int add(int a, int b)表示输入为两个int,输出为一个int。
基本的数据类型
C语言中最常见的数据类型包括:
charshortintlongfloatdouble
例如:
1 | int age = 20; |
这里需要注意的地方'A'表示一个字符,而"A"表示字符串,这两者并不是一个东西。
signed与unsigned
整数类型可以分成:signed和unsigned,例如:
1 | int a = -10; |
unsigned表示这个整数没有负数范围。
固定宽度整数
普通的int、long和short在不同平台上的大小并不一定完全一致。因此在开发中经常会使用:
1 |
|
uint8_t为无符号8bit,uint16_t为无符号16bit,uint32_t为无符号32bit,int32_t为有符号32bit。这在操作硬件寄存器、传感器数据、通信协议时非常有用。
sizeof
C语言可以使用sizeof来查看某个类型或者对象占用多少字节。比如:
1 | printf("%zu\n", sizeof(char)); |
在很多现代电脑上可能输出为:
1 | 1 |
**但不要永远假设int一定是4字节。**这就是为什么在嵌入式场景下经常更喜欢使用uint32_t这种明确指定宽度的类型。
使用printf
C语言最常用的输出函数就是printf(),比如:
1 | int age = 20; |
常见格式符:
| 格式 | 含义 |
|---|---|
%d |
int |
%u |
unsigned int |
%f |
浮点数 |
%c |
字符 |
%s |
字符串 |
%x |
十六进制整数 |
%z |
长度修饰符,专门用于格式化输出size_t或者ssize_t类型的数据 |
比如:
1 | int value = 255; |
输出:
1 | 255 |
在嵌入式开发中,经常需要输出十六进制:printf("reg = 0x%x\n", reg);因为硬件寄存器通常就是以十六进制形式查看的。
变量
C的变量需要先指定类型:int x;然后赋值:x = 10;,也可以直接初始化:int x = 10;,比较推荐每个变量单独写:
1 | int a = 1; |
虽然C语言也允许: int a = 1, b = 2, c = 3;但是前一种形式通常更容易阅读和维护。
const
如果一个值初始化以后不应该再改变,可以使用:const int max_count = 100;之后不能max_count = 200;目前可以简单把const理解成:
这个对象不应该再被修改
需要注意的是,const和指针组合以后会出现更复杂的情况:
1 | const int *p; |
这些会放在后面介绍。
算符运算符
常见算术运算符和大多数语言一致:+ - * / %。比如:
1 | int a = 10; |
整数除法
这里有一个非常重要的细节: 10 / 3结果是3,而不是3.333333。这是因为10和3都是整型,所以得到的也是整型。如果希望得到浮点数,可以使用10.0 / 3.0,又或者使用:
1 | int a = 10; |
这里的(dobule)a表示显式类型转换。
自增与自减
C提供自增与自减的方式:
1 | i++; |
最常见的场景是: for (int i = 0; i < 10; i++) {},如果只是单独写i++和++i最终结果通常一样。但是下面两种写法不同:int a = i++; int a = ++i;一个是先使用再增加,另一个是先增加再使用。
比较运算符
C的比较运算符包括:
1 | == 表示相等 |
比如if (age >= 18){},需要注意的是=表示赋值,而==才表示比较。
逻辑运算
逻辑运算符:
1 | && 代表与,即两者都为真,结果才为真 |
通常在判断中用的比较多,如:
1 | if (age >= 18 && age < 60) |
布尔值
传统C语言中,0表示false,非0表示true。比如:
1 | int value = 5; |
这里会进入if,现代版本的C可以:
1 |
|
分支、循环
if/else
1 | int age = 20; |
switch
如果多个判断条件指向同一个变量的话,可以使用switch来精简if/else的代码。
1 | int command = 2; |
这里需要注意,break;如果漏掉的话,就变成了:
1 | switch (x) |
当 x == 1时,可能输出:
1 | one |
因为程序没有在匹配到条件的时候跳出分支,会继续执行下一个case,这种行为叫fallthrough。
写一个
switch-case最好都带上default这个case,因为你无法保证一定不会出现什么漏掉的情况,此时default分支可以给你进行兜底。
while
当需要在满足某个条件时,一直执行某个操作,可以使用while。
1 | int i = 0; |
此时会输出:
1 | 0 |
格外注意的是,必须要在执行体内让循环判断条件不满足,不然的话会导致死循环。
do while
while是判断满足条件后执行某些操作,do while是先执行某些操作再进行条件判断。
1 | itn i = 0; |
for
C语言中最常见的循环结构,可以使用for:
1 | for (int i = 0; i < 10; i++) |
for循环的结构是for (初始化执行; 循环判断条件; 每轮循环后执行),上面的代码流程表示为:
- 初始化时声明一个变量
i,并初始化为0 - 判断循环条件
i是否小于10,如果小于那么执行循环体,如果大于等于,停止循环 - 然后开始执行循环体,输出当前的
i - 循环结束后,让变量
i自增 - 再次判断循环条件,执行循环体,直到循环条件不满足
break和continue
正常循环,需要等待循环条件不满足时才会停止循环,而break和continue可以提前结束循环。
1 | for (int i = 0; i < 10; i++) |
输出:
1 | 0 |
break关键字可以直接跳出整个for循环。而continue可以用来跳过当前那一轮循环:
1 | for (int i = 0; i < 10; i++) |
结果为:
1 | 0 |
函数
C函数需要写明:返回类型、函数名和参数类型,比如:
1 | int add(int a, int b) |
调用int result = add(10, 20);此时这里的10和20会作为参数放入add函数中,然后执行函数体将返回值赋值给result变量。
没有返回值的函数
如果函数没有返回值,可以这么写:
1 | void say_hello(void) |
这里前面的void是返回值类型,表示没有返回值。后面括号里的void表示没有参数。也可以直接写成void say_hello(),但是有一点区别,这个表示不指定参数。
值传递
C中默认是值传递,比如下面这个例子:
1 | void change(int x) |
输出依旧是10,这是因为在调用change函数的时候,a把值复制了一份给了x,而不是将自己放入到change中,所以这里被修改的是得到赋值后的x,而不是原本的a。如果想要函数能够修改外部的变量,那么就需要使用到指针。
函数声明与定义
1 | //这是函数声明 |
函数声明只确定了函数名以及返回值类型、参数,并不关心函数是什么实现的。而函数定义需要明确的函数体。区分这两者在学习.h和.c文件时会非常重要。
数组
1 | //创建一个包含5个整数的数组: |
数组创建时,int代表数组中存放的每个元素的类型,变量名后面的[]代表声明的变量是一个数组,[5]中的数字代表数组长度。数组要求内部的每个元素的类型都保持一致。
数组下标
数组下标从0开始,比如:
1 | numbers[0] |
一个长度为5的数组,最后一个元素是numbers[4]
遍历数组
1 | int numbers[] = { |
但是最好不要把5硬编码进去,可以这么写:
1 | int length = sizeof(numbers) / sizeof(numbers[0]); |
这个的原理是整个数组占用的字节数/一个元素占用的字节数=元素数量,比如20bytes / 4bytes = 5
数组越界
1 | int numbers[3] = { |
C语言不会像Java或者Python那些语言可靠地给一个清晰的越界异常。可能出现的是: 垃圾值、程序崩溃、其他内存被破坏、看似什么都没发生。这也是C非常核心的特点:
C给程序员很大的控制权,同时也给程序员更多责任。
二维数组
遍历二维数组:
1 | int map[2][3] = { |
struct把多个字段组合起来
struct是C中非常重要的一种数据结构。比如:
1 | struct Player { |
可以把它理解成一个纯数据结构,一个叫Player的结构,它有三种属性hp、mana和level。创建Player可以使用:struct Player player;,赋值可以使用:
1 | player.hp = 100; |
这样就将按照Player这种结构创造出来的实体,即player的每个属性都填充了值。读取也是类似的:printf("%d\n", player.hp);。
创建实例的时候进行初始化
1 | struct Player player = { |
struct可以进行嵌套
1 | struct Point |
struct数组
结构体也可以组成数组,比如有这样的一个结构体Player:
1 | struct Player |
然后构建结构体数组:
1 | struct Player player[3] = { |
访问:players[1].hp
enum一组有名字的整数
有时候我们需要存储某些有限个数带有含义的值,那么可以使用enum,比如:
1 | enum State |
默认情况下:
1 | STATE_IDLE = 0 |
在这种使用方式下enum State state = STATE_IDLE;比直接写int state = 0;更加容易被理解,毕竟看到0并不知道代表什么意思,而STATE_IDLE一眼就是知道这表示待机。
enum可以指定具体的数值
1 | enum Error |
enum和switch组合使用
状态机经常使用这种组合:
1 | enum State |
typedef给类型起别名
typedef的作用可以简单理解为给一个类型起一个新的名字。举个简单的例子:
1 | //以下两者是等价的 |
typedef + struct
C中非常常见的一种写法:
1 | typedef struct |
嵌入式代码经常可以看到,因此这种形式非常值得熟悉:
1 | typedef struct |
typedef + enum
同样可以:
1 | typedef enum |
之后State state = STATE_IDLE;代码会更加简洁。