MyTinySTL项目学习01——Vector实现
知识点
预处理指令的使用
-
#define 标识符 代码或者值:用于定义一个宏常量,比如#define MAX 1200即将MAX常量定义为1200,注意这里没有类型这一说法,本质是字符串替换; -
#ifdef 标识符 \n 执行代码 \n #endif:如果经过#define定义了该标识符,则执行中间的代码; -
#undef 标识符:取消#define定义的标识符;取消已经定义的宏;使得该宏在后续的代码中不再起作用; -
#pragma:指示编译器执行特定的操作,不同编译器的支持可能不同,功能丰富;-
#pragma message("#undefing marco max"):在编译时显示自定义消息; -
#pragma once:指定头文件只被编译一次;替代传统的头文件保护宏;-
// 传统的头文件保护宏: #ifndef HEADER_NAME_H #define HEADER_NAME_H // 头文件内容 #endif
-
-
-
#ifndef 标识符 \n 代码 \n #endif:如果没有定义标识符宏,则执行代码;
静态断言
静态断言static_assert,在编译时对常量内容进行断言检查;普通断言是在运行时才进行断言检查;普通断言返回运行错误,并结束运行;静态断言返回编译错误;
static_assert(expression, message);
static_assert(!std::is_same<bool, T>::value, "vector<bool> is abandoned in mystl");
expression是一个表达式,返回一个布尔值;message是自定义错误消息;
std::is_same<bool, T> 是一个类(或模板类)用于比较T的类型是否和bool相同,然后将比较结果放在内部的value静态变量中,通过::作用域解析运算符来获取value;
作用域解析运算符
::是作用域解析运算符,有两种常见用法:
std::cout:用于获取命名空间std下的cout对象(方法);class::number:在类的内部或者外部访问类的静态成员或者嵌套类型;
嵌套类型
如果在类里面又定义了类、结构体、枚举类型、typedef等,里面定义的那个类型被称为嵌套类型;
例如:
#include <iostream>
using namespace std;
class OuterClass {
public:
class InnerClass {
public:
static int value;
int data;
};
};
int OuterClass::InnerClass::value = 42;
int main () {
OuterClass::InnerClass obj;
obj.data = 10;
// int OuterClass::InnerClass::value = 42;
int value = OuterClass::InnerClass::value; // 不是通过实例访问,而是通过类访问静态变量
cout << obj.data << " " << value << endl;
cin.ignore();
return 0;
}
访问嵌套类型时,通过作用域解析运算符::实现;访问静态变量也是用::;(静态成员变量是属于类本身的,而不是属于类的实例。因此,静态成员变量只能在类的外部进行初始化,而不能在 main 函数或其他成员函数中初始化。)
typedef和typename区别作用
typedef:定义了一个类的别名;
typename:定义一个标识符是类型而不是变量(在模板编程中必须有)
template <typename T>
void foo() {
T::type myVar; // 这里编译器无法确定 T::type 是类型还是变量
}
template <typename T>
void foo() {
typename T::type myVar; // 使用typename明确告诉编译器T::type是一个类型
}
由于模板类编程中,类型名也是我们自定义的,变量名也是自定义的,所以编译器很难区分,比如int i;编译器很明显直到int是类型名,i是变量名,可如果在模板编程中,自定义了类型,inti i,如果不做标识,很难知道inti和i是类型名还是变量名;
所以如果给类起别名用typedef,给类里的变量类型起别名使用typedef typename联合使用;
避免隐式类型转换
explicit关键字:修饰单参数构造函数,防止编译器进行隐式类型转换;
class MyClass {
public:
explicit MyClass(int x) : value(x) {
}
private:
int value;
};
MyClass obj = 10; // 编译错误,禁止隐式类型转换
MyClass obj2(20); // 正确,显式调用构造函数
如果在C++中的类定义中没有使用 explicit 关键字来声明构造函数,则在执行 MyClass obj = 10; 这段代码时会发生隐式转换。
- 编译器会查找适合将
int类型的值 10 转换为MyClass类型的构造函数。如果找到了一个可以执行单参数构造函数的构造函数,编译器就会将这个构造函数用于将 10 转换为MyClass类型的临时对象。 - 然后,编译器将创建一个临时对象,将值 10 传递给该构造函数,创建一个临时的
MyClass对象。 - 最后,编译器将使用拷贝构造函数或者移动构造函数(如果有的话)将临时对象的内容复制或移动到
obj对象中。
所以只对于单参数的构造函数在声明时要加explicit防止隐式类型转换;
左值引用和右值引用
- 左值引用(
lvalue reference):- 左值引用是通过
&符号声明的引用,例如int& ref = value;。 - 左值引用主要用于引用左值(可寻址、具有标识性的值)。
- 左值引用可以绑定到左值和
const左值,但不能绑定到非const右值。 - 通过左值引用,可以对引用的对象进行读写操作。
- 通常用于传递可修改的对象,如函数参数,以减少拷贝开销。
- 左值引用是通过
- 右值引用(
rvalue reference):- 右值引用是通过
&&符号声明的引用,例如int&& ref = 10;。 - 右值引用主要用于引用右值(临时、不具有标识性的值)。
- 右值引用可以绑定到非
const右值,但不能绑定到左值或const右值。 - 通过右值引用,可以在语义上支持移动语义和完美转发。
- 通常用于实现移动构造函数和移动赋值运算符,以提高性能和避免不必要的拷贝操作。
- 右值引用是通过
在构造函数中,左值引用实现了拷贝构造函数,右值引用实现了移动构造函数(用右值引用实现完美转发,避免不必要的拷贝操作)
// 拷贝构造函数
vector(const vector& rhs)
{
range_init(rhs.begin_, rhs.end_);
}
// 移动构造函数
vector(vector&& rhs) noexcept
:begin_(rhs.begin_), // 使用列表初始化 vector(int x) : value(x) {}
end_(rhs.end_),
cap_(rhs.cap_)
{
rhs.begin_ = nullptr;
rhs.end_ = nullptr;
rhs.cap_ = nullptr;
}
拷贝构造函数,传入左值引用,并且声明为const,然后获取到成员变量(vector的起始指针和结束指针),按照成员变量标识的空间来初始化;
移动构造函数,传入右值引用,将成员变量(新数组的起始指针、结束指针、容量)直接赋值为传入数组的成员变量,然后让传入数组成员变量指针变为nullptr;移动语义
在C++中,引入移动语义的目的是为了提高程序的性能和效率。当你需要传递一个临时对象(右值)并且不再需要该对象的内容时,使用移动语义可以避免不必要的内存复制和分配,从而提高程序的性能。
当你传递一个左值参数(通过传值或引用方式)时,会触发拷贝构造函数,从而导致数据的复制,这可能是昂贵且不必要的。但是,通过使用 std::move() 将对象转换为右值引用,可以启用移动构造函数或移动赋值运算符,这样可以直接从源对象“窃取”资源而不是复制,从而提高性能。
因此,在涉及到临时对象的传递或对象所有权转移时,推荐使用 std::move() 来启用移动语义,以减少不必要的资源消耗和提高程序性能。
实现思想
vector实现
自定义命名空间如何实现?
namespace mystl {
// 所有类的定义都写在里面
}
vector的begin_和end_和cap_分别代表什么?
- begin_:
begin_表示指向 vector 中第一个元素的指针。- 通过
begin_可以访问 vector 中的第一个元素。 - 通常用于遍历 vector 中的元素,可以通过
*begin_来获取第一个元素的值。
- end_:
end_表示指向 vector 中最后一个元素的下一个位置的指针。- 通常指向的是一个“越界”的位置,即不属于 vector 的范围。
- 在迭代或遍历 vector 时,
end_用于判断是否到达 vector 的末尾。
- cap_:
cap_表示 vector 内部存储空间的大小,即 vector 的容量。- 容量是指 vector 分配的内存空间能容纳的元素数量,而非当前 vector 中实际存储的元素数量。
- 当 vector 的元素数量达到容量时,会触发内部的重新分配,分配更大的存储空间。
private:
iterator begin_; // 表示目前使用空间的头部
iterator end_; // 表示目前使用空间的尾部
iterator cap_; // 表示目前储存空间的尾部
成员变量一般在命名时:末尾加_或者开头加m_,来区分普通变量;
为什么要定义max和min宏,为什么定义之后又要取消定义?
// #define max
#ifdef max
#pragma message("#undefing marco max")
#undef max // 取消已经定义的宏
#endif // max
#ifdef min
#pragma message("#undefing marco min")
#undef min
#endif // min
因为max和min是常用的函数,如果被定义为宏,在后面的vector各种实现中,标准库中的max和min函数都不会被正常执行,反而会被替换为对应的宏;
所以对于vector这种标准库代码时,为了保证执行的正确性和可移植性,就会在开头取消某些可能造成影响的宏定义;
为什么在模板类中要加静态断言来判断bool类型?
static_assert(!std::is_same<bool, T>::value, "vector<bool> is abandoned in mystl");
因为在vector<bool>中,一般使用压缩存储,比如一个Bit代表一个Bool值,所以无法直接取某个元素的地址,处理方法和其他vector<T>有所不同;所以在定义时,需要单独处理;
什么是迭代器?迭代器的本质是什么?
typedef mystl::allocator<T> allocator_type;
typedef typename allocator_type::value_type value_type;
typedef value_type* iterator;
typedef const value_type* const_iterator;
迭代器的本质就是一个指针,所以使用迭代器需要解引用才能访问到对应元素;
几种构造函数及其具体实现方式?
无参构造函数(默认构造函数)
vector () noexcept {
try_init(); }
// try_init 函数,若分配失败则忽略,不抛出异常
template <class T>
void vector<T>::try_init() noexcept
{
try
{
begin_ = data_allocator::allocate(16); // 分配容量为16的空间,返回空间起始位置迭代器
end_ = begin_; // 开始时,数组size为0
cap_ = begin_ + 16; // 容量更新为16
}
catch (...)
{
// 分配失败,则指针全部变为nullptr,避免野指针
begin_ = nullptr;
end_ = nullptr;
cap_ = nullptr;
}
}
noexcept用于是否抛出异常;对于一定不会抛出异常的函数来说,加上noexcept可以避免处理异常的代码,提高执行效率;
try-catch语法块:
try块中放置可能会抛出异常的代码。- 如果在
try块中的代码抛出异常,程序会立即跳转到catch块进行异常处理。 catch块被用于捕获并处理不同类型的异常,可以有多个catch块,每个匹配不同类型的异常。- 在
catch块中,可以对捕获到的异常进行处理,比如输出错误信息或者执行特定操作。 - 最后一个
catch (...)是用来处理所有其他未被前面catch块捕获的异常。
有参构造函数
explicit vector (size_type) {
fill_init(n, value_type());}
mystl::vector<int> vec(10); // 调用方式
vector (size_type n, const value_type& value) {
fill_init(n, value);}
mystl::vector<int> vec(10, 0);
// fill_init 函数
template <class T>
void vector<T>::
fill_init(size_type n, const value_type& value)
{
const size_type init_size = mystl::max(static_cast<size_type>(16), n); // 使用了max,所以开始要去除max宏,最少分配16个元素大小的空间
init_space(n, init_size);
mystl::uninitialized_fill_n(begin_, n, value);
}
// init_space 函数
template <class T> void vector<T>::init_space(size_type size, size_type cap)
{
try
{
begin_ = data_allocator::allocate(cap);
end_ = begin_ + size;
cap_ = begin_ + cap;
}
catch (...)
{

最低0.47元/天 解锁文章
1372

被折叠的 条评论
为什么被折叠?



