This note covers defining classes in C++, the lifetime of an object, and what these language features correspond to at the memory level.
A class is a struct with access control
In C++ the only linguistic difference between class and struct is the default access: struct defaults to public, class to private. Otherwise they are identical β both can have member functions, inheritance and virtual functions.
C++class Vector3 {
public:
Vector3(float x, float y, float z) : x_(x), y_(y), z_(z) {}
float length() const;
private:
float x_, y_, z_;
};
By convention we use struct for "a few pieces of data travelling together" and class for "there is an invariant to maintain". The compiler does not care, but the reader does.
One fact worth internalizing: member functions take no space in the object. sizeof(Vector3) is 12 bytes β three floats, nothing else. Member functions compile to ordinary functions with one hidden first parameter, the this pointer. The length() above is closer to:
C++float Vector3_length(const Vector3* this);
Which also explains why a const member function cannot modify members: the const applies to the type this points at, so this becomes a const Vector3*.
Constructors
The initializer list is not syntactic sugar
C++class Widget {
public:
Widget(const std::string& name) : name_(name) {} // initialization
// Widget(const std::string& name) { name_ = name; } // assignment
private:
std::string name_;
};
These two differ substantively. Every member is fully constructed before the constructor body begins. The initializer list calls the copy constructor directly; writing it in the body default-constructs an empty string first and then copy-assigns β one construction more than necessary.
For const members and reference members, assignment in the body is impossible at all; the initializer list is the only option.
And a trap: members are initialized in declaration order, regardless of the order you write them in the initializer list.
C++class Buffer {
public:
Buffer(size_t n) : size_(n), data_(new char[size_]) {} // looks fine
private:
char* data_; // declared first, so initialized first
size_t size_; // declared second
};
This code is wrong. data_ is initialized first, at which point size_ still holds an indeterminate value. Keeping the declaration order and the initializer order identical is the cheapest way to never meet this bug.
Default and explicit constructors
Write any constructor of your own and the compiler stops generating the default one. You can ask for it back:
C++Widget() = default;
A single-argument constructor introduces an implicit conversion, which is usually not what you want:
C++class Buffer {
public:
Buffer(size_t n);
};
void consume(Buffer b);
consume(42); // compiles: 42 is implicitly converted to Buffer(42)
explicit blocks that conversion. The rule of thumb is: mark single-argument constructors explicit by default, unless you genuinely want the conversion, as std::string(const char*) does.
Destructors and object lifetime
The destructor runs automatically when the object leaves scope, in the reverse of construction order β stack semantics.
C++{
A a; // 1. a constructed
B b; // 2. b constructed
} // 3. b destroyed 4. a destroyed
"Acquire the resource in the constructor, release it in the destructor" is RAII (Resource Acquisition Is Initialization), and it is one of the most valuable things C++ has that C does not. File handles, mutexes, memory β all of it can be managed this way:
C++class FileHandle {
public:
explicit FileHandle(const char* path) : f_(std::fopen(path, "rb")) {}
~FileHandle() { if (f_) std::fclose(f_); }
private:
std::FILE* f_;
};
Whether the function returns normally or throws, the destructor runs and the file is closed. What C solves with goto cleanup is here guaranteed by the language.
Destructors must not throw. If a destructor throws during stack unwinding, the program calls std::terminate outright. Since C++11 destructors are noexcept by default.
Static members
A static member belongs to the class rather than the object; every instance shares one copy.
C++class Counter {
public:
static int count; // declaration
static int get() { return count; } // a static member function has no this
};
int Counter::count = 0; // definition, required outside the class
C++17 allows inline static int count = 0; directly inside the class, which removes that awkward out-of-class definition.
Static member functions have no this, so they cannot touch non-static members. Their common uses are factory functions, and callbacks for C interfaces that want a plain function pointer.
A memory-level view
The best way to understand a class is to look at what it becomes in memory:
- A class with no virtual functions is laid out exactly as its members in declaration order plus alignment padding β identical to the equivalent
struct. - Add a virtual function and the object gains a pointer to the vtable at its head (8 bytes on 64-bit). This is why adding a virtual function to a class holding one
intmakessizeofjump from 4 to 16: 8 bytes of vtable pointer, 4 of int, 4 of padding. sizeofan empty class is 1, not 0 β two distinct objects must have distinct addresses.
None of this matters for ordinary application code, but when you design data structures that need to pack tightly β particles, vertices, component arrays β it decides your cache behaviour directly. For more on layout, see C++ #2 Struct and Union.