上期C++教程:超级详细的C++教程5:类和对象(上)
1.类的6个默认成员函数
如果一个类中什么成员都没有,简称为 空类。空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
2.构造函数
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void print()
{
cout << _year << "-";
cout << _month << "-";
cout << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
如果我们忘记调用Init函数进行初始化,直接使用print函数来进行打印,结果会是随机值。
所以一个对象出来应该是要被初始化一下,所以C++中就有了构造函数,构造函数是一个特殊的成员函数,它在对象被创建时自动调用。构造函数的主要任务是初始化类的数据成员。
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。构造函数的特征:1. 函数名与类名相同。2. 无返回值(不需要写void)。3. 对象实例化时编译器自动调用对应的构造函数。4. 构造函数可以重载。
1.构造函数的定义
构造函数的定义方法,如下面Date类的构造函数,例如:
class Date
{
public:
Date()//Date类的构造函数
{
_year = 1;
_month = 1;
_day = 1;
}
void print()
{
cout << _year << "-";
cout << _month << "-";
cout << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
对象实例化时编译器自动调用对应的构造函数,例如:
int main()
{
Date d;
d.print();
return 0;
}
编译运行后打印出来的结果为“1-1-1”,说明编译器会自动调用构造函数。
2.构造函数的重载
构造函数可以重载,例如:
class Date
{
public:
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void print()
{
cout << _year << "-";
cout << _month << "-";
cout << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
如果要调用无参的构造函数直接实例化对象就行,但是要调用有参构造函数,需要在实例化对象的时候加上参数,例如:
int main()
{
Date d1;
d1.print();
Date d2(2024,11,11);
d2.print();
return 0;
}
编译运行后d1打印出来的结果为“1-1-1”,d2打印出来的结果为“2024-11-11”。第一个对象d1编译器会自动调用无参的构造函数,第二个对象d2编译器会自动调用有参的构造函数,如下图所示:
需要注意的点:
1.实例化对象的时候没有参数时不能加上括号,例如:
int main()
{
//错误代码演示,实例化对象的时候没有参数时不能加上括号
Date d1();
return 0;
}
如果加上了括号,就和函数的声明分不开,d1就可以看作是一个函数名,它没有参数,返回值是Date。所以实例化对象的时候没有参数时不能加上括号。
2.全缺省的构造函数不能和无参构造函数同时存在,如下图所示:
如果全缺省的构造函数和无参构造函数同时存在,那么在调用的时候会出现歧义,编译器不知道该调用哪一个构造函数。
建议构造函数函数实现成全缺省,缺省参数可以让函数的调用变得灵活,这样可以让我们实例化对象的时候选择变得丰富起来;
3.如果只实现了带参的构造函数,实例化对象的时候必须加上参数,否则编译器会报错,例如:
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year,int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void print()
{
cout << _year << "-";
cout << _month << "-";
cout << _day << endl;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
int main()
{
Date d;//没有合适的构造函数可以调用
d.print();
return 0;
}
编译器报的错是没有合适的默认构造函数,默认构造是我们下面所要学习的。
3.默认构造函数
在学习默认构造函数前,我们要先了解一下C++类型的划分:
C++将类型划分为内置类型和自定义类型;
内置类型:int,double,char......(指针(任何类型的指针,包括自定义类型的指针)也是内置类型)
自定义类型:class,struct......
如果我们不实现构造函数,编译器会默认生成构造函数,如下面的Date类,例如:
class Date
{
public:
void print()
{
cout << _year << "-";
cout << _month << "-";
cout << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
d.print();
return 0;
}
编译运行后打印出来的结果为随机值,难道编译器默认生成的构造函数什么事都不做吗?答案不是什么事都不做,默认生成的构造函数内置类型成员不做处理,自定义类型的成员会去调用它的默认构造函数,例如:
#include <iostream>
using namespace std;
class Timer
{
public:
Timer()
{
_hour = 1;
_minute = 1;
_second = 1;
}
void print()
{
cout << _hour << "-";
cout << _minute << "-";
cout << _second << endl;
}
int _hour;
int _minute;
int _second;
};
class Date
{
public:
void print()
{
cout << _year << "-";
cout << _month << "-";
cout << _day << endl;
}
Timer t;
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
d.t.print();
return 0;
}
运行后打印出来的结果为“1-1-1”,所以默认生成的构造函数会去调用自定义类型成员的默认构造函数。
无参的构造函数、全缺省的构造函数、编译器所生成的构造函数都是默认构造函数,这三个函数不能同时存在,或者会有歧义;
无参的构造函数和全缺省的构造函数是用户自己实现的,编译器所生成的构造函数是编译器自动生成的。
如果我们显示的写了构造函数,编译器就不会去生成默认的构造函数。
4.内置类型成员变量的缺省值
C++规定编译器默认生成的构造函数不处理内置类型,只处理自定义类型这种规则并不好,于是C++11打了个补丁,声明时可以给缺省值,例如:
class Date
{
public:
void print()
{
cout << _year << "-";
cout << _month << "-";
cout << _day << endl;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
内置类型的成员变量给了缺省值依旧是声明,只有实例化对象是才会被定义。
内置类型的成员变量给的缺省值是给编译器所生成默认的构造函数所使用的,例如:
int main()
{
Date d;
d.print();
return 0;
}
运行后打印出来的结果为“1-1-1”,所以内置类型的成员变量给的缺省值是给编译器所生成默认的构造函数所使用的。
什么情况下我们可以不写构造函数,让编译器自动生成:
1.成员变量全部是自定义类型;
2.内置类型的成员变量给了缺省值;
6.总结
1.构造函数的特征: 函数名与类名相同、无返回值(不需要写void)、 对象实例化时编译器自动调用对应的构造函数、构造函数可以重载;2.实例化对象的时候没有参数时不能加上括号;3.全缺省的构造函数不能和无参构造函数同时存在;4.如果我们不实现构造函数,编译器会默认生成构造函数,内置类型不做处理,自定义类型会去调用它的默认构造函数;5.如果我们显示的写了构造函数,编译器就不会去生成默认的构造函数;6.无参的构造函数、全缺省的构造函数、编译器所生成的构造函数都是默认构造函数,这三个函数不能同时存在,否则会有歧义;7.内置类型的成员变量给的缺省值是给编译器所生成默认的构造函数所使用的;
了解部分:
C++语言规则错了后面只打补丁为什么不去改规则,如上面的编译器所生成的构造函数只处理自定义类型,而不处理内置类型?
原因是不能改,只能向前兼容,别人可能已经使用这个规则使用写了代码,如果改了会导致写好的代码出bug。
注意:
构造函数虽然叫构造函数,它不开空间(对象的空间不是构造函数开的)。它只负责对象的初始化,对象的空间是编译器编译的时候建立栈帧时开的,不是构造函数开的,本质的原因是操作系统。
3.析构函数
类里面的构造函数可以在实例化对象的时候帮我们初始化对象,类里面有没有在对象销毁时去做清理工作的成员函数呢?答案是有的,它是析构函数。
析构函数的概念:
1. 析构函数名是在类名前加上字符 ~;2. 无参数无返回值类型;3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载;4. 对象生命周期结束时,C++编译系统系统自动调用析构函数;
析构函数的定义,如下面的Date类,例如:
class Date
{
public:
Date(int year = 1,int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void print()
{
cout << _year << "-";
cout << _month << "-";
cout << _day << endl;
}
~Date()//Date类的析构函数
{
_year = 0;
_month = 0;
_day = 0;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
在对象的生命周期结束时,编译器会自动调用析构函数。Date类其实不用写析构函数,因为没有什么可以清理,如果在堆上开辟了空间,才需要去显示写的析构函数,例如:
#include <iostream>
#include <stdlib.h>
using namespace std;
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int)* capacity);
if(_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_top = 0;
_capacity = capacity;
}
void push(int x)
{
//.....
}
~Stack()
{
free(_a);
_a = nullptr;
_top = 0;
_capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
在C语言中学习数据结构时,初始化你也许可能不会忘记,但是销毁忘记的次数绝对不会是少数,不销毁会造成内存泄漏,而析构函数在对象的生命周期结束时自动调用,我们只需要去实现析构函数就好了,以后就再也不怕忘记调用销毁函数而造成内存泄漏了。
注意:
析构函数虽然叫析构函数,它不释放对象的空间,当对象的生命周期结束,它所在的栈帧销毁,还给操作系统。
如果我们不显示的写析构函数,编译器会默认生成一个析构函数,和构造函数类似,编译器生成的析构函数内置类型不做处理,自定义类型会去调用它的析构函数,例如:
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int)* capacity);
if(_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_top = 0;
_capacity = capacity;
}
void push(int x)
{
//.....
}
~Stack()
{
cout << "Stack的析构函数" << endl;
free(_a);
_top = 0;
_capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
class A
{
public:
Stack s;
int a = 10;
};
int main()
{
A a;
return 0;
}
编译器生成的析构函数,它不会处理内置类型,指针也是内置类型,如果在堆上申请了空间,编译器所生成的析构函数是不会帮我们去释放的,因为它不敢,怕出问题,比如后面我们学STL的模拟实现时会去实现容器的迭代器,迭代器指向的空间是不能被释放的。
什么情况下我们可以不写析构,直接使用编译器生成的析构函数:
1.没有在堆上开辟空间;
2.成员变量都是自定义类型;
总结:
1.析构函数的概念:析构函数名是在类名前加上字符 ~、无参数无返回值类型、一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载、对象生命周期结束时,C++编译系统系统自动调用析构函数;
2.编译器会默认生成一个析构函数,和构造函数类似,编译器生成的析构函数内置类型不做处理,自定义类型会去调用它的析构函数。