Skip to main content

项目基础

1.回调函数与多线程

一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。如果代码立即被执行就称为同步回调,如果过后再执行,则称之为异步回调。

案例

int Callback_1(int a)   ///< 回调函数1
{
printf("Hello, this is Callback_1: a = %d ", a);
return 0;
}

int Callback_2(int b) ///< 回调函数2
{
printf("Hello, this is Callback_2: b = %d ", b);
return 0;
}

int Callback_3(int c) ///< 回调函数3
{
printf("Hello, this is Callback_3: c = %d ", c);
return 0;
}

int Handle(int x, int (*Callback)(int)) ///< 注意这里用到的函数指针定义
{
Callback(x);
}

int main()
{
Handle(4, Callback_1);
Handle(5, Callback_2);
Handle(6, Callback_3);
return 0;
}

2.select、poll、epoll

IO多路复用机制,在Linux中是通过select/poll/epoll机制来实现的。为了解决大量客户端访问的问题,引入IO复用技术:一个进程可以同时对多个客户请求进行服务,复用一个进程对多个IO进行服务。IO读写的数据多数情况下未准备好,需要通过一个函数监听这些数据状态,一旦有数据可以读写就触发服务。elect,poll,epoll都是IO多路复用的机制,监视多个描述符,一旦某个描述符就绪,通知程序进行操作。

select

select()的机制中提供一fd_set的数据结构,实际上是一long类型的数组,每一个数组元素都能与一打开的文件句柄(不管是Socket句柄,还是其他文件或命名管道或设备句柄)建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪一Socket或文件可读。 select函数

#include <sys/select.h>
#include <unistd.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数:
- nfds: 委托内核检测的最大文件描述符的值 + 1
- readfds: select监视的可读文件句柄集合
- 传入传出的参数
- 委托内核检测读缓冲区是不是可以读数据
- writefds: select监视的可写文件句柄集合
- 传入传出的参数
- 委托内核检测写缓冲区是不是还可以写数据(不满就可以写)
- exceptfds: select监视的异常文件句柄集合
- 传入传出的参数
- 委托内核检测哪些文件描述符出现了异常
- timeout:本次select的超时结束时间
struct timeval {
long tv_sec; /* 秒 */
long tv_usec; /* 毫秒 */
};
- NULL: 永久阻塞, 直到检测到了文件描述符有变化
- tv_sec = 0, tv_usec = 0, 不阻塞
- tv_sec > 0 || tv_usec > 0, 阻塞对应的时间长度
返回值:
-1: 失败
>0: 返回已准备好的文件描述符数
0:超时

用户调用select会进入内核空间,并且调用 sys_select() 内核函数,主要完成以下工作。

1.将需要监控的文件描述符集合fd_set拷贝到内核; 2.用pollwait初始化文件设备驱动程序的_qproc函数指针(pollwait函数在下图介绍) 3.遍历fd_set集合中的文件描述符,调用对应poll函数,通过返回mask判断文件描述符是否可读可写等,如条件满足,则将读fe_set集合对应文件描述符置位,并将需返回的描述符个数加1,如条件不满足,则调用poll_wait函数将调用select进程加入设备等待队列中与设置唤醒回调函数 4.如所有条件都不满足,则调用poll_schedule_timeout让当前进程进入睡眠。超时、检测的文件描述符满足条件、有信号要处理这三种情况将唤醒进程。 5.最终返回符合条件的文件描述符个数(retval)

FD_ZERO(fd_set *)将某一个集合清空,每次select前都需要将集合清空

FD_SET(int, fd_set *)将一个给定的文件描述符加入到集合之中

FD_CLR(int, fd_set *)从集合中删除指定的文件描述符。

FD_ISSET(int, fd_set *)检查集合中指定的文件描述符是否准备好(可读或可写)

使用方法:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/select.h>

int main(int argc, char **argv)
{
//创建监听套接字
int lfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(-1 == lfd) {
perror("socket");
exit(0);
}
//绑定IP,PORT
struct sockaddr_in addr;
addr.sin_port = htons(12000);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
if(-1 == bind(lfd, (struct sockaddr *)&addr, sizeof(addr))) {
perror("bind");
exit(0);
}
//监听
if(-1 == linsten(lfd, 64)) {
perror("listen");
exit(0);
}

//select
fd_set rdset;
int nready = 0, fdsize = 0;
int buff[1024] = {0};
buff[fdsize++] = lfd;
while(1) {
FD_ZERO(&rdset);
for(int i = 0; i < fdsize ; ++i) {
FD_SET(buff[i], &rdset);
}
if( (0 > fdsize) || (1024 < fdsize)) {
break;
}
//因为Linux中分配文件描述符时是从当前未被分配的最小文件描述符来分配的,
//所以在select()函数的第一个参数只需要是buff中的最后一个文件描述符+1,
//即可完成对加入到rset中所有文件描述符的监听
nready = select(buff[fdsize - 1] + 1, &rdset, NULL, NULL, NULL);
if(0 == nready) {
//超时
continue;
}
else if(-1 == nready){
//失败
Error(errno);
for(int i = 1; i < fdsize ; ++i) {
close(buff[i]);
}
break;
}
else {
//通信
for(int i = 0; i < fdsize; ++i) {
if(FD_ISSET(buff[i], &rdset)) {
if(lfd == buff[i]) {
struct sockaddr_in caddr;
int nlen = sizeof(caddr);
int cfd = accept(lfd, (struct sockaddr*)&caddr, &nlen);
if(-1 == cfd) {
perror("accept");
exit(0);
}
//新连接加入buff
if(1024 <= fdsize) {
printf("已经达到最大检测数(%d)。", fdsize);
}
else{
buff[fdsize++] = cfd;
}
}
else {
char rbuff[1024] = {0};
int ret = read(buff[i], rbuff, sizeof(rbuff));
if(-1 == ret) {
perror("read");
exit(0);
}
else if(0 == ret) {
printf("client disconnect.......");
close(buff[i]);
for(int j = i--; j < fdsize -1; ++j) {
buff[j] = buff[j + 1];
}
buff[--fdsize] = 0;
}
else {
write(buff[i], rbuff, strlen(rbuff)+1);
}
}
}
}
}
}
close(lfd);
return 0;
}

poll

由于select与poll本质上基本类似,其中select是由BSD UNIX引入,poll由SystemV引入。所以不在介绍poll的实现原理。

#include <poll.h>
struct pollfd {
int fd; /* 委托内核检测的文件描述符 */
short events; /* 委托内核检测文件描述符的什么事件 */
short revents; /* 文件描述符实际发生的事件 */
};
例子:
struct pollfd myfd;
myfd.fd = 5;
myfd.events = POLLIN | POLLOUT;


struct pollfd myfd[100];
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数:
- fds: 这是一个struct pollfd数组, 这是一个要检测的文件描述符的集合
- nfds: 这是第一个参数数组中最后一个有效元素的下标 + 1
- timeout: 阻塞时长
0: 不阻塞
-1: 阻塞, 检测的fd有变化解除阻塞
>0: 阻塞时长
返回值:
-1: 失败
>0(n): 检测的集合中有n个文件描述符发送的变化
0:超时

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <poll.h>

int main(int argc, char **argv)
{
//创建监听套接字
int lfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(-1 == lfd) {
perror("socket");
exit(0);
}
//绑定IP,PORT
struct sockaddr_in addr;
addr.sin_port = htons(12000);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
if(-1 == bind(lfd, (struct sockaddr *)&addr, sizeof(addr))) {
perror("bind");
exit(0);
}
//监听
if(-1 == linsten(lfd, 64)) {
perror("listen");
exit(0);
}

//poll
int nready =0 , fdsize = 0;
struct pollfd events[64];
memset(events, 0, sizeof(events) * 64);
events[fdsize].fd = lfd;
events[fdsize++].events = POLLIN;

while(1) {
if(0 >= fdsize){
break;
}
nready = poll(events, 64, -1);
if(0 == nready) {
//超时
continue;
}
else if(-1 == nready){
//失败
Error(errno);
for(int i = 1; i < fdsize ; ++i) {
close(events[fdsize].fd);
}
break;
}
else {
//通信
for(int i = 0; i < fdsize; ++i) {
if(events[i].revents & (POLLIN | POLLHUP | POLLERR)) {
if(events[i].fd = lfd) {
//新连接到来
struct sockaddr_in caddr;
int nlen = sizeof(caddr);
int cfd = accept(lfd, (struct sockaddr*)&caddr, &nlen);
if(-1 == cfd) {
perror("accept");
exit(0);
}
//新连接加入buff
if(64 <= fdsize) {
printf("已经达到最大检测数(%d)。", fdsize);
}
else{
events[fdsize].fd = cfd;
events[fdsize++].events = POLLIN;
}
}
else {
char rbuff[1024] = {0};
int ret = read(events[i].fd, rbuff, sizeof(rbuff));
if(-1 == ret) {
perror("read");
exit(0);
}
else if(0 == ret) {
printf("client disconnect.......");
close(events[i].fd);
for(int j = i--; j < fdsize -1; ++j) {
events[j] = events[j + 1];
}
memset(&events[--fdsize], 0, sizeof(events[--fdsize]));
}
else {
write(events[i].fd, rbuff, strlen(rbuff)+1);
}
}
}
}
}
}
close(lfd);
return 0;
}

epoll

int epoll_create(int size); // 创建一个epoll的句柄



int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/*
epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:
*/

/*
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
*/

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
/*
等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
*/

关于ET、LT两种工作模式: 可以得出这样的结论: ET模式仅当状态发生变化的时候才获得通知,这里所谓的状态的变化并不包括缓冲区中还有未处理的数据,也就是说,如果要采用ET模式,需要一直read/write直到出错为止,很多人反映为什么采用ET模式只接收了一部分数据就再也得不到通知了,大多因为这样;而LT模式是只要有数据没有处理就会一直通知下去的。

//基本流程
for( ; ; )
{
nfds = epoll_wait(epfd,events,20,500);
for(i=0;i<nfds;++i)
{
if(events[i].data.fd==listenfd) //有新的连接
{
connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept这个连接
ev.data.fd=connfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //将新的fd添加到epoll的监听队列中
}
else if( events[i].events&EPOLLIN ) //接收到数据,读socket
{
n = read(sockfd, line, MAXLINE)) < 0 //读
ev.data.ptr = md; //md为自定义类型,添加数据
ev.events=EPOLLOUT|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改标识符,等待下一个循环时发送数据,异步处理的精髓
}
else if(events[i].events&EPOLLOUT) //有数据待发送,写socket
{
struct myepoll_data* md = (myepoll_data*)events[i].data.ptr; //取数据
sockfd = md->fd;
send( sockfd, md->ptr, strlen((char*)md->ptr), 0 ); //发送数据
ev.data.fd=sockfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改标识符,等待下一个循环时接收数据
}
else
{
//其他的处理
}
}
}

select,poll,epoll区别

支持一个进程所能打开的最大连接数: select:单个进程所能打开的最大连接数由FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是3232,同理64位机器上FD_SETSIZE为3264)。 poll:poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的。 epoll:虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接。 文件描述符剧增后带来的IO效率问题 select/poll:因为每次调用时都会对监控的所有文件描述符进行线性遍历,所以随着的文件描述符的增加会造成遍历速度慢的“线性下降性能”问题。 epoll:因为epoll内核中实现是根据每个文件描述符上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。 消息传递方式 select/poll:用户和内核之间采用数据拷贝的方式传递消息 epoll:epoll通过内核和用户空间共享一块内存实现消息传递。 综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点。

表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。

select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善

ep1

ep2

ep3

ep4

3.union

 共用体,也叫联合体,在一个“联合”内可以定义多种不同的数据类型, 一个被说明为该“联合”类型的变量中,允许装入该“联合”所定义的任何一种数据,这些数据共享同一段内存,以达到节省空间的目的。union变量所占用的内存长度等于最长的成员的内存长度。

先看一个关于struct的例子:

struct student
{
char mark;
long num;
float score;
};

其struct的内存结构如下,sizeof(struct student)的值为12bytes。

下面是关于union的例子:

union test
{
char mark;
long num;
float score;
};

sizeof(union test)的值为4。因为共用体将一个char类型的mark、一个long类型的num变量和一个float类型的score变量存放在同一个地址开始的内存单元中,而char类型和long类型所占的内存字节数是不一样的,但是在union中都是从同一个地址存放的,也就是使用的覆盖技术,这三个变量互相覆盖,而这种使几个不同的变量共占同一段内存的结构,称为“共用体”类型的结构。其union类型的结构如下:

因union中的所有成员起始地址都是一样的,所以&a.mark、&a.num和&a.score的值都是一样的。

4.网络编程的三个重要信号

SIGUP,SIGPIPE,SIGURG

SIGHUP信号   在介绍SIGHUP信号之前,先来了解两个概念:进程组和会话。

进程组   进程组就是一系列相互关联的进程集合,系统中的每一个进程也必须从属于某一个进程组;每个进程组中都会有一个唯一的 ID(process group id),简称 PGID;PGID 一般等同于进程组的创建进程的 Process ID,而这个进进程一般也会被称为进程组先导(process group leader),同一进程组中除了进程组先导外的其他进程都是其子进程;   进程组的存在,方便了系统对多个相关进程执行某些统一的操作,例如,我们可以一次性发送一个信号量给同一进程组中的所有进程。

会话   会话(session)是一个若干进程组的集合,同样的,系统中每一个进程组也都必须从属于某一个会话;一个会话只拥有最多一个控制终端(也可以没有),该终端为会话中所有进程组中的进程所共用。一个会话中前台进程组只会有一个,只有其中的进程才可以和控制终端进行交互;除了前台进程组外的进程组,都是后台进程组;和进程组先导类似,会话中也有会话先导(session leader)的概念,用来表示建立起到控制终端连接的进程。在拥有控制终端的会话中,session leader 也被称为控制进程(controlling process),一般来说控制进程也就是登入系统的 shell 进程(login shell);

PPID 指父进程 id;

PID 指进程 id;

PGID 指进程组 id;

SID 指会话 id;

TTY 指会话的控制终端设备;

COMMAND 指进程所执行的命令;

TPGID 指前台进程组的 PGID。

SIGHUP信号的触发及默认处理   在对会话的概念有所了解之后,我们现在开始正式介绍一下SIGHUP信号,SIGHUP 信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联. 系统对SIGHUP信号的默认处理是终止收到该信号的进程。所以若程序中没有捕捉该信号,当收到该信号时,进程就会退出。

   SIGHUP会在以下3种情况下被发送给相应的进程:   1、终端关闭时,该信号被发送到session首进程以及作为job提交的进程(即用 & 符号提交的进程);   2、session首进程退出时,该信号被发送到该session中的前台进程组中的每一个进程;    3、若父进程退出导致进程组成为孤儿进程组,且该进程组中有进程处于停止状态(收到SIGSTOP或SIGTSTP信号),该信号会被发送到该进程组中的每一个进程。

     例如:在我们登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个 Session。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进 程组和后台有终端输出的进程就会中止。

SIGPIPE   在网络编程中,SIGPIPE这个信号是很常见的。当往一个写端关闭的管道或socket连接中连续写入数据时会引发SIGPIPE信号,引发SIGPIPE信号的写操作将设置errno为EPIPE。在TCP通信中,当通信的双方中的一方close一个连接时,若另一方接着发数据,根据TCP协议的规定,会收到一个RST响应报文,若再往这个服务器发送数据时,系统会发出一个SIGPIPE信号给进程,告诉进程这个连接已经断开了,不能再写入数据。 此外,因为SIGPIPE信号的默认行为是结束进程,而我们绝对不希望因为写操作的错误而导致程序退出,尤其是作为服务器程序来说就更恶劣了。所以我们应该对这种信号加以处理,在这里,介绍两种处理SIGPIPE信号的方式:

  1 、给SIGPIPE设置SIG_IGN信号处理函数,忽略该信号:

signal(SIGPIPE, SIG_IGN);

  前文说过,引发SIGPIPE信号的写操作将设置errno为EPIPE,。所以,第二次往关闭的socket中写入数据时, 会返回-1, 同时errno置为EPIPE. 这样,便能知道对端已经关闭,然后进行相应处理,而不会导致整个进程退出.   2、使用send函数的MSG_NOSIGNAL 标志来禁止写操作触发SIGPIPE信号。

send(sockfd , buf , size , MSG_NOSIGNAL);

   同样,我们可以根据send函数反馈的errno来判断socket的读端是否已经关闭。    此外,我们也可以通过IO复用函数来检测管道和socket连接的读端是否已经关闭。以POLL为例,当socket连接被对方关闭时,socket上的POLLRDHUP事件将被触发。

SIGURG   在介绍SIGURG信号之前,先来说说什么是带外数据。

带外数据   带外数据用于迅速告知对方本端发生的重要的事件。它比普通的数据(带内数据)拥有更高的优先级,不论发送缓冲区中是否有排队等待发送的数据,它总是被立即发送。带外数据的传输可以使用一条独立的传输层连接,也可以映射到传输普通数据的连接中。实际应用中,带外数据是使用很少见,有,telnet和ftp等远程非活跃程序。   UDP没有没有实现带外数据传输,TCP也没有真正的带外数据。不过TCP利用头部的紧急指针标志和紧急指针,为应用程序提供了一种紧急方式,含义和带外数据类似。TCP的紧急方式利用传输普通数据的连接来传输紧急数据。

SIGURG信号的作用   内核通知应用程序带外数据到达的方式有两种:一种就是利用IO复用技术的系统调用(如select)在接受到带外数据时将返回,并向应用程序报告socket上的异常事件。   另一种方法就是使用SIGURG信号。

5.C++适配器

见CPP目录

6.lambda表达式

Lambda表达式是现代C++在C ++ 11和更高版本中的一个新的语法糖 ,在C++11、C++14、C++17和C++20中Lambda表达的内容还在不断更新。

Lambda表达式示例

Lambda有很多叫法,有Lambda表达式、Lambda函数、匿名函数,本文中为了方便表述统一用Lambda表达式进行叙述。 ISO C++标准官网展示了一个简单的lambda表示式实例:

#include <algorithm>
#include <cmath>

void abssort(float* x, unsigned n) {
std::sort(x, x + n,
// Lambda expression begins
[](float a, float b) {
return (std::abs(a) < std::abs(b));
} // end of lambda expression
);
}
1234567891011

在上面的实例中std::sort函数第三个参数应该是传递一个排序规则的函数,但是这个实例中直接将排序函数的实现写在应该传递函数的位置,省去了定义排序函数的过程,对于这种不需要复用,且短小的函数,直接传递函数体可以增加代码的可读性。

Lambda表达式参数详解

Lambda捕获列表

Lambda表达式与普通函数最大的区别是,除了可以使用参数以外,Lambda函数还可以通过捕获列表访问一些上下文中的数据。具体地,捕捉列表描述了上下文中哪些数据可以被Lambda使用,以及使用方式(以值传递的方式或引用传递的方式)。语法上,在“[]”包括起来的是捕获列表,捕获列表由多个捕获项组成,并以逗号分隔。捕获列表有以下几种形式:

  • []表示不捕获任何变量
auto function = ([]{
std::cout << "Hello World!" << std::endl;
}
);

function();
  • [var]表示值传递方式捕获变量var
int num = 100;
auto function = ([num]{
std::cout << num << std::endl;
}
);

function();
  • [=]表示值传递方式捕获所有父作用域的变量(包括this
int index = 1;
int num = 100;
auto function = ([=]{
std::cout << "index: "<< index << ", "
<< "num: "<< num << std::endl;
}
);

function();
  • [&var]表示引用传递捕捉变量var
int num = 100;
auto function = ([&num]{
num = 1000;
std::cout << "num: " << num << std::endl;
}
);

function();
  • [&]表示引用传递方式捕捉所有父作用域的变量(包括this
int index = 1;
int num = 100;
auto function = ([&]{
num = 1000;
index = 2;
std::cout << "index: "<< index << ", "
<< "num: "<< num << std::endl;
}
);

function();
  • [this]表示值传递方式捕捉当前的this指针
#include <iostream>
using namespace std;

class Lambda
{
public:
void sayHello() {
std::cout << "Hello" << std::endl;
};

void lambda() {
auto function = [this]{
this->sayHello();
};

function();
}
};

int main()
{
Lambda demo;
demo.lambda();
}

[=, &]拷贝与引用混合

[=, &a, &b]表示以引用传递的方式捕捉变量ab,以值传递方式捕捉其它所有变量。

int index = 1;
int num = 100;
auto function = ([=, &index, &num]{
num = 1000;
index = 2;
std::cout << "index: "<< index << ", "
<< "num: "<< num << std::endl;
}
);

function();
  • [&, a, this]表示以值传递的方式捕捉变量athis,引用传递方式捕捉其它所有变量。

不过值得注意的是,捕捉列表不允许变量重复传递。下面一些例子就是典型的重复,会导致编译时期的错误。例如:

  • [=,a]这里已经以值传递方式捕捉了所有变量,但是重复捕捉a了,会报错的;
  • [&,&this]这里&已经以引用传递方式捕捉了所有变量,再捕捉this也是一种重复。

如果Lambda主体total通过引用访问外部变量,并factor通过值访问外部变量,则以下捕获子句是等效的:

[&total, factor]
[factor, &total]
[&, factor]
[factor, &]
[=, &total]
[&total, =]

Lambda参数列表

除了捕获列表之外,Lambda还可以接受输入参数。参数列表是可选的,并且在大多数方面类似于函数的参数列表。

auto function = [] (int first, int second){
return first + second;
};

function(100, 200);

可变规格mutable

mutable修饰符, 默认情况下Lambda函数总是一个const函数,mutable可以取消其常量性。在使用该修饰符时,参数列表不可省略(即使参数为空)。

#include <iostream>
using namespace std;

int main()
{
int m = 0;
int n = 0;
[&, n] (int a) mutable { m = ++n + a; }(4);
cout << m << endl << n << endl;
}

异常说明

你可以使用 throw() 异常规范来指示 Lambda 表达式不会引发任何异常。与普通函数一样,如果 Lambda 表达式声明 C4297 异常规范且 Lambda 体引发异常,Visual C++ 编译器将生成警告 throw()

int main() // C4297 expected 
{
[]() throw() { throw 5; }();
}

在MSDN的异常规范中,明确指出异常规范是在 C++11 中弃用的 C++ 语言功能。因此这里不建议不建议大家使用。

返回类型

Lambda表达式的返回类型会自动推导。除非你指定了返回类型,否则不必使用关键字。返回型类似于通常的方法或函数的返回型部分。但是,返回类型必须在参数列表之后,并且必须在返回类型->之前包含类型关键字。如果Lambda主体仅包含一个return语句或该表达式未返回值,则可以省略Lambda表达式的return-type部分。如果Lambda主体包含一个return语句,则编译器将从return表达式的类型中推断出return类型。否则,编译器将返回类型推导为void

auto x1 = [](int i){ return i; };

Lambda函数体

Lambda表达式的Lambda主体(标准语法中的复合语句)可以包含普通方法或函数的主体可以包含的任何内容。普通函数和Lambda表达式的主体都可以访问以下类型的变量:

  • 捕获变量
  • 形参变量
  • 局部声明的变量
  • 类数据成员,当在类内声明this并被捕获时
  • 具有静态存储持续时间的任何变量,例如全局变量
#include <iostream>
using namespace std;

int main()
{
int m = 0;
int n = 0;
[&, n] (int a) mutable { m = ++n + a; }(4);
cout << m << endl << n << endl;
}

7.进程/线程同步与互斥问题

信号量

信号量是一种特殊的变量,它只能取自然数值并且只支持两种操作:等待(P)和信号(V).假设有信号量SV,对其的P、V操作如下:

  • P,如果SV的值大于0,则将其减一;若SV的值为0,则挂起执行
  • V,如果有其他进行因为等待SV而挂起,则唤醒;若没有,则将SV值加一

信号量的取值可以是任何自然数,最常用的,最简单的信号量是二进制信号量,只有0和1两个值.

  • sem_init函数用于初始化一个未命名的信号量
  • sem_destory函数用于销毁信号量
  • sem_wait函数将以原子操作方式将信号量减一,信号量为0时,sem_wait阻塞
  • sem_post函数以原子操作方式将信号量加一,信号量大于0时,唤醒调用sem_post的线程

以上,成功返回0,失败返回errno

示例代码:

class sem
{
public:
sem() //构造函数,初始化默认为0
{
if (sem_init(&m_sem, 0, 0) != 0)
{
throw std::exception();
}
}
sem(int num)//构造函数,初始化num
{
if (sem_init(&m_sem, 0, num) != 0)
{
throw std::exception();
}
}
~sem()
{
sem_destroy(&m_sem);
}
bool wait() // P操作,m_sem--
{
return sem_wait(&m_sem) == 0;
}
bool post() //V操作,m_sem++,唤醒等待的wait
{
return sem_post(&m_sem) == 0;
}

private:
sem_t m_sem;
};

互斥量

互斥锁,也成互斥量,可以保护关键代码段,以确保独占式访问.当进入关键代码段,获得互斥锁将其加锁;离开关键代码段,唤醒等待该互斥锁的线程.

  • pthread_mutex_init函数用于初始化互斥锁
  • pthread_mutex_destory函数用于销毁互斥锁
  • pthread_mutex_lock函数以原子操作方式给互斥锁加锁
  • pthread_mutex_unlock函数以原子操作方式给互斥锁解锁

以上,成功返回0,失败返回errno

class locker
{
public:
locker()
{
if (pthread_mutex_init(&m_mutex, NULL) != 0)
{
throw std::exception();
}
}
~locker()
{
pthread_mutex_destroy(&m_mutex);
}
bool lock()
{
return pthread_mutex_lock(&m_mutex) == 0;
}
bool unlock()
{
return pthread_mutex_unlock(&m_mutex) == 0;
}
pthread_mutex_t *get()
{
return &m_mutex;
}

private:
pthread_mutex_t m_mutex;
};

条件变量

条件变量提供了一种线程间的通知机制,当某个共享数据达到某个值时,唤醒等待这个共享数据的线程.

  • pthread_cond_init函数用于初始化条件变量
  • pthread_cond_destory函数销毁条件变量
  • pthread_cond_broadcast函数以广播的方式唤醒所有等待目标条件变量的线程
  • pthread_cond_wait函数用于等待目标条件变量.该函数调用时需要传入 mutex参数(加锁的互斥锁) ,函数执行时,先把调用线程放入条件变量的请求队列,然后将互斥锁mutex解锁,当函数成功返回为0时,互斥锁会再次被锁上. 也就是说函数内部会有一次解锁和加锁操作.
class cond
{
public:
cond()
{
if (pthread_cond_init(&m_cond, NULL) != 0)
{
//pthread_mutex_destroy(&m_mutex);
throw std::exception();
}
}
~cond()
{
pthread_cond_destroy(&m_cond);
}
bool wait(pthread_mutex_t *m_mutex)
{
int ret = 0;
//pthread_mutex_lock(&m_mutex);
ret = pthread_cond_wait(&m_cond, m_mutex);
//pthread_mutex_unlock(&m_mutex);
return ret == 0;
}
bool timewait(pthread_mutex_t *m_mutex, struct timespec t)
{
int ret = 0;
//pthread_mutex_lock(&m_mutex);
ret = pthread_cond_timedwait(&m_cond, m_mutex, &t);
//pthread_mutex_unlock(&m_mutex);
return ret == 0;
}
bool signal()
{
return pthread_cond_signal(&m_cond) == 0;
}
bool broadcast()
{
return pthread_cond_broadcast(&m_cond) == 0;
}

private:
//static pthread_mutex_t m_mutex;
pthread_cond_t m_cond;
};

8.容器空间配置器allocater

C++的容器空间配置器(allocator)是用于分配和管理容器的内存空间的工具。它负责为容器中的对象分配内存,并在容器不再需要这些对象时释放内存。

容器空间配置器主要用于处理以下几个任务:

内存分配:容器空间配置器负责为容器中的对象分配内存。它通过调用操作系统提供的内存分配函数(如malloc())来获取一块足够大小的连续内存空间。
内存释放:当容器中的对象不再需要时,容器空间配置器负责释放这些对象的内存。它会调用操作系统提供的内存释放函数(如free())来释放已分配的内存空间。
对象构造和析构:容器空间配置器还负责对容器中的对象进行构造和析构操作。当新的对象需要被插入到容器中时,空间配置器会调用对象的构造函数来初始化对象。而当对象被删除或容器被销毁时,空间配置器会调用对象的析构函数来销毁对象。
容器空间配置器的设计允许开发人员自定义内存分配和释放策略。默认情况下,容器空间配置器使用C++标准库提供的全局内存分配函数和析构函数来处理内存的分配和释放。但是,用户可以通过自定义分配器来实现更高效或特定需求的内存管理方式。

容器空间配置器是C++标准库中各种容器的基础。它为容器提供了一种统一的接口,使得不同类型的容器可以共享同样的内存管理机制,同时也提高了容器的灵活性和可扩展性。

9.绑定器bind function

using EventCallback = std::function<void()>
//using定义别名 == typedef 重定义一个类型

bind

bind绑定器是一个函数模板,它可以用来将一个函数的参数绑定到特定的值或另一个函数。这使得我们可以轻松地从现有函数创建新函数。

bind绑定器的语法如下:

#include <functional>

std::bind(func, args...)

其中,func是要绑定的函数或函数指针,args...是要绑定的参数列表。返回值是一个可调用对象,该对象将调用func并使用绑定的参数列表。

示例1:简单的bind用法

#include <functional>
#include <iostream>

int add(int a, int b) {
return a + b;
}

int main() {
auto add2 = std::bind(&add, 2, std::placeholders::_1);
std::cout << add2(3) << std::endl; // 输出5
return 0;
}

在这个例子中,我们定义了一个名为add2的函数对象,该对象是从add函数生成的。我们将2绑定到add函数的第一个参数,然后使用std::placeholders::_1占位符绑定该函数的第二个参数。这使得我们可以使用add2来执行add函数,将第一个参数设为2,第二个参数设为3,从而得到5的结果。

示例2:绑定函数和对象

#include <functional>
#include <iostream>

class Adder {
public:
int add(int a, int b) {
return a + b;
}
};

int main() {
Adder adder;
auto add3 = std::bind(&Adder::add, &adder, std::placeholders::_1, 3);
std::cout << add3(2) << std::endl; // 输出5
return 0;
}

在这个例子中,我们有一个名为Adder的类,其中包含一个名为add的方法。我们将对象的地址传递给bind函数,然后使用std::placeholders::_1占位符绑定add方法的第一个参数,将3绑定到add方法的第二个参数。这使得我们可以使用add3来执行add函数,将第一个参数设为2,从而得到5的结果。

function函数对象

function函数对象是一个能够存储和调用任何可调用对象(包括函数、函数指针、成员函数指针、函数对象)的类模板。

#include <functional>

std::function<return_type(args...)> obj;

其中,obj是一个function对象,args...是该对象接受的参数列表,return_type是该对象返回的类型。返回值是一个可调用的对象,它可以被调用并返回一个返回值。

示例1:存储和调用函数

#include <functional>
#include <iostream>

int add(int a, int b) {
return a + b;
}

int main() {
std::function<int(int, int)> f = add;
std::cout << f(2, 3) << std::endl; // 输出5
return 0;
}

在这个例子中,我们定义了一个名为f的function对象,它接受两个整数作为参数,并调用add函数来计算它们的和。我们可以将f视为一个函数,将其传递给其他函数或存储在容器中等等。

存储和调用函数对象

#include <functional>
#include <iostream>

class Adder {
public:
int add(int a, int b) {
return a + b;
}
};

int main() {
Adder adder;
std::function<int(Adder&, int, int)> f = &Adder::add;
std::cout << f(adder, 2, 3) << std::endl; // 输出5
return 0;
}

在这个例子中,我们定义了一个名为Adder的类,其中包含一个名为add的方法。我们使用std::function模板定义了一个对象f,它接受一个Adder对象、两个整数作为参数,并调用Adder::add方法来计算它们的和。我们存储了一个存储类方法指针的function对象,并通过它成功调用了类方法。

10.容器适配器

容器适配器:以某种已有的既定容器作为底层结构,在其基础上进一步地进行封装接口函数。使其可以满足某种特性。

11.move移动语义和forward类型

move()函数:将左值转换为右值,实现对象资源的转移。

11.Linux网络编程函数用法

11.1 sockaddr和sockaddr_in详解

struct sockaddrstruct sockaddr_in 这两个结构体用来处理网络通信的地址。

//sockaddr在头文件#include <sys/socket.h>中定义,sockaddr的缺陷是:sa_data把目标地址和端口信息混在一起了,如下
struct sockaddr {
sa_family_t sin_family;//地址族
   char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息
   };


//sockaddr_in在头文件#include<netinet/in.h>或#include <arpa/inet.h>中定义,该结构体解决了sockaddr的缺陷,把port和addr 分开储存在两个变量中,如下:
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */

/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr)
- __SOCKADDR_COMMON_SIZE
- sizeof (in_port_t)
- sizeof (struct in_addr)];
};
//sin_port和sin_addr都必须是网络字节序(NBO),一般可视化的数字都是主机字节序(HBO)。

https://img-blog.csdn.net/20161125160930613

二者长度一样,都是16个字节,即占用的内存大小是一致的,因此可以互相转化。二者是并列结构,指向sockaddr_in结构的指针也可以指向sockaddr。

sockaddr常用于bind、connect、recvfrom、sendto等函数的参数,指明地址信息,是一种通用的套接字地址。 sockaddr_in 是internet环境下套接字的地址形式。所以在网络编程中我们会对sockaddr_in结构体进行操作,使用sockaddr_in来建立所需的信息,最后使用类型转化就可以了。一般先把sockaddr_in变量赋值后,强制类型转换后传入用sockaddr做参数的函数:sockaddr_in用于socket定义和赋值;sockaddr用于函数参数。

例子:

包含各种转化函数

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main(int argc,char **argv)
{
int sockfd = 0;
struct sockaddr_in addr_in;
struct sockaddr * addr;

sockfd = socket(AF_INET, SOCK_STREAM, 0); //获得fd
bzero(&addr_in,sizeof(addr_in)); // 初始化结构体
/*
8008的主机字节序 小端字节序 0001 1111 0100 1000 = 8008
8008的网络字节序 大端字节序 0100 1000 0001 1111 = 18463
*/
addr_in.sin_port = htons(8008);
addr_in.sin_family = AF_INET; // 设置地址家族
addr_in.sin_addr.s_addr = inet_addr("192.168.3.30"); //设置地址
printf("sockaddr_in.sin_addr.s_addr = %d \n", addr_in.sin_addr.s_addr);
printf("addr = %s \n", inet_ntoa(addr_in.sin_addr));
// addr_in.sin_addr.s_addr = htonl(INADDR_ANY); //设置地址

printf("struct sockaddr size = %ld \n", sizeof (addr));
printf("struct sockaddr_in size = %ld \n", sizeof (addr_in));
addr = (struct sockaddr *)&addr_in;
// bind(sockfd, (struct sockaddr *)&addr_in, sizeof(struct sockaddr)); /* bind的时候进行转化 */
bind(sockfd, addr, sizeof(struct sockaddr));
... ...
return 0;
}

inet_addr:将常量字符转换为32位整型

addr_.sin_addr.s_addr = inet_addr(ip.c_str());

inet_ntop: 将sockaddr_in输出字符串到指定地方

char buf[64] = {0};
::inet_ntop(AF_INET,&addr_.sin_addr,buf,sizeof buf);

12.智能指针

12.1 不带记数的智能指针unique_ptr

unique_ptr则“独占”所指向的对象,unique_ptr虽然不支持普通的拷贝和赋值操作,但却可以将所有权进行转移,使用std::move方法即可。unique_ptr 是一种独占式智能指针,它拥有对对象的唯一所有权,不能被多个 unique_ptr 对象共享。当 unique_ptr 对象被销毁时,它所管理的对象也会被销毁。

void f1() {
unique_ptr<int> p(new int(5));
unique_ptr<int> p2 = std::move(p);
//error,此时p指针为空: cout<<*p<<endl;
cout<<*p2<<endl;
}

12.2 带记数的智能指针shared_ptr,weak_ptr

shared_ptr 是一种共享式智能指针,可以被多个 shared_ptr 对象共享同一个对象,当最后一个 shared_ptr 对象销毁时,它所管理的对象也会被销毁。shared_ptr 使用引用计数来管理对象的生命周期。

weak_ptr 是一种弱引用智能指针,不能单独管理对象的生命周期,它只能指向被 shared_ptr 管理的对象,用于解决 shared_ptr 循环引用的问题。weak_ptr 不会增加对象的引用计数,当最后一个 shared_ptr 对象销毁时,weak_ptr 不会影响对象的销毁。

shared_ptrweak_ptr 配合使用解决以下两个问题:

1.shared_ptr之间循环引用问题,造成内存泄漏。new 定义对象的时候使用shared_ptr,引用对象的时候定义weak_ptr解决

weak_ptr没有重载->所以不能直接用weak_ptr访问资源

class Monster{
  //尽管父子可以互相访问,但是彼此都是独立的个体,无论是谁都不应该拥有另一个人的所有权。
  std::weak_ptr<Monster> m_father;    //所以都把shared_ptr换成了weak_ptr
  std::weak_ptr<Monster> m_son;      //同上

public:
  void setFather(std::shared_ptr<Monster>& father); //实现细节懒得写了
  void setSon(std::shared_ptr<Monster>& son);    //懒
  ~Monster(){std::cout << "A monster die!";}     //析构时发出死亡的悲鸣
};

2.智能指针线程安全问题。使用shared_ptr.lock()提升为强智能指针,如果成功,说明没有被释放。如果返回NULL,则说明该资源以及被释放。

std::weak_ptr<void> tie_;
std::shared_ptr<void> guard = tie_.lock(); //如果提升成功说明资源没有被释放

12.3 shared_from_this

通过模板方式继承enable_shared_from_this\<T> 然后调用shared_from_this()函数返回对象T的shared_ptr指针,非常方便。使用时需要引用头文件 :#include\<memory>

#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <memory>

using namespace std;

class TestA : public enable_shared_from_this<TestA>
{
public:
TestA()
{
cout << "TestA create" << endl;
}
~TestA()
{
cout << "TesA destory" << endl;
}

shared_ptr<TestA> getSharedFromThis() { return shared_from_this(); }

};

int main(){

{//出了此作用域 ptr1 ptr2 销毁, TestA对象销毁
shared_ptr<TestA> ptr1(new TestA());
shared_ptr<TestA> ptr2 = ptr1->getSharedFromThis();
cout << "ptr1 count: " << ptr1.use_count() << " ptr2 count: " << ptr2.use_count() << endl;
//输出:ptr1 count: 2 ptr2 count: 2 可以正常释放对象
}

return 0;
}

问题:为什么要这么麻烦,要继承一个继承enable_shared_from_this\<T>模板对象,使用其中的成员函数shared_from_this(),直接使用:

shared_ptr<TestA> getSharedFromThis() { return  shared_ptr<TestA> (this); }

这样不好吗?

这样使用就是陷入shared_ptr使用的陷阱里了。我们可以写一个程序试试看:

#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <memory>

using namespace std;

class TestB
{
public:
TestB()
{
cout << "TestB create" << endl;
}
~TestB()
{
cout << "TestB destory" << endl;
}

shared_ptr<TestB> getSharedFromThis() { return shared_ptr<TestB> (this); }

};

int main(){

{
//shared_ptr<TestB> ptr3(new TestB());
//shared_ptr<TestB> ptr4 = ptr3->getSharedFromThis();
//cout << "ptr2 count: " << ptr3.use_count() << " ptr4 count: " << ptr4.use_count() << endl;
//输出:ptr2 count: 1 ptr4 count: 1 然后会崩溃因为重复释放
}

cin.get();
return 0;
}

​ 两个shared_ptr的引用计数都是1,然后释放时,导致对象被释放两遍,导致程序崩溃。 为什么会这样,要从shared_ptr的原理说起;shared_ptr为什么能够在没有shared_ptr指针指向对象时释放对象?

因为所有指向同一个对象的shared_ptr指针共享同一个计数器,当有新的shared_ptr指向对象时,计数器+1,有shared_ptr销毁或者不再指向该对象时,计数器-1,当计数器为0时,对象被销毁。

当如果出现两个shared_ptr指针都指向同一对象,但是计数器不共享时,会导致对象被释放两次,程序出错了,就如同上面的例子。

如何会导致shared_ptr指向同一个对象,但是不共享引用计数器。是因为裸指针与shared_ptr混用,如果我们用一个裸指针初始化或者赋值给shared_ptr指针时,在shared_ptr内部生成一个计数器,当另外一个shared_ptr不用share_ptr赋值或者初始化的话,再次将一个裸指针赋值给另外一个shared_ptr时,又一次生成一个计数器,两个计数器不共享。

#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <memory>

using namespace std;

int main(){
shared_ptr<int> ptr1(new int(101));
shared_ptr<int> ptr2(ptr1);
shared_ptr<int> ptr3;
ptr3 = ptr1;

shared_ptr<int> ptr4(ptr1.get());//ptr1.get()返回ptr1保存对象的裸指针,ptr4会有新的计数器,不跟ptr1,ptr2,ptr3共享计数器
cout << ptr1.use_count() << " " << ptr2.use_count() << " "
<< ptr3.use_count() << " " << ptr4.use_count() << endl;
//所以一定不要既用shared_ptr又用对象裸指针(p.get()相当于裸指针)
cin.get();
return 0;
}

13.C++ 11原子操作-信号量-互斥锁

13.1原子操作

#include <atomic>
std::atomic_bool looping_; // 原子操作,通过CAS实现的
std::atomic_int looping_; // 原子操作,通过CAS实现的

13.2 互斥锁

#include <mutex>
std::mutex mutex_; // 互斥锁,用来保护上面vector容器的线程安全操作
{
std::lock_guard<std::mutex> lock(mutex); // 自动锁定
// 在此处执行互斥操作
} // 离开作用域时自动释放互斥量
//一旦创建了lock_guard对象,并且它获得了互斥量的所有权,就会自动锁定,当离开作用域时,lock_guard对象会自动释放互斥量。只能用在简单函数互斥代码中,不能拷贝和复值



std::unique_lock<std::mutex> lock(mutex_); //用unique_lock 保证出了作用域自动解锁
//unique_lock提供了更高级的功能和更大的灵活性。它可以手动锁定和释放互斥量,并且可以通过提供不同的锁定策略来控制锁的行为。它还允许将锁定状态转移到另一个unique_lock对象,并且可以在锁定期间进行条件变量的等待和通知操作。
std::mutex mutex;
{
std::unique_lock<std::mutex> lock(mutex); // 手动锁定
// 执行互斥操作
lock.unlock(); // 手动释放锁定
// ...
lock.lock(); // 手动重新锁定
// 执行其他互斥操作
} // 离开作用域时手动释放互斥量 ,可以用在函数调用过程中

13.3 线程类

在声明一个std::thread对象之后,都可以使用detach和join函数来启动被调线程,区别在于两者是否阻塞主调线程。

#include <thread>
void func(int x string s)
{
***
}
std::thread thread1(func,12,"123");
//创建线程,可以传lambda表达式
std::shared_ptr<std::thread> thread_ =new std::thread([&]()
{
tid_ = CurrentThread::tid();
sem_post(&sem);
func_();
});
//当使用join()函数时,主调线程阻塞,等待被调线程终止,然后主调线程回收被调线程资源,并继续运行;
thread_->join(); //线程join
//当使用detach()函数时,主调线程继续运行,被调线程驻留后台运行,主调线程无法再取得该被调线程的控制权。当主调线程结束时,由运行时库负责清理与被调线程相关的资源。
thread_->detach(); //分离线程

13.4 信号量

#include <semaphore.h>
sem_t sem;
sem_init(&sem,false , 0); //构造函数,初始化默认为0
sem_post(&sem); //V
sem_wait(&sem); //P

13.5 条件变量

#include <condition_variable>
std::condition_variable cond_;
std::unique_lock<std::mutex> lock(mutex);
cond_.wait(lock); //等待阻塞
cond_.notify_one();//通知一个
cond_.notify_all();//通知所有

14.sprintf与snprintf

sprintf 和 snprintf 是C语言中的字符串格式化函数,它们有以下几个区别:

1.参数不同:

sprintf 的语法为 int sprintf(char *str, const char *format, ...),
其将格式化后的字符串写入到字符数组 str 中。

snprintf 的语法为 int snprintf(char *str, size_t size, const char *format, ...),
其将格式化后的字符串写入到字符数组 str 中,但最多写入 size-1 个字符。

2.安全性:

snprintf 在写入字符串时,会指定最大可写入的字符数,从而避免溢出,确保不会造成缓冲区溢出的安全问题。
sprintf 不具备对输出缓冲区的长度进行检查,可能会造成缓冲区溢出,存在安全风险。因此,使用 sprintf 时需要确保提供的输出缓冲区足够大以容纳生成的字符串。

3.返回值:

sprintf 和 snprintf 的返回值都是生成的字符串的长度(不包括终止符 \0)。
如果生成的字符串长度大于等于提供的缓冲区大小 size,snprintf 会返回 size(或更小),以表示输出被截断(截断的字符串不会以 \0 结束)。

snprintf 还可以通过检查返回值来判断是否发生了截断。
总而言之,snprintf 比 sprintf 更加安全,因为它提供了写入字符数的限制,可以避免缓冲区溢出的问题。在使用时建议优先选择 snprintf,特别是需要处理用户输入或未知长度的字符串时。