C++ Classes
Object-Oriented Programming¶
The fundamentals of object-oriented programming have already been covered in the Object-Oriented Programming course, and they will also be briefly reviewed during the lecture. However, if anyone needs to refresh their knowledge from that course, they can visit this link.
Defining Classes¶
Classes are defined using the class
keyword.
In C++, the class definition must be terminated with a semicolon after the closing curly brace (a common mistake is forgetting this semicolon).
1 2 3 |
|
Of course, data members belonging to the class can also be declared here using the already familiar types.
In this material, we follow the Java-style CamelCase
naming convention (where possible): class names start with a capital letter, while method and data member names start with a lowercase letter.
For compound names, we write the words together, capitalizing the beginning of each word.
In C++, using this convention is not an unwritten rule.
It is common to encounter code that follows the so-called snake_case
convention, where spaces are replaced with underscores in identifiers.
1 2 3 4 5 6 7 8 9 |
|
Access Specifiers¶
Access specifiers exist in C++ as well, and their meaning is the same as what you've learned in Java.
However, C++ only has three: public
, private
, and protected
.
There is no package-private visibility (since there are no packages in C++).
Unlike in Java, you don’t need to prefix every member or method with the access keyword — it's enough to specify the desired visibility once, followed by a colon, and then list the members under it.
If you don't specify an access specifier, class members default to private
.
The previously shown code with explicit access specifier:
1 2 3 4 5 6 7 8 9 10 |
|
An access specifier can appear multiple times in a class. However, it’s common practice to group declarations with the same visibility together by the end of development, so each access specifier is used only once (although this is not mandatory).
The following is valid C++ code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Getter/Setter¶
As we've learned before, private
access means that members can only be accessed within the class itself. The protected
access allows access from derived (child) classes as well, while public
provides full, unrestricted access from outside the class.
The core principles are similar to those in Java: ideally, we want to prevent external code from freely modifying the important internal data of an object. Any change to the object's state should happen in a controlled way — via the object itself. (For example, we shouldn't be able to set the age of a Person
object to -23891 or the name of a Course
should not be empty.)
To achieve this, we can define getter and setter functions:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Besides getter and setter methods, of course, we can define any additional methods as needed for our class.
Object Initialization¶
Object initialization in C++ is handled by the constructor.
In addition to assigning initial values, it's common to allocate resources (e.g. dynamic memory) here if any member variables require it.
Constructors can be parameterless (default) or can take any number of parameters.
You can even define multiple constructors with the same number of parameters — as long as the types and their order differ.
This is possible because in C++, functions and methods are distinguished (among other things, more on this later) by their name, number of parameters, and parameter types.
Of course, you still can't have two functions with the exact same signature (same name and parameter list) within the same scope.
Note: for primitive data members, they are NOT initialized to zero by default. If you don't provide an initializer, they may contain invalid values!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
this¶
The this
keyword is used to refer to the current object — it allows access to the object's data members and methods.
It's important to note that this
is a pointer to the object instance (not to the class), so when accessing members through it, you use the ->
operator.
You can also use the (*this).member
syntax, but for cleaner code, this is generally avoided in favor of this->member
.
Using this
is especially useful when a parameter name matches a member variable name. (More use cases will be discussed later.)
1 2 3 4 5 6 7 |
|
Constructor Initializer List¶
In C++, there's a preferred way to initialize member variables in a constructor — using an initializer list.
This is worth learning, as it's the most commonly used approach in real-world constructors, and some member variables can only be initialized this way.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
After the constructor header, we place a colon and then list the member variables along with their initial values in parentheses — these values can come from the constructor’s parameters, for example.
There’s no name conflict here because only class members can appear in the initializer list.
Inside the constructor’s scope, if a parameter shares a name with a member variable, the parameter takes precedence — so a line like name(name)
makes perfect sense: the first name
refers to the member, while the value in parentheses refers to the parameter.
Here’s the same example, but with different names for the member variables and parameters:
1 2 3 4 5 6 7 8 |
|
It's important to note that initializations are always executed in the order of member variable declarations, not in the order they appear in the constructor’s initializer list.
After the initializations, the constructor body is executed.
The following code is therefore incorrect!
To avoid such issues, it's best practice to write the initializer list in the same order as the member declarations.
1 2 3 4 5 6 7 8 |
|
Let’s look at an example of what happens when we try to initialize certain elements without using an initializer list:
- a
const
data member
1 2 3 4 5 6 7 8 9 10 11 |
|
1 2 3 4 5 6 |
|
- Reference
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
1 2 3 4 5 6 7 |
|
Default Member Values¶
Since C++11, it’s possible to assign default values to member variables directly at the point of declaration.
This eliminates the need to set these values in every constructor — especially useful when you want the same default across all constructors.
If you later decide to change the default value, you won’t need to update each constructor individually.
Of course, you can still override the default in a constructor — in that case, the constructor's value takes precedence.
This concept should feel familiar from Java.
1 2 3 4 |
|
Delegating Constructor¶
Sometimes two constructors behave very similarly, with one doing a bit more or something slightly different than the other.
Fortunately, in C++ we can avoid code duplication by calling one constructor from another.
This is similar to the initializer list, but instead of initializing members directly, we call another constructor (and in this case, you cannot initialize members in the constructor’s initializer list).
To do this, simply write the class name followed by parentheses containing the arguments — just like a regular function call.
Note: This pattern can also be used to call the constructor of a base (inherited) class.
1 2 3 4 5 6 |
|
Role of the Default Constructor¶
The purpose of a constructor is to initialize an object, usually using the values provided as parameters.
However, the default constructor initializes the object without receiving any parameters to use for initialization.
This raises the question: why would we want to allow an option to initialize an object with some “default” values (for example, a Student
with an empty name and code)?
For instance, consider the Student
class which normally gets initialized with a name and code, but we also provide a constructor that sets neither:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
This is important because if we want to extend the Course
class to store enrolled Student
s in an array, we would get a compilation error.
If the Student
class does not have a default constructor, we cannot simply create an array of Student
s (unless we explicitly initialize every element).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
When we create an array of, for example, the Student
class, the array elements need to be initialized.
Let's examine how an object is initialized!
Naturally, this happens via the constructor, but the question is: what values should the system provide?
Since the system doesn't know what values to use, it reports an error. However, if there is a default constructor, it can call a constructor that requires no parameters, so every element is initialized properly with the default values.
The root of the problem is that when initializing the array elements, the system cannot guess the intended initialization values. If we resolve this issue by explicitly providing the expected values, then we can create an array without a default constructor. In that case, however, each element must be individually initialized.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
Since arrays can be initialized using curly braces {}, and each object within the array can also be initialized individually using {}, in the example above we explicitly specified the constructor parameters for the elements at index 0 and 1. However, this approach is not recommended, especially when dealing with larger numbers of elements, as it becomes cumbersome and error-prone.
Methods and Functions¶
Default Parameter Values¶
To avoid so-called "boilerplate" (redundant) lines of code, we have the option to provide default values for function parameters. This is especially useful when we want to write very similar functions that differ only in their parameter lists (for example, if we do not specify the maximum capacity, it will default to 25). Instead of copying the same function multiple times (with potential errors each time), we can assign default values to parameters directly in the function declaration by placing an equals sign and the default value after the parameter name.
The most important rule here is that only the last consecutive parameters can have default values. While it’s allowed for all of the last parameters to have defaults, it is not allowed to skip parameters in between (for example, you cannot provide defaults only for the 2nd and 5th parameters). For a comprehensive list of all rules regarding default parameters, check here.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Default method¶
We have already talked about the default constructor.
If we don’t explicitly write one, a default constructor is automatically provided.
However, if we write any other constructor, then no default constructor is generated automatically — in that case, we need to write it ourselves.
If we write an empty constructor, it might be unclear whether we intended it as a default constructor or simply forgot to implement it properly.
Using the default
keyword results in shorter and more readable code.
1 2 3 4 5 6 7 8 9 10 11 |
|
Deleted method¶
Often we need to forbid certain operations for users. In C++, this can be done on a class by using a deleted method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Instantiating Objects¶
Once we have our class defined, with its data members and constructors, we can create instances of the class - in other words, create objects.
There are several ways to do this, but for now, we’ll focus on the simplest method. To create an object, you need the type declaration, followed by the variable name. After that, the constructor parameters go inside parentheses.
If you want to call a parameterless constructor, DO NOT put empty parentheses after the variable name - doing so does not create a variable definition. This phenomenon is called the "Most Vexing Parse". You can read more about it on Wikipedia.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
|
Output
Native programming (IBXYZG-1, max: 25) none (none, max: 1)
Destroying Objects¶
A destructor
is similar to the already familiar constructor, but it is tied to the end of an object's lifetime.
While the constructor runs when the object is created, the destructor runs automatically when the object ceases to exist.
No explicit call is needed — it's entirely automatic.
So far, we've been creating our objects locally.
When the scope in which they were created ends, the destructors of the objects are invoked.
These destructors can perform tasks such as releasing memory that the object reserved, or returning resources the object was using.
Important: Do not confuse this with Java's finalize method!
The notation of a destructor also highlights its similarity to a constructor — it must have the same name as the class, but is preceded by a tilde (~
).
Unlike constructors, a destructor cannot take any parameters, and there can only be one destructor per class.
Example of a destructor and its automatic execution:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
Output
5
This is the last print statement
Your lucky number was: 5
In the example above, it might seem like nothing happens after the last cout
, but when running the program, the final output is:
Your lucky number was: 5
This happens because the object named m
is destroyed at the end of the main
function.
As a result, the destructor is automatically called, and the cout
inside it is executed before the program ends.
This kind of behavior is extremely useful because we often work with objects that manage some kind of resource - such as dynamically allocated memory, network connections, files, etc. When the object is no longer needed, we must ensure these resources are properly released or closed.
A well-known example from C is reading from a file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Aside from the fact that error handling works differently, it’s important to emphasize that files must be closed after use - they must be returned to the system.
While in small examples this might not seem significant, if the program continues running long after the file’s last use, it does matter whether we release the file or continue to occupy it as a resource.
The responsibility lies with the programmer: they must not forget to call the fclose
function.
If we imagine a file-handling class in C++, we can implement logic in its destructor to properly finalize any open operations and close the file, thus returning it to the system.
This way, the programmer doesn't have to worry about it elsewhere — it's guaranteed not to be forgotten, and it only happens when it should: when the file is no longer usable, i.e., when the object is destroyed.
This is one of the advantages of C++'s RAII (Resource Acquisition Is Initialization) principle: the destructor helps us manage resources automatically and safely.
Parameter Passing¶
There are two main categories of parameter passing:
-
Input mode (pass-by-value):
The object is copied, so the original object remains unchanged.
This can be costly in terms of performance. -
Input/output mode (pass-by-reference):
The original object can be modified during the call.
This can be achieved using references, where the called function operates directly on the original object.
(It can also be done using pointers, but that requires more caution and is less safe — we’ll skip pointers for now.)
A reference behaves like the original object, just under a different name and potentially in a different scope.
Since a reference is essentially an alias for another variable, you must immediately specify what it refers to at the time of its declaration.
This is also the reason why reference data members must be initialized in the constructor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
|
Output
Native programming. (IBXYZG-1, max: 25)
Native programming. (IBXYZG-1, max: 29)
As shown in the figure, the modification is performed on the original Course
object passed in, not on a copy.
So, we pass a Course
by reference.
Note that it is not allowed to have two methods that differ only by whether a parameter is passed by value or by reference.
This is because it would be ambiguous — both functions would take a Course
object, so the compiler would not know which function to call.
1 2 |
|
Error:
error: call of overloaded ‘foo(Course&)’ is ambiguous
const¶
The const
keyword allows us to create constant "things," the simplest example being a constant variable. Whenever we want to ensure that the value of a variable does not change, we use const
!
1 |
|
Const Method¶
In getter functions, we can safely say that their purpose is only to query a specific value from our object without making any modifications. In C++, it is possible to guarantee that a given method does not modify the object. As a result:
- It helps the developer by producing a compile-time error if the method tries to change the object's state;
- The user of the class can be sure that no modifications will occur;
- And finally, the method can be used with const objects as well.
We achieve this by adding the const
keyword after the function name:
1 |
|
A more complete example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
At this point, for objects created from Student
, we can be sure that, for example, calling the getName()
method will not modify the object; otherwise, it wouldn’t compile.
This is useful because we know the object remains unchanged after the function call.
An even more useful consequence is that if we create a constant object from Student
, we can still call the function on it.
1 2 3 4 5 |
|
In the case of getter functions, we need to ensure not only that the getter does not modify the object (const
qualifier), but also that the returned value cannot be used to modify the object.
A trivial solution is to return a copy of the value.
In this way, the original value never actually leaves the object, but the downside is that we have to make copies.
In the previous example,
1 |
|
the getter actually creates a copy of the name string and returns it. If this object is large, copying can be a costly operation and generally unnecessary. Instead, we can return a reference. This way, no copying occurs, but others can access (and potentially modify) the returned element, since the reference is essentially another name for the same data.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
Of course, in this case, the returned value could still be used to modify a member of the object.
To prevent this, we can make the returned value constant as well, so we return it without copying, and ensure that the object cannot be modified through the returned element.
Based on this, we can write the Student
class like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
This guarantees that:
- The getter (or any const method) can also be called on constant objects
- Values are returned efficiently without copying
- The returned value cannot be used to modify the object
(If we returned a reference without const in a const method, a compilation error would occur.)
Interpretation of const
and mutable
keywords¶
Often we need to ensure that a method can be called on constant objects, so we must define a const method, but we still want to make some small modification.
Suppose a Course
class has an informational data member.
We can query this information, and naturally, this does not modify the Course
, so we make this a const method.
If we want to increment a counter that tracks how many times the method was queried, for example, for statistical reasons, then:
- either we count in a global variable (which is ugly and incorrect if there are multiple objects),
- or we drop the const method qualifier, so we can store the counts in a data member, but then the method cannot be used on constant objects,
- or we keep the
const
on the method, but mark the data member with themutable
modifier, allowing the method to modify it.
The proper solution is the third point.
The mutable
keyword is used when we want to modify a data member inside a method marked as const
.
This way, the const method that provides information does not cause a compile error when modifying that data member, since we specify that this member does not really modify the object - it can still be considered to have the same constant value.
1 2 3 4 5 6 7 8 9 10 |
|
Using const
reference in parameters¶
In the examples, the parameters are expected as constant string references. Since they are constants, we cannot modify them. But then what is the point of passing parameters by reference here? If the goal is to not modify the parameter, why not use pass-by-value?
For primitive types or smaller classes, passing by value is obviously not a problem.
The parameter is copied onto the stack, and from there we access and use it.
However, for larger objects (e.g., very long strings, complex data types, etc.), copying onto the stack can be time-consuming and can also quickly exhaust the stack space.
Suppose the name and code values are multi-megabyte strings. If we passed them by value:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
In this case, there is one object for the name and one for the code. Since we pass by value, the actual objects' addresses are not passed, nor is it a reference, but the value itself is passed (in this case, the text). This means that many megabytes of data get copied and new objects are created. This causes unnecessary memory usage and needless data copying.
We can avoid this by using a constant reference:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
The name and code still have their own objects, but when calling the function, their values are not copied; instead, they are passed by reference, meaning we refer to the original ones elsewhere.
We can access their values without copying.
The risk that someone might modify the data is handled by using the const
modifier.
Therefore, especially for large objects but generally in all cases, it is better to pass a constant reference than to pass by value, which would create a temporary copy.
Friend Members¶
When we discussed visibility, we saw that C++ is quite strict in this regard: if a class is not a subclass of another, it can only access the public
members and methods of the other class.
However, in C++, it’s also possible to define certain classes as “trusted” — that is, classes (or functions) that may have more control over another class, or at least access to some of its internal workings.
This is where the friend
modifier comes in.
If a class declares another class as a friend
, then that friend class can access the private members of the class.
It's not just classes - any function can also be declared a friend, in which case it gains access to the private members of the class within that function.
Friend members are sometimes seen as a break from the principles of OOP (Object-Oriented Programming), but they are often necessary and practical.
When you add a friend
to your class, that friend gains access to private members - hence the break in encapsulation.
Important: A
friend
is not a member of the class - it is a distinct, privileged external entity.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
|
Read more about friend
keyword here.
Created: 2025-09-04