C语言——预处理和指针
- 预处理
- 宏
- 宏定义
- 宏的作用域
- 带参的宏
- 文件包含
- 条件编译
- 指针
- 指针的概念
- 指针的定义
预处理
编程的流程分为:编辑、编译、运行、调试四个阶段;
预处理属于编译阶段,编译过程又可以分为:预处理、编译、汇编、链接;
预处理:
预处理是将代码中相关的预处理命令执行最终生成只包含c语言代码的文件,详细来说预处理过程实质上是处理“#”,将#include包含的头文件直接拷贝到.c当中;将#define定义的宏进行替换;将#if #else #endif定义的无用代码过滤掉,同时将代码中没用的注释部分删除等。
预处理所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。
编译,编译是对语法进行检查将源代码生成汇编代码。
汇编,汇编是将汇编代码生成机器代码。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。
目标文件由段组成。通常一个目标文件中至少有两个段:
1、代码段:该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写。
2、数据段:主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。
链接,链接是将使用的其他代码链接到一起生成可执行文件。详细来说链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。
下面详细说明预处理的过程,预处理指令有宏定义、文件包含、条件编译;
宏
宏定义
宏定义的语法形式:
#define 标识符 字符串
或者是#define 宏名 宏值
预处理命令都是以#开头的
例如:#define n 10
注意在定义宏时在宏值后面是不可以加分号的如果加了分号在预处理进行文本替换的时候会一并把分号一起替换了,比如说我在定义#define N 10的后面加上分号可以看到在进行预处理时N会被替换成10;
宏名的命名遵循标识符的命名规则,在定义宏名时为了区分宏名和普通变量名通常把宏名写成大写,比如#define N 10,这个宏定义的含义是将来代码中出现的的N都代表10,在编写代码是可以用N来表示10。这个本质就是在预处理的时候会进行文本替换也就是把宏名替换成宏值。通过预处理指令可以看到上述效果:
通过预处理指令gcc -E testH.c -o testH.i把testH.c文件只做预处理操作得到的目标文件testH.i打开testH.I可以看出宏名N被替换成了10。
宏的作用域
宏的作用域是从定义位置开始往下发挥作用
#include <stdio.h>
int main(void)
{
printf("N = %d\n", N);
return 0;
}
#define N 10
void test(void)
{
printf("N = %d\n", N);
}
预处理后的代码为:
编译上述代码看到说main函数中的N未定义的错误,然后通过预处理指令对testH.c只做预处理操作可以看到main函数中的N并没有报错,进一步说明宏的作用域是从定义位置开始往下发挥作用。如果我们想限制宏的作用域应该怎么做呢?我可以通过**#define 宏名 宏值 #undef 宏名**来限制宏的作用域,通过下面的例子来详细说明:
上述代码中我把#define N 10宏定义限制在main函数的范围内,然后对testH.c文件只做预处理操作可以看出在进行预处理时只有main函数的N替换成了宏值10而test()函数中的N并没有替换成宏值10。所以**#define 宏名 宏值 #undef 宏名**具有限制宏的作用域的作用。
带参的宏
语法:
#define 宏名(参数) 宏值
比如说#define ADD(a, b) a+b这个宏在预处理时会把代码中的ADD(a, b)都替换成a+b从而实现两个数相加的效果,在形式上看着带参的宏有点像函数实际上带参的宏和函数是有本质上的区别的:
1、带参宏和函数的处理阶段不一样,宏是在预处理阶段而函数是在编译阶段;
2、二者的使用阶段也不一样,宏是在预处理阶段就使用结束了而函数是在调用的时候才会进行使用,宏的本质是进行文本的原样替换而函数的使用本质上是函数代码的调用,,宏的参数只是进行文本替换用的不会进行语法检查,而函数的参数是有类型的在编译阶段会进行类型的检查。
宏的副作用
使用宏是可能会是运算优先级发生改变下面以一个具体的例子说明吧;
在调用宏时理想的结果是先让1+2和3+4求和然后再将二者的和相乘可以在进行文本的原样替换时把MUL(1 + 2, 3 + 4)替换成了1 + 2*3+4所以计算结果是11;为了避免发生这样的结果通常在进行宏定义时该加括号的加括号。
文件包含
文件包含分为
1、#include <>
2、#include “”
两种,二者的区别是:查找头文件的方式不一样,<>是到系统默认的路径去找头文件而""实现到当前目录下寻找如果没有再到系统默认的目录下寻找。
条件编译
条件编译总共有三种形式:
1、
# ifdef 标识符
程序段 1
#else
程序段 1
#endif
它的作用是若所指定的标识符已经被# define 命令定义过,则 在程序编译阶段编译程序段 1; 否则编译程序段 。其中# else 部分可以没有。
2、
#fndef 标识符
程序段 1
#else
程序段 1
#endif
上述形式只是第一行与第一种形式不同:将 “if def” 改为 “ifndef” 。它的作用是若标识符未被定义过则编译程序段 1; 否则编译程序段 2。这种形式与第一种形式的作用相反。
3、
#if 表达式
程序段
#else
程序段
#endif
它的作用是当指定的表达式值为真(非零)时就编译程序段 1; 否则编译程序段 。可以事先给定条件,使程序在不同的条件下执行不同的功能。
指针
指针的概念
指针就是地址而地址就是内存单元的编号。指针也是一种数据类型是专门用来处理地址这种数据的类型。
指针的定义
数据类型 变量名
语法:
基类型 * 变量名
其中基类型包含整型、浮点型、字符型、数组类型、指针类型、结构体类型 、函数类型等等;
该类型表示指针类型指向的内存空间所存放的数据是什么样的类型;
**“*”**表示表示此时定义的是一个 指针类型 的变量;
变量名符合标识符的命名规则;
举个例子说明吧:
int a = 10; //表示a的内存空间中存放的是整型类型的数据;
float b = 10;//表示b的内存空间中存放的是浮点型类型的数据;
int p = &a;
int p = &b;
其中&a表示a所在内存空间的首地址,表示获得了一块 可以存在int型数据的内存空间的地址。
int p;
int 含义 首先表示是一个 指针类型,表示指向int型数据的指针类型 。
指针变量的引用
int a = 10;
int *p = &a; 这里p指向a,因为p中保存了a的地址;
“*”是指针运算符,它是一种单目运算符,且运算的对象只能是地址;
*p:表示访问p所指向的基类型的内存空间这种访问是间接访问可以通过a直接访问;
*p的访问完整流程是:
1、首先拿出p中地址,到内存中定位
2、偏移出sizeof(基类型)大小的一块空间
3、将偏移出的这块空间,当做一个基类型变量来看
p最后的运算效果相当于就是一个基类型的变量,也就是p等价于a;
今日先到这里明日再更新!