首页 > 编程语言 >C和CPP程序是如何运行起来的?

C和CPP程序是如何运行起来的?

时间:2023-10-05 12:11:38浏览次数:41  
标签:00 头文件 定义 程序 预处理 CPP 替换 运行 define

C和CPP程序是如何运行起来的?


个人见解,谨慎阅读。
如有错误,欢迎指正!


代码均在Linux下编译运行。

1. C语言程序从源码到可执行文件的过程

C语言程序从源码到可执行文件的过程主要分为以下几个步骤:预处理、编译、汇编、链接。

flowchart LR A1[代码] --"预处理"--> B1[预处理文件] --"编译"--> C1[编译代码] --"汇编"--> D1[机器代码] --"链接"--> F[可执行文件]
  1. 源码:首先,编写C语言源代码文件,通常以.c作为文件扩展名。这个源代码包括程序的逻辑、变量、函数等。
    这里为了举例,我们设置了一个头文件和一个源文件,并在源文件里包含头文件,并设置了宏用于举例。

h.h

//这里是头文件,包含了两个变量的声明
int a;
int b;
int c;

c.c

#include "h.h"
#define A 3
//这里是源码,包含了两个变量的定义
//一个宏的定义
//一个函数的定义
int main(void)
{
    //这里是函数体
    a = 1;
    b = 2;
    c = a + b;
    c = A;
    return 0;
}
  1. 预处理:预处理是在编译之前进行的,主要是对源代码进行一些处理,比如把#include的文件包含进来,把#define定义的宏展开,把注释去掉等。生成一个经过预处理的源文件,通常以 .i 作为文件扩展名。
    上述代码经过预处理后的结果如下:

c.i

# 1 "c.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "c.c"
# 1 "h.h" 1

int a;
int b;
int c;
# 2 "c.c" 2




int main(void)
{

    a = 1;
    b = 2;
    c = a + b;
    c = 3;
    return 0;
}
  1. 编译:编译器将预处理后的源代码翻译成汇编代码。生成的汇编代码通常以.s作为文件扩展名。
    上述代码经过编译后的结果如下:

c.s

        .file   "c.c"
        .text
        .comm   a,4,4
        .comm   b,4,4
        .comm   c,4,4
        .globl  main
        .type   main, @function
main:
.LFB0:
        .cfi_startproc
        endbr64
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        movl    $1, a(%rip)
        movl    $2, b(%rip)
        movl    a(%rip), %edx
        movl    b(%rip), %eax
        addl    %edx, %eax
        movl    %eax, c(%rip)
        movl    $3, c(%rip)
        movl    $0, %eax
        popq    %rbp
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE0:
        .size   main, .-main
        .ident  "GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0"
        .section        .note.GNU-stack,"",@progbits
        .section        .note.gnu.property,"a"
        .align 8
        .long    1f - 0f
        .long    4f - 1f
        .long    5
0:
        .string  "GNU"
1:
        .align 8
        .long    0xc0000002
        .long    3f - 2f
2:
        .long    0x3
3:
        .align 8
4:
  1. 汇编:汇编器将汇编代码翻译成机器代码,生成一个目标文件。目标文件通常以.o(在Unix/Linux系统中)或.obj(在Windows系统中)作为文件扩展名。目标文件包含二进制指令、数据和符号表信息。
    使用objdump -t -S c.o命令,上述代码经过汇编后的结果如下:

c.o

c.o:     file format elf64-x86-64

SYMBOL TABLE:
0000000000000000 l    df *ABS*  0000000000000000 c.c
0000000000000000 l    d  .text  0000000000000000 .text
0000000000000000 l    d  .data  0000000000000000 .data
0000000000000000 l    d  .bss   0000000000000000 .bss
0000000000000000 l    d  .note.GNU-stack        0000000000000000 .note.GNU-stack
0000000000000000 l    d  .note.gnu.property     0000000000000000 .note.gnu.property
0000000000000000 l    d  .eh_frame      0000000000000000 .eh_frame
0000000000000000 l    d  .comment       0000000000000000 .comment
0000000000000004       O *COM*  0000000000000004 a
0000000000000004       O *COM*  0000000000000004 b
0000000000000004       O *COM*  0000000000000004 c
0000000000000000 g     F .text  0000000000000041 main



Disassembly of section .text:

0000000000000000 <main>:
   0:   f3 0f 1e fa             endbr64
   4:   55                      push   %rbp
   5:   48 89 e5                mov    %rsp,%rbp
   8:   c7 05 00 00 00 00 01    movl   $0x1,0x0(%rip)        # 12 <main+0x12>
   f:   00 00 00
  12:   c7 05 00 00 00 00 02    movl   $0x2,0x0(%rip)        # 1c <main+0x1c>
  19:   00 00 00
  1c:   8b 15 00 00 00 00       mov    0x0(%rip),%edx        # 22 <main+0x22>
  22:   8b 05 00 00 00 00       mov    0x0(%rip),%eax        # 28 <main+0x28>
  28:   01 d0                   add    %edx,%eax
  2a:   89 05 00 00 00 00       mov    %eax,0x0(%rip)        # 30 <main+0x30>
  30:   c7 05 00 00 00 00 03    movl   $0x3,0x0(%rip)        # 3a <main+0x3a>
  37:   00 00 00
  3a:   b8 00 00 00 00          mov    $0x0,%eax
  3f:   5d                      pop    %rbp
  40:   c3                      retq
  1. 链接:链接器将目标文件与标准库和其他必要的库文件链接在一起,创建可执行文件。链接的过程包括符号解析、地址重定向和符号解析等步骤。生成的可执行文件通常没有文件扩展名,或者以.exe(在Windows系统中)或 无扩展名(在Unix/Linux系统中)作为文件扩展名。

  2. 可执行文件:最终生成的可执行文件包含了程序的所有机器代码和数据,可以在相应的操作系统上运行。

2. C语言程序是如何预处理的

预处理器执行宏替换、条件编译以及包含指定的文件,以#开头的命令行(#前可以有空格)就是预处理器处理的对象。这些命令行的语法独立于语言的其它部分,它们可以出现在任何地方,其作用可延续到所在翻译单元的末尾(与作用域无关)。

2.1 预处理过程

预处理过程主要分为以下几个步骤:

  1. 处理多字节字符和三字符序列:将多字节字符和三字符序列转换为单字节字符。这里需要提到的是,编译器会把多个连续字符串常量合并为一个字符串常量,这个过程发生在预处理阶段。比如:"hello" "world"会被合并为"helloworld"
  2. 处理行连接符:将行连接符\和换行符\n组合成一个新行。
  3. 划分序列:编译器将文本划分成预处理记号序列、空白序列和注释序列。
    • 预处理记号序列:预处理记号是C源代码中的基本单位,它们是编程语言中的关键字、标识符、文字、操作符等。预处理记号序列是一组连续的预处理记号,它们在预处理阶段被处理和解析。
    • 空白序列:由一个或多个空格、制表符、换行符等空白字符组成的序列。
    • 注释序列:由注释组成的序列。值得注意的是,编译器将用一个空格替换每个注释序列,以便于后续处理。
  4. 处理预处理指令:编译器将预处理指令替换为相应的文本。
    • 头文件包含:将#include指令替换为指定的头文件内容。
    • 宏替换:将#define指令替换为指定的替换体内容。
    • 条件编译:根据#if#ifdef#ifndef等指令的真假来决定是否编译后面的代码块。
    • 其他指令:将#undef#pragma等指令替换为相应的文本。

2.2 明示常量:#define指令

#define指令用于定义宏,它有两种形式:

  • #define 宏:用于定义宏,简单地标记宏为已定义,而不进行任何文本替换。这通常用于创建宏常量或用于条件编译中的标记。
  • #define 宏 替换体:用于定义宏,将代码中的宏名替换为指定的替换体内容。从宏变成最终替换文本的过程称为宏展开。

“明示常量”这一词来自《C Primer Plus第6版 中文版》[1],个人认为这是一个历史遗留词语,因为C语言一开始没有const关键字,所以用#define来定义常量。

预处理器指令从#开始,直到后面的第一个换行符为止。使用\可以将一行代码分成多行,但是\后面不能有空格。按照前文所述,预处理器会处理行连接符。

每行#define(逻辑行)都由3部分组成。第1部分是#define指令本身。第2部分是选定的缩写也称为宏。有些宏代表值,这些宏被称为类对象宏。C语言还有类函数宏,稍后讨论。宏的名称中不允许有空格,而且必须遵循C变量的命名规则:只能使用字符、数字和下划线(_)字符,而且首字符不能是数字。第3部分(指令行的其余部分)称为替换列表或替换体。一旦预处理器在程序中找到宏的示实例后,就会用替换体代替该宏(也有例外,稍后解释)。从宏变成最终替换文本的过程称为宏展开。注意,可以在#define行使用标准C注释。如前所述,每条注释都会被一个空格代替。[1:1]

2.2.1 宏定义的替换体

宏定义的替换体可以是任何字符序列,除特别声明的几个特殊形式,其余字符在对应的宏的位置以纯文本形式进行替换。例如:

#define TWO 2
#define OW "Consistency is the last refuge of the unimagina\
tive. - Oscar Wilde"
#define FOUR TWO*TWO
#define PX printf("X is %d.\n", x)

2.2.2 类函数宏

类函数宏是一种特殊的宏,它们看起来像函数调用。例如:

#define SQUARE(X) X*X

这里,SQUARE是宏标识符,SQUARE(X)中的X是宏参数,X*X是替换列表。后续代码里,出现SQUARE(X)的地方都会被X*X替换。这与前面的示例不同,使用该宏时,既可以用X,也可以用其他符号。宏定义中的X由宏调用中的符号代替。因此,SUARE(2)替换为2*2X实际上起到参数的作用。

2.2.3 记号

从技术角度来看,可以把宏的替换体看作是记号(token)型字符串,而不是字符型字符串。C预处理器记号是宏定义的替换体中单独的“词”。用空白把这些词分开[1:2]。例如:

#define FOUR 2*2

这个宏定义用一个记号:2*2。但是,如果宏定义是这样的:

#define SIX 2 * 3

那么,宏定义用三个记号:2*3

解释为字符型字符串,把空格视为替换体的一部分;解释为记号型字符串,把空格视为替换体中各记号的分隔符。在实际应用中,一些C编译器把宏替换体视为字符串而不是记号[1:3]

这里提到这个概念,主要是想要说明,宏替换体中的空格是有意义的,不是随便加的。这表明宏替换是源代码上的纯文本替换。

2.2.4 #运算符

#运算符是一元运算符,它的作用是将宏参数转换为字符串。例如:

#define PRINT(x) printf(#x " = %d\n", x)

这个宏定义用到了#运算符,它的作用是将宏参数x转换为字符串。这样,PRINT(a)就会被替换为printf("a" " = %d\n", a),这样就可以打印出变量名和变量值了。

2.2.5 ##运算符

##运算符是连接运算符,它的作用是将两个记号连接成一个记号。例如:

#define PRINT(n) printf("x" #n " = %d\n", x##n);

这个宏定义用到了##运算符,它的作用是将两个记号连接成一个记号。这样,PRINT(1)就会被替换为printf("x" "1" " = %d\n", x1);,这样就可以打印出变量名和变量值了。

2.3 预处理命令

C语言里的预处理命令主要有以下几个:

  • #include <头文件>:用于包含系统标准库头文件,通常用于引入标准库函数和类型的声明。
  • #include "头文件":用于包含用户自定义的头文件,通常用于引入自定义函数和类型的声明。
  • #define 宏:用于定义宏,简单地标记宏为已定义,而不进行任何文本替换。这通常用于创建宏常量或用于条件编译中的标记。
  • #define 宏 替换体:用于定义宏,将代码中的宏名替换为指定的替换体内容。从宏变成最终替换文本的过程称为宏展开。
  • #undef 宏:用于取消已定义的宏,将其从预处理符号表中删除。
  • #ifdef 宏:用于检查某个宏是否已经定义,如果定义了就执行后面的代码块。
  • #ifndef 宏:用于检查某个宏是否未定义,如果未定义就执行后面的代码块。
  • #if 表达式:用于条件编译,根据指定的表达式的真假来决定是否编译后面的代码块。
  • #elif 表达式:用于在多个条件之间切换,类似于if-else if结构,用于条件编译。
  • #else:与#ifdef#ifndef#if等一起使用,表示条件不成立时要执行的代码块。
  • #endif:用于结束条件编译的代码块。
  • #pragma 指令:用于向编译器发送特定的指令或控制编译器的行为,具体指令和效果因编译器而异。

2.4 文件包含:#include指令

#include指令用于包含头文件,它有两种形式:

  • #include <头文件>:用于包含系统标准库头文件,通常用于引入标准库函数和类型的声明。
  • #include "头文件":用于包含用户自定义的头文件,通常用于引入自定义函数和类型的声明。

具体而言,对于<>包含的头文件,预处理器会在系统标准库的目录下查找;对于""包含的头文件,预处理器会在当前目录下查找,如果找不到,再去系统标准库的目录下查找。
当预处理器发现#include指令时,会查看后面的文件名并把文件的内容包含到当前文件中,即替换源文件中的#include指令。这相当于把被包含文件的全部内容输入到源文件#include指令所在的位置。

2.4.1 头文件

头文件包含了函数和类型的声明。头文件通常以.h作为文件扩展名。头文件中最常见的内容如下[1:4]

  • 明示常量:例如,stdio.h中定义的EOF、NULL和 BUFSTZE(标准IO缓冲区大小)。
  • 宏函数:例如,getc(stdin)通常用getchar()定义,而getc()经常用于定义较复杂的宏,头文件ctype.h通常包含ctype系列函数的宏定义。
  • 函数声明:例如,string. h头文件(一些旧的系统中是strings.h)包含字符串函数系列的函数声明。在ANSIC和后面的标准中,函数声明都是函数原型形式。
  • 结构模版定义:标准VO函数使用FILE结构,该结构中包含了文件和与文件缓冲区相关的信息。FILE结构在头文件stdio.h 中。
  • 类型定义:标准IO 函数使用指向FILE的指针作为参数。通常,stdio.h 用#define或typedef把FILE定义为指向结构的指针。类似地,size_t和time_t类型也定义在头文件中。

2.5 实例分析:预处理过程

h.h

//这里是头文件,包含了两个变量的声明
int a;
int b;
int c;

c.c

#include "h.h"
#define A 3
//这里是源码,包含了两个变量的定义
//一个宏的定义
//一个函数的定义
int main(void)
{
    //这里是函数体
    a = 1;
    b = 2;
    c = a + b;
    c = A;
    return 0;
}

预处理后:

c.i

# 1 "c.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "c.c"
# 1 "h.h" 1

int a;
int b;
int c;
# 2 "c.c" 2




int main(void)
{

    a = 1;
    b = 2;
    c = a + b;
    c = 3;
    return 0;
}

尽管有部分符号还不理解,但是通过上文的内容,通过这个例子,我们仍然可以看出预处理器的大致逻辑。首先,预处理器会把#include的文件包含进来,然后把#define定义的宏展开,最后把注释去掉。比如:

  • 预处理器把#include "h.h"替换为int a; int b; int c;
  • 预处理器把#define A 3替换为c = 3;
  • 预处理器把//这里是源码,包含了两个变量的定义替换为空。

//其实到这个位置对于C和C++入门来说,已经足够了。后期有时间,再补充。
//TODO



参考与注释:


  1. 《C Primer Plus第6版 中文版》 Stephen Prata 人民邮电出版社 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

标签:00,头文件,定义,程序,预处理,CPP,替换,运行,define
From: https://www.cnblogs.com/BryceAi/p/17742909.html

相关文章

  • 程序员能纯靠技术渡过中年危机吗?
     作者:3R教室-pincman链接:https://www.zhihu.com/question/264237428/answer/2860296073来源:知乎著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。⚡请看完这个哈:此贴只作分享并为同是码农的你提供些思路,同时打点广告。但是由于个人比较忙,请尽量不要......
  • 实验1_c语言输入输出和简单程序应用编程
    实验一1-1#include<stdio.h>intmain(){printf("O\n");printf("<H>\n");printf("II\n");printf("O\n");printf("<H>\n");printf("II\n");......
  • 小目标1:编写一个基本的TCP服务器程序
    小目标1:编写一个基本的TCP服务器程序头文件1#include<cstdio>//C++标准库的头文件2#include<unistd.h>//Unix标准头文件3#include<sys/types.h>//这个头文件定义了各种系统相关的数据类型4#include<sys/socket.h>//这个头文件用于网络编程,包含了与套接字(socket)相关......
  • PowerBuilder现代编程方法X11:PB程序完全跨平台方案
     PB可能要支持Windows、macOS、Linux、iOS、Android与鸿蒙操作系统和X86、ARM、RISC-V与国产龙芯CPU的原生应用了! PowerBuilder现代编程方法X11:PB程序完全跨平台方案 前言《PowerBuilder编程新思维》在写到了WebUI后,陷入了沉寂。原因是我对PB发展的下一代技术方案不太满......
  • 实验1 C语言输入输出和简单程序编写
    1.试验任务1  task1.c//打印一个字符小人#include<stdio.h>intmain(){printf("o\n");printf("<H>\n");printf("II\n");return0;} task1_1.c//在垂直方向上打印出两个小人#include<stdio.h>int......
  • 小程序底层技术机制解读 - 小程序的社交能力
    小程序的社交能力是其成功的关键之一,它允许用户在应用内与其他用户互动、分享内容和建立社交关系。了解小程序的社交能力技术和机制对于开发者来说非常重要,因为它可以帮助他们更好地利用社交功能来增加用户粘性和扩大用户群体。本文将深入解读小程序的社交能力的底层技术机制,包括用......
  • 小程序技术未来发展的思考 - 高级动画和效果
    微信小程序、支付宝小程序等已经成为移动应用开发的主要方式之一,而动画和特效是提高用户体验和吸引用户的重要因素之一。未来的小程序技术将继续发展,提供更高级的动画和效果功能,以满足开发者的创意和用户的需求。在本文中,我们将探讨小程序技术在高级动画和效果方面的发展趋势,并提供......
  • 一个java程序员,手撸app的日记(一)
    首先,我是一名多年的java后端程序员,但刚接触此行的时候,还是写过jsp页面的,因为当年不懂,以为sp页面也是java的一部分,就闷着头给公司写了起来(只想说,html好写,但css是真的难)。jsp的编写是在自己经验不足的年纪,写了不到半年,草草了事,只是学会了ajax和部分js的编写(只觉得js真简单,弱类型,且......
  • MapReduce运行模式
    1、yarn集群运行先将之前写好的MapReduce程序进行打包--Maven-->package;打包完成之后的jar包在target目录下可以找到!!!之后将jar包上传到我们的虚拟机文件夹里面去;之后输入命令:hadoopjarjar包名称jar包主类的全路径名称回车之后开始运行;在hdfs的浏览器界面(9870)能够找到......
  • 活动报名与缴费小程序开发笔记一
    项目背景活动报名与缴费小程序的开发背景主要源于以下几个因素:1.数字化时代的需求:随着移动互联网和智能手机的普及,人们习惯使用手机进行各种活动。传统的纸质报名表格和线下缴费方式变得相对繁琐,而数字化报名与缴费小程序提供了更便捷的解决方案。2.提高效率和减少人力成本:对于活......