C++报错无效的预处理命令include_Chapter2:从C/C++的编译原理说起

C++报错无效的预处理命令include_Chapter2:从C/C++的编译原理说起_第1张图片

上一篇文章简单提到了编译过程。对于笔者而言,C++编译运行似乎只是在IDE界面按下F5按钮,然后把一切事情交给编译器,然后等待正确运行,在终端开始交互测试,或者看着出现的warning和error开始漫长的debug。。。从未想过背后编译器都做了什么事。实际上想成为一个真正的底层开发者,应该学会和编译器打交道。

很遗憾在本科专业学习中并没有《编译原理》这门课程,也没有特地研究过这门学问,只是在一次又一次的debug中获得只言片语的了解。想在短时间内掌握这门技术是很困难的,也难以在一篇文章里说清楚来龙去脉。所以笔者从一个简单的C++ 程序展开,一步一步探究程序员使用的高级语言是如何被一层一层“翻译”直到变成计算机可以读懂并执行的字节的。

笔者写了一个最简单的C++程序,内容是大家在学习无论何种计算机语言的时候通常会写的第一个demo:hello world。这里没有使用using namespace,语句能达成最基本功能即可。囿于这方面的知识有限,分析过程中还请读者指出错误。

int 

C/C++编译是集成的,笔者常用gcc/g++ [source file] -o [executable file]。其中隐藏了很多步骤,拆分开来是以下四个步骤:

  1. 预处理(preprocessing):展开头文件、宏替换、去掉注释、条件编译,产生.i后缀文件,gcc -E helloworld.c -o helloworld.i
  2. 编译(compression):检查语法、生成汇编,产生.s后缀文件,gcc -S helloworld.i -o hello.s
  3. 汇编(assembly):汇编代码转换成机器码,产生.o后缀文件,gcc -S helloworld.i -o helloworld.s
  4. 连接(linking):连接到一起生成可执行文件,产生.out后缀文件,gcc -c helloworld.s -o helloworld.o

先来看第一个步骤预处理的结果,helloworld.i。由于遵循C语言文法,它仍然是C文件。文件有733行,故节选部分笔者认为较有价值的片段进行分析。

首先笔者对define进行了全局搜索,没有结果,可以印证预处理过程中对所有define都完成了变量替换。helloworld.i的最前面是一系列和stdio.h有引用关系的头文件路径。

# 1 "helloworld.c"
# 1 ""
# 1 "<命令行>"
# 31 "<命令行>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<命令行>" 2
# 1 "helloworld.c"
# 1 "/usr/include/stdio.h" 1 3 4
# 27 "/usr/include/stdio.h" 3 4
# 1 "/usr/include/bits/libc-header-start.h" 1 3 4
# 33 "/usr/include/bits/libc-header-start.h" 3 4
# 1 "/usr/include/features.h" 1 3 4
# 450 "/usr/include/features.h" 3 4
# 1 "/usr/include/sys/cdefs.h" 1 3 4
# 460 "/usr/include/sys/cdefs.h" 3 4
# 1 "/usr/include/bits/wordsize.h" 1 3 4
# 461 "/usr/include/sys/cdefs.h" 2 3 4
# 1 "/usr/include/bits/long-double.h" 1 3 4
# 462 "/usr/include/sys/cdefs.h" 2 3 4
# 451 "/usr/include/features.h" 2 3 4
# 474 "/usr/include/features.h" 3 4
# 1 "/usr/include/gnu/stubs.h" 1 3 4
# 10 "/usr/include/gnu/stubs.h" 3 4
# 1 "/usr/include/gnu/stubs-64.h" 1 3 4
# 11 "/usr/include/gnu/stubs.h" 2 3 4
# 475 "/usr/include/features.h" 2 3 4
# 34 "/usr/include/bits/libc-header-start.h" 2 3 4
# 28 "/usr/include/stdio.h" 2 3 4

接下来是一系列常用数据类型的别名定义。

typedef unsigned char __u_char;
typedef unsigned short int __u_short;
typedef unsigned int __u_int;
typedef unsigned long int __u_long;


typedef signed char __int8_t;
typedef unsigned char __uint8_t;
typedef signed short int __int16_t;
typedef unsigned short int __uint16_t;
typedef signed int __int32_t;
typedef unsigned int __uint32_t;

typedef signed long int __int64_t;
typedef unsigned long int __uint64_t

IO_FILE的文件定义。

typedef struct _IO_FILE FILE;
# 43 "/usr/include/stdio.h" 2 3 4
# 1 "/usr/include/bits/types/struct_FILE.h" 1 3 4
# 35 "/usr/include/bits/types/struct_FILE.h" 3 4
struct _IO_FILE;
struct _IO_marker;
struct _IO_codecvt;
struct _IO_wide_data;
typedef void _IO_lock_t;
struct _IO_FILE
{
  int _flags;
  char *_IO_read_ptr;
  char *_IO_read_end;
  char *_IO_read_base;
  char *_IO_write_base;
  char *_IO_write_ptr;
  char *_IO_write_end;
  char *_IO_E_base;
  char *_IO_buf_end;
  char *_IO_save_base;
  char *_IO_backup_base;
  char *_IO_save_end;
  struct _IO_marker *_markers;
  struct _IO_FILE *_chain;
  int _fileno;
  int _flags2;
  __off_t _old_offset;
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];
  _IO_lock_t *_lock;
  __off64_t _offset;
  struct _IO_codecvt *_codecvt;
  struct _IO_wide_data *_wide_data;
  struct _IO_FILE *_freeres_list;
  void *_freeres_buf;
  size_t __pad5;
  int _mode;
  char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
};

三个常用标准输入输出流文件

extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;

之后是大量的文件处理(fseek、fread、fwrite等)、输入输出函数(printf、sprintf、fprintf等)声明,此处截取部分。补充以下此处使用extern关键字,使该函数在实现部分使用原来文件里面的定义。

extern int fgetc (FILE *__stream);
extern int getc (FILE *__stream);
extern int getchar (void);
extern int getc_unlocked (FILE *__stream);
extern int getchar_unlocked (void);
# 510 "/usr/include/stdio.h" 3 4
extern int fgetc_unlocked (FILE *__stream);
# 521 "/usr/include/stdio.h" 3 4
extern int fputc (int __c, FILE *__stream);
extern int putc (int __c, FILE *__stream);
extern int putchar (int __c);
# 537 "/usr/include/stdio.h" 3 4
extern int fputc_unlocked (int __c, FILE *__stream);
extern int putc_unlocked (int __c, FILE *__stream);
extern int putchar_unlocked (int __c);
extern int getw (FILE *__stream);
extern int putw (int __w, FILE *__stream);

最后才是笔者写的程序本体。

C++报错无效的预处理命令include_Chapter2:从C/C++的编译原理说起_第2张图片

可见,预处理阶段,编译器将代码中的stdio.h编译进来,把#include包含进来的.h 文件插入到#include所在的位置,把源程序中使用到的用#define定义的宏用实际的字符串代替。

接着对该文件进行编译处理,这一步编译器检查是否有语法错误,准确无误则开始。结果如下(不太懂Assembly,不作注释,maybe I will add them someday... )。

	.file	"helloworld.c"
	.text
	.section	.rodata
.LC0:
	.string	"Hello World!"
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	leaq	.LC0(%rip), %rdi
	movl	$0, %eax
	call	printf@PLT
	movl	$0, %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	main, .-main
	.ident	"GCC: (GNU) 9.2.0"
	.section	.note.GNU-stack,"",@progbits

汇编阶段,汇编语言被翻译为机器语言,二进制文件。当笔者打算查看此helloworld.o文件的时候,无论是terminal还是VSCode都对笔者发出了“是否继续查看”的提示。果然人类无法阅读:

7a403d6e88b05febd87baa39ee3831eb.png

C++报错无效的预处理命令include_Chapter2:从C/C++的编译原理说起_第3张图片

最后一步是连接,完成这一步即可得到一个可以运行的可执行文件了。但是稍等,有一个问题,“连接”的是什么东西?所需的外部文件在预处理阶段就已经包括进来,为什么还需要引入外部文件?所以提出一个关键概念——链接库。库有两种,一种是静态链接库,一种是动态链接库,不管是哪一种库,要使用它们,都要在程序中包含相应的include头文件。linux中,静态库文件后缀.a,动态库文件后缀.so。

什么是静态链接呢?即在链接阶段,将源文件中用到的库函数与汇编生成的目标文件.o合并生成可执行文件。该可执行文件可能会比较大。这种链接方式的好处是:方便程序移植,因为可执行程序与库函数再无关系,放在如何环境当中都可以执行。缺点是:文件太大。想尝试的话可以在编译参数中加上-static。

那么什么是动态连接呢?我们知道静态链接的话,文件会很大,往往实现很小的一个功能就需要占用很大的空间,而且每次库文件升级的话,都要重新编译源文件,很不方便。而且静态文件在内存中多份拷贝,占用很大空间。gcc默认先采用动态库。但是动态库在内存中存在一份即可。动态链接有一个缺点就是可移植性太差,如果两台电脑运行环境不同,动态库存放的位置不一样,很可能导致程序运行失败。所以应该结合实际环境考虑采用何种库。

此处引用一下yanlei的回答(thanks)

yanlei:编译器编译原理:预处理,编译,汇编,链接各步骤详解​zhuanlan.zhihu.com

静态库链接时搜索路径顺序:

  • 1. ld会去找GCC命令中的参数-L
  • 2. 再找gcc的环境变量LIBRARY_PATH
  • 3. 再找内定目录 /lib /usr/lib /usr/local/lib 这是当初compile gcc时写在程序内的

动态链接时、执行时搜索路径顺序:

  • 1. 编译目标代码时指定的动态库搜索路径
  • 2. 环境变量LD_LIBRARY_PATH指定的动态库搜索路径
  • 3. 配置文件/etc/ld.so.conf中指定的动态库搜索路径
  • 4. 默认的动态库搜索路径/lib
  • 5. 默认的动态库搜索路径/usr/lib

有关环境变量:

  • LIBRARY_PATH环境变量:指定程序静态链接库文件搜索路径
  • LD_LIBRARY_PATH环境变量:指定程序动态链接库文件搜索路径

那么笔者再写一个例子,说明这两种库的使用。

写两个文件,add.h和add.c,并且目录结构如下所示:

[jaimeow@jaimeow-pc demo2]$ ls -R
.:
add.c  addlib  add.o  test  test.c
./addlib:
add.h  libadd.a
#ifndef ADD_H_
#define ADD_H_
int add(int a,int b);
#endif
#include ".addlib/add.h"
#include 
int add(int a,int b){
    return a + b;
}

然后执行gcc -c add.c和ar -crv libadd.a add.o,生成add.o(目标文件)和libadd.a(静态库文件)。把库文件封装在addlib目录下,再写一个测试样例。

#include "./addlib/add.h"
#include 

int main(){
    int a = 1,b = 2;
    printf("%d",add(a,b));
    return 0;
}

结果可以正确运行,输出3。

那么来试一下动态库,gcc -fPIC -shared -o libadd.so add.c,gcc -o test test.c -L./addlib -ladd。如果把libadd.so放入刚才的addlib目录中提示找不到共享库,放入可执行文件所在目录即可正确运行,输出3。从这一点来看,的确静态库是运行之前包括的,动态库是运行时加载的。

写完这篇文章,对计算机语言的认识更进一步,程序员在编程的时候首先要做到了解自己所用的工具,才能更好的使用工具,让它变成你的开发之友。这也是区分程序员水平之处。

(如有转载请注明作者与出处,欢迎建议和讨论,thanks)

你可能感兴趣的:(C++报错无效的预处理命令include_Chapter2:从C/C++的编译原理说起)