五、库
a.c -> a.out
foo()
bar()
hum()
main()
单一模型:将程序中所有功能全部实现于一个单一的源文件内部。
缺点:编译时间长,不易于维护和升级,不易于协作开发。
分离模型:将程序中的不同功能模块划分到不同的源文件中。
优点:缩短编译时间,易于维护和升级,易于协作开发。
a.c -> a.o
foo() | -> …
bar() |
b.c -> b.o /
hum()
a.o
b.o | -> 库 + 其它模块 -> …
c.o |
… /
1.静态库
静态库的本质就是将多个目标文件打包成一个文件。链接静态库就是将库中被调用的代码复制到调用模块中。
使用静态库的程序通常会占用较大的空间,库中代码一旦修改,所有使用该库的程序必须重新链接。
使用静态库的程序在运行无需依赖库,其执行效率高。
静态库的形式:libxxx.a
构建静态库:
1、.c -> .o -> .a
2、ar -r libxxx.a x.o y.o z.o
^ _/
|_|
使用静态库:
gcc … -lxxx -L<库路径>
export LIBRARY_PATH=<库路径>
gcc … -lxxx
举例代码
cal.h cal.c show.h show.c main.h main.c
1、cal.h
#ifndef _CAL_H
#define _CAL_H
int add(int, int);
int sub(int, int);
#endif // _CALC_H
2、cal.c
#include "calc.h"
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
3、show.h
#ifndef _SHOW_H
#define _SHOW_H
void show(int, char, int, int);
#endif // _SHOW_H
4、show.c
#include <stdio.h>
#include "show.h"
void show(int a, char op, int b, int c) {
printf("%d %c %d = %d\n", a, op, b, c);
}
然后使用gcc -c cal.c -o cal.o / gcc -c show.c -o show.o
生成两个点o文件,然后使用 ar -r libmath.a cal.o show.o,生成libmath.a静态库。
如何去使用这个静态库呢?
main.c main.h
main.h
#ifndef _MATH_H
#define _MATH_H
#include "calc.h"
#include "show.h"
#endif // _MATH_H
main.c
#include "math.h"
int main(void) {
int a = 30, b = 20;
show(a, '+', b, add(a, b));
show(a, '-', b, sub(a, b));
return 0;
}
如果我们直接用gcc main.c去编译这个main.c文件会报错,因为找不到show()和add() 以及sub().
故使用gcc main.c -o main -lmath -L.
-l:是指定静态库
-L:是指定静态库的路径
或者我们增加一个环境变量 export LIBRARY_PATH=.
再或者我们将自己的静态库移动到系统指定的静态库路径下。
- 注意:
- 编译出来的main 我们如果使用nm main 查看,会发现add 和sub show 是定义再此文件中的!为什么?因为静态链接是将静态库中的定义函数代码copy到本文件中。
- 如需要改动静态库的内容,我们必须需要重新链接静态库形成可执行文件。
以上是静态库的制作过程。
2、动态(共享)库
命令:ldd 查看一个可执行程序链接的动态库
例如查看自己编写的hello可执行程序,我们可以看到 libc.so.6 啥意思:lib(链接)c(C语言)so(show object共享对象)。
动态库和静态库最大的不同就是,链接动态库并不需要将库中被调用的代码复制到调用模块中,相反被嵌入到调用模块中的仅仅是被调用代码在动态库中的相对地址。
如果动态库中的代码同时为多个进程所用,动态库的实例在整个内存空间中仅需一份,因此动态库也叫共享库或共享对象(Shared Object, so)。
使用动态库的模块所占空间较小,即使修改了库中的代码,只要接口保持不变,无需重新链接。
使用动态库的代码在运行时需要依赖库,执行效率略低。
动态库的形式:libxxx.so
构建动态库:
gcc -c -fpic xxx.c -> xxx.o
-fpic:生成位置无关码!库内部的函数调用也用相对地址表示。
命令:创建动态库:gcc -shared -o libXXX.so X.o XX.o
使用动态库:
gcc … -lxxx -L<库路径>
export LIBRARY_PATH=<库路径>
gcc … -lxxx
- 注意:运行时所调用的动态库必须位于LD_LIBRARY_PATH环境变量所表示的路径中。
- export LD_LIBRARY_PATH=.
可以用以上的静态库例子去验证动态库。
gcc缺省链接共享库,可通过-static选项强制链接静态库。
3、动态加载动态库
#include <dlfcn.h> \ 系统提供的针对动态 dynamic lib
-ldl / 库的动态加载函数集
1、void* dlopen(const char* filename, int flag);
成功返回动态库的句柄,失败返回NULL。
FILE* fp = fopen(...);
fread(fp...);
fwrite(fp...);
filename - 动态库路径,若只给文件名,则根据LD_LIBRARY_PATH环境变量搜索动态库
flag - 加载方式,可取以下值:
RTLD_LAZY - 延迟加载,使用动态中的符号时才加载
RTLD_NOW - 立即加载
该函数所返回的动态库句柄唯一地标识了系统内核所维护的动态库对象,将作为后续函数调用的参数。
2、void* dlsym(void* handle, const char* symbol);
成功返回函数地址,失败返回NULL。
handle - 动态库句柄
symbol - 符号(函数或全局变量)名
该函数所返回的函数指针是void*类型,需要强制类型转换为实际的函数指针类型才能调用。
3、int dlclose(void* handle);
成功返回0,失败返回非零。
handle - 动态库句柄
4、char* dlerror(void);
之前若有错误发生则返回错误信息字符串,否则返回NULL。
load.c
#include <stdio.h>
#include <dlfcn.h>
int main(void) {
// 动态加载动态库libmath.so
void* handle = dlopen("shared/libmath.so",
RTLD_NOW);//此地方的动态库是我们自己做的。内容功能是静态库
if (! handle) {
fprintf(stderr, "dlopen: %s\n",
dlerror());
return -1;
}
// 从动态库中获取add函数的入口地址
int (*add)(int, int) =
(int (*)(int, int))dlsym(handle, "add");
if (! add) {
fprintf(stderr, "dlsym: %s\n",
dlerror());
return -1;
}
// 从动态库中获取sub函数的入口地址
int (*sub)(int, int) =
(int (*)(int, int))dlsym(handle, "sub");
if (! sub) {
fprintf(stderr, "dlsym: %s\n",
dlerror());
return -1;
}
// 从动态库中获取show函数的入口地址
void (*show)(int, char, int, int) =
(void (*)(int, char, int, int))dlsym(
handle, "show");
if (! show) {
fprintf(stderr, "dlsym: %s\n",
dlerror());
return -1;
}
int a = 30, b = 20;
show(a, '+', b, add(a, b));
show(a, '-', b, sub(a, b));
if (dlclose(handle)) {
fprintf(stderr, "dlclose: %s\n",
dlerror());
return -1;
}
return 0;
}
然后使用命令gcc load.c -ldl -o load
4、辅助工具
1.查看符号表:nm
- 列出目标文件(.o)、可执行文件、静态库文件(.a)或动态库文件(.so)中的符号
代码:nm.c
#include <stdio.h>
int a = 0; // 全局变量
static int b = 1; // 静态全局变量
void foo(void) {
int c = 2; // 局部变量
static int d = 3; // 静态局部变量
printf("Hello, World!\n");
}
编译成.o文件使用nm查看。可以看到 a b d 这三个变量,那为什么c 看不到,因为c是局部变量,在函数运行的时候才会在栈里开辟空间。静态局部变量,全局变量,静态全局在可执行程序中一直从在,此三个变量初始化或不初始化在 内存中的不同位置。
data段:存放初始化为非0的全局变量和被初始化为非0的static局部变量。例如:b d
bss段:存放未初始化的全局变量;未被初始化的static修饰的局部变量。以及初始化为0的全局变量。例如:a
2.objdump -S:
显示二进制模块的反汇编信息:objdump -S
如:objdump -S nm.o
3.strip:
删除目标文件(.o)、可执行文件、静态库文件(.a)或动态库文件(.so)中的符号表和调试信息:
4.ldd:
查看可执行程序文件或动态库文件所依赖的动态库文件:
插播:错误号和错误信息
1.通过函数的返回值表达错误
(1).返回整数的函数:通过返回合法值域以外的值表示错误
int age(char const* name) {
...
return 1000;//没有人是1000岁
}
(2).返回指针的函数:通过返回NULL指针表示错误
(3).不需要通过返回值输出信息的函数:返回0表示成功,返回-1表示失败。
int delete(char const* filename) {
...
return 0;
...
return -1;
}
2.通过错误号和错误信息表示产生错误的具体原因
#include <errno.h>
全局变量:errno,整数,标识最近一次系统调用的错误
#include <string.h>
char* strerror(int errnum); // 根据错误号返回错误信息
#include <stdio.h>
void perror(const char* s); // 打印最近错误的错误信息
printf函数的%m标记被替换为最近错误的错误信息
代码:errno.c
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main(void) {
FILE* fp = fopen("none", "r");//此文件不存在
if (!fp) {
printf("fopen: %d\n", errno);
printf("fopen: %s\n", strerror(errno));
perror("fopen");
printf("fopen: %m\n");
return -1;
}
// ...
fclose(fp);
return 0;
}
虽然所有的错误号都不是0,但是因为在函数执行成功的情况下错误号全局变量errno不会被清0,因此不能用errno是否为0作为函数成功失败的判断条件,是否出错还是应该根据函数的返回值来决定。
返回值 = 函数调用(…);
if (返回值表示函数调用失败) {
根据errno判断发生了什么错误
针对不同的错误提供不同的处理
}
代码:iferr.c
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main(void) {
FILE* fp = fopen("none", "r");//文件不存在
if (!fp) {
perror("fopen");
return -1;
}
int* pi = malloc(sizeof(int));
// if (errno) {
if (!pi) {
perror("malloc");
return -1;
}
// ...
free(pi);
fclose(fp);
return 0;
六、环境变量
每个进程都有一张独立的环境变量表,其中的每个条目都是一个形如“键=值”形式的环境变量。
env
全局变量:environ,需要自己在代码做外部声明。
environ->| * |->AAA=aaa\0
| * |->BBB=bbb\0
| * |->CCC=ccc\0
|NULL|
所谓环境变量表就是一个以NULL指针结束的字符指针数组,其中的每个元素都是一个字符指针,指向一个以空字符结尾的字符串,该字符串就是形如”键=值”形式的环境变量。
argv-> * * * NULL
| | |
a.out -c b.c
根据环境变量名获取其值
char* getenv(char const* name);
成功返回变量名匹配的变量值,失败返回NULL。
name - 环境变量名,即等号左边的部分
添加或修改环境变量
int putenv(char* string);
成功返回0,失败返回-1。
string - 形如“键=值”形式的环境变量字符串
若其键已存在,则修改其中,若其键不存在,则添加新变量
添加或修改环境变量
int setenv(const char* name, const char* value,
int overwrite);
成功返回0,失败返回-1。
name - 环境变量名,即等号左边的部分
value - 环境变量值,即等号右边的部分
overwrite - 当name参数所表示的环境变量名已存在,此参数取0则保持该变量的原值不变,若此参数取非0,则将该变量的值修改为value。
删除环境变量
int unsetenv(const char* name);
成功返回0,失败返回-1。
name - 环境变量名,即等号左边的部分
清空环境变量
int clearenv(void);
成功返回0,失败返回-1。
代码:env.c
#include <stdio.h>
#include <stdlib.h>
void penv(char** env) {
printf("---- 环境变量 ----\n");
while (env && *env)
printf("%s\n", *env++);
printf("------------------\n");
}
int main(int argc, char* argv[], char* envp[]) {
extern char** environ;
printf("%p %p\n", environ, envp);
penv(environ/*envp*/);
printf("HOME=%s\n", getenv("HOME"));
/*
if (putenv("NAME=minwei") == -1) {
perror("putenv");
return -1;
}
*/
if (setenv("NAME", "minwei", 0) == -1) {
perror("setenv");
return -1;
}
penv(environ);
/*
if (putenv("NAME=YG") == -1) {
perror("putenv");
return -1;
}
*/
// if (setenv("NAME", "YG", 0) == -1) {
if (setenv("NAME", "YG", 1) == -1) {
perror("setenv");
return -1;
}
penv(environ);
if (unsetenv("NAME") == -1) {
perror("unsetenv");
return -1;
}
penv(environ);
if (clearenv() == -1) {
perror("clearenv");
return -1;
}
printf("%p\n", environ);
penv(environ);
return 0;
}