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。
宏的作用域
宏的作用域是从定义位置开始往下发挥作用
c
#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
程序段 2
#endif
它的作用是若所指定的标识符已经被# define 命令定义过,则 在程序编译阶段编译程序段 1; 否则编译程序段 。其中# else 部分可以没有。
2、
#ifndef 标识符
程序段 1
#else
程序段 2
#endif
上述形式只是第一行与第一种形式不同:将 "ifdef" 改为 "ifndef" 。它的作用是若标识符未被定义过则编译程序段 1; 否则编译程序段 2。这种形式与第一种形式的作用相反。
3、
#if 表达式
程序段1
#else
程序段2
#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;
指针变量初始化
如果指针变量没有初始化此时就是随机值,该指针叫野指针。野指针对程序的执行是有风险的所以在初始化的时候必须让指针有明确的指向例如:
int a = 1;
int *p = &a;
int *p = NULL;此时p表示的是一个空指针,p的地址编号是0;
指针的赋值:
int *p;
p = NULL;
定义多个指针变量:
int*p,*q;
*是用来修饰变量的表示此时定义的是一个指针类型的变量;而不能写成int *p,q;如果这样写p代表的是一个指针变量而q是一个int类型的变量。
指针的作为函数的参数一个重要功能就是实现被调修改主调那么如何实现被调修改主调呢?
其实指针作为函数的参数通过把背调的地址传过来然后就能通过这个地址找到其在内存中所存在的位置,从而访问内存空间中存放的数据来实现被调修改主调的效果。
指针作为函数参数:
形参是一个指针类型的变量,用来接受实参而实参是要操作内存空间的地址;
实参是要修改谁就传谁的地址,且被调函数中一定要有*p运算;
下面以一个例子来说明值传递和址传递要注意的问题:
c
#include <stdio.h>
void minMax(int a, int b, int *max, int *min)
{
*max = a > b ? a : b;
*min = a < b ? a : b;
}
int main(void)
{
int a = 0, b = 0;
int max = 0, min = 0;
scanf("%d %d", &a, &b);
minMax(a, b, &max, &min);
printf("max = %d min = %d\n", max, min);
return 0;
}
通过上述代码我们可以看到在进行函数的传参时既有值传递也有址传递那什么时候用值传递什么时候用址传递呢?如果你想要通过形参去改变实参那么就要用址传递的形式。就比如说上述代码中我要从函数中带出一个最大值和一个最小值但是不能有返回值,那么就要实现形参改变实参的效果通过值传递是实现不了这个效果的所以max和min采用了址传递的方式,我们想改变的是max和min而a和b这两个数是不需要改变的所以采用值传递就可以了。
指针+一维整型数组
如果要定义一个一维数组指针那我们要定义一个什么类型的指针呢?谁又能代表数组首元素的地址呢?首先我们得理解数组名的含义,1、数组名代表数组的类型;2、数组名代表数组首元素的地址;由数组名的含义我们可以知道数组名的首元素可以代表数组首元素的地址,数组首元素也就是a[0]而a[0]对应的数据类型是int型代表a取了一块int类型数据的地址也就是int类型,所以a的类型是int 型,这样我们在定义一个一维数组指针是要定义一个int*类型的指针例如:int a[50];int p = a;指的是创建一个int类型的变量也就是创建了一个指针p,p指向的是a数组所在的内存空间的首地址,p所指向内存空间里存放的数据的数据类型是int型。
指针的访问方式;
下面以一个具体的例子来说明指针的访问方式:
c
#include <stdio.h>
void printArray(int *a, int len)
{
int i = 0;
for(i = 0; i < len; ++i)
{
printf("%d\n", *(a + i));
}
}
int main(void)
{
int a[] = { 1, 2, 3, 4, 5};
printArray(a, 5);
return 0;
}
上述程序实现了一个数组元素打印的过程,通过把数组首元素的地址传给函数在进行数组遍历的时候就能通过数组首元素的地址找到该数组所在的内存空间,在遍历数组元素时通过指针运算来控制指针的偏移使指针指向数组每一个元素所在空间的地址然后通过指针运算符来实现对数组元素的访问,从而实现数组元素的打印功能。其中 (a + i)等价于a[i](a[i]还可以写成i[a]因为*(a + i)等价于*(i + a))。
总的来说数组作为函数参数 :
第一,形参要是数组形式,其本质上是一个指针类型变量例如int *a;除了传进一个指针还要传进去数组长度方便对数组的遍历;
第二,实参是数组名和数组长度,数组名代表的是数组首地址。