一、前言
好久没有更新内容了,今天为大家带来类和对形中期的内容 !
二、正文
1.this指针
1.1this指针的引入
class Date
{
public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year; //年int _month;//月int _day; //日};int main()
{Date d1, d2;d1.Init(2023,5,12);d2.Init(2023,5,13);d1.Print();d2.Print();}
在看完上面关于“日期”这个类的定义后,不知道小伙伴是否有着这样一个疑惑?
Date类中有Init与Print两个成员函数,函数体中没有关于不同对象的区分,那么当d1调用Init函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?
在C语言中当我们在实现这样的功能的时候是我们自己手动要传对象的指针给函数的,而C++是引入this指针来解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参 数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
1.2this指针的特性
在引入了this指针后,我们就来了解下this指针到底有哪些特性呢?
①this指针的类型:类类型* const,即成员函数中,不能给this指针赋值。
②只能在“成员函数”的内部使用
③this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。 所以对象中不存储this指针
④ this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
//看到的
void Print()
{cout << _year << "-" << _month << "-" << _day << endl;
}//实际上[编译器自动处理]
void Print(Date*this)
{cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}
2.类的六个默认成员函数
如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。 默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
3.构造函数
3.1概念
class Date
{
public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year; //年int _month;//月int _day; //日};int main()
{Date d1;d1.Init(2023,5,12);d1.Print();Date d2;d2.Init(2023, 5, 13);d2.Print();
}
对于Date类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,况且有时候还可能会忘记初始化,那能否在对象创建时,就将信息设置进去呢?因此就有了构造函数的存在。
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。
3.2特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
①函数名与类名相同
②无返回值
③对象实例化时编译器自动调用相应的构造函数
④构造函数可以重载
⑤如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
⑥对于自定类型成员会调用它的默认成员函数,而对于内置类型,在不同的编译器下会有不同的处理方式
⑦无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
注:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时 可以给默认值。
class Time
{
public:Time(){cout << "Time()" << endl;_hour = 0;_minute = 0;_second = 0;}private:int _hour;int _minute;int _second;
};class Date
{public://1.无参构造函数Date(){}//2.带参构造函数Date(int year, int month, int day){int _year; int _month;int _day; }
private://基本类型(内置类型)——可以给默认值int _year=2023;int _month=5;int _day=13;//自定义类型——编译器生成的默认构造函数会自动调用 _t的构造函数Time _t;
};void TestDate()
{Date d1; //调用无参构造函数Date d2(2015, 1, 1); //调用带参数的构造函数//注:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明//例:Date d3();
}
3.3应用场景
那么什么时候需要我们自己书写构造函数呢?
一般情况下,构造函数都需要我们自己写,而以下两种情况下可以考虑不用写:
①内置类型成员都有缺省值,且初始化符合我们的要求
②全是自定义类型的构造,且这些类型都定义了默认构造函数
4、析构函数
4.1概念
通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成 的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
4.2特性
析构函数是特殊的成员函数,其特征如下:
①析构函数名是在类名前加上字符 ~。
②无参数无返回值类型。
③ 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
④ 对象生命周期结束时,C++编译系统系统自动调用析构函数。
⑤编译器生成的默认析构函数,对内置类型不做处理,对自定类型成员调用它的析构函数。
注意:析构函数不能重载
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 3){_array = (DataType*)malloc(sizeof(DataType) * capacity);if (NULL == _array){perror("malloc申请空间失败!!!");return;}_capacity = capacity;_size = 0;}void Push(DataType data){// CheckCapacity();_array[_size] = data;_size++;}// 其他函数...//析构函数~Stack(){if (_array){free(_array);_array = NULL;_capacity = 0;_size = 0;}}
private:DataType* _array;int _capacity;int _size;
};
void TestStack()
{Stack s;s.Push(1);s.Push(2);
}
4.3应用场景
在了解完析构函数之后,可能有的小伙伴会问那我们什么时候该自己写析构函数,什么时候写析构函数呢?
在一般情况下下,有动态申请资源就需要写析构函数释放资源,比如malloc了一块内存空间等,而没有动态申请的资源或者需要释放资源的成员都是自定义类型,就不需要写析构函数了
5.拷贝构造函数
5.1概念
在日常的生活中,我们常常可能会见到两个一模一样的物品,比如:水杯,足球……那么我们在创建对象的时候,能不能创建一个与已存在对象一样的新对象呢?答案是肯定的。
于是便有了“拷贝构造函数”,对于该函数而言,它只有单个形参,该形参是对本类类型 对象的引用(一般用const修饰)在用已存在的类类型对象创建新对象时用编译器自动调用。
5.2特征
拷贝构造函数也是特殊的成员函数,其特征如下:
1.拷贝构造函数是构造函数的一个重载形式
2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷
递归调用
//拷贝构造函数Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}//错误写法Date( Date d){_year = d._year;_month = d._month;_day = d._day;}
我们会发现正确的拷贝构造相比于错误写法有两个不同的地方:1.引用 2.const
前者是为了避免调用拷贝构造函数出现无穷递归,当然编译器也会进行检查,后者是为了避免我们书写时容易出现将要被拷贝的对象与拷贝对象写反,导致二者的成员都为随机值的现象
3.若未显式定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝
换句话来说:内置类型成员完成值拷贝/浅拷贝;自定义类型成员会调用它的拷贝构造函数
#include <iostream>
using namespace std;class Date
{
public:Date(int year=0,int month=0,int day=0){_year = year;_month = month;_day = day;}拷贝构造函数//Date(const Date& d)//{// _year = d._year;// _month = d._month;// _day = d._day;//}void print(){cout << _year << '-' << _month <<'-' << _day << endl;}
private:int _year;int _month;int _day;
};int main()
{Date d1(2024, 1, 23);Date d2(d1);d2.print();
}
当我们将我们前文自己写过的拷贝构造函数注释掉后,我们会发现结果是一样的,这就是编译器自己生成的默认构造函数的效果
4.编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝,那么还需要我们自己显式实现吗?虽然像上面的日期类对象无需我们显式实现,但是并不是所有的类都适用的,比如说下面这种情况
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 10){_array = (DataType*)malloc(capacity * sizeof(DataType));_size = 0;_capacity = capacity;}void Push(const DataType& data){//checkCaapacity(); //检查容量_array[_size++] = data;}~Stack(){if (_array){free(_array);_capacity = _size = 0;}}
private:DataType* _array;size_t _size;size_t _capacity;
};void Text2()
{Stack st1=Stack(10);st1.Push(1);st1.Push(2);Stack st2(st1);
}int main()
{Text2();
}
对于上述对象,当我不显式实现,而只依赖于编译器自己生成的默认构造函数后,我们会发现拷贝后的对象st2的大小,容量和内容与st1都是一样的,但是同时我们发现两者的成员_array的地址是一样,那么当函数结束的时候,st1与st2都会调用它们各自的析构函数,这时候_array所指向空间就被释放了两次,于是就出现了问题。
由此这也是为什么编译器能够自己生成默认的拷贝构造函数后,有时候我们还需要显示实现拷贝函数。因为前者只能实现值拷贝/浅拷贝,而一旦涉及内存等,简简单单的拷贝就不能够满足了。这时候我们就要实现深拷贝了,后面也会详细讲解
6.赋值运算符重载
6.1 运算符重载
C++为了增强代码的可读性引入了运算符重载,,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字:关键字“operator”+重载的运算符符号 eg:operator ==
函数原型:返回值类型 operator操作符(参数列表)eg:Data operator+(int day)
注意:
●不能通过连接其他符号来创建新的操作符:比如说operator@
●重载操作符必须有一个类类型参数
●用于内置类型的运算符。其含义不能改变,例如:内置的整型+,不能改变其含义
●作为类成员函数重载时,其形参看起来比操作数目少1,因为成员函数的第一个参数为隐藏的this指针
●“.*”, “: :”, “sizeof”, “?:” , “.”,注意以上5个运算符不能重载
6.2赋值运算符重载
1.赋值运算符重载格式
●参数类型:const T& ,传递引用可以提高传参效率
●返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持哦连续赋值
●检测是否自己给自己赋值
●返回*this:要复合连续赋值的含义
//赋值运算符重载Data& operator=(const Data& d){_year = d._year;_month = d._month;_day = d._day;return *this;}
2. 赋值运算符只能重载成类的成员函数而不能重载成全局函数
这是因为赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数,这也是默认成员函数的一个特点。
3. 由第2点我们知道,当用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝、
注:内置类型成员是直接赋值的,而自定义类型成员变量需要调用对应的赋值运算符重载完成赋值
class Time
{
public:Time(){_hour = 1;_minute = 1;_second = 1;}Time& operator=(const Time& t){if (this != &t){_hour = t._hour;_minute = t._minute;_second = t._second;}return *this;}
private:int _hour;int _minute;int _second;
};class Date
{
private:// 基本类型(内置类型)int _year = 2024;int _month = 1;int _day = 25;// 自定义类型Time _t;
};int main()
{Date d1;Date d2;d1 = d2;return 0;
}
与上面讲过的默认构造函数类似,虽然编译器生成的默认赋值构造函数虽然可以完成值拷贝/浅拷贝,但是对于Stack这样涉及资源管理类仅靠简单拷贝是不行的,也是需要我们去实现深拷贝
6.3前置++与后置++重载
对于前置++与后置++两个函数我们知道前者是先++再使用,而后者是先试用再++,那么对于编译器而言它该如何区别呢,于是我们便在传参中进行变化,如果传参数便是后置++,无则是前置,当然这是编译器的区分。在实际的使用中我们无需传参数,只要像内置类型一样便可以。
Data& operator++(){*this += 1;return *this;}Data operator++(int){Data tmp = (*this);++(*this);return tmp;}int main(){Data d1;d1++;++d1}
7.日期类的实现
在掌握了前面的有关类的知识后,我们就可以简单的写一个类了。
下面以日期类的实现为例
typedef int DataType;class Data
{
public://构造函数Data(DataType year=0,DataType month=0,DataType day=0){_year = year;_month = month;_day = day;}//拷贝构造函数Data(const Data& d){_year = d._year;_month = d._month;_day = d._day;}//赋值运算符重载Data& operator=(const Data& d){_year = d._year;_month = d._month;_day = d._day;return *this;}//>运算符重载bool operator>(const Data& d){if (_year > d._year)return true;else if (_year == d._year && _month > d._month)return true;else if (_month == d._month && _day > d._day)return true;else return false;}//==运算符重载bool operator ==(const Data& d){if (_year == d._year && _month == d._month && _day == d._day)return true;elsereturn false;}///>=运算符重载bool operator >=(const Data& d){return(((*this) > d) || ((*this) == d));}//<运算符重载bool operator <(const Data& d){return !(*this>=d);}//<=运算符重载bool operator <=(const Data& d){return !(*this >d);}//获取指定年月的天数static DataType GetMonthDay(DataType year,DataType month){DataType days[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };//判断闰年if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))){return days[2] + 1;}elsereturn days[month];}//+=运算符重载 日期+=天数Data& operator+=(DataType day){if (day > 0){_day += day;while (_day > GetMonthDay(_year, _month)){_day -= GetMonthDay(_year, _month);_month++;if (_month > 12){_month = 1;_year += 1;}}return *this;}else*this -= (-day);}//+运算符重载 日期+天数Data operator+(DataType day){Data tem(*this);tem += day;return tem;}//前置++运算符重载 Data& operator++(){*this += 1;return *this;}//后置++运算符重载 Data operator++(int){Data tmp = (*this);++(*this);return tmp;}//日期的打印void print(){cout << _year << '-' << _month <<'-' << _day << endl;}//-=运算符重载 日期-=天数Data& operator-=(DataType day){if (day > 0){_day -= day;while (_day <= 0){_month -= 1;if (_month == 0){_month = 12;_year -= 1;}_day += GetMonthDay(_year, _month);}return *this;}else*this += (-day);}//-运算符重载 日期-天数Data operator-(DataType day){Data tem(*this);tem -= day;return tem;}//前置--运算符重载Data& operator--(){*this -= 1;return *this;}//-后置-运算符重载Data operator--(int){Data tmp = (*this);--(*this);return tmp;}//日期-日期DataType operator-(const Data& d){Data tmp = *this;int dis = 0; //天数差值if (tmp >d){while (tmp > d){--(tmp);dis++;}}else if(tmp < d){while (tmp< d){++(tmp);--dis;}}return dis;}private:DataType _year;DataType _month;DataType _day;};
8.const成员
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改,也就是只读不写
void print() const{cout << _year << '-' << _month <<'-' << _day << endl;}
9.取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义,编译器会默认生成。
而且一般需要重载,使用编译器默认取地址的重载即可,只有特殊情况,才需要 重载,比如想让别人获取到指定的内容
class Date
{public :Date* operator&(){return this ;}const Date* operator&()const{return this ;}
private :int _year ; // 年int _month ; // 月int _day ; // 日
};