一、预处理
1.预处理的基本概念
C语言对源程序处理的四个步骤:预处理、编译、汇编、链接。
预处理是在程序源代码被编译之前,由预处理器(Preprocessor)对程序源代码进行的处理。这个过程并不对程序的源代码语法进行解析,但它会把源代码分割或处理成为特定的符号为下一步的编译做准备工作。
1.1 文件包含指令(#include)
1.1.1 文件包含处理
“文件包含处理”是指一个源文件可以将另外一个文件的全部内容包含进来。C语言提供了#include命令用来实现“文件包含”的操作。
1.1.2 #incude<> 和 #include"" 区别
(1)"" 表示系统先在file1.c所在的当前目录找file1.h,如果找不到,再按系统指定的目录检索。
(2)< > 表示系统直接按系统指定的目录检索。
1.1.3 注意:
1. #include <>常用于包含库函数的头文件;
2. #include ""常用于包含自定义的头文件;
3. 理论上#include可以包含任意格式的文件(.c .h等) ,但一般用于头文件的包含;
2.宏定义
2.1 无参数的宏定义(宏常量)
如果在程序中大量使用到了100这个值,那么为了方便管理,我们可以将其定义为:
const int num = 100;
但是如果我们使用num定义一个数组,在不支持c99标准的编译器上是不支持的,因为num不是一个编译器常量,如果想得到了一个编译器常量,那么可以使用:
#define num 100
在编译预处理时,将程序中在该语句以后出现的所有的num都用100代替。这种方法使用户能以一个简单的名字代替一个长的字符串,在预编译时将宏名替换成字符串的过程称为“宏展开”。宏定义,只在宏定义的文件中起作用。
#define PI 3.1415
void test(){
double r = 10.0;
double s = PI * r * r;
printf("s = %lf\n", s);
}
2.2 注意事项:
-
宏名一般用大写,以便于与变量区别;
-
宏定义可以是常数、表达式等;
-
宏定义不作语法检查,只有在编译被宏展开后的源程序才会报错;
-
宏定义不是C语言,不在行末加分号;
-
宏名有效范围为从定义到本源文件结束;
-
可以用#undef命令终止宏定义的作用域;
-
在宏定义中,可以引用已定义的宏名;
2.3 带参数的宏定义(宏函数)
在项目中,经常把一些短小而又频繁使用的函数写成宏函数,这是由于宏函数没有普通函数参数压栈、跳转、返回等的开销,可以调高程序的效率。
宏通过使用参数,可以创建外形和作用都与函数类似地类函数宏(function-like macro). 宏的参数也用圆括号括起来。
#define SUM(x,y) (( x )+( y ))
void test(){
//仅仅只是做文本替换 下例替换为 int ret = ((10)+(20));
//不进行计算
int ret = SUM(10, 20);
printf("ret:%d\n",ret);
}
注意:
- 宏的名字中不能有空格,但是在替换的字符串中可以有空格。ANSI C允许在参数列表中使用空格;
- 用括号括住每一个参数,并括住宏的整体定义。
- 用大写字母表示宏的函数名。
- 如果打算宏代替函数来加快程序运行速度。假如在程序中只使用一次宏对程序的运行时间没有太大提高。
3. 条件编译
一般情况下,源程序中所有的行都参加编译。但有时希望对部分源程序行只在满足一定条件时才编译,即对这部分源程序行指定编译条件。
3.1 防止头文件被重复包含引用
#ifndef _SOMEFILE_H
#define _SOMEFILE_H
//需要声明的变量、函数
//宏定义
//结构体
#endif
3.2 一些特殊的预定宏
C编译器,提供了几个特殊形式的预定义宏,在实际编程中可以直接使用,很方便。
// __FILE__ 宏所在文件的源文件名
// __LINE__ 宏所在行的行号
// __DATE__ 代码编译的日期
// __TIME__ 代码编译的时间
void test()
{
printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
}
二、源代码的组织与使用
我们通常把公用的,成熟的,可复用的函数和类封装好,函数和类的声明在头文件中,函数和类的定义在程序文件中。当其他的程序要使用的时候,程序中要包含头文件,编译时要封装好的函数与类和程序文件一起编译。这样我们就不用每次要用到某个功能或者函数的时候,都要再写一遍代码在程序中。
1. 先自定义一个头文件(.h)
在封装代码之前,首先我先定义(创建)一个头文件。因为你要使用封装的代码,要将这个头文件包含进去,才能找到封装的函数或者类的声明,我们知道一个函数要使用之前要有声明。文件名为:
package.h
文件名是你自己取的,但是要注意后缀是 .h
2. 将代码的声明放入头文件中
定义好了,也就是创建好了一个头文件之后,就将要封装的代码的声明放在头文件里面。这次我要封装的一个两数相加的函数,就把这个函数的声明放在这个头文件里面。
#ifndef PACKAGE_H
#define PACKAGE_H 1
#include <stdio.h>
#include <string.h>
// 用于两个数相加
int Add(int a, int b);
#endif
3.写封装代码的定义(.c)
(1)在声明了函数之后,我们要去实现这个函数。函数的实现,就放在与头文件同名的文件里面,但要注意文件后缀是 .c 。头文件的名为package.h,所以我们要创建一个 package.c 文件,来存放函数的实现。
(2)还要注意一个点,记得将头文件包含进函数的实现的文件里。将函数的实现放在package.c 文件里面。
#include "package.h"
int Add(int a, int b)
{
return a+b;
}
4.使用封装好的函数
当我们想要在其他的程序中用到了封装好的函数,要怎么做
(1)将封装的头文件(package.h)包含到程序中。
(2)像普通函数一样使用
// 测试封装函数
#include "package.h"
int main()
{
int a=3;
int b=4;
int sum = Add(a,b);
printf("sum =%d\n",sum);
}
(3)编译程序的时候,加上封装函数的实现文件(.c)
在程序里面包含了封装好的代码的头文件还不行,还要在编译程序的时候,加上封装代码的实现文件(package.c)
查看结果:函数调用成功,也就代表封装成功。
5.优缺点
这样封装与使用,提高了代码的复用率与效率。但是有一些缺点,就是需要封装的源代码文件一起编译。当我们要实现某些功能,要写一些函数的时候,发现自己能力不足,不能写出来,怎么办。我们写不出来,总有人能写得出来,我们就去跟他要一份源代码,然后我们就能使用。但这个是别人的劳动成果,别人不希望给源代码你,但你又很迫切地要去使用别人实现的功能,这时候怎么办。或者说是我们自己封装的,也不想把源码给别人,那么怎么办。
封装静态或者动态库就解决了不用给源码的问题,下面就来介绍如何封装静态库和动态库。
三、静态库
公用函数库的程序文件public.cpp程序文件是源代码,对任何程序员是可见的,没有安全性可言,但是,在实际开发中,出于技术保密或其它方面考虑,开发者并不希望提供公用函数库的源代码。
C/C++提供了一个可以保证代码安全性的方法,把公共的程序文件编译成库文件,库文件是一种可执行代码的二进制形式,可以与其它的源程序一起编译,也可以被操作系统载入内存执行。
库文件分为静态库与动态库。
1.静态库
静态库在编译的时候,主程序文件与静态库一起编译,把主程序与主程序中用到的库函数一起整合进了目标文件。这样做优点是在编译后的可执行程序可以独立运行,因为所使用的函数都已经被编译进去了。缺点是,如果所使用的静态库发生更新改变,我们的程序必须重新编译。
静态库就相当于是某个时刻打包好的源代码文件的一个复制品,静态库与源文件是独立的,当源文件发生变化时,静态库文件并不会发生改变,除非重新编译库文件。
下面就来介绍一下如何封装静态库。
1.1 静态库的特征
静态库文件名的命名方式是“libxxx.a”,库名前加”lib”,后缀用”.a”,“xxx”为静态库名。所以在封装静态库之前,我们要给这个静态库起一个合适的名字
(1)Library file:库文件,lib 是库文件的简写
(2)static:静态,a 是静态的简写
1.2 将源文件编译成静态库
要将源文件准备好,然后使用编译指令,将源文件编译成静态库。
把程序文件 package.c 编译成静态库的指令:
gcc -c -o libpackage.a package.c
这里有一个注意的点,编译的时候是使用 -c 参数,是生成目标文件,所以静态库其实是目标文件
1.3 使用静态库
(1)使用静态库的方法一,直接把调用者源代码和静态库文件名一起编译。不用将静态库的源文件的头文件包含进去了。
gcc -g -o testPackage testPackage.c libpackage.a
(2)使用静态库的方法二,采用L参数指定静态库文件的目录,-l参数指定静态库名。
gcc -o testPackage testPackage.c -L/oracle/heima/Advanced/day07 -lpackage
执行程序:
1.4 注意事项
1)如果要指定多个静态库文件的目录,用法是“-L/目录1 -L目录2 -L目录3”;
2)链接库的文件名是libpublic.a,但链接库名是”public”,不是“libpublic.a”;
3)如果要指定多个静态库,用法是“-l库名1 -l库名2 -l库名3”。
四、动态库
动态库在编译时并不会被连接到目标代码中,而是在程序运行时才被载入,因此在程序运行时还需要指定动态库的目录。
1.1 动态库的特征
动态库的命名方式与静态库类似,前缀相同,为“lib”,后缀变为“.so” “xxx”为动态库名。
1.2 将源文件编译成动态库
把程序文件public.cpp编译成动态库的指令:
gcc -fPIC -shared -o libpackageso.so package.c
1.3 使用动态库
使用动态库的方法与使用静态库的方法相同。
如果在动态库文件和静态库文件同时存在,优先使用动态库编译。
gcc -o testPackage testPackage.c -L/oracle/heima/Advanced/day07 -lpackageso
执行程序。
./testPackage
出现了下面的提示:
1.4 指定动态库文件的目录
这是因为采用了动态链接库的可执行程序在运行时需要指定动态库文件的目录,Linux系统中采用LD_LIBRARY_PATH环境变量指定动态库文件的目录。
采用以下命令设置LD_LIBRARY_PATH环境变量。
export LD_LIBRARY_PATH=/oracle/heima/Advanced/day07:.
注意:1)如果要指定多个动态库文件的目录,用法是“export LD_LIBRARY_PATH=目录1:目录2:目录3:.”,
目录之间用半角的冒号分隔,最后的圆点指当前目录。
执行程序:成功了
1.5 环境变量永久生效
(1)export 的方式设置环境变量是暂时的,在退出Shell后就会失效,下次登录时需要重新设置。如果希望环境变量永久生效,需要在登录脚本文件中配置。
测试:1)用exit 退出
2)重新登录,执行程序,找不到库文件了
(2)系统环境变量对全部的用户生效,设置系统环境变量有三种方法。但是现在我们只介绍一种,是Linux推荐使用的。
1.5.1 在/etc/profile.d目录中增加环境变量脚本文件
在 /etc/profile.d目录 中增加环境变量脚本文件,这是Linux推荐的方法。
/etc/profile在每次启动时会执行 /etc/profile.d下全部的脚本文件。/etc/profile.d比/etc/profile好维护,不想要什么变量直接删除 /etc/profile.d下对应的 shell 脚本即可。
/etc/profile.d目录下有很多脚本文件,例如:
(1)我们知道了/etc/profile 在每次启动时会执行 /etc/profile.d下全部的脚本文件。那么我们就可以利用这个特点,在这个目录下面创建一个文件,然后把下面这句设置环境变量的指令,放到文件中就行了。每次启动就会执行到这指令,就不用我们每次都要写了。
export LD_LIBRARY_PATH=/oracle/heima/Advanced/day07:.
(2)创建一个文件,文件名如下:注意后缀为 .sh
(3)在脚本文件中设置的环境变量不会立即生效,退出Shell后重新登录时才生效,或者用source命令让它立即生效,例如:
source /etc/profile
1.6 动态库的特性
如果所使用的动态库发生更新改变,动态库重新编译就行,程序不需要重新编译,动态库升级比较方便。
测试:
(1)修改动态库中Add函数的代码。
int Add(int a, int b)
{
return a+b;
}
改为:
int Add(int a, int b)
{
printf("a\n");
return a+b;
}
(2)重新编译动态库:
gcc -fPIC -shared -o libpackageso.so package.c
(3)无需重新编译testPackage ,直接执行程序。
(4)总结:
动态库在编译的时候只做语法检查,并没有被编译进目标代码,当程序执行到动态库中的函数时才调用该函数库里的代码。动态函数库并没有整合进程序,所以程序的运行环境必须提供动态库路径。优点是,如果所使用的动态库发生更新改变,程序不需要重新编译,所以动态库升级比较方便。
五、总结
1.静态库的优缺点
1、优点
静态链接相当于复制一份库文件到可执行程序中,不需要像动态库那样有动态加载和识别函数地址的开销,也就是说采用静态链接编译的可执行程序运行更快。
2、缺点
1)静态链接生成的可执行程序比动态链接生成的大很多,运行时占用的内存也更多。
2)库文件的更新不会反映到可执行程序中,可执行程序需要重新编译。
2.动态库的优缺点
1、优点
1)相对于静态库,动态库在时候更新(修复bug,增加新的功能)不需要重新编译。
2)全部的可执行程序共享动态库的代码,运行时占用的内存空间更少。
2、缺点
1)使可执行程序在不同平台上移植变得更复杂,因为它需要为每每个不同的平台提供相应平台的共享库。
2)增加可执行程序运行时的时间和空间开销,因为应用程序需要在运行过程中查找依赖的库函数,并加载到内存中。
六、编译的优先级
静态库与动态库各有优缺点,该怎么选择,要看应用的场景。
所谓有得必有失,动态库在程序运行时被链接,故程序的运行速度和链接静态库的版本相比必然会打折扣。然而瑕不掩瑜,动态库的不足相对于它带来的好处在现今硬件下简直是微不足道的,所以链接程序在链接时一般是优先链接动态库的,除非用-static参数指定链接静态库。