AlgoMaster Logo

Classes & Objects

High Priority16 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

A class is the C++ tool for describing a kind of thing once and creating many instances of it. Where a struct is usually a bundle of data, a class is what most C++ code uses when a type carries both data and the operations that go with it. This lesson covers what a class is, how to define one, how to create objects on the stack and the heap, how to access their members, and how to split a class across header and implementation files. Constructors, destructors, access modifiers, and the rest of the OOP machinery get their own dedicated chapters; here we stay at the level of "what is a class and how do you use one".

What a Class Is

A class is a user-defined type. It tells the compiler that "a Product is a thing that has a name, a price, a stock count, and the following operations: print itself, restock, mark out of stock". The class itself is not a product; it is the description. You then create objects (also called instances) of that class. Each object is a separate Product with its own name, its own price, its own stock count.

The class is the blueprint. The object is the thing built from the blueprint. From one Product class, you can create thousands of Product objects, each independent.

The class on the left says "this is what a product looks like". The three objects on the right are separate values that all match that description. They share a layout, not their data.

Why do this at all? Two reasons. First, it lets the code talk in domain terms (Product, Cart, Order) instead of loose primitives. A function that takes a Product is clearly about products, not about a string, a double, and an int that happen to be related. Second, a class can keep its data and the operations on that data together, so calling mouse.restock(10) is right next to the field it modifies.

Defining a Class

A minimal Product class with three data members and two member functions:

The pieces:

  • class Product { ... }; declares a new type named Product. The trailing semicolon after the closing brace is required, same as for structs.
  • The fields (name, price, stock) are member variables (also called data members or fields). Each object of the class gets its own copy of every member variable.
  • The functions (print, restock) are member functions (also called methods). When called through an object, they operate on that object's data.
  • The public: label says "everything below me can be used from outside the class". Without it, the members would be private by default and main could not touch them. The full mechanics of public, private, and protected come later in this section; for now, marking everything public: keeps the examples readable.

Inside print(), the names name, price, and stock refer to the data members of whichever object the function was called on. mouse.print() reads mouse.name, mouse.price, mouse.stock. Calling the same function through a different object reads different data. The function is shared across all objects; the data is not.

Creating Objects on the Stack

The simplest way to create an object is to declare it as a local variable. The object lives on the stack, which is the region of memory that holds local variables and function call frames. Its lifetime is tied to the surrounding scope: when the function returns or the block ends, the object is destroyed automatically.

Both mouse and keyboard live on the stack inside main. They are separate objects, each with its own copies of name, price, and stock. When main returns, both objects are destroyed automatically. The string memory inside name is also cleaned up at the same time, because std::string knows how to free its own buffer.

Stack allocation is the default and the right choice for most local code. It is fast (a pointer bump), and the cleanup is automatic, so there is no way to forget to destroy the object.

Creating Objects on the Heap

The other place an object can live is the heap, which is a region of memory you allocate explicitly and manage yourself. You ask for an object with new, get back a pointer to it, and must release it later with delete. The object stays alive across function calls until you delete it.

new Product allocates space for one Product on the heap and returns a Product* pointing at it. The arrow operator -> reaches into the object through the pointer (more on that below). delete mouse releases the heap memory and runs the cleanup that destroys the object's data members. Forgetting the delete leaks the memory; deleting it twice corrupts the heap. Both are undefined behavior.

The pointer variable mouse lives on the stack inside main. The actual Product object lives on the heap. When main ends, the pointer is destroyed automatically, but the heap object is not, because the heap is not tied to scope. The explicit delete is needed for that reason.

Heap allocation is useful when an object's lifetime needs to outlast the current function, when its size is not known at compile time, or when ownership is being passed around. The downsides are real: every new needs a matching delete, and getting that wrong is one of the most common sources of bugs in older C++ code. Modern C++ avoids raw new/delete in favor of smart pointers (std::unique_ptr, std::shared_ptr), which automate the cleanup. For this lesson, raw new/delete is fine for showing the mechanics.

Stack allocation is essentially free, a single subtraction from the stack pointer. Heap allocation calls into the allocator (typically malloc internally) and can take hundreds of cycles, more if the allocator has to fall back to the operating system. Prefer the stack when an object's lifetime fits a scope.

Accessing Members with . and ->

There are two operators for reaching into an object's members, and which one you use depends on whether you have the object or a pointer to it.

  • Dot operator `.` is used when you have the object directly (or a reference to it).
  • Arrow operator `->` is used when you have a pointer to the object. It is shorthand for "dereference the pointer, then apply the dot".

ptr->price and (*ptr).price mean the same thing. The arrow is easier to read and easier to type. Both modify the same mouse object, because ptr is an alias for its address.

A handy rule: if the expression on the left is a pointer, use ->. If it is the object itself or a reference, use .. The compiler will flag the wrong one, but knowing the rule up front saves time. The same applies to objects created with new: those return pointers, so use -> on them until you dereference.

Class Definition vs Declaration

So far the entire class has lived in one file. That works for small examples, but real codebases split a class across two files: a header file (.h or .hpp) that declares the class and its members, and an implementation file (.cpp) that defines what the member functions do.

The header lets other source files use the class without seeing the full implementation. The implementation file is compiled once and linked in.

The same Product class split across two files:

Compile with:

Things to unpack:

  • The header declares the class shape: which data members and which member functions exist. The function bodies are not in the header.
  • The implementation file uses Product::print() syntax (the Product:: part is the scope resolution operator) to say "this is the print function that belongs to Product". Without that prefix, the compiler would treat the definition as a free function called print.
  • The #ifndef PRODUCT_H / #define PRODUCT_H / #endif block is an include guard. It prevents the header's contents from being processed twice if multiple files include it, which would otherwise cause "redefinition" errors.
  • main.cpp only sees the header. It does not need to know how print is implemented to use it.

A member function can also be defined inline in the class body, the way the earlier examples did. Inline definitions are convenient for short functions and tell the compiler it can substitute the function body at the call site if it chooses. The trade-off is that changing the body forces every file that includes the header to recompile. As a rough rule, keep one-liners inline and put anything bigger in the .cpp file.

Default Access: struct vs class

In C++, struct and class are almost the same keyword. They build the same kind of type with the same machinery: data members, member functions, inheritance, the works. The one language-level difference is the default access level for their members.

  • struct members are public by default.
  • class members are private by default.

Uncommenting the b.price = 24.99; line gives an error like:

That is the only mandatory difference. Everything else, member functions, constructors, inheritance, the rest, works identically for both keywords.

A common convention is:

  • Use struct for small data aggregates with no real invariants. Plain old data, like a 2D point, a color, or a config record.
  • Use class for types with real behavior, encapsulated state, or non-trivial invariants. A Product with stock validation, a ShoppingCart with rules about adding items, an Order with state transitions.

The compiler does not care which one you pick. It is a signaling convention that helps readers know what kind of type they are looking at. The full mechanics of access modifiers, why private members are useful, and how getters and setters interact with them, live in the access modifiers and encapsulation lessons later in this section.

Memory Layout: A Class Is a Struct With Methods

A useful intuition for what a class is at runtime: the object's storage holds only its data members, laid out the same way a struct with those same members would be. The member functions are not stored per object; they live once in the code section of the program and are called with a hidden "which object" argument.

The size is 16 bytes: 8 for price, 4 for stock, and 4 bytes of padding so that an array of Product keeps price 8-byte aligned. There is no extra space for the functions. Two Product objects are 32 bytes total, not 32 plus the function code, because the functions live elsewhere in the binary.

When you call mouse.print(), the compiler effectively passes &mouse as a hidden argument to print, so the function knows which object's price and stock to read. This hidden pointer is called this. The takeaway: an object is its data, and the functions are shared.

This is also why sizeof(class WithMethods) equals sizeof(equivalent struct): methods do not take object space. The one exception is virtual functions, which add a single pointer (the vtable pointer) to each object. Virtual functions and inheritance are covered in later sections.

Putting It Together: A Small Cart Example

The example below pulls the pieces into one program: a tiny Cart class that holds a list of products and a few helpers. It uses everything covered above: defining a class, member variables, member functions, dot access, range-based iteration over members, and a clean main.

A few details about this code. Cart holds a std::vector<Product> as a member, so each Cart object has its own independent list of products. The add function takes a Product by value, which means cart.add(mouse) copies mouse into the cart; later changes to mouse will not affect the cart's copy. Cart::print calls Product::print on each item, which shows methods working together cleanly. total() uses a range-based loop over the cart's own items member, no other state needed.

void add(Product p) copies the product on every call, including allocating new memory for the std::string name. For a busy cart, switching the parameter to const Product& p and then items.push_back(p) avoids one copy per call. By-value is shown here for simplicity.

This is a workable shape for a class even before constructors. The next several lessons fill in the parts that make it sturdier: setting up an object's state in one go (constructors), making sure resources are released cleanly (destructors), copying objects correctly, and protecting the internals from the outside world.

Quiz

Classes & Objects Quiz

10 quizzes