目前对C语言感性的理解:C语言,就是一门语言,和英语、日语一样。本科时候学过谭浩强的红皮书,那是一本语法书,和中学的英文语法书没有太大的区别,规定一系列的约定俗成的规范(复数名词后要加s;定义整型变量前面要用int修饰),就是强行记忆,没有什么捷径。语法很重要,学会语法是进入这个语系世界的钥匙。但是,学会语法和学会语言是两件事情。我和刘慈欣都熟练掌握中文语法,但是他能写《三体》获得雨果奖,而我写个开题报告都要被导师骂一顿(苦笑)。语言能力的高低,最终还是体现在思维的深度和广度,语法不过是一种人机的通讯协议。自知能力有限,不敢谈C语言,只是总结一些C语言语法和原理以供日后复习。
C语言编译过程
在Linux环境下,我们编译一个.c文件一般直接使用gcc编译器执行“gcc 文件名.c” 就可以获取二进制可执行文件a.out,其实编译器帮我们完成了很多任务。以编译unicorn.c文件为例,gcc编译器实际编译生成二进制可执行文件的过程如下:
1)gcc -E unicorn.c -o unicorn.i
//编译器执行与处理指令
2)gcc -S unicorn.i -o unicorn.s
//编译器对预处理文件进行编译,生成汇编文件
3)gcc -c unicorn.s -o unicorn.o
//编译器对汇编文件进行汇编,生成二进制文件
//通常第二、三步可统一为一步称为编译
4)gcc unicorn.o -o unicorn
//编译器将二进制文件与其依赖进行链接,并与运行时文件绑定,生成可执行的二进制文件
举例:以一个简单计算程序为例,看看每一步编译器的具体工作
代码:
myMath.h:
1 # ifndef __MYMATH_H__
2 # define __MYMATH_H__
3 int add(int i, int j);
4 double div(int i, int j);
5 int sub(int i, int j);
6 int mul(int i,int j);
7 # endif
myMath.c:
1 # include "myMath.h"
2
3 int add(int i, int j){
4 return i + j;
5 }
6
7 int mul(int i, int j){
8 return i * j;
9 }
10
11 int sub(int i, int j){
12 return i - j;
13 }
14
15 double div(int i, int j){
16 return i / j;
17 }
myMain.c:
1 #include <stdio.h>
2 #include "myMath.h
3
4 int main()
5 {
6 int i = 10;
7 int j = 3;
8 printf(" i + j = %d\n",add(i, j));
9 printf(" i - j = %d\n",sub(i, j));
10 printf(" i * j = %d\n",mul(i, j));
11 printf(" i / j = %d\n",div(i, j));
12 return 0;
13 }
执行gcc -E myMain. -o myMain.i,并打开myMain.i:
539
540 int add(int i, int j);
541 double div(int i, int j);
542 int sub(int i, int j);
543 int mul(int i,int j);
544 # 3 "myMain.c" 2
545
546 int main()
547 {
548 int i = 10;
549 int j = 3;
550 printf(" i + j = %d\n",add(i, j));
551 printf(" i - j = %d\n",sub(i, j));
552 printf(" i * j = %d\n",mul(i, j));
553 printf(" i / j = %lf\n",div(i, j));
554 return 0;
555 }
与myMain.c对比,经过预处理后,原本include包含的头文件名,被实际头文件内容代替
执行gcc -S myMain.i -o myMain.s,并打开myMain.s:
1 .section __TEXT,__text,regular,pure_instructions
2 .build_version macos, 10, 14 sdk_version 10, 14
3 .globl _main ## -- Begin function main
4 .p2align 4, 0x90
5 _main: ## @main
6 .cfi_startproc
7 ## %bb.0:
8 pushq %rbp
9 .cfi_def_cfa_offset 16
10 .cfi_offset %rbp, -16
11 movq %rsp, %rbp
12 .cfi_def_cfa_register %rbp
13 subq $32, %rsp
14 movl $0, -4(%rbp)
15 movl $10, -8(%rbp)
16 movl $3, -12(%rbp)
17 movl -8(%rbp), %edi
预处理文件myMain.i经过编译生成汇编文件
执行gcc -c myMain.s -o myMain.o
再执行ls -l myMain.o:
Desktop Linraffe$ ls -l myMain.o
-rw-r--r-- 1 Linraffe staff 1100 2 29 17:15 myMain.o
发现经过编译起汇编后,生成的二进制文件并不能执行。
执行gcc -c myMain.o myMath.o -o myMain,
在执行myMain:
Desktop Linraffe$ gcc myMath.o myMain.o -o myMain
Desktop Linraffe$ myMain
i + j = 13
i - j = 7
i * j = 30
i / j = 3
经过链接后,生成的可执行文件成功执行。
对于链接过程的理解,可以执行nm命令:
Desktop Linraffe$ nm myMain.o
U _add
U _div
0000000000000000 T _main
U _mul
U _printf
U _sub
Desktop Linraffe$ nm myMath.o
0000000000000000 T _add
0000000000000060 T _div
0000000000000020 T _mul
0000000000000040 T _sub
Desktop Linraffe$ nm myMain
0000000100000000 T __mh_execute_header
0000000100000e40 T _add
0000000100000ea0 T _div
0000000100000ec0 T _main
0000000100000e60 T _mul
U _printf
0000000100000e80 T _sub
U dyld_stub_binder
nm命令+“二进制文件名”:查看该程序实现所依赖的函数
其中,T代表文件中包含函数的实现,U代表文件依赖但是不包含函数的实现。可以看到,myMain.o中并不包含几个函数(add()、su b()…)的实现,而经过链接后,myMain中包含这些函数的实现,从而可以执行。
总结:一个.c文件要经过预处理、编译、汇编、链接,最终才会生成一个可执行的二进制文件。
来源:CSDN
作者:Linraffe
链接:https://blog.csdn.net/weixin_42754578/article/details/104570540