一.友元
1.日期类流提取和流插入的运算符重载
上一篇文章,我们实现了日期类,对于该日期类,我们实现了各种各样的运算符重载,如前置++,后置–,-,+,>,==等等运算符,但是对于日期类,我们可以实现流插入和流提取吗?
Date d;
cin>>d;//流提取
cout<<d;//流插入,这样子写我们可以实现吗?
目前来看显然是不行的。
对于内置类型,我们直接使用cin,我们输入什么类型,cout打印出来就是什么类型,为什么对于内置类型就能自动的识别类型呢?这是由于cout和cin都是被封装起来的,他们都使用了运算符重载,你输入什么类型,它就会自动匹配对应的运算符重载函数。官网如下:
总结:cout是一个全局类型的对象,这个对象的类型是ostream。
所以对于自定义类型,显然需要我们自己实现,这里我们对于Date类的流提取和流插入实现运算符重载。
对于流插入:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << "Print()" << endl;
}
void operator<<(ostream& out)//这是流提取的运算符重载函数
{
out << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date d(2024,3,14);
cout << d;
return 0;
}
这里还是会报错,为什么呢?这是因为我们之前学的,成员函数里面有一个this指针,而运算符重载函数的参数是按顺序匹配的,这里我们两个参数刚好搞反了。
如果我们要把这个日期类打印出来,我们就可以把d和cout的位置交换一下,如下所示。
虽然这样写能得到我们想要的,但是这样写是不符合要求的,那怎么办呢?这里我们就可以把这个流插入的运算符重载函数写成全局的函数,不把它写成类的成员函数,这样我们就可以在该函数的参数把顺序写对。
就像这样:
void operator<<(ostream& out,Date &d)
{
out << d._year << "-" << d._month << "-" << d._day << endl;
}
但是这样又出现了一个新的问题,_year这些成员变量是私有的成员,只有类中才可以访问,类外是不能访问的,于是友元就闪亮登场了。
2.友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
class Date
{
friend void operator<<(ostream& out, Date& d);//这就是友元函数
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << "Print()" << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
void operator<<(ostream& out,Date &d)
{
out << d._year << "-" << d._month << "-" << d._day << endl;
}
int main()
{
Date d(2024,3,14);
cout<<d;
return 0;
}
运行结果:
这样我们就能顺利实现流插入运算符重载函数了。
又有个新的问题:但是对于内置类型我们可以实现连续打印,int a = 10;int b = 20;cout << a << b;但是对于这个Date类我们可以实现连续打印吗?
答案是不能,对于内置类型为什么能实现连续打印呢?我们仔细看看上面的那个文档,该函数是有返回值的。
于是我们把上面的流插入运算符重载函数加上返回值,就能实现Date类的连续打印了。
class Date
{
friend ostream& operator<<(ostream& out, Date& d);
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << "Print()" << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
ostream& operator<<(ostream& out,Date &d)
{
out << d._year << "-" << d._month << "-" << d._day << endl;
return out;
}
int main()
{
Date d1(2024,3,14);
Date d2(2023, 4, 5);
cout<<d1<<d2;
return 0;
}
运行结果:
说明:
①友元函数可以访问类的私有和保护成员,但不是成员函数。
②友元函数不能用const修饰,这是因为友元函数没有this指针。
③友元函数可以在类的任何位置声明,不受访问限定符的限制。
④一个函数可以是多个类的友元函数。就像一个人可以是多个人的朋友。
3.友元类
除了上述的友元函数,一个类也可以是友元。
特性:
①友元关系是单向的,不具有交换性。
class Date
{
frined class Time;
};
像这样,Time类就是Date类的友元类,Time类就可以访问Date类的私有成员变量,但是Date类访问Time类中的私有成员变量就是不可以的。
②友元关系不能传递。
如果C是B的友元, B是A的友元,则不能说明C是A的友元。
③友元关系不能继承,后续再说。
二.再谈构造函数
1.构造函数体赋值
当我们创建一个对象时,编译器会自动调用构造函数,给对象中的各个成员变量一个合适的初始值。
class Date
{
public:
Date(int year=1, int month=1, int day=1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
构造函数语句之中,只能说是赋初值,不能叫做初始化,因为初始化只能初始化一次,而构造函数语句中可以多次赋值。
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
_year = 2021;//构造函数语句中就可以进行多次赋值,故而不是初始化
_month = 5;
}
为了实现初始化,于是初始化列表就出现了。
2.初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
这里通过初始化列表成功把三个成员变量初始化了,这里就有个疑问了,这不是和构造函数差不多吗?使用构造函数不是挺好的吗,初始化列表可以处理以下几个问题,这是构造函数无法处理的。
3.使用注意事项
I每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
II类中包含以下成员时,必须使用初始化列表处理:
①引用成员变量
错误写法:
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
int &_a=10;//这里只是声明的地方,不是初始化的地方
};
所以我们在使用引用成员变量时,放在初始化列表中进行初始化。
正确写法:
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
,_a(10)
{}
private:
int _year;
int _month;
int _day;
int &_a;//这里只是声明的地方,不是初始化的地方
};
②const成员变量
const成员变量也是必须定义的时候初始化,也只能通过初始化列表来实现。
③自定义类型成员(且该类没有默认构造函数时)
class Time
{
public:
Time(int a)
{
_a = a;
}
private:
int _a;
};
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
Time _t;
};
int main()
{
Date d(2023, 1, 1);
return 0;
}
Time类中没有默认构造函数,就会报错,所以我们只能通过初始化列表的形式进行初始化工作。
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
,_t(10)//加上就没错了
{}
III尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
这里我们没对_t进行显示写初始化列表,但是它还是会使用初始化列表进行初始化,因为它会调用它自己的默认构造函数。
IV 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
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();
}
这里先声明的_a2,于是初始化列表会先初始化 _a2,但是 _a1这时还没有初始化,还是随机值,所以 _a2也是随机值,但是 _a1会正确的初始化为1。
还有初始化列表可以和函数体混合使用,具体看你怎么用。
总结:
①尽量都使用初始化列表进行初始化。
②初始化次序和声明次序尽量保持一致。
4.explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
比如说:
class A
{
public:
A(int a=1)
:_a(a)
{
cout << "A(int a=1)" << endl;
}
A(const A&a)
:_a(a._a)
{
cout << "A(const A&a)" << endl;
}
private:
int _a;
};
int main()
{
A a1(10);
A a2 = 20;//这里会单参数构造函数的隐式类型转换
//这里会用20去调用A构造函数会生成一个临时对象,再使用这个对象去拷贝构造a2
//但是编译器会优化成直接去构造。
return 0;
}
这里就会调用两次构造函数,但是如何验证我们说的:构造函数会生成一个临时对象,再使用这个对象去拷贝构造。
验证:
A& ref = 10;//这样写会引发错误
就是因为中间会生成一个临时对象,而临时对象具有常性,故而我们必须加上const。
正确写法:
const A& ref = 10;
因为单参数构造函数的隐式类型转换可读性不是很好,我们可以用explicit修饰构造函数,将会禁止构造函数的隐式转换。
class A
{
public:
explicit A(int a=1)//这里加上explicit关键字,就能禁止单参数构造函数的隐式类型转换
:_a(a)
{
cout << "A(int a=1)" << endl;
}
A(const A&a)
:_a(a._a)
{
cout << "A(const A&a)" << endl;
}
private:
int _a;
};
int main()
{
A a1(10);
A a2 = 20;
const A& ref = 10;
return 0;
}
三.匿名对象
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
class Solution {
public:
int Sum_Solution(int n)
{
return n;
}
};
int main()
{
A aa1;
//A aa1();//这样显然是不行的,因为编译器分辨不了这到底是对象定义还是函数声明。
A();//但是可以这样使用,这就是一个匿名对象,这样很方便,你不需要取名字。
//匿名对象的声明周期就在该行,到下一行它会自动调用析构函数。
// 匿名对象在这样场景下就很好用,当然还有一些其他使用场景,这个我们以后遇到了再说。
Solution().Sum_Solution(10);
return 0;
}
这里程序还没有结束,但是A()匿名对象到了下一行,会自动调用析构函数。
四.static成员
1.引入:计算类中创建了多少个对象。
int ret=0;
class A
{
public:
A()
{
ret++;
}
A(const A& a)
{
ret++;
}
~A()
{
ret--;
}
};
int main()
{
A a1;
A a2;
A a3(a1);
cout << ret << endl;
return 0;
}
结果:
这样有个弊端,就是很容易我们就能够把ret给修改了,不安全。那我们是否可以把ret定义成成员变量呢?似乎也不行,因为如果定义成成员变量,那么每一个对象都有该成员变量了,而我们希望的是创建出一个对象,++的是同一个变量。
于是这里就引出了static成员。
2.概念
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。
于是我们就把上述的代码修改一下。
class A
{
public:
A()
{
_ret++;
}
A(const A& a)
{
_ret++;
}
~A()
{
_ret--;
}
private:
static int _ret;
};
int A::_ret = 0;//只能在类外初始化
int main()
{
A a1;
A a2;
A a3(a1);
return 0;
}
如果我们要把ret打印出来呢?很显然我们不能直接访问,因为它是私有的,于是我们只有写成静态成员函数的形式访问静态成员。
在上述代码中加上一个静态成员函数即可。
static int GetCount()
{
cout << _ret << endl;
}
3.特性
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区。
- 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明。
- 类静态成员即可用 类名::静态成员 或者 对象.静态成员来访问。
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员。
- 静态成员也是类的成员,受public、protected、private 访问限定符的限制。