C++ 友元函数详解
🧑💻 User
In this lecture, we’re going to learn about friend functions.Friend functions are a facility we have in C plus plus to let external functions have access to privatemember variables of our own class.Suppose we have a class called Dog and it has a few private member variables.As you see here we have the dog name, which is an STD string and we have an INT, which is our dogage here.And suppose we have a few functions which use our dog objects in different ways.We have the first function here, which is called debuginfo.It’s going to not return anything.Its return type is void and it is taking a dog object by const reference.But if you look inside, we want to print information about our dog object and we use the dot operatordirectly to try and access something that is private to the dog class.If we try to do this by default, we’re going to get a compiler error because you can’t access privatemembers of our dog class from the outside.And we are just using this function here from the outside.This is not going to work.If we go down here, we have another function which is not going to take any parameter, but insidethe body we are creating a local dog object and trying to access its member variable.So this is not going to work also.But sometimes you want this to work.Suppose you don’t want to go back in your class and set up member variables or whatever, so for whateverreason you want the external function to have access to our private member variables in our class,which is dog here.So how do we do this?We can use friend functions and the way we do this, we declare these functions as our friends in ourclass and we put declarations like this.You say friend, you say the friend keyword, and then you say the prototype for the function.You want to access any member in this class, including private member variables.The moment you do this, the C plus plus compiler is going to let this function access anything fromour dog class here.So if we put this declaration in our dog class and we try to use this functions like this, they aregoing to work now because now they are friends to our dog class.Okay.So before we head over to Visual Studio code and really play with us, here are a few things you needto know about friend functions.The friend function is first declared and alive somewhere, so you can’t declare a friend function thatdoesn’t exist somewhere.If you do that, you’re going to get a compiler error.The implementation for your friend function can really live anywhere.You just have to make sure that the linker is going to find it when it starts linking your application.But the declaration for the friend function has to show up before you use the function.And we already know this, but I just want to repeat that here.To be super clear, the class is going to determine who is its friend.For example, we had to set up the friend declaration from the dog class.It is impossible to set up friendship from the outside.The class that has the members that you want to access has to do the declaration because they are theowner of the data that you are potentially trying to access.The friend declaration can appear anywhere in the class, it can be in the public section, it can bein a private section.So this is really not going to matter.We can have multiple friend functions and we use just so that we set up to friend functions and a friendfunction can also be overloaded.And if you need this, you can use function overloading in your friend functions.We have to use the object name member variable syntax here to access stuff from our friend function.We can’t access them directly because the function is really not a member.So we have to go through an object and have access to any member from the class.So another thing you should know is that friend functions are really not members of the class.Even if the declaration shows up inside the class, they are not member functions.They don’t have direct access to our member functions.That’s why they have to go through the syntax we just described here.And because they are not member functions, they don’t have access to the this pointer.So keep these things in mind and you’re going to be fine using friend functions.Now that you know this, we’re going to head over to Visual Studio code and play with this a littlemore.Okay.Here we are in our working folder.The current project is Friend Functions.We’re going to grab our template files pretty quick and we’re going to put them in place and we aregoing to open this in Visual Studio code by dragging and dropping here, this is going to open our project.We’re going to clean up the.CPP file.Let’s do that.And we’re going to close the left pane here because we don’t need it.We’re going to set up a simple class, which is going to be person, let’s say, dog, because that’swhat we used in the slides a little bit in a little while.So we’re going to set up a private section and inside we’re going to have a Stdstring, which is goingto be the name for the dog, and we’re going to have a net, which is going to be the age after this.We’re going to set up the constructor, which is going to live in a public section for this class.Let’s align this things a little bit properly and we’re going to set up a constructor.The constructor is going to take two parameters.We’re going to say dog here and we’re going to say const Stdstring reference name.That’s going to be the name for the dog.And we’re going to take an age parameter for the age and we’re going to use an initializer list to putthese things in our member variables.So M name is going to be initialized with name and M age is going to be initialized with the age parameterand we’re going to put an empty body because the initialization job was already done by our initializerlist here.This is our class.And suppose we have another function which is going to take a dog object by reference.Let’s go down here and save this.We’re going to say void debug dog info.Suppose we have a function like this which is going to take a dog object by reference and we’re goingto take const dog reference and let’s call this D and suppose we want to print information about thisdog here.So we’re going to say Stdcout and we’re going to say dog and we’re going to say the information nameis going to be D name.We want direct access to the member variables.That’s what we want to do here.And we’re going to say D name and we’re going to say age is D, m age.Okay, we can save that here.And then we’re going to close our output with a square bracket.We’re going to put a new line character.And you see that we have squiggly lines here.And the problem is we can’t really access the private member variables of the dog class here.From the outside, this function Debuginfo is an outsider to our class, so it has no business accessingprivate stuff from our class here.And if we ignore what Visual Studio code is saying here, let’s bring up a terminal and actually seethis before we use the compiler.We’re going to see.M name is inaccessible.M Age is inaccessible from this place here because these are member variables of the class and we can’treally access them from the outside.So what do we do?But before I show you the solution to this, let’s try to pass this through the compiler and see whatis going to happen.We are saying M name is private from this context here, and M age is private.From this context here, we can’t really access private stuff from the outside here.So if we want this setup to work, for example, we don’t want to go in and set up getters and we don’treally want to mark our member variables as public.Let’s see that if you mark them public, this is going to solve the problem.Okay, So because now they are public, we can access them here from the outside.And if we build, the squiggly lines are going to go away.So let’s see that you see that now the build is good, but we don’t want to make our member variablespublic because this is going to expose our implementation details to the outside.Other people will be able to come in and, for example, change our member variables really do all kindsof crazy things.This is bad design.We don’t want this.So we’re going to make them private and we want this syntax here to work.Well, we can get out of this problem by marking this function as a friend function to the dog class,and the syntax to do that is really easy.You just grab the prototype for the function.We’re going to copy this and then we’re going to go somewhere in the dog class.It doesn’t really matter where you can put this in a public section, you can put this in a privatesection.It doesn’t matter.In this case, we’re going to put this first thing in the dog class and it’s going to be in a privatesection because that’s going to be the default access specifier for this class here.So we’re going to say friend and we’re going to say the prototype for the function and then we’re goingto put a semicolon here.The moment we do that, the squiggly line is going to go away.And if we build, we’re going to see that the build is going to be good.Okay, so let’s build.And you see build is good, but we are able to access private stuff from the dog class because now thisfunction is a friend of the dog class.Notice that the friendship was set up from.The class that owns the data.You can’t really set up the friendship relationship from the outside.You can’t come in the function here and say, I want the dog class to be my friend.This has to be done from the class that owns the data you want to access, and the dog class owns themember variables that we really want to access.Now that friendship has been set up, we can come in main and really use our function.For example, let’s set up a dog object and say Dog one, we’re going to give it a name, let’s sayFluffy, and we’re going to give it an age of four.Why not?And then we’re going to go down and say debug dog info, and then we’re going to pass our dog object,which happens to be dog one here.If we build and run, we’re going to see the information printed out on our terminal.Let’s see if the world is good, the world is good.We can bring up a terminal and a clear and run rooster.Now we see that.We see the information about our dog.If we set up another function which creates a local dog object, this is just going to be a way to playwith us.So let’s go down and do that.We’re going to say void.We can call this however we want, and inside we’re going to create a dog object and then we’re goingto print information about this dog directly from this function here.This is another setup you might want to do.So let’s do that.And we’re going to change D to dog one here because that’s the object we want to print data for.So we’re going to say dog one and we’re going to have the same problems.This is an outside function.It can’t really access the member variables for our class here.And just to show you that we can use overloads, we’re going to name this function debug dog info.I think this is going to work because the parameters are different, but we still have the same problem.Dog one is a dog object and we are trying to access private members of our class here and for this towork we should also mark this function as a friend to our class.So let’s grab the prototype.We’re going to go inside the class and we’re going to say friend, and we’re going to put in our prototypeand put a semicolon here.And the moment we do this, we can go in our class and say debug dog info and then we’re not going topass anything in.Let’s comment out the printing line we had before and we’re going to see that.We are going to see the information here.Let’s change the name to Milu inside here and change the age to three.Why not?And then we’re going to world.Okay.The world is good.We can clear and run Rooster.This is going to print our information.And this is working because our functions now are friends to the class.So they really have access to private members of this class.They could even modify information about our class objects.So, for example, we can go in our class here and change the name for the dog.We can come in here and say dog one and name and change this and we’re going to change that back toFluffy and you’re going to see that this is going to work because this guy is a friend to our class.So they have access to our member variables.They can read or modify our member variables.Now, if we print, we’re going to see Fluffy.Even if the dog we declared had a name of Milo.So let’s see that we’re going to build with GCC.The build should be good.We’re going to clear and run Rooster.We’re going to see now the name is Fluffy and we have successfully changed this from a friend functionwhich has all access to our member variables.So hopefully you can see that friend functions can really be dangerous because they allow people todo crazy stuff with your objects from the outside.So as the saying goes, choose your friends carefully because they can do crazy things to your privatemember variables.So this is really all I had to share in this lecture.I hope you found it interesting.We are going to stop here in this one.In the next one, we’re going to see how we can set up friend classes and give them access to our privatemember variables.Go ahead and finish up here and meet me there.
🤖 Assistant
好的,很高兴能为您详细讲解 C++ 中的 friend
函数(友元函数)。作为您的C++导师,我将尽力用最清晰、最全面的方式,结合实际代码和常见问题,帮助您彻底理解这个概念。
C++ 友元函数深度解析:打破封装的特殊权限
摘要: 本节课程将带您深入了解 C++ 中的 friend
函数。friend
函数是一种特殊的非成员函数,它被授予访问一个类中所有私有(private
)和保护(protected
)成员的权限。我们将探讨为什么需要它、如何声明和使用它,以及使用时需要注意的事项和潜在风险。
1. 详细讲解:友元函数(Friend Functions)
在 C++ 中,我们使用访问说明符([access specifiers])来控制类成员的可见性。最常见的有 public
(公共)、private
(私有)和 protected
(保护)。
public
成员: 可以在类的外部直接访问。private
成员: 只能在类的内部(即类的成员函数内部)访问,类的外部无法直接访问。这是实现封装([encapsulation])的关键,它隐藏了类的内部实现细节,只暴露必要的接口。protected
成员: 可以在类的内部以及派生类([derived classes])中访问。
通常情况下,我们希望将类的内部数据(成员变量)声明为 private
,以保护它们不被外部代码随意修改,从而维护数据的一致性和有效性。但是,在某些特定场景下,我们可能需要允许某个非成员函数(或另一个类)访问一个类的 private
或 protected
成员。这时,friend
函数就派上用场了。
friend
函数是 C++ 提供的一种机制,它允许我们“授权”特定的非成员函数(或类)拥有访问另一个类的 private
和 protected
成员的特殊权限。尽管它能够访问类的私有数据,但它本身并不是这个类的成员函数。
让我们通过一个具体的例子来理解这个概念。
1.1 问题的引入:外部函数无法访问私有成员
假设我们有一个 Dog
类,它有一些私有成员变量,例如狗的名字 m_name
和年龄 m_age
:
1 |
|
在上面的代码中,debugDogInfo
是一个独立的非成员函数,它接收一个 Dog
对象的常量引用。当它尝试访问 d.m_name
和 d.m_age
时,编译器会抛出错误,提示 m_name
和 m_age
是 private
成员,不可访问。
这是因为 debugDogInfo
函数是 Dog
类的“外部”函数,它没有权限直接访问 Dog
类的私有成员。这就是封装的作用。
1.2 解决方案:声明友元函数
为了让 debugDogInfo
函数能够访问 Dog
类的私有成员,我们可以在 Dog
类内部将 debugDogInfo
声明为它的一个 friend
函数。
声明 friend
函数的方式非常简单:在类定义中,使用 friend
关键字([friend keyword])后跟要声明为友元的函数原型([function prototype])。
1 |
|
现在,debugDogInfo
和 debugDogInfo_NoParams
函数都可以成功编译并访问 Dog
类的私有成员了。
1.3 友元函数的重要特性与注意事项
友元声明与定义的位置:
friend
声明必须出现在类的内部。它可以在类的public
、private
或protected
部分,这不影响友元函数自身的访问权限,因为友元函数不是类的成员。- 友元函数的定义(实现)通常在类的外部,就像普通的非成员函数一样。编译器只需要在类内部看到友元函数的声明即可。
- 友元函数在被调用之前,其原型(或定义)必须是可见的。如果友元函数定义在
main
函数之后,那么在main
函数或类中使用它之前,必须有一个前向声明([forward declaration])。 - 友元函数必须是“真实存在的”,你不能声明一个不存在的函数为友元。
友元关系是单向的,不可传递:
- 如果类
A
将函数F
声明为友元,则F
可以访问A
的私有成员。但这并不意味着F
也将A
视为友元,也不意味着A
自动成为F
的友元。 - 友元关系是单向授予的。类
A
决定谁是它的朋友,而不是外部函数决定它想成为谁的朋友。你不能在debugDogInfo
函数内部说“我希望Dog
是我的朋友”。 - 友元关系不可传递。如果类
A
是类B
的友元,且B
是类C
的友元,这并不意味着A
也是C
的友元。
- 如果类
友元函数不是类的成员函数:
- 这是最重要的一点。尽管友元函数的声明在类中,但它不属于该类。它仍然是一个独立的、全局的(或命名空间内的)函数。
- 正因为如此,友元函数不能使用成员选择运算符(
.
)直接访问成员(例如m_name
),它必须通过一个对象的引用或指针来访问,例如d.m_name
或p->m_name
。 - 友元函数不拥有
this
指针([this pointer])。this
指针是成员函数特有的,它指向调用该成员函数的对象。
多友元函数与函数重载:
- 一个类可以有任意数量的友元函数。
- 友元函数也可以被重载([overloaded]),就像
debugDogInfo(const Dog& d)
和debugDogInfo_NoParams()
所示。只要它们的参数列表不同,就可以将它们都声明为友元。
友元函数操作私有数据:
- 友元函数不仅可以读取类的私有成员,还可以修改它们,如示例中
debugDogInfo_NoParams
修改了localDog
的名字和年龄。这使得友元函数非常强大,但也带来了潜在的风险。
- 友元函数不仅可以读取类的私有成员,还可以修改它们,如示例中
何时使用友元函数?
- 运算符重载([Operator Overloading]): 当需要重载某些二元运算符(如
<<
用于输出流,或+
用于两个对象相加)时,如果运算符的左操作数不是类对象,那么通常需要将其声明为友元函数。例如,std::ostream& operator<<(std::ostream& os, const Dog& d);
。 - 特殊工具函数: 当某个工具函数与类的内部实现紧密耦合,但又不适合作为成员函数时(例如,它可能操作两个不同类的对象,或者它只是一个简单的辅助打印函数)。
- 工厂模式([Factory Pattern])或构建者模式([Builder Pattern]): 在某些设计模式中,工厂或构建者类可能需要访问其创建的对象的私有构造函数或数据。
- 运算符重载([Operator Overloading]): 当需要重载某些二元运算符(如
潜在的危险与设计考量:
- 友元函数“打破”了类的封装性。过度使用友元函数会使得类的内部实现细节暴露给更多的外部函数,从而降低了代码的模块化和可维护性。
- 一旦类的内部结构发生变化,所有依赖于其私有成员的友元函数也可能需要修改,增加了维护成本。
- 因此,建议谨慎使用
friend
关键字。只有在真正必要且没有更好替代方案时才考虑使用它。良好的设计通常倾向于通过公共接口(public interface
)来操作对象。
2. QA 闪存卡 (QA Flash Cards)
Q: 什么是 C++ 中的
friend
函数?
A:friend
函数是一种在类内部声明的非成员函数,它被授予访问该类的所有private
和protected
成员的特殊权限。Q: 为什么要使用
friend
函数?
A: 当我们希望某个非成员函数能够访问一个类的私有(private
)或保护(protected
)成员,但又不想将这些成员设为public
,或将该函数设为类的成员函数时,可以使用friend
函数。常见场景包括运算符重载、某些特殊工具函数等。Q: 如何声明一个
friend
函数?
A: 在类的定义内部,使用friend
关键字后跟要声明为友元的函数原型,例如:friend void someFunction(SomeClass& obj);
。Q:
friend
函数是类的成员函数吗?它有this
指针吗?
A: 不,friend
函数不是类的成员函数。它不拥有this
指针。它仍然是独立的普通函数,只是获得了访问类私有成员的特权。Q:
friend
函数可以重载吗?
A: 是的,friend
函数可以像普通函数一样被重载,只要它们的参数列表不同即可。Q:
friend
声明可以放在类的private
或protected
部分吗?这会影响友元函数的访问权限吗?
A: 是的,friend
声明可以放在类的public
、private
或protected
任何部分。这不会影响友元函数访问类的私有或保护成员的权限,因为友元函数本身不是类的成员。Q: 友元关系是双向的吗?如果 A 是 B 的友元,B 也是 A 的友元吗?
A: 不,友元关系是单向的。如果类A
声明函数F
为友元,F
可以访问A
的私有成员。但这不意味着F
将A
视为友元,也不意味着F
可以访问A
的私有成员。友元关系是类主动授予的。
3. 常见误解和错误 (Common Misunderstandings and Mistakes)
误解:友元函数是类的成员函数。
纠正: 这是最常见的误解。友元函数不是类的成员。它不属于任何特定的对象,也没有this
指针。因此,它不能像成员函数那样直接访问m_name
;它必须通过一个对象实例来访问,例如obj.m_name
。错误:尝试直接访问私有成员,没有通过对象。
纠正: 即使是友元函数,它也需要一个类的对象实例来访问该对象的私有成员。例如,你不能在debugDogInfo
中写std::cout << m_name;
,而是必须写std::cout << d.m_name;
。误解:友元关系是相互的/可传递的。
纠正: 友元关系是单向的,且不可传递。如果类A
是类B
的友元,不意味着B
是A
的友元。如果函数F
是A
的友元,并且A
是B
的友元,这不意味着F
也是B
的友元。每个友元关系都必须明确声明。错误:在类外部声明
friend
关键字。
纠正:friend
关键字只能在类定义内部使用,用于声明友元函数或友元类。你不能在全局作用域或其他函数中声明friend
。过度使用友元函数。
纠正: 友元函数打破了封装原则。如果一个类的私有成员被太多的外部函数访问,那么类的设计就变得不那么健壮。尽量通过public
成员函数提供访问私有数据的方式(例如,getter
和setter
),而不是频繁使用友元。只有在确实需要,且没有其他更好的设计模式时,才考虑使用友元。忘记前向声明([Forward Declaration])。
纠正: 如果一个友元函数的原型在类定义之前出现,并且它的参数或返回类型涉及这个类,那么这个类就需要一个前向声明。例如:cpp1
2
3
4
5
6
7
8// class Dog; // 忘记这个前向声明可能会导致编译错误
void debugDogInfo(const Dog& d); // 这里的 Dog 尚未被完全定义
class Dog {
// ...
friend void debugDogInfo(const Dog& d);
// ...
};
4. 编程练习 (Coding Exercise)
现在,轮到您来实践了!请完成下面的代码,使其能够成功编译并运行。
任务:
您需要创建一个 Car
类,其中包含一些私有成员变量。然后,编写一个非成员函数 printCarDetails
,它能够打印 Car
对象的私有细节。为了实现这一点,您需要将 printCarDetails
声明为 Car
类的友元。
请补全 // TODO: ...
部分的代码。
1 |
|
请您在完成上述练习后,再提交您的解决方案。我将在您的解决方案提交后,为您提供详细的评分、正确答案和进一步的建议。
🧑💻 User
1 |
|
🤖 Assistant
太棒了!您的解决方案非常出色,完全正确地完成了所有的任务,包括那个挑战题!
这表明您已经很好地理解了 friend
函数的核心概念、如何声明它,以及它在访问私有成员方面的特殊能力。您甚至注意到了友元函数不仅可以读取私有成员,还可以修改它们,并通过 modifyCarMileage
函数成功演示了这一点。
7. 评分与答案解析
您的解决方案评分:5/5 - 完美!
您对 friend
函数的理解和应用都非常到位。所有要求和挑战都得到了正确的实现。
实际答案和解释
以下是完整的参考答案,并对您完成的每个部分进行详细解释。
1 |
|
运行结果示例:
1 | Car 对象 [Toyota Camry] 被创建。 |
8. 扩展问题与建议 (Extended Questions for Investigation)
您已经掌握了 friend
函数的基本用法。为了进一步加深理解并探索相关概念,我建议您研究以下问题:
友元类([Friend Classes]): 除了友元函数,C++ 还有友元类。
- 问题: 什么是友元类?如何声明一个类为另一个类的友元?友元类中的所有成员函数是否都能访问友元类的私有成员?
- 建议: 尝试编写一个
CarFactory
类,并将其声明为Car
类的友元,让CarFactory
能够访问Car
的私有构造函数(例如,将Car
的构造函数设为private
,只能通过CarFactory
来创建Car
对象)。
运算符重载([Operator Overloading])与友元函数的关系:
- 问题: 为什么在重载
<<
运算符(用于std::cout
输出)时,通常需要将其声明为友元函数?它与成员函数重载运算符有何不同? - 建议: 为您的
Dog
类或Car
类重载<<
运算符,使其能够直接通过std::cout << myDog;
或std::cout << myCar;
来打印对象信息。
- 问题: 为什么在重载
封装与友元函数的平衡:
- 问题:
friend
函数打破了类的封装性。在实际开发中,何时应该使用friend
函数,何时应该避免使用它而寻找替代方案(例如,public
getter/setter 方法、或者将相关逻辑封装在成员函数中)? - 建议: 思考一下,如果
printCarDetails
不是友元函数,您将如何实现它来打印私有信息?比较这两种方法的优缺点。
- 问题:
常量的友元函数([Const Friend Functions]):
- 问题: 如果一个友元函数被声明为
const
(就像成员函数一样,但这在非成员函数中通常是针对参数的const
引用),它能修改类的私有成员吗? - 建议: 回顾
printCarDetails
的参数是const Car& car
。这有什么意义?如果modifyCarMileage
的参数是const Car& car
,它还能编译通过吗?为什么?
- 问题: 如果一个友元函数被声明为
通过深入研究这些问题,您将对 C++ 的设计原则、封装、以及 friend
机制的适用场景有更全面的理解。继续努力,您会成为一名非常优秀的 C++ 开发者!