C++学习笔记(十二)
一、对象的初始化和清理
生活中我们买的电子产品都基本会有出厂设置,在某一天我们不用的时候也会删除一些自己信息数据保证安全
C++中的面向对象来源于生活,每个对象也都会有初始设置以及对象销毁前的清理数据的设置
1.1 构造函数和析构函数
对象的初始化和清理是两个非常重要的安全问题
一个对象或者变量没有初始状态,对其使用后果是未知的
使用完一个对象或者变量,没有及时清理,也会造成一定的安全问题
C++利用了构造函数和析构函数解决了上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。
对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供编译器提供的构造函数和析构函数是空实现。
构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用。
析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
构造函数语法:
1. 构造函数,没有返回值也不写void
2. 函数名称与类名相同
3. 构造函数可以有参数,因此可以发生重载
4. 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
析构函数语法:
1. 析构函数,没有返回值也不写void
2. 函数名称与类名相同,在名称前加上符号~
3. 析构函数不可以有参数,因此不可以发生重载
4. 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
#include <iostream>using namespace std;class Person
{
public:// 构造函数Person(){cout << "构造函数已调用......" << endl;}// 析构函数~Person(){cout << "析构函数已调用......" << endl;}
};void test()
{Person p;
}int main(int argc, char* argv[])
{test();return 0;
}
1.2 构造函数的分类及调用
两种分类方式:
按参数分为:有参构造和无参构造
按类型分为:普通构造和拷贝构造
三种调用方式:
括号法
显示法
隐式转换法
#include <iostream>using namespace std;class Person
{
public:// 无参构造函数(普通构造函数)Person(){cout << "无参构造函数已调用......" << endl;}// 有参构造函数(普通构造函数)Person(int p_age){age = p_age;cout << "有参构造函数已调用......" << endl;cout << "年龄:" << age << endl;}// 拷贝构造函数Person(const Person &p){age = p.age;cout << "拷贝构造函数已调用......" << endl;cout << "拷贝的年龄为:" << age << endl;}// 析构函数~Person(){cout << "析构函数已调用......" << endl;}int age;
};void test1()
{// 1. 括号法// 调用无参构造函数Person p1;// 调用有参构造函数Person p2(23);// 调用拷贝构造函数Person p3(p2);// 注意事项// 调用默认构造函数的时候,不能加(),因为下面这行代码,编译器会认为是一个函数的声明,不会认为在创建对象// Person p1();
}void test2()
{// 2. 显示法// 调用无参构造函数Person p1;// 调用有参构造函数Person p2 = Person(23);// 调用拷贝构造函数Person p3 = Person(p2);// Person(23); 匿名函数 特点:当前行执行结束后,系统会立即回收掉匿名函数// 注意事项// 不要利用拷贝构造函数初始化匿名对象,编译器会认为 Person(p6) ===== Person p6;对象声明// Person(p6)
}void test3()
{// 3. 隐式转换法// 调用无参构造函数Person p1;// 调用有参构造函数Person p2 = 23;// 调用拷贝构造函数Person p3 = p1;
}int main(int argc, char* argv[])
{test1();cout << "------------------------" << endl;test2();cout << "------------------------" << endl;test3();cout << "------------------------" << endl;return 0;
}
1.3 拷贝构造函数调用时机
C++中拷贝构造函数调用的时机通常有三种情况
1. 使用一个已经创建完毕的对象来初始化一个新对象
2. 值传递的方式给函数参数传值
3. 以值方式返回局部对象
#include <iostream>using namespace std;class Person
{
public:Person(){cout << "无参构造函数已调用......" << endl;}Person(int p_age){age = p_age;cout << "有参构造函数已调用......" << endl;cout << "年龄:" << age << endl;}Person(const Person &p){age = p.age;cout << "拷贝构造函数已调用......" << endl;cout << "拷贝的年龄为:" << age << endl;}~Person(){cout << "析构函数已调用......" << endl;}int age;
};// 1. 使用一个已经创建完毕的对象来初始化一个新对象
void test1()
{Person p1(23);Person p2(p1);
}// 2. 值传递的方式给函数参数传值
void doWork(Person p)
{}void test2()
{// 创建一个无参构造函数Person p;// 实参传递给形参的时候会创建一个拷贝构造函数doWork(p);
}// 3. 值方式返回局部对象
Person doWork2()
{// 创建一个无参构造函数Person p1;// 局部对象以值的方式返回时,会创建一个新的拷贝构造函数return p1;
}
void test3()
{Person P = doWork2();
}int main(int argc, char* argv[])
{test1();cout << "------------------------" << endl;test2();cout << "------------------------" << endl;test3();cout << "------------------------" << endl;return 0;
}
1.4 构造函数调用规则
默认情况下,C++编译器至少给一个类添加三个函数:
1. 默认构造函数(无参,函数体为空)
2. 默认析构函数(无参,函数体为空)
3. 默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下:
如果用户定义有参构造函数,C++不在提供默认无参构造,但是会提供默认拷贝构造
如果用户定义拷贝构造函数,C++不会再提供其他构造函数
1.5 深拷贝和浅拷贝
深浅拷贝是面试经典问题,也是常见的一个坑
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新深浅空间,进行拷贝操作
#include <iostream>using namespace std;class Person
{
public:Person(int m_age){age = m_age;cout << "有参构造函数已调用......" << endl;}~Person(){cout << "析构函数已调用......" << endl;}int age;
};void test()
{Person p1(18);cout << "p1的年龄为:" << p1.age << endl;Person p2(p1);cout << "p2的年龄为:" << p2.age << endl;
}int main(int argc, char* argv[])
{test();return 0;
}
利用系统提供的默认拷贝构造函数进行普通的数据拷贝不会报错
但是当手动开辟堆区数据并在析构函数中进行释放时会报错
原因:利用编译器提供的默认拷贝构造函数会做浅拷贝操作,会把p1的成员属性逐字节的拷贝到p2中,p1和p2的指针指向了堆区同一地址的内容,堆栈的数据先进后出,所以p2先进行析构,把指向的堆区内存的内容释放干净,然后p1析构时仍然会对堆区的内容进行释放,暴露出来浅拷贝的问题:堆区的内容重复释放。浅拷贝的问题要利用深拷贝进行解决,再堆区重新申请一块内容用来存放相同的数据,p2指向此内存。
使用系统提供的默认拷贝构造函数(报错):
#include <iostream>using namespace std;class Person
{
public:Person(int m_age,int m_height){age = m_age;height = new int(m_height);cout << "有参构造函数已调用......" << endl;}~Person(){// 由程序员手动开辟的堆区数据也需要程序员手动释放,在堆区数据销毁前释放// 堆区开辟的数据在析构函数执行完后销毁,所以应该析构函数内进行释放// 析构代码,将堆区开辟的数据做释放操作if (height != NULL){delete height;height = NULL;}cout << "析构函数已调用......" << endl;}int age;int* height;
};void test()
{Person p1(18,180);cout << "p1的年龄为:" << p1.age << endl;cout << "p1的身高为:" << *p1.height << endl;Person p2(p1);cout << "p2的年龄为:" << p2.age << endl;cout << "p2的身高为:" << *p2.height << endl;
}int main(int argc, char* argv[])
{test();return 0;
}
使用自己实现的拷贝构造函数:
#include <iostream>using namespace std;class Person
{
public:Person(int m_age,int m_height){age = m_age;height = new int(m_height);cout << "有参构造函数已调用......" << endl;}Person(const Person& p){cout << "拷贝构造函数已调用......" << endl;age = p.age;// height = p.height; 编译器默认实现的就是这行代码 // 深拷贝操作height = new int(*p.height);}~Person(){// 由程序员手动开辟的堆区数据也需要程序员手动释放,在堆区数据销毁前释放// 堆区开辟的数据在析构函数执行完后销毁,所以应该析构函数内进行释放// 析构代码,将堆区开辟的数据做释放操作if (height != NULL){delete height;height = NULL;}cout << "析构函数已调用......" << endl;}int age;int* height;
};void test()
{Person p1(18,180);cout << "p1的年龄为:" << p1.age << endl;cout << "p1的身高为:" << *p1.height << endl;Person p2(p1);cout << "p2的年龄为:" << p2.age << endl;cout << "p2的身高为:" << *p2.height << endl;
}int main(int argc, char* argv[])
{test();return 0;
}
总结:1. 浅拷贝:编译器提供的等号赋值操作 深拷贝:从堆区重新创建一块内容存放数据
2. 析构代码的用处:将手动开辟的堆区数据进行释放操作
3. 拷贝构造函数进行深拷贝可以避免堆区数据重复释放的问题
1.6 初始化列表
作用:C++提供了初始化列表语法,用来初始化属性
语法:构造函数():属性1(值1),属性2(值2)......{}
#include <iostream>
#include <string>using namespace std;class Person
{
public:Person(string m_name,int m_age,int m_height):name(m_name),age(m_age),height(m_height){cout << "构造函数已调用......" << endl;}~Person(){cout << "析构函数已调用......" << endl;}void printInfo(){cout << "姓名:" << name << endl;cout << "年龄:" << age << endl;cout << "身高:" << height << endl;}
private:string name;int age;int height;
};
int main(int argc, char* argv[])
{Person p("张三", 23, 180);p.printInfo();return 0;
}
1.7 类对象作为类成员
C++类中的成员可以是另一个类的对象,我们称该成员为对象成员
构造的顺序是:先调用对象成员的构造,在调用本类的构造
析构的顺序与构造相反
#include <iostream>
#include <string>using namespace std;class Phone
{
public:Phone(string name):phone_name(name){cout << "Phone类的构造函数已调用......" << endl;}~Phone(){cout << "Phone类的析构函数已调用......" << endl;}string phone_name;
};
class Person
{
public:// 隐式转换法:phone(phone_name) ======= Phone phone = phone_namePerson(string name,string phone_name):person_name(name),phone(phone_name){cout << "Person类的构造函数已调用......" << endl;}~Person(){cout << "Person类的析构函数已调用......" << endl;}string person_name;Phone phone;
};void test()
{Person p("张三", "Iphone 15 pro max远峰蓝");
}int main(int argc, char* argv[])
{test();return 0;
}
1.8 静态成员
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员
静态成员分为:
静态成员变量:1. 所有对象共享同一份数据
2. 在编译阶段分配内存
3. 类内声明,类外初始化
静态成员函数:1. 所有对象共享同一个函数
2. 静态成员函数只能访问静态成员变量
静态成员变量:
#include <iostream>using namespace std;class Person
{
public:static int age; // 静态成员变量private:static int height; // 静态成员变量
};// 类内声明,类外初始化
int Person::age = 23;
int Person::height = 180;void test()
{// 所有对象共享同一份数据Person p1;cout << p1.age << endl; // 23 Person p2;p2.age = 10;cout << p1.age << endl; // 10// 静态成员变量两种访问方式// 1. 通过对象cout << p1.age << endl;// 2. 通过类名cout << Person::age << endl;// 静态成员变量也有访问权限// cout << p1.height << endl; p1.height不可访问// cout << Person::height << endl; Person::height不可访问
}
int main(int argc, char* argv[])
{test();return 0;
}
静态成员函数:
#include <iostream>using namespace std;class Person
{
public:static void printInfo(){cout << "age:" << age << endl; // 静态成员函数可以访问静态成员变量age = 100;cout << "age:" << age << endl;// cout << "height:" << height << endl; 报错,不可访问cout << "printInfo静态成员函数已调用......" << endl;}static int age; // 静态成员变量int height; // 非静态成员变量
private:static void writeInfo(){cout << "writeInfo静态成员函数已调用......" << endl;}
};int Person::age = 23;void test()
{ Person p1;// 静态成员函数两种访问方式// 1. 通过对象p1.printInfo();// 2. 通过类名Person::printInfo();// 静态成员函数也有访问权限// p1.writeInfo(); 不可访问// Person::writeInfo(); 不可访问
}
int main(int argc, char* argv[])
{test();return 0;
}