1.C语言预处理理论
1.1、由源码到可执行程序的过程
(1)源码.c→(预处理)→预处理过的.i源文件→(编译)→汇编文件.s→(汇编)→目标文件.o→(链接)→elf可执行程序
(2)预处理用预处理器,编译用编译器,汇编用汇编器,链接用链接器,这几个工具再加上其他一些额外的会用到的可用工具,合起来叫编译工具链。gcc就是一个编译工具链。
1.2、预处理的意义
(1)编译器本身的主要目的是编译源代码,将c的源代码转换成.s的汇编代码。编译器聚集核心功能后,就剥离出了一些非核心的功能到预处理了。
(2)预处理器帮编译器做了一些编译前的杂事。
1.3、gcc中只预处理不编译的方法
(1)gcc -E xx.c -o xx.i
可以实现只预处理不编译,一般情况下没必要只预处理不编译,但有时候这种技巧可以用来帮助我们研究预处理过程,帮助debug程序。
1.4、总结:宏定义被预处理时的现象
(1)第一,宏定义语句本身不见了(可见编译器根本就不认识#define,编译器根本不知道还有个宏定义)。
(2)第二,typedef重命名语句还在,说明它和宏定义是有本质区别的(说明typedef是由编译器来处理而不是预处理的)。
#define pchar char *
typedef char * PCHAR
int main(void)
{
pchar p1, p2;
return 0;
}
预处理:gcc -E xx.c -o xx.i
然后打开xx.i文件查看,发现宏定义都被替换
2.C语言预处理代码实战
2.1、头文件包含
(1)#include <>和#include ""的区别:<>专门用来包含系统提供的头文件(就是系统自带的,不是程序员自己写的),""用来包含自己写的头文件;更深层次来说:<>的话C语言编译器只会到系统指定目录(编译器配置的或者操作系统配置的寻找目录,比如在ubuntu中是/usr/include目录,编译器还允许用-I来附加指定其他的包含路径)去寻找这个头文件(隐含的意思就是不会找当前目录下),如果找不到就会提示这个头文件不存在。
(2)""包含的头文件,编译器默认会先在当前目录下寻找相应的头文件,如果没找到然后再到系统指定目录去寻找,如果还没找到则提示文件不存在。
总结:规则虽然允许用双引号来包含系统指定的目录,但是一般的使用原则是:如果是系统指定的自带的用<>;如果是自己写的在当前目录下放着用"",如果是自己写的但是集中放在了一起转么内存放头文件的目录下将来在编译器中用-I参数来寻找,这种情况下用<>。
(3)头文件包含的真实含义就是:在#include <xx.h>
的那一行,将xx.h这个头文件的内容原地展开替换这一行#include语句。过程在预处理中进行。
2.2、注释
(1)注释是给人看的,不是给编译器看的。
(2)编译器既然不看注释,那么编译时最好没有注释。实际上在预处理阶段,预处理器会拿掉程序中所有注释语句,到了编译器阶段程序中其实已经没有注释了。
2.3、条件编译
(1)有时候我们希望程序有多种配置,我们在源代码编写时写好各种配置的代码,然后给个配置开关,在源代码级别去修改配置开关来让程序编译出不同的效果。
(2)条件编译中用的两种条件判定方法分别是#ifdef和#if,它们的区别是#ifdef xxx判定条件成立与否时主要是看xxx这个符号在本语句之前有没有被定义,只要定义了(我们可以直接#define XXX或者#define XXX 12或者#define XXX YYY)这个符号就是成立的。
格式:#if(条件表达式)
它的判定标准是()中的表达式是否为true还是false,跟C中的if语句有点像。
3.宏定义
3.1、宏定义的规则和使用解析
(1)宏定义的解析规则就是:在预处理阶段由预处理器进行替换,这个替换是原封不动的替换。
(2)宏定义替换会递归进行,直到替换出来的值本身不再是一个宏为止。
(3)一个正确的宏定义式子本身分为3部分:第一部分是#define,第二部分是宏名,剩下的宏体为第三部分。
(4)宏可以带参数,称为带参宏。带参宏的使用和带参函数非常像,但是使用上有一些差异。在定义带参宏时,每一个参数在宏体中引用时都必须加括号,最后整体再加括号,括号缺一不可。
3.2、宏定义示例1:MAX宏
#define MAX(a, b) (((a) > (b)) ? (a) : (b))
int main (void)
{
int x =3, y =4;
int max = MAX(2+x, y);
printf("max = %d.\n", max);
return 0;
}
关键:三目运算符,括号使用
3.3、宏定义示例2:SEC_PER_YEAR
用宏定义表示一年有多少秒
#define SEC_PER_YEAR (365*24*60*60UL)
int main(void)
{
unsigned int l = SEC_PER_YEAR;
printf("l = %u.\n", l);
return 0;
}
关键:
①第一点:当一个数字直接出现在程序时,它的类型默认是int。所以用U
②第二点:一年有多少秒,这个数字刚好超过了int类型存储的范围。所以用L
3.4、带参宏和带参函数的区别(宏定义的缺陷)
(1)宏定义是在预处理期间处理的,而函数是在编译期间处理的。这个区别带来的实质差异是:宏定义最终是在调用宏的地方把宏体原地展开,而函数是在调用函数处跳转到函数中去执行,执行完后再跳转回来。
注意:宏定义和函数的最大差别就是
宏定义是原地展开,因此没有调用开销;而函数是跳转执行再返回,因此函数有比较大的调用开销。所以宏定义和函数相比,优势就是没有调用开销,没有传参开销。所以当函数体很短(尤其是一句话时),可以用宏定义来代替,这样效率高。
(2)带参宏和带参函数的一个重要差别是:宏定义不检查参数的类型,返回值也不会附带类型;而函数有明确的参数类型和返回值类型。当我们调用函数时编译器会帮我们做参数的静态类型检查,如果编译器发现我们实际传参和参数声明不同时会报警告或错误。
注意:用函数的时候程序员不太用操心类型不匹配,因为编译器会检查,如果不匹配编译器会提示;用宏定义的时候程序员必须很注意实际传参和宏所希望的参数类型要一致,否则可能编译不报错但是运行有误。
3.5、内联函数和inline关键字
(1)内联函数通过在函数定义前加inline关键字实现。
(2)内联函数本质上是函数,所以有函数的优点(内联函数和是编译负责处理的,编译器可以帮我们做参数的静态类型检查);但是他同时也有带参宏的优点(不用调用开销,而是原地展开)。所以几乎可以这样认为:内联函数就是带了参数静态类型检查的宏。
(3)当我们的函数内的函数体很短(比如只有一两句)的时候,我们又希望利用编译器的参数类型检查来排错,我还希望没有调用开销时,最适合使用内联函数。
3.6、宏定义来实现条件编译(#define 、#undef、#ifdef)
程序有DEBUG版本和RELEASE版本,区别就是编译时有无DEBUG宏。
#define DEBUG
#undef DEBUG // 注销一个宏,如果前面有宏定义这个宏则取消这个宏
#ifdef DEBUG
#define debug(x) printf(x)
#else
#define debug(x)
#endif
4.递归函数
4.1、什么是递归函数
(1)递归函数就是函数中调用了自己本身这个函数的函数。
int jiecheng(int n)
{
// 传参错误检验
if (n < 1)
{
printf("n必须大于或等于1.\n");
return -1;
}
if (n == 1)
{
return -1;
}
else
{
// 一直往里走,直到最后执行return才一层层出来
return (n*jiecheng(n-1));
}
}
(2)递归函数和循环的区别。递归不等于循环;
(3)递归函数解决的经典问题:求阶乘、斐波那契数列
4.2、函数的递归调用原理
(1)实际上递归函数是在栈内存上递归执行的,每次递归执行一次就需要耗费一些栈内存。
(2)栈内存的大小是限制递归深度的重要因素。
4.3、使用递归函数的原则:收敛性、栈溢出
(1)收敛性就是说:递归函数必须有一个终止递归的条件。当每次这个函数被执行时,我们判断一个条件决定是否递归,这个条件最终必须能够被满足。如果没有递归终止条件或者这个条件永远不能被满足,则这个递归没有收敛性,这个递归最终要失败。
(2)因为递归是占用栈内存的,每次递归调用都会消耗一些栈内存。因此必须在栈内存消耗之前递归收敛(终止),否则会栈溢出,段错误。
(3)递归函数的使用时有风险的。
void digui(int n)
{
int a[100];
if (n >1)
{
digui(n-1);
}
else
{
printf("结束递归,n = %d.\n", n);
}
printf("递归后:n = %d.\n", n);
}
5.函数库
5.1、什么是函数库
(1)函数库就是一些事先写好的函数的集合,给别人复用。
(2)函数是模块化的,因此可以被复用。我们写好了一个函数,可以被反复使用。也可以A写好了一个函数然后共享出来,当b有相同的需求时就不需要自己写,直接用A写好的这个函数即可。
5.2、函数库的由来
(1)最开始是没有函数库,每个人写程序都要从零开始自己写。时间长了慢慢的,早起程序员就积累下来了一些有用的函数。
(2)后来程序员中的一些大神就提出把大家各自的函数库收拢在一起,然后经过校准和管理,最后形成了一份标准化的函数库,就是现在的标准函数库,比如glibc。
5.3、函数库的提供形式:动态链接库与静态链接库
(1)早起的函数共享都是以源代码的形式进行的。这种方式共享是最彻底的(后来这种源码共享的方式就形成了我们现在的开源社区)。但是这种方式有它的缺点,缺点是无法以商业化形式来发布函数库。
(2)商业公司需要将自己的有用函数库共享给别人(当然是付费的),但是又不能给客户源代码。这时候的解决方案就是以库(主要有2种:静态库和动态库)的形式来提供。
(3)比较早出现的是静态链接库。静态库其实就是商业公司将自己的函数库源代码经过只编译不链接形成.o的目标文件,然后用ar工具将.o文件归档为.a的归档文件(.a的归档文件又叫静态链接库文件)。商业公司通过发布.a库文件和.h头文件来提供静态库给客户使用。客户拿到.a和.h文件后,通过.h文件得知库中的库函数的原型(就是函数名,参数列表,返回值类型),然后在自己的.c文件中直接调用这些库文件,在链接的时候链接器会去.a文件中拿出被调用的那个函数的编译后的.o二进制代码段链接进去形成最终的可执行程序。
(4)动态链接库比静态链接库出现的晚一些,效率更高一些,是改进型的。现在我们一般都是使用动态库。静态库在用户链接自己的可执行程序时就已经把调用的库中的函数的代码段链接进最终可执行程序中了,这样好处是可以执行,坏处是太占地方了。
尤其是有多个应用程序都是用了这个库函数时,实际上在多个应用程序最后生成的可执行程序中都各自有一份这个库函数的代码段。当这些应用程序同时在内存中运行时,实际上在内存中有对个这个库函数的代码,这完全重复了。而动态链接库本身不将库函数的代码段链接入可执行程序,只是做个标记。然后当应用程序在内存中执行时,运行时环境发现它调用了一个动态库中的库函数时,会去加载这个动态库的函数到内存中,然后以后不管有多少个应用程序去调用这个库中的函数都会跳转到第一次加载的地方去执行(不会重复加载)。
举例:
int main(void)
{
printf("hello world.\n");
return 0;
}
编译对比:
①动态链接库:gcc xx.c
②静态链接库:gcc xx.c -static
5.4、函数库中函数的使用
(1)gcc中编译链接程序默认是使用动态库的,要像静态链接需要显式用"-static"来强制静态链接。
(2)库函数的使用需要注意3点:
①第一,包含相应的头文件;
②第二,调用库函数时注意函数原型;
③第三,有些库函数链接时需要额外用"-lxx"来指定链接;
④第四,如果是动态库,要注意"-L"指定动态库的地址。
6.字符串处理函数
6.1、什么是字符串
字符串就是由多个字符在内存中连续分布组成的字符结构。字符串的特点是指定了开头(字符串的指针)和结尾(结尾固定字符为'\0'),而没有指定长度(长度由开头地址和结尾地址相减得到)。
6.2、为什么要讲字符串处理函数
(1)函数库为什么要包含字符串处理函数?因为字符串处理的需求时客观的,所以从很早开始人们就在写很多关于字符串处理的函数,然后逐渐形成了现在的字符串处理函数库。
(2)面试笔试时,常用字符串处理函数也是经常考到的。
6.3、常用字符串处理函数
(1)C库中字符串处理函数包含在string.h中,这个文件在ubuntu系统中在/usr/include中。
(2)常见字符串处理函数及作用:
①memcpy:确定src和dst不会overlap(内存重叠),则用memcpy效率高;
②memmove:不确定overlap,则用memmove更保险
③还有很多,研究库函数是最好的打基础方法,先以4~5个博客看知识,然后总结。
7.数学库函数
7.1、math.h
(1)真正的数学运算的函数定义在:
/usr/include/i386-linux-gnu/bits/mathcalls.h
(2)使用数学库函数的时候,只需要包含math.h即可。
7.2、计算开平方
(1)库函数:double sqrt(double x);
#include <stdio.h>
#include <math.h>
int main(void)
{
double a = 16.0;
double b = sqrt(a);
printf("b = %lf.\n", b);
return 0;
}
直接编译报错:
xx.c:(.text+0x1b):undefined reference to 'sqrt' collect2:error:ld returned 1 exit status
看到ld知道这是个链接错误:sqrt函数有声明(声明在math.h中)有引用但是没定义,链接器找不到函数体。sqrt本来是库函数,在编译器库中是有.a和.so链接库的(函数体在链接库中)。
(2)C链接器的工作特点:因为库函数有很多,链接器去库函数目录搜索的时间比较久。为了提升速度想到了一个折中的方案:链接器只是默认的寻找几个最常用的库,如果是一些不常用的库中的函数被调用,需要程序员在链接时明确给出要扩展查找的库的名字。链接时可以用"-lxx"来指示链接器去到"libxx.so"中去查找这个函数。
示例:
gcc xx.c -lm
// 编译正常,链接到了math.h库函数
ldd a.out
// 可查找链接器链接了哪些文件
7.3、链接时加“-lm”
(1)"-lm"就是告诉链接器到libm中去查找用到的函数
(2)实战中发现在高版本的gcc中,经常会出现没加"-lm"也可以编译链接的。
7.4、自己制作静态链接库并使用
(1)第一步:自己制作静态链接库
首先,使用gcc -c只编译不链接,生成.o目标文件,然后使用ar工具进行打包成.a归档文件,库名不能随便乱起,一般是lib+库名字,后缀名是.a表示是一个归档文件。
注意:制作出来了静态库之后,发布时需要发布.a文件和.h文件。
(2)第二步:使用静态链接库
把.a和.h都放在我引用的文件夹下,然后在.c文件中包含库的.h,然后直接使用库函数。
(aston.c)
#include <stdio.h>
void func1()
{
printf("func1 in aston.c.\n");
}
int func(int a, int b)
{
printf("func2 in aston.c.\n");
return a+b;
}
(test.c)
#include <stdio.h>
#include "aston.h"
int main(void)
{
func1();
int b = func2(3, 8)
printf("b = %d.\n", b);
return 0;
}
(aston.h)
void func1(void)
int func2(int a, int b)
(touch Makefile)
all:
gcc aston -o aston.o -c
ar -rc libaston.a aston.o
Makefile是执行语句的集合文件,第一句表示只编译不链接;libaston.a这个起名有讲究,lib后面的名字必须与包名一致,也就是上面的aston.c。
这个起名可以通过下面的例子联想
math.c → limb → -lm
aston.c → libaston → -laston
编译尝试并分析:
①第一次,编译方法:gcc test.c -o test
报错信息:test.c:(.text+0xa):undefined reference to 'func1'. test.c:~~
②第二次,编译方法:gcc test.c -o test -laston
报错信息:/usr/bin/ld:cannot find -laston collect2:error:ld return 1exit status
③第三次,编译方法:gcc test.c -o test -laston -L.
无报错,生成test,执行正确。注意L后面有个点.
(3)除了ar命令以外,还有个nm命令也很有用,它可以用来查看一个.a文件中都有哪些符号。
7.5、自己制作动态链接库并使用
(1)动态链接库的后缀名是.so(对应Windows的ddl),静态库的扩展名是.a。
(2)第一步。创建一个动态链接库
(touch Makefile)
gcc aston.c -o aston.o -c -fPIC
gcc -o libaston.so aston.o -shared
-fPIC是位置无关码
-shared是按照共享库的方式来链接的
文件存放:
sotest文件夹内:aston.h、libaston.so、test.c
sotest文件夹本级目录:aston.c、aston.h、Makefile
(test.c)
#include <stdio.h>
#include "aston.h"
int main(void)
{
func1();
int b = func2(3, 8)
printf("b = %d.\n", b);
return 0;
}
注意:做库的人给用库的人发布时,只要发布libxxx.so和xxx.h即可。
(3)第二步,使用动态链接库
gcc test.c -o test -laston -L.
编译成功。但是运行报错:error while loading shared libraries:libaston.so:cannot open shared object file:No such file or directory。
错误的原因:动态链接库运行时需要被加载(运行时环境在执行test程序的时候发现它动态链接libaston.so,于是乎回去固定目录尝试加载libaston.so,如果加载失败则会打印错误信息)。
解决方法:
①将libaston.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:(pwd查看)
,无括号
③在ubuntu中还有个解决方法:用ldconfig,具体可以去网上查找。
(4)ldd命令:作用是可以在一个使用了共享库的程序执行之前解析出这个程序使用了哪些共享库,并且查看这些共享库是否能被找到,能被解析(决定这个程序是否能正确执行)。
比如:ldd test
发现有一个not found则运行后肯定会报错。这是一种不运行程序也可以查看是否会报错的方法。
网友评论