一、类的概述
1.类的引入
类的封装:将数据和方法封装在一起,加以权限区分,用户只能通过公共方法访问私有数据。
为什么要将数据和方法封装在一起呢,而且还要通过公共方法才能访问私有数据?
- C语言中数据和方法分开可能产生的问题:
// 定义一个狗结构体
struct Dog
{
char name[32];
};
// 定义一个人结构体
struct Person
{
char name[32];
};
// 定义一个狗的方法
void dogRun(struct Dog *dog)
{
printf("%s正在用四条腿跑\n", dog->name);
}
// 定义一个人的方法
void personRun(struct Person *per)
{
printf("%s正在用两条腿跑\n", per->name);
}
void test01()
{
struct Dog laifu = {"来福"};
struct Person changwei = {"常威"};
dogRun(&laifu);
personRun(&changwei);
dogRun((struct Dog *)&changwei);
}
- 运行结果
来福正在用四条腿跑
常威正在用两条腿跑
常威正在用四条腿跑
- 可以看到,如果将数据和方法分开的话,由于对函数和数据的使用不规范,可能造成非常尴尬的局面。
2.类的封装
- 定义类的关键字为:class;
- 对类的数据加以权限区分:public 公有数据 、protected 受保护数据 、private 私有数据;
- public 修饰的数据和方法,类外可以直接访问;
- private、protected 修饰的数据和方法,类外不可以访问;
- 没有涉及继承时,private 和 protected 没有任何区别;
- 权限限制只是针对类外访问而言的,类的内部没有权限的区分,因此可以通过类的成员函数访问类中的任意数据;
- 一般建议,数据为私有,方法为公有。
- 代码演示
// 定义一个类
class Data
{
private:
int a;
protected:
int b;
public:
int c;
// 定义一个成员函数,用于初始化数据
void initData(int a1, int b1, int c1)
{
a = a1;
b = b1;
c = c1;
}
// 定义一个成员函数,打印所有变量的值
void showData()
{
printf("a = %d,b = %d, c = %d\n",a , b, c);
}
};
void test02()
{
// 实例化一个对象
Data data1;
data1.initData(11, 22, 33);
// 访问data1中的数据
// printf("a = %d\n", data1.a); // 报错,数据私有,不允许访问
// printf("b = %d\n", data1.b); // 报错,数据受保护,不允许访问
printf("c = %d\n", data1.c); // 访问成功,数据公有
// 通过成员函数访问
data1.showData();
}
- 运行结果
c = 33
a = 11,b = 22, c = 33
- 说明:
- 定义类的时候和结构体一样,是不占用空间的,实例化对象的时候才会开辟空间;
- 可以看到,我们在类的外部访问私有和受保护数据的时候,访问失败,访问公共数据,访问成功;
- 但是无论公有私有还是公共数据,通过类的成员函数进行读写操作都能成功,这也证明了在类的内部没有权限区分;
- 因此以后,想要访问类私有属性,就需要通过公共方法来访问。
3.如何设计一个类
因为类是封装了数据和方法,因此在设计类前我们要想好需要有哪些数据,然后要对这些数据进行哪些操作。
3.1案例一
- 案例1:定义一个 Person 类,包括人的姓名和年龄数据,包含初始化方法,对每个数据的读写操作,显示所有信息,同时限制年龄为合理范围。
// 定义一个 Person 类
class Person
{
private:
char name[32];
int age;
public:
// 定义函数,初始化对象
void initPerson(char *new_name, int new_age)
{
if (new_age >= 0 && new_age <= 120)
{
age = new_age;
}
else
{
cout << "请输入有效年龄" << endl;
return;
}
strcpy(name, new_name);
}
// 定义函数,获取name的值
char *getName(void)
{
return name;
}
// 定义函数,获取age的值
int getAge(void)
{
return age;
}
// 定义函数,修改name
void setName(char *new_name)
{
strcpy(name, new_name);
}
// 定义函数,修改age
void setAge(int new_age)
{
if (new_age >= 0 && new_age <= 120)
{
age = new_age;
}
else
{
cout << "请输入有效年龄" << endl;
return;
}
}
// 定义函数,打印所有信息
void showPerson()
{
cout << "name = " << name << ", age = " << age << endl;
}
};
void test03()
{
// 创建一个Person对象
Person jack;
jack.initPerson("jack", 20);
cout << "name = " << jack.getName() << endl;
cout << "age = " << jack.getAge() << endl;
jack.setName("rose");
jack.setAge(18);
jack.showPerson();
}
- 运行结果
name = jack
age = 20
name = rose, age = 18
3.2案例二
- 案例2:设计立方体类(Cube),立方体的长宽高为 a b c ,求出立方体的面积和体积,分别用全局函数和成员函数判断两个立方体是否相等。
// 定义一个立方体类
class Cube
{
private:
int a;
int b;
int c;
public:
// 定义函数,初始化长宽高
void initCube(int a1, int b1, int c1);
// 定义函数,设置长宽高
void setA(int a1);
void setB(int b1);
void setC(int c1);
// 定义函数,获取长宽高
int getA();
int getB();
int getC();
// 定义函数,计算立方体的面积
int getArea();
// 定义函数,计算立方体的体积
int getVolume();
// 定义成员函数,判断两个立方体是否相等
bool cmpCube(Cube &other_cube);
};
void Cube::initCube(int a1, int b1, int c1) {
a = a1;
b = b1;
c = c1;
}
void Cube::setA(int a1) {
a = a1;
}
void Cube::setB(int b1) {
b = b1;
}
void Cube::setC(int c1) {
c= c1;
}
int Cube::getA() {
return a;
}
int Cube::getB() {
return b;
}
int Cube::getC() {
return c;
}
int Cube::getArea() {
int area = (a * b + a * c + b * c) * 2;
return area;
}
int Cube::getVolume() {
int volume = a * b *c;
return volume;
}
bool Cube::cmpCube(Cube &other_cube) {
if ((a == other_cube.getA()) && (b == other_cube.getB()) && (c == other_cube.getC()))
return true;
return false;
}
// 定义全局函数,判断两个立方体是否相等
bool cmpCube1(Cube &cube1, Cube &cube2)
{
if ((cube1.getA() == cube2.getA()) && (cube1.getB() == cube2.getB()) && (cube1.getC() == cube2.getC()))
return true;
return false;
}
void test04()
{
// 创建一个立方体对象
Cube cube1;
cube1.initCube(2, 3, 5);
cout << "立方体1面积:" << cube1.getArea() << ", 立方体1体积:" << cube1.getVolume() << endl;
// 创建另一个立方体对象
Cube cube2;
cube1.initCube(1, 4, 8);
cout << "立方体2面积:" << cube1.getArea() << ", 立方体2体积:" << cube1.getVolume() << endl;
// 判断两个立方体是否相等
// if (cmpCube1(cube1, cube2)) // 全局函数判断
if (cube1.cmpCube(cube2)) // 成员函数判断
{
cout << "两个立方体完全一样" << endl;
}
else
{
cout << "两个立方体不一样" << endl;
}
}
- 运行结果
立方体1面积:62, 立方体1体积:30
立方体2面积:88, 立方体2体积:32
两个立方体不一样
- 说明:
- 上面定义类的时候,将类的成员函数在类中声明,类外定义。注意,类外定义的函数得加上作用域的限制,标明这个成员函数是属于哪一个类的;
- 这里在比较两个立方体是否相等的时候,返回值用到了 bool 类型,表示真假值,在C语言中没有用过,c++阶段学习;
- 当使用全局函数比较两个立方体时,需要传两个参数,但使用成员函数比较两个立方体大小的时候,只需要传另外一个立方体就行。
3.3案例三
- 案例3:设计一个圆形类(Circle),和一个点类(Point),计算点和圆的关系。 假如圆心坐标为x0, y0, 半径为 r,点的坐标为 x1, y1,判断点在圆上、圆外还是园内。
// 定义一个点类
class Point
{
private:
int x;
int y;
public:
// 初始化点坐标
void initPoint(int x1, int y1)
{
x = x1;
y = y1;
}
// 获取点的横纵坐标
int getX()
{
return x;
}
int getY()
{
return y;
}
};
// 定义一个圆类
class Circle
{
private:
int r;
Point p;
public:
// 初始化圆
void initCircle(int r0, int x0, int y0)
{
// 初始化圆半径
r = r0;
// 初始化圆心
p.initPoint(x0, y0);
}
// 定义函数,判断点和圆的位置(外 上 内 对应 1 0 -1)
int getRelation(Point &p1)
{
// 定义变量分别表示半径的平方,横坐标、纵坐标差值的平方
int R = r*r;
int X = (p1.getX() - p.getX()) * (p1.getX() - p.getX());
int Y = (p1.getY() - p.getY()) * (p1.getY() - p.getY());
// 判断位置
if (R == X + Y)
return 0;
else if (R < X + Y)
return 1;
else
return -1;
}
};
void test05()
{
// 创建一个点对象
Point p;
p.initPoint(3, 4);
// 创建一个圆对象
Circle c;
c.initCircle(5, 0, 0);
int ret = c.getRelation(p);
switch(ret)
{
case 0:
cout << "点在圆上" << endl;
break;
case 1:
cout << "点在圆外" << endl;
break;
case -1:
cout << "点在圆内" << endl;
break;
default:
cout << "未得出结果" << endl;
break;
}
}
- 运行结果
点在圆上
- 说明:上面的案例中用到了对象的嵌套,圆对象里面又嵌套了一个点对象。
二、构造析构
1.构造函数
1.1构造函数的概述
我们前面定义类的时候,都会设置一个给对象初始化的函数,创建对象以后再手动调用,其实 c++ 类里有可以主动调用的初始化函数,叫做构造函数。
构造函数:类实例化对象的时候自动调用。构造函数的本质功能就是初始化对象中的数据成员。
1.2构造函数的定义
构造函数名和类名称相同,没有返回值类型,连 void 都不可以,可以有参数,可以重载,因为创建对象是在类外创建,构造函数在创建对象的时候调用,相当于要在类外调用构造函数,因此构造函数权限为 public。
- 代码演示
// 定义一个类
class Data
{
private:
int a;
int b;
public:
Data()
{
a = 0;
b = 0;
cout << "无参的构造函数被调用" << endl;
}
Data(int a1 ,int b1)
{
a = a1;
b = b1;
cout << "两个参数的构造函数被调用" << endl;
}
};
void test06()
{
// Data obj; // error: no matching function for call to 'Data::Data()'
Data obj;
Data obj1(1, 2);
}
- 运行结果
无参的构造函数被调用
两个参数的构造函数被调用
- 说明:
- 我们前面没有学习构造函数的时候,可以直接
Data obj
来实例化对象,而这里,我们定义了两个参数的构造函数以后,没有定义无参的构造函数,再去用Data obj
实例化对象会报错:error: no matching function for call to 'Data::Data()
,可以看到错误提示信息是 Data 类中没有 Data() 函数; - 上面问题的原因是,如果不提供任何构造函数,编译器会为类提供一个默认的无参的构造函数,因此不定义任何构造函数的时候,通过
Data obj
实例化对象,其实是调用了默认的无参构造; - 但定义了构造函数以后,
Data obj
创建对象失败是因为,用户提供任何一个构造函数都会屏蔽默认无参的构造函数; - 因此,我们在定义构造函数的时候,最好定义一个无参构造。
- 我们前面没有学习构造函数的时候,可以直接
1.3构造函数的调用
构造函数的调用形式,本质上也就是我们创建对象的形式,因为创建对象时主动触发构造函数调用。
- 代码演示
// 定义一个类
class Data
{
private:
int a;
int b;
public:
Data()
{
a = 0;
b = 0;
cout << "无参的构造函数被调用" << "a = " << a << " b = " << b << endl;
}
Data(int a1)
{
a = a1;
b = 0;
cout << "一个参数的构造函数被调用" << "a = " << a << " b = " << b << endl;
}
Data(int a1 ,int b1)
{
a = a1;
b = b1;
cout << "两个参数的构造函数被调用" << "a = " << a << " b = " << b << endl;
}
};
void test06()
{
// 隐式调用
cout << "--------------------隐式调用--------------------" << endl;
Data obj;
Data obj1(1);
Data obj2(2, 3);
// 显示调用
cout << "--------------------显式调用--------------------" << endl;
Data obj3 = Data();
Data obj4 = Data(4);
Data obj5 = Data(5, 6);
// 为什么有参的隐式调用都加(),无参的不加
cout << "--------------------无参加()--------------------" << endl;
Data obj6();
cout << "--------------------隐式转换--------------------" << endl;
Data obj7 = 7;
cout << "--------------------匿名对象--------------------" << endl;
Data();
Data(8);
Data(9, 10);
}
- 运行结果
--------------------隐式调用--------------------
无参的构造函数被调用a = 0 b = 0
一个参数的构造函数被调用a = 1 b = 0
两个参数的构造函数被调用a = 2 b = 3
--------------------显式调用--------------------
无参的构造函数被调用a = 0 b = 0
一个参数的构造函数被调用a = 4 b = 0
两个参数的构造函数被调用a = 5 b = 6
--------------------无参加()--------------------
--------------------隐式转换--------------------
一个参数的构造函数被调用a = 7 b = 0
--------------------匿名对象--------------------
无参的构造函数被调用a = 0 b = 0
一个参数的构造函数被调用a = 8 b = 0
两个参数的构造函数被调用a = 9 b = 10
- 说明:
- 上面的演示,构造函数的调用有两种方式:隐式调用、显示调用,推荐使用隐式调用;
- 无参的构造在隐式调用的时候是不加 () 的,因为加了会产生冲突,如:
Data obj6()
,加了 () 以后就不是创建对象了,而是声明了一个名为 obj6 的函数,且函数的返回值是一个 Data 类型的对象; - 当类中有一个参数的构造函数的时候,
Data obj7 = 7
这种写法会触发一个参数构造函数的隐式转换,不推荐这种写法; - 创建对象的时候,可以定义一个变量名来接收创建的对象,当没有定义变量接收的时候,这里创建的对象叫匿名对象,匿名对象没有变量保存,也没有被使用的话,创建完就会释放。
2.析构函数
2.1析构函数的概述
析构函数:当对象生命结束的时候,系统自动调用析构函数,完成对象的清理工作。
构造函数:先为对象开辟空间,然后调用构造函数完成初始化。
析构函数:先调用析构函数,然后释放对象自身的空间。
- 析构函数注意点:
- 如果不提供析构函数,系统会自动提供一个空的析构函数;
- 析构函数并不是清理对象自身的空间(由系统自动释放),而是清理指针成员指向的堆区空间(避免内存泄漏);
- 如果类中有指针成员且指向堆区,必须实现析构函数手动释放堆区空间。
2.2析构函数的定义
析构函数:~类名
称为析构函数名,析构函数没有返回值类型,连 void 都不可以,不能有参数,因此不能被重载。
- 代码演示:还是上面的代码,加一个析构函数,看现象
// 定义一个类
class Data
{
private:
int a;
int b;
public:
// 定义构造函数
Data()
{
a = 0;
b = 0;
cout << "无参的构造函数被调用" << "a = " << a << " b = " << b << endl;
}
Data(int a1)
{
a = a1;
b = 0;
cout << "一个参数的构造函数被调用" << "a = " << a << " b = " << b << endl;
}
Data(int a1 ,int b1)
{
a = a1;
b = b1;
cout << "两个参数的构造函数被调用" << "a = " << a << " b = " << b << endl;
}
// 定义析构函数
~Data()
{
cout << "调用析构函数" << "a = " << a << " b = " << b << endl;
}
};
void test06()
{
// 隐式调用
cout << "--------------------隐式调用--------------------" << endl;
Data obj;
Data obj1(1);
Data obj2(2, 3);
// 显示调用
cout << "--------------------显式调用--------------------" << endl;
Data obj3 = Data();
Data obj4 = Data(4);
Data obj5 = Data(5, 6);
// 为什么有参的隐式调用都加(),无参的不加
cout << "--------------------无参加()--------------------" << endl;
Data obj6();
cout << "--------------------隐式转换--------------------" << endl;
Data obj7 = 7;
cout << "--------------------匿名对象--------------------" << endl;
Data();
Data(8);
Data(9, 10);
cout << "--------------------析构函数--------------------" << endl;
}
- 运行结果
--------------------隐式调用--------------------
无参的构造函数被调用a = 0 b = 0
一个参数的构造函数被调用a = 1 b = 0
两个参数的构造函数被调用a = 2 b = 3
--------------------显式调用--------------------
无参的构造函数被调用a = 0 b = 0
一个参数的构造函数被调用a = 4 b = 0
两个参数的构造函数被调用a = 5 b = 6
--------------------无参加()--------------------
--------------------隐式转换--------------------
一个参数的构造函数被调用a = 7 b = 0
--------------------匿名对象--------------------
无参的构造函数被调用a = 0 b = 0
调用析构函数a = 0 b = 0
一个参数的构造函数被调用a = 8 b = 0
调用析构函数a = 8 b = 0
两个参数的构造函数被调用a = 9 b = 10
调用析构函数a = 9 b = 10
--------------------析构函数--------------------
调用析构函数a = 7 b = 0
调用析构函数a = 5 b = 6
调用析构函数a = 4 b = 0
调用析构函数a = 0 b = 0
调用析构函数a = 2 b = 3
调用析构函数a = 1 b = 0
调用析构函数a = 0 b = 0
- 说明:
- 根据上面代码运行的结果可以看出,匿名对象,在定义以后没有变量保存其值,匿名对象也没有被使用,定义完立马就释放了;
- 而非匿名对象,这里的对象是在同一作用域定义的前提下,函数调用(当前复合语句)结束,调用析构函数,且满足先定义的后释放,后定义的先释放。
2.3析构顺序
前面的案例中,同级别的对象,先创建的后释放,当对象在不同作用域定义的时候呢?
- 代码演示
// 定义一个类
class Data
{
private:
int a;
public:
Data()
{
a = 0;
cout << "无参的构造函数被调用" << "a = " << a << endl;
}
Data(int a1)
{
a = a1;
cout << "有参的构造函数被调用" << "a = " << a << endl;
}
~Data()
{
cout << "析构函数被调用" << "a = " << a << endl;
}
};
// 在全局创建对象
Data obj1(1);
Data obj2(2);
void test07()
{
Data obj3(3);
{
Data obj4(4);
Data obj5(5);
}
Data obj6(6);
}
- 运行结果
有参的构造函数被调用a = 1
有参的构造函数被调用a = 2
有参的构造函数被调用a = 3
有参的构造函数被调用a = 4
有参的构造函数被调用a = 5
析构函数被调用a = 5
析构函数被调用a = 4
有参的构造函数被调用a = 6
析构函数被调用a = 6
析构函数被调用a = 3
析构函数被调用a = 2
析构函数被调用a = 1
- 说明:
- 上面的演示可以看出:对象的创建是按照代码执行的顺序依次执行的;
- 但是析构函数,会先遵循作用域的变量释放顺序,和前面学习的普通变量的释放顺序一样,即当前复合语句结束,变量就释放了。最先结束的符合语句里的对象最先释放,全局的对象最后释放。满足上述对象释放顺序的前提下,还满足同一作用域,先创建的后释放。
3.拷贝构造
3.1拷贝构造函数的定义
- 代码演示
// 定义一个类
class Data
{
private:
int a;
int b;
public:
// 定义构造函数
Data()
{
a = 0;
b = 0;
cout << "无参的构造函数被调用" << endl;
}
Data(int a1, int b1)
{
a = a1;
b = b1;
cout << "有参的构造函数被调用" << endl;
}
// 定义拷贝构造函数
Data(const Data &obj)
{
a = obj.a;
b = obj.b;
cout << "拷贝构造函数被调用" << endl;
}
// 显示对象信息的函数
void showData()
{
cout << "a = " << a << " b = " << b << endl;
}
};
void test08()
{
// 先创建一个对象
Data obj1(11, 22);
// 再创建一个对象,并将旧对象赋值给它
Data obj2 = obj1;
obj2.showData();
}
- 运行结果
有参的构造函数被调用
拷贝构造函数被调用
a = 11 b = 22
- 说明:
- 如果用户没有提供拷贝构造函数,系统会提供默认拷贝构造函数,默认拷贝构造是浅拷贝,对应成员为普变量的话,浅拷贝足够了。但如果类中有指针成员且指向堆区, 必须实现拷贝构造函数,深拷贝;
- 拷贝构造函数定义和普通构造函数一样,类名作为函数名,只是参数不一样,参数
const Data &obj
中,obj 为形参名,加 & 表示引用传递,因为实参是一个对象,对象占用空间一般很大,引用传参节约空间。同时,参数要加 const 修饰,不允许函数内部修改传入的对象的数据。
3.2拷贝构造函数的调用
拷贝构造函数和构造函数一样,不需要手动调用,而是在特定的情况下自动触发调用。
拷贝构造函数是:创建一个新对象的时候,通过旧对象给新对象初始化,会自动调用拷贝构造函数,会自动将旧对象的引用作为实参传入拷贝构造函数,将旧对象的数据拷贝给新对象。
3.2.1普通对象作为函数的参数
普通对象作为函数的参数的时候,会调用拷贝构造。
- 代码演示
// 定义一个类
class Data {
private:
int a;
int b;
public:
// 定义构造函数
Data() {
a = 0;
b = 0;
cout << "无参的构造函数被调用" << endl;
}
Data(int a1, int b1) {
a = a1;
b = b1;
cout << "有参的构造函数被调用" << endl;
}
// 定义拷贝构造函数
Data(const Data &obj) {
a = obj.a;
b = obj.b;
cout << "拷贝构造函数被调用" << endl;
}
};
void func(Data obj){}
// void func(Data &obj){}
void test09() {
// 创建一个对象
Data obj1(11, 22);
// 普通对象作为函数的参数
func(obj1);
}
- 运行结果
有参的构造函数被调用
拷贝构造函数被调用
- 说明:
- 可以看到,上面的代码中,我们并没有像前面一样创建一个新对象,然后将旧对象的值赋给新对象,却发送了拷贝构造。其实这里也发生了旧对象赋值给新对象的动作,只是比较隐晦;
- 前面学习函数的时候知道,函数在定义的时候不占用空间,在调用的时候才占用空间,因为函数调用的时候会为形参开辟空间,然后定义一个形参指定类型的变量,将实参赋值给形参变量,而这里的形参就是一个新创建的 Data 类型的对象,实参是一个旧的对象,那么调用函数传参的过程就是一个旧对象给新对象赋值的过程,因此会调用拷贝构造;
- 为了防止拷贝构造被调用,一般将形参设置为引用传递,引用传递只是给旧对象起了个别名,没有旧对象给新对象赋值的过程,不会调用拷贝构造。
3.2.2普通对象作为函数的返回值
普通对象作为函数的返回值,外部通过一个新对象去接收,按照我们前面的理解,是将一个旧对象返回,赋值给新对象,理论上会发生拷贝构造,但事实并不一定如此,不同的高级编译器会有不同的结果。在 visual studio 里面会发生拷贝构造,但是在 Qt 或者 CLion 里面不会,具体可以在自己习惯使用的编译器里验证。
- 这里演示 CLion 的
// 定义一个类
class Data {
private:
int a;
int b;
public:
// 定义构造函数
Data() {
a = 0;
b = 0;
cout << "无参的构造函数被调用" << endl;
}
Data(int a1, int b1) {
a = a1;
b = b1;
cout << "有参的构造函数被调用" << endl;
}
// 定义拷贝构造函数
Data(const Data &obj) {
a = obj.a;
b = obj.b;
cout << "拷贝构造函数被调用" << endl;
}
void showData()
{
cout << "a = " << a << " b = " << b << endl;
}
// 析构函数
~Data()
{
cout << "析构函数被调用" << endl;
}
};
Data func()
{
Data obj1(11, 22);
cout << &obj1 << endl;
return obj1;
}
void test09() {
Data obj2 = func();
obj2.showData();
cout << &obj2 << endl;
}
- 运行结果
有参的构造函数被调用
0xc718fffd88
a = 11 b = 22
0xc718fffd88
析构函数被调用
- 说明
- 根据上面的运行结果,可以看到,只调用了一次构造函数,没有调用拷贝构造,而且明明创建了两个对象,但是结果却只析构了一次;
- 打印两个对象的内存地址,发现内存地址相同,结合上面的分析,可以知道,这两个变量本质上是指向同一个空间;
- 因此,可以得出结论, func 函数调用完成以后,局部变量 obj1 被删除了,但它指向的空间并没有被释放,而是由 obj2 指向了同一个空间,等到 obj2 释放的时候,空间才会释放。
3.2.3无参、有参、拷贝构造总结
- 如果用户提供了有参构造或拷贝构造会屏蔽默认的无参构造;
- 但用户提供有参构造或无参构造不会屏蔽默认拷贝构造,只有用户提供拷贝构造,才会屏蔽默认拷贝构造;
- 如果类中有指针成员且指向堆区,必须实现析构函数和拷贝构造函数(深拷贝);
- 构造函数实例化对象自动调用,先开辟空间,后调用构造函数,可以重载;
- 析构函数,对象结束的时候自动调用,先调用析构函数,后释放对象自身空间,不能重载。