Demystifying C++ - Classes
Classes are a cornerstone of C++ programming, embodying the object-oriented paradigm, which is essential for structuring modern, complex software applications. In C++, classes encapsulate data and behavior through attributes and methods. In contrast, Assembler and C primarily rely on data structures and functions.
Methods
Just as the C++ compiler does, the transpiler translates classes into C structures and functions. The methods of the class are turned into functions that take a "this" pointer to the structure representing the class. When accessing attributes, the this-pointer is used automatically, and when calling other methods, it is simply passed on.
Methods | class C1 { int attr; public: void onChangeAttr(); void setAttr(int v) { attr = v; onChangeAttr(); } }; |
struct C1 { int attr; }; void C1_onChangeAttr(struct C1* this); void C1_setAttr(struct C1* this, int v) { this->attr = v; C1_onChangeAttr(this); } |
---|
Inheritance
In C++, inheritance is a central principle that allows classes to inherit attributes and methods from base classes. However, C does not naturally support inheritance. Therefore, the C++ to C transpiler uses special techniques to simulate inheritance relationships by embedding each base class as its own field in the derived structure.
A crucial aspect of inheritance is the casting between base classes (Base) and derived classes (Derived):
- Derived-to-Base Casts: In assembler, such a cast corresponds to an addition with a constant value. These are relatively simple to implement in C by accessing the respective field in the derived structure. This happens implicitly in C++ in many places, such as when calling base class methods or accessing attributes.
- Static Base-to-Derived Casts: In assembler, such a cast corresponds to a subtraction with a constant value. This is more complex in C and requires some C casting.
- Dynamic Base-to-Derived Casts: These are produced with the
dynamic_cast
keyword and are more complex in implementation since they require type checking at runtime. Dynamic casts will be discussed in the chapter on virtual classes.
Representation of Base Classes | class Derived : public Base1, public Base2 { ... }; |
struct Derived { struct Base1 base1; struct Base2 base2; ... }; |
---|---|---|
Derived-to-Base Cast | static_cast<Base2*>(derived) |
&derived->base2 |
Static Base-To-Derived Cast | static_cast<Derived*>(base2) |
(Derived*)((char*)base2 - offsetof(Derived, base2)) |
Constructors and Destructors
Constructors and destructors play a pivotal role in C++ programming as they manage automatic initialization and cleanup processes for objects. Constructors are special class methods that are automatically invoked when an object is created. They're often used to define initial values for object attributes and perform other initialization tasks. Destructors are the opposite of constructors; they're invoked when an object is destroyed and are ideal for cleanup tasks like releasing memory and other resources.
In C, there's no direct counterpart for constructors and destructors. Instead, they are replaced by regular functions that are called upon object creation or destruction. For completeness, it should be mentioned that for the Itanium ABI, at least two constructor functions (ctor_complete
, ctor_base
) and destructor functions (dtor_complete
, dtor_base
, dtor_delete
) are generated. The complete variants are typically used, base variants are required for base classes in the constructor/destructor, and dtor_delete
is used when applying delete to a class with a virtual destructor.
Constructors | class C1 { // Attributes public: C1() { // Initialization code } ~C1() { // Cleanup code } }; |
struct C1 { // Attributes }; void C1_ctor_complete(struct C1* this) { // Initialization code } void C1_dtor_complete(struct C1* this) { // Cleanup code } |
---|---|---|
Stack Variables | { C1 obj; ... } |
{ struct C1 obj; C1_ctor_complete(&obj); ... C1_dtor_complete(&obj); } |