缓存问题与程序员影响
假设库文件包含a.h、a1.cpp、a2.cpp(示例3.1)
//a.h
void f();
void g();
//a1.cpp
#include<iostream>
#include"a.h"
using namespace std;
void f(){
cout << "f()" << endl;
}
//a2.cpp
#include<iostream>
#include"a.h"
using namespace std;
void g(){
cout << "g()" << endl;
}
#include"a.h"
#include<iostream>
using namespace std;
int main(){
f();
g();
return 0;
}
创建库
$g++ -c a1.cpp a2.cpp
#生成两个文件
$ar -rc libtest.a a1.o a2.o #打包 遵循规范libtest.a
使用库
$g++ -o statictest statictest.cpp –L. -ltest # 符合规范使用库,自动补全libtest.a文件
动态库的编写
$g++ -fpic –shared –o libtest.so a1.cpp a2.cpp # 生成libtest.so
fpic f:编译选项,p:位置,i:无关 code 位置无关代码
打开动态链接库,动态加载程序
#include<dlfcn.h>
void *dlopen(const char *file, int mode);
- file:动态链接库的文件名,包括路径信息
- mode:动态链接库的使用方式,例如RTLD_LAZY:动态地加入动态链接库中的函数
- 返回值:引用动态链接库的句柄;出错返回NULL
映射动态链接库中的函数
#include<dlfcn.h>
void *dlsym(void *handle, const char *FuncName);
- handle:dlopen的返回值
- FuncName:动态链接库中的函数名
- 返回值:FuncName函数被加载后,在进程地址空间中的地址;出错返回NULL
查看出错原因
#include<dlfcn.h>
char *dlerror();
返回值:当dlopen、dlsym等函数出错时,dlerror返回字符串说明这些函数出错的原因
卸载动态链接库
#include<dlfcn.h>
int dlclose(void *handle);
参数
handle:dlopen的返回值
动态库使用者的编译
$g++ -o test test.cpp –ldl
#test 出错?
//test.cpp
#include"a.h"
#include<dlfcn.h>
#include<iostream>
using namespace std;
int main()
{
void *handle = dlopen("./libtest.so", RTLD_LAZY);// 以类的方式打开当前目录下的libtest.so
if(0 == handle)
{
cout << "dlopen error" << endl;
return 0;
}
typedef void (*Fun)();// f和g的函数原型
Fun f1 = (Fun)dlsym(handle, "f");//f1函数原型
if(0 == f1)
{
cout << "f1 error" << endl;
char *str = dlerror();
cout << str << endl;
return 0;
}
(*f1)(); //用函数指针调用函数
dlclose(handle);
return 0;
}
$./test
f1 error
./libtest.so:undefined symbol: f #没有找到f函数
$nm libtest.so #查看文件符号
#这返回结果中没有看到f函数
#看可以到_Z1fv和_Z1gv 发现名字有变化
动态库导出函数的变形
查看动态库导出的函数 #nm libtest.so
f函数实际上在动态库中的名字是:_Z1fv
将成了Fun f1 = (Fun)dlsym(handle, "f"); f改成f1就可以使用了
- 导出函数的名称
- 函数调用约定
- 结构体对齐
- 谁分配谁释放
在动态库的实现文件中函数的名称,与动态库导出的函数名称可能不同,编译器会自己绑定名字
使用extern “C”使的导出的函数名称和实际名称一致
//(示例3.3)a.h
extern "C" void f();
extern "C" void g();
extern “C”:告诉编译器按C语言的方式设定函数的导出名
不同的编译器、不同的语言,对函数名的修改都有可能不同
void __cdecl f(int a, int b); //VC环境
void f(int a, int b) __attribute__((cdecl)) //g++环境
-
f被表示成_f(不同版本编译器不一定一样)
-
从右至左,将参数压入堆栈(通过栈来传递参数)
-
函数调用者负责压入参数和堆栈平衡
void __stdcall f(int a, int b); //VC环境
-
f被表示成_f@8;8表示参数的字节数,两个参数是8字节
-
从右至左,将参数压入堆栈
-
函数内负责堆栈平衡
void __fastcall f(int a, int b); //VC环境
-
由寄存器传送参数,用ecx和edx传送参数列表中前两个双字或更小的参数,剩下的参数仍然从右至左压入堆栈
-
函数内负责堆栈平衡(64位寄存器前7个用寄存器传
-
C++类成员函数的调用约定:thiscall (可以通过压栈来传,可以通过寄存器传,64位用RDI寄存器存放this指针)
-
this指针存放于ecx寄存器中
-
参数从右至左压入堆栈
struct A{
int i;
};//4字节
struct A{
char j;
int i;
};//一字节对齐5字节,二字节对齐6字节,四字节对齐8B
struct B{
char j;
short k;
int i;
}//一字节对齐7字节,二字节对齐8字节,四字节对齐8B,八字节对齐8B
struct B{
short k;
int i;
char j;
}//一字节对齐7字节,二字节对齐8字节,四字节对齐12B,八字节对齐12B
对齐min{对齐字节数,成员最大字节长度}
#pragma(4)
struct A{
short k;
int i;
char j;
virtual void f();//指针大小为4,虚函数存在虚函数表中
}
库内部有自己的堆,库的使用者使用堆的时候,必须由库提供的函数释放。
设计目的
- 减少read、write的次数
- 确定缓冲区大小
fopen等函数 (TC2.0的stdio.h)里面 FILE结构体内fd是char类型
标准I/O库提供缓冲的目的:尽可能减少使用read、write调用的次数,以提高I/O效率。
通过标准I/O库进行的读写操作,数据都会被放置在标准I/O库缓冲中中转。
何时进行实际的读写操作?
全缓冲
- 在填满标准I/O缓冲区后,才进行实际I/O操作(例如调用write函数)
- 调用fflush函数也能强制进行实际I/O操作
行缓冲
-
在输入和输出遇到换行符时,标准I/O库执行I/O操作
-
因为标准I/O库用来收集每一行的缓存的长度是固定的,所以,只要填满了缓存,即使没有遇到新行符,也进行I/O操作
-
终端(例如标准输入和标准输出),使用行缓冲
不带缓冲
- 标准I/O库不对字符进行缓冲存储
- 标准出错是不带缓冲的,为了让出错信息尽快显示出来
用于设置缓冲类型
void setbuf(FILE *fp, char *buf);
fp:fopen函数的返回值
buf:用户提供的文件缓冲区,其长度为BUFSIZ
- 若buf为NULL,则为无缓冲
- 若buf不为NULL,则为全缓冲
用于设置缓冲类型
void setvbuf(FILE *fp, char *buf, int mode, size_t size);
fp:fopen函数的返回值
int main(){
char a[2];
FILE *fp;
fp = fopen("a.txt", "w");
//setbuf(fp, 0); //如果关闭缓存,数据则直接到磁盘
a[0] = 'a';
a[1] = 'b';
fwrite(a, 1, 2, fp);//a写入C库的缓存
sleep(600);
return 0;
}
//调用close 时会刷缓存,这样会写进文件中
//普通文件是全缓存的,如果杀死进程,则a数据在C库中没有写入文件中
强制标准I/O库进行系统调用,以将数据传递到内核
int fflush(FILE *fp);
fp:文件指针;若fp=NULL,则刷新所有输出流
成功返回0,出错返回EOF
对于ASCII字符集,一个字符用一个字节表示
对于国际字符集,一个字符可用多个字节表示
流的定向决定了所读、写的字符是单字节还是多字节的
改变流的定向
int fwide(FILE *fp, int mode);
- mode<0,字节定向
- mode>0,宽定向
- mode=0,返回当前流的定向
在一个特定的流上打开一个指定的文件,如若该流已经打开了,则先关闭该流
FILE *freopen(const char *pathname, const char *type, FILE *fp);
- pathname:要打开的文件名
- type:指定流的读写方式
- fp:特定的流
freopen函数主要用于重定向
即将fp重定向到pathname指定的文件中
int main(){
freopen("a.txt", "w", stdout); //将标准输出stdout重定向到a.txt文件中
printf("Hello World\n");
return 0;
}
文件里面并没有,标准输出是行缓冲,重定向之后变成了全缓冲
取一个现存的文件描述符,并使一个标准I/O流与该描舒符相结合
FILE *fdopen(int filedes, const char *type);
- filedes:文件描述符
- type:指定流的读写方式
- 返回文件指针
特性与用途
-
fdopen常用于由创建管道和网络通信通道函数返回的描述符。
-
这些特殊类型的文件,不能用fopen打开
-
因此必须先调用设备专用函数以获得一个文件描述符
-
然后再用fdopen使一个标准I/O流与该描述符相关联
-
对于fdopen函数,type参数的意义稍由区别
-
因为该描述符已被打开,所以fdopen为写而打开并不截短该文件
-
标准I/O添写方式,也不能用于创建该文件(因为如若一个描述符引用一个文件,则该文件一定已经存在)
关闭一个打开了的流
int fclose(FILE *fp);
fp:要关闭的流对应的文件指针
成功返回0,出错返回EOF
在该文件被关闭之前,刷新缓存中的输出数据。缓存中的输入数据被丢弃,如果标准I/O库已经为该流自动分配了一个缓存,则释放此缓存。
类似于lseek函数,即指定从文件的什么地方开始进行读写
通常有两种方法定位标准I/O流
long ftell(FILE *fp);//用于获取当前文件偏移量
int fseek(FILE *fp, long offset, int whence);//用于设置当前文件偏移量
int fgetpos(FILE *fp, fpos_t *pos);//获取当前文件偏移量
//pos: fgetpos函数将文件偏移量填入pos中
int fsetpos(FILE *fp, fpos_t *pos);//设置文件偏移量
//pos:存储了要设置的文件偏移量
后者是ANSI C引入的。程序要移植到非unix类操作系统,应使用后者。
对流有三种读写方式
- 每次读写一个字符
- 每次读写一行
- 每次读写任意长度的内容
int getc(FILE *fp); //通常是宏
int fgetc(FILE *fp); //函数
int getchar();//等同于getc(stdin)
成功返回欲读字符,若已处于文件尾或出错返回EOF
不管出错还是到达文件尾,都是返回EOF。调用ferror或feof
int ferror(FILE *fp);
int feof(FILE *fp);
- 当遇到文件结束符时,feof返回真,ferror返回假
- 当出错时,feof返回假,ferror返回真
在大多数实现中,为每个流在FILE对象中维持了两个标志
- 出错标志
- 文件结束标志
调用clearerr清除这两个标志
void clearerr(FILE *fp);
char* fgets(char *buf, int n, FILE *fp);
/*
- buf:存放从fp读出的数据
- n:buf的大小
- fp:文件指针
- 返回值:成功返回buf,出错或读到文件尾则为EOF
*/
char* fputs(const char *str, FILE *fp);
/*
str:以null为结尾的字符串
fp:文件指针
返回值:成功返回非负值,出错则为EOF
*/
fgets函数一直读到下一个新行符为止,但是不超过n-1个字符
buf缓存以null字符结尾🌟
若读到下一个新行符,会超过n-1个字符,则只会返回一个不完整的行,缓存总是以null字符结尾。
下一次的fgets调用会继续读该行。
int main(){
char buf[10];
FILE *fp;
fp = fopen("a.txt", "r");//一行的文件,一个换行
if (fgets(buf, 10, fp) == NULL){
printf("file end\n");
}
else{
printf("not file end\n");
}
printf("%s\n", buf);
if (fgets(buf, 10, fp) == NULL){
printf("file end\n");
}
else{
printf("not file end\n");
}
fclose(fp);
return 0;
}
int main(){
char buf[10];
FILE *fp;
fp = fopen("b.txt", "r");//b.txt两行的文件
fgets(buf, 10, fp);
printf("%s\n", buf);
fclose(fp);
return 0;
}
每次I/O操作读写某种数量的对象,而每个对象具有指定的长度
例如,可读写一个二进制数组、结构
size_t fread(void *ptr, size_t size, size_t nobj, FILE *fp);
size_t fwrite(const void *ptr, size_t size, size_t nobj, FILE *fp);
- ptr:用于读写的缓冲区
- size:每个对象的大小
- nobj:要读写的对象个数
- fp:文件指针
返回值
- 返回读/写的对象数
//读写一个二进制数组
float data[10];
fwrite(data, sizeof(float), 4, fp);
//读写一个结构
struct{
int a;
int b;
}item;
fwrite(&item, sizeof(item), 1, fp);
int printf(const char* format, ....);
int fprintf(FILE *fp, const char *format, ...);
int sprintf(char *buf, const char *format, ...);
- printf将格式化数据写到标准输出
- fprintf写至指定的流
- sprintf写入数组buf中
- 前两者返回输出字符数,后者返回写入数组的字符数
sprintf将格式化的字符送入数组buf中,并在该数组的尾端自动加入一个null字符,但该字节不包括在返回值中
sprintf函数可能会造成由buf指向的缓冲区的溢出,调用者要确保缓冲区足够大
int snprintf(char* buf, size_t n, const char* format, ……);
缓冲区长度为n,超过缓冲区尾端的任何字符都会被丢弃
int scanf(const char* format, ....);
int fscanf(FILE *fp, const char *format, ...);
int sscanf(char *buf, const char *format, ...);
创建临时文件可以用以前介绍的creat,然后立即unlink,也可以用以下方法
char *tmpnam ( char *ptr );
FILE *tmpfile ( void ); /* 返回文件指针 */
char *tempnam ( const char *directory, const char *prefix );
- 第一个函数产生一个与现在文件名不同的有效路径名字符串(每次调用均不同)
- 第二个函数创建一个临时二进制文件(wb+),在关闭该文件或程序结束时,该文件自动删除
- 第三个函数是第一个函数的变体,在产生路径名时,指定其目录和文件名前缀