C++基础语法(6~10)

48 阅读19分钟

一、区分指针类型

在 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/deletemalloc/free 都用于动态内存管理,但它们的设计理念、使用方式和功能有显著区别。以下是它们的异同点及详细介绍。


1. 相同点

  • 动态内存分配:两者都用于在堆(Heap)上动态分配内存。
  • 手动管理:都需要程序员手动释放内存,否则会导致内存泄漏。
  • 返回指针:两者都返回指向分配内存的指针。

2. 不同点

特性new/deletemalloc/free
语言特性C++ 关键字C 标准库函数
内存初始化调用构造函数初始化对象不调用构造函数,仅分配内存
内存释放调用析构函数销毁对象不调用析构函数,仅释放内存
类型安全类型安全,返回具体类型的指针类型不安全,返回 void*,需要强制转换
内存大小计算自动计算所需内存大小需要手动计算内存大小
异常处理分配失败时抛出 std::bad_alloc 异常分配失败时返回 NULL
重载支持支持重载 newdelete 操作符不支持重载
数组支持支持 new[]delete[] 分配数组不支持数组分配和释放的专用语法
性能通常较慢,因为涉及构造函数调用通常较快,仅分配内存

3. 详细说明

(1) new/delete

  • 分配内存并初始化对象
    • new 不仅分配内存,还会调用对象的构造函数。
    • delete 不仅释放内存,还会调用对象的析构函数。
  • 类型安全
    • new 返回具体类型的指针,无需强制转换。
  • 异常处理
    • 如果内存分配失败,new 会抛出 std::bad_alloc 异常。
  • 数组支持
    • 使用 new[] 分配数组,delete[] 释放数组。
  • 重载支持
    • 可以重载 newdelete 操作符,自定义内存管理行为。

示例

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/deletemalloc/free
语言特性C++ 关键字C 标准库函数
内存初始化调用构造函数初始化对象不调用构造函数,仅分配内存
内存释放调用析构函数销毁对象不调用析构函数,仅释放内存
类型安全类型安全,返回具体类型的指针类型不安全,返回 void*,需要强制转换
异常处理分配失败时抛出 std::bad_alloc 异常分配失败时返回 NULL
数组支持支持 new[]delete[] 分配数组不支持数组分配和释放的专用语法
性能通常较慢,因为涉及构造函数调用通常较快,仅分配内存

在 C++ 中,优先使用 new/delete,因为它们更安全、更符合面向对象的设计理念。只有在特定场景(如与 C 库交互或需要高性能)时,才使用 malloc/free


三、new和delete是如何实现的?

newdelete 是 C++ 中用于动态内存管理的操作符,它们不仅分配和释放内存,还会调用对象的构造函数和析构函数。它们的实现依赖于 C++ 运行时库和底层的内存管理机制。以下是对 newdelete 实现机制的详细说明。


1. new 的实现

new 操作符用于动态分配内存并调用对象的构造函数。

实现机制

  1. 内存分配

    • new 首先调用 operator new 函数分配内存。operator new 是 C++ 标准库提供的全局函数,其底层通常调用 malloc 或操作系统的内存分配函数(如 mmap)。
    • 如果内存分配失败,operator new 会抛出 std::bad_alloc 异常。
  2. 调用构造函数

    • 在分配的内存上调用对象的构造函数,初始化对象。
  3. 返回指针

    • 返回指向已初始化对象的指针。

示例伪代码

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 分配的内存,并调用对象的析构函数。

实现机制

  1. 调用析构函数

    • delete 首先调用对象的析构函数,释放对象持有的资源(如动态内存、文件句柄等)。
  2. 释放内存

    • 调用 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[] 的实现

  1. 内存分配

    • new[] 调用 operator new[] 函数分配内存。operator new[] 的底层通常调用 operator new
    • 分配的内存大小包括数组元素的大小和额外的元数据(如数组长度)。
  2. 调用构造函数

    • 对数组中的每个元素调用构造函数。
  3. 返回指针

    • 返回指向数组的指针。

delete[] 的实现

  1. 调用析构函数

    • 对数组中的每个元素调用析构函数。
  2. 释放内存

    • 调用 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. 底层内存管理

newdelete 的底层通常依赖于操作系统的内存管理机制,例如:

  • 在 Linux 上,mallocfree 可能使用 glibcptmalloc 实现。
  • 在 Windows 上,mallocfree 可能使用 HeapAllocHeapFree

5. 总结

操作符功能底层实现
new分配内存并调用构造函数调用 operator new,底层通常为 malloc
delete调用析构函数并释放内存调用 operator delete,底层通常为 free
new[]分配数组内存并调用每个元素的构造函数调用 operator new[],底层通常为 malloc
delete[]调用每个元素的析构函数并释放数组内存调用 operator delete[],底层通常为 free

通过理解 newdelete 的实现机制,可以更好地掌握 C++ 的动态内存管理,并避免常见的内存管理错误。


四、malloc和free是如何实现的?

mallocfree 是 C 标准库中用于动态内存管理的函数,它们的实现依赖于底层的内存管理机制。虽然具体的实现可能因编译器和操作系统而异,但它们的基本工作原理是相似的。以下是对 mallocfree 实现机制的详细说明。


1. malloc 的实现

malloc 用于在堆(Heap)上动态分配内存。

实现机制

  1. 内存块管理

    • 动态分配的内存通常由内存管理器(如 glibcptmalloc)维护。
    • 内存管理器将堆划分为多个内存块,每个块包含一个头部(header),用于存储元数据(如块大小、是否已分配等)。
  2. 分配内存

    • malloc 首先在空闲链表(free list)中查找足够大的空闲块。
    • 如果找到合适的块,则将其标记为“已分配”,并返回指向用户可用内存的指针。
    • 如果没有找到合适的块,则向操作系统申请更多的内存(如通过 sbrkmmap 系统调用)。
  3. 内存对齐

    • 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 用于释放由 malloccallocrealloc 分配的内存。

实现机制

  1. 释放内存

    • free 通过传入的指针找到对应的内存块头部。
    • 将内存块标记为“未分配”,并将其加入空闲链表(free list)中。
  2. 合并空闲块

    • 如果相邻的内存块也是空闲的,可能会进行合并(coalescing),以减少内存碎片。
  3. 返回内存给操作系统

    • 如果释放的内存块较大,内存管理器可能会将其返回给操作系统(如通过 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. 底层内存管理

mallocfree 的底层通常依赖于操作系统的内存管理机制,例如:

  • 在 Linux 上,mallocfree 可能使用 glibcptmalloc 实现。
  • 在 Windows 上,mallocfree 可能使用 HeapAllocHeapFree

常见内存管理策略

  1. 首次适应(First Fit)
    • 在空闲链表中查找第一个足够大的块。
  2. 最佳适应(Best Fit)
    • 在空闲链表中查找最小的足够大的块。
  3. 最差适应(Worst Fit)
    • 在空闲链表中查找最大的块。
  4. 伙伴系统(Buddy System)
    • 将内存划分为大小固定的块,便于合并和分配。

4. 内存碎片

动态内存管理的一个常见问题是内存碎片,分为两种:

  1. 外部碎片
    • 空闲内存块分散在堆中,无法满足大块内存的分配请求。
  2. 内部碎片
    • 分配的内存块比实际需要的更大,导致部分内存浪费。

mallocfree 通过合并空闲块和合理的内存分配策略来减少内存碎片。


5. 总结

函数功能实现机制
malloc在堆上动态分配内存查找空闲块或向操作系统申请内存
free释放动态分配的内存标记为未分配,合并空闲块,返回给操作系统

通过理解 mallocfree 的实现机制,可以更好地掌握动态内存管理的工作原理,并避免常见的内存管理错误。


五、被free或者delete回收的内存是直接交还给OS吗?

freedelete 回收的内存是否直接交还给操作系统,取决于具体的内存管理策略和实现。通常情况下,内存管理器(如 glibcptmalloc)会采用一种分层的内存管理机制,将内存分为小块和大块,并根据不同的策略决定是否将内存交还给操作系统。以下是对这一过程的详细说明。


1. 内存管理器的分层机制

现代内存管理器通常将内存分为以下几层:

  1. 小块内存(Small Blocks)
    • 小块内存通常由内存管理器维护在进程的堆(Heap)中,不会立即交还给操作系统。
    • 这些内存块会被加入空闲链表(Free List),供后续分配使用。
  2. 大块内存(Large Blocks)
    • 大块内存通常直接通过系统调用(如 mmap)从操作系统分配。
    • 当大块内存被释放时,内存管理器可能会立即将其交还给操作系统(如通过 munmap)。

2. free 的行为

free 的行为取决于内存块的大小和内存管理器的策略:

  1. 小块内存
    • 小块内存被释放后,通常会被加入空闲链表,供后续分配使用。
    • 这些内存块不会立即交还给操作系统,以减少频繁的系统调用开销。
  2. 大块内存
    • 大块内存被释放后,可能会立即交还给操作系统(如通过 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 类似,但会先调用对象的析构函数:

  1. 小块内存
    • 小块内存被释放后,通常会被加入空闲链表,供后续分配使用。
  2. 大块内存
    • 大块内存被释放后,可能会立即交还给操作系统。

示例

#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. 内存管理器的优化策略

内存管理器通常会采用以下优化策略:

  1. 缓存机制
    • 小块内存会被缓存在空闲链表中,以减少频繁的系统调用。
  2. 合并空闲块
    • 相邻的空闲块会被合并,以减少内存碎片。
  3. 延迟释放
    • 大块内存可能会延迟释放,以避免频繁的系统调用开销。

5. 手动控制内存释放

如果需要强制将内存交还给操作系统,可以使用以下方法:

  1. malloc_trim(Linux)
    malloc_trim(0); // 尝试将空闲内存交还 OS
    
  2. mmapmunmap
    • 直接使用 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

通过理解 freedelete 的行为,可以更好地管理内存资源,避免内存泄漏和碎片化。