——gets()与sanf() ,输入字符串区别
gets()函数用来从标准输入设备(键盘)读取字符串直到回车结束, 但回车符
不属于这个字符串。其调用格式为:
gets(s);
其中s为字符串变量(字符串数组名或字符串指针)。
gets(s)函数与scanf("%s", s)相似, 但不完全相同, 使用scanf("%s", s)
函数输入字符串时存在一个问题, 就是如果输入了空格会认为输入字符串结束,
空格后的字符将作为下一个输入项处理, 但gets() 函数将接收输入的整个字符
串直到回车为止。
——cin>>
cin输入时遇到空格会结束,所以一般用getline作为字符串的输入(含空格)。
该操作符是根据后面变量的类型读取数据。
输入结束条件 :遇到Enter、Space、Tab键。
对结束符的处理 :丢弃缓冲区中使得输入结束的结束符(Enter、Space、Tab)
——fopen()
功能:打开一个特定的文件,并把一个流和这个文件相关联
头文件:#include<stdio.h>
原型:
FILE*fopen(const char *path,const char*mode)
参数说明:
path:是一个字符串,包含欲打开的文件路径及文件名
mode:mode字符串则代表着流形态
参数mode:
"r" | read: 为输入操作打开文件,文件必须存在。 |
"w" | write: 为输出操作创建一个空文件,如果文件已存在,则将已有文件内容舍弃,按照空文件对待。 |
"a" | append: 为输出打开文件,输出操作总是再文件末尾追加数据,如果文件不存在,创建新文件。 |
"r+" | read/update: 为更新打开文件(输入和输出),文件必须存在 |
"w+" | write/update: 为输入和输出创建一个空文件,如果文件已存在,则将已有文件内容舍弃,按照空文件对待。 |
"a+" | append/update: 为输出打开文件,输出操作总是再文件末尾追加数据,如果文件不存在,创建新文件。 |
表中指定的模式都是以文本的方式打开文件,如果要以二进制形式打开,需要在模式中加上“b”,既可以在模式字符串的末尾(如"rb+"),也可以在两个字符中间(如"r+b")。
返回值
成功:它返回一个指向FILE结构的指针,该结构代表这个新创建的流
(文件顺利打开后,指向该流的文件指针就会被返回)
失败:它就会返回一个空指针,errno会提示问题的性质
(如果文件打开失败,则返回NULL,并把错误代码存在errno中)
例子:
1 /* fopen example */ 2 #include <stdio.h> 3 int main () 4 { 5 FILE * pFile; 6 pFile = fopen ("myfile.txt","w"); 7 if (pFile!=NULL) 8 { 9 fputs ("fopen example",pFile); 10 fclose (pFile); 11 } 12 return 0; 13 }
ps:应始终检查fopen函数的返回值
FILE*input; input = fopen("data3", "r"); if (NULL == input) { perror("data3"); exit(EXIT_FAILURE); }
——fclose()
功能:关闭一个流
头文件:#include<stdio.h>
原型:int fclose(FILE*f);
返回值:对于输出流,fclose函数会在文件关闭前刷新缓冲区,
如果它执行成功,fclose返回0,否则返回EOF(-1)。(如果流为NULL,而且程序可以继续执行,fclose设定error number给EINVAL,并返回EOF。)
注意:使用fclose函数就可以把缓冲区内最后剩余的数据输出到内核缓冲区,并释放文件指针和有关的缓冲区
代码实例:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>//exit函数的头文件
int main()
{
FILE*fp=fopen("myfile","w+");
if(NULL==fp)
{
perror("fopen");
exit(1);
}
const char*msg="hello\n";
int count = 5;
while(count--)
{
fwrite(msg,1,strlen(msg),fp);
}
fclose(fp);//fopen之后记得fclose
return 0;
}
运行结果:
可在fclose(fp)后使用
if(fclose()) { perror("fclose"); }
来判断是否成功关闭文件,关闭失败,则fclose返回“1”并输出出错原因。
——fock()
在Linux 中,创建一个新进程的唯一方法是有某个已存在的进程调用fork()函数,被创建的新进程称为子进程,已存在的进程称为父进程.
1.fork()函数
fork()函数的实质是一个系统调用(和write函数类似),其作用是创建一个新的进程,当一个进程调用它,完成后就出现两个几乎一模一样的进程,其中由fork()创建的新进程被称为子进程,而原来的进程称为父进程.子进程是父进程的一个拷贝,即子进程从父进程得到了数据段和堆栈的拷贝,这些需要分配新的内存;而对于只读的代码段,通常使用共享内存方式进行访问.
1.fork()函数
fork()函数的实质是一个系统调用(和write函数类似),其作用是创建一个新的进程,当一个进程调用它,完成后就出现两个几乎一模一样的进程,其中由fork()创建的新进程被称为子进程,而原来的进程称为父进程.子进程是父进程的一个拷贝,即子进程从父进程得到了数据段和堆栈的拷贝,这些需要分配新的内存;而对于只读的代码段,通常使用共享内存方式进行访问.
/*这是一个调用fork()函数创建子进程的实例,当创建成功之后会分别打印两者对应的进程标志符*/ #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main(int argc,char *argv[]) { pid_t pid; //进程标识符 pid = fork(); //创建一个新的进程 if(pid<0) { printf("创建进程失败!"); exit(1); } else if(pid==0) //如果pid为0则表示当前执行的是子进程 printf("这是子进程,进程标识符是%d\n",getpid()); else //否则为父进程 printf("这是父进程,进程标识符是%d\n",getpid()); return 0; }我自己的运行结果:
这是父进程,进程标识符是3014
这是子进程,进程标识符是3015
/*这是一个调用fork()函数创建一个子进程,然后分别打印输出子进程和父进程中的变量的实例*/ #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <errno.h> int glob = 6; int main(int argc,char *argv[]) { int var; //内部变量 pid_t pid; //文件标识符 var = 88; //内部变量赋值 printf("创建新进程之前.\n"); //还没有创建子进程 if((pid=fork())<0) { perror("创建子进程失败!\n"); } else if(pid==0) { glob++; var++; } else { sleep(2); //父进程阻塞两秒 } printf("进程标识符为=%d,glob=%d,var=%d\n",getpid(),glob,var);//分别在子进程中输出两个变量的值 exit(0); }运行结果:
创建新进程之前.
进程标识符为=3070,glob=7,var=89 //等待2秒
进程标识符为=3069,glob=6,var=88
fork()子进程只进行fork后面的语句,复制前面的父进程的数据,但不执行语句
——write()和read()
~write( )
write函数是C语言函数。头文件为 <unistd.h>
write有两种用法。
1)ssize_twrite(int handle, void *buf, int nbyte);
1)ssize_twrite(int handle, void *buf, int nbyte);
- handle 是文件描述符;
- buf 是指定的缓冲区,即指针,指向一段内存单元;
- nbyte 是要写入文件指定的字节数;返回值:写入文档的字节数(成功);-1(出错)
2)write(const char* str,int n)
- str是字符指针或字符数组,用来存放一个字符串。
- n是int型数,它用来表示输出显示字符串中字符的个数。
write("string",strlen("string");表示输出字符串常量
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <sys\stat.h> #include <io.h> #include <string.h> int main(void) { int *handle; char string[40]; int length, res; /* Create a file named "TEST.$$$" in the current directory and write a string to it. If "TEST.$$$" already exists, it will be overwritten. */ if ((handle = open("TEST.$$$", O_WRONLY | O_CREAT | O_TRUNC, S_IREAD | S_IWRITE)) == -1) { printf("Error opening file.\n"); exit(1); } strcpy(string, "Hello, world!\n"); length = strlen(string); if ((res = write(handle, string, length)) != length) { printf("Error writing to the file.\n"); exit(1); } printf("Wrote %d bytes to the file.\n", res); close(handle); return 0; }😫对于普通文件,写操作始于 cfo 。如果打开文件时使用了 O_APPEND,则每次写操作都将数据写入文件末尾。成功写入后,cfo 增加,增量为实际写入的字节数。
~read( )
read函数是C语言函数。头文件为 <unistd.h>
ssize_t read(int filedes, void *buf, size_t nbytes);
read 函数从 filedes 指定的已打开文件中读取 nbytes 字节到 buf 中。
以下几种情况会导致读取到的字节数小于 nbytes :
- 读取普通文件时,读到文件末尾还不够 nbytes 字节。例如:如果文件只有 30 字节,而我们想读取 100 字节,那么实际读到的只有 30 字节,read 函数返回 30 。此时再使用 read 函数作用于这个文件会导致 read 返回 0 。
- 从终端设备(terminal device)读取时,一般情况下每次只能读取一行。
- 从网络读取时,网络缓存可能导致读取的字节数小于 nbytes 字节。
- 读取 pipe 或者 FIFO 时,pipe 或 FIFO 里的字节数可能小于 nbytes 。
- 从面向记录(record-oriented)的设备读取时,某些面向记录的设备(如磁带)每次最多只能返回一个记录。
- 在读取了部分数据时被信号中断。
——malloc和free()
~基本概念及用法:
函数原型及说明:
void *malloc(long NumBytes)该函数分配了NumBytes个字节,并返回了指向这块内存的指针。如果分配失败,则返回一个空指针(NULL)。
关于分配失败的原因,应该有多种,比如说空间不足就是一种。
void free(void *FirstByte)该函数是将之前用malloc分配的空间还给程序或者是操作系统,也就是释放了这块内存。
函数的用法:
用起来就是malloc()之后 内存觉得用够了就把它给free()了
// Code... char *Ptr = NULL; Ptr = (char *)malloc(100 * sizeof(char)); if (NULL == Ptr) { exit (1); } gets(Ptr); // code... free(Ptr); Ptr = NULL; // code...当然,具体情况要具体分析以及具体解决。比如说,你定义了一个指针,在一个函数里申请了一块内存然后通过函数返回传递给这个指针,那么也许释放这块内存这项工作就应该留给其他函数了。
需要注意的一些地方:
- 申请了内存空间后,必须检查是否分配成功。
- 当不需要再使用申请的内存时,记得释放;释放后应该把指向这块内存的指针指向NULL,防止程序后面不小心使用了它。
- 这两个函数应该是配对。如果申请后不释放就是内存泄露;如果无故释放那就是什么也没有做。释放只能一次,如果释放两次及两次以上会出现错误(释放空指针例外,释放空指针其实也等于啥也没做,所以释放空指针释放多少次都没有问题)。
- 虽然malloc()函数的类型是(void *),任何类型的指针都可以转换成(void *),但是最好还是在前面进行强制类型转换,因为这样可以躲过一些编译器的检查。
~malloc()以及free()的机制
malloc()到底从哪里得到了内存空间?
从堆里面获得空间。也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。
使用malloc()分配内存空间后,一定要记得释放内存空间,否则就会出现内存泄漏!!!
free()到底释放了什么?
free()释放的是指针指向的内存!注意!释放的是内存,不是指针!指针并没有被释放,指针仍然指向原来的存储空间。指针是一个变量,只有程序结束时才被销毁。释放了内存空间后,原来指向这块空间的指针还是存在!只不过现在指针指向的内容的垃圾,是未定义的,所以说是垃圾。因此,释放内存后把指针指向NULL,防止指针在后面不小心又被解引用了。
事实上,仔细看一下free()的函数原型,也许也会发现似乎很神奇,free()函数非常简单,只有一个参数,只要把指向申请空间的指针传递给free()中的参数就可以完成释放工作!这里要追踪到malloc()的申请问题了。😮申请的时候实际上占用的内存要比申请的大。因为超出的空间是用来记录对这块内存的管理信息。
大多数实现所分配的存储空间比所要求的要稍大一些,额外的空间用来记录管理信息——分配块的长度,指向下一个分配块的指针等等。这就意味着如果写过一个已分配区的尾端,则会改写后一块的管理信息。这种类型的错误是灾难性的,但是因为这种错误不会很快就暴露出来,所以也就很难发现。将指向分配块的指针向后移动也可能会改写本块的管理信息。
malloc()申请的空间实际就是分了两个不同性质的空间。一个就是用来记录管理信息的空间,另外一个就是可用空间了。而用来记录管理信息的实际上是一个 结构体。在C语言中,经常用结构来记录信息!
下面看看这个结构体的原型:
struct mem_control_block { int is_available; //一般来说应该是一个可用空间的首地址,但这里英文单词却显示出空间是否可用的一个标记 int size; //这是实际空间的大小 };free()就是根据这个结构体的信息来释放malloc()申请的空间!
下面看看free()的源代码:
void free(void *ptr) { struct mem_control_block *free; free = ptr - sizeof(struct mem_control_block); free->is_available = 1; Return 0; }
这里is_available应该只是一个标记而已!这个变量的值是1,表明是可以用的空间!
当然,这里可能还是有人会有疑问,为什么这样就可以释放呢??后来我想到,释放是操作系统的事,那么就free()这个源代码来看,什么也没有释放,对吧?但是它确实是确定了管理信息的那块内存的内容。所以,free()只是记录了一些信息,然后告诉操作系统那块内存可以去释放。
那么,我之前有个错误的认识,就是认为指向那块内存的指针不管移到那块内存中的哪个位置都可以释放那块内存!但是,这是大错特错!释放是不可以释放一部分的!首先这点应该要明白。而且,从free()的源代码看,ptr只能指向可用空间的首地址,不然,加上结构体大小之后一定不是指向管理信息空间的首地址。所以,要确保指针指向可用空间的首地址!
——new()和delete()
~运行机制
new/delete 简介
new 和 delete 是 C++ 用于管理 堆内存 的两个运算符,对应于 C 语言中的 malloc 和 free,但是 malloc 和 free 是函数,new 和 delete 是运算符。除此之外,new 在申请内存的同时,还会调用对象的构造函数,而 malloc 只会申请内存;同样,delete 在释放内存之前,会调用对象的析构函数,而 free 只会释放内存。
new 运算符的内部实现分为两步:
- 内存分配
调用相应的 operator new(size_t) 函数,动态分配内存。如果 operator new(size_t) 不能成功获得内存,则调用 new_handler() 函数用于处理new失败问题。如果没有设置 new_handler() 函数或者 new_handler() 未能分配足够内存,则抛出 std::bad_alloc 异常。“new运算符”所调用的 operator new(size_t) 函数,按照C++的名字查找规则,首先做依赖于实参的名字查找(即ADL规则),在要申请内存的数据类型T的 内部(成员函数)、数据类型T定义处的命名空间查找;如果没有查找到,则直接调用全局的 ::operator new(size_t) 函数。
- 构造函数
在分配到的动态内存块上 初始化 相应类型的对象(构造函数)并返回其首地址。如果调用构造函数初始化对象时抛出异常,则自动调用 operator delete(void*, void*) 函数释放已经分配到的内存。
delete 运算符的内部实现分为两步:
- 析构函数
调用相应类型的析构函数,处理类内部可能涉及的资源释放。
- 内存释放
调用相应的 operator delete(void *) 函数。 调用顺序参考上述 operator new(size_t) 函数(ADL规则)。
关于 new/delete 的内部实现,参考如下代码。
class T{ public: T(){ cout << "构造函数。" << endl; } ~T(){ cout << "析构函数。" << endl; } void * operator new(size_t sz){ T * t = (T*)malloc(sizeof(T)); cout << "内存分配。" << endl; return t; } void operator delete(void *p){ free(p); cout << "内存释放。" << endl; return; } }; int main() { T * t = new T(); // 先 内存分配 ,再 构造函数 delete t; // 先 析构函数, 再 内存释放 return 0; }结果如下:
每个 new 获取的对象,必须用 delete 析构并释放内存,以免 内存泄漏。
举例说明:
class Test{ public: Test(){ str = new char[2]; } ~Test(){ delete [] str; } private: char * str; }; int main(){ // ① Test * t = new Test; free(t); // ② Test * t2 = (Test*)malloc(sizeof(Test)); delete t2; return 0; }(1)对于 ①,new Test 的时候将会产生两方面的内存:
- Test 对象本身的内存( Win32环境,4 Bytes 存储 char * 指针);
- str 所指向的 2 bytes 堆内存。
如果调用 free 释放内存,那么由于 free 并不会调用 Test 的析构函数,所以 free 只能释放 Test 对象的内存(4 bytes),而 str 所指向的 2-bytes 堆内存并不能得到释放,因此而造成 内存泄漏 。
(2)对于 ②,malloc 并不会调用类的构造函数,所以只分配了 Test 对象的内存,str 并未初始化为指向一块堆内存。所以当调用 delete 释放内存的时候,将调用类的析构函数 ( delete [] str ),此时 delete 一块没有使用权的内存,程序崩溃 。
总之,编写C++程序时,在进行动态内存分配的时候,最好使用 new 和 delete。并且记住,new 出来的对象用 delete “消灭”它。
~new表达式语法
1)普通的 new 运算符表达式
new 的基本语法 :
type * p_var = new typeint * a = new int; // 分配内存,但未初始化,垃圾值
通过new初始化对象,使用下述语法:
type * p_var = new type( init )int * a = new int(8) //分配内存时,将 *a 初始化为 8
其中 init 是传递给构造函数的实参表或初值。
2)动态生成对象数组的 new 运算符表达式
new 也可创建一个对象数组:
type p_var = new type [size]int * a = new int[3] // 分配了 3个 int 大小的连续内存块, 但未初始化
C++98 标准规定,new 创建的对象数组不能被显式初始化, 数组所有元素被缺省初始化。如果数组元素类型没有缺省初始化(默认构造函数),则编译报错。但 C++11 已经允许显式初始化,例如:
int *p_int = new int[3] {1,2,3}
如此生成的对象数组,在释放时必须调用 delete [] 表达式。
~placement new 运算符表达式
placement new 运算符表达式 就是 在用户指定的内存位置上构建新的对象 ,这个构建过程并不需要额外分配内存,只需要调用对象的构造函数即可。
placement new 的语法是:
new ( expression-list ) new-type-id ( optional-initializer-expression-list );
使用这种 placement new 运算符表达式,原因之一是 用户的程序不能在一块内存上自行调用其构造函数,必须由编译系统生成的代码调用构造函数。原因之二 是可能需要把对象放在特定硬件的内存地址上,或者放在多处理器内核的共享的内存地址上。(PS:构造函数没办法直接这么调用 p->A(),而析构函数可以直接这么调用 p->~A()。)
释放这种 placement new 运算符对象时,不能调用 placement delete,应直接调用析构函数,如:pObj->~ClassType() ; 然后再自行释放内存。
注意: C++ 中并没用与 placement new 运算符 功能相对应 的 placement delete 运算符(没有placement delete 运算符的概念,但是有 placement delete 函数)。
解释:
<1> 首先看看 C++ 设计者,大牛 - Bjarne Stroustrup 的说法 Is there a “placement delete”?
class Arena { public: void * allocate(size_t); void deallocate(void\*); .... }; void * operator new(size_t sz, Arena& a) { return a.allocate(sz); } Arena a1(some arguments); Arena a2(some arguments); X* p1 = new(a1) X; Y* p2 = new(a1) Y; Z* p3 = new(a2) Z;对于上述代码,C++的类型机制并不能推断 p1 指向的对象是否位于 a1 之上。那么直接调用 delete(a1) p1; 就容易出错。所以为了安全,C++不提供 placement delete 运算符。
<2> placement new 运算符不另外分配内存,换句话说,不是new运算符。它完成的功能是在给定地址上调用构造函数。如果提供p->T(),那么 placement new 运算符就不需要了。如果存在功能对应的 placement delete 运算符,那么功能就应该是在给定地址上调用析构函数。但因为 C++已经提供了p->~T(),就没必要有 placement delete 运算符
<3> 如果存在对应的 placement delete 运算符,其实就是调用析构函数。而本身析构函数就可以自行主动调用,那么自己调用就好了,但是对象本身所占用这块内存还可以继续使用。如果想 placement delete 运算符像打洞一样,连对象内存一起回收,那 operator new(size_t ) 的大块蜂窝煤内存如何 delete 。这不科学,既然整块内存是 operator new(size_t) 的,就应该由 operator delete(void *) 回收,而不能用 placement delete 运算符部分回收。
<4> 总之,没有与 placement new 运算符功能相对应的 placement delete 运算符。而且需要注意的是,运算符和函数是两个不同的概念,C++有 placement new 运算符和函数的概念,但是没有 placement delete 运算符的概念,有 placement delete 函数的概念 。
所以,对于 placement new 运算符,我们需要主动调用对象的析构函数。如下示例:
#include <iostream> using namespace std; class Test{ public: Test(){ cout << "Test 构造" << endl; str = new char[2]; } ~Test(){ cout << "Test 析构" << endl; delete [] str; } private: char * str; }; int main(int argc, char* argv[]) { char buf[100]; // 栈变量 Test *p = new(buf) Test(); // Test()产生的临时变量用于初始化 指定内存地址 p->~Test(); // 一定要主动调用析构函数,避免内存泄漏。 而且调用必须在 buf 生命周期内调用才有效。 // buf 指向的栈内存并不需要程序员主动释放。 // 栈变量过了生命周期会自动释放内存 // 其实栈内存的释放也不叫内存释放,只是栈顶指针移动,如果该块栈内存没有被其他程序刷新,那么该栈内存的值依然不变。 char * buf2 = new char[100]; Test * p2 = new(buf2) Test(); p2->~Test(); // 切记,主动调用析构函数 delete [] buf2; // 堆内存需要主动释放 return 0; }如上代码,如果把 p->~Test(); 注释掉,上述代码的结果将为:
Test 构造显然是只调用构造函数。所以对于placement new,我们需要主动调用对象的析构函数 pObj->~ClassType()。
new 是用于管理堆内存,那又怎么可能在 栈 上new出一个对象呢?
通过上面的讨论,我们发现,new 除了能用于动态分配内存,还能够使用 placement new 在特定内存位置进行初始化。所以,如何在栈上 new 呢?上述代码(2.1.2)就是一个很好的例子。
在分配内存失败时,new运算符的标准行为是抛出std::bad_alloc异常。也可以让new运算符在分配内存失败时 不抛出异常而是返回空指针。
new (nothrow) Type ( optional-initializer-expression-list );
new (nothrow) Type[size]; // new (std::nothrow_t) Type[size];其中 nothrow 是 std::nothrow_t 的一个实例.
~delete 表达式语法
1)普通的 delete 运算符
delete val_ptr;
2)释放对象数组的 delete 运算符
delete [] val_ptr
没有 placement delete 运算符表达式 !!!
通过上面的讨论,我们可以知道 C++ 中并没有提供与 placement new 运算符功能相对应 placement delete 运算符。但是仍然有placement delete函数的概念,功能在后面有介绍。
C++ 不能使用 placement delete 运算符表达式直接析构一个对象但不释放其内存。因此,对于placement new表达式构建的对象,析构释放时有两种办法:
<1> 是直接写一个函数,完成析构对象、释放内存的操作:
void destroy (T * p, A & arena) { // *p 是在 arena 之上构建的对象,即 T * a = new(&arena) T; p->~T() ; // 先析构 *p 对象 arena.deallocate(p) ; // 再释放 arena 整个内存,而不是位于arena中的部分内存(*p) } A arena ; T * p = new (arena) T ; .... destroy(p, arena) ;<2> 分两步显式 调用析构函数 与 带位置的 operator delete 函数:
A arena ; T * p = new (arena) T ; /* ... */ p->~T() ; // 先析构 operator delete(p, arena) ; // 调用 placement delete 函数(非运算符) // Then call the deallocator function indirectly via operator delete(void *, A &) .带位置的 operator delete(void *,void *) 函数,可以被 placement new 运算符表达式自动调用。这是在对象的构造函数抛出异常的时候,用来释放掉 placement new 函数获取的内存(类内部可能涉及的内存分配)。以避免内存泄露。
#include <cstdlib> #include <iostream> char buf[100]; struct A {} ; struct E {} ; class T { public: T() { std::cout << "T 构造函数。" << std::endl; throw E(); //抛出异常 } void * operator new(std::size_t,const A &) { std::cout << "Placement new called for class T." << std::endl; return buf; } void operator delete(void*, const A &) { std::cout << "Placement delete called for class T." << std::endl; } } ; void * operator new ( std::size_t, const A & ) { std::cout << "Placement new called." << std::endl; return buf; } void operator delete ( void *, const A & ) { std::cout << "Placement delete called." << std::endl; } int main () { A a ; try { T * p = new (a) T ; /* do something */ } catch (E exp) { std::cout << "Exception caught." << std::endl; } return 0 ; }结果如下:
C++ 有 placement delete 函数,但是没有 placement delete 运算符的概念。
~delete 类对象时该注意的问题
如下一段代码,是否是产生内存泄漏?class A { public: A(){} virtual void f(){} private: int m_a; }; class B : public A { public: virtual void f(){} private: int m_b; }; int main() { A *pa = new B; delete pa; pa = NULL; return 0; }答案:不会产生内存泄漏。
delete 释放内存时,会调用类的析构函数。 但是需要明确的是 析构函数并不会释放 对象本身 的内存 。
delete 运算符分为2个阶段。 第一个阶段是调用类的析构函数,第二阶段才是释放对象内存(但是这个工作不是析构函数在做)。
析构函数是free()之前的调用,而真正释放内存的操作是 free(void *ptr),注意只有指针一个参数,没有长度参数,这说明了什么?说明了 A *pa = new B; 时带着长度sizeof(B)最终调用了malloc(sizeof(B));申请的内存及长度已经被记录,当free(pa)时就会释放掉自pa开始长度为sizeof(B)的内存。析构函数仅仅是应用逻辑层次的释放资源,不是物理层次的释放资源。
修改一下上面的题目,如下是否会造成内存泄漏呢?
class A { public: A(){ m_a = new int(1); } ~A(){ // 声明为virtual, 防止内存泄漏 delete m_a; } private: int * m_a; }; class B : public A { public: B() : A(){ m_b = new int(2); } ~B(){ delete m_b; } private: int * m_b; }; int main() { A * pa = new B; delete pa; pa = NULL; return 0; }答案:会造成内存泄漏。
delete pa 的时候,只会调用基类的析构函数。所以 m_b 指向的内存块没得到释放。造成内存泄漏。
通过这个例子,应该深刻理解 析构函数的作用: 程序员处理类内部可能涉及的内存分配、资源释放。而不是释放类本身的内存。
~operator new/delete() 的函数重载
平时使用 new 动态生成一个对象,实际上是调用了 new 运算符。
该运算符首先调用了operator new(std::size_t )函数动态分配内存,然后调用类型的构造函数初始化这块内存。 new / delete 运算符是不能被重载的,但是下述各种 operator new/delete()函数既可以作为 1. 全局函数重载,也可以作为 2. 类成员函数或 3. 作用域内的函数重载,即由编程者指定如何获取内存。
(1) 普通的operator new/delete(size_t size)函数:
new 运算符 首先调用 operator new(std::size_t ) 函数动态分配内存。首先查找 类内 是否有 operator new(std::size_t)函数可供使用(即依赖于实参的名字查找)。
- operator new(size_t )函数的参数是一个 size_t 类型,指明了需要分配内存的规模。
- operator new(size_t )函数可以被每个 C++ 类作为成员函数重载。也可以作为全局函数重载:
void * operator new (std::size_t) throw(std::bad_alloc); void operator delete(void*) throw();内存需要回收的话,调用对应的operator delete(void *)函数。
例如,在 new 运算符表达式的第二步,调用构造函数初始化内存时如果抛出异常,异常处理机制在栈展开(stack unwinding)时,要回收在new运算符表达式的第一步已经动态分配到的内存,这时就会 自动调用 对应 operator delete(void*) 函数。(注意:此处调用的是非位置delete函数)
struct E{}; class T{ public: T(){ cout << "构造函数。" << endl; throw E(); } ~T(){ cout << "析构函数。" << endl; } void * operator new(size_t sz){ T * t = (T*)malloc(sizeof(T)); cout << "内存分配。" << endl; return t; } void operator delete(void *p){ free(p); cout << "内存释放。" << endl; return; } }; int main() { try { T * p = new T; /* do something */ } catch (E exp){ std::cout << "Exception caught." << std::endl; } return 0; }结果:
(2) 数组形式的operator new/delete[](size_t size)函数:
new type[] 运算符,用来动态创建一个对象数组。这需要调用数组元素类型内部定义的void* operator new[ ](size_t)函数来分配内存。如果数组元素类型没有定义该函数,则调用全局的 void* operator new[ ](size_t )函数来分配内存。
在 #include <new> 中声明了 void* operator new[](size_t) 全局函数:
void * operator new [] (std::size_t) throw(std::bad_alloc); void operator delete [](void*) throw();
(3) placement new/delete 函数
void * operator new(size_t,void*) 函数用于带位置的 new 运算符调用。C++标准库已经提供了operator new(size_t,void*)函数的实现,包含 <new> 头文件即可。
这个实现只是简单的把参数的指定的地址返回,带位置的new运算符就会在 该地址上调用构造函数 来初始化对象:
// Default placement versions of operator new. inline void* operator new(std::size_t, void* __p) throw() { return __p; } inline void* operator new[](std::size_t, void* __p) throw() { return __p; } // Default placement versions of operator delete. inline void operator delete (void*, void*) throw() { } inline void operator delete[](void*, void*) throw() { }禁止重定义这4个函数。因为都已经作为 <new> 的内联函数了。在使用时,实际上不需要#include <new>
虽然上面的4个 placement new/delete 函数不能重载,但是仍然可以写一个自己的 placement new/delete 函数,例如:
inline void* operator new(std::size_t, A * /* 或者 const A &*/); inline void* operator new[](std::size_t, A * /* 或者 const A &*/); inline void operator delete (void*, A* /* 或者 const A &*/); inline void operator delete[](void*, A* /* 或者 const A &*/);但是,基本没有什么意义。。
(4) 保证不抛出异常的operator new/delete函数:
C++标准库的<new> 中还提供了一个nothrow的实现,用户可写自己的函数替代:
void* operator new(std::size_t, const std::nothrow_t&) throw(); void* operator new[](std::size_t, const std::nothrow_t&) throw(); void operator delete(void*, const std::nothrow_t&) throw(); void operator delete[](void*, const std::nothrow_t&) throw();
(5) Clang关于operator new/delete 的实现
以下这段代码是Clang编译器关于operator new(std::size_t)和 operator delete (void *) 的实现:
void * operator new(std::size_t size) throw(std::bad_alloc) { if (size == 0) size = 1; void* p; while ((p = ::malloc(size)) == 0) { std::new_handler nh = std::get_new_handler(); if (nh) nh(); else throw std::bad_alloc(); } return p; } void operator delete(void* ptr) { if (ptr) ::free(ptr); }这段代码很简单,神秘的 operator new/delete 在背后也不过是在偷偷地调用C函数库的 malloc / free !当然,这跟具体实现有关,Clang libcxx 是这样实现,不代表其它实现也是如此。
需要意识到的是, operator new 和 operator + () 一样,只不过是普通的函数,是可以重载的,所谓的 placement new ,也是一个全局 operator new 的重载版本,在Clang libcxx 中定义如下:
inline _LIBCPP_INLINE_VISIBILITY void* operator new (std::size_t, void* __p) _NOEXCEPT { return __p; }
~小结
new 和 delete 是 C++ 用于管理 堆内存 的两个运算符。 new 运算符 进行动态内存申请的时候,包含 2 个阶段:
- 内存申请 new
- 构造函数。
delete 运算符 进行内存释放的时候,也包含 2 个阶段:
- 析构对象。
- 内存释放 delete。
本步骤对应operator delete(void*) 函数。
除了用于内存管理的 new/delete 运算符,还有带位置的 placement new 运算符,但是没有带位置的 placement delete 运算符。
placement new 运算符
- 解决不能主动调用构造函数的“矛盾”。
- 对应的函数是 operator new(size_t , void *)。
placement delete 运算符
- 没有此类运算符。
- 但有带位置的 placement delete 函数,如全局的 operator delete(void *,void*) 。
free/delete 怎么知道有多少内存要释放 ?
在使用c或者c++的时候我们经常用到malloc/free和new/delete,在使用malloc申请内存的时候我们给定了需要申请的内存大小,但是在free或者delete的时候并不需要提供这个大小,那么程序是怎么实现准确无误的释放内存的呢?
实际上,在申请内存的时候,申请到的地址会比你实际的地址大一点点,他包含了一个存有申请空间大小的结构体。
比如你申请了20byte的空间,实际上系统申请了48bytes的block
比如你申请了20byte的空间,实际上系统申请了48bytes的block
16-byte header containing size, special marker, checksum, pointers to next/previous block and so on.32 bytes data area (your 20 bytes padded out to a multiple of 16))
这样在 free的时候就不需要提供任何其他的信息,可以正确的释放内存。