UC学了有一阵子了,打算把笔记整理一下,发到网上,方便以后随时复盘,也供给大家拿去学习交流,希望不要收藏吃灰,要坚持学习,通过自己的努力,为这世界中你喜欢的方向添砖加瓦。
先说两个概念引入:
对于初学者,一般会把程序中所有功能全部实现于一个单一的源文件内部。这会导致编译时间长,不易于维护和升级,不易于协作开发,这属于单一模型。
将程序中的不同功能模块划分到不同的源文件中。缩短编译时间,易于维护和升级,易于协作开发,这属于分离模型。
1.静态库
静态库的本质就是将多个目标文件打包成一个文件。
链接静态库就是将库中被调用的代码复制到调用模块中。
使用静态库的程序通常会占用较大的空间,库中代码一旦修改,所有使用该库的程序必须重新链接。
使用静态库的程序在运行时无需依赖库,其执行效率高。
静态库的命名形式:libxxx.a
构建静态库:ar -r libxxx.a x.o y.o z.o
使用静态库:法一:gcc ... -lxxx -L<库路径>
法二:export LIBRARY_PATH=<库路径>
2.动态库
动态库和静态库最大的不同就是,链接动态库并不需要将库中被调用的代码复制到调用模块中,相反被嵌入到调用模块中的仅仅是被调用代码在动态库中的相对地址。
如果动态库中的代码同时为多个进程所用,动态库的实例在整个内存空间中仅需一份,因此动态库也叫共享库或共享对象(Shared Object, so)。
使用动态库的模块所占空间较小,即使修改了库中的代码,只要接口保持不变,无需重新链接。
使用动态库的代码在运行时需要依赖库,执行效率略低。
动态库的命名形式:libxxx.so
构建动态库:
gcc -c -fpic xxx.c -> xxx.o
一般在创建.so动态链接库的时候,都要加上-fPIC参数。 -fPIC 作用于编译阶段,告诉编译器产生与位置无关代码 (Position-Independent Code),则产生的代码中,没有绝对地址,全部使用相对地址,故而代码可以被加载器加载到内存的任意位置,都可以正确的执行。(即使不加 fPIC 也可以生成 .so 文件,但是对于源文件有要求,例如因为不加 fPIC 编译的 so 必须要在加载到用户程序的地址空间时重定向所有表目,所以在它里面不能引用其它地方的代码)
打包命令:gcc -shared -o libxxx.so x.o y.o z.o
使用动态库:法一:gcc ... -lxxx -L<库路径>
法二:export LIBRARY_PATH=<库路径>
gcc ... -lxxx
与静态库的使用有所不同的一点:运行时所调用的动态库必须位于LD_LIBRARY_PATH环境变量所表示的路径中。
gcc缺省(即不做任何说明时)链接共享库,可通过-static选项强制链接静态库。
3.动态加载动态库
#include <dlfcn.h> 这个头文件不属于标准c里的头文件,是属于系统提供的
-ldl : dl是动态加载库的意思,即链接动态加载库,说明使用的是系统提供的针对动态库的动态加载函数集
void* dlopen(const char* filename, int flag);
成功返回动态库的句柄,失败返回NULL。
句柄:相当于一个线索,让系统内核找到对应的动态库,但是他不直接指向动态库,他只是指向一个数据块,这个数据块里存储着和动态库有关的信息,这样的数据块就叫做句柄。(Handle)只需要得到这个块的地址就可以操作。
filename - 动态库路径,若只给文件名,则根据LD_LIBRARY_PATH环境变量搜索动态库
flag - 加载方式,可取以下值:
RTLD_LAZY - 延迟加载,使用动态中的符号时才加载
RTLD_NOW - 立即加载
该函数所返回的动态库句柄唯一地标识了系统内核所维护的动态库对象,将作为后续函数调用的参数。
void* dlsym(void* handle, const char* symbol);
成功返回函数地址,失败返回NULL。
handle - 动态库句柄
symbol - 符号(函数或全局变量)名
该函数所返回的函数指针是void*类型,需要强制类型转换为实际的函数指针类型才能调用。
int dlclose(void* handle);
成功返回0,失败返回非零。
handle - 动态库句柄
char* dlerror(void);
之前若有错误发生则返回错误信息字符串,否则返回NULL。
1.查看符号表:nm
列出目标文件(.o)、可执行文件、静态库文件(.a)或动态库文件(.so)中的符号
注意:没有c,是因为c是局部变量,只有等这个程序运行起来,才会在栈中分配内存空间,局部变量不在可执行文件中。
2.显示二进制模块的反汇编信息:objdump -S xx
3.删除目标文件(.o)、可执行文件、静态库文件(.a)或动态库文件(.so)中的符号表和调试信息:strip (可以用来隐藏一些信息或者减少空间)
1.通过函数的返回值表达错误
例如:返回整数的函数:通过返回合法值域以外的值表示错误
int age (char const* name){
...
return 1000;
}
例如:返回指针的函数:通过返回NULL指针表示错误
例如:不需要通过返回值输出信息的函数:返回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标记被替换为最近错误的错误信息
例如:
虽然所有的错误号都不是0,但是因为在函数执行成功的情况下错误号全局变量errno不会被清0,因此不能用errno是否为0作为函数成功失败的判断条件,是否出错还是应该根据函数的返回值来决定。
返回值 = 函数调用(...);
if (返回值表示函数调用失败) {
根据errno判断发生了什么错误
针对不同的错误提供不同的处理
}
每个进程都有一张独立的环境变量表,其中的每个条目都是一个形如“键=值”形式的环境变量。
语法 : env
所谓环境变量表就是一个以NULL指针结束的字符指针数组,其中的每个元素都是一个字符指针,指向一个以空字符结尾的字符串,该字符串就是形如”键=值”形式的环境变量。
根据环境变量名获取其值
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。