我的C语言学习笔记(十五)

15.0.0 预处理指令

15.1.0 预处理指令概述

15.1.1 预处理指令的特点

  • 都是以#开头
  • 预处理指令都是在编译之前执行
  • 预处理指令后面都没有分号

C程序的一般流程:

  • 新创建一个.c的源文件,.c的文件是C程序的源文件
  • 在.c的源文件中写上符合C语法规范的源代码
  • C语言严格区分大小写
  • 除了字符串的内容,其他地方都必须使用英文字符
  • 使用cc -c指令编译源文件

格式:cc -c 源文件名称

  • 先执行源文件中的预处理指令
  • 检查源代码语法是否符合规范
  • 如果符合语法九生成.o目标文件,这个文件就是源代码.c文件对应的二进制指令;如果不符合就报错,不会生成目标文件
  • 链接

格式:cc .o目标文件

  • 为.o的目标文件添加启动代码
  • 链接函数。告诉编译器,要调用的函数在什么地方
  • 链接成功以后,就会生成一个可执行文件a.out

预处理指令的分类

  • 文件包含指令
  • 宏定义:可以将一段C代码定义为1个标识,使用这个标识就可以使用这段代码
  • 条件编译指令:只编译指定的C代码为二进制指令

15.2.0 宏

15.2.1 宏定义

  • 宏定义:它是一个预处理指令,所以它在编译之前执行。
  • 作用:可以为一段C代码定义一个标识,如果你要使用这段C代码,那么你就使用这个标识就可以了。
  • 语法结构:#define 宏名宏值
  • 如何使用宏:在C代码中,直接使用宏的名称就可以了。
#define N 10
int a = N + 1
  • 宏的原理:在预编译的时候,就会执行源文件中的预处理指令,会将C代码中使用宏名的地方替换为宏值。

将C代码中的宏名替换为宏值的过程叫做宏替换/宏代换。

15.2.2 使用宏的注意事项

  • 宏值可以是任意的东西。
  • 在定义宏的时候,并不会检查语法,只有在完成了宏替换的时候,才会检查替换后是否符合语法。
  • 如果宏值是一个表达式,宏值并不是这个表达式的结果,而是这个表达式本身。
  • 如果宏值中包括一个变量名,那么在使用这个宏之前必须要保证这个变量已经存在
  • 无法通过赋值符号为宏赋值
  • 宏的作用域:
  • 宏可以定义在函数的外部,也可以定义在函数的内部
  • 从定义宏的地方开始,后面的所有的地方都可以直接使用这个宏。就算宏在函数内部,也可以被外部的其他函数使用。
  • 默认情况下,宏从定义的地方一直到文件结束都可以使用,而undef可以使宏提前失效。
#undef 宏名
  • 字符串中如果出现了宏名(双引号中出现了宏名),系统不会认为这是一个宏,而认为是字符串的一部分,字符串中并不会出现宏替换。
  • 宏可以实现层层替换
#define PI 3.14
#define R 5
#define AREA PI * R * R
  • 如果宏定义的宏值后面跟了一个分号,系统就会认为分号是宏值的一部分,替换的时候会连分号一起
  • 可以把任意的C代码定义为宏
  • #define和typedef的区别
  • #define是一个预处理指令,在预编译的时候执行,在预编译的时候会把宏名换成宏值
  • typedef是一个c代码,在运行的时候才执行
  • #define可以将任意的C代码去一个标识名
  • typedef只能为数据类型取名字

15.2.3 带参数的宏

我们在定义宏的时候,宏名是可以带参数的。

在这个宏值当中,可以直接使用这个参数。

语法结构:#define 宏名(参数) 宏值

如果使用的宏有参数,那么就必须要在使用它的时候为这个宏的参数传值。

例如:

#define N(a) a + 10
int b = N(20);

宏代换的原理是什么?

  • 先将传入的值传递给宏的参数,那么宏的参数的值就是我们传递的值
  • 再把宏值当中使用参数的地方换成参数的值
  • 再把这个最终的宏值替换到使用这个宏名的地方

使用有参数的宏的注意事项:

  • 宏不是函数,所以宏的参数不需要加类型说明符,直接写参数名就可以了
  • 在定义宏的时候,编译器是如何区分宏名和宏值的?

#define后面第一个空格后的连续无空格内容是宏名,宏名后的第一个空格后是宏值。

  • 为带参数的宏传值的时候是本色传递。本色传递是指:如果传递一个变量,并不是传递这个变量的值,而是直接传递的是这个变量的串。

宏一定程度上可以实现函数的功能效果,但是宏值一旦换行就表示这个宏定义就结束了,不适合大量代码的情况。如果代码只有一句或两句的情况是可以的,但是代码量多的时候是不适合使用宏的。

15.3.0 条件编译指令

15.3.1 条件编译指令的用法

条件编译指令,它是一个预处理指令,所以在预编译阶段执行。

作用:默认情况下,我们所有的C代码都会被编译为二进制代码,而条件编译指令的作用是可以让编译器只编译指定部分的代码。

  • 条件编译指令的第一种用法:
#if 条件
C代码;
#endif

在预编译的时候,如果条件成立,就会将其中的C代码编译成二进制指令;如果条件不成立,就不会将其中的C代码编译成二进制指令。

注意:条件只能是宏,不能是变量。

  • 条件编译指令的第二种用法:
#if 条件1
C代码;
#elif 条件2
C代码;
#elif 条件3
C代码;
#endif

这个类似if-else if结构,但记得最后一定要用#endif结束这段预编译指令。

与if-else if结构的区别是:

  • 条件编译指令是一个预处理指令,在预处理阶段执行,而if结构是C代码,在程序运行时执行,要晚于预编译指令。
  • if语句无论如何全部都要被编译为二进制指令,条件编译指令只会将符合条件的C代码编译为二进制指令。
  • 实际上,if语句一定程度上可以换成条件编译指令,但是条件编译指令的条件不能是变量参与,只能是宏。
  • 条件编译指令的第三种用法:
#ifdef 宏名
C代码;
#endif

如果定义了指定的宏,就编译其中的C代码,否则就结束。

还有一种:

#ifndef 宏名
C代码;
#endif

如果没有定义了指定的宏,就编译其中的C代码,否则就结束。

15.3.2 使用条件编译指令防止同一个文件被重复包含

实现的效果:

无论一个文件被#include多少次,我只希望它最终只会被包含一次。

解决方案:

文件

#ifndef 宏名
#define 宏名
代码;
#endif

这种做法通常是为了防止同一个头文件被重复包含多次代码。

15.4.0 static和extern两个关键字

static和extern是C语言中的两个关键字,用来修饰变量和函数。

15.4.1 static和extern修饰局部变量

static修饰局部变量的效果

如果局部变量被static修饰,那么这个变量就叫做静态变量。

静态变量不再存储于栈区域,而是存储在常量区。

当函数执行完毕之后,这个静态变量不会被回收。下次再执行这个函数的时候,当初在函数中声明静态变量的那句代码就不会再执行,直接略过,直接使用上一次运行后存储在常量区的变量的值。函数无论执行多少次,这个静态变量只有一份。

例如:

void test()
{
 static int num = 0;
 num++;
 printf(“num = %d\n”,num);
}
int main()
{
 test();
 test();
 test();
 test();
 test();
 return 0;
}

我们可以发现,最终输出的结果是:

1
2
3
4
5

而不是从前我们学习过的都是1。这就是static修饰局部变量的效果。

extern修饰局部变量的效果

extern不能修饰局部变量。

15.4.2 static和extern修饰全局变量

我们知道,写一个函数,完整的步骤应该是先声明,后实现。

而写一个全局变量,完整的步骤也应该分两步:

  • 先写全局变量的声明。即只定义全局变量而不赋值;

例如:int num;

  • 再写全局变量的定义,定义全局变量并初始化,也叫做全局变量的实现。

例如:int num = 10;

注意:如果全局变量只有定义没有实现,那么系统会自动去实现这个全局变量,自动初始化为0.

当我们分模块开发的时候,声明全局变量需要注意以下几点:

  • 全局变量的声明要写在.h的头文件中
  • 全局变量的定义也就是实现,要写在包含该头文件的.c文件中
  • 如果将全局变量定义在模块中,这个全局变量就必须要用static或extern修饰
  • 如果定义在模块中的全局变量用extern修饰,这个模块中的全局变量就可以跨模块访问
  • 如果定义在模块中的全局变量用static修饰,这个模块中的全局变量就只能在当前模块中访问。如果跨模块使用,虽然编译时不会报错,但是会发生取不到真正的值这样的问题。

15.4.3 static和extern修饰函数

  • 如果函数被extern修饰,那么这个函数可以跨模块调用。
  • 如果函数被static修饰,那么这个函数只能在当前模块中调用,无法跨模块使用。
  • 如果函数没有写extern或者static,则默认是extern修饰。
Get it on Apple Books

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

Optimized with PageSpeed Ninja