C++ Class Methods

A class method (also called a member function) is a function that belongs to a class and operates on the data of a specific object of that class. Methods are how you give an object behavior — instead of writing loose functions that take a struct as a parameter, you attach the operations directly to the class so objects can “do things” to themselves. Understanding methods thoroughly — where they live in memory, how this works, and when to mark them const or static — is essential to writing correct, idiomatic C++.

Overview / How Methods Work

A method is declared inside a class body just like a regular function, but it has implicit access to the object’s member variables without needing them passed as arguments. When you write:

class Rectangle {
public:
    double width, height;
    double area() { return width * height; }
};

the compiler generates code roughly equivalent to a free function double area(Rectangle* self) { return self->width * self->height; }. Every non-static member function secretly receives a hidden pointer to the object it was called on — this pointer is called this. When you write width inside a method, the compiler actually looks it up as this->width. This is why calling rect.area() and otherRect.area() can return different results even though they run the exact same compiled instructions: only the value of this changes.

Importantly, member functions are not stored inside each object. An object in memory only contains its data members (its width and height in the example above); the compiled instructions for area() exist once, shared by every instance of the class. This is one reason methods are cheap: creating a thousand Rectangle objects does not create a thousand copies of area().

Methods can be declared and defined right inside the class body (an inline definition), or just declared in the class and defined later outside it using the scope resolution operator ::. Larger, real-world classes almost always separate declaration (in a header file) from definition (in a source file) to keep the public interface easy to read and to avoid recompiling every file that uses the class whenever the implementation changes.

Syntax

class ClassName {
public:
    ReturnType methodName(ParameterList);       // declaration only
    ReturnType inlineMethod() { /* body */ }     // defined inline
};

// definition outside the class:
ReturnType ClassName::methodName(ParameterList) {
    // body, has access to member variables
}
  • ReturnType — the type the method returns (void if it returns nothing).
  • ClassName:: — the scope resolution operator ties the definition back to the class it belongs to; without it, the compiler treats the function as an unrelated free function.
  • const (optional, after the parameter list) — promises the method will not modify any member variables, so it can be called on const objects and references.
  • static (before the return type) — marks a method that belongs to the class itself rather than any single object; it has no this pointer and can only touch static members.

Examples

Example 1: Methods defined inline

#include <iostream>
using namespace std;

class Rectangle {
public:
    double width;
    double height;

    double area() {
        return width * height;
    }

    double perimeter() {
        return 2 * (width + height);
    }
};

int main() {
    Rectangle r;
    r.width = 5.0;
    r.height = 3.0;

    cout << "Area: " << r.area() << endl;
    cout << "Perimeter: " << r.perimeter() << endl;

    return 0;
}

Output:

Area: 15
Perimeter: 16

Both area() and perimeter() read the object’s own width and height through the implicit this pointer. Because they’re defined right inside the class, the compiler treats them as candidates for inlining — small, frequently called methods like these often get compiled directly into the caller with no function-call overhead at all.

Example 2: Declaring in the class, defining outside with ::

#include <iostream>
#include <string>
using namespace std;

class BankAccount {
private:
    string owner;
    double balance;

public:
    BankAccount(string ownerName, double startingBalance);
    void deposit(double amount);
    bool withdraw(double amount);
    double getBalance() const;
    void printSummary() const;
};

BankAccount::BankAccount(string ownerName, double startingBalance) {
    owner = ownerName;
    balance = startingBalance;
}

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

bool BankAccount::withdraw(double amount) {
    if (amount > balance) {
        return false;
    }
    balance -= amount;
    return true;
}

double BankAccount::getBalance() const {
    return balance;
}

void BankAccount::printSummary() const {
    cout << owner << "'s balance: $" << getBalance() << endl;
}

int main() {
    BankAccount acc("Alice", 100.0);
    acc.deposit(50.0);

    bool success = acc.withdraw(30.0);
    cout << "Withdrawal successful: " << (success ? "yes" : "no") << endl;

    acc.printSummary();

    bool overdraft = acc.withdraw(1000.0);
    cout << "Overdraft attempt successful: " << (overdraft ? "yes" : "no") << endl;

    return 0;
}

Output:

Withdrawal successful: yes
Alice's balance: $120
Overdraft attempt successful: no

Notice each definition repeats BankAccount:: before the method name — that is what tells the compiler “this function body belongs to the BankAccount class, give it access to owner and balance.” The two accessor methods, getBalance() and printSummary(), are marked const because they only read data; this lets them be called on const BankAccount objects and documents the intent clearly.

Example 3: Overloaded methods and a static method

#include <iostream>
using namespace std;

class Counter {
private:
    int count;
    static int totalCounters;

public:
    Counter() {
        count = 0;
        totalCounters++;
    }

    void increment() {
        count++;
    }

    void increment(int amount) {
        count += amount;
    }

    int getCount() const {
        return count;
    }

    static int getTotalCounters() {
        return totalCounters;
    }
};

int Counter::totalCounters = 0;

int main() {
    Counter a;
    Counter b;

    a.increment();
    a.increment(5);
    b.increment(10);

    cout << "Counter a: " << a.getCount() << endl;
    cout << "Counter b: " << b.getCount() << endl;
    cout << "Total counters created: " << Counter::getTotalCounters() << endl;

    return 0;
}

Output:

Counter a: 6
Counter b: 10
Total counters created: 2

This example shows two features at once. First, overloading: there are two methods named increment, one with no parameters and one that takes an int; the compiler picks the right one based on the arguments you pass. Second, static members: totalCounters is shared by all Counter objects (there is exactly one copy, not one per object), and getTotalCounters() is a static method — called as Counter::getTotalCounters() without needing any specific object, because it has no this pointer at all.

Under the Hood: What Happens When You Call a Method

  1. The compiler resolves obj.method(args) to a specific function based on the object’s type and, for overloaded methods, the argument types.
  2. The address of obj is passed as the hidden first argument (this), together with the explicit arguments.
  3. Inside the method body, every bare member name is rewritten by the compiler as this->memberName.
  4. The method executes using the object’s own data through that pointer, and returns a value (or nothing) to the caller.
  5. For a static method, step 2 is skipped entirely — no object address is passed, so the method can only touch other static members.
  6. For a const method, the compiler treats this as a pointer-to-const, so any attempt to assign to a non-mutable member inside the method is a compile error.

Common Mistakes

Mistake 1: Forgetting const on a read-only method

If a method only reads data but isn’t marked const, it can’t be called through a const reference or on a const object — a common source of confusing compiler errors when passing objects by const&.

// Wrong: getX() is not const
class Point {
public:
    int x, y;
    int getX() {
        return x;
    }
};

void printPoint(const Point& p) {
    cout << p.getX() << endl; // error: passing 'const Point' discards qualifiers
}
#include <iostream>
using namespace std;

class Point {
public:
    int x, y;
    int getX() const {
        return x;
    }
};

void printPoint(const Point& p) {
    cout << "x = " << p.getX() << endl;
}

int main() {
    Point pt;
    pt.x = 7;
    pt.y = 9;
    printPoint(pt);
    return 0;
}

Output:

x = 7

Marking getX() as const fixes the error and lets the method be used anywhere a const Point shows up, such as when passed by const& to avoid an unnecessary copy.

Mistake 2: Trying to modify members inside a const method

// Wrong: reset() is const but tries to modify a member
class Temperature {
public:
    double degrees;
    void reset() const {
        degrees = 0.0; // error: assignment of member in a const method
    }
};
#include <iostream>
using namespace std;

class Temperature {
public:
    double degrees;
    void reset() {
        degrees = 0.0;
    }
    void show() const {
        cout << "Degrees: " << degrees << endl;
    }
};

int main() {
    Temperature t;
    t.degrees = 98.6;
    t.show();
    t.reset();
    t.show();
    return 0;
}

Output:

Degrees: 98.6
Degrees: 0

The const qualifier is a promise to the compiler that the method won’t change the object’s observable state; if a method genuinely needs to modify data, drop const from that method instead of trying to force it through.

Best Practices

  • Mark every method that doesn’t modify object state as const — it documents intent and lets your objects be used through const references and pointers.
  • Keep the class declaration (in a header) focused on what the class can do; move longer method bodies to a separate .cpp file using ClassName:: so the interface stays readable.
  • Reserve static methods for behavior that truly doesn’t depend on a specific object (factory helpers, counters, utility functions tied to the class).
  • Prefer overloading a method name (same name, different parameters) over inventing awkward names like incrementBy when the operations are conceptually the same action.
  • Keep methods small and focused — a method that does one clear thing is easier to test, reuse, and reason about.
  • Pass object parameters by const& when a method doesn’t need to modify or copy them, to avoid unnecessary copying.

Practice Exercises

  • Exercise 1: Write a Circle class with a radius member and inline methods area() and circumference() (use 3.14159 for pi). Create a circle with radius 4 and print both values.
  • Exercise 2: Write a Student class with private members name and grade. Declare the constructor and a const method printReport() inside the class, but define both outside the class using the :: operator.
  • Exercise 3: Add a static member int studentCount to your Student class from Exercise 2, incremented in the constructor, plus a static method getStudentCount(). Create three students and print the total count using Student::getStudentCount().

Summary

  • A method (member function) is a function that belongs to a class and implicitly receives a this pointer to the calling object.
  • Methods can be defined inline inside the class body, or declared inside and defined outside using ClassName::methodName.
  • Only member data is duplicated per object; compiled method code is shared by all instances of the class.
  • const methods promise not to modify the object, enabling them to be called on const objects and references.
  • static methods belong to the class itself, have no this pointer, and can be called without an object using ClassName::method().
  • Methods can be overloaded by giving multiple versions different parameter lists under the same name.