第一部分 --- 对象的初始化和清理
1.对象也有初始化设置和销毁对象前的数据清理工作
那么我们要如何实现对象的初始化和清理呢?答案就是我们要用到以下两个函数:
构造函数和析构函数
1.对对象的初始化和数据清理是我们必须要做的工作,如果我们不提供构造函数和析构函数的话,编译器就会调用其内置的构造函数和析构函数
2.但是编译器提供的构造函数和析构函数是空实现,也就是说关于函数实现的花括号里一行代码也没有。
3.构造函数:给对象的成员属性赋初始值 ; 析构函数:执行一些数据清理工作
1.一个类的构造函数是写在类里面的
2.写构造函数前也需要规定他的权限
3.构造函数在对象被调用的时候就会自动调用而且只调用一次,不需要我们手动调用
1.一个类的析构函数也要写到他所属的类中
2.写析构函数前也需要先规定他的权限
3.析构函数会在对象销毁前自动调用且只会调用一次
第二部分 --- 构造函数的分类及调用
1.拷贝构造以外的都是普通构造
无参构造又被称为 --- 默认构造函数
上面和下面那个都是普通构造,而中间那个就是拷贝构造了
所谓的拷贝构造其实就是将一个已经给成员属性赋值的对象作为参数传给构造函数() --- 采用引用传参的方式来接收参数 ---- 然后再在函数中通过别名+点操作符的方式将拷贝过来的对象的成员属性一一对应的赋值给类中的成员属性
拷贝构造中对于传给构造函数的对象的参数的要求:
1.要用const修饰,因为我们不会对传过来的参数进行写
2.传过来的对象的类名要和构造函数所在的类的类名相同
3.要用引用传参的形式
接下来讲如何调用构造函数
1.创建对象的时候,对象的后面无括号的话调用无参构造函数 --- 即默认构造函数
2.创建对象的时候,对象的后面有括号且括号内的参数类型不是和我们创建的对象a同一个类的对象b的话 --- 则调用有参构造函数,反之则调用拷贝构造函数
3.括号法调用默认构造函数就是在对象后面不加括号
如果加了括号且不传参的话,编译器就会认为这段代码是一个函数的声明。
1.显示法调用默认(无参)构造函数的方法和括号法一致,就是直接创建对象然后什么也不加。
2.显示法调用有参构造和拷贝构造的格式是 :
类名 对象名 = 类名(相同类的另一个对象(拷贝构造)/传与左边那个不同的参数(有参构造))
类名(参数) --- 这个单独拿出来的话就称其为 匿名对象 --- 即找不到对象名
而我们把它放在等号的右边,且等号的左边就是一个已经创建好的对象(有对象名) --- 这样一行代码的意思是:我们给这个匿名对象(没有对象名)赋予了对象名 --- 即左边那个我们创建好的对象的对象名
匿名对象的特点就是:匿名对象所在的代码行一结束,匿名对象的相关数据就会被回收销毁 --- 就会调用析构函数(对象被回收和销毁时自动调用的函数,且只会调用一次)
注意!匿名对象也是一个对象,只不过它没有对象名
由于它是一个对象,所以对象的相关知识点依然适用于它
所以它也可以用括号法调用构造函数,所以 Person(10) ---这行代码的意思就是匿名对象调用构造函数
1.如果利用拷贝构造函数初始化匿名对象的话,就会变成这样 --- Person( p3 ) ;--- 这行代码会被编译器识别为这个 ---- Person p3 ; ---- 即编译器会将其识别为我们在创建一个对象,而不是在给匿名对象初始化。
1.隐式转换法其实就是显示法的优化版 --- 写下面这个代码的时候
编译器会自动将其隐式转换为下面这行代码:
2.隐式转换法调用默认(无参)构造函数的方法是:
直接创建对象,啥也不加就行
三个方法调用默认(无参)构造函数的方式都是上面这个
第三部分 --- 拷贝构造函数的调用时机
当实参以值传递的方式传给函数形参的时候 --- 形参是实参的一份临时拷贝,这份临时拷贝的实现代码就是 --- 以上面那个为例 --- Person p(形参) = p(实参) --- 这个代码其实就是在用隐式转换法调用拷贝结构函数给形参对象初始化
综上,对象以值传递的方式传给函数形参的时候,函数形参是对象的一份临时拷贝,形参对象的初始化是通过拷贝结构函数实现的
当我们以值方式返回局部对象时,它不是直接返回局部对象这一个操作,首先它会在调用函数的那行代码处创建一个匿名对象,这个匿名对象执行了拷贝结构函数进行了初始化,且这个拷贝结构函数的参数是我们返回的局部对象
1.如果在调用函数处我们有创建对象来接收这个匿名对象的话:
Person p = dowork(); --- 这就相当于接收匿名对象了,则相当于给匿名对象赋予了对象名,这个对象名就是承接它的对象的对象名。
被赋名后的匿名对象将不会在其所处的代码行结束后就被回收和销毁,其数据都会被保存下来,我们可以根据它新获得的对象名+点操作符+要访问的成员的方式访问和操作匿名对象中的数据
关于承接它的对象的要求:要和匿名对象同一类
2.如果没有创建对象去接收的话
这个匿名对象就会在它所处的代码行结束的时候自动被回收和销毁(匿名对象的特点)
第四部分 --- 构造函数调用规则
关于默认拷贝构造函数:
1.它有函数形参 --- 这个函数形参是个对象,这个对象的类和函数所在类一样
2.其函数实现就是将形参对象所有的成员属性赋值给类中的成员属性(由于是同一个对象,所以形参的对象的成员属性和类的成员属性能够一一对应赋值)
关于函数调用规则:
(写在开头 --- 如果我们想调用构造函数的话有两种途径一种是调用我们自己写的各种构造函数,另一种就是在我们没有写构造函数的时候,c++踢狗的默认构造函数,当我们只写了一部分构造函数的时候,编译器则要通过下面这些函数调用规则来判断该用那种方式调用函数)
1.如果我们定义了有参构造函数的话,c++不再提供默认无参构造和默认有参构造,不过依然会提供默认拷贝构造函数;
也就是说当我们写了有参构造函数,且没有写默认(无参)构造函数的话,通过类创建对象的时候将再也无法调用默认(无参构造函数 ---- 因为已经没有了)
2.如果我们写了一个拷贝构造函数的话,编译器就不会再提供任何一种构造函数了
下面这个就是函数调用规则表
默认无参构造函数 | 默认有参构造函数 | 默认拷贝构造函数 | |
无参(默认)构造函数 自己写时 | 不提供 | 提供 | 提供 |
有参构造函数 自己写时 | 不提供 | 不提供 | 提供 |
拷贝构造函数 自己写时 | 不提供 | 不提供 | 不提供 |
第五部分 --- 深拷贝和浅拷贝
上面的第一个函数是有参构造函数,下面的函数是析构函数
类的成员属性可以是指针,这个成员指针的作用一般是用来承接我们用new在堆区开辟完内存空间后返回的地址或者我们直接赋予其地址
接下来我们讨论一个情景 --- 就是我们在构造函数中用new在堆区中开辟内存空间并存储数据后,用我们的成员指针接收new返回来的地址,然后构造函数结束。
但是构造函数结束之后没有解决一个问题,那就是我们在堆区的开辟的内存空间没有被释放!!
关于这个问题我们首先要回答的是什么时候释放这块内存空间? --- 答案是创建的对象被销毁前
欸既然讲到对象被销毁前,那就一定要提到在对象被销毁前会被自动调用的函数 --- 析构函数
没错,堆区开辟的内存空间的释放操作就是在析构函数中进行的
我们一般都会在析构函数中写:
delete 承接堆区内存空间地址的指针名 ;
这行代码来释放内存空间,如上图
不过直接写是不标准的,标准的流程应该是:
1.判断该指针是不是空指针,如果不是就进行delete释放
(delete释放数组时需要加上【】来告诉计算机我们释放的是数组 --- delete[ ] arr --- new开辟完数组空间后返回的是堆区中存放数组首元素的内存空间的地址)
2.释放完后为了避免指针成为野指针导致程序错误,我们还需要将该指针置为空 -- NULL
做完这两个流程之后才能够称为释放内存空间完毕
所谓的浅拷贝操作就是将形参对象的成员属性一个字节一个字节拷贝给新的对象
此时如果我们用浅拷贝的方式将成员指针也一个字节都不落的全都拷贝到新的对象的话就会出现一种情况:形参对象的成员指针和新对象的成员指针中所存放的地址是一样的!!它们指向的都是同一个内存空间
这个时候就会出现一个问题:当我们在销毁俩个对象时,两个对象都会调用析构函数,而析构函数中包含了对堆区中创建的内存空间的释放。
假设形参对象的成员指针指向的就是堆区中的一块内存空间,若采用浅拷贝的方式的话,新对象也会指向同一块内存空间。那么当我们销毁形参对象时会调用一次析构函数,其成员指针指向的内存空间被释放,而当我们销毁新对象的时候又会调用一次析构函数,此时就会出现一个现象:
我们对已经释放过的内存空间进行再一次进行释放!!!
这种行为显示是非法的,是会导致程序崩溃的!
那么我们要怎么去解决这个问题呢?
答案是浅拷贝出现的问题要用深拷贝来解决
深拷贝就是不像c++默认拷贝函数那样全都用值拷贝的方式,而是在堆区中开辟自己的内存空间,然后存相同的数据 --- 通过这种方式就能够有效避免内存空间重复释放的问题 --- 每个人都释放自己的内存空间,就不会重复释放了
上面这个就是深拷贝(浅拷贝的话就只是单纯的值拷贝,而深拷贝则自己创建自己的内存空间)