目录
1.日期类实现
函数列表:
bool operator==(const Date& d);
bool operator!=(const Date& d);
bool operator>(const Date& d);
bool operator>=(const Date& d);
bool operator<(const Date& d);
bool operator<=(const Date& d);
Date operator+(int day);
Date& operator+=(int day);
Date& operator++(); //前置
Date operator++(int i); //后置
Date operator-(int day);
Date& operator-=(int day);
int operator-(const Date&d);
Date& operator--();
Date operator--(int);
代码实现:
//Date.h
#pragma once
#include<iostream>
#include<stdlib.h>
#include<stdbool.h>
using namespace std;
class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//获取某年某月天数:
int GetMonthDay(int year, int month)
{
//静态修饰数组,避免频繁调用导致的反复创建销毁栈帧;
static int 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 29;
}
else
{
return days[month];
}
}
//构造函数需被频繁调用,设为inline效率更高
Date(int year = 1997, int month = 8, int day = 5)
{
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date& d);
bool operator!=(const Date& d);
bool operator>(const Date& d);
bool operator>=(const Date& d);
bool operator<(const Date& d);
bool operator<=(const Date& d);
Date operator+(int day);
Date& operator+=(int day);
Date& operator++(); //前置
Date operator++(int i); //后置重载增加一个int型参数
Date operator-(int day);
Date& operator-=(int day);
int operator-(const Date&d);
Date& operator--();
Date operator--(int);
private:
int _year;
int _month;
int _day;
};
//Date.cpp
#include "Date.h"
//任何一个类只需要写一个== >或== < ,其余复用即可;
bool Date::operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
bool Date::operator!=(const Date& d)
{
return !(*this == d);
}
bool Date::operator>(const Date& d)
{
if ((_year > d._year)
|| (_year == d._year && _month > d._month)
|| (_year == d._year && _month == d._month && _day > d._day))
{
return true;
}
else
return false;
}
bool Date::operator>=(const Date& d)
{
return (*this > d) || (*this == d);
}
bool Date::operator<(const Date& d)
{
return !(*this >= d);
}
bool Date::operator<=(const Date& d)
{
return !(*this > d);
}
Date Date::operator+(int day)
{
Date ret(*this);
ret += day;
return ret;
}
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++()
{
*this += 1;
return *this;
}
Date Date::operator++(int i)
{
Date tmp(*this);
*this += 1;
return tmp;
}
Date& Date::operator-=(int day)
{
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
--_year;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date Date::operator-(int day)
{
Date tmp(*this);
tmp -= day;
return tmp;
}
Date& Date::operator--() //前置
{
return *this -= 1;
}
Date Date::operator--(int) //后置
{
Date tmp(*this);
*this -= 1;
return tmp;
}
//d1-d2
int Date::operator-(const Date& d)
{
int flag = 1;
Date max = *this;
Date min = d;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
++min;
++n;
}
return n*flag;
}
PS:
(1)构造函数的完善:
在上文日期类实现构造函数时,若在对象初始赋值时输入了不易察觉的非法日期,将会给后续的日期类计算带来麻烦,故而我们可以在类构造时就进行日期是否合法的检查:
具体操作为:增加CheckDate函数:
//Date.h
class Date
{
public:
//...
bool CheckDate()
{
if (_year >= 1
&& _month > 0 && _month < 13
&& _day>0 && _day <= GetMonthDay(_year, _month))
{
return true;
}
else
{
return false;
}
}
//...
private:
//...
}
并在构造函数内增加判断操作:
//Date.h
class Date
{
public:
//...
//法一:提示
Date(int year=1,int month=1,int day=1)
{
_year=year;
_month=month;
_day=day;
assert(CheckDate());
}
//法二:终止
Date(int year=1,int month=1,int day=1)
{
_year=year;
_monh=month;
_day=day;
if(!CheckDate())
{
Print();
cout<<"日期非法"<<endl;
}
}
//...
private:
//...
}
(2)Date& Date::operator+=(int day);与Date& Date::operator-=(int day);的完善:
试在Test.cpp中测试:
Date d1(2022,7,25);
(d1+-100).Print();
运行会发现出现非法日期:
观察我们实现的Date& Date::operator+=(int day);函数,发现该函数实现逻辑默认day>0,试根据复用进行完善:
Date& Date::operator+=(int day)
{
if(_day<0)
{
return *this-=-day;
}
_day+=day;
if(_day>GetMonthDay(_year,_month))
{
_day-=GetMonthDay(_year,_month);
++_month;
if(_month==13)
{
++year;
_month=1;
}
}
return *this;
}
同理Date& Date::operator-=(int day);也可优化为:
Date& Date::operator-=(int day)
{
if (day < 0)
{
return *this += -day;
}
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
--_year;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
(3)函数重载与运算符重载:
运算符重载:实现自定义类型可与内置类型一般使用运算符,转换成调用这个重载函数;
函数重载:函数名相同参数不同的函数同时存在;
运算符重载与函数重载虽然都使用了“重载”,但二者之间并没有必然联系;
(4)流插入与流提取:
在C++中库中定义了两个类型,ostream与istream,cout与cin分别是ostream与istream定义在全局的对象,包含在iostream的头文件中。
内置类型可以直接使用<< 与 >>进行输入和输出,
(1)是因为库中利用运算符重载已经实现了相关函数,如:
int i=0;
double d=1.1;
cout<<i; //cout.operator<<(i);
cout<<d; //cout.operator<<(d);
//实际上是cout重载了<<运算符
(2)自动识别类型是因为实现了函数重载:
但是自定义类型却不支持直接使用流插入进行输出,当测试用例运行如下代码:
Date d1(2022,7,25);
cout<<d1;
时,编译器会报错,根据C++库支持的内置类型运算符重载与函数重载的原理,我们可以试通过operator<< 进行使用流插入实现自定义类型的输出:
//Date.h
class Date
{
public:
//...
void operator<<(ostream& out);
private:
//...
}
//Date.cpp
void Date::operator<<(stream& out)
{
out<<_year<<"-"<<_month<<"-"<<_day<<endl;
}
实现了流插入的运算符重载函数,我们可以直接对自定义类型利用流插如进行输出:
//Test.c
Date d1(2022,7,25);
//两种调用方式:
//法一:
d1.operator<<(cout);
//法二:
d1<<cout;
注意法二,实现运算符重载后,若运算符为二元操作符,则必须遵守左为第一操作数,右为第二操作数,当我们将void operator<<(ostream& out);函数编写为日期类的成员函数时,默认日期类对象为左操作数即第一操作数,故而需要写为:d1<<cout;
为了符合我们正常的代码编写习惯,试改写为cout<<d1;
首先将void operator<<(ostream& out);函数由成员函数修改为全局函数:
//Date.h
class Date
{
//...
}
void operator<<(ostream& out,const Date& d);
//Date.cpp
void operator<<(ostream& out,const Date& d)
{
out<<d._year<<"-"<<d._month<<"-"<<d._day<<endl;
}
此时又出现了类外不能直接访问private封装成员变量 的问题,先前已提过的① 调用GetYear...函数法 ② 私有成员变量公有化 ③ 将函数修改为成员函数法 在此处都不能最好的解决问题
故而,引出友元的概念:
2.友元
友元提供了一种可以突破封装的方式,有时提供了便利,但友元会增加耦合度,破坏了封装,不宜多使用。
友元分为友元函数和友元类。
2.1 友元函数
友元函数:在该函数内部可以使用Date对象访问私有保护成员;
将上文代码完善为:
(1)增加友元函数
//Date.h
class Date
{
friend void operator<<(ostream& out, const Date& d);
public:
//...
pricate:
//...
}
//Date.cpp
void operator<<(ostream& out, const Date& d);
{
out<<d._year<<"-"<<d._month<<"-"<<d._day<<endl;
}
//Test.cpp
Date d1(2022,7,25);
cout<<d1;
(2)参考赋值运算符重载时,为了实现连续赋值,我们为函数设置了返回值便于从右往左依次赋值,同理在此处,为了支持连续输出,我们也需要为函数设置返回值:
//Date.h
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);
public:
//...
pricate:
//...
}
//Date.cpp
ostream& operator<<(ostream& out, const Date& d);
{
out<<d._year<<"-"<<d._month<<"-"<<d._day<<endl;
return out;
}
//Test.cpp
Date d1(2022,7,25);
Date d2(2022,7,24);
cout<<d1<<d2;
(3)同时因为该函数会被频繁调用,建议设为内联函数,又因为内联函数声明与定义分离易造成链接错误,故而省去声明,直接在.h文件中定义内联函数:
//Date.h
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);
public:
//...
pricate:
//...
}
inline ostream& operator<<(ostream& out, const Date& d);
{
out<<d._year<<"-"<<d._month<<"-"<<d._day<<endl;
return out;
}
//Test.cpp
Date d1(2022,7,25);
Date d2(2022,7,24);
cout<<d1<<d2;
(4)增加流提取运算符重载:
//Date.h
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& out,Date& d);
public:
//...
pricate:
//...
}
inline ostream& operator<<(ostream& out, const Date& d);
{
out<<d._year<<"-"<<d._month<<"-"<<d._day<<endl;
return out;
}
inline istream& operator>>(istream& in,Date& d)
{
in >> d._year >> d._month >> d._day;
assert(d.CheckDate());
return in;
}
说明:
1.友元函数可以访问类的私有和保护成员,但不是类的成员函数;
2.友元函数不能用const修饰;
3.友元函数可以在类定义的任何地方声明,不受访问限定符的限制;
4.一个函数可以是多个类的友元函数;
5.友元函数的调用与普通函数的调用原理相同;
2.1 友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
(1)友元关系是单向的,不具有交换性。
(2)友元关系不能传递;
(3)友元关系不能继承;
3. const成员
仍以日期类为例,我们知道下文代码:
void Date::Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
编译器会处理为:
void Date::Print(Date* const this)
{
cout << _year << "-" << _month << "-" << _day << endl;
}
测试用例运行如下代码:
Date d1(2022, 7, 25);
const Date d2(2022, 7, 25);
d1.Print();
d2.Print();
编译器报错为:
对于d1,实参为&d1,是一个Date*类型的参数,而对于const修饰的d2,需要保证其指向的地址不能被修改,此时实参为const Date* ,我们知道函数形参为Date* const this,d1传递给函数的实参&d1与形参时匹配的,而d2所传递的参数类型为const Date*,权限放大,出现错误。
故而修改为:
void Date::Print()const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//表示将编译器处理方式修改为:void Date::Print(const Date* const this)
此时是d1权限缩小而d2权限不变,可以运行;
增加const后,便不能再对this指向的内容进行修改了。
PS:区别Date* const this与const Date* this:
Date* const this表示this指针本身不能修改,但指向的内容可以修改,比如this=nullptr不被允许;
const Date* this表示this指针指向的地址不能修改,比如this->_year=1不被允许;
同时只有指针和引用才会设计权限放大与缩小的问题;
建议在不改变指针指向对象的函数后都增添const,即修饰this指向的内容,也就保证了成员函数内部不会修改成员变量,同时const对象与非const对象都可以调用这个成员函数,防止实参被const修饰而导致的错误;
4.取地址及const取地址操作符重载
class A
{
public:
A* operator&()
{
return this;
}
const A* operator&()const
{
return this;
}
void Print()const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
void Print()
{
_year = 1;
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
A d1;
const A d2;
d1.Print();
d2.Print();
cout << &d1 <<endl;
cout << &d2 <<endl;;
return 0;
}
(1)对于const是否修饰构成的重载函数,编译器会调用更匹配的函数;
(2)这种情况通常会应用到取地址运算符的重载中,对于const修饰的对象,调用其取地址函数希望得到的是const 修饰的地址,而非const修饰的对象,我们也希望得到非const修饰的地址,此时便体现出取地址运算符重载的意义;
(3)取地址运算符函数也是6个默认成员函数之一,我们不显式书写时,编译器也会自动生成,一般情况下都是不需要我们自行书写的;
(4)只有当特殊情况,我们不希望他人取到该类型对象的地址,就可以通过const修饰取地址函数进行权限的锁定;
5. 再谈构造函数
5.1 构造函数体赋值
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值:
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
5.2 初始化列表
1.每个成员变量在初始化列表中只能出现一次(只能初始化一次);
2.类中包含以下成员,必须放在初始化列表位置进行初始化:
① 引用成员变量
引用变量必须在定义的地方进行初始化;
② const成员变量
const变量也必须在定义的地方初始化,此后不能再进行修改;
③ 自定义类型成员(且该类没有默认构造函数时)
class Time
{
public:
Time(int hour)
{
_hour = hour;
}
private:
int _hour;
};
class Date
{
public:
Date(int year, int hour, int& x)
//成员变量定义
:_t(hour)
, _N(10)
, _ref(x)
{
_year=year;
_ref++;
}
//上代码表示_year未显式给值的情况(会被赋值缺省值)
/*Date(int year, int hour, int& x)
//成员变量定义
:_year(year)
,_t(hour)
, _N(10)
, _ref(x)
{
_ref++;
} */
//上代码表示_year显式给值的情况
private:
//成员变量声明
int _year=0; //缺省值:初始化列表没有显式给值,就会使用这个缺省值
Time _t;
const int _N;
int& _ref;
};
int main()
{
int y = 0;
//对象的定义
Date d(2022,1,y);
return 0;
}
3.自定义类型成员当存在默认构造函数时,也可以在函数体内赋值:
class Time
{
public:
Time(int hour=0)
{
_hour = hour;
}
private:
int _hour;
};
class Date
{
public:
Date(int year, int hour)
{
//函数体内初始化
_year = year;
Time t(hour);
_t = t;
}
private:
int _year;
Time _t;
};
int main()
{
Date d(2022,1);
return 0;
}
调试监视_hour:
发现_hour在初始化为1之前首先会被初始化为0,说明自定义类型先调用了默认构造函数进行了初始化,而后再在函数体内被赋值。
也就是说,当我们提供了默认构造函数时,对于自定义类型,我们可以选择在函数体内进行赋值,
但在函数体内赋值还是会调用Time的默认构造函数,故而自定义类型成员建议使用初始化列表;
4. 内置类型也建议使用初始化列表进行初始化,在函数体内进行初始化也可以。
5.成员变量在类中的声明顺序就是其在初始化列表中的顺序,与其在初始化列表中的顺序无关:
class A
{
public:
A(int a)
:_a1(a)
,_a2(_a1)
{}
void Print() {
cout<<_a1<<" "<<_a2<<endl;
}
private:
int _a2;
int _a1;
};
int main() {
A aa(1);
aa.Print();
}
上文代码的运行结果为: 1 随机值,初始化的顺序是_a2 _a1,类的对象实例化调用构造函数时,虽然在初始化列表中_a1在前_a2在后,但是仍然先初始化_a2,将_a1赋值给a2,_a1是随机值,故而a2也是随机值,再初始化a1,将aa=1赋值给_a1,所以输出结果为1 随机值;
6.自定义类型成员如果没有在初始化列表中显式调用对应构造函数的话,则会默认调用其默认构造函数(无参构造函数或全缺省构造函数),如果此时没有无参构造函数或是全缺省构造函数,编译器就会报错,我们知道创建类的对象是对象定义的地方,而初始化列表是成员变量定义的地方;
7.初始化列表是默认存在于构造函数赋值操作前的,即是不显式书写,初始化列表也是存在的;
5.3 explicit关键字
我们知道:
int i = 10;
double d = i;
//本质是产生一个double类型的临时变量tmp,将i的值赋值给tmp,再拷贝给d
double& d = i;
//无法编译通过的原因是临时变量tmp具有常性,将临时变量拷贝给d时会出现权限的放大,导致错误
//需要修改为:
const double& d = i;
同理:
class Date
{
public:
explicit Date(int year)
:_year(year)
{
cout << " Date(int year) " << endl;
}
Date(const Date& d)
{
cout << " Date (const Date& d) " << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022);
//直接调用构造
cout << endl;
Date d2 = 2022;
//先构造+拷贝构造+优化->直接调用构造
//本质是隐式类型的转化
return 0;
}
explicit关键字可以防止int 类转化为日期类,若允许隐式类型转化发生,则删去关键字explicit即可;
比如对于字符串类string,若不允许隐式类型转化,类对象实例化时需写为string s1("hello"); 而当隐式类型转化允许时,便可以书写为:string s2="hello"; 更符合我们的表达习惯,同时若需调用函数,可以略过创建对象而直接传参:
void func(const string& s)
{
}
int main()
{
string s1("hello");
string s2 = "hello";
string str("insert");
func(str);
func("insert");
}
(同时也印证了函数接收参数尽量加const修饰,避免权限放大导致的错误)
6.static成员
6.1 概念
引入:设计一个代码计算建了多少个对象:
class A
{
public:
A()
{++_scount;}
A(const A& t)
{++_scount;}
//静态成员函数:没有this指针
static int GetScount()
{
//不能访问_a
return _scount;
}
private:
int _a;
//静态成员变量属于整个类,生命周期是整个程序,存储在静态区
static int _scount; //只是声明
};
//static成员只能在类外面定义初始化
int A::_scount = 0;
int main()
{
A a1;
A a2;
cout << a1.GetScount() << endl;
cout << A::GetScount() << endl;
return 0;
}
6.2 静态成员的特征:
1.静态成员为所有类对象共享,不属于某个具体的对象,存放在静态区;
2.静态成员变量必须在类外定义,定义时不添加任何static关键字,类中只是声明;
3.类静态成员即可用 类名::静态成员 或 对象.静态成员 来访问;
4.静态成员函数没有隐藏的this指针,不能访问任何非静态成员;
5.静态成员也是类的成员,受public,private,protected访问限定符的限制;
注意:
静态成员函数不能调用非静态成员函数,但是非静态成员函数可以调用静态成员函数;
6.3 应用
例:设计一个只能在栈上定义的类
class StackOnly
{
public:
static StackOnly CreatObj()
{
StackOnly so;
return so;
}
private:
StackOnly(int x=0,int y=0)
:_x(x)
,_y(y)
{ }
private:
int _x = 0;
int _y = 0;
};
int main()
{
//StackOnly so1; //栈
//static StackOnly so2; //静态区
StackOnly so3=StackOnly::CreatObj();
return 0;
}
7.内部类
7.1 概念
如果一个类定义在另一个类的内部,这个内部的类就称为内部类。
内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象取访问内部类的成员。
外部类对内部类没有任何优越的访问权限;
7.2 特征
1. 内部类可以定义在外部类的public protected private都可以;
2. 内部类可以直接访问外部类中的static成员,不需要外部类的对象或类名;
3. sizeof (外部类)=外部类,和内部类没有任何关系;
但是当我们在内部类为B的A类中若创建了B类的对象,则sizeof(A)就会改变:
(1)下文代码运行结果为4:
class A
{
private:
int _h;
public:
class B
{
public:
private:
int _b;
};
};
int main()
{
cout << sizeof(A) << endl;
return 0;
}
(2)下文代码运行结果为8:
class A
{
private:
int _h;
public:
class B
{
public:
private:
int _b;
};
B _b;
};
int main()
{
cout << sizeof(A) << endl;
return 0;
}
4. 内部类受外部类的类域限制,即受访问限定符的限制;
5.在类外不能单独用内部类创建对象,必须指定类域:
如在上文代码中,如需创建B类的对象,需写为:
A::B b;
6.内部类就是外部类的友元类,即内部类可以访问外部类的private与protected;
8.编译器优化
8.1 隐式类型转化
单参数类型的构造函数会对隐式类型进行转化,参考explicit关键字部分;
8.2 连续构造的优化
在连续的一个表达式步骤中,连续构造一般都会优化:
(1)以匿名对象为例:
class W
{
public:
W()
{
cout << "W( )" << endl;
}
W(const W& w)
{
cout << "W(const W& w)" << endl;
}
W& operator=(const W& w)
{
cout << "W(const W& w)" << endl;
return *this;
}
~W()
{
cout << "~W( )" << endl;
}
private:
};
void func1(W w)
{
}
int main()
{
//W w1;
//func1(w1);
fun1(W());
return 0;
}
普通创建对象进行传值传参时,编译器会处理为:构造+拷贝构造+析构:
直接将匿名对象作为参数进行传值传参时,编译器会优化为:构造+析构:
PS:
匿名对象的概念:没有对象名,直接用类名进行初始化的对象称为匿名对象;
匿名对象的特点:生命周期只有创建它时的这一行代码,随即立刻调用其析构函数;
匿名对象的应用:当一个变量只在这一行使用,其余位置不予使用就可以创建匿名对象;
比如:假设现在存在类Solution与类的成员函数Sum_Solution,我们需要调用Sum_Solution函数的方法是:
Solution slt;
slt.Sum_Solution(10);
在该例中,slt变量存在的意义是调用成员函数,其他地方不会再进行使用,就可以利用匿名对象:
Solution().Sum_Solution(10);
(2)
class W
{
public:
W(int x=0)
{
cout << "W( )" << endl;
}
W(const W& w)
{
cout << "W(const W& w)" << endl;
}
W& operator=(const W& w)
{
cout << "W(const W& w)" << endl;
return *this;
}
~W()
{
cout << "~W( )" << endl;
}
private:
};
void func1(W w)
{
}
void func2(const W& w)
{
}
W func3()
{
W ret;
return ret;
}
int main()
{
//func3(); //1次构造,1次拷贝构造
W w1 = func3(); //1次构造,1次拷贝构造
return 0;
}
运行func3()时:
运行W w1=func3(); 时:
此种情况本来应是1次构造 2次拷贝构造(ret进行拷贝构造创建临时变量+函数返回值拷贝构造给w1),但是编译器优化为1次构造+1次拷贝构造(ret进行拷贝构造直接复制给w1);
此处的优化又涉及到函数调用时栈帧的创建与销毁,我们知道在传值调用时临时变量会生成一份拷贝,当临时变量较小(4or8 byte)时,会存储在寄存器(哪个编译器取决于编译器)中,效率更高且防止丢失,当临时变量较大时,会存储在上一层栈帧(或两个函数栈帧中间的位置),w1直接在f3结束前充当tmp的价值,即若没有w1,就产生tmp,如果有w1,则w1充当tmp的作用;
(3)
class W
{
public:
W(int x=0)
{
cout << "W( )" << endl;
}
W(const W& w)
{
cout << "W(const W& w)" << endl;
}
W& operator=(const W& w)
{
cout << "W(const W& w)" << endl;
return *this;
}
~W()
{
cout << "~W( )" << endl;
}
private:
};
W f(W u)
{
W v(u);
W w = v;
return w;
}
int main()
{
W x;
W y = f(f(x));
return 0;
}
上文代码共进行了1次构造,7次拷贝构造:
图示解析: