• 【C++基础】(6)类和对象(下):友元,内部类,初始化列表,静态成员



    紧接上一篇👉: 【C++】(5)类和对象练习,日期类的实现,运算符重载

    友元

    友元函数

    要想实现流插入<<运算符的重载,首先简单看一下下图。

    img

    原来cout其实是一个对象,它的类型为ostream。一个<<有两个操作数,那么我们也可以把它作为运算符重载的一个参数。

    如果重载<<写在类里面:

    void operator<<(std::ostream& out)
    {
        out << _year << "-" << _month << "-" << _day << endl;
    }
    
    • 1
    • 2
    • 3
    • 4

    调用:

    因为this指针默认是第一个参数,所以d要写在cout前面。

    void test4()
    {
    	Date d(1919, 8, 10);
    	d << cout;
    }
    //结果:1919-8-10
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    👆:程序跑起来是没什么问题,但是这个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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    但是全局的函数在类外,访问不了类里面的私有成员。

    我们可以把它设置为类的==友元函数==:

    友元函数特性

    • 友元函数需要在类内声明,声明时要加friend关键字
    • 友元函数可以在类的任意地方声明,不受访问限定符的限制
    • 友元函数可以直接访问类内的私有和保护成员,它是定义在类外的普通函数,不属于任何类
    class Date
    {
    	friend std::ostream& operator<<(std::ostream& out, const Date& d); //友元函数声明
    public:
        //略...
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这样我们的调用就可以写成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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    友元类

    一个类可以是另一个类的友元,友元类可以访问另一个类的私有成员。

    • 友元关系不具有交换性,比如下述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;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    内部类

    如果一个类定义在另一个类的内部,那么这个类就叫做内部类

    • 内部类天生就是外部类的友元。
    • 内部类受外部的访问限定符的限制。
    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;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    严格来说,函数体内初始化并不能叫做初始化,应该叫赋初值,因为初始化只有一次,而函数体内可以多次赋值。对于必须在定义时就初始化的类型,比如引用,函数体内就写不了了。

    初始化列表

    使用方法

    初始化列表:以一个冒号:开始,接着是由逗号,分隔的数据成员列表,每个成员变量后跟一个括号,括号内为初始值或表达式。

    //初始化列表初始化
    Date(int year = 1, int month = 1, int day = 1)
    	:_year(year)
    	, _month(month)
    	, _day(day)
    {}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    初始化列表才是对象里的成员定义的地方,编译器自己生成的默认构造函数也有初始化列表,用来定义成员变量,这也是为什么它对内置类型不处理(随机值),而自定义类型会调用它的默认构造函数。

    C++11支持在成员变量声明处给缺省值,给的缺省值其实就是给初始化列表用的。


    注意

    1. 每个成员变量在初始化列表中只能出现一次(初始化只能有一次)
    2. 以下三类必须在初始化列表初始化:引用成员变量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; //无默认构造的自定义类型
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    👆: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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    答案: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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    对于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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    静态成员函数

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    👆:因为没有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<n200

    进阶: 空间复杂度 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();
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
  • 相关阅读:
    贷中存量客户的价值挖掘与分类实现,试试这一重要的场景模型
    Mybatis中使用${}和使用#{}
    CORBA 架构体系指南(通用对象请求代理体系架构)​
    解决2K/4K高分屏下Vmware等虚拟机下Kail Linux界面显示问题
    信息安全:使用程序编写基于密钥的加密方式
    Hadoop简明教程
    nodeJs 基础
    网络安全(黑客)—2024自学
    Opencv源码解析(2)算法
    【C++】C++面向对象编程三大特性之一——继承
  • 原文地址:https://blog.csdn.net/CegghnnoR/article/details/125434337