首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >C++内存管理基石:POD类型与面向对象语义解析

C++内存管理基石:POD类型与面向对象语义解析

作者头像
云泽808
发布2025-12-30 18:29:22
发布2025-12-30 18:29:22
1070
举报

前言

大家好啊,我是云泽Q,欢迎阅读我的文章,一名热爱计算机技术的在校大学生,喜欢在课余时间做一些计算机技术的总结性文章,希望我的文章能为你解答困惑~ 这篇内容还是建议衔接上一篇内容来看,如果直接看建议直接带着下面的源码看深入理解vector:模拟实现与现代C++技巧

一、C++ 相关的资源管理

这里实现的vector还有一个隐藏很深的bug很难发现

涉及的类型与转换逻辑 代码中传入 push_back 的是字符串字面量(如 “1111…”),其类型是 const char*;而 yunze::vector< string > 的 push_back 期望接收的是 std::string 类型的参数; 由于 std::string 类提供了一个非 explicit 的构造函数 std::string(const char*),因此编译器会自动触发隐式类型转换:将 const char* 类型的字符串字面量转换为 std::string 临时 std::string 对象,再将临时对象传入 push_back,完成元素插入。

图中插入4组数据没有问题,第5组就会出现下图的情况,上一篇vector的文章也证实了整型的扩容不会有问题

在这里插入图片描述
在这里插入图片描述

这里肯定是和扩容有关系的,插入第5组数据一定会发生扩容 这里调试时将旧空间的数据拷贝给新空间这一步是没有问题的,但是旧空间delete后新旧空间都出现问题了

在这里插入图片描述
在这里插入图片描述

这里有一个经验,若delete/free出问题了,若释放的位置没有问题,那就不是这两者导致的,就比如说这里delete的指针_start没有变化过,一直指向空间开始的位置(因为不能部分释放),那就不是delete的问题(比如说空间开少了越界了也会在delete处报错)

这里主要是浅拷贝的问题,只不过层次更深,这里memcpy出现了问题

假设调用了push_back(“hello”),这时候vector内部会经历扩容(reserve) 过程

一、先看string对象的内存结构 std::string内部不是直接存字符串,而是用指针管理动态内存(类似下面的简化版):

代码语言:javascript
复制
class string {
private:
    char* _str;   // 指向堆上的字符数组(比如"hello"存这里)
    size_t _size; // 字符串长度
    size_t _cap;  // 容量
};

比如string s = “hello”,内存里是这样的:

代码语言:javascript
复制
s:
  _str → 堆内存地址0x100(存着'h','e','l','l','o','\0')
  _size = 5
  _cap = 5

二、旧代码用memcpy复制的问题(浅拷贝的坑) 这里的的reserve函数里用memcpy复制旧数据到新空间:

代码语言:javascript
复制
memcpy(tmp, _start, sizeof(T) * sz); // T是string时,这里出问题

memcpy的本质是 “按字节复制内存”,它不管对象内部的逻辑,只把旧对象的二进制数据原封不动搬到新空间。 当T是string时,复制后新空间的string对象会变成这样:

代码语言:javascript
复制
旧空间的string对象s_old:
  _str → 0x100(堆上的"hello")

新空间的string对象s_new(memcpy复制后):
  _str → 0x100(和s_old的_str完全一样!)
  _size = 5(复制了s_old的_size)
  _cap = 5(复制了s_old的_cap)

这时候s_old和s_new的_str指针指向同一块堆内存(0x100),这就是 “浅拷贝”—— 只复制了指针,没复制指针指向的实际数据。

三、崩溃的关键步骤:旧空间释放导致新对象 “悬空” 扩容的最后一步是释放旧空间:

代码语言:javascript
复制
delete[] _start; // 释放旧空间

delete[]会做两件事:

  1. 对旧空间里的每个string对象调用析构函数;
  2. 释放整块旧空间的内存。

当string的析构函数被调用时,它会释放自己 _str指向的堆内存(0x100)。这时候:

  • 旧空间的s_old被销毁,0x100 的内存被释放;
  • 新空间的s_new的_str依然指向 0x100(但这块内存已经无效了,析构之后指向的数组里的值就置为随机值,随机值查编码表就是上图打印出来的字符)。

四、后续操作触发崩溃 当继续使用这个vector< string >时,比如:

  • 调用push_back添加新元素;
  • 程序结束时vector析构,释放新空间的string对象。

这时候新空间的s_new析构函数会再次尝试释放_str指向的 0x100 内存 —— 但这块内存早就被旧空间的s_old释放过了!这就是 “双重释放”(同一内存被释放两次),是 C++ 中最常见的内存错误之一,会直接导致程序崩溃。

在这里插入图片描述
在这里插入图片描述

1.1 POD类型与非POD类型

当模板参数 T 是整型(如 int、double 等 POD 类型)时,不会出现上述 bug,核心原因在于POD 类型的内存模型和资源管理特性,具体分析如下:

  1. POD 类型的定义与内存模型 POD(Plain Old Data)类型是指内存布局简单、无复杂资源管理逻辑的类型,包括:
    • 内置整型(int、char、long 等)、浮点型(float、double 等),还有如日期类这样的类型;
    • 无构造 / 析构 / 虚函数的简单结构体(仅包含 POD 成员)。

以 int 为例,它的内存模型是 “值语义”—— 变量本身直接存储数值,没有指向堆内存的指针或动态资源。

  1. memcpy 对 POD 类型的浅拷贝是安全的 当 T 是 int 时,memcpy(tmp, _start, sizeof(T) * sz) 的行为是按字节复制int的数值,例如:
    • 旧空间的 int 值为 10,内存中直接存储二进制 0x0000000A;
    • memcpy 会把这个二进制值原封不动复制到新空间,新空间的 int 也会存储 10。

由于 int 没有动态分配的资源(如堆内存、文件句柄等),浅拷贝不会导致 “资源共享” 或 “重复释放”—— 旧空间的 int 被释放时,只是简单的内存回收,新空间的 int 数值不受影响,后续使用完全正常。

  1. 对比 std::string 的非 POD 特性 std::string 是非 POD 类型,它的内存模型是 **“资源语义”—— 对象内部有指针指向堆上的字符缓冲区 **(用于存储字符串内容)。 此时 memcpy 仅复制了 “指针的值”(而非指针指向的字符数据),导致新旧 string 对象共享同一块堆内存。当旧对象析构时,会释放这块堆内存,新对象的指针就变成了 “野指针”,后续操作(如析构、访问)会触发内存错误。

总结 整型等POD 类型无动态资源管理逻辑,memcpy 的浅拷贝能直接复制 “数值本身”,不会引发资源冲突;而 std::string 是带动态资源的非 POD 类型,浅拷贝会破坏其资源的独立管理,从而导致崩溃。

1.2 C++ 面向对象语义

总的来说扩容的时候把旧空间的数据拷贝到新空间这个过程要多一步考虑,只是单一的进行深拷贝也不好,深拷贝一般都需要更多的性能开销

在这里插入图片描述
在这里插入图片描述

库里面是使用的一种比较复杂的类型萃取技术,如果是内置类型直接memcpy

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

我这里写两种简单的解决方案:

方案 1:tmp[i] = _start[i](赋值深拷贝) 当T是std::string这类带动态资源的类型时,tmp[i] = _start[i]会调用string的赋值运算符。std::string的赋值运算符是深拷贝逻辑—— 它会为tmp[i]重新分配一块独立的堆内存,将_start[i]的字符数据完整复制过去,确保tmp[i]和_start[i]的资源完全独立。

在这里插入图片描述
在这里插入图片描述

方案 2:std::swap(tmp[i], _start[i])(资源所有权交换) std::swap针对std::string会调用其成员 swap 函数,交换两个string的内部资源(如字符缓冲区指针、大小、容量)。

  • 交换后,tmp[i]持有了_start[i]的有效资源(旧空间的字符串数据);
  • _start[i]则持有tmp[i]原来的资源(新空间中tmp默认构造的空资源或可安全释放的资源)。 这种方式是O (1) 时间复杂度(仅交换指针等成员),比深拷贝更高效
在这里插入图片描述
在这里插入图片描述

补充:这里如果是内置类型会调用算法库的swap函数模板的交换

核心逻辑:逐个元素的 “语义级拷贝” 通过 for 循环逐个处理每个元素,利用 C++ 类的赋值运算符或 swap 函数的 “深拷贝 / 资源转移” 特性,替代了memcpy的 “字节级浅拷贝”。

  • 对于int等 POD 类型,赋值或 swap 的开销可以忽略,性能接近memcpy;
  • 对于std::string等非 POD 类型,确保了资源的独立管理(无共享、无重复释放),从根源上解决了内存错误。

总结 这种方案的本质是利用 C++ 类的 “面向对象语义”(赋值、swap 的深拷贝 / 资源管理),替代底层的 “字节级浅拷贝”,从而保证无论是 POD 还是非 POD 类型,都能安全完成扩容时的数据迁移,避免内存崩溃。

二、相关源码

vector.h

代码语言:javascript
复制
#pragma once
#include<iostream>
#include<assert.h>
#include<vector>
using namespace std;

namespace yunze
{
	template<class T>
	class vector
	{
	public:
		//typedef T* iterator;//定义迭代器
		using iterator = T*;//和typedef作用一样
		using const_iterator = const T*;

		iterator begin()
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}

		//新增const版本的begin/end
		const_iterator begin() const
		{
			return _start;
		}

		const_iterator end() const
		{
			return _finish;
		}

		//看似什么都没写,实际上会用缺省值走初始化列表
		vector()
		{
		}

		vector(initializer_list<T> i1)
		{
			//这里直接使用范围for遍历
			//否则使用...的迭代器太麻烦
			reserve(i1.size());
			for (const auto& e : i1)
			{
				push_back(e);
			}
		}

		~vector()
		{
			if (_start)
			{
				delete[] _start;
			}
			_start = _finish = _end_of_storage = nullptr;
		}

		////传统写法
		////v2(v1)
		////v就是v1,this就是v2
		//vector(const vector<T>& v)
		//{
		//	//成员函数前没有显示写就是v2在调用
		//	reserve(v.size());
		//	for (const auto& e : v)
		//	{
		//		push_back(e);
		//	}
		//}

		////v1 = v3,传统写法
		////传值返回还要调用拷贝,使用引用返回
		////返回自己以支持连续赋值
		//vector<T>& operator=(const vector<T>& v)
		//{
		//	//排除自己给自己赋值
		//	if (this != &v)
		//	{
		//		//赋值前先把左边的对象的数据清理掉(不释放空间)
		//		clear();
		//		//容量够就不用扩容
		//		reserve(v.capacity());
		//		for (const auto& e : v)
		//		{
		//			push_back(e);
		//		}
		//	}
		//	return *this;
		//}

		vector(size_t n, const T& val = T())
		{
			reserve(n);
			for (size_t i = 0; i < n; i++)
			{
				push_back(val);
			}
		}

		vector(int n, const T& val = T())
		{
			reserve(n);
			for (int i = 0; i < n; i++)
			{
				push_back(val);
			}
		}

		//函数模板,迭代器不一定是vector迭代器,也可以是其他容器的迭代器
		template<class InputInterator>
		vector(InputInterator first, InputInterator last)
		{	//提前一次性开好空间
			//迭代器区间左闭右开,减了就是个数
			//reverse(last - first);
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}

		//拷贝构造现代写法
		vector(const vector<T>& v)
		{
			vector<T> tmp(v.begin(), v.end());
			swap(tmp);
		}

		//指针是内置类型,内置类型的交换可以直接std::swap通用模板函数来完成
		void swap(vector<T>& v)
		{
			std::swap(_start, v._start);
			std::swap(_finish, v._finish);
			std::swap(_end_of_storage, v._end_of_storage);
		}

		//v1 = v3
		//赋值现代写法
		vector<T> operator=(vector<T> tmp)
		{
			swap(tmp);
			return *this;
		}

		void clear()
		{
			_finish = _start;
		}

		bool empty() const
		{
			return _start == _finish;
		}

		void reserve(size_t n)
		{
			//n比capacity小,不进行缩容
			if (n > capacity())
			{
				size_t sz = size();
				T* tmp = new T[n];
				//把旧空间的数据拷贝过来
				//第一次进来没有旧空间,(特殊处理)
				if (_start)
				{
					for (size_t i = 0; i < sz; i++)
					{
						tmp[i] = _start[i];//如果是string,调用string的赋值深拷贝
						std::swap(tmp[i], _start[i]);//如果是string,调用string的交换,交换资源指向
					}
					//memcpy(tmp, _start, sizeof(T) * sz);
					delete[] _start;
				}
				_start = tmp;
				_finish = _start + sz;
				//不能+capacity(),此时的_end_of_storage还未更新
				_end_of_storage = _start + n;
			}
		}

		size_t size() const
		{
			return _finish - _start;
		}

		//这里的[]既可以读也可以写
		T& operator[](size_t i)
		{
			assert(i < size());
			return _start[i];
		}

		//这里的[]只能读
		const T& operator[](size_t i) const
		{
			assert(i < size());
			return _start[i];
		}

		//类比库中resize的写法
		void resize(size_t n, T val = T())
		{
			if (n < size())
			{
				_finish = _start + n;
			}
			else {
				//直接扩容,是否扩容reserve内会检查
				reserve(n);
				while (_finish < _start + n)
				{
					*_finish = val;
					++_finish;
				}
			}
		}

		size_t capacity() const
		{
			return _end_of_storage - _start;
		}

		void push_back(const T& x)
		{
			if (_finish == _end_of_storage)
			{
				//满了
				reserve(capacity() == 0 ? 4 : capacity() * 2);
			}
			//这里我自己写的vector开空间不用内存池,用new
			//new对于自定义类型会调用对应默认构造
			//不用担心自定义类型对象赋值给随机值的问题
			*_finish = x;
			++_finish;
		}

		void pop_back()
		{
			assert(!empty());
			--_finish;
		}

		iterator insert(iterator pos, const T& x)
		{
			assert(pos >= _start);
			assert(pos <= _finish);
			//空间满了,扩容
			if (_finish == _end_of_storage)
			{
				//若扩容要特殊处理
				size_t len = pos - _start;
				reserve(capacity() == 0 ? 4 : capacity() * 2);
				//此时的_start是新空间的_start
				pos = _start + len;
			}
			//在pos位置插入数据
			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}
			*pos = x;
			++_finish;

			return pos;
		}

		iterator erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos < _finish);
			iterator it = pos + 1;
			while (it != _finish)
			{
				*(it - 1) = *it;
				++it;
			}
			--_finish;
			//返回删除数据的下一个位置
			return pos;
		}

	private:
		iterator _start = nullptr;
		iterator _finish = nullptr;
		iterator _end_of_storage = nullptr;
	};
}

test.cpp

代码语言:javascript
复制
#define _CRT_SECURE_NO_WARNINGS 666
#include"vector.h"

namespace yunze
{
	//支持迭代器就支持范围for
	void Print(const vector<int>& v)
	{
		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;

		////const调const[]版本,只可以读
		//for (size_t i = 0; i < v.size(); i++)
		//{
		//	//v[0]++;//const对象只能读不能写
		//	cout << v[i] << " ";
		//}
		//cout << endl;
	}

	void test_vector1()
	{
		yunze::vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		v.push_back(5);
		v.push_back(5);
		v.push_back(5);
		v.push_back(5);
		v.push_back(5);
		v.push_back(5);
		v.push_back(5);
		//普通对象调普通[]版本
		v[0]++;

		Print(v);
	}

	void test_vector2()
	{
		yunze::vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		//v.push_back(5);
		Print(v);

		v.insert(v.begin(), 0);
		Print(v);

		//在下标为3的位置插入30
		auto it = v.begin() + 3;
		v.insert(it, 30);
		Print(v);
	}

	void test_vector3()
	{
		yunze::vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		v.push_back(5);
		Print(v);
		v.erase(v.begin());
		Print(v);

		//删除下标为2位置的值
		auto it = v.begin() + 2;
		v.erase(it);
		Print(v);
	}

	void test_vector4()
	{
		yunze::vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		v.push_back(5);
		v.push_back(6);
		//Print(v);
		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;

		////删除所有偶数
		//auto it = v.begin();
		//while (it != v.end())
		//{
		//	if (*it % 2 == 0)
		//	{
		//		v.erase(it);
		//	}
		//	++it;
		//}

		auto it = v.begin();
		while (it != v.end())
		{
			if (*it % 2 == 0)
			{
				it = v.erase(it);
			}
			else
			{
				++it;
			}
		}

		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;
	}

	void test_vector5()
	{
		yunze::vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		v.push_back(5);
		v.push_back(6);
		Print(v);
		v.resize(3);
		Print(v);
		v.resize(20, 5);
		Print(v);
	}

	void test_vector6()
	{
		yunze::vector<int> v1;
		v1.push_back(1);
		v1.push_back(2);
		v1.push_back(2);
		v1.push_back(3);
		v1.push_back(4);
		v1.push_back(5);
		v1.push_back(6);
		Print(v1);

		yunze::vector<int> v2(v1);
		Print(v2);

		//原生写法
		//yunze::vector<int> v3({ 10,20,30,40 });
		//隐式类型转换+优化
		yunze::vector<int> v3 = { 10,20,30,40 };
		v1 = v3;
		Print(v1);

		yunze::vector<int> v4(10u, 1);
		//yunze::vector<int> v4(10, 1);
		Print(v4);

		//实参类型一个为int,一个为char
		yunze::vector<char> v5(10, 'x');
	}

	//这里有隐式类型转换,string支持用const char* 构造
	void test_vector7()
	{
		yunze::vector<string> v1;
		v1.push_back("111111111111111111111111");
		v1.push_back("111111111111111111111111");
		v1.push_back("111111111111111111111111");
		v1.push_back("111111111111111111111111");
		v1.push_back("111111111111111111111111");
		for (auto& e : v1)
		{
			cout << e << " ";
		}
		cout << endl;
	}
}

using namespace yunze;
int main()
{
	//test_vector1();
	//test_vector2();
	//test_vector3();
	//test_vector4();
	//test_vector5();
	//test_vector6();
	test_vector7();

	//C语言部分的定义
	int i = 0;
	//C++98后还可以这样定义
	int j = int();
	int k = int(1);
	//C++11后还可用花括号初始化
	int y = {};
	int t = { 1 };
	int z{ 2 };
	return 0;
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-11-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 一、C++ 相关的资源管理
    • 1.1 POD类型与非POD类型
    • 1.2 C++ 面向对象语义
  • 二、相关源码
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档