目录
前言:
一:类的6个默认成员函数
如果一个类中什么成员都没有,我们简称为空类。
但是空类并不是真的什么都没有,即使我们什么都不写,编译器也会默认生成六个成员函数。
class Date{};
这一部分大家只需要有个基本了解,后面我会一一展开讲解的。(这些成员函数都比较特殊,不要以看待普通函数的眼光去看待它们)
二:构造函数(第一个成员)
(1)概念
我们看下面这个Date类:
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;
}
在用C语言写代码的时候,我们往往会写初始化函数,每次创建结构体变量都需要手动去调用初始化函数,那能不能每次创建变量后自动去调用初始化函数呢?
答案是有的,构造函数就能解决这个问题。
构造函数是一个特殊的成员函数,没有返回值(就是真正意义上的没有,连空都不是),函数名和类名相同,创建对象时编译器会自动调用这个函数,并且对单个对象来说只会调用一次。
(2)特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任
务并不是开空间创建对象,而是初始化对象。(构造函数负责初始化!初始化!!初始化!!!)
其特性如下:
- 函数名和类名相同
- 无返回值(就是真正的没有,连空都不是)
- 对象实例化时由编译器自行去调用构造函数
- 构造函数可以重载
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦
用户显式定义编译器将不再生成。 - 类的每个成员变量都有对应的构造函数,一个类默认生成的构造函数其实是去调用成员变量对应类型的构造函数。(这个不好理解,后面我会展开讲)
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为
是默认构造函数。
(3)特性的解析
三:初始化列表
(1)引入
我们看下面这个代码:
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
//我自己写了一个构造函数,不会自动生成构造函数了
//_t还会被初始化吗?
Date(int year = 1,int month = 1,int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
我们知道默认生成的构造函数会去调用成员变量的构造函数,那现在的构造函数是显示定义的,还会去调用成员变量的构造函数吗?
答案是会,这一切都是初始化列表干的。
(2)概念
简单的说,初始化列表干的活就是在进入构造函数的函数体前调用成员变量的构造函数,并且这个过程是可以人为去控制的。
看代码,这个不难理解:
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1,int month = 1,int day = 1)
//初始化列表在这个位置发挥作用
//大家也可以把这个地方理解为对象的成员开辟了空间
//展示一下如何人为控制
:_year(year)
,_month(10) //括号后面也可以自己给值
,_day(day) //前面所讲的声明时给默认值其实就是这里给值
{}
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
学习了初始化列表后,建议统一使用初始化列表进行初始化,因为它可以在对象构造时对其成员变量进行初始化。这样做可以提高程序的执行效率,避免一些初始化问题,也更加规范和清晰。
但对于一些复杂的情况还是要在函数体中进行的。(比如数组的初始化和malloc申请空间的检查)
(3)注意
成员变量初始化的顺序是由声明顺序决定的,和初始化列表顺序无关。
class Date
{
public:
Date(int year = 1,int month = 1,int day = 1)
:_month(12)
,_year(_month)
,_day(day)
{}
void print()
{
cout << "年:>" << _year << endl;
cout << "月:>" << _month << endl;
cout << "日:>" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
d.print();
return 0;
}
成因:
- 声明顺序决定了初始化顺序,_year最先初始化
- 但初始化列表却用_month来初始化_year,这个时候_month还没初始化,是未知数,就导致了上面的结果
四:析构函数(第二个成员)
(1)概念
图解:
(2)特性
析构函数是特殊的成员函数,其特征如下:
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。(注意:因为析构函数规定不能有参数,故无法存在重载)
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
- 如果类中没有显式定义析构函数,则C++编译器会自动生成一个默认的析构函数,一旦
用户显式定义编译器将不再生成。 - 类的每个成员变量都有对应的析构函数,一个类默认生成的析构函数其实是去调用成员变量对应类型的析构函数。(内置类型对应的析构函数不会进行处理,这一部分的特性和前面的构造函数一致)
- 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数。
(3)例子
class SeqList
{
public:
//析构函数
~SeqList()
{
//清理向堆申请的空间
free(_a);
_a = nullptr;
}
//构造函数
SeqList(int capacity)
:_capacity(capacity)
,_a(nullptr)
,_size(0)
{
_a = (int*)malloc(sizeof(int) * _capacity);
//检查是否申请成功
if (_a == nullptr)
{
cout << "malloc error" << endl;
assert(false);
}
}
//一系列成员函数
private:
int* _a;
int _capacity;
int _size;
};
int main()
{
SeqList s(10);
return 0;
}
五:拷贝构造函数(第三个成员)
(1)概念
例子:
class A
{
public:
A(int a = 0)
:_a(a)
{}
//和构造函数构成函数重载
A(const A& a)
{
_a = a._a;
}
private:
int _a;
};
int main()
{
A a1(10);
//调用拷贝构造
A a2(a1);
}
(2)特性
拷贝构造函数也是特殊的成员函数,其特征如下:
- 拷贝构造函数是构造函数的一个重载形式(写了拷贝构造就不会生成默认构造)。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,
因为会引发无穷递归调用。(一会会解释为什么造成无穷递归) - 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按
字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝(就是一个个字节的把成员变量拷贝过去)。 - 类的每个成员变量都有对应的拷贝构造函数,一个类默认生成的拷贝构造函数其实是去调用成员变量对应类型的拷贝构造函数。(内置类型对应的拷贝构造函数会进行处理,这一部分的特性和前面的构造函数有一些不同)
-
类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请
时,则拷贝构造函数是一定要写的,否则就是浅拷贝。 (一会举个例子) -
拷贝构造的调用用两种写法,第一种是A a1(a2),第二种是A a3 = a2,这两种写法是完全等价的,一定要牢记这一点。
(3)特性的解析
六:运算符重载
剩下的三个成员函数都涉及到运算符重载,而且运算符重载的意义很大,所以这里单独讲一下。
(1)概念
(2)实例
代码:
//以 == 重载为例
class A
{
public:
A(int a)
:_a(a)
{}
bool operator==(const A& x)
{
return (x._a == _a);
}
private:
int _a;
};
int main()
{
A a1(5);
A a2(10);
A a3(10);
cout << (a1 == a2) << endl;
cout << (a1 == a2) << endl;
//(a1 == a2)实际上就是a1.operator==(&a1,a2)
//这里加()是因为cout本质也是函数重载,不加括号就会先和a1结合,cout和a1结合去调用函数
//这个调用的返回值是另一个类的对象,这个对象再和>a2结合,我们没有实现这个函数重载,会报错
//后面专门讲一下输入输出实现
}
(3)注意
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
-
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
藏的this -
*(解引用)、::(作用域限定符)、 sizeof 、?:(三目操作符)、 .(类成员访问操作符)、 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
-
运算符重载的结合顺序与运算符优先级、结合性相关。
-
运算符重载大多数情况下作为类的成员函数,实在没办法的情况可以做成正常函数,本质都是替换成函数调用。(比如要实现类的输入输出,后面单独讲)
七:赋值运算符重载(第四个成员)
(1)赋值运算符重载格式
(2)特性
- 用户没有显式实现时,编译器会生成一个默认赋值运算符重载(这一点很特殊),以值的方式逐字节拷贝(这种属于前面所谓的浅拷贝,对需要清理资源的情况不适用),显示定义则不会生成。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
- 基于第一点,引出另一个特性,赋值运算符只能重载成类的成员函数不能重载成全局函数。
⭐特性1
对于什么时候应该自己实现赋值重载,以我们前面所说的栈为例子。
总结:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必
须要实现。
⭐特性2
代码:
//这一段代码无法通过编译
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
if (&left != &right)
{
left._year = right._year;
left._month = right._month;
left._day = right._day;
}
return left;
}
原因:赋值运算符如果不显式定义,编译器会生成一个默认的。此时用户再在类外自己实现
一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值
运算符重载只能是类的成员函数。
八:const修饰的成员
我们先看下面这段代码:
class A
{
public:
A(int a = 0)
:_a(a)
{}
void print()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
const A a1;
a1.print();
return 0;
}
编译结果:
我们知道C++中用const修饰的变量已经属于常量了,但即使是常量,我们也希望它可以打印、比较、作为拷贝母本,但上面的代码却连编译都通过不了。
原因:这涉及到权限放大和缩小的问题,因为被隐藏的this指针传递时默认的类型是->类名* const this,*前面没有用const修饰,表示这个指针指向的对象是可修改的,但是上面的对象用了const修饰,传过来的指针类型是->const 类名* const this,这属于权限的放大,是不被允许的。
解决方法:想解决这个问题,需要把被隐藏的this的类型前面加上const,但this被隐藏了,我们应该怎么告诉编译器我们的需求呢?
小结:对于一些不需要修改对象的成员函数,我们尽量在函数后加上const,可以让代码的应用更加广泛。(前面的大部分例子其实都应该加const修饰)
九:取地址及const取地址操作符重载(第五、六个成员)
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
十:友元
在默认的情况下,一个对象的成员变量只能在类的内部访问,但如果我们想在其它地方访问类的成员变量,该怎么办呢?这个就需要友元了。
注意:友元可以更加灵活,但是破坏了封装,应该尽量少用。
(1)友元函数
class A
{
public:
//关键字friend,在类中进行函数声明即可。
friend void print(A& a);
A(int a = 0)
:_a(a)
{}
private:
int _a;
};
//想让非成员函数访问类成员变量
void print(A& a)
{
a._a++;
cout << a._a << endl;
}
int main()
{
A a1(20);
print(a1);
}
(2)友元类
class A
{
public:
//利用关键字friend,A是B的友元,B可以访问A,A不可以访问B
//A把B当朋友,B不一定把A当朋友
friend class B;
A(int a = 0)
:_a(a)
{}
private:
int _a;
};
class B
{
public:
B(int b = 0)
:_b(b)
{}
void print()
{
//在B类中访问A类的对象
cout << "A:>" << a._a << "B:>"<< _b << endl;
}
private:
int _b;
A a;
};
十一:实现一个比较完整的日期类
(1)先讲几个比较重要的点
(2)日期类实现(分文件)
大家可以先对照声明尝试自己写一下,大部分的逻辑都比较简单。
⭐Date.h(函数声明)
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <assert.h>
using namespace std;
class Date
{
public:
//两个友元函数
friend ostream& operator<<(ostream& out,const Date& d);
friend istream& operator>>(istream& in,Date& d);
// 获取某年某月的天数
int GetMonthDay(int year, int month)const;
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1);
// 拷贝构造函数
// d2(d1)
Date(const Date& d)
:_year(d._year)
,_month(d._month)
,_day(d._day)
{
}
// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
//不传引用返回会多构造一个
Date& operator=(const Date& d);
// 析构函数(日期类可以不写,这里是为了方便观察)
~Date()
{
//cout << "析构" << endl;
}
// 日期+=天数
Date& operator+=(int day);
// 日期+天数
Date operator+(int day) const;
// 日期-天数
Date operator-(int day) const;
// 日期-=天数
Date& operator-=(int day);
// 前置++
Date& operator++();
// 后置++,这个int参数只是为了区分,传什么值又编译器自行处理
Date operator++(int);
// 后置--
Date operator--(int);
// 前置--
Date& operator--();
// >运算符重载
bool operator>(const Date& d)const;
// ==运算符重载
bool operator==(const Date& d) const;
// >=运算符重载
bool operator >= (const Date& d) const;
// <运算符重载
bool operator < (const Date& d) const;
// <=运算符重载
bool operator <= (const Date& d) const;
// !=运算符重载
bool operator != (const Date& d) const;
// 日期-日期 返回天数
int operator-(const Date& d) const;
//进行日期检查,合法返回1,否则返回0
int CheckDate() const;
//打印
void print()
{
cout << _year << "|" << _month << "|" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
//某一天是星期几
int GetWeek(Date& d);
⭐Date.cpp(函数实现)
#define _CRT_SECURE_NO_WARNINGS 1
#include "Date.h"
//打印重载
ostream& operator<<(ostream& out,const Date& d)
{
cout << d._year << "." << d._month << "." << d._day << endl;
return out;
}
//输入重载
istream& operator>>(istream& in,Date& d)
{
cin >> d._year >> d._month >> d._day;
//输入完检查一下是否合法
assert(d.CheckDate());
return in;
}
// 全缺省的构造函数
Date::Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
//检查一下是否合法
assert(CheckDate());
}
// 获取某年某月的天数
int Date::GetMonthDay(int year, int month) const
{
//会多次调用这个函数,所以直接设计成静态的数组
static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int day = days[month];
if (month == 2
&& ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
day += 1;
}
return day;
}
// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
//不传引用返回会多构造一个
Date& Date::operator=(const Date& d)
{
if (this != &d)
{
_day = d._day;
_month = d._month;
_year = d._year;
}
return *this;
}
// 日期+=天数
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) const
{
Date tmp(*this);
tmp += day;
return tmp;
}
// 日期-=天数
Date& Date::operator-=(int day)
{
if (day < 0)
{
return *this += -day;
}
_day -= day;
//减到0要变成下个月的最后一天
while (_day <= 0)
{
_month--;
//如果减到0,要变成下一年的12月
if (_month <= 0)
{
_month = 12;
_year--;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
// 日期-天数
Date Date::operator-(int day) const
{
Date tmp(*this);
tmp -= day;
return tmp;
}
// 前置++
Date& Date::operator++()
{
*this += 1;
return *this;
}
// 后置++,这个int参数只是为了构成函数重载,传什么值编译器自行处理
Date Date::operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
// 后置--
Date Date::operator--(int)
{
Date tmp(*this);
*this -= 1;
return tmp;
}
// 前置--
Date& Date::operator--()
{
*this -= 1;
return *this;
}
// >运算符重载
bool Date::operator>(const Date& d) const
{
if (_year > d._year)
{
return true;
}
else if ((_year == d._year) && (_month > d._month))
{
return true;
}
else if ((_year == d._year) && (_month == d._month) && (_day > d._day))
{
return true;
}
//前面三种情况都是大于,最后一定是小于
else
{
return false;
}
}
// ==运算符重载
bool Date::operator==(const Date& d) const
{
return (_year == d._year)
&& (_month == d._month)
&& (_day == d._day);
}
// >=运算符重载
bool Date::operator >= (const Date& d) const
{
return (*this > d) || (*this == d);
}
// <运算符重载
bool Date::operator < (const Date& d) const
{
return !((*this > d) || (*this == d));
}
// <=运算符重载
bool Date::operator <= (const Date& d) const
{
return !(*this > d);
}
// !=运算符重载
bool Date::operator != (const Date& d) const
{
return !(*this == d);
}
// 日期-日期 返回天数
int Date::operator-(const Date& d) const
{
Date max = *this;
Date min = d;
int n = 0;
//一开始默认前面大于后面
int flag = 1;
if (*this < d)
{
max = d;
min = *this;
//如果后面大于前面,最后结果为负数
flag = -1;
}
while (max > min)
{
min++;
n++;
}
return n * flag;
}
int GetWeek(Date& d)
{
Date tmp(1, 1, 1);
int week = 0; //0 - 周一
int n = d - tmp;
week += n;
return week % 7;
}
//合法返回1,否则返回0
int Date::CheckDate() const
{
return _month > 0 && _month < 13 && (_day > 0 && _day <= GetMonthDay(_year, _month));
}
⭐test.cpp(测试)
#include "Date.h"
void text1()
{
// =
/*Date d1;
Date d2(3000, 11, 11);
Date d3;
d3 = d1 = d2;
d1.print();
d2.print();
d3.print();*/
// +=
/*Date d1(2022,12,1);
d1 += 50;
d1.print();
d1 += 500;
d1.print();
d1 += 5000;
d1.print();*/
// +
//Date d1(2022, 11, 1);
//Date d2 = d1 + 50;
//d1.print();
//d2.print();
// -
/*Date d1(2022, 12, 20);
Date d2 = d1 - 20;
d1.print();
d2.print();*/
// -=
//Date d1(2022, 12, 20);
//d1 -= d1 -= 20;
// 前置++
/*Date d1(2022, 11, 30);
Date d2 = ++d1;*/
// 后置++
/*Date d1(2022, 11, 1);
Date d2 = d1++;*/
//前置--
/*Date d1(2022, 11, 1);
Date d2 = --d1;*/
// 后置--
/*Date d1(2022, 11, 1);
Date d2 = d1--;*/
}
void text2()
{
Date d1(2022, 12, 11);
Date d2(2023, 12, 12);
//cin >> d1 >> d2;
cout << d1 << d2 << endl;
// >
/*Date d1(2022, 12, 20);
Date d2(2023, 12, 20);
cout << (d2 > d1) << endl;*/
// ==
/*Date d1(2023, 11, 20);
Date d2(2023, 12, 20);
cout << (d2 == d1) << endl; */
// >=
/*Date d1(2022, 12, 20);
Date d2(2023, 12, 20);
cout << (d2 >= d1) << endl;*/
// <
/*Date d1(2022, 12, 20);
Date d2(2022, 12, 20);
cout << (d2 < d1) << endl; */
// <=
/*Date d1(2022, 12, 20);
Date d2(2023, 12, 20);
cout << (d2 <= d1) << endl; */
// !=
/*Date d1(2022, 12, 20);
Date d2(2022, 12, 21);
cout << (d2 != d1) << endl; */
// -(日期-日期 == 天数)
/*Date d1(2022, 12, 20);
Date d2(2023, 12, 30);
cout << (d2 - d1) << endl; */
}
void menu()
{
cout << "*****************************" << endl;
cout << "1.日期加/减天数 2.日期-日期" << endl;
cout << "3.算星期几 -1.退出 " << endl;
cout << "*****************************" << endl;
}
//用来计算某一天是星期几,日期间的加减
void text3()
{
int input = 0;
int day = 0;
Date d1;
Date d2 = d1;
//用下标来一一对应,0下标代表星期一
const char* WeekArr[] = { "周一","周二","周三","周四","周五","周六","周日" };
do
{
menu();
cout << "请输入:>";
cin >> input;
if (input == 1)
{
cout << "请输入一个日期(空格隔开):>";
cin >> d1;
cout << "请输入天数:>";
cin >> day;
cout << d1 + day << endl;
}
else if (input == 2)
{
cout << "请输入两个日期(空格隔开):>";
cin >> d1;
cin >> d2;
cout << d1 - d2 << endl;
}
else if (input == 3)
{
cout << "请输入一个日期(空格隔开):>";
cin >> d1;
cout << WeekArr[GetWeek(d1)] << endl;
}
else if (input != -1)
{
cout << "非法输入" << endl;
}
} while (input != -1);
}
int main()
{
//text1();
//text2();
text3();
return 0;
}