?本博客将记录:类的相关知识点的第6节的笔记!
(这个在学习C++基础课程时已经学习过一次了,这里再次简单地回顾一下而已)
今天总结的知识分为以下2个点:
? 一、重载运算符(最经典的就是拷贝赋值运算符的重载) ? 二、析构函数
? 三、补充知识 ? ? ? ? a)函数重载 b)构造函数的成员初始化 c)析构函数的成员销毁?d)new对象和delete对象
? 一、重载运算符:
????????所谓的重载运算符,概括地说就是:(根据你自己的需求)重载一些运算符的函数,使得你可以进行各种自定义类型对象的比较或者赋值等操作。
重载任何一个运算符的格式:
函数返回值 operatorXX符号(形参){/.../}
当然,这个operatorXX运算符的重载函数大括号{里面的函数体你要写的逻辑就由程序员自身按需来决定了}
重载运算符本质上 就是一个函数。整个函数的正式名字:operator关键字 + 运算符号
比如:为了能够精确地控制自定义数据类型(类类型or结构体类型)的赋值动作,我们程序员往往会选择自己来定义一个重载=号运算符的函数。
下面这份代码中我自己的分类总结非常非常非常(重要的事情说三遍)地重要!!!
在之前,我们已经学习过(或使用过)很多常见的运算符,比如:
==,>,>=,<,<=,!=,++,--,+=,-=,+,-,(用于cout的)<<,(用于cin的)>>,=赋值等运算符
还有一个特殊的符号:函数调用的符号()
①类:operator ==,>,>=,<,<=,!= 这些符号一般都是用布尔值bool作其重载函数的返回值的!
②类:operator ++,--,+=,-=,+,- 这些符号一般都是用void作其重载函数的返回值的!
(因为这只是让对象和对象间的某个or某些成员变量做计算的操作而已,不需要啥返回值)
③类(特殊):operator(用于cout的)<<,(用于cin的)>>
这2个运算符就必须要返回 ostream& 和 istream& 类型的变量了
因为cin 和 cout是属于标准的输入输出流iostream类的!
比如:
ostream& operator<<(const ostream& out,const Person& p){
//当然const ostream& out是不对的!对于输出流类来说只能用ostream&作为传入的参数的类型
//且<<左移运算符必须要用作全局函数,一定不可单独在某个类中进行重载!
//某个类中若想用到你定义的重载左移运算符的函数,就必须要声明为friend友元函数才能使用!
out<<p.m_Age<<"\t"<<p.m_Name<<endl;
return out;
}
④类(特殊):operator=号运算符 就必须要让使用该重载运算符的 类类型的引用 作为该函数的返回值
//因为只有这样才能让对象与对象之间也可以do链式赋值的操作!比如 p1 = p2 = p3;
//就类似于int a=0,b,c; a = b = c;
比如:
Person& operator=(const Person& p){
this->m_Age = p.m_Age;
this->m_Name = p.m_Name;
return *this;
}
注意:当我们写一个类时,如果我们不写任何的构造函数和析构函数。那么编译器就会自动给我们生成一个默认的构造函数,一个默认的拷贝构造函数,一个默认的重载=号赋值运算符的函数,和一个析构函数。
当我们正常地对内置的数据类型使用这些操作时,一点问题都没有哈。
请看以下代码:
//在main.cpp中写下
int a = 1, b = 1;
if (a == b) cout << "a == b" << endl;
//result: a == b
但是,当我们对自定义的类型(类or结构体类型)使用这些操作时,就会出大问题。
请看以下代码:
#include<iostream>
#include<string>
using namespace std;
class Person {
public:
int m_Age;
string m_Name;
public:
Person():m_Age(0),m_Name(""){}
Person(const int& age,const string& name) :m_Age(age), m_Name(name) {}
};
int main(void) {
Person p1(22, "lzf");
Person p2(21, "lyf");
if (p1 == p2) cout << "p1 == p2 " << endl;
//报错!因为Person类中并没有重载==符号,所以你没法do这种直接比较是否相等的运算!
return 0;
}
?你如果不重载该==符号的运算符函数的话,编译器根本就不知道你要比较的是Person对象中的哪个成员变量,编译器的“头也晕乎乎的”,不知所措!
因此,我现在就引入运算符重载这一个小知识点!
在这个例子中,我只需要在Person类中重载==号运算符就可以做以上的p1和p2对象的比较操作了。
请看以下代码:
#include<iostream>
#include<string>
using namespace std;
class Person {
public:
int m_Age;
string m_Name;
public:
Person() :m_Age(0), m_Name("") {}
Person(const int& age, const string& name) :m_Age(age), m_Name(name) {}
bool operator==(const Person& p) {
//用Person对象的成员变量m_Age年龄的高低作为两个对象之间的比较转换结果
if (this->m_Age == p.m_Age)return true;
return false;
}
void showInfo() {
cout << "Age: " << this->m_Age <<
"\tName: " << this->m_Name << endl;
}
};
int main(void) {
Person p1(22, "lzf");
Person p2(21, "lyf");
p1.showInfo();//Age:22 Name:lzf
p2.showInfo();//Age:21 Name:lyf
if (p1 == p2) cout << "p1 == p2 " << endl;
else cout << "p1 != p2 " << endl;
if(p1.operator==(p2)) cout << "p1 == p2 " << endl;
cout << "p1 != p2 " << endl;
return 0;
}
运行结果:
? ? ? ? 从运行结果可以看出,当我们重载了满足需要的运算符之后,就可以对自定义类型的对象做这种运算符的操作了!
? 二、析构函数(总结析构函数时还会回顾下面这4个小问题)
????????析构函数与构造函数正好的相互对立而存在的。创建对象时编译器会自动地帮你调用构造函数,而释放对象(或者说对象被销毁时)时编译器也会自动地帮你调用析构函数。构造函数也属于一个类的成员函数。
析构函数的创建格式:
~类名(无参数){//也无返回值
/.../
}
注意事项(共4条):
①析构函数无参数,无返回值,也不可以发生函数的重载。一个类只能存在唯一一个析构函数。
②就算你在类中不自己定义一个析构函数,编译器也会自动给我们生成要给默认的空实现的析构函数!
????????比如上述我写的Person类中就没有自定义其对应的析构函数,那么此时编译器就会为我们自动定义一个默认的空实现的析构函数!
~Person(){//默认的空实现的析构函数
//空实现,啥都不do
}
③在一个函数中,如果创建了某个类的对象,那么在离开这个函数的作用域之后,这个局部对象所占据的空间(栈区空间和堆区空间)会给释放掉,对于只存在于栈区的对象的空间,一般我们都不需要do什么事情来释放,因为在离开这个函数后OS也会给我们自动释放掉。但是,存在于堆区的对象的空间就必须要我们手动开辟后手动释放了!
????????一般,如果我们在创建类的对象,在heap堆区new了一段内存,那么就必须要用delete关键字把我们手动在堆区开辟的内存给释放掉,且注意:
①new x;? ?<--> delete x;
②new x[]; <--> delete[] x;
new和delete的动作只有这两种case,你要记住,只能够让new和delete配对使用,也即要么用①do一次释放操作,要么用②do一次释放操作!
那么具体的析构函数的操作请看以下代码:
#include<iostream>
#include<string>
using namespace std;
class Person {
public:
int* m_Age;
string m_Name;
double* scores;//保存语数英三科的成绩
public:
Person() :m_Age(nullptr), m_Name(""), scores(nullptr) {}
Person(const int& age,const string& name,const double& chinese,const double& math,const double& english)
: m_Name(name){
m_Age = new int(age);
scores = new double[3];//在堆区开辟一个大小为3的数组空间,数组每一个元素存储的是double型的数据
scores[0] = chinese;
scores[1] = math;
scores[2] = english;
}
bool operator==(const Person& p) {
if (this->m_Age == p.m_Age)return true;
return false;
}
void showInfo() {
cout << "Age: " << *this->m_Age <<
"\t Name: " << this->m_Name << " ";
cout << "Marks: ";
for (auto i = 0; i < 3;i++) {
cout << scores[i] << "\t";
}
cout << endl;
}
Person& operator=(const Person& p) {
cout << "=号赋值运算符被调用了!" << endl;
this->m_Age = p.m_Age;
this->m_Name = p.m_Name;
for (int i = 0; i < 3; i++) {
scores[i] = p.scores[i];
}
return *this;
}
~Person() {
cout << "析构函数~Person()被调用了!" << endl;
//这是释放变量x的标准代码,不为空表明在堆区成功地开辟了空间,此时才需要释放(不成功开辟你就不用释放啦)
if (m_Age != nullptr) {
delete m_Age;//释放在heap堆区开辟的存放int型变量的空间
m_Age = nullptr;//并让该指针指向nullptr,防止其乱指向从而变成野指针!
}
//这是释放数组x[n]的标准代码,不为空表明在堆区成功地开辟了空间,此时才需要释放(不成功开辟你就不用释放啦)
if (scores != nullptr) {
delete[] scores;//释放在heap堆区开辟的存放3个double型变量的数组[]空间
scores = nullptr;//并让该指针指向nullptr,防止其乱指向从而变成野指针!
}
}
};
void test() {
Person p1(22, "lzf",100,100,100);
Person p2(21, "lyf",99,99,98);
p1.showInfo();//Age:22 Name:lzf Marks: 100 100 100
cout << "-------------------------------------------------" << endl;
p2.showInfo();//Age:21 Name:lyf Marks: 99 99 98
}
int main(void) {
test();
return 0;
}
运行结果:
④析构顺序于构造函数的顺序是正好相反的!
不信的话请看以下代码:
Time.h
#ifndef __TIME_H__
#define __TIME_H__
#include<iostream>
using namespace std;
class Time {
public:
mutable int m_Hour;//时
int m_Minute;//分
int m_Second;//秒
public:
Time():m_Hour(0),m_Minute(0),m_Second(0){
cout << "调用了 m_Hour = " << this->m_Hour << "的Time类的构造函数!" << endl; }
Time(int h,int m,int s) :m_Hour(h), m_Minute(m), m_Second(s) {
cout << "调用了 m_Hour = " << this->m_Hour << "的Time类的构造函数!" << endl; }
~Time() {
cout << "调用了 m_Hour = " << this->m_Hour << "的Time类的析构函数!" << endl;
}
};
#endif
main.cpp
#include<iostream>
#include"Time.h"
using namespace std;
void test() {
Time t1(1,1,1);
Time t2(2,2,2);
Time t3(3,3,3);
}
int main(void) {
test();
return 0;
}
运行结果:
?从上面的运行结果,相信你已经非常清楚构造函数和析构函数的调用顺序了,即:
1)创建对象时先创建的对象会优先调用其构造函数以创建该对象。
2)释放对象时后创建的对象会优先调用其析构函数释放该对象
(类比数据结构当中的栈stack结构,我们可以这样记:后创建的先析构、先创建的后析构)
? 三、补充知识:
(这些小的细节知识点往往就是我们在学习Cpp时遇到疑惑或者迷茫的小点,所以我们务必搞clear这些小点,这样才能对C++11,14,17的各种新特性的知识更有整体性的把握!)
a)函数重载
????????相信但凡是看过我前面的详述或者之前的博客中总结的笔记的小伙伴都知道这个小知识点靠啥了吧?这里就不多赘述了?只提及几个关键字让大家一起回忆起来。
????????1-函数重载的三大条件是啥?(参数个数or类型or顺序这3大条件不同)
????????2-还有一个特殊case下也可作为函数重载条件的关键字是谁?(const关键字用在类定义中可作类的成员函数的重载)
????????3-函数返回值可以作函数重载的条件吗?(肯定不可以)
b)构造函数的成员初始化
? ? ? ? ? ? ? ?其实,类中的非拷贝的构造函数本质上是干了两件事情的!
????????第一个是进行成员变量的初始化(构造函数的函数体之外)
????????第二个就是成员变量之间的赋值(构造函数的函数体之内)
????????我们千万不能把构造函数中的成员变量的初始化和成员变量之间的赋值操作这两种本质上不一样的操作混淆在一起!
????????1-在函数体之外的成员初始化列表中do的就是成员变量的初始化工作!
????????2-大括号内,或者说构造函数的函数体内的语句都不是成员变量初始化的时间(作用域)
????????(这也就解释了为什么之前我总结说,规范的专业的构造函数的写法是一定是要写为成员初始化列表的形式的!否则,你直接在构造函数的函数体内去do赋值操作,这就白白浪费了我们自己给成员变量初始化的时机了,此时编译器在成员变量初始化时就会随机给该类对象的成员变量分配值,等执行到构造函数的函数体内的赋值语句的时候再用对应的值覆盖掉这个随机的值,这样不是专业的规范的写法!)
请看以下代码:
//不规范的不专业的很粗鲁的构造函数写法:
Time(int h,int m,int s)//没有成员初始化列表,就默认了是让编译器给类的成员变量do随机的初始化(不好)
{
this->m_Hour = h;
this->m_Minute = m;
this->m_Second = s;
}
//规范的专业的构造函数写法:
Time(int h,int m,int s):m_Hour(h),m_Minute(m),m_Second(s)
//在函数体之外 的成员初始化列表中 do的就是成员变量的初始化工作!
{
//大括号内的,或者说构造函数的函数体内的语句都不是成员变量初始化的时间(作用域)
}
还有一个小坑:
????????成员变量的在构造函数中的初始化时机与其在该类中的声明的先后顺序有关,也即:在该类中
????????1-先声明的成员变量就会先给构造函数初始化
????????2-后声明的成员变量就后给(构造函数)初始化
????????因此我们一定不能这样写构造函数:
Time(int h,int m,int s):m_Minute(m),m_Hour(h),m_Second(s){}//×!
Time(int h,int m,int s):m_Minute(h),m_Hour(m),m_Second(s){}//×!
Time(int h,int m,int s):m_Minute(m_Second),m_Hour(m_Minute),m_Second(s){}//×!
.....还有许多错误的写法!即使编译器没有察觉出来你也不能这么干!
这些写法都是错误的!我们必须老老实实地按照类中声明的成员变量的顺序来给成员变量初始化!
c)析构函数的成员销毁?????????
????????其实,类中的析构函数本质上也是干了两件事情的!???????
????????第一个是(手动)释放成员变量在堆区中开辟的空间(析构函数的函数体之内)
//因为编译器不会给我们自动的do delete操作,因此我们手动new了,就必须手动写对应的delete语句!
(当然,我后面讲解会讲到有给我们自动释放的智能指针的几种类型)
new x;<-->delete x; 和
new?x[n];<-->delete[] x;
(我们手动写的上述这些语句就是在析构函数的 函数体内 去释放掉堆区中我们手动开辟的空间的!)
????????我们可以理解为,析构函数只会do释放我们new的堆区内存的工作,析构函数执行完成之后,才有第二步。如果我们不手动delete在堆区开辟的内存,会引起内存泄露!而一旦内存泄露得多了,超出OS允许你的代码使用的内存时,那么你的程序很有可能会崩溃掉(给OS kill掉了)!
????????第二个就是销毁该对象以及其所对应的各个成员变量(析构函数的函数体之外,或者说是离开了该类的析构函数才会让编译器销毁掉该对象以及其所对应的各个成员变量)
d)new对象和delete对象
????????其实就是用new去创建某个类的对象,然后一定记得再用delete去释放掉刚才我们开辟在heap堆区的存储一个该类对象的内存空间即可。
(delete对象时,编译器也会自动调用该类的析构函数以释放掉该对象以及顺便释放掉其all的成员变量)
下面废话不多说了,直接看代码:
//仍然以上述写过的Time.h do例子
#include<iostream>
#include"Time.h"
using namespace std;
void test() {
Time* t1 = new Time;//调用默认无参的构造函数
Time* t2 = new Time();//调用默认无参的构造函数
//其实,t1和t2的new的方式没啥区别,wjw老师让我们直接当成等价的语句即可了!
//如果你硬是要理解的话,我觉得不妨可以这样理解:
/*t1则是直接在堆区开辟一个空间,且t1本身就是调用的无参的构造函数来创建的!
或者说t2是在堆区开辟了一个空间,这个空间存储的是一个匿名对象
而这个匿名对象是调用的默认的无参构造函数来创建的
然后再把这个匿名对象的值作为t2的初始值。*/
Time* t3 = new Time(3, 3, 3);//调用有参构造函数
Time* t4 = new Time(4, 4, 4);//调用有参构造函数
Time* t5 = new Time(5, 5, 5);//调用有参构造函数
//我们一定要注意这样一个点:编译器不会自动帮我们释放我们自己new出来的all的对象的!
//so我们务必要注意:自己new的对象,shi也要自己delete掉
//不然就算是离开了main函数编译器也不会给你自动释放掉
//你在堆区为new 对象而开辟的内存空间!否则,就和不delete成员变量的后果一样严重
//同样地也会造成内存的泄露!
//你什么时候delete,系统就会在什么时候调用该类的析构函数去释放掉该类以及其all的成员变量
delete t1;
delete t2;
delete t3;
delete t4;
delete t5;
}
int main(void) {
test();
return 0;
}
?
???????好,那么以上就是这一3.6小节我所回顾的内容的学习笔记,希望你能读懂并且消化完,也希望自己能牢记这些小小的细节知识点,加油吧,我们都在coding的路上~
|