类和对象(中)
1. 类的默认六个成员函数
如果一个类中什么成员都没有,称为空类。空类中什么都没有吗?并不是的。任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数。这就是C++比较复杂的初始化机制。
class Date{}
它们是特殊的成员函数,特殊的点非常多,后面一一展开。
2. 构造函数
2.1 构造函数概念
构造函数是特殊的成员函数。注意,构造函数的虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
在写数据结构时,我就吃过这样的亏,最后出了稀奇古怪的错误,调试然后发现忘记调用初始化函数了。忘记销毁了我好像还没有直观的感受,但我也看过别人有忘记释放资源把服务器搞挂了的故事。
🍓 那么构造函数就是,对象定义出来就自动调用,保证对象一定是被初始化的了。
2.2 构造函数特征
🍓 特性 ——
- 函数名和类名相同
- 无返回值
- 对象实例化时,编译器自动调用对应的构造函数
- 构造函数可以重载
❄️来看日期类 ——
class Date
{
public:
//1.无参构造函数
Date()
{
_year = 0;
_month = 1;
_day = 1;
}
//2.带参构造函数 - 初始化成指定值
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//对象实例化时,自动调用
Date d1;//调用无参构造函数
Date d2(2022, 1, 17);
return 0;
}
上面这两个构造函数构成了函数重载,其实他们也可以合并成一个函数,实现同样功能 —— 那就是通过全缺省【推荐】like this
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
注:无参构造函数 Date();
和全缺省函数Date(int year = 2002, int month = 2, int day = 19);
构成函数重载,语法上可以同时存在,但是,若有 无参调用Date d1;
,则有二义性会报错。
❄️5. 但是如果在类中我们没有写构造函数,则C++编译器会自动生成一个无参的默认构造函数,(一旦用户显式定义编译器将不再生成)。d1对象调用了编译器生成的默认构造函数,但是d对象的_year /_month/_day,依旧是随机值。那么这个默认生成的构造函数干了什么?
在C++中把类型分为了两类 ——
- 内置类型(基本类型)—— C语言原生带类型int/char/double/指针/内置类型的数组
- 自定义类型 —— struct/class定义的类型
🍓 我们啥也不写编译器会默认生成构造函数 ——
-
对于内置类型的成员变量不做处理
-
对于自定义类型的成员变量,会去调用它的默认构造函数(即不用传参就可以调)初始化
注:如果没有构造函数,编译器就会报错。(比如我显式的写了一个带参的
Date()
)
为此,写了一个自定义类型,来验证第二点 —— 对于自定义类型,会去调用它的默认构造函数
class A
{
public:
A()
{
cout << "A()" << endl;
}
private:
int _a;
};
class Date
{
public:
private:
int _year;
int _month;
int _day;
A _aa;
};
int main()
{
//对象实例化时,自动调用
Date d1;
return 0;
}
可以看到,确实是调了_aa的默认构造函数,打印了 ——
再来解释一下注 —— 所谓如果没有构造函数,编译器就会报错
如果我把上段代码中的class A
做一点修改,就报错了——
在上面代码我们实例化d1
时,在Date
类中我们啥也没写,对于自定义类型变量_aa
,会去调用它无参的默认构造函数。对于A
这个类,我们没有写无参/全缺省的构造函数,然后还故意手欠写了一个带参的,那编译器也就没再生成。这就没有默认构造函数可调了,就报错咯。
❄️6. 任何一个类的默认构造函数(不用参数就可以调用),有三个 —— 无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数。无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个(语法上他们可以同时存在,但是如果有对象定义去调用就会报错)。
3. 析构函数
3.1 析构函数概念
与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象的一些资源清理工作。
3.2 析构函数的特征
析构函数是特殊的成员函数。
🍓 特征 ——
- 析构函数名是在类名前加上字符
~
。 - 无参数无返回值。
- 一个类有且只有一个析构函数(无参数无法构成重载)。若未显式定义,系统会自动生成默认的析构函数。
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
以Date
类为例,通过打印/调试都能看到在d1生命周期结束时,编译器自动调用了析构函数 ——
class Date
{
public:
Date(int year = 2002, int month = 2, int day = 19)
{
_year = year;
_month = month;
_day = day;
}
~Date()
{
cout << "~Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
调试发现,这个析构函数好像什么都没有做。事实上,这个Date
类也没有资源需要清理,不是所有的类都要析构函数。所以对于它不实现析构函数都是可以的。
那对于我们之前实现的栈这个类 ——
class Stack
{
public:
//构造函数
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int)* capacity);
if (_a == nullptr)
{
cout << "malloc failed" << endl;
exit(-1);
}
_top = 0;
_capacity = capacity;
}
//析构函数
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack st1;
Stack st2(20);
return 0;
}
这样就保证了,栈定义出来,就一定被初始化了;出作用域,在堆上申请的空间一定被回收了。就不会再忘记手动Init和Destroy。
注:析构顺序?st2先清理,st1后清理(调试可看)
- 如果我们不写,编译器自动生成的析构函数,会做一些什么呢?
🍓与构造函数类似,它——
- 对内置类型的成员变量不做处理
- 对于自定义类型的成员变量会回去调它的析构函数
我们以用两个栈实现队列为例,(在这儿不谈题目思路,思路看我题解),主要看默认生成的作用 ——
class Stack
{
public:
//构造函数
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int)* capacity);
if (_a == nullptr)
{
cout << "malloc failed" << endl;
exit(-1);
}
_top = 0;
_capacity = capacity;
}
//析构函数
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
class MyQueue {
public:
// 我们不需要写,构造函数和析构函数
// 默认生成的很有用
// 对于自定义类型,会自动调用它的默认构造函数和析构函数
/*MyQueue() {} */
void push(int x) {}
int pop() {}
int peek() {}
bool empty() {}
private:
Stack _pushST;
Stack _popST;
};
int main()
{
MyQueue mq;
return 0;
}
而之前的C语言实现,哎,要手动调用 ——
MyQueue* myQueueCreate() {
MyQueue* q = (MyQueue*)malloc(sizeof(MyQueue));
StackInit(&q->pushST);
StackInit(&q->popST);
return q;
}
void myQueueFree(MyQueue* obj) {
StackDestroy(&obj->pushST);
StackDestroy(&obj->popST);
free(obj);
}
4. 拷贝构造函数
4.1 拷贝构造函数特征
拷贝构造函数也是特殊的成员函数。
🍓其特征如下:
-
拷贝构造函数是构造函数的一个重载形式。它的函数名就是类名,无返回值。
-
拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
class Date
{
public:
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//构造函数
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2002, 3, 7);
Date d2(d1);
return 0;
}
注:这里const Date&
常引用,最明显的原因是防止误写,当然还有很多原因,后续学习。
🍓下面来解释一下,为什么拷贝构造函数的必须使用引用传参,因为使用传值方式会引发无穷递归调用。
如果我们传值传参 ——
这里可能比较有疑惑的是,传值传参为什么是拷贝构造 ——
传值传参,就是把实参的值拷贝赋给形参,用同类型的来初始化你,其实就是一个拷贝构造。下面这段代码,调试可以观察到,先进入了拷贝构造函数,再进入了f(Date d)
函数 ——
但是引用传参,d
就是d1
的一个别名。
- 如果没有显式定义,系统生成默认的拷贝构造函数
🍓这块儿和之前的构造函数和析构函数有点差别 ——
- 对内置类型成员,会完成字节序拷贝(浅拷贝)
- 对自定义类型成员,会去调用它的拷贝构造
我们来验证一下:
可以看到,编译器生成的默认构造函数,对于内置类型成员,确实完成了字节序的拷贝。也就是说像日期类这样的我们完全可以不写。
而对于栈呢 ?我们还啥也不写 ——
class Stack
{
public:
//构造函数
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int)* capacity);
if (_a == nullptr)
{
cout << "malloc failed" << endl;
exit(-1);
}
_top = 0;
_capacity = capacity;
}
//析构函数
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack st1(10);
Stack st2(st1);
}
就崩了💩 ——
这是因为 ——
像这种类,就不能用默认的了,要我们自己实现。
对于自定义类型变量,确实会调用它的拷贝构造函数 ——
class A
{
public:
A(const A& a)
{
cout << "A(const A&)" << endl;
}
A()
{
}
};
class Date
{
public:
//构造函数
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
A _a;
};
int main()
{
Date d1(2002, 3, 7);
Date d2(d1);
return 0;
}
5. 总结
上文描述了太多细节了,确实容易晕,在此汇总一下,就非常非常清晰了 ——
5.1 构造函数
5.2 析构函数
5.3 拷贝构造函数
本文完@边通书