Table of Contents
- Origins of the Class Keyword
- Class Definitions
- Access Specifiers and Encapsulation
- Memory Layout of Classes
- The this Pointer
- Constructors (Implicitly Generated Function 1/6)
- Destructors (Implicitly Generated Function 2/6)
- Copy Constructors (Implicitly Generated Function 3/6)
- Assignment Operator Overloading (Implicitly Generated Function 4/6)
- The const Keyword with Classes
- Address-of Operators (Implicitly Generated Functions 5/6 and 6/6)
- Initializer Lists
- Implicit Type Conversions
- Static Members
1. Origins of the Class Keyword
In C, structs are limited to containing only variables. C++ extended struct capabilities to also contain functions. To differentiate from C-style structs, Bjarne Stroustrup introduced the class keyword as an alternative to struct for defining custom types.
2. Class Definitions
When both declaration and definition are placed within the class body, be aware that member functions defined inside the class may be treated as inline functions by the compiler.
3. Access Specifiers and Encapsulation
C++ implements encapsulation by bundling an object's attributes and behaviors (functions) together within a class, allowing selective exposure of interfaces through access modifiers.
publicmembers can be accessed directly from outside the classprotectedandprivatemembers cannot be accessed directly from outside the class- Access scope begins at the specifier and continues until the next specifier or the class closing brace
- Without a subsequent specifier, the scope extends to the closing brace
- Default access for
classisprivate, whilestructdefaults topublic
Important: Access specifiers only function at compile time. Once data is mapped to memory, there is no distinction between them.
Differences between struct and class:
C++ maintains backward compatibility with C, so struct can still be used as a traditional structure. However, struct in C++ can also define classes with default public access, whereas class defaults to private. Additionally, there are differences in inheritance and template parameter contexts.
4. Memory Layout of Classes
Empty classes have special sizing: the compiler allocates 1 byte to uniquely identify objects of that class.
5. The this Pointer
- The
thispointer type is a const pointer to the class type, such asStudent* const, meaning the pointer itself cannot be reassigned—this is a safety design. thiscan only be used within member functionsthisis essentially a parameter to the member function, passed implicitly when an object invokes the functionthisis the first implicit parameter, typically passed automatically by the compiler via theecxregister
6. Constructors (Implicitly Generated Funcsion 1/6)
A class can implicitly generate six special member functions if not explicitly defined by the programmer. The constructor is the first.
Constructors are special member functions with an important distinction: their primary purpose is not to allocate space, but to initialize objects.
Characteristics:
- Function name matches the class name
- No return type
- Automatically called when an object is instantiated
- Can be overloaded
- If no constructor is explicitly defined, the compiler generates a parameterless default constructor; once explicitly defined, the compiler stops generating one
class Calendar
{
public:
/*
Calendar(int year, int month, int day)
{
_yearValue = year;
_monthValue = month;
_dayValue = day;
}
*/
void Display()
{
std::cout << _yearValue << "-" << _monthValue << "-" << _dayValue << std::endl;
}
private:
int _yearValue;
int _monthValue;
int _dayValue;
};
int main()
{
// Works without explicit constructor—compiler provides default
Calendar cal1;
return 0;
}
-
You might wonder: the compiler-generated default constructor appears useless. In the example above,
cal1's members remain uninitialized (indeterminate values). C++ categorizes types into built-in types (primitives likeint,char) and user-defined types (classes, structs, unions). The default constructor calls constructors for user-defined type members—if it didn't exist, when would those constructors be invoked? -
Both parameterless constructors and constructors with all default parameters are called default constructors, but only one default constructor can exist per class. Note: parameterless construcotrs, all-default constructors, and compiler-generated constructors are all considered default constructors.
7. Destructors (Implicitly Generated Function 2/6)
Destructors do not destroy objects—local object storage is managed by the compiler. Destructors allow programmers to clean up resources held by objects.
When called:
When an object's lifetime ends, the C++ runtime automatically calls the destructor to release resources, then the compiler deallocates the object's memory.
Default destructor behavior:
Calls destructors for user-defined types, performs no action on built-in types.
8. Copy Constructors (Implicitly Generated Function 3/6)
What Problem Does the Copy Constructor Solve?
Consider a calendar class with three member variables, a default constructor with default arguments, and a display function:
class Calendar
{
public:
Calendar(int yearVal = 2000, int monthVal = 1, int dayVal = 1)
{
_yearValue = yearVal;
_monthValue = monthVal;
_dayValue = dayVal;
}
void Display()
{
std::cout << _yearValue << "/" << _monthValue << "/" << _dayValue << std::endl;
}
private:
int _yearValue, _monthValue, _dayValue;
};
void process1(Calendar cal)
{
cal.Display();
}
int main()
{
Calendar cal1(2023, 11, 26);
process1(cal1);
}
Calling process1 from main directly copies the argument.
However, consider a class that allocates dynamic memory:
class Array
{
public:
Array(size_t capacity = 3)
{
std::cout << "Array(size_t capacity)" << std::endl;
_data = (int*)malloc(sizeof(int) * capacity);
if (_data == nullptr)
{
std::perror("malloc allocation failed");
}
_capacity = capacity;
_size = 0;
}
~Array()
{
std::cout << "~Array()" << std::endl;
free(_data);
_capacity = _size = 0;
_data = nullptr;
}
private:
int* _data;
int _capacity, _size;
};
void process2(Array arr)
{
// Function body
}
int main()
{
Array arr1(5);
process2(arr1);
}
This code causes problems:
The issue is that process2 receives the argument by value, which performs a simple copy. The parameter's _data pointer points to the same memory as the argument's _data. This copying is called a shallow copy. When process2 returns, the parameter is destroyed, triggering its destructor which frees that memory—now both pointers point to freed memory (dangling pointers).
Copy constructors were introduced to solve this problem. Some user-defined types can use the default copy constructor, which performs shallow copying (for C compatibility). For instance, the Calendar class's default copy constructor doesn't simply copy bytes like C would; it invokes the copy constructor. But some types require custom implementations—like Array with pointer members, which need to allocate new memory. This is called deep copying.
Why Are Copy Constructor Parameters Const Rfeerences?
- If the copy constructor used pass-by-value:
class Calendar
{
public:
Calendar(int yearVal = 2000, int monthVal = 1, int dayVal = 1)
{
_yearValue = yearVal;
_monthValue = monthVal;
_dayValue = dayVal;
}
Calendar(Calendar original)
{
_yearValue = original._yearValue;
_monthValue = original._monthValue;
_dayValue = original._dayValue;
}
void Display()
{
std::cout << _yearValue << "/" << _monthValue << "/" << _dayValue << std::endl;
}
private:
int _yearValue, _monthValue, _dayValue;
};
void process1(Calendar cal)
{
cal.Display();
}
int main()
{
Calendar cal1(2023, 12, 1);
Calendar cal2(cal1);
}
In main, creating cal2 from cal1 calls the copy constructor. But before the copy constructor body executes, cal1 must be passed as an argument to the parameter original—which triggers another call to the copy constructor, creating infinite recursion.
- Using references avoids this:
Calendar(Calendar& original)
{
_yearValue = original._yearValue;
_monthValue = original._monthValue;
_dayValue = original._dayValue;
}
A reference is an alias—no copying needed, so the function executes successfully.
- Adding
constprevents accidental modification:
Calendar(const Calendar& original)
{
_yearValue = original._yearValue;
_monthValue = original._monthValue;
_dayValue = original._dayValue;
// Cannot modify original here
}
9. Assignment Operator Overloading (Implicitly Generated Function 4/6)
Function name:
operator=
Operator Overloading
For user-defined types, whether operations are needed between two objects and how they work should be determined by the programmer. The assignment operator is the fourth implicitly generated function—default implementation performs shallow copying.
For a calendar class, if not explicitly implemented, the class generates a default assignment operator:
int main()
{
Calendar d1(2023, 12, 3);
Calendar d2(2020, 1, 1);
d2 = d1; // Assignment via operator=
}
Implementation details depend on class requirements. Below is a complete calendar class example.
Calendar Class Implementation
Calendar.h
#pragma once
#include <iostream>
#include <cassert>
using namespace std;
class Calendar
{
public:
Calendar(int yearVal = 2023, int monthVal = 1, int dayVal = 1);
int GetDaysInMonth(int yearVal, int monthVal);
Calendar(const Calendar& other);
void Display();
bool operator==(const Calendar& other) const;
bool operator!=(const Calendar& other) const;
bool operator>(const Calendar& other) const;
bool operator>=(const Calendar& other) const;
bool operator<(const Calendar& other) const;
bool operator<=(const Calendar& other) const;
Calendar& operator=(const Calendar& other);
Calendar& operator+=(int days);
Calendar operator+(int days);
Calendar& operator++();
Calendar operator++(int);
Calendar& operator-=(int days);
Calendar operator-(int days);
Calendar& operator--();
Calendar operator--(int);
int operator-(const Calendar& other) const;
friend ostream& operator<<(ostream& os, const Calendar& cal);
friend istream& operator>>(istream& is, Calendar& cal);
private:
int _yearValue, _monthValue, _dayValue;
};
Calendar.cpp
#include "Calendar.h"
Calendar::Calendar(int yearVal, int monthVal, int dayVal)
{
_yearValue = yearVal;
_monthValue = monthVal;
_dayValue = dayVal;
if (_yearValue < 1 || _monthValue < 1 || _monthValue > 12 ||
_dayValue > GetDaysInMonth(_yearValue, _monthValue))
{
Display();
assert(0);
}
}
Calendar::Calendar(const Calendar& other)
{
_yearValue = other._yearValue;
_monthValue = other._monthValue;
_dayValue = other._dayValue;
}
int Calendar::GetDaysInMonth(int yearVal, int monthVal)
{
assert(yearVal >= 1 && monthVal >= 1 && monthVal <= 12);
int days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if (monthVal == 2 && ((yearVal % 4 == 0 && yearVal % 100 != 0) || (yearVal % 400 == 0)))
{
return 29;
}
return days[monthVal];
}
void Calendar::Display()
{
cout << _yearValue << "/" << _monthValue << "/" << _dayValue << endl;
}
bool Calendar::operator==(const Calendar& other) const
{
return _yearValue == other._yearValue && _monthValue == other._monthValue && _dayValue == other._dayValue;
}
bool Calendar::operator!=(const Calendar& other) const
{
return !(*this == other);
}
bool Calendar::operator>(const Calendar& other) const
{
if (_yearValue > other._yearValue)
return true;
if (_yearValue == other._yearValue && _monthValue > other._monthValue)
return true;
if (_yearValue == other._yearValue && _monthValue == other._monthValue && _dayValue > other._dayValue)
return true;
return false;
}
bool Calendar::operator>=(const Calendar& other) const
{
return *this > other || *this == other;
}
bool Calendar::operator<(const Calendar& other) const
{
return !(*this >= other);
}
bool Calendar::operator<=(const Calendar& other) const
{
return !(*this > other);
}
Calendar& Calendar::operator=(const Calendar& other)
{
if (this != &other)
{
_yearValue = other._yearValue;
_monthValue = other._monthValue;
_dayValue = other._dayValue;
}
return *this;
}
Calendar& Calendar::operator+=(int days)
{
_dayValue += days;
while (_dayValue > GetDaysInMonth(_yearValue, _monthValue))
{
_dayValue -= GetDaysInMonth(_yearValue, _monthValue);
_monthValue++;
if (_monthValue == 13)
{
_yearValue++;
_monthValue = 1;
}
}
return *this;
}
Calendar Calendar::operator+(int days)
{
Calendar temp(*this);
temp += days;
return temp;
}
Calendar& Calendar::operator++()
{
*this += 1;
return *this;
}
Calendar Calendar::operator++(int)
{
Calendar temp(*this);
*this += 1;
return temp;
}
Calendar& Calendar::operator-=(int days)
{
_dayValue -= days;
while (_dayValue <= 0)
{
--_monthValue;
if (_monthValue == 0)
{
--_yearValue;
_monthValue = 12;
}
_dayValue += GetDaysInMonth(_yearValue, _monthValue);
}
return *this;
}
Calendar Calendar::operator-(int days)
{
Calendar temp(*this);
temp -= days;
return temp;
}
Calendar& Calendar::operator--()
{
*this -= 1;
return *this;
}
Calendar Calendar::operator--(int)
{
Calendar temp(*this);
*this -= 1;
return temp;
}
int Calendar::operator-(const Calendar& other) const
{
int sign = 1;
Calendar max = other;
Calendar min = *this;
if (max < min)
{
max = *this;
min = other;
sign = -1;
}
int count = 0;
while (max != min)
{
++count;
++min;
}
return count * sign;
}
ostream& operator<<(ostream& os, const Calendar& cal)
{
os << cal._yearValue << "年" << cal._monthValue << "月" << cal._dayValue << "日" << endl;
return os;
}
istream& operator>>(istream& is, Calendar& cal)
{
is >> cal._yearValue >> cal._monthValue >> cal._dayValue;
return is;
}
Overloading Stream Operators
Attempting to implement << as a member function like other operators fails:
// Declaration in header
ostream& operator<<(ostream& out);
// Implementation
ostream& Calendar::operator<<(ostream& out)
{
out << _yearValue << "年" << _monthValue << "月" << _dayValue << "日" << endl;
return out;
}
// Usage
Calendar cal(2023, 12, 4);
cout << cal; // Interpreted as operator<<(cout, cal)
This fails to compile.
C++ rules state: For binary operators, the first parameter is the left operand, the second is the right operand. In cout << cal, cout is the left operand and should be the first parameter, but the implicit this pointer occupies the first parameter position, causing a mismatch.
Therefore, << and >> must be implemented as non-member functions with friend access:
// Declaration
friend ostream& operator<<(ostream& out, const Calendar& cal);
friend istream& operator>>(istream& in, Calendar& cal);
// Implementation
ostream& operator<<(ostream& out, const Calendar& cal)
{
out << cal._yearValue << "年" << cal._monthValue << "月" << cal._dayValue << "日" << endl;
return out;
}
istream& operator>>(istream& in, Calendar& cal)
{
in >> cal._yearValue >> cal._monthValue >> cal._dayValue;
return in;
}
Why Did C++ Add << and >>?
C++ introduced object-oriented concepts. C uses printf for formatted output, but user-defined types cannot be formatted. C++ introduced cout and cin as stream objects to handle output/input uniformly.
Operator Overloading Guidelines
- Cannot create new operators by combining symbols: e.g., cannot create
operator@ - Overloaded operators must have at least one class-type parameter
- Built-in operator meanings cannot be changed: e.g., the meaning of
+for integers cannot be altered - When overloaded as member functions, parameters appear one fewer because
thisis implicit - The following five operators cannot be overloaded:
.*,::,sizeof,?:,.
10. The const Keyword with Classes
Consider this code:
const Calendar cal(2023, 12, 5);
cal.Display();
This fails to compile:
When Display is called, the this pointer receives &cal with type const Calendar*, but this has type Calendar* const—this is an unsafe permission widening. The solution is to mark the member function as const, changing this to const Calendar* const:
void Calendar::Display() const
{
cout << _yearValue << "/" << _monthValue << "/" << _dayValue << endl;
}
// If declaration and definition are separated, const is needed in both places
Summary:
- const objects cannot call non-const member functions (permission widening)
- const member functions solve this—both const and non-const objects can call them
- If a function's implementation doesn't modify any member variables, mark it const
bool operator==(const Calendar& other) const;
bool operator!=(const Calendar& other) const;
bool operator>(const Calendar& other) const;
bool operator>=(const Calendar& other) const;
bool operator<(const Calendar& other) const;
bool operator<=(const Calendar& other) const;
11. Address-of Operators (Implicitly Generated Functions 5/6 and 6/6)
// Regular address-of operator
Calendar* operator&()
{
return this;
}
// const version
const Calendar* operator&() const
{
return this;
}
These operators rarely need overloading—use the compiler-generated versions unless special behavior is needed, such as returning specific content.
12. Initializer Lists
Consider a calendar class with a reference member:
private:
int _yearValue, _monthValue, _dayValue;
int& _ref;
Writing the constructor this way fails:
Calendar::Calendar(int yearVal, int monthVal, int dayVal)
{
_yearValue = yearVal;
_monthValue = monthVal;
_dayValue = dayVal;
_ref = _dayValue;
}
Compilation fails because references must be initialized at the point of definition.
Attempting this also doesn't work:
private:
int _yearValue, _monthValue, _dayValue;
int& _ref = _dayValue; // All objects share the same reference
Member variables declared in the class are declarations, not definitions. The class is a type specification; the assignment here is just a default value. True initialization can only happen once. Constructor body assignments are not initialization—they're assignments that can happen multiple times.
Objects are actually defined when constructed, allocating the object's memory. If each object needs its own reference, there must be a separate place where references are defined.
Initializer lists:
class Helper
{
public:
Helper(int x = 10) { _value = x; }
private:
int _value;
};
class Calendar
{
private:
int _yearValue, _monthValue, _dayValue;
int& _ref;
const int _constVal;
Helper _helper;
public:
Calendar(int yearVal, int monthVal, int dayVal)
: _ref(_dayValue)
, _constVal(1)
, _helper(20)
{
_yearValue = yearVal;
_monthValue = monthVal;
_dayValue = dayVal;
}
};
The name "initializer list" is appropriate because when an object is defined, the constructor is called, and the initializer list performs actual initialization of members. If not explicitly written, built-in types get indeterminate values and user-defined types get their default constructors called.
What Problems Do Initializer Lists Actually Solve?
First problem:
MyQueue has two user-defined type members, but Stack has no default constructor:
class Stack
{
public:
Stack(int x) { _capacity = x; }
private:
int _capacity;
};
class MyQueue
{
public:
MyQueue() {}
private:
Stack _stack1, _stack2;
};
Compilation fails. Initializer lists solve this: initializing members that are user-defined types without default constructors.
Second problem:
Even when all members have default constructors, you may want explicit initialization with specific values rather than defaults:
MyQueue(int x, int y)
: _stack1(x)
, _stack2(y)
{
}
Can Initializer Lists Alone Replace Constructor Bodies?
Some constructors have additional logic beyond initialization:
class Array
{
public:
Array(int capacity = 10) : _capacity(capacity)
{
_buffer = (int*)malloc(sizeof(int) * _capacity);
if (_buffer == nullptr)
{
exit(-1);
}
}
private:
int* _buffer;
int _capacity;
};
This additional logic requires the constructor body. Use initializer lists where possible, but constructor bodies are still necessary for complex initialization logic.
13. Implicit Type Conversions
Constructors not only create and initialize objects, but constructors with a single parameter (or multiple parameters where all but the first have defaults) also perform type conversion.
For a calendar class with a single-parameter constructor:
class Calendar
{
public:
Calendar(int yearVal) : _yearValue(yearVal) {}
private:
int _yearValue;
int _monthValue;
int _dayValue;
};
int main()
{
Calendar cal = 2023; // Implicit conversion
return 0;
}
This is similar to built-in type conversions: a temporary const object is created via the constructor, then the copy constructor is called. To disable this behavior, mark the constructor explicit:
explicit Calendar(int yearVal, int monthVal = 1, int dayVal = 1) : _yearValue(yearVal) {}
Explicit conversions via cast still work:
Calendar cal = (Calendar)2023;
C++11 supports multi-parameter implicit conversions:
Calendar cal2 = {2023, 12, 9};
14. Static Members
To count how many objects a class has created, static members are ideal:
- Static members belong to the class, not individual objects
- Declaration and definition must be separated
class Calendar
{
public:
Calendar() { ++_instanceCount; }
static int GetCount()
{
return _instanceCount;
}
private:
int _yearValue;
int _monthValue;
int _dayValue;
static int _instanceCount;
};
int Calendar::_instanceCount = 0;
- Static member access is subject to class scope and access specifiers. Static member functions (which have no
thispointer) are typically used to access them.