[C++ 기초] 9장: 벡터(공통 인터페이스 사용 + 시뮬레이션 구현)

벡터 소개

  1. 벡터가변 크기 배열을 나타내는 시퀀스 컨테이너입니다.
  2. 벡터배열과 마찬가지로 연속적인 저장 공간 도 요소를 저장하는 데 사용됩니다. 즉, 배열만큼 효율적인 첨자를 사용하여 벡터의 요소에 액세스할 수 있습니다. 그러나 배열과 달리 크기는 동적으로 변경될 수 있으며 크기는 컨테이너에서 자동으로 처리됩니다.
  3. 새 요소가 삽입되고 벡터 크기를 조정해야 하는 경우 방법은 새 배열을 할당한 다음 모든 요소를 ​​이 배열로 이동하고 원래 배열 공간을 해제하는 것입니다.
  4. 벡터 공간 할당 전략: 벡터는 가능한 성장을 수용하기 위해 약간의 추가 공간을 할당하므로 저장 공간은 일반적으로 필요한 실제 저장 공간보다 큽니다. 서로 다른 라이브러리는 서로 다른 전략을 사용하여 공간 사용과 재할당을 절충하므로 끝에 요소를 삽입하는 것은 일정한 시간 복잡도에서 수행됩니다.
  5. 벡터는 요소를 저장하기 위해 연속적인 공간을 사용하기 때문에 다른 동적 시퀀스 컨테이너와 비교할 때 벡터가 요소에 액세스할 때 더 효율적이며 마지막에 요소를 추가하고 삭제하는 것이 상대적으로 효율적이며 삭제 및 삽입 작업의 효율성은 수행되지 않습니다. 끝이 낮습니다. 상대적으로 낮습니다.

벡터의 사용

여기에 이미지 설명 삽입
벡터의 맨 아래 레이어는 클래스 템플릿으로 정의할 때 인스턴스화 유형을 표시해야 합니다.

벡터의 정의

방법 1: 특정 유형의 빈 컨테이너를 생성합니다.

vector<int> v1; //构造int类型的空容器

방법 2: n개의 값을 포함하는 특정 유형의 컨테이너를 구성합니다.

vector<int> v2(10, 2); //构造含有10个2的int类型容器

방법 3: 특정 유형의 컨테이너 구성을 복사합니다.

vector<int> v3(v2); //拷贝构造int类型的v2容器

방법 4: 반복자를 사용하여 특정 콘텐츠를 복사하고 구성합니다.

vector<int> v4(v2.begin(), v2.end()); //使用迭代器拷贝构造v2容器的某一段内容

참고: 이 방법은 다른 컨테이너의 특정 콘텐츠를 복사하는 데에도 사용할 수 있습니다.

string s("hello world");
vector<char> v5(s.begin(), s.end()); //拷贝构造string对象的某一段内容

벡터 이터레이터(iterator)의 사용

여기에 이미지 설명 삽입

시작과 끝

begin 함수는 컨테이너에 있는 첫 번째 요소의 반복자를 반환 할 수 있고 end 함수는 컨테이너에 있는 마지막 요소의 반복자를 반환할 수 있습니다.

push_back()사전에 사용법을 소개합시다 .
push_back 함수를 사용하여 컨테이너 꼬리 삽입

void test_vector1()
{
    
    
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	//正向迭代器遍历容器
	vector<int>::iterator it = v.begin();
	while (it != v.end())
	{
    
    
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

여기에 이미지 설명 삽입

r시작하고 찢다

rbegin 함수는 컨테이너에 있는 마지막 요소의 역방향 반복자를 가져오는 데 사용할 수 있으며 rend 함수는 컨테이너에 있는 첫 번째 요소의 이전 위치에 대한 역방향 반복자를 가져오는 데 사용할 수 있습니다.

참고: 역방향 반복기 작성:reverse_iterator

void test_vector2()
{
    
    
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	//反向迭代器遍历容器
	vector<int>::reverse_iterator it = v.rbegin();
	while (it != v.rend())
	{
    
    
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

여기에 이미지 설명 삽입

벡터 공간 성장 문제

크기와 용량

size 함수를 통해 현재 컨테이너의 유효 요소 수를 구하고, capacity 함수를 통해 현재 컨테이너의 최대 용량을 구합니다.

void test_vector3()
{
    
    
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);

	cout << v.size() << endl;//获取当前容器中的有效元素个数
	cout << v.capacity() << endl;//获取当前容器的最大容量
}

여기에 이미지 설명 삽입

예약 및 크기 조정(강조)

컨테이너의 최대 용량(capacity)은 reserved 기능에 의해 변경되고 컨테이너의 유효 요소 수(size)는 resize 기능에 의해 변경됩니다.

예약 규칙:
 1. 주어진 값이 컨테이너의 현재 용량보다 크면 이 값으로 용량을 확장합니다.
 2. 주어진 값이 컨테이너의 현재 용량보다 작을 때 아무것도 하지 않는다.

Resize 규칙:
 1. 주어진 값이 컨테이너의 현재 크기보다 큰 경우 이 값으로 크기를 확장하고 확장된 요소는 두 번째 매개 변수에서 지정한 값이며, 지정하지 않은 경우 기본값은 0입니다.
 2. 주어진 값이 컨테이너의 현재 크기보다 작을 때 이 값으로 크기를 줄인다. (참고: 용량의 크기는 변경되지 않습니다. 즉, 축소되지 않습니다.)

void test_vector4()
{
    
    
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
	cout << v.size() << endl;//5
	cout << v.capacity() << endl << endl;//6

	//reserve
	// 大于容器当前的capacity时
	v.reserve(10);
	cout << v.size() << endl;//size不变   5
	cout << v.capacity() << endl << endl; //改变容器的capacity为10,

	// 比当前容量小时,不缩容
	v.reserve(4);
	cout << v.size() << endl;//size不变   5
	cout << v.capacity() << endl << endl;//不变   10

	//resize
	v.resize(8);
	cout << v.size() << endl;//改变容器的size为8
	cout << v.capacity() << endl << endl;//10

	v.resize(15, 1);
	cout << v.size() << endl;//改变容器的size为15,扩大的元素用1初始化
	cout << v.capacity() << endl << endl;//15

	v.resize(3);//不會縮容
	cout << v.size() << endl;//size缩小到3
	cout << v.capacity() << endl << endl;//不变  15
}

벡터의 기본 확장 메커니즘 테스트

테스트 코드

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';
		}
	}
}

vs 환경에서 실행한 결과:
여기에 이미지 설명 삽입

Linux 환경에서 실행한 결과:
여기에 이미지 설명 삽입
이 코드는 각각 vs와 g++에서 실행되며 vs에서 용량이 약 1.5배 증가하고 g++에서 용량이 2배 증가함을 알 수 있습니다.
이 질문은 종종 조사됩니다.벡터의 용량이 두 배가 된다고 생각하지 마십시오.특정 증가는 특정 요구에 따라 정의됩니다. vs는 PJ 버전 STL이고 g++는 SGI 버전 STL입니다.
Reserve는 공간을 여는 역할만 담당하며, 얼마나 많은 공간이 필요한지 알면 벡터 용량 확장의 비용 결함을 완화할 수 있습니다.

비어 있는

empty 함수를 사용하여 현재 컨테이너가 비어 있는지 확인합니다.

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

int main()
{
    
    
	vector<int> v(10, 2);
	cout << v.empty() << endl;
	return 0;
}

벡터 추가, 삭제, 수정

푸시백과 팝백

컨테이너는 push_back 함수를 통해 꼬리 삽입되고 pop_back 함수는 꼬리 삭제됩니다.

void test_vector5()
{
    
    
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);

	for (const auto& e : v)
	{
    
    
		cout << e << " ";
	}
	cout << endl;
	
	v.pop_back();
	v.pop_back();
	v.pop_back();
	v.pop_back();

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

여기에 이미지 설명 삽입

삽입 및 지우기

insert 함수는 주어진 반복자 위치 앞에 하나 이상의 요소를 삽입하는 데 사용할 수 있으며 지우기 함수는 주어진 반복자 위치에서 요소를 삭제하거나 주어진 반복자 범위의 모든 요소를 ​​삭제하는 데 사용할 수 있습니다(왼쪽은 닫히고 오른쪽은 열림). ).
여기에 이미지 설명 삽입

void test_vector6()
{
    
    
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
	for (const auto& e : v)
	{
    
    
		cout << e << " ";
	}
	cout << endl;

	v.insert(v.begin(), 0); //在容器开头插入0
	v.insert(v.begin()+3, 5, 1); //在3位置之前插入5个1
	for (const auto& e : v)
	{
    
    
		cout << e << " ";
	}
	cout << endl;

	v.erase(v.begin()); //删除容器中的第一个元素
	v.erase(v.begin()+2, v.begin() + 7); //删除在该迭代器区间内的元素(左闭右开)
	for (const auto& e : v)
	{
    
    
		cout << e << " ";
	}
	cout << endl;
}

여기에 이미지 설명 삽입
위는 위치별로 요소를 삽입하거나 삭제하는 방법인데, 값으로 삽입하거나 삭제(특정 값 위치에 삽입하거나 삭제)하려면 찾기 기능을 사용해야 합니다.

찾다

기능: 검색.
참고: 찾기 기능은 vector 의 멤버 인터페이스가 아닌 algorithm 모듈 에 의해 구현되며 vector 의 멤버 인터페이스에는 find가 없습니다.

find 함수는 총 3개의 매개변수가 있는데 처음 두 매개변수는 반복자 범위(왼쪽 닫힘, 오른쪽 열림)를 결정하고 세 번째 매개변수는 찾을 값을 결정합니다.
find 함수는 주어진 반복자 범위에서 일치하는 첫 번째 요소를 찾아 해당 반복자를 반환하거나 찾지 못한 경우 주어진 두 번째 매개변수를 반환합니다.

void test_vector7()
{
    
    
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
	for (const auto& e : v)
	{
    
    
		cout << e << " ";
	}
	cout << endl;

	//在3位置之前插入一个30
	vector<int>::iterator it = find(v.begin(), v.end(), 3); //获取值为3的元素的迭代器
	if (it != v.end())
	{
    
    
		v.insert(it, 30);
	}
	for (const auto& e : v)
	{
    
    
		cout << e << " ";
	}
	cout << endl;
	
	//删除30
	it = find(v.begin(), v.end(), 30); //获取值为30的元素的迭代器
	if (it != v.end())
	{
    
    
		v.erase(it);
	}
	for (const auto& e : v)
	{
    
    
		cout << e << " ";
	}
	cout << endl;
}

여기에 이미지 설명 삽입

교환

두 컨테이너의 데이터 공간은 스왑 기능을 통해 교환하여 두 컨테이너의 교환을 실현할 수 있습니다.

void test_vector8()
{
    
    
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
	cout << "交换之前的v: ";
	for (const auto& e : v)
	{
    
    
		cout << e << " ";
	}
	cout << endl;

	vector<int> v1;
	v1.push_back(10);
	v1.push_back(20);
	v1.push_back(30);
	cout << "交换之前的v1: ";
	for (const auto& e : v1)
	{
    
    
		cout << e << " ";
	}
	cout << endl << endl;

	v1.swap(v);//交换v1,v的数据空间
	
	cout << "交换之后的v: ";
	for (const auto& e : v)
	{
    
    
		cout << e << " ";
	}
	cout << endl;

	cout << "交换之后的v1: ";
	for (const auto& e : v1)
	{
    
    
		cout << e << " ";
	}
	cout << endl;

}

여기에 이미지 설명 삽입

operator[] 요소 액세스(강조)

[ ] 연산자의 오버로드는 벡터에서 구현되므로 "첨자 + [ ]"를 통해 컨테이너의 요소에 액세스할 수도 있습니다.

void test_vector9()
{
    
    
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);

	//使用“下标+[]”的方式遍历容器
	for (size_t i = 0; i < v.size(); ++i)
	{
    
    
		cout << v[i] << " ";
	}
	cout << endl;

	//使用“下标+[]”的方式遍修改容器内的数据
	for (size_t i = 0; i < v.size(); ++i)
	{
    
    
		 v[i]++;
	}
	cout << endl;

	//使用“下标+[]”的方式遍历容器
	for (size_t i = 0; i < v.size(); ++i)
	{
    
    
		cout << v[i] << " ";
	}
	cout << endl;
}

여기에 이미지 설명 삽입

벡터 반복자 무효화 문제(강조)

반복자의 주요 기능은 각 컨테이너를 사용할 때 기본 데이터 구조에 신경 쓰지 않도록 하는 것이며 벡터의 반복자는 실제로 아래쪽에 있는 포인터입니다. Iterator 실패는 Iterator의 기본 포인터가 가리키는 공간이 소멸되고 해제된 공간 조각을 가리키는 것을 의미하며, 잘못된 Iterator를 계속 사용하면 프로그램이 충돌할 수 있습니다.

반복자 무효화 문제의 예

예 1:

void test_vector10()
{
    
    
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	//v: 1 2 3 4 
	vector<int>::iterator pos = find(v.begin(), v.end(), 2); //获取值为2的元素的迭代器
	if (pos != v.end())
	{
    
    
		v.insert(pos, 10); //在值为2的元素的位置前插入10
	}
	
	//v: 1 10 2 3 4 
	v.erase(pos); //删除元素2 ???error(迭代器失效)
	//v: 1 2 3 4 
}

이 코드에서는 요소 2의 반복자를 사용하여 원래 시퀀스의 2 위치에 10을 삽입한 다음 2를 삭제하려고 하지만 실제로 얻는 것은 위치에 10을 삽입할 때 2에 대한 포인터입니다. 의 2 포인터는 10을 가리키므로 나중에 삭제하는 것은 실제로 2가 아니라 10입니다.

예 2:

void test_vector11()
{
    
    
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	vector<int>::iterator it = v.begin();
	while (it != v.end())
	{
    
    
		if (*it % 2 == 0) //删除容器当中的全部偶数
		{
    
    
			v.erase(it);
		}
		it++;
	}
}

코드에 오류가 없어 보이지만 그림을 그려서 잘 분석해보면 코드의 문제점을 발견할 수 있는데 반복자가 컨테이너에 속하지 않은 메모리 공간에 접근해 프로그램이 크래시를 일으키는 원인이 된다.
사진 설명을 추가해주세요
뿐만 아니라 Iterator가 컨테이너의 요소를 통과하여 판단할 때 3개의 요소를 판단하지 않습니다.

반복자 무효화 솔루션

반복자를 사용할 때 항상 한 문장을 기억하세요. 매번 사용하기 전에 반복자를 다시 할당하세요.

예제 1 솔루션:

void test_vector10()
{
    
    
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	//v: 1 2 3 4 
	vector<int>::iterator pos = find(v.begin(), v.end(), 2); //获取值为2的元素的迭代器
	if (pos != v.end())
	{
    
    
		v.insert(pos, 10); //在值为2的元素的位置前插入10
	}
	
	pos = find(v.begin(), v.end(), 2); //重新获取值为2的元素的迭代器
	v.erase(pos); //删除元素2
	//v: 1 10 3 4 
}

예를 들어 1의 경우 iterator를 사용하여 요소 2를 삭제할 때 요소 2를 재할당하여 해결할 수 있습니다.

예제 2 솔루션:

void test_vector11()
{
    
    
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	vector<int>::iterator it = v.begin();
	while (it != v.end())
	{
    
    
		if (*it % 2 == 0) //删除容器当中的全部偶数
		{
    
    
			it = v.erase(it); //删除后获取下一个元素的迭代器
		}
		else
		{
    
    
			it++; //是奇数则it++
		}

	}
}

예를 들어 2, 지우기 함수의 반환 값을 수신하고(지우기 함수는 요소가 삭제된 후 요소의 새 위치를 반환함) 코드의 논리를 제어할 수 있습니다. 요소가 삭제되면 요소를 계속 판단합니다. 이 위치에서(이 위치의 요소가 업데이트되었기 때문에 다시 판단해야 함).

요약하다

위는 벡터의 공통 인터페이스 사용에 대한 개요입니다.실제로 문자열을 학습한 후 곧 벡터 사용을 시작합니다.


벡터의 아날로그 구현

벡터의 멤버 변수 소개

시뮬레이션 구현 문자열에서 다음 세 개의 멤버 변수를 정의하고 벡터
여기에 이미지 설명 삽입
에도 세 개의 멤버 _start변수 가 있습니다 . 그들의 유형은 다음과 같은 반복자입니다. 그들과 문자열의 세 멤버 변수 사이의 해당 관계는 그림에 표시됩니다._finish_endofstorage

여기에 이미지 설명 삽입

여기에 이미지 설명 삽입

벡터 시뮬레이션 구현 인터페이스 기능 미리보기

namespace cl
{
    
    
	//模拟实现vector
	template<class T>
	class vector
	{
    
    
	public:
		typedef T* iterator;
		typedef const T* const_iterator;
		
		//迭代器相关函数
		iterator begin();
		iterator end();
		const_iterator begin()const;
		const_iterator end()const;

		//访问容器相关函数
		T& operator[](size_t i);
		const T& operator[](size_t i)const;

		//默认成员函数
		vector();                                           //构造函数
		vector(int n, const T& val = T())					//构造函数
		vector(size_t n, const T& val = T());               //构造函数
		template<class InputIterator>                      
		vector(InputIterator first, InputIterator last);    //构造函数
		
		vector(const vector<T>& v);                         //拷贝构造函数
		
		vector<T>& operator=(const vector<T>& v);           //赋值运算符重载函数
		~vector();                                          //析构函数

		//容量和大小相关函数
		size_t size()const;
		size_t capacity()const;
		void reserve(size_t n);
		void resize(size_t n, const T& val = T());
		bool empty()const;

		//修改容器内容相关函数
		void push_back(const T& x);
		void pop_back();
		iterator insert(iterator pos, const T& x);
		iterator erase(iterator pos);
		void swap(vector<T>& v);
		void clear()


	private:
		iterator _start;        //指向容器的头
		iterator _finish;       //指向有效数据的尾
		iterator _endofstorage; //指向容器的尾
	};
}

참고: 표준 라이브러리의 벡터와 이름 충돌을 방지하려면 시뮬레이션 구현을 자체 네임스페이스에 배치해야 합니다.

기본 멤버 함수

생성자 1

벡터는 먼저 인수 없는 생성자를 지원하는데, 이 인수 없는 생성자의 경우 생성된 객체의 세 가지 멤버 변수를 널 포인터로 직접 설정할 수 있습니다.

vector()
	:_start(nullptr)
	, _finish(nullptr)
	, _endofstorage(nullptr)
{
    
    }

생성자 2

둘째, 벡터는 반복자 범위를 사용하여 개체 생성도 지원합니다. 반복자 범위는 다른 컨테이너의 반복자 범위가 될 수 있기 때문에, 즉 함수가 받는 반복자의 유형이 불확실하므로 여기서는 생성자를 함수 템플릿으로 설계하고 데이터를 반복자 범위는 컨테이너에 하나씩 삽입할 수 있습니다.

template <class InputIterator>
vector(InputIterator first, InputIterator last)
	:_start(nullptr)
	, _finish(nullptr)
	, _endofstorage(nullptr)
{
    
    
	//将迭代器区间在[first,last)的数据一个个尾插到容器当中
	while (first != last)
	{
    
    
		push_back(*first);
		++first;
	}
}

생성자 3

또한 벡터는 값이 val인 n 데이터를 포함하는 컨테이너 구성도 지원합니다. 이 생성자의 경우 먼저 reserve 함수를 사용하여 컨테이너 용량을 n으로 설정한 다음 push_back 함수를 사용하여 값이 val인 n 데이터를 컨테이너에 삽입할 수 있습니다.

//构造
vector(size_t n, const T& val = T())
	:_start(nullptr)
	, _finish(nullptr)
	, _endofstorage(nullptr)
{
    
    
	reserve(n);//调用reserve函数将容器容量设置为n
	for (size_t i = 0; i < n; ++i)
	{
    
    
		push_back(val);//尾插n个值为val的数据到容器当中
	}
}

알아채다:

  1. 생성자는 n개의 데이터를 저장하기 위한 공간이 필요하다는 것을 알고 있으므로, push_back 함수를 호출할 때 용량을 여러 번 늘릴 필요가 없도록 예약 기능을 사용하여 한 번에 좋은 공간을 여는 것이 가장 좋습니다. .
  2. 생성자는 또한 오버로드된 함수를 구현해야 합니다.

이 생성자의 오버로드된 함수를 구현해야 하는 이유는 무엇입니까? 이유는 다음과 같습니다.

모의 구현 생성자 호출이 모호합니다.

1. 문제 설명

vector(size_t n, const T& val = T())//这里的形参用size_t就会引发这两个构造函数调用问题
    :_start(nullptr)
    , _finish(nullptr)
    , _endofstorage(nullptr)
{
    
    
    reserve(n);
    for (size_t i = 0; i < n; ++i)
    {
    
    
        push_back(val);
    }
}
 
template <class InputIterator>
vector(InputIterator first, InputIterator last)
    :_start(nullptr)
    , _finish(nullptr)
    , _endofstorage(nullptr)
{
    
    
    while (first != last)
    {
    
    
        push_back(*first);
        ++first;
    }
}

여기에 이미지 설명 삽입
시공을 위해 5개의 6을 사용하는 첫 번째 공법을 사용하려는 의도입니다. 컴파일러는 형식 매개 변수를 기반으로 가장 근접하게 일치하는 함수 오버로드를 호출합니다.
세 번째 생성자의 첫 번째 형식 매개변수는 size_t이며 형식 매개변수와 일치하려면 암시적 형식 변환이 필요합니다.

그러나이 두 매개 변수는 두 번째 생성자에 더 적합합니다 (두 번째 템플릿은 정확히 일치하는 int 일 수 있기 때문에). 두 번째 생성자가 사용되면 생성자 내부가 먼저 역 참조되므로 컴파일러는 불법 간접 주소 지정 (역참조) 오류가 보고됩니다.

2. 모호한 호출을 해결하는 방법은
생성자 벡터(size_t n, const T& val = T())에 대한 것이며 벡터(int n, const T& val = T()) 버전을 오버로드하여 해결할 수 있습니다. 생성자 문제.

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

복사 생성자

벡터의 복사 생성자는 딥 카피 문제를 수반하는데 딥 카피를 작성하는 두 가지 방법은 다음과 같습니다.

쓰기 방법 1: 전통적인 쓰기 방법
복사 구성의 전통적인 쓰기 방법의 아이디어는 우리가 생각하기 가장 쉽습니다. 먼저 컨테이너와 같은 크기의 공간을 연 다음 컨테이너의 데이터를 하나씩 복사합니다. , 마지막으로 업데이트 _finish 및 _ endofstorage 값이 충분합니다.

//传统写法
vector(const vector<T>& v)
	:_start(nullptr)
	,_finish(nullptr)
	,_endofstorage(nullptr)
{
    
    
	//开辟一块和容器v大小相同的空间
	_start = new T[v.capcity()];
	for (size_t i = 0; i < v.size(); ++i)
	{
    
    
		_start[i] = v[i];
	}
	_finish = _start + v.size(); //容器有效数据的尾
	_endofstorage = _start + v.capacity(); //整个容器的尾
}

쓰기 방법 2: 현대 쓰기
방법 복사 생성자의 현대 쓰기 방법도 비교적 간단합니다.먼저 tmp를 구성한 다음 둘을 교환합니다.

//拷贝构造
vector(const vector<T>& v)
	:_start(nullptr)
	, _finish(nullptr)
	, _endofstorage(nullptr)
{
    
    
	vector<T> tmp(v.begin(), v.end());//利用v构造tmp
	swap(tmp);//交换
}

참고: 복사 생성자를 작성하는 현대적인 방법도 깊은 복사입니다.먼저 두 번째 유형의 생성자를 호출하여 새 객체를 생성한 다음 복사 생성자에서 생성된 새 객체를 lvalue로 교환합니다. .

다른 작성 방법:

vector(const vector<T>& v)
	:_start(nullptr)
	, _finish(nullptr)
	, _endofstorage(nullptr)
{
    
    
	reserve(v.capacity()); //调用reserve函数将容器容量设置为与v相同
	for (const auto& e : v)
	{
    
    
		push_back(e); //将容器v当中的数据一个个尾插过来
	}
}

할당 연산자 오버로드 함수

물론 벡터의 대입 연산자 오버로딩도 깊은 복사 문제를 수반하며 여기에서 깊은 복사를 작성하는 두 가지 방법을 제공합니다.

쓰기 방법 1: 전통적인 쓰기 방법
자신에게 값을 대입하는 것인지 먼저 판단하고 자신에게 값을 대입하면 별도의 조작이 필요하지 않습니다. 자신에게 값을 할당하지 않으면 먼저 컨테이너 v와 같은 크기의 공간을 연 다음 컨테이너 v의 데이터를 하나씩 복사하고 마지막으로 _finish 및 _endofstorage 값을 업데이트합니다.

//传统写法
vector<T>& operator=(const vector<T>& v)
{
    
    
	if (this != &v) //防止自己给自己赋值
	{
    
    
		delete[] _start; //释放原来的空间
		_start = new T[v.capacity()]; //开辟一块和容器v大小相同的空间
		for (size_t i = 0; i < v.size(); i++) //将容器v当中的数据一个个拷贝过来
		{
    
    
			_start[i] = v[i];
		}
		_finish = _start + v.size(); //容器有效数据的尾
		_endofstorage = _start + v.capacity(); //整个容器的尾
	}
	return *this; //支持连续赋值
}

쓰기 방법 2: 현대 쓰기
방법 대입 연산자 오버로딩의 현대 쓰기 방법은 매우 예리합니다.우선 매개 변수를 전달할 때 매개 변수를 전달하기 위해 참조를 사용하지 않습니다. 벡터의 복사 생성자를 간접적으로 호출할 수 있기 때문입니다. lvalue( this)를 사용하여 이 복사본으로 구성된 컨테이너 v가 교환되며 이는 할당 작업을 완료하는 것과 동일하며 컨테이너 v는 함수 호출이 끝나면 자동으로 소멸됩니다.

vector<T>& operator=(vector<T> v)//编译器接收传参的时候自动调用其拷贝构造函数
{
    
    
	swap(v); //交换这两个对象
	return *this;//支持连续赋值
}

참고: 대입 연산자 오버로딩의 현대적 표현도 딥 카피, 호출된 벡터의 복사 생성자의 딥 카피에 불과합니다.

오물 소각로

컨테이너를 파괴할 때는 먼저 컨테이너가 빈 컨테이너인지 판단하여 비어 있으면 파괴 작업이 필요하지 않으며, 비어 있지 않으면 컨테이너가 데이터를 저장하는 공간을 먼저 해제한 다음 컨테이너의 각 멤버 변수를 설정합니다. 컨테이너를 그냥 널 포인터로.

~vector()
{
    
    
	delete[] _start;//释放容器存储数据的空间
	_start = _finish = _endofstorage = nullptr;
}

반복자 관련 함수

벡터의 반복자는 실제로 컨테이너에 저장된 데이터 유형에 대한 포인터입니다.

typedef T* iterator;
typedef const T* const_iterator;

시작과 끝

벡터의 시작 함수는 컨테이너의 첫 번째 주소를 반환하고 종료 함수는 컨테이너의 유효한 데이터의 다음 데이터 주소를 반환합니다.

iterator begin()
{
    
    
	return _start;
}

iterator end()
{
    
    
	return _finish;
}

또한 const 객체에 적용할 수 있는 시작 및 종료 함수 쌍을 오버로드해야 합니다. 그래야 const 객체가 시작 및 종료 함수를 호출할 때 얻은 반복자가 데이터를 읽을 수만 있고 수정할 수는 없습니다.

const_iterator begin() const
{
    
    
	return _start;
}

const_iterator end() const
{
    
    
	return _finish;
}

이쯤에서 벡터가 이터레이터를 사용하는 코드를 한눈에 살펴보자 실제로는 컨테이너를 순회하기 위해 포인터를 사용한다.

vector<int> v(5, 3);
vector<int>::iterator it = v.begin();
while (it != v.end())
{
    
    
	cout << *it << " ";
	it++;
}
cout << endl;

이제 반복자를 구현했으므로 실제로 범위를 사용하여 컨테이너를 탐색할 수 있습니다. 컴파일러가 컴파일할 때 자동으로 범위를 반복자 형식으로 대체하기 때문입니다.

vector<int> v(5, 3);
//范围for进行遍历
for (const auto& e : v)
{
    
    
	cout << e << " ";
}
cout << endl;

컨테이너 관련 기능에 액세스

운영자[ ]

벡터는 또한 "하첨자 + [ ]" 방법을 사용하여 컨테이너의 데이터에 액세스하고 구현 시 해당 위치에 데이터를 직접 반환하도록 지원합니다.

//可读可写
T& operator[](size_t pos)
{
    
    
	assert(pos < size());
	return _start[pos];
}
//只读
const T& operator[](size_t pos) const
{
    
    
	assert(pos < size());
	return _start[pos];
}

참고: [ ] 연산자를 오버로딩할 때 "첨자 + [ ]"를 통해 const 컨테이너에서 얻은 데이터는 읽기만 가능하고 데이터 수정은 불가능하므로 const 컨테이너에 적합한 연산자를 오버로드해야 합니다.

용량 및 크기 관련 기능

크기와 용량

벡터에서 세 멤버의 각각의 포인터를 순회함으로써 유효한 데이터의 수와 현재 컨테이너의 최대 용량을 쉽게 얻을 수 있습니다.
여기에 이미지 설명 삽입

구간은 왼쪽이 닫히고 오른쪽이 열리므로 두 포인터를 뺀 결과는 두 포인터 사이에 해당하는 타입의 데이터 개수이므로 크기는 로, 용량은 로 구할 수 _finish - _start있다 _endofstorage - _start. .

size_t size() const
{
    
    
	return _finish - _start;//返回容器当中有效数据的个数
}

size_t capacity() const
{
    
    
	return _endofstorage - _start;//返回当前容器的最大容量
}

예약하다

예약 규칙:
 1. n이 개체의 현재 용량보다 크면 용량을 n 또는 n보다 크게 확장합니다.
 2. n이 개체의 현재 용량보다 작으면 아무 작업도 수행하지 않습니다.

예비 기능의 구현 아이디어도 매우 간단합니다.먼저 주어진 n이 현재 컨테이너의 최대 용량보다 큰지 판단하고(그렇지 않으면 작업이 필요하지 않음) n을 수용할 수 있는 공간을 직접 엽니다. 이 공간에 데이터를 복사한 후 원래 컨테이너가 데이터를 저장한 공간은 해제하고 새로 열린 공간은 유지 보수를 위해 컨테이너로 넘겨줍니다. .컨테이너에 있는 각 멤버 변수의 값을 업데이트하는 것이 가장 좋습니다.

void reserve(size_t n)
{
    
    
	if (n > capacity())//判断是否需要进行操作
	{
    
    
		size_t oldSize = size();//记录当前容器当中有效数据的个数
		T* tmp = new T[n]; //开辟一块可以容纳n个数据的空间

		if (_start)//判断是否为空容器
		{
    
    
			for (size_t i = 0; i < oldSize; ++i)
			{
    
    
				tmp[i] = _start[i]; //调用赋值运算符重载完成深拷贝
			}

			delete[] _start;
		}

		_start = tmp;
		_finish = tmp + oldSize;
		_endofstorage = _start + n;
	}
}

예약 기능 구현 시 주의할 점이 두 가지 있다.
1) 연산을 수행하기 전에 현재 컨테이너에 유효한 데이터 개수를 미리 기록해 두어야 한다.
끝에서 _finish 포인터의 포인트를 업데이트해야 하고 _finish 포인터의 포인트는 컨테이너의 유효한 데이터 수에 _start 포인터를 더한 값과 같기 때문에 _start 포인터의 포인트가 변경되면 호출합니다. _finish - _start를 통해 계산하는 크기 함수 유효 데이터 출력 개수는 임의의 값입니다.
여기에 이미지 설명 삽입

2) 컨테이너의 데이터를 복사할 때 memcpy 기능을 사용하여 복사할 수 없습니다.

타입이 vector에 저장될 때 vector<int>, memcpy 함수에 의해 예약된 컨테이너와 vector<int>원래 컨테이너의 각 해당 멤버가 동일한 문자열 공간을 가리키지만 원래 컨테이너에 데이터를 저장하기 위한 공간이 예약되지 않았다고 생각할 수 있습니다. 릴리스되면 이러한 문자열 공간을 유지하는 하나의 컨테이너와 동일하므로 이것의 영향은 무엇입니까?
하지만 원래 컨테이너 공간을 해제하면 원래 컨테이너에 저장된 각 소멸자가 vector<int>해제될 때 호출되고 vector<int>그것이 가리키는 문자열도 해제되므로 memcpy 함수를 사용하여 컨테이너를 예약하십시오. 각각이 가리키는 문자열 vector<int>은 실제로는 해제된 공간의 조각이며 컨테이너에 액세스하는 것은 메모리 공간에 대한 불법적인 액세스입니다.
여기에 이미지 설명 삽입
따라서 우리는 여전히 for 루프를 사용하여 컨테이너에 vector<int>하나씩 값을 할당 해야 합니다. vector<int>간접 호출이 가능한 할당 연산자를 오버로드하여 vector<int>딥 카피를 달성할 수 있기 때문입니다.

여기에 이미지 설명 삽입

크기 조정

크기 조정 규칙:
 1. n이 현재 크기보다 크면 크기를 n으로 확장하고 확장된 데이터는 val입니다. .
 2. n이 현재 크기보다 작으면 크기를 n으로 줄입니다.

resize 함수의 규칙에 따라 우리는 함수에 들어갈 때 주어진 n이 컨테이너의 현재 크기보다 작은지 먼저 판단하여 증가시킬 필요가 있는지 판단한 다음 확장된 데이터를 val에 할당할 수 있습니다.

비어 있는

empty 함수는 컨테이너에서 _start와 _finish의 포인터를 비교하여 컨테이너가 비어 있는지 직접 판단할 수 있으며, _finish가 가리키는 위치와 _start가 가리키는 위치가 같으면 컨테이너가 비어 있는 것입니다.

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

컨테이너 콘텐츠 관련 기능 수정

푸시백

데이터를 End-insert 하기 위해서는 먼저 컨테이너가 꽉 찼는지 확인해야 하며, 꽉 찼다면 먼저 용량을 늘린 후 _finish가 가리키는 위치에 데이터를 end-insert하고 _finish++가 가리키는 위치에 데이터를 end-insert해야 합니다.

void push_back(const T& x)
{
    
    
	if (_finish == _endofstorage)//判断是否需要增容
	{
    
    
		size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;//将容量扩大为原来的两倍
		reserve(newCapacity);//增容
	}

	*_finish = x; //尾插数据
	++_finish;//_finish指针后移
}

팝백

데이터 꼬리를 삭제하기 전에 컨테이너가 비어 있는지 여부도 판단해야 합니다. 비어 있으면 어설션을 만들고 비어 있지 않으면 괜찮습니다 _finish--.

//尾删数据
void pop_back()
{
    
    
	assert(!empty()); //容器为空则断言
	_finish--; //_finish指针前移
}

끼워 넣다

insert 함수는 주어진 iterator의 pos 위치에 데이터를 삽입할 수 있으며, 데이터를 삽입하기 전에 용량을 늘릴 필요가 있는지 판단한 후 pos 위치와 후속 데이터를 한 비트 뒤로 이동시켜 pos 위치를 남깁니다. 마지막으로 데이터를 pos 위치에 삽입할 수 있습니다.

iterator insert(iterator pos, const T& val)
{
    
    
	assert(pos >= _start);
	assert(pos < _finish);

	if (_finish == _endofstorage)
	{
    
    
		size_t len = pos - _start;//记录pos与_start之间的间隔
		size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
		reserve(newCapacity);

		// 扩容会导致pos迭代器失效,需要更新处理一下
		pos = _start + len;
	}

	// 挪动数据
	iterator end = _finish - 1;
	while (end >= pos)
	{
    
    
		*(end + 1) = *end;
		--end;
	}

	*pos = val;
	++_finish;

	return pos;
}

참고: 용량을 늘려야 하는 경우 용량을 늘리기 전에 pos와 _start 사이의 간격을 기록한 다음 이 간격을 사용하여 용량 증가 후 컨테이너에서 pos의 포인팅을 결정해야 합니다. 그렇지 않으면 pos는 여전히 원래 해제된 공간을 가리킵니다. .

삭제

지우기 기능은 주어진 iterator의 pos 위치에서 데이터를 삭제할 수 있으며, 데이터를 삭제하기 전에 컨테이너가 비어 있는지, pos 위치가 범위 내에 있는지 판단해야 합니다. pos 위치의 데이터.

iterator erase(iterator pos)
{
    
    
	assert(!empty()); 
	assert(pos >= _start);
	assert(pos < _finish);

	iterator it = pos + 1;
	while (it < _finish)
	{
    
    
		*(it- 1) = *(it);
		++it;
	}

	--_finish;//数据个数减少一个,_finish前移

	return pos;
}

교환

swap 함수는 두 컨테이너의 데이터를 교환하는데 사용되며 라이브러리에서 직접 swap 함수를 호출하여 두 컨테이너의 멤버 변수를 교환할 수 있습니다.

void swap(vector<T>& v)
{
    
    
	std::swap(_start, v._start);
	std::swap(_finish, v._finish);
	std::swap(_endofstorage, v._endofstorage);
}

참고: 여기에서 라이브러리에서 스왑을 호출하려면 스왑 전에 "::"(범위 한정자)를 추가하여 지정된 라이브러리에서 스왑 함수를 찾도록 컴파일러에 지시해야 합니다. 스왑 기능(근접성 원칙)을 구현하는 것입니다.

요약하다

벡터의 전체 코드 구현 시뮬레이션

namespace wyt
{
    
    
	template<class T>
	class vector
	{
    
    
	public:
		typedef T* iterator;
		typedef const T* const_iterator;

		iterator begin()
		{
    
    
			return _start;
		}

		iterator end()
		{
    
    
			return _finish;
		}

		const_iterator begin() const
		{
    
    
			return _start;
		}

		const_iterator end() const
		{
    
    
			return _finish;
		}

		T& operator[](size_t pos)
		{
    
    
			assert(pos < size());
			return _start[pos];
		}

		const T& operator[](size_t pos) const
		{
    
    
			assert(pos < size());
			return _start[pos];
		}
		//构造
		vector()
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
    
    }

		// v2(v1)
		/*vector(const vector<T>& v)
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
			reserve(v.capacity());
			for (const auto& e : v)
			{
				push_back(e);
			}
		}*/

		//vector<int> v1(10, 1);
		//vector<char> v1(10, 'A');
		//构造
		vector(int n, const T& val = T())
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
    
    
			reserve(n);
			for (int i = 0; i < n; ++i)
			{
    
    
				push_back(val);
			}
		}
		//构造
		vector(size_t n, const T& val = T())
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
    
    
			reserve(n);
			for (size_t i = 0; i < n; ++i)
			{
    
    
				push_back(val);
			}
		}
		//构造
		template <class InputIterator>
		vector(InputIterator first, InputIterator last)
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
    
    
			while (first != last)
			{
    
    
				push_back(*first);
				++first;
			}
		}

		//拷贝构造 -- 现代写法
		vector(const vector<T>& v)
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
    
    
			vector<T> tmp(v.begin(), v.end());
			swap(tmp);
		}
		//传统写法
		//vector(const vector<T>& v)
		//	:_start(nullptr)
		//	,_finish(nullptr)
		//	,_endofstorage(nullptr)
		//{
    
    
		//	//开辟一块和容器v大小相同的空间
		//	_start = new T[v.capcity()];
		//	for (size_t i = 0; i < v.size(); ++i)
		//	{
    
    
		//		_start[i] = v[i];
		//	}
		//	_finish = _start + v.size(); //容器有效数据的尾
		//	_endofstorage = _start + v.capacity(); //整个容器的尾
		//}
		

		//赋值运算符重载
		// v1 = v2
		// v1 = v1;  // 极少数情况,能保证正确性,所以这里就这样写没什么问题
		vector<T>& operator=(vector<T> v)
		{
    
    
			swap(v);
			return *this;
		}
		//传统写法
		//vector<T>& operator=(const vector<T>& v)
		//{
    
    
		//	if (this != &v) //防止自己给自己赋值
		//	{
    
    
		//		delete[] _start; //释放原来的空间
		//		_start = new T[v.capacity()]; //开辟一块和容器v大小相同的空间
		//		for (size_t i = 0; i < v.size(); i++) //将容器v当中的数据一个个拷贝过来
		//		{
    
    
		//			_start[i] = v[i];
		//		}
		//		_finish = _start + v.size(); //容器有效数据的尾
		//		_endofstorage = _start + v.capacity(); //整个容器的尾
		//	}
		//	return *this; //支持连续赋值
		//}
		~vector()
		{
    
    
			delete[] _start;
			_start = _finish = _endofstorage = nullptr;
		}

		
		void reserve(size_t n)
		{
    
    
			if (n > capacity())
			{
    
    
				size_t oldSize = size();
				T* tmp = new T[n];

				if (_start)
				{
    
    
					//memcpy(tmp, _start, sizeof(T)*oldSize);//memcpy是按字节拷贝,如果T是vector<T>类型,就会发生浅拷贝
					for (size_t i = 0; i < oldSize; ++i)
					{
    
    
						tmp[i] = _start[i];
					}

					delete[] _start;
				}

				_start = tmp;
				_finish = tmp + oldSize;
				_endofstorage = _start + n;
			}
		}

		void resize(size_t n, T val = T())
		{
    
    
			if (n > capacity())
			{
    
    
				reserve(n);
			}

			if (n > size())
			{
    
    
				//reserve(n);
				while (_finish < _start + n)
				{
    
    
					*_finish = val;
					++_finish;
				}
			}
			else
			{
    
    
				_finish = _start + n;
			}
		}

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

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

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

		void push_back(const T& x)
		{
    
    
			if (_finish == _endofstorage)
			{
    
    
				size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newCapacity);
			}

			*_finish = x;
			++_finish;
		}

		void pop_back()
		{
    
    
			assert(!empty());

			--_finish;
		}

		// 迭代器失效 : 扩容引起,野指针问题
		iterator insert(iterator pos, const T& val)
		{
    
    
			assert(pos >= _start);
			assert(pos < _finish);

			if (_finish == _endofstorage)
			{
    
    
				size_t len = pos - _start;
				size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newCapacity);

				// 扩容会导致pos迭代器失效,需要更新处理一下
				pos = _start + len;
			}

			// 挪动数据
			iterator end = _finish - 1;
			while (end >= pos)
			{
    
    
				*(end + 1) = *end;
				--end;
			}

			*pos = val;
			++_finish;

			return pos;
		}

		iterator erase(iterator pos)
		{
    
    
			assert(pos >= _start);
			assert(pos < _finish);


			iterator begin = pos + 1;
			while (begin < _finish)
			{
    
    
				*(begin - 1) = *(begin);
				++begin;
			}

			--_finish;

			return pos;
		}

		void swap(vector<T>& v)
		{
    
    
			std::swap(_start, v._start);
			std::swap(_finish, v._finish);
			std::swap(_endofstorage, v._endofstorage);
		}

		void clear()
		{
    
    
			_finish = _start;
		}

	private:
		iterator _start;
		iterator _finish;
		iterator _endofstorage;
	};
}

추천

출처blog.csdn.net/m0_58124165/article/details/130040454