C++ 内存相关

来源:互联网 发布:fanuc离线编程软件 编辑:程序博客网 时间:2024/06/07 22:04

常见的内存错误及其对策

发生内存错误是件非常麻烦的事情。编译器不能自动发现这些错误,通常是在程序运行时才能捕捉到。而这些错误大多没有明显的症状,时隐时现,增加了改错的难度。有时用户怒气冲冲地把你找来,程序却没有发生任何问题,你一走,错误又发作了。 常见的内存错误及其对策如下:

  内存分配未成功,却使用了它。

  编程新手常犯这种错误,因为他们没有意识到内存分配会不成功。常用解决办法是,在使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行

  检查。如果是用mallocnew来申请内存,应该用if(p==NULL) if(p!=NULL)进行防错处理。

  内存分配虽然成功,但是尚未初始化就引用它。

  犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。 内存的缺省初值究竟是什么并没有统一的标准,尽管有些时候为零值,我们宁可信其无不可信其有。所以无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略,不要嫌麻烦。

  内存分配成功并且已经初始化,但操作越过了内存的边界。

  例如在使用数组时经常发生下标“多1”或者“少1”的操作。特别是在for循环语句中,循环次数很容易搞错,导致数组操作越界。

  忘记了释放内存,造成内存泄露。

  含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,你看不到错误。终有一次程序突然死掉,系统出现提示:内存耗尽。

  动态内存的申请与释放必须配对,程序中mallocfree的使用次数一定要相同,否则肯定有错误(new/delete同理)。

  释放了内存却继续使用它。


对策:

  【规则1】用mallocnew申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。

  【规则2】不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。

  【规则3】避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。

  【规则4】动态内存的申请与释放必须配对,防止内存泄漏。

  【规则5】用freedelete释放了内存之后,立即将指针设置为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;}


//strcpy 函数实现
char* strcpy(char* strDst, const char* strSrc)
{
    assert((NULL!=strDst) && (NULL!=strSrc));//断言字符串地址非0
    char* address = strDst;
    while((*strDst++ = *strSrc++) != '\0');
    return address;
}

//strlen 函数实现
int strlen(const char* str)
{
    assert(NULL != str);//断言字符串地址非0
    int len = 0;
    while (*str++ != '\0')
    {
        len++;
    }

    return len;
}



数组的定义和初始化与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循环对其赋值初始化)

显示初始化数组,无需指定维数:

[cpp] view plaincopy
  1. string str_array[] = {"aa""bb""cc"};  


与vector不一样,数组不允许直接复制和赋值

[cpp] view plaincopy
  1. int ia[] = {0, 1, 2};   // ok: array of ints  
  2. 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 = newint(1024);//error:必须使用直接初始化的形式
shared_ptr<int> p2(newint(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(newint(5));
std::unique_ptr<int> p2 = p1; // 编译会出错
std::unique_ptr<int> p3 = std::move(p1); // 转移所有权, 现在那块内存归p3所有, p1成为无效的指针.
 
p3.reset();//释放内存.
p1.reset();//实际上什么都没做.

0 0
原创粉丝点击