前情回顾
上回说到,我踏入C++古塔,在第一层中了解到了面向对象的第一大特性——封装,通过努力,我成功获得封装的力量,并且上到了第二层…
🚄上章地址:第一层:封装
对象特性——对象的初始化和清理
踏入第二层当中,那道声音再次飘来:“挑战者,这层的任务需要你掌握对象特性中的初始化和清理,祝你好运…”一块石板再次出现在我的眼前
构造函数和析构函数
什么是构造函数和析构函数
在C++中,对象在被调用和销毁的时候,会默认调用两个函数,在创建对象的时候,会调用构造函数,在销毁的时候会调用析构函数,它们在一个对象中,只会被调用一次,所以构造函数和析构函数是必须存在的,当程序员本身不提供任何的构造和析构函数的时候,编译器会提供默认构造函数
构造函数
构造函数的语法
构造函数没有返回值,也不用写void
构造函数的函数名与类名相同
构造函数可以有参数,可以发生函数重载1
基本语法了解,那构造函数就可以尝试的去写一下。
class A { public: A() { cout << "A的构造函数" << endl; } string a; }; int main() { A c; }
构造函数的分类
按照参数分类
按照参数可以分为:无参构造函数(默认构造函数)和有参构造函数,它们的区别在于函数有无参数。
按照类型分类
按照类型可以分为:普通构造函数和拷贝构造函数。
拷贝构造函数
上面提到了拷贝构造函数,那拷贝构造函数是什么?从表面来看,在构造的同时拷贝,那拷贝什么?数据吗?拷贝到哪里去呢?这里我们先将拷贝构造函数的语法写罗列出来。
类名(const 类名 引用2)
可以看到,它的参数是我们的对象,那它的作用大致就可以猜到了,就是将对象的数据拷贝到调用拷贝构造函数的对象上面去,可以用代码来试验一下。
#include<string> #include<iostream> using namespace std; class A { public: A() { cout << "A的构造函数" << endl; } A(A &c) { b = c.b; } int b; }; int main() { A c; cin >> c.b; A b(c); cout<< b.b << endl; }
可以看到c里面的内容拷贝到了b里面。
拷贝函数的调用时机
通常在三种情况下调用我们的拷贝函数:
1.使用一个已经创建完毕的对象来初始化一个新对象,可以参考上面
2.值传递的方式给函数参数传值
3.以值的方式返回局部变量
对于第二点,可以看下方这张图来理解
int main函数内部调用test1函数,函数参数为类,在调用函数时候,会调用拷贝构造函数,然后在回去调用test1函数内部。
而第三点,是因为函数中,当返回的值是我们的对象时,因为出了函数体,局部变量销毁,所以函数内对象会进行销毁,这个时候就会发生拷贝,将函数内的对象数据重新拷贝到一块新的空间内,进行返回。
深拷贝和浅拷贝
浅拷贝是进行简单的拷贝操作,只会进行值拷贝
深拷贝是在堆区重新申请空间,进行拷贝
那为什么会出现深拷贝和浅拷贝呢?当类内的属性有指针的时候,这个时候发生拷贝,会拷贝的是地址。
因为指针内存放数据之前,我们要使用new3函数进行开辟空间,这个时候我们就需要在析构函数内使用delete4释放空间,但是因为两个对象都同时指向了同一块空间,就会释放两个同一块空间的地址,对堆区进行了重复释放,那这个时候想要解决,就需要程序员自己设计出拷贝构造函数,进行深拷贝,对指针的拷贝用new操作符重新开辟一块空间。
深拷贝
class A { public: A() { cout << "A的构造函数" << endl; } //浅拷贝 A(A &c) { b=c.b; d=c.d; } //深拷贝 A(A &c) { cout << "拷贝构造函数" << endl; b = c.b; d = new int(*c.d); } int b; int* d; };
构造函数的调用方法
括号法
括号法在上面我们进行拷贝构造函数调用时已经用过,就是在对象后面加括号,括号内是参数,但是这个时候要注意!对于无参构造函数而言,是在后面加括号的,这样编译器会认为这一个函数声明
在编译器眼中,加括号的就会认为是这样。
显示法
显示法是将我们的构造函数当赋值一样,书写方式如下:
类名 对象名 = 类名(参数);
函数名(参数)叫做匿名对象,特点是当执行结束后,系统会立即回收匿名对象。
注意!:不要利用拷贝构造函数去初始化匿名对象,这个时候编译器会认为这是一个函数声明。
隐式转换法
隐式转换法是将我们的显示法中等号右侧的类名省略,书写方式如下:
类名 对象名 =参数;
构造函数规则
在默认情况下,C++编译器至少会给一个类添加三个函数:
1.默认构造函数(无参)(内部空实现)
2.默认析构函数(内部空实现)
3.默认拷贝构造函数,对属性进行浅拷贝
构造函数规则如下:
如果程序员定义有参构造函数,C++不在提供默认构造函数(无参),但是会提供默认拷贝构造函数,如果程序员定义拷贝构造函数,C++则不会提供任何构造函数。
析构函数
析构函数的语法
没有返回值,也不需要写void
函数名和类名相同,但是需要在名称前加~
析构函数不可以有参数,不能发生重载
class A { public: A() { cout << "A的构造函数" << endl; } //析构函数 ~A() { } string a; }; int main() { A c; }
析构函数的作用
当类内调用new操作符开辟空间时,需要在析构函数内部进行释放空间的操作,用delete操作符,最好在将指向new操作符开辟的空间的指针,置空,防止野指针。
初始化列表
作用
对类内的属性进行初始化操作
语法
构造函数():属性1(初始值),属性2(初始值)…
class man { public: //构造函数,初始化列表 man() :_name("狗蛋"), age(18), car("鬼火") { } //属性 string _name;//名字 int age;//年龄 string car;//汽车 };
这是最基础的用法,还可以和有参构造函数进行结合:
#include<string> #include<iostream> using namespace std; class man { public: //构造函数,初始化列表 man(string a, int b, string c) :_name(a), age(b), car(c) { } //属性 string _name;//名字 int age;//年龄 string car;//汽车 }; int main() { man m1("狗蛋", 18, "鬼火"); return 0; }
也可以这样去进行初始化。
类对象作为类成员
在C++中,类中的成员可以是另一个类的对象,这个时候称这个成员为对象成员:
class A { }; class B { A a; };
上述代码中,A a就是对象成员,在我们创建B类的对象时,A a为对象成员,要先有成员才能有这个类,所以会先调用A的构造函数,在调用B的构造函数,但是在解析的时候时相反的,先析构B,在析构A。
静态成员
静态成员变量
静态成员变量,是将我们的成员变量放入到静态区,需要在成员的类型前面加static关键字。
静态成员变量特点
1.因为放入到了静态区,所以所有对象是共享同一份数据的,不属于任何的一个对象了
2.会在编译阶段就分配内存,相当于在运行之前就已经分配好内存了,分配在全局区
3.类内声明,类外初始化,比如有一个初始值才可以用,或者就无法访问到那片内存空间。
对于静态成员变量的初始化我们可以这样:
class A { static int a; } int A::a=//初始值;
也是因为上述特点,静态成员不属于任何对象,所有有了两种访问方式:
1.通过对象进行访问,访问方式:对象名.成员名
2.通过类名进行访问,访问方式:类名:: 成员名
静态成员变量也是遵循访问权限的。
静态成员函数
访问方式
与静态成员变量的访问方式是一样的。
注意事项
1.静态成员函数可以访问静态成员变量
2.静态成员函数不可以访问非静态的成员变量
3.放访问非静态的时候,这个时候静态成员函数是不知道修改的是哪个对象的,如果修改,它会把所有对象的这个属性进行修改
静态成员函数同样遵循访问权限。
掌握对象初始化和清理,步入第三层
随着面前石板的倒下,第三层的楼梯显现在了我的眼前…
本章知识点(图片形式)
😘预知后事如何,关注新专栏,和我一起征服C++这座巨塔
🚀专栏:C++爬塔日记
🙉都看到这里了,留下你们的👍点赞+⭐收藏+📋评论吧🙉
出现的陌生名词解释
看到函数重载,可能有些人就懵了,这里解释一下,在C++中,函数名是可以重复的,这个时候参数不同,或者参数的顺序不一样,都是可以的,但是如果光返回类型不同,参数相同,则不能发生函数重载,编译器会报错。 ↩︎
引用,换一种说法就是取别名,比如,我有一个同学,他叫做李鑫,那我们可能会给他取个外号叫做李三金,当说到这个外号的时候,我们会想到李鑫这个人,所以引用是不会在去内存当中重新开辟一块空间的,它和它引用的变量是用了同一块空间,它的书写形式为:
数据类型 &变量名=要引用的变量名;
还要注意,对于引用来说,我们不能对其进行第二次改变,如果aa是a的别名,那aa就不能在改变成别的变量的别名了,对于引用来说,引用就等于我们的指针加const,,把const加在我们的解引用右侧就等于我们的引用。 ↩︎
new操作符是C++用于内存开辟的函数,它的书写形式为:
指针=new 和指针同类型 (这里可以直接赋值);
指针=new 和指针同类型 [开辟多少个];
上面第一种为开辟一个变量空间,第二个为开辟数组空间。 ↩︎
delete操作符,用于对new申请下来的空间进行空间释放,书写形式:
delete(指针); ↩︎