类和对象(中)(1)
类的默认成员函数
默认成员函数就是用户没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数。
⼀个类,我们不写的情况下编译器会默认⽣成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最后两个取地址重载不重要,稍微了解⼀下即可。
其次就是C++11以后还会增加两个默认成员函数,移动构造和移动赋值,这个以后再说。默认成员函数很重要,也比较复杂,我们要从两个方面去学习:
• 第一:我们不写时,编译器默认生成的函数⾏为是什么,是否满⾜我们的需求。
• 第二:编译器默认⽣成的函数不满⾜我们的需求,我们需要自己实现,那么如何自己实现?
注意,我们说C++复杂,其实从这一部分就开始体现。这一部分:知识点很多,彼此间千丝万缕关系,要注意的点很多。
构造函数
(最复杂的一个)
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象(我们常使用的局部对象是栈帧创建时,空间就开好了),⽽是对象实例化时初始化对象。构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能,构造函数⾃动调⽤的特点就完美的替代的了Init。
构造函数的特点
我们先来理解这四点。
- 函数名与类名相同。
- ⽆返回值。(返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此)
- 对象实例化时系统会自动调用对应的构造函数。
- 构造函数可以重载。
- 如果类中没有显式定义构造函数,则C++编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦用户显式定义编译器将不再⽣成。
#include<iostream>
using namespace std;
class Date
{
public:// 1.⽆参构造函数 Date()//函数名和类名相同,没有返回值连void都没有{_year = 1;_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(2025, 1, 1); // 调⽤带参的构造函数的写法return 0;
}
然后
#include<iostream>
using namespace std;
class Date
{
public:// 1.⽆参构造函数 //Date()//函数名和类名相同,没有返回值连void都没有//{// _year = 1;// _month = 1;// _day = 1;//}// 2.带参构造函数 //Date(int year, int month, int day)//{// _year = year;// _month = month;// _day = day;//}// 3.全缺省构造函数 //和第一种构成函数重载,但是会产生歧义,所以不能同时存在Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}private:int _year;int _month;int _day;
};int main()
{Date d1; // 调用了默认构造函数 Date d2(2025, 1, 1); // 调⽤带参的构造函数的写法Date d3(2024);//调用了全缺省的构造函数return 0;
}
然后
-
⽆参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的构造函数,都叫做默认构造函数。但是这三个函数有且只有⼀个存在,不能同时存在。
⽆参构造函数和全缺省构造函数虽然构成函数重载,但是调⽤时会存在歧义。要注意很多人会认为默认构造函数是编译器默认⽣成那个叫默认构造,实际上⽆参构造函数、全缺省构造函数也是默认构造,总结⼀下就是不传实参就可以调用的构造就叫默认构造!(可以理解为“无实参构造”)
-
我们不写,编译器默认生成的构造,对内置类型成员变量的初始化没有要求,也就是说是否初始化是不确定的,看编译器。(早期的编译器对于内置类型都不初始化,也就是随机值)(建议当做不处理)
对于⾃定义类型成员变量,要求调⽤这个成员变量的默认构造函数(注意有三种)初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要⽤初始化列表才能解决,初始化列表,我们之后再说。
说明:C++把类型分成内置类型(基本类型)和⾃定义类型。内置类型就是语⾔提供的原⽣数据类型, 如:int/char/double/指针等,⾃定义类型就是我们使⽤class/struct等关键字⾃⼰定义的类型。
大多数情况下我们不写,编译器默认生成的函数不能满足我们的需求。所以大多数情况下构造函数需要我们自己写。
下面的这个场景中,编译器默认⽣成MyQueue的构造函数调⽤了Stack的构造,完成了两个成员的初始化 ,是一个默认构造有用的例子。
#include<iostream>
using namespace std;typedef int STDataType;
class Stack
{
public:Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}// ...
private:STDataType* _a;size_t _capacity;size_t _top;
};// 两个Stack实现队列
class MyQueue
{
public://编译器默认⽣成MyQueue的构造函数调⽤了Stack的构造,完成了两个成员的初始化
private:Stack pushst;Stack popst;//int size;//如果监视我们可以看到是0不是随机值,这里倒是编译器处理了内置类型。但是是个坑,以后再说解决方法。
};int main()
{MyQueue mq;return 0;
}
>总结:
大多数情况下构造函数都需要自己实现,少数情况类似MyQueue且Stack有默认构造时,MyQueue自动生成的就可以用。(等一个后续C++11增加的缺省值的概念)
构造函数,应写尽写。
析构函数
(可以感受到设计的好处)
析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,⽐如局部对象是存在栈帧的, 函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会⾃动调⽤析构函数,完成对象中资源的清理释放工作。析构函数的功能类⽐我们之前Stack实现的Destroy功能,⽽像Date没有 Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的。
我们在main中定义一个对象,是在编译时系统就开好空间了。销毁也是在main结束时和函数栈帧一起销毁。所以空间销毁不是我们担心的问题,也就是“析构函数不是完成对对象本身的销毁”。
析构函数的特点
- 析构函数名是在类名前加上字符**~**。
- ⽆参数⽆返回值(就不会存在重载情况了)。(这⾥跟构造类似,也不需要加void)
- ⼀个类只能有⼀个析构函数。若未显式定义,系统会⾃动⽣成默认的析构函数。
- 对象⽣命周期结束时,系统会⾃动调⽤析构函数。
- 跟构造函数类似,我们不写,编译器⾃动⽣成的析构函数对内置类型成员不做处理,⾃定类型成员会调⽤他的析构函数。
#include<iostream>
using namespace std;typedef int STDataType;
class Stack
{
public:Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}~Stack(){free(_a);_a = nullptr;_top = _capacity = 0;}private:STDataType* _a;size_t _capacity;size_t _top;
};int main()
{Stack st1;//只要写这一句return 0;
}
可以看到,对象实例化时系统会自动调用对应的构造函数。我们完成了对象初始化。
再看,对象⽣命周期结束时,系统会⾃动调⽤析构函数。
所以我们可以发现,原本我们需要显示去调用函数Init和Destroy,现在都不需要了,我们只写一句 Stack st1;
就会在实例化时自动初始化,在main函数栈帧要销毁时(也就是对象生命周期结束时)自动释放对象的资源。
-
还需要注意的是我们显⽰写析构函数,对于自定义类型成员也会调⽤他的析构,也就是说自定义类型成员⽆论什么情况都会⾃动调⽤析构函数。(怕出现内存泄漏)
-
如果类中没有申请资源时,析构函数可以不写,直接使⽤编译器⽣成的默认析构函数,如Date;
如果默认⽣成的析构就可以⽤,也就不需要显⽰写析构,如MyQueue;
但是有资源申请时,⼀定要⾃⼰写析构,否则会造成资源泄漏,如Stack。
-
⼀个局部域的多个对象,C++规定后定义的先析构。(栈帧的性质就像数据结构中的栈,要求后进先出)
也就是析构函数我们只需要去管内置类型中有资源申请的,自定义类型的成员可以不管。
》在学习栈的时候我们曾经做过一道匹配括号的题目(没做过的可以跳过该部分)
// ⽤之前C版本Stack实现
bool isValid(const char* s) {ST st;STInit(&st);while (*s){// 左括号⼊栈 if (*s == '(' || *s == '[' || *s == '{'){STPush(&st, *s);}else // 右括号取栈顶左括号尝试匹配 {if (STEmpty(&st)){STDestroy(&st);return false;}char top = STTop(&st);STPop(&st);// 不匹配 if ((top == '(' && *s != ')')|| (top == '{' && *s != '}')|| (top == '[' && *s != ']')){STDestroy(&st);return false;}}++s;}// 栈不为空,说明左括号⽐右括号多,数量不匹配 bool ret = STEmpty(&st);STDestroy(&st);return ret;
}int main()
{cout << isValid("[()][]") << endl;cout << isValid("[(])[]") << endl;return 0;
}
// ⽤最新加了构造和析构的C++版本Stack实现
bool isValid(const char* s) {Stack st;//无需调用Init,会自动调用构造函数,防忘while (*s){if (*s == '[' || *s == '(' || *s == '{'){st.Push(*s);//this隐式传了,无需传st地址}else{// 右括号⽐左括号多,数量匹配问题 if (st.Empty()){return false;//无需调用Destroy,自动调用析构函数,防忘}// 栈⾥⾯取左括号 char top = st.Top();st.Pop();// 顺序不匹配 if ((*s == ']' && top != '[')|| (*s == '}' && top != '{')|| (*s == ')' && top != '(')){return false;}}++s;}// 栈为空,返回真,说明数量都匹配 左括号多,右括号少匹配问题 return st.Empty();//可以直接return,也是因为会自动调用析构函数
}
可以看到,自动调用真的很好。
赋值运算符重载
运算符重载
- 当运算符被⽤于类类型的对象时,C++语⾔允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使⽤运算符时,必须转换成调⽤对应运算符重载(函数),若没有对应的运算符重载,则会编译报错。
自定义类型的比较应该由我们自己定义而不是系统定义。
- 运算符重载是具有特殊名字的函数,他的名字是由operator和后⾯要定义的运算符共同构成(构成了函数名)。和其他函数⼀样,它也具有其返回类型和参数列表以及函数体。
- 重载运算符函数的参数个数和该运算符作⽤的运算对象数量⼀样多。⼀元运算符有⼀个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第二个参数。
//以比较日期类为例
bool operator==(Date d1,Date d2)
{return d1._year==d2.year&&d1._month==d2._month&&d1._day==d2._day;
}
bool operator<(Date d1,Date d2)
{}int main()
{Date x1(2024,7,10);Date x2(2024,7,10);operator==(x1,x2);//显式调用x1 == x2;//会转换成上面调用函数return 0;
}
这时有一个问题,在函数operator==内部我们不能访问私有的类成员。
解决办法:
直接变为公有的方法显然不好。
我们可以提供Get函数:
//在类中
int GetYear()
{return _year;
}
还有一种友元方式,这个以后再说。
还有一种方法,我们重载本就是函数,可以直接放到类里面,变为成员函数。
- 如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数⽐运算对象少⼀个。
就变成这样写
//在类中
bool operator==(Date d2)
{return _year==d2.year&& _month==d2._month&& _day==d2._day;
}int main()
{x1.operator==(x2);x1==x2;//这样写会转换成上面return 0;
}
c++
//在类中
int GetYear()
{
return _year;
}
还有一种友元方式,这个以后再说。还有一种方法,我们重载本就是函数,可以直接放到类里面,变为成员函数。* 如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数⽐运算对象少⼀个。就变成这样写```c++
//在类中
bool operator==(Date d2)
{return _year==d2.year&& _month==d2._month&& _day==d2._day;
}int main()
{x1.operator==(x2);x1==x2;//这样写会转换成上面return 0;
}
本文到此结束,类和对象(中)还没有讲完,敬请期待后文=_=