MyTinySTL项目学习01——Vector实现

MyTinySTL项目之Vector实现学习

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

作用域解析运算符

::是作用域解析运算符,有两种常见用法:

  1. std::cout:用于获取命名空间std下的cout对象(方法);
  2. 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,如果不做标识,很难知道intii是类型名还是变量名;

所以如果给类起别名用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; 这段代码时会发生隐式转换。

  1. 编译器会查找适合将 int 类型的值 10 转换为 MyClass 类型的构造函数。如果找到了一个可以执行单参数构造函数的构造函数,编译器就会将这个构造函数用于将 10 转换为 MyClass 类型的临时对象。
  2. 然后,编译器将创建一个临时对象,将值 10 传递给该构造函数,创建一个临时的 MyClass 对象。
  3. 最后,编译器将使用拷贝构造函数或者移动构造函数(如果有的话)将临时对象的内容复制或移动到 obj 对象中。

所以只对于单参数的构造函数在声明时要加explicit防止隐式类型转换;

左值引用和右值引用

  1. 左值引用(lvalue reference):
    • 左值引用是通过 & 符号声明的引用,例如 int& ref = value;
    • 左值引用主要用于引用左值(可寻址、具有标识性的值)。
    • 左值引用可以绑定到左值和 const 左值,但不能绑定到非 const 右值。
    • 通过左值引用,可以对引用的对象进行读写操作。
    • 通常用于传递可修改的对象,如函数参数,以减少拷贝开销。
  2. 右值引用(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_分别代表什么?
  1. begin_
    • begin_ 表示指向 vector 中第一个元素的指针。
    • 通过 begin_ 可以访问 vector 中的第一个元素。
    • 通常用于遍历 vector 中的元素,可以通过 *begin_ 来获取第一个元素的值。
  2. end_
    • end_ 表示指向 vector 中最后一个元素的下一个位置的指针。
    • 通常指向的是一个“越界”的位置,即不属于 vector 的范围。
    • 在迭代或遍历 vector 时,end_ 用于判断是否到达 vector 的末尾。
  3. 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

因为maxmin是常用的函数,如果被定义为宏,在后面的vector各种实现中,标准库中的maxmin函数都不会被正常执行,反而会被替换为对应的宏;

所以对于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 (...)
    {
   
   
    
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

OutlierLi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值