Hello! Welcome back to our second module, "Object-Oriented C++: Memory and Mechanics."
In our last lesson, we established a crucial foundation: the memory layout of a simple C++ class is identical to that of a C struct. We saw that its size is determined by its data members and the padding required for memory alignment. We also made a key observation: non-virtual member functions and access specifiers like private do not increase the size of an object.
This lesson directly addresses the "how" behind that observation. Our learning outcome is to analyze how non-virtual member functions are resolved at compile time and do not increase object size. We'll dismantle the "magic" of member function calls and see that the compiler uses a simple, efficient trick you're already familiar with from your C programming experience. This is another example of a "zero-cost abstraction" in C++, where you get the organizational benefits of object-oriented programming without a runtime performance penalty.
The Compiler's Transformation: A Hidden Pointer
If a member function like my_object.get_data() isn't stored inside my_object, how does it know which object's data to access?
The answer is that the compiler performs a transformation. It converts the member function into what is essentially a regular, non-member function and secretly passes the address of the object as a hidden first argument. This hidden argument becomes the this pointer inside the function.
Let's make this concrete.
Class members and member functions memory location
The following Stack Overflow answer provides an excellent, concise explanation of this mechanism. It directly compares a C++ member function to its C-style equivalent, which should be very intuitive given your background.
Read the first bullet point in the top-rated answer by Basile Starynkevitch. Focus on the C++ getA method and its C-function equivalent, C$getA(A*thisptr). This transformation is the central concept of this lesson.
As the resource explains, a C++ call like:
A my_a_object(10);
int value = my_a_object.getA();
is effectively translated by the compiler into something like this at a conceptual level:
/* C-style equivalent */
struct A { int m_a; };
// The compiler generates a 'mangled' name to make it unique
int A_getA(const struct A* thisptr) {
return thisptr->m_a;
}
// The call site is transformed
struct A my_a_object;
my_a_object.m_a = 10;
int value = A_getA(&my_a_object);
This explains everything:
- Resolution at Compile Time: The compiler knows the type of
my_a_objectisA, so it knows to translate the call into a call to the specificA_getAfunction. This is often called static binding or early binding, because the function to be called is determined entirely at compile time. - No Object Size Increase: The function's machine code for
A_getAis stored only once in the program's read-only memory (the text segment or code segment), just like any other global function. It is not duplicated for each object. Each object only needs to store its own data (m_a).
Peeking Under the Hood: The Assembly Perspective
Your goal is to understand C++ from first principles, so let's verify this by looking closer at the boundary between C++ and assembly. From the perspective of assembly language, there's very little difference between a C struct and a C++ class. The object-oriented abstractions are mostly compiler-managed.
The following video does an excellent job of demonstrating this. It confirms that methods don't take up space within the class instance and that the compiler abstractions (like private) don't exist at the machine code level.
Modern x64 Assembly 14: Structures in ASM and C++
To see this from a low-level perspective, let's watch a segment that explains how C++ classes and methods look from the viewpoint of assembly language. This will directly confirm that member functions are not stored inside the object.
Watch from 08:46 to 12:28. Pay close attention to the key statements: 'the methods in a class... don't take up any RAM within the class' and 'classes in C++ are actually structures in assembly'. The demonstration of accessing a private member from assembly is particularly insightful, reinforcing what we learned in the last lesson.
As the video powerfully demonstrates, object-oriented programming in this context is largely an abstraction layer built by the compiler. When you call a member function, the compiler passes a pointer to the object's data (a C-style struct, effectively) into a register (like RCX on x64 Windows/Linux) and then makes a normal function call. The assembly code inside the function then uses that pointer to access the member data.
The Role of Name Mangling
There's one final piece to the puzzle. If MyClass::foo() and AnotherClass::foo() are both just global functions in the final binary, how does the linker tell them apart?
This is solved by name mangling. The compiler converts the C++ function name, including its class scope and parameter types, into a unique, "mangled" name for the linker.
For example, under the Itanium ABI (used by GCC/Clang on Linux), a function like void MyClass::foo() might be mangled into a symbol name like _ZN7MyClass3fooEv. The details aren't important, but the principle is: the mangled name uniquely identifies the function, preventing name collisions. The C$getA name from the Stack Overflow article was a simplified illustration of this very concept.
We will revisit name mangling in more detail when we discuss function overloading, as it's the core mechanism that makes overloading possible.
Test your understanding!
Given the following C++ class:
class Vector2D {
public:
Vector2D(float x_val, float y_val) {
x = x_val;
y = y_val;
}
float length() const {
return sqrt(x * x + y * y);
}
private:
float x;
float y;
};
Write the equivalent C code that mimics the memory layout and the length function call mechanism. You can ignore the constructor for this exercise.
Show answer
The equivalent C code would look something like this:
#include <math.h>
// The C struct has the same data members and memory layout.
// 'private' has no meaning here, as it's a compile-time C++ concept.
typedef struct {
float x;
float y;
} Vector2D;
// The member function becomes a global function that takes a pointer
// to the struct as its first argument (the equivalent of 'this').
// Note the 'const' C++ keyword translates to passing a pointer-to-const in C.
float Vector2D_length(const Vector2D* thisptr) {
return sqrt(thisptr->x * thisptr->x + thisptr->y * thisptr->y);
}
// Example of how a call would be made:
int main() {
Vector2D v;
v.x = 3.0f;
v.y = 4.0f;
// This is how `v.length()` is effectively handled by the C++ compiler.
float len = Vector2D_length(&v);
// len will be 5.0
return 0;
}
This exercise demonstrates that you understand the transformation from a C++ member function call to a C-style function call with a pointer to the data structure.
Conclusion
In this lesson, we demystified the implementation of non-virtual member functions, connecting high-level C++ syntax to the low-level machine reality. This is a critical step in building your "first principles" understanding of the language.
Key Takeaways:
- Compile-Time Resolution: Non-virtual member function calls are resolved at compile time through static binding. The compiler knows the exact function to call based on the object's static type.
- The
thisPointer: A member function call is transformed by the compiler into a regular function call where the object's address is passed as a hidden first argument. This argument is accessible within the function as thethispointer. - Zero-Cost Abstraction: Because member functions are stored once in the code segment and not inside each object, they do not increase object size. Encapsulation is achieved without any runtime overhead compared to a C-style approach.
- Assembly Equivalence: From an assembly language perspective, a simple C++ class is treated as a
struct, and its methods are regular functions that receive a pointer to thatstruct.
We've now seen how a member function gets access to its object's data. In our next lesson, we will focus exclusively on the this pointer, exploring its properties, how it behaves in different contexts (like const member functions), and how the compiler uses it to manage access to the object's state.