在ANSI C(C语言的标准)的任何一种实现中,存在两个不同的环境。
第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令(机器指令就是2进制代码,机器只能处理2进制数据)。编译器,链接器
第2种是执行环境,它用于实际执行代码。
1、程序的翻译环境
工程中有多个.c源文件,怎么经过翻译环境?
任何一个.c源文件,都会单独作为一个单元被编译器进行处理,生成自己对应的目标文件(.obj文件)
翻译环境(编译器,链接器)
1、组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。
2、每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
3、链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。
翻译环境图解
编译本身也分为几个阶段:预编译、编译、汇编
2、运行环境
程序执行的过程
1、程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
2、程序的执行便开始。接着便调用main函数。
3、开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
4、终止程序。正常终止main函数;也有可能是意外终止。
3、预处理详解(预编译)
1、预定义符号
预定义符号,这些预定义符号都是语言内置的。
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
__FUNCTION__ //所在函数的名字
例如
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#define MAX 100 //这个是自己定义的符号,不是预定义符号
int main()
{
//printf("%s\n", __FILE__);//__FILE__所在文件的名称和绝对路径
//printf("%d\n", __LINE__);//当前的行号
//printf("%s\n", __DATE__);//文件被编译的日期
//printf("%s\n", __TIME__);//文件被编译的时间
//printf("%d\n", __STDC__);//未定义
//写日志文件
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 date:%s time:%s i=%d\n",
__FILE__, __LINE__, __DATE__, __TIME__, i);
//printf("%s\n", __FUNCTION__);//打印所在函数的名字
}
fclose(pf);
pf= NULL;
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
2、预处理指令
预处理指令,以#开头的,都是预处理指令。
#define
#include
#pragma pack(4)
#pragma
#if
#endif
#ifdef
#line
4、#define 定义标识符
在define定义标识符的时候,要不要在最后加上分号; ?
建议不要加上分号;,这样容易导致问题。
例如
#include <stdio.h>
#define MAX 100
//#define MAX 100;
#define STR "hello"
#define reg register //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;) //用更形象的符号来替换一种实现
// 如果定义的stuff(内容)过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\nline:%d\ndate:%s\ntime:%s\n",\
__FILE__,__LINE__ ,\
__DATE__,__TIME__ )
int main()
{
//#define定义标识符
//int max= MAX;
//printf("%d\n",max);
//printf("%s\n",STR);
//reg inta;
//registerint a;//和上面等价
//do_forever;//程序陷入死循环
//DEBUG_PRINT;
//在define定义标识符的时候,要不要在最后加上; ?
//建议不要加上;,这样容易导致问题。
//int a =MAX;//要是定义标识符的时候加;,等价于int a= 100;;。可能会出现问题
//printf("%d\n",a);
//printf("%d\n",MAX);//等价于printf("%d\n", 100;);。会报错
//if语句不带{},默认后面跟一条语句,两个分号会有语法错误。
//if(condition)
// max = MAX;
//else
// max = 0;
return 0;
}
5、#define 定义宏
1、#define定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)
定义宏的时候都使用大写
2、宏的申明方式
#define name(parament-list) stuff
其中的parament-list是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意:
参数列表的左括号必须与name紧邻。
如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
3、在写宏的时候,宏的参数如果是表达式,表达式里操作符的优先级和替换进去某些操作符的优先级,如果不相同,可能会导致表达式的计算顺序发生意外,不是我们期望的。
为了能够明确指明计算顺序,希望给宏替换进去的内容分别加上括号(),让其变成独立的部分,这样不容易出错。
4、注意
1)在写宏的时候,不要吝啬括号()
2)宏是完成替换的,不是传参的
提示,所有用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。
5、#define替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
1、在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
2、替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。
3、最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意
1)宏参数和#define定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。
2)当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
例如
#include <stdio.h>
#define MAX 100
#define SQUARE(X) X*X
#define SQUARE1(X) (X)*(X)
#define DOUBLE(X) (X)+(X)
#define DOUBLE1(X) ((X)+(X))
int main()
{
#define定义宏
//int ret= SQUARE(6);
//printf("ret= %d\n", ret);
//这个宏接收一个参数x,置于程序中,预处理器就会用表达式(x*x)替换SQUARE(x)
//在写宏的时候,不要吝啬括号()
//int a = SQUARE(5 + 1);
//printf("%d\n", a);//结果是11,不是36
//5 + 1 * 5 + 1 = 11
//注意宏是完成替换的,不是传参的。
//int b = SQUARE1(5 + 1);
//printf("%d\n", b);//结果是36,不是11
//(5 + 1) * (5 + 1) = 36
//在宏定义上加上两个括号,参数替换当成一个整体
//再次强调,写宏的时候不要吝啬括号
//int n = 10 * DOUBLE(5);
10 * (5) + (5) = 55
//int n1 = 10 * DOUBLE1(5);
10 * ((5) + (5)) = 100
//printf("%d\n", n);//结果是55
//printf("%d\n", n1);//结果是100
//提示
//所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,
//避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。
//int m = 10 * DOUBLE1(MAX);
//printf("MAX = %d\n", m);//""中的MAX不被替换
//当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
return 0;
}
6、#和##
1、如何把参数插入到字符串中?
使用#,把一个宏参数变成对应的字符串。
代码中的 #X 会预处理器处理为:"X"
2、## 的作用
##可以把位于它两边的符号合成一个符号。
它允许宏定义从分离的文本片段创建标识符。##可以把位于它两边的符号合成一个符号。注意这样的连接必须产生一个合法的标识符,否则其结果就是未定义的。
例如
#include <stdio.h>
#define CAT(X,Y) X##Y
#define PRINT(X) printf("the value of "#X" is %d\n",X)
void Print(int a)
{
printf("the value of a is %d\n", a);
}
int main()
{
// #和##
//int a = 10;
//int b = 20;
printf("the value of a is %d\n", a);
//Print(a);//应该输出the value of a is
//Print(b);//应该输出the value of b is,但是函数做不到,函数里面写死了。考虑使用宏
//字符串是有自动连接的特点的。
//printf("hello world\n");//输出hello world
//printf("hello " "world\n");//输出hello world
//printf("he" "llo " "world\n");//输出hello world
//如何把参数插入到字符串中?
//使用#,把一个宏参数变成对应的字符串。
//代码中的 #X 会预处理器处理为:"X"
//int a = 10;
//int b = 20;
//PRINT(a);
printf("the value of ""a"" is %d\n", a)
//PRINT(b);
printf("the value of ""b"" is %d\n", b)
//## 的作用
//##可以把位于它两边的符号合成一个符号。
//它允许宏定义从分离的文本片段创建标识符。
int Hao666 = 2022;
//printf("%d\n", Hao666);
//##可以把位于它两边的符号合成一个符号。注意这样的连接必须产生一个合法的标识符,否则其结果就是未定义的。
printf("%d\n", CAT(Hao, 666));//##可以把位于它两边的符号合成一个符号
//等价于
//printf("%d\n", Hao##666);
//printf("%d\n", Hao666);
return 0;
}
7、带副作用的宏参数
带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
例如
#include <stdio.h>
#define MAX(X,Y) ((X)>(Y)?(X):(Y))
int main()
{
//int a = 10;
//int b = a + 1; //不带副作用,a的值没有发生改变
//int c = ++a; //带副作用,a的值发生了改变
//注意带有副作用的宏参数在使用的时候一定要小心
//牢记宏的参数是替换进去的,不是计算好后替换进去的
int a = 10;
int b = 11;
//int max = MAX(a, b);
int max = MAX(a++, b++);//传进来的参数带有副作用
//int max = ((a++) > (b++) ? (a++) : (b++));//宏替换,等价于上面语句,b++使用完赋值给max,然后再++,b=13
printf("%d\n", max); //12
printf("%d\n", a); //11
printf("%d\n", b); //13
return 0;
}
8、宏和函数对比
1、宏通常被应用于执行简单的运算。比如在两个数中找出较大的一个。
那为什么不用函数来完成这个任务?原因有二
1)用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
2)更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏可以适用于整形、长整型、浮点型等可以用于 >来比较的类型。宏是类型无关的。
2、当然和宏相比函数也有劣势的地方
1)每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2)宏是没法调试的。调试的是可执行程序(.exe文件),但在预处理阶段,已经完成了宏替换
3)宏由于类型无关,也就不够严谨。
4)宏可能会带来运算符优先级的问题,导致程容易出现错。
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型(int、double...),但是函数做不到。
宏的参数只是替换,不同类型的数据可以用同一个宏。宏更加灵活一点
函数在调用的时候,会有函数调用和返回的开销
预处理阶段就完成了宏替换,没有函数调用和返回的开销
3、命名约定
一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。
那我们平时的一个习惯是
1)把宏名全部大写
2)函数名不要全部大写
例如
#include <stdio.h>
#define SIZEOF(type) sizeof(type) //宏的参数可以出现类型,但是函数做不到
#define MALLOC(num, type) (type*)malloc(num* sizeof(type))
//宏
#define MAX(X,Y) ((X)>(Y)?(X):(Y))
//函数
int Max(int x, int y)
{
return (x > y ? x : y);
}
//函数2
float Max2(float x, float y)
{
return (x > y ? x : y);
}
int main()
{
//宏和函数对比
//inta = 10;
//int b =20;
//int max= Max(a, b);//函数
//printf("max= %d\n", max);
//max =MAX(a, b);//宏
//printf("max= %d\n", max);
//用函数的形式比较c和d的大小,要重新写一份函数,因为要改成float类型
//函数在调用的时候,会有函数调用和返回的开销
//宏的参数只是替换,不同类型的数据可以用同一个宏。宏更加灵活一点
//预处理阶段就完成了宏替换,没有函数调用和返回的开销
//floatc = 3.0f;
//float d= 4.0f;
//floatmax = Max2(c, d);//函数
//printf("max= %lf\n", max);
//max =MAX(c, d);//宏
max =((c)>(d)?(c):(d));//宏替换,同上
//printf("max= %lf\n", max);
//int ret= SIZEOF(int);
intret = sizeof(int);//宏替换
//printf("%d\n",ret);
//宏替换让使用更方便
int* p = MALLOC(10, int);
//int* p = (int*)malloc(10 * sizeof(int));//宏替换
free(p);
p= NULL;
return 0;
}
9、#undef这条指令用于移除一个宏定义
#undef这条指令用于移除一个宏定义。
如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
例如
#include <stdio.h>
#define MAX 100
int main()
{
#undef这条指令用于移除一个宏定义。
如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
//printf("%d\n", MAX);
#undef MAX
printf("%d\n", MAX);
//许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
//例如,当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。
//(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,
//但是另外一个机器内存大些,我们需要一个数组能够大些。)
//在Linux系统下演示
//编译指令:gcc - D ARRAY_SIZE = 10 programe.c
//程序中并没有指定ARRAY_SIZE大小,在命令行中定义
//intarray[ARRAY_SIZE];
//int i =0;
//for (i= 0; i < ARRAY_SIZE; i++)
//{
// array[i] = i;
//}
//for (i= 0; i < ARRAY_SIZE; i++)
//{
// printf("%d ", array[i]);
//}
//printf("\n");
return 0;
}
10、条件编译
条件编译,在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。比如说调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。某个条件满足才编译,条件不满足就不编译。
常见的条件编译指令
1、
#if 常量表达式
...
#endif
常量表达式由预处理器求值。常量表达式的结果为真,执行后面的语句;常量表达式的结果为假,不执行后面的语句。
2、多个分支的条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif
常量表达式的结果为真,执行后面的语句;常量表达式的结果为假,不执行后面的语句
3、判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
4、嵌套指令
#if defined(OS_UNIX)
#ifdefOPTION1
unix_version_option1();
#endif
#ifdefOPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdefOPTION2
msdos_version_option2();
#endif
#endif
例如
#include <stdio.h>
#define DEBUG //定义了就可以,不给值也无所谓
int main()
{
// intarr[] = { 0,1,2,3,4,5,6,7,8,9 };
// inti = 0;
// for(i = 0; i < 10; i++)
// {
// arr[i]= 0;
//#ifdef DEBUG
// printf("%d", arr[i]);
//#endif
// }
//常见的条件编译指令
//1.
//#if 常量表达式
// ...
//#endif
//常量表达式由预处理器求值。
//常量表达式的结果为真,执行后面的语句;常量表达式的结果为假,不执行后面的语句
// intarr[] = { 0,1,2,3,4,5,6,7,8,9 };
// inti = 0;
// for(i = 0; i < 10; i++)
// {
// arr[i]= 0;
//#if 1+1
// printf("%d", arr[i]);
//#endif
// }
//2.多个分支的条件编译
//#if 常量表达式
// //...
//#elif 常量表达式
// //...
//#else
// //...
//#endif
//常量表达式的结果为真,执行后面的语句;常量表达式的结果为假,不执行后面的语句
//#if 1==2
// printf("111\n");
//#elif 2==2
// printf("222\n");
//#else
// printf("333\n");
//#endif
//3.判断是否被定义
//#ifdefined(symbol)
//#ifdefsymbol
//#if!defined(symbol)
//#ifndefsymbol
//#if defined(DEBUG)
// printf("hello\n");
//#endif
//写法等价#if defined(DEBUG)
//#ifdef DEBUG
// printf("helloworld\n");
//#endif
//#if !defined(DEBUG)
// printf("hello\n");
//#endif
//写法等价#if defined(DEBUG)
#ifndef DEBUG
printf("hello world\n");
#endif
//4.嵌套指令
//#ifdefined(OS_UNIX)
// #ifdef OPTION1
// unix_version_option1();
// #endif
// #ifdef OPTION2
// unix_version_option2();
// #endif
//#elifdefined(OS_MSDOS)
// #ifdef OPTION2
// msdos_version_option2();
// #endif
//#endif
return 0;
}
11、文件包含
1、文件包含
我们已经知道,#include指令可以使另外一个文件被编译。就像它实际出现于#include指令的地方一样。
这种替换的方式很简单:预处理器先删除这条指令,并用包含文件的内容替换。
这样一个源文件被包含10次,那就实际被编译10次。
2、#include包含头文件的时候,有时候用<>,有时候用""
头文件被包含的方式:
1)本地文件包含
#include "filename"
"" 查找策略:先在源文件所在目录下查找,如果该头文件未找到,
编译器就像查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误。
2)库文件包含
#include <filename.h>
<> 查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
对于库文件也可以使用"" 的形式包含?
答案是肯定的,可以。
但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了
12、嵌套文件包含
comm.h和comm.c是公共模块。
test1.h和test1.c使用了公共模块。
test2.h和test2.c使用了公共模块。
test.h和test.c使用了test1模块和test2模块。
这样最终程序中就会出现两份comm.h的内容。这样就造成了文件内容的重复。
如何解决这个问题?
答案:条件编译。
1)每个头文件的开头写:
#ifndef __TEST_H__
#define __TEST_H__
头文件的内容
#endif //__TEST_H__
说明:第一次引入头文件,如果没有定义__TEST_H__,就定义__TEST_H__,执行后面的语句。
第二次引入头文件,定义了__TEST_H__,条件编译指令为假,后面的代码不参与编译,防止代码被重复包含。
2)或者:
#pragma once
就可以避免头文件的重复引入。
笔试题
1、头文件中的ifndef / define / endif是干什么用的?防止头文件被重复多次包含
2、#include<filename.h> 和#include "filename.h"有什么区别?引入场景、引入策略不一样