1. auto 和 register
auto关键字
- 只能修饰局部变量, 局部变量如果没有其它修饰符, 默认就是auto的
- 特点: 随用随开, 用完即销
register关键字
- 只能修饰局部变量, 原则上将内存中变量提升到CPU寄存器中存储, 这样访问速度会更快
- 但是由于CPU寄存器数量相当有限, 通常不同平台和编译器在优化阶段会自动转换为auto
2. static
对局部变量的作用
- 延长局部变量的生命周期,从程序启动到程序退出,但是它并没有改变变量的作用域
定义变量的代码在整个程序运行期间仅仅会执行一次
原因是静态局部变量的初值是在编译时赋予的,在程序执行期间不再赋予初值
对全局变量的作用
- 默认情况下多个同名的全局变量共享一块空间, 这样会导致全局变量污染问题
- 如果想让某个全局变量只在某个文件中使用, 并且不和其他文件中同名全局变量共享同一块存储空间, 那么就可以使用static
3. extern
对局部变量的作用(无意义的)
- extern不能用于局部变量
- extern代表声明一个变量, 而不是定义一个变量, 变量只有定义才会开辟存储空间
- 所以如果是局部变量, 虽然提前声明有某个局部变量, 但是局部变量只有执行到才会分配存储空间
对全局变量的作用
- 声明一个全局变量, 代表告诉编译器我在其它地方定义了这个变量, 你可以放心使用。举例来说,如果文件a.c需要引用b.c中变量int v,就可以在a.c中声明extern int v,然后就可以引用变量v。
与包含头文件引用函数的区别
对变量而言,如果你想在本源文件(例如文件名A)中使用另一个源文件(例如文件名B)的变量,方法有2种:(1)在A文件中必须用extern声明在B文件中定义的变量(当然是全局变量);(2)在A文件中添加B文件对应的头文件,当然这个头文件包含B文件中的变量声明,也即在这个头文件中必须用extern声明该变量,否则,该变量又被定义一次。
对函数而言,如果你想在本源文件(例如文件名A)中使用另一个源文件(例如文件名B)的函数,方法有2种:(1)在A文件中用extern声明在B文件中定义的函数(其实,也可省略extern,只需在A文件中出现B文件定义函数原型即可);(2)在A文件中添加B文件对应的头文件,当然这个头文件包含B文件中的函数原型,在头文件中函数可以不用加extern。
总结:
使用extern时,会加速程序的编译(确切的说是预处理)的过程,节省时间。但是当需要对变量/函数名进行修改时需要把所有使用extern关键字的地方全部修改一次,而使用头文件包含只需更改一次,效率更高。
4. 条件编译
- 是将代码编译进可执行程序, 而不是执行代码
- 条件编译后面的条件表达式中不能识别变量,它里面只能识别常量和宏定义
5. typedef
C语言不仅提供了丰富的数据类型,而且还允许由用户自己定义类型说明符,也就是说允许由用户为数据类型取“别名”。
基本数据类型
1
2
3typedef int INTEGER;
typedef INTEGER MyInteger;
MyInteger a; // 等价于 int a;数组
1
2typedef char NAME[20]; // 表示NAME是字符数组类型,数组长度为20。然后可用NAME 说明变量
NAME a; // 等价于 char a[20];结构体
1
2
3
4
5
6typedef struct {
int age;
char *name;
} PersonType;
PersonType person;
//有多种写法,此写法可读性强一些指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14// 定义一个sum函数,计算a跟b的和
int sum(int a, int b)
{
int c = a + b;
printf("%d + %d = %d", a, b, c);
return c;
}
typedef int (*MySum)(int, int);
int main()
{
MySum p = sum;// 定义一个指向sum函数的指针变量p
printf("%d",(*p)(1,2)); //3
return 0;
}详细解释可参考FreeRTOS的任务相关 | 小董的BLOG (gitee.io)中有详细案例
也就是说,在正常的定义前加上typedef,即把该定义的变量改为一个新的类型,要再次定义该类型的变量时,直接使用该类型的名字即可。
6. const
- 对变量声明只读特性,保护变量值以防被修改。
使用方法
修饰常量、数组
1
2const int a[5]={1, 2, 3, 4, 5};
a[1] = 55; // 错误修饰指针
- const int *A; //const修饰指针,A可变,A指向的值不能被修改
- int const *A; //const修饰指向的对象,A可变,A指向的对象不可变
- int *const A; //const修饰指针A, A不可变,A指向的对象可变
- const int *const A;//指针A和A指向的对象都不可变
ps.以下开始不是关键字
1、宏定义
对程序中用双引号扩起来的字符串内的字符,不进行宏的替换操作
宏定义与变量定义不同,宏定义仅是一种机械的字符替换,并不分配内存空间
宏名的有效范围是从定义位置到文件结束。如果需要终止宏定义的作用域,可以用#undef命令
定义一个宏时可以引用已经定义的宏名
1
2
3
4可用宏定义表示数据类型,使书写方便
1
2
3
4
5
6
int main()
{
String str = "This is a string!";
return 0;
}C语言允许宏带有参数。在宏定义中的参数称为形式参数,在宏调用中的参数称为实际参数。(类似函数的使用)
1
2
3
4
5
6
7
8
int main ()
{
int a = average(10, 4);
printf("平均值:%d", a); //7
return 0;
}带参数的宏在展开时,只作简单的字符和参数的替换,不进行任何计算操作。所以在定义宏时,一般用一个小括号括住字符串的参数。
1
2
3
4
5
6
7
8
9
10
11
12
//计算结果最好也用括号括起来,小型计算不括问题应该不大
int main ()
{
int b = D1(3+4);
int c = D2(3+4);
printf("%d", b); //输出等价于2*3+4=10
printf("%d", c); //2*(3+4)=14
return 0;
}
2、动态存储分配
此前,用于存储数据的变量和数组在定义后,C编译程序通过定义的类型来了解其所需存储空间的大小并预先为其分配适当的存储空间,这些空间一经分配,在变量或数组的生存期内是固定不变的,所以这种方式也称为“静态存储分配”
C中还有一种“动态存储分配”的方法,在程序执行期间需要空间来存储数据时,通过申请分配指定的内存空间;当有闲置不用的空间时可以随时将其释放。C标准库提供了相关函数来进行对应操作:
库函数
malloc函数
| 函数名 | malloc(unsigned int size); |
|---|---|
| 参数 | size:字节数 |
| 返回值 | 成功则返回一个类型为void的首地址,失败为null |
| 功能 | 在内存的动态存储区中分配一块长度为size字节的连续区域 |
例:
1 | //使用时需要包含"stdlib.h"或"malloc.h" |
- 需要注意的是,malloc只会负责分配空间。并不能对其进行初始化,所以此时*pi的值是随机的
- 该函数常用于初始化一个指针,这样可以避免使该指针成为野指针
calloc函数
| 函数名 | calloc(unsigned int num,unsigned int size) |
|---|---|
| 参数 | num:分配数据项的个数 size:每个数据项的大小 |
| 返回值 | 成功则返回一个类型为void的首地址,失败为null |
| 功能 | 在内存的动态存储区中分配n块长度为size字节的连续区域 |
- 不同于malloc,calloc会对分配的存储单元值自动初始化为0
因此,可以用以下方法开辟一个一维数组
1 | char *p; |
free函数
| 函数名 | free(void *ptr); |
|---|---|
| 参数 | 一个指针 |
| 返回值 | 无 |
| 功能 | 释放ptr所指向的一块内存空间 |
- 需要注意的是,free释放的区域应为malloc或calloc所分配的区域
- free函数的实际功能是告诉系统输入参数对应的内存不再使用,即不再被保护,也就是说其他变量随时会占用占用那个地址。因此,原则上不再访问已经被free的的内存