04C++析构函数原理分析

析构函数

1.析构介绍

析构函数(Destructor)也是一种特殊的成员函数,没有返回值,不需要程序员显式调用(程序员也没法显式调用),而是在销毁对象时自动执行。

Student::~Student()
{
    cout << "析构被执行" << endl;
}

析构地时候,基本类型是自动回收地。

2.堆上内存析构

1.拷贝与赋值介绍

我们先看下C++ primer上写的话。

img

通过简化,我们将其分解成两句话:

1.拷贝基本类型(系统回收)和指针类型(手动堆内存和自行释放内存)

2.赋值和拷贝区分,二者之间差异

首先分析第二句话(与java是完全不同的,不能以java的方式去理解)。

#include <iostream>
using namespace std;

int main(int argc, char const *argv[])
{
    string s1 = "hello";
    string s2 = "world";
    string s3 = s2;//拷贝,风格不友好s3(s2)
    s2 = s1;//值传递,仅仅将s1的数值传递过来
    system("pause");
    return 0;
}

拷贝与值传递细节区分,可以这样去分析,

  • 除基本类型,string s1; 声明定义,会在内存空间中分配空间,返回地址。

  • string s3 = s2; string s3也是声明定义,但是s2先执行,s3并没有立即分配内存空间,而是等s2执行完,

    等价于,string s3 = string(“world”);s3此时是没有开辟内存空间,所以s3执行地是拷贝

  • s2 = s1;//s2此时已经分配了内存空间,s1只是将自己的数值传递给了s2,其实内部进行了操作符=操作

————注意:以上概念,在java中是完全不同的。

package com.company.test2;
public class Test24 {

    static class Student{
        private String name;
        private int age;

        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }

        @Override
        public String toString() {
            return "Student{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
    public static void main(String[] args) {
        String str = "hello";
        String s2 = str;//非拷贝
        //怎么验证
        Student stu = new Student("zhangsan",20);
        Student stu2 = stu;//如果是拷贝,则stu2不会影响到stu
        stu2.setName("lisi");
        System.out.println(stu.getName());//"lisi",stu被修改
    }
}

通过java的案例,我们可以分析出来以下结论:

  • java非基本类型,都是地址值传递
  • String s2 = str;或者Student stu2 = stu;从C++角度理解,s2先是生成地址空间(指针),指向str/stu,间接说明地址值传递(生成四字节或8字节的指针),C++没有类似java隐式转换,java非基本类型赋值转换等价于:string* s7 = &s2; // s7先生成地址空间(指针),指向s2

C++和java的分析,我们了解C++的拷贝与赋值原理,以及java非基本类型所谓的赋值(不应该叫做赋值,而是新开辟一个4字节或8字节地址空间,生成一个新的指针,指向同一片内存空间)

2.对比基本类型与非基本类型差异
#include <iostream>
using namespace std;

int main(int argc, char const *argv[])
{
    string s1 = "hello";
    string s2 = "world";
    string s3 = s1 + s2;//没有问题
    string s4 = "hello" + "world";//报错
    int a = 1 + 2; //不报错
    system("pause");
    return 0;
}

解答:

1.string s4 = “hello” + “world”;//报错:"hello"非基本类型,内存空间中也不知道它是什么类型,故无法使用操作符+,

2.string s3 = s1 + s2;//没有问题: s1/s2明确是字符串类型,字符串内部有自己的+操作符。

3.int a = 1 + 2; //不报错, 1基本类型,内存空间存储时就知道什么类型,可以使用操作符。

在java中

        String s4 = "hello" + "world";//不报错,s4等于"helloworld"
        Student stu = new Student("zhangsan",20);
        Student stu2 = stu;//如果是拷贝,则stu2不会影响到stu
        stu2.setName("lisi");
        Student stu3 = stu + stu2;//报错,操作符问题,java中,除了基本类型和字符串,在java中其他类型没有操作符的概念

为什么,string在c++中报错,而在java中不报错,原因:在java中,对字符串string做了特殊处理,"hello"能在内存空间中自动识别为字符串,所以”hello“ + "world"在内存空间中生成一个匿名的全新的字符串,s4指针指向该内存空间。而除了基本类型和字符串,在java中其他类型没有操作符的概念,故报错。

3.指针堆内存析构

在这一块,C++和java的内存销毁方案又是不同的,我们一步一步来分析。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wtNXdlGN-1625408195211)(G:\markdown\C++\C++基础\析构.png)]

person2 ptr指针和person1 ptr指针都指向了同一片内存空间(这是java的方式,不适合C++),导致析构时出现问题,所以我们需要自行开辟内存空间。

1.简单指针析构

#include <iostream>
using namespace std;

class Teacher
{
public:
    string name;
    int *height;

public:
    Teacher(string name, int height);
    Teacher(const Teacher &teacher);
    ~Teacher();
};

Teacher::Teacher(string name, int height)
{
    this->height = new int; //开辟内存空间
    // *(this->height) = height; //拷贝值
    *this->height = height;
    // *(*this).height = height;//等价于上面-> (*this).
}

Teacher::Teacher(const Teacher &teacher)
{
    cout << "拷贝执行了" << endl;
    this->height = new int;
    *this->height = *teacher.height;
}

Teacher::~Teacher()
{
    cout << "析构执行了" << endl;
    if (this->height != NULL)
    {
        delete this->height;
        this->height = NULL;
    }
}

void test()
{
    Teacher t("你好", 20); //“你好”是没有类型,const字面常量,会产生一个临时对象返回地址,name指向了临时对象
    Teacher t2(t);         //拷贝
    cout << t.height << endl;
    cout << t2.height << endl; //于t的地址不一样
    cout << *t2.height << endl;
}

int main(int argc, char const *argv[])
{
    test();
    system("pause");
    return 0;
}

注意:我们在删除一个指针之后,编译器只会释放该指针所指向的内存空间,而不会删除这个指针本身。

另外,**编译器默认将释放掉的内存空间回收然后分配给新开辟的空间(跟编译器有关)。**后面分析

2.类中嵌套类对象

#include <iostream>
using namespace std;

class Student
{
public:
    int *x;
    int *y;
    Student();
    Student(int x, int y); //基本类型加上String,尽量值传递
    Student(const Student &stu);
    ~Student();
};

Student::Student()
{
    this->x = new int;
    this->y = new int;
}
Student::Student(int x, int y)
{
    cout << "student创建了" << endl;
    this->x = new int(x);
    this->y = new int(y);
    //等价
    // this->x = new int;
    // *this->x = x;
}

Student::Student(const Student &stu)
{
    this->x = new int(*stu.x);
    this->y = new int(*stu.y);
}

Student::~Student()
{
    cout << "student析构了" << endl;
    if (this->x != NULL)
    {
        delete this->x;
        this->x = NULL;
    }
    if (this->y != NULL)
    {
        delete this->y;
        this->y = NULL;
    }
}

class Teacher
{
public:
    string name;
    Student *stu;
    Teacher(string name, const Student &s);
    Teacher(const Teacher &t);
    ~Teacher();
};

Teacher::Teacher(string name, const Student &s)
{
    this->name = name;
    this->stu = new Student(*s.x, *s.y); //这个不同于java, new Student显示开辟一片内存空间
                                         // this->stu = &Student(*s.x, *s.y);//记住,千万不要这样写,Student(*s.x, *s.y)是一个局部对象,会被回收的,最后导致结果就是this->stu指向一个随机数
    //等价于下面
    // this->stu = new Student();
    // *this->stu->x = *s.x; //注意,不要写成this->stu->x = s.x;
    // *this->stu->y = *s.y;
}

Teacher::Teacher(const Teacher &t)
{
    this->name = t.name;
    this->stu = new Student(*t.stu);
}

Teacher::~Teacher()
{
    cout << "teacher析构了" << endl;
    if (this->stu != NULL)
    {
        delete this->stu;
        this->stu = NULL;
    }
}

void test()
{
    Student stu = Student(1, 2);
    Teacher t = Teacher("zhangsan", stu);
}

int main(int argc, char const *argv[])
{
    test();
    system("pause");
    return 0;
}

————————C++中,类中的成员,都是独立的个体(都有内存空间),java中不是。

析构执行顺序:

student创建了
student创建了
teacher创建了
teacher析构了
student析构了
student析构了
构建,先执行成员属性,析构,先执行本身,在执行成员

在java中,我们看个案例

Student.java:

package com.company.test2;

public class Student {
    private String name;
    private int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

Teacher.java:

package com.company.test2;

public class Teacher {
    public Student student;
    public String name;
    public Teacher(Student student, String name) {
        this.student = student;
        this.name = name;
    }

    public void setStudentName() {
        this.student.setName("wangwu");
    }
}

test.java:

package com.company.test2;

public class Test25 {
    public static void main(String[] args) {
        Student student = new Student("zhangsan",20);
        Teacher teacher = new Teacher(student,"lisi");
        teacher.setStudentName();
        System.out.println(student.getName());//wangwu
    }
}

——————通过案例,我们发现student的姓名由zhangsan变成wangwu,显然可以看到teacher中的student指向的内存空间和Student的是一样的,从这里面我们可以知道,java中,除基本类型之外所有类型(包括string,个人觉得这是string为什么不能修改的核心所在)都是地址拷贝复制,向引用但不是引用(指针常量)。

java和C++二者在一块由非常大的差异,根本原因是GC的方式以及内存模型有差异? java基本类型的变量和对象的引用变量都是在函数的栈内存中分配,其他new对象在堆上,程序员不能直接地设置栈或堆,而C++不一样,如果这一块不懂,可以去看前面的知识分析。这也是为什么C++不用java引用(本质不是引用)的方式的根本原因。

3.深拷贝和浅拷贝

C++默认执行的是浅拷贝

1.浅拷贝:浅拷贝按位拷贝对象,对源对象进行的精准拷贝,如果源对象是基本类型,拷贝地就是精准对象的值和地址(内存空间直接开辟内存,地址空间也开辟4字节或8字节),如果属性是地址类型(指针),则拷贝的是地址(地址空间开辟4字节或8字节的指针),如果一个对象修改了改内存空间的值,则另外一个对象也会收到影响。

2.深拷贝:深拷贝,针对非基本类型的对象时,不仅仅只是拷贝对象的地址,而且还在内存空间重新开辟内存,用于保存拷贝对象的数值。 在C++中,一般就是指针类型,需要深拷贝。 在java中,没有必要去使用深拷贝,因为java中非基本类型是在堆中生成的,而且GC方式是采用可达性分析。C++不同,

注意:深拷贝相比于浅拷贝速度较慢并且花销较大。

总结:

1.构建,先执行成员属性,析构,先执行本身,在执行成员

2.类中的成员,都需要创建成独立的个体(都有自己的内存空间),不要创建引用或者指针指向其他的类对象
+默认执行的是浅拷贝

1.浅拷贝:浅拷贝按位拷贝对象,对源对象进行的精准拷贝,如果源对象是基本类型,拷贝地就是精准对象的值和地址(内存空间直接开辟内存,地址空间也开辟4字节或8字节),如果属性是地址类型(指针),则拷贝的是地址(地址空间开辟4字节或8字节的指针),如果一个对象修改了改内存空间的值,则另外一个对象也会收到影响。

2.深拷贝:深拷贝,针对非基本类型的对象时,不仅仅只是拷贝对象的地址,而且还在内存空间重新开辟内存,用于保存拷贝对象的数值。 在C++中,一般就是指针类型,需要深拷贝。 在java中,没有必要去使用深拷贝,因为java中非基本类型是在堆中生成的,而且GC方式是采用可达性分析。C++不同,

注意:深拷贝相比于浅拷贝速度较慢并且花销较大。

总结:

1.构建,先执行成员属性,析构,先执行本身,在执行成员

2.类中的成员,都需要创建成独立的个体(都有自己的内存空间),不要创建引用或者指针指向其他的类对象

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值