紧接上一篇👉: 【C++】(5)类和对象练习,日期类的实现,运算符重载
友元
友元函数
要想实现流插入<<
运算符的重载,首先简单看一下下图。
原来cout
其实是一个对象,它的类型为ostream
。一个<<
有两个操作数,那么我们也可以把它作为运算符重载的一个参数。
如果重载<<
写在类里面:
void operator<<(std::ostream& out)
{
out << _year << "-" << _month << "-" << _day << endl;
}
调用:
因为this指针默认是第一个参数,所以d
要写在cout
前面。
void test4()
{
Date d(1919, 8, 10);
d << cout;
}
//结果:1919-8-10
👆:程序跑起来是没什么问题,但是这个d << cout;
的调用方式也太奇怪了吧,我们平时都是cout
写在前面的啊。
看来只能写成全局的:
注意:
- 全局函数不能写在.h文件里,因为在Date.cpp和test.cpp同时展开,会造成重定义。
- 为了支持连续的流插入,需要有返回值。
std::ostream& operator<<(std::ostream& out, const Date& d)
{
out << d._year << "-" << d._month << "-" << d._day << endl;
return out;
}
但是全局的函数在类外,访问不了类里面的私有成员。
我们可以把它设置为类的==友元函数==:
友元函数特性:
- 友元函数需要在类内声明,声明时要加
friend
关键字。 - 友元函数可以在类的任意地方声明,不受访问限定符的限制。
- 友元函数可以直接访问类内的私有和保护成员,它是定义在类外的普通函数,不属于任何类。
class Date
{
friend std::ostream& operator<<(std::ostream& out, const Date& d); //友元函数声明
public:
//略...
private:
int _year;
int _month;
int _day;
};
这样我们的调用就可以写成cout << d;
了。
流提取>>
运算符的重载与上述同理,cin
也是对象,类型是istream
。
注意检查输入的日期是否合法。
class Date
{
friend std::ostream& operator<<(std::ostream& out, const Date& d); //友元函数声明
friend std::istream& operator>>(std::istream& in, Date& d);
public:
//略...
private:
int _year;
int _month;
int _day;
};
std::istream& operator>>(std::istream& in, Date& d)
{
int year, month, day;
in >> year >> month >> day;
if (year >= 1
&& month <= 12 && month >= 1
&& day >= 1 && day <= d.GetMonthDay(year, month))
{
d._year = year;
d._month = month;
d._day = day;
}
else
cout << "日期非法" << endl;
return in;
}
友元类
一个类可以是另一个类的友元,友元类可以访问另一个类的私有成员。
- 友元关系不具有交换性,比如下述Date类可以访问Time类的私有成员,Time不能访问Date类的私有成员。
- 友元关系不具有传递性,A类是B类的友元,B类是C类的友元,A类不一定是C类的友元。
class Date; // 前置声明
class Time
{
friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
public:
Time(int hour = 1, int minute = 1, int second = 1)
{
_hour = hour;
_minute = minute;
_second = second;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void SetTimeOfDate(int hour, int minute, int second)
{
// 直接访问时间类私有的成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
内部类
如果一个类定义在另一个类的内部,那么这个类就叫做内部类。
- 内部类天生就是外部类的友元。
- 内部类受外部的访问限定符的限制。
class A
{
public:
class B //B是A的友元
{
public:
void foo(const A& a)
{
cout << k << endl;
cout << a.h << endl; //B可以访问A的私有成员
}
private:
int _b;
};
private:
static int k;
int h;
};
sizeof(A)
是多少?
答:A
类型的对象里面没有B
类型的对象,static
修饰的k
不算大小,所以只有一个int h
占大小,答案为4
再谈构造函数
构造函数体内赋值
我们之前写的构造函数,成员变量在函数体内初始化:
//函数体内初始化
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
严格来说,函数体内初始化并不能叫做初始化,应该叫赋初值,因为初始化只有一次,而函数体内可以多次赋值。对于必须在定义时就初始化的类型,比如引用,函数体内就写不了了。
初始化列表
使用方法:
初始化列表:以一个冒号:
开始,接着是由逗号,
分隔的数据成员列表,每个成员变量后跟一个括号,括号内为初始值或表达式。
//初始化列表初始化
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
初始化列表才是对象里的成员定义的地方,编译器自己生成的默认构造函数也有初始化列表,用来定义成员变量,这也是为什么它对内置类型不处理(随机值),而自定义类型会调用它的默认构造函数。
C++11支持在成员变量声明处给缺省值,给的缺省值其实就是给初始化列表用的。
注意:
- 每个成员变量在初始化列表中只能出现一次(初始化只能有一次)
- 以下三类必须在初始化列表初始化:引用成员变量,
const
成员变量,自定义类型成员(其类里面没有默认构造函数)
三种类型成员变量在初始化列表初始化:
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
class A
{
public:
A(int a, int ci, const Date& d)
:_a(a)
, _ci(ci)
, _d(d)
{}
private:
int& _a; //引用
const int _ci; //const修饰
Date _d; //无默认构造的自定义类型
};
👆:A
类中有Date
类型的成员变量,而Date
类没有提供默认构造函数,所以Date
类在定义的时候就必须手动传参初始化。
建议:成员变量尽量都在初始化列表初始化。
成员变量在初始化列表中的初始化顺序是它在类中的声明次序,与它在初始化列表中的顺序无关
如下代码的结果是多少?
class A
{
public:
A(int n)
:_a(n)
, _b(_a)
{}
void Print()
{
cout << _a << " " << _b << endl;
}
private:
int _b;
int _a;
};
int main()
{
A aa(1);
aa.Print();
return 0;
}
答案:1 随机值
解析:_b
先声明,先初始化_b
,因为_b(_a)
中的_a
还未初始化,所以_b
最后被初始化为随机值。接着_a(n)
,n
为1,最后_a
被初始化为1,所以最终结果为:1 随机值
explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有类型转换的作用。
class Date
{
public:
Date(int year)
:_year(year)
{}
void Print()
{
cout << _year << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022); //构造
Date d2 = 2023; //隐式类型转换
d2.Print(); //结果:2023
return 0;
}
对于Date d2 = 2023;
,一般编译器会使用2023构造一个临时对象,然后用它拷贝构造d2,不过编译器通常会对这种过程进行优化,直接使用2023去构造d2。
另外因为临时对象具有常性,它的引用必须加const
,比如const Date& d3 = 2022;
explicit
关键字可以用来修饰构造函数,被修饰的构造函数会被禁止隐式类型转换:
class Date
{
public:
explicit Date(int year) //被explicit修饰的构造函数
:_year(year)
{}
void Print()
{
cout << _year << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022); //构造
Date d2 = 2023; //此处报错:不存在从 "int" 转换到 "Date" 的适当构造函数
d2.Print();
return 0;
}
static成员
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态的成员变量一定要在类外进行初始化
静态成员变量
静态成员变量存储在静态区(不占用对象的空间),其属于整个类,也属于该类的所有对象。
面试题:实现一个类,计算程序中创建出多少个该类的对象。
class A
{
public:
A()
{
++_count;
}
A(const A& aa)
{
++_count;
}
static int _count; //静态成员变量
};
int A::_count = 0; //在类外初始化
A func(A a)
{
A copy(a);
return copy;
}
int main()
{
A a1;
A a2 = func(a1);
cout << A::_count << endl;//也可以a1._count访问
return 0;
}
//结果:4
静态成员函数
static也可以修饰成员函数
注意:static成员函数没有this指针,正因如此,它也无法直接访问其他非静态成员函数
class A
{
public:
A()
{
++_count;
}
A(const A& aa)
{
++_count;
}
static int GetCount() //静态成员函数
{
return _count;
}
private:
static int _count; //设为私有,不能在类外直接访问的情况
};
int A::_count = 0; //在类外定义
A func(A a)
{
A copy(a);
return copy;
}
int main()
{
A a1;
A a2 = func(a1);
cout << A::GetCount() << endl; //调用GetCount函数//也可以a1.GetCount()调用
return 0;
}
👆:因为没有this指针,所以这里调用静态成员函数不需要指定对象,只要指定类域即可。
下面这道题,可以用静态成员变量解决:
原题链接:求1+2+3+…+n__牛客网 (nowcoder.com)
求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
数据范围: 0 < n ≤ 200 0<n≤200 0<n≤200
进阶: 空间复杂度 O ( 1 ) O(1) O(1),时间复杂度 O ( n ) O(n) O(n)
示例1
输入 5
输出 15
示例2
输入 1
输出 1
参考代码:
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 a[n];
return Sum::GetRet();
}
};