C++ Programming
Classes, Access, Static Members and Friends
PGCP-AC
A class defines a new type by combining representation with supported operations. Its central purpose is not to hide every field mechanically, but to preserve meaningful conditions while allowing clients to perform valid work. C++ classes support access control, member functions, per-object and shared state, nested types, const operations and selected friendship.
1. Class definition and objects
class Counter {
private:
int value_ = 0;
public:
void increment() {
++value_;
}
int value() const {
return value_;
}
};
A class definition describes a type. An object is one instance of that type and contains its non-static data members. Every Counter object has a separate value_. Calling increment on one object does not change another object.
The terminating semicolon after a class definition is required. Members can be declared in any access section and an access label remains active until another label or the class end.
2. class and struct
Both class and struct can contain data members, functions, constructors, destructors, operators, nested types, templates and inheritance. Their language difference is the default access:
- class members and base inheritance are private by default;
- struct members and base inheritance are public by default.
Use struct for simple value aggregates whose representation is intentionally public. Use class when an invariant requires a controlled interface. This is a convention rather than a feature restriction.
3. Access control
Public members form the interface available to clients. Private members are accessible to the class and its friends. Protected members are accessible to the class, its friends and qualifying derived-class code.
class BankAccount {
public:
void deposit(Money amount);
bool withdraw(Money amount);
Money balance() const;
private:
Money balance_{};
};
Clients cannot assign an arbitrary balance directly. Every change passes through operations that can reject negative deposits and insufficient withdrawals.
Access control is checked during compilation. It does not encrypt memory or protect an object from machine-level inspection.
4. Invariants
An invariant is a condition valid operations preserve throughout the observable lifetime of an object. For BankAccount, balance may be required to remain non-negative. For a Date, month and day must form a valid calendar date.
Constructors establish the invariant. Public operations preserve it. Destruction ends the object's lifetime. Exposing modifiable references or pointers to private representation can bypass the invariant even when the field itself remains private.
An interface should describe domain actions rather than simply provide a getter and setter for every field.
5. Defining members outside the class
Members can be declared inside and defined later:
class Rectangle {
public:
double area() const;
private:
double width_{};
double height_{};
};
double Rectangle::area() const {
return width_ * height_;
}
The scope-resolution operator states that area belongs to Rectangle. The declaration and definition must agree in parameters, return type, const qualification, reference qualification and exception specification where relevant.
Keeping short functions in the class can improve readability. Moving implementation to a source file reduces header dependencies and rebuild cost.
6. The this pointer
Every non-static member function has an implicit current-object pointer named this:
class Counter {
int value_{};
public:
Counter& add(int value) {
this->value_ += value;
return *this;
}
};
It can disambiguate members from parameters and supports returning the current object for chaining. It is not an independently stored data member.
A static member function has no current object and therefore no this pointer.
7. Const member functions
The trailing const declares an operation callable on const objects:
int value() const {
return value_;
}
Inside it, ordinary non-mutable state cannot be modified through this. Const and non-const member functions may form overloads:
char& at(std::size_t index);
const char& at(std::size_t index) const;
This preserves constness: a const object receives read-only access, while a modifiable object can receive a modifiable reference.
Const describes observable mutation through the interface. It does not automatically make another object reached through a pointer immutable.
8. mutable members
A mutable data member may change in a const member function:
class Document {
std::string text_;
mutable std::optional<std::size_t> cached_words_;
public:
std::size_t word_count() const;
};
This supports internal caches, lazy computation and synchronization that do not change the logical value. Mutable should not conceal changes clients would consider part of the object's state.
Thread safety remains necessary. A mutable cache written concurrently needs suitable coordination.
9. Static data members
A static data member belongs to the class rather than each object:
class Connection {
public:
static int active_count();
private:
static int active_;
};
int Connection::active_ = 0;
There is one active_ entity for the class in the program, subject to linkage rules. Ordinary static members traditionally need one out-of-class definition. C++17 inline static data members can be defined in the class:
inline static int active_ = 0;
Static shared state can introduce initialization-order, lifetime and concurrency problems. Use it only when the state genuinely belongs to the type.
10. Static member functions
int Connection::active_count() {
return active_;
}
A static member function is called through the class name and has no this pointer. It can directly access static members, but needs an explicit object to access a non-static member.
Static functions are useful for named factories, class-level utilities closely associated with the type and operations over shared type state. A free function in the same namespace may be better when the operation does not need private access.
11. Static local variables
A local static object has block scope but static storage duration:
int next_identifier() {
static int next = 1;
return next++;
}
Initialization occurs the first time control reaches the declaration and since C++11 that initialization is thread-safe. Subsequent mutation is not automatically thread-safe.
The object lives until normal program termination. Its long lifetime can complicate testing and destruction ordering when it owns other global-lifetime resources.
12. Friend functions
A friend declaration grants selected access:
class Distance {
double metres_{};
friend Distance operator+(Distance, Distance);
};
The friend function remains a nonmember. Friendship gives it access to private and protected members; it does not make it a class member and does not supply a this pointer.
Friend nonmember operators can preserve symmetric conversions because neither operand is privileged as the implicit object. Friendship should support a coherent abstraction, not avoid designing an interface.
13. Friend classes and members
class Inspector;
class Machine {
friend class Inspector;
int internal_state_{};
};
Inspector's members can access Machine's private representation. Friendship is neither reciprocal nor transitive: Machine is not automatically a friend of Inspector and Inspector's friends gain no access to Machine.
A class can grant friendship to one named member function instead of an entire class when declarations are arranged correctly. Narrow grants reduce coupling.
14. Nested types
class List {
struct Node {
int value;
Node* next;
};
Node* head_ = nullptr;
};
A nested class is a member type and obeys access control. It does not automatically carry an enclosing-object pointer as Java inner classes do. Each nested object is an ordinary independent C++ object unless the design stores a reference or pointer to an outer object.
Private nested implementation types prevent clients from depending on internal structure. A public nested type is useful when the type is conceptually part of the enclosing interface.
15. Namespaces and class scope
Namespaces organize related names:
namespace geometry {
class Circle {};
double area(const Circle&);
}
Class scope organizes member names, while namespace scope organizes types and free functions. Argument-dependent lookup can find namespace functions associated with argument types, which is useful for nonmember operators and swap functions.
Avoid broad using directives in headers because they affect name lookup in every including translation unit.
16. Object size and layout
An object normally stores its non-static data members and any implementation-required padding. Static data members are not stored in every object. Member functions are code, not repeated function bodies inside each instance.
Empty class objects still have nonzero size so distinct complete objects can have distinct addresses. Virtual functions can add implementation data such as a virtual-table pointer, though exact layout is implementation-defined.
Do not serialize class objects by copying their raw bytes. Padding, pointers, endianness, lifetime and ABI differences make that representation unsuitable as a portable format.
17. Encapsulation and getters
Encapsulation combines data with the rules governing it. A getter is appropriate when information is part of the abstraction. A setter is appropriate only when arbitrary replacement is a valid domain operation.
Instead of setBalance, an account should offer deposit and withdraw. Instead of exposing a vector by modifiable reference, a collection can offer controlled insertion, removal, iteration or a read-only view.
Small value types can expose values directly when every representation is valid. The goal is meaningful validity, not maximum ceremony.
18. Passing class objects
Pass small value types by value. Pass a large read-only object by const reference to avoid an ordinary copy:
void print_report(const Report& report);
Use a non-const reference for required modification and a pointer when optional access is meaningful. Return values normally express produced objects safely and efficiently.
Do not return references to internal members unless callers are allowed to depend on the owner's lifetime and invalidation rules.
19. Class interface design
A useful class interface:
- establishes a valid object at construction;
- exposes domain operations rather than raw representation;
- states ownership through value, reference, pointer or smart pointer types;
- uses const for observation;
- reports failure consistently;
- minimizes dependencies in public headers;
- avoids global or static mutable state without a clear reason.
The class should be easy to use correctly and difficult to place in an invalid state.
20. Practical considerations
- Class defaults to private access; struct defaults to public access.
- Both class and struct can have constructors and member functions.
- Each object contains its own non-static data members.
- Static data belongs to the class and is shared across instances.
- A static member function has no this pointer.
- A trailing const constrains mutation through current-object access.
- Mutable supports logical constness for carefully selected state.
- Friendship grants access but does not create membership.
- Friendship is neither reciprocal nor transitive.
- Scope resolution selects a name in class or namespace scope.
- An invariant is a condition every valid public operation preserves.
- Public interfaces should expose meaningful supported operations.
Continue learning
Related notes
Put this topic into timed practice
Open mock tests when you want full-exam pacing, or keep drilling in practice mode.