使用隐式空闲链表实现简单的动态内存分配,动态内存分配器维护一个大块区域,也就是堆,处理动态的内存分配请求。
分配器将堆视为一组不同大小的块的集合来维护,每个块要么是已分配的,要么是空闲的。隐式空闲链表就是通过每个块的头部中存放的信息可以方便的定位到下一个块的位置。头部一般就是本块的大小及使用情况(分配或空闲)。返回给用户的区域并不包含控制信息。
当接收到一个内存分配请求时,从头开始遍历堆,找到一个空闲的满足大小要求的块,若有剩余,将剩余部分变成一个新的空闲块,更新相关块的控制信息。调整起始位置,返回给用户。
释放内存时,仅需把使用情况标记为空闲即可。当找不到满足请求的空闲块时,即就是合并空闲块问题。有两种策略,一个是立即合并,另一个是推迟合并。
推迟合并简单实现:
// 内部使用的合并空闲块函数
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
Malloc在申请内存时,其中当申请内存小于128K时,会使用系统函数brk在堆区中分配;
而当申请内存大于128K时,会使用系统函数mmap在映射区分配。
这么做的主要原因是:
32bit地址总线可寻址4G线性空间,每个进程都有各自独立的4G逻辑地址,其中0~3G是用户态空间,3~4G是内核空间,不同进程相同的逻辑地址会映射到不同的物理地址中。其逻辑地址其划分如下:
静态区域:
动态区域:
1)申请方式:
栈由系统自动分配和管理,堆由程序员手动分配和管理。
2)效率:
栈由系统分配,速度快,不会有内存碎片。
堆由程序员分配,速度较慢,可能由于操作不当产生内存碎片。
3)扩展方向
栈从高地址向低地址进行扩展,堆由低地址向高地址进行扩展。
4)程序局部变量是使用的栈空间,new/malloc动态申请的内存是堆空间,函数调用时会进行形参和返回值的压栈出栈,也是用的栈空间。
栈的效率高的原因:
栈是操作系统提供的数据结构,计算机底层对栈提供了一系列支持:分配专门的寄存器存储栈的地址,压栈和入栈有专门的指令执行;而堆是由C/C++函数库提供的,机制复杂,需要一些列分配内存、合并内存和释放内存的算法,因此效率较低。
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
网络字节序为大端(低地址存高位);主机字节序为小端存储(低地址存低位),所以需要进行字节序的转换;
首先看一下用到的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;
}
1建立套接字文件描述符,使用函数socket(),生成套接字文件描述符。
2设置服务器地址和侦听端口,初始化要绑定的网络地址结构。
3绑定侦听端口,使用bind()函数,将套接字文件描述符和一个地址类型变量进行绑定。
4接收客户端的数据,使用recvfrom()函数接收客户端的网络数据。
5向客户端发送数据,使用sendto()函数向服务器主机发送数据。
6关闭套接字,使用close()函数释放资源。UDP协议的客户端流程
2、客户端流程
1建立套接字文件描述符,socket()。
2设置服务器地址和端口,struct sockaddr。
3向服务器发送数据,sendto()。
4接收服务器的数据,recvfrom()。
5关闭套接字,close()。
确实可以给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。
阻塞模式:执行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的个数灵活的决定线程个数,而且可以让减少线程调度的次数从而提高性能。
在单线程模型中,可以采用I/O复用来提高单线程处理多个请求的能力,然后再采用事件驱动模型,基于异步回调来处理事件。
IO多路复用就是我们说的select,poll,epoll。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。
select:是最初解决IO阻塞问题的方法。用结构体fd_set来告诉内核监听多个文件描述符,该结构体被称为描述符集。由数组来维持哪些描述符被置位了。对结构体的操作封装在三个宏定义中。通过轮寻来查找是否有描述符要被处理。
存在的问题:
poll:通过一个可变长度的数组解决了select文件描述符受限的问题。数组中元素是结构体,该结构体保存描述符的信息,每增加一个文件描述符就向数组中加入一个结构体,结构体只需要拷贝一次到内核态。poll解决了select重复初始化的问题。轮寻排查的问题未解决。
epoll:轮寻排查所有文件描述符的效率不高,使服务器并发能力受限。因此,epoll采用只返回状态发生变化的文件描述符,便解决了轮寻的瓶颈。
epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
LT模式与ET模式的区别如下:
示例:
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,从而保证临时变量析构时不会释放对内存。
全局对象的构造函数会在main函数之前执行。
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、动态链接:
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
共享库:就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多分,副本,而是这多个程序在执行时共享同一份副本;
更新方便:更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。
性能损耗:因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。
Linux epoll机制是通过红黑树和双向链表实现的。 首先通过epoll_create()系统调用,在内核中创建一个eventpoll类型的句柄,其中包括红黑树根节点和双向链表头节点。然后通过epoll_ctl()系统调用,向epoll对象的红黑树结构中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示失败。最后通过epoll_wait()系统调用判断双向链表是否为空,如果为空则阻塞。当文件描述符状态改变,fd上的回调函数被调用,该函数将fd加入到双向链表中,此时epoll_wait函数被唤醒,返回就绪好的事件。
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保证。
共有4种类型的强转const_cast , static_cast , dynamic_cast , reinterpret_cast;
静态数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。指针可以随时指向任意类型的内存块。
(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 字节
}
危害:指针指向的内存以及无效了,而指针没有被置空,解引用一个非空的无效指针的未定义的行为,不知道何时失效,也不好查找错误原因。
未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
主要关注两种类型的内存泄漏:
常见的溢出主要有:
内存分配未成功,却使用了它。
经常使用解决的方法是,在使用内存之前检查指针是否为NULL。假设指针p 是函数的參数,那么在函数的入口处用assert(p!=NULL)进行检查。假设是用malloc 或new 来申请内存,应该用if(p==NULL)或if(p!=NULL)进行防错处理。
内存分配尽管成功,可是尚未初始化就引用它。
内存分配成功而且已经初始化,但操作越过了内存的边界。
比如在使用数组时常常发生下标“多1”或者“少1”的操作。特别是在for 循环语句中,循环次数非常easy搞错,导致数组操作越界。
使用free 或delete 释放了内存后,没有将指针设置为NULL。导致产生“野指针”。