IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> MyString类模拟实现(写实拷贝,柔性数组) -> 正文阅读

[C++知识库]MyString类模拟实现(写实拷贝,柔性数组)

分享一张上课摸鱼画的图

知识储备:

对于知识储备我仅仅说一下概念和理解,稍微举下例子,因为涉及的知识点太多,有兴趣可以自行查阅资料!

一、左值右值:

能出现在赋值号左边的表达式称为“左值”,不能出现在赋值号左边的表达式称为“右值”。一般来说,左值是可以取地址的,右值则不可以
非 const 的变量都是左值。函数调用的返回值若不是引用,则该函数调用就是右值。一般的“引用”都是引用变量的,而变量是左值,因此它们都是“左值引用”。

C++11 新增了一种引用,可以引用右值,因而称为“右值引用”。无名的临时变量不能出现在赋值号左边,因而是右值。右值引用就可以引用无名的临时变量。定义右值引用的格式如下:

类型 && 引用名 = 右值表达式;

引入右值引用的主要目的是提高程序运行的效率。当类持有其它资源时,例如动态分配的内存、指向其他数据的指针等,拷贝构造函数中需要以深拷贝(而非浅拷贝)的方式复制对象的所有数据。深拷贝往往非常耗时,合理使用右值引用可以避免没有必要的深拷贝操作。?
?

这里引入了移动语义,所谓移动语义(Move语义),指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。对于程序执行过程中产生的临时对象,往往只用于传递数据(没有其它的用处),并且会很快会被销毁。因此在使用临时对象初始化新对象时,我们可以将其包含的指针成员指向的内存资源直接移给新对象所有,无需再新拷贝一份,这大大提高了初始化的执行效率。

二、移动构造:

当数据成员中有指针时,深拷贝(重新开辟空间)解决了浅拷贝之后析构同一块内存的问题

但对于深拷贝需要不断地开辟空间,我们用对象a初始化对象b,后对象a我们就不在使用了,但是对象a的空间还在呀(在析构之前),既然拷贝构造函数,实际上就是把a对象的内容复制一份到b中,那么为什么我们不能直接使用a的空间呢?这样就避免了新的空间的分配,大大降低了构造的成本。这就是移动构造函数设计的初衷。

移动构造可以理解为文件的剪切操作,或者理解为二手交易,比如一个人要挂掉了,临走时,他要将自己的东西交付给其他人,之前的做法是通过拷贝构造进行拷贝,然后自己析构挂掉,但移动构造是直接将自己的东西转交给下个人,自己挂掉。

示例:

class A {
public:
	int x;
	A(int x) : x(x)
	{
		cout << "构建对象" << endl;
	}
	A(A& a) : x(a.x)
	{
		cout << "拷贝构建" << endl;
	}
	A& operator=(A& a)
	{
		x = a.x;
		cout << "拷贝赋值" << endl;
		return *this;
	}
	A(A&& a) : x(a.x)
	{
		cout << "移动构建" << endl;
	}
	A& operator=(A&& a)
	{
		x = a.x;
		cout << "移动赋值" << endl;
		return *this;
	}
};
int main()
{
	A a(1);
	A b = a;
	A c(a);
	b = a;
	A e = move(a);
	b = move(e);
	return 0;
}

A a(1),调用构造函数。
A b = a,创建新对象b,使用a初始化b,因此调用拷贝构造函数。
A c(a),创建新对象c,使用a初始化c,因此调用拷贝构造函数。
b = a,使用a的值更新对象b,因为不需要创建新对象,所以调用拷贝赋值运算符。
A e = move(a),创建新对象e,使用a的值初始化e,但调用move(a)将左值a转化为右值,所以调用移动构造函数。
b=move(e),移动e的值取赋值给b,调用移动赋值

?

移动构造函数的参数和拷贝构造函数不同,拷贝构造函数的参数是一个左值引用,但是移动构造函数的初值是一个右值引用。这意味着,移动构造函数的参数是一个右值或者将亡值的引用。也就是说,只用用一个右值,或者将亡值初始化另一个对象的时候,才会调用移动构造函数。而那个move语句,就是将一个左值变成一个将亡值。


三、柔性数组

struct TEST//柔性数组结构体
{
    int state;
    int len;
    char cData[0]; 
};

对于柔性数组的解释:

  1. 用途 : 长度为0的数组的主要用途是为了满足需要变长度的结构体
  2. 用法 : 在一个结构体的最后, 申明一个长度为0的数组, 就可以使得这个结构体是可变长的. 对于编译器来说, 此时长度为0的数组并不占用空间, 因为数组名本身不占空间, 它只是一个偏移量, 数组名这个符号本身代表了一个不可修改的地址常量
  3. 优点 比起在结构体中声明一个指针变量、再进行动态分 配的办法,这种方法效率要高。因为在访问数组内容时,不需要间接访问,避免了两次访存。

  4. 缺点在结构体中,数组为0的数组必须在最后声明,使用上有一定限制。

四、写实拷贝?

在类默认值函数的学习中,我们知道深,浅拷贝:

  1. 浅拷贝:系统提供的拷贝构造函数,赋值运算符重载函数是浅拷贝。是一个简单的赋值,让两个指针指向相同的数据,数据共享,但是当释放资源时,就会出现一个内存块被释放两次的错误。
  2. 深拷贝:每一个对象拥有自己独立的资源;浪费内存;当多个对象的内存数据一样,只是对内存进行读取操作,那么就会浪费大量的内存。

结合,深拷贝,浅拷贝的优缺点,产生了写时拷贝,分为两部分理解:

  • 写之前:浅拷贝,读取进行共享。
  • 写时:深拷贝,谁进行修改就给谁开辟一块内存。假如a需要修改数据,那么就给它单独开辟一块内存,其他对象可以继续共享这个内存,没有影响

  • 当共享同一块内存的类发生内容改变,发生写实拷贝

引用计数 :

c++引用计数的可以节省内存,而且同时可以降低构建对象和析构的开销,所谓引用计数简单说来就是对各对象共享一份实体的数据,但是我们需要实现对该数据的引用的对象的记录,这样最后一个对象引用结束后能够安全的删除数据。

用字符串举例,假设我们想要实现字符串的拷贝或者赋值,那么我们想要呈现的客户的是各自独立的字符串。如下:

但是,对于计算机的内部实现而言,这样的方式显然出现了冗余存储的现象,那么我们期待计算机内部是这样实现的:

这样所有的用户拥有的字符串都是同一个,但是,这会出现一个问题,那就是的那个其中一个销毁对象时,其它对象的数据也将不可访问,因此,我们需要对该份数据的引用进行计数,只有最后一个引用对象才能真正的销毁数据。
因此实现是这样的:

虽然这样能够使得多个相同的字符串共享同一份数据,但是,当其中任何一个对象修改数据时,其它对象所拥有的数据也就都改变,因此,为了避免这种情况的发生,采用写时复制写实拷贝的技巧,也就是当某个对象要修改数据时便重新赋值一份进行修改,从而该对象就有一份新的数据,不在和其它对象共享一份数据。

对于写实拷贝推荐一篇大牛的文章:C++ STL string的Copy-On-Write技术 | 酷 壳 - CoolShell

MyString实现:

设计框架:

class MyString
{
private:
	enum { ALIGN = 8 };
	struct StrNode //占12字节 ,柔性数组不会计算后面数组大小
	{
		int ref;//引用计数
		int len;//字符串长度
		int capc;//容量
		char data[]; //柔性数组
	};
private:
	StrNode* pstr;
	MyString(StrNode* p) :pstr(p) {}
	//获取内存大小,对齐
	static size_t round_up(size_t n)//将newcap提升成8的倍数(不一定是8倍数,4,16都行等等),扩容
	{
		return (n + ALIGN - 1) & ~(ALIGN - 1);
	}

}

e2b309a6ffbd47828bc030fb30abd83a.png

构造函数:

public:
	MyString(const char* p = nullptr) :pstr(nullptr)//构造函数
	{
		if (p != nullptr)
		{
			int n = strlen(p);
			int total = round_up(2 * n); //扩容后柔型数组大小
			pstr = A_Malloc(total);  //封装后,不需要考虑结构体大小
			if (pstr == nullptr)exit(1);
			pstr->ref = 1;
			pstr->len = n;
			pstr->capc = total - 1;
			strcpy_s(pstr->data, total, p);

		}
	}
	MyString(const MyString& p) :pstr(p.pstr) //拷贝构造
	{
		if (pstr != nullptr)
		{
			pstr->ref += 1;
		}
	}

析构函数:

  1. 首先要判断pstr是否为NULL,不为NULL时再对其引用计数 ref -= 1
  2. 若减完 ref为0,说明再没有对象指向该字符串,需要将其空间释放。
  3. 释放完成后将pstr 置为NULL。
	static void A_Free(StrNode* p)
	{
		free(p);
	}
    void clear() //释放函数
	{
		if (this->pstr != nullptr && --this->pstr->ref == 0)
		{
			A_Free(this->pstr);
		}
		this->pstr = nullptr;
	}
    ~MyString()
	{
		clear();
	}

赋值运算符重载:

  1. 待赋值对象是否和源对象是同一个对象,若不是则
  2. 如果待赋值对象不为空且只有他一个对象指向堆空间,则释放该空间,若对象本身为空,不执行
  3. 将源对象赋值给待赋值对象
  4. 如果源对象不为空,那么赋值过后的待赋值对象也指向堆空间,则需要将引用计数加一
  5. 返回this指针
MyString& operator=(const MyString& s) //共享资源
	{
		//s1=s2   //两个都空 或都相等
		if (this == &s || this->pstr == s.pstr)
		{
			return *this;
		}
		//s2空 s1不空  s2(s1)
		if (this->pstr == nullptr && s.pstr != nullptr)
		{
			this->pstr = s.pstr;
			this->pstr->ref += 1;
		}
		//s1空 s2不空   s2(s1)   //都不空
		else
		{
			if (this->pstr != nullptr && --this->pstr->ref == 0)
			{
				free(this->pstr);
			}
			this->pstr = s.pstr;
			if (this->pstr != nullptr)
			{
				this->pstr->ref += 1;
			}
		}
		/*等价
		 if (this == &sx || this->pstr == sx.pstr)
		{
			return *this;
		}
		this->clear();
		this->pstr = sx.pstr;
		if (this->pstr != nullptr)
		{
			this->pstr->ref += 1;
		}
		*/
		return *this;
	}

?移动构造,移动赋值:

  1. 移动构造,就是将自己的资源进行转移,转移到要构造的对象里。
  2. 移动赋值,将自己的资源赋值给目标对象,自身置为空。?
MyString(MyString&& s) //移动构造,我把我的资源给你(资源的转移)
	{
		this->pstr = s.pstr;
		s.pstr = nullptr;
	}
	MyString& operator=(MyString&& s)//移动赋值 资源转移,资源的引用计数不会改变
	{
		if (this == &s)return *this;
		if (this->pstr == s.pstr)
		{
			s.clear();
			return *this;
		}
		clear();
		this->pstr = s.pstr;
		s.pstr = nullptr;
		return *this;
	}

写实拷贝:

  1. 边界条件,如果为空,程序退出。如果下标不在范围内,程序终止。
  2. 判断引用计数是否大于1,如果不大于,那么可以直接返回该下标元素,如果大于1,说明还有其他对象也指向该字符串,需要拷贝。
  3. 重新申请堆区空间,将源空间内容拷贝进去
  4. 将新空间的引用计数加一,源空间引用计数减一,最后将新空间赋给该对象的指针pstr。
//共享同一块内存的类发生内容改变 发生写时拷贝
	char& operator[](const int index)//写时拷贝
	{
		assert(index >= 0 && index < pstr->len);
		if (pstr->ref > 1) //就要深拷贝了 克隆
		{
			int total = pstr->capc + 1;
			StrNode* newstr = A_Malloc(total);
			memmove(newstr, pstr, sizeof(StrNode) + sizeof(char) * (pstr->len + 1));
			newstr->ref = 1;
			pstr->ref -= 1;
			pstr = newstr;
		}
		return pstr->data[index];
	}

其他函数实现:

1、重载“+”,封装字符串连接实现对象和对象,对象和字符串,对象和字符的连接操作

private:
	static StrNode* AddString(const char* pa, const char* pb, size_t n)//字符串连接
	{

		int total = round_up(2 * n);
		StrNode* newdata = A_Malloc(total);
		// newdata--> ref,len,capc,data	 4
		newdata->ref = 1;
		newdata->len = n;
		newdata->capc = total - 1;
		newdata->data[0] = '\0';
		if (pa != nullptr)
		{
			strcpy_s(newdata->data, total, pa);
		}
		if (pb != nullptr)
		{
			strcat_s(newdata->data, total, pb);
		}
		return newdata;
	}
public:
	//对象和对象
	MyString operator+(const MyString& s)const
	{
		int n = this->size() + s.size();
		if (0 == n)
		{
			return MyString();
		}
		const char* pa = pstr != nullptr ? pstr->data : nullptr;
		const char* pb = s.pstr != nullptr ? s.pstr->data : nullptr;
		StrNode* newdata = AddString(pa, pb, n);
		return MyString(newdata);
	}

	//对象和字符串
	MyString operator+(const char* s)const
	{
		int n = this->size() + ((s == nullptr) ? 0 : strlen(s));
		if (0 == n)
		{
			return MyString();
		}
		const char* pa = pstr != nullptr ? pstr->data : nullptr;
		StrNode* newdata = AddString(pa, s, n);
		return MyString(newdata);
	}
	//对象和字符
	MyString operator+(const char s)const
	{
		char str[2] = { s };
		return *this + str;
	}

2、实现扩容操作 对于malloc和realloc进行封装

private:
// 使用静态将其malloc封装
	static StrNode* A_Malloc(size_t n)
	{
		StrNode* s = (StrNode*)malloc(sizeof(StrNode) + sizeof(char) * n);
		if (nullptr == s) exit(1);
		return s;
	}
	// realloc 
	static StrNode* A_Realloc(StrNode* p, size_t ns)//ns新的大小
	{
		StrNode* s = (StrNode*)realloc(p, sizeof(StrNode) + sizeof(char) * ns);
		if (nullptr == s) exit(1);
		return s;
	}
//进行相应的增容
	void reserve(size_t newcap)
	{
		if (newcap <= capacity()) return;//如果小于容量,不需要扩容
		int total = round_up(newcap);

		if (pstr == nullptr)
		{
			pstr = A_Realloc(pstr, total);
			pstr->ref = 1;
			pstr->len = 0;
			pstr->capc = total - 1;

		}
		else if (pstr->ref > 1) //需要克隆一个新的空间
		{                   //如果直接realloc,之前的空间再被克隆后会还给堆区,成为失效指针
			pstr->ref -= 1; 
			StrNode* newdata = A_Malloc(total);//自己的malloc
			memmove(newdata, pstr, sizeof(StrNode) + pstr->len + 1);//把pstr的值移动到newdata
			newdata->ref = 1;
			newdata->len = pstr->len;
			newdata->capc = total - 1;
			pstr = newdata;
		}
		//如果引用计数等于1,在原有的基础上后面扩容
		else{
			StrNode* newdata = A_Realloc(pstr, total);//自己的realloc
			pstr = newdata;
			pstr->capc = total - 1;
		}
	}

3、其他例如获取字符串首字符,尾字符,容量,长度等等基础函数在下面源码给出(偷波懒~~)?

源码源码:

#include<iostream>
#include<cassert>
using namespace std;


class MyString
{
private:
	enum { ALIGN = 8 };
	struct StrNode //占12字节 ,柔性数组不会计算后面数组大小
	{
		int ref;//引用计数
		int len;//字符串长度
		int capc;//容量
		char data[]; //柔性数组
	};
private:
	StrNode* pstr;
	MyString(StrNode* p) :pstr(p) {}
	//获取内存大小,对齐
	static size_t round_up(size_t n)//将newcap提升成8的倍数(不一定是8倍数,4,16都行等等),扩容
	{
		return (n + ALIGN - 1) & ~(ALIGN - 1);
	}
	// 使用静态将其malloc封装
	static StrNode* A_Malloc(size_t n)
	{
		StrNode* s = (StrNode*)malloc(sizeof(StrNode) + sizeof(char) * n);
		if (nullptr == s) exit(1);
		return s;
	}
	// realloc 
	static StrNode* A_Realloc(StrNode* p, size_t ns)//ns新的大小
	{
		StrNode* s = (StrNode*)realloc(p, sizeof(StrNode) + sizeof(char) * ns);
		if (nullptr == s) exit(1);
		return s;
	}
	static void A_Free(StrNode* p)
	{
		free(p);
	}
	static StrNode* AddString(const char* pa, const char* pb, size_t n)//字符串连接
	{

		int total = round_up(2 * n);
		StrNode* newdata = A_Malloc(total);
		// newdata--> ref,len,capc,data	 4
		newdata->ref = 1;
		newdata->len = n;
		newdata->capc = total - 1;
		newdata->data[0] = '\0';
		if (pa != nullptr)
		{
			strcpy_s(newdata->data, total, pa);
		}
		if (pb != nullptr)
		{
			strcat_s(newdata->data, total, pb);
		}
		return newdata;
	}

public:
	void clear() //释放函数
	{
		if (this->pstr != nullptr && --this->pstr->ref == 0)
		{
			A_Free(this->pstr);
		}
		this->pstr = nullptr;
	}

	//进行相应的增容
	void reserve(size_t newcap)
	{
		if (newcap <= capacity()) return;//如果小于容量,不需要扩容
		int total = round_up(newcap);

		if (pstr == nullptr)
		{
			pstr = A_Realloc(pstr, total);
			pstr->ref = 1;
			pstr->len = 0;
			pstr->capc = total - 1;

		}
		else if (pstr->ref > 1) //需要克隆一个新的空间
		{                   //如果直接realloc,之前的空间再被克隆后会还给堆区,成为失效指针
			pstr->ref -= 1; 
			StrNode* newdata = A_Malloc(total);//自己的malloc
			memmove(newdata, pstr, sizeof(StrNode) + pstr->len + 1);//把pstr的值移动到newdata
			newdata->ref = 1;
			newdata->len = pstr->len;
			newdata->capc = total - 1;
			pstr = newdata;
		}
		//如果引用计数等于1,在原有的基础上后面扩容
		else{
			StrNode* newdata = A_Realloc(pstr, total);//自己的realloc
			pstr = newdata;
			pstr->capc = total - 1;
		}
	}
public:
	MyString(const char* p = nullptr) :pstr(nullptr)//构造函数
	{
		if (p != nullptr)
		{
			int n = strlen(p);
			int total = round_up(2 * n); //扩容后柔型数组大小
			pstr = A_Malloc(total);  //封装后,不需要考虑结构体大小
			if (pstr == nullptr)exit(1);
			pstr->ref = 1;
			pstr->len = n;
			pstr->capc = total - 1;
			strcpy_s(pstr->data, total, p);

		}
	}
	MyString(const MyString& p) :pstr(p.pstr) //拷贝构造
	{
		if (pstr != nullptr)
		{
			pstr->ref += 1;
		}
	}
	~MyString()
	{
		clear();
	}
	MyString& operator=(const MyString& s) //共享资源
	{
		//s1=s2   //两个都空 或都相等
		if (this == &s || this->pstr == s.pstr)
		{
			return *this;
		}
		//s2空 s1不空  s2(s1)
		if (this->pstr == nullptr && s.pstr != nullptr)
		{
			this->pstr = s.pstr;
			this->pstr->ref += 1;
		}
		//s1空 s2不空   s2(s1)   //都不空
		else
		{
			if (this->pstr != nullptr && --this->pstr->ref == 0)
			{
				free(this->pstr);
			}
			this->pstr = s.pstr;
			if (this->pstr != nullptr)
			{
				this->pstr->ref += 1;
			}
		}
		/*等价
		 if (this == &sx || this->pstr == sx.pstr)
		{
			return *this;
		}
		this->clear();
		this->pstr = sx.pstr;
		if (this->pstr != nullptr)
		{
			this->pstr->ref += 1;
		}
		*/
		return *this;
	}
	MyString(MyString&& s) //移动构造,我把我的资源给你(资源的转移)
	{
		this->pstr = s.pstr;
		s.pstr = nullptr;
	}
	MyString& operator=(MyString&& s)//移动赋值 资源转移,资源的引用计数不会改变
	{
		if (this == &s)return *this;
		if (this->pstr == s.pstr)
		{
			s.clear();
			return *this;
		}
		clear();
		this->pstr = s.pstr;
		s.pstr = nullptr;
		return *this;
	}

	//共享同一块内存的类发生内容改变 发生写时拷贝
	char& operator[](const int index)//写时拷贝
	{
		assert(index >= 0 && index < pstr->len);
		if (pstr->ref > 1) //就要深拷贝了 克隆
		{
			int total = pstr->capc + 1;
			StrNode* newstr = A_Malloc(total);
			memmove(newstr, pstr, sizeof(StrNode) + sizeof(char) * (pstr->len + 1));
			newstr->ref = 1;
			pstr->ref -= 1;
			pstr = newstr;
		}
		return pstr->data[index];
	}
	const char& operator[](const int index)const
	{
		assert(index >= 0 && index < pstr->len);
		return pstr->data[index];
	}
	void Print() const 
	{
		if (pstr != nullptr)
		{
			cout << pstr->data << endl;
		}
	}

public:
		size_t size()const//字符有效个数
		{
			return pstr == nullptr ? 0 : pstr->len;
		}
		size_t length()const
		{
			return size();
		}
		size_t capacity()//获取容量
		{
			return pstr == nullptr ? 0 : pstr->capc;
		}
		bool empty()const { return size() == 0; }//判空
		const char* c_str()const//返回首字符的地址
		{
			return pstr != nullptr ? pstr->data : nullptr;
		}
		const char* data()const//返回首字符的地址
		{
			return c_str();
		}
		char& front()//取第一个字符
		{
			assert(pstr != nullptr);
			return pstr->data[0];
		}
		const char& front()const//取第一个字符
		{
			assert(pstr != nullptr);
			return pstr->data[0];
		}
		char& back()//取最后一个字符
		{
			assert(pstr != nullptr);
			return pstr->data[pstr->len - 1];
		}
		const char& back()const//取最后一个字符
		{
			assert(pstr != nullptr);
			return pstr->data[pstr->len - 1];

		}
		char& at(const int index) //取index下标下的字符
		{
			assert(pstr != nullptr && index > 0 && index < pstr->len);
			return pstr->data[index];
		}
		const char& at(const int index)const //取index下标下的字符
		{
			assert(pstr != nullptr && index >= 0 && index < pstr->len);
			return pstr->data[index];
		}
	
public:
	//对象和对象
	MyString operator+(const MyString& s)const
	{
		int n = this->size() + s.size();
		if (0 == n)
		{
			return MyString();
		}
		const char* pa = pstr != nullptr ? pstr->data : nullptr;
		const char* pb = s.pstr != nullptr ? s.pstr->data : nullptr;
		StrNode* newdata = AddString(pa, pb, n);
		return MyString(newdata);
	}

	//对象和字符串
	MyString operator+(const char* s)const
	{
		int n = this->size() + ((s == nullptr) ? 0 : strlen(s));
		if (0 == n)
		{
			return MyString();
		}
		const char* pa = pstr != nullptr ? pstr->data : nullptr;
		StrNode* newdata = AddString(pa, s, n);
		return MyString(newdata);
	}
	//对象和字符
	MyString operator+(const char s)const
	{
		char str[2] = { s };
		return *this + str;
	}

	//对象和对象
	MyString& operator+=(const MyString& s);
	//对象和字符串
	MyString& operator+=(const char* s);
	//对象和字符
	MyString& operator+=(const char s);

};
//字符串和对象
MyString operator+(const char* str, const MyString& s);
//字符和对象
MyString operator+(const char val, const MyString& s);

int main()
{
	MyString s1("I love");
	s1 = s1 + " you"+" very"+" much";
	s1.Print();
}

? ? ? ? ? ? ? ? ? ? ? ? ? ??? 看到这里麻烦点个赞再走~

最近太忙了..代码也碰的少,等忙完这阵子继续更新,虽然也不知道什么时候了

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-06-14 22:19:58  更:2022-06-14 22:20:13 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 16:55:37-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码