Linux 系统下 C/C++ 程序编译

文章目录

  • 简介
  • C 程序编译
    • 单个源文件生成可执行程序
    • 多个源文件生成可执行程序
    • 源文件生成对象文件
    • 编译预处理
    • 生成汇编代码
    • 构建静态库
    • 构建共享库
  • C++ 程序编译
    • 单个源文件生成可执行程序
    • 多个源文件生成可执行程序
    • 源文件生成对象文件
    • 编译预处理
    • 生成汇编代码
    • 构建静态库


简介


在 Linux 操作系统中,GCC 是一种现代化的编译器集合,它可用于编译多种程序设计语言,包括 C 语言C++ 语言

通过使用GCC 编译器,可以编写高效、安全和可靠的程序,对于 C 程序,推荐使用 gcc 进行编译,但对于 C++ 程序,建议使用 g++ 。如果需要构建大规模的 C 程序并管理源文件,可以使用 makefilemake 工具 来完成这项任务。

  • Makefile 是一个文本文件,其中包含目标、依赖关系和要执行的命令,可以根据程序复杂性创建多个目标并指定依赖关系和命令序列。

  • Make 命令可以自动构建程序,它会阅读 Makefile 并生成必要的命令,以确保目标依赖关系被遵循而不必手动输入一系列编译命令。

因此,在面对大型 C 程序时,使用 makefilemake 工具 可以显著提高编译的效率和便利性,并减少常见错误和问题的出现。

编译 C 程序的过程主要包括四个阶段:编译预处理编译优化汇编链接

  • 在编译预处理阶段,编译器会读取源程序中的预处理命令(例如,宏定义命令,条件编译指令,头文件包含指令等)和特殊符号(例如,在源程序中出现的 LINE 则被解释为十进制表示的当前行号;FILE 则被解释为当前编译的源程序的文件名,并将其替换为对应的内容,同时加入头文件中的定义等)。

  • 在编译优化阶段,编译程序通过词法分析、语法分析,在确认所有的指令都符合语法规则之后将代码翻译成对应的汇编代码,并进行优化处理。优化有两种方式,一种是针对代码本身的优化(例如,删除公共表达式、循环优化、代码外提、无用代码赋值的删除等),另一种是针对计算机硬件的指令优化(例如,根据机器硬件执行指令的特点对指令进行调整优化,减少目标代码长度,提高执行效率等)。

  • 在汇编阶段,编译程序将产出汇编文件,再通过汇编程序将其转换为目标机器指令的序列,生成对应的目标文件,目标文件一般至少包含代码段和数据段。

  • 在链接阶段,连接程序将把多个目标文件连接在一起,解决符号引用问题,生成可执行文件。根据连接的方式不同,链接库可以分为静态链接和动态链接。在静态链接中,函数的代码会被直接拷贝到最终的可执行文件中;在动态链接中,函数的代码被放到共享对象中,在生成的可执行文件中记录下共享对象的名字和少量关键信息。


C 程序编译


单个源文件生成可执行程序

通过以下一个简单的 C 程序为例,该程序的代码如下:

/* hello.c */
#include 

int main(int argc,char *argv[])
{
	printf("Hello cqupthao!\n");
	return 0;
}

最简单直接的编译以上代码为可执行程序的方法是将该代码保存为文件 hello.c ,并在相应目录下执行如下指令:

gcc hello.c -Wall

GCC 编译器通过检查命令行中指定的文件的后缀名可识别其为 C 源代码文件( GCC 默认的动作:编译源代码文件生成对象文件,链接对象文件得到可执行程序,删除对象文件,编译器默认的可执行程序的文件名 a.out )。

在终端中输入以下格式的命令,使其运行并显示结果:

./a.out

## result
Hello cqupthao!

可以通过选项 -o 来指定所生成指定文件名的可执行程序,例如,输入以下的命令生成名为 hello 的可执行程序:

gcc hello.c -o hello -Wall

在终端中输入以下的命令格式,使其运行并显示结果:

./hello

## result
Hello cqupthao!

注意:如果编译需要用到 math.h 库等非 GCC 默认调用的标准库,应使用选项 -lm


多个源文件生成可执行程序

当多个源码文件被编译时,GCC 编译器会自动进行链接操作。例如,一个名为 hellomain.c 文件程序调用一个名 sayhello.c 文件程序的 sayhello() 函数,该程序的代码如下:

/* hellomain.c */
void sayhello(void);

int main(int argc,char *argv[])
{
	sayhello();
	return 0;
}

以下的程序代码保存在名为 sayhello.c 文件程序定义的 sayhello() 函数:

/* sayhello.c */
#include 

void sayhello()
{
	printf("Hello cqupthao!\n");
}

将两个文件分别编译为对象文件且将其链接为可执行程序 hello,并删除对象文件,输入以下格式的命令:

gcc hellomain.c sayhello.c -o hello -Wall

在终端中输入以下的命令格式,使其运行并显示结果:

./hello

## result
Hello cqupthao!

源文件生成对象文件

当编译过程中使用选项 -c 时,GCC 会进行编译源代码文件的操作,但不会链接对象文件生成可执行文件。与此同时,程序可以保留所生成的目标文件到磁盘中,方便后续的操作和调试。

在这种情况下,GCC 默认的输出文件名与源代码文件名相同,只不过后缀变为了 .o ,表示这是一个目标文件,即未被链接的二进制文件。因此,在编译大型 C 程序时,多个源文件可以分别编译成目标文件,然后在链接阶段将它们合并起来,以生成最终的可执行文件。

例如,生成名为 hello.o 的对象文件,输入以下格式的命令:

gcc -c hello.c -Wall

选项 -o 可用来生成指定文件名的对象文件。例如,输入以下命令将产生名为 sayhello.o 的对象文件:

gcc -c hello.c -o sayhello.o -Wall 

当构建对象库或者生成一系列对象文件以备稍后链接用时,可以从多个源码文件(如,hello.c callhello.c sayhello.c)生成对应的对象文件名hello.ocallhello.osayhello.o 对象文件,输入以下格式的命令:

gcc -c hello.c callhello.c sayhello.c -Wall

## result
hello.o callhello.o sayhello.o

编译预处理

选项 -E 指示编译器只进行编译预处理。例如,将预处理源码文件 hello.c 并将结果在标准输出中列出,输入以下格式的命令:

gcc -E hello.c

选项 -o 用来将预处理过的代码定向到一个不需经过预处理且后缀为 .i 的 C 源码文件,例如,输入以下格式的命令:

gcc -E hello.c -o hello.i

生成汇编代码

选项 -S 指示编译器生成汇编语言代码然后结束。例如,将由 C 源码文件 hello.c 生成汇编语言文件 hello.s ,输入以下格式的命令:

gcc -S hello.c

汇编语言的形式依赖于编译器的目标平台,如果多个源码文件被编译,每个文件将分别产生对应的汇编代码模块。


构建静态库

静态库是编译器生成的一组以 .o 为后缀的文件集合,它们由同一个源代码库编译而来。与动态库相比,代码静态链接意味着当程序运行时,完成了所有依赖项的链接以及复制,因此可执行文件中包含了所有必需代码。

这样可以避免在程序运行时出现动态链接的错误,并且消除了动态链接库的性能损失。另外,静态库具有可移植性和独立性,因为它们不依赖于操作系统或者计算机硬件的特定版本。

静态库的另一个名字叫归档文件(archive),因为它实际上是一系列的 .o 文件的归档,构建一个库,首先要编译出库中需要的对象模块。例如,下面的两个程序文件名分别为 firsthello.csecondhello.c ,其源码如下:

/* firsthello.c */
#include 

void firsthello()
{
        printf("This is the first hello!\n");
}
/* secondhello.c */
#include 

void secondhello()
{
	printf("This is the second hello!\n");
}

使用静态库之前,需要将需要编译的源代码文件分别编译成目标文件(后缀为 .o ),然后才能使用 ar 命令将它们打包成一个归档文件(也叫静态库文件,后缀为 .a )。例如,将这两个源码文件编译成对象文件,输入以下格式的命令:

gcc -c firsthello.c secondhello.c -Wall 

在 Linux 系统下,通常使用 ar 工具管理和创建归档文件,ar 配合参数 -r 可以创建一个新库并将对象文件插入。如果库不存在的话,参数 -r 将创建一个新的,并将对象模块添加(如有必要,通过替换)到归档文件中。例如,将创建一个包含本例中两个对象模块的名为 libhello.a 的静态库,输入以下格式的命令:

ar -r libhello.a firsthello.o secondhello.o

调用该库中的这两个函数,编写一个名为 uselib.c 的程序,该程序的代码如下:

/* uselib.c */
void firsthello(void);
void secondhello(void);

int main(int argc,char *argv[])
{
	firsthello();
	secondhello();
	return 0;
}

程序可以通过在命令行中指定库用一条命令来编译和链接,命令如下:

gcc uselib.c libhello.a -o uselib -Wall 

在终端中输入以下的命令格式,使其运行并显示结果:

./uselib

## result
This is the first hello!
This is the second hello!

静态库的命名惯例是以 lib 开头且后缀为 .a ,例如,libhello.a,这种命名约定被广泛应用于各种操作系统和编程语言中,在Linux系统中,所有的系统库都采用这种命名规则。编译器提供了 -l 选项来指定需要链接的库文件名,并且可以通过简写方式指定命令行中的库名,类似于 -l<库名> 的形式。举个例子,在上述命令中,使用 -lhello 指向了 libhello.a 静态库,将该库与 uselib.c 文件进行链接,生成可执行文件 uselib

需要注意的是,编译器会从默认的系统库目录中寻找 libhello.a 库文件,如果库文件存在于某个特定的目录下,则可以使用完整的路径名或者相对路径名来指定库文件的位置。如果是指定具体路径名,可以使用绝对路径或相对路径格式来表示。例如,假设 libhello.a 库文件存储在当前目录下的 lib 目录中,则可以使用以下命令:

gcc twohellos.c -L./lib -lhello -o twohellos -Wall 

该命令中,-L./lib 选项指定库文件在当前目录下的 lib 目录中,-lhello 选项指代 libhello.a 静态库文件,这样编译器就会到指定的目录中查找 libhello.a 静态库文件,从而实现链接操作。


构建共享库

共享库(shared library),也称为动态链接库(dynamic library),是编译器以一种特殊的方式生成的目标文件集合。在编译过程中,对象文件模块中所有地址(包括变量引用和函数调用)都是相对的而不是绝对的,这使得共享库可以在程序运行时被动态地加载,并支持多个进程同时使用,从而节省内存空间并实现代码复用。

与静态库不同,共享库并不会被链接到最终的执行文件中,而是在程序运行时才会被加载到内存中。因此,共享库具有更好的可移植性和重用性,能够在不同的应用程序中被共享使用,从而更好地实现代码复用。

构建一个共享库,首先需要编译目标文件(.o 文件)。例如,在以下示例中我们将两个源码文件 sharedfirst.csharedsecond.c 进行编译,并分别生成目标文件 sharedfirst.osharedsecond.o

/* sharedfirst.c */
#include 

void sharedfirst()
{
	printf("This is the first hello from a shared library!\n");
}
/* sharedsecond.c */
#include 

void sharedsecond()
{
	printf("This is the second hello from a shared library!\n");
}

将以上两个源码文件编译成对象文件,输入以下格式的命令:

gcc -c -fpic sharedfirst.c sharedsecond.c -Wall 

选项 -c 告诉编译器只生成 .o 的对象文件。选项 -fpic 使生成的对象模块采用浮动的(可重定位的)地址。

下面的 gcc 命令将对象文件构建成一个名为 hello.so 的共享库:

gcc -shared shellofirst.o shellosecond.o -o hello.so

在编译共享库时,没有一个明显的入口点函数,如 main() 函数,在程序运行时被调用,因此需要使用选项 -shared 告诉编译器创建一个共享库而不是可执行文件。这个选项告诉编译器不要依据操作系统约定的入口地址进行链接,而是将编译出的目标文件集合作为共享对象来处理。

由于编译器能够识别文件后缀名 .c ,并知道如何将其编译成为目标文件,可以省略编译步骤,直接通过一条以下格式的命令将直接编译和链接两个源码文件 sharedfirst.csharedsecond.c,并将它们构建成为一个共享库 libshello.so。在这种情况下,编译器会自动将源码文件编译为目标文件,并将这些目标文件链接为共享库:

gcc -fpic -shared sharedfirst.c sharedsecond.c -o hello.so -Wall 

编写一个程序文件名为 uessharedlib.c 调用共享库中两个函数的主程序,该程序的代码如下:

/* usesharedlib.c */
void sharedfirst(void);
void sharedsecond(void);

int main(int argc,char *argv[])
{
	sharedfirst();
	sharedsecond();
	return 0;
}

该程序可以用下面的命令编译并链接共享库:

gcc usesharedlib.c hello.so -o usesharedlib -Wall 

运行它必须让其能定位到共享库 hello.so ,因为库中的函数要在程序运行时被加载。 需要注意的是,当前工作目录可能不在共享库的查找路径中,因此需要使用如下的命令行设定环境变量 LD_LIBRARY_PATH

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./

在终端中输入以下的命令格式,使其运行并显示结果:

./usesharedlib

## result
This is the first hello from a shared library!
This is the second hello from a shared library!

C++ 程序编译


单个源文件生成可执行程序

通过以下一个简单的 C++ 程序为例,该程序的代码如下:

/* hello.cpp */
#include 

int main(int argc,char *argv[])
{
    std::cout << "Hello cqupthao!" << std::endl;
    return(0);
}

程序使用定义在头文件 iostream 中的 cout,向标准输出写入一个简单的字符串,最简单直接的编译以上代码为可执行程序的方法是将该代码保存为文件 hello.cpp ,并在相应目录下执行如下指令:

g++ hello.cpp -Wall

编译器 g++ 通过检查命令行中指定的文件的后缀名可识别其为 C++ 源代码文件。编译器默认的动作:编译源代码文件生成对象文件,链接对象文件和 libstdc++ 库中的函数得到可执行程序后删除对象文件。编译器默认未指定可执行程序的文件名为 a.out ,在终端中输入以下形式便可使其运行并显示结果:

./a.out

## result
Hello cqupthao!

可以通过选项 -o 来指定所生成指定文件名的可执行程序,例如,输入以下的命令生成名为 hello 的可执行程序:

g++ hello.cpp -o hello -Wall

在终端中输入以下的命令格式,使其运行并显示结果:

./hello

## result
Hello cqupthao!

注意:g++ 是将 gcc 默认语言设为 C++ 的一个特殊的版本,链接时它自动使用 C++ 标准库而不用 C 标准库。

通过遵循源码的命名规范并指定对应库的名字,用 gcc 来编译链接 C++ 程序是可行的,例如,输入以下格式的命令:

gcc hello.cpp -lstdc++ -o hello -Wall

在终端中输入以下的命令格式,使其运行并显示结果:

./hello

## result
Hello cqupthao!

选项 -l (ell) 通过添加前缀 lib 和后缀 .a 将跟随它的名字变换为库的名字 libstdc++.a ,而后它在标准库路径中查找该库。gcc 的编译过程和输出文件与 g++ 是完全相同的。

在大多数系统中,GCC 安装时会安装一名为 c++ 的程序,它和 g++ 是等同。例如,输入以下格式的命令:

c++ hello.cpp -o hello -Wall

在终端中输入以下的命令格式,使其运行并显示结果:

./hello

## result
Hello cqupthao!

多个源文件生成可执行程序

若多个源码文件在 g++ 命令中指定,它们都将被编译并被链接成一个单一的可执行文件。例如,以下的一个名为 speak.h 的头文件,它包含一个仅含有一个函数的类的定义,该程序的代码如下:

/* speak.h */
#include 

class Speak
{
    public:
        void sayHello(const char *);
};

以下的程序代码列出的是文件 speak.cpp 的内容,包含 sayHello() 函数的函数体:

/* speak.cpp */
#include "speak.h"

void Speak::sayHello(const char *str)
{
    std::cout << "Hello " << str << "\n";
}

编写文一个件名为 hellospeak.cpp 的程序使用 Speak 类的,该程序的代码如下:

/* hellospeak.cpp */
#include "speak.h"

int main(int argc,char *argv[])
{
    Speak speak;
    speak.sayHello("cqupthao!");
    return(0);
}

将上述两个源码文件编译链接成一个单一的可执行程序,输入以下格式的命令:

g++ hellospeak.cpp speak.cpp -o hellospeak -Wall

在终端中输入以下的命令格式,使其运行并显示结果:

./hellospeak

## result
Hello cqupthao!

注意:在 speak.cpp 中包含有 #include "speak.h" 这句代码,它的意思是搜索系统头文件目录之前将先在当前目录中搜索文件 “speak.h“ ,而其在该目录中,故不需再在命令中指定。


源文件生成对象文件

当编译过程中可使用选项 -c 来告诉编译器编译源代码但不要执行链接,输出结果为对象文件,文件默认名与源码文件名相同,只是将其后缀变为 .o 。例如,以下格式的命令将编译源码文件 hellospeak.cpp 和生成对象文件 hellospeak.o

g++ -c hellospeak.cpp -Wall

g++ 能识别 .o 文件并将其作为输入文件传递给链接器。例如,输入以下格式的命令将编译源码文件为对象文件并将其链接成单一的可执行程序:

g++ -c hellospeak.cpp -Wall 
g++ -c speak.cpp -Wall 
g++ hellospeak.o speak.o -o hellospeak 

在终端中输入以下的命令格式,使其运行并显示结果:

./hellospeak

## result
Hello cqupthao!

选项 -o 不仅仅能用来命名可执行文件,它也用来命名编译器输出的其它文件。例如,除了中间的对象文件有不同的名字外,下列命令生将生成和上面完全相同的可执行文件:

g++ -c hellospeak.cpp -o hellotmp.o -Wall
g++ -c speak.cpp -o speaktmp.o -Wall
g++ hellotmp.o speaktmp.o -o hellospeak 

在终端中输入以下的命令格式,使其运行并显示结果:

./hellospeak

## result
Hello cqupthao!

编译预处理

选项 -E 使 g++ 将源代码用编译预处理器处理后不再执行其它动作。例如,将预处理源码文件 hellospeak.cpp 并将结果显示在标准输出中,输入以下格式的命令:

g++ -E hellospeak.cpp

预处理过的文件后缀为 .ii ,它可以通过 -o 选项来生成,例如,输入以下格式的命令:

gcc -E hellospeak.cpp -o hellospeak.ii

生成汇编代码

选项 -S 指示编译器将程序编译成汇编语言,输出汇编语言代码而后结束。例如,将由 C++ 源码文件 hellospeak.cpp 生成汇编语言文件 hellospeak.s ,输入以下格式的命令:

g++ -S hellospeak.cpp

汇编语言的形式依赖于编译器的目标平台,如果多个源码文件被编译,每个文件将分别产生对应的汇编代码模块。


构建静态库

静态库是编译器生成的一系列对象文件的集合。链接一个程序时用库中的对象文件还是目录中的对象文件都是一样的,库中的成员包括普通函数,类定义,类的对象实例等等。静态库的另一个名字叫归档文件,管理这种归档文件的工具叫 ar

例如,创建两个对象模块,然后用其生成静态库。编写一个头文件名为 say.h 的程序包含函数 sayHello() 的原型和类 Say 的定义,该程序的代码如下:

/* say.h */
#include 

void sayhello(void);

class Say {
    private:
        char *string;
    public:
        Say(char *str)
        {
            string = str;
        }
        void sayThis(const char *str)
        {
            std::cout << str << " from a static library!\n";
        }
        void sayString(void);
};

以下文件名为 say.cpp 的程序是需要加入到静态库中的两个对象文件之一的源码,它包含 Say 类中 sayString() 函数的定义体和类 Say 的一个实例 librarysay 的声明,该程序的代码如下:

/* say.cpp */
#include "say.h"

void Say::sayString()
{
    std::cout << string << "\n";
}
 
Say librarysay("Library instance of Say!");

源码文件名为 sayhello.cpp 的程序是需要加入到静态库中的第二个对象文件的源码,它包含函数 sayhello() 的定义,该程序的代码如下:

/* sayhello.cpp */
#include "say.h"

void sayhello()
{
    std::cout << "The hello from a static library!\n";
}

以下格式的命令序列将源码文件编译成对象文件,命令 ar 将其存进库中:

g++ -c sayhello.cpp -Wall
g++ -c say.cpp -Wall
ar -r libsay.a sayhello.o say.o

编写一个主程序 saymain.cpp 调用库 libsay.a 中的相应的程序代码:

/* saymain.cpp */
#include "say.h"

int main(int argc,char *argv[])
{
    extern Say librarysay;
    Say localsay = Say("Local instance of Say!");
    sayhello();
    librarysay.sayThis("howdy");
    librarysay.sayString();
    localsay.sayString();
    return(0);
}

该程序可以使用以下格式的命令来进行编译和链接:

g++ saymain.cpp libsay.a -o saymain

在终端中输入以下的命令格式,使其运行并显示结果:

./saymain

## result
The hello from a static library!
howdy from a static library!
Library instance of Say!
Local instance of Say!

  • 参考书籍:《Linux嵌入式C开发》(华清远见 著)

你可能感兴趣的:(软件工具的使用系列,linux,c语言,c++)