C++中的面向对象来源于生活,每个对象也都会有初始设置以及 对象销毁前的清理数据的设置
1.构造函数和析构函数作用与意义
对象的初始化和清理重要性:
一个对象或者变量没有初始状态,对其使用后果是未知
同样的,使用完一个对象或变量,没有及时清理,也会造成一定的安全问题
C++中利用构造函数完成对象的初始化,利用析构函数完成对象的清理。
构造函数和析构函数会被编译器自动调用,程序员可以不写,但是编译器提供的构造函数和析构函数是空实现,也就是这个函数体是空的。但如果程序员写了,编译器会自动调用。
构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
2.构造函数和析构函数的语法
构造函数的语法:
类名() {};
构造函数,没有返回值也不写void;
函数名称与类名相同;
构造函数可以有参数,因此可以发生重载;也可以没有参数;
程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次。
如果没有构造函数,编译器也会有写一个Person构造函数,只是这是一个空函数,即:Person() { }
析构函数的语法:
~类名(){}
析构函数,没有返回值也不写void
函数名称与类名相同,在名称前加上符号 ~
析构函数不可以有参数,因此不可以发生重载
程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
注意:析构函数前面有~;析构函数不能有参数,构造函数可以有参数
语法理解——main函数只运行test1
构造和析构函数都调用的原因:
首先,test1创建的对象p是一个局部变量,也就是局部对象,放在栈区。而栈区的数据,在test1执行完毕后,就会被释放。也就是说test1运行完后,这个对象才被释放。
然后,释放对象前,编译器会自动调用析构函数,并且只调用一次。也就是,看到的输出结果了。
构造和析构函数都是必须有的实现,如果程序员不提供,编译器会提供一个空实现的构造和析构
语法理解——main函数中只创建对象
没有调用析构函数的原因:
首先,main函数中创建局部变量p,这个p还没有被释放。
然后,程序创建完p之后,运行下一行”system("pause");“就中断了,也就是窗口的”请按任意键继续“。只有在这行代码运行完之后,p就会被释放,这时候才调用析构函数。
但是,再下一行”return 0;“,也就是关闭窗口,有可能我们就看不到析构函数的调用了。视力好的话,在关闭的一瞬间可以看到这个函数的调用。
3.构造函数的两种分类方式:
按参数分为: 有参构造和无参构造(默认构造);
按类型分为: 拷贝构造和普通构造(除了拷贝构造都是普通)
有参构造就是有参数的构造函数
拷贝构造把本体构造函数的的所有属性都拷贝
注意拷贝构造函数的写法,类名 (const 类名& 对象) { 拷贝属性 }
4.构造函数的三种调用方式:
有三种调用方式,分别是括号法、显示法、隐式转换法
括号法
test2运行:
首先是p1被创建,调用无参构造函数,即1;
然后,p2被创建,调用有参构造函数,即2;
接着,p3被创建,调用拷贝构造函数,即3。
这时p1被释放,调用析构,即4;
再p2释放,调用析构,即5;
最后,p3释放,调用析构,即6,结束。
拷贝的作用
p3的年龄也是18,是因为p3是直接拷贝p2的年龄的.
注意事项1:调用默认构造函数时,不要加()。否则编译器会认为是一个函数的声明,不会认为是创建对象的过程。
显示法
注意事项2:Person1(18)是一个匿名对象,当前行执行结束后,系统会立即回收掉匿名对象。对”Person1 p2 = Person1(18)”这行代码而言,p2是Person1(18)这个匿名对象的名。可以看到,这个匿名对象创建完之后,还没等test2执行完,就立马被释放了。
注意事项3:不要利用拷贝构造函数来初始化匿名对象,否则编译器会认为 Person(p3) == Person p3;编译器会认为这是一个对象的声明,也就是重定义。
隐式转换法
5.拷贝构造函数调用时机
C++中拷贝构造函数调用时机通常有三种情况:
使用一个已经创建完毕的对象来初始化一个新对象
前面的分类有提到这种方式,不赘诉。
值传递的方式给函数参数传值
首先,p使用默认构造函数创建对象;然后,通过doWork函数以值传递的方式(值传递就是临时拷贝一份数据)把test4中的p拷贝给doWork中的形参p,相当于是拷贝构造的用法。
另外,如果在doWork中,加上p.age=1000;也不会改变test4中p的年龄,形参不改变实参。
以值方式返回局部对象
2022版的VS返回值做了优化,没有拷贝。老师的版本有拷贝。其实就是值传递的方式,p值传递给doWork2,值传递(拷贝新对象)的时候会触发拷贝构造函数,而新拷贝的对象就是doWork2返回的值。
6.构造函数调用规则
默认三个函数
默认情况下,c++编译器至少给一个类添加3个函数:
默认构造函数(无参,函数体为空)
默认析构函数(无参,函数体为空)
默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下
如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造
如果用户定义拷贝构造函数,c++不会再提供其他构造函数
属性值拷贝
自己写了定义了拷贝函数,会调用拷贝函数。首先创建默认构造函数,然后调用拷贝构造,把p1拷贝给p2
如果把拷贝函数注释掉,此时还是输出p2的年龄,C++中做了简单的值拷贝(所有属性做了赋值操作),但没有调用拷贝函数,只是把p1的年龄拷贝给p2,也就是“age = p.age;“。
调用规则1
写了有参构造时,系统不在提供默认构造
编译器会提供拷贝构造做值拷贝
调用规则2
如果程序员提供了拷贝构造,编译器不会提供默认和有参构造
调用的规则总结来说就是,3个函数的优先级是拷贝>有参>默认。有拷贝,不提供有参和默认;有有参,不提供默认,但提供拷贝。
7. ★深拷贝和浅拷贝
浅拷贝:值拷贝,p1的值赋值给p2
class Person {
public:
Person() {
cout << "默认构造函数" << endl;
}
Person(int age, int height) {
cout << "有参构造函数" << endl;
m_age = age;
m_height = new int(height);
}
//析构函数
~Person() {
cout << "析构函数!" << endl;
if (m_height != NULL)
{
delete m_height;
}
}
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;
}
指针在堆区中,先进后出。调用析构时,先释放p2,所以堆区0x0011这块被释放了。等到p1释放时,这块已经没有了,所以程序浅拷贝时,指针这块会导致程序崩掉。也就是说,浅拷贝的话,堆区的内存会重复释放。
深拷贝
深拷贝就是在堆区重新申请空间,进行拷贝操作,解决浅拷贝重复释放堆区内存的问题。自己实现一个拷贝构造函数,在堆区新开一块内存来存放相同的数据,这样释放时不会干涉。也就是在堆区新开一块内存,存放160,p2存放的是这个新内存的地址。
//添加拷贝构造函数
Person(const Person& p) {
cout << "拷贝构造函数" << endl;
//如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
//m_age = p.m_age;//编译器默认实现的就是这行代码
m_height = new int(*p.m_height);
}
总结:
浅拷贝就是编译器提供的值拷贝,”m_age = p.m_age;“这种等号赋值操作。
浅拷贝会导致堆区数据重复释放的问题。
深拷贝就是自己实现拷贝构造函数,重新在堆区创建一块内存,实现拷贝操作,”m_height = new int(*p.m_height);“。
使用析构函数的场景:如果在堆区开辟了内存,那么需要通过析构代码将堆区的内存释放干净。