在VS编译中,Debug模式下会生成.pdb
文件和.exe
文件,DPB文件主要存储了VS调试程序时所需要的基本信息,主要包括源文件名、变量名、函数名、FPO(帧指针)、对应的行号等等。
而本篇文章主要讲代码编译后生成.exe
文件的过程
在ANSI C的任何一种实现中,存在两个环境:
编译也分为好几个阶段:预编译、编译、汇编、链接(以下都是在gcc编译器中)
gcc -E
指令,会生成xxx.i
文件,可以打开看到预编译处理之后代码gcc -S
指令,会生成xxx.s
文件,可以看到编译之后的汇编代码gcc -c
指令会生成xxx.o
文件。在Linux系统下,test.o的二进制文件是以elf的格式来组织文件的。在Linux平台下我们可以用readelf翻译并查看它的内容test.o
等链接在一起形成可执行的程序test.out
。符号表的合并,就是将各自的符号表合并到一起,比如说:test.o中Add的无效地址,需把add.o中Add的地址与之合并再重新定位到变量的真实地址,这样才是有意义的。总结: 编译期间的符号汇总,到汇编阶段的形成符号表,再到链接时的合并段表和符号表的合并和重新定位,都是为了最后生成可执行程序时能够找到并链接各个源文件文件中的符号(全局变量或者函数)
程序执行的过程:
__FILE__ //进行编译的源文件 __LINE__ //文件当前的行号 __DATE__ //文件被编译的日期 __TIME__ //文件被编译的时间 __STDC__ //如果编译器遵循ANSI C,值为1,否则未定义
这些预定义符号都是语言内置的
printf("file:%s line:%d\n", __FILE__, __LINE__);
#define MAX 100 #define CASE break;case #define reg register
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
宏的申明方式:
#define name( parament-list ) stuff
其中的parament-list
是一个由逗号隔开的符号表,可能出现在stuff中
注: 参数列表的左括号必须与name紧邻;如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
#define SQUARE(x) ((x)*(x))
在程序中扩展#define定义符号和宏时,需要涉及几个步骤
注意:
如何把参数插入到字符串中?
char* p = "hello ""bit\n"; printf("hello"" bit\n"); printf("%s", p);
可以发现字符串是有自动连接的特点的
#的作用:
把一个宏参数变成对应的字符串。
int i = 10; #define PRINT(FORMAT, VALUE)\ printf("the value of " #VALUE "is "FORMAT "\n", VALUE); ... PRINT("%d", i+3);/
代码中的#VALUE会预处理器处理为:
“VALUE”
##的作用:
可以把位于它两边的符号合成一个符号
它允许宏定义从分离的文本片段创建标识符
#define ADD_TO_SUM(num, value) \ sum##num += value; //... ADD_TO_SUM(5, 10);//作用是:给sum5增加10.
这样的连接必须产生一个合法的标识符。否则其结果就是未定义的
宏的优点:
宏的缺点:
这条指令用于移除一个宏定义
#undef NAME //如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令
常见的条件编译指令:
1. #if 常量表达式 //... #endif //常量表达式由预处理器求值。 如: #define __DEBUG__ 1 #if __DEBUG__ //.. #endif 2.多个分支的条件编译 #if 常量表达式 //... #elif 常量表达式 //... #else //... #endif 3.判断是否被定义 #if defined(symbol) #ifdef symbol #if !defined(symbol) #ifndef symbol 4.嵌套指令 #if defined(OS_UNIX) #ifdef OPTION1 unix_version_option1(); #endif #ifdef OPTION2 unix_version_option2(); #endif #elif defined(OS_MSDOS) #ifdef OPTION2 msdos_version_option2(); #endif #endif
#include
指令可以使另外一个文件被编译。就像它实际出现于 #include
指令的地方一样
这种替换的方式很简单:
预处理器先删除这条指令,并用包含文件的内容替换。
这样一个源文件被包含10次,那就实际被编译10次。
#include "filename"
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误。
Linux环境的标准头文件的路径:
/usr/include
VS环境的标准头文件的路径:
C:\Program Files(x86)\Microsoft Visual Studio 12.0\VC\include
#include <filename.h>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误
库文件也可以使用 “” 的形式包含
但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了
这样最终程序中就会出现两份comm.h的内容。这样就造成了文件内容的重复
条件编译可以解决这个问题
每个头文件的开头写:
#ifndef __TEST_H__ #define __TEST_H__ //头文件的内容 #endif
或者
#pragma once
就可以避免头文件的重复引入
#error //在编译期间产生错误信息,并阻止程序编译 #pragma #line //#line可以修改当前源文件中的宏的值,__FILE__和__LINE__ #pragma pack() //设置结构体的对齐数
int main() { printf("%s,line:%d \n", __FILE__, __LINE__); #line 2 "test.c" printf("%s,line:%d \n", __FILE__, __LINE__); #line 10 "test1.c" printf("%s,line:%d \n", __FILE__, __LINE__); return 0; }