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

What's Next