我们知道,C语言源代码生成可执行文件需要两个步骤:编译和链接。C语言的很多默认行为和链接的过程相关,这篇笔记我们介绍链接过程的相关知识。
下面例子中,两个.c
源代码文件编译链接在一起。
#直接编译链接
gcc main.c stack.c -o main
#分别编译连接
gcc -c main.c
gcc -c stack.c
gcc main.o stack.o -o main
链接的过程是由一个链接脚本(Linker Script)控制的,链接脚本决定了给每个段(Segment)分配什么地址,如何对齐,哪个段在前,哪个段在后,哪些段合并到同一个段,另外链接脚本还要插入一些符号到最终生成的文件中,例如__bss_start
、_edata
、_end
等。
如果用ld
做链接时没有用-T
选项指定链接脚本,则使用ld
的默认链接脚本,默认链接脚本可以用ld --verbose
命令查看。
我们看下面例子代码。
stack.c
int container[100];
int *top_ptr = container;
int is_empty = 1;
extern void push(int i);
extern void pop(void);
extern int top(void);
void push(int i)
{
if(!is_empty)
{
top_ptr++;
}
else
{
is_empty = 0;
}
*top_ptr = i;
}
void pop(void)
{
if(top_ptr != container)
{
top_ptr--;
if(top_ptr == container)
{
is_empty = 1;
}
}
}
int top(void)
{
return *top_ptr;
}
main.c
#include <stdio.h>
int main(void)
{
push(1);
push(2);
push(3);
printf("%d\n", top());
pop();
printf("%d\n", top());
pop();
printf("%d\n", top());
pop();
return 0;
}
extern关键字表示这个标识符具有External Linkage。push
这个标识符具有External Linkage指的是:如果把main.c
和stack.c
链接在一起,如果push
在main.c
和stack.c
中都有声明(在stack.c
中的声明同时也是定义),那么这些声明指的是同一个函数,链接之后是同一个GLOBAL
符号,代表同一个地址。函数声明中的extern
也可以省略不写,不写extern
的函数声明也表示这个函数具有External Linkage。
如果用static
关键字修饰一个函数或变量声明,则表示该标识符具有Internal Linkage,函数只在那个文件能多次声明;如果在另一个.c
文件中声明,编译器就认为这个函数不是原来那个函数了。
注意,变量声明和函数声明有一点不同,函数声明的extern
可写可不写,而变量声明如果不写extern
意思就完全变了,如果变量在函数中,不写extern
就表示在函数中定义一个局部变量。
另外要注意,extern
的变量声明不能进行初始化,因为extern
的变量声明不会开辟内存空间,自然也不能进行初始化。
实际上extern
和static
的规则更复杂一些,但这样简单理解不影响使用。
C语言中,除了.c
源代码文件,比较常见的还有.h
头文件。我们编写代码时,可以使用#include
预处理指令引入头文件。
<>
:gcc先查-I
指定的目录,再查系统头文件目录""
:gcc先查包含头文件的.c
文件所在的目录,然后查找-I
指定的目录,再找系统头文件目录.h
头文件和.c
源文件不在同一目录,也可以在编译时由-I
参数指定给gcc#include
预处理指令也可以带有路径,如#include "stack/stack.h"
头文件重复包含会引起函数重复声明,导致编译报错。使用#ifndef #define #endif
宏可以巧妙的实现防止头文件重复包含,这是一个相当常见的技巧。
#ifndef STACK_H
#define STACK_H
extern void push(int i);
extern void pop(void);
extern int top(void);
#endif