对象的初始化和清理
构造函数和析构函数会被编译器自动调用,完成对象初始化和对象清理工作。即使自己不提供初始化操作和清理操作,编译器也会增加默认的操作,所以编写类就应该顺便提供初始化函数。
构造函数和析构函数必须写在public下才可以调用到。
1)构造函数和析构函数的概念
构造函数主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用;析构函数主要用于对象销毁前系统自动调用,执行一些清理工作。
2)构造函数和析构函数的语法
构造函数与类名相同,没有返回值,不写void,可以发生重载(可以有参数),由编译器自动调用而不是手动,而且只会调用一次。
析构函数与类名相同,但类名前面需要加"~",没有返回值,不写void,不可以有参数(不能发生重载)。
class Person{
public:
Person(){
cout << "构造函数调用" << endl; //自动调用
}
~Person(){
cout << "构造函数调用" << endl; //自动调用,在函数结束后
}
}
3)构造函数的分类及调用
构造函数按照参数进行分类,分为无参构造函数(默认构造函数)和有参构造函数;
class Person{
public:
//默认构造函数调用时不要加小括号
//加上括号编译器认为是函数声明,不会调用构造和析构
Person() {
cout << "默认构造函数" << endl;
}
Person(int a) {
cout << "有参构造函数" << endl;
}
};
按照类型进行分类,可分为普通构造函数和拷贝构造函数。
class Person{
public:
//拷贝构造函数
Person(const Person& p) {
cout << "拷贝构造函数" << endl;
}
Person(int a) {
cout << "有参构造函数" << endl;
}
};
构造函数的调用方式分为括号法调用、显示法调用以及隐式类型转换。
使用括号法调用时,默认构造函数不要加(),加括号编译器会认为是函数声明;
Person(100)叫做匿名对象,匿名对象特定,如果编译器发现对象是匿名的,那么在这行代码结束就释放这个对象;
不能用拷贝构造函数初始化匿名对象,如果写成Person(p5)编译器会认为写的是Person p5,按照对象的声明处理,写成右值才会按拷贝构造进行处理。
void test01(){
//括号法调用
Person p1(1); //有参
p1.m_age = 10;
Person p2(p1); //拷贝
Person p3; //默认构造函数,不要加(),加括号编译器会认为是函数声明
//显示法调用
Person p4 = Person(100); //有参函数调用
Person p5 = Person(p4); //拷贝函数调用
Person(100); //叫做匿名对象
//隐式类型转换
Person p = 100; //会使用隐式类型转换,转换成Person p = Person(100);
Person p6 = p5; //会转换成拷贝函数
}
4)拷贝构造函数调用时机
1.用已经创建好的对象来初始化新的对象
void test01(){
Person p1;
p1.m_age = 10;
Person p2(p1);
}
2.以值传递的方式给函数参数传值
//值传递会调用拷贝构造,引用传递不会调用拷贝构造
void doWork(Person p1) { //Person p1 = Person(p)
}
void test02() {
Person p;
p.m_age = 10;
doWork(p);
}
3.以值的方式返回局部对象
Person doWork2(){
Person p1;
return p1;
}
void test03() {
Person p = doWork2();
}
5)构造函数的调用规则
系统默认会给一个类提供三个函数:默认构造,拷贝构造,析构函数。
1.当提供了有参构造函数,系统就不会再提供默认构造函数,但是系统还会提供默认的拷贝构造函数;
2.当提供了拷贝构造,系统就不会提供其他构造了。
6)深拷贝和浅拷贝
有参构造函数就是为了初始化属性。
class Person {
public:
Person() {}
//初始化属性
Person(char * name, int age) {
m_Name = (char *)malloc(strlen(name) + 1);
strcpy(m_Name, name);
m_age = age;
}
//拷贝函数系统会默认提供,并且是简单的值拷贝
char * m_Name;
int m_age;
};
void test01() {
Person p1("老王", 10);
Person p2(p1); //调用了拷贝构造
}
当上述代码加上拷贝函数后,无法运行成功,因为当p1调用析构函数之后指针指向的地址里的内容已经被释放,当p2再调用析构时,m_Name也不为空,进行释地址内容时会发生异常。
//析构函数
~Person() {
cout << "析构函数调用" <<endl;
if(m_Name != NULL) { //m_Name是一个指针,需要释放
free(m_Name);
m_Name = NULL;
}
}
上述拷贝方式叫做浅拷贝,浅拷贝直接将p1的地址拷贝给p2,导致析构时释放堆区空间两次发生异常,解决方法是使用深拷贝。深拷贝不会直接拷贝地址,而是开辟一块新的空间,此时都调用析构函数时不会再发生错误。
class Person {
public:
Person() {}
//初始化属性
Person(char * name, int age) {
m_Name = (char *)malloc(strlen(name) + 1);
strcpy(m_Name, name);
m_age = age;
}
//拷贝函数系统会默认提供,并且是简单的值拷贝
//深拷贝写法
Person(const Person&p) {
m_age = p.m_age;
m_Name = (char *)malloc(strlen(p.m_Name) + 1);
strcpy(m_Name, p.m_Name);
}
//析构函数
~Person() {
cout << "析构函数调用" <<endl;
if(m_Name != NULL) { //m_Name是一个指针,需要释放
free(m_Name);
m_Name = NULL;
}
}
char * m_Name;
int m_age;
};
void test01() {
Person p1("老王", 10);
Person p2(p1); //调用了拷贝构造
}
7)初始化列表
数据的初始化可以使用有参构造函数以及初始化列表。
利用构造函数初始化数据:
class Person {
public:
//有参构造函数进行数据初始化
Person(int a, int b, int c) {
ma = a;
mb = b;
mc = c;
}
int ma;
int mb;
int mc;
};
void test() {
Person p1(10, 20, 30);
}
利用初始化列表初始化数据:构造函数后面 + : 属性(参数),属性(参数)...
class Person {
public:
//有参构造函数进行数据初始化
Person(int a, int b, int c) : ma(a), mb(b), mc(c) {}
int ma;
int mb;
int mc;
};
void test() {
Person p1(10, 20, 30);
}
8)类对象作为类成员的实例
class Phone {
public:
Phone(){
cout << "手机的默认构造函数调用" << endl;
}
Phone(string name){
Phone_Name = name;
}
~Phone(){
cout << "手机的析构函数调用" << endl;
}
string Phone_Name;
};
class Game {
public:
Game(){
cout << "游戏的默认构造函数调用" << endl;
}
Game(string name){
Game_Name = name;
}
~Game(){
cout << "游戏的析构函数调用" << endl;
}
string Game_Name;
};
class Person{
public:
Person(){
cout << "Person的默认构造函数调用" << endl;
}
Person(string name, string phoneName, string gameName){
m_name = name;
m_phone = phoneName;
m_game = gameName;
}
~Person(){
cout << "Person的析构函数调用" << endl;
}
string m_name;
Phone m_phone;
Game m_game;
};
void test() {
Person p("老王", "华为", "斗地主");
}
类对象作为类成员的时候,构造顺序先将类对象一一构造,然后构造自己,析构的顺序相反。
9)explicit关键字作用
使用了explicit关键字就不能用隐式类型转换来构造对象,explicit用来防止隐式类型转换。
10)动态对象创建
C语言提供了动态内存分配函数malloc和free,用于从堆中分配和释放存储单元,然而这些函数在C++中不能很好的运行,因为它不能很好完成对象的初始化工作。
1.使用malloc存在的问题:
程序员必须确定对象的长度;malloc返回一个void*指针,C++不允许将void*赋值给其他指针,必须强制类型转换;malloc可能申请内存失败,所以必须判断返回值确保内存分配成功;用户在使用对象之前必须对他初始化,构造函数不能显示调用初始化,用户可能会忘记调用。
2.可以使用new来开辟空间,new的对象会默认调用构造函数,使用方法如下:
Person * p2 = new Person;
3.new和malloc的区别:
new在堆区开辟空间,所有new出来的对象都会返回该类型的指针;
malloc不会调用构造,new会调用构造;
new是一个运算符,malloc是一个函数。
释放堆区的空间用delete,delete也是一个运算符,要配合new使用;malloc配合free使用。
void * p = new Person;
void*接收new的对象会造成无法释放,所以要避免这种写法。
4.通过new开辟数组
void test(){
Person * array = new Person[10]; //会调用十次默认构造函数
delete [] array; //释放数组对象必须要delete[]
}