11.2 对象的初始化和清理
对象的 初始化 和 清理 是两个非常重要的安全问题
11.2.1 构造函数和析构函数
C++中利用构造函数和析构函数解决上述的安全问题,有以下特性:
(1) 两个函数被编译器自动调用,以完成对象初始化和清理工作
(2) 若用户不提供构造、析构函数,编译器会提供两个函数的空实现,因为初始化和清理是编译器强制用户做的事情
- 构造函数:用于创建对象时为对象的成员属性赋值
- 析构函数:用于对象销毁前执行清理工作
构造函数语法:类名(){}
(1) 构造函数没有返回值,也不写void
(2) 函数名称与类名相同
(3) 构造函数可以有参数,可以发生重载
(4) 程序在调用对象时会自动调用,且仅调用一次
实际上就是一个 和类同名的 、 不写返回值类型的 、 被自动调用的 、 类中的 一个成员函数
析构函数语法:~类名(){}
(1) 析构函数没有返回值,也不写void
(2) 函数名称与类名相同,在名称前加 ~
(3) 构造函数不可以有参数,不可以发生重载
(4) 程序在对象销毁前会自动调用,且仅调用一次
实际上就是一个 以 ~类名 为函数名的 、 不写返回值类型的 、 被自动调用的 、 类中的 一个成员函数
实例:
class Person { // Person类创建
public:
// 1.构造函数:进行初始化操作
Person() {
cout << "Person 构造函数 的调用" << endl;
}
// 2.析构函数:进行清理操作
~Person() {
cout << "Person 析构函数 的调用" << endl;
}
};
void test1() { // 测试函数
Person p_local; // 在函数中创建了一个Person类的实例p_local
}
int main() { // 主函数
test1(); // 调用测试函数
Person p_global; // 在主函数中创建了一个Person类的实力p_global
system("pause");
return 0;
}
/************** 结果显示 **************/
Person 构造函数 的调用
Person 析构函数 的调用
Person 构造函数 的调用
解释:
-
主函数中调用了函数
test1(),在该函数中创建了一个实例p_local,由于p_local是函数的局部变量,存放于栈区,栈区中的局部变量会随着函数调用结束而被销毁,故结果显示调用了Person类的构造函数和析构函数 -
主函数中创建的实例
p_global,由于p_global是全局变量,存放于全局区,在程序结束之后由编译器自动释放,故结果只显示调用了Person类的构造函数(因为还没有结束程序)
11.2.2 构造函数的分类与调用
(一)两种分类方式:
-
按参数分:有参构造 与 无参构造(也叫默认构造)
class Person{ public: Person(){ // 无参构造 } Person(int a){ // 有参构造 } } -
按类型分:普通构造 和 拷贝构造
class Person{ public: Person(int a){ age = a; // 普通构造 } Person(const Person &p){ // 拷贝构造 age = p.age; // 拷贝构造后可写类似于这样的代码进行属性拷贝 } }
(二)三种调用方式:
-
括号法
class Person{ public: Person(){ // 无参构造 } Person(int a){ // 有参构造 age = a; } Person(const Person &p){ // 拷贝构造 age = p.age; } } int main(){ Person p1; // 括号法调用默认构造函数(无参构造函数) Person p2(10); // 括号法调用有参构造函数 Person p3(p2); // 括号法调用拷贝构造函数 cout << "p2的年龄为:" << p2.age << endl; cout << "p3的年龄为:" << p3.age << endl; system("pause"); return 0; }注意:在调用默认构造函数时,无需加括号,否则编译器会认为是一个“返回值为Person,名称为p1的函数声明”,不认为在创建对象
-
显示法
class Person{ public: Person(){ // 无参构造 } Person(int a){ // 有参构造 age = a; } Person(const Person &p){ // 拷贝构造 age = p.age; } } int main(){ Person p1; // 显示法调用默认构造函数(无参构造函数) Person p2 = Person(10); // 显示法调用有参构造函数 Person p3 = Person(p2); // 显示法调用拷贝构造函数 cout << "p2的年龄为:" << p2.age << endl; cout << "p3的年龄为:" << p3.age << endl; system("pause"); return 0; }解释:
Person(10)作为右值时称为 “匿名对象” ,p2 = Person(10)表示将p2作为该匿名对象的名称,在当前行执行结束后,系统会自动回收该匿名对象(不会收p2)注意:不要利用拷贝函数来初始化匿名对象,即类似于
Person(p3),其中p3为拷贝构造函数,因为编译器会认为Person (p3) == Person p3 -
隐式转换法
class Person{ public: Person(){ // 隐式转换法无参构造 } Person(int a){ // 隐式转换法有参构造 age = a; } Person(const Person &p){ // 隐式转换法拷贝构造 age = p.age; } } int main(){ Person p1; // 调用默认构造函数(无参构造函数) Person p2 = 10; // 调用有参构造函数 Person p3 = p2; // 调用拷贝构造函数 cout << "p2的年龄为:" << p2.age << endl; cout << "p3的年龄为:" << p3.age << endl; system("pause"); return 0; }
11.2.3 拷贝构造函数的调用时机
C++中拷贝构造函数的调用时机通常有以下三种情况:(以下方Person类为例)
class Person {
private:
int m_age;
public:
Person() { // 默认构造/无参构造
cout << "Person默认构造函数的调用" << endl;
}
Person(int age) { // 参数构造
m_age = age;
cout << "Person有参构造函数的调用" << endl;
}
Person(const Person& p) { // 拷贝构造
m_age = p.m_age;
cout << "Person拷贝构造函数的调用" << endl;
}
~Person() { // 析构函数
cout << "Person析构函数的调用" << endl;
}
};
-
使用一个已经创建完毕的对象来初始化一个新对象
void test01() { Person p1(20); Person p2(p1); } -
值传递的方式给函数参数传值
void func(Person p) { } void test02() { Person p0; func(p0); }解释:有某函数
func(Person p),在函数test02()中以值传递方式向func()传入参数p0,由于是值传递,实际上是系统拷贝p0的副本作为参数传入func(),对p0拷贝过程中会调用拷贝构造函数 -
以值方式返回局部对象
Person func2() { Person p1; cout << (int*)&p1 << endl; return Person(p1); } void test03() { Person p2 = func2(); cout << (int*)&p2 << endl; }解释:
- 在主函数中调用
test03()函数,创建了一个实例p2用于接收func()返回的p1,由于使用的是值返回,所以系统自动拷贝了一份p1用于返回,在这过程中调用了一次拷贝构造函数; - 为什么在返回值使用Person(p1)而非之间使用p1:在C++11及以上版本中,引入了“移动语义”,即当实例
p1作为右值时,编译器利用移动构造函数来避免不必要的深拷贝,这样会避免一次拷贝工作;此处为体现“值方式返回局部对象”时的拷贝行为,特意使用11.2.2中的显示法强制对返回值进行拷贝,从而调用一次拷贝构造函数 cout << (int*)&p1 << endl;与cout << (int*)&p2 << endl;分别对p1和p2的地址进行了打印,结果为:当返回值为Person(p1)时,两地址不同;当返回值为p1时,两地址相同。原因如下:- 返回值为
Person(p1)时,编译器执行流程为:- 在
func()栈上创建一个局部对象p1 return Person(p1)执行时,编译器创建一个临时对象,使用拷贝构造函数将p1的内容拷贝至该临时变量中- 返回该临时变量
- 在
- 返回值为
p1时,编译器执行流程为:- 在
func()栈上创建一个局部对象p1 return p;执行时,编译器不会创建临时对象,而是直接返回已经在栈上构造好的对象p1
- 在
- 返回值为
- 在主函数中调用
11.2.4 构造函数的调用规则
默认情况下,C++编译器至少给一个类添加3个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下:
- 若用户定义 有参构造函数 ,则C++不再提供 默认构造函数 ,但会提供 默认拷贝构造函数
- 若用户定义 拷贝构造函数 ,则C++不再提高 其他构造函数(包含默认构造、有参构造、默认拷贝构造三种构造函数
11.2.5 浅拷贝与深拷贝
1.定义:
-
浅拷贝:简单的复制拷贝
-
深拷贝:在堆区申请空间进行拷贝
2.实例:
class Person {
public:
int m_age;
int* m_height;
Person() { // 无参构造
cout << "Person类的无参构造函数调用" << endl;
}
Person(int age, int height) {
m_age = age;
m_height = new int(height);
cout << "Person类的有参构造函数调用" << endl;
}
Person(const Person& p) {
m_age = p.m_age;
// m_height = p.m_height;
// 深拷贝操作
m_height = new int(*p.m_height);
cout << "Person类的拷贝构造函数调用" << endl;
}
~Person() { // 析构函数,将堆区开辟的数据进行释放
if (m_height != 0) {
delete m_height;
m_height = NULL;
}
cout << "Person类的析构函数调用" << endl;
}
};
void test01() {
Person p1(20, 175);
cout << "p1的身高为:" << *p1.m_height << endl;
Person p2(p1);
cout << "p2的身高为:" << *p2.m_height << endl;
}
int main() {
test01();
system("pause");
return 0;
}
当拷贝构造函数Person(const Person& p){}未被定义时,系统会崩溃,原因如下:
-
首先,Person类中
m_height属性为int*型,是一个指针变量 -
在
test01()函数中调用Person p1(20,175),实际上是调用了有参构造函数,将20传给m_age,将175存入堆区,该堆区首地址(如0x0011)传给m_height -
之后调用默认构造函数(未定义拷贝构造函数的情况下)
Person p2(p1),由于使用编译器所拷贝构造函数,会做浅拷贝操作,所以拷贝效果为:p2.m_age = p1.m_age = 20 p2.m_height = p1.m_height = 0x0011 -
在主函数执行完
test01()后,编译器将栈上的p1、p2变量释放,会调用Person类的析构函数~Person(){} -
由先进后出原则可知先释放p2变量,执行析构函数:
p2满足p2.m_height != 0,故释放0x0011指向的堆区,并将该指针指向空 -
再释放
p1变量,执行析构函数:p1满足p1.m_height != 0,故释放0x0011指向的地址。但是在上一步已经将0x0011地址指向的堆区释放过了,这就出现了重复释放堆区内存的非法操作
所以需要进行深拷贝操作,如代码中的拷贝构造函数Person(const Person& p){}所示:
-
对非指针变量进行普通的拷贝赋值:
m_age = p.m_age -
对指针变量进行深拷贝操作:
m_height = new int(*p.m_height);,这样就可以在拷贝时额外开辟一片堆区内存空间用于存p.m_height解引用的值,并将该片堆区首地址传回给m_height,避免了p1.m_height的值直接传给p2.m_height
什么时候需要提供析构函数?:若堆区开辟内存空间,则需要自己提供析构函数进行堆区空间释放
什么时候需要提供拷贝构造函数?:若属性有在堆区开辟的,一定要自己提供拷贝构造函数,进行深拷贝,防止浅拷贝带来的问题
使用浅拷贝与深拷贝的效果分别如下:
11.2.6 初始化列表
作用:一种初始化属性列表的方法,只是一种简化代码的初始化方法,
语法:构造函数():属性1(值1),属性2(值2),……{}
举例:
class Person {
public:
Person(int a, int b, int c) :m_a(a), m_b(b), m_c(c) {
}
/*等同于常见初始化方法,只是单纯减少了代码行数:
Person(int a, int b, int c){
m_a = a;
m_b = b;
m_c = c;
}*/
int m_a;
int m_b;
int m_c;
};
void test() {
Person p(10, 20, 30);
}
11.2.7 类对象作为类成员
解释:一个类的实例化对象也可作为另一个类的成员变量
举例:
class Phone {
public:
Phone(string phone) :m_phone(phone) {
}
string m_phone;
};
class Person {
public:
// Phone M_phone = phone; // 隐式转换法
Person(string name,string phone):M_name(name),M_phone(phone){ //这行的 M_phone(phone) 相当于做了上述隐式转换法的构造函数调用
}
string M_name;
Phone M_phone;
};
void test() {
Person p1("张三", "HUAWEI");
cout << p1.M_name << "有" << p1.M_phone.m_phone << endl;
//注意:这里的p1.m_phone.m_phone,p1是Person类的对象,M_phone是Phone类的对象,m_phone是Phone类的属性
}
构造与析构顺序:构造时先小后大,析构时先大后小(本例中Person类为大)
11.2.8 静态成员
定义:静态成员是在成员变量或成员函数前加关键字static。静态成员包括 静态成员变量 和 静态成员函数 。
静态成员变量:
-
类内声明,类外初始化
class Person{ public: static string m_name; // 类内声明 }; string Person::m_name = "David"; // 类外初始化注意:静态成员变量的类外初始化时格式要严格按照:
变量类型 类名::变量名 = 变量值 -
所有对象共享同一份数据
-
静态成员变量不单独属于任一实例化对象,因此对静态变量的访问方式有两种
-
通过对象访问
Person p1; cout << p1.m_name << endl; -
通过类名访问
cout << Person::m_name << endl;
-
-
同一类的所有对象的静态成员变量指向同一片内存空间
string Person::m_name = "张三"; Person p1,p2; // 实例化两个对象 cout << p1.m_name << endl; // 输出:张三 p1.m_name = "李四"; cout << p2.m_name << endl; // 输出:李四 Person::m_name = "王五"; cout << p1.m_name << endl; // 输出:王五
-
-
在编译阶段分配内存
静态成员函数:
-
所有对象共享同一个函数
-
同上,静态成员函数同样有两种调用方式
-
通过对象访问
static void func(){} p1.func(); -
通过类名访问
Person::func();
-
-
-
静态成员函数只能访问静态成员变量

被折叠的 条评论
为什么被折叠?



