常见的内存错误及其对策
发生内存错误是件非常麻烦的事情。编译器不能自动发现这些错误,通常是在程序运行时才能捕捉到。而这些错误大多没有明显的症状,时隐时现,增加了改错的难度。有时用户怒气冲冲地把你找来,程序却没有发生任何问题,你一走,错误又发作了。 常见的内存错误及其对策如下:
* 内存分配未成功,却使用了它。
编程新手常犯这种错误,因为他们没有意识到内存分配会不成功。常用解决办法是,在使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行
检查。如果是用malloc或new来申请内存,应该用if(p==NULL) 或if(p!=NULL)进行防错处理。
* 内存分配虽然成功,但是尚未初始化就引用它。
犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。 内存的缺省初值究竟是什么并没有统一的标准,尽管有些时候为零值,我们宁可信其无不可信其有。所以无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略,不要嫌麻烦。
* 内存分配成功并且已经初始化,但操作越过了内存的边界。
例如在使用数组时经常发生下标“多1”或者“少1”的操作。特别是在for循环语句中,循环次数很容易搞错,导致数组操作越界。
* 忘记了释放内存,造成内存泄露。
含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,你看不到错误。终有一次程序突然死掉,系统出现提示:内存耗尽。
动态内存的申请与释放必须配对,程序中malloc与free的使用次数一定要相同,否则肯定有错误(new/delete同理)。
* 释放了内存却继续使用它。
对策:
【规则1】用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。
【规则2】不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。
【规则3】避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。
【规则4】动态内存的申请与释放必须配对,防止内存泄漏。
【规则5】用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。
解释:
杜绝“野指针”
指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。例如
char *p = NULL; char *str = (char *) malloc(sizeof(char)*100); |
数组内存申请和释放:
void f() { int* p=new int[5]; }
释放内存:delete []p;
内容复制与比较
char a[] = "hello";
char b[10];
strcpy(b, a); // 不能用 b = a;
if(strcmp(b, a) == 0) // 不能用 if (b == a)
int len = strlen(a);
char *p = (char *)malloc(sizeof(char)*(len+1)); // 指针同样
strcpy(p,a); // 不要用 p = a;
if(strcmp(p, a) == 0) // 不要用 if (p == a)
…
new/delete的使用要点
运算符new使用起来要比函数malloc简单得多,例如:
int *p1 = (int *)malloc(sizeof(int) * length); int *p2 = new int[length]; |
这是因为new内置了sizeof、类型转换和类型安全检查功能。对于非内部数据类型的对象而言,new在创建动态对象的同时完成了初始化工作。如果对象有多个构造函数,那么new的语句也可以有多种形式。例如
class Obj { public : Obj(void); // 无参数的构造函数 Obj(int x); // 带一个参数的构造函数 … } void Test(void) { Obj *a = new Obj; Obj *b = new Obj(1); // 初值为1 … delete a; delete b; } |
如果用new创建对象数组,那么只能使用对象的无参数构造函数。例如:
Obj *objects = new Obj[100]; // 创建100个动态对象 |
不能写成:
Obj *objects = new Obj[100](1);// 创建100个动态对象的同时赋初值1 |
在用delete释放对象数组时,留意不要丢了符号‘[]’。例如:
delete []objects; // 正确的用法 delete objects; // 错误的用法 |
后者有可能引起程序崩溃和内存泄漏。
2,对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。
malloc 只管分配内存,并不能对所得的内存进行初始化,所以得到的一片新内存中,其值将是随机的。
new如果不提供显示初始化,会默认初始化。
String类的重载问题:
#include<iostream>
#include<assert.h>
using namespace std;
class String
{
public:
String(const char *str = NULL);// 普通构造函数
String(const String &other); // 拷贝构造函数
~String(void); // 析构函数
String & operator = (const String &other);// 赋值函数
friend ostream &operator <<(ostream &os,const String &str);
private:
char *m_data;// 用于保存字符串
};
//注意new之后最好都要assert(NULL != m_data);
String::String(const char *str){
if (NULL == str)
{
m_data = new char[1];//得分点:对空字符串自动申请存放结束标志'\0'的空
assert(NULL != m_data);//加分点:对m_data加NULL 判断
*m_data = '\0';
}
else
{
int len = strlen(str);
m_data = new char[len+1];
assert(NULL != m_data);// 若能加 NULL 判断则更好
strcpy(m_data, str);
}
}
String::String(const String &other){
int len = strlen(other.m_data);
m_data = new char[len+1];
assert(NULL != m_data);// 若能加 NULL 判断则更好
strcpy(m_data, other.m_data);
}
String::~String(){
if (m_data)
{
delete[] m_data;
m_data = NULL;
}
}
String& String::operator= (const String &other){
if (this == &other)//得分点:检查自赋值
{
return *this;
}
//得分点:释放原有的内存资源
if (m_data)
{
delete[] m_data;
}
int len = strlen(other.m_data);
m_data = new char[len+1];
assert(NULL != m_data);// 若能加 NULL 判断则更好
strcpy(m_data, other.m_data);
return *this;//得分点:返回本对象的引用
}
ostream &operator <<(ostream &os,const String &str)
{
os<<str.m_data;
return os;
}
int main(){
String a("Hello World.");
String b(a);
String c = b;
char* p = NULL;
String d(p);
cout<<"a: "<<a<<endl;
cout<<"b: "<<b<<endl;
cout<<"c: "<<c<<endl;
cout<<"d: "<<d<<endl;
}
数组的定义和初始化与vector的定义和初始化有些不同。
vector<int> ivec; // 初始状态为空
// 在此处给ivec添加一些值
vector<int> ivec2(ivec); // 把ivec的元素拷贝给 ivec2
vector<int> ivec3 = ivec; // 把ivec的元素拷贝给 ivec3
如果提供的是初始元素值的列表,则只能把初始值都放在花括号里进行列表初始化,而不能放在圆括号里:
vector<string> v1{"a", "an", "the"}; // 列表初始化
vector<string> v2("a", "an", "the"); // 错误
还可以用vector对象容纳的元素数量和所有元素的统一初始值来初始化vector对象:
vector<int> ivec(10, -1); // 10个int类型的元素,每个都被初始化为-1
vector<string> svec(10, "hi!"); // 10个string类型的元素,每个都初始化为”hi!”
vector<int> ivec(10); // 10个元素,每个都初始化为0
而数组
在C++中,利用花括号给数组初始化,如果花括号中给的初始值的个数不够,则会自动将未给出初始值的元素赋0。例如
array[8] = {1};
结果是第一位初始化为1,剩余的元素初始化为0。
动态分配(例如分配n个单元的): int *array=new int [n];
初始化:memset(array,0,n*sizeof(array)); (也可以利用一个for循环对其赋值初始化)
显示初始化数组,无需指定维数:
- string str_array[] = {"aa", "bb", "cc"};
与vector不一样,数组不允许直接复制和赋值
- int ia[] = {0, 1, 2}; // ok: array of ints
- int ia2[] = ia; // error: initializer fails to determine size of 'ia2'
如果不想手动delete,可以考虑智能指针:
共享资源的智能指针——shared_ptr
如下代码:
1
|
shared_ptr<
int
> pi;
//指向int
|
当然我们也可以shared_ptr和new来结合使用,但是必须使用直接初始化的形式来初始化一个智能指针,
1
2
|
shared_ptr<
int
> p1 =
new
int
(1024);//error:必须使用直接初始化的形式
shared_ptr<
int
> p2(
new
int
(1024));
|
但是最好不要混合使用普通指针和智能指针,最安全的分配和使用动态内存的方法是调用make_shared的标准库函数。在使用它的时候,必须指定想要创建的对象类型。
1
2
|
shared_ptr<
int
>pi = make_shared<
int
>(1);
//指向一个值为1的int的shared_ptr
shared_ptr<string>ps = make_shared<string>(10,
'a'
);
//ps为指向“aaaaaaaaaa”的string
|
如果我们不传递任何参数,对象会进行值初始化
1
|
shared_ptr<
int
>pi = make_shared<
int
>();
//初始化默认值为0;
|
shared_ptr 实现了引用计数型的智能指针,当进行拷贝的时候,计数器都会递增。而对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(减1,如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数(加1),如下代码:
1
2
3
4
|
auto p = make_shared<
int
>(13);
//p 指向的对象只有p一个引用者
auto q(p);
//此时对象有两个引用者
auto r = make_shared<
int
>(10);
r = q;
|
此时r的引用计数为0,r原指对象被自动释放。q的引用计数增加。
shared_ptr被用来表示共享的拥有权。也就是说,当两段代码都需要访问一些数据,而它们又都没有独占该数据的所有权(从某种意义上来说就是该段代码负责销毁该对象)。这是我们就需要shared_ptr。shared_ptr是一种计数指针。当引用计数变为0时,shared_ptr所指向的对象就会被删除。下面我们用一段代码来说明这点。
void test()
{
shared_ptr p1(new int); // 计数是1
{
shared_ptr p2(p1); //计数是2
{
shared_ptr p2(p1); // 计数是3
} //计数变为2
}//计数变为1
} // 在此,计数变为0。同时int对象被删除
来看一段代码:
#include <iostream>
#include <memory>
using namespace std;
int main(){
shared_ptr<int> p = make_shared<int>(1);
cout<<"p:"<<p<<" pcount:"<<p.use_count()<<endl;
auto r = make_shared<int>(4);
cout<<"r:"<<r<<" rcount:"<<r.use_count()<<endl;
shared_ptr<int> s;
cout<<"s:"<<s<<" scount:"<<s.use_count()<<endl;
{
shared_ptr<int> q(p);
cout<<"p:"<<p<<" pcount:"<<p.use_count()<<" q:"<<q<<" qcount:"<<q.use_count()<<endl;
r = p;
cout<<"p:"<<p<<" pcount:"<<p.use_count()<<" q:"<<q<<" qcount:"<<q.use_count()<<" r:"<<r<<" rcount:"<<r.use_count()<<endl;
}
cout<<"p:"<<p<<" pcount:"<<p.use_count()<<" r:"<<r<<" rcount:"<<r.use_count()<<endl;
cout<<"pvalue:"<<*p<<endl;
}
结果是
可以总结以下几点:
1、用make_shared 创建以后引用自动初始化为1. 如果仅仅定义shared_ptr<int> s; 那么引用为0.
2、一开始r对象是0069CC14. 经过r = p;这句话,r原本对象0069CC14的引用减1,结果为0,自动销毁,现在r对象变成了原来p的对象。所以经过子作用域{}之后,p q r其实就是同一个对象了。这个对象的引用为3。
3、由于q的作用域是在{} 内,所以在离开子作用域以后,q对象被销毁。所以这个共同对象的引用计数就会减1。变成2.
还有一个unique_ptr
当我们定义一个unique_ptr的时候,需要将其绑定到一个new返回的指针。
只能有一个uniqu_ptr指向对象,也就是说它不能被拷贝,也不支持赋值。但是我们可以通过move来移动
1
2
3
4
5
6
|
std::unique_ptr<
int
> p1(
new
int
(5));
std::unique_ptr<
int
> p2 = p1;
// 编译会出错
std::unique_ptr<
int
> p3 = std::move(p1);
// 转移所有权, 现在那块内存归p3所有, p1成为无效的指针.
p3.reset();
//释放内存.
p1.reset();
//实际上什么都没做.
|