[C++ Memory Management] Summary

Click me –> C++ language foundation
click me –> object-oriented
click me –> STL
click me –> new features

[Operating System] Summary and Answers to Frequently Asked Questions

[Database] Summary and Answers to Frequently Asked Questions

[Computer Network] Summary and Answers to Frequently Asked Questions

1. The difference between heap and stack

  • Space allocation is different : the stack is automatically allocated and released by the operating system, storing function parameter values, local variable values, etc. The heap is generally allocated and freed by the programmer.
  • The cache method is different : the stack uses a first-level cache, which is usually stored in the storage space when called, and released immediately after the call. The heap is stored in the second-level cache, which is slower.
  • The data storage methods are different : the data stored in the stack is stored in a "last in, first out" manner, which is also a common way of using the stack. The data stored in the heap has no specific storage order and can be accessed by the program at any time.
  • The growth direction is different : the stack grows in the direction of decreasing memory address, from the high address of the memory to the low address. The heap grows in the direction of increasing memory addresses, from low addresses of memory to high addresses.
  • The application size limit is different : the top and bottom of the stack are preset, and the size is fixed, usually in the range of several hundred KB to several MB. The heap is a discontinuous memory area whose size can be flexibly adjusted.

2. C++ memory management

Memory allocation method
In C++, memory is divided into 5 areas, which are heap, stack, free storage area, global/static storage area and constant storage area.

  • Stack: When a function is executed, the storage units of local variables in the function can be created on the stack, and these storage units are automatically released when the function execution ends.
  • Heap: These are the memory blocks allocated by new. Generally, a new will correspond to a delete.
  • Free storage area: Those memory blocks allocated by malloc, etc., are very similar to the heap, but use free to end their own lives.
  • Global/static storage area: Global variables and static variables are allocated in the same block of memory.
  • Constant storage area: This is a relatively special storage area, which stores constants and cannot be modified.

Common memory errors and their countermeasures
Common errors:

  • Memory allocation was unsuccessful, but it was used;
  • Although the memory allocation is successful, it is referenced before initialization;
  • The memory was allocated successfully and initialized, but the operation crossed the boundary of the memory;
  • Forgot to release memory, causing memory leaks;
  • freed memory but continued to use it.

Countermeasures:

  • When defining a pointer, first initialize it to NULL;
  • After applying for memory with malloc or new, you should immediately check whether the pointer value is NULL to prevent the use of memory with a pointer value of NULL;
  • Don't forget to assign initial values ​​to arrays and dynamic memory to prevent uninitialized memory from being used as rvalues;
  • Avoid subscripts of numbers or pointers out of bounds, especially beware of "more 1" or "less 1" operations;
  • The application and release of dynamic memory must be paired to prevent memory leaks;
  • After releasing the memory with free or delete, immediately set the pointer to NULL to prevent "wild pointer";
  • Use smart pointers.

Memory leaks and solutions
Memory leaks: Simply put, a piece of memory space is applied for, but it is not released after use.

  • After new and malloc apply for resources, they are not released with delete and free;
  • When the subclass inherits the parent class, the parent class destructor is not a virtual function;
  • Windows handle resources are not released after use.

Solution:

  • Good coding habits, use the function of memory allocation, once used, remember to use the corresponding function to release it;
  • Manage the allocated memory pointer in the form of a linked list, delete it from the linked list after use, and check and change the linked list at the end of the program;
  • using smart pointers;
  • Some common tool plugins, such as ccmalloc, Dmalloc, Leaky, Valgrind, etc.

3. Talk about whether malloc and local variables are allocated on the heap or the stack

  • malloc allocates memory on the heap
  • Local variables are allocated memory on the stack , and are automatically recovered when they exceed the scope.

4. Talk about the sections of the program, their respective functions, the process of program startup, and how to judge whether the data is allocated on the stack or the heap

insert image description here
As shown above, from low address to high address , a program consists of code segment, data segment, BSS segment, heap, shared area, stack, etc.

  • Code segment (text segment): A memory area that stores program execution code. Read-only, the header of the code segment will also contain some read-only constant variables.
  • Data segment: A memory area that stores initialized global variables and static variables in the program.
  • BSS segment (bss segment): stores uninitialized global variables and static variables, and these variables are initialized to 0 by default.
  • When the executable program is running, there will be two more areas: the heap area and the stack area.
    • Heap: A dynamically allocated memory area whose size can be dynamically increased or decreased when the program is running (through functions such as malloc()).
    • Stack area (stack): The memory area for function calls and local variables. Each time a function is called, associated parameters and local variables are allocated on the stack. At the end of the function call, the memory space for these variables is freed. is a continuous space.
  • Finally, there is a shared area, located between the heap and the stack.

The process of program startup :

  • Load program code: The operating system reads the executable file into memory.
  • Initialization: The operating system allocates memory for the program and initializes global and static variables.
  • Execute the main function: The program will execute the code in the main function.
  • Exit: The program exits after executing the main function, and the operating system reclaims memory resources.

How to judge whether the data is allocated on the stack or the heap :

  • The main ways to determine whether data is allocated on the stack or on the heap are scope and lifetime. Data scope refers to the accessible range of variables in the program. If a variable is defined inside a function body, it has a local scope, and usually when the function finishes executing, these variables are also deallocated. The memory space for these variables will be allocated on the stack.
  • And if the variable is defined outside the function, it has a global scope, that is, it can be accessed throughout the program. The memory space for these variables is usually allocated and initialized by the operating system when the program starts, and they will exist in the data segment or BSS segment.
  • Local variables are allocated on the stack, while space allocated by malloc and new is on the heap.

5. Global variables initialized to 0 are in bss or data

In the bss section. The bss segment usually refers to a memory area used to store uninitialized or initialized to 0 global variables and static variables in the program. The feature is readable and writable, and the bss segment will be automatically cleared to 0 before the program is executed.

6. Brief description of atomic memory order

Atomic operation is an important concept in multithreaded programming, which is used to ensure data consistency and correctness in a multithreaded environment. In atomic operations, a group of operations are either all completed or not completed.

The memory order of atomic operations refers to the execution order of read and write operations on shared variables among multiple threads. Consider the following code example:

int x = 0;
int y = 0;

Suppose there are two threads A and B, and the code they execute is as follows:
Thread A:

y = 1;
x = y;

threadB:

if(x == 1){
    
    
    // do something
}

In the above example, if thread A and thread B are executing at the same time, there may be problems. Assuming that thread A executes first, you can see that it first assigns y to 1, then assigns x to y, and the value of x is 1 at this time. Then, thread B executes, and it reads the value of x as 1, so it executes the code in the if statement, but the value of y is not actually updated, which is obviously incorrect.

To solve this problem, we need to use atomic operations. In C++, you can use std::atomic introduced by the C++11 standard to perform atomic operations. The memory order is a mechanism provided by std::atomic, which is used to define the execution order when reading and writing shared variables between multiple threads. Common memory sequences are as follows:

  • memory_order_relaxed: loose memory order. Use this ordering for operations that do not depend on other atomic operations, it is the fastest in-memory ordering because it does not force operations from other threads to wait.
  • memory_order_consume: memory_order_consume will only ensure that the object identified by it is stored in advance of those operations that need to load the object.
  • memory_order_release: Release memory order. This ordering ensures that all other memory operations prior to the current thread occur before this release operation.
  • memory_order_acquire: Acquire memory order. This ordering ensures that all other memory operations following this acquire operation occur before the current thread. It is used together with the release memory order to form the memory order of the atomic lock.
  • memory_order_acq_rel: memory_order_acq_rel The read-modify-write operation in this memory sequence is both a load and a release operation. No operation can be reordered after this operation to before this operation, and no operation can be reordered from before this operation to after this operation.
  • memory_order_seq_cst: sequentially consistent memory order. This order enforces a consistent execution order and can be seen as the most conservative memory order. It is performance-intensive, but provides strong guarantees.

"Note": When using atomic operations, you need to select an appropriate memory order according to actual needs to ensure the correctness and consistency of data in a multi-threaded environment. The memory order option defaults to memory_order_seq_cst for all atom types unless you specify an order option for a particular operation.

7. Usage scenarios of memory alignment in C++

Memory alignment applies to three data types: struct / class / union . There are four struct/class/union memory alignment principles:

  • Alignment rules for data members: For data members of a structure (struct) or union (union), the first data member is placed at the position where the offset is 0, and the starting position of each data member storage in the future should be from the size of the member or the child of the member Start with an integer multiple of the member size.
  • Structure as a member: If there are some structure members in a structure, the structure members shall be stored from the integer multiple address of its internal "widest basic type member". (struct b is stored in struct a, and there are char, int, double and other elements in b, then b should be stored from an integer multiple of 8).
  • Finishing work: The total size of the structure, which is the result of sizeof, must be an integer multiple of the "widest basic type member" of the largest internal member. The deficiencies must be made up. (Basic types do not include struct/class/uinon).
  • sizeof(union), the size of the largest element in the structure is the size of the union, because at a certain moment, only one member of the union is actually stored at this address.

Answer analysis:

  1. What is memory alignment

In C++, memory alignment means that in order to improve the access speed and efficiency of the program, the compiler will adjust the address of the variable in the memory to a specific multiple according to specific rules, so that the addresses of different variables in the memory There is a certain interval between them, making the CPU more efficient when reading data.
In order for the CPU to quickly access the variable, the starting address of the variable should have certain characteristics, the so-called "alignment", such as the 4-byte int type, its starting address should be located on the 4-byte boundary, That is, the starting address can be divisible by 4, that is, "alignment" is related to the position of the data in memory. If the memory address of a variable is exactly an integer multiple of its length, it is said to be naturally aligned.
For example, under a 32-bit CPU, if the address of an integer variable is 0x00000004 (a multiple of 4), then it is naturally aligned, and if its address is 0x00000002 (a multiple of 4), it is not aligned. The memory space in modern computers is divided by byte. In theory, it seems that access to any type of variable can start from any address, but the actual situation is that when accessing a specific type of variable, it is often accessed at a specific memory address. It requires various types of data to be arranged in space according to certain rules, instead of being arranged one by one in sequence, which is alignment.

  1. Why byte alignment

The root cause of byte alignment is the efficiency of CPU access to data. Assuming that the address of the above integer variable is not naturally aligned, such as 0x00000002, the CPU needs to access the memory twice if it fetches its value. The first time fetches a short from 0x00000002-0x00000003, and the second fetches a short from 0x00000004-0x00000005 A short is then combined to obtain the desired data. If the variable is at the address of 0x00000003, the memory needs to be accessed three times. The first time is char, the second time is short, the third time is char, and then combined to obtain integer data.
And if the variable is in the natural alignment position, the data can be fetched only once. Some systems have very strict alignment requirements, such as the sparc system, and errors will occur if unaligned data is fetched, but no errors will occur on x86, but the efficiency will decrease.
Each hardware platform handles storage space very differently. Some platforms can only access certain types of data from certain addresses. For example, some platforms start from an even address every time. If an int type (assumed to be a 32-bit system) is stored at the beginning of an even address, then the 32bit can be read out in one read cycle, and if it is stored at an odd address. At the beginning, two read cycles are required, and the high and low bytes of the two read results are pieced together to obtain the 32bit data. Obviously, the reading efficiency drops a lot.

  1. Example of Byte Alignment
#include <iostream>
using namespace std;

union example1
{
    
    
	int a[5];
	char b;
	double c;
};
/*
如果以最长20字节为准,内部double占8字节,这段内存的地址0x00000020并不是double的整数
倍,只有当最小为0x00000024时可以满足整除double(8Byte)同时又可以容纳int a[5]的大小,
所以正确的结果应该是 sizeof(example1) = 24
*/
struct example2
{
    
    
	int a[5];
	char b;
	double c;
};
/*
如果我们不考虑字节对齐,那么内存地址0x0021不是double(8Byte)的整数倍,所以需要字节对
齐,那么此时满足是double(8Byte)的整数倍的最小整数是0x0024,说明此时char b对齐int扩充
了三个字节。所以最后的结果是 sizeof(example2) = 32
*/
struct example3
{
    
    
	int a;
	char b;
	double c;
};
/*
字节对齐除了内存起始地址要是数据类型的整数倍以外,还要满足一个条件,那就是占用的内存空间大
小需要是结构体中占用最大内存空间的类型的整数倍,所以20不是double(8Byte)的整数倍,我们还
要扩充四个字节,最后的结果是 sizeof(example3) = 16
*/
int main() 
{
    
    
	cout << "sizeof(example1) = " << sizeof(example1) << endl; // 24
	cout << "sizeof(example2) = " << sizeof(example2) << endl; // 32
	cout << "sizeof(example3) = " << sizeof(example3) << endl; // 16

	return 0;
}

8. The relationship between new/delete and malloc/free

New and delete in C++ are operators, while malloc and free are functions. They can both be used to dynamically allocate and deallocate memory while the program is running, but there are some important differences:

  • new and delete are C++-specific operators, while malloc and free are C standard library functions, so it is more convenient and intuitive to use new and delete in C++ code.
  • New and delete also call the object's constructor and destructor while performing memory allocation and release, because in C++, the life cycle of an object is closely related to the life cycle of memory. And malloc and free simply allocate and release memory, and will not involve the construction and destruction of objects.
  • new and delete support overloading, and you can implement custom memory management behavior by overloading operators new and delete. And malloc and free do not support overloading.

"Note": The use of new and delete requires the programmer to ensure the correct release of memory, otherwise it will lead to memory leaks. The use of malloc and free requires the programmer to manually manage the size and memory address of the allocated memory block, which is difficult and prone to errors. Therefore, in C++, it is recommended to use new and delete, and try to avoid using malloc and free directly.

9. Talk about how to deal with malloc and new returning null pointers because the memory block is too small

When using malloc or new to apply for a block of memory, if the requested memory size exceeds the allocated memory or the allocation fails, they will return a null pointer (pointing to address 0). There are many possible reasons for this to happen, one of the common reasons is that the memory block size requested to be allocated is too small.

For this situation, we can take the following approaches:

  • Check the memory allocation size in the code. If there is a situation where a null pointer is returned, we need to double check the size of the memory requested to be allocated to see if it is too small. If this is indeed the problem, we need to reconsider the required memory size and modify the code.
  • Check if there is enough memory available. A possible reason for malloc or new returning a null pointer is that the available memory has been exhausted. We can check the memory usage of the system to confirm this, if this is the case we need to reduce the memory being used or increase the memory available to the system.
  • Use a more efficient memory allocation method. Sometimes malloc or new cannot allocate the required memory for some reasons, but we can try to use other more efficient memory allocation methods to avoid this situation. For example, try to allocate memory using stack memory or buffer pool.
  • error handling. If the memory application fails and returns a null pointer, we need to handle this situation incorrectly, such as outputting an error message or calling some error handling functions. At the same time, we need to check whether the pointer is null when freeing the memory to prevent the program from crashing.

10. How to construct a class so that memory can only be allocated on the heap or only on the stack

In C++, we can use specific keywords and syntax to control whether objects are allocated on the heap or on the stack. A class that allows memory allocation only on the heap or only on the stack can be implemented by adding a constructor, destructor, and copy constructor to the class.

To construct a class that can only allocate memory on the heap, you can use the new operator to implement dynamic memory allocation. This class needs to disable the default constructor, prohibit the copy constructor, and prohibit the external call of the destructor.
For example:

class HeapOnlyClass {
    
    
public:
    // 禁用默认构造函数
    HeapOnlyClass() = delete;

    // 禁用拷贝构造函数
    HeapOnlyClass(const HeapOnlyClass&) = delete;

    // 析构函数只能在类内使用
    ~HeapOnlyClass() {
    
    }

    static HeapOnlyClass* create() {
    
    
        return new HeapOnlyClass();
    }
};

Thus, memory can only be allocated on the heap by calling the HeapOnlyClass::create() method. Objects of this class cannot be declared on the stack because the default constructor and copy constructor are disabled.

To construct a class that can only allocate memory on the stack, you can use the placement new syntax, which places the class object at the specified location. This class needs to have a private operator new, copy constructor, etc.
For example:

class StackOnlyClass {
    
    
private:
    // 定义operator new,因为这个类不能再堆上分配内存
    // 申请自定义内存
    void* operator new(size_t size, void* ptr) {
    
    
        return ptr;
    }

public:

    // 禁用默认构造函数
    StackOnlyClass() = delete;

    // 禁用operator new,使该类不能再堆上分配内存
    void* operator new(size_t) = delete;

    // 定义一个public的内存分配函数,用于在栈上分配对象
    static StackOnlyClass create() {
    
    
        void* stack_mem = alloca(sizeof(StackOnlyClass));
        return StackOnlyClass(stack_mem);
    }

    // 定义一个拷贝构造函数,因为该类没有禁用拷贝构造函数,但是不允许在堆上构造新对象
    StackOnlyClass(const StackOnlyClass&) {
    
    }

    // 析构函数
    ~StackOnlyClass() {
    
    }

private:
    // 私有构造函数,只能由create()方法调用
    StackOnlyClass(void* mem) {
    
    }
};

In this example, we have used the alloca() function to allocate memory on the stack. The create() method returns an object of type StackOnlyClass constructed using a block of memory. We also need to disable the default constructor and operator new operator to ensure that the class cannot allocate memory on the heap.

"Note": Classes that use "allocate memory only on the heap" and "allocate memory only on the stack" should be used carefully to avoid potential risks and problems.

11. The difference between static memory allocation and dynamic memory allocation

  • Static memory allocation is done at compile time and does not take up CPU resources; dynamic memory allocation is done at run time, and allocation and release need to take up CPU resources.
  • Static memory allocations are allocated on the stack; dynamic memory allocations are allocated on the heap.
  • Static memory allocation does not require support for pointer or reference types; dynamic memory allocation does.
  • Static memory allocation is allocated according to the plan, and the size of the memory block is determined before compilation; dynamic memory allocation is allocated as needed.
  • Static memory allocation is to give the control of memory to the compiler; dynamic memory allocation is to give the control of memory to the programmer.
  • Static memory allocation is more efficient than dynamic memory allocation, and improper dynamic memory allocation may cause memory leaks.

12. The difference between delete and delete[]

  • For simple types, after using new allocation, no matter whether it is an array of arrays or a non-array form, memory can be released in two ways:
int *a = new int(1);
delete a;
int *b = new int(2);
delete [] b;
int *c = new int[11];
delete c;
int *d = new int[12];
delete [] d;
  • For a custom type, you need to use delete for a single object, use delete[] for an object array, and call the destructor of the objects in the array one by one, so as to release all memory; if used in reverse, that is, use delete[ for a single object ], using delete on an array of objects, the behavior is undefined.
  • Therefore, the most appropriate way is to use delete if you use new; if you use new[ ], use delete[ ].

13. In C++, can the memory allocated by malloc be released by delete, and can the memory allocated by new be released by free?

In C++, the memory allocated by malloc cannot be released by delete, and the memory allocated by new cannot be released by free. This is because the malloc function and the new operator use different methods when allocating memory.

  • malloc is a memory allocation function in C language. The memory it applies for is in the heap area instead of the stack area, and it returns a void*type pointer, and the number of allocated bytes needs to be manually specified. In C++, you can use the malloc function for memory allocation, but use the free function to release memory. Using delete or delete[ ] to free malloc-allocated memory results in undefined behavior.
  • The new operator is a memory allocation operator in C++. The memory it applies for is initialized by calling the object's constructor and returns a pointer to the object. In C++, you can use the new operator for memory allocation, and use delete or delete[] to free the allocated memory. Using the free function to free memory allocated by the new operator results in undefined behavior.

"Note": It is recommended to use the new operator for memory allocation in C++, and use delete or delete[] to release the allocated memory to ensure the correct object life cycle and avoid memory leaks. If you need to use the malloc and free functions in C language in C++ code, you need to pay attention to ensure the consistency of allocation and release to avoid undefined behavior.

Guess you like

Origin blog.csdn.net/m0_51913750/article/details/130315070