c++ 开发生态环境、工作流程、生命周期-拾遗

拾遗 1  生态环境初识

当您使用Visual Studio 2019进行C++开发时,您将进入C++生态环境。以下是一些重要的概念和步骤:

  1. C++程序的结构: 一个典型的C++程序包括源文件(.cpp)、头文件(.h)、编译后的目标文件(.obj)和最终的可执行文件(.exe)或库文件(.lib、.dll)。

  2. 第三方库的工作方式: 如果您需要使用第三方库,通常会将其源代码或者预编译的库文件(.lib、.dll)包含到您的项目中。第三方库通常会包含头文件和库文件,您需要将这些文件包含到您的项目中,并在编译和链接过程中告诉编译器和链接器去哪里寻找这些文件。

  3. 相互引用和VS配置: 在Visual Studio中,您可以通过项目属性来配置第三方库的引用。具体步骤包括:

    • 将第三方库的头文件路径添加到项目的“包含目录”中,这样您的程序中就可以包含这些头文件。
    • 将第三方库的库文件路径添加到项目的“库目录”中,这样链接器就知道在哪里找到这些库文件。
    • 在“链接器->输入”中添加需要链接的库文件名,告诉链接器在链接时需要使用这些库文件。
  4. C++生态环境的其他方面:

    • C++标准库:C++提供了丰富的标准库,包括容器、算法、输入输出等功能。
    • C++编译器:Visual Studio 2019内置了Microsoft的C++编译器(MSVC),也可以选择其他编译器比如GCC或者Clang。
    • 调试工具:Visual Studio提供了强大的调试工具,可以帮助您调试C++程序。

总的来说,C++生态环境包括了C++编译器、标准库、第三方库、开发工具等,通过合理配置和使用这些工具,您可以进行高效的C++开发。

拾遗 2  从源码到目标码(obj)、可执行码(exe)、文件库(lib,dll)

        当进行C++开发时,源码经历了多个阶段才能最终转化为可执行文件或库文件。以下是这个过程的详细步骤:

  1. 预处理阶段: 在预处理阶段,C++编译器会处理源文件(.cpp)中的预处理指令,比如#include和#define。预处理器会将头文件内容替换到源文件中,并进行宏替换等操作,生成一个经过预处理的源文件(通常是一个临时文件)。

  2. 编译阶段: 经过预处理的源文件会被编译器翻译成汇编代码(.asm文件),这个过程包括词法分析、语法分析、语义分析和优化等步骤。编译器将源码翻译成目标代码,生成一个或多个目标文件(.obj文件),每个目标文件对应源文件中的一个编译单元。

  3. 链接阶段: 在链接阶段,链接器会将多个目标文件以及所需的库文件(静态库或动态库)链接在一起,生成最终的可执行文件(.exe)或者库文件(.lib、.dll)。链接器会解析符号引用,处理重定位等工作,确保最终的可执行文件或库文件可以正确运行。

        目标码(Object Code)是编译器生成的中间文件,包含了机器代码、符号表、重定位信息等。目标码是编译后的源码的中间表示,还不能被直接执行。目标码的作用是为了后续的链接阶段使用,链接器会将多个目标文件合并成最终的可执行文件或库文件。

        总的来说,C++程序从源码到目标码再到可执行文件或库文件经历了预处理、编译和链接三个阶段。目标码是编译器生成的中间文件,用于链接器生成最终的可执行文件或库文件。

拾遗3 一套具体的流程解释:

1. C/C++运行的四个步骤

        编写完成一个C/C++程序后,想要运行起来,必须要经过四个步骤:预处理、编译、汇编和链接。每个步骤都会生成对应的文件,如下图所示(注意后缀名):

 

2.名词解释

为了后面过程的介绍更方便,这里对C++编译过程中涉及的一些常用名词进行解释。

2.1 GCC、GNU、gcc与g++

    GNU:一个操作系统,具体内容不重要,感兴趣可以参考:

GCC、GNU到底啥意思?-CSDN博客文章浏览阅读1.9w次,点赞182次,收藏201次。本文目录GCC/GNU名字的由来GCCGNU理查德·斯托曼GPLLinux参考文献GCC/GNU名字的由来今天闲来无事,想要清理一下磁盘,遇到了一个名为 TDM-GCC-64 的文件夹,也就是gcc编译器所在的文件,突然好奇,编译器为什么叫GCC呢?于是Wiki了一下,然后便一发不可收拾……GCC原来GCC是——GNU编译器套装,那么问题来了,GNU又是啥?不知道你有没有好奇过:这..._gnuhttps://blog.csdn.net/qq_43617936/article/details/104504992

  • GCC:GNU Compiler Collection(GNU编译器集合)的缩写,可以理解为一组GNU操作系统中的编译器集合,可以用于编译C、C++、Java、Go、Fortan、Pascal、Objective-C等语言。
  • gcc:GCC(编译器集合)中的GNU C Compiler(C 编译器)
  • g++:GCC(编译器集合)中的GNU C++ Compiler(C++ 编译器)

简单来说,gcc调用了GCC中的C Compiler,而g++调用了GCC中的C++ Compiler。 - 对于 *.c 和 *.cpp 文件,gcc分别当作 c 和 cpp文件编译,而g++则统一当作cpp文件编译。

2.2 代码编译命令

gcc/g++常用指令选项:

2.3 GDB(gdb)

GDB(gdb)全称“GNU symbolic debugger”,是 Linux 下常用的程序调试器。 为了能够使用 gdb 调试,需要在代码编译的时候加上-g,如

g++ -g -o test test.cpp

本文中只演示从源代码生成可执行二进制文件的过程,暂不涉及调试过程。调试的配置会在另一篇文章中专门介绍。

3. C++编译过程详解

        初次使用Visual C++的时候一定有经历过项目的配置,肯定会对预编译头,链接器之类的东西心生疑惑,敲代码就敲代码,怎么有这么多要设置的东西。还有看着Windows版本的程序和Linux版本的编译完成后的文件夹也会疑惑,编译完除了程序怎么还有一堆.obj和.o文件。

        首先我们要明白一个流程,程序要运行起来,必须要经过四个步骤:预处理、编译、汇编和链接。每个步骤都会生成对应的文件。像下图一样,接下来我会分成四个步骤描述一下每个步骤都会做什么事

预编译:

        预编译如其名,就是编译前的一些准备工作。代码文件放到编译器面前,他肯定也不知道怎么去读,看到头文件他也不知道头文件里是什么,所以预编译就要把一些#define的东西完成文本替换,然后#include的文件里的内容复制到.cpp文件里,如果.h文件里还有.h文件,那递归展开,注释这些编辑器也不管的,完全是给程序员看的。简而言之,就是剔除掉编译器看不懂的东西,然后把所有头文件展开的一个过程。

编译:

        编译只是把我们写的代码转为汇编代码,它的工作是检查词法和语法规则,所以,如果程序没有词法或则语法错误,那么不管逻辑是怎样错误的,都不会报错。

汇编:

        汇编代码转换机器码,这个阶段,非底层的程序员不需要考虑, 编译器不会搞错的。也与c/c++开发者无关,但是我们可以利用反汇编来调试代码,学习汇编语言依然是必备的。

链接:

        配置链接库的重要性就体验在这里,配置的静态链接库还是动态链接库都会在这部分和最后的程序链接到一起。

        在这之前引用百度过来的一段话介绍下静态链接和动态链接的区别

    (1)静态链接是由链接器在链接时将库的内容加入到可执行程序中的做法。链接器是一个独立程序,将一个或多个库或目标文件(先前由编译器汇编器生成)链接到一块生成可执行程序。静态链接是指把要调用的函数或者过程链接到可执行文件中,成为可执行文件的一部分。

  (2)动态链接所调用的函数代码并没有被拷贝到应用程序的可执行文件中去,而是仅仅在其中加入了所调用函数的描述信息(往往是一些重定位信息)。仅当应用程序被装入内存开始运行时,在Windows的管理下,才在应用程序与相应的DLL之间建立链接关系。当要执行所调用DLL中的函数时,根据链接产生的重定位信息,Windows才转去执行DLL中相应的函数代码。

        C语言代码经过编译以后,并没有生成最终的可执行文件(.exe 文件),而是生成了一种叫做目标文件(Object File)的中间文件(或者说临时文件)。目标文件也是二进制形式的,它和可执行文件的格式是一样的。对于 Visual C++,目标文件的后缀是.obj;对于 GCC,目标文件的后缀是.o。这就是一开始所说的编译完一堆.obj和.o文件的来源。

        目标文件经过链接(Link)以后才能变成可执行文件。既然目标文件和可执行文件的格式是一样的,为什么还要再链接一次呢,因为编译只是将我们自己写的代码变成了二进制形式,它还需要和系统组件(比如标准库、动态链接库等)结合起来,这些组件都是程序运行所必须的。链接(Link)其实就是一个“打包”的过程,它将所有二进制形式的目标文件和系统组件组合成一个可执行文件。完成链接的过程也需要一个特殊的软件,叫做链接器(Linker)。

        这里还要讲一个东西,C++程序编译的时候其实只识别.cpp文件。每个cpp文件都会分别编译一次,生成一个.o或者.obj文件。这个时候,链接器除了将目标文件和系统组件组合起来,还需要将编译器生成的多个.o或者.obj文件组合起来。

本节内容用下面的简单C++工程做演示。示例的文件结构如下:

|—— include
      |—— func.h
|—— src
      |—— func.cpp
|—— main.cpp

其中,main.cpp是主要代码,include/func.h是自定义函数的头文件,src/func.cpp是函数的具体实现

各个文件的内容如下:

// main.cpp
#include <iostream>
#include "func.h"

using namespace std;

int main(){
    int a = 1;
    int b = 2;
    cout << "a + b = " << sum(a, b) << endl;; 

    return 0;
}
// func.h
#ifndef FUNC_H
#define FUNC_H

int sum(int a, int b);

#endif
// func.cpp
#include "func.h"

int sum(int a, int b) {
    return a + b;
}

3.1 预处理(Preprocess)

        预处理,顾名思义就是编译前的一些准备工作。

        预编译把一些#define的宏定义完成文本替换,然后将#include的文件里的内容复制到.cpp文件里,如果.h文件里还有.h文件,就递归展开。在预处理这一步,代码注释直接被忽略,不会进入到后续的处理中,所以注释在程序中不会执行。

        gcc/g++的预处理实质上是通过预处理器cpp(应该是c preprocess的缩写?)来完成的,所以我们既可以通过g++ -E,也可以通过cpp命令对main.cpp进行预处理:

g++ -E -I include/ main.cpp -o main.i
# 或者直接调用 cpp 命令
cpp -I include/ main.cpp -o main.i

        上述命令中: - g++ -E 是让编译器在预处理之后就退出,不进行后续编译过程,等价于cpp指令 - -I include/用于指定头文件目录 - main.cpp是要预处理的源文件 - -o main.i用于指定生成的文件名

        预处理之后的程序格式为 *.i,仍是文本文件,可以用任意文本编辑器打开。

执行完预处理后的文件结构如下:

|—— include
      |—— func.h
|—— src
      |—— func.cpp
|—— main.cpp
|—— main.i

 3.2 编译(Compile)

编译 只是把我们写的代码转为汇编代码它的工作是检查词法和语法规则,所以,如果程序没有词法或则语法错误,那么不管逻辑是怎样错误的,都不会报错
编译不是指程序从源文件到二进制程序的全部过程,而是指将经过预处理之后的程序转换成特定汇编代码(assembly code)的过程

编译的指令如下:

g++ -S -I include/ main.cpp -o main.s

与预处理类似,上述命令中: - g++ -S是让编译器在编译之后停止,不进行后续过程 - -I include/用于指定头文件目录 - main.cpp是要编译的源文件 - -o main.s用于指定生成的文件名

编译完成后,会生成程序的汇编代码main.s这也是文本文件,可以直接用任意文本编辑器查看。

执行完编译后的文件结构如下:

|—— include
      |—— func.h
|—— src
      |—— func.cpp
|—— main.cpp
|—— main.i
|—— main.s

3.3 汇编(Assemble)

汇编过程将上一步的 汇编代码( main.s) 转换成机器码(machine code), 这一步产生的文件叫做目标文件( main.o), 是二进制格式

gcc/g++的汇编过程通过 as 命令完成,所以我们可以通过g++ -cas命令完成汇编:

g++ -c -I include/ main.cpp -o main.o
# 或者直接调用 as 命令
as main.s -o main.o

上述指令中: - g++ -c让编译器在汇编之后退出,等价于 as - -I include/仍是用于指定头文件目录 - main.cpp是要汇编的源文件 - -o main.o用于指定生成的文件名

汇编这一步需要为每一个源文件(本文示例代码中为main.cppfunc.cpp)产生一个目标文件。因此func.cpp也需要执行一次这个汇编过程产生一个func.o文件:

# 可以用 g++ -c 命令一步生成 func.o
g++ -c -I include/ src/func.cpp -o src/func.o
# 当然也可以按照上面的预处理、编译、汇编三个步骤生成func.o

到了这一步,代码的文件结构如下:

|—— include
      |—— func.h
|—— src
      |—— func.cpp
      |—— func.o
|—— main.cpp
|—— main.i
|—— main.s
|—— main.o

 3.4 链接(Link)

C/C++代码经过汇编之后生成的目标文件(*.o)并不是最终的可执行二进制文件,而仍是一种中间文件(或称临时文件),目标文件仍然需要经过链接(Link)才能变成可执行文件

既然目标文件和可执行文件的格式是一样的(都是二进制格式),为什么还要再链接一次呢?
因为 编译只是将我们自己写的代码变成了二进制形式,它还需要和系统组件(比如标准库、动态链接库等)结合起来,这些组件都是程序运行所必须的
链接(Link)其实就是一个“打包”的过程,它将所有二进制形式的目标文件(.o)和系统组件组合成一个可执行文件。完成链接的过程也需要一个特殊的软件,叫做 链接器(Linker)
此外需要注意的是: C++程序编译的时候其实只识别.cpp文件,每个cpp文件都会分别编译一次,生成一个.o文件。这个时候,链接器除了将目标文件和系统组件组合起来,还需要将编译器生成的多个.o或者.obj文件组合起来生成最终的可执行文件(Executable file)

以本文中的代码为例,将func.omain.o链接成可执行文件main.out,指令如下:

g++ src/func.o main.o -o main.out
  • -o main.out用于指定生成的可执行二进制文件名
  • 由于g++自动链接了系统组件,所以我们只需要把自定义函数的目标文件与main.o链接即可。

运行main.out,结果如下:

./main.out
a + b = 3

3.5 小结

从上面的介绍可以看出,从C++源代码到最终的可执行文件的中间过程并不简单。了解预处理、编译、汇编、链接各个步骤的作用有助于我们处理更加复杂的项目工程。

不过也不必被这么麻烦的编译过程劝退,当我们编译简单.cpp代码时,

// hello.cpp
#include <iostream>
using namespace std;

int main(){
    cout << "Hello, world!" << endl;
    return 0;
}

仍然可以直接使用g++命令生成可执行文件,而不必考虑中间过程:

g++ hello.cpp -o hello
./hello
Hello, world!

4. 参考教程

  1. GCC、GNU到底啥意思?
  2. C++ 预编译,编译,汇编,链接
  3. C/C++语言编译链接过程

  • 17
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
在C/C++开发中,对象变量的生命周期管理非常重要。对于一个程序,对象的生命周期涉及到内存的分配和释放,如果管理不当,可能会导致内存泄漏和程序崩溃等问题。因此,本篇文章将为大家详细介绍如何在C/C++开发中管理对象变量的生命周期。 一、对象的创建和销毁 在C/C++中,对象的创建和销毁是由构造函数和析构函数来完成的。构造函数用于初始化对象的各个成员变量,而析构函数则用于释放对象占用的内存空间和资源。下面是一个简单的示例代码: ```c++ class MyClass{ public: // 构造函数 MyClass(){ // 初始化成员变量 } // 析构函数 ~MyClass(){ // 释放资源 } }; ``` 在上述代码中,MyClass是一个类,它包含了一个构造函数和一个析构函数。当程序实例化MyClass对象时,构造函数会被调用,用于初始化对象的成员变量;当程序销毁MyClass对象时,析构函数会被调用,用于释放对象占用的内存空间和资源。 二、堆和栈的区别 在C/C++中,对象的生命周期还涉及到堆和栈的使用。堆和栈都是内存空间,但它们在使用上有着很大的区别。 栈内存是由系统自动分配和释放的,程序员不能主动控制。当程序进入一个函数时,它会为该函数的局部变量和参数在栈内存中分配一段空间,当函数执行完毕后,这些空间会自动被释放。因此,栈内存的生命周期是由系统自动管理的。 而堆内存则需要程序员手动分配和释放。程序员可以使用new操作符在堆内存中分配一段空间,并使用delete操作符释放这些空间。因此,堆内存的生命周期是由程序员手动控制的。 三、内存泄漏和野指针 在C/C++开发中,内存泄漏和野指针是非常常见的问题。内存泄漏指程序中使用了动态分配的内存空间,但在使用完毕后未释放,导致内存资源无法被回收。野指针则是指指向已经释放的内存空间或者未初始化的内存空间,这种指针的使用会导致程序崩溃或者产生未知的错误。 为避免内存泄漏和野指针等问题,程序员需要注意以下几点: 1. 确保每个new操作都有对应的delete操作,避免动态分配的内存空间无法释放。 2. 不要使用已经释放的内存空间,避免野指针的产生。 3. 对于堆内存中分配的对象,需要在对象的生命周期结束时手动释放内存空间。 4. 对于栈内存中的对象,不需要手动释放内存空间,因为它们的生命周期是由系统自动管理的。 四、对象引用和拷贝 在C/C++中,对象引用和拷贝也是需要注意的问题。对象引用是指使用一个对象的别名来访问该对象,而对象拷贝则是指将一个对象的值复制到另一个对象中。 对于对象引用,程序员需要注意以下几点: 1. 对象引用不会占用额外的内存空间,它只是原对象的别名,因此在使用对象引用时要确保原对象的生命周期长于对象引用。 2. 对象引用不能指向空对象或已经释放的对象,否则会产生野指针。 对于对象拷贝,程序员需要注意以下几点: 1. 对象拷贝会占用额外的内存空间,因此在使用对象拷贝时要注意内存的分配和释放。 2. 对于类中包含指针成员变量的情况,需要自定义拷贝构造函数和赋值运算符,防止浅拷贝和指针重复释放等问题。 总之,在C/C++开发中,对象变量的生命周期管理是非常重要的。程序员需要注意对象的创建和销毁、堆和栈的使用、内存泄漏和野指针、对象引用和拷贝等问题,从而保证程序的稳定性和性能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小道士写程序

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值