OSpre——C

1. 源代码到可执行文件

  • 预处理->编译->汇编->链接

1.1 预处理——处理#

gcc -E hello.c -o hello.i   ## -E选项
  • 预处理的工作内容:处理#开头的预处理指令
    • 删除#define,展开宏定义
    • 处理条件编译指令,#if,ifdef
    • 处理#include预处理指令,将包含的文件内容插入到该预处理指令的位置

1.2 编译——预处理文件生成汇编代码文件

gcc -S hello.i -o hello.s   # -S参数

1.3 汇编——汇编代码转换为机器代码

gcc -c hello.s -o hello.o   # -c参数

1.4 链接

​ 把多个目标文件的代码段放在一起、数据段放在一起,以及库函数等形成可执行文件。

生成可执行文件过程.drawio

2. C语言中变量存储类别

2.1 存储期

2.1.1对象与标识符

​ C语言中对象是连续的一片内存空间,具有起始地址与大小两个属性。标识符是我们用来访问修改对象的字符串(变量名)

例如

int a = 5; // &a = 0x1000

​ 我们声明了一个int类型,起始地址为0x1000,占用内存大小为4字节的整型变量,变量的标识符为a。

2.2.2 存储期——变量在内存中的生命周期

  • 静态存储期static

    • 使用static关键字定义的变量static int a
    • 在函数外定义的变量(全局变量)

    若对象具有静态存储期,则在程序运行期间一直存在。并且对象的属性不变,即对象的起始地址和所占用的内存空间大小不会变化,不初始化自动初始化为0

  • 自动存储期auto

    • 不使用static关键字定义的变量(例如局部变量)auto int a

      程序执行到该变量声明的时候会创建变量对应的对象,在执行到该变量作用域结束后释放对象,不进行初始化则初始值不确定

      • 如在函数中生命的局部变量,在他的一次调用中的作用域中具有固定的值和地址属性,不同的调用地址属性可能不同

2.2 作用域——标识符在程序中可以被使用的区域

  • 块作用域:块(block)是用花括号括起来的代码区域。定义在块中的变量具有块作用域。块作用域变量的可见范围是从定义处到包含该定义的块的结尾(右花括号)。(对应着局部变量)

    int foo(int a, int b) {
    	int c; //c 作用域开始
    	{
    		c=1;
    		int d; // d作用域开始
    		d=0;  
    	} // d作用域结束
    } // abc作用域结束
    

    注:函数的形式参数块作用域属于函数体块

  • 文件作用域:在函数外定义的变量,从定义处到文件末尾均可见

  • 全局变量:文件作用域,静态存储期

  • 局部变量:块作用域,自动存储期

2.3 链接

链接属性:该变量是否可在别的文件中被使用

  • 内部链接:内部链接变量只能在定义他的文件中使用

    • 使用static关键字声明的全局变量

      static int a = 1;//static 修饰全局变量
      
  • 外部链接:可以在所有文件中使用

    • 全局变量默认是外部链接的

      int b = 2//默认全局变量
      
  • 无链接:变量没有链接属性

    • 在函数中定义的变量,即只有全局变量有链接属性,局部变量没有

      void foo(int c) {
      	int d;
      }
      

​ 若要在外部文件中使用具有外部链接属性的变量,需要extern关键字进行引用式声明

  • 定义式声明:创建对象

  • 引用式声明:引用其他地方变量

    extern int foo;
    

2.4 总结

存储类别说明符变量声明位置存储期链接备注
auto函数内自动存储期无链接auto 关键字可以省略
static函数内静态存储期无链接
static函数外静态存储期内部链接
函数外静态存储期外部链接若要在别的文件中使用这种变量需要使用 extern 关键字进行引用式声明
extern函数内静态存储期引用的变量需要具有外部链接引用式声明,不会创建对象
extern函数外静态存储期引用的变量需要具有外部链接引用式声明,不会创建对象

,有以下三个C语言文件

/* main.c */
static int v;

extern int func(int value);

int main() {
    int value = 1;
    v = func(value);
    {
        extern int value; // 在块中引用变量 value value的作用域为当前代码块 value = 2
        v += func(value);
    }
    printf("%d\n", v);
    return 0;
}
/* value.c */
int value = 2;
/* func.c */
int func(int value) {
    static int x = 1; //static类型 静态存储期 会保留前一次调用后x的值,不会进行重新初始化
    int y = 1; // auto类型 每次调用重新初始化
    x += value;
    y += value;
    return x + y;
}

main.c中引用了value.c中的全局变量和func.c中的函数,编译他们需要同时进行编译

gcc main.c value.c func.c
./a.out

​ 输出结果为11。

3. 函数的存储类别

  • 默认函数为外部链接

    使用别的文件中的函数,使用extern关键字进行声明

    extern int bar(int a)
    
  • 函数内部链接:在函数定义前加上static关键字

4. 预处理指令

4.1 宏定义

  • 变量式宏定义

    #define N 20
    
  • 函数式宏定义

    #define MAX(a,b) ((a) > (b) ? (a) : (b))
    

预处理器发现程序中的宏后,会用宏等价的替换文本进行替换(只是字符串层面的替换),直到不包含宏为止。

  • 预处理器只负责对宏定义进行形式上的替换,函数式宏定义的参数没有类型,不做参数检查即如果参数发生类型错误,在预处理阶段不会报错,在编译阶段报错

4.2 在MOS内核中推荐的函数宏定义形式

#define MACRO_NAME(para1, para2)\
do {\
	express1;\
	express2;\
}while(0)
  • 注:在函数宏定义中每一行后添加的,其作用就相当于换行,防止一些离谱的错误

4.3 宏定义运算符

4.3.1 宏参数创建字符串 #运算符

#是预处理运算符,用于创建字符串,#会把传入的参数自动合并为用双引号括起来的字符串,参数中的多个连续空格会被替换为一个空格。

#define toStr(s) #s
printf(toStr(hello world));

​ 经过#预处理后得到

printf("hello world!");

​ 若#在双引号中,需要在#外再加双引号使其发挥作用,例如

#define PSQR(x)  printf(" The square of " #x " is %d",((x)*(x)))
int y = 5;
PSQR(y);
// 输出:the square of y is 25

4.3.2 预处理器粘合剂 ##运算符

##运算符的作用是将前后两个预处理符号连接成一个预处理符号

#define CONCAT(a, b) a##b
// CONCAT(con, cat) 展开为 concat

4.3.3 变参宏:...__VA_ARGS__

  • 函数的宏定义的参数列表中使用…表示可变参数,在宏定义中可变参数的部分用__VA_ARGS__表示
#define showlist(...) printf(#__VA_ARGS__)
showlist(The first, second, and third items.);

​ 预处理后结果为

printf("The first, second, and third items.");

​ 若##运算符用在__VA_ARGS__前面,当它为空参数时,##运算符会把他前面的,吃掉

  • 变参宏编写打印函数用于内核调试
    #define DEBUGP(format, ...) printf(format, ## __VA_ARGS__)
    // DEBUGP("info no. %d", 1) 会展开为 printf("info no. %d", 1)
    // DEBUGP("info") 会展开为 printf("info"),注意展开式中的宏定义中的 format 后的逗号没有了。
    

4.3.4 关于define

  • undef

    #undef <macro>取消对宏的定义,可以对<macro>赋新值。若之前没有定义过<macro>则会被忽略

  • #include

    • #include<name.h> 在标准包含目录中查找该文件
    • #include"name.h"现在引用该头文件的目录下查找,然后查找标准包含目录
    • 注:使用<>的一般是官方头文件,““一般是自行定义的头文件
  • ifndef:if not define

    常见于头文件的编写中,用于避免头文件的内容被重复包含。

    //例如stdio.h
    #ifndef _STDIO_H
    #define _STDIO_H
    ...
    #endif
    

    这个规范要应用于自己的编写中

4.3.5 typedef 与 #define的区别

​ 区别在于,#define 是单纯的字符串替换,在预处理阶段完成,没有作用域。而 typedef 是给类型一个别名,在编译阶段完成,它有自己的作用域。 typedef 一般用来定义类型的别名,定义与平台无关的数据类型,与 struct 的结合使用等。常见的 typedef 别名风格是以 _t 结尾,如 integer_t 或者 ptr_t

  • #define 出的类型宏可以被其他说明符修饰,但 typedef 定义的别名本身就是一个类型(类型说明符),所以在声明中不能和 unsigned 等其他类型说明符一起出现:
#define INTEGER int;
unsigned INTEGER n;  //没问题
typedef int integer_t;
unsigned integer_t n;  //错误,不能在 integer_t 前面添加 unsigned
  • typedef 定义的类型名用来连续声明几个变量时,能够保证定义的所有变量均为同一类型,而 #define 则无法保证。
#define PTR_INT int *
PTR_INT p1, p2;        //p1、p2 类型不相同,宏展开后变为 int *p1, p2;
typedef int * ptr_int_t;
ptr_int_t p1, p2;        //p1、p2 类型相同,它们都是指向 int 类型的指针。