【C++】STL 容器 - STL 容器的值语意 ( 容器存储任意类型元素原理 | STL 容器元素可拷贝原理 | STL 容器元素类型需要满足的要求 | 自定义可存放入 STL 容器的元素类 )

2024-01-04 08:23:15 浏览数 (2)

一、STL 容器的 值 ( Value ) 语意

1、STL 容器存储任意类型元素原理

C 语言中的 STL 容器 , 可以存储任何类型的元素 , 是因为 STL 容器 使用了 C 模板技术进行实现 ;

C 模板技术 是 基于 2 次编译实现的 ;

  • 第一次编译 , 扫描模板 , 收集有关模板实例化的信息 , 生成模板头 , 进行词法分析和句法分析 ;
  • 第二次编译 , 根据实际调用的类型 , 生成包含真实类型的实例化的代码 ;

2、STL 容器元素可拷贝原理

STL 容器 定义时 , 所有的 STL 容器 的相关操作 , 如 插入 / 删除 / 排序 / 修改 , 都是 基于 值 Value 语意 的 , 不是 基于 引用 Reference 语意的 ;

  • 比如 : 向 STL 容器中 插入元素时 , 插入的都是实际的 值 Value 语意 , 不是 引用 Reference 语意 ;

如果 基于 引用 或者 指针 操作 , 假如 在外部 该 指针 / 引用 指向的对象被回收 , 那么容器操作就会出现问题 ;

STL 容器 中 , 存储的元素 , 必须是可拷贝的 , 也就是 元素类 必须提供 拷贝构造函数 ;

3、STL 容器元素类型需要满足的要求

STL 容器元素类型需要满足的要求 :

  • 提供 无参 / 有参 构造函数 : 保证可以创建元素对象 , 并存放到容器中 ;
  • 提供 拷贝构造函数 : STL 容器的元素是可拷贝的 , 这是容器操作的基础 ;
  • 提供 重载 = 操作符函数 : STL 容器的元素可以被赋值 ;

4、STL 容器迭代器遍历

除了 queue 队列容器 与 stack 堆栈容器 之外 , 每个 STL 容器都可以使用 迭代器 进行遍历 ;

  • 调用 begin() 函数 , 获取 指向 首元素 的迭代器 ;
  • 调用 end() 函数 , 获取 末尾迭代器 , 该迭代器 指向 最后一个元素的后面位置 ;

除了 queue 与 stack 容器外 , 都可以使用如下代码进行遍历 ;

代码语言:javascript复制
	//容器的遍历
	cout << "遍历容器 :" << endl;
	for (auto it = container.begin(); it != container.end(); it  )
	{
		// 遍历当前元素 , 打印 / 判断 等操作
	}
	cout << "遍历结束" << endl;

二、代码示例 - 自定义可存放入 STL 容器的元素类

1、代码示例

STL 容器元素类型需要满足的要求 :

  • 提供 无参 / 有参 构造函数 : 保证可以创建元素对象 , 并存放到容器中 ;
  • 提供 拷贝构造函数 : STL 容器的元素是可拷贝的 , 这是容器操作的基础 ;
  • 提供 重载 = 操作符函数 : STL 容器的元素可以被赋值 ;

这里自定义 Student 类 , 需要满足上述要求 , 在 Student 类中 , 定义两个成员 , char* 类型指针 和 int 类型成员 ;

其中 char* 类型指针涉及到 堆内存 的 申请 和 释放 ;

在 有参构造 函数中 , 主要作用是 创建新对象 , 这里 直接 申请内存 , 并使用参数中的值 进行赋值 ;

代码语言:javascript复制
	/// <summary>
	/// 创建普通构造函数
	/// </summary>
	/// <param name="name">传入的常量字符串</param>
	/// <param name="age">传入的年龄</param>
	Student(char* name, int age)
	{
		// 为 m_name 指针分配内存
		// 内存大小是传入字符串大小   1
		// 最后   1 是为了设置  字符串结尾用的
		// 在析构函数中还要将该内存析构
		m_name = new char[strlen(name)   1];
		// 将实际的值拷贝到
		// 拷贝字符串数据
		// 需添加 #define _CRT_SECURE_NO_WARNINGS 声明
		strcpy(m_name, name);
		m_age = age;
	}

在 拷贝构造函数 中 , 主要作用是 使用 现有 Student 对象 初始化新对象 , 直接申请内存 , 并将 被拷贝的对象 的值 赋值给新创建的 Student 对象 ;

代码语言:javascript复制
	/// <summary>
	/// 拷贝构造函数
	/// 在 Student s = s2 情况下调用
	/// </summary>
	/// <param name="obj">使用该 obj 对象初始化新的 Student 对象</param>
	Student(const Student& obj)
	{
		// 为 m_name 指针分配内存
		m_name = new char[strlen(obj.m_name)   1];
		// 拷贝字符串数据
		// 需添加 #define _CRT_SECURE_NO_WARNINGS 声明
		strcpy(m_name, obj.m_name);
		// 设置年龄
		m_age = obj.m_age;
	}

在 重载等号 = 操作符函数 中 , 主要作用是 使用 现有的 Student 对象 B 为一个 已存在的 Student 对象 A 进行赋值 , 先将 A 对象的 char* 指针释放 , 然后重新申请内存 , 最后再赋值 , int 类型的成员直接赋值 ;

代码语言:javascript复制
	/// <summary>
	/// 重载 等号 = 操作符 函数
	/// </summary>
	/// <param name="obj">等号右边的值</param>
	/// <returns>调用者本身</returns>
	Student& operator=(const Student& obj)
	{
		//先释放 调用者 本身的 m_name 指针指向的内存
		if (m_name != NULL)
		{
			// 使用 new 分配的内存需要使用 delete 释放
			delete[] m_name;
			// 释放内存后指针置空避免野指针
			m_name = NULL;
			// 年龄也设置为默认值
			m_age = 0;
		}

		// 重新分配新的 字符串 内存
		m_name = new char[strlen(obj.m_name)   1];
		// 拷贝字符串数据
		// 需添加 #define _CRT_SECURE_NO_WARNINGS 声明
		strcpy(m_name, obj.m_name);
		// 拷贝年龄
		m_age = obj.m_age;
		// 返回调用者本身, 以便进行链式调用
		return *this;
	}

此外 , 还有析构函数 , 在析构函数中 , 释放申请的 char* 内存 , 然后置空 ;

代码语言:javascript复制
	~Student()
	{
		if (m_name != NULL)
		{
			// 释放使用 new 关键字分配的内存
			delete[] m_name;
			// 释放内存后的指针置空 避免野指针
			m_name = NULL;
			// 将年龄字段设置为默认值
			m_age = 0;
		}
	}

代码示例 :

代码语言:javascript复制
// 调用 strcpy 函数需要添加该声明, 否则编译报错
#define _CRT_SECURE_NO_WARNINGS
#include "iostream"
using namespace std;
#include "vector"

class Student
{
public:
	/// <summary>
	/// 创建普通构造函数
	/// </summary>
	/// <param name="name">传入的常量字符串</param>
	/// <param name="age">传入的年龄</param>
	Student(char* name, int age)
	{
		// 为 m_name 指针分配内存
		// 内存大小是传入字符串大小   1
		// 最后   1 是为了设置  字符串结尾用的
		// 在析构函数中还要将该内存析构
		m_name = new char[strlen(name)   1];
		// 将实际的值拷贝到
		// 拷贝字符串数据
		// 需添加 #define _CRT_SECURE_NO_WARNINGS 声明
		strcpy(m_name, name);
		m_age = age;
	}
	~Student()
	{
		if (m_name != NULL)
		{
			// 释放使用 new 关键字分配的内存
			delete[] m_name;
			// 释放内存后的指针置空 避免野指针
			m_name = NULL;
			// 将年龄字段设置为默认值
			m_age = 0;
		}
	}

	/// <summary>
	/// 拷贝构造函数
	/// 在 Student s = s2 情况下调用
	/// </summary>
	/// <param name="obj">使用该 obj 对象初始化新的 Student 对象</param>
	Student(const Student& obj)
	{
		// 为 m_name 指针分配内存
		m_name = new char[strlen(obj.m_name)   1];
		// 拷贝字符串数据
		// 需添加 #define _CRT_SECURE_NO_WARNINGS 声明
		strcpy(m_name, obj.m_name);
		// 设置年龄
		m_age = obj.m_age;
	}
	
	/// <summary>
	/// 重载 等号 = 操作符 函数
	/// </summary>
	/// <param name="obj">等号右边的值</param>
	/// <returns>调用者本身</returns>
	Student& operator=(const Student& obj)
	{
		//先释放 调用者 本身的 m_name 指针指向的内存
		if (m_name != NULL)
		{
			// 使用 new 分配的内存需要使用 delete 释放
			delete[] m_name;
			// 释放内存后指针置空避免野指针
			m_name = NULL;
			// 年龄也设置为默认值
			m_age = 0;
		}

		// 重新分配新的 字符串 内存
		m_name = new char[strlen(obj.m_name)   1];
		// 拷贝字符串数据
		// 需添加 #define _CRT_SECURE_NO_WARNINGS 声明
		strcpy(m_name, obj.m_name);
		// 拷贝年龄
		m_age = obj.m_age;
		// 返回调用者本身, 以便进行链式调用
		return *this;
	}

public:
	/// <summary>
	/// 打印类的成员变量
	/// </summary>
	void print()
	{
		cout << "姓名 : " << m_name << " , 年龄 : " << m_age << endl;
	}
protected:
private:
	// 姓名
	char* m_name;

	// 年龄
	int	m_age;
};

int main() {

	Student s((char*)"Tom", 18);
	s.print();

	// 将 s 对象加入到 vec 动态数组中
	vector<Student> vec;
	vec.push_back(s);


	// 控制台暂停 , 按任意键继续向后执行
	system("pause");

	return 0;
};

2、执行结果

执行结果:

姓名 : Tom , 年龄 : 18 Press any key to continue . . .

0 人点赞