## 今天学了什么
今天学习C++对象的初始化和清理:构造函数/析构函数、深拷贝与浅拷贝、初始化列表。
## 构造函数
作用:主要作用在于创建对象时,为对象成员属性赋值,由编译器自动调用,无需手动调用。
语法:类名(){}
- 不需要返回值,不写void;
- 函数名与类名相同;
- 可以有参数,可以发生重载;
- 程序在调用对象时会自动调用构造函数,无需手动调用,且只会调用一次。
### 构造函数的分类和调用
两种分类:
- 参数:有参构造、无参构造(默认构造);
- 类型:普通构造、拷贝构造;
三种调用方式:
括号法:
Person p; //默认
Person p1(10); //Person (int a) 有参
Person p2(p1); // 拷贝
*注:调用默认构造时,不要写Person p()。原因:编译器会认为是一个函数声明,不会认为在创建对象。
显示法:
Person p3=Person (10); //有参 Person (10); //匿名对象
Person p4=Person (p3); //拷贝
匿名对象特点:当前行执行结束后,系统会立即回收匿名对象。
*注:不要利用拷贝构造函数初始化匿名对象,例: Person (p3); 原因:编译器会认为是对象的声明,等价于 Person p3;
隐式转换法:
- Person p5=10; //有参 == Person p5=Person (10);
- Person p6=p5; //拷贝
### 拷贝构造函数调用时机
①使用一个已经创建完毕的对象来初始化一个新对象:
Person p;
Person p1(p);
②值传递的方式给函数参数传值:
void test(Person p) {…}
…
Person p;
test(p); …
③以值方式返回局部对象:
Person test() {
Person p;
return p; }
*注:Release模式下直接 return p 会触发NRVO(具名返回值优化),跳过拷贝,直接在外接收变量p1的内存空间原地构造对象;Debug模式下,无优化,会拷贝。
### 构造函数调用规则
- 默认情况下,编译器至少给一个类添加3个构造函数:构造函数(空实现)、析构函数(空实现)、拷贝函数(值拷贝)。
- 写了有参构造,编译器不再提供默认构造函数,但会提供拷贝构造。
- 如果自己写了拷贝构造,编译器不再提供其他函数。
### 深拷贝&浅拷贝
浅拷贝:简单的赋值拷贝操作;
深拷贝:在堆区重新申请空间,进行拷贝操作。
调试验证:打开visual studio2022,创建一个Person类,在堆区创建一个身高变量,不写拷贝构造,让编译器自动生成:
class Person
{
public:
Person()
{
cout << "Person的默认构造函数" << endl;
}
Person(int age,int height)
{
cout << "Person的有参构造函数" << endl;
m_height =new int(height);
m_age = age;
}
~Person()
{
if (m_height != nullptr)
{
delete m_height;
m_height = nullptr;
}
cout << "Person的析构函数" << endl;
}
int m_age;
int* m_height;
};
void test03()
{
Person p(18, 160);
cout << "年龄: " << p.m_age << " 身高: " << *p.m_height << endl;
Person p1(p);
cout << "年龄: " << p.m_age << " 身高: " << *p1.m_height << endl;
}
int main()
{
test03();
return 0;
}
运行报错,在构造函数下一个断点,逐步调试,发现p.m_height和p1.m_height的地址一模一样,由此可见,编译器提供的拷贝构造函数属于浅拷贝,导致两个指针指向同一个堆区内存,导致在调用析构时,堆区内存被重复释放,程序报错。解决方法,在堆区开辟独立空间存放拷贝地址,重写拷贝构造:
Person(const Person& p)
{
cout << "Person的拷贝构造函数" << endl;
m_age = p.m_age;
m_height=new int(*p.m_height);
}
错误逻辑思考:在析构里面已经有if判断m_height是否为空,为什么还会报错?
原因:p.m_height,p1.m_height存的只是值传递拷贝过来的地址,只是一个数字,即使p1执行析构释放了对应的堆内存,p.m_height的值不会被改变,仍然是之前存的地址。所以当p执行析构时,会产生错误,重复释放。
### 初始化列表
适用于:const常量成员、引用成员。
语法:构造函数():属性1(值1),属性2(值2)…{}
例:Person(int age, int height): m_height(new int(height)),m_age(age)
{
…
}
## 析构函数
作用:主要作用在于对象销毁前系统自动调用,执行清理工作。
语法:~类名(){}
- 不需要返回值,不写void;
- 函数名与类名相同;
- 没有参数,不可以发生重载;
- 程序在调用对象时会自动调用,无需手动调用。
*注:构造和析构必须有,如果自己不写,编译器会提供默认的构造/析构。
## 补充
** NULL与nullptr的区别 **
NULL:宏,等价整数0,重载可能会选错函数。(只是数字0,重载会匹配int)
nullptr:C++11关键字,专门用于空指针,不会和int混淆。
** 栈对象和堆对象的构造析构顺序 **
①栈:
Person p1;
Person p2;
构造:p1-p2
析构:p2-p1 先进后出。
②堆:
Person *p1=new Person();
Person *p2=new Person();
delete p1;
delete p2;
构造:p1-p2
析构:取决于delete顺序
③混合(栈里面保存指向堆对象的指针):
Person *p=new Person();
没有写delete,只会释放栈上p的地址,不会释放堆区的,内存泄漏。
delete作用:获取指针p存的地址,找到对应堆对象,调用*p的析构,释放对应内存。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2201_75293893/article/details/166596911



