C++中使用指针时,野指针、空指针以及内存泄露等问题总结

发布于:2023-10-25 ⋅ 阅读:(90) ⋅ 点赞:(0)

前言

在C++中指针是一个非常重要的数据类型,指针时,可以程序员自己创建一个变量,也可以使用new或着malloc将变量开辟到堆区,使用一个指针变量接收。当使用指针时,也会有几点非常容易出错的地方,这里进行一个总结,不足之处,请各位指教。

正文

首先,需要了解指针的定义,然后是相关用法,指针与数组、指针与函数、指针与常量的用法总结在https://blog.csdn.net/m0_59951855/article/details/132612307本文中已经进行详细说明。

01-指针

定义:指针本身是一种数据类型,当定义一个变量,并且为其赋值之后,该数据在内存中拥有自己的地址,该地址可以使用一个指针存储。当需要获取这个地址所指向的值时,可以对指针使用解引用的方法获取值。也就是说当定义一个指针时,就将它想象成一个地址。这样可能更容易理解一些

注:所有指针类型变量,在32位操作系统中都是占有4个字节。

代码示例: 在定义指针变量 p 的同时对它进行初始化,并将变量 a 的地址赋予它,此时 p 就指向了 a。值得注意的是,p 需要一个地址,a 前面必须要加取地址符&

int a = 10;
int * p = &a  

02-空指针和野指针

定义: 空指针即在初始化指针变量时,将其置为空,这样做可以避免野指针的发生,原因在下文野指针中详细讲解。但是,需要注意的是,空指针指向的内存是0~255之间的内存,这一块内存是无法访问的,被系统占用。

代码示例:

int main() {

    int * p = NULL;  // 定义一个指向为空的指针变量
    //访问空指针报错
    //内存0~255为系统占用内存,不允许用户访问
    cout << *p << endl;
    system("pause");
    return 0;

}

定义:以下两种情况都有可能产生野指针

       (1)、野指针指的是指向不可用内存的指针,也就是非法的空间,因为当指针被创建时,不可能自动指向为空,默认值就是随机的,很有可能指向非法的内存空间。

int main() {
    //指针变量p指向内存地址编号为0x1100的空间
    int * p = (int *)0x1100;
    //访问野指针报错
    cout << *p << endl;
    system("pause");
    return 0;
}

       (2)、当使用free或者malloc创建开辟内存空间后,又将其释放了,此时,如果没有将变量置为空,也会产生野指针。释放的仅仅是指针指向的内存,而不是指针本身

代码示例: 

int main() {
    
    int *a = new int(10);
    delete p;
    //访问野指针报错,未置为空
    cout << *p << endl;
    system("pause");
    return 0;
}
 避免野指针

      (1)、对指针进行初始化,置为空或者使用new或malloc分配内存,

      (2)、当使用new或者malloc分配内存,指针使用完毕后,先使用delete或free释放内存,然后再将指针置为空。如果是将数组开辟到堆区,在释放内存时,还需要在delete后加 [ ]。

int main() {
    
    int *a = new int(10);
    delete p;
    p = NULL;
    
    system("pause");
    return 0;
}



int main() {
    int* arr = new int[10];

    //释放数组 delete 后加 []
    delete[] arr;
    arr = NULL;
    system("pause");
    return 0;
}

03-深拷贝与浅拷贝

定义: 在C++中使用class创建类时,系统默认提供构造函数,语法:

构造函数语法: 类名 ( ) { }
        1. 构造函数,没有返回值也不写void
        2. 函数名称与类名相同
        3. 构造函数可以有参数,因此可以发生重载
        4. 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次

这里的拷贝构造函数就是单纯的属于浅拷贝,也就是使用时,只是对构造函数的简单调用,直接按已经创建好的对象完整复制一个对象。

class Person {
public:
   
    Person(const Person& p) {
        cout << "拷贝构造函数!" << endl;
        mAge = p.mAge;
    }
public:
    int mAge;
};

int main() {
    Person man(100); //p对象已经创建完毕
    Person newman(man); //调用拷贝构造函数
    Person newman2 = man; //拷贝构造
    //Person newman3;
    //newman3 = man; //不是调用拷贝构造函数,赋值操作
    system("pause");
    return 0;
}

  深拷贝的使用具体见如下代码,相关说明:

       (1)、仅仅使用浅拷贝时,因为在类中有些成员属性开辟到堆区。因此,在使用完毕之后,必须在使用delete或者free进行释放内存,此时,仅在类中的析构函数中加入一个判断语句即可,也就是析构函数就是用来释放内存时使用的,因为最后才会调用析构函数。

       (2)、此时由于两个对象p1和p2同时指向同一块内存空间,如果释放,将会导致堆区数据重复释放。也就是将同一块地址释放了两次,那程序肯定崩溃,具体原因在代码注释中已经详细说明。

       (3)、而使用深拷贝就是将默认的拷贝构造函数做一部分修改,在函数里面使用new将成员变量开辟到堆区,也就是说虽然p2和p1的成员属性都是指向同一个值,但是却不指向同一个地址。

代码示例:

class Person {
public:
    //无参(默认)构造函数
    Person() {
        cout << "无参构造函数!" << endl;
    }
    //有参构造函数, 这里和拷贝构造没有任何关系,只是用于对象使用的一个构造函数
    Person(int age ,int height) {
        cout << "有参构造函数!" << endl;
        m_age = age;
        m_height = new int(height);
    }
    //拷贝构造函数
    Person(const Person& p) {
        cout << "拷贝构造函数!" << endl;
        //如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
        m_age = p.m_age;
        //m_height = p.m_height; 这里属于默认拷贝构造函数执行的拷贝过程 
        m_height = new int(*p.m_height); //这里括号里面就是使用解引用的方式解出需要拷贝的数据
    }
    //析构函数
    ~Person() {
        cout << "析构函数!" << endl;
        if (m_height != NULL)
        {
            delete m_height;
            m_height = NULL;  // 防止野指针出现
        }
    }
public:
    int m_age;
    int* m_height;   // 只有当有成员变量开辟到堆区时,才会使用深拷贝,解决浅拷贝带来的问题
};
void test01()
{
    Person p1(18, 180);  // 这里用到的是有参构造函数
    Person p2(p1);   // 这里用到的是拷贝构造函数
    cout << "p1的年龄: " << p1.m_age << " 身高: " << *p1.m_height << endl;
    cout << "p2的年龄: " << p2.m_age << " 身高: " << *p2.m_height << endl;
}
int main() {
    test01();
    system("pause");
    return 0;
}

 04-析构函数与内存泄漏问题

定义: 说到这里,就需要了解C++类中的三大特性之一,多态:派生类和虚函数实现运行时的多态成为动态多态。

多态满足条件:
        有继承关系
        子类重写父类中的虚函数
多态使用条件:
        父类指针或引用指向子类对象
重写:函数返回值类型  函数名  参数列表  完全一致称为重写

        对上述的解释就是,首先需要有继承关系,也就是 class A:public B{};A为子类,B为父类。然后,父类B中有一个虚函数,也就是函数返回值前加virtual的函数,子类需要重写父类中的这个虚函数,也就是再写一个函数,只是不带virtual。
        最后,多态使用方式,写一个全局函数,这个函数里传入的是父类对象的引用,也就是所说的父类指针或引用指向子类对象,使用指针居多,而且需要使用new 加子类名创建子类对象指针。然后使用父类名创建一个指针变量指向使用new创建的子类对象。

接着,还需要明白几个定义,虚析构,纯虚析构,虚函数,纯虚函数

虚函数语法:virtual 返回值类型 函数名 (参数列表) {   };

纯虚函数语法: virtual 返回值类型 函数名 (参数列表)= 0 ;   不需要类外实现

虚析构语法:virtual ~类名() {  };

纯虚析构语法:类内声明,virtual ~类名() = 0;   类外初始化,类名::~类名( ){ }  具体原因下文解释

内存泄漏:就是说使用new或者malloc申请了一块内存空间,在使用结束后,忘记释放。如果程序运行时间过长,占用内存越来越多,最终就会耗尽全部内存。造成系统崩溃。


接着就来说明虚析构和纯虚析构与内存泄漏之间的关系,具体代码如下。

       (1)、当我们在子类中定义了一个指针类型的成员变量,当使用完之后,必须要释放内存,否则就会造成内存泄漏。然而,当父类中的析构函数并非虚析构函数或者纯虚析构函数时,仅仅调用父类中的析构函数,也就是说只是将父类中一些开辟到堆区的数据释放了,并没有调用子类中的析构函数

       (2)、此时,可以使用将父类析构函数改成虚析构或者纯虚析构的方法实现调用子类的析构函数,在释放父类指针时,将会先执行子类的析构函数,将子类指针释放干净,再释放父类指针。

注:(1)、父类中的析构函数一定要初始化,并且加以实现,因为父类中也有可能有些成员属性开辟到堆区,必须执行父类析构析构函数才能释放。

       (2)、C++中默认的析构函数为什么不是虚析构函数呢?主要是因为虚函数管理需要额外的虚函数表和虚函数表指针,占用内存,如果是不需要继承的类,就不需要使用虚析构。

class Teacher {
public:
    Teacher()
    {
        cout << "Teacher 构造函数调用!" << endl;
    }
    virtual void Speak() = 0;
    //析构函数加上virtual关键字,变成虚析构函数
    //virtual ~Teacher()
    //{
    // cout << "Teacher虚析构函数调用!" << endl;
    //}
    virtual ~Teacher() = 0;  // 类内初始化
};
Teacher::~Teacher()   // 类外实现
{
    cout << "Teacher 纯虚析构函数调用!" << endl;
}
    //和包含普通纯虚函数的类一样,包含了纯虚析构函数的类也是一个抽象类。不能够被实例化。
class Student : public Teacher {
public:
    Student(string name)
    {
        cout << "Student构造函数调用!" << endl;
        m_Name = new string(name);
    }
    virtual void Speak()
    {
        cout << *m_Name << "学生调用!" << endl;
    }
    ~Student()
    {
        cout << "Student析构函数调用!" << endl;
        if (this‐>m_Name != NULL) {
            delete m_Name;
            m_Name = NULL;
        }
    }
public:
    string *m_Name;
};
void test01()
{
    Teacher *teacher = new Student("Tom");
    teacher‐>Speak();
    
    delete teacher;
}
int main() {
    test01();
    system("pause");
    return 0;
}

总结

指针方面:

1、我们可以通过 & 符号 获取变量的地址
2、利用指针可以记录地址
3、对指针变量解引用,可以操作指针指向的内存 

深拷贝与浅拷贝:

4、如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题

内存泄漏问题:

5、虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
6、如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
7、拥有纯虚析构函数的类也属于抽象类

本文含有隐藏内容,请 开通VIP 后查看