Classes in Object-Oriented Programming: Members, Constructors and Static Members in C++, Java and Python

One Temperature class written in C++, Java and Python, what a class stores once versus per object, and why a Python list in a class body was shared.

A blue cookie cutter beside three green cookies of the same shape with different decorations, representing one class and three objects created from it.

Give a Python class a list as a class attribute, add one item to each of two objects, and both objects report two items. Nothing in the code looks shared, but the list belongs to the class, not to either object. The distinction behind that bug, between what a class stores once and what each object stores for itself, is the most useful thing to understand about classes, and it works the same way in C++, Java and Python.

This guide explains classes as an object-oriented concept, with one class written in all three languages: what a class is, what it contains, how constructors establish valid objects, the difference between instance and static members, and how each language controls access. Every program was run for this article on Ubuntu 24.04 with GCC 13.3 and Clang 18.1.3 (-std=c++17 -Wall -Wextra -pedantic -Werror), OpenJDK 21 (javac -Xlint:all -Werror) and Python 3.11, 3.12 and 3.13 (-W error), and the three versions of the main example print identical output, captured verbatim. The code is in a GitHub repository whose build repeats these checks on each commit.

The Short Answer

What is a class in OOP? A class is a user-defined type that bundles data (fields) with the code that works on it (methods). It describes what every object of that type will hold and do; objects are the values created from it.

What is the difference between a class and an object? The class is the definition, written once; an object is one instance, created at run time with its own copy of the fields. One Temperature class can produce any number of Temperature objects, each holding a different value.

What is a static (class) member? A field or method that belongs to the class itself rather than to any object. All objects share one copy: a counter declared static in C++ or Java, or as a class attribute in Python, has the same value whichever object you read it through.

What Is a Class?

A class in object-oriented programming is a template that defines a new type: the data each object of that type holds (fields, also called attributes or member variables) and the operations it supports (methods, also called member functions). Creating an object from a class is called instantiation, and every object made from the same class has the same structure but its own values.

The parts of a class have different names in each language:

PartC++JavaPython
Data of each objectData member (double celsius_;)Field (private final double celsius;)Instance attribute (self._celsius)
OperationsMember functionMethodMethod (def fahrenheit(self))
InitializationConstructor (Temperature(double))Constructor (Temperature(double))Initializer (__init__)
Shared by all objectsstatic memberstatic field or methodClass attribute, @classmethod, @staticmethod
Access controlpublic, protected, private (enforced by the compiler)public, protected, package-private, private (enforced)Naming conventions; __name is renamed, not hidden
Text form of an objectYour own function, or operator<<toString()__str__ and __repr__

A class is not an object, and in C++ and Java it does not exist at run time as a value you can modify. In Python a class is itself an object (of type type), which is why class attributes can be read and changed at run time, and why the shared-list bug below is possible. Objects themselves, with identity, state and lifetime, are covered in the companion article on objects in this series.

One Class in Three Languages

A Temperature class stores a value in degrees Celsius, refuses values below absolute zero, converts to Fahrenheit, offers a second way to create an object from a Fahrenheit value, and counts how many objects were created.

C++:

// temperature.cpp - one class in C++: private state, a validating constructor,
// a computed value, a static factory and a static counter.
#include <initializer_list>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <string>

class Temperature {
public:
    explicit Temperature(double celsius) {
        if (celsius < -273.15)
            throw std::invalid_argument("below absolute zero: " + format(celsius) + " C");
        celsius_ = celsius;
        ++created_;                                   // counts objects, not calls
    }

    static Temperature from_fahrenheit(double f) { return Temperature((f - 32) * 5 / 9); }
    static int created() { return created_; }

    double celsius() const { return celsius_; }
    double fahrenheit() const { return celsius_ * 9 / 5 + 32; }

    std::string to_string() const {
        return format(celsius()) + " C = " + format(fahrenheit()) + " F";
    }

private:
    static std::string format(double v) {
        std::ostringstream out;
        out << std::fixed << std::setprecision(1) << v;
        return out.str();
    }

    double celsius_ = 0;                              // one per object
    static inline int created_ = 0;                   // one per class (C++17)
};

int main() {
    Temperature room(21.5);
    Temperature boiling(100);
    Temperature cold = Temperature::from_fahrenheit(-40);

    for (const Temperature* t : {&room, &boiling, &cold})
        std::cout << t->to_string() << '\n';
    std::cout << "objects created: " << Temperature::created() << '\n';

    try {
        Temperature impossible(-300);
    } catch (const std::invalid_argument& e) {
        std::cout << "error: " << e.what() << '\n';
    }
    std::cout << "objects created: " << Temperature::created() << '\n';
}

Java:

// Temperature.java - the same class in Java.
import java.util.List;
import java.util.Locale;

public class Temperature {
    private static int created = 0;                   // one per class
    private final double celsius;                     // one per object

    public Temperature(double celsius) {
        if (celsius < -273.15)
            throw new IllegalArgumentException("below absolute zero: " + format(celsius) + " C");
        this.celsius = celsius;
        created++;
    }

    public static Temperature fromFahrenheit(double f) { return new Temperature((f - 32) * 5 / 9); }
    public static int created() { return created; }

    public double celsius() { return celsius; }
    public double fahrenheit() { return celsius * 9 / 5 + 32; }

    @Override
    public String toString() { return format(celsius()) + " C = " + format(fahrenheit()) + " F"; }

    private static String format(double v) { return String.format(Locale.ROOT, "%.1f", v); }

    public static void main(String[] args) {
        Temperature room = new Temperature(21.5);
        Temperature boiling = new Temperature(100);
        Temperature cold = Temperature.fromFahrenheit(-40);

        for (Temperature t : List.of(room, boiling, cold))
            System.out.println(t);
        System.out.println("objects created: " + Temperature.created());

        try {
            new Temperature(-300);
        } catch (IllegalArgumentException e) {
            System.out.println("error: " + e.getMessage());
        }
        System.out.println("objects created: " + Temperature.created());
    }
}

Python:

"""temperature.py - the same class in Python."""


class Temperature:
    created = 0                                   # class attribute: one per class

    def __init__(self, celsius):
        if celsius < -273.15:
            raise ValueError(f"below absolute zero: {celsius:.1f} C")
        self._celsius = celsius                   # instance attribute: one per object
        Temperature.created += 1

    @classmethod
    def from_fahrenheit(cls, f):
        return cls((f - 32) * 5 / 9)

    @property
    def celsius(self):
        return self._celsius

    @property
    def fahrenheit(self):
        return self._celsius * 9 / 5 + 32

    def __str__(self):
        return f"{self.celsius:.1f} C = {self.fahrenheit:.1f} F"


if __name__ == "__main__":
    room = Temperature(21.5)
    boiling = Temperature(100)
    cold = Temperature.from_fahrenheit(-40)

    for t in (room, boiling, cold):
        print(t)
    print("objects created:", Temperature.created)

    try:
        Temperature(-300)
    except ValueError as e:
        print("error:", e)
    print("objects created:", Temperature.created)

Output (identical for all three):

21.5 C = 70.7 F
100.0 C = 212.0 F
-40.0 C = -40.0 F
objects created: 3
error: below absolute zero: -300.0 C
objects created: 3
One class, three objects The Temperature class from the examples, after the program created three objects class Temperature Stored once, on the class created = 3 static field Code, shared by every object Temperature(celsius) constructor from_fahrenheit(f) static fahrenheit() method to_string() method Declared here, stored in each object celsius instance field room : Temperature celsius = 21.5 (70.7 F) boiling : Temperature celsius = 100.0 (212.0 F) cold : Temperature celsius = -40.0 (-40.0 F) instance of Each object holds only its own data. Methods and static members belong to the class, so every object reads the same counter: 3. Values from temperature.cpp, Temperature.java and temperature.py, which print identical output.
Each object keeps its own temperature; the class keeps the code and the count. That split is why one counter can read 3 for three different objects, and why a list assigned in a Python class body ends up shared.

The three versions make the same five design decisions:

  1. The data is private. celsius_ (C++) and celsius (Java) are private, and Python’s _celsius is private by convention. Callers read it through celsius() or the celsius property and cannot set it at all, so a Temperature cannot be changed into an impossible one after it is built.
  2. The constructor establishes a valid object. It checks the value first and throws (std::invalid_argument, IllegalArgumentException, ValueError) if it is below −273.15. The last two lines of the output show the result: the -300 object was never created, and the counter stayed at 3.
  3. Fahrenheit is computed, not stored. Each object holds one number; fahrenheit() derives the other. Storing both would let them disagree.
  4. from_fahrenheit is a static (class) method. It needs no existing object, so it belongs to the class. In C++ and Java it is static; in Python it is a @classmethod, which receives the class as cls and therefore also works for subclasses.
  5. created is per class, not per object. It is static in C++ and Java and a class attribute in Python. The counter is incremented after validation, so a failed construction does not count.

The C++ class here keeps everything in one file; in a larger program the declaration goes in a header and the member function definitions in a .cpp file. C++-specific details such as initializer lists, const member functions and the rules for compiler-generated constructors are in C++ classes explained and constructors and destructors in C++.

Constructors: Making Every Object Valid

A constructor runs once, when an object is created, and its job is to leave the object in a valid state. If it cannot, it should fail (throw an exception) rather than produce an object that breaks the class’s rules. That is what turns a class from a group of fields into a type with guarantees: every Temperature that exists is at or above absolute zero, so no method needs to check again.

Each language has a different default when a class declares no constructor:

SituationC++JavaPython
No constructor declaredCompiler supplies a default constructor (members of built-in type are left uninitialized unless they have initializers)Compiler supplies a no-argument constructor; fields get default values (0, null, false)object.__init__ is used; the object starts with no instance attributes
Several ways to constructOverloaded constructors, default argumentsOverloaded constructorsOne __init__; alternatives as @classmethod factories
Named alternative constructorsStatic member functionsStatic methods@classmethod

Python’s __init__ is strictly an initializer: the object already exists when it runs (it is created by __new__). In everyday code the difference rarely matters, and __init__ plays the role a constructor plays in C++ and Java.

Instance Members and Static Members

Every field is either stored in each object (an instance member) or stored once for the whole class (a static or class member). In C++ and Java the keyword static makes the choice explicit. In Python the choice depends on where you assign: in a method through self for an instance attribute, or in the class body for a class attribute. That second rule is where the bug from the introduction comes from:

"""class_attribute_pitfall.py - a mutable class attribute is shared by every
object; the fix is to create the list in __init__."""


class SharedCart:
    items = []                      # one list, stored on the class

    def add(self, item):
        self.items.append(item)     # finds the class's list and changes it


class Cart:
    def __init__(self):
        self.items = []             # a new list for each object

    def add(self, item):
        self.items.append(item)


a, b = SharedCart(), SharedCart()
a.add("book")
b.add("lamp")
print("SharedCart:", a.items, b.items, "same list:", a.items is b.items)

c, d = Cart(), Cart()
c.add("book")
d.add("lamp")
print("Cart:      ", c.items, d.items, "same list:", c.items is d.items)

Output:

SharedCart: ['book', 'lamp'] ['book', 'lamp'] same list: True
Cart:       ['book'] ['lamp'] same list: False

SharedCart.items is created once, when the class body runs. self.items.append(...) looks up items, does not find it on the object, finds the class’s list, and changes it, so both carts see both items and a.items is b.items is True. Cart creates a new list in __init__ for each object. The Python tutorial shows the same mistake with a list of tricks shared by every dog. Assigning (self.items = [...]) would have created an instance attribute instead, which is why the bug appears only with mutable values that are modified in place.

In C++ and Java the equivalent mistake requires writing static, so it is rarer, but the behavior is the same: one copy for the class (Java tutorial: Understanding Class Members, cppreference: static members).

UseInstance memberStatic / class member
StoredOnce per objectOnce per class
Accessed throughAn object: t.celsius()The class: Temperature::created(), Temperature.created
Can use instance dataYesNo (no this or self)
Typical usesThe object’s state and behaviorCounters, caches, constants, factory methods

Access Control: public, private and Python’s Conventions

C++ and Java enforce access at compile time: code outside the class that touches a private member does not compile. Python enforces nothing; it relies on naming conventions, plus one renaming rule:

"""name_mangling.py - Python has no private keyword. A leading underscore is a
convention; two leading underscores rename the attribute, which hides it from
accidental use but not from deliberate access."""


class Account:
    def __init__(self):
        self._balance = 100         # "internal": a convention only
        self.__pin = 1234           # stored as _Account__pin


acct = Account()
print("acct._balance =", acct._balance)
try:
    print(acct.__pin)
except AttributeError as e:
    print("AttributeError:", e)
print("acct._Account__pin =", acct._Account__pin)
print("attributes:", sorted(vars(acct)))

Output:

acct._balance = 100
AttributeError: 'Account' object has no attribute '__pin'
acct._Account__pin = 1234
attributes: ['_Account__pin', '_balance']
  • _balance (one underscore) is an ordinary attribute. The underscore tells other programmers it is internal, and tools such as linters respect that, but nothing stops access.
  • __pin (two leading underscores) is renamed to _Account__pin, as vars(acct) shows. acct.__pin raises AttributeError, but acct._Account__pin works. The renaming exists to prevent accidental name clashes in subclasses, not to provide security (Python tutorial: Private Variables).
LevelC++JavaPython
Anyonepublicpublicno prefix
The class and its subclassesprotectedprotected (also the same package)_name by convention
The class onlyprivateprivate__name (renamed, still reachable)
Default if unspecifiedprivate in a class, public in a structPackage-privatePublic

Why classes hide their data at all, and how to decide what to expose, is the subject of encapsulation; the C++ treatment is in encapsulation in C++.

Designing a Good Class

The syntax of a class takes minutes to learn; deciding what belongs in one takes longer. These checks catch most design problems:

  1. One responsibility. A class should have one reason to change. Temperature converts and validates temperatures; it does not format reports or read sensors.
  2. A rule it protects. If every field could take any value and every method is a getter or setter, the class is a record, and a struct, record or @dataclass says so more honestly.
  3. Valid from the first line. The constructor rejects bad input, so no method has to check again.
  4. The smallest public interface that works. Every public method is a promise to callers. Temperature exposes four operations and keeps formatting private.
  5. Derived values are computed. Fahrenheit is calculated from Celsius rather than stored next to it.

A class that passes these checks is also a good base for inheritance, because subclasses can rely on the rules it enforces.

Key Takeaways

  • A class is a type: it defines the fields each object holds and the methods that operate on them. Objects are its instances.
  • The same design reads the same in C++, Java and Python; the three Temperature classes printed identical output.
  • Constructors guarantee validity. The -300 object was rejected, and the counter stayed at 3.
  • Static members are shared. One created counter served all three objects.
  • In Python, where you assign decides what is shared. A list in the class body was shared by every cart.
  • C++ and Java enforce private; Python uses conventions. __pin was renamed to _Account__pin, not hidden.

Frequently Asked Questions

Conclusion

A class is a decision about what belongs together: which data, which operations, and which rules every object of that type will keep. The syntax differs between C++, Java and Python, but the decisions are the same, and so are the mistakes, such as storing something once that should exist per object. Getting those decisions right is what makes the other object-oriented ideas, from encapsulation to inheritance and polymorphism, work on top of it.

More object-oriented programming topics are collected in the OOP section.

Source Code and Tests

classes-concepts

All programs on this page are in the oop/classes-concepts directory of the MYCPLUS C++ examples repository.

Build and test:

bash tests/run_tests.sh g++

What the build checks. On Linux, with GCC and with Clang, it compiles the C++ program with -Wall -Wextra -pedantic -Werror, compiles the Java program with javac -Xlint:all -Werror on Java 21, runs the Python programs with -W error on Python 3.12, and checks that every program prints exactly the output shown on this page. Python 3.11 and 3.13 were checked locally and produced the same output.

Scroll to Top