类和对象——友元、内部类和匿名对象

友元、内部类和匿名对象

友元

友元类比现实生活就是房东的每个房间都是一个类,这个房东将房子租给其他租客并给了钥匙,则那个租客可以访问那个房间里的家具等。友元可以理解为这把“钥匙”。

友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度(软件工程中的定义是模块间的紧密程度,模块在这里可以理解为类和非类的成员的函数),破坏了封装,所以友元不宜多用。

友元分为:友元函数友元类

1 友元函数

问题:现在尝试去重载operator<<,然后发现没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置

#include<iostream>
using namespace std;
class Date {
public:
	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
	// d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
	//这种情况就是参数顺序不匹配
	ostream& operator<<(ostream& _cout) {
		_cout << _year << "-" << _month << "-" << _day << endl;
		return _cout;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main() {
	Date d(2025, 1, 1);
	d << cout;//<<重载在Date类内部,cout作为实参只能放在第二个
	return 0;
}

this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。

所以要将operator<<重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。

友元函数可以直接访问类的私有成员,它是定义在类外部普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。

class Date {
	friend ostream& operator<<(ostream& _cout, const Date& d);
	friend istream& operator>>(istream& _cin, Date& d);
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};
//写成全局的函数则可以自由控制顺序
ostream& operator<<(ostream& _cout, const Date& d) {
	_cout << d._year << "-" << d._month << "-" << d._day;
	return _cout;//为了支持连续的流输出
}

istream& operator>>(istream& _cin, Date& d) {
	_cin >> d._year;
	_cin >> d._month;
	_cin >> d._day;
	return _cin;
}
int main() {
	Date d;
	cin >> d;
	cout << d << endl;
	return 0;
}

java最常用的方法是使用函数获取私有变量的数据(getter方法)。比如:

public class A{
    private int a;
    public int geta(){
        return a;
    }
}

因为java需要做到严格的封装,而友元其实是破坏封装的行为。

但java也有其他的方法做到同样的效果,这里不讨论。

c++也可以用java的这一套,只要不嫌麻烦。

说明:

  • 友元函数可访问类的私有和保护成员,但不是类的成员函数
  • 友元函数不能用const修饰。
  • 友元函数可以在类定义的任何地方声明不受类访问限定符限制

类比朋友来我家玩,我家的泳池等可以对朋友开放。

  • 一个函数可以是多个类的友元函数。
  • 友元函数的调用与普通函数的调用原理相同。

c语言的scanfprintf不能很好地支持自定义类型。

一个原因是格式问题,它们不能很好地支持全部的内置类型成员(最直观的感受是所有内置型成员都要设置单独的格式);另一个原因是如何访问访问私有数据,将scanfprintf设置为友元不现实,使用java的getter方法会调用多余的函数。

所以c++需要引入cincout,并将<<重载和>>重载设置为多个标准库里的类的友元函数。

<<实现自动识别的机制是函数重载。

摘自:ostream::operator<< - C++ Reference

请添加图片描述

2 友元类

除了函数,类也可以设置成友元,可以用friend关键字在类中声明。

类比的话两个家庭是两个类,一个家庭可以使用另一个家庭的公共设备比如泳池。

  • 友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
#include<iostream>
using namespace std;
class B {//因为是A类成员函数访问B类私有变量,所以B类先声明A并设置友元
public:
	B(int _b=9)
	:b(_b){ }
	int getb() {
		return b;
	}
private:
	friend class A;//友元和访问限定符无关
	int b;
};

class A {
public:
	void makeb(B& x,int num) {
		x.b = num;
	}
private:
	int a;
};

int main() {
	A a; B b;
	a.makeb(b, 6);
	cout << b.getb() << endl;
	return 0;
}
  • 友元关系是单向的,不具有交换性。

比如上述的A类和B类,A类的成员函数可以访问B类对象。

以及下述的Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。

class Time {
	friend class Date;// 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
public:
	Time(int hour = 0, int minute = 0, int second = 0)
		: _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;
};
  • 友元关系不能传递

如果C类是B类的友元, B类是A类的友元,则不能说明C类是A类的友元。

  • 友元关系不能继承。继承是类和类之间的一种关系。

内部类

概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。

内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。

注意:内部类就是外部类的友元类,即内部类的成员函数可以访问外部类的私有成员,以及通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。

形象解释的话就是房子是一个类,书房是房子的内部类。书房不同于房子的功能,并且书房内的主人可以访问房子内的其他东西,而房子内的园丁、管家等访问书房内的东西可能不允许访问书房。

#include<iostream>
using namespace std;
class A {
public:
	class B {
	public:
		B(int _b=6)
		:b(_b){}
		int geta(A& a) {
			return a.a;
		}
	private:
		int b;
	};
	A(int _a = 7)
		:a(_a) {}
	int geta() {
		return a;
	}
private:
	int a;
};

int main() {
	A::B b;//内部类生成的对象
	A a;
	cout<<b.geta(a)<<endl;//内部类的成员函数访问外部类的私有变量
	cout<<a.geta()<<endl;
	return 0;
}

特性:

  1. 内部类可以定义在外部类的publicprotectedprivate都是可以的。内部类的访问受外部类的访问限定符限制

  2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。

  3. sizeof(外部类)=外部类,和内部类没有任何关系。

class A {
private:
	static int k;
	int h;
public:
	class B {// B天生就是A的友元
	public:
		void foo(const A& a) {
			cout << k << endl;//OK
			cout << a.h << endl;//OK
		}
	};
};
int A::k = 1;
int main() {
	A::B b;
	b.foo(A());

	return 0;
}

两个代码很相似但不一样,下面的类B不受类A的访问限定符影响。

#ifndef _CRT_SECURE_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS 1
#endif

#include<iostream>
using namespace std;
class A {
	friend class B;
private:
	static int k;
	int h;
public:

};
class B {
public:
	void foo(const A& a) {
		cout << a.k << endl;//OK
		cout << a.h << endl;//OK
	}
};
int A::k = 1;
int main() {
	B b;
	b.foo(A());
	return 0;
}

通过内部类,这个OJ求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)还能通过内部类实现:

#include <cmath>
class Solution {
public:
    int Sum_Solution(int n) {
        A* a=new A[n];
        free(a);//析构函数并没有处理static成员
        return sum;
    }
    class A{//利用内部类可以访问外部类的成员的特点
    public:
        A(){
            ++i;
            sum+=i;
        }
    };
    static int i;
    static int sum;
};
int Solution::i=0;
int Solution::sum=0;

匿名对象

类名(上传构造函数的实参列表)表示临时生成一个没有对象名的对象。

匿名对象的特点是生命周期只在所在行,对比有名对象的生命周期在所在的局部({}内)。

因为很多时候生成这个对象就是为了调用这个函数,既然如此,于是就有了绕过生成对象来调用函数的方法。

匿名对象常用于OJ的补全(常见的是Leetcode和Nowcoder)。

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();
	A aa2(2);
	// 匿名对象在这样场景下就很好用(指绕过生成对象调用函数),当然还有一些其他使用场景
	Solution().Sum_Solution(10);
	return 0;
}

匿名对象具有常属性。也就是说不能用匿名对象给有名对象进行拷贝构造。

const+引用会延长匿名对象的生命周期,所以函数的缺省值可以用匿名对象。

const A& ref = A();的生命周期变成了所在局部。

#include<iostream>
using namespace std;
class A {
public:
	A(int _a = 9)
	:a(_a){
		cout << "int _a = 9" << endl;
	}
	A(A& x) 
	:a(x.a){
		cout << "A(A& x) " << endl;
	}
	~A() {
		cout << "~A()" << endl;
	}
	void f1() {
		cout << "void A::f1()" << endl;
	}
	void f2() const{
		cout << "void A::f2()" << endl;
	}
	int geta() const{
		return a;
	}
private:
	int a;
};

void f() {
	const A& a = A();//生命周期延长
	cout << "void f()" << endl;
	//a.f1();//常属性对象不可调用非常属性成员函数
	a.f2();
}

void f2(const A& x=A()) {
	cout << "x.a=" << x.geta() << endl;
}
int main() {
	//A a=A();//匿名对象具有常属性,所以不允许
	f();
	cout << "int main()" << endl;
	return 0;
}

匿名对象的玩法很多。详细见编译器的优化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Darkwanderor

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值