Classes and Objects in Object-Oriented Programming
Learning Objectives
By the end of this page, you will be able to:
- Define a class and explain how it differs from an object
- Instantiate objects from a class in Java, Python, and C++
- Explain the role of constructors and destructors in an object's lifecycle
- Distinguish instance members from class (static) members
- Explain how
this/selflets an object refer to itself - Describe the basic purpose of access modifiers (public, private, protected)
- Trace an object's lifecycle from creation to destruction
- Identify common mistakes students make when reasoning about classes vs. objects
Quick Answer
A class is a blueprint that defines what data (attributes) and behavior (methods) a group of objects will have. An object is a concrete instance created from that blueprint, living in memory with its own copy of the attributes. Classes matter because they let you model real-world entities and reuse code: you write the behavior once in the class, then stamp out as many independent objects as you need. Constructors initialize a new object, this/self lets code inside a class refer to "the current object," and access modifiers control what parts of an object other code is allowed to touch. Together, these ideas are the foundation of encapsulation and reuse in OOP.
Core Content
1. Class Definition
Definition: A class is a user-defined data type that bundles together data (attributes/fields) and the functions (methods) that operate on that data, under one name.
Explanation: Before OOP, code was often organized as separate data structures and separate functions that acted on them, with no formal link between the two. A class fixes that by grouping related data and behavior together and giving the compiler/interpreter a template it can use to generate many similar objects. Declaring a class does not use any memory for objects — it only registers the shape future objects will have (which fields exist, what methods are available). Memory is only allocated once you instantiate the class.
Simple Example (Python):
class Car:
def __init__(self, model, color):
self.model = model
self.color = color
def drive(self):
print(f"The {self.color} {self.model} is driving.")
Simple Example (Java):
public class Car {
private String model;
private String color;
public Car(String model, String color) {
this.model = model;
this.color = color;
}
public void drive() {
System.out.println("The " + color + " " + model + " is driving.");
}
}
Real-World Example: An architect's blueprint for a housing model isn't a house you can live in — it's a plan that says "every house built from this plan has two bedrooms, one kitchen, and a front door that can be opened." The class is the blueprint; each house built from it is a separate object.
Why It Matters: Classes are the unit of reuse and organization in OOP. Once a Car class exists, any part of a program can create as many cars as it needs without rewriting the logic for how a car behaves.
Common Misunderstanding: Students often think defining a class itself creates something in memory that "is" a car. It doesn't — a class is metadata/template only. No model or color values exist until you instantiate it.
2. Object Instantiation
Definition: Instantiation is the process of creating an actual object in memory from a class, using the new keyword (Java/C++) or by simply calling the class name (Python).
Explanation: When you instantiate a class, the runtime allocates memory for the object's fields, links the object to the class's method table, and runs a constructor to set the object into a valid initial state. Each object gets its own storage for instance fields, but the code of the methods is shared — it isn't duplicated for every object.
Simple Example (C++):
#include <iostream>
using namespace std;
class Car {
public:
string model;
string color;
Car(string m, string c) : model(m), color(c) {}
void drive() {
cout << "The " << color << " " << model << " is driving." << endl;
}
};
int main() {
Car car1("Toyota Corolla", "red"); // stack-allocated object
Car* car2 = new Car("Honda Civic", "blue"); // heap-allocated object
car1.drive();
car2->drive();
delete car2; // must free heap memory manually in C++
return 0;
}
Simple Example (Python):
car1 = Car("Toyota Corolla", "red")
car2 = Car("Honda Civic", "blue")
car1.drive() # The red Toyota Corolla is driving.
car2.drive() # The blue Honda Civic is driving.
Real-World Example: A cookie cutter (class) doesn't become a cookie until you press it into dough (instantiation). Each pressed-out cookie (object) is separate — eating one doesn't affect the others, even though they came from the same cutter.
Why It Matters: Instantiation is what turns your design into working, independent data at runtime. Without it, a class is just unused code.
Common Misunderstanding: Beginners sometimes assume that changing one object's attribute changes all objects made from the same class. It doesn't — instance data is per-object unless the field is explicitly declared static/class-level.
3. Constructors and Destructors
Definition: A constructor is a special method automatically invoked when an object is created, used to initialize its state. A destructor (or finalizer/cleanup hook) runs when an object is destroyed, used to release resources.
Explanation: Constructors have the same name as the class (C++/Java) or are named __init__ (Python, called after the object is already allocated by __new__). They typically assign initial values to fields and can validate arguments. Destructors matter most in languages with manual or deterministic memory management: in C++, ~ClassName() runs when a stack object goes out of scope or when delete is called on a heap object, giving you a place to close files, free memory, or release locks. Java and Python use garbage collection, so there's no deterministic destructor — Java has finalize() (deprecated, unreliable) and try-with-resources/AutoCloseable instead; Python has __del__, which also isn't guaranteed to run at a specific time.
Simple Example (Java — constructor overloading):
public class Car {
private String model;
private String color;
// Constructor 1
public Car(String model, String color) {
this.model = model;
this.color = color;
}
// Constructor 2 (overloaded, default color)
public Car(String model) {
this(model, "white");
}
}
Simple Example (C++ — constructor and destructor):
class FileHandler {
public:
FileHandler(string name) {
cout << "Opening " << name << endl;
}
~FileHandler() {
cout << "Closing file automatically" << endl;
}
};
void useFile() {
FileHandler f("data.txt"); // constructor runs
} // destructor runs automatically here, when f goes out of scope
Real-World Example: Checking into a hotel room (constructor) sets up the room for you — keys issued, lights on. Checking out (destructor) releases the room: keys returned, lights off, so the next guest can use it.
Why It Matters: Constructors guarantee an object never exists in a half-initialized, invalid state. Destructors (where deterministic) prevent resource leaks like unclosed files or dangling connections.
Common Misunderstanding: Students assume Java/Python objects are destroyed the instant they go out of scope, like in C++. They aren't — garbage collection is non-deterministic, so relying on __del__/finalize() for critical cleanup (like closing a database connection) is unsafe. Use try/finally, with blocks, or try-with-resources instead.
4. Instance vs. Class (Static) Members
Definition: Instance members belong to a specific object and have separate storage per object. Class (static) members belong to the class itself and are shared by every object of that class.
Explanation: Instance fields are declared normally inside the class and initialized per-object, usually in the constructor. Static/class fields are declared with the static keyword (Java/C++) or defined directly on the class body outside __init__ (Python) and exist exactly once regardless of how many objects are created. This makes them useful for data that's genuinely shared, like a running count of how many objects exist, or a constant configuration value.
Simple Example (Python):
class Car:
total_cars = 0 # class variable, shared by all instances
def __init__(self, model):
self.model = model # instance variable
Car.total_cars += 1
c1 = Car("Corolla")
c2 = Car("Civic")
print(Car.total_cars) # 2
Simple Example (Java):
public class Car {
private String model; // instance member
private static int totalCars = 0; // class member
public Car(String model) {
this.model = model;
totalCars++;
}
public static int getTotalCars() {
return totalCars;
}
}
Real-World Example: Each student (object) has their own roll number (instance member), but the school's name (class member) is the same for every student and doesn't change per student.
Why It Matters: Static members let you track or share data across all instances without passing it around manually — e.g., a counter, a shared cache, or a constant like Math.PI.
Common Misunderstanding: Students often try to access a static member through an object reference and think it "belongs" to that object (e.g., car1.totalCars = 5). In reality this either creates a new instance attribute (Python) or modifies the shared class value in a misleading way (Java allows car1.totalCars syntactically but it still refers to the one shared field) — the safe convention is always to access static members via the class name.
5. this / self Reference
Definition: this (Java, C++) and self (Python, by convention) is an implicit reference inside instance methods that points to the specific object the method was called on.
Explanation: When you call car1.drive(), the method body needs a way to know it's operating on car1's data and not car2's. In Java/C++, this is passed implicitly by the compiler. In Python, self is passed explicitly as the first parameter of every instance method — Python doesn't hide it, which is why every method signature starts with self. this/self is mainly used to disambiguate a field from a same-named constructor/method parameter, and to return the current object (return this;) to support method chaining.
Simple Example (C++):
class Car {
public:
string model;
Car(string model) {
this->model = model; // 'this->model' is the field, 'model' is the parameter
}
};
Simple Example (Python):
class Car:
def __init__(self, model):
self.model = model # 'self.model' is the field, 'model' is the parameter
def rename(self, new_model):
self.model = new_model
return self # enables chaining: car.rename("X").rename("Y")
Real-World Example: When you say "my address," the word "my" plays the same role as this/self — it tells the listener which specific person's address you mean, out of everyone in the room.
Why It Matters: Without this/self, a method would have no way to tell which object's data to read or modify, since the method's code is shared across all instances.
Common Misunderstanding: Beginners think self in Python is a keyword like this in Java. It's actually just a strongly-followed naming convention — you could technically name it anything, but every Python programmer expects self, so deviating from it makes code confusing.
6. Access Modifiers (Basics)
Definition: Access modifiers control which parts of a program can read or modify a class's fields and methods — commonly public, private, and protected.
Explanation: public members are accessible from anywhere. private members are accessible only within the class itself. protected members are accessible within the class and its subclasses. This is the mechanism behind encapsulation — hiding internal implementation details and exposing only a controlled interface (usually via getters/setters), so the class can change its internals later without breaking code that uses it.
Simple Example (Java):
public class BankAccount {
private double balance; // hidden from outside code
public double getBalance() {
return balance;
}
public void deposit(double amount) {
if (amount > 0) balance += amount; // validation lives in one place
}
}
Simple Example (Python — convention-based):
class BankAccount:
def __init__(self):
self.__balance = 0 # name-mangled, weakly "private" by convention
def deposit(self, amount):
if amount > 0:
self.__balance += amount
def get_balance(self):
return self.__balance
Real-World Example: An ATM lets you deposit and withdraw money (public methods) but doesn't let you directly reach into the vault and change the balance number yourself (private data) — every change goes through validated, controlled operations.
Why It Matters: Access modifiers prevent external code from putting an object into an invalid state (e.g., setting a bank balance to a negative number directly) and let the class enforce its own rules.
Common Misunderstanding: Python's single/double underscore (_balance, __balance) is often believed to make a field truly inaccessible like Java's private. It doesn't — Python has no enforced private access; __balance is just name-mangled to _ClassName__balance and can still be accessed if you know the mangled name. It's a convention, not a hard restriction.
7. Object Lifecycle
Definition: The object lifecycle is the sequence of stages an object goes through: declaration, memory allocation, construction/initialization, usage, and destruction/garbage collection.
Explanation: In garbage-collected languages (Java, Python), the lifecycle is: (1) new/class-call triggers memory allocation, (2) constructor runs, (3) the object is used and referenced by variables, (4) once no references remain, the garbage collector reclaims the memory at some later, unspecified time. In C++, the lifecycle is deterministic: stack objects are destroyed the instant their scope ends; heap objects (created with new) live until explicitly deleted, and forgetting to do so causes a memory leak.
Simple Example (Java, GC-based):
Car car = new Car("Corolla", "red"); // allocation + construction
car.drive(); // usage
car = null; // no more references; eligible for GC
// JVM decides when (or if) to actually reclaim the memory
Real-World Example: A library book is checked out (allocated), read (used), and returned (destroyed/freed) so it can be reused for the next request — but in a garbage-collected system, it's like the book automatically vanishes back to the shelf sometime after everyone stops reading it, not necessarily the moment you put it down.
Why It Matters: Understanding the lifecycle explains bugs like memory leaks (forgetting delete in C++) and why you can't rely on a destructor to run immediately in Java/Python.
Common Misunderstanding: Students think setting a Java/Python object reference to null/None immediately destroys the object. It only removes one reference; the object is only eligible for collection once no references remain, and actual reclamation timing is up to the garbage collector.
Key Terms
| Term | Definition | Context/Related Concepts |
|---|---|---|
| Class | A blueprint defining attributes and methods for a category of objects | Foundation of OOP; template for objects |
| Object | A concrete instance of a class, with its own state in memory | Created via instantiation |
| Instantiation | The act of creating an object from a class | new in Java/C++, direct call in Python |
| Constructor | Special method that initializes a new object | Same name as class (Java/C++), __init__ (Python) |
| Destructor | Special method/routine that runs when an object is destroyed | ~ClassName() in C++; unreliable in GC languages |
| Instance member | Field/method belonging to a specific object | Separate copy per object |
| Static/Class member | Field/method belonging to the class, shared by all objects | Declared with static (Java/C++) or at class body level (Python) |
this/self | Implicit reference to the current object inside a method | Disambiguates fields from parameters |
| Access modifier | Keyword controlling visibility of members (public, private, protected) | Basis of encapsulation |
| Encapsulation | Hiding internal state and exposing a controlled interface | Achieved via access modifiers + getters/setters |
| Garbage collection | Automatic reclamation of memory for unreferenced objects | Used in Java, Python; not deterministic |
Common Mistakes
Misconception 1: "A class and an object are basically the same thing, just different names." Why It's Wrong: A class is a static template that exists once in code; an object is a distinct, dynamic instance that exists in memory at runtime, and many objects can come from one class simultaneously. Correct Understanding: Think "class = recipe, object = the dish you cooked." You can cook many dishes from one recipe, and changing one dish doesn't change the others.
Misconception 2: "Static/class variables act like instance variables, just accessed a different way." Why It's Wrong: A static variable has exactly one copy shared across every object of the class; modifying it through any object changes the value seen by all objects, unlike instance variables which are independent per object. Correct Understanding: Use static members only for genuinely shared data (counters, constants, shared configuration), and always prefer accessing them via the class name to avoid confusion.
Misconception 3: "In Java or Python, you can rely on the destructor/__del__/finalize() to clean up resources like open files or network connections at a predictable time."
Why It's Wrong: Garbage collection timing is non-deterministic — an unreferenced object might sit uncollected for a while, or in some cases never be collected before program exit.
Correct Understanding: Use deterministic cleanup mechanisms instead: try/finally or with (context managers) in Python, and try-with-resources/AutoCloseable in Java.
Comparison and Connections
| Concept A | Concept B | Key Difference |
|---|---|---|
| Class | Object | Class is the blueprint (compile-time template); object is the runtime instance with actual data |
| Instance variable | Class (static) variable | Instance variable has separate storage per object; static variable has one shared copy for the whole class |
| Constructor | Regular method | Constructor runs automatically once at creation to initialize state; a regular method is called explicitly, any number of times, after the object exists |
| Stack allocation | Heap allocation (C++) | Stack objects are destroyed automatically at end of scope; heap objects persist until explicitly deleted, risking leaks if forgotten |
public | private | public members are accessible from any code; private members are accessible only within the defining class |
| Destructor (C++) | Garbage collection (Java/Python) | Destructor runs deterministically at scope end/delete; GC runs at an unspecified time chosen by the runtime |
Practice Questions
Recall
-
What is the difference between a class and an object? Answer: A class is a blueprint/template defining attributes and methods; an object is an actual instance of that class created in memory, with its own data.
-
Which special method is automatically called when a new object is created in Python? Answer:
__init__().
Understanding
-
Why does Java allow multiple constructors (constructor overloading) for the same class? Answer: To let objects be created with different sets of initial information (e.g., with or without an optional parameter) while reusing shared initialization logic, often via
this(...)chaining to another constructor. -
Why can't you reliably use a destructor in Java to close a database connection the moment it's no longer needed? Answer: Java uses garbage collection, which reclaims memory (and calls
finalize(), if used) at a time the JVM decides, not immediately when the object becomes unreachable. Deterministic cleanup requirestry-with-resources/AutoCloseableinstead.
Application
-
You need a
Bookclass where every book shares onelibraryNamefield but has its own uniquetitle. How would you declare each in Java? Answer:private static String libraryName;for the shared field, andprivate String title;for the per-object field, set via the constructor. -
Write the C++ code to create one
Carobject on the stack and one on the heap, and correctly clean up the heap one. Answer:Car stackCar("Corolla", "red"); // destroyed automatically at scope endCar* heapCar = new Car("Civic", "blue"); // must manage manuallyheapCar->drive();delete heapCar; // prevents memory leak
Analysis
-
A student writes a Python class where
total_countis defined inside__init__asself.total_count = 0instead of at the class body level, expecting it to track the number of objects ever created. Why won't this work as intended? Answer:self.total_count = 0creates a new instance variable for every object, each starting at 0 and incrementing independently — it never becomes a shared counter. To track a count across all instances,total_countmust be defined as a class-level variable and incremented viaClassName.total_count += 1. -
Compare what happens in memory when you run
Car car2 = car1;in Java versusCar car2 = car1;in C++ (assuming a normal, non-pointerCarobject in C++). Answer: In Java, bothcar1andcar2are references to the same object on the heap — modifyingcar2affects whatcar1sees too. In C++, this invokes the copy constructor, creating an entirely independent copy of the object; changes tocar2do not affectcar1.
FAQ
Q: Do I need to explicitly write a constructor, or does every class get one automatically?
A: If you don't write any constructor, Java and C++ provide a default no-argument constructor that does nothing beyond default-initializing fields. Python doesn't require __init__ either — objects can be created without it, though most real classes define one to set up meaningful state.
Q: Can a class have more than one constructor?
A: In Java and C++, yes — this is called constructor overloading, distinguished by parameter lists. Python doesn't support true overloading; you typically use default argument values or classmethod factory functions to achieve similar flexibility.
Q: What's the difference between an instance method and a static method?
A: An instance method receives an implicit reference to the calling object (this/self) and can access that object's instance fields. A static method belongs to the class, has no access to this/self, and can only work with static data or its own parameters.
Q: Is self required in every Python method?
A: Yes, for instance methods — Python passes the calling object explicitly as the first parameter, and by convention it's named self. Static methods (marked @staticmethod) and class methods (@classmethod, which takes cls instead) are the exceptions.
Q: Why does C++ need destructors while Java and Python largely don't?
A: C++ uses manual/deterministic memory management — if you allocate memory with new, nothing frees it automatically, so a destructor is your chance to release it and other resources exactly when an object dies. Java and Python use automatic garbage collection, so plain memory doesn't need manual freeing, though other resources (files, sockets) still need explicit closing.
Q: If two objects are created from the same class, do they share memory? A: No, each object gets its own separate memory for instance fields. They only "share" the method code (which is stored once, associated with the class) and any static fields, which genuinely do have a single shared copy.
Quick Revision
- Class = blueprint/template; Object = instance created from that blueprint, with its own memory.
- Instantiation creates an object; in Java/C++ typically via
new, in Python by calling the class name. - Constructors initialize a new object's state and run automatically once at creation.
- Destructors run deterministically in C++ (
~ClassName()); Java/Python rely on non-deterministic garbage collection instead. - Instance members have separate storage per object; static/class members have exactly one shared copy.
this(Java/C++) andself(Python) refer to the current object inside instance methods.- Access modifiers (
public,private,protected) implement encapsulation by controlling visibility. - Python has no true private access —
__fieldis name-mangled, not enforced. - Stack-allocated C++ objects are destroyed automatically at scope end; heap objects need explicit
delete. - Assigning
null/Noneremoves one reference to an object; it doesn't force immediate destruction. - Always access static/class members via the class name, not an object reference, to avoid confusion.
- Constructor overloading (Java/C++) lets a class support multiple ways of being initialized.
Related Topics
Prerequisites
- Variables, data types, and functions
- Basic control flow (if/else, loops)
Related Topics
- Encapsulation and access control
- Constructors and constructor overloading
- Static vs. instance methods
Next Topics
- Inheritance
- Polymorphism
- Abstraction and interfaces