Essential C++ Knowledge for AI Deployment – Must-Know for Interviews (Part 2)

1. What is Memory Leak and Its Types

What is Memory Leak?

A memory leak refers to a situation where a program fails to release memory that is no longer in use after dynamically allocating it, due to negligence or errors. Memory leak does not mean that the memory physically disappears, but rather that the applicationloses control over the allocated memory segment due to design flaws. Although the memory still exists, the program can no longer use it, leading to a decline in system performance.

Common Types of Memory Leaks

  1. Heap Memory Leak

  • Heap memory is allocated using functions like <span>malloc</span>, <span>new</span>, etc. If the program does not properly release this memory, a heap memory leak occurs.
  • Resource Leak

    • System resources (such as file handles, sockets, bitmaps, etc.) that are not released in a timely manner lead to resource wastage, affecting system stability and performance.
  • Base Class Destructor Not Defined as Virtual

    Example:

    class Base {
    public:
        ~Base() {
            cout << "Base destructor called" << endl;
        }
    };
    
    class Derived :public Base {
    public:
        ~Derived() {
            cout << "Derived destructor called" << endl;
        }
    };
    
    int main() {
        Base* basePtr = new Derived();
        delete basePtr;  // Error: The base class destructor is not virtual, only the Base destructor will be called
        return 0;
    }
    

    Solution: Declare the base class destructor as a virtual function to ensure that the resources of the derived class are also released correctly.

    class Base {
    public:
        virtual ~Base() {  // Virtual destructor
            cout << "Base destructor called" << endl;
        }
    };
    
    • If the base class destructor is not a virtual function, the program will lose the correct release of derived class resources, leading to memory leaks.
  • Using <span>delete</span> Instead of <span>delete[]</span>

    • If <span>delete</span> is used to release an array of objects, only the first element of the array will be destructed, causing the remaining elements to not be released, resulting in memory leaks.
  • Missing Copy Constructor

    Example:

    class MyClass {
    public:
        char* data;
    
        MyClass(const char* str) {
            data = new char[strlen(str) + 1];
            strcpy(data, str);
        }
    
        MyClass(const MyClass& other) {  // Default shallow copy
            data = other.data;  // Directly copying the pointer, not allocating new memory
        }
    
        ~MyClass() {
            delete[] data;  // Error: Double free
        }
    };
    

    Solution: Implement a deep copy constructor to ensure each object has its own independent memory space.

    MyClass(const MyClass& other) {
        data = new char[strlen(other.data) + 1];
        strcpy(data, other.data);
    }
    
    • If the copy constructor is not correctly implemented, copying objects may lose control over memory, leading to memory leaks.

    How to Determine if There is a Memory Leak?

    1. Use tools like Valgrind (in Linux environment) to detect memory leaks.
    2. Manually add statistics for memory allocation and release to check for memory leaks.

    How to Solve Memory Leaks?

    1. Strictly manage memory allocation and release.
    2. Use smart pointers (like <span>std::unique_ptr</span> or <span>std::shared_ptr</span>), which automatically release memory when they go out of scope, reducing the risk of memory leaks.

    2. <span>new/delete</span> vs <span>malloc/free</span>

    <span>new</span> and <span>delete</span> Characteristics

    • C++ Keywords: <span>new</span> and <span>delete</span> are C++ keywords and support overloading.
    • Automatic Invocation of Constructors and Destructors: <span>new</span> and <span>delete</span> automatically call the object’s constructor and destructor, while <span>malloc</span> and <span>free</span> only allocate and free memory without involving object construction and destruction.
    • Memory Calculation: <span>malloc</span> requires explicit calculation of memory size, while <span>new</span> automatically calculates it.
    • Return Type: <span>malloc</span> returns a <span>void*</span> pointer that needs to be cast, while <span>new</span> returns a pointer of the specific type.

    <span>malloc</span> and <span>free</span> Characteristics

    • C Standard Library Functions: <span>malloc</span> and <span>free</span> are standard library functions in C and cannot be overloaded.
    • Do Not Call Constructors and Destructors: <span>malloc</span> and <span>free</span> only handle memory allocation and deallocation, not the object’s lifecycle.

    3. Runtime Polymorphism and Virtual Functions

    Concept of Polymorphism

    Polymorphism refers to the ability of the same operation to produce different results when applied to different objects. It is mainly divided into static polymorphism and dynamic polymorphism.

    Static Polymorphism (Compile-time Polymorphism)

    1. Function Overloading: Distinguishing functions with the same name based on different parameters.
    2. Templates: Polymorphism achieved through generic programming.

    Dynamic Polymorphism (Runtime Polymorphism)

    Dynamic polymorphism relies on virtual functions, where the key point is that a base class pointer or reference points to a derived class object, and when calling a virtual function through the base class pointer, the actual implementation of the derived class is called instead of the base class implementation.

    Example Code:

    class Animal {
    public:
        virtual void speak() { cout << "The animal is speaking" << endl; }
    };
    
    class Dog :public Animal {
    public:
        void speak() override { cout << "Woof Woof" << endl; }
    };
    
    int main() {
        Animal* ptr = new Dog();
        ptr->speak();  // Output: Woof Woof
        return 0;
    }
    

    How Virtual Functions Work

    • Each class containing virtual functions has a virtual function table (vtable) that stores the addresses of the virtual functions.
    • Objects access the virtual function table through a virtual function pointer (vptr), achieving dynamic binding at runtime.

    4. Hash Table Principles and Collision Resolution

    Basic Principles of Hash Tables

    A hash table maps the keys of data to a fixed-size array using a hash function. The hash function computes a hash value based on the key and stores the data at the corresponding array position.

    Common Hash Functions:

    1. Digital Analysis Method
    2. Square Middle Method
    3. Segment Addition Method
    4. Division Remainder Method
    5. Pseudorandom Number Method

    Hash Collisions and Solutions

    Hash Collision occurs when multiple elements map to the same position after being processed by the hash function. Common collision resolution methods include:

    1. Open Addressing (Rehashing): If the current position is occupied, find an empty slot using linear probing, quadratic probing, etc.
    2. Chaining: Use linked lists to store elements in the hash table, with colliding elements stored in the same array slot as a linked list.
    3. Establishing a Common Overflow Area: Use a common area to store colliding data.
    4. Rehash Table: When the hash table reaches a certain load factor, reallocate memory and rehash the data.

    5. <span>sizeof</span> vs <span>strlen</span> Differences

    <span>sizeof</span> Detailed Explanation

    <span>sizeof</span> is a compile-time operator used to calculate the memory size of a data type or variable. It can be used for any data type, not limited to strings.

    Example:

    char str1[100] = "hello";
    cout << sizeof(str1);  // Output 100
    

    <span>strlen</span> Detailed Explanation

    <span>strlen</span> is a runtime function used to calculate the actual length of a C-style string (excluding the terminating character <span>\0</span>).

    Example:

    char str[] = "hello";
    cout << strlen(str);  // Output 5
    

    6. C++ Strings: <span>std::string</span> vs C-style Strings

    Differences Between C-style Strings and <span>std::string</span>

    1. C-style Strings: C-style strings are stored as character arrays, typically ending with <span>\0</span> (null character). The length can be calculated using <span>strlen</span>, but memory management must be done manually, which can easily lead to memory leaks or buffer overflow errors.

      Example:

      char str[50] = "Hello, world!";
      cout << strlen(str);  // Output 13
      
    2. <span>std::string</span>: <span>std::string</span> is a standard library class provided by C++, which automatically manages memory and offers many convenient member functions, such as <span>.length()</span>, <span>.size()</span>, etc., effectively avoiding memory management errors.

      Example:

      #include <iostream>
      #include <string>
      
      using namespace std;
      
      int main() {
          string str = "Hello, world!";
          cout << str.length();  // Output 13
          return 0;
      }
      

    Advantages of Using <span>std::string</span>

    1. Automatic Memory Management: <span>std::string</span> automatically adjusts its size, handling memory allocation and deallocation, avoiding the hassle of manual memory management in C-style strings.
    2. Safety: <span>std::string</span> provides bounds checking, reducing the risk of accessing invalid memory.
    3. Rich Functionality: <span>std::string</span> offers many convenient member functions, such as <span>.substr()</span>, <span>.find()</span>, <span>.append()</span>, etc., greatly simplifying string operations.

    Example: Using <span>std::string</span> for String Operations

    #include <iostream>
    #include <string>
    
    using namespace std;
    
    int main() {
        string str = "Hello, world!";
        
        // Substring
        string subStr = str.substr(7, 5);  // Start at index 7, length 5
        cout << subStr << endl;  // Output "world"
        
        // Find substring
        size_t pos = str.find("world");
        if (pos != string::npos) {
            cout << "Found 'world' at position: " << pos << endl;
        }
        
        // Concatenate strings
        str.append(" Welcome!");
        cout << str << endl;  // Output "Hello, world! Welcome!"
        
        return 0;
    }
    

    Conclusion

    This article discusses common causes of memory leaks in C++ and how to avoid them, including issues like base class destructors not being declared as virtual and missing copy constructors, illustrated with examples on how to prevent these problems.

    Additionally, we discussed the differences between <span>new/delete</span> and <span>malloc/free</span>, explained how to achieve runtime polymorphism through virtual functions, and introduced the basic principles of hash tables and collision resolution methods. Finally, we analyzed the differences between <span>sizeof</span> and <span>strlen</span>, as well as the differences between <span>C++ strings</span> and <span>C-style strings</span> to help you better understand these common C++ operations.

    I hope this is helpful to you, to be continued.

    Leave a Comment