Objects in Object-Oriented Programming: Identity, State, Copying and Lifetime in C++, Java and Python

Why q = p1 copies an object in C++ but shares it in Java and Python, how equality differs from identity, and how each language ends an object's life.

Four identical blue lockers, two closed and two open holding different green items, representing objects of one class with their own identity and state.

Write q = p1, then q.x = 99. In C++, p1.x is still 1. In Java and Python, it is now 99. The three programs are line-for-line the same, and all three behave correctly: they disagree about what a variable holds. In C++ a variable is an object; in Java and Python it refers to one. Most surprises with objects, from shared data to equality checks that fail, come from that difference.

This guide explains objects as an object-oriented concept, with the same examples in C++, Java and Python: what an object is, the difference between equality and identity, what assignment and copying do, and how an object’s lifetime ends. 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 every output below is captured verbatim. The code is in a GitHub repository whose build repeats these checks on each commit.

The Short Answer

What is an object in OOP? A value created from a class at run time. It has its own state (the values of its fields), the behavior its class defines (methods), and an identity that distinguishes it from every other object, even one with the same values.

What is the difference between a class and an object? The class is the definition, written once; objects are its instances, created as the program runs. One class can produce any number of objects, each with its own data. Classes are covered in classes in object-oriented programming.

Are two objects with the same values the same object? No. They are equal but not identical. In all three languages below, two Point(1, 2) objects compared equal, and the test for “same object” returned false.

What Is an Object?

An object in object-oriented programming is a run-time instance of a class. It combines state (the current values of its fields), behavior (the methods its class defines) and identity (the property that makes it a distinct object, independent of its values). Objects interact by calling each other’s methods, and each object is responsible for keeping its own state valid.

Those three properties, state, behavior and identity, are the classic description of an object in object-oriented design (Grady Booch uses exactly these three). In code they look like this:

PropertyMeaningExample
StateThe values of the object’s fields right nowA Point with x = 1, y = 2
BehaviorWhat the object can do, defined by its classfahrenheit() on a Temperature
IdentityWhich object it is, independent of its stateTwo Point(1, 2) objects are still two objects

Python’s language reference puts identity first: every object has an identity, a type and a value, and the identity never changes once the object exists (Python data model).

Equality, Identity and Assignment

Two questions sound alike and are not: do these two objects have the same value? (equality) and are these the same object? (identity). Each language spells them differently:

QuestionC++JavaPython
Same value?a == b, if the class defines operator==a.equals(b), if the class overrides ita == b, which calls __eq__
Same object?&a == &b (compare addresses)a == ba is b
What b = a doesCopies the objectCopies the referenceBinds a second name to the object

This program asks both questions and then assigns one variable to another:

C++:

// identity.cpp - equality, identity and what assignment does in C++.
// A variable of class type holds the object itself, so assignment copies it.
#include <iostream>

struct Point {
    int x = 0, y = 0;
    bool operator==(const Point& other) const { return x == other.x && y == other.y; }
};

int main() {
    Point p1{1, 2};
    Point p2{1, 2};
    std::cout << std::boolalpha;
    std::cout << "p1 == p2 (equal values):  " << (p1 == p2) << '\n';
    std::cout << "&p1 == &p2 (same object): " << (&p1 == &p2) << '\n';

    Point q = p1;          // copies the object
    q.x = 99;
    std::cout << "after q = p1; q.x = 99:   p1.x = " << p1.x << ", q.x = " << q.x << '\n';

    Point& r = p1;         // a reference is a second name for p1
    r.x = 99;
    std::cout << "after r = ref to p1; r.x = 99:   p1.x = " << p1.x << '\n';
}

Output:

p1 == p2 (equal values):  true
&p1 == &p2 (same object): false
after q = p1; q.x = 99:   p1.x = 1, q.x = 99
after r = ref to p1; r.x = 99:   p1.x = 99

Java:

// Identity.java - equality, identity and what assignment does in Java.
// A variable of class type holds a reference, so assignment shares the object.
import java.util.Objects;

public class Identity {
    static final class Point {
        int x, y;
        Point(int x, int y) { this.x = x; this.y = y; }
        Point(Point other) { this(other.x, other.y); }      // copy constructor

        @Override public boolean equals(Object o) {
            return o instanceof Point p && p.x == x && p.y == y;
        }
        @Override public int hashCode() { return Objects.hash(x, y); }
    }

    public static void main(String[] args) {
        Point p1 = new Point(1, 2);
        Point p2 = new Point(1, 2);
        System.out.println("p1.equals(p2) (equal values): " + p1.equals(p2));
        System.out.println("p1 == p2 (same object):       " + (p1 == p2));

        Point q = p1;                  // copies the reference, not the object
        q.x = 99;
        System.out.println("after q = p1; q.x = 99:   p1.x = " + p1.x + ", q.x = " + q.x);

        p1.x = 1;
        Point c = new Point(p1);       // an explicit copy
        c.x = 99;
        System.out.println("after c = copy of p1; c.x = 99:   p1.x = " + p1.x);
    }
}

Output:

p1.equals(p2) (equal values): true
p1 == p2 (same object):       false
after q = p1; q.x = 99:   p1.x = 99, q.x = 99
after c = copy of p1; c.x = 99:   p1.x = 1

Python:

"""identity.py - equality, identity and what assignment does in Python.
A name refers to an object, so assignment shares it."""
import copy
from dataclasses import dataclass


@dataclass
class Point:          # @dataclass generates __init__ and a value-based __eq__
    x: int
    y: int


p1 = Point(1, 2)
p2 = Point(1, 2)
print("p1 == p2 (equal values):", p1 == p2)
print("p1 is p2 (same object): ", p1 is p2)

q = p1                 # binds a second name to the same object
q.x = 99
print(f"after q = p1; q.x = 99:   p1.x = {p1.x}, q.x = {q.x}")

p1.x = 1
c = copy.copy(p1)      # an explicit copy
c.x = 99
print(f"after c = copy of p1; c.x = 99:   p1.x = {p1.x}")

Output:

p1 == p2 (equal values): True
p1 is p2 (same object):  False
after q = p1; q.x = 99:   p1.x = 99, q.x = 99
after c = copy of p1; c.x = 99:   p1.x = 1
What q = p1 does, then q.x = 99 The same two statements, run in identity.cpp, Identity.java and identity.py C++: assignment copies a variable holds the object itself Point p1 x = 1 y = 2 Point q x = 99 y = 2 two objects: p1.x is still 1 Java, Python: assignment shares a variable holds a reference p1 q Point object x = 99 y = 2 one object: p1.x is now 99 Each language can do the other on request: share in C++: Point& r = p1; copy in Java: new Point(p1); copy in Python: copy.copy(p1) Equal values are not the same object: p1 and p2 compared equal, and the same-object test was false, in all three.
The same two lines leave two objects in C++ and one in Java and Python. A C++ variable holds its object, so assignment makes a copy; a Java or Python variable holds a reference, so assignment gives the object a second name.

The first two lines agree everywhere: p1 and p2 hold equal values and are different objects. The third line is where the languages part:

  • C++ copied. Point q = p1; created a second Point with the same values. Changing q.x left p1.x at 1. To get a second name for the same object, C++ needs an explicit reference, Point& r = p1;, and then r.x = 99 did change p1.
  • Java and Python shared. q = p1 copied a reference, so q and p1 name one object, and q.x = 99 changed what p1 sees too. To get an independent object they need an explicit copy: a copy constructor in Java, copy.copy in Python.

Two details are easy to miss. In Java, == on objects always compares identity; equals compares values only if the class overrides it, and a class that overrides equals must also override hashCode so that equal objects produce equal hash codes (Java Object documentation). Without hashCode, two equal points would be treated as different keys in a HashMap. In Python, @dataclass generated the value-based __eq__; a plain class would fall back to identity, and p1 == p2 would have printed False.

The same rule explains how objects are passed to functions. A C++ function that takes a Point by value gets a copy, while Java and Python functions receive a reference to the caller’s object; the C++ choices and their costs are measured in functions in C++.

Copying Objects: Shallow and Deep

When an object contains other objects, “copy” has two meanings. A shallow copy makes a new outer object whose fields refer to the same inner objects. A deep copy copies the inner objects as well:

"""shallow_copy.py - copy.copy makes a new outer object but shares the objects
inside it; copy.deepcopy copies those too."""
import copy


class Playlist:
    def __init__(self, name, songs):
        self.name = name
        self.songs = songs


original = Playlist("road trip", ["intro", "chorus"])
shallow = copy.copy(original)
deep = copy.deepcopy(original)

shallow.name = "copy"              # rebinding: affects only the copy
shallow.songs.append("bridge")     # mutating a shared list: affects `original` too

print("original:", original.name, original.songs)
print("shallow: ", shallow.name, shallow.songs)
print("deep:    ", deep.name, deep.songs)
print("shallow shares songs:", shallow.songs is original.songs)
print("deep shares songs:   ", deep.songs is original.songs)

Output:

original: road trip ['intro', 'chorus', 'bridge']
shallow:  copy ['intro', 'chorus', 'bridge']
deep:     road trip ['intro', 'chorus']
shallow shares songs: True
deep shares songs:    False

Renaming the shallow copy did not affect original, because name was rebound to a new string. Appending to shallow.songs did, because both playlists hold the same list: shallow.songs is original.songs is True. The deep copy kept its own list and was unaffected. The copy module documentation describes the same distinction.

LanguageDefault copyHow to get a deep copy
C++Member-by-member copy; members such as std::vector and std::string copy their contents, raw pointers copy only the addressUsually automatic with value members; write a copy constructor for raw owning pointers
JavaNo copy unless written; clone() is shallowA copy constructor or factory that copies the nested objects
PythonNo copy on assignment; copy.copy is shallowcopy.deepcopy

C++ is the odd one out again: because its standard containers are values, the default copy of a class built from them is already deep. The exception is a raw owning pointer, where the default copy shares the pointed-to memory, a case covered in C++ memory management.

Object Lifetime: Creation, Use and Cleanup

Every object is created, used and eventually destroyed. Creation runs a constructor (covered with classes); destruction is where the languages differ most:

  • C++ destroys an object at a point the program determines: at the end of its scope for a local variable, or when its owner deletes it. The destructor runs then, which lets an object release files, locks or memory exactly when it goes away. This is called RAII (resource acquisition is initialization) (cppreference: RAII).
  • Java objects are reclaimed by the garbage collector at a time the program does not control. Finalizers, the old hook for cleanup, are deprecated for removal (JEP 421); resources are released with try-with-resources, which calls close() at the end of the block.
  • Python (CPython) frees most objects as soon as their last reference disappears, but the language reference says not to depend on immediate finalization and to close resources explicitly; the with statement does that.

The three programs below release a resource at the same point, by three different mechanisms:

C++:

// lifetime.cpp - an object that owns a resource releases it when its scope
// ends: the destructor runs at a known point (RAII).
#include <iostream>
#include <string>
#include <utility>

class LogFile {
public:
    explicit LogFile(std::string name) : name_(std::move(name)) { std::cout << "open " << name_ << '\n'; }
    ~LogFile() { std::cout << "close " << name_ << '\n'; }
    LogFile(const LogFile&) = delete;              // one owner of the file
    LogFile& operator=(const LogFile&) = delete;

    void write(const std::string& line) { std::cout << "write " << line << '\n'; }

private:
    std::string name_;
};

int main() {
    {
        LogFile log("app.log");
        log.write("started");
    }                                              // ~LogFile runs here
    std::cout << "after the block\n";
}

Java:

// Lifetime.java - Java has no destructors; try-with-resources calls close()
// when the block ends, which gives the same deterministic cleanup.
public class Lifetime {
    static final class LogFile implements AutoCloseable {
        private final String name;
        LogFile(String name) { this.name = name; System.out.println("open " + name); }
        void write(String line) { System.out.println("write " + line); }
        @Override public void close() { System.out.println("close " + name); }
    }

    public static void main(String[] args) {
        try (LogFile log = new LogFile("app.log")) {
            log.write("started");
        }                                          // close() runs here
        System.out.println("after the block");
    }
}

Python:

"""lifetime.py - a with statement calls __exit__ when the block ends, so the
cleanup does not depend on when the object is garbage collected."""


class LogFile:
    def __init__(self, name):
        self.name = name
        print("open", name)

    def write(self, line):
        print("write", line)

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc, tb):
        print("close", self.name)
        return False                               # do not swallow exceptions


if __name__ == "__main__":
    with LogFile("app.log") as log:
        log.write("started")                       # __exit__ runs after this block
    print("after the block")

Output (identical for all three):

open app.log
write started
close app.log
after the block

close app.log printed before after the block in every language. The difference is what guarantees it. In C++ it is the object itself: any LogFile that goes out of scope closes its file, with no special syntax at the call site. In Java and Python it is the try or with statement around the object; a LogFile created without one would be closed only if someone remembered to call close(). The C++ class also deletes its copy operations, so two objects can never both believe they own the same file. Smart pointers apply the same idea to heap objects, as described in smart pointers in C++.

Where Objects Live

LanguageWhere objects are storedWho decides when they are destroyed
C++Anywhere: in a local variable (automatic storage), inside another object, or on the heap with new or std::make_uniqueThe program: scope end, or the owning object or smart pointer
JavaOn the heap; variables hold references (the JIT compiler may optimize some allocations away)The garbage collector
PythonOn the heap; every value, including integers and functions, is an objectReference counting, plus a cycle collector, in CPython

For day-to-day code the third column matters more than the second: it decides whether cleanup can live in the object (C++) or must be requested by the code that uses it (Java and Python).

Key Takeaways

  • An object has state, behavior and identity. Two objects can be equal in value and still be two objects.
  • Assignment differs by language. q = p1 copied the object in C++ and shared it in Java and Python, so p1.x ended as 1 in one and 99 in the others.
  • Equality must be defined. C++ needs operator==, Java needs equals and hashCode, Python needs __eq__ or a @dataclass.
  • A shallow copy shares inner objects. Appending to the copy’s list changed the source object’s list too.
  • Cleanup is deterministic in all three only if you ask for it the right way: a destructor in C++, try-with-resources in Java, with in Python.

Conclusion

Objects are where a program’s data actually lives, and most bugs involving them are questions of identity in disguise: which object does this variable mean, does this copy share anything with the object it came from, and who decides when it goes away. C++, Java and Python answer those questions differently, but each answer is consistent, and once you know which one your language gives, the surprising results above become predictable.

The rest of the object-oriented series starts from here: inheritance defines new classes from existing ones, and more topics are collected in the OOP section.

Source Code and Tests

objects-concepts

All programs on this page are in the oop/objects-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++ programs with -Wall -Wextra -pedantic -Werror, compiles the Java programs 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, with a separate expected output per language for the identity example. Python 3.11 and 3.13 were checked locally and produced the same output.

Scroll to Top