友元、隐式类型转化

发布时间:2024年01月22日

友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类

(一)友元函数

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

友元函数不是类的成员函数,就相当于你的朋友再亲密也不是你的家人,既然不是类成员函数,那和普通成员函数调用一样,不需要通过对象调用

特征:

  1. 友元函数可访问类的私有和保护成员,但不是类的成员函数
  2. 友元函数不能用const修饰
  3. 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  4. 一个函数可以是多个类的友元函数
  5. 友元函数的调用与普通函数的调用原理相同

?(二)友元类

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。

特征:

  1. 友元关系是单向的,不具有交换性。比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
  2. 友元关系不能传递,如果C是B的友元, B是A的友元,则不能说明C时A的友元。
  3. 友元关系不能继承

(三)内部类

概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。


特性:


1. 内部类可以定义在外部类的public、protected、private都是可以的。

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

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

(四)隐式类型转化

class A
{
public:
	A(int a)
		:_a(a)
	{}

private:
	int _a; 
};
int main()
{
	A a1(1);

	A a2 = 3;
	return 0;
}

内置类型对象隐式转换成自定义类型对象,能支持这个转换,是因为有A的需要传一个参数(int)构造函数支持。

如果没有合适的构造函数,则不能进行转换:

class A
{
public:
	A(int a)
		:_a(a)
	{}

private:
	int _a; 
};
int main()
{
	A a2 = 3;
	int* p = nullptr;
	A a3 = p;
	return 0;
}

如果A类具有int*作为参数的构造函数,就可以进行转化:

class A
{
public:
	A(int a)
		:_a(a)
	{}
	A(int* a)
	{}
private:
	int _a;
};
int main()
{
	A a2 = 3;
	int* p = nullptr;
	A a3 = p;
	return 0;
}

并且用于转化产生的临时变量具有常性:

class A
{
public:
	A(int a)
		:_a(a)
	{}

private:
	int _a; 
};
int main()
{
	A a1(1);

	A a2 = 3;
	A& a3 = 3; // 临时变量具有常性
	return 0;
}

3->const A temp(3)-> A& a3 =?const A temp,发生错误。

总结:如果你想要进行隐式类型转化,就需要有合适的构造函数。

如果你不想进行隐式类型转化,可以使用explicit关键字。

explicit

explicit修饰构造函数,禁止类型转换

class A
{
public:
	explicit A(int a)
		:_a(a)
	{}
private:
	int _a; 
};
int main()
{
	A a2 = 3;
	return 0;
}

注意:虽然不能进行隐式类型转化,但如果我们显式地进行转化还是可以进行转化的。

class A
{
public:
	explicit A(int a)
		:_a(a)
	{}
private:
	int _a; 
};
int main()
{
	A a2 = (A)3; // 显式转化
	return 0;
}

隐式类型转化的应用

class A
{
public:
	A(int a)
		:_a(a)
	{}
private:
	int _a; 
};

class Date
{
public:
	Date(int year = 2024, int month = 1, int day = 16)
		:_year(year), _month(month), _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	// 单参数类型转化
	list<A>lt;
	A aa1(1);
	lt.push_back(aa1); // 第一种

	lt.push_back(A(2)); // 第二种

	lt.push_back(3); // 第三种

	// 多参数类型转化
	list<Date>lt1;
	Date d6(2023, 11, 2);
	lt1.push_back(d6); // 第一种

	lt1.push_back(Date(2023, 11, 2)); // 第二种

	lt1.push_back({ 2023,11,2 }); // 第三种
	return 0;
}

(五)编译器对拷贝构造和构造函数的优化

注意:这个部分取决于编译器,编译器不同操作会有所不同。

同一个表达式中,构造+构造 / 构造+拷贝构造 / 拷贝构造+拷贝构造会被编译器优化成为调用一个函数:

  1. 构造+构造->构造
  2. 构造+拷贝构造->构造
  3. 拷贝构造+拷贝构造->拷贝构造

下面使用这个类型举一些例子:

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a)" << endl;
	}
	A(const A& c)
		:_a(c._a)
	{
		cout << "A(A&)" << endl;
	}
private:
	int _a; 
};

  • 构造+拷贝构造->构造

int main()
{
	A aa1 = 1;
	return 0;
}

原先的步骤是:用1构造一个临时对象,再用临时对象拷贝构造aa1。这样就要调用一个构造函数和一个拷贝构造函数两个函数。

但由于编译器的优化,只需要调用一个构造函数即可:

void func(A aa)
{}
int main()
{
	A aa(1); // 构造函数
	func(aa); // 拷贝构造函数
	return 0;
}

这个程序虽然也是先调用构造函数构造出aa对象,再调用拷贝函数进行传参调用func函数,但是编译器却没有“合二为一”。这是因为这两个步骤不在同一个表达式中。

例如,这样就可以“合二为一”:

int main()
{
	func(A(2));
	return 0;
}

或者这样:

int main()
{
	func(3);
	return 0;
}
  • 拷贝构造+拷贝构造->拷贝构造

A func()
{
	A aa; // 构造函数
	return aa; // 拷贝构造
}

int main()
{
	A ret = func(); // 拷贝构造
	return 0;
}

原先需要调用拷贝构造将aa拷贝给一个临时变量,再将这个临时变量用拷贝构造函数构造ret,经过编译器的优化,只调用一个拷贝构造函数:

注意:一个表达式中,连续拷贝构造+赋值重载->无法优化

aa2 = func3(); 和 A aa1 = func3(); 是不一样的:前者是一个赋值行为,后者是一个拷贝构造行为。

  • 构造+拷贝构造->优化为一个构造

void func(A aa) // 传参-拷贝构造
{
	
}

int main()
{
	func(A(2)); // 构造
	return 0;
}

今天的分享就到这里了,如果,你感觉这篇博客对你有帮助的话,就点个赞吧!感谢感谢……

文章来源:https://blog.csdn.net/2201_75479723/article/details/133699153
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。