C++编译之(1)-g++单/多文件/库的编译及C标准的发展历程

g++编译入门

本文为您介绍g++的编译用法;通过从最简单的单文件编译,到多文件编译,再到动态库、静态库的编译及使用;
例子都经过实际编译并运行,可谓全网最良心之作呐,放心拷贝粘贴学习!

1、g++的编译单文件

c++的程序编译的过程如下:

1.预编译->2.编译->3.汇编->4.链接

以个Test.cpp为例说明整个编译过程

Test.cpp 文件如下

#include <iostream>
#define MAX_NUM 10
// 定义宏
int main() {
    
    
    // 输出Hello world
    std::cout<<"Hello world!"<<std::endl;
    // 输出Max+n
    int 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行开始截取部分内容如下:

扫描二维码关注公众号,回复: 14971435 查看本文章
	.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_startproc
	endbr64
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	subq	$16, %rsp
	leaq	.LC0(%rip), %rsi
	leaq	_ZSt4cout(%rip), %rdi
	call	_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@PLT
	movq	%rax, %rdx
	movq	_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GOTPCREL(%rip), %rax
	movq	%rax, %rsi
	movq	%rdx, %rdi
	call	_ZNSolsEPFRSoS_E@PLT
	movl	$100, -4(%rbp)
	leaq	.LC1(%rip), %rsi
	leaq	_ZSt4cout(%rip), %rdi
	call	_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@PLT
	movq	%rax, %rdx
	movl	-4(%rbp), %eax
	addl	$10, %eax
	movl	%eax, %esi
	movq	%rdx, %rdi
	call	_ZNSolsEi@PLT
	movq	%rax, %rdx
	movq	_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GOTPCREL(%rip), %rax
	movq	%rax, %rsi
	movq	%rdx, %rdi
	call	_ZNSolsEPFRSoS_E@PLT
	movl	$0, %eax
	leave
	.cfi_def_cfa 7, 8
	ret
	.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.cppHelloToolsClass.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 world
    std::cout<<"Hello world!"<<std::endl;
    // 输出Max+n
    int n = 100;
    std::cout<<"MAX_NUM+n:"<<MAX_NUM+n<<std::endl;

    std::cout<<"================================="<<std::endl;
    // create obj
    HelloTools hello;
    // call function
    hello.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 10

int main() {
    
    
    // 输出Hello world
    std::cout<<"Hello world!"<<std::endl;
    // 输出Max+n
    int n = 100;
    std::cout<<"MAX_NUM+n:"<<MAX_NUM+n<<std::endl;

    std::cout<<"================================="<<std::endl;
    // create obj
    HelloTools hello;
    // call function
    hello.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 10

int main() {
    
    
    // 输出Hello world
    std::cout<<"Hello world!"<<std::endl;
    // 输出Max+n
    int n = 100;
    std::cout<<"MAX_NUM+n:"<<MAX_NUM+n<<std::endl;

    std::cout<<"================================="<<std::endl;
    // create obj
    HelloTools hello;
    // call function
    hello.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.hToolLibs.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 compiler
           accepts 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 incompatible
           with 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 the
           other 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 a
           result, 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 are

           c90
           c89
           iso9899:1990
               Support all ISO C90 programs (certain GNU extensions that conflict with ISO C90 are disabled). Same as -ansi for C code.

           iso9899:199409
               ISO C90 as modified in amendment 1.

           c99
           c9x
           iso9899:1999
           iso9899:199x
               ISO 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.

           c11
           c1x
           iso9899:2011
               ISO 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 to
               optional C11 features from Annexes F and G) and the optional Annexes K (Bounds-checking interfaces) and L (Analyzability).  The name c1x is deprecated.

           c17
           c18
           iso9899:2017
           iso9899:2018
               ISO 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日完成。

猜你喜欢

转载自blog.csdn.net/youlinhuanyan/article/details/128746101