Friend Functions in C++: Controlled Class Access with a Worked Example
See how a non-member friend function can validate and update two Wallet objects while private balances stay hidden from ordinary callers. Includes runnable C++ and an exact trace.
KnowledgeGate Team
Exam prep & CS education

A transfer operation needs to inspect and update the private balances of two Wallet objects. Making the balances public would discard the class boundary, while two general-purpose setters would expose more mutation than this one operation needs. A friend function gives us a narrower choice: it grants named access, remains a non-member, and lets one transfer change 1200 and 350 into 925 and 625 without opening private data to every caller. The C++ Tutorial: Complete Learning Path in 12 Weeks places this access-control example within the wider language.
Friend functions grant named access, not public access
A friend function is a non-member that a class names with a friend declaration, permitting access to private and protected members. The class grants the privilege. An outside function cannot make itself a friend.
Private members remain inaccessible to ordinary callers. The friend has no implicit object or this pointer and uses ordinary function syntax. See Object Oriented Technology Explained for the surrounding ideas of encapsulation and object interaction. Friendship is neither inheritance nor membership.
"Controlled access" selects a function or class, not fields. Once transfer is a friend, it can access every private and protected Wallet member. Keep it narrow and preserve the invariant.
Read the declaration and call model line by line
The minimal declaration goes inside the class:
friend bool transfer(Wallet& from, Wallet& to, int amount);The definition goes outside the class and does not repeat friend:
bool transfer(Wallet& from, Wallet& to, int amount) {
// Access from.balance_ and to.balance_ here.
}Call transfer(alice, bob, 275), not alice.transfer(bob, 275), because transfer is not a member. Its definition uses from.balance_ and to.balance_; no unqualified balance_ belongs to a hidden this object.
Placing the declaration under private:, protected: or public: does not change its power. Its location affects readability only.
Work a two-wallet transfer from start to finish
The two-wallet transfer uses integers for a simple trace. Integers simplify the trace; they are not production financial modelling advice.
#include <iostream>
class Wallet {
private:
int balance_;
public:
explicit Wallet(int opening) : balance_(opening) {}
int balance() const { return balance_; }
friend bool transfer(Wallet& from, Wallet& to, int amount);
};
bool transfer(Wallet& from, Wallet& to, int amount) {
if (amount <= 0 || &from == &to || from.balance_ < amount) return false;
from.balance_ -= amount;
to.balance_ += amount;
return true;
}
int main() {
Wallet alice{1200};
Wallet bob{350};
std::cout << std::boolalpha;
std::cout << "before: alice=" << alice.balance()
<< ", bob=" << bob.balance() << '\n';
bool first = transfer(alice, bob, 275);
std::cout << "first result: " << first << '\n';
std::cout << "after first call: alice=" << alice.balance()
<< ", bob=" << bob.balance() << '\n';
bool second = transfer(alice, bob, 1000);
std::cout << "second result: " << second << '\n';
std::cout << "after rejected call: alice=" << alice.balance()
<< ", bob=" << bob.balance() << '\n';
}The guard requires a positive amount, different objects and enough source balance. For transfer(alice, bob, 275), all checks pass. Alice becomes 1200 - 275 = 925, Bob becomes 350 + 275 = 625, and the function returns true.
The next call requests 1000. Since 925 < 1000, it returns false before either update. The balances remain 925 and 625.
Observation | Alice | Bob | Result |
|---|---|---|---|
Before | 1200 | 350 | Not called |
After | 925 | 625 |
|
After | 925 | 625 |
|
The public balance() observer can read the values, but only the named friend performs the coordinated mutation.

See why this is controlled access rather than broken encapsulation
Code in main cannot compile alice.balance_ = 0;. The named transfer can update both private balances. It validates first, instead of giving every caller a general setter.
A member such as alice.transferTo(bob, 275) fits the source object. A non-member friend suits a symmetric operation or equal access to two objects. Prefer a non-friend free function when the public interface is sufficient. The broader Coding and DSA path gives context.
Friendship itself does not enforce good logic. A careless friend could write from.balance_ = -400. Access permission and invariant-preserving validation are separate responsibilities.

Apply the friendship rules that questions often hide
Friendship follows three important independence rules:
Not reciprocal: if
Auditoris a friend ofWallet,Walletis not automatically a friend ofAuditor.Not inherited: a class derived from
Auditordoes not automatically receiveAuditor's friendship withWallet.Not transitive: if
Auditoris a friend ofWalletandReportis a friend ofAuditor,Reportis not automatically a friend ofWallet.
The accessed class must grant friendship. It may name a free function, an appropriately declared member of another class, or an entire class.
The friend can access any Wallet passed to it. Friendship relates the function and class, not one runtime object, so one call can change both objects.
Fix the common friend-function traps
Member-style call: the in-class declaration grants access but adds no member. Call
transfer(alice, bob, 275)and expect nothispointer.Access-label confusion: a declaration under
private:is no stronger than one underpublic:. Labels do not alter friendship.Repeated keyword: use
friendin the granting declaration. Define the ordinary non-member function without it.Avoiding interface design: ask whether a public observer or member can express the operation safely. Add a friend only when a named collaborator needs the private representation.
Assuming automatic safety: friendship permits access but does not validate it. Keep checks, updates and failure behaviour together, as the successful
275and rejected1000calls demonstrate.
How exams and interviews test friend functions
A syntax question may ask you to classify friend bool transfer(Wallet&, Wallet&, int);. It is a non-member with private access, not a member, constructor or virtual function. Therefore transfer(alice, bob, 275) is valid, while alice.transfer(bob, 275) is not.
The 275 transfer produces 925, 625. The 1000 request fails before mutation, so the answer stays 925, 625. In true-or-false questions, reciprocal, inherited and transitive friendship are false. Access labels affecting friendship is also false.
Classify each prompt before tracing it: declaration syntax, call form, friendship independence, or output after a guarded mutation. The Coding For Placements course extends the work into C++, OOP and placement-oriented coding practice.
The short version and the next step
Check four facts: the class grants friendship, the friend stays a non-member, ordinary callers still cannot access private state, and the friend must preserve the invariant. The trace is 1200,350 -> transfer 275 -> 925,625; the later request for 1000 is rejected without mutation.
Use the C++ Programming course for a structured next step through C++ concepts, MCQs and coding questions. Then retype this example, remove the friend declaration to observe the private-access error, restore it, and add a test showing that transfer(alice, alice, 100) returns false with the value unchanged.
Keep learning
Templates in C++: Reusable Parameterized Code with Worked Examples
Learn how one C++ template produces type-specific functions and classes. Follow exact calls, calculations, deduction failures, and practical corrections.
Data Types in C++: Fundamental and Derived Types with Worked Examples
Learn how C++ fundamental types differ from arrays, pointers, references, and function types. Trace one complete program, then test the conversion traps.
C++ Constructors and Object Initialization: Techniques, Order and a Worked Trace
Learn how C++ chooses default, parameterised, delegating and copy constructors, then trace member states and initialization order through complete code.
Virtual Functions MCQs: 11 Solved C++ Runtime Polymorphism Questions
Test C++ virtual dispatch with 11 solved exam MCQs, worked explanations and three code traces covering final overriders and safe base-class destruction.





