类和对象--构造函数和析构函数
1 构造函数
1.1 定义
C++中的类可以定义与类名相同的特殊成员函数,这种与类名相同的成员函数叫做构造函数.
/*
class 类名
{
类名(形参)
{
构造体
}
}
*/
class A
{
A(/*形参*/)
{}
}
1.2 调用
自动调用:一般情况下C++编译器会自动调用构造函数.
手动调用:在一些情况下则需要手工调用构造函数.
1.3 特点
- 在对象创建时自动调用,完成初始化相关工作。
- 无返回值,与类名同,默认无参,可以重载,可默认参数。
- 一经实现,默认不复存在。
- 一般是公有访问权限。
2 析构函数
1.1 定义
C++中的类可以定义一个特殊的成员函数清理对象,这个特殊的成员函数
叫做析构函数.
/*
class 类名
{
~类名()
{
析构体
}
}
*/
class A
{
~A()
{}
}
2.2 调用
对象销毁时,自动调用。
2.3 特点
- 对象销毁时,自动调用。完成销毁的善后工作。
- 无返值 ,与类名同。无参。不可以重载与默认参数
- 析构函数的作用,并不是删除对象,而在对象销毁前完成的一些清理工作。
3 构造函数的分类及调用 ★
3.1 分类
- 按照参数分类: 有参 无参(默认)
- 按照类型分类: 普通 拷贝构造 (const Test &obj)
3.2 调用
- 括号法
- 显示法
- 隐式法
调用的注意事项
- 不要使用括号法调用无参构造函数
- 不要用拷贝构造函数初始化匿名对象
匿名对象特点
- 当前行执行完,立即释放
#include <iostream>
using namespace std;
class Test
{
// 无参(默认)构造函数
Test()
{
cout << "无参(默认)构造函数调用" << endl;
}
// 有参构造函数
Test(int a)
{
cout << "有参构造函数调用" << endl;
}
// 拷贝构造函数
Test(const Test &obj)
{
cout << "拷贝构造函数调用" << endl;
}
};
int main()
{
Test t;
// 1.括号法
Test t1(10); // 有参构造
Test t2(t); // 拷贝构造
// 注意事项一
// Test t(); 不要使用括号法调用无参构造函数
// 因为编译器会认为此代码为返回值是Test对象的函数声明
// 2.显示法
Test t3 = Test(10); // 有参构造
Test t4 = Test(t); // 拷贝构造
Test(10); // 匿名对象,特点: 当前行执行完,立即释放
// 注意事项二
// Test(t3); 不要用拷贝构造函数初始化 匿名对象
// 因为编译器认为此代码为Test t3;(对象实例化), 如果已经有t3 t3会重定义
// 3.隐式法(隐式类型转换法) 不建议使用
Test t5 = 10; // 相当于 Test t5 = Test(10); 有参构造
Test t6 = t5; // 相当于 Test t6 = Test(t5); 拷贝构造
return 0;
}
4 无参构造函数的调用
#include <iostream>
using namespace std;
class Test
{
public:
Test()
{
m_a = 0;
m_b = 0;
cout << "Test()无参构造函数调用" << endl;
}
private:
int m_a;
int m_b;
};
int main()
{
Test t; // 调用无参构造函数
return 0;
}
5 有参构造函数的调用
#include <iostream>
using namespace std;
class Test
{
public:
Test(int a)
{
cout << "Test(int a)有参构造函数调用" << endl;
}
Test(int a, int b)
{
cout << "Test(int a, int b)有参构造函数调用" << endl;
}
};
int main()
{
Test t(10); // 调用有参构造函数Test(int a)
Test t(10, 20); // 调用有参构造函数Test(int a, int b)
return 0;
}
6 拷贝构造函数的调用 ★★
6.1 拷贝构造函数的概念
由己存在的对象,创建新对象。也就是说新对象,不由构造器来构造,而是由拷贝构造器来完成。拷贝构造器的格式是固定的。
/*
class 类名
{
类名(const 类名 & obj)
{
拷贝构造体
}
}
*/
class Test
{
Test (const Test & obj)
{}
}
6.2 拷贝构造函数的使用场景
#include <iostream>
using namespace std;
class Test
{
public:
Test()
{
m_a = 10;
cout <<"无参构造函数调⽤" <<endl;
}
Test(int a)
{
m_a = a;
cout << "Test(int a)有参构造函数调用" << endl;
}
Test(int a, int b)
{
m_a = a + b;
cout << "Test(int a, int b)有参构造函数调用" << endl;
}
Test(const Test &obj)
{
m_a = obj.m_a;
cout << "拷贝构造函数调用" << endl;
}
~Test()
{
cout << "析构函数调用" << endl;
}
void print()
{
cout << "m_a = " << m_a << endl;
}
private:
int m_a;
};
场景一
用已经创建好的对象来初始化新的对象
int main()
{
Test t;
// 用已经创建好的对象t来初始化新的对象
Test t2(t); // 调用拷贝构造函数 括号法
Test t3 = Test(t); // 调用拷贝构造函数 显示法
Test t4 = t; // 调用拷贝构造函数 隐式法
t2.print();
t3.print();
t4.print();
return 0;
}
场景二
以值传递的方式,给函数参数传值
void func(Test t)
{
cout << t.m_age << endl;
}
int main()
{
test t1;
// 以值传递的方式,给函数参数传值
func(t1);
return 0;
}
场景三
以值方式,返回局部对象
// 以值方式,返回局部对象
Test func()
{
Test t1;
return t1;
}
int main()
{
// 以值方式,返回局部对象
Test t = func();
cout << t.m_age << endl;
}
7 构造函数规则 ★★
- 编译器会给一个类 添加4个函数:默认构造函数(空实现)、析构函数(空实现)、拷贝构造函数(值拷贝、浅拷贝)、operator=(值拷贝、浅拷贝)。
- 如果我们自己提供了有参构造函数,编译器就不会提供默认构造函数,但是依然会提供拷贝构造函数。
- 如果我们自己提供了 拷贝构造函数,编译器就不会提供其他构造函数。
- 系统提供默认的拷贝构造器。一经实现,不复存在。
- 要实现深拷贝,必须要自定义。
8 浅拷贝与深拷贝 ★★
系统提供默认的拷贝构造器,一经定义不再提供。但系统提供的默认拷贝构造器是等位拷贝,也就是通常意义上的浅拷贝。
如果类中包含的数据元素全部在栈上,浅拷贝也可以满足需求的。但如果堆上的数据,则会发生多次析构行为。
#include <iostream>
#include <string.h>
using namespace std;
class Person
{
public:
// 默认构造函数
Person()
{
cout << "无参构造函数调用" << endl;
}
// 有参构造函数
Person(char *name, int age)
{
cout << "有参构造函数调用" << endl;
// 申请堆内存
m_name = new char[128];
strcpy(m_name, name);
m_age = age;
}
// 析构函数
~Person()
{
cout << "析构构造函数调用" << endl;
if (m_name != NULL)
{
delete m_name;
m_name = NULL;
}
}
char *m_name;
int m_age;
};
int main()
{
char name[64];
strcpy(name, "李四");
Person p1(name, 18);
cout << "姓名: " << p1.m_name << endl;
cout << "年龄: " << p1.m_age << endl;
#if 0
Person p2(p1); // 调用拷贝构造函数
cout << "姓名: " << p2.m_name << endl;
cout << "年龄: " << p2.m_age << endl;
Person p3;
p3 = p2; // 调用赋值运算符重载函数
cout << "姓名: " << p2.m_name << endl;
cout << "年龄: " << p2.m_age << endl;
#endif
return 0;
}
运行结果如下:
乍一看好像没什么问题,但如果我们将此代码主函数中注释的几行打开,再编译重新运行,程序就会出现段错误。
而导致出现段错误的原因就是:堆内存重复释放
而为什么会出现堆内存重复释放呢???
原因
Person p2(p1);
此行代码调用了系统提供的默认拷贝构造函数,而默认拷贝构造函数只是简单的值拷贝(浅拷贝),导致对象p1和对象p2中的char指针m_name指向的是同一块堆内存。
当对象p1释放前调用析构函数delet掉这块堆内存,而对象p2释放前也需要调用析构函数来delet掉这块堆内存,这就导致了堆内存重复释放。
解决办法–深拷贝 ★★
自己写类的拷贝构造函数和重载赋值运算符来替换系统提供的默认拷贝构造函数和赋值函数,以此来实现深拷贝。代码如下(在上述代码,类中添加以下几行):
Person(const Person& obj) // 深拷贝
{
cout << "拷贝构造函数调用" << endl;
m_name = new char[128];
strcpy(m_name, obj.m_name);
m_age = obj.m_age;
}
// 重载赋值运算符
Person& operator=(const Person& obj) // 深拷贝
{
cout << "赋值运算符重载" << endl;
// 先判断原来堆区释放有内容,如果有先释放
if (this->m_name != NULL)
{
delete this->m_name;
this->m_name = NULL;
}
m_name = new char[128];
strcpy(m_name, obj.m_name);
m_age = obj.m_age;
}
再编译运行就没有问题了。运行结果如下:
9 构造函数初始化列表
我们知道,对类中的成员变量初始化可以在构造函数中对其进行赋值。
代码如下:
#include <iostream>
using namespace std;
class Test
{
public:
Test()
{
m_a = 10;
m_b = 20;
m_c = 30;
}
Test(int a, int b, int c)
{
m_a = a;
m_b = b;
m_c = c;
}
Test(const Test &obj)
{
m_a = obj.m_a;
m_b = obj.m_b;
m_c = obj.m_c;
}
int m_a;
int m_b;
int m_c;
};
int main()
{
Test t; // 调用无参构造函数对成员变量初始化
cout << t.m_a << " " << t.m_b << " " << t.m_c << endl;
Test t1(1, 2, 3); // 调用有参构造函数对成员变量初始化
cout << t1.m_a << " " << t1.m_b << " " << t1.m_c << endl;
Test t2(t); // 调用拷贝构造函数对成员变量初始化
cout << t2.m_a << " " << t2.m_b << " " << t2.m_c << endl;
return 0;
}
但是当类成员中含有一个const对象时,或者是一个引用时,他们也必须要通过成员初始化列表进行初始化,因为这两种对象要在声明后马上初始化,而在构造函数中,做的是对他们的赋值,这样是不被允许的。(当然,对于不含有const,或者是引用的普通成员变量也是可以通过初始列表的方式初始化)
初始化列表的语法
构造函数名称后 : 成员变量(值), 成员变量(值)...
代码如下:
#include <iostream>
using namespace std;
class Test
{
public:
Test():m_a(10), m_b(20), m_c(30)
{}
Test(int a, int b, int c):m_a(a), m_b(b), m_c(c)
{}
Test(const Test &obj): m_a(obj.m_a), m_b(obj.m_b), m_c(obj.m_c)
{}
int m_a;
int m_b;
const int m_c;
};
int main()
{
// 使用初始化列表对成员变量初始化
Test t;
cout << t.m_a << " " << t.m_b << " " << t.m_c << endl;
Test t1(1, 2, 3);
cout << t1.m_a << " " << t1.m_b << " " << t1.m_c << endl;
Test t2(t);
cout << t2.m_a << " " << t2.m_b << " " << t2.m_c << endl;
return 0;
}
初始化列表中的初始化顺序,与声明顺序有关,与前后赋值顺序无关
10 类对象作为类成员变量
当其他类对象作为本类成员,先构造其他类对象,再构造自身,析构的顺序和构造相反
#include <iostream>
#include <string.h>
using namespace std;
class Phone
{
public:
Phone(string pName)
{
cout << "Phone有参构造函数" << endl;
}
~Phone()
{
cout << "Phone析构函数" << endl;
}
private:
string m_Name;
};
class Game
{
public:
Game(string gName)
{
cout << "Game有参构造函数" << endl;
}
~Game()
{
cout << "Game析构函数" << endl;
}
private:
string m_Name;
};
class Person
{
public:
Person(string personName, string phoneName, string gameName):m_Name(personName), m_phone(phoneName), m_game(gameName)
{
cout << "Person有参构造函数" << endl;
}
~Person()
{
cout << "Person析构函数" << endl;
}
private:
string m_Name;
Phone m_phone;
Game m_game;
};
int main()
{
Person p("张三", "华为", "王者荣耀");
return 0;
}
11 explicit 关键字
防止利用隐式类型转换方式来构造对象
#include <iostream>
#include <string.h>
using namespace std;
class Data
{
public:
Data(const char *data)
{
cout << "Data(const char *data)构造函数" << endl;
}
explicit Data(int len)
{
cout << "explicit Data(int len)构造函数" << endl;
}
};
int main()
{
// 括号法
Data d1("abc");
// 隐式法
Data d = "abc";
// 括号法
Data d2(10);
// 隐式法
// 下一行代码如果打开,编译不通过,因为Data(int len)构造函数前面加了关键字explicit,限制了隐式法来构造对象
//Data d3 = 10;
return 0;
}