【C++笔记】深浅拷贝以及vector的深度剖析及其实现

【C++笔记】深浅拷贝以及vector的深度剖析及其实现

🔥个人主页大白的编程日记

🔥专栏C++笔记


文章目录

前言

哈喽,各位小伙伴大家好!上期我们讲了string的深度剖析和实现。今天我们来讲一下编码和string的深浅拷贝以及vector。话不多说,我们进入正题!

一.编码

1.1编码的定义

我们都知道计算机只能存储0或1。那像平时我们的文字或者英文,这些字符如何在计算机存储呢?这时有个编码表的东西就出来了。我们让一个字符和一个整形对应。也就是让字符和整形建立映射关系。这样计算机存储符号时存储对应的整形即可。所以编码本质就是------值和符号的映射编码关系。

1.2编码的理解

例如英美的文字都是由字母组成的。那他就只需要让字母与值建立映射关系即可。所以他们就发明出来了ascll码表。

ascll码表的本质------英文符号和值的映射关系。

所以我们看到字符在内存中存储的是字符对应的ascll码值。

我们输出字符的时候,编译器就会根据内存中存储的值去查编码表,打印出对应的符号。所以打印的过程是查编码表的过程。

  • 验证
    可能说前面存的是字符,大家觉得不太相信。
    那现在我们存储对应的ascll码值。

发现程序并不是打印数字。而是字符。这就是因为前面我们说的打印的过程是查编码表的过程。这些数字对应ascll码表中的符号就是abc\0所以打印出来的就是abc.

  • 乱码
    所以乱码就是存的值和编码表对应不上,例如存的是ascll编码表的,查的编码表缺是其他的。值和表对应不上就会出现乱码。而我们平时看到的烫烫烫或屯屯屯就是因为我们没有初始化,里面的值是编译器给的默认随机值。随机值去编码表查到的值是什么打印的就是什么。

1.3常见编码表

那大家看到ascll编码只适用与英美。而我们的汉字博大精深,ascll码表表示不了我们的汉字。而计算机发展需要兼顾全世界的国家。因此一个适用全世界文字的编码表就出来了------Unicode.统一码也叫万国码。

Unicode里面给出了三种编码方式:

分别是:UTF-8、UTF-16、UTF-32。

  • UTF-8
    UTF-8是一种变长编码方式。这样的好处就可以兼容ascll码表。
    因为兼容性更好所以使用的更多。
    - UTF-16
  • UTF-32

    所以为了兼容不同的编码方式。库里的string写成了模版。

常见的汉字是用两个字节编码一个汉字。

还有我们国家自己搞了一个编码表------GBK,大家可以自己了解一下。

二.深浅拷贝

cpp 复制代码
class String
{
public:
	/*String()
	:_str(new char[1])
	{*_str = '\0';}
	*/
	//String(const char* str = "\0") 错误示范
	//String(const char* str = nullptr) 错误示范
	String(const char* str = "")
	{
		// 构造String类对象时,如果传递nullptr指针,可以认为程序非
		if (nullptr == str)
		{
			assert(false);
			return;
		}
		_str = new char[strlen(str) + 1];
		strcpy(_str, str);
	}
	~String()
	{
		if (_str)
		{
			delete[] _str;
			_str = nullptr;
		}
	}
private:
	char* _str;
};
// 测试
void TestString()
{
	String s1("hello bit!!!");
	String s2(s1);
}

这段代码运行崩溃。为什么呢?

因为s1和s2指向同一块空间,就会对同一块空间析构两次。

导致s1和s2指向同一块空间的原因就是因为浅拷贝。

上述String类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用s1构造s2时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝。

  • 浅拷贝

浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。

如果类没有指向资源。那浅拷贝是可以的。

但是如果类指向资源,浅拷贝就不止西沟两次的问题。

还有两个对象共享一个资源空间的风险。

  • 深拷贝
    所以解决浅拷贝的问题就是重新开一块新的空间即可。
    如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。

三.引用计数与写时拷贝

写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。

引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源,如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。

三.vector

3.1vector的介绍及使用

vector学习时一定要学会查看文档,vector的文档使用vector在实际中非常的重要,在实际中我们熟悉常见的接口就可以,下面列出了哪些接口是要重点掌握的。

由于前面我们已经讲过string了vector这里很多都是类似的。所以这里就不过多赘述了。

  • vector的定义
构造函数声明 接口说明
vector()(重点) 无参构造
vector(size_type n, const value_type& val =value_type()) 构造并初始化n个val
vector (const vector& x); (重点) 拷贝构造
vector (InputIterator first, InputIterator last); 使用迭代器进行初始化构造
  • vector iterator 的使用
iterator的使用 接口说明
begin+end 获取第一个数据位置的iterator/const_iterator, 获取最后一个数据的下一个位置的iterator/const_iterator
rbegin+rend 获取最后一个数据位置的reverse_iterator,获取第一个数据前一个位置的reverse_iterator
  • vector空间增长问题
容量空间 接口说明
size 获取数据个数
capacity 获取容量大小
empty 判断是否为空
resize 改变vector的size
reserve 改变vector的capacity
  • reserve
cpp 复制代码
void TestVectorExpand()
{
	size_t sz;
	vector<int> v;
	sz = v.capacity();
	cout << "making v grow:\n";
	for (int i = 0; i < 100; ++i)
	{
		v.push_back(i);
		if (sz != v.capacity())
		{
			sz = v.capacity();
			cout << "capacity changed: " << sz << '\n';
		}
	}
}

我们验证一下vector的扩容方式。

capacity的代码在vs和g++下分别运行会发现,vs下capacity是按1.5倍增长的,g++是按2倍增长的。这个问题经常会考察,不要固化的认为,vector增容都是2倍,具体增长多少是 根据具体的需求定义的。vs是PJ版本STL,g++是SGI版本STL。

reserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以缓解vector增容的代价缺陷问题。

那reserve的扩容规则还是和string的一样吗?

和string不同的是不会缩容。string可能会缩容。

  • resize
    resize有三种情况。

3.2 vector的增删查改

这里erase和insert不支持下标。

vector不支持流插入和流提取。

因为vector的流插入和流提取是不确定的。

如果我们实现vector的流插入和流提取这样写就可以了。

3.3vector嵌套

vector除了可以存内置类型。还可以存储自定义类型。

例如vector存储string。

这时我们将的隐式类型转化就可以用上了。

同时范围for这里的引用就很有必要,减少深拷贝。

不改变加上const。

那vector嵌套vector是什么呢?

vector嵌套就是多维数组。

vector再嵌套一层vector就是二维数组。

这里vv[i][j]的调用本质是两个operator=.

cpp 复制代码
template<class T>
class vector
{
	T& operator[](size_t n)
	{
		return a[i];
	}
private:
	T* _a;
	size_t size;
	size_t capacity
};

vector是个模版。这里编译器实例化了两个类。

所以本质vv[i][j]是两个operator[]调用的另一种写法。

四.vector深度剖析及模拟实现

我们先看库里的实现。

库里用是三个迭代器实现vector。

一个指向起始位置,一个指向最后一个数据的位置的下一个位置,一个指向空间的下一个位置。

大体结构如下:

那现在我们自己来模拟实现一个vector.

4.1vector的定义

cpp 复制代码
template<class T>
class vector 
{
public:
	typedef T* iterator;
	typedef const T* const_iterator;
	private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _end_of_storage = nullptr;
};

4.1 reserve

我们检查一下是否需要扩容。如果扩容就申请空间拷贝数据。更新三个迭代器即可。

cpp 复制代码
void reserve(size_t n)
{
	if (n > capacity())
	{
		T* tmp = new T[n];
		if (tmp != nullptr)
		{
			size_t old_size =size();
			/*memcpy(tmp, _start, sizeof(T) * size());*///浅拷贝
			for (int i = 0; i < old_size; i++)
			{
				tmp[i] = _start[i];
			}
			delete[] _start;
			_start = tmp;
			_finish = _start + old_size;
			_end_of_storage = _start+n;
		}
	}
}

4.2 迭代器

迭代器只需要返回起始位置和有效数据的最后一个数据即可。

cpp 复制代码
iterator begin()
{
	return _start;
}
iterator end()
{
	return _finish;
}
const_iterator begin() const
{
	return _start;
}
const_iterator end() const
{
	return _finish;
}

4.3 构造和拷贝构造

构造直接用缺省值走初始化列表即可。

拷贝构造先reserve开空间,然后复用push_back拷贝数据即可。

n个T的val值构造。直接reserve,然后尾插val即可。

迭代器区间构造。直接尾插迭代器区间数据即可。

cpp 复制代码
vector()
{
}
vector(const vector<T>& v)
{
	reserve(v.capacity());
	for (auto& x : v)
	{
		push_back(x);
	}
}
vector(size_t n,const T& val=T())//调用T的默认构造
{
	reserve(n);
	while (n--)
	{
		push_back(val);
	}
}
//类模板的成员函数还可以是函数模板
template<class InputIterator>
vector(InputIterator first, InputIterator last)
{
	while (first != last)
	{
		push_back(*first);
		++first;
	}
}

类模板的成员函数还可以是函数模板。

4.4 size

直接返回_finish - _start就是数据个数

cpp 复制代码
size_t size() const
{
	return _finish - _start;
}

4.5 push_back

我们检查一下是否扩容。需要就reserve即可。

然后插入数据,++_finish即可。

cpp 复制代码
void push_back(const T x)
{
	if (_finish == _end_of_storage)
	{
		reserve(capacity()==0?4:2*capacity());
	}	
	*_finish = x;
	++_finish;
}

4.6 capacity

cpp 复制代码
size_t capacity() const
{
	return _end_of_storage - _start;
}

4.7 pop_back

尾删直接_finish--即可。

cpp 复制代码
void pop_back()
{
	assert(size());
	_finish--;
}

4.8 empty

empty直接判断是否_start==_finish即可。

cpp 复制代码
bool emtpy()
{
	return _start==_finish;
}

4.9 clear

直接让_start = _finish即可。

cpp 复制代码
void clear()
{
	_start = _finish;
}

4.10 resize

n>size就扩容然后插入数据即可。否则更新_finish删除数据即可。

cpp 复制代码
void resize(size_t n, T val = T())//调用构造 内置类型也有构造和析构
{
	if (n > size())//大于size插入数据
	{
		reserve(n);
		while (_finish != _start + n)
		{
			*_finish = val;
			++_finish;
		}
	}
	else
	{
		_finish = _start+n;
	}
}

为了兼容这种场景,内置类型也认为有构造和析构函数。

4.11 operator=

赋值重载我们继续用string的交换方式即可。

cpp 复制代码
void swap(vector<T>& v)
{
	std::swap(_start, v._start);
	std::swap(_finish, v._finish);
	std::swap(_end_of_storage,v._end_of_storage);
}
vector& operator=(vector& v)//类里面可以类名替代类型 类外面不行
{
	swap(v);
	return *this;
}

五.迭代器失效及语法问题

5.1语法问题

  • default
    C++11 标准引入了一个新特性:default函数。程序员只需在函数声明后加=default;,就可将该函数声明为 default 函数,编译器将为显式声明的 default 函数自动生成函数体。例如:
cpp 复制代码
vector() = default;//强制生成

Default 函数特性仅适用于类的特殊成员函数,且该特殊成员函数没有默认参数.

cpp 复制代码
template<class T>
void print_Container(const  vector<T>& v)
{
	//规定不能去没有实例化的类里面取东西
	//因为编译器无法区分这里的const_iterator是类型还是成员变量
	//typename就是说是类型,让程序员自己确认。用auto也可以自动识别
	vector<T>::const_iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		it++;
	}
}
  • typename
  • 类名替代类型
    类里面类名可以替代类型。
cpp 复制代码
vector<T>& operator=(vector<T>& v)//类里面可以类名替代类型 类外面不行
{
	swap(v);
	return *this;
}

所以这个代码还可以这样写

cpp 复制代码
vector& operator=(vector& v)//类里面可以类名替代类型 类外面不行
{
	swap(v);
	return *this;
}

5.2迭代器失效

  • 插入后失效
cpp 复制代码
void 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);
		//pos = _start + len;
	}
	for (iterator i = _finish; i >=pos ; i--)
	{
		 *i =*(i - 1);
	}
	*(pos) = x;
	_finish++;
}

以insert的pos为例,扩容后pos还指向旧空间。但是旧空间销毁了。

这时pos就是野指针。我们要认为此时pos已经失效了。

所以我们扩容后要计算相对位置更新pos。但是形参的改变不影响实参。函数内部修复,不能改变外面的实参。

cpp 复制代码
void 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);
		pos = _start + len;
	}
	for (iterator i = _finish; i >=pos ; i--)
	{
		 *i =*(i - 1);
	}
	*(pos) = x;
	_finish++;
}
cpp 复制代码
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
print_Container(v);
cout << endl;
vector<int>::iterator it = find(v.begin(), v.end(), 2);
v.insert(it, 10);
*it *= 10;
print_Container(v);
cout << endl;

所以这里扩容后再insert访问到的位置就是旧空间的位置。所以会出现随机值。

并且也没有再2的位置插入。

那是不是不扩容就没事了。

cpp 复制代码
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	print_Container(v);
	cout << endl;
	vector<int>::iterator it = find(v.begin(), v.end(), 2);
	v.insert(it, 100);
	*it*= 10;

我们原本是想让2*=10.却在100的位置*=10.

这时因为insert后数据挪动,2的位置后移

迭代器指向的位置是插入的100.这也是一种变相的迭代器失效。

因为我们无法得知什么时候扩容。

所以vs下进行强制检查。不能继续访问,访问就报错。

一句话insert后就认为迭代器已经失效,不要再访问了。

  • 删除后失效

    我们这里写一个删除所有偶数的程序。
cpp 复制代码
vector<int>::iterator it = v.begin();
while (it != v.end())
{
	if (*it % 2 == 0)
	{
		v.erase(it);
	}
	it++;
}

如果我们继续访问erase后的迭代器就会出现各种问题。

后言

这就是编码和深浅拷贝以及vector的深度剖析及其实现。大家自己好好消化理解。

今天就分享到这里,感谢的大家耐心垂阅!咱们下期见!拜拜~

相关推荐
鸿蒙自习室几秒前
鸿蒙UI开发——组件滤镜效果
开发语言·前端·javascript
星河梦瑾几秒前
SpringBoot相关漏洞学习资料
java·经验分享·spring boot·安全
黄名富3 分钟前
Redis 附加功能(二)— 自动过期、流水线与事务及Lua脚本
java·数据库·redis·lua
love静思冥想5 分钟前
JMeter 使用详解
java·jmeter
言、雲8 分钟前
从tryLock()源码来出发,解析Redisson的重试机制和看门狗机制
java·开发语言·数据库
TT哇15 分钟前
【数据结构练习题】链表与LinkedList
java·数据结构·链表
东风吹柳27 分钟前
观察者模式(sigslot in C++)
c++·观察者模式·信号槽·sigslot
A懿轩A36 分钟前
C/C++ 数据结构与算法【栈和队列】 栈+队列详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·栈和队列
汪洪墩37 分钟前
【Mars3d】设置backgroundImage、map.scene.skyBox、backgroundImage来回切换
开发语言·javascript·python·ecmascript·webgl·cesium
云空43 分钟前
《QT 5.14.1 搭建 opencv 环境全攻略》
开发语言·qt·opencv