对象的生离死别
实验介绍
在构建一个类时,一般情况下需要编写构造函数、拷贝构造函数以及析构函数,这将直接影响程序的运行。而初始化列表是在调用构造函数时初始化参数的方式。
一个对象从实例化到销毁的历程:
知识点
- 内存分区
- 构造函数
explicit
关键字- 初始化列表
- 拷贝构造函数
- 析构函数
内存分区
介绍
根据存储数据的类型,系统将不同类型的数据存储在不同的区域,作为
C++
开发者,必须对内存的分区以及使用了然于心。
分区
一般情况下,根据内存用途将内存划分为五个区:
内存区 | 用途解释 |
---|---|
栈区 | 存储函数的参数、局部变量等 |
堆区 | 由程序员分配、释放内存 |
全局区 | 存储全局变量和静态变量 |
常量区 | 存储常量 |
代码区 | 存储逻辑代码的二进制 |
栈与堆对比
功能 | 栈 | 堆 |
---|---|---|
申请与释放 | 编译器自动分配、回收 | 程序员分配和释放(C 使用 malloc 申请内存、free 释放内存;C++ 使用 new 申请内存、delete 释放内存) |
申请内存后系统的响应 | 申请空间大于栈空间时程序将提示异常 (栈溢出) | 超过内存空间程序报异常 |
空间大小限制 | window 1 M / linux 8 M | 系统内存,比栈大很多 |
申请效率 | 快 | 比较慢(只是相对栈,实际也很快) |
示例代码 1
#include <iostream>
using namespace std;
class Plan
{
// 默认为私有属性
int wingCount;
public:
// 代码区
Plan() { wingCount = 0; }
~Plan() {}
int getWingCount() { return wingCount; }
};
int main()
{
// 栈上
Plan p1;
Plan p2;
Plan p3;
p1.getWingCount();
p2.getWingCount();
p3.getWingCount();
return 0;
}
代码解释
- 编译时
getWingCount
只存储一份。 - 实例化的对象
p1
、p2
、p3
存储在栈上。 - 当对象调用
getWingCount
成员函数只需要找到相应地址即可。 - 当变量没有被初始化时是一个随机值,建议所有变量都初始化。
- 示例代码 1 中数据在类默认私有属性区域,但一般建议添加封装属性关键字。
构造函数
定义:构造函数又称构造方法、建构子、构造器,是类里用于创建对象的特殊子程序。可以初始化一个新建的对象,并时常接受参数用以设定实例变量。
规则与特点
- 对象实例化时自动被调用。
- 与类名同名。
- 没有返回值。
- 可以有多个重载形式。
- 实例化对象时仅用到一个构造函数。
- 当用户没有定义构造函数时,编译器自动生成。
示例代码 2
#include <iostream>
#include <string>
using namespace std;
class Teacher
{
public:
// 1. 无参构造函数
Teacher() {}
// 2. 有参构造函数
Teacher(string name, int age) {
this->name = name;
this->age = age;
}
// 3. 有参构造函数--全部有默认参数-->默认构造函数
/*Teacher(string name = "jake", int age = 15) {
this->name = name;
this->age = age;
}*/
private:
string name;
int age;
};
注意:
- 根据重载函数的规则,例如示例代码 2 中有参构造函数与缺省构造函数不能同时使用,因为参数的个数和类型都是相同的。
- 示例代码 2 中无参构造函数与缺省构造函数也不能同时使用,因为编译器无法识别是使用无参构造函数还是使用缺省函数。
- 如果实例化对象不需要传递参数的构造函数统称默认构造函数。
explicit
关键字
作用:指定构造函数或转换函数或推导指引为显示,即不能用于隐式转换赋值初始化。默认情况下类构造函数为 implicit(隐式)。
在讲解构造函数时需要提到 explicit
关键字,因为很多时候会使用到 explicit
关键字。
示例代码 3
#include <iostream>
#include <string>
using namespace std;
class A
{
public:
// 默认 implicit(隐式转换)
A(int) { } // 转换构造函数
A(int, int) { } // 转换构造函数 (C++11)
operator bool() const { return true; }
};
class B
{
public:
// 申明构造函数使用显示声明,不能隐式转换
explicit B(int) { }
explicit B(int, int) { }
explicit operator bool() const { return true; }
};
int main()
{
A a1 = 1; // OK赋值初始化选择 A::A(int)
A a2(2); // OK:直接初始化选择 A::A(int)
A a3 {4, 5}; // OK:直接列表初始化选择 A::A(int, int)
A a4 = {4, 5}; // OK赋值列表初始化选择 A::A(int, int)
A a5 = (A)1; // OK:显式转型进行 static_cast
if (a1) ; // OK:A::operator bool()
bool na1 = a1; // OK赋值初始化选择 A::operator bool()
bool na2 = static_cast<bool>(a1); // OK:static_cast 进行直接初始化
// B b1 = 1; // err赋值初始化不考虑 B::B(int)
B b2(2); // OK:直接初始化选择 B::B(int)
B b3 {4, 5}; // OK:直接列表初始化选择 B::B(int, int)
// B b4 = {4, 5}; // err赋值列表初始化不考虑 B::B(int,int)
B b5 = (B)1; // OK:显式转型进行 static_cast
if (b2) ; // OK:B::operator bool()
// bool nb1 = b2; // err赋值初始化不考虑 B::operator bool()
bool nb2 = static_cast<bool>(b2); // OK:static_cast 进行直接初始化
}
- 防赋值初始化时使用
explicit
关键字 - 防赋值列表初始化时使用
explicit
关键字
拷贝构造函数
- 语法格式:
类名(const 类名 &变量)
。 - 如果没有自定义构造函数,系统自动生成。
- 采用直接初始化或赋值初始化实例化对象时系统自动调用拷贝构造函数。
示例代码 4
#include <iostream>
#include <string>
using namespace std;
class Teacher
{
public:
// 1. 无参构造函数
Teacher() {}
// 2. 有参构造函数
Teacher(string name, int age) {
this->name = name;
this->age = age;
}
// 3. 拷贝构造函数
Teacher(const Teacher &tea) {
this->name = tea.name;
this->age = tea.age;
}
private:
string name;
int age;
};
int main()
{
// 执行默认构造函数
Teacher tea1;
// 执行拷贝构造函数
Teacher tea2 = tea1;
Teacher tea3 = Teacher(tea1);
return 0;
}
注意:
- 拷贝构造函数也是构造函数的一种,当执行了拷贝构造函数后就不会执行其他构造函数。
- 如果不涉及深拷贝,可以不实现拷贝默认构造函数,使用系统自动生成的拷贝构造函数即可。
初始化列表
- 先于构造函数执行。
- 只能用于构造函数。
- 可以同时初始化多个数据成员,多个数据成员之间使用逗号隔开。
示例代码 5
#include <iostream>
using namespace std;
class Circle
{
public:
Circle() : Pi(3.14) {}
// 错误:Circle() { Pi = 3.14; }
private:
const double Pi;
};
注意:
- 语法格式:
类名() : 数据成员 1(参数), 数据成员 2(参数) {}
。 - 由于初始化列表先于构造函数执行,当类中有
const
常量时就必须要用到初始化列表来初始化。 - 推荐使用初始化列表的方式来初始化数据成员。
- 如果类中有数据成员时,推荐将数据成员都初始化。
析构函数
析构函数是在对象销毁时自动调用的函数,一般将释放内存的工作放在析构函数中完成。
- 语法格式:
~类名()
。 - 析构函数不允许有任何参数、不允许重载、没有返回值。
- 如果没有自定析构函数,系统自动生成。
- 对象销毁时自动调用。
示例代码 6
#include <iostream>
using namespace std;
class Student
{
public:
Student() : buffer(new char[16]) {}
~Student() {
// 析构函数释放内存
delete [] buffer;
buffer = nullptr;
}
private:
char *buffer;
};
实验总结
类定义要素
- 一个好的构造函数、拷贝构造函数和析构函数可以使程序使用更加稳健。
- 在编写构造函数时需要考虑是否使用
explicit
关键字修饰。 - 推荐在编写程序时使用初始化列表的方式初始化参数。
- 析构函数时要注意释放堆中的内存,但也要注意避免重复释放内存造成程序崩溃。