源码.c
——> (预处理)——>预处理过的.i
文件——>(编译)——>汇编文件.S
——>(汇编)——>目标文件.o
->(链接)——>elf
可执行程序
预处理用预处理器,编译用编译器,汇编用汇编器,链接用链接器,这几个工具再加上其他一些额外的会用到的可用工具,合起来叫编译工具链(gcc就是一个编译工具链)。
gcc中各选项的使用方法:
-E
:只预处理不编译(执行头文件的扩展、宏替换、条件编译筛选、去掉注释等),生成.i
文件-S
:只编译不汇编(将C/C++语言程序翻译成汇编语言),生成.S
文件-c
:只汇编不链接(将汇编语言翻译成机器指令),生成.o
文件-o
:链接(将目标文件和库文件进行链接,得到可执行文件),生成elf
可执行文件gcc xx.c -o xx
可以指定可执行程序的名称;譬如gcc xx.c -c -o xx.o
可以指定只编译不连接,仅生成.o
目标文件。gcc -E xx.c -o xx.i
可以实现只预处理不编译。一般情况下没必要只预处理不编译,但有时候这种技巧可以用来帮助我们研究预处理过程,帮助debug程序。star.s
、main.c
、led.c
这三个源文件,分别被编译成三个目标文件,每个目标文件有很多函数集合。链接的时候会根据运行思路把这些杂乱的函数给排列组合起来,不是把目标文件简单的排列组合。strip
工具:strip是把可执行程序中的符号信息给拿掉,以节省空间。objcopy
工具:可将可执行程序转变成可烧录的bin文件。#define
用#符号作为行的开头。预处理指令从#开始,到其后第一个换行符为止。也就是说,指令的长度限于一行代码。如果想把指令扩展到几个物理行,可使用反斜线后紧跟换行符的方法实现,该出的换行符代表按下回车键在源代码文件中新起一行所产生的字符,而不是符号 \n
代表的字符。在预处理开始钱,系统会删除反斜线和换行符的组合,从而达到把指令扩展到几个物理行的效果。可以使用标准C注释方法在#define行中进行注释。//使用反斜线+回车
#define OW "hello\
world!" /*注意第二行要左对齐*/
每一个#define行由三部分组成:
下面来看一个例子:
#include
#define OW 2 * 2
#define OW 2 * 2
//#undef OW 需要先取消宏定义
#define OW 2*2
int main (void)
{
printf ("%d\n", OW);
return 0;
}
输出结果:
define.c:5:0: 警告: “OW”重定义 [默认启用]
define.c:4:0: 附注: 这是先前定义的位置
#define OW 2 * 2
#define OW 2 * 2
#define OW 2*2
/* 宏演示 */
#include
int main()
{
int num=0;
int arr[SIZE]={}; //使用gcc -D可以宏定义这个数字
for(num = 0;num <= SIZE - 1;num++){
arr[num]=num;
printf("%d ",arr[num]); }
printf("\n"); return 0;
}
/*
gcc -DSIZE=4 define.c
输出结果:
0 1 2 3
*/
通过使用参数,可以创建外形和作用都与函数相似的类函数宏。宏的参数也用圆括号括起来。类函数宏的定义中,用圆括号括起来一个或多个参数,随后这些参数出现在替换部分。
#include
#define SQUARE(X) X*X
#define PR(X) printf ("The result is %d\n", X)
int main (void)
{
int x = 4;
int z;
printf ("x = %d\n", x); z = SQUARE(x);
printf ("Evaluating SQUARE(x): ");
PR(z);
z = SQUARE(2);
printf ("Evaluating SQUARE(2): ");
PR(z);
printf ("Evaluating 100/SQUARE(2): ");
PR(100/SQUARE(2));
z = SQUARE(x+2);
printf ("Evaluating SQUARE(x+2): ");
PR(z);
printf ("x is %d\n", x);
z = SQUARE(++x);
printf ("Eavluating SQUARE(++x): ");
PR(SQUARE (++x));
printf ("After incrementing, x is %x\n", x);
return 0;
}
/*
输出结果:
x = 4
Evaluating SQUARE(x): The result is 16Evaluating
SQUARE(2): The result is 4
Evaluating 100/SQUARE(2): The result is 100
Evaluating SQUARE(x+2): The result is 14x is 4
Eavluating SQUARE(++x): The result is 36
After incrementing, x is 6
*/
#define SQUARE(x) ((x) * (x))
。从中得到的经验是使用必须的足够多的圆括号来保证以正确的顺序进行运行和结合。参看:C 语言再学习 – 运算符与表达式
在类函数宏的替换部分中,#符号用作一个预处理运算符,它可以把语言符号转化为字符串。例如:如果x是一个宏参量,那么#x可以把参数名转化为相应的字符串。该过程称为字符串化。
#include
#define PSQR(x) printf("The square of "#x" is %d\n", ((x)*(x)))
int main (void)
{
int y = 2;
PSQR (y);
PSQR (2 + 4);
return 0;
}
/*
输出结果:
The square of y is 4
The square of 2 + 4 is 36
*/
#include
#include
#define VEG(n) #n
int main()
{
char str[20];
strcpy(str,VEG(num)); //num
printf("%s\n",str); //拷贝
return 0;
}
/*
输出结果:
num
*/
和#运算符一样,##运算符可以用于类函数宏的替换部分。另外,##还可用于类对象宏的替换部分。这个运算符把两个语言符号组合成单个语言符号。
#include
#define XNAME(n) x##n
#define PRINT_XN(n) printf ("x"#n" = %d\n", x##n)
int main (void)
{
int XNAME (1) = 14; //变为 int x1 = 14;
int XNAME (2) = 20; //变为 int x2 = 20;
PRINT_XN (1); //变为 printf ("x1 = %d\n", x1);
PRINT_XN (2); //变为 printf ("x2 = %d\n", x2);
return 0;
}
/*
输出结果:
x1 = 14
x2 = 20
*/
#include
#define MAX(x,y) ((x)>(y) ? (x) : (y)) /*比较大小*/
#define ABS(x) ((x) < 0 ? -(x) : (x)) /*绝对值*/
#define ISSIGN(x) ((x) == '+' || (x) == '-' ? 1 : 0) /*正负号*/
int main()
{
printf ("较大的为: %d\n", MAX(5,3));
printf ("绝对值为: %d\n", ABS (-2));
printf ("正负号为: %d\n", ISSIGN ('+'));
return 0;
}
/*
输出结果:
较大的为: 5
绝对值为: 2
正负号为: 1
*/
下面是需要注意的几点:
面试:用预处理指令#define 声明一个常数,用以表明1年中有多少秒(忽略闰年问题)
#define SEC (60*60*24*365)UL
面试:写一个“标准”宏MIN ,这个宏输入两个参数并返回较小的一个
#define MIN(A,B) ((A) <= (B) ? (A) : (B))
参看:宏名必须用大写字母吗?
#define P1 int *
typedef (int *) P2
P1 a, b;
在宏代换后变成: int *a, b;
表示 a 是指向整型的指针变量,而 b 是整型变量。P2 a, b;
表示a,b都是指向整型的指针变量。因为PIN2是一个类型说明符。#define ENG_PATH_1 E:\English\listen_to_this\listen_to_this_3
#define ENG_PATH_2 "E:\English\\listen_to_this\\listen_to_this_3"
取消定义一个给定的 #define
#define LIMIT 40
,则指令#undef LIMIT
会取消该定义。现在就可以重新定义LIMIT,以使它有一个新的值。#include
#define X 3
#define Y X*3
#undef X
#define X 2
int main (void)
{
printf ("Y = %d\n", Y);
printf ("X = %d\n", X);
return 0;
}
/*
输出结果:
Y = 6
X = 2
*/
#include
#define X 3
#define Y X*3
#define X 2 //不可重复定义
int main (void)
{
int z = Y;
printf ("Y = %d\n", z);
printf ("X = %d\n", X);
return 0;
}
/*
输出结果:
test.c:4:0: 警告: “X”重定义 [默认启用]
test.c:2:0: 附注: 这是先前定义的位置
*/
#include
指令后,就会寻找后跟的文件名并把这个文件的内容包含但当前文件中。被包含文件中的文件将替换源代码文件中的#include
指令,就像你把被包含文件中的全部内容键入到源文件中的这个特定位置一样。#include
指令有两种使用形式:
#include
#include "filename.h"
扩展:C语言再学习 – 常用头文件和函数(转)
Lniux的文件系统中有一个大分组,它包含了文件系统中所有文件,这个大的分组用一个专门的目录表示,这个目录叫做根目录,根目录可以使用“/”表示。
路径可以用来表示文件或者文件夹所在的位置,路径是从一个文件夹开始走到另一个文件夹或者文件位置中间的这条路。把这条路经过的所有文件夹名称按顺序书写出来的结果就可以表示这条路。
路径分为绝对路径和相对路径 :
绝对路径:起点必须是根目录,如 /abc/def 所有绝对路径一定是以“/”作为开头的
相对路径:可以把任何一个目录作为起点,如…/…/abc/def 相对路径编写时不应该包含起点位置 。相对目录中“…”表示上层目录,用“.”表示当前目录。
终端窗口里的当前目录是所有相对路径的起点,当前目录的位置是可以修改的。
pwd
命令可以用来查看当前目录的位置
cd
命令可以用来修改当前目录位置
ls
命令可以用来查看一个目录的内容
参看:条件编译#ifdef的妙用详解_透彻
#if
:表示如果…
#ifdef
: 表示如果定义…
#ifndef
:表示如果没有定义…
#else
:表示否则…,与#ifdef、#ifndef搭配使用。
#elif
:表示否则如果…,与#if、#ifdef、#ifndef搭配使用。==没有#elseif ==
#endif
:表示结束判断,与#if、#ifdef、#ifndef搭配使用
注意:#if 和 if 区别
#if
=>主要用于编译期间的检查和判断if
=>主要用于程序运行期间的检查和判断(1)最常见的形式:
#ifdef 标识符
程序段1
#else
程序段2
#endif
#ifdef 标识符
程序段1
#endif
long
类型表示,而在其他平台应该使用float
表示,这样往往需要对源程序做必要的修改,这就降低了程序的通用性。可以用以下的条件编译:#ifdef WINDOWS
#define MYTYPE long
#else
#define MYTYPE float
#endif
#define WINDOWS
,这样则编译#define MYTYPE long
#define WINDOWS 0
,则预编译后程序中的MYTYPE都用float代替。#ifdef DEBUG
print ("device_open(%p)\n", file);
#endif
#define DEBUG
,则在程序运行时输出file指针的值,以便调试分析。调试完成后只需将这个define命令行删除即可。printf
语句,调试后再将prntf语句全部删除。的确,这是可以的。但是,当调试时加的printf语句比较多时,修改的工作量是很大的。用条件编译,则不必一一删除printf语句。只需删除前面的一条#define DEBUG 命令即可,这时所有的用DEBUG 作标识符的条件编译段都使其中的printf语句不起作用,起到统一控制的作用,如同一个“开关”一样。(2)有时也采用下面的形式
#ifndef 标识符
程序段1
#else
程序段2
#endif
只是第一行与第一种形式不同:将#ifdef
改为#ifndef
。它的作用是,若标识符未被定义则编译程序段1,否则编译程序段2。这种形式与第一种形式的作用相反。
一般地,当某文件包含几个头文件,而且每个头文件都可能定义了相同的宏,使用#ifndef可以防止该宏重复定义。
/*test.h*/
#ifndef SIZE
#define SIZE 100
#endif
#ifndef
指令通常用于防止多次包含同一文件,也就是说,头文件可采用类似下面几行的设置:
//头文件卫士
#ifndef THINGS_H_
#define THINGS_H_
#endif
(3) #if 后面跟一个表达式,而不是一个简单的标识符
#if 表达式
程序段1
#else
程序段2
#endif
当指定的表达式为真(非零)时就编译程序段1,否则编译程序段2。可以事先给定一定条件,使程序在不同的条件下执行不同的功能。例如:
#include
#define LETTER 1
int main (void)
{
#if LETTER
printf ("111\n");
#else
printf ("222\n");
#endif
return 0;
}
/*
输出结果:
111
*/
这种形式也可以用作注释用:#if 1 和 #if 0
#include
int main (void)
{
#if 0
printf ("111\n");
#endif
printf ("222\n");
return 0;
}
/*
输出结果:
222
*/
(4)最后一种形式
#if 标识符
程序段
#elif
程序段1
#elif
程序段2
...
#else
程序段n
#endif
#if...#elif(任意多次)...#else...#endif
,以上结构可以从任意逻辑表达式选择一组编译,这种结构可以根据任意逻辑表达式进行选择。/* 条件编译演示 */
#include
#define SAN
int main()
{
#if defined(YI) //布尔值
printf("1\n");
#elif defined(ER) //布尔值
printf("2\n");
#elif defined(SAN)
printf("3\n");
#else
printf("4\n");
#endif
return 0;
}
/*
输出结果:
3
*/
应用示例
#define DEBUG
#define TEST
利用#ifdef 和 #endif 将程序功能模块包括进去,以向某用户提供该功能
//在程序首部定义#define HNLD:
#ifdef HNLD
include"n166_hn.c"
#endif
在每一个子程序前加上标记,以便追踪程序的运行
#ifdef DEBUG
printf(" Now is in hunan !");
#endif
避开硬件的限制。有时一些具体应用环境的硬件不一样,但限于条件,本地缺乏这种设备,于是绕过硬件,直接写出预期结果。具体做法是:
#ifndef TEST
i=dial(); //程序调试运行时绕过此语句
#else
i=0;
#endif
//调试通过后,再屏蔽TEST的定义并重新编译,即可发给用户使用了。
确保使用的标识符在其他任何地方都没有定义过
通常编译器提供商采用下述方法解决这个问题:用文件名做标识符,并在文件名中使用大写字母、用下划线代替文件名中的句点字符、用下划线(可能使用两条下划线)做前缀和后缀。例如,检查头文件read.h,可以发现许多类似的语句:
#ifndef __READ_H__ //防止被重复定义
#define __READ_H__
extern int num=0;
#endif
参看:C语言再学习 – 标识符
extern "C"
可以要求 C++ 编译器按照 C方式处理函数接口,即不做换名,当然也就无法重载。
C 调 C++,在 C++ 的头文件如下设置:
extern "C" int add (int x, int y);
extern "C" {
int add (int x, int y);
int sub (int x, int y);
}
//示例 add.h
#ifndef _ADD_H
#define _ADD_H
#ifdef __cplusplus
extern "C" {
#endif
int add (int ,int );
#ifdef __cplusplus
}
#endif
#endif
C++ 调 C,在C++ 的主函数如下设置:
extern "C" {
#include "chead.h"
}
//示例 main.cpp
#include
using namespace std;
extern "C" {
#include "05sub.h"
}
int main (void)
{
int x=456,y=123;
cout << x << "+" << y << "=" << sub(x, y) << endl;
return 0;
}
__DATE__
:进行预处理的日期(“mm dd yyyy”形式的字符串文字)
__FILE__
:代表当前源代码文件名的字符串
__BASE_FILE__
:获取正在编译的源文件名
__LINE__
:代表当前源代码文件中的行号
__TIME__
:源文件编译时间,格式为“hh: mm: ss”
__STDC__
:设置为 1时,表示该实现遵循 C标准
__STDC_HOSTED__
:为本机环境设置为 1,否则设为 0
__STDC_VERSION__
:为C99时设置为199901L,C11为201112L,C18、C17为201710L
__FUNCTION__
或者 __func__
:获取所在的函数名(预定义标识符,而非预定义宏)
//part.c
#include
int main (void)
{
printf ("The file is %s\n", __FILE__);
printf ("The base_file is %s\n", __BASE_FILE__);
printf ("The line is %d\n", __LINE__);
printf ("The function is %s\n", __FUNCTION__);
printf ("The func is %s\n", __func__);
printf ("The date is %s\n", __DATE__);
printf ("The time is %s\n", __TIME__);
return 0;
}
/*
输出结果:
The file is part.c
The base_file is part.c
The line is 6
The function is main
The func is main
The date is Nov 22 2016
The time is 15:46:30
*/
#line 整数n
=>表示修改代码的行数/指定行号 插入到程序中表示从行号n开始执行,修改下一行的行号为n
#error 字符串
=> 表示产生一个错误信息
#warning 字符串
=> 表示产生一个警告信息
//#line 预处理指令的使用
#include
#line 200
int main(void)
{
printf("The line is %d\n",__LINE__);
return 0;
}
/*
输出结果:
The line is 203
*/
//#error和#warning的使用
#include
#define VERSION 4
#define VERSION 2
#define VERSION 3
#if(VERSION < 3)
#error "版本过低"
#elif(VERSION > 3)
#warning "版本过高"
#endif
int main(void){
printf("程序正常运行\n");
return 0;
}
/*
输出结果:
警告: #warning "版本过高"
//错误: #error "版本过低"
//程序正常运行
*/
#pragma GCC dependency 文件名
#include
#pragma GCC dependency "01print.c" //当前程序依赖于01print.c文件
int main(void){
printf("Good Good Study,Day Day Up!\n");
return 0;
}
/*
输出结果:
致命错误: 01print.c:没有那个文件或目录编译中断。
*/
#pragma GCC poison 标示符
表示将后面的标示符设置成毒药,一旦使用标示符,则产生错误或警告信息
//毒药的设置
#include
//#define GOTO goto
//将goto设置为毒药
#pragma GCC poison goto
int main(void){
//GOTO ok;
goto ok;
printf("main函数开始\n");
ok: printf("main函数结束\n");
return 0;
}
/*
输出结果:
错误: 试图使用有毒的“goto”
*/
#pragma pack(n)
表示按照整数n倍进行补齐和对齐
//设置结构体的对齐和补齐方式
#include
//设置结构体按照2的整数倍进行对齐补齐
#pragma pack(2) //8
//#pragma pack(1) //6
//#pragma pack(3) //error
//char short int long int long long
int main(void){
struct S{
char c1;
int i;
char c2;
};
printf("sizeof(struct S) = %d\n",sizeof(struct S)); //12
return 0;
}
/*
输出结果:
sizeof(struct S) = 8
*/
#pragma message(“string”)
message 参数: message 参数是我最喜欢的一个参数,它能够在编译信息输出窗口中输出相应的信息,这对于源代码信息的控制是非常重要的。其使用方法为:
#pragma message(“消息文本”)
当编译器遇到这条指令时就在编译输出窗口中将消息文本打印出来。当我们在程序中定义了许多宏来控制源代码版本的时候,我们自己有可能都会忘记有没有正确的设置这些宏,此时我们可以用这条指令在编译的时候就进行检查。假设我们希望判断自己有没有在源代码的什么地方定义了_X86
这个宏可以用下面的方法.
#define _X86
#ifdef _X86
#pragma message ("_X86 macro activated!")
#endif
/*
输出结果:
附注: #pragma message:_X86 macro activated!
*/
#pragma code_seg
code_seg
,它能够设置程序中函数代码存放的代码段,格式如:#pragma code_seg( ["section-name"[,"section-class"] ] )
当我们开发驱动程序的时候就会使用到它。#pragma once
#pragma hdrstop
#pragma startup
指定编译优先级,如果使用了#pragma package(smart_init)
, BCB就会根据优先级的大小先后编译。#pragma resource
*.dfm
表示把*.dfm
文件中的资源加入工程。 *.dfm
中包括窗体外观的定义。#pragma warning
#pragma warning( disable : 4507 34; once : 4385; error : 164 )
等价于:#pragma warning(disable:4507 34) // 不显示 4507 和 34 号警告信息
#pragma warning(once:4385) // 4385 号警告信息仅报告一次
#pragma warning(error:164) // 把 164 号警告信息作为一个错误。
#pragma warning( push [ ,n ] ) //n 代表一个警告等级(1-4)
#pragma warning( pop )
#pragma warning( push ) //保存所有警告信息的现有的警告状态。
#pragma warning( push, n) //保存所有警告信息的现有的警告状态,并且把全局警告等级设定为 n。
#pragma warning( pop ) //向栈中弹出最后一个警告信息,在入栈和出栈之间所作的一切改动取消。例如:
#pragma warning( push )
#pragma warning( disable : 4705 )
#pragma warning( disable : 4706 )
#pragma warning( disable : 4707 )
//.......
#pragma warning( pop )
在这段代码的最后,重新保存所有的警告信息(包括 4705, 4706 和 4707)。
#pragma comment()
#pragma comment(lib, "user32.lib")
将 user32.lib 库文件加入到本工程中。#pragma comment(linker, "/include:__mySymbol")
/usr/include
中字符串函数有:
memcpy(dest, src)
:内存字符串复制,它直接将源空间内容复制到目标内存空间,所以使用它的前提是确定src和dst不会overlap重复,执行效率高。memmove(dest, src)
:内存字符串复制,它先将源空间内容复制到一个临时内存空间,然后再复制到目标空间。memset()
strncmp()
memcmp()
strdup()
memchr()
strndup()
strcpy()
strchr()
strncpy()
strstr()
strcat()
strtok()
strncat()
strcmp()
-lm
告诉链接器到libm.so
中去查找用到的函数。-lxxx
来指示链接器去到libxxx.so
中去查找这个函数。静态链接库就是商业公司将自己的函数库源代码经过只编译不连接形成
.o
的目标文件,然后用ar
工具将.o
文件归档成.a
的归档文件(.a的归档文件又叫静态链接库文件)。商业公司通过发布.a库文件和.h头文件来提供静态库给客户使用;客户拿到.a和.h文件后,通过.h头文件得知库中的库函数的原型,然后在自己的.c文件中直接调用这些库文件,在连接的时候链接器会去.a
文件中拿出被调用的那个函数的编译后的.o
二进制代码段,链接进去形成最终的可执行程序。
gcc -c
只编译不连接,生成.o
文件;ar
工具进行打包成.a
归档文件。
.a
和.h
都放在自己引用的文件夹下;.c
文件中包含库的.h
;.a
文件时,前缀一定要加lib,如libzf.a
-l
(小L)后跟库名(不要lib和.a)-L
表示库的路径-I
表示库的头文件路径gcc cdw.c -o cdw -lzf -L./include -I(大i)./include
动态链接库本身不将库函数的代码段链接入可执行程序,只是做个标记。当应用程序在内存中执行时,运行时环境发现它调用了一个动态库中的库函数时,会去加载这个动态库到内存中,然后以后不管有多少个应用程序去调用这个库中的函数都会跳转到第一次加载的地方去执行(不会重复加载)。也就是在运行时,会把库函数代码放入内存中,然后多个程序要用到库函数时,就从这段内存去找,而静态链接对于多程序就是重复使用库函数,比较占内存。
.so
(对应windows系统中的.dll
),静态库的扩展名是.a
。gcc aston.c -o aston.o -c -fPIC
:-fPIC
表示设置位置无关码。gcc -o libaston.so aston.o -shared
:-shared
表示使用共享库gcc cdw.c -o cdw -lmax.so -L./动态链接库路径 -I./动态链接库头文件路径
上述命令虽然可以编译成功,但是在执行时会报错。原因是采用动态链接时,只是在可执行文件中做了一个标记(标记是使用了哪个函数库的哪个函数),但并没有将库函数加载到源文件中,所以可执行文件很小。但在执行时,需要立即从系统里面找到使用到的函数库,然后加载到内存中,在linux系统中会先从环境变量的路径,然后再从系统默认的路径
/usr/lib
中寻找,所以我们可以用两种办法解决运行的问题:
一是:将动态库libmax.so
复制到/usr/lib
下面,但是如果以后所有的库都这样放的话,会越来越臃肿,导致运行速度变慢(系统会一个一个查找)
二是新添加一个环境变量:export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/mnt/hgfs/share/include
,然后将库libmax.so
复制到这个路径下面就可以了。
""
括起来表示该头文件从自定义目录下找。如果没加路径属性,默认当前路径找,如果在其他文件夹下,必须用-I(大i)路径。<>
括起来表示该头文件从编译器的默认库目录下找,或者是自定义的库目录下找,但是要明确其路径。nm ./include/libmax.a
:可以查看max库中有哪些.o文件,各自含有哪些函数。ldd
命令判断可执行文件是否能运行成功ldd cdw
linux-gate.so.1=>(0xb77a8000)
libmax.so=>notfound #notfound意思就是没有找到对应的函数库
libc.so.6=>/lib/i386-linux-gnu/libc.so.6(0xb75e2000)
/lib/ld-linux.so.2(0xb77a9000)
-static
来强制静态链接。