友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类
(一)友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
友元函数不是类的成员函数,就相当于你的朋友再亲密也不是你的家人,既然不是类成员函数,那和普通成员函数调用一样,不需要通过对象调用
特征:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
(二)友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
特征:
- 友元关系是单向的,不具有交换性。比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
- 友元关系不能传递,如果C是B的友元, B是A的友元,则不能说明C时A的友元。
- 友元关系不能继承
(三)内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
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;
}
(五)编译器对拷贝构造和构造函数的优化
注意:这个部分取决于编译器,编译器不同操作会有所不同。
在同一个表达式中,构造+构造 / 构造+拷贝构造 / 拷贝构造+拷贝构造会被编译器优化成为调用一个函数:
- 构造+构造->构造
- 构造+拷贝构造->构造
- 拷贝构造+拷贝构造->拷贝构造
下面使用这个类型举一些例子:
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;
}
今天的分享就到这里了,如果,你感觉这篇博客对你有帮助的话,就点个赞吧!感谢感谢……