c++ 学习笔记1——理解 gcc 编译和链接
声明和定义
首先来说两个概念,声明和定义(此处仅针对函数)。
声明,可以简单理解为说我们有这样一个函数;
定义,可以简单理解为这个函数要作什么,是怎么实现的。
先来一个小例子,创建 main.cpp 文件
#include <iostream>
// 声明 add 方法
int add(int, int);
int main()
{
printf("add(3, 4) = %d\n", add(3, 4));
return 0;
}
// 定义 add 方法
int add(int a, int b)
{
return a + b;
}
很简单,其实就是在最上面声明了一个 add 方法,最下面对该方法进行定义,然后就可以通过执行 main 函数,完成内部对 add 方法的调用。
如执行如下命令:
➜ g++ main.cpp
➜ ./a.out
add(3, 4) = 7
这里通过 g++ 命令通过源代码生成了可执行文件,接下来看一下从源代码到可执行文件的过程。
编译和链接
直接说结论,从源代码到可执行文件,中间存在两个过程,分别为 编译 和 链接。
编译
g++ 命令将两个过程合在了一起,为了更清晰的认识两个过程,我们将其过程拆解,具体如下:
➜ g++ -c main.cpp
➜ ls
main.cpp main.o
➜ g++ -c
-c -- Compile and assemble, but do not link
通过 g++ -c 我们生成了一个 main.o 的对象文件,其实这个对象文件中的内容就是我们对一些函数的定义。我们使用 nc -C 命令查看其中的内容:
➜ hello nm -C main.o
U __cxa_atexit
U __dso_handle
U _GLOBAL_OFFSET_TABLE_
0000000000000096 t _GLOBAL__sub_I_main
0000000000000000 T main
U printf
0000000000000031 T add(int, int)
0000000000000049 t __static_initialization_and_destruction_0(int, int)
U std::ios_base::Init::Init()
U std::ios_base::Init::~Init()
0000000000000000 r std::piecewise_construct
0000000000000000 b std::__ioinit
我们看到的内容就是程序中所用到的函数,主要关注我们显示用到的函数 main、printf、add,前面有数字的就代表该函数的定义是储存在该对象文件中,对应此处的 main、add。
回过头来,编译阶段作了什么呢?其实编译阶段仅仅只是作了语法检查的工作,还句话说,只要我们的代码没有语法错误,就可以编译通过。
可以通过删除 add 函数的定义来验证。可以明确知道的是,删除之后,代码是没有办法正常工作的,但是能否通过编译呢?我们可以执行 g++ -c 来验证。
我们先将 add 函数的定义注释掉。
// int add(int a, int b)
// {
// return a + b;
// }
然后执行 g++ -c
➜ ls
main.cpp main.o
➜ rm main.o
➜ ls
main.cpp
➜ g++ -c main.cpp
➜ ls
main.cpp main.o
➜ nm -C main.o
U __cxa_atexit
U __dso_handle
U _GLOBAL_OFFSET_TABLE_
000000000000007e t _GLOBAL__sub_I_main
0000000000000000 T main
U printf
U add(int, int)
0000000000000031 t __static_initialization_and_destruction_0(int, int)
U std::ios_base::Init::Init()
U std::ios_base::Init::~Init()
0000000000000000 r std::piecewise_construct
0000000000000000 b std::__ioinit
可以明确看到,g++ -c 的操作成功色和功能成了 main.o 对象文件,该文件内部有了变化,add 函数前面的数字没有了,该函数的定义不在此对象文件中了。验证了我们所说的编译阶段就是语法检查。
我们知道了前面带有数字的 main 函数在该对象文件中被定义,那没有数字的 printf 以及 add 函数的定义在哪呢?显然如果需要让程序能够使用,我们就需要提供这些(printf 在标准库中已经提供),当然不必一定要在同一个对象文件中提供,我们尝试在外部提供,让链接 这个过程帮我们将两个对象文件合到一起。
链接
创建一个 other.cc 文件
int add(int a, int b)
{
return a + b;
}
对 other.cc 进行编译
➜ ls
main.cpp main.o other.cc
➜ g++ -c other.cc
➜ ls
main.cpp main.o other.cc other.o
➜ nm -C other.o
0000000000000000 T add(int, int)
接下来就可以通过链接将两个 .o 文件链接到一起,输出可执行文件。
➜ g++ main.o other.o
➜ ls
a.out main.cpp main.o other.cc other.o
➜ ./a.out
add(3, 4) = 7
至此就完成了编译、链接两个过程。
总结
本文通过编译和链接的两个过程,加深了对声明和定义的理解。
简单可以这样概括
编译:检查语法错误
链接:将声明和定义链接起来
用到的命令
g++ -c 编译
nm -C *.o 查看 *.o 文件内容
g++ *1.o *2.o 将多个.o对象文件进行链接
附
在执行 g++ 命令的时候,我们常会出现这样的错误。
➜ g++ main.cpp
main.cpp: In function ‘int main()’:
main.cpp:7:32: error: ‘add’ was not declared in this scope
7 | printf("add(3, 4) = %d\n", add(3, 4));
|
➜ g++ main.cpp
/usr/bin/ld: /tmp/ccR71J1A.o: in function `main':
main.cpp:(.text+0x13): undefined reference to `add(int, int)'
collect2: error: ld returned 1 exit status
以及
➜ g++ main.cpp other.cc
/usr/bin/ld: /tmp/cc0XOMFk.o: in function `add(int, int)':
other.cc:(.text+0x0): multiple definition of `add(int, int)'; /tmp/ccWGLjai.o:main.cpp:(.text+0x31): first defined here
collect2: error: ld returned 1 exit status
现在已经可以更为深刻的理解这几个错误的意思了。