析构函数
1.析构介绍
析构函数(Destructor)也是一种特殊的成员函数,没有返回值,不需要程序员显式调用(程序员也没法显式调用),而是在销毁对象时自动执行。
Student::~Student()
{
cout << "析构被执行" << endl;
}
析构地时候,基本类型是自动回收地。
2.堆上内存析构
1.拷贝与赋值介绍
我们先看下C++ primer上写的话。
通过简化,我们将其分解成两句话:
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.类中的成员,都需要创建成独立的个体(都有自己的内存空间),不要创建引用或者指针指向其他的类对象