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文件所在目录,输入main或main.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.h、max.c、main.c,我们使用前面所学的编译命令的前3条命令,分别把max.c、main.c编译为max.o、main.o,最后一步生成一个exe文件:gcc main.o max.o -o main.exe。如果不使用分步的话,有一个非常快捷的编译方式:gcc *.c -o main.exe,这样可以编译当前目录下所有的c源文件为一个exe文件。
#include可以间接包含。
关于#include的更多细节,可查看https://www.cdsy.xyz/computer/programme/vc/230210/cd40432.html中的使用函数或类时,必须在前面有声明知识点。
#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 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;
}
带参宏相比函数的优点,它直接是简单的替换,没有函数入栈出栈的操作,所以效率会比较高,而且参数没有类型,可实现重载。而函数的优点是代码只有一份,可以节省空间。
示例如下:
#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
#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分支,也没有使用的必要。
示例如下:
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;
}