g++编译入门
本文为您介绍g++的编译用法;通过从最简单的单文件编译,到多文件编译,再到动态库、静态库的编译及使用;
例子都经过实际编译并运行,可谓全网最良心之作呐,放心拷贝粘贴学习!
1、g++的编译单文件
c++的程序编译的过程如下:
1.预编译
->2.编译
->3.汇编
->4.链接
以个Test.cpp
为例说明整个编译过程
Test.cpp
文件如下
#include <iostream>
#define MAX_NUM 10
// 定义宏
int main() {// 输出Hello worldstd::cout<<"Hello world!"<<std::endl;// 输出Max+nint n = 100;std::cout<<"MAX_NUM+n:"<<MAX_NUM+n<<std::endl;return 0;
}
1)预编译
宏的替换,还有注释的消除,还有找到相关的库文件
g++ -E Test.cpp > Test.i
只做预处理,生成一个Test.i
的文件,该文件多了很多内容,我们省略前面1万+行
,只看最下面对我们的写的源码做了什么处理;
Test.i
内容如下
int main() {std::cout<<"Hello world!"<<std::endl;int n = 100;std::cout<<"MAX_NUM+n:"<<10 +n<<std::endl;return 0;
}
这就是预处理做的我们能理解的事,其他的有空自己再慢慢深挖。
2)编译
将预处理后的文件转换为汇编文件,里面为汇编指令
g++ -S Test.i
当然也可以直接将源代码cpp处理 g++ -S Test.cpp
,两者编译后的输出结果一模一样
编译后,将得到一个Test.s
的文件,前面0行开始截取部分内容如下:
.file "Test.cpp".text.section .rodata.type _ZStL19piecewise_construct, @object.size _ZStL19piecewise_construct, 1
_ZStL19piecewise_construct:.zero 1.local _ZStL8__ioinit.comm _ZStL8__ioinit,1,1
.LC0:.string "Hello world!"
.LC1:.string "MAX_NUM+n:".text.globl main.type main, @function
main:
.LFB1522:.cfi_startprocendbr64pushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6subq $16, %rspleaq .LC0(%rip), %rsileaq _ZSt4cout(%rip), %rdicall _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@PLTmovq %rax, %rdxmovq _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GOTPCREL(%rip), %raxmovq %rax, %rsimovq %rdx, %rdicall _ZNSolsEPFRSoS_E@PLTmovl $100, -4(%rbp)leaq .LC1(%rip), %rsileaq _ZSt4cout(%rip), %rdicall _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@PLTmovq %rax, %rdxmovl -4(%rbp), %eaxaddl $10, %eaxmovl %eax, %esimovq %rdx, %rdicall _ZNSolsEi@PLTmovq %rax, %rdxmovq _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GOTPCREL(%rip), %raxmovq %rax, %rsimovq %rdx, %rdicall _ZNSolsEPFRSoS_E@PLTmovl $0, %eaxleave.cfi_def_cfa 7, 8ret.cfi_endproc
...
显然,上面的是机器无关的汇编指令
3)汇编
将汇编文件转为目标文件
g++ -c Test.s # 当然也可以直接使用`g++ -c Test.cpp`从源文件直接输出
汇编执行后,将生成Test.o
的目标文件(二进制文件)
4)链接
将目标文件和库文件整合为一个可执行文件
g++ Test.o -L usr/include/iostream
-L后为库文件目录
命令执行后,将生成一个a.out
,我们可以运行一下该文件,前面的代码正常运行,如下所示:
$ ./a.out
Hello world!
MAX_NUM+n:110
使用-o可以为可执行文件命名
g++ Test.o -o Test -L usr/include/iostream
# 同理可以直接对源码输出`g++ Test.cpp -o Test -L usr/include/iosream`
执行后,将输出Test
的可执行文件
5)一步到位编译
# 直接编译
g++ Test.cpp -o Test
# 执行结果
./Test
Hello world!
MAX_NUM+n:110
直接输出Test
可执行文件
g++常用的参数
-c 生成.o目标文件
-o可执行文件命名
-shared 指定生成动态链接库
-static 指定生成静态链接库
-L 要链接的库所在目录
-l 指定链接时需要的库,隐含命名规则,即自动在前加lib,在后加.a或.so确定库文件名
-std 设置C语言版本 g++ -std=c11 Test.cpp -o Test
2、g++编译多文件
前面我们知道如何编译一个c++代码,如果多个c++文件组织在一起如何用g++编译呢?
我们基于前面测试项目,再创建一个文件HelloToolsClass.cpp
,现在我们有两个文件了:Test.cpp
及HelloToolsClass.cpp
其中HelloToolsClass.cpp
为完整的类声明及定义为一体文件,内如如下;
class HelloTools{public:void print(int a, int b);int add(int a, int b);
};void HelloTools::print(int a, int b){std::cout<<"a+b="<<a<<"+"<<b<<"="<<a+b<<std::endl;
}int HelloTools::add(int a, int b){int c = 0;c = a+b;print(a , b);return c;
}
而Test.cpp
改造成如下所示
#include <iostream>
// 定义宏
#define MAX_NUM 10// 声明类
class HelloTools{public:void add(int a, int b);
};int main() {// 输出Hello worldstd::cout<<"Hello world!"<<std::endl;// 输出Max+nint n = 100;std::cout<<"MAX_NUM+n:"<<MAX_NUM+n<<std::endl;std::cout<<"================================="<<std::endl;// create objHelloTools hello;// call functionhello.add(200,300);return 0;
}
现在Test.cpp中调用了HelloToolsClass.cpp,如何编译他们呢?
如果我们像前面一样,一步到位直接编译Test.cpp
它能自动找到我们需要引用的类吗?往下看
$ g++ Test.cpp -o Test
/usr/bin/ld: /tmp/ccVmXnOM.o: in function `main':
Test.cpp:(.text+0xbf): undefined reference to `HelloTools::add(int, int)'
collect2: error: ld returned 1 exit status
显然,出错了,不是我们要的结果,提示HelloTools
未定义
这里需要注意,一般我们会将类的声明与定义拆开两个文件,声明放在
*.h
,函数的定义放在*.cpp
中,这样在需要引用其他类或方法时,只需要使用#include "*.h"
引用头文件即可;而*cpp
文件是不允许用#include
引用的;
那如何直接使用一个声明与定义的都在cpp的类呢,可以像例子那样,直接在需要引用的地方,再次声明一下,其作用就等同#include
做的工作
正确的姿势如下:
g++ Test.cpp HelloToolClass.cpp -o Test
运行测试一下结果
./Test
Hello world!
MAX_NUM+n:110
=================================
a+b=200+300=500
成功了!
这里,我们得到一个结论:一次编译环境下,源文件之间的函数一般情况下是可以互相调用的,前提是要声明以及不限制它的作用域同
3、g++编译复杂多层次文件
我们先对前面的项目做个小改动,把HelloToolsClass.cpp
拆分成*.h
及*.cpp
,如下所示:
// HelloToolsClass.h
#include <iostream>class HelloTools{public:void print(int a, int b);int add(int a, int b);
};
// HelloToolClass.cpp
#include <iostream>
#include "HelloToolClass.h"void HelloTools::print(int a, int b){std::cout<<"a+b="<<a<<"+"<<b<<"="<<a+b<<std::endl;
}int HelloTools::add(int a, int b){int c = 0;c = a+b;print(a , b);return c;
}
Test.cpp
则改造成如下所示
// Test.cpp
#include <iostream>
#include "HelloToolClass.h"
// 定义宏
#define MAX_NUM 10int main() {// 输出Hello worldstd::cout<<"Hello world!"<<std::endl;// 输出Max+nint n = 100;std::cout<<"MAX_NUM+n:"<<MAX_NUM+n<<std::endl;std::cout<<"================================="<<std::endl;// create objHelloTools hello;// call functionhello.add(200,300);return 0;
}
编译,并执行
# 编译
g++ Test.cpp TestHelloToolClass.cpp -o Test
# 执行结果
Hello world!
MAX_NUM+n:110
=================================
a+b=200+300=500
执行成功
显然,分离后的
*.h
文件,并没有直接参与我们的编译命令,编译命令及结果都与前面的简单的多文件编译一样。但是*.h
确实必须参与编译
这里再次说明,有*.h
头文件在开发复用时,非常方便,否则,我们得一个个声明我们需要引用的外部方法或函数
我们继续改造前面的项目,使之更加结构化、项目化、工程化。首先我们构造出如下项目结构
- mutilFilesDemo- include // 头文件目录- HelloTools.h- Prints.h- src // 源码目录- module // 源码模块- Prints.cpp // Prints类- HelloTools.cpp // HelloTools类- main.cpp // main类
首先我们把前面的Test.cpp
重命名为 main.cpp
看起来高大上一点;再修改内容如下:
// main.cpp
#include <iostream>
#include "include/HelloTools.h"
// 定义宏
#define MAX_NUM 10int main() {// 输出Hello worldstd::cout<<"Hello world!"<<std::endl;// 输出Max+nint n = 100;std::cout<<"MAX_NUM+n:"<<MAX_NUM+n<<std::endl;std::cout<<"================================="<<std::endl;// create objHelloTools hello;// call functionhello.add(200,300);return 0;
}
接着我们继续在相应目录下创建两个新文件,其内容如下:
Prints.h
内容如下:
// Prints.h
class Prints{public:void printOneLine(std::string s);
};
Prints.cpp
内容如下:
// Prints.cpp
#include <iostream>
#include "../../include/Prints.h"void Prints::printOneLine(std::string s){std::cout<<s<<std::endl;
}
最后把原来的HelloTools.cpp
拆分成两个文件*.h
及*.cpp
HelloTools.h
内容如下:
// HelloTools.h
class HelloTools{public:void print(int a, int b);int add(int a, int b);
};
HelloTools.cpp
内容如下:
// HelloTools.cpp
#include <iostream>
#include "../include/HelloTools.h"
#include "../include/Prints.h"void HelloTools::print(int a, int b){std::cout<<"a+b="<<a<<"+"<<b<<"="<<a+b<<std::endl;// 调用Prints类Prints p;p.printOneLine("call Prints Class:PrintOneLine.");}int HelloTools::add(int a, int b){int c = 0;c = a+b;print(a , b);return c;
}
完整的目录解构见前面开始给出的解构
代码编码完成后开始编译,如何编译呢?我们参考前面的方法进行编译!
g++ main.cpp ./src/HelloTools.cpp ./src/modules/Prints.cpp -o main
编译成功输出main
,执行后
$ ./main
Hello world!
MAX_NUM+n:110
=================================
a+b=200+300=500
call Prints Class:PrintOneLine.
成功!
4、g++编译为静态编库(*.a
)及调用
静态库编译(lib*.a
)
我们创建两个文件ToolLibs.h
及ToolLibs.cpp
,内容如下
ToolLibs.h
// ToolLibs.h
class ToolLibs{public:int add(int a, int b);
};
ToolLibs.cpp
// ToolLibs.cpp
#include <iostream>
#include "ToolLibs.h"int ToolLibs::add(int a, int b){std::cout<<"使用库-add(a,b)"<<std::endl;std::cout<<"结果为:a+b="<<a+b<<std::endl;return a+b;
}
执行编译,生成目标文件*.o
# 生成目标文件
$ g++ -c ToolLibs.cpp
# 查看生成的结果*.o
$ ls
ToolLibs.cpp ToolLibs.h ToolLibs.o
执行生成静态库文件
注意库文件应符合linux命名规则,
lib*.a
为静态库,lib*.so
为动态库
# 创建静态库
$ ar crv libToolLibs.a ToolLibs.o
a - ToolLibs.o
# 查看结果
$ ls
libToolLibs.a ToolLibs.cpp ToolLibs.h ToolLibs.o
上面生成了libToolLibs.a
,而文件*.o
则可删除丢弃
ar命令用于 创建建、修改库,也可以从库中提出单个模块;常用的命令参数如下:
ar crv <创建的静态库名> <目标文件名1> <目标文件名n>
可合并多个目标库文件(*.o)
c - 创建库文件
r - 将文件插入库文件中
v - 程序执行时显示详细的信息
t - 显示库文件中所包含的文件
如何使用静态库?
我们还是已前面的例子为例,介绍如何使用这个库
我们把这个库的源代码copy到前面的项目解构下方便介绍,新的目录解构如下:
- mutilFilesDemo- include // 头文件目录- HelloTools.h- Prints.h- libs // 库子项目目录- ToolLibs.h- ToolLibs.cpp- libToolLibs.a // 编译好的静态库文件- src // 源码目录- module // 源码模块- Prints.cpp // Prints类- HelloTools.cpp // HelloTools类- main.cpp // main类
使用一个外部的资源,必须先声明,我们修改一下前面的HelloTools.cpp
,修改后的文件如下:
// HelloTools.cpp
#include <iostream>
#include "../include/HelloTools.h"
#include "../include/Prints.h"
// 开发的静态库头文件
#include "../libs/ToolLibs.h"void HelloTools::print(int a, int b){std::cout<<"a+b="<<a<<"+"<<b<<"="<<a+b<<std::endl;// 调用Prints类Prints p;p.printOneLine("call Prints Class:PrintOneLine.");}int HelloTools::add(int a, int b){int c = 0;ToolLibs tool;c = tool.add(a,b);return c;
}
其他不做任何修改,我们参考前面的方式编译一下
g++ main.cpp ./src/HelloTools.cpp ./src/modules/Prints.cpp -o main
报如下错误:
/usr/bin/ld: /tmp/cc6RvAl9.o: in function `HelloTools::add(int, int)':
HelloTools.cpp:(.text+0x17c): undefined reference to `ToolLibs::add(int, int)'
collect2: error: ld returned 1 exit status
提示,未定义ToolLibs:add
,显然我们只引入一个库的头文件是不够的,我们需要想办法把库的内容真正引进来,怎么办呢?
- 方案1,源码直接编译
因为我们有源码(
开源真是好
),我们可以不顾任何道德,直接编译
显然这种方式必定可行的,效果就跟前面我们的多文件编译一样
# 直接一块编译libs库源码
g++ main.cpp ./src/HelloTools.cpp ./src/modules/Prints.cpp ./libs/ToolLibs.cpp -o main
然而,这样就不需要创造静态库这东西了;于是的核心主题终于出场
- 方案2,编译时链接到静态库
g++ main.cpp ./src/HelloTools.cpp ./src/modules/Prints.cpp -L ./libs -l ToolLibs -o main
g++ 参数含义
L - 需要链接的库的目录地址
l - 需要链接的库名称,注意:库名需要吧去除前缀'lib'及后缀'.a'
编译成功,执行测试一下:
$ ./main
Hello world!
MAX_NUM+n:110
=================================
使用库-add(a,b)
结果为:a+b=500
显示,这种方式可以让我们达到构件级的程序复用,实现核心库与应用代码的解耦,而不是仅仅是代码级的复用(复制/粘贴代码);而且如果我们想保护自己的库,或者不希望公开源码,这也是一种很好的方式
5、g++编译为动态编库(*.so
)及调用
动态库编译(lib*.so
)
接着前面的项目,在libs
目录下,执行如下命令
# 编译为动态库
$ g++ -fPIC -shared ToolLibs.cpp -o libToolLibs.so
# 查看输出
$ ls
libToolLibs.a libToolLibs.so ToolLibs.cpp ToolLibs.h ToolLibs.o
注意输出的动态库的命名规范,务必满足
lib*.so
规则
我们看到,输出了我们期待的*.so
动态库
如何链接到动态库呢?
我们先把前面生成的静态库删除,同时把前面出来的可执行main
也更改一下名
# 改名
cd mutilFilesDemo
mv main main2
# 删除静态库
cd libs
rm libToolLibs.so toolLibs.o
注意,如果静态库与动态库同名(仅仅后缀
.a
与.so
不一样),则链接时,优先链接到动态库*.so
;为避免分析干扰,这里删除了先前的静态库
编译时的链接方式跟我们前面的基本一致,如下命令:
g++ main.cpp ./src/HelloTools.cpp ./src/modules/Prints.cpp -L ./libs -l ToolLibs -o main
注意:编译时,无论链接动态库还是静态库,我们需要本地必须存在这样一个库,否则将会编译失败
编译成功,输出main
,我们查看对比一下原来main2
的发现采用动态库链接编译的结果会更小一点
我们执行一下编译的结果
$ ./main
./main: error while loading shared libraries: libToolLibs.so: cannot open shared object file: No such file or directory
发现报错,无法执行并提示缺少libToolsLibs.so
库,我们把前面编译好的libToolsLibs.so
复制到/usr/lib/
目录下,再次执行/main
$ ./main
Hello world!
MAX_NUM+n:110
=================================
使用库-add(a,b)
结果为:a+b=500
执行成功!
由此可见,静态库链接编译的会吧整个静态库的内容放在编译结果中,而显然动态库链接编译的结果并不会;这就是导致静态编译的结果更大的原因;
有上可知,动态链接在运行时,本地必须存在该动态库资源,且存放系统指定的目录下/usr/lib
,当然这个目录是可配置的,我们可以查看一下哪些放在哪些目录下,可以被自动定位到
cat /etc/ld.so.conf
include /etc/ld.so.conf.d/*.conf
/etc/ld.so.conf
及 /etc/ld.so.conf.d/*.conf
文件都是配置的动态库加载路径配置文件,可添加一个so自动加载配置文件实现动态库自动加载
当然,我们还有临时的解决方案,临时设置某个动态库路径
$ cd mutilFilesDemo
# 添加路径
$ LD_LIBRARY_PATH=LD_LIBRARY_PATH:./libs
# 执行
$ ./main
Hello world!
MAX_NUM+n:110
=================================
使用库-add(a,b)
结果为:a+b=500
当然这是临时方案,也只能在当面session中有效,且上面用的相对路径,只能在当前目录下有效;如需要长久一点有效(本次用户登录内),则需要设置在~/.bashrc
中,并使用srouce命令生效;永久有效可在设置到/etc/profile
;更优雅的方案,建议还是用前面/etc/ld.so.conf.d/*.conf
的方案
C语言版本的发展历程简介
如何查看编译支持C语言标准?
先查看一下gcc
的版本
$ gcc --version
gcc (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
版本为9.4.0
,可以去官网查看9.4.0
支持那些C的标准,或者直接用下面的命令
$ man gcc
-std=Determine the language standard. This option is currently only supported when compiling C or C++.The compiler can accept several base standards, such as c90 or c++98, and GNU dialects of those standards, such as gnu90 or gnu++98. When a base standard is specified, the compileraccepts all programs following that standard plus those using GNU extensions that do not contradict it. For example, -std=c90 turns off certain features of GCC that are incompatiblewith ISO C90, such as the "asm" and "typeof" keywords, but not other GNU extensions that do not have a meaning in ISO C90, such as omitting the middle term of a "?:" expression. On theother hand, when a GNU dialect of a standard is specified, all features supported by the compiler are enabled, even when those features change the meaning of the base standard. As aresult, some strict-conforming programs may be rejected. The particular standard is used by -Wpedantic to identify which features are GNU extensions given that version of the standard.For example -std=gnu90 -Wpedantic warns about C++ style // comments, while -std=gnu99 -Wpedantic does not.A value for this option must be provided; possible values arec90c89iso9899:1990Support all ISO C90 programs (certain GNU extensions that conflict with ISO C90 are disabled). Same as -ansi for C code.iso9899:199409ISO C90 as modified in amendment 1.c99c9xiso9899:1999iso9899:199xISO C99. This standard is substantially completely supported, modulo bugs and floating-point issues (mainly but not entirely relating to optional C99 features from Annexes F and G).See <http://gcc.gnu.org/c99status.html> for more information. The names c9x and iso9899:199x are deprecated.c11c1xiso9899:2011ISO C11, the 2011 revision of the ISO C standard. This standard is substantially completely supported, modulo bugs, floating-point issues (mainly but not entirely relating tooptional C11 features from Annexes F and G) and the optional Annexes K (Bounds-checking interfaces) and L (Analyzability). The name c1x is deprecated.c17c18iso9899:2017iso9899:2018ISO C17, the 2017 revision of the ISO C standard (published in 2018). This standard is same as C11 except for corrections of defects (all of which are also applied with -std=c11)and a new value of "__STDC_VERSION__", and so is supported to the same extent as C11.c2x The next version of the ISO C standard, still under development. The support for this version is experimental and incomplete.
上面存在C11
C17
C18
说明,都支持这些C语言版本
C语言版本的发展历程
C89之前,并没有统一C标准规范
-
C89
考虑到标准化的重要,ANSI(American National Standards Institute)制定了第一个 C 标准,在1989年被正式采用(American National Standard X3.159-1989),故称为 C89,也称为 ANSI C。
该标准随后被 ISO 采纳,成为国际标准(ISO/IEC 9899:1990)。 -
C95
这是对 C89 的一个修订和扩充,称为“C89 with Amendment 1”或 C95,严格说来并不是一个真正的标准。 -
C99
1999年,在做了一些必要的修正和完善后,ISO 发布了新的 C 语言标准,命名为 ISO/IEC 9899:1999,简称“C99” -
C11
2007 年,C语言标准委员会又重新开始修订C语言,到了2011 年
正式发布了 ISO/IEC 9899:2011,简称为 C11 标准。
C11 标准新引入的特征尽管没 C99 相对 C90 引入的那么多,但是这些也都十分有用,比如:字节对齐说明符、泛型机制(generic selection)、对多线程的支持、静态断言、原子操作以及对 Unicode 的支持。 -
C17
C17(也被称为为 C18)是于2018年6月发布
的 ISO/IEC 9899:2018 的非正式名称,也是目前(截止到2020年6月)为止最新的 C语言编程标准,被用来替代 C11 标准。
C17 没有引入新的语言特性,只对 C11 进行了补充和修正。 -
C2x
C 2x下一个版本的 C 标准,预计将于2022年12月1日
完成。