C++知识点

C++知识点

      • 1、动态内存分配(malloc):
        • 1.1 内存分配系统调用(brk和mmap)
        • 1.2 C++/C的内存分配
        • 1.3 堆和栈的区别
      • 2、内存对齐
      • 3、new和malloc的区别
      • 4、socket网络编程
        • 4.1 TCP协议通信流程
        • 4.2 UDP协议通信流程
        • 4.3 udp的connect函数
        • 4.4 socket模型
      • 5、如何采用单线程的方式处理高并发
      • 6、IO多路复用
      • 7、智能指针(C++11特性)
      • 8、左值右值
      • 9、static静态成员变量和成员函数
      • 10、编译过程
      • 11、 epoll怎么实现的
      • 12、 支持高并发网络I/O的编程
      • 13、 强制类型转换
        • 1 static_cast(最常用)
        • 2 dynamic_cast
        • 3 reinterpret_cast
        • 4 const_cast
      • 14、 const与define区别
      • 15、数组和指针的区别
    • 16、内存泄漏,野指针
      • 野指针
      • 内存泄漏

1、动态内存分配(malloc):

使用隐式空闲链表实现简单的动态内存分配,动态内存分配器维护一个大块区域,也就是堆,处理动态的内存分配请求。

分配器将堆视为一组不同大小的块的集合来维护,每个块要么是已分配的,要么是空闲的。隐式空闲链表就是通过每个块的头部中存放的信息可以方便的定位到下一个块的位置。头部一般就是本块的大小及使用情况(分配或空闲)。返回给用户的区域并不包含控制信息。

当接收到一个内存分配请求时,从头开始遍历堆,找到一个空闲的满足大小要求的块,若有剩余,将剩余部分变成一个新的空闲块,更新相关块的控制信息。调整起始位置,返回给用户。
释放内存时,仅需把使用情况标记为空闲即可。当找不到满足请求的空闲块时,即就是合并空闲块问题。有两种策略,一个是立即合并,另一个是推迟合并。

推迟合并简单实现:

// 内部使用的合并空闲块函数
void _merge_free_blocks()
{
	mem_block *pos = (mem_block*)g_heap_start;//pos指向堆地址开始
	while((void*)((char*)pos+pos->size) < g_heap_end)//pos的end没有超过堆结尾时
	{
		mem_block *next_pos = (mem_block*)((char*)pos+pos->size);//记录下一个内存块的起始地址
		// 若相邻的两个块都是空闲,合二为一
		if(pos->is_free && next_pos->is_free)
			pos->size = pos->size+next_pos->size;
		else
			pos = next_pos;//如果不为空则寻找下一个
	}
	return;
}

隐式空闲链表的优点是简单。显著的缺点是任何操作的开销,例如放置分配的块,要求空闲链表的搜索与堆中已分配块和空闲块的总数呈线性关系。
https://blog.csdn.net/luckyxiaoqiang/article/details/8669602

1.1 内存分配系统调用(brk和mmap)

Malloc在申请内存时,其中当申请内存小于128K时,会使用系统函数brk在堆区中分配;
而当申请内存大于128K时,会使用系统函数mmap在映射区分配

  • brk是将进程堆的最高地址指针往高地址推。
  • mmap是在进程的虚拟地址空间中文件映射区域(堆与栈中间)找一块空闲的虚拟内存,通常是动态库的映射等。
    这两种方式都是分配虚拟内存。
    edata地址是指向数据段的最高地址,也是堆顶指针。

这么做的主要原因是:

  • brk分配的内存需要等高地址的内存释放以后才能释放,这就是内存碎片产生的原因。
  • mmap分配的内存可以单独释放,可以避免内存碎片的产生。
  • brk指针只有一个,如果先开辟的内存空间释放了,那么那块内存会空在那里,如果此时有一个申请同样大小内存的请求,就可以复用,不然就是碎片一直在那里。
  • 内存紧缩操作,当brk指针最高地址有超过128k的空闲内存,brk指针就会回退,执行紧缩操作。

1.2 C++/C的内存分配

32bit地址总线可寻址4G线性空间,每个进程都有各自独立的4G逻辑地址,其中0~3G是用户态空间,3~4G是内核空间,不同进程相同的逻辑地址会映射到不同的物理地址中。其逻辑地址其划分如下:

静态区域:

  • text segment(代码段):包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
  • data segment(数据段):存储程序中已初始化的全局变量和静态变量
  • bss segment:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量,对于未初始化的全局变量和静态变量,程序运行main之前时会统一清零。即未初始化的全局变量编译器会初始化为0.

动态区域:

  • heap(堆): 当进程未调用malloc时是没有堆段的,只有调用malloc时采用分配一个堆,并且在程序运行过程中可以动态增加堆大小(移动break指针),从低地址向高地址增长。分配小于128K内存时使用该区域。 堆的起始地址由mm_struct 结构体中的start_brk标识,结束地址由brk标识。
  • memory mapping segment(映射区):存储动态链接库等文件映射、申请大于128K内存(malloc时调用mmap函数)
  • stack(栈):使用栈空间存储函数的返回地址、参数、局部变量、返回值,从高地址向低地址增长。在创建进程时会有一个最大栈大小,Linux可以通过ulimit命令指定,通常为8M。
    C++知识点_第1张图片

1.3 堆和栈的区别

1)申请方式:
栈由系统自动分配和管理,堆由程序员手动分配和管理。
2)效率:
栈由系统分配,速度快,不会有内存碎片。
堆由程序员分配,速度较慢,可能由于操作不当产生内存碎片。
3)扩展方向
栈从高地址向低地址进行扩展,堆由低地址向高地址进行扩展。
4)程序局部变量是使用的栈空间,new/malloc动态申请的内存是堆空间,函数调用时会进行形参和返回值的压栈出栈,也是用的栈空间。

栈的效率高的原因:
栈是操作系统提供的数据结构,计算机底层对栈提供了一系列支持:分配专门的寄存器存储栈的地址,压栈和入栈有专门的指令执行;而堆是由C/C++函数库提供的,机制复杂,需要一些列分配内存、合并内存和释放内存的算法,因此效率较低。

2、内存对齐

1、原因:
1)平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2)性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

2、规则
结构体struct的总长度要可以整除每个成员的长度
1)数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。
2)结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。
3)结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。

3、示例

struct A{
    char a;//2
    short b;//2
    int c;//4
};//8
struct B{
    short a;//2
    char b;//1+5	
    double c;//8
    int *d;//4
    float e;//4
};//24

3、new和malloc的区别

  • new分配内存按照数据类型进行分配,malloc分配内存按照指定的大小分配;
  • new返回的是指定对象的指针,而malloc返回的是void*,因此malloc的返回值一般都需要进行类型转化。
  • new不仅分配一段内存,而且会调用构造函数,malloc不会。
  • new分配的内存要用delete销毁,malloc要用free来销毁;delete销毁的时候会调用对象的析构函数,而free则不会。
  • new是一个操作符可以重载,malloc是一个库函数。
  • malloc分配的内存不够的时候,可以用realloc扩容。
  • new如果分配失败了会抛出bad_malloc的异常,而malloc失败了会返回NULL。
  • 申请数组时: new[]一次分配所有内存,多次调用构造函数,搭配使用delete[],delete[]多次调用析构函数,销毁数组中的每个对象。而malloc则只能sizeof(int) * n。

4、socket网络编程

网络字节序为大端(低地址存高位);主机字节序为小端存储(低地址存低位),所以需要进行字节序的转换;

4.1 TCP协议通信流程

C++知识点_第2张图片
首先看一下用到的socket API,这些函数都在sys/socket.h中。
socket()

#include 
#include 
int socket(int domain,int type,int protocol);

 - socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,调用出错则返回-1- domain参数指定为AF_INET,表示IPV4,若是AF_INET6(IPV6);
 - type参数指定为SOCK_STREAM,表示面向流的传输协议TCP,SOCK_DGRAM表示UDP连接;
 - protocol参数的介绍从略,指定为0即可;
 - 应用程序可以像读写文件一样用read/write在网络上收发数据;

bind()

int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);

 - 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,服务器需要调用bind绑定的一个固定的网络地址和端口号;
 - bind()成功返回0,失败返回-1- bind()的作用是将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号;
 - struct sockaddr* 是一个通用指针类型,addr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen制定结构体的长度。

listen()

int listen(int sockfd,int backlog);

 - listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接等待状态,如果接收到更多的连接请求就忽略;
 - listen()成功返回0;失败返回-1- 当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求

accept()

int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);

 - 三次握手完成后,服务器用accept()接受连接;
 - 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户连接上来;
 - addr是一个传出参数,accept()返回时传出客户端的地址和端口号;
 - 如果给addr参数传NULL,表示不关心客户端的地址;
 - addrlen参数是一个传入传出参数,传入的是调用者提供的,缓冲区addr的长度以避免缓冲区溢出的问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。
 - accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的IP地址和端口号,而 sockfd 是服务器端的套接字。后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。

客户端connect()

int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen);

- sockaddr:传入参数,服务器的地址
- addrlen:传入参数,地址的长度

示例C/S,服务器将客户端发送的数据转换为大写返回为客户端

server.c
#include 
#include 
#include 
#include 
#include 
#include 
#define SERV_PORT 6666
#define SERV_IP "127.0.0.1"
int main()
{
    int lfd,cfd;
    char buf[BUFSIZ],clie_IP[BUFSIZ];
    int n;
    struct sockaddr_in serv_addr, clie_addr;
    socklen_t clie_addr_len;
    /*创建一个socket 指定IPv4协议族 TCP协议*/
    lfd = socket(AF_INET, SOCK_STREAM, 0);

    /*初始化一个地址结构 man 7 ip 查看对应信息*/
    bzero(&serv_addr, sizeof(serv_addr));           //将整个结构体清零
    serv_addr.sin_family = AF_INET;                 //选择协议族为IPv4
    serv_addr.sin_port = htons(SERV_PORT);          //绑定端口号    
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY这个宏可以自动转换为本地有效的IP
    /*绑定服务器地址结构*/
    bind(lfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr));

    listen(lfd, 128);   //同一时刻允许向服务器发起链接请求的数量
    printf("wait for client connect ...\n");

    clie_addr_len = sizeof(clie_addr);/*获取客户端地址结构大小*/

    /*参数1是sfd; 参2传出参数, 参3传入传出参数, 全部是client端的参数*/
    cfd = accept(lfd, (struct sockaddr *)&clie_addr, &clie_addr_len);
    printf("client IP:%s\tport:%d\n", 
            inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_IP, sizeof(clie_IP)), 
            ntohs(clie_addr.sin_port));
    while(1){
        /*读取客户端发送数据*/
        n = read(cfd, buf, sizeof(buf));
        //小写转大写发送给客户端
        for(i=0;i<n;i++)
            buf[i] = toupper(buf[i]);
        write(cfd, buf,n);
    }
    
    close(lfd);
    close(cfd);
    return 0;
}

client.c
#include 
#include 
#include 
#include 
#include 

#define SERV_IP "127.0.0.1"
#define SERV_PORT 6666
int main()
{
    int sfd,serv_addr_len,cfd;
    char buf[BUFSIZ];
    int n;

    sfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in serv_addr;//服务器IP+port
    bzero(&serv_addr, sizeof(serv_addr));//清零
    serv_addr.sin_family = AF_INET;
    inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr);//IP字符串转换为网络字节序 参3:传出参数
    serv_addr.sin_port = htons(SERV_PORT);//端口号转网络字节序

    connect(sfd,(struct sockaddr*)&serv_addr, sizeof(serv_addr));
    while(1){
        fgets(buf, sizeof(buf), stdin);/*从标准输入获取数据*/
        write(sfd, buf, strlen(buf));/*将数据写给服务器*/
        len = read(sfd, buf, sizeof(buf));/*从服务器读回转换后数据*/
        write(STDOUT_FILENO, buf,len); /*写至标准输出*/
    }
    close(sfd);
    return 0;
}

4.2 UDP协议通信流程

C++知识点_第3张图片
1、服务器端流程

1建立套接字文件描述符,使用函数socket(),生成套接字文件描述符。

2设置服务器地址和侦听端口,初始化要绑定的网络地址结构。

3绑定侦听端口,使用bind()函数,将套接字文件描述符和一个地址类型变量进行绑定。

4接收客户端的数据,使用recvfrom()函数接收客户端的网络数据。

5向客户端发送数据,使用sendto()函数向服务器主机发送数据。

6关闭套接字,使用close()函数释放资源。UDP协议的客户端流程

2、客户端流程

1建立套接字文件描述符,socket()。

2设置服务器地址和端口,struct sockaddr。

3向服务器发送数据,sendto()。

4接收服务器的数据,recvfrom()。

5关闭套接字,close()。

4.3 udp的connect函数

确实可以给UDP套接字调用connect,然而这样做的结果与TCP连接不同的是没有三路握手过程。内核只是检查是否存在立即可知的错误,记录对端的IP地址和端口号,然后立即返回调用进程。
对于已连接UDP套接字,与默认的未连接UDP套接字相比,发生了三个变化。
其实一旦UDP套接字调用了connect系统调用,那么这个UDP上的连接就变成一对一的连接,但是通过这个UDP连接传输数据的性质还是不变的,仍然是不可靠的UDP连接。一旦变成一对一的连接,在调用系统调用发送和接受数据时也就可以使用TCP那一套系统调用了。
1、再也不能给输出操作指定目的IP地址和端口号。也就是说,我们不使用sendto,而改用write或send。写到已连接UDP套接字上的任何内容都自动发送到由connect指定的协议地址。可以给已连接的UDP套接字调用sendto,但是不能指定目的地址。sendto的第五个参数必须为空指针,第六个参数应该为0.
2、不必使用recvfrom以获悉数据报的发送者,而改用read、recv或recvmsg。在一个已连接UDP套接字上,由内核为输入操作返回的数据报只有那些来自connect指定协议地址的数据报。这样就限制一个已连接UDP套接字能且仅能与一个对端交换数据报。
3、由已连接UDP套接字引发的异步错误会返回给它们所在的进程,而未连接的UDP套接字不接收任何异步错误。

来自任何其他IP地址或断开的数据报不投递给这个已连接套接字,因为它们要么源IP地址要么源UDP端口不与该套接字connect到的协议地址相匹配。
UDP客户进程或服务器进程只在使用自己的UDP套接字与确定的唯一对端进行通信时,才可以调用connect。调用connect的通常是UDP客户,不过有些网络应用中的UDP服务器会与单个客户长时间通信TFTP,这种情况下,客户和服务器都可能调用connect。

4.4 socket模型

  • 两种I/O模式

阻塞模式:执行I/O操作完成前会一直进行等待,不会将控制权交给程序。套接字默认为阻塞模式。可以通过多线程技术进行处理。

非阻塞模式:执行I/O操作时,Winsock函数会返回并交出控制权。这种模式使用起来比较复杂,因为函数在没有运行完成就进行返回,会不断地返回 WSAEWOULDBLOCK错误。但功能强大。

socket模型共有5种:Select模型,异步选择,事件选择,重叠I/O模型,完成端口模型;

1、select模型:轮询fd_set集合

int select(
    int nfds,
    fd_set* readfds,//可读性检查的套接口
    fd_set* writefds,//可写性检查的套接口
    fd_set* exceptfds,//等待错误检查的套接口
    const struct timeval* timeout//最多等待时间,对阻塞操作则为NULL
);

阻塞模式下:select将一直阻塞到有一个描述字满足条件。否则指向一个timeval的结构,其中指定了select调用在返回前等待多长时间。
服务器来轮询查看某个套接字是否仍然处于读集中,如果是,则接收数据。如果接收的数据长度为0,或者发生WSAECONNRESET错误,则表示客户端套接字主动关闭,这时需要将服务器中对应的套接字所绑定的资源释放掉,然后调整我们的套接字数组(将数组中最后一个套接字挪到当前的位置上)。
非阻塞模式:select 会在超时时间测试fd_set集合是否可用,如果超时/没有数据可读了,会清除当前集合成员。

2、异步选择(WSAAsyncSelect模型)

应用程序可以在一个套接字上接收以WINDOWS消息为基础的网络事件通知。
首先定义一个消息标示常量:const WM_SOCKET = WM_USER + 55;
再在主Form的private域添加一个处理此消息的函数声明
private
procedure WMSocket(var Msg: TMessage); message WM_SOCKET;
然后就可以使用WSAAsyncSelect了。
该模型的实现方法是通过调用WSAAsynSelect函数自动将套接字设置为非阻塞模式,并向WINDOWS注册一个或多个网络时间,并提供一个通知时使用的窗口句柄当注册的事件发生时,对应的窗口将收到一个基于消息的通知。

3、事件选择(WSAEventSelect模型)

异步I/O模型。在套接口上将一个或多个网络事件与事件对象关联在一起。一旦在某个套接字上发生了我们关注的事件(FD_READ和FD_CLOSE),与之相关联的WSAEVENT对象被Signaled。该模型与异步选择最主要的差别在于网络事件会投递至一个事件对象句柄,而非投递至一个窗口例程。

4、重叠I/O模型

Overlapped模型是让应用程序使用重叠数据结构(WSAOVERLAPPED),一次投递一个或多个Winsock I/O请求。这些提交的请求完成后,应用程序会收到通知就是说,如果你想从socket上接收数据,只需要告诉系统,由系统为你接收数据,而你需要做的只是为系统提供一个缓冲区。

5、完成端口模型

完成例程要求用户提供一个回调函数,发生新的网络事件的时候系统将执行这个函数。完成端口内部提供了线程池的管理,可以避免反复创建线程的开销,同时可以根据CPU的个数灵活的决定线程个数,而且可以让减少线程调度的次数从而提高性能。

5、如何采用单线程的方式处理高并发

在单线程模型中,可以采用I/O复用来提高单线程处理多个请求的能力,然后再采用事件驱动模型,基于异步回调来处理事件。

6、IO多路复用

IO多路复用就是我们说的select,poll,epoll。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。

  • 基本原理:是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
  • 特点是:通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。

select:是最初解决IO阻塞问题的方法。用结构体fd_set来告诉内核监听多个文件描述符,该结构体被称为描述符集。由数组来维持哪些描述符被置位了。对结构体的操作封装在三个宏定义中。通过轮寻来查找是否有描述符要被处理。

存在的问题:

  1. 内置数组的形式使得select的最大文件数受限与FD_SIZE;
  2. 每次调用select前都要重新初始化描述符集,将fd从用户态拷贝到内核态,每次调用select后,都需要将fd从内核态拷贝到用户态;
  3. 轮寻排查当文件描述符个数很多时,效率很低;

poll:通过一个可变长度的数组解决了select文件描述符受限的问题。数组中元素是结构体,该结构体保存描述符的信息,每增加一个文件描述符就向数组中加入一个结构体,结构体只需要拷贝一次到内核态。poll解决了select重复初始化的问题。轮寻排查的问题未解决。
epoll:轮寻排查所有文件描述符的效率不高,使服务器并发能力受限。因此,epoll采用只返回状态发生变化的文件描述符,便解决了轮寻的瓶颈。
epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式

  1. LT模式
    LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。
  2. ET模式
    ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)

ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

LT模式与ET模式的区别如下:

  • LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
  • ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

7、智能指针(C++11特性)

  • shared_ptr:是为了解决 auto_ptr 在对象所有权上的局限性。允许多个指针指向同一个对象,使用计数机制来表明资源被几个指针共享。当然这需要额外的开销。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。
  • weak_ptr:weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。
  • unique_ptr:旨在替代不安全的auto_ptr。持有对对象的独有权——两个unique_ptr不能指向一个对象,即unique_ptr不共享它的所管理的对象。将 unique_ptr 实例添加到 STL 容器运行效率很高,因为通过 unique_ptr 的移动构造函数(std::move()),不再需要进行复制操作。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用。

8、左值右值

  • 左值:通常指可以取地址,有名字的值就是左值;
  • 右值:不能取地址,没有名字。右值是由两个概念构成,将亡值和纯右值。纯右值是用于识别临时变量和一些不跟对象关联的值,比如1+3产生的临时变量值,2、true等,而将亡值通常是指具有转移语义的对象,比如返回右值引用T&&的函数返回值等。
    一般只能通过右值表达式获得其引用,比如:
    T && a=ReturnRvale();
    假设ReturnRvalue()函数返回一个右值,那么上述语句声明了一个名为a的右值引用,其值等于ReturnRvalue函数返回的临时变量的值。
  • 移动语义
示例:
class HasPtrMem{
	...
	HasPtrMem GetTemp() { return HasPtrMem();}
}
int main()
{
	HasPtrMem a = GetTemp();
}

当类HasPtrMem包含一个成员函数GetTemp,其返回值类型是HasPtrMem,如果我们定义了深拷贝的拷贝构造函数,那么在调用该函数时需要调用两次拷贝构造函数。第一次是生成GetTemp函数返回时的临时变量,第二次是将该返回值赋值给main函数中的变量a。与此对应需要调用三次析构函数来释放内存。
使用临时变量构造a时会调用拷贝构造函数分配对内存,而临时对象在语句结束后会释放它所使用的堆内存。这样重复申请和释放内存,在申请内存较大时会严重影响性能。因此C++使用移动构造函数,从而保证使用临时对象构造a时不分配内存,从而提高性能。
如下列代码所示,移动构造函数接收一个右值引用作为参数,使用右值引用的参数初始化其指针成员变量(int * d)。

HasPtrMem(HasPtrMem && h):d(h.d){ //移动构造函数
	h.d = nullptr;	//将临时值的指针成员置空
	cout<<"Move construct: "<< ++ n_mvtr << endl;
}

其原理就是使用在构造对象a时,使用h.d来初始化a,然后将临时对象h的成员变量d指向nullptr,从而保证临时变量析构时不会释放对内存。

9、static静态成员变量和成员函数

  • static成员变量实现了同类对象间信息共享,
  • static成员在类外存储,求类的大小时并不包含static成员。static成员是命名空间属于类的全局变量,存储在data区;
  • static成员只能类外初始化;可以通过类名访问(无对象时亦可)
  • static成员函数只能返回static成员变量(因为static成员函数属于整个类,没有this指针。而普通成员变量一定是属于某个对象的,如果返回普通成员变量就一定会隐含使用this指针。)

全局对象的构造函数会在main函数之前执行。

10、编译过程

1)预处理
主要处理源代码文件中的以“#”开头的预编译指令。处理规则见下
1、删除所有的#define,展开所有的宏定义。
2、处理所有的条件预编译指令,如“#if”、“#endif”、“#ifdef”、“#elif”和“#else”。
3、处理“#include”预编译指令,将文件内容替换到它的位置,这个过程是递归进行的,文件中包含其他文件。
4、删除所有的注释,“//”和“/**/”。
5、保留所有的#pragma 编译器指令,编译器需要用到他们,如:#pragma once 是为了防止有文件被重复引用。
6、添加行号和文件标识,便于编译时编译器产生调试用的行号信息,和编译时产生编译错误或警告是能够显示行号。
2)编译
把预编译之后生成的xxx.i或xxx.ii文件,进行一系列词法分析、语法分析、语义分析及优化后,生成相应的汇编代码文件。
1、词法分析:利用类似于“有限状态机”的算法,将源代码程序输入到扫描机中,将其中的字符序列分割成一系列的记号。
2、语法分析:语法分析器对由扫描器产生的记号,进行语法分析,产生语法树。由语法分析器输出的语法树是一种以表达式为节点的树。
3、语义分析:语法分析器只是完成了对表达式语法层面的分析,语义分析器则对表达式是否有意义进行判断,其分析的语义是静态语义——在编译期能分期的语义,相对应的动态语义是在运行期才能确定的语义。
4、优化:源代码级别的一个优化过程。
5、目标代码生成:由代码生成器将中间代码转换成目标机器代码,生成一系列的代码序列——汇编语言表示。
6、目标代码优化:目标代码优化器对上述的目标机器代码进行优化:寻找合适的寻址方式、使用位移来替代乘法运算、删除多余的指令等。
3)汇编
将汇编代码转变成机器可以执行的指令(机器码文件)。 汇编器的汇编过程相对于编译器来说更简单,没有复杂的语法,也没有语义,更不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译过来,汇编过程有汇编器as完成。经汇编之后,产生目标文件(与可执行文件格式几乎一样)xxx.o(Windows下)、xxx.obj(Linux下)。
4)链接
将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。链接分为静态链接和动态链接:
1、静态链接:
函数和数据被编译进一个二进制文件。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件。
空间浪费:因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,会出现同一个目标文件都在内存存在多个副本;
更新困难:每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。
运行速度快:但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。
2、动态链接:
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
共享库:就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多分,副本,而是这多个程序在执行时共享同一份副本;
更新方便:更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。
性能损耗:因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。

11、 epoll怎么实现的

Linux epoll机制是通过红黑树和双向链表实现的。 首先通过epoll_create()系统调用,在内核中创建一个eventpoll类型的句柄,其中包括红黑树根节点和双向链表头节点。然后通过epoll_ctl()系统调用,向epoll对象的红黑树结构中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示失败。最后通过epoll_wait()系统调用判断双向链表是否为空,如果为空则阻塞。当文件描述符状态改变,fd上的回调函数被调用,该函数将fd加入到双向链表中,此时epoll_wait函数被唤醒,返回就绪好的事件。

12、 支持高并发网络I/O的编程

https://blog.csdn.net/wwb456/article/details/70649548
可用的I/O技术有同步I/O,非阻塞式同步I/O(也称反应式I/O),以及异步I/O。在高TCP并发的情形下,如果使用同步I/O,这会严重阻塞程序的运转,除非为每个TCP连接的I/O创建一个线程。但是,过多的线程又会因系统对线程的调度造成巨大开销。因此,在高TCP并发的情形下使用同步 I/O是不可取的,这时可以考虑使用非阻塞式同步I/O或异步I/O。非阻塞式同步I/O的技术包括使用select(),poll(),epoll等机制。异步I/O的技术就是使用AIO。

从I/O事件分派机制来看,使用select()是不合适的,因为它所支持的并发连接数有限(通常在1024个以内)。如果考虑性能,poll()也是不合适的,尽管它可以支持的较高的TCP并发数,但是由于其采用“轮询”机制,当并发数较高时,其运行效率相当低,并可能存在I/O事件分派不均,导致部分 TCP连接上的I/O出现“饥饿”现象。而如果使用epoll或AIO,则没有上述问题(早期Linux内核的AIO技术实现是通过在内核中为每个 I/O请求创建一个线程来实现的,这种实现机制在高并发TCP连接的情形下使用其实也有严重的性能问题。但在最新的Linux内核中,AIO的实现已经得到改进)。

综上所述,在开发支持高并发TCP连接的Linux应用程序时,应尽量使用epoll或AIO技术来实现并发的TCP连接上的I/O控制,这将为提升程序对高并发TCP连接的支持提供有效的I/O保证。

  • 并发服务器有三种设计模式
    1) 多进程。每个进程服务一个客户端。优势是有各自独立的地址空间,可靠性高,但进程调度开销大,无法资源共享,进程间通信机制复杂。
    2) 多线程。每个线程服务一个客户端。优势是开销小,通信机制简单,可共享内存。但共享地址空间,可靠性低,一个服务器出现问题时可能导致系统崩溃,同时全局共享可能带来竞争,共享资源需要互斥,对编程要求高。
    3) 单进程:占有的进程及线程资源少,通信机制简单。但监听服务器及各个子服务器揉和在一起,程序结构复杂不清晰,编程麻烦。

13、 强制类型转换

共有4种类型的强转const_cast , static_cast , dynamic_cast , reinterpret_cast;

1 static_cast(最常用)

  • 由于没有运行时类型检查来保证转换的安全性,所以这类型的强制转换有安全隐患。
  • 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。
  • 用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性需要开发者来维护。
  • static_cast不能转换掉原有类型的const、volatile、或者 __unaligned属性。(前两种可以使用const_cast 来去除)
  • 在c++ primer 中说道:c++ 的任何的隐式转换都是使用 static_cast 来实现。

2 dynamic_cast

  • 最特殊的一个,因为他涉及到面向对象的多态性和程序运行时的状态,也与编译器的属性设置有关.所以不能完全使用C语言的强制转换替代,它也是最常有用的,最不可缺少的一种强制转换。
  • dynami_cast 在程序运行时对类型转换对“运行期类型信息”(Runtime type information,RTTI)进行了检查。

3 reinterpret_cast

  • 用来处理无关类型转换的,通常为操作数的位模式提供较低层次的重新解释!但是他仅仅是重新解释了给出的对象的比特模型,并没有进行二进制的转换!
  • 他是用在任意的指针之间的转换,引用之间的转换,指针和足够大的int型之间的转换,整数到指针的转换.

4 const_cast

  • 常量指针被转化成非常量的指针,并且仍然指向原来的对象;
  • 常量引用被转换成非常量的引用,并且仍然指向原来的对象;
  • const_cast一般用于修改指针。如const char *p形式。

14、 const与define区别

  1. const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误。
  2. 有些集成化的调试工具可以对 const 常量进行调试,但是不能对宏常量进行调试。

15、数组和指针的区别

静态数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。指针可以随时指向任意类型的内存块。
(1)修改内容上的差别

char a[] = “hello”; 
a[0] = ‘X’; 
char *p = “world”; // 注意 p 指向常量字符串
p[0] = ‘X’; // 编译器不能发现该错误,运行时错误

(2) 用运算符 sizeof 可以计算出数组的容量(字节数)。sizeof§,p 为指针得到的是一个指针变量的字节数,而不是 p 所指的内存容量。C++/C 语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。

char a[] = “hello world”; 
char *p = a; 
cout<< sizeof(a) << endl; // 12 字节
cout<< sizeof(p) << endl; // 4 字节
计算数组和指针的内存容量
void Func(char a[100]) 
{ 
	cout<< sizeof(a) << endl; // 4 字节而不是 100 字节
}

16、内存泄漏,野指针

野指针

  • 指针定义时未被初始化:会指向随机区域,默认值是随机的。
  • 指针释放时没有被置空
  • 指针操作超越变量作用域:不要返回栈内存的指针和引用(局部临时变量),因为栈内存在函数结束的时候会被释放。

危害:指针指向的内存以及无效了,而指针没有被置空,解引用一个非空的无效指针的未定义的行为,不知道何时失效,也不好查找错误原因。

内存泄漏

未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

主要关注两种类型的内存泄漏:

  • 堆内存泄漏(Heap leak):通过malloc, new等从堆中分配的一块内存,必须通过调用相应的 free或者delete 删掉。假设程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak。
  • 系统资源泄露(Resource Leak):主要指程序使用系统分配的资源(handle ,SOCKET)等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

常见的溢出主要有:

  • 内存分配未成功,却使用了它。
    经常使用解决的方法是,在使用内存之前检查指针是否为NULL。假设指针p 是函数的參数,那么在函数的入口处用assert(p!=NULL)进行检查。假设是用malloc 或new 来申请内存,应该用if(p==NULL)或if(p!=NULL)进行防错处理。

  • 内存分配尽管成功,可是尚未初始化就引用它。

  • 内存分配成功而且已经初始化,但操作越过了内存的边界。
    比如在使用数组时常常发生下标“多1”或者“少1”的操作。特别是在for 循环语句中,循环次数非常easy搞错,导致数组操作越界。

  • 使用free 或delete 释放了内存后,没有将指针设置为NULL。导致产生“野指针”。

你可能感兴趣的:(C++)