C语言入门学习——程序的编译


在ANSI C的任何一种实现中,存在两个不同的环境。

第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
第2种是执行环境,它用于实际执行代码。

翻译环境的工作就是把源文件翻译成可执行文件
从test.c ————翻译环境————> test.exe

一般翻译环境就是编译器完成的,执行环境就是操作系统完成的

编译+链接

例如一个程序有多个 .C 文件,那么它们会通过编译器,生成他们对应的 .obj 文件,最后通过链接库把这些 .obj 文件生成一个**.exe**可执行程序
在这里插入图片描述
我们平常所使用的编译器都是集成开发环境(IDE),编辑、编译、链接、调试一体化的
一个源文件编译的过程
在这里插入图片描述
这里只是简单画了一下一个源文件的过程,最后链接的合并段表等操作就是多个目标文件的合并,最后生成 .exe 文件
所以当函数未定义时,是在链接中查到的,因为没定义就没形成符号表

预处理指令

define

用法1:定义标识符常量

代码如下:


#define NUM 100
# define STR "abcdef"

int main()
{
	int num = NUM;
	char* ch = STR;
	printf("%d,%s\n",num,ch);
	return 0;
}

在这里插入图片描述
#define在预编译时会把程序里的 STR替换为 “abcdef”,NUM替换为100
这里可以通过预处理器观察一下区别
在这里插入图片描述
在debug文件目录下找到.i文件打开
在这里插入图片描述
这里可以看出,预编译阶段就是把常量标识符全部替换;但是在程序中,实际上是编译后就替换了而表面上是没有变化的,不利于调式,这时候可以试试枚举
一些内置预定义符号

__FILE__      //进行编译的源文件
__LINE__     //文件当前的行号
__DATE__    //文件被编译的日期
__TIME__    //文件被编译的时间
__STDC__    //如果编译器遵循ANSI C,其值为1,否则未定义

用法2:宏定义

#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(definemacro)。
例如:

#define MAX(x,y) (x>y?x:y)

int main()
{
	int a = 10;
	int b = 20;
	int c = MAX(a,b);
	printf("%d\n",c);
	return 0;
}

在这里插入图片描述
可以看到,MAX(a,b) 被替换为 (a>b?a:b)
在这里插入图片描述
宏的名字必须与参数括号紧邻,否则容易被编译器认为是一个常量标识符替换

define使用的易错点

例1.

#define MAX 1000;
#define MAX 1000`
int main()
{
	int a = MAX;
}

上述两个常量标识符在替换时,假如在程序末尾习惯性的加上;则在预编译时就会变为 int a = 1000;; 在 ;后面又加了 ;出现语法错误还不好找
所以一般使用define时不用加 ;

例2:

#define SQUARE( x ) x * x

int main()
{
	int a = 5;
	int c = SQUARE(a+1);
	printf("%d\n",c);
	return 0;
}

请看结果:在这里插入图片描述
按正常理解应该是36,为什么会出现11呢?
当系统预编译后,会把 a+1替换为 a + 1 * a + 1 那么结果就是5+1 * 5+1 = 11
最好在宏定义上加上两个括号

例3:

#define DOUBLE(x) (x) + (x)

int main()
{
	int a = 5;
	int c = 2*DOUBLE(5);
	printf("%d\n",c);
	return 0;
}

在这里插入图片描述
上述代码虽然加了()括号,但还是容易造成优先级上的问题
2*DOUBLE(5)替换成了 2 *(5)+(5) = 10+5 = 15
在使用define时不要吝啬使用括号

#define替换规则

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
  2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

注意:

  1. 宏参数和#define 定义中可以出现其他#define定义的变量(可以嵌套)。但是对于宏,不能出现递归。
  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

带副作用的宏

例如:宏传参自增运算符

#define MAX(a, b) ( (a) > (b) ? (a) : (b) ) 
... 
int x = 5; 
int y = 8; 
int z = MAX(x++, y++); 
printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?

x++与y++替换到宏中,会先把 5 和 8比较,之后在自增,x为6,y为9
当a大于b为假时,执行b,等于又执行了一遍y++,先把y的值返回给z,之后自增
最后输出结果为:x = 6,y = 10,z = 9
在这里插入图片描述
带有副作用的宏参数是替换进去才执行的,可能会对结果产生影响,所以最好不要使用带有副作用的宏的参数

#和##的用法

例1.使用 # ,把一个宏参数变成对应的字符串

#define print(N,fmoat) printf("the value of "#N" is "fmoat"\n",N);

int main()
{
	int a = 20;
	int b = 10;
	print(a,"%d");
	print(b,"%d");
	return 0;
}

#a 变成 “a” 字符
在这里插入图片描述
例2.##可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符。

#define CAT(name1,name2) name1##name2

int main()
{
	printf("%s",CAT("hello","111"));
}

在这里插入图片描述

宏和函数的对比

宏通常被应用于执行简单的运算。比如在两个数中找出较大的一个。

  1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序
    的规模和速度方面更胜一筹。
  2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可
    以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的。
    1.使用宏

#define MAX(x,y) (x>y?x:y)
int main()
{
	int a = 10;
	int b = 20;
	int c = MAX(a,b);
	printf("%d\n",c);
	return 0;
}

在这里插入图片描述

2.使用函数

int max(int a,int b)
{
	return (a>b?a:b);
}
int main()
{
	int a = 10;
	int b = 20;
	int c = max(a,b);
	printf("%d\n",c);
	return 0;
}

1.先调用函数
在这里插入图片描述
2.进入函数执行
在这里插入图片描述
3.返回值
在这里插入图片描述

上面两个例子通过反汇编可以看出,在简单功能上,宏的速度比函数快,而且无数据类型限制

但劣势也是有的:

  1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
  2. 宏是没法调试的。
  3. 宏由于类型无关,也就不够严谨。
  4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
    宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。
    例如:
#define MALLOC(num,type) (type*)malloc(num*sizeof(type));
int main()
{
	正常写法
	int* p = (int*)malloc(10*sizeof(int));
	使用宏
	int* p2 = MALLOC(10,int);
	return 0;
}

在这里插入图片描述
命名约定
一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。 那我们平时的一个习惯是:把宏名全部大写 函数名不要全部大写

#undef

undef用于移除宏

#define MAX(x,y) (x>y?x:y)
#undef 	MAX
int main()
{
	int a = 10;
	int b = 20;
	int c = MAX(a,b);
	printf("%d\n",c);
	return 0;
}

在这里插入图片描述
就是说MAX宏的作用域 只存在 #define和#undef之间,没有#undef时就是#define到程序结束

#undef NAME 
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。

条件编译

当我们在调试代码时,有一些代码删除可惜,保留又碍事,所以我们可以选择性的编译,就用到了条件编译
示例1:

int main()
{
	int arr[10] = {0};
	int i = 0;
	for(i = 0;i < 10;i++)
	{
#if 1
		arr[i] = i;
#else
		arr[i] = i+1;
#endif
		printf("%d\n",arr[i]);
	}
}

在这里插入图片描述

int main()
{
	int arr[10] = {0};
	int i = 0;
	for(i = 0;i < 10;i++)
	{
#if 0
		arr[i] = i;
#else
		arr[i] = i+1;
#endif
		printf("%d\n",arr[i]);
	}
}

在这里插入图片描述
上述代码中,当#if的表达式为真,则执行他后面的程序,为假则执行#else的程序

例2:多个分支的条件编译

#define NUM 1
int main()
{
#if NUM==1
	printf("你好");
#elif NUM == 2
	printf("不好");
#else
	printf("再见");
#endif
	return 0;
}

上述代码中,当 #if 的表达式为真时执行他下面的程序,若当 #elif 的表达式为真,则执行他下面的程序,当#if和#elif 都为假则执行#else下面的程序

例3:判断是否被定义

#define MAX 0
int main()
{
#if define(MAX)     等价于 #ifdef MAX,还有#ifndef 意思是没有定义
	printf("你好");
#endif
	return 0}

跟定义的数组没关系,只是判断定义,定义就为真,没定义就为假
在这里插入图片描述

文件包含

头文件被包含的方式:

本地文件包含
#include "filename"
查找策略:
先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。 如果找不到就提示编译错误

库文件包含
#include <filename.h>
查找策略:
先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。 如果找不到就提示编译错误

本地文件是自己选择文件目录创建的
库文件:
1.linux环境的标准头文件的路径:/usr/include
2.VS环境的标准头文件的路径:C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\include

对于库文件也可以使用 “” 的形式包含,但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。
我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样。这种替换的方式是: 预处理器先删除这条指令,并用包含文件的内容替换。 这样一个源文件被包含10次,则实际也就被编译10次。
对于库文件也可以使用 “” 的形式包含,但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

为了避免重复定义,影响编译器的速度可以在头文件中
写入这样一段代码:
#ifndef TEST_H
#define TEST_H
//头文件的内容
#endif //TEST_H

#ifndef和#define组合在一起就是,当第一次编译时,如果没有定义test.h那就定义他,当第二次编译时若已经定义test.h了就不需要在定义了,这样就避免了重定义

总结

1.#define定义标识符常量时不要吝啬使用括号
2.注意 宏 与 函数 的区别
3.条件编译可以很方便调试
4.注意不同的头文件包含方式(< > ," ")和头文件重定义的方式

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值