环境:
编译器:CLion2021.3;操作系统:macOS Ventura 13.0.1
文章目录
- 一、构造函数和析构函数
- 1.1 构造函数
- 1.2 析构函数
- 1.3 示例
- 二、构造函数的分类及调用
- 1.1 构造函数的分类
- 1.2 构造函数的调用
- 三、拷贝构造函数调用时机
- 3.1 调用时机
- 3.2 返回值优化
- 四、构造函数调用规则
- 五、深拷贝与浅拷贝
- 5.1 浅拷贝
- 5.2 深拷贝
- 六、初始化列表
- 七、类对象作为类成员
- 八、静态成员
- 8.1 静态成员变量
- 8.2 静态成员函数
地表最强C++系列传送门:
「地表最强」C++核心编程(一)内存分区模型
「地表最强」C++核心编程(二)引用
「地表最强」C++核心编程(三)函数提高
「地表最强」C++核心编程(四)类和对象—封装
「地表最强」C++核心编程(五)文件操作——暂未更新
一、构造函数和析构函数
构造函数和析构函数是必须实现的两个函数,即使自己没有写,编译器也会默认调用默认的,空的构造和析构函数。
1.1 构造函数
构造函数主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
语法: 类名(){}
构造函数的特点:
- 没有返回值,不用写void
- 函数名与类名相同
- 构造函数可以有参数,可以发生重载
- 创建对象时自动调用,且只调用一次
1.2 析构函数
析构函数主要作用在于对象销毁前系统自动调用,执行一些清理工作。
语法: ~类名(){}
析构函数的特点:
- 没有返回值,不用写void
- 函数名与类名相同,前面加个~
- 析构函数不可以有参数,不可以发生重载
- 销毁对象时自动调用,且只调用一次
1.3 示例
class Person {
public:
Person() {
cout << "Person的构造函数" << endl;//自己写了就自动调用自己写的。否则就调用默认的空的构造函数
}
~Person() {
cout << "Person的析构函数" << endl;
}
};
//构造和析构函数都必须实现,自己不提供编译器会提供一个空的构造和析构函数
void test01(){
Person p;
}
int main() {
test01();
return 0;
}
二、构造函数的分类及调用
1.1 构造函数的分类
构造函数按照不同的标准可以分为不同的类型:
按参数分类可分为无参构造函数(也叫做默认构造函数)和有参构造函数;
按类型分类可分为普通构造函数和拷贝构造函数。
⚠️由于创建对象需要调用构造函数,因此构造函数的权限应该是public,否则类外无法调用构造函数,也就无法实例化对象。
⚠️拷贝构造函数的参数需要是常量对象
class Person {
public://要加public作用域,否则默认是private,就无法调用构造析构函数,也就无法创建对象
int age;
Person() {
cout << "Person的无参构造函数" << endl;
}
Person(int a) {
age = a;
cout << "Person的有参构造函数" << endl;
}
Person(const Person &p) {//拷贝构造函数,要用const和&的方式传参
age = p.age;//将传入的人身上的所有属性,拷贝到自己身上
cout << "Person的拷贝构造函数" << endl;
}
~Person() {
cout << "Person的析构函数" << endl;
}
};
1.2 构造函数的调用
构造函数的调用有三种方法,分别是括号法、显示法和隐式转换法。
void test() {
//括号法
Person p1;//默认构造函数调用
Person p2(10);//有参构造函数调用
Person p3(p2);//拷贝构造函数调用
//⚠️括号法调用默认构造函数的时候不要加(),否则编译器会认为这是一个函数的声明
// Person p1();//虽然不报错,但是逻辑错误,编译器会认为有一个返回值类型是Person,名为p1的函数
cout<<"p2的年龄是:"<<p2.age<<endl;//10
cout<<"p3的年龄是:"<<p3.age<<endl;//10
//显示法
Person p1;
Person p2 = Person(10);//有参构造,实际上是将匿名对象赋值给p2
Person p3 = Person(p2);//拷贝构造
// Person(10);//这是一个创建匿名对象,但没有办法使用 特点:当前行执行结束后,系统会立即回收掉匿名对象
// cout<<"匿名对象已经没了"<<endl;
//⚠️不要用拷贝构造函数初始化匿名对象,编译器会认为这是重定义。 Person(p3); 会被转换为 Person p3;
// Person(p3);//err,Redefinition of 'p3'
//隐式转换法
Person p4 = 10;//相当于Person p4 = Person(10); 有参构造
Person p5 = p4;//相当于Person P5 = Person(p4); 拷贝构造
}
三、拷贝构造函数调用时机
3.1 调用时机
C++中拷贝构造函数调用时机通常有三种情况:
- 1使用一个已经创建完毕的对象来初始化一个新对象
- 2值传递的方式给函数参数传值
- 3以值方式返回局部对象
//1使用一个已经创建完毕的对象来初始化一个新对象
void test01() {
Person p1(20);//括号法调用构造函数
Person p2(p1);//用对象p1来初始化p2,实际上就是调用了拷贝构造函数
cout << "p2的年龄:" << p1.mAge << endl;
}
//2值传递的方式给函数参数传值
void doWork1(Person p) {}
void test02() {
Person p;//调用默认构造函数
doWork1(p);//值传递,doWork函数会调用拷贝构造函数给形参临时开辟空间
}
//3以值方式返回局部对象
Person doWork2() {
Person p;//调用默认构造函数
return p;//返回的不是上边的p,因为已经被释放掉了。这里返回的是另一个创建的对象,这个对象是用拷贝构造函数创建的。
}
void test03() {
Person p = doWork2();//CLion测试下不会调用析构函数,这是因为返回值优化技术。
cout << &p << endl;
}
3.2 返回值优化
这里解释一下第3点以值的方式返回对象:doWork2函数中首先定义了一个Person类型变量p,我们假设这个对象的地址是add1,然后该函数返回了对象p。这一步的操作实际上是用拷贝构造函数的方法又创建了一个对象p’,我们假设p’的地址是add2,然后把p作为拷贝构造函数的参数传给p’。而实际返回的是p’而不是p,这两个对象的地址是不同的,是两个对象。
返回值优化是一项编译优化技术,使得返回对象时不必调用拷贝构造函数,经过测试后返回的就是已经创建好的p的地址而不会产生p’。
关于返回值优化,更多的可以参考一下这里:Return value optimization-Wikipedia
四、构造函数调用规则
默认情况下,c++编译器至少给一个类添加3个函数:
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下:
- 如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造
- 如果用户定义拷贝构造函数,c++不会再提供其他构造函数
class Person {
public:
int age;
//无参(默认)构造函数
Person() {
cout << "无参构造函数!" << endl;
}
//有参构造函数
Person(int a) {
age = a;
cout << "有参构造函数!" << endl;
}
//拷贝构造函数
Person(const Person &p) {
age = p.age;
cout << "拷贝构造函数!" << endl;
}
//析构函数
~Person() {
cout << "析构函数!" << endl;
}
};
void test(){
Person p;//若只写有参构造或者拷贝构造,不写无参,此处会报错。因为此行代码会调用无参,但是我们写了有参,编译器就不会提供无参
Person p(28);//若没写无参和有参,只写了拷贝构造,此处会报错。因为写了拷贝构造,编译器就不会提供其他无参和有参
Person p2(p);
cout << "p2的年龄:" << p2.age << endl;
}
五、深拷贝与浅拷贝
5.1 浅拷贝
浅拷贝就是简单的赋值操作,默认的拷贝构造函数进行的就是浅拷贝。
class Person{
private:
string m_name;
int m_age;
public:
Person(const Person& p){//这里其实就是浅拷贝
m_name = p.m_name;
m_age = p.m_age;
}
5.2 深拷贝
从下边这个实例说起,注意这里的身高我定义成了指针,指向的内容是身高
class Person {
public:
int m_age;
int *m_height;//指针,指向的内容是身高
public:
//有参构造函数
Person(int age, int height) {
m_age = age;
m_height = new int(height);//用new创建在堆区
}
//拷贝构造函数
Person(const Person &p) {
m_age = p.m_age;
m_height = p.m_height;//默认的浅拷贝
}
//析构函数
~Person() {//堆区开辟的数据在此时可以释放了
if (m_height != NULL) {
delete m_height;
m_height = NULL;
}
}
};
void test01() {
Person p1(18, 180);//调用有参构造
Person p2(p1);//调用拷贝构造
}
如图,通过有参构造创建了p1,然后通过浅拷贝的拷贝构造创建了p2。而浅拷贝的只是在机械的赋值,因此p2的所有属性都和p1一样,此时他们的身高的指针都是指向同一堆空间的,那么此时他们中任何一个对这块空间的操作会直接影响到另一个人。在test()调用结束后,这两个对象也会调用自己的析构函数来销毁空间。p2调用了自己的析构函数,将0x003这块儿空间释放掉,然后p1才调用自己的析构函数(这里涉及到析构函数的调用时间,简单来说就是先构造的后析构,想想栈的特性就明白了),但此时此空间已经被释放掉了,还要释放那就是非法的。
想解决这个问题,只需要在构造p2的时候,让他的身高指针指向与p1不同的空间即可。因此只需重写拷贝构造函数即可:
Person(const Person &p) {
//如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题。若不自己重写拷贝构造函数,拷贝构造出来的对象和被拷贝的对象指向同一块儿空间。
m_age = p.m_age;
m_height = new int(*p.m_height);//重新申请一块堆区,属于深拷贝
}
这样就不会有重复释放的问题,这就是深拷贝。
六、初始化列表
初始化列表是一种利用构造函数初始化属性的另一种方式。
语法: 构造函数():属性1(值1),属性2(值2)… {}
class Person {
public:
int m_A;
int m_B;
int m_C;
public:
//传统方式初始化
Person(int a, int b, int c) {
m_A = a;
m_B = b;
m_C = c;
}
//初始化列表方式初始化
Person() : m_A(10), m_B(20), m_C(30) {}
Person(int a, int b, int c) : m_A(a), m_B(b), m_C(c) {}
};
七、类对象作为类成员
假设一个类的对象B是另一个类A的成员,那么在调用构造函数的时候,会先调用B的构造函数:
class Phone {
public:
string m_PhoneName;
Phone(string name) {
m_PhoneName = name;
cout << "Phone构造" << endl;
}
~Phone() {
cout << "Phone析构" << endl;
}
};
class Person {
public:
string m_Name;
Phone m_Phone;
//初始化列表可以告诉编译器调用哪一个构造函数 Phone m_Phone = pName;其实是隐式转换法
Person(string name, string pName) : m_Name(name), m_Phone(pName) {
cout << "Person构造" << endl;
}
~Person() {
cout << "Person析构" << endl;
}
void playGame() {
cout << m_Name << " 使用" << m_Phone.m_PhoneName << " 手机! " << endl;
}
};
void test01() {
//当类中成员是其他类对象时,我们称该成员为对象成员
//构造的顺序是 :先调用对象成员的构造,再调用本类构造,析构顺序与构造相反
Person p("张三", "苹果1024");//先构造phone
p.playGame();
}
八、静态成员
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员。包括静态成员变量和静态成员函数。
8.1 静态成员变量
特点:
1.所有对象共享同一份数据,因此不属于某个对象
2.在编译阶段分配内存,全局区
3.类内声明,类外初始化
class Person {
public:
static int m_A; //静态成员变量
private:
static int m_B; //静态成员变量也是有访问权限的
};
//类外初始化,否则无法正常访问
int Person::m_A = 10;
int Person::m_B = 10;//private也需要类外初始化,这是可以的
void test01() {
//静态成员变量两种访问方式,这是由于静态成员变量被所有对象共享
//1、通过对象 非静态成员变量只能这么访问
Person p1;
p1.m_A = 100;
cout << "p1.m_A = " << p1.m_A << endl;//100
Person p2;
p2.m_A = 200;
cout << "p1.m_A = " << p1.m_A << endl; //200 共享同一份数据,不属于某个对象
cout << "p2.m_A = " << p2.m_A << endl; //200
//2、通过类名 非静态成员变量不能这么访问
cout << "m_A = " << Person::m_A << endl;
//cout << "m_B = " << Person::m_B << endl; //err,私有权限访问不到
}
8.2 静态成员函数
特点:
1.所有对象共享同一个函数
2.静态成员函数只能访问静态成员变量
class Person
{
public:
static int m_A; //静态成员变量
int m_B; //只属于对象本身,不能被对象共享
//静态成员函数
static void func()
{
cout << "func调用" << endl;
m_A = 100;
// m_B = 100; //err,不可以访问非静态成员变量,无法区分到底是哪个对象的属性
}
private:
//静态成员函数也是有访问权限的
static void func2()
{
cout << "func2调用" << endl;
}
};
int Person::m_A = 10;
void test01()
{
//静态成员变量两种访问方式
//1、通过对象
Person p1;
p1.func();
//2、通过类名
Person::func();
// Person::func2(); //err,私有权限访问不到
}