【C++标准库】模拟实现string类
模拟实现string类
- 一.命名空间与类成员变量
- 二.构造函数
- 1.无参(默认)构造
- 2.有参构造
- 3.兼容无参和有参构造
- 4.拷贝构造
- 1.传统写法
- 2.现代写法
- 三.析构函数
- 四.string类对象的容量操作
- 1.size
- 2.capacity
- 3.clear
- 4.empty
- 5.reserve
- 6.resize
- 五.string类对象的访问及遍历操作
- 1.operator[]
- 2.实现迭代器:begin+end
- 六.string类对象的增删查改操作
- 1.operator=
- 1.传统写法
- 2.现代写法
- 2.push_back
- 3.append
- 4.operator+=
- 5.insert
- 6.erase
- 7.find
- 8.substr
- 9.c_str
- 10.swap
- 七.非成员函数
- 1.string比较函数
- 2.流插入与流提取
- 3.getline
一.命名空间与类成员变量
根据string的结构,显然可知string实质就是字符数组,但有一点区别就是,string可以扩容,再类比动态顺序表,就不难得出string的成员变量。在模拟实现string时,为了与C++标准库中的string作区分,可以给定命名空间。
成员变量:
- char* str:指向string第一个字符的指针。
- size_t size:string中有效数据的个数。
- size_t capacity:string可以存放有效数据的容量。
- static const size_t npos:静态成员。
大体结构如下:
namespace xzy
{class string{private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;static const size_t npos; //静态成员类内声明};const size_t string::npos = -1; //类外初始化
}
二.构造函数
class string
{
public:string():_str(nullptr),_size(0),_capacity(0){}string(const char* str):_size(strlen(str)), _capacity(_size),_str(new char[_capacity + 1]){}const char* c_str() const{return _str;}private:char* _str;size_t _size;size_t _capacity;
};
- 第一种由于将_str初始化为nullptr,通过C语言中的返回_str直到遇到’\0’停止打印字符串的方法,而_str为nullptr,打印nullptr导致程序崩溃。
- 第二种看似程序正常但真的是正确的吗?其实:初始化列表出现的顺序,并不是初始化的顺序,而是按照成员变量声明的顺序初始化成员变量,先初始化_str,而_capacity是随机值,导致开辟的空间不确定,导致出现错误。
正确的方法如下:
1.无参(默认)构造
由于string默认含有’\0’,可以提前开辟一个’\0’,而’\0’不是有效的数据,也不算入容量之中。
string():_str(new char[1]{'\0'}), _size(0), _capacity(0)
{}
2.有参构造
注意:容量中不包含’\0’,而string中有包含’\0’,所以在开辟空间时要加上一个’\0’的空间。
string(const char* str)
{_size = strlen(str);_capacity = _size;_str = new char[_capacity + 1];strcpy(_str, str);
}
3.兼容无参和有参构造
string(const char* str = "")
{_size = strlen(str);_capacity = _size;_str = new char[_capacity + 1];strcpy(_str, str);
}
- 不传参时:用缺省值,str为空的常量字符串,strlen(str)为0,且sizeof(str)为1,含有一个隐藏的’\0’,刚好满足无参构造。
- 传参时:就用实参,满足有参构造。
4.拷贝构造
string(const string& str)
{_str = str._str;_size = str._size;_capacity = str._capacity;
}
int main()
{xzy::string s1;xzy::string s2(s1);return 0;
}
分析:当我们未提供拷贝构造时,编译器会提供拷贝构造,进行简单的值拷贝(浅拷贝),正如以上代码。但是存在很大的漏洞,s1的_str与s2的_str指向堆区同一块空间,程序结束时分别调用各自的析构函数,从而对同一块空间释放两次,这是未定义行为,导致程序崩溃。
1.传统写法
思路:先开空间,再利用strcpy拷贝,最后修改有效数据大小与容量。
string(const string& str)
{_str = new char[str._capacity + 1];strcpy(_str, str._str);_size = str._size;_capacity = str._capacity;
}
2.现代写法
构造一个临时对象,进行交换。
void swap(string& str)
{std::swap(_str, str._str);std::swap(_size, str._size);std::swap(_capacity, str._capacity);
}string(const string& str)
{string tmp(str._str);swap(tmp);
}
注意
:由于没有初始化列表,不确定s2_str被初始化为nullptr,取决于编译器,可以在类成员变量声明时加上缺省值,确保s2.str为nullptr,而避免s2._str为随机值,交换给tmp变成野指针,函数结束时tmp调用析构函数释放不合法的空间导致程序崩溃。
class string
{
private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;
};
三.析构函数
_str是在堆区开辟的空间,要用delete[]释放空间,否则造成内存泄漏。
~string()
{delete[] _str;_str = nullptr;_size = _capacity = 0;
}
四.string类对象的容量操作
1.size
size_t size() const
{return _size;
}
2.capacity
size_t capacity() const
{return _capacity;
}
3.clear
void clear()
{_str[0] = '\0';_size = 0;
}
4.empty
5.reserve
扩容时:先开辟新空间,千万记得多开一个空间保存’\0’,再将旧空间拷贝到空间,释放旧空间,修改_str指向新空间,最后修改容量。学了C++,new就取代realloc了。
void reserve(size_t n)
{if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}
}
6.resize
修改有效数据的个数时:先比较修改后的有效数据与原有数据的大小,若小于则修改_size,若大于再比较容量与修改后的有效数据的大小,判断是否扩容,利用memset函数初始化。
void string::resize(size_t n, char c)
{if (n > _size){// 如果newSize大于底层空间大小,则需要重新开辟空间if (n > _capacity){reserve(n);}memset(_str + _size, c, n - _size);}_size = n;_str[n] = '\0';
}
五.string类对象的访问及遍历操作
1.operator[]
char& operator[](int pos)
{assert(pos >= 0 && pos < _size);return _str[pos];
}const char& operator[](int pos) const
{assert(pos >= 0 && pos < _size);return _str[pos];
}
-
提供两个版本的operator[]:普通重载[]与const修饰的重载[]。若初始化一个常量字符串时:const string s(“123”); 由于存在权放大问题,就无法调用普通重载[],而const修饰的重载[]就可以使用。
-
重载operator[],本质就是函数重载,而函数的返回值是不支持函数重载条件的,为了让两个operator[]满足函数重载的条件,可以const随便修饰一个成员函数。隐藏了this指针,实际const修饰的是this所指的对象。
-
第一个函数的参数列表的第一个位置隐藏了string* const this;第二个函数的参数列表的第一个位置隐藏了const string* const this;函数的参数不同就满足了函数重载的条件,可以共存。
2.实现迭代器:begin+end
typedef char* iterator;
typedef const char* const_iterator;iterator begin()
{return _str;
}
iterator end()
{return _str + _size;
}const_iterator begin() const
{return _str;
}
const_iterator end() const
{return _str + _size;
}
为了与标准库里的类似,重定义char* 为iterator。同理提供两个版本的迭代器iterator与const_iterator。
六.string类对象的增删查改操作
1.operator=
注意:operator=只能写成成员函数
,不能写成成员函数。
1.传统写法
与传统写法的拷贝构造类似。
string& operator=(const string& str)
{if (this != &str){delete[] _str;_str = new char[str._capacity + 1];strcpy(_str, str._str);_size = str._size;_capacity = str._capacity;}return *this;
}
注意:如果没写 if (this != &str) 自己给自己赋值时,delete[] _str 后_str为野指针,自己给自己拷贝程序崩溃。
2.现代写法
与现代写法的拷贝构造类似。
string& operator=(const string& str)
{if (this != &str){//string tmp(str.c_str()); //调用构造string tmp(str); //调用拷贝构造swap(tmp); //刚好函数结束时,tmp将赋值前的空间释放,相当的完美}return *this;
}//更完美的方法:一行搞定
string& operator=(string tmp)
{swap(tmp);return *this;
}
2.push_back
尾插时:先检查容量,再进行尾插。注意:最后要补上'\0'
。
void push_back(char ch)
{if (_size == _capacity){reserve(_capacity == 0 ? 4 : 2 * _capacity);}_str[_size] = ch;++_size;_str[_size] = '\0';
}
3.append
追加时:先要判断容量是否大于有效数据+所追加的字符串大小。若小于则无需扩容;若大于两倍则需要多少就扩容多少;小于两倍就按照两倍扩容。最后拷贝字符串即可。
void append(const char* str)
{size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);}strcpy(_str + _size, str);_size += len;
}
4.operator+=
- +=一个字符:直接调用push_back即可。
string& operator+=(char ch)
{push_back(ch);return *this;
}
- +=一个字符串:直接调用append即可。
string& operator+=(const char* str)
{append(str);return *this;
}
5.insert
- 插入一个字符:先检查容量,再整体往后挪动一位,最后插入即可。
但是存在一些坑如下:
- 当在pos=0位置插入字符时:end=0时进入循环,- -end,由于end类型为无符号整形size_t,则end不是-1而是一个非常大的值,进入死循环。
- 就算将end修改为int ,循环条件end>=pos时,两边类型不同会进行算数转换,int转换成size_t,end转换成size_t类型,依旧进入死循环。
正确写法:
void insert(size_t pos, char ch)
{assert(pos >= 0 && pos <= _size);if (_size == _capacity){reserve(_capacity == 0 ? 4 : 2 * _capacity);}//第一种:强转size_t为int//int end = _size;//while (end >= (int)pos)//{// _str[end + 1] = _str[end];// --end;//}//_str[pos] = ch;//++_size;//推荐这种:end始终大于0size_t end = _size + 1;while (end > pos){_str[end] = _str[end - 1];--end;}_str[pos] = ch;++_size;
}
- 插入一个字符串:先检查容量,再整体往后挪动为插入的字符串预留空间,最后插入字符串即可。
void insert(size_t pos, const char* str)
{assert(pos >= 0 && pos <= _size);size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);}//整体后移//memmove(_str + len, _str, sizeof(char) * len);size_t end = _size + len;while (end > pos + len - 1){_str[end] = _str[end - len];--end;}//插入字符串for (size_t i = 0; i < len; i++){_str[pos + i] = str[i];}_size += len;
}
6.erase
删除时:比较要删除的子串长度与pos及其以后字符串的的大小,判断是否pos及其以后得字符全删除。
void erase(size_t pos, size_t len = npos
)
{assert(pos >= 0 && pos < _size);if (len >= _size - pos){_str[pos] = '\0';_size = pos;}else{//memmove(_str + pos, _str + pos + len, sizeof(char) * (_size - pos - len + 1));for (size_t i = pos; i <= _size - len; i++){_str[i] = _str[i + len];}_size -= len;}
}
7.find
- 查找字符:找到返回下标,未找到返回npos。
size_t find(char ch, size_t pos = 0)
{assert(pos >= 0 && pos < _size);for (size_t i = 0; i < _size; i++){if (_str[i] == ch){return i;}}return npos;
}
- 查找字符串:利用C语言接口strstr查找子串函数,找到返回下标,未找到返回npos。
size_t find(const char* str, size_t pos = 0)
{assert(pos >= 0 && pos < _size);const char* ptr = strstr(_str + pos, str);if (ptr == nullptr){return npos;}else{return ptr - _str;}
}
8.substr
返回子串:比较要返回子串长度与pos及其以后字符串的的大小,判断是否pos及其以后得字符全返回。
注意:深浅拷贝问题;
由于是返回局部string,而局部string出函数被销毁。此时会拷贝构造一个临时string作为返回,而默认的拷贝构造是浅拷贝(简单的值拷贝),局部string销毁时,临时变量string中的_str变成野指针,外面又拷贝构造接收该临时string,本身就是无效的string,程序结束前调用析构函数释放空间,重复的delete导致程序崩溃。解决方法:自己写一个深拷贝构造。
string substr(size_t pos = 0, size_t len)
{assert(pos >= 0 && pos < _size);if (len > _size - pos){len = _size - pos;}string sub;sub.reserve(len);for (size_t i = 0; i < len; i++){sub += _str[pos + i];}return sub;
}
9.c_str
返回字符串首字符的地址:用于调用C语言接口,例如strcpy,memmove等。
const char* c_str() const
{return _str;
}
10.swap
调用std::swap进行对象(值)交换。
void swap(string& str)
{std::swap(_str, str._str);std::swap(_size, str._size);std::swap(_capacity, str._capacity);
}
七.非成员函数
1.string比较函数
只需要利用strcmp函数比较,实现两个函数,就可以调用实现多个函数。
bool operator<(const string& s1, const string& s2)
{return strcmp(s1.c_str(), s2.c_str()) < 0;
}bool operator>(const string& s1, const string& s2)
{return !(s1 <= s2);
}bool operator==(const string& s1, const string& s2)
{return strcmp(s1.c_str(), s2.c_str()) == 0;
}bool operator<=(const string& s1, const string& s2)
{return s1 < s2 || s1 == s2;
}bool operator>=(const string& s1, const string& s2)
{return !(s1 < s2);
}bool operator!=(const string& s1, const string& s2)
{return !(s1 == s2);
}
2.流插入与流提取
在C++中,屏幕和键盘分别通过标准输出流(std::cout)和标准输入流(std::cin)来实现数据的流插入(输出)和流提取(输入)。以下是针对屏幕(输出)和键盘(输入)的流插入与流提取的详细介绍:
- 屏幕(输出)与流插入(operator<<):流插入(operator<<)用于将数据发送到输出流中,在C++中,标准输出流std::cout是与屏幕(通常是控制台或命令行界面)相关联的。当你使用<<操作符将数据发送到std::cout时,数据会被格式化(如果需要的话)并显示在屏幕上。
- 键盘(输入)与流提取(operator>>):流提取(operator>>)用于从输入流中读取数据,在C++中,标准输入流std::cin是与键盘(或任何标准输入设备)相关联的。当你使用>>操作符从std::cin中读取数据时,它会从键盘获取输入,并根据需要将其存储在提供的变量中。
注意:
- 流插入与流提取不推荐写成成员函数,例如ostream& operator<<(ostream& out); 因为<<左边是类对象,调用时要写成s<<out,非常别扭。
- 不需要写成友元函数,可以做到不用访问类内的私有成员,完成流插入与流提取。
ostream& operator<<(ostream& out, const string& str)
{/*string::const_iterator it = str.begin();while (it != str.end()){cout << *it;++it;}*/for (auto ch : str){out << ch;}return out;
}istream& operator>>(istream& in, string& str)
{str.clear();char ch;//in >> ch; //错误,ch不会提取空白字符,陷入死循环ch = in.get();while (ch != ' ' && ch != '\n'){str += ch;//in >> ch;ch = in.get();}return in;
}
注意:流提取cin默认跳过空白字符(不会读取空白字符)
,例如:空格、换行,可以用cin.get()
函数从键盘获得空白字符,类似C语言中的getc()函数。
优化方法
:减少扩容,临时存放到字符数组中,等到满了时,再+=到其中。
istream& operator>>(istream& in, string& str)
{str.clear();const int N = 256;char buff[N];int i = 0;char ch;ch = in.get();while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == N - 1){buff[i] = '\0';str += buff;i = 0;}ch = in.get();}if (i > 0){buff[i] = '\0';str += buff;}return in;
}
3.getline
getline函数:可以读取含有空格的字符串,将’\n’作为分隔符。
istream& getline(istream& in, string& str)
{str.clear();char ch;ch = in.get();while (ch != '\n'){str += ch;ch = in.get();}return in;
}
相关文章:

【C++标准库】模拟实现string类
模拟实现string类 一.命名空间与类成员变量二.构造函数1.无参(默认)构造2.有参构造3.兼容无参和有参构造4.拷贝构造1.传统写法2.现代写法 三.析构函数四.string类对象的容量操作1.size2.capacity3.clear4.empty5.reserve6.resize 五.string类对象的访问及…...
ArcGIS for js 标记(vue代码)
一、引入依赖 import Graphic from "arcgis/core/Graphic"; import GraphicsLayer from "arcgis/core/layers/GraphicsLayer"; import Color from "arcgis/core/Color"; import TextSymbol from "arcgis/core/symbols/TextSymbol.js"…...
全网最全最新100道C++面试题:40-60
前述:本文初衷是为了总结本人在各大平台看到的C面经,我会在本文持续更新我所遇到的一些C面试问题,如有错误请一定指正我。新建立了一个收集问答的仓库,欢迎各位小伙伴来更新鸭interview_experience: 本仓库初衷是想为大家提供一个…...

RAG+内容推荐,应该如何实践?
最近业务有需求:结合RAG内容推荐,针对实践部分,做一点探究。 话不多说,直接开冲! 背景 首先回顾一下 RAG 技术定义,它可以结合信息检索和生成模型的混合。简单来说,RAG 预训练的语言模型 信…...
SFTTrainer loss多少合适
在机器学习和深度学习中,“loss”(损失函数)的合理值并没有一个固定的标准,因为它依赖于多种因素,包括模型的类型、任务的性质、数据的规模和特性等。然而,我们可以从一些通用的原则和经验值来讨论损失函数…...

HTTP协议详解(一)
协议 为了使数据在网络上从源头到达目的,网络通信的参与方必须遵循相同的规则,这套规则称为协议,它最终体现为在网络上传输的数据包的格式。 一、HTTP 协议介绍 HTTP(Hyper Text Transfer Protocol): 全…...

RK3568平台(触摸篇)串口触摸屏
一.什么是串口屏 串口屏,可组态方式二次开发的智能串口控制显示屏,是指带有串口通信的TFT彩色液晶屏显示控制模组。利用显示屏显示相关数据,通过触摸屏、按键、鼠标等输入单元写入参数或者输入操作指令,进而实现用户与机器进行信…...

MySQL数据库-事务
一、什么是事务 1.概念 事务(Transaction):一个最小的不可再分的工作单元,一个事务对应一个完整的业务,一个完整的业务需要批量的DML(insert、update、delete)语句共同联合完成,事务只针对DML语句。 数据…...
qt事件类型列表
t提供了一系列丰富的事件类型,这些事件允许应用程序响应各种用户输入、系统通知以及其他类型的交互。以下是一些常见的Qt事件类型及其用途概述: QEvent::None (0): 无事件,用于初始化或作为默认值。 QEvent::Timer (1): 定时器事件ÿ…...

ElasticSearch父子索引实战
关于父子索引 ES底层是Lucene,由于Lucene实际上是不支持嵌套类型的,所有文档都是以扁平的结构存储在Lucene中,ES对父子文档的支持,实际上也是采取了一种投机取巧的方式实现的. 父子文档均以独立的文档存入,然后添加关联关系,且父子文档必须在同一分片,由于父子类型文档并没有…...

二百四十九、Linux——在Linux中创建新用户、赋予新用户root权限并对文件夹赋予新用户的权限
一、目的 安装国产化数据库OceanBase的时候,需要创建新用户、赋予新用户root权限并对文件夹赋予新用户的权限 二、创建新用户 #创建账户 oceanadmin [roothurys22 ~]#useradd -U oceanadmin -d /home/oceanadmin -s /bin/bash [roothurys22 ~]#mkdir -p /home/oc…...

com.mysql.cj.jdbc.Driver 爆红
出现这样的问题就是pom.xml文件中没有添加数据库依赖坐标 添加上这个依赖即可,添加完后重新加载一下Maven即可。 如果感觉对你有用就点个赞!!!...

传神论文中心|第19期人工智能领域论文推荐
在人工智能领域的快速发展中,我们不断看到令人振奋的技术进步和创新。近期,开放传神(OpenCSG)社区发现了一些值得关注的成就。传神社区本周也为对AI和大模型感兴趣的读者们提供了一些值得一读的研究工作的简要概述以及它们各自的论…...

案例分享-国外轻松感UI设计赏析
国外UI设计倾向于采用简洁的布局、清晰的排版和直观的交互方式,减少用户的认知负担,从而营造出轻松的使用体验。这种设计风格让用户能够快速找到所需信息,降低操作难度,提升整体满意度。 在注重美观的同时,更加重视用户…...

操作系统(4)——文件系统
目录 小程一言文件系统管理基础概念&功能基本概念文件的结构和属性文件的操作文件的安全性和权限控制文件系统的实现和分配方式 问题&解答1、文件系统在操作系统中起到什么作用?2、文件的逻辑结构和物理结构有何区别?3、如何理解文件权限控制在操…...

C# 调用Webservice接口接受数据测试
1.http://t.csdnimg.cn/96m2g 此链接提供测试代码; 2.http://t.csdnimg.cn/64iCC 此链接提供测试接口; 关于Webservice的基础部分不做赘述,下面贴上我的测试代码(属于动态调用Webservice): 1ÿ…...

工作流流程引擎框架推荐来了
近期有不少粉丝客户朋友都在询问工作流流程引擎框架推荐。随着行业竞争激烈化,实现流程化办公已经成为当务之急。低代码技术平台及工作流流程引擎拥有够灵活、更可靠、可视化界面等诸多个优势特点,在推动企业实现数字化转型的过程中深受行业信赖与喜爱。…...

从技术博客到个人 IP 矩阵:全面攻略与实战示例
文章目录 摘要引言创建博客选择平台设计和布局 内容规划明确目标受众设定内容方向制定发布计划 SEO 优化关键词研究内链和外链元标签优化 社交媒体推广选择社交平台制定推广策略 可运行的 Demo 代码模块QA 环节问:如何增加博客的曝光度?问:如…...
SOFAJRaft 简介
SOFAJRaft 简介 SOFAJRaft是一个基于Raft一致性算法的生产级高性能Java实现,由蚂蚁金服自主研发。以下是关于SOFAJRaft的详细介绍: 来源与背景: SOFAJRaft是从百度的braft移植而来,并在其基础上进行了一系列的优化和改进。它作为…...
c#中Oracle.DataAccess.dll连接数据库的报错处理
通过DataAccess.dll连接Oracle数据库时,报如下错误 The provider is not compatible with the version of Oracle client 最终原因: dll 文件复制不全(4个文件必须) oracle.dataaccess.dll oci.dll oraociei11.dll oraops11w.dll...

Xshell远程连接Kali(默认 | 私钥)Note版
前言:xshell远程连接,私钥连接和常规默认连接 任务一 开启ssh服务 service ssh status //查看ssh服务状态 service ssh start //开启ssh服务 update-rc.d ssh enable //开启自启动ssh服务 任务二 修改配置文件 vi /etc/ssh/ssh_config //第一…...

工业安全零事故的智能守护者:一体化AI智能安防平台
前言: 通过AI视觉技术,为船厂提供全面的安全监控解决方案,涵盖交通违规检测、起重机轨道安全、非法入侵检测、盗窃防范、安全规范执行监控等多个方面,能够实现对应负责人反馈机制,并最终实现数据的统计报表。提升船厂…...
k8s从入门到放弃之Ingress七层负载
k8s从入门到放弃之Ingress七层负载 在Kubernetes(简称K8s)中,Ingress是一个API对象,它允许你定义如何从集群外部访问集群内部的服务。Ingress可以提供负载均衡、SSL终结和基于名称的虚拟主机等功能。通过Ingress,你可…...

循环冗余码校验CRC码 算法步骤+详细实例计算
通信过程:(白话解释) 我们将原始待发送的消息称为 M M M,依据发送接收消息双方约定的生成多项式 G ( x ) G(x) G(x)(意思就是 G ( x ) G(x) G(x) 是已知的)࿰…...

前端开发面试题总结-JavaScript篇(一)
文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包(Closure)?闭包有什么应用场景和潜在问题?2.解释 JavaScript 的作用域链(Scope Chain) 二、原型与继承3.原型链是什么?如何实现继承&a…...

SAP学习笔记 - 开发26 - 前端Fiori开发 OData V2 和 V4 的差异 (Deepseek整理)
上一章用到了V2 的概念,其实 Fiori当中还有 V4,咱们这一章来总结一下 V2 和 V4。 SAP学习笔记 - 开发25 - 前端Fiori开发 Remote OData Service(使用远端Odata服务),代理中间件(ui5-middleware-simpleproxy)-CSDN博客…...
Python Einops库:深度学习中的张量操作革命
Einops(爱因斯坦操作库)就像给张量操作戴上了一副"语义眼镜"——让你用人类能理解的方式告诉计算机如何操作多维数组。这个基于爱因斯坦求和约定的库,用类似自然语言的表达式替代了晦涩的API调用,彻底改变了深度学习工程…...

FFmpeg:Windows系统小白安装及其使用
一、安装 1.访问官网 Download FFmpeg 2.点击版本目录 3.选择版本点击安装 注意这里选择的是【release buids】,注意左上角标题 例如我安装在目录 F:\FFmpeg 4.解压 5.添加环境变量 把你解压后的bin目录(即exe所在文件夹)加入系统变量…...

脑机新手指南(七):OpenBCI_GUI:从环境搭建到数据可视化(上)
一、OpenBCI_GUI 项目概述 (一)项目背景与目标 OpenBCI 是一个开源的脑电信号采集硬件平台,其配套的 OpenBCI_GUI 则是专为该硬件设计的图形化界面工具。对于研究人员、开发者和学生而言,首次接触 OpenBCI 设备时,往…...
【SpringBoot自动化部署】
SpringBoot自动化部署方法 使用Jenkins进行持续集成与部署 Jenkins是最常用的自动化部署工具之一,能够实现代码拉取、构建、测试和部署的全流程自动化。 配置Jenkins任务时,需要添加Git仓库地址和凭证,设置构建触发器(如GitHub…...