C++ 挣扎之旅 —— C++ 到 Qt 的手动编译原理与方法

前言

是的,第三次开始学习 Linux,起因有两方面原因

  • 对 C++ 程序的编译、链接等过程不明白
  • 闲(对不起我导)

于是乎,再次尝试了虚拟机和双系统之后,重新在 VMwarePro 中开启了第三次 Linux 学习之旅,主题为——挣扎

Ubuntu 常用命令记录

dpkg

在Ubuntu中,dpkg是用于管理软件包的命令行工具。它是基于Debian操作系统的dpkg软件包管理系统的前端工具,用于安装、升级、配置和删除软件包。dpkg可以直接处理.deb格式的软件包文件,并能够解决软件包之间的依赖关系

说白了,就是下载了 .deb 格式的包之后,可以通过以下命令来解压并安装软件:

sudo dpkg -i xxx.deb 

其中,sudo 的含义为 super user do,该命令赋予指令 root 权限,-i 的含义为 install,表示安装软件包

apt-get

apt-get是Debian及其衍生发行版(如Ubuntu)中用于管理软件包的命令行工具。它是Advanced Packaging Tool(高级包管理工具)的一部分,用于从软件仓库安装、升级、配置和删除软件包

  • 安装软件包

    sudo apt-get install <package>
    

    用于安装指定的软件包及其依赖关系

  • 升级软件包

    sudo apt-get upgrade
    

    用于升级系统中已安装的所有软件包到最新版本

  • 更新软件包列表

    sudo apt-get update
    

    用于更新本地软件包列表,以获取最新的可用软件包信息

  • 删除软件包

    sudo apt-get remove <package>
    

    用于从系统中彻底删除指定的软件包

  • 清理已下载的软件包

    sudo apt-get clean
    

    用于清理已下载的软件包缓存,释放磁盘空间

C++ 程序编译过程与运行原理

C++ 程序结构

一般来说,C++ 程序由头文件、源代码文件组成,源代码中又包含了一个单独的主函数文件

  • 头文件 Header Files
    头文件包含了函数和类的声明,通常以 .h.hpp 为后缀名。它们描述了程序中定义的函数、类、结构和全局变量的接口

  • 源代码文件 Source Code Files
    源代码文件包含了实际的代码实现,通常以 .cpp.cc 为后缀名。它们定义了在程序中使用的函数和类的具体实现

  • 主函数 main
    主函数一般是一个单独的 main.cpp 文件,它是程序的入口点

预编译

在 C++ 程序正式编译之前,需要对一些带 # 开头的关键字进行预编译,一般有三种

  • 头文件包含
    预编译器会将源代码中的 #include 指令替换为对应的头文件内容。这样,程序可以从其他文件中引入函数和类的声明

    这里对 #include “animal.h”#include <animal.h> 进行说明一下区别,<>“” 表示编译器搜索头文件的顺序不同:

    • <> 表示从系统目录下开始搜索,然后再搜索PATH环境变量所列出的目录,不搜索当前目录
    • "" 表示先从当前目录搜索,然后是系统目录和PATH环境变量所列出的目录下搜索

    所以如果我们知道头文件在系统目录或者环境变量目录下时,可以用<>来加快搜索速度。

  • 宏替换
    预编译器会根据宏定义,即使用 #define 指令定义的标识符,执行替换操作。遇到宏名称时,预编译器会将其替换为预定义的文本
    比如说 #define Five 5,那么在该阶段会将程序中的 Five 全部替换成 5

  • 条件编译
    预编译器根据条件语句,如 #ifdef#ifndef#else#elif#endif 等,来判断是否包含某段代码。这样,在不同的条件下可以选择性地编译不同的代码块

  • 删除注释和多余空格

此外,预编译处理会将 .cpp 文件转换为 .i 的文件格式

编译过程

在 C++ 程序完成预编译后,正式进入编译过程,主要有以下几个步骤:

  1. 编译 Compilation
    在这个阶段,编译器会将经过预处理的代码文件转换为汇编语言(或中间代码)。编译器会对源代码进行语法分析、词法分析、语义检查等操作,生成对应的汇编语言代码

    • 输入:经过预处理的代码文件
    • 输出:汇编语言文,通常以 .s.asm 为后缀,包含了将源代码转换成汇编指令的结果
  2. 汇编 Assembly
    在这个阶段,汇编器会将汇编语言代码转换为机器可执行的目标代码。汇编器会将汇编语言的指令翻译成机器指令,并生成与特定系统平台相关的目标文件

    • 输入:汇编语言文件
    • 输出:目标文件,通常以 .o.obj.elf 为后缀,包含了机器可执行的二进制指令,但还没有解决符号引用和地址重定向
  3. 链接 Linking
    在这个阶段,链接器将编译生成的目标文件与其他相关的目标文件和库文件进行链接,生成最终的可执行程序。链接器会解决符号引用、地址重定向、库函数链接等问题,将多个目标文件组合成一个完整的可执行文件

    • 输入:目标文件、库文件(可以是静态库或动态库)
    • 输出:最终的可执行文件(通常没有后缀,或以 .exe.out 等为后缀),包含了所有的目标文件和库文件链接后的结果

配置 C++ 编译环境

要想配置 C++ 编译环境,还需要知道 C++ 可以由哪些编译器进行编译,此处通过一篇知乎文章说明不同名词之间的区别

由于本文建立在 Linux 环境下,因此选择 GCC/G++ 作为编译器

安装 build-essential

Ubuntu 系统在默认情况下并不具备 C/C++ 编译环境,但可以通过安装 build-essential 来安装编译 C/C++ 的相关软件

构建基础包(build-essential)实际上是属于 Debian 的。在它里面其实并不是一个软件。它包含了创建一个 Debian 包(.deb)所需的软件包列表。这些软件包包括 libc、gcc、g++、make、dpkg-dev 等。构建基础包包含这些所需的软件包作为依赖,所以当你安装它时,你只需一个命令就能安装所有这些软件包

可以通过下边两行代码来安装 build-essential

sudo apt-get update
sudo apt-get install build-essential

安装过后,可以通过 -v 命令查看安装的结果

gcc -v
g++ -v
make -v

编写 C++ 程序验证环境是否可用

首先打开某一代码存放目录,用 VS Code 创建并打开新的 .cpp 文件

code hola.cpp

在 VS Code 中输入以下代码:

#include <iostream>

using namespace std;

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

使用 ctrl + s 保存后,转到命令行(或者使用 VS Code 下方命令行),使用命令编译 C++ 程序并运行

g++ hola.cpp -o hola

这行代码表示用 g++ 编译 hola.cpp,并指定输出的可执行文件名称为 hola.o

接下来,通过 ./ 命令打开可执行文件便可看到运行结果

./hola

安装 Cmake

通过 sudo apt-get install cmake 命令便可以安装 cmake

其与 make 的关系和功能同样参考知乎文章,且以下为复制

有了编译器 GCC 等,为什么还要有 make 这个构建生成器,同样是老生常谈的内容

编译 hello.c 非常简单,只需要

gcc hello.c

就可以了,但当项目庞大起来后,假设 hello.c 依赖于 a.cb.c,而 a.c 又依赖于库 w.lib,每一次编译,我们都要重新编写一次 `gc 编译命令行吗?

所以,GNU 发明了 make 这个工具软件,可以编写 makefile 文件来指定特定的项目构建过程,当项目一个文件的代码更改时,我们只需要重新 make 一下就可以了。

但 make 依然有很多不足,比如

  1. make 对于类 unix 系统是通用的,但对 windows 系统并不友好(不能跨平台)
  2. make 语法简单,也就导致了它功能的限制
  3. 不同编译器的语法规则不同,编写的 makefile 语法如果适合 GCC 则不适合 MSVC

所以,CMake 就应运而生啦

CMake 是比 Make 更高一层的工具,Make 是编写对应编译器的 makefile 从而实现编译,而 CMake 是写一份独立的 CmakeList.txt 文件,然后该文件会根据当前系统环境选择适合的构建生成器(如 VS 或者 make ),然后将 CmakeList.txt 翻译为适合的文件,再进一步调用系统编译器进行项目构建

配置 Qt 编译环境

Qt 下载与安装

Qt 推荐使用 5.12.12 版本,附下载地址:

https://download.qt.io/archive/qt/

Ubuntu 系统下,下载 qt-opensource-linux-x64-5.12.12.run 就行,大小 1.3G 推荐使用多线程下载器下载

安装过程的教程网上很多,随便找一个就行,但不知道安装了什么的话可以参考组件选择

个人推荐安装时只选择需要编译环境下的编译器、Qt 尚未舍弃的模块,以及 Qt Creator 就行

个人猜测的是 Qt 在安装时,默认会安装 Qt 的库文件,位置就在 Qt\5.12.12\gcc_64 下(即 QTDIR),当然,如果是 Windows 环境,则第三项为 mingw_32 或 mingw_64,该目录下的 src 为 Qt 源代码目录,用户可以自己编译使用

安装目录下还有个 Tools 文件夹,里边是安装的 Qt Creator 和编译器,当然,Linux 环境下需要用户自己安装编译器

验证 Qt 环境是否可用

打开 Qt Creator,选择任意示例工程,便可以创建一个官方项目

此外,在 Qt Creator 设置的 Kits 选项中,可以看到自动检测到的 C 和 C++ 编译器、调试器、CMake 等,它们之间的组合就成了一个 Kit,也就是编译 Qt 项目时选择的东西

打开复制的示例工程,使用 ctrl + r 便可编译运行,如果出现相应的界面,并且功能没有问题,则可认为安装成功(有的项目运行不了,我也不知道为啥)

添加环境变量

Qt 的库文件中有很多 Qt 所需要的工具,如果不加入环境变量则无法在终端中调用

首先通过以下命令打开 Bash Shell 配置文件

sudo gedit ~/.bashrc

然后在文件添加以下语句,注意需要根据自己的安装目录和版本修改

# Qt Environment Varies
export PATH="/opt/Qt5.12.12/Tools/QtCreator/bin:$PATH"
export PATH="/opt/Qt5.12.12/5.12.12/gcc_64/bin:$PATH"

然后,通过 qmake -v 命令即可测试是否添加成功

多 cpp 文件下的 C++ 程序编译

手动挡 g++

如前文可知,C++ 程序的编译过程需要先编译,再链接(链接的过程中会检查函数的定义)

当有多个 .cpp 文件,如有主函数 main.cpp 和函数 fun.cpp、头文件 fun.h,有两种方法,可以实现编译输出可执行文件

// main.cpp

#include "fun.h"

int main()
{
	function_test();
}
// fun.h

#include <iostream>

using namespace std;

void function_test();
// fun.cpp

#include "fun.h"

void function_test()
{
    cout << "For god sake!" << endl;
}
  1. 一次性编译、链接

    g++ main.cpp fun.cpp -o out
    

    这行命令会直接完成两个文件的编译和链接过程

    fun.h 文件不需要被编译,因为它是一个头文件,主要作用是在源文件中引入函数声明和定义的声明。当编译器编译源文件时,会将源文件中包含的头文件打开,然后将头文件中声明的函数放入符号表中,这样就可以进一步链接其他目标文件

  2. 单独编译再链接

    当部分文件需要修改时,可以仅重新编译修改过后的文件,再进行链接,这样做有助于提高编译速度,同时也允许在修改单个源文件时只重新编译该文件,而无需重新编译整个项目

    g++ -c main.cpp -o main.o
    g++ -c fun.cpp -o fun.o
    
    g++ main.o fun.o -o out
    

    在命令行中,-cg++ 编译器的一个选项,用于指定只进行编译而不进行链接的操作

    需要注意的是,当 fun.h 文件发生变化时,它所依赖的源文件可能也会受到影响,这时需要重新编译相应的源文件,并链接生成新的可执行文件

手自一体 Make

当程序包含很多源文件时,用 gcc 命令逐个去编译时,就很容易混乱而且工作量大。所以就出现了 Make 工具。它是一个自动化编译工具,我们可以使用一条命令实现完全编译,但是需要编写一个规则文件,Make 工具依据它来批量处理编译,这个文件就是 Makefile 文件

对于同样的例子,如有主函数 main.cpp 和函数 fun.cpp、头文件 fun.h

此时如果要通过 Make 完成编译,则需要通过 code Makefile 命令创建 Makefile 文件,并输入以下内容:

out:main.o fun.o
	g++ -o out main.o fun.o
main.o:main.cpp
	g++ -c main.cpp
func.o:fun.cpp
	g++ -c fun.cpp

在 Makefile 中,: 左侧的内容是生成的目标文件,右侧是生成所依赖的文件,这样就很好解释上述文件的含义,即主要目标是生成可执行文件 out,其依赖二进制指令文件main.ofun.o,而第二行是生成该文件的编译规则

值得注意的是 g++ 命令前是制表符,而不能使用空格,否则会报错

Makefile 在一些简单的工程完全可以人工手写,但是当工程非常大的时候,手写 Makefile 也是非常麻烦的,如果换了个平台 Makefile 又要重新修改(修改的内容包含编译器命令)

既然是手自一体,那也能自动起来,即在 Makefile 里可以通过通配符简化内容

objs = main.o fun.o
out:${objs}
	g++ -o out ${objs}
%.o:%.cpp
	g++ -c $<
clear:
	rm *.o

其中,objs 是变量,可以通过 ${objs} 的方式来调用,% 是通配符,%.o 表示所有以 .o 结尾的文件,clear 是自定义命令,make 之后,再输入 make clear 命令即可清除 .o 文件

自动挡 CMake

makefile
makefile
Source Files
CMake
Make
可执行文件

在人们发现 Make 的种种弊端后,出现了 CMake 这个工具,CMake 能够输出各种各样的 Makefile 或者 project 文件,从而帮助程序员减轻负担。但是随着而来的也是需要编写 CMakeLists 文件。当然 CMake 还有其他功能,就是可以跨平台生成对应能用的 Makefile,我们不需要自己再去修改

对于同样的例子,如有主函数 main.cpp 和函数 fun.cpp、头文件 fun.h

此时如果要通过 CMake 完成编译,则需要通过 code CMakeLists.txt 命令创建 CMakefile 文件,并输入以下内容:

# CMake 最低版本号要求
cmake_minimum_required (VERSION 2.8)
 
# 项目信息
project (CMakeLearn)
 
# 指定生成目标
add_executable(out main.cpp fun.cpp)

如果源文件很多,可以使用 aux_source_directory 命令,来查找指定目录下的所有源文件

# CMake 最低版本号要求
cmake_minimum_required (VERSION 2.8)
 
# 项目信息
project (CMakeLearn)
 
# 查找当前目录下的所有源文件
# 并将名称保存到 DIR_SRCS 变量
aux_source_directory(. DIR_SRCS)
 
# 指定生成目标
add_executable(out ${DIR_SRCS})

这样,CMake 会将当前目录所有源文件的文件名赋值给变量 DIR_SRCS,再指示变量 DIR_SRCS 中的源文件编译成一个名称为 out 的可执行文件

接下来,便可以通过 cmake . 命令生成 Makefile 并 Make 生成可执行文件

cmake .
make
./out

QMake 编译 Qt 程序

首先,创建包含多个文件的项目,如包含 main.cppmainwindow.hmainwindow.cppmainwindow.ui 的项目文件,其内容分别如下:

// main.cpp

#include "mainwindow.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}
// mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private:
    Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
// mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
}

MainWindow::~MainWindow()
{
    delete ui;
}
<!--mainwindow.cpp-->

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>500</width>
    <height>200</height>
   </rect>
  </property>
  <property name="sizePolicy">
   <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
    <horstretch>0</horstretch>
    <verstretch>0</verstretch>
   </sizepolicy>
  </property>
  <property name="minimumSize">
   <size>
    <width>500</width>
    <height>200</height>
   </size>
  </property>
  <property name="maximumSize">
   <size>
    <width>500</width>
    <height>200</height>
   </size>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralwidget">
   <widget class="QWidget" name="verticalLayoutWidget">
    <property name="geometry">
     <rect>
      <x>0</x>
      <y>100</y>
      <width>501</width>
      <height>101</height>
     </rect>
    </property>
    <layout class="QVBoxLayout" name="verticalLayout">
     <item>
      <widget class="QLineEdit" name="lineEdit_2">
       <property name="inputMask">
        <string/>
       </property>
       <property name="text">
        <string/>
       </property>
      </widget>
     </item>
     <item>
      <widget class="QLineEdit" name="lineEdit"/>
     </item>
     <item>
      <layout class="QHBoxLayout" name="horizontalLayout">
       <item>
        <spacer name="horizontalSpacer_2">
         <property name="orientation">
          <enum>Qt::Horizontal</enum>
         </property>
         <property name="sizeHint" stdset="0">
          <size>
           <width>40</width>
           <height>20</height>
          </size>
         </property>
        </spacer>
       </item>
       <item>
        <widget class="QPushButton" name="pushButton_2">
         <property name="text">
          <string>Sign up</string>
         </property>
        </widget>
       </item>
       <item>
        <spacer name="horizontalSpacer">
         <property name="orientation">
          <enum>Qt::Horizontal</enum>
         </property>
         <property name="sizeHint" stdset="0">
          <size>
           <width>40</width>
           <height>20</height>
          </size>
         </property>
        </spacer>
       </item>
       <item>
        <widget class="QPushButton" name="pushButton">
         <property name="text">
          <string>Log in</string>
         </property>
        </widget>
       </item>
       <item>
        <spacer name="horizontalSpacer_3">
         <property name="orientation">
          <enum>Qt::Horizontal</enum>
         </property>
         <property name="sizeHint" stdset="0">
          <size>
           <width>40</width>
           <height>20</height>
          </size>
         </property>
        </spacer>
       </item>
      </layout>
     </item>
    </layout>
   </widget>
   <widget class="QLabel" name="label">
    <property name="geometry">
     <rect>
      <x>0</x>
      <y>0</y>
      <width>501</width>
      <height>101</height>
     </rect>
    </property>
    <property name="font">
     <font>
      <pointsize>35</pointsize>
     </font>
    </property>
    <property name="text">
     <string>Tencent QQ</string>
    </property>
    <property name="alignment">
     <set>Qt::AlignCenter</set>
    </property>
   </widget>
  </widget>
 </widget>
 <resources/>
 <connections/>
</ui>

此时如果要通过 QMake 完成编译,则需要通过 code qqlog.pro 命令创建 pro 工程文件,并输入以下内容:

QT += core gui widgets

TARGET = qqloger

CONFIG += c++11

HEADERS += mainwindow.h
SOURCES += main.cpp \
           mainwindow.cpp
FORMS += mainwindow.ui

pro 工程文件的详细解释可以参考博客

保存后,通过以下命令,使用 QMake 生成 Makefile 后 make 即可生成可执行文件

qmake qqlog.pro -o Makefile
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值