c++ 学习笔记1——理解 gcc 编译和链接

2020-11-12  本文已影响0人  落撒

声明和定义

首先来说两个概念,声明和定义(此处仅针对函数)。

声明,可以简单理解为说我们有这样一个函数;

定义,可以简单理解为这个函数要作什么,是怎么实现的。

先来一个小例子,创建 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

现在已经可以更为深刻的理解这几个错误的意思了。

上一篇下一篇

猜你喜欢

热点阅读