预处理
翻译环境和执行环境
翻译环境:源代码被转换为可执行的机器指令
执行环境:实际执行代码
(C代码文本文件) (二进制文件)
源文件----编译+链接----二进制的信息----运行
从test.c------------------>test.exe------------------->结果
---------- 翻译环境 --------调试----------运行环境
编译器 test.obj 链接器
预编译、编译、汇编
在Linux环境下
gcc-E test.c 预编译/预处理 1、头文件的包含 2、注释删除(使用空格来替代注释)3、将#define定义的常量替换 生成test.i文件
编译 gcc-S test.i 把C代码翻译成汇编代码 1、语法分析2、词法分析3、语义分析4、符号汇总(函数名、全局变量) 生成test.s文件
汇编 gcc-c test.s 把汇编代码转换成二进制指令 形成符号表 生成test.o文件
链接 add.o test.o捆绑 1、合并段表2、符号表的合并和符号表的重定位
程序执行的过程:
1、程序必须载入内存中,在有操作系统的环境下,一般这个由操作系统完成。在独立的环境下,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成
2、程序的执行便开始。接着便调用main函数
3、开始执行程序代码,程序使用一个运行时堆栈,存储函数的局部变量和返回地址,程序同时也可以使用静态内存,存储于静态内存中的变量在程序的整个执行过程一直保存他们的值。
4、终止程序。正常结束main函数,也可能意外结束。
组成一个程序的每个源文件通过编译过程分别转换成目标代码
每个目标文件由链接器捆绑在一起,形成一个单一而完整的可执行程序
链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其所需要的函数也链接到程序中。
预处理详解
预定义符号
int main(void)
{
// __DATE__, __TIME__, __FILE__, __LINE__ 为预定义宏
printf("Date : %s\n", __DATE__);
printf("Time : %s\n", __TIME__);
printf("File : %s\n", __FILE__);
printf("Line : %d\n", __LINE__);
//写日志文件
int i = 0;
int arr[10] = { 0 };
FILE* pf = fopen("log.txt", "w");
for (i = 0; i < 10; i++)
{
arr[i] = i;
fprintf(pf, "file:%s line:%d data:%s time:%s i=%d\n", __FILE__, __LINE__, __DATE__, __TIME__, i);
printf("%s\n", __FUNCTION__);
}
fclose(pf);
pf = NULL;
}
预处理指令,其后不要;
#define
#include
#pragma pack(4)
#pragma
#if
#endif
#ifdef
#line
#define MAX 100
#define SQUARE(X) X*X
int main()
{
int ret = SQUARE(MAX);
printf("%d\n", ret);
int ret1 = SQUARE(5+1);//宏接受参时,不是传参,而是替换5+1*5+1=11
printf("%d\n", ret1);
}
提示:所以对于数值表达式进行求值的宏定义都应该加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。
#define的替换规则
1、在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号,如果是,它们首先被替换
2、替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被它们的值替换
3、最后,对结果文件进行扫描,看看它是否包含任何由#define定义的符号,如果是,就重复。
注意:宏参数和#define定义中可以出现其他#define定义的变量,但是对于宏,不能递归
当预处理器搜索#define定义的符号时,字符串常量的内容并不被搜索 例如:printf(“MAX=%d\n”, MAX);
如何把参数插入到字符串中?
#把一个宏参数变成对应的字符串
##可以把位于它两边的符号合成一个符号。它允许宏定义从分离的文本片段创建标识符
#define PRINT(X) printf("the value of "#X" is %d\n",X);
int main()
{
//printf("hello world\n");
//printf("hello ""world\n");
//printf("he""llo ""world\n");//字符串是有自动连接的特点的
int a = 10;
int b = 20;
PRINT(a);//#X变成"a"
PRINT(b);//#X变成"b"
}
#define CAT(X,Y) X##Y
int main()
{
int Class84 = 2020;
printf("%d\n", CAT(Class, 84));//class84 2020
}
带副作用的宏参数,参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果
int main()
{
int a = 10;
int b = a + 1;//不带副作用
int b = ++a;//带副作用
}
#define MAX(X,Y) ((X)>(Y)?(X):(Y))
int main()
{
int a = 10;
int b = 11;
int max = MAX(a++, b++);//带有副作用的宏参数
//int max = ((a++) > (b++) ? (a++) : (b++));//先使用再++ 10>11 a=11,b=12 返回max=b=12 然后b++ b=13
printf("%d\n", max);//12
printf("%d\n", a);//11
printf("%d\n", b);//13
}
函数和宏的对比
1、函数调用和返回所需时间比实际执行这个计算更多,所以宏的规模和速度更高。
2、函数的参数必须声明特定类型,函数只能在合适的表达式上使用,而宏时类型无关的。
宏的不足:
1、每次使用宏的时候,一份宏定义的代码将插入到程序中,若宏比较长,可能会大幅度增加程序的长度
函数代码只出现于一个地方,每次使用这个函数时,都调用那个地方的同一份代码
2、宏在预处理阶段已经被替换,无法调试,函数可以逐语句调试
3、宏类型无关,不够严谨
4、宏会出现优先级问题,容易出错。建议加上括号,函数参数只在函数调用的时候求值一次,它的结果值传递给函数,表达式的求值结果更容易预测
5、宏不可以递归,函数可以递归
宏的特点:宏的参数可以出现类型
书写:宏名全部大写,函数名不全部大写
//宏
#define MAX(X,Y) ((X)>(Y)?(X):(Y))
//函数
int Max(int x, int y)
{
return (x > y ? x : y);
}
int main()
{
int a = 10;
int b = 20;
float c = 3.0f;
float d = 4.0f;
int max = Max(c, d);//函数在调用的时候,会有函数调用和返回的开销
printf("max=%d\n", max);
max = MAX(c, d);//预处理阶段就完成了替换,没有函数调用和返回的开销
printf("max=%d\n", max);
}
#define SIZEOF(type) sizeof(type)
int main()
{
int ret = SIZEOF(int);
printf("%d\n", ret);
}
#define MALLOC(num,type) (type*)malloc(num*sizeof(type))
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
int* p = MALLOC(10, int);
}
#undef移除一个宏定义
#define MAX 100
int main()
{
printf("%d", MAX);
#undef MAX//移除一个宏定义
printf("%d", MAX);//MAX未声明
}
在命令行中定义符号,在预编译阶段将值替换到符号所在地方
条件编译 有选择的编译
#define DEBUG
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
int i = 0;
for (i = 0; i < 10; i++)
{
arr[i] = 0;
#ifdef DEBUG//如果定义debug就执行下列语句
printf("%d\n", arr[i]);
#endif
#if 1-1//1为真就执行下列语句
printf("%d\n", arr[i]);
#endif
}
}
int main()
{
#if 1==2
printf("haha");
#elif 2==1
printf("hehe");
#else
printf("heihei");
#endif
}
#define DEBUG
int main()
{
#if defined(DEBUG)
printf("hehe\n");
#endif
}
#define DEBUG
int main()
{
#if !defined(DEBUG)
printf("hehe\n");
#endif
}
int main()
{
#ifndef DEBUG
printf("hehe\n");
#endif
}
头文件包含 就是把它的内存拷贝一份过来,多次使用会造成冗余代码 为避免头文件重读引入
//头文件前 根据头文件名字修改
#ifndef __TEST _ H __
#define __TEST _ H __
//头文件后
#endif
或者#pragma once
本地文件包含 “” 先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。
库文件包含<>直接去标准路径下查找
文件
文件分为程序文件和数据文件
程序文件:源程序文件(.c),目标文件(.obj),可执行程序(.exe)
数据文件:文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出数据的文件。
数据文件被称为文本文件或者二进制文件
二进制文件:数据在内存中以二进制形式存储,不加转换的输出到外存
文本文件:以ASCII字符的形式存储的文件
一个文件要有一个唯一的文件标识,文件名包含3部分:文件路径+文件名主干+文件后缀
例如:c:\code\test.txt
数据在内存中存储,字符以ASCII存储
数值型数据既可以用ASCII存储,也可以用二进制存储
例如:10000,如果以ASCII的形式输出到磁盘中,则磁盘中占用5个字节(一个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节
int main()
{
int a = 10000;//00 00 27 10
FILE* pf = fopen("test.txt", "wb");
fwrite(&a, 4, 1, pf);//二进制形式写到文件中 取a地址,写入一个4字节到pf文件中
fclose(pf);//关闭文件
pf = NULL;
}
文件缓冲区
系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”
从内存向磁盘输出数据会先送到内存的缓冲区,装满缓冲区后才一起送到磁盘上。
如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后从缓冲区逐个地将数据送到程序数据区。缓冲区的大小根据编译系统决定。
文件指针
每个被使用的文件都在内存中开辟了一个相应的文件信息区,存放文件的相关信息,这些信息保存在一个结构变量中,该结构体类型有系统声明,取名FILE
每当打开一个文件的时候,系统会根据文件的情况创建一个FILE结构的变量,并填充其中信息,一般通过FILE的指针来维护这个FILE结构的变量
FILE* pf//文件指针变量
定义pf是一个指向FILE类型数据的指针变量,使pf指向某个文件的文件信息区,通过信息访问该文件,也就是通过文件指针变量能找到与它关联的文件
fopen打开文件,fclose关闭文件
FILE* fopen(const char* filename,const char* mode)
int fclose(FILE* stream)
打开方式如下:
struct S
{
int n;
float score;
char arr[10];
};
int main()
{
//打开文件test.txt
//相对路径
// ..表示上一级路径
// . 表示当前路径
//fopen("../test.txt", "r");
//struct S s = { 100,3.14f,"bit" };//没有f默认是double型
struct S s = { 0 };
//FILE* pf=fopen("test.txt", "w");
//char buf[1024] = { 0 };
FILE* pf = fopen("test.txt", "r");
//绝对路径
//fopen("C:\\Users\\15822\\source\\repos\\test_3_8\\test_3_8\\test.txt", 'r');
if (pf == NULL)
{
printf("%s\n", strerror(errno));
}
//打开成功
// 读文件 字符输入
//printf("%c\n", fgetc(pf));//b
//printf("%c\n", fgetc(pf));//i
//printf("%c\n", fgetc(pf));//t
//写文件 字符输出
//fputc('b', pf);
//fputc('i', pf);
//fputc('t', pf);
// 读文件
//fgets(buf, 1024, pf);
//printf("%s", buf);//buf将文件中的\n也读取了 bit
//puts(buf);//puts天生就有换行
//fgets(buf, 1024, pf);
//printf("%s", buf);// hello
//puts(buf);
// 写文件
//fputs("hello", pf);
//fputs("world", pf);
//格式化的形式写文件
//fprintf(pf, "%d %f %s", s.n, s.score, s.arr);
// 格式化输入文件
fscanf(pf, "%d %f %s", &(s.n), &(s.score), &(s.arr));
printf("%d %f %s", s.n, s.score,s.arr);
//关闭文件
fclose(pf);
pf = NULL;
}
文件的顺序读写
scanf/printf 是针对标准输入流/标准输出流的 格式化输入/输出语句
fscanf/fprintf 是针对所有输入流/所有输出流的 格式化输入/输出语句
sscanf/sprintf sscanf是从字符串中读取格式化的数据 sprintf是把格式化数据输出成(存储到)字符串
int main()
{
struct S s = { 100,3.14f,"abcdef" };
struct S tmp = { 0 };
char buf[1024] = { 0 };
//把格式化的数据转换成字符串存储到buf
sprintf(buf, "%d %f %s", s.n, s.score, s.arr);
//printf("%s\n", buf);
//从buf中读取格式化的数据到tmp中
sscanf(buf, "%d %f %s", &(tmp.n), &(tmp.score), tmp.arr);
printf("%d %f %s\n", tmp.n, tmp.score, tmp.arr);
}
struct S
{
char name[20];
int age;
double score;
};
int main()
{
//struct S s = { "张三",20,55.6 };
struct S tmp = { 0 };
//FILE* pf = fopen("test.txt", "wb");
FILE* pf = fopen("test.txt", "rb");
if (pf == NULL)
{
return 0;
}
//二进制形式写文件
//fwrite(&s, sizeof(struct S), 1, pf);
//二进制形式读文件
fread(&tmp, sizeof(struct S), 1, pf);
printf("%s %d %lf\n", tmp.name, tmp.age, tmp.score);
fclose(pf);
pf = NULL;
}
文件的随机读写
fseek int fseek(FILE* stream,long offset,int origin) 根据文件指针的位置和偏移量来定位文件指针
pf文件
文件指针以当前位置为起点根据偏移量调整
pf ,偏移量 , 当前位置(SEEK_CUR文件指针的当前位置、SEEK_END文件的末尾位置、SEEK_SET文件的开始位置)
ftell long int ftell(FILE* stream)返回文件指针相对于起始位置的偏移量
//随机读取函数
int main()
{
FILE* pf = fopen("test.txt", "r");//abcdef
if (pf == NULL)
{
return 0;
}
//1、定位文件指针
//fseek(pf,-2, SEEK_END);
fseek(pf, 4, SEEK_CUR);
//2、读取文件
int ch = fgetc(pf);
printf("%c", ch);//e
fclose(pf);
pf = NULL;
}
int main()
{
FILE* pf = fopen("test.txt", "r");//abcdef
if (pf == NULL)
{
return 0;
}
//1、定位文件指针
fseek(pf,-2, SEEK_END);
int pos = ftell(pf);
printf("%d\n", pos);//4
fclose(pf);
pf = NULL;
}
int main()
{
FILE* pf = fopen("test.txt", "r");//abcdef
if (pf == NULL)
{
return 0;
}
int pos = ftell(pf);
printf("%d\n", pos);//0
fclose(pf);
pf = NULL;
}
int main()
{
FILE* pf = fopen("test.txt", "r");//abcdef
if (pf == NULL)
{
return 0;
}
fgetc(pf);//读取一个
int pos = ftell(pf);
printf("%d\n", pos);//1
fclose(pf);
pf = NULL;
}
rewind 让文件指针回到原始位置
int main()
{
FILE* pf = fopen("test.txt", "r");//abcdef
if (pf == NULL)
{
return 0;
}
int ch=fgetc(pf);//读取一个
printf("%c\n", ch);//a
ch = fgetc(pf);
printf("%c\n", ch);//b
rewind(pf);//回到原始位置
ch = fgetc(pf);
printf("%c\n", ch);//a
fclose(pf);
pf = NULL;
}
feof 当文件读取结束的时候,判断读取失败结束,还是遇到文件尾结束
文本文件读取是否结束,判断返回值是否为EOF(fgetc),或者NULL(fgets)
二进制文件读取结束,判断返回值是否小于实际要读的个数
int main()
{
//strerror -把错误码对应的错误信息的字符串地址返回
//printf("%s\n", strerror(errno));
//perror
FILE* pf = fopen("test2.txt", "r");
if (pf == NULL)
{
perror("open file test2.txt");
}
fclose(pf);
pf = NULL;
}
int main()
{
FILE* pf = fopen("test.txt", "r");//abcdef
if (pf == NULL)
{
perror("open file test.txt");
}
//读文件
int ch = 0;
while((ch = fgetc(pf)) != EOF)
{
putchar(ch);
}
if (ferror(pf))
{
printf("error\n");
}
else if (feof(pf))
{
printf("end of file\n");
}
fclose(pf);
pf = NULL;
}