【C++】模拟实现string

文章目录

  • 前言
  • 成员变量
  • 成员函数
    • 构造函数
    • 拷贝构造函数
      • 浅拷贝
      • 深拷贝
      • 拷贝构造函数实现
    • 析构函数
    • 赋值重载
  • 空间管理函数
  • 元素访问
  • 元素修改
  • 字符串运算
  • 流提取 & 流插入
    • 流提取
    • 流插入
  • 迭代器
    • begin & end
    • rbegin & rend
  • 总结




前言


模拟实现不是为了写得和库里面一样好。而是为了更好的了解底层,从而能够更熟练的使用这些类,同时也能学习大佬们的代码风格。

在学习这章之前,需要对类与对象有一定的知识基础,如果对类与对象有些生疏的话,可以看看这篇文章:《类与对象》
string 类是 C++标准库中的一个重要类,用于表示字符串。

以下是一些关于 string 类的主要特点:

  1. 动态存储:可以自动管理内存,根据字符串的实际长度动态分配和释放内存。
  2. 丰富的操作:提供了很多方便的操作方法,如字符串连接、查找、比较、提取子串等。
  3. 高效性:在性能上表现较好,适用于各种字符串处理场景。


成员变量


在正式模拟实现之前,我们得要先确定我们要实现的类中的成员变量由哪些类型构成:

  1. 首先需要有一个字符指针,用于存储字符串。
  2. 再定义一个变量,用来记录字符串的有效个数。
  3. 再定义一个变量,用来记录该字符指针能存储的有效字符个数大小
  4. 再定义一个全局变量npos,来完成以下操作:
    • npos是一个静态成员常量值,具有size_t类型元素的最大可能值。
    • 当在string的成员函数中作为len(或sublen)参数的值时,该值表示“直到字符串结束”。
    • 作为返回值,它通常用于表示没有匹配。
    • 该常量定义为-1,由于size_t是无符号整型,因此它是该类型的最大可表示值。
class string
{private:char* _str = nullptr;  // 字符指针, 字符串用于存储字符串size_t _size = 0; // 用于记录该字符串有几个有效字符(即字符串的长度)size_t _capacity = 0; // 用于记录该字符指针能存储的有效字符个数大小// Member constants:const static size_t npos = -1;
};


成员函数


在这里插入图片描述

构造函数

空字符串构造函数(默认构造函数)
构造一个长度为0个字符的空字符串。

	string(const char* str = ""){size_t size = strlen(str);_size = _capacity = size;_str = new char[_capacity + 1];strcpy(_str, str);}

拷贝构造函数

这里需要先介绍两个概念:

浅拷贝

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

就像一个家庭中有两个孩子,但父母只买了一份玩具,两个孩子愿意一块玩,则万事大吉,万一不想分享就你争我夺,玩具损坏。在这里插入图片描述

深拷贝

在这里插入图片描述
可以采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享。父母给每个孩子都买一份玩具,各自玩各自的就不会有问题了。

拷贝构造函数实现

构造一个str的副本,这里因为涉及到资源管理的问题,所以拷贝构造时,我们需要重新创建一个空间,来存储str的内容。否则如果直接浅拷贝的话,会导致一下两个问题:

  1. 对其中一个字符串改变时,另一个字符串也会跟着被改变(因为它们管理的是同一片空间)
  2. 在调用析构函数时,同一份资源被释放两次
	string(const string& str){_str = new char[str._capacity + 1];strcpy(_str, str._str);_size = str._size;_capacity = str._capacity;}

string构造函数不止有这两个,如果想要去实现其他的,也不是很困难。只是这两个构造函数,在大部分的情况下就够用了。

析构函数

对于这些参与空间资源分配的类,我们都要自己写析构函数,否则很可能导致内存泄漏。

	~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}

赋值重载

为字符串赋一个新值,替换其当前内容(如果是两个相等的字符串就没必要相互赋值了)。

	string& operator=(const string& str){if (str != *this){delete[] _str;_str = new char[str._capacity + 1];strcpy(_str, str._str);_size = str._size;_capacity = str._capacity;}return *this;}

这里还有一种写法,我们可以复用拷贝构造函数:

	string& operator=(const string& str){if (str != this->_str){string tmp = str;swap(tmp);}return *this;}

在这里插入图片描述
当然这里的swap函数是需要我们自己实现的:

	void swap(string& str){std::swap(_str, str._str);std::swap(_size, str._size);std::swap(_capacity, str._capacity);}

这里我复用了std空间里面的swap函数,将string类中的成员变量都交换掉。


空间管理函数


在这里插入图片描述
这里只模拟实现我勾选的这些函数。
实现这些功能并不复杂,代码如下:

	size_t capacity() const{return _capacity;}size_t size() const{return _size;}void reserve(size_t n = 0){assert(_str);// 我实现的是只考虑扩容的,不考虑缩容if (_capacity < n){char* tmp = new char[n + 1]; // 开辟空间strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}void resize(size_t n, char c = '\0'){assert(_str);// 不考虑缩容的情况if (n > _capacity){reserve(n);for (size_t i = _size;i < n;++i)_str[i] = c;_str[_size = n] = '\0';}}void clear(){_str[_size = 0] = '\0';}bool empty() const{return _size == 0;}


元素访问

在这里插入图片描述

要实现[]重载并不复杂,但是需要写两个,一个是可读可写的,一个是只读的:

	char& operator[](size_t n){assert(n >= 0);assert(n < _size);return _str[n];}const char& operator[](size_t n) const{assert(n >= 0);assert(n < _size);return _str[n];}


元素修改

在这里插入图片描述

这里有些函数我是重载了几个版本的,具体实现细节我写进注释里了

	void push_back(char c){assert(_str);// 先判断是否需要扩容if (_size == _capacity){size_t newcapacity = _capacity == 0 ? 10 : 2 * _capacity;reserve(newcapacity);}_str[_size] = c;_str[++_size] = '\0'; // 增加字符以后不要忘记将_size增加}string& append(const string& str){assert(_str && str._str);// 先判断是否需要扩容size_t len = strlen(str._str);if (_size + len >= _capacity){size_t newcapacity = _size + len + 10; // 每次扩容都多增加是个容积的容错reserve(newcapacity);}memmove(_str + _size, str._str, len);_str[_size += len] = '\0';return *this;}string& operator+=(char c){push_back(c);return *this;}string& operator+=(const string& str){append(str);return *this;}string& insert(size_t pos, const string& str){assert(pos <= _size);size_t len = strlen(str._str);if (_size + len >= _capacity)reserve(_size + len);size_t end = _size + len;while (end - len + 1 > pos){_str[end] = _str[end - len];--end;}memmove(_str + pos, str._str, len);_size += len;return *this;}string& insert(size_t pos, char c){assert(pos <= _size);if (_size >= _capacity){size_t newcapacity = _capacity == 0 ? 10 : 2 * _capacity;reserve(newcapacity);}size_t end = _size + 1;while (end > pos){_str[end] = _str[end - 1];--end;}_str[pos] = c;++_size;return *this;}string& erase(size_t pos = 0, size_t len = npos){assert(pos < _size);if (len == npos || pos + len >= _size){_str[pos] = '\0';_size = pos;}else{size_t end = pos + len;while (end <= _size){_str[end - len] = _str[end];++end;}_size -= len;}return *this;}void swap(string& str){std::swap(_str, str._str);std::swap(_size, str._size);std::swap(_capacity, str._capacity);}


字符串运算


在这里插入图片描述

c_strdata的功能都是一样的。这两个函数是为了能够让string类兼容C语言中的一些函数。比如:我们使用C语言中的fopen函数打开一个文件名(文件名是一个常量字符串),而假设我们使用string类来管理文件名,这是,就需要这两个函数来将string类转换成常量字符串了。

	const char* c_str(){return _str;}const char* data(){return _str;}size_t find(const string& str, size_t pos = 0) const{char* tmp = strstr(_str + pos, str._str);if (tmp == nullptr) // 查找子串失败return npos;return tmp - _str; // 返回两个指针之间的间隔距离,即表示找到的子串的起始位置}size_t find(char c, size_t pos = 0) const{for (size_t i = 0;i < _size;++i)if (_str[i] == c) return i;return npos;}size_t rfind(char c, size_t pos = 0) const{for (size_t i = _size - 1;i != npos;--i)if (_str[i] == c) return i;return npos;}string substr(size_t pos = 0, size_t len = npos) const{assert(pos < _size);string str;if (len == npos || pos + len >= _size)len = _size;for (size_t i = pos;i < len;++i)str += _str[i];return str;}


流提取 & 流插入


流提取

因为在STL中string类的对象是可以直接使用cout打印出来的,如果我们也想让我们自己实现的string能够直接打印出来,就需要重载一下<<操作符,具体操作如下:

std::ostream& operator<<(std::ostream& out, const string& str)
{for (auto& e : str)out << e;return out;
}

流插入

STL中string类也是支持cin插入数据的,如果我们也想让我们自己实现的string能够使用cin插入数据,就需要重载一下>>操作符,具体操作如下:

std::istream& operator>>(std::istream& in, string& str)
{str.clear();char ch = in.get();while (ch != ' ' && ch != '\n'){str += ch;ch = in.get();}return in;
}

我们需要先将原string对象中的资源清理一下,再插入。这里是利用了istream类中的get()函数从输入缓冲区获取我们输入的字符,然后再使用我们在上面重载的+=操作符,来将我们输入的一组数据插入到一个string对象中。

但这里存在一个问题:
就是如果我们每次读取一个字符,再用+=这个操作符时,假设输入缓冲区中的数据量很大,那么必定存在很多次扩容,扩容还是会有一定的消耗的,所以我们使用一个字符数组来充当缓冲区,现将字符读入到这个数组中,待数组满了以后,我们再将这个数组中的数据插入到string对象中,这样就能有效的降低扩容的次数了

std::istream& operator>>(std::istream& in, string& str)
{str.clear();char buffer[128] = {};int count = 0;for (char ch = in.get();ch != ' ' && ch != '\n';ch = in.get()){buffer[count++] = ch;if (count == 127){str += buffer;count = 0;}}if (count > 0){buffer[count] = '\0';str += buffer;}return in;
}

这里还需要重载一个函数getline()获取一行字符串。因为>>操作符,遇到空格或者回车就结束读取了,如果我们需要读取一行字符串(里面可能含有空格),这是就需要使用getline()

std::istream& getline(std::istream& in, string& str)
{str.clear();char buffer[128] = {};int count = 0;for (char ch = in.get();ch != '\n';ch = in.get()){buffer[count++] = ch;if (count == 127){str += buffer;count = 0;}}if (count > 0){buffer[count] = '\0';str += buffer;}return in;
}


迭代器


迭代器的设计很巧妙的,不同的容器都能够通过相同的方式去访问,这就是迭代器的强大之处。下面就先通过string的迭代器带大家先了解一下迭代器,随着后面模拟实现其他容器,迭代器的结构也会相应的发生变化。

begin & end

在这里插入图片描述
这里只模拟实现我标记出来的那些,C++11出的那几个迭代器,主要是为了区分普通迭代器(iterator)和常量迭代器(const_iterator),但其实,我们用上面的那些就能够涵盖普通迭代器与常量迭代器了(因为普通迭代器和常量迭代器是可以重载的)

	typedef char* iterator;typedef const char* const_iterator;// 普通迭代器 (可读可写)iterator begin()	{return _str; // 返回字符串的第一个字符的位置}iterator end(){return _str + _size;  // 返回字符串的最后一个字符的下一个位置}// const迭代器 (只可读不可写)const_iterator begin() const{return _str;   // 返回字符串的第一个字符的位置}const_iterator end() const{return _str + _size;    // 返回字符串的最后一个字符的下一个位置}

因为string的底层空间是连续的,所以我们可以直接使用原生指针来定义为迭代器。
当然,使用范围for的前提是需要满足满足迭代器beginend,才能满足范围for
在这里插入图片描述
这里我只是将begin改为Begin,就不能支持范围for了

rbegin & rend

再说反向迭代器之前,需要具备适配器模式的知识,如果不是很了解可以先看看这篇文章:适配器模式

解释一下为什么要具有适配器模式的知识再往下看:
如果我们只是单纯的完成string类的反向迭代器的功能,其实就可以直接复制一遍正向迭代器的代码,改一下就能实现这个功能了。

但STL中有很多容器,每个容器的底层实现都不一样,所以C++中的迭代器主要有以下几种类型:

  1. 输入迭代器:只读,只能顺序向前移动。
  2. 输出迭代器:只写,只能顺序向前移动。
  3. 前向迭代器:可读写,能顺序向前移动。
  4. 双向迭代器:可读写,能顺序向前和向后移动。
  5. 随机访问迭代器:可读写,能跳跃式地访问元素。

有了适配器模式,我们写一份代码。就能让与该迭代器模式相似的容器中的反向迭代器复用这里的代码(比如,stringvector的迭代器都是支持随机访问的,所以我们写一份能够支持随机访问的反向迭代器的代码,就能使stringvector都能够复用)

这里使用了三个模板参数,目的如下:
在这里插入图片描述
在使用迭代器时,我们难免会使用->(结构体指针访问成员)操作符,或者使用*(解引用)操作符(关于这两个操作符的重载,在模拟实现list章节会重点讲,因为在stringvector的底层物理结构是连续的空间,我们用原生指针就能当做它的迭代器,所以不需要重载这两个操作符。但是我们在这里实现的反向迭代器不是原生指针,而是用一个结构体封装起来的结构,从上面实现可以看出。所以需要重载这两个操作符。这里只要先知道,重载->操作符的返回值是char*或者const char*,重载*操作符的返回值是char&或者const char&)。

因为 const char& 是只读的,而 char& 可读可写,同理char*可以改变所指向的内容,const char*不能改变所指向的内容。所以我们需要将两个迭代器区分开来,而我们嵌套三个模板参数,就可以用后面两个模板参数来区分了。这样,我们就只用写一份代码,const非const的迭代器就都能使用这一份代码了。我在这里用一个图给大家展示一下:
在这里插入图片描述
但是请注意:在string中,我们并不会用到->操作符,因为->操作符是自定义类型的指针对其成员的一个访问操作符,string类不会去存一个自定义类型,因为在后面模拟实现vector类的时候,反向迭代器我也会使用这段代码,所以这里我设计成三个模板参数。(实际上char&const char&是不用传递过来的,但为了能够使用这串代码传过来也一样的)

再来看看实现部分,这里我是模拟库里面实现了一个对称的结构:
在这里插入图片描述
所以我们在对迭代器进行解引用操作时,是对当前位置的前一个位置解引用(此时迭代器的位置不发生改变,所以我们需要创建一个迭代器用于记录当前位置的前一个位置,再对该位置解引用)。
在这里插入图片描述

而对于前置++/–,和后置++/–的实现,是利用正向迭代器向相反方向移动实现的:
在这里插入图片描述

具体一些操作实现如下:

	template<class Iterator, class Reference, class Point>struct __random_reverse_iterator{typedef __random_reverse_iterator self;__random_reverse_iterator(const Iterator& it):_cur(it){}Reference operator*(){Iterator tmp = _cur;return *(--tmp);}Point operator->(){return &(this->operator*());}self& operator++(){--_cur;return *this;}self operator++(int){self tmp = *this;--_cur;return tmp;}self& operator--(){++_cur;return *this;}self operator--(int){self tmp = *this;++_cur;return tmp;}self& operator+=(int nums){_cur -= nums;return *this;}self& operator-=(int nums){_cur += nums;return *this;}bool operator!=(const self& it){return _cur != it._cur;}bool operator==(const self& it){return _cur == it._cur;}private:Iterator _cur;};// 下面是我们实现的string类(只含反向迭代器部分)
class string
{
public:typedef __random_reverse_iterator<iterator, char&, char*> reverse_iterator;typedef __random_reverse_iterator<const_iterator, const char&, const char*> const_reverse_iterator;reverse_iterator rbegin(){return reverse_iterator(end());}reverse_iterator rend(){return reverse_iterator(begin());}const_reverse_iterator rbegin() const{return const_reverse_iterator(end());}const_reverse_iterator rend() const{return const_reverse_iterator(begin());}
};

这里我并没有将这个反向迭代器的所用功能实现完,但是基本的框架也是有了。



总结


我在这里将我模拟实现string类的源码附在下面,供大家参考一下如何封装的(因为是在学习阶段,所以封装做得可能不是太好,以下只是我个人的理解):

#include <cstring>
#include <cassert>namespace hyt
{template<class Iterator, class Reference, class Point>struct __random_reverse_iterator{typedef __random_reverse_iterator self;__random_reverse_iterator(const Iterator& it):_cur(it){}Reference operator*(){Iterator tmp = _cur;return *(--tmp);}Point operator->(){return &(this->operator*());}self& operator++(){--_cur;return *this;}self operator++(int){self tmp = *this;--_cur;return tmp;}self& operator--(){++_cur;return *this;}self operator--(int){self tmp = *this;++_cur;return tmp;}self& operator+=(int nums){_cur -= nums;return *this;}self& operator-=(int nums){_cur += nums;return *this;}bool operator!=(const self& it){return _cur != it._cur;}bool operator==(const self& it){return _cur == it._cur;}private:Iterator _cur;};class string{public: // 声明友元函数friend bool operator==(const string& lhs, const string& rhs);friend bool operator!=(const string& lhs, const string& rhs);friend bool operator>(const string& lhs, const string& rhs);friend bool operator>=(const string& lhs, const string& rhs);friend bool operator<(const string& lhs, const string& rhs);friend bool operator<=(const string& lhs, const string& rhs);public: // 迭代器typedef char* iterator;typedef const char* const_iterator;typedef __random_reverse_iterator<iterator, char&, char*> reverse_iterator;typedef __random_reverse_iterator<const_iterator, const char&, const char*> const_reverse_iterator;// 普通迭代器iterator begin(){return _str;}iterator end(){return _str + _size;}// const迭代器const_iterator begin() const{return _str;}const_iterator end() const{return _str + _size;}// 反向普通迭代器reverse_iterator rbegin(){return reverse_iterator(end());}reverse_iterator rend(){return reverse_iterator(begin());}// 反向const迭代器const_reverse_iterator rbegin() const{return const_reverse_iterator(end());}const_reverse_iterator rend() const{return const_reverse_iterator(begin());}public:// 构造函数string(const char* str = ""){size_t size = strlen(str);_size = _capacity = size;_str = new char[_capacity + 1];strcpy(_str, str);}// 拷贝构造string(const string& str){_str = new char[str._capacity + 1];strcpy(_str, str._str);_size = str._size;_capacity = str._capacity;}// 赋值操作符重载string& operator=(const string& str){if (str != this->_str){string tmp = str;swap(tmp);}return *this;}// 析构~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}// Capacity:size_t capacity() const{return _capacity;}size_t size() const{return _size;}void reserve(size_t n = 0){assert(_str);// 我实现的是只考虑扩容的,不考虑缩容if (_capacity < n){char* tmp = new char[n + 1]; // 开辟空间strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}void resize(size_t n, char c = '\0'){assert(_str);// 不考虑缩容的情况if (n > _capacity){reserve(n);for (size_t i = _size;i < n;++i)_str[i] = c;_str[_size = n] = '\0';}}void clear(){_str[_size = 0] = '\0';}bool empty() const{return _size == 0;}// String operations:const char* c_str(){return _str;}const char* data(){return _str;}size_t find(const string& str, size_t pos = 0) const{char* tmp = strstr(_str + pos, str._str);if (tmp == nullptr) // 查找子串失败return npos;return tmp - _str; // 返回两个指针之间的间隔距离,即表示找到的子串的起始位置}size_t find(char c, size_t pos = 0) const{for (size_t i = 0;i < _size;++i)if (_str[i] == c) return i;return npos;}size_t rfind(char c, size_t pos = 0) const{for (size_t i = _size - 1;i != npos;--i)if (_str[i] == c) return i;return npos;}string substr(size_t pos = 0, size_t len = npos) const{assert(pos < _size);string str;if (len == npos || pos + len >= _size)len = _size;for (size_t i = pos;i < len;++i)str += _str[i];return str;}// Element access:char& operator[](size_t n){assert(n >= 0);assert(n < _size);return _str[n];}const char& operator[](size_t n) const{assert(n >= 0);assert(n < _size);return _str[n];}//Modifiers:void push_back(char c){assert(_str);// 先判断是否需要扩容if (_size == _capacity){size_t newcapacity = _capacity == 0 ? 10 : 2 * _capacity;reserve(newcapacity);}_str[_size] = c;_str[++_size] = '\0'; // 增加字符以后不要忘记将_size增加}string& append(const string& str){assert(_str && str._str);// 先判断是否需要扩容size_t len = strlen(str._str);if (_size + len >= _capacity){size_t newcapacity = _size + len + 10; // 每次扩容都多增加是个容积的容错reserve(newcapacity);}memmove(_str + _size, str._str, len);_str[_size += len] = '\0';return *this;}string& operator+=(char c){push_back(c);return *this;}string& operator+=(const string& str){append(str);return *this;}string& insert(size_t pos, const string& str){assert(pos <= _size);size_t len = strlen(str._str);if (_size + len >= _capacity)reserve(_size + len);size_t end = _size + len;while (end - len + 1 > pos){_str[end] = _str[end - len];--end;}memmove(_str + pos, str._str, len);_size += len;return *this;}string& insert(size_t pos, char c){assert(pos <= _size);if (_size >= _capacity){size_t newcapacity = _capacity == 0 ? 10 : 2 * _capacity;reserve(newcapacity);}size_t end = _size + 1;while (end > pos){_str[end] = _str[end - 1];--end;}_str[pos] = c;++_size;return *this;}string& erase(size_t pos = 0, size_t len = npos){assert(pos < _size);if (len == npos || pos + len >= _size){_str[pos] = '\0';_size = pos;}else{size_t end = pos + len;while (end <= _size){_str[end - len] = _str[end];++end;}_size -= len;}return *this;}void swap(string& str){std::swap(_str, str._str);std::swap(_size, str._size);std::swap(_capacity, str._capacity);}private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;// Member constants:const static size_t npos = -1;};bool operator==(const string& lhs, const string& rhs){return (!(strcmp(lhs._str, rhs._str)) && lhs._size == rhs._size);}bool operator!=(const string& lhs, const string& rhs){return !(lhs == rhs);}bool operator>(const string& lhs, const string& rhs){return strcmp(lhs._str, rhs._str) > 0;}bool operator>=(const string& lhs, const string& rhs){return (lhs > rhs || lhs == rhs);}bool operator<(const string& lhs, const string& rhs){return !(lhs >= rhs);}bool operator<=(const string& lhs, const string& rhs){return (lhs < rhs || lhs == rhs);}// 重载 流插入 & 流提取std::ostream& operator<<(std::ostream& out, const string& str){for (auto& e : str)out << e;return out;}std::istream& operator>>(std::istream& in, string& str){str.clear();char buffer[128] = {};int count = 0;for (char ch = in.get();ch != ' ' && ch != '\n';ch = in.get()){buffer[count++] = ch;if (count == 127){str += buffer;count = 0;}}if (count > 0){buffer[count] = '\0';str += buffer;}return in;}std::istream& getline(std::istream& in, string& str){str.clear();char buffer[128] = {};int count = 0;for (char ch = in.get();ch != '\n';ch = in.get()){buffer[count++] = ch;if (count == 127){str += buffer;count = 0;}}if (count > 0){buffer[count] = '\0';str += buffer;}return in;}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/9758.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

OmniReader Pro mac激活版:智慧阅读新选择,开启高效学习之旅

在追求知识的道路上&#xff0c;一款优秀的阅读工具是不可或缺的。OmniReader Pro作为智慧阅读的新选择&#xff0c;以其独特的功能和卓越的性能&#xff0c;为您开启高效学习之旅。 OmniReader Pro具备高效的文本识别和处理技术&#xff0c;能够快速准确地提取文档中的关键信息…

《QT实用小工具·五十四》果冻弹出效果的动画按钮

1、概述 源码放在文章末尾 该项目实现动画按钮&#xff0c;鼠标放在按钮上可以弹性拉出的三个按钮&#xff0c;使用贝塞尔曲线实现&#xff0c;项目demo显示如下所示&#xff1a; 项目部分代码如下所示&#xff1a; #ifndef WATERCIRCLEBUTTON_H #define WATERCIRCLEBUTTON…

Day 43 1049. 最后一块石头的重量 II 494. 目标和 474.一和零

最后一块石头重量Ⅱ 有一堆石头&#xff0c;每块石头的重量都是正整数。 每一回合&#xff0c;从中选出任意两块石头&#xff0c;然后将它们一起粉碎。假设石头的重量分别为 x 和 y&#xff0c;且 x < y。那么粉碎的可能结果如下&#xff1a; 如果 x y&#xff0c;那么两…

Transformers中加载预训练模型的过程剖析

使用HuggingFace的Transformers库加载预训练模型来处理下游深度学习任务很是方便,然而加载预训练模型的方法多种多样且过程比较隐蔽,这在一定程度上会给人带来困惑。因此,本篇文章主要讲一下使用不同方法加载本地预训练模型的区别、加载预训练模型及其配置的过程,藉此做个记…

【go项目01_学习记录10】

操作数据库 1 插入数据2 显示文章2.1 修改 articlesShowHandler() 函数2.2 代码解析 3 编辑文章3.1 添加路由3.2 编辑articlesEditHandler()3.3 新建 edit 模板3.4 代码重构3.5 完善articlesUpdateHandler()3.6 测试更新3.7 封装表单验证 1 插入数据 . . . func articlesStore…

厚德提问大佬答4:AI绘画生成的心得

遇到难题不要怕&#xff01;厚德提问大佬答&#xff01; 厚德提问大佬答 你是否对AI绘画感兴趣却无从下手&#xff1f;是否有很多疑问却苦于没有大佬解答带你飞&#xff1f;从此刻开始这些问题都将迎刃而解&#xff01;你感兴趣的话题&#xff0c;厚德云替你问&#xff0c;你解…

车载电子电器架构 —— 应用软件开发(中)

车载电子电器架构 —— 应用软件开发(中) 我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 屏蔽力是信息过载时代一个人的特殊竞争力,任何消耗你的人和事,多看一眼都是你的不对。非必要不费力证明…

算法设计课第五周(贪心法实现活动选择问题)

目录 一、【实验目的】 二、【实验内容】 三、实验源代码 一、【实验目的】 &#xff08;1&#xff09;熟悉贪心法的设计思想 &#xff08;2&#xff09;理解贪心法的最优解与正确性证明之间的关系 &#xff08;3&#xff09;比较活动选择的各种“贪心”策略&#xff0c;…

动态IP避坑指南:如何挑选合适的动态代理IP?

在如今的网络环境中&#xff0c;使用动态IP代理成为实现隐私保护、访问受限内容和提高网络效率的一种常见方式&#xff0c;选择合适的国外动态IP代理可以让我们的业务处理事半功倍。面对市面上琳琅满目的选择&#xff0c;如何挑选购买适合自己的动态IP代理服务呢&#xff1f;在…

【数据结构】手把手带你玩转线性表

前言&#xff1a; 哈喽大家好&#xff0c;我是野生的编程萌新&#xff0c;首先感谢大家的观看。数据结构的学习者大多有这样的想法&#xff1a;数据结构很重要&#xff0c;一定要学好&#xff0c;但数据结构比较抽象&#xff0c;有些算法理解起来很困难&#xff0c;学的很累。我…

弱监督语义分割-对CAM的生成过程进行改进1

一、仿射变换图像结合正则项优化CAM生成 论文&#xff1a;Self-supervised Equivariant Attention Mechanism for Weakly Supervised Semantic Segmentation &#xff08;CVPR,2020&#xff09; 1.SEAM方法 孪生网络架构&#xff08;Siamese Network Architecture&#xff09…

2024.5.10

TCP服务器端 #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this);//设置窗口大小和窗口大小固定this->resize(727,879);this->setFixedSize(727,879);//创建…

STC8增强型单片机开发【定时器Timer⭐】

目录 一、引言 二、定时器基础知识 三、STC8定时器配置 四、代码示例 五、总结 一、引言 在单片机开发中&#xff0c;定时器&#xff08;Timer&#xff09;是一个极其重要的组件&#xff0c;它允许开发者基于时间触发各种事件或任务。STC8增强型单片机作为一款功能丰富的…

使用2G内存求20亿个数字中出现次数最多的N个

又是一个TOP -N的题目 我看了一下CSDN上大多数人的回答和GPT说的差不多&#xff0c;都是说使用哈希之类的&#xff1b; 我今天说一下我的解法&#xff0c;首先说一下不太快的基础解法 20亿数字使用uint32需要80GB&#xff0c; &#xff08;1&#xff09;分为40块读取&#…

c++opencv Project3 - License Plate Detector

俄罗斯车牌识别案例&#xff1a;实时识别车牌&#xff0c;并且读取到指定文件夹中。 惯例先展示结果图&#xff1a; 对于摄像头读取图片进行车牌匹配&#xff0c;原理和人脸识别其实是一致的。 利用训练好的模型进行匹配即可。可参考&#xff1a; 对视频实现人脸识别-CSDN博…

电能表远程抄表系统是什么?

电能表远程抄表系统是一种优秀的电力管理方法&#xff0c;它通过自动化的形式搜集、解决与分析电能表的信息&#xff0c;进而取代了传统人工抄水表方法。其主要原理是运用物联网技术、通讯技术和大数据处理技术&#xff0c;完成对电度表数据信息实时、远程控制获取和管理方法。…

探索无界知识:用 ChatGPT 的原理学习任何事物!

为避免文章重复&#xff0c;您的文本已通过更改句式、用词以及句子结构进行了修改。现在的文本应该能更好地满足去重的需求&#xff1a; 从ChatGPT原理出发&#xff0c;我们探讨GPT如何启发人类学习和构建个人知识体系。 1. 明确学习目标 机器学习必须依靠目标函数。同样&…

【Qt 开发基础体系】QMap 类和 QHash 类以及 QVector 类

文章目录 1.QMap 详解1.1 QMap 的介绍1.2 QMap 的具体用法如下1.3 QmultiMap类 2.QHash 详解3. QMap 和 QHash 的对比4. QVector 详解 1.QMap 详解 1.1 QMap 的介绍 &#x1f427;① QMap<key,T>提供一个从类型为Key的键到类型为T的值的映射。通常&#xff0c;QMap存储的…

STC8增强型单片机开发【串口调试UART⭐⭐】

目录 一、引言 二、UART基础知识 三、STC8 UART配置 四、代码示例 上列代码中所需的库函数文件&#xff1a; 引入库函数的流程&#xff1a; 五、总结 一、引言 在单片机开发中&#xff0c;串口调试&#xff08;UART&#xff09;是一种常用的通信方式&#xff0c;用于实现…

linux grep命令搜索指定路径

在Linux开发的过程中grep这个搜索命令&#xff0c;是必不可少的存在。它可以快速的搜索出来我们需要的关键字所在的位置。 有助于我们快速分析定位问题。 下面&#xff0c;分享一个简单实用的小技巧。 原始grep 最终grep grep过滤掉二进制的文件 -I选项 结论 这样子是不…