Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Understanding Classes, Objects, and Member Functions in C++

Tech Jul 11 1

In C++, a class keyword is used to define a blueprint for creating objects. The class name follows the class keyword, and the class body is enclosed in curly braces {}. A semicolon ; must terminate the class definition. Elements within the class body are called members. Variables declared inside a class are known as member variables or attributes, while functions are referred to as member functions or methods.

It's a common convention to prefix member variables with an underscore _ or m, or append an underscore, to distinguish them from local variables. While not strictly enforced by C++, this practice improves code readability and is often dictated by company coding standards.

C++ also supports the struct keyword for defining classes. It maintains compatibility with C's struct usage while enhancing it to class capabilities, including the ability to define functions. However, using class is generally recommended for defining classes.

Member functions defined within a class are implicitly considered inline by default.

#include <iostream>
#include <cassert> // For assert
#include <cstdlib> // For malloc, free
#include <cstdio>  // For perror

class Stack
{
public:
   // Initializes the stack with a given capacity.
   void Initialize(int capacity = 4)
   {
       // Allocate memory for the array.
       internalArray = (int*)malloc(sizeof(int) * capacity);
       if (nullptr == internalArray)
       {
           perror("malloc failed to allocate memory");
           return;
       }
       maxCapacity = capacity;
       currentTop = 0;
   }

   // Adds an element to the top of the stack.
   void Push(int value)
   {
       // TODO: Implement capacity expansion logic if needed.
       if (currentTop < maxCapacity) {
           internalArray[currentTop++] = value;
       } else {
           // Handle stack overflow or implement resizing
           std::cerr << "Stack overflow!" << std::endl;
       }
   }

   // Returns the element at the top of the stack.
   int Top()
   {
       assert(currentTop > 0); // Ensure stack is not empty
       return internalArray[currentTop - 1];
   }

   // Frees the allocated memory and resets stack state.
   void Destroy()
   {
       free(internalArray);
       internalArray = nullptr;
       currentTop = 0;
       maxCapacity = 0;
   }

private:
   // Member variables
   int* internalArray;
   size_t maxCapacity;
   size_t currentTop;
}; // Semicolon is mandatory

int main()
{
   Stack myStack;
   myStack.Initialize();
   myStack.Push(10);
   myStack.Push(20);
   std::cout << "Top element: " << myStack.Top() << std::endl;
   myStack.Destroy();
   return 0;
}

class CalendarDate
{
public:
   // Initializes the date with year, month, and day.
   void SetDate(int year, int month, int day)
   {
       year_ = year;
       month_ = month;
       day_ = day;
   }

private:
   // Member variables, often prefixed with '_' for distinction.
   int year_;
   int month_;
   int day_;
};

int main()
{
   CalendarDate eventDate;
   eventDate.SetDate(2024, 3, 31);
   return 0;
}

// C++ enhances struct to have class-like features.
struct Node
{
   // Initializes a node.
   void Initialize(int data)
   {
       nextPtr = nullptr;
       value = data;
   }
   Node* nextPtr; // Pointer to the next node
   int value;     // Data stored in the node
};

int main()
{
   Node myNode;
   myNode.Initialize(100);
   return 0;
}

Access specifiers (public, protected, private) are fundamental to encapsulation in C++. They control the visibility and accessibility of class members. Members declared as public can be accessed from anywhere, both inside and outside the class. protected and private members are not directly accessible from outside the class; their distinction becomes apparent in inheritance scenarios.

The scope of an access specifier begins at its declaration and extends to the next access specifier or the end of the class definition (}). By default, members of a class are private, whereas members of a struct are public.

It is standard practice to make member variables private or protected to protect data integrity, while providing public member functions as interfaces for external interaction.

Instantiation is the process of creating an object from a class blueprint. A class serves as an abstract description or a template, defining the structure and behavior of objects. While a class declares member variables, it doesn't allocate memory for them. Memory is allocated only when an object is instantiated from the class.

A single class can be used to instantiate multiple objects, each occupying its own distinct memory space to store its member variables. Think of a class as an architectural blueprint: it outlines the rooms and their specifications but doesn't constitute a physical house. Building a house from the blueprint (instantiating an object) creates a tangible entity that can be occupied (store data).

#include <iostream>

class TimePoint
{
public:
   // Initializes the time point.
   void SetTime(int year, int month, int day)
   {
       eventYear = year;
       eventMonth = month;
       eventDay = day;
   }

   // Prints the time point.
   void Display()
   {
       std::cout << eventYear << "/" << eventMonth << "/" << eventDay << std::endl;
   }

private:
   // These are declarations; memory is allocated upon object instantiation.
   int eventYear;
   int eventMonth;
   int eventDay;
};

int main()
{
   // Instantiating two objects, instance1 and instance2, from the TimePoint class.
   TimePoint instance1;
   TimePoint instance2;

   instance1.SetTime(2024, 3, 31);
   instance1.Display();

   instance2.SetTime(2024, 7, 5);
   instance2.Display();

   return 0;
}

When an object is instantiated, it receives its own dedicated memory space for member variables. Member functions, however, are not stored within each object. Since compiled functions exist as executable code in a separate memory segment (code segment), storing them within each object would be inefficient. Instead, objects contain only the data members. Member functions are accessed via their code, not by being stored in the object itself. This design prevents redundant storage of function code for every object created from the same class.

Memory Alignment Rules

  • The first member of a structure or class is typically placed at an offset of 0 from the beginning of the memory block.
  • Subsequent members are aligned to memory addresses that are multiples of their required alignment value. The alignment value is the smaller of the compiler's default alignment and the member's size.
  • In Visual Studio, the default alignment is often 8 bytes.
  • The total size of a structure or class is rounded up to be a multiple of its largest alignment requirement.
  • If a structure contains nested structures, the nested structure's size must also adhere to its own alignment rules, and the overall structure's size will be a multiple of the largest alignment requirement among all its members (including nested ones).

When a member function is called on an object (e.g., obj.MemberFunction()), the function needs to know which object's data to operate on. C++ provides an implicit this pointer to handle this. The compiler automatically adds a pointer to the current object (of the class type) as the first parameter to each non-static member function. For instance, a member function void Init(int year, int month, int day) is internally treated as void Init(Date* const this, int year, int month, int day).

Accessing member variables within a member function is effectively done through the this pointer. For example, this->memberVariable = value;. While you cannot explicitly write this in the function signature or argument list, it is available for use within the function body.

Object-Oriented Programming (OOP) principles like encapsulation, inheritance, and polymorphism are key features of C++. Comparing a C-style implementation of a Stack with its C++ counterpart highlights the benefits of encapsulation.

In C++, data and functions are bundled within a class, with access specifiers controlling external access. This prevents arbitrary modification of data, a core aspect of encapsulation. This stricter management enhances code robustness and prevents unintended side effects.

C++ offers more convenient syntax, such as default arguments for member functions and the implicit handling of the object's address via the this pointer, simplifying function calls. Additionally, using class names directly eliminates the need for typedefs.

While the initial C++ implementation of a Stack might appear significantly different, the underlying logic often remains similar. The true power of C++ becomes more evident when exploring advanced concepts like the Standard Template Library (STL).

Default member functions are those automatically generated by the compiler when a user does not explicitly define them. For any class, the compiler may generate up to six default member functions: constructor, destructor, copy constructor, copy assignment operator, address-of operator, and const address-of operator. C++11 introduced move constructor and move assignment operator as well.

Understanding default member functions involves two aspects:

  1. The behavior of the compiler-generated functions and whether they meet the requirements.
  2. How to explicitly define these functions when the default behavior is insufficient.

Constructors are special member functions responsible for initializing objects upon instantiation. Unlike memory allocation (which often occurs at scope entry for local variables), constructors focus on setting up the initial state of an object. They effectively replace the need for explicit initialization functions like the Init() functions seen previously.

Constructor Characteristics:

  • The constructor's name is the same as the class name.

  • Constructors do not have a return type, not even void.

  • The system automatically calls the appropriate constructor when an object is created.

  • Constructors can be overloaded (i.e., a class can have multiple constructors with different parameter lists).

  • If a class does not have any explicitly defined constructors, the compiler generates a default constructor (a constructor with no parameters). However, if you define any constructor, the compiler will not generate the default one.

  • A default constructor can be:

  • A constructor with no parameters.

  • A constructor where all parameters have default values (a fully defaulted constructor).

  • The compiler-generated default constructor.

  • Note: A no-argument constructor and a fully defaulted constructor can lead to ambiguity if both exist, as they might both be invoked without arguments. A "default constructor" is any constructor that can be called without providing arguments.

  • The compiler-generated default constructor does not initialize built-in type members (e.g., int, double); their initial values are indeterminate. For user-defined type members (objects of other classes), it calls their default constructors. If a user-defined type member lacks a default constructor, an error will occur unless an initializer list is used.

#include <iostream>

class Book
{
public:
   // 1. No-argument constructor (default constructor)
   Book() : title_("Untitled"), author_("Unknown"), year_(1970)
   {
       std::cout << "Book created with default values." << std::endl;
   }

   // 2. Parameterized constructor
   Book(const std::string& title, const std::string& author, int year)
       : title_(title), author_(author), year_(year)
   {
       std::cout << "Book '" << title_ << "' created." << std::endl;
   }

   // 3. Fully defaulted constructor (can be ambiguous with #1 if not careful)
   /*
   Book(const std::string& title = "Untitled", const std::string& author = "Unknown", int year = 1970)
       : title_(title), author_(author), year_(year)
   {
       std::cout << "Book (potentially defaulted) created." << std::endl;
   }
   */

   void DisplayInfo() const
   {
       std::cout << "Title: " << title_ << ", Author: " << author_ << ", Year: " << year_ << std::endl;
   }

private:
   std::string title_;
   std::string author_;
   int year_;
};

int main()
{
   // If only the parameterized constructor (#2) is uncommented,
   // Book defaultBook; // would cause a compilation error: "no suitable default constructor exists"

   Book defaultBook;        // Calls the no-argument constructor
   Book specificBook("The C++ Programming Language", "Bjarne Stroustrup", 2013); // Calls the parameterized constructor

   // Note: Book ambiguousBook(); declares a function named ambiguousBook that returns Book,
   // rather than creating an object. Use Book ambiguousBook; for object creation.
   // Book ambiguousBook(); // This is a function declaration, not object instantiation.

   defaultBook.DisplayInfo();
   specificBook.DisplayInfo();

   return 0;
}

#include <cstdlib> // For malloc, free
#include <cstdio>  // For perror
#include <cstddef> // For size_t

// Assuming STDataType is defined, e.g., as int
typedef int STDataType;

class DataStore
{
public:
   // Constructor with a default capacity.
   DataStore(size_t initialCapacity = 4)
   {
       dataArray = (STDataType*)malloc(sizeof(STDataType) * initialCapacity);
       if (nullptr == dataArray)
       {
           perror("malloc failed to allocate memory for DataStore");
           return;
       }
       capacity = initialCapacity;
       count = 0;
   }

   // Other member functions like Push, Pop, etc. would go here.
   // ...

private:
   STDataType* dataArray;
   size_t capacity;
   size_t count;
};

class Manager
{
public:
   // The Manager constructor will implicitly call the DataStore constructors
   // for its member variables pushStore and popStore.
   Manager() = default; // Explicitly request the compiler-generated default constructor

private:
   DataStore pushStore; // First member
   DataStore popStore;  // Second member
};

int main()
{
   Manager systemManager; // Manager constructor is called, which in turn calls DataStore constructors.
   return 0;
}

A destructor is a special member function that performs cleanup tasks when an object's lifetime ends. It's the counterpart to the constructor. Destructors are automatically invoked by the system as objects are destroyed (e.g., when a local variable goes out of scope). Their primary role is to release any resources acquired by the object during its lifetime, such as dynamically allocated memory.

Destructor Characteristics:

  • The destructor's name is formed by prefixing the class name with a tilde (~).
  • Destructors do not have parameters or a return type (not even void).
  • A class can have only one destructor. If not explicitly defined, the compiler generates a default destructor.
  • The system automatically calls the destructor when an object's scope ends or when it is explicitly deleted (for dynamically allocated objects).
  • The compiler-generated default destructor performs no action for built-in type members but calls the destructors of user-defined type members.
  • If you explicitly define a destructor, it will also call the destructors of user-defined type members.
  • If a class does not manage any resources (like dynamic memory), you may omit the destructor, relying on the compiler-generated default. For example, a Date class without dynamic allocation typically doesn't need an explicit destructor.
  • If a class acquires resources, an explicit destructor is crucial to prevent resource leaks.
  • For multiple objects in the same scope, destructors are called in the reverse order of their construction.

A copy constructor is a special type of constructor that initializes a new object as a copy of an existing object. It's typically defined as a constructor whose first parameter is a reference to the class type, and any additional parameters must have default values.

Copy Constructor Characteristics:

  • It is a form of constructor overloading.
  • The first parameter must be a reference to the class type (e.g., const ClassName&). Passing by value would lead to infinite recursion as creating the parameter would invoke the copy constructor itself.
  • When an object of a user-defined type is passed by value to a function or returned by value, the copy constructor is invoked to create a copy.
  • If a copy constructor is not explicitly defined, the compiler generates a default one. This default copy constructor performs a member-wise copy (shallow copy) for built-in types and calls the copy constructor for user-defined type members.
  • For classes like Date with only built-in types and no resource management, the default copy constructor is usually sufficient. However, for classes like Stack that manage dynamic resources (e.g., a pointer to allocated memory), a custom copy constructor is needed to perform a deep copy, ensuring the new object's resource is a distinct copy, not just another pointer to the original resource.
  • A common rule of thumb: if a class explicitly defines a destructor to release resources, it likely needs an explicit copy constructor (and copy assignment operator) to handle resource copying correctly.
  • Returning an object by value typically involves a copy constructor. Returning by reference can avoid this copy if the returned object remains valid after the function exits. However, returning a reference to a local object is dangerous as it leads to a dangling reference.

Operator overloading allows you to define custom behavior for operators when applied to objects of your class. The assignment operator (=) is one such operator that can be overloaded.

Assignment Operator Overloading Characteristics:

  • It's a special member function.
  • Typically takes a constant reference to the class type as its parameter (e.g., const ClassName& other).
  • Returns a reference to the class type (e.g., ClassName&) to support chained assignments (e.g., a = b = c;).
  • If not explicitly defined, the compiler generates a default assignment operator. This default version performs a member-wise assignment (shallow copy) for built-in types and calls the copy assignment operator for user-defined type members.
  • Similar to copy constructors, if a class manages resources, a custom assignment operator is needed for deep copying to avoid issues like double-freeing memory or creating shallow copies of resources.
  • A class that explicitly defines a destructor often requires an explicit assignment operator.
  • It's important to handle self-assignment (e.g., obj = obj;) correctly within the overloaded operator.

Note: Copy constructors are used when initializing a new object from an existing one, while assignment operators are used to copy the value of an existing object into another existing object.

Operator Overloading Basics:

  • Operators are functions with special names: operator+, operator<<, etc.
  • The number of parameters depends on the operator (unary, binary). For member functions, the left-hand operand is often implicitly handled by this.
  • Overloaded operators generally retain their original precedence and associativity.
  • Certain operators cannot be overloaded (e.g., ., ::, ?:, sizeof, .*).
  • At least one operand must be of a class type.
  • Operator overloading cannnot create new operators.

Overloading Increment/Decrement Operators:

  • Prefix increment/decrement (++obj, --obj) are overloaded with one parameter.
  • Postfix increment/decrement (obj++, obj--) are overloaded with an extra int parameter (which is ignored) to differentiate them from the prefix versions.

You can overload the address-of operator (&) to control how the address of an object is obtained. This is generally less common and often unnecessary unless you want to prevent clients from getting the address of your objects.

There are two forms:

  • The regular address-of operator: operator&()
  • The const address-of operator: operator&() const

The compiler-generated versions are usually sufficient.

const Member Functions

A const member function is declared with const after its parameter list. This keyword signifies that the member function does not modify the state of the object it is called on. It effectively makes the object's this pointer a pointer to a const object (const ClassName* const this), preventing modification of member variables within the function.

Constructors can initialize member variables in two ways: assignment within the constructor body or using an initialization list. An initialization list starts with a colon (:) after the constructor's parameter list and before its body, followed by a comma-separated list of member variables and their initial values in parentheses.

Key Points about Initialization Lists:

  • Each member variable can appear only once in the initialization list.

  • Reference members, const members, and members of class types that do not have a default constructor *must* be initialized in the initialization list.

  • C++11 allows default member initializers directly in the class definition. These are used if the member is not explicitly initialized in the initialization list.

  • It is generally preferred to use initialization lists because:

  • For built-in types, it performs direct initialization. If not initialized in the list and no defaultt value is provided, their initial state is indeterminate.

  • For user-defined types, it calls their default constructor if not explicitly initialized in the list.

  • Members are initialized in the order they are declared in the class, regardless of their order in the initialization list. It's good practice to maintain consistency between declaration order and initialization list order.

#include <iostream>
#include <string>

class Configuration
{
public:
   // Constructor using initialization list and default values
   Configuration(std::string id = "default_id", int timeout = 30)
       : configId_(id), connectionTimeout_(timeout), status_("initialized")
   {
       // Constructor body can contain other logic if needed
       std::cout << "Configuration object created." << std::endl;
   }

   void DisplayConfig() const
   {
       std::cout << "ID: " << configId_
                 << ", Timeout: " << connectionTimeout_
                 << ", Status: " << status_ << std::endl;
   }

private:
   std::string configId_;          // Initialized via initialization list
   const int connectionTimeout_;   // Must be initialized via initialization list
   std::string status_;            // Initialized via initialization list
};

int main() {
   Configuration defaultConfig;
   defaultConfig.DisplayConfig();

   Configuration customConfig("server_A", 60);
   customConfig.DisplayConfig();

   return 0;
}

C++ allows implicit conversions from built-in types to class types if the class has a constructor that accepts a single argument of that built-in type. To prevent potentially unintended implicit conversions, you can use the explicit keyword before the constructor declaration.

Members declared with the static keyword belong to the class itself, rather than to any specific object of the class.

static Member Variables:

  • They are shared among all objects of the class.
  • They must be defined and initialized outside the class definition, usually in the source file.
  • They are stored in the static storage area, not within individual objects.
  • They cannot be initialized within the class declaration (except for const static integral types in C++11 and later).

static Member Functions:

  • They do not have a this pointer because they are not associated with a specific object.
  • They can only access other static members (variables and functions) of the class directly. They cannot access non-static members.
  • Non-static member functions can access both static and non-static members.
  • static members can be accessed using the class name and the scope resolution operator (::), e.g., ClassName::staticMember, or via an object using the dot operator (.), e.g., object.staticMember.
  • They are subject to access specifiers (public, protected, private).
#include <iostream>

class ToolCounter {
public:
   ToolCounter(const std::string& name) : toolName(name) {
       instanceCount++; // Increment static counter when an object is created
   }

   // Static member function to get the count of instances
   static int GetInstanceCount() {
       return instanceCount;
   }

   // Static member variable (must be defined outside the class)
   static int instanceCount;

private:
   std::string toolName; // Non-static member variable
};

// Definition and initialization of the static member variable
int ToolCounter::instanceCount = 0;

int main() {
   std::cout << "Initial tool count: " << ToolCounter::GetInstanceCount() << std::endl;

   ToolCounter tool1("Hammer");
   ToolCounter tool2("Wrench");
   ToolCounter tool3("Screwdriver");

   std::cout << "Current tool count: " << ToolCounter::GetInstanceCount() << std::endl; // Access static member via class name

   return 0;
}

A friend declaration allows a non-member function or another class to access the private and protected members of the class granting friendship. This provides a controlled way to break encapsulation for specific, trusted entities.

  • Friend Functions: Declared within a class using the friend keyword. They are not member functions of the granting class but have privileged access. A function can be a friend to multiple classes.
  • Friend Classes: If class B is a friend of class A, all member functions of class B can access the private and protected members of class A. Friendship is not transitive (if A is a friend of B, and B is a friend of C, then A is not necessarily a friend of C) and not reciprocal (friendship must be explicitly granted by both classes if mutual access is needed).

While friends offer convenience, they increase coupling and reduce encapsulation, so they should be used judiciously.

Anonymous objects (also known as temporary objects) are objects created without being assigned to a named variable. They are typically used for short-lived tasks within a single expression or statement. Their lifetime is limited to the expression in which they are created.

Example: MyClass(arg1, arg2); creates an anonymous object. This is in contrast to MyClass obj(arg1, arg2);, which creates a named object.

Modern compilers employ various optimizations to improve performance, including reducing unnecessary object copying during parameter passing and return values. These optimizations, such as copy elision and constructor merging, aim to eliminate redundant copy operations when they do not affect the program's correctness. The specific optimization strategies can vary among compilers.

Tags: C++classes

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.