C++-------动态内存管理
堆 - 栈图
在 C++ 中,栈和堆是两块不同的内存区域,有着不同的特性:
- 栈(Stack):
- 是一块连续的内存区域,由编译器自动管理。函数调用时,局部变量、函数参数等都会被压入栈中,函数结束时,相应的数据会按顺序弹出栈,内存自动回收。例如:
void func() {
int localVar = 10; // localVar 存储在栈上
}
- 栈内存分配和释放速度快,因为它的管理机制简单、规整,但大小通常有限制,取决于编译器和操作系统。
- 堆(Heap):
- 是一块相对较大、不连续的内存区域,用于动态分配内存。程序员使用
new
、malloc
等操作符来申请堆内存,使用完毕后需手动用delete
、free
释放,否则容易造成内存泄漏。例如:
- 是一块相对较大、不连续的内存区域,用于动态分配内存。程序员使用
int* ptr = new int; // 在堆上分配内存
*ptr = 20;
delete ptr;
- 堆内存的优点是灵活性高,可以按需分配较大的内存块,但分配和释放速度比栈慢,且管理难度较大。
下面是一个简单的堆 - 栈示意图:
内存区域 | 管理方式 | 分配速度 | 内存连续性 | 典型用途 |
---|---|---|---|---|
栈 | 编译器自动管理 | 快 | 连续 | 局部变量、函数参数 |
堆 | 程序员手动管理 | 慢 | 不连续 | 动态分配大内存块、对象生存期控制 |
拷贝对象
- 浅拷贝:
- 简单地复制对象的值,对于指针成员,只是复制指针的值,而不是指针指向的数据。这意味着多个对象的指针成员会指向同一块内存。例如:
class MyClass {
public:
int* data;
MyClass() {
data = new int(10);
}
MyClass(const MyClass& other) {
data = other.data;
}
};
- 浅拷贝的问题在于,如果一个对象释放了指针指向的内存,其他共享该指针的对象就会出现悬空指针。
- 深拷贝:
- 不仅复制对象的值,还递归地复制指针成员指向的数据。这样每个对象都有独立的、完整的数据副本。例如:
class MyClass {
public:
int* data;
MyClass() {
data = new int(10);
}
MyClass(const MyClass& other) {
data = new int(*other.data);
}
};
- 深拷贝确保了对象间的独立性,但代价是额外的内存和时间开销,因为要复制深层次的数据结构。
- 赋值和拷贝构造函数:
- 拷贝构造函数:用于创建一个新对象,初始化为已有对象的副本,语法形式为
ClassName(const ClassName& other)
。上面的MyClass(const MyClass& other)
就是拷贝构造函数示例。 - 赋值运算符:用于将一个已有对象的值赋给另一个已存在的对象,语法形式为
ClassName& operator=(const ClassName& other)
。例如:
- 拷贝构造函数:用于创建一个新对象,初始化为已有对象的副本,语法形式为
class MyClass {
public:
int* data;
MyClass& operator=(const MyClass& other) {
if (this!= &other) {
delete data;
data = new int(*other.data);
}
return *this;
}
};
关键字 const 的使用
- 修饰变量:
const
修饰的变量值不能被修改,一旦初始化就固定了。例如:const int num = 10;
,num
在后续代码中无法重新赋值。 - 修饰指针:
const int* ptr
:指针指向的内容是常量,不能通过该指针修改所指的值,但指针本身可以指向其他地方。int* const ptr
:指针本身是常量,不能指向其他地方,但可以修改指针指向的值。const int* const ptr
:指针和它指向的内容都是常量。
- 修饰函数参数:表示函数不会修改传入的参数,这有助于代码可读性和编译器优化,同时防止意外修改。例如:
void print(const std::string& str)
,函数内部不能修改str
。 - 修饰函数返回值:当返回值为对象或指针时,用
const
修饰可以防止调用者意外修改返回值,常用于返回类的成员变量。
charstack 类的效率
- 空间效率:
- 如果采用动态数组实现
charstack
类,如之前代码示例,可能会存在一定的空间浪费。因为动态数组每次扩容时,通常是翻倍扩容,扩容后的部分在短时间内可能闲置。例如,初始容量为 10,扩容到 20 后,可能栈里只有 11 个元素,剩余 9 个空间未使用。
- 如果采用动态数组实现
- 时间效率:
- 入栈(push):平均情况下,入栈操作时间复杂度为 O ( 1 ) O(1) O(1),但在扩容时,需要复制旧数组元素到新数组,时间复杂度变为 O ( n ) O(n) O(n),其中 n n n 是当前栈中元素个数 。
- 出栈(pop):时间复杂度为 O ( 1 ) O(1) O(1),只需调整栈顶指针即可完成出栈操作。
- 判断空满:时间复杂度均为
O
(
1
)
O(1)
O(1),只涉及简单的指针或索引比较。如果想进一步提升效率,可以考虑采用链表结构实现栈,避免数组扩容带来的额外开销,但链表实现会增加指针操作的复杂性。