C++ Memory Model, References, Functions, and Object-Oriented Programming
Program Memory Layout
During execution, a C++ program partitions memory into four distinct regions, each governing the lifecycle of stored data.
- Code Region: Holds binary machine instructions, managed by the operating system.
- Global Region: Stores global variables, static variables, and constants.
- Stack Region: Managed automatically by the compiler for function parameters and local variables.
- Heap Region: Controlled by the programmer; memory allocated here persists until manually freed or until the program terminates.
Code Region Details
The code region is established before the program runs. It contains CPU-executable instructions and is characterized by being shared (a single copy serves all processes) and read-only to prevent accidental modification.
Global Region Details
This region stores globals, statics, and constants. The operating system reclaims this memory when the program exits. The example below prints the addresses of variables residing in different areas:
#include <iostream>
using namespace std;
int global_a = 10;
int global_b = 10;
const int c_global_a = 0;
int main() {
int local_a = 10, local_b = 10;
static int static_a = 0, static_b = 0;
const int c_local_a = 0;
cout << "Local a addr: " << (long long)&local_a << endl;
cout << "Global a addr: " << (long long)&global_a << endl;
cout << "Static a addr: " << (long long)&static_a << endl;
cout << "String literal addr: " << (long long)&"sample text" << endl;
cout << "Const global addr: " << (long long)&c_global_a << endl;
cout << "Const local addr: " << (long long)&c_local_a << endl;
return 0;
}
The output demonstrates that global and static variables reside in adjacent memory locations, far from the addresses of local variables.
Stack Region Details
The compiler handles allocation and deallocation automatically. Returning the address of a local variable is unsafe because the memory becomes invalid once the function exits:
int* dangerous_func() {
int x = 5; // local variable
return &x; // unsafe: address becomes dangling
}
Heap Region Details
Programmers manage heap memory using new and delete. Forgetting to release memory results in leaks, while premature release leads to dangling pointers.
int* allocate_on_heap() {
int* ptr = new int(100);
return ptr;
}
void demo_heap_array() {
int* numbers = new int[10];
for (int i = 0; i < 10; ++i)
numbers[i] = i + 200;
delete[] numbers;
}
References in C++
A reference creates an alias for an existing variable; both names refer to the same memory location.
Syntax: DataType& alias = original_name;
- A reference must be initialized when declared.
- Once initialized, it cannot be re-bound to a different variable.
int main() {
int value = 5;
int& ref = value;
ref = 20; // value becomes 20
int other = 99;
ref = other; // This assigns 99 to value, does NOT rebind ref
cout << value; // prints 99
return 0;
}
References and Functions
Passing by reference allows a function to modify the caller's arguments, achieving the same effect as passing pointers but with cleaner syntax.
void swap_by_ref(int& x, int& y) {
int temp = x;
x = y;
y = temp;
}
Returning a reference enables a function call to appear on the left side of an assignment, but never return a reference to a local (non-static) variable.
int& get_static_number() {
static int num = 42;
return num;
}
int main() {
get_static_number() = 500; // modifies static variable
return 0;
}
The underlying implementation of a reference is a pointer constant (e.g., int* const), meaning the pointer address cannot change, but the pointed-to value can.
Constant References
Using const references prevents unintended modifications, especially useful for function parameters.
void display(const int& val) {
// val = 10; // compilation error
cout << val << endl;
}
Advanced Function Techniques
Default Arguments
Parameters can have default values defined in the declaration or definition, but not both simultaneously. If one parameter has a default, all subsequent parameters must also have defaults.
int sum(int x, int y = 20, int z = 30) {
return x + y + z;
}
Placeholder Parameters
Used to reserve a parameter position; callers must supply a value for it unless a default is provided.
void placeholder_demo(int a, int) {
// second parameter serves as a placeholder
}
// call: placeholder_demo(5, 99);
Function Overloading
Multiple functions can share the same name if their parameter lists differ in type, count, or order. Return type alone is insufficient for differentiation.
void print() { /* ... */ }
void print(int x) { /* ... */ }
void print(double x) { /* ... */ }
void print(int x, double y) { /* ... */ }
void print(double x, int y) { /* ... */ }
Overloading with reference types or combining overloading with default parameters can create ambiguity; care must be taken.
Classes and Objects
Encapsulation
Encapsulation bundles attributes and behaviors within a class and controls access via permissions.
Example: Circle Class
const double PI = 3.1415926;
class Circle {
public:
int radius;
double circumference() {
return 2 * PI * radius;
}
};
int main() {
Circle c;
c.radius = 10;
cout << c.circumference();
}
Access Modifiers:
public: accessible from anywhere.protected: accessible within the class and its subclasses.private: accessible only within the class itself.
Setting member variables to private allows controlled read/write access and input validation.
struct vs class
The only difference is the default access level: struct defaults to public, class defaluts to private.
Object Initialization and Cleanup
Constructors and Destructors:
A constructor is called automatically when an object is created; its name matches the class name, has no return type, and can be overloaded. A destructor (~ClassName) is called when the object is destroyed; it cannot be overloaded and is used for cleanup.
Classification: Constructors are categorized as default (no parameters), parameterized, or copy constructors.
Calling Syntax:
Person p1; // default constructor
Person p2(25); // parameterized constructor
Person p3(p2); // copy constructor (parentheses)
Person p4 = p2; // copy constructor (implicit conversion)
Copy Constructor Trigger:
- Initializing a new object from an existing one.
- Passing an object by value to a function.
- Returning an object by value from a function.
Constructor Rules: If you define any constructor, the compiler does not generate a default one. If you define a copy constructor, the compiler provides no other constructors.
Deep Copy vs. Shallow Copy
Shallow copying simply duplicates pointer values, leading to double-deletion errors when multiple objects manage the same heap memory. Deep copying allocates new heap storage and copies the content.
Person(const Person& source) {
name = source.name;
height = new int(*source.height); // deep copy
}
Member Initialization List
Used to initialize member attributes before the constructor body executes.
class Rectangle {
public:
int width, height;
Rectangle(int w, int h) : width(w), height(h) {}
};
Objects as Members
When a class contains another class object as a member, the member's constructor executes before the containing class's constructor, and destruction follows reverse order.
Static Members
A static member variable belongs to the class rather than any specific instance. It must be declared inside the class and defined outside. Static member functions can only access static member variables.
class Database {
public:
static int connectionCount;
static void status() {
cout << connectionCount;
}
};
int Database::connectionCount = 0;
this Pointer
Within non-static member functions, this points to the invoking object. It resolves name conflicts between parameters and member variables, and allows returning a reference to the current object for chaining (*this).
const Member Functions
Adding const after the function declaration prevents the function from modifying member variables (except those declared mutable). Constant objects can only invoke constant member functions.
Friend Declarations
The friend keyword grants non-member functions or other classes access to private and protected members.
- Global function as friend:
friend void helper(MyClass& obj); - Class as friend:
friend class AnotherClass; - Member function as friend:
friend void AnotherClass::specific_method();
Operator Overloading
Operators can be redefined for user-defined types. Overloads can be member functions or global functions.
Addition Operator (+)
Complex operator+(const Complex& lhs, const Complex& rhs) {
Complex result;
result.real = lhs.real + rhs.real;
result.imag = lhs.imag + rhs.imag;
return result;
}
Stream Insertion Operator (<<)
ostream& operator<<(ostream& out, const Point& p) {
out << "(" << p.x << ", " << p.y << ")";
return out;
}
Assignment Operator (=)
Necessary for deep copy when a class contains heap-allocated memory. Must check for self-assignment and release existing resources.
Inheritance
Syntax: class Derived : accessSpecifier Base { };
Inheritance modes:
public: base class public and protected members retain their access levels in the derived class.protected: public base members become protected.private: public and protected base members become private.
All non-static member attributes, including private ones (though inaccessible directly), are inherited.
Construction/Destruction Sequence: Base constructor called first, then derived constructor; destructor sequence is reversed.
Name Hiding: When a derived class defines a member with the same name as one in the base, it hides all base class versions. Access them using scope resolution: obj.Base::method().
Multiple Inheritance: A class can inherit from multiple base classes, but same-named members require explicit scope qualification.
Diamond Inheritance: Solved using virtual inheritance. A virtual base class shared among multiple inheritance paths ensures only one copy of its members exists in the most-derived class.
Polymorphism
Polymorphism requires inheritance and function overriding. Use virtual in the base class and override in derived classes. Call functions through a base class pointer or reference; the appropriate derived version is selected at runtime based on the actual object type.
The mechanism relies on virtual function tables (vtables). Each class with virtual functions has a vtable storing function addresses; when a subclass overrides a virtual function, it replaces the corresponding entry.
Abstract Classes and Pure Virtual Functions: A class with a pure virtual function (= 0) cannot be instantiated. Derived classes must implement all pure virtual functions to become concrete.
Virtual Destructors: If a derived class holds heap resources, base class destructors should be virtual (or pure virtual with a body) so that delete through a base pointer correctly invokes the derived destructor.
File Operations
Use <fstream> for file I/O.
Writing Text Files
#include <fstream>
ofstream out_file("output.txt");
out_file << "Example text" << endl;
out_file.close();
Reading Text Files
ifstream in_file("output.txt");
string line;
while (getline(in_file, line)) {
cout << line << endl;
}
Binary File I/O
struct Record {
char key[50];
int value;
};
Record rec = {"alpha", 100};
ofstream bin_out("data.bin", ios::binary);
bin_out.write(reinterpret_cast<char*>(&rec), sizeof(rec));
ifstream bin_in("data.bin", ios::binary);
bin_in.read(reinterpret_cast<char*>(&rec), sizeof(rec));
Employee Management System
A practical case study demonstrating polymorphism, file operations, and memory management in C++. The system manages employees categorized as regular employees, managers, and bosses. Each category has specific responsibilities. Core functionalities include adding, displaying, deleting, modifying, searching, sorting by ID, and clearing records, with data persistently stored in a text file. The implementation involves abstract worker classes, concrete employee types, and a manager class coordinating operations and file persistence.