目录
本章为C语言的最后一章了,终于要结束了吗?结束了,本文是预处理和编程环境的精选部分,也是由于特殊原因这时候才写出来,由于链表,线性表的内容比较贴近数据结构的知识,所以将会放到后面讲。总之要想了解更多的数据结构的知识,请关注这个账号。
话不多说,进入今天的内容!!!!!!!!
1。编译环境
经过日常的学习我们知道我们的代码源文件的后缀是.c的比如test.c,但是这个文件计算机是读不懂的,计算机内部需要将其转化成.exe结尾的文件,这是test.c会被转化成可执行程序test.exe。这个.exe结尾的文件是二进制文件。那这里只是体现了开始和结尾的步骤而已,请看中间的秘密。
由于VS的集成开发环境是IDE而不是gcc,所以这个编译和链接的过程是看不到的,我们也不需要知道为什么是这样。
编译 = 预处理(预编译) + 编译 + 链接,编译环境又可以分为编译+链接,那这5个部分又分别有什么作用呢?
1。预编译将.c的文件先变成.i的,编译再将.i的文件变成.s的文件,最后汇编生成.o的文件。最后生成目标文件.obj
2。所有的预处理指令都是在预编译中完成的。并且会删除注释部分,所以注释部分就不会被编译就是这个原因
3。编译中会进行语法分析,词法分析,语义分析,这些过程就是在告诉计算机要怎么执行代码。
4。汇编过程会形成符号表。如果进行词法分析就是将词法分析的结果整理成一个表格。
5。编译会进行符号汇总,再由汇编过程形成符号表,再在链接中进行符号决议和重定义。这个过程可以通过代码演示。
在add.c中写下Add函数。
在test.c中用extern引用这个函数
这个有点基础的都知道这个代码是可以实现的,就是这个ret的值一定是可以算出300的,那是怎么实现的呢?
首先add.c和test.c这两个文件同时进入预处理阶段,预处理采集到语法,语义和词义同时传入编译阶段进行符号汇总,这时由于Add和main函数都是函数所以在链接阶段形成同一类的符号表,由于test.c的Add是没有被分配空间的所以是没有地址的,记为0x000,有空间的main函数和add.c中的Add函数记为0x100,这时到链接阶段没有地址的test.c中的Add函数就会在符号决议和重定义中被舍弃。最后Add函数和main函数都有地址了,生成可执行程序test.exe,然后ret的值就被算出来了!!!
对于这个编译器cl.exe和链接器link.exe我们可以在Everything这个工具上找到:可以看到有很多
2。运行环境
其实运行环境的知识不是很重要,看得懂以上图解就行!!!
3。预处理(精选)
#define MAX 1000//预定义MAX一定是大写的为1000,不要加问号,不然会认为MAX等于1000;
int main()
{
int a = 0;
int e = MAX;
printf("%d\n", MAX);
printf("%d\n", e);
printf("%d\n", a);
return 0;
}
我们不仅可以将我们要定义的东西定义成数字,还可以定义成字符串:
#define MAX "hehe"
int main()
{
//define定义标准符
printf("%s", MAX);
return 0;
}
这样可以正常打印hehe,但是如果这样写呢?这就形成了我们的易错点:
#define MAX hehe
int main()
{
printf("%s", "MAX");
return 0;
}
可能有的人会认为MAX已经被定义为hehe那打印“MAX”不就是在打印hehe了吗?,很好数学的等效替换都被你用上了,但是你打印“MAX”,计算机就会把“MAX”认为是字符串了,就不再替换了。
还可以定义成运算式:
#define M 100+2
#define STR "hehe"
int main()
{
int a = M;
printf("%s\n", STR);
printf("%d\n", a);//此时M会自己相加
return 0;
}
还可以定义成一段代码:
#define CASE break;case
int main()
{
int n = 0;
switch(n)
{
case 1:
//
CASE 2:
//
CASE 3:
//
CASE 4:
}
return 0;
}
这样用可以防止程序员在写switch语句时忘记写break;是不是非常巧妙呀,但是其实不推荐这样写
4。预定义符号
VS支持的预定义符号只有4个,还有一个是VS不支持的__STDC__。
int main()
{
printf("%s\n", __FILE__);//预定义符号,显示文件准确位置
printf("%s\n", __TIME__);//显示编译时间
printf("%s\n", __DATE__);//显示日期
printf("%d\n", __STDC__);//gcc是支持ANSI C的,会等于1但是VS不支持所以会报错
printf("%d\n", __LINE__);//显示行号
}
注意一下这个__TIME__,它打印的是你程序被编译的时间,你后面如果没有再编译,而是直接运行的话,它还是打印你之前编译的时间,而不是当前的实际时间。
学了#define后我们还可以这样写:
\t是空格,,之后的\是换行符,这个只有在预处理中可以这么用,用这个\是为了防止定义部分太长。
5。#define定义宏
标准格式:#define MIN(x) ((x) + (x))
#define SQUARE(X) (X)*(X)
//宏的参数不运算,直接替换到宏的体内
//1. 宏的参数中如果有操作符,和宏的内容中的操作符
//因为运算符有优先级的问题,可能导致运算顺序不达预期
//所以容易产生问题
//2. 宏在书写的时候,给参数都带上括号,不要吝啬括号
以下例子说明为什么不要吝啬括号:
#define MAX(x) (x) + (x)
#define MIN(x) ((x) + (x))//
int main()
{
int a = 3;
int r = 0;
int c = 0;
r = 3 * MAX(a);
c = 3 * MIN(a);
printf("%d\n", r);
printf("%d\n", c);
return 0;
}
有的同学肯定会认为最后算出来,r和c的值会一样,那事实是如此吗?
如果没有最外层的括号,这就有了运算优先级的问题了,c等于18很容易得到的就是3*((3)+ (3)),而r是这样算的 r = 3 * 3 + 3 = 12,由于乘号的优先级大于加号。
那根据宏的运算特性题目会怎么出呢?
题目
//写一个宏,求2个整数的较大值
#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);// 3 5
int m = MAX(a++, b++);
//int m = ((a++)>(b++)?(a++):(b++));
printf("m = %d\n", m);
printf("a = %d\n", a);
printf("b = %d\n", b);
return 0;
}
如果a = 3,b = 5的话那编译的结果会是多少呢?
首先宏的定义格式非常完整,所以坑点一定不会在宏上,那就是这个++了。其实我们也称a++这种后置加加为带副作用的宏参数。
由于是后置++所以是先代入再++,这里就有先后之分了(循环语句没有),由于a比b小所以,m = :后面的b++但是由于a和b在前面已经加加过了,所以m = b++,b还要再加加,所以b就相当于++了两次,所以b = 7,a 也相当于加了两次,所以a = 4,但是由于m = b++,又是一个后置++,先代入再加加,b在第2次++还没进行时就待入m了,所以m = 6。
6。#define替换规则
7。#与##(没什么用的知识)
由于两个字符串有自动连接的特点,所以如果要大印两个字符串就需要把两个字符串都用“”引起来。
#的作用是将一个宏参数变成对应的字符串。
#define PRINT(val, format) printf("the value of "#val" is " format"\n", val)//把宏的参数变成对应的字符串加#后要再加一个“”
//
int main()
{
int a = 10;
PRINT(a, "%d");
int b = 20;
PRINT(b, "%d");
float f = 3.5f;
PRINT(f, "%f");
return 0;
}
宏可以大印不同形式的数字,但是原本应该是the value of val,但是如果这样写的话,这个val就无法被翻译成参数的val,因为系统会默认这一整行是一个字符串。所以#的作用的得以显现,"#val"就会加上“”表示是字符串。
##的作用:
我们换个更直观的例子:
#define GENERIC_MAX(type) \
type type##_max(type x, type y)\ //如果没加##计算机就会认为type_max是一个整体,
{\
return (x > y ? x : y); \
}
/*GENERIC_MAX(int)
GENERIC_MAX(float)
GENERIC_MAX(double)*/
int main()
{
int m = int_max(3, 5);
printf("%d\n", m);
return 0;
}
这个例子是利用宏来实现比较函数,type type##_max(type x, type y),如果没加##计算机就会认为 type_max是一个整体,是一个字符串,使得type不会被替换。
8。#undef
9。条件编译
这个常量表达式只能是 参数 =常数,M==a这种不是常量表达式。
对于多分支的条件编译:
#define a 2
int main()
{
#if a == 1
printf("%s", "hehe");
#elif a == 2
printf("%s", "haha");//有遇到匹配的会亮
#elif a == 6
printf("%s", "hihi");
#else
printf("%s", "jiji");
#endif
return 0;
}
和选择语句不同的一点是,如果#else 之前已经有匹配项了,这个#else就不会运行了。
3。判断是否被定义。
#define MAX 0
int main()
{
#if defined(MAX)
printf("hehe\n");
#endif
#ifdef MAX//缩写
printf("haha\n");
#endif
#ifdef MAX是#if defined(MAX)的缩写。
#if !defined(symbol) 的缩写是 #ifndef symbol
4。嵌套定义
嵌套定义的实质就是以上三种的组合。
10。头文件的包括
首先我们从写代码的第一天就可以看到#include<stdio.h>这个头文件是C语言标准库里的头文件,而我们在编写扫雷游戏是有这么一个头文件#include"test.h",那这两个头文件包括方式有什么区别呢?
如果这个头文件是在标准库里那就是用<>,如果这个头文件是自己创建的,也就是标准库里没有的,那就是用“”。
就是说,如果头文件是用<>包括的,计算机就会直接在库里面找,如果是用""包括的计算机会先在当前文件中找,等到找不到了再去库里面找。所以所有的在C语言标准库里的头文件也可以用“”引起来,只是在找的时候比较慢。#include<stdio.h> == #include"stdio.h"
为了防止重复包括头文件,这里有两种防止重复包括的方式:
#pragma once//防止头文件包含多次
#ifndef __TEST_H__
#define __TEST_H__
#endif//防止重复包括
由于计算机会去.h的文件中找有没有这个头文件,所以这两种方式都应写在.h文件中。