C++ Classes Explained: Constructors, Access Specifiers, Members and Objects

C++ classes group data with the functions that operate on it. Learn constructors, access specifiers, RAII and more — every example compiled and tested.

A blueprint producing three different houses, illustrating a C++ class as a blueprint for objects

Almost everything interesting in C++ — from std::string to the game engines and databases written in it — is built out of classes. If you understand how a class groups data with the functions that operate on it, the rest of object-oriented programming falls into place quickly.

This guide walks through C++ classes from the ground up: defining a class, controlling access to its members, writing constructors and destructors, and finishing with a real-world example that manages memory the way professional C++ code does. Every code example in this article compiles clean with g++ -std=c++17 -Wall -Wextra and runs exactly as shown.

Table of Contents

What Is a Class in C++?

A class in C++ is a user-defined type that groups data (data members) together with the functions that operate on that data (member functions). A class acts as a blueprint: it describes what an object looks like and what it can do, and you create as many objects from it as you need.

The distinction between a class and an object trips up many beginners, so it is worth stating plainly:

  • A class is the definition — the blueprint. It occupies no memory by itself.
  • An object is a concrete instance of that class, created at runtime, with its own copy of the data members.

If BankAccount is a class, then your account and my account are two separate objects of that class. Both have a balance, but each holds its own value.

Anatomy of a C++ Class class BankAccount { public: void deposit(double) double getBalance() const private: std::string owner double balance }; Member functions (behavior) The interface other code uses to work with the object Data members (state) Kept private so only the class can modify them Access specifiers control visibility public: anyone • private: class only • protected: + derived objects created from the class (the blueprint) account1 account2

Grouping data and behavior into one type is called encapsulation, and it is the core idea behind object-oriented programming in C++. Instead of loose variables and free functions that anyone can misuse, a class exposes a small, controlled interface and hides everything else. If you are new to the language, our C++ programming tutorials cover the fundamentals this guide builds on.

Defining a Class: Syntax and a First Example

A class definition starts with the class keyword, followed by the class name, a body in curly braces, and a terminating semicolon — forgetting that final semicolon is one of the most common beginner compile errors.

#include <iostream>
#include <string>

class BankAccount {
public:
    void deposit(double amount) {
        balance += amount;
    }

    double getBalance() const {
        return balance;
    }

private:
    std::string owner;
    double balance = 0.0;
};

int main() {
    BankAccount account;   // create an object of the class
    account.deposit(250.0);
    std::cout << "Balance: " << account.getBalance() << '\n';
    return 0;
}

Output:

Balance: 250

Three things to notice in this small example:

  1. Creating an object looks just like declaring a variable: BankAccount account;. C++ objects are values by default — no new keyword required, unlike Java or C#.
  2. Members are accessed with the dot operator: account.deposit(250.0) calls the deposit member function on that specific object.
  3. balance = 0.0 is a default member initializer (available since C++11). Every new BankAccount starts at zero instead of holding garbage — uninitialized members were a classic source of bugs in older C++ tutorials.

Variables, functions, and the dot operator are all covered in detail in The Basics of C++ Programming.

Access Specifiers: public, private and protected

Access specifiers decide who is allowed to touch each member of a class. C++ has exactly three:

Access specifierWho can access the memberTypical use
publicAny code that can see the objectThe interface: member functions others call
privateOnly member functions (and friends) of the class itselfData members and internal helpers
protectedThe class itself plus classes derived from itMembers shared with subclasses in inheritance

One rule surprises developers coming from other languages: in a C++ class, members are private by default. Anything you declare before the first access specifier is private.

Here is what access control buys you in practice:

#include <iostream>

class Thermostat {
public:
    void setTarget(double celsius) {
        if (celsius >= 5.0 && celsius <= 30.0) {  // validation lives with the data
            target = celsius;
        }
    }
    double getTarget() const { return target; }

private:
    double target = 20.0;
};

int main() {
    Thermostat t;
    t.setTarget(24.5);              // OK: public member function
    std::cout << t.getTarget() << '\n';

    // t.target = 500.0;            // Error: 'target' is private
    return 0;
}

Output:

24.5

Uncomment the last line and the compiler stops you cold:

error: 'double Thermostat::target' is private within this context

Because target is private, the only way to change it is through setTarget(), which enforces a sane range. No code anywhere in the program can set the thermostat to 500 °C. That is encapsulation doing real work — the class defends its own invariants.

Member Functions: Inside and Outside the Class

A member function (often called a method) can be defined in two places. Short functions are usually defined directly inside the class body, as in the examples above; functions defined inside the class are implicitly inline. Longer functions are typically declared in the class and defined outside it using the scope resolution operator ::.

#include <iostream>

class Rectangle {
public:
    Rectangle(double w, double h) : width(w), height(h) {}

    double area() const;          // declared here...

private:
    double width;
    double height;
};

// ...defined here, using the scope resolution operator ::
double Rectangle::area() const {
    return width * height;
}

int main() {
    Rectangle r(4.0, 2.5);
    std::cout << "Area: " << r.area() << '\n';
    return 0;
}

Output:

Area: 10

This split matters in real projects: the class definition goes in a header file (rectangle.h), and the member function definitions go in a source file (rectangle.cpp). Client code includes the header and never needs to see the implementation.

Note the const after area(). A const member function promises not to modify the object, and the compiler enforces that promise. Mark every member function that only reads state as const — it documents intent, catches accidental writes, and lets the function be called on const objects and references. getBalance() and getTarget() earlier follow the same rule.

Constructors in C++

A constructor is a special member function that runs automatically when an object is created. It has the same name as the class and no return type, and its job is to put the object into a valid initial state. A class can have several constructors with different parameter lists — this is ordinary function overloading.

The four kinds you will meet constantly:

  1. Default constructor — takes no arguments.
  2. Parameterized constructor — takes arguments to initialize members.
  3. Copy constructor — builds a new object as a copy of an existing one.
  4. Move constructor — transfers resources from a temporary object instead of copying them (C++11 and later; demonstrated in the dynamic array example below).
#include <iostream>
#include <string>

class BankAccount {
public:
    // Default constructor
    BankAccount() : owner("Unassigned"), balance(0.0) {
        std::cout << "Default constructor called\n";
    }

    // Parameterized constructor with a member initializer list
    BankAccount(const std::string& name, double openingBalance)
        : owner(name), balance(openingBalance) {
        std::cout << "Parameterized constructor called for " << owner << '\n';
    }

    // Copy constructor
    BankAccount(const BankAccount& other)
        : owner(other.owner + " (copy)"), balance(other.balance) {
        std::cout << "Copy constructor called\n";
    }

    void print() const {
        std::cout << owner << ": " << balance << '\n';
    }

private:
    std::string owner;
    double balance;
};

int main() {
    BankAccount a;                        // default constructor
    BankAccount b("Sara", 1500.0);        // parameterized constructor
    BankAccount c = b;                    // copy constructor

    a.print();
    b.print();
    c.print();
    return 0;
}

Output:

Default constructor called
Parameterized constructor called for Sara
Copy constructor called
Unassigned: 0
Sara: 1500
Sara (copy): 1500

The : owner(name), balance(openingBalance) syntax is a member initializer list, and it is the preferred way to initialize members. Members are initialized directly instead of being default-constructed and then assigned, and for reference members and const members it is the only way.

Two more facts worth knowing:

  • If you write no constructors at all, the compiler generates a default constructor for you. The moment you write any constructor of your own, that freebie disappears — a very common source of “no matching function for call” errors.
  • Mark single-argument constructors explicit unless you specifically want implicit conversions. Without it, BankAccount acc = 500.0; could silently compile if a BankAccount(double) constructor existed.

Destructors and RAII

A destructor is the constructor’s mirror image: a member function named ~ClassName() that runs automatically when an object’s lifetime ends — when it goes out of scope, or when a dynamically allocated object is deleted. A class has exactly one destructor, and it takes no arguments.

Destructors power the single most important idiom in C++: RAII — Resource Acquisition Is Initialization. The constructor acquires a resource (a file, memory, a lock, a network socket); the destructor releases it. Because destructors run deterministically, the resource is cleaned up on every path out of a scope, including early returns and exceptions.

#include <cstdio>
#include <iostream>

class LogFile {
public:
    explicit LogFile(const char* path) : file(std::fopen(path, "w")) {
        if (file == nullptr) {
            std::cout << "Could not open log file\n";
        }
    }

    // Destructor: releases the resource automatically
    ~LogFile() {
        if (file != nullptr) {
            std::fclose(file);
            std::cout << "Log file closed\n";
        }
    }

    void write(const char* message) {
        if (file != nullptr) {
            std::fprintf(file, "%s\n", message);
        }
    }

private:
    std::FILE* file;
};

int main() {
    {
        LogFile log("session.log");
        log.write("Application started");
    }   // log goes out of scope here -- the destructor closes the file

    std::cout << "Back in main\n";
    return 0;
}

Output:

Log file closed
Back in main

Nobody called a close() function — the destructor did it the instant log left its scope. This is why idiomatic C++ has no finally blocks and rarely needs manual cleanup: std::vector, std::string, std::unique_ptr, and std::lock_guard are all RAII classes doing exactly what LogFile does here.

Static Data Members and Static Member Functions

A static data member belongs to the class itself rather than to any object — there is exactly one copy shared by every instance. A static member function can be called without an object and can only touch static members.

The classic use case is counting live instances:

#include <iostream>

class Sensor {
public:
    Sensor() { ++count; }
    ~Sensor() { --count; }

    static int activeCount() {   // static member function
        return count;
    }

private:
    static inline int count = 0;  // static data member (C++17 inline variable)
};

int main() {
    Sensor a, b;
    std::cout << "Active sensors: " << Sensor::activeCount() << '\n';
    {
        Sensor c;
        std::cout << "Active sensors: " << Sensor::activeCount() << '\n';
    }
    std::cout << "Active sensors: " << Sensor::activeCount() << '\n';
    return 0;
}

Output:

Active sensors: 2
Active sensors: 3
Active sensors: 2

Notice the call syntax: Sensor::activeCount() uses the class name, not an object. Also note static inline int count = 0; — since C++17, inline lets you initialize the static member right in the class definition. Before C++17 you had to define it separately in a .cpp file (int Sensor::count = 0;), which is the pattern you will still see in older codebases.

struct vs class in C++: The Only Real Difference

In C++, struct and class both define classes, and the difference between them is smaller than most tutorials suggest.

The only language difference: members and base classes of a struct are public by default, while members and base classes of a class are private by default. Everything else — constructors, destructors, member functions, inheritance, templates — works identically in both.

Featurestructclass
Default member accesspublicprivate
Default inheritancepublicprivate
Can have constructors and member functionsYesYes
Can use access specifiers explicitlyYesYes
ConventionPassive data bundlesTypes with invariants and hidden state
#include <iostream>

struct Point {        // struct: members are public by default
    double x;
    double y;
};

class Circle {        // class: members are private by default
public:
    Circle(Point c, double r) : center(c), radius(r) {}
    double area() const { return 3.14159265 * radius * radius; }

private:
    Point center;
    double radius;
};

int main() {
    Point p{3.0, 4.0};          // aggregate initialization works: x and y are public
    Circle c(p, 2.5);
    std::cout << "Area: " << c.area() << '\n';
    return 0;
}

Output:

Area: 19.635

The convention followed by the C++ Core Guidelines is simple: use struct when the members can vary independently (plain data, no rules to enforce), and use class when the type has an invariant to protect — like Circle, where a negative radius would be nonsense.

Friend Functions and Operator Overloading

Sometimes a function that is not a member genuinely needs access to a class’s private data. C++ handles this with the friend keyword: the class grants access to a specific external function (or an entire other class).

The most common real-world friend is the stream output operator, which pairs naturally with operator overloading — defining what operators like +, ==, or << mean for your own types:

#include <iostream>

class Money {
public:
    Money(long dollars, int cents) : totalCents(dollars * 100 + cents) {}

    // Operator overloading: add two Money objects with +
    Money operator+(const Money& other) const {
        return Money(0, static_cast<int>(totalCents + other.totalCents));
    }

    // Friend function: operator<< needs access to private data
    friend std::ostream& operator<<(std::ostream& out, const Money& m);

private:
    long totalCents;
};

std::ostream& operator<<(std::ostream& out, const Money& m) {
    out << '$' << m.totalCents / 100 << '.'
        << (m.totalCents % 100 < 10 ? "0" : "") << m.totalCents % 100;
    return out;
}

int main() {
    Money price(19, 99);
    Money shipping(4, 5);
    std::cout << "Total: " << price + shipping << '\n';
    return 0;
}

Output:

Total: $24.04

operator<< cannot be a member of Money (its left operand is the stream, not the Money object), so declaring it a friend is the standard idiom. Use friendship sparingly — every friend is a small hole in your encapsulation — but do not fear it where the design calls for it.

A Real-World Example: Building a Simple Dynamic Array

Everything above comes together when a class owns a resource. Here is a simplified version of what std::vector does internally: a class that manages a heap-allocated array, with a destructor, copy constructor, move constructor, and an overloaded [] operator.

#include <algorithm>
#include <cstddef>
#include <iostream>
#include <utility>

class IntArray {
public:
    // Parameterized constructor acquires the resource
    explicit IntArray(std::size_t n) : size(n), data(new int[n]{}) {}

    // Destructor releases it (RAII)
    ~IntArray() { delete[] data; }

    // Copy constructor: deep copy
    IntArray(const IntArray& other)
        : size(other.size), data(new int[other.size]) {
        std::copy(other.data, other.data + size, data);
        std::cout << "Deep copy of " << size << " elements\n";
    }

    // Move constructor: steal the buffer, no copying (C++11 and later)
    IntArray(IntArray&& other) noexcept
        : size(other.size), data(other.data) {
        other.size = 0;
        other.data = nullptr;
        std::cout << "Moved buffer, zero elements copied\n";
    }

    // Copy and move assignment via copy-and-swap
    IntArray& operator=(IntArray other) noexcept {
        std::swap(size, other.size);
        std::swap(data, other.data);
        return *this;
    }

    int& operator[](std::size_t i) { return data[i]; }
    std::size_t length() const { return size; }

private:
    std::size_t size;
    int* data;
};

int main() {
    IntArray a(1000);
    a[0] = 42;

    IntArray b = a;             // copy constructor
    IntArray c = std::move(a);  // move constructor

    std::cout << "b[0] = " << b[0] << ", c[0] = " << c[0]
              << ", c holds " << c.length() << " elements\n";
    return 0;
}

Output:

Deep copy of 1000 elements
Moved buffer, zero elements copied
b[0] = 42, c[0] = 42, c holds 1000 elements

The copy constructor allocates a fresh buffer and copies all 1000 elements. The move constructor just takes the pointer from the temporary and leaves it empty — constant time regardless of size. This is why moves make modern C++ fast: returning large objects from functions or putting them into containers no longer means copying them.

Without the destructor, this class would leak memory; without the deep-copy constructor, two objects would delete the same buffer and crash. (This example was additionally verified with AddressSanitizer — no leaks, no double frees.)

Key Takeaways

  • A class is a user-defined type combining data members and member functions; an object is a runtime instance of a class.
  • Class members are private by default; struct members are public by default — that is the only language difference between the two keywords.
  • Keep data members private and expose a small public interface so the class can protect its own invariants.
  • Constructors initialize objects (prefer member initializer lists); destructors clean up and enable RAII, the idiom behind std::vector, std::unique_ptr, and safe resource handling generally.
  • Mark read-only member functions const and single-argument constructors explicit.
  • Static members belong to the class, not to any object; friends and operator overloading let your types integrate naturally with the rest of C++.
  • If a class manages a raw resource, it needs a destructor, copy operations, and move operations — or better, it should hold the resource in a standard RAII type and need none of them.

Frequently Asked Questions

Conclusion

The old way to learn C++ classes was to memorize syntax; the way that actually sticks is to see what each feature protects you from. Access specifiers stop other code from corrupting your state, constructors stop objects from existing half-initialized, and destructors stop resources from leaking. Once you read a class definition as a set of guarantees rather than a block of syntax, unfamiliar codebases — and the standard library itself — become far easier to navigate.

From here, the natural next step is what classes make possible: inheritance, virtual functions, and polymorphism, where one interface serves many implementations. You can explore those and related topics in our object-oriented programming tutorials — and if any example here behaves differently on your compiler, that is worth investigating: it usually means a different language standard is selected.

Scroll to Top