一、区分指针类型
在 C++ 中,指针的声明可以非常复杂,尤其是在涉及数组、函数和指针的组合时。以下是几种常见的复杂指针声明及其详细解释:
1. int *p[10]
解释
p是一个数组,包含 10 个元素。- 每个元素是一个指向
int的指针。
示例
int a = 1, b = 2, c = 3;
int* p[10] = {&a, &b, &c}; // p 是一个包含 10 个 int* 的数组
std::cout << *p[0]; // 输出 1
特点
p是一个指针数组。- 每个元素是一个
int*。
2. int (*p)[10]
解释
p是一个指针,指向一个包含 10 个int元素的数组,是一个指向数组的指针,而不是数组本身。
示例
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int (*p)[10] = &arr; // p 指向一个包含 10 个 int 的数组
std::cout << (*p)[0]; // 输出 1
特点
p是一个指针,指向一个int[10]类型的数组。*p是一个数组,大小为 10。sizeof(p)返回指针的大小(通常为 8 字节,在 64 位系统上)。sizeof(*p)返回数组的大小(10 * sizeof(int))。
3. int *p(int)
解释
p是一个函数,接受一个int参数,并返回一个int*。
示例
int* func(int x) {
static int value = x;
return &value;
}
int* (*p)(int) = func; // p 是一个函数指针,指向 func
std::cout << *p(10); // 输出 10
特点
p是一个函数指针。- 函数返回类型是
int*。
4. int (*p)(int)
解释
p是一个指针,指向一个函数,该函数接受一个int参数并返回一个int。
示例
int add(int x) {
return x + 10;
}
int (*p)(int) = add; // p 是一个函数指针,指向 add
std::cout << p(5); // 输出 15
特点
p是一个函数指针。- 函数返回类型是
int。
5. int (*p[10])(int)
解释
p是一个数组,包含 10 个元素。- 每个元素是一个函数指针,指向一个接受
int参数并返回int的函数。
示例
int add(int x) { return x + 1; }
int sub(int x) { return x - 1; }
int (*p[10])(int) = {add, sub}; // p 是一个包含 10 个函数指针的数组
std::cout << p[0](5); // 输出 6
std::cout << p[1](5); // 输出 4
特点
p是一个函数指针数组。- 每个元素是一个
int (*)(int)类型的函数指针。
6. int (*(*p)[10])(int)
解释
p是一个指针,指向一个包含 10 个元素的数组。- 每个元素是一个函数指针,指向一个接受
int参数并返回int的函数。
示例
int add(int x) { return x + 1; }
int sub(int x) { return x - 1; }
int (*funcArray[10])(int) = {add, sub}; // funcArray 是一个包含 10 个函数指针的数组
int (*(*p)[10])(int) = &funcArray; // p 是一个指针,指向 funcArray
std::cout << (*p)[0](5); // 输出 6
std::cout << (*p)[1](5); // 输出 4
特点
p是一个指向函数指针数组的指针。p指向一个int (*[10])(int)类型的数组。
7. int *(*p[10])(int)
解释
p是一个数组,包含 10 个元素。- 每个元素是一个函数指针,指向一个接受
int参数并返回int*的函数。
示例
int* func(int x) {
static int value = x;
return &value;
}
int* (*p[10])(int) = {func}; // p 是一个包含 10 个函数指针的数组
std::cout << *p[0](10); // 输出 10
特点
p是一个函数指针数组。- 每个元素是一个
int* (*)(int)类型的函数指针。
8. int (*const p)(int)
解释
p是一个常量指针,指向一个函数,该函数接受int参数并返回int。
示例
int add(int x) {
return x + 10;
}
int (*const p)(int) = add; // p 是一个常量函数指针,指向 add
std::cout << p(5); // 输出 15
// p = sub; // 错误:p 是常量指针,不能修改
特点
p是一个常量函数指针。p的值(即指向的函数)不能修改。
9. int *const p[10]
解释
p是一个数组,包含 10 个元素。- 每个元素是一个常量指针,指向
int。
示例
int a = 1, b = 2;
int* const p[10] = {&a, &b}; // p 是一个包含 10 个常量指针的数组
std::cout << *p[0]; // 输出 1
// p[0] = &b; // 错误:p[0] 是常量指针,不能修改
特点
p是一个常量指针数组。- 每个元素是一个
int* const类型的常量指针。
10. int const *p[10]
解释
p是一个数组,包含 10 个元素。- 每个元素是一个指针,指向
const int。
示例
const int a = 1, b = 2;
const int* p[10] = {&a, &b}; // p 是一个包含 10 个指针的数组,指向 const int
std::cout << *p[0]; // 输出 1
// *p[0] = 10; // 错误:p[0] 指向 const int,不能修改
特点
p是一个指向常量的指针数组。- 每个元素是一个
const int*类型的指针。
总结
| 声明形式 | 解释 |
|---|---|
int *p[10] | p 是一个包含 10 个 int* 的数组。 |
int (*p)[10] | p 是一个指针,指向一个包含 10 个 int 的数组。 |
int *p(int) | p 是一个函数,接受 int 参数并返回 int*。 |
int (*p)(int) | p 是一个指针,指向一个接受 int 参数并返回 int 的函数。 |
int (*p[10])(int) | p 是一个包含 10 个函数指针的数组,每个指针指向 int (*)(int)。 |
int (*(*p)[10])(int) | p 是一个指针,指向一个包含 10 个函数指针的数组。 |
int *(*p[10])(int) | p 是一个包含 10 个函数指针的数组,每个指针指向 int* (*)(int)。 |
int (*const p)(int) | p 是一个常量指针,指向一个接受 int 参数并返回 int 的函数。 |
int *const p[10] | p 是一个包含 10 个 int* const 的数组。 |
int const *p[10] | p 是一个包含 10 个 const int* 的数组。 |
通过理解这些复杂的指针声明,可以更好地掌握 C++ 的指针和函数的高级用法。
二、new/delete 与 malloc/free的异同
在 C++ 中,new/delete 和 malloc/free 都用于动态内存管理,但它们的设计理念、使用方式和功能有显著区别。以下是它们的异同点及详细介绍。
1. 相同点
- 动态内存分配:两者都用于在堆(Heap)上动态分配内存。
- 手动管理:都需要程序员手动释放内存,否则会导致内存泄漏。
- 返回指针:两者都返回指向分配内存的指针。
2. 不同点
| 特性 | new/delete | malloc/free |
|---|---|---|
| 语言特性 | C++ 关键字 | C 标准库函数 |
| 内存初始化 | 调用构造函数初始化对象 | 不调用构造函数,仅分配内存 |
| 内存释放 | 调用析构函数销毁对象 | 不调用析构函数,仅释放内存 |
| 类型安全 | 类型安全,返回具体类型的指针 | 类型不安全,返回 void*,需要强制转换 |
| 内存大小计算 | 自动计算所需内存大小 | 需要手动计算内存大小 |
| 异常处理 | 分配失败时抛出 std::bad_alloc 异常 | 分配失败时返回 NULL |
| 重载支持 | 支持重载 new 和 delete 操作符 | 不支持重载 |
| 数组支持 | 支持 new[] 和 delete[] 分配数组 | 不支持数组分配和释放的专用语法 |
| 性能 | 通常较慢,因为涉及构造函数调用 | 通常较快,仅分配内存 |
3. 详细说明
(1) new/delete
- 分配内存并初始化对象:
new不仅分配内存,还会调用对象的构造函数。delete不仅释放内存,还会调用对象的析构函数。
- 类型安全:
new返回具体类型的指针,无需强制转换。
- 异常处理:
- 如果内存分配失败,
new会抛出std::bad_alloc异常。
- 如果内存分配失败,
- 数组支持:
- 使用
new[]分配数组,delete[]释放数组。
- 使用
- 重载支持:
- 可以重载
new和delete操作符,自定义内存管理行为。
- 可以重载
示例
class MyClass {
public:
MyClass() { std::cout << "Constructor\n"; }
~MyClass() { std::cout << "Destructor\n"; }
};
int main() {
MyClass* obj = new MyClass; // 分配内存并调用构造函数
delete obj; // 调用析构函数并释放内存
int* arr = new int[10]; // 分配数组
delete[] arr; // 释放数组
return 0;
}
(2) malloc/free
- 仅分配内存:
malloc仅分配内存,不调用构造函数。free仅释放内存,不调用析构函数。
- 类型不安全:
malloc返回void*,需要强制转换为具体类型。
- 异常处理:
- 如果内存分配失败,
malloc返回NULL。
- 如果内存分配失败,
- 数组支持:
- 没有专门的语法,需要手动计算数组大小。
- 性能:
- 通常比
new快,因为不涉及构造函数调用。
- 通常比
示例
#include <cstdlib>
class MyClass {
public:
MyClass() { std::cout << "Constructor\n"; }
~MyClass() { std::cout << "Destructor\n"; }
};
int main() {
MyClass* obj = (MyClass*)malloc(sizeof(MyClass)); // 仅分配内存
free(obj); // 仅释放内存
int* arr = (int*)malloc(10 * sizeof(int)); // 分配数组
free(arr); // 释放数组
return 0;
}
4. 使用场景
new/delete
- 适用于 C++ 对象,尤其是需要调用构造函数和析构函数的情况。
- 需要类型安全和异常处理的场景。
- 需要分配和释放数组的场景。
malloc/free
- 适用于 C 代码或与 C 库交互的场景。
- 需要高性能且不需要调用构造函数和析构函数的场景。
- 需要手动管理内存大小的场景。
5. 注意事项
- 不要混用:
- 使用
new分配的内存必须用delete释放。 - 使用
malloc分配的内存必须用free释放。 - 混用会导致未定义行为。
- 使用
- 内存泄漏:
- 无论是
new/delete还是malloc/free,都需要确保释放内存,否则会导致内存泄漏。
- 无论是
- 析构函数调用:
- 使用
malloc/free时,不会调用构造函数和析构函数,可能导致资源泄漏。
- 使用
6. 总结
| 特性 | new/delete | malloc/free |
|---|---|---|
| 语言特性 | C++ 关键字 | C 标准库函数 |
| 内存初始化 | 调用构造函数初始化对象 | 不调用构造函数,仅分配内存 |
| 内存释放 | 调用析构函数销毁对象 | 不调用析构函数,仅释放内存 |
| 类型安全 | 类型安全,返回具体类型的指针 | 类型不安全,返回 void*,需要强制转换 |
| 异常处理 | 分配失败时抛出 std::bad_alloc 异常 | 分配失败时返回 NULL |
| 数组支持 | 支持 new[] 和 delete[] 分配数组 | 不支持数组分配和释放的专用语法 |
| 性能 | 通常较慢,因为涉及构造函数调用 | 通常较快,仅分配内存 |
在 C++ 中,优先使用 new/delete,因为它们更安全、更符合面向对象的设计理念。只有在特定场景(如与 C 库交互或需要高性能)时,才使用 malloc/free。
三、new和delete是如何实现的?
new 和 delete 是 C++ 中用于动态内存管理的操作符,它们不仅分配和释放内存,还会调用对象的构造函数和析构函数。它们的实现依赖于 C++ 运行时库和底层的内存管理机制。以下是对 new 和 delete 实现机制的详细说明。
1. new 的实现
new 操作符用于动态分配内存并调用对象的构造函数。
实现机制
-
内存分配:
new首先调用operator new函数分配内存。operator new是 C++ 标准库提供的全局函数,其底层通常调用malloc或操作系统的内存分配函数(如mmap)。- 如果内存分配失败,
operator new会抛出std::bad_alloc异常。
-
调用构造函数:
- 在分配的内存上调用对象的构造函数,初始化对象。
-
返回指针:
- 返回指向已初始化对象的指针。
示例伪代码
void* operator new(std::size_t size) {
void* ptr = std::malloc(size); // 调用底层内存分配函数
if (!ptr) {
throw std::bad_alloc(); // 分配失败时抛出异常
}
return ptr;
}
template <typename T>
T* new_impl() {
void* ptr = operator new(sizeof(T)); // 分配内存
new (ptr) T(); // 在分配的内存上调用构造函数(placement new)
return static_cast<T*>(ptr); // 返回指针
}
示例使用
class MyClass {
public:
MyClass() { std::cout << "Constructor\n"; }
~MyClass() { std::cout << "Destructor\n"; }
};
int main() {
MyClass* obj = new MyClass; // 分配内存并调用构造函数
delete obj; // 调用析构函数并释放内存
return 0;
}
2. delete 的实现
delete 操作符用于释放由 new 分配的内存,并调用对象的析构函数。
实现机制
-
调用析构函数:
delete首先调用对象的析构函数,释放对象持有的资源(如动态内存、文件句柄等)。
-
释放内存:
- 调用
operator delete函数释放内存。operator delete是 C++ 标准库提供的全局函数,其底层通常调用free或操作系统的内存释放函数(如munmap)。
- 调用
示例伪代码
void operator delete(void* ptr) noexcept {
std::free(ptr); // 调用底层内存释放函数
}
template <typename T>
void delete_impl(T* ptr) {
if (ptr) {
ptr->~T(); // 调用析构函数
operator delete(ptr); // 释放内存
}
}
示例使用
class MyClass {
public:
MyClass() { std::cout << "Constructor\n"; }
~MyClass() { std::cout << "Destructor\n"; }
};
int main() {
MyClass* obj = new MyClass; // 分配内存并调用构造函数
delete obj; // 调用析构函数并释放内存
return 0;
}
3. new[] 和 delete[] 的实现
new[] 和 delete[] 是用于动态分配和释放数组的操作符。
new[] 的实现
-
内存分配:
new[]调用operator new[]函数分配内存。operator new[]的底层通常调用operator new。- 分配的内存大小包括数组元素的大小和额外的元数据(如数组长度)。
-
调用构造函数:
- 对数组中的每个元素调用构造函数。
-
返回指针:
- 返回指向数组的指针。
delete[] 的实现
-
调用析构函数:
- 对数组中的每个元素调用析构函数。
-
释放内存:
- 调用
operator delete[]函数释放内存。operator delete[]的底层通常调用operator delete。
- 调用
示例伪代码
void* operator new[](std::size_t size) {
void* ptr = operator new(size); // 调用 operator new 分配内存
return ptr;
}
void operator delete[](void* ptr) noexcept {
operator delete(ptr); // 调用 operator delete 释放内存
}
template <typename T>
T* new_array_impl(std::size_t count) {
void* ptr = operator new[](sizeof(T) * count); // 分配内存
T* arr = static_cast<T*>(ptr);
for (std::size_t i = 0; i < count; ++i) {
new (&arr[i]) T(); // 对每个元素调用构造函数
}
return arr;
}
template <typename T>
void delete_array_impl(T* ptr, std::size_t count) {
if (ptr) {
for (std::size_t i = 0; i < count; ++i) {
ptr[i].~T(); // 对每个元素调用析构函数
}
operator delete[](ptr); // 释放内存
}
}
示例使用
class MyClass {
public:
MyClass() { std::cout << "Constructor\n"; }
~MyClass() { std::cout << "Destructor\n"; }
};
int main() {
MyClass* arr = new MyClass[3]; // 分配数组并调用构造函数
delete[] arr; // 调用析构函数并释放内存
return 0;
}
4. 底层内存管理
new 和 delete 的底层通常依赖于操作系统的内存管理机制,例如:
- 在 Linux 上,
malloc和free可能使用glibc的ptmalloc实现。 - 在 Windows 上,
malloc和free可能使用HeapAlloc和HeapFree。
5. 总结
| 操作符 | 功能 | 底层实现 |
|---|---|---|
new | 分配内存并调用构造函数 | 调用 operator new,底层通常为 malloc |
delete | 调用析构函数并释放内存 | 调用 operator delete,底层通常为 free |
new[] | 分配数组内存并调用每个元素的构造函数 | 调用 operator new[],底层通常为 malloc |
delete[] | 调用每个元素的析构函数并释放数组内存 | 调用 operator delete[],底层通常为 free |
通过理解 new 和 delete 的实现机制,可以更好地掌握 C++ 的动态内存管理,并避免常见的内存管理错误。
四、malloc和free是如何实现的?
malloc 和 free 是 C 标准库中用于动态内存管理的函数,它们的实现依赖于底层的内存管理机制。虽然具体的实现可能因编译器和操作系统而异,但它们的基本工作原理是相似的。以下是对 malloc 和 free 实现机制的详细说明。
1. malloc 的实现
malloc 用于在堆(Heap)上动态分配内存。
实现机制
-
内存块管理:
- 动态分配的内存通常由内存管理器(如
glibc的ptmalloc)维护。 - 内存管理器将堆划分为多个内存块,每个块包含一个头部(header),用于存储元数据(如块大小、是否已分配等)。
- 动态分配的内存通常由内存管理器(如
-
分配内存:
malloc首先在空闲链表(free list)中查找足够大的空闲块。- 如果找到合适的块,则将其标记为“已分配”,并返回指向用户可用内存的指针。
- 如果没有找到合适的块,则向操作系统申请更多的内存(如通过
sbrk或mmap系统调用)。
-
内存对齐:
malloc通常会对分配的内存进行对齐,以满足硬件或操作系统的要求。
示例伪代码
void* malloc(size_t size) {
if (size == 0) return NULL;
// 对齐内存大小
size = align(size);
// 在空闲链表中查找合适的块
BlockHeader* block = find_free_block(size);
if (block) {
// 标记为已分配
block->is_allocated = 1;
return (void*)(block + 1); // 返回用户可用内存
}
// 向操作系统申请更多内存
block = request_memory_from_os(size);
if (!block) {
return NULL; // 申请失败
}
// 标记为已分配
block->is_allocated = 1;
return (void*)(block + 1); // 返回用户可用内存
}
2. free 的实现
free 用于释放由 malloc、calloc 或 realloc 分配的内存。
实现机制
-
释放内存:
free通过传入的指针找到对应的内存块头部。- 将内存块标记为“未分配”,并将其加入空闲链表(free list)中。
-
合并空闲块:
- 如果相邻的内存块也是空闲的,可能会进行合并(coalescing),以减少内存碎片。
-
返回内存给操作系统:
- 如果释放的内存块较大,内存管理器可能会将其返回给操作系统(如通过
munmap系统调用)。 - 较小的内存块通常保留在进程的内存池中,供后续分配使用。
- 如果释放的内存块较大,内存管理器可能会将其返回给操作系统(如通过
示例伪代码
void free(void* ptr) {
if (ptr == NULL) return; // 忽略空指针
// 获取内存块头部
BlockHeader* header = (BlockHeader*)ptr - 1;
// 标记为未分配
header->is_allocated = 0;
// 加入空闲链表
add_to_free_list(header);
// 合并相邻的空闲块
coalesce(header);
// 如果内存块较大,返回给操作系统
if (header->size > THRESHOLD) {
return_memory_to_os(header);
}
}
3. 底层内存管理
malloc 和 free 的底层通常依赖于操作系统的内存管理机制,例如:
- 在 Linux 上,
malloc和free可能使用glibc的ptmalloc实现。 - 在 Windows 上,
malloc和free可能使用HeapAlloc和HeapFree。
常见内存管理策略
- 首次适应(First Fit):
- 在空闲链表中查找第一个足够大的块。
- 最佳适应(Best Fit):
- 在空闲链表中查找最小的足够大的块。
- 最差适应(Worst Fit):
- 在空闲链表中查找最大的块。
- 伙伴系统(Buddy System):
- 将内存划分为大小固定的块,便于合并和分配。
4. 内存碎片
动态内存管理的一个常见问题是内存碎片,分为两种:
- 外部碎片:
- 空闲内存块分散在堆中,无法满足大块内存的分配请求。
- 内部碎片:
- 分配的内存块比实际需要的更大,导致部分内存浪费。
malloc 和 free 通过合并空闲块和合理的内存分配策略来减少内存碎片。
5. 总结
| 函数 | 功能 | 实现机制 |
|---|---|---|
malloc | 在堆上动态分配内存 | 查找空闲块或向操作系统申请内存 |
free | 释放动态分配的内存 | 标记为未分配,合并空闲块,返回给操作系统 |
通过理解 malloc 和 free 的实现机制,可以更好地掌握动态内存管理的工作原理,并避免常见的内存管理错误。
五、被free或者delete回收的内存是直接交还给OS吗?
被 free 或 delete 回收的内存是否直接交还给操作系统,取决于具体的内存管理策略和实现。通常情况下,内存管理器(如 glibc 的 ptmalloc)会采用一种分层的内存管理机制,将内存分为小块和大块,并根据不同的策略决定是否将内存交还给操作系统。以下是对这一过程的详细说明。
1. 内存管理器的分层机制
现代内存管理器通常将内存分为以下几层:
- 小块内存(Small Blocks):
- 小块内存通常由内存管理器维护在进程的堆(Heap)中,不会立即交还给操作系统。
- 这些内存块会被加入空闲链表(Free List),供后续分配使用。
- 大块内存(Large Blocks):
- 大块内存通常直接通过系统调用(如
mmap)从操作系统分配。 - 当大块内存被释放时,内存管理器可能会立即将其交还给操作系统(如通过
munmap)。
- 大块内存通常直接通过系统调用(如
2. free 的行为
free 的行为取决于内存块的大小和内存管理器的策略:
- 小块内存:
- 小块内存被释放后,通常会被加入空闲链表,供后续分配使用。
- 这些内存块不会立即交还给操作系统,以减少频繁的系统调用开销。
- 大块内存:
- 大块内存被释放后,可能会立即交还给操作系统(如通过
munmap)。 - 这是因为大块内存占用较多资源,及时释放可以减轻内存压力。
- 大块内存被释放后,可能会立即交还给操作系统(如通过
示例
#include <stdlib.h>
int main() {
void* small_block = malloc(100); // 分配小块内存
void* large_block = malloc(1024 * 1024); // 分配大块内存
free(small_block); // 小块内存加入空闲链表,不交还 OS
free(large_block); // 大块内存可能立即交还 OS
return 0;
}
3. delete 的行为
delete 的行为与 free 类似,但会先调用对象的析构函数:
- 小块内存:
- 小块内存被释放后,通常会被加入空闲链表,供后续分配使用。
- 大块内存:
- 大块内存被释放后,可能会立即交还给操作系统。
示例
#include <iostream>
int main() {
int* small_block = new int[100]; // 分配小块内存
int* large_block = new int[1024 * 1024]; // 分配大块内存
delete[] small_block; // 小块内存加入空闲链表,不交还 OS
delete[] large_block; // 大块内存可能立即交还 OS
return 0;
}
4. 内存管理器的优化策略
内存管理器通常会采用以下优化策略:
- 缓存机制:
- 小块内存会被缓存在空闲链表中,以减少频繁的系统调用。
- 合并空闲块:
- 相邻的空闲块会被合并,以减少内存碎片。
- 延迟释放:
- 大块内存可能会延迟释放,以避免频繁的系统调用开销。
5. 手动控制内存释放
如果需要强制将内存交还给操作系统,可以使用以下方法:
malloc_trim(Linux):malloc_trim(0); // 尝试将空闲内存交还 OSmmap和munmap:- 直接使用
mmap分配内存,并在释放时使用munmap交还 OS。
void* ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); munmap(ptr, size); // 直接交还 OS- 直接使用
6. 总结
| 内存块大小 | free/delete 行为 | 是否交还 OS |
|---|---|---|
| 小块内存 | 加入空闲链表,供后续分配使用 | 否 |
| 大块内存 | 可能立即交还给操作系统(如通过 munmap) | 是 |
通过理解 free 和 delete 的行为,可以更好地管理内存资源,避免内存泄漏和碎片化。