C/C++语言内存对齐

内存对齐:在计算机中,内存空间按照字节划分,理论上可以从任何起始地址访问任何类型的变量。但实际上在访问特定类型的变量的时候需要从特定的地址开始,这就需要各种类型的数据按照一定的规则在空间上排列,而不是顺序的一个接一个的存放,这就是内存对齐,也叫字节对齐。

内存对齐的作用:

  • 可移植性:因为不同平台对数据的在内存中的访问规则不同,不是所有的硬件都可以访问任意地址上的数据,某些硬件平台只能在特定的地址开始访问数据。所以需要内存对齐。
  • 性能原因:一般使用内存对齐可以提高CPU访问内存的效率。如32位的intel处理器通过总线访问内存数据,每个总线周期从偶地址开始访问32位的内存数据,内存数据以字节为单位存放。如果32为的数据没有存放在4字节整除的内存地址处,那么处理器需要两个总线周期对数据进行访问,显然效率下降很多;另外合理的利用字节对齐可以有效的节省存储空间。

默认内存对齐影响因素:与平台架构(位数)和编译器的默认设置有关。

总线周期:CPU通过总线和存储器或者IO设备进行一次数据传输需要的时间,通常为四个或者多个时钟周期组成。

内存对齐规则

  1. 整体类型的对齐规则:若设置了内存对齐为m个字节,类中的最大成员的对齐字节为n,则该数据类型的对齐字节为p=min(m,n)。(一般32位机器的默认pack为4位;64位机器的默认pack为8位,程序中可以显式设置pack的大小)
  2. 类型中成员的对齐规则:类中的第一个成员放在offset为0的位置;对于其他成员,若设置了内存对齐为m个字节,假设该数据成员的对齐字节数(即当前成员所占的字节数)为k,则该数据成员的起始位置是min(m,k)的整数倍。
  3. 整体对齐规则:最后整个类型的大小为p=min(m,n)的整数倍。
  4. 当设置对齐字节数大于类中最大成员的对齐字节数的时候,这个设置实际不产生任何效果;当设置对齐字节数为1时,类的大小就是简单的把所有成员大小相加。

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <stddef.h>
using namespace std;
//前提条件:32位机器,当前编译器默认pack为4
struct T {
int a;
short b;
int c;
double d;
};
int main() {
cout << offsetof(T, a) << endl
<< offsetof(T, b) << endl
<< offsetof(T, c) << endl
<< offsetof(T, d) << endl;
return 0;
}

根据上面的分析:

  • a为第一个成员,offset为0。
  • b为short,对齐字节数为2,所以其对齐字节为min(2,4)=2,故offset为4
  • c为int,对齐字节数为4,所以其对齐字节数为min(4,4)=4,故offset为8
  • d为double,对齐字节数为8,故对齐字节数为min(4,8)=8,故offset为12
  • 总的大小为20,是,min(8,4)的倍数.

使用pragma pack修改系统默认pack

修改系统的默认pack可以使用系统函数pragma的pack参数,但是修改之后的pack一定是2的n次幂

1
2
3
#pragma pack(16) //修改pack修改为16
#pragma pack() //恢复系统的默认pack
#pragma pack(show) //返回系统当前的pack,由警告信息显示,注意gcc不支持。只有VS支持

此外pack还有push,pop其他参数可选,但是不同的编译器对这些参数的实现有不同的含义,如果需要了解可以参考对应的资料。

Comment and share

System V IPC 机制和key_t本质类型

System V三种IPC机制

System V IPC包括三种不同的通信机制

  • 消息队列:消息队列类似于管道,但是又有很大的差别。第一,消息队列是有边界的,所以消息队列的通信机制是通过消息进行传递,而管道是通过字节流进行通信。第二,每条消息包括一个完整的整形字段,消息队列可以通过类型来选择消息。
  • 信号量:用于同步两个进程。
  • 共享内存:共享内存允许两个进程共享同一个内存段。即一块内存被映射到不同进程的虚拟内存中。

key_t本质

System V系统调用都有一个相关的get系统调用,它与文件io中的open函数类似,不同的是它的参数是一个整数,给定一个整数之后,系统会做以下操作:

  • 使用给定key创建一个新的IPC对象,并将IPC对象标识key_t返回给调用者.
  • 若该key对应的IPC对象存在,直接将标识返回给调用者,相当于使用key换取标识,而不做创建操作.

上述IPC对象的标识类型为key_t。key_t的本质是什么?我们可以通过grep命令追踪到

在文件/usr/include/sys/ipc.h中:

1
2
3
4
#ifndef __key_t_defined
typedef __key_t key_t;
# define __key_t_defined
#endif

在文件/usr/include/bits/types.h中:

1
__STD_TYPE __KEY_T_TYPE __key_t; /* Type of an IPC key. */

在文件/usr/include/bits/typesizes.h中:

1
#define __KEY_T_TYPE __S32_TYPE

在文件中:

1
#define __S32_TYPE int

故从上面的追踪,可以看出key_t的本质为int类型.

获取key_t方法

获取ket_t的方法三种

  • 随机选取一个整数值作为key值,多个进程共享一个key值。注意此时不能使用已经存在的key
  • 在创建IPC对象的get方法中用IPC_PRIVATE作为key值,这样会导致每个调用都会创建一个新的IPC对象
  • 使用ftok函数生成一个key,这个方法产生的key接近唯一。

IPC_PRIVATE

IPC_PRIVATE本质上为0的宏定义.使用方法是直接使用IPC_PRIVATE作为key生成一个IPC对象。这种做法保证每次产生的都是全新的IPC对象。
这种做法适用于在父进程fork前创建IPC对象,从而达到子进程继承IPC对象的目的。

ftok生成key

函数原型:

1
2
#include <sys/ipc.h>
key_t ftok(char* pathname, int proj);

函数功能:

此函数的功能是根据pathname和proj的值生成key,该算法只使用proj的最低的8个有效位,应用程序必须确保pathname引用一个可以应用stat()的既有文件。否则该函数会返回-1。

在linux中,key_t一般是32位的值。它通过取proj参数的最低8个有效位,包含该文件所属的文件系统的设备的设备号的最低8个有效位以及pathname所引用文件的inode号的最低16个有效位组合而成,这样保证了唯一性。所以从上面可以看出,对于同意个文件的不同路径(inode值相同),proj值相同,此时产生的key是相同的。 

Comment and share

Unix高级编程之signal

signal函数相关的细节描述详见另外两篇篇博客,这里不详细赘述:
https://langzi989.github.io/2017/09/08/C++%E5%87%BD%E6%95%B0%E6%8C%87%E9%92%88%E5%AD%A6%E4%B9%A0/
https://langzi989.github.io/2017/05/04/Wait%E5%87%BD%E6%95%B0%E8%AF%A6%E8%A7%A3/

显示信号的描述

信号的个数可以用宏NSIG获取。

显示信号的描述有三种方法:

1
2
3
4
5
6
7
8
9
#include <string.h>
//first method
char* strsignal(int sig);
//second method,
void psignal(int sig, char* msg);
//third memthod
sys_siglist[sig];

上述三种方法的区别

sys_siglist是直接存储信号描述的数组,一般情况下,推荐使用strsignal。

strsignal和psignal函数对locale敏感,会打印出当地的语言。
调用psignal会在本地的错误出输出流输出,msg:strsignalmsg;

如:

1
2
//此时错误数据流将会打印出:SIGINT:Interrupt
psignal(SIGINT, "SIGINT");

信号集

许多相关的系统调用涉及到一组不同的信号,这时候需要信号集。linux中使用sigset_t结构体来表示信号集。一般情况,信号集是使用掩码实现的,但是可能有一些是其他实现方式。
信号集结构体相关的函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <signal.h>
//初始化空信号集。注意不可使用memset或者静态变量全局变量初始化信号集为空,这样会出问题。因为不是所有的信号集都是通过掩码实现的
//0出错,1成功
int sigemptyset(sigset_t* set);
//初始化信号集包括所有的信号
//0出错,1成功
int sigfillset(sigset_t* set);
//向信号集中添加信号
//0出错,1成功
int sigaddset(sigset_t* set, int sig);
//从信号集中去除信号
//0出错,1成功
int sigdelset(sigset_t* set, int sig);
//检查某一信号是不是在当前信号集中。返回1在,0不在
int sigismember(const sigset_t* set, int sig);
//以下三个为GNU C中的非标准函数,
#define _GNU_SOURCE
//对两个信号集作交集存储于dest中
int sigandset(sigset_t* dest, sigset_t* left, sigset_t* right);
//对两个信号集做并集存储于dest中
int sigorset(sigset_t* dest, sigset_t* left, sigset_t* right);
//判断信号集是否为空
int sigisemptyset(const sigset_t* set);

信号掩码(进程中阻塞信号传递)

内核会为每个进程维护一个信号掩码(标识一组信号),当一个信号被传递到该进程的时候,若该信号在信号掩码中,进程会阻塞该信号的传递,直到将该信号从信号掩码中剔除。

向信号掩码中添加一个信号的方式有以下几种:

  • 当调用信号处理器程序的时候,可将引发该调用的信号自动添加到信号掩码中,这取决于sigaction函数在安装信号时使用的标志。
  • 使用sigaction函数建立信号处理程序时,可以指定一组额外信号,当调用该处理器程序时将阻塞。
  • 使用sigprocmask函数修改进程的信号掩码。

sigprocmask函数

1
2
#include <signal.h>
int sigprocmask(int how, const sigset_t* set, sigset_t* old);

参数:

  • how : 指定修改信号掩码的方式,有三种方式
    • SIG_BLOCK : 向指定信号中添加指定信号.
    • SIG_UNBLOCK: 将指定信号从原有的信号掩码中移除。若被移除的信号掩码不存在不报错
    • SIG_SETMASK: 直接设置(赋值),覆盖原有的值
  • set : 需要设置的新的信号掩码集
  • old: 旧的信号掩码集。可在设置信号掩码集之后回复原有的信号掩码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <unistd.h>
#include <signal.h>
int main() {
time_t start = time(NULL);
sigset_t new_set, pre;
sigemptyset(&new_set);
sigaddset(&new_set, SIGINT);
if (sigprocmask(SIG_SETMASK, &new_set, &pre) == -1) {
std::cout<< "sigprocmask set error" << std::endl;
}
while (true) {
sleep(1);
time_t end = time(NULL);
if (end - start >= 15) {
std::cout << "hahah 接触阻塞" << std::endl;
sigprocmask(SIG_SETMASK, &pre, NULL);
}
}
}

sigpending获取正在等待状态的信号

若进程接收信号被阻塞之后,我们希望获取被阻塞的信号,则可以使用sigpending函数

1
2
#include <signal.h>
int sigpending(sigset_t* set);

使用此函数的场景是:若某个进程接收到被阻塞的信号,如果希望这些信号被移出阻塞队列,此时可以通过sigpending获取被阻塞的信号,然后将此信号的处理器函数IGNORE,并将其剔除信号掩码即可。

在信号被阻塞的时候,不对信号做排队处理,即即使进程阻塞了100个SIGINT信号,此时当SIGINT从信号掩码中去除时,该进程接收的还是只是一个SIGINT信号。

sigaction函数

除了signal函数之外,sigaction系统调用是设置信号处理的另一个选择。sigaction和signal函数相比更加灵活和具有可移植性。sigaction允许在不改变信号处理器程序的情况下获取信号的默认处理方式。

Comment and share

Unix时间相关函数总结

在进行Unix编程的过程中,我们不可避免的会遇到需要时间相关的操作,如文件的创建修改时间,数据库中字段插入或更新的时间。

Unix时间相关的类型

  • time_t
  • struct timeb
  • struct timeval
  • struct timespec
  • struct tm
  • clock_t

time_t

time_t是一个有符号的整数类型,表示的含义是从1970年1月1日到某一个时间点的秒数。若为32位系统,由int类型的范围可以推算出,time_t可以表示的时间范围是1901-12-13 20:45:52到2038-01-19 03:14:07。

struct ime_b

time_b结构体是一个精确到毫秒的结构体,其有四个成员,成员列表如下:

1
2
3
4
5
6
struct timeb{
time_t time;
unsigned short millitm;
short timezone;    //时区标志
short dstflag;     //夏令时标志
};

可以通过下列函数获取当前的timeb:

1
int ftime(struct timeb* tb);

struct timeval

timeval是一个精确到微妙的结构体。其中主要包含两个成员:

1
2
3
4
struct timeval{
time_t tv_sec;
suseconds tv_usec;
};

此值通常通过gettimeofday获取

1
2
#include <sys/time.h>
int gettimeofday(struct timeval* tv, struct timezone* tz); //timezone参数已废弃,一般设为NULL,

struct timespec

timespec是一个精确到纳秒的结构体。其主要包含两个成员

1
2
3
4
struct timespec{
time_t tv_sec; //秒
long tv_nsec; //纳秒
}

此结构体一般通过下列函数获取:

1
2
#include <sys/time.h>
long clock_gettime(clockid_t which_clock, struct timespec* tp);

上述参数中which_clock用于标识那种时钟时间,可选值如下

  • CLOCK_REALTIME       : 系统当前时间,1970-1-1开始
  • CLOCK_MONOTONIC      : 系统的启动时间,不能被设置
  • CLOCK_PROCESS_CPUTIME_ID : 进程运行时间
  • CLOCK_THREAD_CPUITME_ID : 线程运行时间
  • CLOCK_REALTIME_HR : CLOCK_REALTIME的高精度版本
  • CLOCK_MONOTONIC_HR : CLOCK_MONOTONIC的高精度版本

struct tm

struct tm被称为一种分解时间,日期和时间被分解成多个独立字段。其形式如下:

1
2
3
4
5
6
7
8
9
10
11
struct tm {
int tm_sec; //秒 (0-60)
int tm_min; //分 (0-59)
int tm_hour; //时 (0-23)
int tm_mday; //日 (1-31)
int tm_mon; //月 (0-11)
int tm_year; //年 (1900-)
int tm_wday; //一周中的周几(周日为0)
int tm_yday; //一年中的第几天。(1月1号为0)
int m_isdst; //DST大于0表示为夏令时时间。
}

Unix时间相关函数

Unix时间相关的函数除了上面已经提到的函数还包括以下函数

  • time(time_t timep)
  • ctime
  • gmtime
  • localtime
  • mktime
  • asctime
  • strftime
  • strptime

time

函数原型

1
2
#include <time.h>
time_t time(time_t* timep);

time函数返回当前时间的时间戳,此时间戳为从1970年1月1日到当前时间的时间戳,此值不受时区和夏令时(DST)。此函数的返回值为当前的时间戳,函数参数为time_t指针,当前时间除了返回值之外,还将此时间放入timep中。所以使用time函数时我们将timep设置为NULL即可。

ctime

函数原型

1
2
#include <time.h>
char* ctime(const time_t* timep);

函数功能

ctime函数的功能为将time_t转化为打印字符串格式。把一个指向time_t的指针timep传入函数ctime,将返回一个长度为26字节的字符串,包含\0和换行符。ctime进行转换的时候,会考虑时区和夏令时,所以返回的时间字符串为当地时间。返回时间的格式如下:

1
Wed Jun 8 14:22:34 2011

特别注意返回的字符串是经由静态分配的,若多次调用此函数,之前获取的时间会受影响。SuSv3规定,调用ctime(),gmtime(),localtime()以及asctime()中的任意一个函数,都可能覆盖其他函数返回的结果。

静态分配的意思是这些函数返回的数据都是般存在同一个静态变量中,所以下一次的结果会覆盖上一次的数据。如果需要对之前的数据保存,此时需要将结果拷贝到自己分配的内存中。

### gmtime和localtime

函数原型

1
2
3
#include <time.h>
struct tm* gmtime(time_t* timep);
struct tm* localtime(time* timep);

函数功能

gmttime和localtime的作用是将time_t值转换为分解时间struct tm类型。他们的主要区别是gmtime转换为对应于UTC的分解时间,而localtime考虑时区和夏令时

mktime

函数原型

1
2
#include <time.h>
time_t mktime(struct tm* timeptr);

#### 函数功能
mktime的作用是将一个本地时区的分解时间转化为time_t类型。需要注意的是,mktime可能改变timeptr所指的内容。若分解时间不符合要求,mktime将其自动转换为有效时间。如秒为61,此时会将其变成1并讲分加1。

asctime

函数原型

1
2
3
#incude <time.h>
char* asctime(const struct tm* timeptr);

函数功能

asc的功能是将分解时间转化为打印时间,特别注意的是asctime转化的过程中不考虑时区和夏令时,返回的数据也是静态分配的。

strftime

函数原型

1
2
#include <time.h>
size_t strftime(char* outstr, size_t maxsize, const char* format, const struct tm* timeptr);

函数功能

此函数的功能是将分解时间转换为打印时间,并可以指定打印时间的格式为format。不同于ctime和asctime,strftime结果不包含换行符。若返回的字符串超过了maxsize大小,函数返回0指示为转换错误。定义的格式中其格式化字符串可以参考预定义的格式。参考《Linux/Unix系统编程手册.上册》第158页。

Comment and share

Linux Regex正则表达式库

标准的C/C++库不支持正则表达式。在Posix函数库中包含了正则表达式库。

## 正则表达式匹配框架

标准的正则表达式匹配框架:

  • 编译正则表达式.
  • 匹配正则表达式.
  • 释放正则表达式.

编译正则表达式

1
2
#include <regex.h>
int Regcomp(regex_t* preg, const char* regex, int cflags);

参数说明

  • preg : 用来保存编译之后的结果
  • regex : 正则表达式字符串,表示被编译的正则表达式。
  • cflags : 编译控制参数
    • REG_EXTENDED : 使用扩展正则表达式模式
    • REG_ICASE : 对规则中字符串不区分大小写
    • REG_NOSUB : 只检查是否有符合规则的子串。

返回值

编译成功返回0,否则返回非0

匹配正则表达式

1
2
3
4
5
6
7
8
9
10
typedef struct {
regoff_t rm_so;
regoff_t rm_eo;
} regmatch_t;
int regexec(const regex_t* preg,
const char* string,
size_t nmatch,
regmatch_t pmatch[],
int eflags);

参数说明

  • preg : 上述编译之后的正则表达式regex_t指针。
  • string : 被匹配的字符串。
  • match : 被匹配的个数。告诉函数regexec最多可以把多少个匹配结果写入pmatch,一般为pmatch数组的长度。
  • pmatch : 匹配结果数组。
    • rm_so : 满足子串在string中的起始偏移量
    • rm_eo : 满足子串在string中的结束偏移量
  • eflags : 匹配的特性
    • REG_NOTBOL : 是否为第一行
    • REG_NOTEOL : 是否是最后一行

返回值

0表示匹配成功,1表示REG_NOMATCH。

报错信息

1
size_t regerror(int errcode, const regex_t* preg, char* buf, size_t buffer_size);

参数说明

  • errcode : 来自regcomp和regexec的错误码。
  • preg : 编译后的正则表达式
  • buf : 缓冲区错误信息字符串
  • buffer_size : 缓冲区最大长度。

释放正则表达式

1
2
//释放reget_t指针,无返回值。
void regfree(reget_t* preg);

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <regex.h>
#include <stdlib.h>
int main() {
regex_t preg;
int iErr = 0;
regmatch_t subs[256];
char acReg[] = "[0-9a-zA-Z]+$";
iErr = regcomp(&preg, acReg, REG_EXTENDED);
if (iErr) {
printf("compile reg error\n");
exit(1);
}
iErr = regexec(&preg, "12345", 256, subs, 0);
if (REG_NOMATCH == iErr) {
printf("no match\n");
} else {
printf("match\n");
}
}

参考资料:

Comment and share

Unix标准IO库相关函数总结之读写流(二)

读写流的三种方式

一旦一个流被打开,则可以选择三种方式对其进行读写。

  • 每次一个字符的IO,一次读或者写一个字符,如果流是带缓冲的,则标准IO函数处理所有的缓冲。
  • 每次一行的IO。每次读写一行数据,可以使用fgets和fputs函数,但是要说明最大行的长度。
  • 直接IO。通常使用fread和fwrite函数。

一个字符的IO

输入函数

通常使用以下三个函数进行一个字符的读。

1
2
3
4
#include <stdio.h>
int getc(FILE*);
int fgetc(FILE*);
int getchar();

区别与联系

这三个函数都用于一个字符的读取。其区别和联系如下:

  • getchar()相当于getc(stdin)。即每次从标准输入流读入一个字符。实质上getc是宏
  • getc和fgetc的区别是getc可以被实现为宏,而fgetc不能被实现为宏。这就意味着:
    • getc的参数不能是具有副作用的表达式,因为它的值可能被计算多次。
    • fgetc是一个函数,可以获得其地址, 这就允许将fgetc作为参数传递给另一个函数。
    • 调用fgetc时间比getc时间长,因为调用宏的时间更短。

有副作用的表达式是指:表达式的作用本质是用于计算的,原则上只返回一个计算结果,而不会改变表达式中的变量的值。这种不改变表达式中变量值的表达式叫做无副作用的表达式。如:x+y,y-z等。除此之外,若表达式中变量的值被改变则成为有副作用的表达式,如x++,y+=2;

由于在宏中宏可能出现在程序的很多位置,也就是表达式会被计算多次,这个时候若表达式有副作用就会GG。

返回值

这三个函数的返回值都是int类型,这三个函数在返回下一个字符的时候,将其unsigned char类型转换为int。说明无符号的理由是,如果最高位为1,也不会使返回值为负。返回整形的理由是这样既可以返回所有的字符,也可以返回出错或到达文件为的指示值

注意不管是到达文件为还是出错,这个时候三个函数的返回值都一样。为了区分这两种情况,常调用ferror或feof函数。

1
2
3
4
5
6
7
#include <stdio.h>
//用于判断流fp是否遇到读取错误,若读取错误,返回非零值,否则返回0。
int ferror(FILE* fp);
//用于判断流是否遇到文件结尾,若到达文件结尾,返回非零值,否则返回0.
int feof(FILE* fp);
void clearerr(FILE* fp);

在大多数的实现中,为每个流在FILE对象中维护了两个标志:

  • 出错标志
  • 文件结束标志

调用clearerr可以清除这两个标志。

压送字符到流中。

从流中读取字符以后,可以使用ungetc将字符押送回流中,压回的字符又可以从流中读出,读出的顺序与压送的顺序相反。压送的字符不会被写到流中。

1
2
#include <stdio.h>
int ungetc(int c, FILE* fp);

###输出函数
输出函数为以下三个,与输入对应,区别与联系和输入函数相同。

1
2
3
4
#include <stdio.h>
int putc(int c, FILE* fp);
int fputc(int c, FILE* fp);
int putchar(int c);

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE* test = fopen("test.txt", "rw");
char a;
while ((a =getc(test)) != EOF) {
putc(a, stdout);
putc('\n', stdout);
}
ungetc('1', test);
ungetc('2', test);
putc(getc(test), stdout);
putc(getc(test), stdout);
if (ferror(test)) {
printf("this is the read error\n");
}
if (feof(test)) {
printf("this is the eof\n");
}
fclose(test);
}

一行的IO

输入

标准IO中提供了一下两个函数进行一行的读取:

1
2
3
#include <stdio.h>
char* fgets(char* restrict buf, int n, FILE* fp);
char* gets(char* buf);

返回值

若读取成功返回buf,若读取失败或者读取到文件结尾返回NULL.

区别与联系

  • 对于fgets必须指定缓冲区的长度n.此函数一直读到下一个换行符为止,但是不能超过n-1个字符,读入的字符将被送入缓冲区,该缓冲区以null结尾。若改行包含换行符超过了n-1个字符,fgets只返回一个不完整的行。下一次读取的时候将继续从该行继续往下读。
  • gets函数用于从标准输入读取,但是gets不包含缓冲区的长度,所以在读取的时候可能会出现缓冲区溢出的情况。一般最好不要用gets函数.
  • gets函数不将换行符存入缓冲区,而fgets将换行符存入缓冲区

虽然ISO要求提供gets函数,但一般使用fgets不要使用gets函数。

输出

对应的,标准IO提供了以下连个函数进行输出:

1
2
3
#include <stdio.h>
int fputs(const char* restrict buf, FILE* restrict fp);
int puts(const char* buf);

返回值

成功返回非负值,失败返回EOF。

区别

  • fputs将一个以null结尾的字符串写到指定的流,但是null不写出,注意这不是每次输出一行,而取决于缓冲区中的内容。
  • puts将一个以null结尾的字符串写到指定的流,null不写出,但是在最后又添加了一个换行符
  • 一般情况下使用fgets和fputs,不适用gets和puts函数。

实例

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main() {
FILE* fp = fopen("test.txt", "rw");
char buf[5];
while (fgets(buf, 5, fp)) {
printf("%s", buf);
}
return 0;
}

直接IO

直接IO通常用于对二进制文件的读写,除此之外也可以对文本文件进行读写。其中对二进制文件的读写只能用直接IO的方式,因为二进制文件中可能含有null字符,会导致使用行或者字符获取时出现错误。直接IO进行读写的两个函数如下:

1
2
3
4
#include <stdio.h>
size_t fread(void* restrict ptr, size_t size, size_t nobj, FILE* fp);
size_t fwrite(const void* restrict ptr, size_t size, size_t nobj, FILE* fp);

参数说明

  • ptr:为缓冲区指针
  • size:为一个结构体(类型)的大小
  • nobj:结构体的个数,若在fread中说明将要写入nobj个大小为size的结构体(类型),若为fwrite,则为要从流fp中读取nobj个大小为size的数据
  • fp:文件指针

返回值

返回值为实际读取或写入的对象的个数。对于fread,若文件出错或者读到文件结尾处都可以少于nobj,对于fwrite,若返回值小于nobj,则写入出错。

注意

使用这两个函数存在一个问题就是,他们只能用于读写在同一个系统上已写的数据。若是通过网络挂载的文件则不可行。
fread,fwrie可移植,而read,write不可移植。

格式化输出

格式化输出有以下几个函数:

1
2
3
4
5
6
7
#include <stdio.h>
int printf(const char* restrict format, ...);
int fprintf(FILE* restrict fp, const char* restrict format, ...);
int dprintf(int fd, const char* restrict format, ...);
int sprintf(char* restrict buf, const char* restrict format, ...);
int snprintf(char* restrict buf, size_t n, const char* restrict format, ...);

返回值

前三个函数若输出成功,则返回输出字符的个数;若输出出错,则返回负值。

sprintf和snprintf若执行成功,则返回存入buf中的字符串的长度,否则返回负值。

函数说明

  • printf:向标准输出输出字符串。
  • fprintf:向标准文件流输fp出字符串。
  • dprintf:向文件描述符所指向的文件输出字符串。
  • sprintf:向缓冲区buf写入字符串。
  • snprintf:安全的向缓冲区buf写入字符串并指定缓冲区的最大长度.一般用此函数代替sprintf

格式化字符串format的格式

1
2
//其中[]表示可选部分,converter不可选
%[flags][fldwidth][precision][lenmodifier]converter
  • flags : 是该输出的标志,其包含如下几个值:
    • ‘ : 将整数按千位分组字符。
      • : 在字段内左对齐输出。
      • : 总是显示正负号
    • (空格) : 如果第一个字符不是正负号,则在其前面加上一个空格
    • : 指定另一种转换形式,如对于16进制,在前面加0x

    • 0 : 添加前导0进行填充
  • fldwidth : 说明最小字段宽度,若参数字段小于此宽度,多余位置用空格填充。
  • precision : 浮点数精度。前导为.

注意:fldwidth可以用*作为占位符,然后在后面对其进行指定。

1
2
3
4
5
6
7
8
#include <stdio.h>
int main () {
// 10.10
printf("%10.2lf", 10.1);
//与上面写法等效。
printf("%*.*lf", 10, 2, 10.1);
}

Comment and share

Unix标准IO库相关函数总结之打开关闭流(一)

Unix标准IO类型FILE

在Unix相关的文件IO中几乎所有的函数都用到了文件描述符,文件描述符是打开一个文件时返回的一个可用的最小的文件描述标识。相应的在Unix标准IO相关的函数中,几乎每个函数都用到了FILE数据类型。本小结简单介绍一下FILE结构体的内容。

FILE实际上是一个struct的typedef,可以在/usr/include/stdio.h中找到它的定义为:

1
typedef _IO_FILE FILE;

_IO_FILE_的定义在文件/usr/include/libio.h中,我们可以看到它的具体定义为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

从上面的struct中可以观察到FILE中是有文件描述符标志的,即为fileno参数。

打开标准IO流

打开标准文件IO流的方法有如下三个:

1
2
3
4
5
FILE* fopen(const char* restrict pathname, const char* restrict type);
FILE* freopen(const char* restrict pathname, const char* restrict type, FILE* restrict fp);
FILE* fdopen(int fd, const char* type);

上述三个函数区别以及使用场景说明:

  • fopen的作用是打开一个指定文件路径的文件流。
  • freopen的作用是在一个指定的留上打开一个指定文件。如果当前流已经打开,则先关闭该流;若该流被重定向,则清除重定向。该函数常用于将一个指定的文件打开为一个默认的流,如若想使用printf函数将打印出来的内容输出到一个指定文件中,此时可以讲stdout重定向到指定的文件路径并指定打开模式。
  • fdopen的作用是将一个已经打开的文件描述符(该文件描述符可能是open,dup,socket等获取的)绑定到一个标准IO流上。此函数常用于由管道和网络通信通道函数返回的文件描述符,因为这些无法显式的指定文件。

参数说明:

  • pathname: 文件路径,相对或绝对
  • type: 打开模式(r,w,a,r+,w+,a+以及所有后面加b)

打开模式说明:

  • r : 读模式
  • w : 写模式,在写之前将原有文件内容全部清楚
  • a : 追加模式,offset为文件结尾
  • r+ : 读写模式(不删除文件原有内容,offset初始为文件开头)。如原有文件内容为”123456”,若以此模式打开文件并写入”abc”,此时写入之后的结果是”abc456”。
  • w+ : 读写模式(删除文件原有内容,offset为文件开头).如原有文件内容为”123456”,若以此模式打开文件并写入”abc”,此时写入之后的结果是”abc”。
  • a+ : 读写模式(offset为文件结尾).如原有文件内容为”123456”,若以此模式打开文件并写入”abc”,此时写入之后的结果是”123456abc”。

上述所有的模式后面都加上b表示对二进制文件的操作(rb,wb,ab,rb+,wb+,ab+)。

注意:对于fdopen函数由于文件已经由文件描述符打开,此时w模式时将不清除文件内容,追加模式不常见文件。

当以读写模式打开文件时候,将有一些限制。如果中间没有fflush,fseek,fsetpos以及rewind函数,标注输出之后不能直接进行输入;如果没有fseek,fsetpos或者rewind,或者一个输入没有到达文件尾,则输入操作之后不能跟输出操作。

关闭标准文件流

一般情况下在关闭文件流之前通常先使用fflush刷新缓冲区防止数据丢失,关闭标准文件流使用的函数为:

1
2
#include <stdio.h>
int fclose(FILE* fp);

Comment and share

Unix标准IO文件流及缓冲类型

Unix标准IO文件流

文件IO相关函数的一节中,我们所有的I/O函数都是围绕着文件描述符来操作的,当打开一个文件的时候,即返回一个文件描述符,然后该文件描述符用于后续的文件操作。而对于标准IO库,对于文件的操作都是围绕这 文件流 file stream进行的。当我们使用标准IO库打开或创建一个文件的时候,我们已经使一个流和一个文件进行关联。

文件流

由于历史原因,C语言中原来表示流的数据结构是FILE,而不是叫做流。由于大多数的库函数使用到了FILE类型,有的时候在使用FILE指针的时候也叫其为流,这导致后来很多数据把FILE和流搞得十分混乱。实际上流就是标准IO库中程序与文件交互的一种方式。

标准IO函数fopen打开一个文件时返回一个指向FILE对象的指针,该对象通常是一个结构,它包含了标准IO库为管理该流所需要的所有信息,包括该文件的文件描述符,用于指向该流缓冲区的指针,缓冲区的长度,当前缓冲区中的字符数以及出错标志等等

标准输入,标准输出以及标准错误

标准库中对于每一个进程都预定义了三个流,分别是stdin,stdout以及stderr,他们分别对应与Linux文件IO中的STDIN_FILENO,STDOUT_FILENO和STDERR_FILENO。它们的定义在stdio.h中

I/O文件流的缓冲类型

标准IO提供缓冲的目的是为了通过减少使用read和write调用的次数来提高IO读写的效率,它对每个IO流自动的进行缓冲处理,从而避免了用户程序在使用read和write需要考虑的这一点。

标准IO流提供了三种缓冲。分别是全缓冲(fully buffering),行缓冲(line Buffering)以及无缓冲(nonBuffering)。

全缓冲

在使用全缓冲的情况下,当数据填满整个缓冲区之后才进行实际的IO操作。对于驻留在磁盘上的文件的读写通常是使用全缓冲。通常如果不给文件流指定缓冲区的情况下,标准IO函数会首先调用malloc函数获取所需要的缓冲区。

行缓冲

在使用行缓冲的情况下,每当输入输出遇到换行或者缓冲区满了的情况下才会进行实际的IO操作,当涉及到终端输入输出的时候通常使用行缓冲。

对于行缓冲有两个限制。1.由于接收行缓冲的缓冲区的长度是固定的,所以只要填满了缓冲区,即使还没有遇到换行符,也会进行IO操作。2.任何时候,只要通过IO库要求从一个不带缓冲的流或者一个行缓冲的流得到输入数据,那么就会冲洗所有缓冲输出流。

###不带缓冲
此时标准IO库不对字符进行缓冲存储。这就使得输入流要求IO立即进行,如标准错误流,若果出现错误,会立马输出。

flush一个流即刷新缓冲区有两个含义。

  • 在IO库方面,flush意味着将缓冲区中的内容写到磁盘上,该缓冲区可能还没有满
  • 在终端驱动方面表示丢弃已经存储在缓冲区中的内容。

##标准文件流与缓冲类型之间的关系

  • 当标准输入输出指向的是交互式设备(如终端)的时候,它们是行缓冲的,若不是则是全缓冲的。
  • 标准错误永远是无缓冲的。

与缓冲相关的函数

我们可以通过一下两个函数对将缓冲关闭或者改变缓冲的类型。其中这些函数应该在流被打开之后调用,而且也应该在对流进行一切操作之前调用。

1
2
3
#include <stdio.h>
void setbuf(FILE* restrict fd, char* restrict buf);
int setvbuf(FILE* restrict fd, char* restrict buf, int mode, size_t size);

使用setbuf函数打开或者关闭缓冲,当buf是一个有效缓冲区时,此时缓冲打开,若流指向的是终端设备,则此时该流是行缓冲的,否则该流是全缓冲的;当buf为NULL的时候,表示关闭该缓冲。

使用setvbuf可以精确的说明缓冲的类型,这里是使用mode来说明的,mode的值包括以下几个:

  • _IOFBF 全缓冲
  • _IOLBUF 行缓冲
  • _IONBUF 无缓冲

如果指定一个不带缓冲的流,则忽略buf和size参数。如果指定缓冲,则buf和size分别指定一个缓冲区域和缓冲区域的长度。若此时buf为NULL,则标准IO库将自动制定一个适合长度的缓冲区。

上述函数与缓冲之间的关系

函数 mode buf 缓冲区及长度 缓冲类型
setbuf 非空 长度为size的缓冲区buf 全缓冲或行缓冲
setbuf NULL 无缓冲区 不带缓冲
setvbuf _IOFBF 非空 长度为size的缓冲区buf 全缓冲
setvbuf _IOFBF NULL 合适长度的缓冲区buf 全缓冲
setvbuf _IOLBF 非空 长度为size的缓冲区buf 行缓冲
setvbuf _IOLBF NULL 合适长度的缓冲区buf 行缓冲
setvbuf _IONBF 忽略 无缓冲区 不带缓冲

我们还可以通过fflush强制冲洗一个流,此函数使该流所有未写的数据都被传送到内核。作为一种特殊的情况,当流的NULL时,所有的流将被冲洗:

1
2
#include <stdio.h>
int fflush(FILE* fd);

Comment and share

Unix环境变量

概述

在Unix中,每个进程都有自己的一组环境变量,这些环境变量,要么是一组全局字符串,要么是子进程从父进程继承而来的,如果子进程不对其修改则与父进程的环境变量一模一样。
Unix内核并不查看这些字符串,它们的解释权完全取决于各个应用程序。例如shell是Unix中一个可执行程序,通常shell的启动文件中会对环境变量进行设置。所以当我们进入shell之后可以查看path等环境变量。在当前shell中启动的进程会继承其父进程shell的环境变量,也就可以查看path等环境变量,环境变量可以在登录的时候自动设置,也可以由用户自行设置。

环境变量相关变量

每个程序都会接收到一张环境表。与参数表一样,环境表也是一个字符指针数组。其中每个指针都包含一个以NULL结尾的字符串的地址。全局变量
environ指向了这个数组的地址。

代码如下:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
extern char **environ;
if (environ!= NULL) {
for (i = 0; environ[i] != NULL; i++) {
printf("env: %s\n", environ[i]);
}
}

环境变量相关的函数

与环境变量相关的函数包括以下几种:取环境变量的值,添加环境变量、修改环境变量、以及删除环境变量.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdlib.h>
//获取环境变量的值
char* getenv(const char* name); //若包含此key返回对应的值,否则返回NULL
/*
* function: 修改环境变量的值 前面两个分别为键值对,最后一个表示是否需要删除原有的定义重写。
* params:键没有存在,则创建此键值对。若键已经存在:rewrite=0时不覆盖原来的值;rewrite!=0覆盖原来的值
* return:成功返回0, 出错返回非0。
*/
int setenv(const char* name, const char* value, int rewrite);
/*
* function: 添加环境变量,若存在则删除原有的,添加新的,不存在则直接添加
* params: 参数为一个键值对字符串,如"name=test"
* return: 成功返回0,不成功返回-1
*/
int putenv(char *str);
/*
* function: 删除name的定义,即使不存在也不出错。
* params: 参数为键
* return:出错返回-1,不出错返回0
*/
int unsetenv(const char*name);
/*
* function: 清除所有的环境变量
* return: 成功返回0, 失败返回-1。
*/
int clearenv();

putenv和setenv的区别

putenv可以使用程序中已经定义的且形如”name=value”的字符串作为参数。此时系统不再为该环境变量分配内存,环境变量将使用程序中定义变量的内存。
并将该字符串的地址保存在环境变量中。所以要使用putenv一定要用全局变量作为参数,否则程序退出栈内存被释放,再次访问环境变量将会出现未定义行为,
导致环境变量不可用。

putenv也可用字符串常量做参数,这个时候系统将为其分配内存。

但是setenv去设置环境变量系统将会先malloc出一块内存给环境变量使用,所以此时不需要担心环境不可用的情况。

环境变量在进程空间中的存储位置

环境变量和环境字符串通常放在进程存储空间的顶部,也就是栈内存之上

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
extern char **environ;
int main() {
int i;
printf("the address of the environment: %p\n", environ);
printf("the adress of first i: %p\n", &i);
}

环境变量中进行增删改操作的实现机制

删除环境变量比较容易,当增加或者修改环境变量的时候由于环境表和环境字符串通常占用的是进程地址空间的顶部,所以它不能再向
高地址(向上)扩展,同时也不能在移动在它之下的各栈帧,所以也不能向下扩展。两者的组合使得该空间的长度不能再增加。

  • 删除环境变量:删除环境变量时只需要先找到该指针,然后将所有后续指针都向环境表的首部顺序移一个位置。
  • 修改环境变量:
    • 若新的环境变量value长度小于或者等于原有的值,则直接将其复制到旧值。
    • 否则,先调用malloc在堆上分配一块内存,然后将新字符串指向该空间,接着使环境变量表中针对name的指针指向新分区。
  • 增加环境变量: 增加新环境变量比较复杂。必须首先通过调用malloc为新的name=value分配内存空间,然后将字符串复制到此空间中。
    • 如果该name是第一次增加,则必须调用malloc为新的指针表分配内存空间,然后将原来的环境表复制到新的内存,并将指向新的name=value字符串
      的指针存放在该指针表的表尾,然后将空指针放在其后面。最后使environ指向新的环境表。这样就导致原来位于栈顶之上的环境表移到了堆内存中。
      但是大多数的环境指针仍然指向栈顶之上的name=value字符串。
    • 如果不是第一次新增加一个name,可知之前已经将环境表迁移到堆内存中,所以只需要调用realloc,以分配比原空间多存放一个指针的空间。然后将指针指向name=value
      字符串的指针,最后是一个NULL指针。

Comment and share

魏传柳(2824759538@qq.com)

author.bio


Tencent


ShenZhen,China