C语言预处理理论
4.6.1C语言预处理理论
4.6.1.1由源代码到可执行程序的过程
- (1)源码->(编码)->elf可执行程序
- (2)源码->(编译)->目标文件.o->(链接)->.elf可执行程序
- (3)源码.c->(编译)->汇编文件.S->(汇编)->目标文件.o->链接->elf可执行程序
- (4)源码.c->(预处理)-预处理过的.i文件->(编译)->汇编文件.S->(汇编)->目标文件.o->链接->elf可执行程序
预处理用预处理器,编译用编译器,汇编用链接器,这几个工具再加上其他一些额外的会用到的可用工具,合起来叫编译工具链。gcc就是一个编译工具链。
4.6.1.2、预处理指令
- (1)编译器本身的主要目的是编译源代码,将C的源代码转换成.S的汇编代码。编译器聚焦核心功能后,就剥离出了一些非核心的功能到预处理器去了。
- (2)预处理器帮编译器做一些编译前的杂事
#编程中常见的预处理
- (1)#include(#include<>和#include" ")
- (2)注释
- (3)#if #elif #ifdef
- (4)宏定义
4.6.1.4 gcc中只预处理不编译的方法。
- (1)gcc编译时可以给一些参数做一些设置,譬如gcc xx.c -o xx可以指定可执行程序的名称;
- 譬如gcc xx.c -c -o xx.o可以指定只编译不连接,也可以生成.o的目标文件。
- (2)gcc -E xx.c -o xx.i 可以实现只预处理,不编译。一般情况下没必要只预处理不编译,但是有时候这种技巧可以帮助我们研究预处理过程,帮助debug程序
总结:宏定义被预处理时的现象:第一个是宏定义语句本身不见了(可见编译器根本就不认识#define,编译器根本就不知道还有个宏定义)。
第二:typedef的重命名语言还在说明它和宏定义是有着本质区别的,(它不是由预处理器处理的,而是由编译器处理的)
4.6.2.C语言预处理代码实战
头文件包含
- (1)#include<>和#include“ ”的区别:<>专门用来包含系统提供的头文件(就是系统自带的编译器,不是程序员自己写的)
- “”用来包含自己写的头文件,更深层次来说,如果用<>那么系统会到系统里的指定目录(譬如在ubuntu中是/usr/include目录,编译器还允许用-I来附加指定)去寻找这个头文件,如果找不到就会提示这个头文件不存在。
- (2)“”包含的头文件,编译器会默认先在当前目录下寻找相应的头文件,如果没找到,会到系统指定目录去找,如果还没找到则提示文件不存在
总结+注意:规则虽然允许用双引号来包含指定目录,但是一般的使用原则是:如果是系统指定的自带的用<>,如果是自己写的但是集中放在了一起,专门存放头文件的目录下将来在编译器中用-I来寻找这种情况下用<>。
- (3)头文件包含的真实含义就是在#include<xx.h>的那一行,将xx。h原地展开替换这一行#include<xx.h>语句。这个过程在预处理中进行。
注释
- (1)注释是给程序员看的,而不是给编译器看的,编译器看程序只需要看语句和代码。对于编译器来说,注释是没有用的,注释对于人来是添加的理解。
- (2)编译器既然不看注释,那么编译器最好没有注释,实际上在预处理阶段,预处理器已经将注释清除了。
- (3)对于.c文件不会做任何改动,实际改动的位.i文件。
条件编译
- (1)有时候我们希望程序有多种配置,我们在源代码编写时,写好了各种配置的代码,然后给个配置开关,在源代码级别去修改配置开关来让程序编译出不同的效果
- (2)条件编译中用的两种条件判定方法分别是#ifdef 和#if
区别:
#ifdef XXX判定条件成立与否时主要看xxx这个符号在本语句之前有没有被定义,只要定义了这个符号(我们可以直接定义#define xxx 或者#define xxx 12或者#define xxx yyy)就是成立的。
#if(条件表达式),他的判断标准是()中的内容是真还是假
4.6.3宏定义1
4.6.3.1宏定义的规则和使用解析
- (1)宏定义的解析规则就是:在预处理阶段由预处理器进行替换,这个替换是原封不动的替换
- (2)宏定义替换会递归进行,知道替换出来的值本身不再是一个宏
- (3)一个正确的宏定义的式子本身分为三个部分,第一部分为:#define 第二部分为:宏名+空格 第三部分为:剩下的所有为第三部分
- (4)宏可以带参数,称为带参宏,带参宏的使用和带参函数非常像,但是使用上有一些差异,在定义带参宏时,,每一个参数在宏体中引用时都必须加括号,最后整体再加括号。
4.6.3.2宏定义实例1:MAX求2个数中较大的一个
- #define MAX(a, b ) (((a)>(b))?(a):(b))
关键:
- 第一点:要想到使用三目运算符来完成
- 第二点:注意括号使用
4.6.3.3宏定义示例2:SEC_PER_YEAR
关键:
- 第一点:当一个数字直接出现在程序中时,他的类型默认是int
- 第二点:一年有多少秒,这个数字刚好超过int类型的存储范围
4.6.4宏定义2
4.6.4.1带参宏和带参函数的区别(宏定义的缺陷)
- (1)宏定义是在预处理器件处理的,而函数是在编译期间处理的。这个区别带来的实质上的差异是:宏定义最终是在调用宏的地方把宏原地展开,而函数是在调用函数处跳转到函数处执行,执行完再返回到调用处。
注:宏定义和函数的最大区别就是:宏定义是原地展开,因此没有开销,而函数是跳转执行再返回,因此函数有比较大的调用开销。所以宏定义和函数相比优势就是没有调用开销,没有传参开销,所以当函数本身很短(尤其是只有一句话时)可以用宏来代替,这样效率高。
- (2)带参宏和带参函数的一个重要差别就是宏定义不会检查参数的类型,返回值也不会附带类型,而函数有明确的参数类型和返回值类型。当我们调用函数时编译器会帮我们做参数的静态类型检查,如果编译器发现我们实际传参和参数声明不同时会报警告或错误
注:用函数的时候程序员不用太操心类型不匹配因为编译器会检查,如果不匹配编译器会叫;用宏的时候,程序员必须很注意实际传参和宏所希望的参数类型一致,否则可能编译不报错,但是运行有误。
总结:宏和函数各有千秋,各有优劣,总的来说,如果代码比较多用函数比较适合,不会音响効率,而对于那些只有一两句话的额函数开销就太大了,适合用带参宏。但是带参宏又有缺点:不检查参数类型。
4.6.4.2内联函数和inline关键字
- (1)内联函数通过在函数定义前加inline关键字实现。
- (2)内联函数本质上是函数,所以有函数的优点(内联函数是编译器负责处理,编译器可以帮我们做参数的静态类型检查);但是他同时也有带参宏的优点(不用调用开销,而是原地展开)。所以几乎可以这样认为:内联函数就是带了静态类型检查的宏
- (3)当我们函数内函数体很短(譬如只有一两句话)我们又希望利用函数的静态类型检查来排错,而且希望没有调用开销就适合使用内联函数
4.6.4.3宏定义来实现条件编译(#define #undef #ifdef)
(1)程序有DEBUG版本和RELEASE版本,区别就时有无DEBUG宏。
#define DEBUG //#undef DEBUG #ifdef DEBUG #define debug(x) printf(x) #else #define debug(x) #endif
4.6.5函数的本质
4.6.5.1、C语言为什么会有函数
- (1)整个程序分为多个晕源文件,一个文件分成多个函数,一个函数分成多个语句,这就是整个程序的组织形式。这样组织的好处在于:分化问题,便于编写程序,便于分工。
- (2)函数的出现是人(程序员和架构师)的需要,而不是机器(编译器、CPU)需要
- (3)函数的目的就是为了实现模块化编程。说白了就是为了提高程序的可移植性
4.6.5.2函数书写的一般规则:
- 第一:遵循一定格式。 函数的返回类型、函数名、参数列表
- 第二:一个函数只做一件事。 函数不能太长,也不宜太短。原则上,一个函数只做一件事情。
- 第三:传参不宜太多。 在ARM体系下,传参不宜超过4个,如果需要的传参过多则考虑结构体打包。
- 第四:尽量少碰全局变量。 函数最好用传参返回值来和外部交换数据,不要用全局变量。
4.6.5.3、函数是动词、变量是名词(面向对象中分别叫方法和成员变量)
- (1)函数将来被编译成可执行的代码段,变量(主要指全局变量)经过编译后变成了数据或者在运行时编程数据。一个程序的运行需要代码和数据两方面的配合。
- (2)代码和数据需要彼此配合,代码是为了加工数据,数据必须借助代码起作用。拿现实生活中的工厂来比喻:数据是原材料,代码是加工流水线。名词性的数据必须经过动词性的加工才能变成最终我们需要产出的数据。这个加工的过程就是程序的执行过程。
4.6.5.4、函数的实质是:数据处理器
- (1)程序的主体是数据,也就是说程序运行的主要目标是生成目标数据,我们写代码也是为了目标数据。我们如何得到数据?必须2个因素:原材料+加工算法。原材料就是程序的输入数据,加工算法就是程序。
- (2)程序的编写和运行就是为了把原数据加工成目标数据,所以程序的实质是一个数据处理器
- (3)函数就是程序的一个缩影,函数的参数列表其实就是为了给函数输入原材料数据,函数的返回值和输出型参数就是位了向外部输出目标数据。函数的函数体内的代码就是加工算法。
- (4)函数在静止没有执行时(乖乖的在硬盘里)的时候,只占用一些存储空间,并不占用资源(cpu+内存);函数的每一次运行就好像机器的每一次开机运行。运行时需要耗费资源:CPU+内存。运行时可以对数据进行加工生成目标数据,运行完毕可以释放占用的资源。
- (5)整个程序的运行其实就是多个函数相继运行的连续过程。
4.6.6函数的基本使用
4.6.6.1函数三要素:定义、声明、调用
- (1)函数的定义就是函数体、函数声明是函数原型,函数调用就是使用函数
- (2)函数定义是函数的根本,函数定义中的函数名表示了这个函数在内存中的首地址,所以可以用函数名来调用这个函数(实质上是指针解引用访问);函数定义中的函数体是函数执行的关键。函数将来执行时主要是执行函数体,所以一个函数没有定义就是无基之塔。
- (3)函数声明的主要作用就是告诉编译器这个函数的原型。
- (4)函数调用就是调用执行一个函数,函数调用是可以反复执行的。
4.6.6.2函数原型和作用
- (1)函数原型就是函数的声明,说白了就是函数的函数名、返回值类型、参数列表。
- (2)函数原型的主要作用就是给编译器提供原型,让编译器在编译我们的程序是,帮我们进行参数的静态类型检查
- (3)必须明白:编译器在编译的时候是以单个源文件来编译的(所以一定要在哪里定义就在哪里声明),而且编译器工作时已经过了预处理处理了。最最重要的是编译器编译文件时是按照文件中语句的先后顺序执行的。
- (4)编译器从源文件的第一行开始编译,遇到一个函数声明时就会收到编译器的函数声明的表中。然后继续向后。当遇到一个函数调用时,就在我的本文件函数声明表,看看函数原型中有没有相对应的函数(这个相对应的函数有且只有一个)。如果没有或者只有部分匹配,则会报错或者报警告,如果发现多个就会报错或者警告(函数重复了,C语言中不允许两个函数的额原型完全一样,这个过程是在编译器遇到函数定义时完成的。所以函数可以重复声明但是不可以重复定义。)
4.6.6.3函数传参
传参个数太多则通过打包成结构体传指针
const 关键字
递归函数
4.6.7.1什么是递归函数
- (1)递归函数就是函数调用了自己本身这个的函数
- (2)递归函数和循环的区别。递归不等于循环
- (3)递归函数解决问题的典型就是:求阶乘、求菲波那切数列
4.6.7.2函数的递归调用原理
- (1)实际上递归函数是在栈内存上递归执行的,每次递归执行一次就需要耗费一定的栈内存。
- (2)栈内存的大小是限制递归深度的重要因素
4.6.7.3使用递归函数的原则:收敛性、栈溢出
- (1)收敛性就是说:递归函数必须要有一个终止递归的条件,当每次这个函数被执行的时候,我们判断一个条件决定是否继续递归,这个条件最终必须要满足,如果没有递归种植条件或者这个条件永远不能被满足,则这个递归没有收敛性,这个递归最终要失败。
- (2)因为递归是占用栈内存的。每次递归调用都会消耗一些栈内存,因此必须在栈内存耗尽之前递归收敛(终止),否则栈会溢出。
- (3)递归函数的使用时有一定的风险的,必须把握好
//用递归函数来计算阶乘 #include<stdio.h> int main(void) { printf("%d的阶乘等于:%d.\n",n,jiecheng(5)); return 0; } int jiecheng(int n) { if(n<0) { printf("n必须大于等于1.\n"); } else if((n == 0)||(n == 1)) { return 1; } else { return(n * jiecheng(n-1)); } }
4.6.8函数库
4.6.8.1什么是函数库?
- (1)函数库就是一些事先写好的函数的集合,给别人复用。
- (2)函数是模块化的,因此可以被复用。我们写好一个函数,可以被反复使用。也可以A写好了一个函数然后共享出来,当B有需求时就不需要自己写,直接用A的这个函数就可以
函数库的由来
- (1)最开始是没有函数库的,每个人写程序都要自己从零开始写,时间长了,慢慢的早起的程序员就积累下来了一些有用的函数。
- (2)早起的程序员经常参加行业聚会,在聚会上大家互相交换自己的函数库
- (3)后来程序员中的一些大神就提出把大家各自的函数库收拢在一起,然后经过校准和整理,最后形成了一份标准化的函数库,就是现在的标准的函数库,譬如说glibc.
4.6.8.3函数库的提供形式:动态链接库与静态链接库
- (1)早起的函数共享都是以源代码的形式进行进行的。这种方式是最彻底的(后来这种源码共享的方向就形成了我们现在的开源社区)。但是这种方式的缺点就是无法以商业形式来发布函数库。
- (2)商业公司需要将自己的有用的函数库共享给别人(当然是付费的),但是又不能给客户源代码,这时候的解决办法就是以库的方式(主要有两种:动态链接库,静态链接库)的形式来提供
- (3)比较早出现的是静态链接库,静态链接库其实就是商业公司将自己的函数库源代码经过只编译不连接形成.o的目标文件,然后用ar工具将.o文件归档成.a的归档文件(.a的归档文件又叫做静态链接库文件)。商业公司通过发布.a库文件和.h头文件来提供静态链接库给客户使用。客户拿到.a和.h文件得知库文件中库函数的原型是什么样子的,然后在自己的.c文件中直接调用这些库文件。在链接的时候,编译器会在.a归档文件中拿出被调用的那个函数的编译后的.o的二进制代码段链接进去最终生成可执行程序。
- (4)动态链接库比静态链接库出现的晚一些,效率更高一些,是改进型,现在我们一般是使用动态链接库的。静态库在用户链接自己的可执行程序时,就已经把调用的库函数的代码段链接进最终可执行程序中了。这样的好处是可以执行了,坏处是太占地方了。尤其是有多个应用程序都使用了库函数时,实际上在多个应用程序最后生成的可执行程序中都各自包含有一份这个库函数的代码段。这些应用程序同时在内存中运行时,实际上在内存中有多个代码段,这完全重复了。而动态链接库本身不将代码段连接到可执行程序,知识做个标记。然后当应用程序在内存中执行时,运行时环境发现他调用了饿一个额动态链接库中的库函数时,会去加载这个动态库到内存中。以后不管有多少个应用程序去调用这个库中的函数都会跳转到第一次加载的地方去运行(不会重复加载)
4.6.8.4函数库中库函数的使用
- (1)gcc中编译链接程序默认是使用动态链接库的,要想静态链接就需要显示用 -static来强制静态链接。
- (2)库函数的使用需要注意,第一,包含相应的头文件;第二,调用库函数时注意函数原型。第三,有些库函数连接时需要额外-lxxx来指定链接。第四,如果是动态库,要注意-L指定动态库地址。
4.6.9字符串函数
4.6.9.1、什么是字符串
(1)字符串就是由多个字符在内存中连续分布组成的字符结构。字符串的特点是指定了开头(字符串的指针)和结尾(结尾固定位‘\0’)而没有指定长度(长度由开头地址和结尾地址相减得到)
4。6.9.2为什么要讲字符串处理函数
- (1)函数库为什么要包含字符串处理函数?因为字符串处理的需求时客观的,所以从很早开始人们就在写很多关于字符串处理的函数,逐渐形成了现在的字符串处理函数库。
- (2)面试笔试时,常用字符串处理也是经常考到的点。
4.6.9.3、常用字符串处理函数
(1)C库中字符串处理函数包含在string.h中,这个文件在ubuntu系统中/usr/include/string.h
(2)常见的字符串处理函数及其作用:
memcpy 确定src和dst不会overlap,则使用memcpy效率高
memmove 确定overlap或者不确定但是有可能overlap,则使用memmove保险
memset
memcmp
memchr
strcpy
strncpy
strcat
strncat
strcmp
strncmp
strdup
strndup
strchr
strstr
strtok
.........
4.6.10.数学库函数
4.6.10.1、math.h
- (1)真正的数学运算的函数定义在:/usr/include/i386-linux-gnu/bits/matncalls.h
- (2)使用数学库函数的时候,只需包含math.h即可
4.6.10.2、计算开平方
- (1)库函数:double aqrt(double x);
注意区分编译时警告/错误,和链接时的错误:
编译时警告/错误:
4.6.10.math.c: In function ‘main’: 4.6.10.math.c:9:13: warning: incompatible implicit declaration of built-in function ‘sqrt’ [enabled by default] double b = sqrt(a); ^ /tmp/ccAgb7ti.o: In function `main': 4.6.10.math.c:(.text+0x1b): undefined reference to `sqrt' collect2: error: ld returned 1 exit status
链接时错误:
4.6.10.math.c:(.text+0x1b): undefined reference to `sqrt' collect2: error: ld returned 1 exit status
分析:表明编译是没有问题的,函数本身有声明也有定义,但是链接的时候找不到对应的函数的函数体,从而发生错误。sqrt本来是库函数,在编译器
中是.a和.so链接库的(函数体在链接库中的)
C连接器的工作特点:因为库函数有很多,连接器去库函数目录搜索时间比较久。为了提升速度想了一个这种的方案:链接器只是默认寻找几个常用的库,
如果一些不常用的函数库被调用,则需要程序员连接时明确给出要扩展查找的库的名字。链接时可以用-lxxx来指示连接器去到libxxx.so中查找这个函数。
4.6.10.3链接时加-lm
(1)-lm就是告诉编译器到libm中去查找用到的函数。
(2)实战中发现在高版本的gcc中,经常会出现没加-lm也可以编译链接
root@ubuntu:/mnt/hgfs/winshare/cAdverance/4.6# gcc 4.6.10.math.c -lm //到libm.so中寻找 root@ubuntu:/mnt/hgfs/winshare/cAdverance/4.6# ./a.out b = 4.000000. //查找这个函数执行一共用到了哪些库 root@ubuntu:/mnt/hgfs/winshare/cAdverance/4.6# ldd a.out linux-gate.so.1 => (0xb7755000) libm.so.6 => /lib/i386-linux-gnu/libm.so.6 (0xb76f8000) libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7549000) /lib/ld-linux.so.2 (0xb7756000)
4.6.11.自己制作静态链接库并使用后
(1)第一步:自己制作静态链接库
.c文件写好自己的函数 写好.c文件对应的头文件.h文件 写个makefile
/*******aston.c*******/ #include <stdio.h> void func1(void) { printf("func1 in aston.c.\n"); } int func2(int a, int b) { printf("func2 in aston.c.\n"); return a+b; } /********aston.h*********/ void func1(void); int func2(int a, int b); /******make******/ all: gcc aston.c -o aston.o -c ar -rc libaston.a aston.o
首先使用gcc -c只编译不链接,生成.o文件。然后使用ar工具进行打包成.a归档文件
库名不能随便乱起,一般是lib+库名称,后缀名是.a表示一个归档文件
注意:制作出来静态库之后,发布时需要发布.a文件和.h文件
(2)第二步使用静态链接库
把.a和.h都放在我引用的文件夹下,然后在.c文件中包含库的.h文件,然后直接使用库函数
第一次编译方法:gcc test.c -o test
报错信息:test.c:(.text+0xa): undefined reference to `func1'
test.c:(.text+0x1e): undefined reference to `func2'
第二次编译方法:gcc test.c -o test -laston
报错信息:/usr/bin/ld: cannot find -laston
第三次编译方法:gcc test.c -o test -laston -L.
无报错生成test
(3)除了ar命令外,还有个nm命令也很有用,他可以用来查看一个.a文件中有哪些符号
4.6.12自己制作动态链接库
(1)动态链接库的后缀名是.so(对应windows系统的dll),静态链接库的扩展名是.a
(2)第一步:创建一个动态链接库
gcc aston.c -o aston.o -c -fPIC
gcc -o libaston.so aston.o -shared
-fPIC是位置无关吗,-shared是按照共享库的方式来连接
注意:做库的人给用库的人发布库时
(3)第二步:使用自己创建的共享库
第一次编译:编译方法:gcc test.c -o test
报错信息:test.c:(.text+0xa): undefined reference to `func1'
test.c:(.text+0x1e): undefined reference to `func2'
collect2: error: ld returned 1 exit status
第二次编译:编译方法:gcc test.c -o test -laston
报错信息:/usr/bin/ld: cannot find -laston
collect2: error: ld returned 1 exit status
第三次编译:编译方法:gcc test.c -o test -laston -L.
编译成功但是运行出错:./test: error while loading shared libraries: libaston.so: cannot open shared object file: No such file or directory
错误原因:动态链接库运行时需要被加载(运行时环境在执行test程序是发现他动态链接了libaston.so,于是就会去固定目录尝试加载libaston.so,如果加载失败就会打印以上错误信息。)
- 解决方法一:将linaston.so放在固定目录下,这个固定目录一般是/usr/lib目录
cp libaston.so /usr/lib即可
- 解决方法二:使用环境变量 LD_LIBRARY_PATH操作系统在加载固定目录/usr/lib之前,会先去LD_LIBRARY_PATH这个环境变量所指定的目录下去寻找,如果找到就不用去/usr/lib下面找了,如果没找到再去/usr/lib下面找。所以解决方案就是将libaston.so导出到环境变量LD_LIBRARY_PATH中即可。
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/mnt/hgfs/winshare/cAdverance/4.6/4.6.12.shareobject/sotetest
- 在ubuntu中还有解决方法三:用ldconfig
(4)ldd命令:作用是可以在一个使用了共享库的程序的执行之前解析出这个程序使用了哪些共性库,并且查看这些共享库是否能被找到,能被解析(决定这个程序能否被正确执行)