C++ new 与 delete 的使用规范

2022-11-29 21:20:20 浏览数 (1)

文章目录

  • 1.new 与 delete 需一一对应
  • 2.new[] 与 delete[] 需一一对应
  • 3.构造函数中的 new/new[] 与析构函数的中 delete/delete[] 需一一对应
  • 参考文献

C 的动态内存管理是通过 new 和 delete 两个操作来完成的,即用 new 来申请空间,用 delete 来释放空间。在使用 new 和 delete 时,注意以下原则。

1.new 与 delete 需一一对应

用 new 操作申请空间,如果申请成功,必须在以后的某个时刻用 delete 释放该空间,既不能忘记释放,也不能多次释放。前者会引起内存泄露,后者会引起运行时错误。

如下面的程序:

代码语言:javascript复制
#include <iostream>
using namespace std;

int main() {
	int *p;
	p=new int(3);
	if (p) {
		delete p;
	}
	delete p;
	return 0;
}

以上程序对指针p所指向的空间进行两次释放,这种内存错误对 C 程序危害极大,也是很多人对 C 忘而却步的原因。多次释放同一块内存空间,并不一定立即引起程序运行错误,也不一定会导致程序运行的崩溃,这跟具体的编译器实现有关。但是,多次释放同一块内存空间绝对是一个编程错误,这个编程错误可能会在其后的某个时刻导致其他的逻辑错误的发生,从而给程序的调试和纠错带来困难。

考察如下程序:

代码语言:javascript复制
#include <iostream>
using namespace std;

int main() {
	int *p, *q, *one;
	one = new int;
	if(one) {
		cout<<one<<endl;
	}
	delete one;
	p=new int(3);
	if(p) {
		cout<<p<<endl;
	}
	delete one;//假设这句语句是程序员不小心加上的
	q=new int(5);
	if(q) {
		cout<<q<<endl;
	}
	cout<<(*p) (*q)<<endl;
	delete p;
	delete q;
}

程序通过编译,运行结果如下:

代码语言:javascript复制
003289A0
003289A0
003289A0
10

程序运行过程中会产生中断。从程序的输出可以看出,在将指针one所指向的空间释放后,为指针p申请的空间就是原来one所指向的空间。由于不小心在为p分配空间之后再次使用了delete one,导致q申请到的空间就是原来p所申请的空间,这样赋给*q的值就改写了原来p所指向的单元的值,导致最后输出结果为10。由此可知,多次释放同一块内存空间,即使不导致程序运行中断,也会破坏环境,使指针与所对应的空间的隶属关系出现混乱,从而导致逻辑错误。在大型程序设计中,这种逻辑错误的查找会变得十分费时费力。

**注意:**当指针 p 的值为 NULL 时,多次使用 delete p 并不会带来麻烦,因为释放空指针的空间实际上不会导致任何操作。所以,将“不用”的指针设置为 NULL 是一个好的编程习惯。

2.new[] 与 delete[] 需一一对应

在申请对象数组时,需要使用new[]运算符,与之对应,释放对象数组时,需要使用delete[]运算符。这一点与C语言有所区别,C中无论申请单个还是多个对象,均使用 malloc()/free() 函数。首先看一下 delete 与 delete[] 运算符的区别。

代码语言:javascript复制
class Test {
public:
	Test() { cout<<"ctor"<<endl; }
	~Test() { cout << "dtor" << endl; }
};

// segment1
Test* pArray1 = new Test[3];
delete pArray1;

// segment2
Test* pArray2 = new Test[3];
delete[] pArray2;

其中代码片段segment1运行结果如下:

代码语言:javascript复制
ctor
ctor
ctor
dtor

segment2运行结果如下:

代码语言:javascript复制
ctor
ctor
ctor
dtor
dtor
dtor

可以看出,delete 与 delete[] 区别在于释放对象数组时,delete 只调用了一次析构函数,delete[] 调用了三次析构函数,完成了对象数组的释放。实际上,在使用 new 和 new[] 申请内存空间时,会申请一段额外的内存来保存用户申请的内存空间大小,元素个数等信息。当使用delete[]释放内存空间时,会逐个调用对象的析构函数并完成最终的内存空间的释放。使用 delete 释放对象数组时,则只会调用单个对象的析构函数,造成内存泄漏。符号[]告诉编译器,在 delete 一块内存时,先去获取内存保存的元素个数,然后一一清理。所以使用 delete 释放 new[] 申请的内存空间和使用 delete[] 释放 new 申请的内存空间都是错误的做法。

具体使用时,需要注意以下两点: (1)对于内置数据类型,因为没有构造和析构函数,所以使用delete和delete[]的效果是一样的。比如:

代码语言:javascript复制
int* pDArr = new int[3];
//processing code
delete pDArr;	// 等同于 delete[] pDArr

对于内置数据类型,虽然可以使用delete完成对象数组内存空间的释放,但是为了保证代码的可读性,建议使用delete[]来完成。所以,new[] 与 delete[] 使用时应一一对应。

(2)对于经常使用 typedef 的程序员来说,很容易 new[] 与 delete 的混用,例如有如下操作:

代码语言:javascript复制
typedef int Height[NUM];
int* pHeight = new Height;

这个情况应该使用 delete 还是 delete[] 呢?答案如下:

代码语言:javascript复制
delete	pHeight;	// wrong,但容易错误地使用 delete。
delete[] pHeight;	// right。

为了避免出现上面的错误,建议不要对数组使用 typedef,或者采用 STL 中的 vector 代替数组。

3.构造函数中的 new/new[] 与析构函数的中 delete/delete[] 需一一对应

当类的成员中有指针变量时,在构造函数中用new申请空间并且在析构函数中用delete释放空间是一种标准的、安全的做法。

例如下面的程序:

代码语言:javascript复制
#include <iostream>
using namespace std;

class Student {
	char* name;
public:
	Student() {
		cout<<"Default constructor"<<endl;
	}
	Student(char*);
	~Student();
};

Student::Student(char*s) {
	// Student(); // 此句运行时报错,构造函数不能调用其他构造函数。
	cout<<"In constructor,allocating space"<<endl;
	name=new char[strlen(s) 1];
	strcpy(name,s);
	cout<<"name:"<<name<<endl;
}

Student::~Student() {
	cout<<"In destructor, free space"<<endl;
	delete name;
}

int main() {
	Student s1("张三");
}

程序运行输出:

代码语言:javascript复制
In constructor,allocating space
name:张三
In destructor, free space

由于任何一个对象,其构造函数只调用一次,其析构函数也只调用一次,这样就能保证运行时new和delete操作是一一对应的,也就保证了内存管理的安全性。

在 C 中,一个构造函数不能调用本类的另一个构造函数,其原因就是为了防止构造函数的相互调用打破了内存申请与释放之间的这种对应关系。


参考文献

C 高级进阶教程.陈刚.P260-264 编写高质量代码改善C 程序的150个建议.李健.P69-71

0 人点赞