4.2 对象的初始化和清理
4.2.1 构造函数和析构函数
对象的初始化和清理也是两个非常重要的安全问题
一个对象或者变量没有初始状态,对其使用后果是未知
同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题
C++利用构造函数和析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造函数和析构函数,编译器会提供
编译器提供的构造函数和析构函数是空实现
- 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用
- 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作
构造函数语法:类名(){}
- 构造函数,没有返回值也不写void
- 函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
析构函数语法:~类名(){}
- 析构函数,没有返回值也不写void
- 函数名称与类名相同,在名称前加上符号~
- 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
//对象的初始化和清理
//1、构造函数 进行初始化操作
class Person{
public:
//1、构造函数
Person(){
cout << "Person 构造函数的调用" << endl;
}
//2、析构函数 进行清理的操作、
~Person(){
cout << "Person 析构函数的调用" << endl;
}
};
//构造和析构都是必须有的视线,如果我们自己不提供,编译器会提供一个空实现的构造和析构
void test01(){
Person p;//在栈上的数据,test01执行完毕后,释放这个对象
}
int main(){
// test01();
Person p;
return 0;
}
4.2.2 构造函数的分类及调用
两种分类方式:
- 按参数分为:有参构造和无参构造
- 按类型分为:普通构造和拷贝构造
三种调用方式:
- 括号法
- 显示法
- 隐式转换法
/*
两种分类方式:<br>
- 按参数分为:有参构造和无参构造
- 按类型分为:普通构造和拷贝构造
*/
class Person
{
private:
/* data */
public:
int age;
//构造函数
Person() //无参构造(默认构造)
{
cout << "Person的无参构造函数调用" << endl;
}
Person(int a)
{
age = a;
cout << "Person的有参构造函数调用" << endl;
}
//拷贝构造函数
//因为需要拷贝传进来的数据,因此改数据不能更改,因此加上const,并以引用的方式
Person(const Person &p){
//将传入的人身上的属性,拷贝到我身上
age = p.age;
cout << "Person的拷贝构造函数调用" << endl;
}
~Person()
{
cout << "Person的析构函数调用" << endl;
}
};
//调用
void test01(){
//1、括号法
// Person p1; //默认构造函数调用
// Person p2(10);
// Person p3(p2);
// // cout << "p1的年龄为:" << p1.age << endl;
// // cout << "p2的年龄为:" << p2.age << endl;
// // cout << "p2的年龄为:" << p3.age << endl;
// //注意事项
// //调用默认构造函数时候,不要加()
// Person P(); //不会调用构造函数,因为编译器会认为是一个函数的声明
//2、显示法
// Person p1;
// Person p2 = Person(10); //有参构造
// //拷贝构造函数
// Person p3 = Person(p2);
// //等号的右边 Person(10); 被叫做“匿名对象”,
// //特点:当前行执行结束后,系统会立即回收掉匿名对象
// Person(10); //匿名对象
// cout << "aaa" << endl;
/*
可以看到,Person(10)调用的是有参构造函数,并且执行完这一行之后,立马调用析构函数
Person的无参构造函数调用
Person的有参构造函数调用
Person的拷贝构造函数调用
Person的有参构造函数调用
Person的析构函数调用
aaa
Person的析构函数调用
Person的析构函数调用
Person的析构函数调用
*/
//注意事项
//不要利用拷贝构造函数 初始化匿名对象
// Person(p3); 错误,编译器会认为Person(p3) === Person p3,会导致p3重定义
//3、隐式转换法
Person p4 = 10; //相当于Person p4 = Person(10);
Person p5 = p4; //隐式转换法的拷贝构造函数调用
}
int main(){
test01();
}
4.2.3 拷贝构造函数调用时机
三种情况:
- 使用一个已经创建完毕的对象来初始化一个新对象
- 值传递的方式给函数参数传值
- 以值方式返回局部对象
// - 使用一个已经创建完毕的对象来初始化一个新对象
void test01(){
Person p1(2000);
cout << "p1的年龄为:" << p1.m_age << endl;
Person p2(p1);
cout << "p2的年龄为:" << p2.m_age << endl;
}
// - 值传递的方式给函数参数传值
void func(Person p){
p.m_age = 100;
}
void test02(){
Person p(10);
func(p);
cout << p.m_age << endl;
}
// - 以值方式返回局部对象
Person func2(){
Person p1;
cout << (long long)&p1 << endl;
return p1;
}
void test03(){
Person p = func2();
cout << (long long)&p << endl;
}
int main(){
// test01();
// test02();
test03();
/*test03()的结果
Person默认构造函数调用
600821464652
600821464652
Person析构函数调用
*/
}
4.2.4. 构造函数调用规则
默认情况下,c++编译器至少给一个类添加3个函数:
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下:
- 如果用户定义有参构造函数,c++不再提供默认无参构造函数,但是会提供默认拷贝构造
void test01(){
Person p;
p.m_age = 18;
Person p2(p); //不写拷贝构造函数的话,系统会自动提供一个拷贝构造函数,并进行值传递
cout << "p2的年龄为:" << p2.m_age << endl;
/*
Person默认构造函数调用
p2的年龄为:18
Person析构函数调用
Person析构函数调用
*/
}
- 如果用户定义拷贝构造函数,c++不会再提供其他构造函数
总结一下:用户提供了拷贝构造函数,必须再提供默认构造函数和有参构造函数;用户提供了有参构造函数,就必须提供默认构造函数。
4.2.5 深拷贝与浅拷贝
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间,进行拷贝操作
~Person(){
//析构代码,将堆区开辟的数据做释放操作
if(m_height != NULL){
delete m_height;
m_height = NULL;
}
cout << "Person析构函数调用" << endl;
}
//自己实现拷贝构造函数,解决浅拷贝带来的问题
Person(const Person &p){
cout << "Person 拷贝构造函数调用" << endl;
m_age = p.m_age;
m_height = new int(*p.m_height);
}
void test01(){
Person p1(18, 160);
cout << "p1的年龄为:" << p1.m_age << endl;
cout << "p1的身高为:" << *p1.m_height << endl;
cout << "p1的身高为:" << p1.m_height << endl;
Person p2(p1);
cout << "p2的年龄为:" << p2.m_age << endl;
cout << "p2的身高为:" << *p2.m_height << endl;
cout << "p2的身高为:" << p2.m_height << endl;
/*
如果利用编译器提供的拷贝构造函数,会做浅拷贝操作
浅拷贝带来的问题就是堆区的内存重复释放
*/
}
如果用浅拷贝的话,p1和p2指向的m_height的地址是相同的,
并且在执行析构函数的时候,先执行p2的析构函数,此时p2.m_height指向的内存已经被释放了,
而后再执行p1的析构函数,此时p1.m_height指向的内存已经被释放了,就会造成非法访问。
如果用深拷贝的话,p1和p2指向的m_height的地址是不同的,这样在进行析构的时候就不会报错了。
总结:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题
4.2.6 初始化列表
作用:c++提供了初始化列表语法,用来初始化属性
语法:构造函数():属性1(值1),属性2(值2)…{}
//初始化列表
Person():m_age(18), m_height(160){
cout << "Person初始化列表构造函数调用" << endl;
}
void test01(){
Person p();
cout << p.m_age << " " << p.m_height << endl;
}
//初始化列表
Person(int a, int b):m_age(a), m_height(b){
cout << "Person初始化列表构造函数调用" << endl;
}
void test01(){
Person p(18, 160);
cout << p.m_age << " " << p.m_height << endl;
}
以上两种方法都可以
4.2.7 类对象作为类成员
c++类中的成员可以是另一个类的对象,我们称该成员为 对象成员
例如:
class A{};
class B{
A a;
};
B类中有对象A作为成员,A为对象成员
那么当创建B对象时,A与B的构造和析构的顺序是谁先谁后?
当其他类的对象作为本类的成员的时候,会先调用其他类的构造函数;析构的顺序与之相反
例如上面的会先调用A的构造函数
4.2.8 静态成员
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员
静态成员分为:
- 静态成员变量
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
//静态成员变量
class Person{
public:
/*
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
*/
static int m_age;
//静态成员变量也是有访问权限的
private:
static int m_A;
};
//告知编译器,这是Person类作用域下的成员
//类内声明,类外初始化
int Person::m_age = 100;
int Person::m_A = 1000;
void test01(){
Person p1;
cout << p1.m_age << endl; //100
Person p2;
p2.m_age = 200;
cout << p2.m_age << endl; //200
//因为m_age是所有对象共享的一份数据,p2将其修改为200,p1对象的m_age同样也变成了200
}
void test02(){
//静态成员变量 不属于某个对象上,所有对象都共享同一份数据
//因此静态成员变量有两种访问方式
//1、通过对象访问
Person p;
cout << p.m_age << endl;
//2、通过类名进行访问
cout << Person::m_age << endl;
// cout << Person::m_A << endl; 错误,类外访问不到私有静态成员变量
}
- 静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
//静态成员函数
class Person{
public:
/*
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
*/
static void func(){
m_A = 100;
// m_B = 200; //静态成员函数不能访问非静态成员变量
cout << "static void func 调用" << endl;
}
static int m_A; //静态成员变量
int m_B; //非静态成员变量
//静态成员函数也是有访问权限的
private:
static void func2(){
cout << "static void func2 调用" << endl;
}
};
int Person::m_A = 0;
void test01(){
//1、通过对象访问
Person p1;
p1.func();
//2、通过类名访问
Person::func();
// Person::func2(); 错误,类外访问不到私有的静态成员函数
}