⭐博客主页:️CS semi主页
⭐欢迎关注:点赞收藏+留言
⭐系列专栏:C++初阶
⭐代码仓库:C++初阶
家人们更新不易,你们的点赞和关注对我而言十分重要,友友们麻烦多多点赞+关注,你们的支持是我创作最大的动力,欢迎友友们私信提问,家人们不要忘记点赞收藏+关注哦!!!
类和对象在讲解完上篇以后,最关键的下篇来了,最重点为类的六个默认成员函数,这六个默认成员函数很重要,大家要细细品味。
我们先来明确一个概念,一个类中什么成员也没有,也就是没有成员变量和成员函数,我们称之为空类,那么空类中真的什么也没有吗?当然不是,编译器会自动生成6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
我们先看理解一个引例:当我们开辟了一个动态的空间,经常忘记将这个空间释放掉,这个会导致内存泄漏,是个大问题,当我们在一个函数的时候,经常会忘记初始化函数成员内的变量,这也是个大问题,那我们就有接下来的两种默认构造函数:构造函数和析构函数。
我们先使用成员函数来进行定义函数的传参。
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(2022, 7, 5);
d1.Print();
Date d2;
d2.Init(2022, 7, 6);
d2.Print();
return 0;
}
我们发现,如果我们定义很多个Date的实例化对象,那都需要去调用一遍Date的Init函数,是不是有点麻烦,那我们能否在对象创建的时候这个初始化函数就已经在实例化对象了呢?那么我们使用构造函数。
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
总结一下构造函数的特性为:
我们先来一波显式定义,其会自动调用我们定义的显式构造函数,如下代码演示:
//日期类
class Date
{
public:
Date(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(2023, 4, 28);
d1.Print();
Date d2(2023, 4, 29);
d2.Print();
return 0;
}
//自定义类
class Stack
{
public:
Stack()
{
cout << "Stack()" << endl;
_a = (int*)malloc(sizeof(int) * 4);
if (nullptr == _a)
{
perror("malloc fail\n");
return;
}
_capacity = 0;
_top = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack st1;
return 0;
}
没错,根据第4条特性,构造函数是可以重载的,但重载需要注意很多的问题,我们先进行重载试一试:
class Stack
{
public:
Stack()
{
cout << "Stack()" << endl;
_a = (int*)malloc(sizeof(int) * 4);
if (nullptr == _a)
{
perror("malloc fail\n");
return;
}
_capacity = 0;
_top = 0;
}
Stack(int capacity)
{
cout << "Stack(int capacity)" << endl;
_a = (int*)malloc(sizeof(int) * capacity);
if (nullptr == _a)
{
perror("malloc fail\n");
return;
}
_capacity = capacity;
_top = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack st1;
Stack st2(10);
return 0;
}
这里两个问题:
1、为什么main函数内部调用无参构造函数写的是Stack st1而不是加括号。解决:因为编译器无法很好地区分st1是对象还是函数名,这样定义是为了方便大家进行理解。
2、那么我们给缺省值呢?解释:
跟着第六条性质,缺省值和无参构造的构造函数只能存在一个!
结论:我们都用全缺省的,这样有个备胎更好一点。
我们在看完显性的构造函数,我们随之而来的是隐形的构造函数,就是我们的第5条特性,我们先给结论:
1、对于内置类型成员,不做处理。
2、对于自定义类型成员,会去调用它的默认构造。
3、有些编译器会进行处理内置类型成员的初始化。
4、C++11后打了补丁,声明可以给缺省值,给了缺省值会用缺省值去进行初始化。
class Stack
{
public:
/*Stack()
{
cout << "Stack()" << endl;
_a = (int*)malloc(sizeof(int) * 4);
if (nullptr == _a)
{
perror("malloc fail\n");
return;
}
_capacity = 0;
_top = 0;
}*/
private:
int* _a = nullptr;//缺省值
int _top = 0;
int _capacity = 0;
};
int main()
{
Stack st1;
return 0;
}
i、一般情况下,构造函数是需要自己写。
ii、不需要写构造函数的情况:
no1.内置类型成员都有缺省值,并且其值是我们需要的。
no2.全是自定义类型的构造函数,且这些构造函数都定义过默认构造函数。
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
class Stack
{
public:
~Stack()
{
cout << "~Stack()" << endl;
_a = (int*)malloc(sizeof(int) * 4);
if (nullptr == _a)
{
perror("malloc fail\n");
return;
}
_capacity = 0;
_top = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack st1;
return 0;
}
内置类型成员不做处理。
自定义类型会去调用它的析构函数。
1、一般情况下,有动态申请资源,就需要显示写析构函数释放资源,我们最好的例子就是栈,自定义的栈需要自己写析构函数释放资源。
2、没有动态申请资源,不需要写析构函数,我们的日期类就是最好的例子。
3、需要释放资源的成员都是自定义类型,不需要写析构函数。
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
我们先讲结论:在每次调用正规函数的时候是需要先调用拷贝构造的。
我们来一波例子:
class Date
{
public:
Date(int year = 2023, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
void func(Date d1)
{
}
void func(int i)
{
}
int main()
{
Date d1;
func(d1);
func(10);
return 0;
}
所以,C++规定的结论:
1、内置类型直接拷贝。
2、自定义类型必须调用拷贝构造完成拷贝。
因为指针是内置类型,直接给地址的,我们规定过了内置类型不用调用拷贝构造函数。
缺陷是代码实在是太丑了,而且野指针现象频发。
class Date
{
public:
Date(int year = 2023, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2002, 8, 28);
Date d2(d1);
return 0;
}
class Date
{
public:
Date(int year = 2023, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
/*Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}*/
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2002, 8, 28);
Date d2(d1);
return 0;
}
class Stack
{
public:
Stack()
{
cout << "Stack()" << endl;
_a = (int*)malloc(sizeof(int) * 4);
if (nullptr == _a)
{
perror("malloc fail\n");
return;
}
_capacity = 0;
_top = 0;
}
~Stack()
{
cout << "~Stack()" << endl;
_a = (int*)malloc(sizeof(int) * 4);
if (nullptr == _a)
{
perror("malloc fail\n");
return;
}
_capacity = 0;
_top = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack st1;
Stack st2(st1);
return 0;
}
所以必须需要重新开一块一模一样的空间,进行深拷贝,难度较大,暂时不讲。
问题1、析构两次会进行报错。
问题2、一个修改会影响另一个。
解决方法:利用深拷贝,深拷贝我们在这里简单画个图,不进行代码书写。
拷贝构造函数典型调用场景:
1、使用已存在对象创建新对象。
2、函数参数类型为类类型对象。
3、函数返回值类型为类类型对象。
1、对于内置类型,直接默认生成的拷贝构造就可以用。
2、自定义类型必须实现深拷贝,必须自己实现。
举个例子:
class Date
{
public:
Date(int year = 2023, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//private:
int _year;
int _month;
int _day;
};
bool Less(const Date& x1, const Date& x2)
{
if (x1._year < x2._year)
{
return true;
}
if (x1._year == x2._year && x1._month < x2._month)
{
return true;
}
if (x1._year == x2._year && x1._month == x2._month && x1._day < x2._day)
{
return true;
}
return false;
}
int main()
{
Date d1(2002, 8, 28);
Date d2(2023, 8, 22);
cout << Less(d1, d2) << endl;
return 0;
}
大家看这个函数的定义,是不是很简单,但是如果有程序员命名错误是不是很难理解,我们就引出了运算符重载的概念,我们先讲一讲概念:
补充一个概念:我们是否要重载运算符,是取决于这个运算符对这个类是否有意义。
class Date
{
public:
Date(int year = 2023, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//private:
int _year;
int _month;
int _day;
};
bool operator<(const Date& x1, const Date& x2)
{
if (x1._year < x2._year)
{
return true;
}
if (x1._year == x2._year && x1._month < x2._month)
{
return true;
}
if (x1._year == x2._year && x1._month == x2._month && x1._day < x2._day)
{
return true;
}
return false;
}
int main()
{
Date d1(2002, 8, 28);
Date d2(2023, 8, 22);
cout << (d1 < d2) << endl;
return 0;
}
看起来舒服多了,所以就有了我们的运算符重载的。
但这里有一个问题,这里的内置类型需要是公有的,那我想私有的内置类型怎么办呢?
注意,这里有个隐形的this指针代表着d1。
class Date
{
public:
Date(int year = 2023, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
// d1 < d2
//d1 == this d2 == x2
bool operator<(const Date& x2)
{
if (_year < x2._year)
{
return true;
}
if (_year == x2._year && _month < x2._month)
{
return true;
}
if (_year == x2._year && _month == x2._month && _day < x2._day)
{
return true;
}
return false;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2002, 8, 28);
Date d2(2023, 8, 22);
cout << (d1 < d2) << endl;
cout << d1.operator<(d2) << endl;
return 0;
}
1、赋值运算符重载格式
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回*this :要复合连续赋值的含义
2. 赋值运算符只能重载成类的成员函数不能重载成全局函数
3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
我们先来个简单的版本,可是这个简单的版本是否有缺陷呢?我们进行演示一下:
画图分析:
改void为返回值即可:
大家前面肯定能理解,但为什么返回这个*this指针呢?
解释一下原因:因为我们main函数中进行传参的是this指针和d1这两个实参,实际上this指针是隐含的,我们看下图:
d5 = d4 = d1;
也就是说,d是d1的形参,this指针是d4的形参,解引用this指针意味着用的是d4,返回的就是d4,给d5,这就解释通了。
缺陷:还是有不完美的地方,因为会调用其拷贝构造,浪费太多的空间。
用引用,无拷贝构造,效率高。
其实还有一个bug,如果自己给自己赋值呢?
默认生成赋值重载跟拷贝构造行为一样:
1、内置类型成员 – 值拷贝/浅拷贝
2、自定义类型成员会去调用它的赋值重载。
Date.h:
#include
using namespace std;
class Date
{
public:
Date(int year = 2023, int month = 1, int day = 1);
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
bool operator<(const Date& d);
bool operator==(const Date& x);
bool operator<=(const Date& x);
bool operator>(const Date & x);
bool operator>=(const Date & x);
bool operator!=(const Date & x);
private:
int _year;
int _month;
int _day;
};
Date.cpp:
#include"Date.h"
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
bool Date::operator<(const Date& x)
{
if (_year < x._year)
{
return true;
}
else if (_year == x._year && _month < x._month)
{
return true;
}
else if (_year == x._year && _month == x._month && _day < x._day)
{
return true;
}
return false;
}
bool Date::operator==(const Date& x)
{
return _year == x._year
&& _month == x._month
&& _day == x._day;
}
bool Date::operator<=(const Date& x)
{
return *this < x || *this == x;
}
bool Date::operator>(const Date& x)
{
return !(*this <= x);
}
bool Date::operator>=(const Date& x)
{
return !(*this < x);
}
bool Date::operator!=(const Date& x)
{
return !(*this == x);
}
换而言之,也就是利用一个小于,一个等于的函数的定义即可养活大于等于小于组合的函数,因为可以用this去定义。
我们在第5节讲述的是两个自定义类型的比较,我们接下来玩一下自定义类型+常数。
我们想算一下100天后是哪天,我们来进行书写一下:
Date Date::operator+(int day)
{
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
++_month;
if (_month == 13)
{
++_year;
_month = 1;
}
}
return *this;
}
看似这个代码很完美了,可是有一个比较大的问题,当我们使用+的函数的时候,发现d1会发生改变,我们原本不想改变d1,只想输出的是加100天后的值,最后反倒d1发生了改变,我们的解决方法是使用+=。
Date& Date::operator+=(int day)
{
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
++_month;
if (_month == 13)
{
++_year;
_month = 1;
}
}
return *this;
}
Date& Date::operator-=(int day)
{
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
_month = 12;
--_year;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date Date::operator+(int day)
{
//拷贝构造
Date tmp(*this);
tmp._day += day;
while (tmp._day > GetMonthDay(tmp._year, tmp._month))
{
tmp._day -= GetMonthDay(tmp._year, tmp._month);
++tmp._month;
if (tmp._month == 13)
{
++tmp._year;
tmp._month = 1;
}
}
return tmp;
}
int main()
{
Date d1(2023, 5, 5);
d1 += 100;
d1.Print();
Date d2(2023, 5, 5);
Date d3(d2 + 100);
d2.Print();
d3.Print();
return 0;
}
Date Date::operator-(int day)
{
Date tmp = *this;
tmp -= day;
return tmp;
}
大家看,我们+= 负的100,相当于-=100,则我们调用-=的函数,这时day要取负号!
Date& Date::operator+=(int day)
{
if (day < 0)
{
return *this -= -day;
}
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
++_month;
if (_month == 13)
{
++_year;
_month = 1;
}
}
return *this;
}
同理+=也是如此。
Date& Date::operator-=(int day)
{
if (day < 0)
{
return *this += -day;
}
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
_month = 12;
--_year;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
//前置++
Date& Date::operator++()
{
*this += 1;
return *this;
}
//后置++
// 这里加一个int参数不是为了接收具体的值,仅仅是占位,跟前置++构成重载
Date Date::operator++(int)
{
Date tmp = *this;
*this += 1;
return tmp;
}
int main()
{
Date d1(2023, 5, 5);
//都要++,前置++返回++以后的对象的值,后置++返回++之前的值
++d1;
d1.Print();
d1++;
d1.Print();
return 0;
}
//前置--
Date& Date::operator--()
{
*this -= 1;
return *this;
}
//后置--
// 这里加一个int参数不是为了接收具体的值,仅仅是占位,跟前置--构成重载
Date Date::operator--(int)
{
Date tmp = *this;
*this -= 1;
return tmp;
}
思路:算两个日期之间间隔的天数只需要将小的那个日期++直到到了大的那个日期,计算中间的天数即可。
int Date::operator-(const Date& day)
{
Date max = *this;
Date min = day;
int flag = 1;
if (*this < day)
{
max = day;
min = *this;
flag = -1;
}
int sum = 0;
while (min != max)
{
++min;
++sum;
}
return sum * flag;
}
int main()
{
Date d1(2023, 5, 5);
Date d2(2002, 8, 28);
cout << d1 - d2 << endl;
cout << d2 - d1 << endl;
return 0;
}
什么是流插入和流提取?我们在最前面简单讲述过了,一个符号是>>,另一个符号是<<。接下来讲解一下这两个符号的底层逻辑和用处。
发现是实现重载实现流插入。
void Date::operator<<(ostream& out)
{
out << _year << "年" << _month << "月" << _day << "日" << endl;
}
int main()
{
Date d1(2023, 1, 1);
d1 += 100;
//流插入
//cout << d1; // 不等价于d1.operator<<(cout);
d1 << cout; // 等价于d1.operator<<(cout);
return 0;
}
这实在是太奇怪了,我们需要怎么样才能让d1流入到终端控制台呢,这样子符合我们的日常使用习惯?我们使用一个在后面才会讲到的友元函数配合全局函数,我们现在只需要知道是声明,将私有拿出来。
void operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}
int main()
{
Date d(2023, 1, 1);
d += 100;
cout << d;
return 0;
}
我们如果想连续使用<<操作符呢?看下图报错了,因为返回值都是void呀,怎么可能连续流插入?
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
int main()
{
Date d(2023, 1, 1);
Date d1(2023, 2, 2);
Date d2(2023, 3, 2);
d += 100;
cout << d << d1 << d2;
return 0;
}
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
int main()
{
Date d(2023, 1, 1);
Date d1(2023, 2, 2);
Date d2(2023, 3, 2);
d += 100;
cout << d << d1 << d2;
cin >> d >> d1 >> d2;
return 0;
}
可以是可以,但是,有没有可能一种情况,我们输入2023,13,32,是不是很荒谬,根本没有这种日子呀!
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
简单而言,我们在进行看似无参传参的时候,实际上传了一个this指针,但这个this指针不能显性地进行写在形参的部分,但为了表示在该函数内部的成员不能进行修改,所以需要加一个const,所以加到后面了。
只要是成员函数内部不修改成员变量,都应该加const。这样const对象和普通对象都可以调用。
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
这两种运算符一般不需要重载,用计算机内部系统默认的即可。
什么是是C++的类和对象,很好,在进行完上述的讲解以后,相信大家对于类和对象有了更深层次的了解,这章是承前启后的重要章节,大家一定要好好总结进行考量。
家人们不要忘记点赞收藏+关注哦!!!