C++ 数据结构 | 数组的底层原理

今天我们来讲一下C++的数据结构,C++的数据结构类型多样,主要有数组、字符串、栈、队列、堆、树、图、哈希表、集合、映射等等,今天我们主要讲的是C++数据结构最基础的数组及其底层原理

数组

数组是C++编程中最基础也最常用的数据结构之一,它是在连续的存储空间中存储同类型数据的数据结构 ,比如,在一块连续内存全部存储int类型的数据,或者全部double类型的数据 在 C++ 中,数组分为静态数组动态数组

静态数组

静态数组是存储空间连续 的并且存储大小固定的数组,是数组最基础的形式

底层存储原理

静态数组存储空间的大小在编译阶段就已确定,运行时无法修改,比如在写代码的时候声明int arr[5];,编译时其总大小固定为 20 字节(int通常占 4 字节)

当你声明一个静态数组时,编译器计算数组所需的内存空间后,向内存管理器申请一段连续的字节空间,其中这段空间的第一个字节的地址,就是数组的首地址,编译器会把数组名和首地址绑定,比如你声明int arr[5];,编译器会将arr作为这段空间的首地址(常量指针)保存起来,你使用arr时,本质就是在使用这个首地址

静态数组的下标访问

数组的下标访问arr[i]本质是地址偏移计算,因为内存是连续的,通过首地址 + 偏移量可以直接定位到目标元素,所以它的时间复杂度为 O (1) ,访问速度很快

静态数组的局限性

静态数组虽然存储简单并且访问速度快,但同时也存在两个致命的缺陷,就是大小固定内存位置固定

大小固定使得声明时必须指定确定的大小,若实际数据量超过数组大小会导致越界,不足则浪费内存

内存位置固定则会导致存储在栈上的静态数组容易触发栈溢出,因为栈的总大小是固定的而且空间比较小;存储在全局 / 静态区的数组则生命周期贯穿整个程序,无法灵活释放。

如果对C++内存分区不了解的可以查看《每日一个C++知识点|底层内存管理》,里面有详细介绍

那么针对静态数组的局限性,下面的动态数组提供了解决的办法~

动态数组

动态数组是在运行时确定大小并且可手动分配和释放内存的数据结构

底层实现原理

C++的动态数组依赖new和delete来分配和释放内存

运行时 根据实际需求计算所需内存大小,并且是存储在堆区 (堆区空间大,不会像静态数组那样存储在栈上容易触发栈溢出问题),向堆区申请连续内存,返回首地址;使用完毕后手动释放 内存,避免内存泄漏

下面用代码实现动态数组:

cpp 复制代码
#include <iostream>
using namespace std;

int main() {
    // 运行时确定数组大小
    int n;
    cout << "输入数组大小:";
    cin >> n;
    
    // 动态申请堆内存,返回首地址
    int* arr = new int[n];
    
    // 赋值并访问
    for (int i = 0; i < n; i++) {
        arr[i] = i * 2; // 等价于*(arr + i) = i * 2
    }
    
    // 输出验证
    for (int i = 0; i < n; i++) {
        cout << arr[i] << " ";
    }
    
    // 手动释放堆内存(必须!)
    delete[] arr;
    arr = nullptr; // 避免野指针
    return 0;
}

运行结果如下:

动态数组的局限性

虽然动态数组解决了 "大小固定" 的问题,但仍有如下的局限性:

  1. 扩容麻烦:若数据量超过当前数组大小,需要手动申请更大的内存块,拷贝原有数据,释放旧内存
  2. 需要手动管理内存,容易出现内存泄漏
  3. 功能单一:仅支持基础的访问和修改,无内置的增删、判空、求大小等功能

针对原生的动态数组的局限性,那么C++的标准模板库STL中的vector正是为解决这些问题而生的加强版本的动态数组

vector

vector是 C++ 标准库提供的动态数组容器,并封装了自动扩容、内存管理、便捷操作等功能,其底层仍然是基于连续的堆内存实现的

vector 的核心用法

在了解vector容器的底层原理之前,我们先简单过一遍其基本功能:

使用vector前需包含头文件#include <vector>

其核心用法包括添加元素push_back(),删除元素pop_back(),重新设置大小resize(n),清空元素vec.clear(),下面用一段代码展示 vector 的基本用法

cpp 复制代码
#include <iostream>
#include <vector>   // 使用vector必须包含的头文件
using namespace std;

int main() {
    vector<int> vec = {10, 20, 30};  // 初始化
    
    vec.push_back(40);  // 向尾部添加元素40 → vec: [10, 20, 30, 40]
    vec.push_back(50);  // 继续添加元素50 → vec: [10, 20, 30, 40, 50]
    vec.pop_back();     // 删除最后一个元素50 → vec: [10, 20, 30, 40]

    int val1 = vec[1];  // 访问下标1的元素,值为20
    vec[1] = 200;       // 修改下标1的元素为200 → vec: [10, 200, 30, 40]

    int val2 = vec.at(2); // 访问下标2的元素,值为30
    vec.at(2) = 300;      // 修改下标2的元素为300 → vec: [10, 200, 300, 40]   
    
    vec4.reserve(20); // 预留容量
    vec.clear(); // 清空所有元素,容量不变
    
    return 0;
}

vector 的底层原理

简单回顾下vector的核心用法之后,接下来我们深入了解 vector 的底层原理:主要分为三个关键指针,自动扩容机制核心操作的底层逻辑这三个方面

三个关键指针

首先是 vector 的核心成员变量通常包含三个关键指针 :_start,_finish,_end_of_storage

  1. _start:指向数组首元素的指针
  2. _finish:指向数组最后一个元素的下一个位置的指针
  3. _end_of_storage:指向数组内存空间末尾的下一个位置的指针

通过这三个指针,可快速计算元素个数容量大小是否为空

元素个数:_finish - _start

容量大小:_end_of_storage - _start

是否为空:if(_start == _finish)

自动扩容机制

然后是 vector 的自动扩容机制 :当向vector中添加元素,并且元素个数等于容量大小时,就会触发扩容,其扩容的核心步骤是如下:

  1. 申请新内存通常扩容为原容量的 2 倍
  2. 拷贝数据将原数组的所有元素拷贝到新内存
  3. 释放旧内存,销毁原数组元素,释放旧内存;
  4. 更新指针是将_start、_finish、_end_of_storage指向新内存的对应位置

示意图如下所示:

核心操作的底层逻辑

了解完vector 的自动扩容机制 后,结合自动扩容机制,我们进一步核心操作的底层逻辑

push_back()若容量足够,直接在_finish位置构造元素,_finish++;若容量不足,先扩容再添加

pop_back()销毁_finish - 1位置的元素,_finish--

resize(n)若n < 元素个数,销毁多余元素;若n > 元素个数且n <= 容量大小,则构造新元素;若n > 容量大小,扩容后构造新元素

reserve(n)仅扩容容量到n

clear()仅销毁所有元素,不释放内存,容量保持不变

手动实现vector

理解了vector的用法和底层原理后,为了进一步深入了解动态数组,我们手动实现一个简化版的vector,覆盖构造析构添加删除容量大小访问等功能

需要结合模板的知识,如果还没有了解过模板的知识的朋友可以先看一遍《每日一个C++知识点|模板》这篇文章,有一个初步的印象

cpp 复制代码
#include <iostream>
#include <cstring> // 用于memcpy
using namespace std;

template <typename T>
class MyVector {
public:
    // 构造函数:初始化空数组
    MyVector() : _start(nullptr), _finish(nullptr), _end_of_storage(nullptr) {}

    // 析构函数:释放内存
    ~MyVector() {
        // 销毁所有元素
        while (_finish > _start) {
            --_finish;
            _finish->~T(); // 调用元素的析构函数
        }
        // 释放堆内存
        if (_start) {
            ::operator delete(_start);
        }
    }

    // 禁用拷贝构造和赋值(简化版,避免浅拷贝问题)
    MyVector(const MyVector&) = delete;
    MyVector& operator=(const MyVector&) = delete;

    // 向尾部添加元素
    void push_back(const T& value) {
        // 检查容量,不足则扩容
        if (_finish == _end_of_storage) {
            size_t new_capacity = capacity() == 0 ? 4 : capacity() * 2;
            reserve(new_capacity);
        }
        // 在_finish位置构造元素
        new (_finish) T(value); // 定位new,不申请内存仅构造对象
        ++_finish;
    }

    // 从尾部删除元素
    void pop_back() {
        if (empty()) {
            throw out_of_range("MyVector is empty!");
        }
        --_finish;
        _finish->~T(); // 销毁最后一个元素
    }

    // 重载[]运算符,支持随机访问
    T& operator[](size_t index) {
        if (index >= size()) {
            throw out_of_range("Index out of range!");
        }
        return *(_start + index);
    }

    // const版本的[]访问
    const T& operator[](size_t index) const {
        if (index >= size()) {
            throw out_of_range("Index out of range!");
        }
        return *(_start + index);
    }

    // 获取元素个数
    size_t size() const {
        return _finish - _start;
    }

    // 获取容量
    size_t capacity() const {
        return _end_of_storage - _start;
    }

    // 判断是否为空
    bool empty() const {
        return _start == _finish;
    }

    // 预留容量(扩容)
    void reserve(size_t new_capacity) {
        if (new_capacity <= capacity()) {
            return;
        }
        // 1. 申请新内存
        T* new_start = (T*)::operator new(new_capacity * sizeof(T));
        // 2. 拷贝原有元素到新内存
        size_t old_size = size();
        if (_start) {
            // 内存拷贝(浅拷贝,简化版)
            memcpy(new_start, _start, old_size * sizeof(T));
            // 释放旧内存
            ::operator delete(_start);
        }
        // 3. 更新指针
        _start = new_start;
        _finish = _start + old_size;
        _end_of_storage = _start + new_capacity;
    }

private:
    T* _start;          // 指向数组首元素
    T* _finish;         // 指向最后一个元素的下一个位置
    T* _end_of_storage; // 指向内存末尾的下一个位置
};

// 测试代码
int main() {
    MyVector<int> vec;
    
    // 测试push_back和size/capacity
    for (int i = 0; i < 10; i++) {
        vec.push_back(i);
        cout << "添加元素" << i << ":size=" << vec.size() 
             << ",capacity=" << vec.capacity() << endl;
    }

    // 测试[]访问
    cout << "\n数组元素:";
    for (size_t i = 0; i < vec.size(); i++) {
        cout << vec[i] << " ";
    }
    cout << endl;

    // 测试pop_back
    vec.pop_back();
    vec.pop_back();
    cout << "\n删除2个元素后,size=" << vec.size() 
         << ",capacity=" << vec.capacity() << endl;
    cout << "最后一个元素:" << vec[vec.size() - 1] << endl;

    return 0;
}

运行结果如下: 以上就是实现一个简化版的vector

总结

本文通过引入数据结构的概念,进而介绍最基本的数据结构--数组,然后分别介绍数组的两种形式--静态数组动态数组,在动态数组中重点介绍STL的vector容器,并手动实现一个vector容器,由浅入深,层层递进

以上就是本文的主要内容如果这篇文章对你有帮助的话,欢迎点赞收藏

相关推荐
阿猿收手吧!2 小时前
【C++】brpc与grpc对比
开发语言·c++
oioihoii2 小时前
C++虚函数表与多重继承内存布局深度剖析
java·jvm·c++
BestOrNothing_20152 小时前
C++ 内存泄漏的“真实成本”: 内存单位换算、堆分配开销与工程级判断
c++·内存管理·内存泄漏·堆内存·raii·内存换算·异常安全
WBluuue3 小时前
Codeforces Global 31 Div1+2(ABCD)
c++·算法
zmzb01033 小时前
C++课后习题训练记录Day58
开发语言·c++
Sunsets_Red4 小时前
待修改莫队与普通莫队优化
java·c++·python·学习·算法·数学建模·c#
爱学习的梵高先生4 小时前
C++:友元
开发语言·c++
星火开发设计4 小时前
深度优先搜索(DFS)详解及C++实现
c++·学习·算法·计算机·深度优先·大学生·期末考试
郝学胜-神的一滴4 小时前
Linux线程错误调试指南:从原理到实践
linux·服务器·开发语言·c++·程序人生