Skip to content
C++
Language
since C++11
Beginner

Build Your First C++ Class: Bundling Data and Behaviour Together

Define a C++ class, add member variables and functions, and use access specifiers to keep your data safe from accidental misuse.

By the end of this page, you will know how to define a class with member variables and member functions, use public and private to control access, write a constructor that initialises an object in a valid state, and create multiple independent objects from a single class definition.

What and Why

Imagine you are writing a program that tracks bank accounts. Each account has an owner name and a balance. You also need operations: deposit, withdraw, check the current balance.

Without classes you might juggle separate variables for every account:

cpp
std::string owner1 = "Alice";
double balance1 = 1000.0;

std::string owner2 = "Bob";
double balance2 = 500.0;

This works for two accounts. For hundreds it becomes unmanageable β€” and nothing stops you from accidentally pairing balance1 with owner2, or setting a balance to -99999.

A class solves this by grouping related data and the operations on that data into a single named type. Once you define BankAccount, you can create as many account objects as you need. Each object carries its own data, and the operations always act on the right one.

Two ideas underpin every class you will ever write:

  • Encapsulation β€” hide the internal data; expose only what callers need to see.
  • Abstraction β€” callers think about what an account does, not how it does it.

You will see both in action as you build the examples below.

Step by Step

1. The Bare-Minimum Class

A class declaration starts with the class keyword, a name, a pair of braces, and β€” critically β€” a semicolon after the closing brace.

cpp
#include <iostream>
#include <string>

class BankAccount {
public:
    std::string owner;
    double balance;
};

int main() {
    BankAccount alice;
    alice.owner   = "Alice";
    alice.balance = 1000.0;

    std::cout << alice.owner << " has $" << alice.balance << "\n";
    return 0;
}

Compile with any C++11-capable compiler:

cpp
g++ -std=c++11 -o bank bank.cpp

alice is an object β€” a concrete instance of the BankAccount blueprint. The dot operator (.) lets you read or write its members.

2. Adding Member Functions

Right now any caller can write alice.balance = -9999. Moving the logic inside the class fixes that. Functions defined inside a class are called member functions.

cpp
#include <iostream>
#include <string>

class BankAccount {
public:
    std::string owner;
    double balance;

    void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }

    void withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
        }
    }

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

int main() {
    BankAccount alice;
    alice.owner   = "Alice";
    alice.balance = 0.0;

    alice.deposit(500.0);
    alice.deposit(200.0);
    alice.withdraw(100.0);
    alice.print();  // Alice: $600
    return 0;
}

Notice const after print(). That tells the compiler β€” and anyone reading the code β€” that print promises not to modify the object. Mark every read-only function const from day one.

3. Access Specifiers: public vs private

Making balance public still allows alice.balance = -9999 from outside the class. The right tool is a private section.

cpp
#include <iostream>
#include <string>

class BankAccount {
public:
    BankAccount(std::string ownerName, double initialBalance)
        : owner(ownerName), balance(initialBalance) {}

    void deposit(double amount) {
        if (amount > 0) balance += amount;
    }

    bool withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            return true;
        }
        return false;
    }

    double      getBalance() const { return balance; }
    std::string getOwner()   const { return owner;   }

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

int main() {
    BankAccount alice("Alice", 1000.0);

    alice.deposit(250.0);
    alice.withdraw(100.0);

    std::cout << alice.getOwner() << " has $" << alice.getBalance() << "\n";
    // alice.balance = -9999; // compile error β€” balance is private
    return 0;
}

private members are invisible outside the class. Callers can only use what you expose in public.

The BankAccount(...) function is a constructor β€” it runs once at object creation and ensures the object always starts in a valid state. The : owner(ownerName), balance(initialBalance) syntax is a member initialiser list: it initialises members directly rather than assigning to them inside the body. Prefer this form; it is more efficient and is the idiomatic C++11 style.

Common Patterns

Pattern 1 β€” In-Class Default Values

C++11 lets you give members a default value right where they are declared. The constructor only needs to override what differs:

cpp
#include <iostream>

class Counter {
public:
    explicit Counter(int start = 0) : count(start) {}

    void increment() { ++count; }
    int  value() const { return count; }

private:
    int count = 0;  // in-class initialiser β€” C++11 and later
};

int main() {
    Counter c;
    c.increment();
    c.increment();
    std::cout << c.value() << "\n";  // 2
}

explicit on a single-argument constructor prevents the compiler from silently converting an int to a Counter when you did not intend it. Prefer explicit unless you specifically want that implicit conversion.

Pattern 2 β€” Immutable Snapshot

Sometimes you want an object whose state is fixed after construction. Mark members const and provide no mutating functions:

cpp
#include <iostream>
#include <string>

class TransactionRecord {
public:
    TransactionRecord(std::string from, std::string to, double amount)
        : from(from), to(to), amount(amount) {}

    void print() const {
        std::cout << from << " -> " << to << "  $" << amount << "\n";
    }

private:
    const std::string from;
    const std::string to;
    const double      amount;
};

int main() {
    TransactionRecord t("Alice", "Bob", 50.0);
    t.print();
}

Pattern 3 β€” Classes as Building Blocks

Classes compose naturally. One class can own another as a member:

cpp
#include <iostream>
#include <string>
#include <vector>

class BankAccount {
public:
    BankAccount(std::string owner, double balance)
        : owner(owner), balance(balance) {}
    std::string getOwner()   const { return owner;   }
    double      getBalance() const { return balance; }
private:
    std::string owner;
    double      balance;
};

class Bank {
public:
    void addAccount(BankAccount account) {
        accounts.push_back(account);
    }

    void printAll() const {
        for (const auto& acc : accounts) {
            std::cout << acc.getOwner() << ": $" << acc.getBalance() << "\n";
        }
    }

private:
    std::vector<BankAccount> accounts;
};

int main() {
    Bank myBank;
    myBank.addAccount(BankAccount("Alice", 1000.0));
    myBank.addAccount(BankAccount("Bob",    500.0));
    myBank.printAll();
}

What Can Go Wrong

Forgetting the Semicolon After the Closing Brace

cpp
class Foo {
    int x;
}   // ← missing semicolon
// The error appears on the next line and looks completely unrelated.

Every class definition must end with };. When you see baffling errors on the line after a class, check the brace first.

Uninitialised Built-In Members

cpp
class Rect {
public:
    int width;
    int height;
    int area() const { return width * height; }  // undefined behaviour if not set!
};

int main() {
    Rect r;
    std::cout << r.area();  // reads garbage values
}

Built-in types (int, double, bool) are not automatically zeroed when you create a stack object. Always initialise them β€” in a constructor, with an in-class initialiser (int width = 0;), or via value-initialisation (Rect r{}).

Calling a Non-const Function on a const Reference

cpp
void display(const BankAccount& acc) {
    acc.deposit(10.0);  // error: deposit is not marked const
}

A const& parameter restricts you to const member functions only. If deposit modifies balance, it cannot be const β€” and the fix is to restructure your design, not to remove the const from the parameter.

Members Are private by Default in a class

cpp
class Secret {
    int x = 42;  // private β€” no access specifier was written
};

int main() {
    Secret s;
    std::cout << s.x;  // error: 'x' is private
}

In a class, anything written before the first access specifier is private. Add public: above the members you want callers to reach. (struct behaves the opposite way: its members are public by default.)

Quick Reference

ConceptSyntaxNotes
Class definitionclass Name { ... };Semicolon after } is mandatory
Public memberpublic: int x;Accessible from anywhere
Private memberprivate: int x;Accessible inside the class only
Default visibilityclass β†’ private, struct β†’ publicDiffers between the two keywords
ConstructorName(args) : member(arg) {}Member initialiser list is preferred
In-class initialiserint x = 0;C++11 and later
Const member functionint get() const { ... }Promises not to modify the object
Create an objectName obj(args);Calls the matching constructor
Access a memberobj.member or obj.method()Use -> when going through a pointer

The this Pointer

Every non-static member function receives a hidden pointer to the object it was called on, and that pointer is named this. Most of the time the compiler uses it automatically β€” when you write balance += amount inside deposit, the compiler silently reads this->balance += amount. You need to write this explicitly in two situations: when a parameter shadows a member name, and when a function must return a reference to the object itself (enabling method chaining).

cpp
#include <iostream>
#include <string>

class BankAccount {
public:
    // Parameter 'balance' shadows the member 'balance'.
    // Without this-> the assignment would be a no-op (self-assignment to the parameter).
    BankAccount(std::string owner, double balance)
        : owner(owner), balance(balance) {}

    // Returning *this enables chaining: account.deposit(100).deposit(50);
    BankAccount& deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
        return *this;
    }

    double getBalance() const { return balance; }

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

int main() {
    BankAccount acc("Alice", 0.0);

    // Method chaining works because each call returns a reference to the same object.
    acc.deposit(100.0).deposit(50.0).deposit(25.0);

    std::cout << acc.getBalance() << "\n";  // 175
}

The type of this inside a non-const member function is BankAccount* const β€” a const pointer to a non-const object. Inside a const member function it becomes const BankAccount* const, which is why const functions cannot modify members.


Static Class Members

A static data member belongs to the class itself, not to any individual object β€” all instances share one copy. This is useful for counters, default values, or shared resources that logically belong to the type rather than to a single instance. Static member functions work the same way: they can be called without an object and have no this pointer.

Before C++17, a static data member had to be defined outside the class body in exactly one translation unit. C++17 introduced inline static, which lets you define and initialize it inside the class.

cpp
#include <iostream>
#include <string>

class BankAccount {
public:
    // C++17: inline static β€” definition lives inside the class
    inline static int accountCount = 0;

    BankAccount(std::string owner, double balance)
        : owner(owner), balance(balance)
    {
        ++accountCount;   // increments the shared counter on every construction
    }

    ~BankAccount() {
        --accountCount;   // decrements on destruction
    }

    // Static member function: no 'this', called as BankAccount::showCount()
    static void showCount() {
        std::cout << "Accounts open: " << accountCount << "\n";
    }

    double getBalance() const { return balance; }

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

// Pre-C++17 style (still valid): define outside the class in exactly one .cpp file
// int BankAccount::accountCount = 0;

int main() {
    BankAccount::showCount();   // 0

    {
        BankAccount a("Alice", 100.0);
        BankAccount b("Bob",   200.0);
        BankAccount::showCount();   // 2
    }   // a and b destroyed here

    BankAccount::showCount();   // 0
}

Static member functions cannot access non-static members β€” they have no this pointer to work with. If you need both static and per-instance behavior, pass the object as an explicit parameter.


Friend Functions and Classes

The friend keyword grants a specific function or class access to private members, without making those members public to everyone. Use it sparingly β€” friendship breaks encapsulation for the named party. The most common legitimate uses are operator overloads (where the function logically operates on two objects) and tightly coupled class pairs that must share implementation details.

cpp
#include <iostream>
#include <string>

class BankAccount {
public:
    BankAccount(std::string owner, double balance)
        : owner(owner), balance(balance) {}

    // Declare the free function as a friend β€” it can now access private members.
    friend void transfer(BankAccount& from, BankAccount& to, double amount);

    // Friend operator: allows std::cout << account;
    friend std::ostream& operator<<(std::ostream& os, const BankAccount& acc);

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

// Defined outside the class β€” no BankAccount:: prefix needed.
void transfer(BankAccount& from, BankAccount& to, double amount) {
    if (amount > 0 && from.balance >= amount) {
        from.balance -= amount;   // direct private access via friendship
        to.balance   += amount;
    }
}

std::ostream& operator<<(std::ostream& os, const BankAccount& acc) {
    return os << acc.owner << ": $" << acc.balance;
}

int main() {
    BankAccount alice("Alice", 500.0);
    BankAccount bob  ("Bob",   100.0);

    std::cout << alice << "\n";   // Alice: $500
    transfer(alice, bob, 150.0);
    std::cout << alice << "\n";   // Alice: $350
    std::cout << bob   << "\n";   // Bob: $250
}

Friendship is not inherited and not transitive: if A is a friend of B, and C is a friend of A, that does not make C a friend of B.


Nested Classes

A class defined inside another class is called a nested class; it has access to the outer class's type names but not to its non-static members. Nested classes are a scoping tool β€” they express that a type only makes sense in the context of its enclosing class. The iterator types inside standard library containers are the canonical real-world example.

cpp
#include <iostream>
#include <string>
#include <vector>

class Bank {
public:
    // Nested class β€” scoped inside Bank, accessed as Bank::Account from outside.
    class Account {
    public:
        Account(std::string owner, double balance)
            : owner(owner), balance(balance) {}

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

        double getBalance() const { return balance; }

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

    void addAccount(const Account& acc) {
        accounts.push_back(acc);
    }

    double totalAssets() const {
        double sum = 0;
        for (const Account& a : accounts) {
            sum += a.getBalance();
        }
        return sum;
    }

private:
    std::vector<Account> accounts;
};

int main() {
    Bank myBank;

    // Create accounts using the nested type Bank::Account
    myBank.addAccount(Bank::Account("Alice", 1000.0));
    myBank.addAccount(Bank::Account("Bob",    500.0));

    std::cout << "Total assets: $" << myBank.totalAssets() << "\n";  // 1500
}

The nested class Bank::Account does not automatically have access to Bank's private members. If you need that, declare a friend relationship or pass a reference to the outer object explicitly.


Forward Declarations

A forward declaration tells the compiler that a class exists, without providing its full definition β€” this is enough to declare pointers and references to it. The main use case is breaking circular include chains: if A.h includes B.h and B.h includes A.h, you get infinite recursion at compile time. Forward-declaring one type in the other header cuts the cycle.

cpp
// File: Transaction.h
#pragma once
#include <string>

// Forward declaration β€” we say "BankAccount exists as a class".
// We don't need the full definition because we only use a reference below.
class BankAccount;

class Transaction {
public:
    Transaction(BankAccount& acc, double amount);
    void execute();

private:
    BankAccount& account;   // reference is fine with just a forward declaration
    double       amount;
};
cpp
// File: BankAccount.h
#pragma once
#include <string>

class BankAccount {
public:
    BankAccount(std::string owner, double balance)
        : owner(owner), balance(balance) {}

    void credit(double amount) { balance += amount; }
    double getBalance() const  { return balance; }

private:
    std::string owner;
    double      balance;
};
cpp
// File: Transaction.cpp
// The .cpp file includes the full definitions it needs.
#include "Transaction.h"
#include "BankAccount.h"

Transaction::Transaction(BankAccount& acc, double amount)
    : account(acc), amount(amount) {}

void Transaction::execute() {
    account.credit(amount);   // now we can call methods β€” full definition is visible
}

You cannot use a forward-declared type in a way that requires knowing its size: you cannot declare a plain value member (BankAccount account;) or call its methods until the full definition is included. Pointers and references, however, always have a fixed machine-word size regardless of what they point to, so they work fine.


What's Next