gcc、g++编译的使用与区别【含vscode中简单的GDB调试】

1. 了解编译过程

  • 步骤一:预处理(预编译):编译处理宏定义等宏命令———生成后缀为“.i”的文件
  • 步骤二:编译:将预处理后的文件转换成汇编语言———生成后缀为“.s”的文件
  • 步骤三:汇编:由汇编生成的文件翻译为二进制目标文件———生成后缀为“.o”的文件
  • 步骤四:连接:多个目标文件(二进制)结合库函数等综合成的能直接独立执行的执行文件———生成后缀为“.out”的文件

1.1 例子:

使用 g++ 编译C++源代码的时候,我们可使用以下命令 即可完成编译C++源代码文件,并且直接产生可执行的二进制文件

# 编译test.cpp 文件,在 Linux 下,默认产生名为 a.out 的二进制可执行文件
g++ test.cpp

实际上,上面的一步编译指令包含了以下几个过程

第一步:预处理 Pre-processing,生成.i 文件

# -E 选项指示编译器仅对输入文件进行预编译
g++ -E test.cpp -o testr.i

第二步:编译-Compiling,生成.s 文件

# -S 编译选项告诉 g++ 在为 c++ 代码产生了汇编语言文件后停止编译
# g++ 产生的汇编语言文件的缺省扩展名是 .s
g++ -S test.i -o test.s

第三步:汇编-Assembing,生成.o 文件

# -c 选项告诉 g++ 仅把源代码编译为机器语言的目标代码
# 缺省时 g++ 建立的目标代码文件有一个 .o 的扩展名
g++ -c test.s -o test.o

第四步:链接-Lingking,生成 bin 二进制文件

# -o 编译选项来为将产生的可执行文件指定文件名,如果不使用-o参数,在Linux下默认输出名为 a.out 的可执行文件
g++ test.o -o test
  • ps:VSCode 是通过调用 GCC 编译器来实现 C/C++的编译工作的。

2. 了解动态库与静态库

2.1动态库与静态库的区别

  • 根据链接时期的不同,库又有静态库和动态库之分。静态库是在链接阶段被链接的,所以生成的可执行文件就不受库的影响,即使库被删除,程序依然可以成功运行。而动态库是在程序执行的时候被链接的。程序执行完,库仍需保留在系统上,以供程序运行时调用。
  • 链接静态库从某种意义上来说是一种复制粘贴,被链接后库就直接嵌入可执行程序中了,这样系统空间有很大的浪费,而且一旦发现系统中有bug,就必须一一把链接该库的程序找出来,然后重新编译,十分麻烦。而动态库刚好弥补了这个缺陷,因为动态库是在程序运行时被链接的,所以磁盘上只需保留一份副本,一次节约了空间,如果发现bug或者是要升级,只要用新的库把原来的替换掉就可以了。
  • 静态库是不是一无是处了呢?非也。如果代码在其他系统上运行,且没有相应的库时,解决办法就是使用静态库。而且由于动态库是在程序运行的时候被链接,因此动态库的运行速度比较慢。
  • 静态库的扩展名一般为“.a”或“.lib”;动态库的扩展名一般为“.so”或“.dll”。

3. gcc与g++的一些区别:

  • gcc无法进行库文件的连接;而g++则能完整编译出可执行文件。(实质上,g++从步骤1-步骤3均是调用gcc完成,步骤4连接则由自己完成)

  • 对于 .c和.cpp文件,使用gcc编译会分别当做c和cpp文件编译(cpp的语法规则比c的更强一些)

  • 对于 .c和.cpp文件,使用g++则统一当做cpp文件编译

  • 在编译c++代码时,g++会调用gcc,所以两者是等价的,但是因为gcc命令不能自动和C++程序使用的库链接,所以通常用g++来完成链接,为了统一起见,干脆编译/链接都用g++,这就给人一种错觉,好像cpp程序只能用g++似的。

  • gcc在编译.c文件时,可使用的预定义宏是比较少的,很多都是未定义的。 gcc在编译cpp文件时、以及g++在编译c文件和cpp文件时(这时候gcc和g++调用的都是cpp文件的编译器),会加入一些额外的宏,这些宏如下:

    #define __GXX_WEAK__ 1
    #define __cplusplus 1
    #define __DEPRECATED 1
    #define __GNUG__ 4
    #define __EXCEPTIONS 1
    #define __private_extern__ extern
    

    所以,既然使用g++编译时,会统一当做cpp文件编译,那么为了对c编译的支持,利用默认的宏定义__cplusplus,在c代码中添加如下内容。

    #ifdef __cplusplus 
    extern "C" {
          
           
    #endif
    
    //一段c代码
    
    #ifdef __cplusplus 
    } 
    #endif 
    

4. gcc使用

4.1 语法

gcc [options] file

4.2 常用编译参数

编译参数 详细说明
-o Object的简写,指定编译的⽬标文件名,否则会⽣成的⽬标⽂件名是a.out;eg: gcc main.c -o main
-S 把源文件编译成汇编代码
-E 只执行预处理
-include 包含头文件,功能如同在源码的语句。eg:#include <xxx.h>
-I (大写i) include简写,指定程序包含头文件的路径,一般用于指定第三方库的头文件。默认在/usr/include,没有就要用 -I 参数指定了
-L 编译时,用于指定程序第三方库的查找路径。
-l 链接时,指定程序需要进行链接的库。注:一般库文件名是libxxx.so,-I指定xxx即可。如-Ixxx在/lib和/usr/lib和usr/local/lib里面的库直接用-l参数就能链接,如果库文件没有放在系统库目录中,需要使用-L 参数(大写l)指定库文件所在目录
-rpath 程序执⾏需要指定动态库的路径,但是可以⽤-rpath参数在编译时指定程序运⾏时需要加载的库的路径。
-D 程序编译阶段可以定义一些宏,该方法可以让程序有选择性的运行代码。
-0n 这是程序的优化等级。n的范围是0-3。n越大优化等级越高,程序运行的越快。否则越慢,n==0时是关闭优化。利于程序的调试,一般程序调试阶段会关闭优化等级,发布程序会把优化等级设为-O2。-O0:不进行优化处理。-O 或 -O1:优化生成代码。-O2:进一步优化。-O3 比 -O2 更进一步优化,包括 inline 函数。
-g 打印程序的调试信息,如果需要使⽤gdb⼯具进⾏调试程序,程序编译的时候,需要加上该参数。
-share 编译的时候尽量使用动态库。(除非只有静态库,没有动态库)
-static 禁止使用动态库,编译的时候只加载静态库,这会导致执行件很大。
-w 不生成任何的警告信息。
-Wall 生成所有的警告信息。
-fpic 使输出的对象模块可重定位地址方式行成的。
-shared 把对应的源文件形成对应的动态链接库。
-std=c++11 设置编译标准

4.3 例子

只执行预处理,输出 hello.i 源文件

gcc -E hello.c -o hello.i

只执行预处理和编译,输出 hello.s 汇编文件

gcc -S hello.c

也可以由 hello.i 文件生成 hello.s 汇编文件

gcc -S hello.i -o hello.s

只执行预处理、编译和汇编,输出 hello.o 目标文件

gcc -c hello.c

也可以由 hello.i或hello.s生成目标文件hello.o

gcc -c hello.i -o hello.o
gcc -c hello.s -o hello.o

由目标文件hello.o链接成可执行文件 hello

gcc hello.o -o hello

使用-std设置编译标准

# 使用 c++11 标准编译 test.cpp
g++ -std=c++11 test.cpp

-D 定义宏,为了演示宏的作用,创建源码文件 gcc_02_test/test.cpp ,并添加以下C++源代码

#include <stdio.h>

int main()
{
    
     
    // 根据是否存在 DEBUG 进行逻辑处理
    #ifdef DEBUG
        printf("DEBUG LOG\n");
    #endif
        printf("in\n");
    return 0;
}

使用 g++ -DDEBUG test.cpp 编译的同时定义DEBUG宏,执行编译后的可执行文件可以看到 “DEBUG LOG” 被输出。原因是我们使用-DDEBUG 参数定义 DEBUG 宏,在执行程序的时候,程序检测到了 DEBUG 宏的存在,并执行了对应的逻辑。

4.4 生成、调用静态库

test.c

int add(int a,int b)
{
    
    
	retrun a+b;
}

test.h

#ifndef _TEST_H_
#define _TEST_H_
extern int add(int a,int b);

main.c

#include<stduo.h>
#include<test.h>
int main()
{
    
    
int a,b;
	printf("please input a and b\n");
	scanf("%d %d",&a,&b);
	printf("The add:%d\n",add(a,b));
}

在此例中,test.c用于编译生成静态库libtest.a,test.h为libtest.a对应的头文件。

生成test.o目标文件

gcc -c test.c -o test.o

使用ar将test.o打包成libtest.a静态库

ar rcs -o libtest.a test.o

生成libtest.a静态库后,可以使用命令查看libtest.a文件中包含哪些文件

ar t libtest.a

知识点补充:

Linux 中 ar命令:
-功能:创建静态库.a文件 -指令参数
-d  删除静态库中的成员文件。
-m  变更成员文件在静态库中的次序。
-p  显示静态库中的成员文件内容。
-q  将文件附加在静态库的末端。
-r  将文件插入静态库中。
-t  显示静态库中所包含的文件。
-x  静态库中取出成员文件。

编译 main.c, 并链接静态库 libtest.a,生成的可执行文件名为app_static

gcc -o app_static main.c libtest.a

可以加参数指定路径和库的名称

  • 链接时,-l参数后不加空格指定所需要链接的库名,这里库名是libtest.a,但是只需要给出-ltest即可,ld会以libtest作为库的实际名字。
  • 链接时,-L 指定库的搜索路径,.表示的当前路径
gcc -o app_static main.c -L. -ltest 

运行app_static 【静态库编译得到的文件可以直接执行,而动态库则不行

$ ./app_static 

查看文件描述

$ file app_static 
hello: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=b72236c2211dd8f0c3003bc02ad5e70bb2354e8c, for GNU/Linux 3.2.0, not stripped

file:用来识别文件类型,也可用来辨别一些文件的编码格式。它是通过查看文件的头部信息来获取文件类型。

4.5 生成、调用共享库\动态库

生成test.o目标文件,-fPIC 作用于编译阶段,告诉编译器产生与位置无关代码(Position-Independent Code),则产生的代码中,没有绝对地址,全部使用相对地址,故而代码可以被加载器加载到内存的任意位置,都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的。

由于动态库可以被多个进程共享加载,所以需要使用-fPIC选项生成位置无关的代码

gcc -c -o test.o -fPIC test.c

使用-shared参数生成动态库

gcc -shared -o libmyshare.so test.o

上述两个命令可以连在一起

gcc -shared -fPIC -o libmyshare.so test.c

编译main.c,使用libmyshare.so动态库,命令如下

gcc -o app_share main.c -L. -lmyshare

查看app_share使用动态库,

ldd app_share

ldd:用来打印或者查看程序运行所需的共享库(访问共享对象依赖关系),常用来解决程序因缺少某个库文件而不能运行的一些问题。

如果libmyshare.so无法找到,直接执行./app_share就会出现错误。解决方法:首先使用export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH将动态库的目录加入LD_LIBRARY_PATH变量中。再次运行ldd app_share

另一种编译main.c,并链接libmyshare.so的方式如下(该方式通过./libmyshare.so直接指定使用当前目录下的libmyshare.so文件)

gcc -o app_share main.c ./libmyshare.so

直接运行会报错,执行如下指令

LD_LIBRARY_PATH=动态库路径 可执行文件名路径

例如:

LD_LIBRARY_PATH=src ./app_share

5. 添加共享库路径的三种方式

5.1 方式一:设置环境变量 LD_LIBRARY_PATH

LD_LIBRARY_PATH是Linux环境变量名,该环境变量主要用于指定查找共享库(动态链接库)时,除了默认路径之外的其他路径。

export LD_LIBRARY_PATH=$(pwd)

将 libmyshare.so 所在的当前目录添加到 LD_LIBRARY_PATH 变量,再次执行 app_share

5.2 方式二:使用 rpath 将共享库位置嵌入到程序

gcc hello.c -L. -lfoo -Wl,-rpath=`pwd` -o hello

rpath 即 run path,是种可以将共享库位置嵌入程序中的方法,从而不用依赖于默认位置和环境变量。

这里在链接时使用 -Wl,-rpath=xxxx 选项,-Wl 会发送以逗号分隔的选项到链接器,注意逗号分隔符后面没有空格。pwd:打印当前目录

这种方式要求共享库必须有一个固定的安装路径,欠缺灵活性,不过如果设置了 LD_LIBRARY_PATH,程序加载时也是会到相应路径寻找共享库的。

5.3 方式三:将 libmyshare.so 共享库添加到系统路径

sudo cp libmyshare.so /usr/lib/

执行程序,如果程序仍然运行失败,请尝试执行 ldconfig 命令更新共享库的缓存列表。

此时,再次查看程序的共享库依赖

$ ldd app_share
linux-vdso.so.1 (0x00007ffecfbb1000)
libmyshare.so => /lib/libmyshare.so (0x00007f3f3f1ad000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3f3efbb000)
/lib64/ld-linux-x86-64.so.2 (0x00007f3f3f1d6000)

可以看到 libmyshare.so已经被发现了,其中 /lib 是 /usr/lib 目录的软链接。

6. g++使用

  • 用法与gcc基本无异,不同的是g++编译过程包含了编译和链接。

3. g++重要编译参数

3.1. 编译带调试信息的可执行文件

-g 选项告诉GCC产生能被 GNU调试器(DGB) 使用的调试信息,以调试程序

# 产生带调试信息的可执行文件test
g++ -g test.cpp -o test

3.2. 优化源代码

所谓优化,是指如省略代码中从来未使用过的变量、直接常量表达式用结果替代等操作,这些操作会缩减目标文件所含的代码,提高最终生成的可执行文件的运行效率。

O 参数告诉 g++ 对源代码进行基本优化。这些优化在大多数情况下都使程序执行得更快,常用优化级别如下:

-O: 同时减少代码的长度和执行时间,其效果等价于 -O1

-O0: 表示不做优化

-O1: 表示默认优化

-O2: 告诉 g++ 产生尽可能小和尽可能快的代码。除了完成-O1 的优化之外,还进行一些额外的调整工作,如指令调整等

-O3: 包括循环展开和其他一些与处理性相关的优化工作,选项将使编译的速度比 -O 慢,但通常产生的代码执行速度会更快。

# 使用 -O2 优化源代码,并输出可执行文件。
g++ -O2 test.cpp

3.3 使用time查看可执行文件运行时间

在这里插入图片描述

7. 在windows系统下使用GDB工具调试C++单文件程序

编译环境设置:windows安装minggw支持linux程序编译

  1. 创建c++文件

  2. 终端输入指令:g++ -g .\main.cpp

  3. 点击左侧的调试按钮:在这里插入图片描述

  4. 点击:在这里插入图片描述

  5. 点击:在这里插入图片描述

  6. 进入调试界面:快捷键F9是设置短点,F10是设置下一步,

  7. F5是设置运行,或者按下设置运行:在这里插入图片描述

  8. launch.json中最重要的信息:【第一个生成可调试的可执行文件,和main.cpp同一个路径下,第二个在编译前指定要执行task.exe任务,在.vscode路径下】在这里插入图片描述

8. 在windows系统下使用GDB工具调试C++多文件程序

采用上述单文件的操作会报错,需要手动写launch.json和task.json

  1. 修改launch.json文件:在这里插入图片描述
  2. 注释掉preLaunchTask

参考博文:

树莓派——Linux共享库、静态库、动态库详解
静态库和动态库的区别
gcc与g++

猜你喜欢

转载自blog.csdn.net/NRWHF/article/details/129773729