12 对象初始化和清理
生活中我们买的电子产品都基本会有出厂设置,在某一天不用时候也会删除一些自己信息数据保证安全。C++中的面向对象来源于生活,每个对象也都会有初始设置以及对象销毁前的清理数据的设置。
12.1 构造函数和析构函数
对象的初始化和清理也是两个非常重要的问题:
- 一个对象或者变量没有初始状态,对其使用后果是未知
- 同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题
C++提供了构造函数和析构函数来解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理的工作。对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供编译器提供的构造函数和析构函数是空实现。
构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
构造函数语法:类名(){}
- 构造函数,没有返回值也不写void
- 函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
析构函数语法:~类名(){}
- 析构函数,没有返回值也不写void
- 函数名称与类名相同,在名称前加上符号
~ - 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include <iostream> using namespace std;
class Person { public: Person() { cout << "Person的构造函数调用" << endl; }
~Person() { cout << "Person的析构函数调用" << endl; } };
void test01() { Person p; }
int main() {
test01();
system("pause");
return 0; }
|
运行结果:
1 2
| Person的构造函数调用 Person的析构函数调用
|
注意:构造和析构都是系统自动调用的,不要手动去调用。栈上的对象在作用域结束时自动销毁,销毁前自动调用析构函数。
12.2 构造函数的分类及调用
两种分类方式:
- 按参数分为:有参构造和无参构造(无参构造又称默认构造)
- 按类型分为:普通构造和拷贝构造
拷贝构造函数的语法:Person(const Person& p)
- 拷贝构造函数只有一个形参,且必须是本类的const引用
三种调用方式:
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| #include <iostream> using namespace std;
class Person { public: Person() { cout << "无参构造函数!" << endl; }
Person(int a) { age = a; cout << "有参构造函数!" << endl; }
Person(const Person& p) { age = p.age; cout << "拷贝构造函数!" << endl; }
~Person() { cout << "析构函数!" << endl; }
int age; };
void test01() { Person p1; Person p2(10); Person p3(p2);
Person p5 = Person(10); Person p6 = Person(p5);
Person p7 = 10; Person p8 = p7; }
int main() {
test01();
system("pause");
return 0; }
|
注意:不能用括号法调用无参构造函数——Person p(); 会被编译器当作函数声明(声明了一个返回值为Person、名为p的函数),而不是创建对象。写 Person p; 即可。
提示:Person(10) 单独写出来是匿名对象,当前行执行结束后系统会立即回收,调用一次构造一次析构。
12.3 拷贝构造函数调用时机
C++中拷贝构造函数调用时机通常有三种情况:
- 使用一个已经创建完毕的对象来初始化一个新对象
- 值传递的方式给函数参数传值
- 值方式返回局部对象
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| #include <iostream> using namespace std;
class Person { public: Person() { cout << "无参构造函数!" << endl; mAge = 0; }
Person(int age) { cout << "有参构造函数!" << endl; mAge = age; }
Person(const Person& person) { cout << "拷贝构造函数!" << endl; mAge = person.mAge; }
~Person() { cout << "析构函数!" << endl; }
int mAge; };
void test01() { Person man(100); Person newman(man); Person newman2 = man;
}
void doWork(Person p) { cout << "doWork函数中p的年龄:" << p.mAge << endl; }
void test02() { Person man(100); doWork(man); }
Person doWork2() { Person p1; cout << "p1的地址:" << &p1 << endl; return p1; }
void test03() { Person p = doWork2(); cout << "p的地址:" << &p << endl; cout << (Person *)&p << endl; }
int main() {
test01();
cout << "----------" << endl;
test02();
cout << "----------" << endl;
test03();
system("pause");
return 0; }
|
注意:第三种情况(值方式返回局部对象)在 VS debug / Release 现代编译器下可能被 RVO 优化,拷贝构造不一定被调用(输出中两个地址可能相同,且看不到”拷贝构造函数!”)。这是编译器的优化行为,原理上仍应按”会调用拷贝构造”来理解,考试与课程口径均以此为准。
提示:Person newman3; newman3 = man; 是赋值不是拷贝构造——newman3 已先由默认构造创建,之后只是把 man 的值复制给它,不会调用拷贝构造函数。
12.4 构造函数调用规则
默认情况下,C++编译器至少给一个类添加3个函数:
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数(对属性进行值拷贝)
构造函数的调用规则如下:
- 如果用户定义了有参构造,C++不再提供默认无参构造,但是会提供默认拷贝构造
- 如果用户定义了拷贝构造函数,C++就不会再提供其他构造函数
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| #include <iostream> using namespace std;
class Person { public: Person(const Person& p) { age = p.age; cout << "Person的拷贝构造函数调用" << endl; } ~Person() { cout << "Person的析构函数调用" << endl; }
int age; };
void test01() { Person p3(10); Person p4(p3); }
int main() {
test01();
system("pause");
return 0; }
|
提示:验证规则时采用”注释—放开”的方式:先只放开拷贝构造,Person p; 与 Person p2(10); 两行会报错,说明无参、有参构造不再提供;再注释掉拷贝构造、放开有参构造,Person p2(10); 可用但 Person p; 报错,说明写了有参构造后默认无参构造不再提供,而拷贝构造系统仍会提供。
注意:如果用户定义了有参构造,还想要无参构造,就必须自己再写一个无参构造,二者可以构成重载共存。
12.5 深拷贝与浅拷贝
深浅拷贝是面试经典问题,也是常见的一个坑。
浅拷贝:简单的赋值拷贝操作。编译器提供的默认拷贝构造做的就是浅拷贝。
深拷贝:在堆区重新申请空间,进行拷贝操作。
浅拷贝带来的问题:两个对象内部的指针指向同一块堆区内存,对象析构时这块内存会被重复释放,程序崩溃。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| #include <iostream> using namespace std;
class Person { public: Person(int age, int height) { m_age = age; m_height = new int(height); }
Person(const Person& p) { cout << "Person 拷贝构造函数调用" << endl; m_age = p.m_age; m_height = new int(*p.m_height); }
~Person() { if (m_height != NULL) { delete m_height; m_height = NULL; } cout << "Person的析构函数调用" << endl; }
int m_age; int* m_height; };
void test01() { Person p1(18, 180); cout << "p1的年龄为:" << p1.m_age << ",身高为:" << *p1.m_height << endl;
Person p2(p1); cout << "p2的年龄为:" << p2.m_age << ",身高为:" << *p2.m_height << endl; }
int main() {
test01();
system("pause");
return 0; }
|
注意:如果注释掉自己写的拷贝构造函数,使用编译器默认的浅拷贝,上述程序在结束时会发生重复释放堆区内存的问题:p1 和 p2 的 m_height 指向同一块内存,析构时第一次 delete 正常,第二次 delete 同一块地址,程序崩溃。
总结:类中有指针成员指向堆区空间时,必须自己重写拷贝构造函数实现深拷贝。析构函数中释放堆区内存前先判断指针是否为空,释放后置空是好习惯。
12.6 初始化列表
作用:C++提供了初始化列表语法,用来初始化属性。
语法:构造函数():属性1(值1),属性2(值2)... {}
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| #include <iostream> using namespace std;
class Person { public:
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 test01() { Person p(10, 20, 30); cout << "mA = " << p.m_A << endl; cout << "mB = " << p.m_B << endl; cout << "mC = " << p.m_C << endl; }
int main() {
test01();
system("pause");
return 0; }
|
初始化列表也可以配合默认值使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| #include <iostream> using namespace std;
class Person { public: 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) { }
int m_A; int m_B; int m_C; };
void test01() { Person p1; Person p2(100, 200, 300); cout << "p1的 mA = " << p1.m_A << ", mB = " << p1.m_B << ", mC = " << p1.m_C << endl; cout << "p2的 mA = " << p2.m_A << ", mB = " << p2.m_B << ", mC = " << p2.m_C << endl; }
int main() {
test01();
system("pause");
return 0; }
|
提示:初始化列表里 Person() :m_A(10), m_B(20) {} 冒号后的部分就是逐个用括号给属性初始化,多个属性之间用逗号隔开。
12.7 类对象作为类成员
C++类中的成员可以是另一个类的对象,我们称该成员为对象成员。
例如:A类中有对象B作为成员,A为对象成员B所属的外部类。
那么当创建A对象时,A与B的构造和析构顺序是谁先谁后?
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| #include <iostream> #include <string> using namespace std;
class Phone { public: Phone(string name) { m_PhoneName = name; cout << "Phone构造函数调用" << endl; }
~Phone() { cout << "Phone析构函数调用" << endl; }
string m_PhoneName; };
class Person { public: Person(string name, string pName) :m_Name(name), m_Phone(pName) { cout << "Person构造函数调用" << endl; }
~Person() { cout << "Person析构函数调用" << endl; }
string m_Name; Phone m_Phone; };
void test01() { Person p("张三", "苹果X"); cout << p.m_Name << "拿着" << p.m_Phone.m_PhoneName << endl; }
int main() {
test01();
system("pause");
return 0; }
|
运行结果(析构顺序):
1 2
| Person析构函数调用 Phone析构函数调用
|
- 构造顺序:先构造对象成员(Phone),再构造自身(Person)
- 析构顺序:与构造相反,先析构自身(Person),再析构对象成员(Phone)
提示:可以把 Phone 换成有多个构造函数的类,用初始化列表指定调用哪个构造。
12.8 静态成员
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员。
静态成员分为:
静态成员变量:
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
静态成员变量两种访问方式:
静态成员函数:
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| #include <iostream> using namespace std;
class Person { public: static void func() { m_B = 100; cout << "static void func调用" << endl; }
static int m_B; int m_A; };
int Person::m_B = 0;
void test01() { Person p1; p1.m_B = 100; cout << "通过对象访问 m_B = " << p1.m_B << endl;
cout << "通过类名访问 m_B = " << Person::m_B << endl;
Person p2; p2.m_B = 200; cout << "p1.m_B = " << p1.m_B << ", p2.m_B = " << p2.m_B << endl;
p1.func(); Person::func(); }
int main() {
test01();
system("pause");
return 0; }
|
静态成员变量也有访问权限:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| #include <iostream> using namespace std;
class Person { public: static int m_A; protected: static int m_B; private: static int m_C; };
int Person::m_A = 100; int Person::m_B = 200; int Person::m_C = 300;
void test01() { Person p; cout << "p.m_A = " << p.m_A << endl; cout << "Person::m_A = " << Person::m_A << endl;
}
int main() {
test01();
system("pause");
return 0; }
|
注意:静态成员变量的初始化必须写在类外,格式为 int Person::m_B = 0;,不要漏掉类型名和作用域运算符。
总结:静态成员函数不能访问非静态成员变量——因为静态成员函数不属于任何一个对象,没有 this 指针,无法区分要访问的是哪个对象里的 m_A;静态成员函数也可以通过对象或类名两种方式调用。