Advanced Concepts of C++ Classes and Objects
Additional Notes on Constructors
Constructor Body Assignment
When creating an object, the compiler calls the constructor to assign starting values to each member variable.
class Calendar {
public:
Calendar(int year, int month, int day) {
m_year = year;
m_month = month;
m_day = day;
}
private:
int m_year;
int m_month;
int m_day;
};
Although members have values after the constructor runs, this process is not initialization — it is assignment. Initialization can only occur once, while you can perform multiple assignments to the same variable inside the constructor body.
The default constructor generated by the compiler does not initialize built-in type members. Default constructors fall into three categories: the compiler-generated default when you write no constructor, a user-defined constructor with no parameters, and a user-defined constructor with all parameters defaulted. For custom type members, the compiler will call their default constructor. If the custom type's default constructor is also compiler-generated, it will again not initialize its own built-in members, leading to all uninitialized values down the chain. What if you want to initialize a custom member without relying on its default constructor?
Example:
class TimePoint {
private:
int m_hour;
};
class Calendar {
public:
Calendar(int year = 1900) {
m_year = year;
}
private:
int m_year;
TimePoint m_time;
};
Compiling this will produce a warning that m_time is uninitialized, because TimePoint uses the compiler-generated default constructor that does not set any value. If you add a constructor to TimePoint that initializes m_hour, the warning disappears. But what if TimePoint has no default constructor, and you want to set a custom value for the TimePoint member?
We have two cases when TimePoint has no default constructor:
Calendardoes not have a user-defined default constructor:
class TimePoint {
public:
TimePoint(int hour) {
m_hour = hour;
}
private:
int m_hour;
};
class Calendar {
private:
int m_year;
TimePoint m_time;
};
This code produces a compile error for Calendar, because when creating a Calendar object, it needs to call TimePoint's default constructor which doesn't exist.
Calendarhas a user-defined constructor:
class TimePoint {
public:
// This is not a default constructor
TimePoint(int hour) {
m_hour = hour;
}
private:
int m_hour;
};
class Calendar {
public:
Calendar(int year) {
m_year = year;
}
private:
int m_year;
TimePoint m_time;
};
This still errors, because before entering the constructor body, m_time must already be constructed, which requires a default constructor that is missing. This problem leads us to the solution: the member initializer list.
Member Initializer Lists
A member initializer list starts with a colon after the constructor signature, followed by a comma-separated list of members, each with its initial value or expression in parentheses.
Example:
class Calendar {
public:
Calendar(int year, int month, int day)
: m_year(year)
, m_month(month)
, m_day(day)
{}
private:
int m_year;
int m_month;
int m_day;
};
You can think of the initializer list as the actual location where member variables are defined. The member declarations inside the class body are just declarations, not definitions.
Going back to the TimePoint and Calendar example, we have three key points:
- If the custom type (TimePoint here) has no default constructor, you must use the initializer list in the containing class's constructor to initialize the custom member.
- If the custom type does have a default constructor, the compiler will automatically use the initializer list to call the default constructor even if you don't write it explicitly.
- You can pass non-default arguments to the custom constructor via the initializer list even when a default constructor exists.
Key rules for initializer lists:
- Each member can only appear once in an initializer list, matching the rule that initialization can only happen once per member.
- The following types of members must be initialized in the initializer list:
- Reference member variables
constmember variables- Custom type members where the custom type has no default constructor
- It is best practice to use initializer lists for all members whenever possible. The only common exception is when initializing in the body improves readability after some preprocessing.
- Initialization order follows the order members are declared in the class body, not the order they appear in the initializer list.
A common test question on this rule:
What is the output of the following code?
A. Output 1 1
B. Program crashes
C. Compilation fails
D. Output 1 followed by a random value
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass(int a)
: m_a1(a)
, m_a2(m_a1)
{}
void print() {
cout << m_a1 << " " << m_a2 << endl;
}
private:
int m_a2;
int m_a1;
};
int main() {
MyClass obj(1);
obj.print();
return 0;
}
Following the fourth rule, m_a2 is declared first so it is initialized first. When initializing m_a2, m_a1 has not been initialized yet, so m_a2 gets a random indeterminate value. Only after m_a2 is done does m_a1 get set to 1. The answer is therefore D.
The explicit Keyword
Constructors that take a single parameter, or have all parameters after the first with default values, can perform implicit type conversion. Adding the explicit keyword to the constructor disables this implicit conversion.
For example, with class Calendar { Calendar(int year); };, you can write Calendar c = 2024;, which works via implicit conversion: the compiler converts the integer 2024 to a temporary Calendar object, which is then used to initialize c. Modern compilers optimize away the intermediate copy, directly constructing c in place. However, if you try Calendar& c = 2024;, this will error: temporary objects are const-qualified, and binding a non-const lvalue reference to a temporary increases access permission (you cannot modify a const temporary via a non-const reference). Adding const to the reference fixes this: const Calendar& c = 2024; works correctly. If you want to prevent implicit conversion entirely, just add explicit to the constructor: explicit Calendar(int year); will cause Calendar c = 2024; to fail compilation.
Static Class Members
Members declared with the static keyword inside a class are called static members. static member variables are static member variables, and static member functions are static member functions. Static member variables must be defined outside the class body.
Key Properties
- Static members are shared across all objects of the class, they do not belong to any individual object, and are stored in the static memory region.
- Static member variables must be defined outside the class; the
statickeyword is only used for the declarasion inside the class, not the external definition. - Static members can be accessed via either
ClassName::memberorobject.member. - Static member functions do not have an implicit
thispointer, so they cannot access any non-static members of the class. - Static members are still class members, so they follow the access rules set by
public/protected/privatespecifiers.
Static member variables get their space allocated during compilation, before any objects of the class are created, and do not contribute to the size of individual class objects.
Common Examples
Count the number of objects of a class created during runtime:
#include <iostream>
using namespace std;
class ObjectCounter {
public:
ObjectCounter() {
s_objectCount++;
}
static void printCount() {
cout << s_objectCount << endl;
}
private:
static int s_objectCount;
};
int ObjectCounter::s_objectCount = 0;
int main() {
ObjectCounter arr[100];
ObjectCounter::printCount();
return 0;
}
Solution for the common "sum 1 to n" problem using static members:
class Accumulator {
public:
Accumulator() {
s_total += s_current;
s_current++;
}
static int getResult() {
return s_total;
}
private:
static int s_current;
static int s_total;
};
int Accumulator::s_current = 1;
int Accumulator::s_total = 0;
class Solution {
public:
int sumToN(int n) {
Accumulator arr[n];
return Accumulator::getResult();
}
};
Friends
Friends are a mechanism that break encapsulation to allow external functions or classes to access private members of a class. They increase coupling and reduce encapsulation, so they should be used sparingly. There are two types: friend functions and friend classes.
Friend Functions
A common use case for friend functions is overloading the << output operator for custom classes. If you overload << as a member function, the implicit this pointer takes the first (left-hand) parameter position, forcing you to write object << cout which is unnatural and breaks output chaining. You therefore need to overload << outside the class, but it needs access to private members, which you can enable by declaring it as a friend in side the class.
Key properties of friend functions:
- Friend functions can access private and protected members of the class, but are not member functions of the class.
- Friend functions cannot be qualified with
const, becauseconstfor member functions modifies thethispointer, which friend functions do not have.- Friend functions can be declared anywhere inside the class, regardless of access specifiers.
- A single function can be a friend to multiple classes.
- Friend functions are called exactly like regular functions.
Friend Classes
All member functions of a friend class can access non-public members of the host class.
Key properties of friend classes:
- Friendship is one-way: if class A declares class B as a friend, B can access A's private members, but A cannot access B's.
- Friendship is not transitive: if C is a friend of B, and B is a friend of A, that does not make C a friend of A.
- Friendship is not inherited, a topic we will cover when discussing inheritance.
Example:
class Time {
friend class Calendar;
public:
Time(int hour = 0, int minute = 0, int second = 0)
: m_hour(hour)
, m_minute(minute)
, m_second(second)
{}
private:
int m_hour;
int m_minute;
int m_second;
};
class Calendar {
public:
Calendar(int year = 1900, int month = 1, int day = 1)
: m_year(year)
, m_month(month)
, m_day(day)
{}
void setTime(int hour, int minute, int second) {
// Directly access private members of Time
m_time.m_hour = hour;
m_time.m_minute = minute;
m_time.m_second = second;
}
private:
int m_year;
int m_month;
int m_day;
Time m_time;
};
Inner Classes
If a class is defined inside another class, the inner class is called an inner class. Inner classes are independent classes that do not belong to the outer class, and you cannot access inner class members via an outer class object. Inner classes are automatically friend classes of the outer class, so they can access all static members of the outer class directly without any extra qualification.
Key Properties
- Inner classes can be defined in
public,protected, orprivatesections of the outer class. - Inner classes can access static members of the outer class directly, no object or class qualifier is needed.
- The size of the outer class is not affected by the inner class, and does not include the size of the inner class.
When creating an instance of an inner class outside the outer class, you need to qualify it with the outer class name, because it exists in the outer class's scope. Example:
#include <iostream>
using namespace std;
class Outer {
public:
Outer(int hour = 0) : m_hour(hour) {}
class Inner {
public:
Inner(int year) : m_year(year) {
cout << s_outerStatic << endl;
}
private:
int m_year;
};
private:
int m_hour;
static int s_outerStatic;
};
int Outer::s_outerStatic = 10;
// Create an inner instance: Outer::Inner obj(2024);
Anonymous Objects
Anonymous objects are objects created without a name. Their lifetime ends immediately after the line they are created on, and they are destroyed once the line finishes executing. They are commonly used for one-off operations or passing temporary objects to functions. For example, MyClass(); creates an anonymous object that is destroyed at the end of the current line.
Compiler Optimizations for Object Copies
When passing or returning objects by value, compilers often optimize away unnecessary copy operations to improve performance. For example, the implicit conversion discussed earlier: the original sequence of "construct temporary + copy construct target" is optimized into directly constructing the target in place. Optimization levels differ between debug and release builds, with release builds performing more aggressive optimizations.
Practice Problems
After learning the content above, you can solve the following common practice problems:
- Convert a date to the cumulative day of the year
- Calculate the difference between two dates
- Print the date given a year and cumulative day
- Calculate the sum of the first N natural numbers