构造函数
-
我们如果要了解类的构造函数是什么,就需要问自己以下几个问题。
-
构造函数长什么样子?
-
1,函数名和类名相同
-
2,没有返回值(函数类型是 void类型)
-
3,如果不写构造函数,任何类中都存在一个默认的构造函数
-
默认的构造函数是无参的。
-
当我们自己写了构造函数,默认的构造函数就不存在
-
-
构造函数在构造对象的时候调用
-
delete可以用来删掉默认的函数
-
指定使用默认的无参构造函数,用default说明
-
允许构造函数调用另一个构造函数,只是要用初始化参数列表的写法
-
初始化参数列表 : 只有构造函数有
构造函数名(参数1,参数2,...):成员1(参数1),成员2(参数2),...{}
-
避免形参名和数据成员名相同的导致问题
-
-
-
构造函数干嘛的?
-
构造函数用来构造对象
-
构造函数更多是用来初始化数据成员
-
-
思考题?
-
为什么不写构造函数可以构造对象? ——是因为存在一个默认的无参构造函数,所以可以构造无参对象
-
构造函数重载为了什么? ——为了构造不同的对象。
-
接下来我们就要通过写代码的形式来解释上面这几个特点
#include <iostream>
using namespace std;
class MM
{
public:
void print()
{
cout << name << " " << age << endl;
}
protected:
string name = "Lisa";
int age = 18;
};
int main() {
MM mm; /*如果我们没有写构造函数,
那么定义类类型变量的时候和结构体是一样的 */
mm.print();
/*打印结果
Lisa 18
*/
return 0;
}
- 当我们在类中加入 类型()=delete(MM()=delete)时就已经把默认构造函数删除,也就可以看见在main函数中的类类型变量的定义发生了报错。
。
class MM
{
public:
MM(string mmName, int mmAge) //这是自己写的构造函数,那么类类型的定义也要发生变化
{
name = mmName;
age = mmAge;
cout << "带参构造函数" << endl;
}
void print()
{
cout << name << " " << age << endl;
}
protected:
string name = "Lisa";
int age = 18;
};
int main() {
MM mm("小红",20); /*当我们自己加入了构造函数之后,
默认的构造函数也就失去的效果
所以像结构体那样的定义也会报错*/
mm.print();
/*打印结果
带参构造函数
小红 20
*/
return 0;
}
- 但是如果我们在自己定义了构造函数之后还想用无参数的构造函数怎么办呢?
这个时候我们有两种解决办法,第一种是自己创建一个无参数的构造函数;第二种是把原来默认的构造函数拿回来
class MM
{
public:
MM(){ //这是通过自己写无参数构造函数的方法
cout << "无参构造函数" << endl;
}
void print()
{
cout << name << " " << age << endl;
}
protected:
string name = "Lisa";
int age = 18;
};
int main() {
MM mm;
mm.print();
/*打印结果
无参构造函数
Lisa 18
*/
return 0;
}
class MM
{
public:
MM(string mmName, int mmAge)
{
name = mmName;
age = mmAge;
cout << "带参构造函数" << endl;
}
MM() = default; /*这样的操作就可以在已经有构造函数的情况下
再次调用原来的默认构造函数*/
void print()
{
cout << name << " " << age << endl;
}
protected:
string name = "Lisa";
int age = 18;
};
int main() {
MM mm("小红",20);
MM girl; //不报错
mm.print();
girl.print();
/*打印结果
带参构造函数
小红 20
Lisa 18
*/
return 0;
}
但是我们建议使用第二种方式,因为默认的构造函数比我们自己写的无参数构造函数速度快
- 我们写的构造函数也可以是重载函数或者是缺省函数,目的是为了构造不同的对象
//为了能够构造不同长相的对象,我们会给构造函数缺省处理 class Boy { public: //Boy(string mname="我是默认值", int mage=19) //{ // name = mname; // age = mage; //} // //上面函数 等效可以实现下面三个函数的功能 /*简单的说就是缺省函数的处理就是为了让函数有更多的调用形式 但是我们不建议用以下三种的方式去代替缺省函数,因为如果函数过多的话容易搞混*/ Boy() {} Boy(string mName) { name = mName; } Boy(string mName, int mage) { name = mName; age = mage; } void print() { cout << name << "\t" << age << endl; } protected: string name= "我是默认值"; int age= 19; }; int main() { Boy boy1; Boy boy2("流浪之子"); Boy boy3("王子", 18); boy1.print(); boy2.print(); boy3.print(); /*打印结果 我是默认值 19 流浪之子 19 王子 18 */ return 0; }
- 接下来我们来说一下什么是初始化参数列表,初始化参数列表也是用来构造对象的,并且它可以避免形参名和数据成员名相同的问题
class TT { public: TT(string name, int age) :name(name), age(age) {} /*我们可以看到形参名与数据成员名是一样的, 但是不用担心,它是可以自己去区别的,所以不会报错*/ //委托构造:允许构造函数调用另一个构造函数 TT() :TT("默认", 18) {} //没有给数据初始化时,用这个默认值来赋初值 void print() { cout << name << "\t" << age << endl; } protected: string name; int age; }; int main() { TT mm{ "小红",20 }; mm.print(); TT girl; //此变量没有初始化,所以就会用默认值 girl.print(); /*打印结果 小红 20 默认 18 */ return 0; }
- 对于初始化参数列表呢,它主要是用在继承和类的组合
- 还有一点,当我们在使用初始化参数列表给数据成员赋值的时候,可以用到全局变量,不一定非得用形参来赋值。
析构函数
-
析构函数长什么样子?
-
无返回值
-
无参数
-
函数名: ~类名
-
不写的话会存在默认的析构函数
-
析构函数不需要自己 调用,对象死亡的之前会调用析构函数
-
-
析构函数用来干嘛?(什么时候需要自己手动写析构函数)
-
当类中的数据成员是指针,并且动态申请内存就需要手写析构
-
析构函数用来释放数据成员申请动态内存
-
class MM
{
public:
MM(const char* mname, int age):age(age) {
name = new char[20];
strcpy(name, mname);
}
void print()
{
cout << name << "\t" << age << endl;
}
~MM() {
cout << "我是析构函数" << endl;
delete[]name;
}
protected:
char* name;
int age;
};
int main() {
{
MM mm{ "小红",20 };
mm.print();
}
cout << "我是主函数" << endl; /*由打印结果可以看出,
当mm的作用域结束后,就会自动调用析构函数*/
/*打印结果
* 小红 20
我是析构函数
我是主函数
*/
return 0;
}
- 要注意,析构函数不需要我们手动调用,如果手动调用可能会导致内存重复释放的问题
class MM
{
public:
MM(const char* mname, int age):age(age) {
name = new char[20];
strcpy(name, mname);
}
void print()
{
cout << name << "\t" << age << endl;
}
~MM() {
cout << "我是析构函数" << endl;
delete[]name;
}
protected:
char* name;
int age;
};
int main() {
MM* mm = new MM{ "小红",20 };
mm->print();
delete mm;
cout << "我是主函数" << endl; /*由打印结果可以看出,
如果定义类类型指针,一旦对内存进行释放
就会立刻调用析构函数*/
/*打印结果
* 小红 20
我是析构函数
我是主函数
*/
return 0;
}
构造和析构顺序问题
-
普通对象,构造顺序和析构顺序是相反
-
new出来的对象,delete会直接调用析构函数
-
static对象,当程序关闭的时候,生命周期才结束,所以是最后释放
#include <iostream> #include <string> using namespace std; class MM { public: MM(string name = "x") :name(name) { cout << name; } ~MM() { cout << name; } protected: string name; }; int main() { { MM mm1("A"); //A static MM mm2("B"); //B 程序关闭时候才死亡,最后析构 MM* p3 = new MM("C"); //C MM mm4[4]; //xxxx delete p3; //C delete 直接调用析构 p3 = nullptr; } /*打印结果 ABCxxxxCxxxxAB */ return 0; }
如果大家将上面这个例子的打印结果搞清楚,那么构造与析构的顺序问题就可以掌握了