涉及到了手动内存管理、堆分配以及模板类的深层逻辑。
简要概述#
- 核心目标:实现动态扩容
- 作者明确了本集的目标:创建一个可以根据需要自动增长内存空间的容器,模拟
std::vector 的核心行为。
- 成员变量的改变:从栈到堆
- 不再使用
T m_Data[S],而是改为 T* m_Data 指针。 - 引入两个关键计数器:
m_Size(当前元素个数)和 m_Capacity(当前分配的总空间)。
- 构造函数与析构函数
- 在构造函数中预分配一小块初始内存。
- 强调了析构函数的重要性:必须使用
delete[] m_Data 释放堆内存,否则会导致内存泄漏。
- 实现
PushBack 方法(核心逻辑)- 这是本集最精彩的部分:当
m_Size >= m_Capacity 时,触发“重新分配(Reallocation)”。 - 扩容策略:作者演示了将容量翻倍(
m_Capacity * 2)的常用工程策略。
- 手动执行内存迁移
- 详细步骤:申请一块更大的新内存 -> 将旧数据拷贝到新内存 -> 释放旧内存 -> 指针重定向。
- 模板化的挑战
- 讨论了为什么 Vector 需要模板化,以及在处理非简单类型(如
std::string)时,简单的拷贝可能会失效(引出对后续移动语义的思考)。
- 性能测试与优化预告
- 对比了自定义 Vector 与标准库
std::vector 的性能。 - 提到目前的实现虽然可用,但频繁的拷贝开销巨大,引出后续关于
emplace_back 的话题。
代码vector.h#
//91_01_vector.h
#pragma once
#ifdef LY_EP92
#include <memory>
template<typename T>
class Vector
{
public:
Vector()
{
//allocate 2 elements
ReAlloc(2);
std::cout << "====构造器中初始化容量2======" << std::endl;
}
void PushBack(const T& value)
{
//如果当前数量量已经大于等于容量
if (m_Size >= m_Capacity)
{
//如果增长,容量扩大为1.5倍
ReAlloc(m_Capacity + m_Capacity / 2);
}
//m_Data[m_Size] 是一个已经存在的对象,所以
//这里调用的是拷贝赋值函数
m_Data[m_Size] = value;
m_Size++;
std::cout << "添加元素:" << value << std::endl;
std::cout << "==================" << std::endl;
}
T& operator[](size_t index)
{
if (index >= m_Size)
{
//assert
}
return m_Data[index];
}
const T& operator[](size_t index) const
{
if (index >= m_Size)
{
//assert被设计为一种调试期工具,在发布版本会被去掉
//assert
}
return m_Data[index];
}
size_t Size() const { return m_Size; }
~Vector()
{
delete[] m_Data;
}
private:
void ReAlloc(size_t newCapacity) {
// 1. allocate a new block of memory
// 2. copy/move old elements into new block
// 3. delete
//1. 尽可能低层次的访问内存,而不是智能指针
//2. 在堆上申请了一块连续(物理连续)的内存空间
//3. 在堆上创建了 newCapacity 个对象,并调用了它们
//的默认构造函数。也就是说,此时内存里已经存在了一
//堆“空”的 Vector3 对象
T* newBlock = new T[newCapacity];
//如果新容量小于当前大小,即缩小容量
if (newCapacity < m_Size)
{
// 当前大小设置为新容量大小
m_Size = newCapacity;
std::cout << "缩小容量--" << std::endl;
}
else
{
std::cout << "增大容量--" << m_Capacity << "->" << newCapacity << std::endl;
}
for (size_t i = 0; i < m_Size; i++)
{
//不会触发复制构造函数
//memcpy(newBlock, m_Data, i * sizeof(T);
//复制,会触发复制构造函数
newBlock[i] = m_Data[i];
}
//1. 逐一析构:编译器会根据该内存块记录的大小信息(通常存储在数组头部的隐藏偏移量中),从后往前(或从前往后,取决于实现)调用每一个 Vector3 对象的析构函数。
//2. 释放内存:在所有对象的析构函数执行完毕后,才会一次性将整块堆内存归还给操作系统。
delete[] m_Data;//释放原来指向的内存块
//delete m_Data;只会触发第一个元素的析构函数
m_Data = newBlock;//指向新的那个内存块
m_Capacity = newCapacity;
}
T* m_Data = nullptr;
//实际数组个数
size_t m_Size = 0;
//可分配存储的元素个数
size_t m_Capacity = 0;
};
#endif
应用:添加std::string元素#
#ifdef LY_EP92
//在堆上分配内存,创建动态数组
//需要一个指向堆分配内存(数据缓冲区)的指针,随着不断
//添加,当元素越来越多会达到临界点,没有足够空间存储新元素时,
//分配一个新的内存块使它有足够空间容纳这个新元素,将内存块中所有元素
//复制到新内存块,然后删除旧内存块
#include <iostream>
#include <string>
#include "92_01_vector.h"
template<typename T>
void PrintVector(const Vector<T>& vector)
{
for (size_t i = 0; i < vector.Size(); i++)
{
std::cout << vector[i] << std::endl;
}
std::cout << "==================" << std::endl;
}
int main()
{
Vector<std::string> vector;
vector.PushBack("Cherno");
vector.PushBack("C++");
vector.PushBack("Vector01");
vector.PushBack("Vector02");
vector.PushBack("Vector03");
vector.PushBack("Vector04");
vector.PushBack("Vector05");
vector.PushBack("Vector06");
vector.PushBack("Vector07");
PrintVector(vector);
std::cin.get();
return 0;
}
/*
增大容量--0->2
====构造器中初始化容量2======
添加元素:Cherno
==================
添加元素:C++
==================
增大容量--2->3
添加元素:Vector01
==================
增大容量--3->4
添加元素:Vector02
==================
增大容量--4->6
添加元素:Vector03
==================
添加元素:Vector04
==================
增大容量--6->9
添加元素:Vector05
==================
添加元素:Vector06
==================
添加元素:Vector07
==================
Cherno
C++
Vector01
Vector02
Vector03
Vector04
Vector05
Vector06
Vector07
==================
*/
#endif
应用添加自定义类元素#
#ifdef LY_EP92
#include <iostream>
#include <string>
#include "92_01_vector.h"
struct Vector3
{
float x = 0.0f, y = 0.0f, z = 0.0f;
Vector3() {}
Vector3(float scalar)
: x(scalar), y(scalar), z(scalar) {
}
Vector3(float x, float y, float z)
: x(x), y(y), z(z) {
}
// 拷贝构造函数 (Copy Constructor)
Vector3(const Vector3& other)
: x(other.x), y(other.y), z(other.z)
{
std::cout << "Copy\n";
}
//移动构造函数 (move Constructor)
Vector3(Vector3&& other)
: x(other.x), y(other.y), z(other.z)
{
std::cout << "Move\n";
}
//拷贝赋值
Vector3& operator=(const Vector3& other)
{
std::cout << "copy=\n";
x = other.x;
y = other.y;
z = other.z;
return *this;
}
//移动赋值
Vector3& operator=(Vector3&& other)
{
std::cout << "move=\n";
x = other.x;
y = other.y;
z = other.z;
return *this;
}
~Vector3()
{
std::cout << "Destroy\n";
}
};
//全局重载函数,C++ 规定:对于二元运算符(有两个操作数的运算符),如果写成全局函数,第一个参数对应运算符左边的对象,第二个参数对应右边的对象
std::ostream& operator<<(std::ostream& stream, const Vector3& v)
{
stream << v.x << ", " << v.y << ", " << v.z;
return stream;
}
void PrintVector(const Vector<Vector3>& vector)
{
for (size_t i = 0; i < vector.Size(); i++)
{
std::cout << vector[i] << std::endl;
}
std::cout << "==================" << std::endl;
}
int main()
{
Vector<Vector3> vector;
//Vector3(1.0f)->创建一个匿名临时对象
//临时对象会在包含它的那个“完整表达式(Full-expression)”结束时被销毁。即这行代码结束后该临时对象被析构
vector.PushBack(Vector3(1.0f));
vector.PushBack(Vector3{ 2,3,4 });
vector.PushBack(Vector3{});
PrintVector(vector);
std::cin.get();
return 0;
}
/*
增大容量--0->2
====构造器中初始化容量2======
copy=
添加元素:1, 1, 1
==================
Destroy
copy=
添加元素:2, 3, 4
==================
Destroy
增大容量--2->3
copy=
copy=
Destroy
Destroy
copy=
添加元素:0, 0, 0
==================
Destroy
1, 1, 1
2, 3, 4
0, 0, 0
==================
*/
#endif
如上,每次添加元素,以及扩容时的搬运原元素,都触发了复制赋值函数