【C++笔记】类和对象的深入理解(三)
【C++笔记】类和对象的深入理解(三)
🔥个人主页:大白的编程日记
🔥专栏:C++笔记
文章目录
- 【C++笔记】类和对象的深入理解(三)
- 前言
- 一.日期类的实现
- 1.1声明和定义分离
- 1.2日期类+=整数
- 1.3日期类+整数
- 1.4日期类-=整数
- 1.5日期类-日期
- 1.6复用对比
- 1.7日期类==日期类
- 1.8日期类<日期类
- 1.9比较复用
- 1.10前置和后置
- 1.11日期类-日期类
- 1.12日期类IO
- 二.取地址运算符重载
- 2.1const成员函数
- 2.2取地址运算符重载
- 三.再探构造函数
- 四.类型转换
- 五.static成员
- 六.友元
- 七.内部类
- 八.匿名对象
- 后言
前言
哈喽,各位小伙伴大家好!上期我们讲了类和对象的更深入的内容。今天我们就给类和对象收尾。话不多说,咱们进入正题!向大厂冲锋!
一.日期类的实现
为了加深我们前面对类和对象的理解。我们现在来试着手搓一个日期类。
1.1声明和定义分离
我们想让.h放函数的声明。.cpp放函数的定义。
#pragma once
#include<iostream>
using namespace std;
#include<assert.h>
class Date
{
friend istream& operator>>(istream& in, Date& d);
friend ostream& operator<<(ostream& out, const Date& d);//友元声明
public:
Date(int year ,int month ,int day );
void Print();
// Ĭinline
int GetMonthDay(int year, int month)
{
assert(month > 0 && month < 13);
static int monthDayArray[13] = { -1, 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;
}
return monthDayArray[month];
}
int GetYear();
int GetMonth();
int GetDay();
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;
bool CheckDate();
Date operator+(int day) const;
Date& operator+=(int day);
Date operator++(int);//后置
Date& operator++();//前置
Date operator--(int);//后置
Date& operator--();//前置
Date& operator-=(int day);
Date operator-(int day) const;
int operator-(const Date& d) const;
private:
int _year;
int _month;
int _day;
};
istream& operator>>(istream& in, Date& d);
ostream& operator<<(ostream& out, const Date& d);
我们日期类主要实现他的比较和日期类之间的相互运算。
1.2日期类+=整数
这里我们的思路是先将整数加到天数上。然后如果天数不超过当月天数。直接返回即可。否则说明日期比当月的所有日期都大。则向下月借位。天数减去当月天数之和,以此类推知道天数不超过当月的最大天数位置。如果12借位则年份+1,月份改为1即可。
int GetMonthDay(int year, int month)
{
assert(month > 0 && month < 13);
static int monthDayArray[13] = { -1, 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;
}
return monthDayArray[month];
}
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;
}
1.3日期类+整数
现在我们实现日期类+整数,也就是不改变日期类。返回日期类+=整数的结果。那我们可以复用日期类+=整数的逻辑。为了不修改日期类。我们对日期类拷贝一份tmp。让tmp进行+=的逻辑这样改变就不会日期类。
Date Date::operator+(int day) const
{
Date tmp=*this;
tmp +=day;
return tmp;
}
1.4日期类-=整数
这里思路和我们前面实现+的思路一样。都是通过借位的思想。我们向当天数-day。
然后当天数!>0时,向上一个月借位,天数+=上个月的天数(注意是上个月的,因为当月的天数已经被减过了)。如果是1月的话,借去年12月的天数。让月份=12然后年份–。
Date& Date::operator-=(int day)
{
_day -= day;
while (_day <= 0)
{
if (_month == 1)
{
_month = 12;
_day += GetMonthDay(--_year, _month);
}
else
{
_day += GetMonthDay(_year, --_month);
}
}
return *this;
}
1.5日期类-日期
日期类-日期我们就复用-=即可。
Date Date::operator-(int day) const
{
Date tmp=*this;
tmp -= day;
return tmp;
}
1.6复用对比
1.7日期类==日期类
判断两个日期类很简单。我们只需要判断是否他们的年月日都相等即可。
bool Date::operator==(const Date& d) const
{
if (_year == d._year && _month == d._month && _day == d._day)
{
return true;
}
else
{
return false;
}
}
1.8日期类<日期类
判断两个日期类<。
先比较年。
<返回true。
==继续比较月。月<返回true。 ==返回比较天。
剩下的情况都是false的情况。直接返回false.
bool Date::operator<(const Date& d) const
{
if (_year < d._year)//比较年
{
return true;
}
else if (_year == d._year)//年相等
{
if (_month < d._month)//判断月
{
return true;
}
else if (_month == d._month)//月相等
{
return _day < d._day; //判断天
}
}
return false;//年不相等
}
1.9比较复用
剩下的我们通过前面实现的<和==复用即可。
bool Date::operator!=(const Date& d) const
{
return !(*this == d);
}
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);
}
1.10前置和后置
前置我们拷贝一份*this,在复用+=或-=即可。
Date Date::operator++(int)
{
Date tmp = *this;
*this += 1;
return tmp;
}
Date& Date::operator++()
{
*this += 1;
return *this;
}
Date Date::operator--(int)
{
Date tmp=*this;
*this-= 1;
return tmp;
}
Date& Date::operator--()
{
*this -= 1;
return *this;
}
1.11日期类-日期类
两个日期类相减得出他们之间相差的天数。
我们的思路就是直接让小的天数一直相加,直到相加到小的日期和大的日期相等即可。每次日期++就用一个变量记录天数即可。
int Date::operator-(const Date& d) const
{
int sum = 0;
int flag = 1;
Date max, min;
max = *this;
min = d;
if (*this < d)//假设法
{
swap(max, min);
flag = -1;
}
while (min!=max)
{
sum++;
min++;
}
sum *=flag;
return sum;
}
1.12日期类IO
因为类的成员函数默认*this抢占了第一个位置。和我们平时的写法不一样。所以我们把日期类的输出和输出放在类外面。那放在类外面我们如何访问类的成员变量呢?那我们就可以加一个友元函数声明。
istream& operator>>(istream& in, Date& d)
{
cout<<"请依次输入年月日:"<<endl;
in >>d._year>> d._month >> d._day;
return in;
}
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year <<"年" << d._month<<"月" << d._day<<"日";
return out;
}
为了实现连续输入或输出。我们需要返回istrem或ostrem对象。
同时注意流对象不支持拷贝。所以istrem或ostrem必须用引用。
二.取地址运算符重载
2.1const成员函数
-
定义
将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后面。 -
const修饰
const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。const 修饰Date类的Print成员函数,Print隐含的this指针由 Date* const this 变为const Date* const this。
不写加const。
加上const。
同时非const成员也可以调用const成员函数。因为权限可以缩小。所以加上const可以防止我们的程序不小心篡改(原本不应该修改却不小心修改会报错),同时也让我们的的成员函数传参更宽泛,const成员也可以调用。所以能加尽加。
2.2取地址运算符重载
取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,⼀般这两个函数编译器自动生成的就可以够我们⽤了,不需要去显示实现。除非⼀些很特殊的场景,比如我们不想让别⼈取到当前类对象的地址,就可以自己实现⼀份,胡乱返回⼀个地址。
class Date
{
public:
Date* operator&()
{
return this;
// return nullptr;
}
const Date* operator&()const
{
return this;
// return nullptr;
}
private:
int _year; // 年
int _month; // ⽉
int _day; // ⽇
};
一般这两个成员函数都不需要我们显示的写。因为他们两个是默认成员函数。
按理说我们只写const成员函数即可。因为不论是const成员还是非const成员都可以调用。但是const成员调用返回的是const this*。所以还需要写两份。并且编译器调用时会调用最匹配的。
三.再探构造函数
- 初始化列表
之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值,构造函数初始化还有⼀种方式,就是初始化列表,初始化列表的使用方式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成员列表,每个"成员变量"后面跟⼀个放在括号中的初始值或表达式。
大概形式如下:
Date(int& x, int year = 1, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
,_t(12)
,_ref(x)
,_n(1)
{
// error C2512: “Time”: 没有合适的默认构造函数可⽤
// error C2530 : “Date::_ref” : 必须初始化引⽤
// error C2789 : “Date::_n” : 必须初始化常量限定类型的对象
}
-
定义的位置
每个成员变量在初始化列表中只能出现⼀次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。因为初始化列表是定义的地方所以,只能初始化一次。否则就会变成多次定义。 -
定义初始化
引用成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进行初始化,否则会编译报错。
class Time
{
public:
Time(int hour=0)
:_hour(hour)
{
cout << "Time()" << endl;
}
private:
int _hour;
};
class Date
{
public:
Date(int& x,int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
,a(1)
,_ref(x)//成员定义
{
// error C2512: “Time”: 没有合适的默认构造函数可⽤
// error C2530 : “Date::_ref” : 必须初始化引⽤
// error C2789 : “Date::_n” : 必须初始化常量限定类型的对象
}
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
//声明
int _year;
int _month;
int _day; int main();
int& _ref;
const int a;
Time _t;
};
引用成员变量,const成员变量必须在初始化列表初始化。因为他们必须在定义的地方初始化。
没有默认构造的类类型也必须在初始化列表初始化,因为他必须手动传参调用构造函数。
那初始化列表和函数体内赋值可以混着用吗?
可以,因为有些场景必须混着用。
Date(int& x,int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
,a(1)
,_ref(x)
,_ptr((int*)malloc(size(int)//成员定义
{
if (_ptr == nullptr)
{
perror(malloc fail!);
}
}
例如有个指针,我们malloc以后需要检查是否失败。那就必须在函数体内检查。
- 缺省值
C++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的成员使用的。
private:
//声明
int _year=1;
int _month=1;
int _day=1;
int* _ptr=((int*)malloc(sizeof(int)));
在声明的位置给一个缺省值。
-
总结
尽量使用初始化列表初始化,因为那些你不在初始化列表初始化的成员也会走初始化列表,如果这个成员在声明位置给了缺省值,初始化列表会用这个缺省值初始化。如果你没有给缺省值,对于没有显⽰在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。对于没有显示在初始化列表初始化的⾃定义类型成员会调⽤这个成员类型的默认构造函数,如果没有默认构造会编译错误。
所以初始化列表能写尽写,因为就算你不写他也会走初始化列表。
-
初始化顺序
初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的的先后顺序无关。建议声明顺序和初始化列表顺序保持⼀致。
大家看一下这道题选什么。
可能很多同学都会觉得选A。但实际不是这样的。因为初始化列表初始化的顺序是声明的顺序。
声明是顺序是他们在内存存放的顺序。初始化是按照内存中的顺序初始化。
四.类型转换
class A
{
public:
//构造函数explicit就不再⽀持隐式类型转换
// explicit A(int a1)
A(int a1)
:_a1(a1)
{}
//explicit A(int a1, int a2)
A(int a1, int a2)
:_a1(a1)
, _a2(a2)
{}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a1 = 1;
int _a2 = 2;
};
int main()
{
// 1构造⼀个A的临时对象,再⽤这个临时对象拷⻉构造aa3
//编译器遇到连续构造+拷⻉构造->优化为直接构造
A aa1 = 1;
aa1.Print();
const A& aa2 = 1;
// C++11之后才⽀持多参数转化
A aa3 = { 2,2 };
return 0;
}
- 隐式类型转化
C++支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。
这里我们可以验证一下。
这里报错是为啥?
因为隐式类型转化生成是临时对象,临时对象具有常性。所以要加上const引用。
那隐式类型转化有什么用?
class Stack
{
public:
void Push(const A& aa)
{
}
private:
int top;
};
int main()
{
Stack s;
A aa = 1;
s.Push(aa);
s.Push(1);
return 0;
}
可以发现隐式类型转化可以让我们写代码更加方便简洁。
- explicit
如果不想发生隐式类型转化, 构造函数前面加explicit就不再支持隐式类型转换。
五.static成员
- 静态成员变量
用static修饰的成员变量,称之为静态成员变量,静态成员变量⼀定要在类外进行初始化。 - 共享
静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。
-
静态成员函数
用static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。 -
访问权限
静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有this指针。非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
-
指定类域
突破类域就可以访问静态成员,可以通过类名::静态成员或者对象.静态成员来访问静态成员变量和静态成员函数。
-
访问限定符
静态成员也是类的成员,受public、protected、private访问限定符的限制。 -
缺省值
静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表。 -
练习一
题目:求1+2+3…+n
这道题目把常规的方法都限制了。但是这道题可以用我们的静态成员解决。
这道题主要修饰构造出循环这个条件。所以我们可以用一个类数组来构造这个条件。
定义一个n大小的类数组。那就会调用n次构造。我们只需要定义两个静态成员。然后每次构造把当前的值累加即可。
class Sum {
public:
Sum() {
_ret += _i;
++_i;
}
static int GetRet() {
return _ret;
}
private:
static int _i;
static int _ret;
};
int Sum::_i = 1;
int Sum::_ret = 0;
class Solution {
public:
int Sum_Solution(int n) {
//变⻓数组
Sum arr[n];
return Sum::GetRet();
}
};
- 练习二
C c;
int main()
{
A a;
B b;
static D d;
return 0;
}
六.友元
- 定义
友元提供了⼀种突破类访问限定符封装的方式,友元分为:友元函数和友元类,在函数声明或者类声明的前面加friend,并且把友元声明放到⼀个类的里面。
class A
{
// 友元声明
friend void func(const A& aa, const B& bb);
private:
int _a1 = 1;
int _a2 = 2;
};
void func(const A& aa, const B& bb)
{
cout << aa._a1 << endl;
cout << bb._b1 << endl;
}
例如这里func是A的好朋友,func就可以访问A的成员。
-
声明
外部友元函数可访问类的私有和保护成员,友元函数仅仅是⼀种声明,他不是类的成员函数。 -
声明位置
友元函数可以在类定义的任何地方声明,不受类访问限定符限制。 -
多个友元
⼀个函数可以是多个类的友元函数。
class A
{
// 友元声明
friend void func(const A& aa, const B& bb);
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
// 友元声明
friend void func(const A& aa, const B& bb);
private:
int _b1 = 3;
int _b2 = 4;
};
func既是A的友元又是B的友元。
- 友元类
友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员
class A
{
// 友元声明
friend class B;
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
public:
void func1(const A& aa)
{
cout << aa._a1 << endl;
cout << _b1 << endl;
}
void func2(const A& aa)
{
cout << aa._a2 << endl;
cout << _b2 << endl;
}
private:
int _b1 = 3;
int _b2 = 4;
};
int main()
{
A aa;
B bb;
bb.func1(aa);
bb.func1(aa);
return 0;
}
B是A的友元类。B的所有成员函数都可以访问A的成员变量。
-
单向性
友元类的关系是单向的,不具有交换性,比如A类是B类的友元,但是B类不是A类的友元。 -
传递性
友元类关系不能传递,如果A是B的友元,B是C的友元,但是A不是C的友元。 -
使用
有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
七.内部类
- 定义
如果⼀个类定义在另⼀个类的内部,这个内部类就叫做内部类。内部类是⼀个独立的类,跟定义在全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
class A
{
private:
static int _k;
int _h = 1;
public:
class B // B默认就是A的友元
{
public:
void foo(const A& a)
{
cout << _k << endl; //OK
cout << a._h << endl; //OK
}
private:
int _b;
};
};
int A::_k = 1;
int main()
{
cout << sizeof(A) << endl;
A::B b;
A aa;
b.foo(aa);
}
所以刚刚OJ的代码也可以改成内部类
class Solution {
// 内部类
class Sum
{
public:
Sum()
{
_ret += _i;
++_i;
}
};
static int _i;
static int _ret;
public:
int Sum_Solution(int n) {
// 变⻓数组
Sum arr[n];
return _ret;
}
- 默认友元
内部类默认是外部类的友元类。 - 封装
内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方都用不了。
八.匿名对象
- 定义
用类型(实参)定义出来的对象叫做匿名对象,相比之前我们定义的类型对象名(实参)定义出来的叫有名对象。
匿名对象让我们写代码更方便。
- 生命周期
匿名对象的生命周期只在当前⼀行,⼀般临时定义⼀个对象当前用⼀下即可,就可以定义匿名对象。
可以看到未打印1之前,A就析构了。因为匿名对象声明周期只在当前行。
后言
这就是类和对象的深入理解。今天讲了很多内容。大家自己好好消化。感谢各位的耐心垂阅!咱们下期见!拜拜~