Dynamic Memory之前的问题
在这篇文章中已经总结了C++中的变量和各种初始化方式,了解这些内容对于变量的内存使用是必要的。到目前为止,就scope和lifetime来说,有三种类型的变量:
Global objects
are allocated at program start-up and destroyed when the program ends.Local, automatic objects
are created and destroyed when the block in which they are defined is entered and exited.Local static objects
are allocated before their first use and are destroyed when the program ends.
虽然,它们在scope和lifetime上均不同,但是有一个共同点,即都是由程序隐式控制整个lifetime,即内存的分配和释放不用程序员操心。
但是,有时候,内存的管理非常复杂,根据该文:
- you cannot determine the maximum amount of memory to use at compile time;
- you want to allocate a very large object;
- you want to build data structures (containers) without a fixed upper size;
另外一个目的,根据C++ Primer的说法:
One common reason to use dynamic memory is to allow multiple objects to share the same state.
这时候,Dynamic Memory
能够解决问题。
Dynamically allocated objects have a lifetime that is independent of where they are created; they exist until they are explicitly freed.
但是,在Dynamic Memory
中的一个核心的问题是——在正确的时间,申请和释放正确的内存。
变量在内存中的分配
在C++中,变量在内存中的分配区域,多种资料使用的术语不尽相同,虽然查了很多资料,但是无法统一。虽然存在差异,但是基本的原则大体相同,如下图所示:
基本可以包括:
- stack
- used for nonstatic objects defined inside functions.
- 也就是之前提到的automatic objects;
- heap
- objects which are dynamically allocated at run time
- static data section
- bss segment: uninitialized data
- stand for block starting symbol
- including
- uninitialized global objects
- uninitialized local static objects
- these uninitialized objects will be initialized by zero/nullptr.
- data segment: initialized data
- including
- initialized global objects
- initialized local static objects
- constants: declared by
const
; - external variables: declared by
extern
keyword
- including
- bss segment: uninitialized data
- code/text segments
- store program executable code.
- read-only with fixed size
new & delete的使用
C++中,最开始使用new & delete 两个操作符来实现内存的申请和释放:
new
, which allocates, and optionally initializes, an object in dynamic memory and returns a pointer to that object.delete
, which takes a pointer to a dynamic object, destroys that object, and frees the associated memory.
new
使用new在heap中分配的内存是没有名字的,因为不能通过变量名来访问,但是new返回了指向该内存地址的一个指针,可以通过指针来访问这个内存中的object。
// pi points to int object without initialization
int *pi = new int
初始化方式
default initialization
虽然分配了内存,但是如果没有显式初始化的话,这些object使用default initialization。
- objects of built-in or compound type have undefined value;
- objects of class type are initialized by their default constructor
// initialized to empty string string * ps = new string; // pi points to an uninitialized int int *pi = new int;
direct initialization
// object to which pi points has value 1024 int * pi = new int(1024); // *ps is "9999999999" string *ps = new string(10, ’9’);
list initialization
// vector with ten elements with values from 0 to 9 vector<int> * pv = new vector<int>{0,1,2,3,4,5,6,7,8,9};
value initialization(following the type name with a pair of empty parentheses)
- For class types, the object is initialized by the default constructor.
- For built-in types, they are initialized by well-defined values. (一般为0)
// value initialized to the empty string string * ps = new string(); // value initialized to 0; *pi2 is 0 int *pi = new int();
考虑到不同的初始化方式的影响,我们最好显式地对动态分配的内存进行初始化。
auto的使用限制
使用auto可以进行类型推导,但是必须从initilizer中推导,当使用direct initialization时,只能提供一个initializer。
// ok
auto p1 = new auto(1);
// error: initialization of new-expression for type ‘auto’ requires exactly one element
auto p2 = new auto{1, 2, 3};
// error: initialization of new-expression for type ‘auto’ requires exactly one element
auto p2 = new auto(1, 2, 3);
处理const
使用new
为const
对象分配内存是可行的:
// allocate and initialize a const int
const int * pci = new const int(1024);
// allocate a default-initialized const empty string
const string * pcs = new const string;
当为const
对象分配内存时,必须要初始化。
对于基本类型,必须显式初始化,否则报错。
// ok, *pci = 0 // pointer to const int const int *pci = new const int(); // error: uninitialized const in ‘new’ of ‘const int’ const int *pci = new const int;
对于class type,则情况有点复杂。
类有成员变量,无默认构造函数;
class A { public: int i; }; // ok A *a = new A; // ok A *a = new A(); // error: uninitialized const in ‘new’ of ‘const class A’ const A *a = new const A; // ok const A *a = new const A();
类有成员变量,有默认构造函数;
class A { public: int i; A() = default; }; // ok A *a = new A; // ok A *a = new A(); // error: uninitialized const in ‘new’ of ‘const class A’ const A *a = new const A; // ok const A *a = new const A();
类无成员变量,无默认构造函数;
class A { }; // ok A *a = new A; // ok A *a = new A(); // ok const A *a = new const A; // ok const A *a = new const A();
类无成员变量,有默认构造函数;
class A { public: A() = default; }; // ok A *a = new A; // ok A *a = new A(); // ok const A *a = new const A; // ok const A *a = new const A();
总结来看:
- 对于使用new来动态分配const对象的内存时,如果有成员变量时,必须显式初始化,不能隐式初始化;
- 对于没有成员变量时,可以使用隐式初始化;
异常处理
当内存分配失败时,使用new
会抛出异常,如下:
// if allocation fails, new throws std::bad_alloc
int *p1 = new int;
一般在分配大内存时,有可能会失败。 如果我们不希望new
抛出异常,可以使用nothrow
关键字。
// if allocation fails, new returns a null pointer
int *p2 = new (nothrow) int;
这种使用方式称为placement new。
bad_alloc
与nothrow
均定义在<new>
header中。
delete
与new对应的,delete完成2件事情:
- It destroys the object to which its given pointer points,
- and it frees the corresponding memory.
特点
传入的pointer value可以是new分配的,也可以是nullptr;
也就是说,delete一个空指针也是可以的。
// p must point to a dynamically allocated object or be null delete p;
删除不是new分配的内存时,其行为是undefined的。
int i = 10; int *pi1 = &i; int *pi2 = nullptr; // error: i is not a pointer // error: type ‘int’ argument given to ‘delete’, expected pointer delete i; // undefined: pi1 refers to a object on stack, no errors occurred from the compiler. delete pi1; // ok: it is always ok to delete a null pointer delete pi2;
对同一块内存释放两次,其行为也是undefined的。
double *pd = new double(33); double *pd2 = pd; // ok delete pd; // undefined: the memory pointed to by pd2 was already freed delete pd2;
在clang++编译器下,对于释放两次内存的这种操作,运行时出现错误如下:
释放const对象对应的资源,其方式与一般的object并无二致。
const int * pci = new const int(1024); // ok: deletes a const object delete pci;
通过new创建的动态内存,在没有显式释放前会一直存在;
这意味着block scope对其不起作用,即便函数返回了,其中创建的动态内存也不会释放,对应的object也不会有任何变化。这与smart pointers是不同的。
这样的好处是,这块内存以及对应的变量会一直存在,不用担心由系统收回。但是也存在弊端,当我们忘记显式释放这块内存时,会导致内存泄露,尤其是当在函数中返回时,我们无法获取到对应的指针了,就无法释放对应的内存。
void func(){ int *p = new int(10); return; }
此时,其中p指向的内存,在函数返回后仍然存在,但是无法获取到指针p,也就无法释放了。
在delete指向对应内存的指针后,将该指针置nullptr是一种推荐的方式。
因为即便删除了对应的内存,但是在一些系统中,对应的指针仍然持有这块已经被删除的内存的地址,这时候该指针成为dangling pointer。为了避免再次使用该指针,显式地将其赋值为nullptr,起到提醒的作用。
但是对于指向相同内存空间的不同指针,将其中一个置为nullptr,不会影响另外的指针。
int * p(new int(42)); // p and q point to the same memory auto q = p; // invalidates both p and q delete p; // indicates that p is no longer bound to an object p = nullptr; // q is still a dangling pointer .....
参考资料
- C++ Primer - 12.1.2 Managing Memory Directly
- https://en.cppreference.com/w/cpp/language/new
- https://courses.engr.illinois.edu/cs225/fa2022/resources/stack-heap/
- https://sbme-tutorials.github.io/2018/data-structures/notes/2_week2a.html
- https://www.javatpoint.com/memory-layout-in-c
- https://developerinsider.co/memory-layout-representation-of-c-program/
- https://lovemesomecoding.com/data-structure-algorithm/data-structure-algorithm-memory
- https://www.scaler.com/topics/c/memory-layout-in-c/
- https://hackthedeveloper.com/memory-layout-c-program/
- https://en.wikipedia.org/wiki/Data_segment