C提高~预处理

引言

预处理所做工作涉及头文件包含、常见注释风格、宏定义使用及各种预条件编译的作用。函数库主要涉及静态库和动态库

为什么需要编译链接

看下下编译流程

 

举一个简单的例子,helloc

#include<stdio.h>

int main(int argc,char *argv[]){
    printf("hello world\n");
    return 0;
}

对于C语言程序,我们需要将它编译链接成为可执行的二进制文件,然后由系统加载执行。在Linux中,GCC编译程序会读取源代码hello.c文件,将其翻译成一个可执行文件hello,经历四个过程,由编译工具链完成

第一个过程

预处理(cpp)。在命令行输入以下命令,预处理器会对以#开头的预处理命令进行处理

gcc -E hello.c -o hello.i

如hello.c中#include<stdio.h>,预处理器会将系统中的hello.h具体内容读到文本中,替换原有的#include<stdio.h>,得到一个新的C程序,称之为.i文件

hello.i
...
...
extern void funlockfile(FILE *__stream) __attribute__ ((__nothrow__,__leaf__));

int main(int argc,char *argv[]){
    printf("hello world\n");
    return 0;
}

第二个过程

编译(cc)。在命令行下输入gcc编译命令,编译器将hello.i翻译成了hello.s汇编文件得到一条条通用的机器语言指令

gcc -S hello.i -o hello.s

或者将预处理器和编译器一起完成

gcc -S hello.c -o hello.s

第三个过程

汇编(ss)。在命令行输入以下命令,汇编器会将hello.s翻译成机器语言指令,将这些指令打包称为***.o格式的可重定位文件,并把结果保存到目标文件hello.o中目标文件由不同的段组成通常一个目标至少有两个段---数据段和代码段。得到hello.o是一个二进制文件

gcc -c hello.s -o hello.o

第四个过程

连接(ld)。输入以下命令,连接是最后一个过程,连接器会将hello.o和其他库文件、目标代码链接后形成可执行文件。在hello.c程序中调用了printf函数,连接器会将printf.o文件并入hello.out可行执行文件中。最后将可执行文件加载到存储器后,然后由系统执行。在Linux中可以通过以下命令查看生成的文件。

gcc hello.o -o hello.out
ls -l hello*

编译链接中各种文件扩展名的含义

在Linux中,分为可执行文件和不可执行文件,由源码到可执行程序的过程中,以扩展名(即后缀)来区分各个阶段

GCC中一些常见的扩展名
扩展名含义扩展名含义
.cC源代码文件.mObjective-C源代码文件
.a由目标文件构成的静态库文件.o编译后的目标文件
.CC++源代码文件.out链接器生成的可执行文件
.h程序中包含的头文件.s汇编语言源代码,后期不进行预处理
.i预处理过C源代码文件.S汇编语言源代码,后期需预处理,包含预处理指令
.ii预处理过C++源代码文件  

预处理详解

C语言预处理意义

编译器的主要目的是编译源代码,将C语言源代码转化成.s的汇编代码。编译器聚焦核心功能后,剥离出一部分非核心功能由预处理器执行。预处理器对程序源码进行一些预处理,为后续编译做好基础,在由编译器编译。预处理的意义使得编译器实现功能变得更为专一

预处理涉及内容

  1. 文件包含
  2. 宏定义
  3. 条件编译
  4. 一些特殊的预处理关键字
  5. 去掉程序中的注释

预处理指令很多,如#include(文件包含);#if  #ifdef #ifndef #else #elif #endif(条件编译),#define宏实现。由此可见,编译器根本不需要处理宏定义,因为预处理器已经处理完成;typedef重命名语言还在,说明它和宏定义右本质区别,typedef是由编译器处理

文件包含

每个程序一般分为两个部分一个用于保存各种声明的头文件,一个用于保存程序的实现;头文件作用在于声明和实现的分离,可以在头文件中查阅需要调用的函数,并定义很多的宏定义,这样只修改头文件内容,而不用去繁琐代码中去更改,提高了效率和代码可读性

尖括号<>专门用来包含系统提供的头文件(由操作系统自带的,不是程序员自己的写的)。双引号" "用来包含自己写的头文件

尖括号<>表示,C编译器会到系统指定目录(编译器中配置或操作系统配置的目录中寻找)去寻找该头文件,隐含意思不会找当前目录;双引号" "包含的头文件,编译器默认会先从当前目录下寻找相应的头文件,如果没找到然后再到系统指定目录去寻找。

注释

注释为了增加代码的可读性,C中一般有两种注释方式/* */ 和 //,常见注释有模块描述,头文件开头的版权和版本声明,函数的说明,重要代码的注释。

  1. 注释格式尽量统一,建议使用/* */,linux内核注释几乎使用它;
  2. 注释考虑程序易读及排版优美,注释语言尽量统一;
  3. 函数头部应该注释,使用代码自注释;
  4. 建议边写代码边注释,更改代码同时更改注释,保持注释与代码一致
  5. 防止代码二义性,注释必须简洁,不要用缩写;
  6. 在单行注释时,注释放在代码上方或右方;
  7. 注释和代码通缩进,注释和代码之间要空出一行;

预处理时预处理器会移除所有注释用空格替换,到了编译器进行编译阶段,程序中已经没有注释了。

宏定义

宏定义简化了重复性劳动便于程序修改,增加程序可读性,如使用宏定义字符串3.14定义一个标识符来替代;一般将宏定义放在头文件中,便于查找和提高编程效率标识符后的字符串预处理中仅仅只是文本上的替换

宏定义规则和使用

  1. 宏定义在预处理阶段由预处理器进行替换(原封不动的替换);
  2. 宏定义替换会递归进行,直到替换出来的值本身不再是一个宏为止;
  3. 一个完整的宏定义含三个部分:#define、宏名、剩下部分
  4. 宏可带参数(带参宏),带参宏和带参函数使用类似且有差异,定义时每个参数在宏体中引用时都必须加括号,最后整体再加括号,括号缺一不可;

无参数宏定义

#define 标识符(符号常量) 宏体(常数、表达式、格式串)
#define SEC_PER_YREAR  (365*24*60*60UL)

带参数宏定义

MAX宏 求两个数中较大的一个
#define MAX(a,b) (((a)>(b))?(a):(b))

注意:使用三目运算符、添加括号防止有关优先级问题

#define X(a,b) a+b

int main(){
  int x = 1,y=2;
  int x = 3*X(x,y);//3*x+y
  return 0;
}

宏定义的缺陷

宏定义在预处理期间处理的,而函数是在编译期间处理的两者实质差别是,宏定义最终在调用宏的地方把宏体原地展开,而函数是在调用函数处跳转到函数中去执行执行完在调整回来

宏定义是原地展开,没有调用开销;而函数是跳转执行再返回,因此函数有较大调用开销。宏定义和函数相比,没有调用开销,没有传参开销,所以当函数体很短(尤其只有一句代码时),可用宏定义来替代,效率高

带参宏和带参函数差别宏定义不会检查参数的类型返回值不会附带类型;而函数有明确的参数类型和返回值类型

带参函数执行时编译器会检测形参和实参类型匹配问题,而宏定义实际传参和宏所希望的参数类型必须一致否则编译不报错但运行有误

 带参函数内联函数
优点

编译器自行检查形参和实参类型                                                          

原地展开,无调用开销;预处理阶段完成,不占编译时间                                        

 

函数代码被放入符号表中,使用时类似宏替换,无调用开销

效率高且会检查参数类型

缺点函数带参,返回地址等压栈和出栈,栈变量开辟和销毁,运行效率没有带参宏高不检查参数类型多次宏替换导致代码体积变大,宏原地替换由于一些参数副作用导致出错,例++,--操作函数代码较长会消耗过多内存,若函数内有循环,导致执行时间长

内联函数和inline关键字

  1. 内联函数通过函数定义前加inline关键字实现,若仅把inline放在函数声明处是不起作用的;
  2. 内联函数本质是函数,有函数的优点,内联函数是编译器负责处理的,编译器可对参数做静态类型检查;但是同时有带参宏的优点无调用开销, 原地展开
  3. 函数体很短(一两句代码时),且希望利用编译器的参数类型检查来排错,希望没有调用开销,适合使用内联函数

只有宏名没有宏体的宏主要用于条件编译,用于实现跨平台;

#define DEBUG

条件编译

我们希望程序有多种配置,在编写源码时写好了各种配置的代码,然后给个配置开关,在源码代码级别去修改配置开关来让程序编译出不同的效果;常见的条件编译如下:

#if #else #elif #endif

#ifdef #endif

宏定义来实现条件编译(#define #undef #ifdef)

程序有DEBUG版本和RELEASE版本,区别是编译时有无定义DEBUG宏

//macro.c
#include<stdio.h>

#define DEBUG   //法1:注释掉或法2:下一行添加#undef DEBUG
#ifdef DEBUG
#define debug(x)  print(x)
#else
#define debug(x)
#endif
int main(void){
  debug("this is a debug info.\n");
  return 0;
}

注释掉#define DEBUG或法2:#define DEBUG下一行添加#undef DEBUG,通过这样的开关就可以配置DEBUG版本和RELEASE版本的程序

避免重复包含头文件

#ifndef _ASM_ARM_BITOPS_H   //如果不存在asm-arm/bitops.h
#define _ASM_ARM_BITOPS_H   //就引入asm-arm/bitops.h
/*
  头文件内容
*/
#endif  /*_ARM_BITOPS_H */ //否则不引入asm-arm/bitops.h

第一次包含头文件时,会定义出_ASM_ARM_BITOPS_H宏后续再次包含该头文件时,已经有了该宏定义,文件#ifndef _ASM_ARM_BITOPS_H不会成立,所以头文件不会被再次包含

#if define、#ifdef和 #if !define、#ifndef用法

#if define(x)
...code...
#endif

#if define中,不管括号里x的逻辑是真还是假只管该程序前面的宏定义有无定义“x”这个宏,若定义,则编译器会编译中间的...code...否则直接忽略中间...code...代码,#if defined 取反就是 #if !defined,具体看个例子。

#include<stdio.h>
#define NUM
int main(void){
  int a = 0;
  #ifdef NUM            //如果前面定义NUM
  
  a = 111;
  printf("#ifdef NUM.\n");
  #else                //如果前面没有定义NUM
                       //这个符号,则执行下面的//语句
  a = 222;
  printf("#else.\n");
  #endif
  return 0;
}

结果

#ifdef NUM.
--------------------------------
//预处理
gcc -E preprocess_test.c -o preprocess_test.i
//preprocess_test.i
int main(void){
  int a = 0;

  
  a = 111;
  printf("#ifdef NUM.\n");

  return 0;
}

preprocess_test.i中,已经找不到头文件包含的指令了。例子可见,头文件包含是原地替换,所有注释被移除,用一个空格替换;条件编译部分,在预处理中也被移除,用一个空格替换

#ifdef 与 #if define的区别

区别在于#if defined可以组成复杂的预编译条件

#if define(A) && defined(B)
<code>
#endif

表示A和B这两个宏定义都存在的时候才编译代码,而#ifdef只能判断单个宏定义,不能判断多个复杂条件;

内容查考:C语言内核深度解析

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值