C语言编译原理介绍

本文详细介绍了C语言的编译原理,包括预处理、编译、汇编和链接四个阶段。预处理涉及宏定义、条件编译等,编译阶段进行词法、语法和语义分析,汇编阶段生成汇编代码,链接器则负责处理模块间的相互引用,形成可执行程序。同时,文章探讨了宏定义的使用,包括宏常量、宏表达式和宏代码块,并强调了条件编译在工程实践中的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

c语言编译原理图


1、预处理指令:gcc -E file.c -o hello.i

# 删除所有的注释,以空格代替

# 将所有的#define删除,并且展开所有的宏定义

# 处理条件编译指令#if,#ifdef,#elif,#else,#endif

    指令         用途
    #           空指令,无任何效果
    #include    包含一个源代码文件
    #define     定义宏
    #undef      取消已定义的宏
    #if         如果给定条件为真,则编译下面代码
    #ifdef      如果宏已经定义,则编译下面代码
    #ifndef     如果宏没有定义,则编译下面代码
    #elif       如果前面的#if给定条件不为真,当前条件为真,则编译下面代码,其实就是else if的简写
    #endif      结束一个#if……#else条件编译块
    #error      停止编译并显示错误信息

#处理#include,展开被包含的文件

#保留编译器需要使用的#pragma指令

'#pragma'它的作用是设定编译器的状态或者是指示编译器完成一些特定的动作

2、编译指令:gcc -S file.c -o hello.S

# 对预处理文件进行一系列词法分析,语法分析和语义分析

词法分析主要分析关键字,标示符,立即数等是否合法

语法分析主要分析表达式是否遵循语法规则

语义分析在语法分析的基础上进一步分析表达式是否合法

# 分析结束后进行代码优化生成相应的汇编代码文件

3、汇编指令:gcc -c file.S -o hello.o

# 汇编器将汇编代码转变为机器可以执行的指令

每个汇编语句几乎都对应一条机器指令

4、链接器的意义

链接器的主要作用是把各个模块之间的相互引用的部分处理好,使得各个模块之间能够正确的衔接。

链接分为静态链接和动态链接:静态链接指的是.o文件与链接库.a文件拼接在一起,组成可执行文件。这样编译出的可执行文件较大,也占用较大的内存空间,如果可执行程序,被并发运行多次,则会重复调用.a链接库文件进入内存。这样就造成了很大的浪费。静态链接是以空间换取时间。


动态链接是指,链接库文件不是和.o文件拼接在一起,而是当执行文件运行时,才动态的去内存调用库文件,如果没有就动态的加载。动态链接是以时间换取空间。


# 编译器将编译工作主要分为预处理,编译和汇编三部分

# 链接器的作用是把各个独立的模块链接为可执行程序

# 静态链接在编译期完成,动态链接在运行期完成

一、宏定义与使用分析
1、定义宏常量
#define定义宏常量可以出现在代码的任何地方
#define从本行开始,之后的代码都可以使用这个宏常量

宏定义的常量或表达式没有作用域的限制。

下面是示例代码:
example2-1.c
#include <stdio.h>

int f1(int a, int b)
{
    #define _MIN_(a,b) ((a)<(b) ? a : b) 
    return _MIN_(a, b);
    
    //#undef _MIN_//取消已经定义的宏,如何使用,就会报错,因为f2中找不到_MIN_
}

int f2(int a, int b, int c)
{
    return _MIN_(_MIN_(a,b), c);
}

int main()
{
    printf("%d\n", f1(2, 1));
    printf("%d\n", f2(5, 3, 2));
    
    return 0;
}
如下定义宏常量都正确:
#define ERROR -1
#define PI 3.1415926
#define PATH_2 “D:\delphi\c\topic3.ppt”
#define PATH_1 D:\delphi\c\topic3.ppt
#define PATH_3 D:\delphi\c\                                                 // 续接符
topic3.ppt
因为宏常量在预处理阶段只是做简单的宏替换,所以不会出现错误。除非在编译阶段做语法检查时,可能会出现语法错误。
2、定义宏表达式
1、#define表达式给有函数调用的假象,却不是函数
2、#define 表达式可以比函数更强大
3、#define表达式比函数更容易出错
#define sum(a,b) (a)+(b)
#define min(a, b) ((a)<(b) ? (a) : (b))
#define dim(a) (sizeof(a)/sizeof(*a))
下面是代码演示:
text.c
#include <stdio.h>
#define sum(a, b) ((a) + (b))  //如果不加上括号,容易出现错误
#define min(a, b) ((a<b) ? (a) : (b))
#define dim(array) (sizeof(array)/sizeof(*array))

int main()
{
printf("%d\n", sum(1, 2) * sum(1, 2));
printf("%d\n", min(3, 4));
    return 0;
}
3、宏代码块的定义:
2-1.c
#include <stdio.h>
#include <malloc.h>

#define MALLOC(type, x) (type*)malloc(sizeof(type)*x)
#define FOREVER() while(1)

#define BEGIN {
#define END   }
#define FOREACH(i, m) for(i=0; i<m; i++)

int main()
{
    int array[] = {1, 2, 3, 4, 5};
    int x = 0;
    int*p = MALLOC(int, 5);
    
    FOREACH(x, 5)
    BEGIN
        p[x] = array[x];
    END
    
    FOREACH(x, 5)
    BEGIN
        printf("%d\n", p[x]);
    END
    
    FOREVER();
    
    free(p);
    
    printf("Last printf...\n");
    
    return 0;
}
很好的运行宏可以拓展C语言的关键字,但#define不能实现递归
如果在程序出现错误,而没有报错行号的时候,就选择单步调试,来查找错误。
预处理,只是做替换,所以不会理会语法是否错误。
宏表达式与函数的对比
# 宏表达式在预编译期被处理,编译器不知道宏表达式的存在
#宏表达式用“实参”完全替代形参,不进行任何运算
# 宏表达式没有任何的“调用”开销
#宏表达式中不能出现递归定义
#define fac(n) ((n>0) ? (fac(n-1)+1) : 0)
int j = fac(100);   //这里的递归是无法实现的,原因就是宏只是做简单的替换。
强大的内置宏:

代码示例:教你定义日志宏
#include <stdio.h>
#include <time.h>

#define LOG(s) do{                \
        time_t t;                        \
struct tm* ti;               \
time(&t);                     \
ti = localtime(&t);            \
printf("%s", asctime(ti));  \
printf(s);                             \
}while(0)

int main()
{
LOG("Enter main()...\n");

LOG("Exit main()...\n");

return 0;
}
课后思考:
#define f (x) ((x)-1)
1、上面的宏定义代表什么意思?
f   ==  (x) ((x)-1)

2、宏定义对空格敏感吗?宏“调用”对空格敏感吗?

敏感,敏感。

二、条件编译使用分析

1、基本概念

# 条件编译的行为类似于C语言中的if...else

# 条件编译是预编译指示命令,用于控制是否编译某段代码

#define C 1

int main()

{

#if ( C == 1 )

printf("this is first printf...\n");

#else

printf("this is second printf...\n");

#endif

return 0;

}

条件编译与if语句的区别就是,虽然结果相同,但中间结果不同。

在编译时可以使用宏定义行命令来定义宏:gcc -DC=1 test.c -o test

if语句是根据条件表达式,分开来执行某行代码。而条件编译时告诉编译器要处理哪块,不处理哪块,而if语句都要编译。

2、#include的困惑

2.1#include的本质是将已经存在的文件内容嵌入到当前文件中

2.1#include的间接包含同样会产生嵌入文件内容的动作,如何来解决这样的问题,下面就用条件编译来解决此问题

// global.h
#ifndef _GLOBAL_H_   //使用条件编译避免了global.h的重复嵌入

#define _GLOBAL_H_

int global = 10;
#endif
// test.h
#ifndef _TEST_H_
#define _TEST_H_
#include <stdio.h>
#include "global.h"   //由于使用了条件编译,所以这里不会重复嵌入
const char* NAME = "Hello world!";
void f()
{
    printf("Hello world!\n");
}
#endif
// test.c
#include <stdio.h>
#include "test.h" 
#include "global.h"
int main()
{
    f();   
    printf("%s\n", NAME);  
    return 0;
}

3.条件编译的意义

3.1、条件编译使得我们可以按不同的条件编译不同的代码段,因而可以产生不同的目标代码

3.2、#if...#else...#endif被预编译器处理;而if...else语句被编译器处理,必然被编译进目标代码

3.3、实际工程中条件编译主要用于两种情况:

# 不同的产品线共用一份代码

# 区分编译产品的调试版和发布版

产品线区分及条件编译应用:

#include <stdio.h>
#ifdef DEBUG
    #define LOG(s) printf("[%s:%d] %s\n", __FILE__, __LINE__, s)
#else
    #define LOG(s) NULL
#endif
#ifdef HIGH
void f()
{
    printf("This is the high level product!\n");
}
#else
void f()
{
}
#endif
int main()
{
    LOG("Enter main() ..."); 
    f();   
    printf("1. Query Information.\n");
    printf("2. Record Information.\n");
    printf("3. Delete Information.\n");
    #ifdef HIGH
    printf("4. High Level Query.\n");
    printf("5. Mannul Service.\n");
    printf("6. Exit.\n");
    #else
    printf("4. Exit.\n");
    #endif
    LOG("Exit main() ...");
    return 0;
}

以上代码可以实现一份代码变为两个不同的产品线(利用宏技术)。

宏命令行的使用技巧:

gcc -DDEBUG -DHIGH test.c

通过宏可以使代码变得更有魅力。

4、小结

1、通过编译器命令行能够定义预处理器使用的宏

2、条件编译可以避免重复包含同一个头文件

3、条件编译是在工程开发中可以区别不同产品线的代码

4、条件编译可以定义产品的发布版和调试版

三、#error和#line

1、#error的用法

#error用于生成一个编译错误信息,并停止编译

#error message

注:message不需要用双引号包围

#error编译器指示字用于自定义程序员特有的编译错误信息

类似的,#warning用于生成编译警告,但不会停止编译

代码示例:#error和#warning的使用,自定义错误信息

#include <stdio.h>

#define CONST_NAME1 "CONST_NAME1"
#define CONST_NAME2 "CONST_NAME2"

int main()
{  
    #ifndef COMMAND
    #warning Compilation will be stoped ...    //自定义的错误报告
    #error No defined Constant Symbol COMMAND
    #endif

    printf("%s\n", COMMAND);
    printf("%s\n", CONST_NAME1);
    printf("%s\n", CONST_NAME2);

    return 0;
}

2、#line的用法

2.1、#line用于强制指定新的行号和编译文件名,并对源程序的代码重新编号

#line number filename //可以理解为对__LINE__和__FILE__的重定义

注:filename可省略

#line编译指示字的本质是重定义__LINE__和__FILE__(下划线为两个)

代码示例:

#include <stdio.h>

//用于标示自己写的代码;
//这一行向后,变为14行。文件名变为Hello.c
#line 14 "Hello.c" 

#define CONST_NAME1 "CONST_NAME1"
#define CONST_NAME2 "CONST_NAME2"

void f()
{
    return 0;
}

int main()
{
    printf("%s\n", CONST_NAME1);
    printf("%s\n", CONST_NAME2);
    printf("%d\n", __LINE__);
    printf("%s\n", __FILE__);
    
    f();

    return 0;
}

四、#pragma预处理分析

1、#pragma简介

#pragma是编译器指示字,用于指示编译器完成一些特定的动作

#pragma所定义的很多指示字是编译器和操作系统特有的

#pragma在不同的编译器间是不可移植的

预处理器将忽略它不认识的#pragma指令

两个不同的编译器可能以两种不同的方式解释同一条#pragma指令

一般的用法: #pragma parameter

注:不同的parameter参数语法和意义各不相同

#pragma message

1、message参数在大多数的编译器中都有相似的实现

2、message参数在编译时输出消息到编译输出窗口中

3、message可用于代码的版本控制

注意message是VC特有的编译指示符,GCC中将其忽略

实例代码:

#include <stdio.h>

#if defined(ANDROID20)
    #pragma message("Compile Android SDK 2.0...")
    #define VERSION "Android 2.0"
#elif defined(ANDROID23)
    #pragma message("Compile Android SDK 2.3...")
    #define VERSION "Android 2.3"
#elif defined(ANDROID40)
    #pragma message("Compile Android SDK 4.0...")
    #define VERSION "Android 4.0"
#else
    #error Compile Version is not provided!
#endif

int main()
{
    printf("%s\n", VERSION);

    return 0;
}

C语言中一个非常重要,但有容易让人忽略的知识点:#pragma pack

1、什么是内存对齐

不同类型的数据在内存中按照一定的规则排列;而不是顺序的一个接一个的排放的,这就是对齐

2、为什么需要内存对齐?

2.1、CPU对内存的读取不是连续的,而是分成块读取的,块的大小只能是1、2、4、8、16字节

2.2、当读取操作的数据未对齐,则需要两次总线周期来访问内存,因此性能会大打折扣。也就是说,字节对齐一定程度上提高了效率。

2.3、某些硬件平台只能从规定的地址处取某些特定类型的数据,否则抛出硬件异常

实例代码:

#include <stdio.h>

#pragma pack(8)

struct S1
{
    short a;    
    long b;
};
成员对齐数	要求对齐字节	结构体成员	起始地址	大小	空字节数
2		8		short a		0		2	0
4		8		long b		4		4	2
struct S2
{
    char c;
    struct S1 d;
    double e;
};
成员对齐数	要求对齐字节	结构体成员	起始地址	大小	空字节数
1		8		char c		0		1	0
4(成员最大)<span style="white-space:pre">	</span>8		struct s1 d	4		8	3
8		8		double e	16		8	4


#pragma pack()

int main()
{
    struct S2 s2;
    
    printf("%d\n", sizeof(struct S1));
    printf("%d\n", sizeof(struct S2));
    printf("%d\n", (int)&(s2.d) - (int)&(s2.c));

    return 0;
}

3、struct占用的内存大小

3.1、第一个成员起始于0偏移处

3.2、每个成员按其类型大小和指定对齐参数n中较小的一个进行对齐

3.2.1、偏移地址和成员占用大小均需对齐

3.2.2、结构体成员的对齐参数为其所有成员使用的对齐参数的最大值

3.3、结构体总长度必须为所有对齐参数的整数倍

课后问题:

结构体变量是否可以直接用memcmp函数进行相等判断?为什么?

答:不可以。因为结构体相等是指,其含有相同的成员变量。而由于对齐方式不同,会导致成员在内存中的位置发生变化。而这就不可以使用memcmp函数通过内存空间数据来判断结构体是否相等。

五、#和##号

1、#运算符

#运算符用于在预编译期将宏参数转换为字符串

#include <stdio.h>

#define CONVERS(x) #x

int main()
{
    
    printf("%s\n", CONVERS(Hello world!));
    printf("%s\n", CONVERS(100));
    printf("%s\n", CONVERS(while));
    printf("%s\n", CONVERS(return));

    return 0;
}
运行结果:

Hello world!

100

while

return

#运算符在宏中的妙用

#include <stdio.h>

#define CALL(f, p) (printf("Call function %s\n", #f), f(p)) //#只能在宏中使用
   
int square(int n)<pre code_snippet_id="275014" snippet_file_name="blog_20140404_4_4836671" name="code" class="cpp">#include <stdio.h>

#define NAME(n) name##n

int main()
{
    
    int NAME(1);
    int NAME(2);
    
    NAME(1) = 1;
    NAME(2) = 2;
    
    printf("%d\n", NAME(1));
    printf("%d\n", NAME(2));

    return 0;
}

{ return n * n;}int f(int x){ return x;}int main(){ printf("1. %d\n", CALL(square, 4));
 
//调用sequare函数 printf("2. %d\n", CALL(f, 10)); //调用f函数 return 0;}

 2、##运算符用于在预编译期粘连两个符号 

#include <stdio.h>

#define NAME(n) name##n

int main()
{
    
    int NAME(1);
    int NAME(2);
    
    NAME(1) = 1;
    NAME(2) = 2;
    
    printf("%d\n", NAME(1));
    printf("%d\n", NAME(2));

    return 0;
}

/*高通使用的代码*/

#include <stdio.h>
#define STRUCT(type) typedef struct _tag_##type type;\
struct _tag_##type

STRUCT(Student)
{
    char* name;
    int id;
};

/*
typedef struct _tag_student student;\
struct _tag_student
{
    char* name;
    int id;
}
*/

int main()
{
    
    Student s1;
    Student s2;
    
    s1.name = "s1";
    s1.id = 0;
    
    s2.name = "s2";
    s2.id = 1;
    
    printf("%s\n", s1.name);
    printf("%d\n", s1.id);
    printf("%s\n", s2.name);
    printf("%d\n", s2.id);

    return 0;
}


                
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值