您当前的位置:首页 > 计算机 > 编程开发 > C语言

C语言编译和预处理

时间:02-10来源:作者:点击数:

编译

C语言编译过程:

  1. 预编译:将.c中的头文件展开、宏定义展开、条件编译等,同时将代码中的注释删除,生成.i文件。这里并不会检查语法。
  2. 编译:将.i文件生成.s汇编文件(将C语言翻译为汇编语言)。这里会检查语法。
  3. 汇编:将.s文件生成.o目标文件(将汇编语言翻译为机器语言,二进制文件)
  4. 链接:将.o文件链接成目标文件(可执行程序,如.exe文件),C语言写的程序是需要依赖各种库的,所以编译之后还需要把依赖到的库链接到最终的可执行程序中去。

编译命令:

假设某个目录下有main.c文件,如下:

#include<stdio.h>
int main() {
	int a = 5;
	int b = 6;
	int c = a + b;
	printf("c = %d\n", c);
	return 0;
}

打开cmd窗口,并切换到main.c文件所在目录,然后执行下面的命令进行编译:

gcc -E main.c -o main.i // 1、预处理
gcc -S main.i -o main.s // 2、编译
gcc -c main.s -o main.o // 3、汇编
gcc main.o -o main.exe  // 4、链接

经实验,发现命令参数的顺序是可以调整的,比如第一条命令,也可以这样:gcc -o main.i -E main.c,这里就是先指定输出文件,再指定源文件。

执行完第1步后,查看main.i,可看到有差不多900行,如下:

在这里插入图片描述

main函数在文件的最后,前面是#include<stdio.h>所展开的内容。

执行完第二步后,查看生成的main.s汇编文件,这次的文件内容不多,只有36行,如下:

	.file	"main.c"
	.text
	.def	__main;	.scl	2;	.type	32;	.endef
	.section .rdata,"dr"
.LC0:
	.ascii "c = %d\12\0"
	.text
	.globl	main
	.def	main;	.scl	2;	.type	32;	.endef
	.seh_proc	main
main:
	pushq	%rbp
	.seh_pushreg	%rbp
	movq	%rsp, %rbp
	.seh_setframe	%rbp, 0
	subq	$48, %rsp
	.seh_stackalloc	48
	.seh_endprologue
	call	__main
	movl	$5, -4(%rbp)
	movl	$6, -8(%rbp)
	movl	-4(%rbp), %edx
	movl	-8(%rbp), %eax
	addl	%edx, %eax
	movl	%eax, -12(%rbp)
	movl	-12(%rbp), %eax
	movl	%eax, %edx
	leaq	.LC0(%rip), %rcx
	call	printf
	movl	$0, %eax
	addq	$48, %rsp
	popq	%rbp
	ret
	.seh_endproc
	.ident	"GCC: (x86_64-win32-seh-rev0, Built by MinGW-W64 project) 8.1.0"
	.def	printf;	.scl	2;	.type	32;	.endef

执行第3步编译后,生成的是.o文件,即二进制文件,是电脑可以识别的机器指令了,是01010等的二进制,所以打开该文件的话看到的是乱码,因为它不是字符文件了。

执行第四步后生成main.exe,双击运行将会出现cmd窗口一闪而过,可以手动打开命令行窗口,然后切换到main.exe文件所在目录,输入mainmain.exe,然后回车,即可看到输出结果,如下:

在这里插入图片描述

以上是四步分开编译,也可以4步一起编译,执行gcc main.c即可生成一个a.exe的程序,这里没有指定输出文件名,所以默认名称为a,也可以指定一个文件名(文件名也可以带路径),如:gcc main.c -o main.exe

如果有多个文件怎么编译为一个exe?比如有如下文件:

max.h文件:

int max(int a, int b);

max.c文件:

int max(int a, int b) {
	return a > b ? a : b;
}

main.c文件:

#include <stdio.h>
#include "max.h"

int main() {
	printf("max = %d\n", max(10, 20));
	return 0;
}

这里一共有3个文件:max.hmax.cmain.c,我们使用前面所学的编译命令的前3条命令,分别把max.cmain.c编译为max.omain.o,最后一步生成一个exe文件:gcc main.o max.o -o main.exe。如果不使用分步的话,有一个非常快捷的编译方式:gcc *.c -o main.exe,这样可以编译当前目录下所有的c源文件为一个exe文件。

#include

  • #include <>用尖括号包含头文件,在系统指定的路径下找头文件
  • #include ""用双引号包含头文件,先在当前目录下找头文件,找不到再到系统指定的路径下找。

#include可以间接包含。

关于#include的更多细节,可查看https://www.cdsy.xyz/computer/programme/vc/230210/cd40432.html中的使用函数或类时,必须在前面有声明知识点。

#define宏定义

不带参数的#define

#define用于定义宏,一般宏名称用大写,如:#define PI 3.14,注,不要加分号,因为宏定义是简单的进行复制替换。

宏定义的作用域:从宏定义的地方开始,到当前文件的末尾。 宏定义也可以写在头文件中,这样包含这个头文件的都可以使用这个宏。这说明不同的源文件中可以声明同名称的宏。

#undef终止宏的作用

示例:

#define PI 3.14 // 定义宏

int main(void) {
	int a = PI;
	int b = PI;
	int c = PI;
	#undef PI // 终止宏
	int d = PI;
	#define PI 3.1415926 // 定义宏
	int e = PI;
	return 0;
}

可以看到,宏定义不但可以在函数外定义,也可以在函数中定义,而且我实验过,在一个函数中定义宏,在另一个函数中也可以使用该宏。

使用预编译命令:gcc -E main.c -o main.i,然后查看main.i,如下:

int main(void) {

 int a = 3.14;
 int b = 3.14;
 int c = 3.14;

 int d = PI;

 int e = 3.1415926;
 return 0;
}

带参数的#define

带参数的#define宏,它的功能跟函数差不多,语法如下:

#define MAX(a, b) a > b ? a : b

这里MAX是宏的名称,相当于函数名,a 和 b是宏的参数,与函数的参数不同,宏的参数是没有类型的,使用如下:

#define MAX(a, b) a > b ? a : b

int main(void) {
	int a = MAX(1, 2);
	int b = MAX(7, 9);
	int c = MAX(15, 49);
	return 0;
}

预编译后的效果如下:

int main(void) {
 int a = 1 > 2 ? 1 : 2;
 int b = 7 > 9 ? 7 : 9;
 int c = 15 > 49 ? 15 : 49;
 return 0;
}

再来一个示例:

#define MAX(a, b) a > b ? a : b

int main(void) {
	int a = MAX(1 + 2, 5);
	return 0;
}

预编译后如下:

int main(void) {
 int a = 1 + 2 > 5 ? 1 + 2 : 5;
 return 0;
}

可以看到,宏并不等于函数,宏只是实行简单的替换。基于这种简单替换,有时候也会出现问题,如下:

#define FUN(a, b) a * b

int main(void) {
	int a = FUN(1 + 2, 3);
	return 0;
}

如上代码,我们是期待3 * 3 = 9的结果,但实例并非如此,预编译代码如下:

int main(void) {
 int a = 1 + 2 * 3;
 return 0;
}

这里结果是6,并不是9。为了预防这种问题出现,可以给每个参数添加一个括号,如下:

#define FUN(a, b) (a) * (b)

预编译结果如下:

int main(void) {
 int a = (1 + 2) * (3);
 return 0;
}

这样结果就是9了。

因为宏参数没有类型,所以可以实现类似函数重载,如下:

#define MAX(a, b) a > b ? a : b

int main(void) {
	int a = MAX(3, 5);
	int a = MAX(1.5, 3.4);
	return 0;
}

带参宏相比函数的优点,它直接是简单的替换,没有函数入栈出栈的操作,所以效率会比较高,而且参数没有类型,可实现重载。而函数的优点是代码只有一份,可以节省空间。

带可变参数的#define

示例如下:

#include <iostream>

#define hello(...) sayHello("Even", 18, __VA_ARGS__)

void sayHello(std::string name, int age, ...) {
    std::cout << "Hello " << name << "! Your age is " << age << "." << std::endl;
}

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

如上代码,在宏中用...代表可变参数,在实际展开时,可变参数将替换在__VA_ARGS__的位置。对于C语言中的可变参数,访问该可变参数比较麻烦,具体可参考:https://www.cdsy.xyz/computer/programme/C_language/230210/cd40434.html

#ifdef选择性编译

#define HELLO

int main(void) {
	#ifdef HELLO
	printf("a\n");
	printf("b\n");
	printf("c\n");
	#else
	printf("d\n");
	printf("e\n");
	#endif
	return 0;
}

预编译后效果如下:

int main(void) {
    printf("a\n");
    printf("b\n");
    printf("c\n");
    return 0;
}

把宏定义删除,如下:

int main(void) {
	#ifdef HELLO
	printf("a\n");
	printf("b\n");
	printf("c\n");
	#else
	printf("d\n");
	printf("e\n");
	#endif
	return 0;
}

再进行预编译,效果如下:

int main(void) {
    printf("d\n");
    printf("e\n");
    return 0;
}

#ifdef HELLO,如果定义HELLO这个宏,则使用if中的代码,否则使用else中的代码。有一个类型的语法:#ifndef HELLO,则如果没有定义HELLO这个宏,则使用if中的代码,否则使用else中的代码。示例如下:

#define HELLO

int main(void) {
	#ifndef HELLO
	printf("a\n");
	printf("b\n");
	printf("c\n");
	#else
	printf("d\n");
	printf("e\n");
	#endif
	return 0;
}

预编译如下:

int main(void) {
    printf("d\n");
    printf("e\n");
    return 0;
}

#ifdef的这种形式一般和#define结合使用,用于防止头文件重复包含,示例如下:

fun.h文件如下:

int max(int a, int b);

main.c文件如下:

#include "fun.h"
#include "fun.h"

int main(void) {
	return 0;
}

预编译如下:

# 1 "main.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "main.c"
# 1 "fun.h" 1
int max(int a, int b);
# 2 "main.c" 2
# 1 "fun.h" 1
int max(int a, int b);
# 3 "main.c" 2

int main(void) {
 return 0;
}

可以看到,头文件中的函数声明被引入了两次,我们使用#ifndef来预防重复包含,如下:

fun.h文件如下:

#ifndef QQ_FUN_H
#define QQ_FUN_H
int max(int a, int b);
#endif

这里QQ为项目名称,可以随意修改,FUN为类名,H表示头文件,一般会以这种方式命名,以防宏定义重复,随便写个宏名称也可以。这里定义的宏并没有值,只有宏名称,也足够了,且这里没有使用到#else分支,也没有使用的必要。

#if选择性编译

示例如下:

int main(void) {
	#if 1 // 1为真,0为假
	printf("a\n");
	printf("b\n");
	#else
	printf("c\n");
	printf("d\n");
	#endif
	return 0;
}

#if 表达式,表达式的值为真时使用if中的代码,否则使用else中的代码,也可以不要#else分支。在真实开发中,一般#if中的开关定义成宏,如下:

#define HELLO 1

int main(void) {
	#if HELLO // 1为真,0为假
	printf("a\n");
	printf("b\n");
	#else
	printf("c\n");
	printf("d\n");
	#endif
	return 0;
}
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门