深度解读C++17中的std::string_view:解锁字符串处理的新境界
深入研究C++17中的std::string_view:解锁字符串处理的新境界
- 一、简介
- 二、std::string_view的基础知识
- 2.1、构造函数
- 2.2、成员函数
- 三、std::string_view为什么性能高?
- 四、std::string_view的使用陷阱
- 五、std::string_view源码解析
- 六、总结
一、简介
C++中有两类字符串,即C风格字符串(字符串字面值、字符数组、字符串指针)和std::string
对象两大类。
C风格字符串:
#include <string.h>int main()
{//C风格字符串初始化方式char* arr = "LionLong";char arr[] = "LionLong";char arr[] = { 'L', 'i', 'o', 'n', 'L', 'o','n', 'g', '\0' }; //结尾必须有\0结束符//C风格字符串函数strlen(arr);strcmp(arr1, arr2);strcat(arr1, arr2);strcpy(arr1, arr2);return 0;
}
C++ std::string
对象:
#include <string>//初始化方式
std::string s1;
std::string s2(s1);
std::string s3 = s1;
std::string s4("LionLong");
std::string s4 = "LionLong";
std::string s5 = std::string("LionLong");
std::string s6(6, 'L'); //LLLLLL//对象操作
s1.empty();
s1.size();
s[n];
s.substr(3, 5);
当需要将字符串作为参数传递给函数时,往往会伴随字符串的拷贝。当数据占用较大内存时,减少数据的拷贝显得尤为重要。
在C++17之前,可以通过C风格字符串指针作为函数形参,也可以通过std::string
字符串引用类型 作为函数形参。但是这并不完美,从实践上看,存在以下问题:
- C风格字符串的传递仍会进行拷贝。字符数组、字符串字面量和字符串指针是可以隐式转换为
std::string
对象的,当函数的形参是std::string
,而传递的实参是C风格字符串时,编译器会做一次隐式转换,生成一个临时的std::string
对象,再让形参指向这个对象。字符串字面值一般较小,性能消耗可以忽略不计;但是字符数组和字符串指针往往较大,频繁的数据拷贝就会造成较大的性能消耗,不得不重视。 substr()
的复杂度是O(N)。std::string
提供了一个返回字符串子串的函数,但是每次返回的都是一个新的对象,也需要进行构造。
那么有没有办法在原始字符串的基础上进行操作呢?答案是std::string_view
。
在C++17中引入的std::string_view
是一种轻量级的字符串视图类型,类似于Golang的slice。它的出现主要是为了提供一种非拥有性的字符串引用机制,用于处理字符串的读取和操作,而无需进行内存拷贝或分配新的字符串对象。
std::string_view
并不会真正分配存储空间,而只是原始数据的一个只读窗口,可以认为它是一个内存的观察者。std::string_view
的结构非常简单,只会保持原始字符串的起始指针以及字符串的长度,这个结构不会占用太多内存,开销非常小。
std::string_view
的出现意义和重要性:
-
减少内存拷贝:使用std::string_view可以避免不必要的字符串拷贝操作,特别是在函数参数传递和返回值返回时,可以显著提高性能和效率。
-
std::string_view
提供了类似std::string
的接口,可以方便地进行字符串的访问和操作,例如查找子串、比较字符串、截取子串等,而无需额外的内存分配和释放。现有的基于std::string
的代码可以无缝地迁移到使用std::string_view
的代码。 -
std::string_view
不仅可以用于处理std::string
类型的字符串,还可以用于处理其他字符序列,包括字符数组、字符指针等。
二、std::string_view的基础知识
std::string_view
是对字符串的一种非拥有式(non-owning)表示,意味着它不拥有字符串的内存,而是通过指针和长度来引用现有的字符串数据。
std::string_view
定义于C++标准库头文件<string_view>
中,std::string_view
的定义如下:
namespace std {template<class charT, class traits = std::char_traits<charT>>class basic_string_view {public:// 构造函数constexpr basic_string_view() noexcept;constexpr basic_string_view(const charT* str);constexpr basic_string_view(const charT* str, size_t len);// 成员函数constexpr const charT* data() const noexcept;constexpr size_t size() const noexcept;constexpr bool empty() const noexcept;constexpr charT operator[](size_t pos) const;constexpr charT front() const;constexpr charT back() const;constexpr basic_string_view substr(size_t pos, size_t count = npos) const;constexpr int compare(basic_string_view other) const noexcept;constexpr size_t find(basic_string_view str, size_t pos = 0) const noexcept;// ...};// 类型别名using string_view = basic_string_view<char>;using wstring_view = basic_string_view<wchar_t>;using u16string_view = basic_string_view<char16_t>;using u32string_view = basic_string_view<char32_t>;
}
std::string_view
实际上是一种模板类basic_string_view
的一种实现。与之类似的还有wstring_view
、u8string_view
、u16string_view
、u32string_view
。
std::string_view的特点:
- 轻量级:std::string_view本身只包含一个指向字符串数据的指针和一个长度,因此它的大小非常小。
- 非拥有式:std::string_view不拥有字符串数据的内存,它只是对现有字符串数据的引用。这意味着它可以安全地引用临时字符串、字符串字面量或其他字符串对象,而无需复制数据。
- 零拷贝:由于std::string_view不拥有字符串数据,它可以在不进行数据复制的情况下对字符串进行操作。
- 不可变性:std::string_view是只读的,它提供了一系列成员函数来访问和操作字符串数据,但不能修改字符串的内容。
- 字符串操作支持:std::string_view提供了一组成员函数,例如data()、size()、empty()、substr()、compare()和find()等,使得对字符串数据的常见操作变得方便和高效。
通过使用std::string_view
,可以在不引入额外的内存开销的情况下,对字符串进行查看和操作,这在许多情况下都是非常有用的。
相比传统的字符串类型(如std::string或C风格的字符串),传统的字符串类型(如std::string或C风格的字符串)需要进行内存分配和拷贝操作,导致额外的开销和性能损失。而std::string_view则更加轻量级和高效,适用于对字符串进行读取和操作,特别是在函数参数传递、字符串处理和性能敏感的场景下。
需要注意的是,由于std::string_view只是对字符串的引用,使用时需要确保字符串的生命周期长于std::string_view的使用范围,以避免悬空引用或访问已释放的内存。
std::string_view是C++17中引入的一种轻量级字符串视图类型,用于以非拥有(non-owning)的方式引用字符串数据。它提供了一种有效的方式来访问字符串,而无需进行复制或拥有内存。
2.1、构造函数
//默认构造函数
constexpr basic_string_view() noexcept;
//拷贝构造函数
constexpr basic_string_view(const string_view& other) noexcept = default;
//直接构造,构造一个从s所指向的字符数组开始的前count个字符的视图
constexpr basic_string_view(const CharT* s, size_type count);
//直接构造,构造一个从s所指向的字符数组开始,到\0之前为止的视图,不包含空字符
constexpr basic_string_view(const CharT* s);
std::string_view的构造方法:
- 默认构造方法:
std::string_view()
,创建一个空的string_view。 - 字符串指针构造方法:
std::string_view(const char* str)
,创建一个string_view,指向以null结尾的C风格字符串。 - 字符串指针和长度构造方法:
std::string_view(const char* str, size_t len)
,创建一个string_view,指向给定长度的字符序列。 - std::string构造方法:
std::string_view(const std::string& str)
,创建一个string_view,指向std::string对象的字符序列。 - 字符串迭代器构造方法:
std::string_view(InputIt first, InputIt last)
,创建一个string_view,指向[first, last)区间内的字符序列。
std::string
类重载了从string
到string_view
的转换操作符:
operator std::basic_string_view<CharT, Traits>() const noexcept;
因此可以通过std::string来构造一个std::string_view:
std::string_view foo(std::string("LionLong"));
这个过程其实包含三步:
- 构造
std::string
的临时对象a
; - 通过转换操作符将临时对象
a
转换为string_view
类型的临时对象b
; - 调用
std::string_view
的拷贝构造函数。
2.2、成员函数
std::string_view的成员函数和操作符:
- data():返回string_view所指向的字符序列的指针。
- size()、length():返回string_view所指向的字符序列的长度。
- max_size():返回可以容纳的最大长度。
- empty():检查string_view是否为空,即长度是否为0。
operator[]()
:访问string_view中指定位置的字符。- at():以安全的方式访问string_view中指定位置的字符,会进行边界检查。
- front():返回string_view中第一个字符。
- find():返回首次出现给定子串的位置。
- back():返回string_view中最后一个字符。
- begin():返回指向string_view中第一个字符的迭代器。
- end():返回指向string_view末尾的迭代器。
- cbegin():返回指向string_view中第一个字符的const迭代器。
- cend():返回指向string_view末尾的const迭代器。
- substr():返回一个新的string_view,包含原始string_view的子字符串。不同于
std::string::substr()
的时间复杂度O(n),它的时间复杂度是O(1)。 - remove_prefix():移除前缀,将string_view的起始位置向后移动指定数量的字符。
- remove_suffix():移除后缀,将string_view的结束位置向前移动指定数量的字符。
- swap():交换两个string_view的内容。
- compare():比较两个视图是否相等。
- starts_with() :C++20新增,判断视图是否以以给定的前缀开始。
- ends_with():C++20新增,判断视图是否以给定的后缀结尾。
- contains():C++23新增,判断视图是否包含给定的子串。
这些成员函数与std::basic_string
的相同成员函数完全兼容,可以认为是对其调用的一层封装。不同于std::basic_string::data()
和字符串字面量,data()
可以返回指向非空终止的缓冲区的指针。
data()
示例:
#include <string_view>using namespace std::string_view_literals;int main() {std::string_view sv("hello, LionLong");std::cout << "sv = " << sv<< ", size() = " << sv.size()<< ", data() = " << sv.data() << std::endl;std::string_view sv2 = sv.substr(0, 5);std::cout << "sv2 = " << sv2<< ", size() = " << sv2.size()<< ", data() = " << sv2.data() << std::endl;std::string_view sv3 = "hello\0 LionLong"sv;//std::string_view sv4("hello\0 LionLong"sv)std::cout << "sv3 = " << sv3<< ", size() = " << sv3.size()<< ", data() = " << sv3.data() << std::endl;std::string_view sv4("hello\0 LionLong");std::cout << "sv4 = " << sv4<< ", size() = " << sv4.size()<< ", data() = " << sv4.data() << std::endl;
}
输出:
sv = hello, LionLong, size() = 14, data() = hello, LionLong
sv2 = hello, size() = 5, data() = hello, LionLong
sv3 = hello LionLong, size() = 14, data() = hello
sv4 = hello, size() = 5, data() = hello
可以看到data()
会返回的是起始位置的字符指针(const char*),以data()
返回值进行打印会一直输出直到遇到空字符。因此使用data()
需要非常小心。
max_size()
示例:
std::string_view sv;
std::cout << sv.max_size() << std::endl; //4611686018427387899
remove_prefix()
示例:视图的起始位置向后移动n位,收缩视图的大小。
std::string str = " hello";
std::string_view v = str;
v.remove_prefix(std::min(v.find_first_not_of(" "), v.size()));
std::cout << "String: '" << str << "', View : '" << v << << "'" << std::endl;
//输出
// String: ' hello', View : 'hello'
三、std::string_view为什么性能高?
-
std::string_view采用享元设计模式,通常以
ptr
和length
的结构来实现,非常轻便。 -
std::string_view上的字符串操作具有和std::string同类操作一致的复杂度。
-
std::string_view中的字符串操作大多数是
constexpr
的,都可在编译器执行,省去了运行时的复杂度。
四、std::string_view的使用陷阱
- 前面介绍
data()
函数的时候有提到过,data()
会返回的是起始位置的字符指针,若以其返回值进行输出打印,会一直输出直到遇到\0结束符。 std::string_view
不持有所指向内容的所有权,所以如果把std::string_view
局部变量作为函数返回值,则在函数返回后,内存会被释放,将出现悬垂指针或悬垂引用。- 由于
std::string_view
只是字符串数据的视图,并不拥有字符串数据,它不能用于修改原始字符串的内容。如果尝试修改std::string_view
所引用的字符串数据,将导致未定义行为。如果需要修改字符串数据,应该使用std::string
而不是std::string_view
。 - 当使用
std::string_view
时,需要注意空指针的风险。如果将一个空指针传递给std::string_view
,它的行为是未定义的。在使用std::string_view
之前,应该检查字符串指针是否为空,以避免潜在的问题。
std::string_view foo() {std::string s { "hello, LionLong" };return std::string_view { s };
}int main() {std::cout << foo() << std::endl; //可能的输出:=�;Vreturn 0;
}
五、std::string_view源码解析
//<string_view>
template<typename _CharT, typename _Traits = std::char_traits<_CharT>>
class basic_string_view
{
public:// typesusing traits_type = _Traits;using value_type = _CharT;using pointer = value_type*;using const_pointer = const value_type*;using reference = value_type&;using const_reference = const value_type&;using const_iterator = const value_type*;using iterator = const_iterator;using const_reverse_iterator = std::reverse_iterator<const_iterator>;using reverse_iterator = const_reverse_iterator;using size_type = size_t;using difference_type = ptrdiff_t;static constexpr size_type npos = size_type(-1);constexpr basic_string_view() noexcept: _M_len{0}, _M_str{nullptr}{ }constexpr basic_string_view(const basic_string_view&) noexcept = default;constexpr basic_string_view(const _CharT* __str) noexcept: _M_len{traits_type::length(__str)}, _M_str{__str}{ }constexpr basic_string_view(const _CharT* __str, size_type __len) noexcept: _M_len{__len}, _M_str{__str}{ }//...private:size_t _M_len;const _CharT* _M_str;
};
std::string_view
的实现并不复杂,在底层其实是一个非常简单的结构。std::string_view
通常由两个成员变量组成:
- 指向字符串数据的指针(通常是const char*)。
- 字符串数据的长度。
构造函数只是对这两个成员变量进行初始化。这两个成员变量使得std::string_view
能够表示字符串的范围,而不需要复制字符串数据。因此,它的创建和销毁成本非常低。
这两个成员变量使得std::string_view能够表示字符串的范围,而不需要复制字符串数据。因此,它的创建和销毁成本非常低。
看一下std::string_view
的几个成员函数实现:
//<string_view> class basic_string_view
constexpr const_pointer data() const noexcept
{return this->_M_str;
}constexpr void remove_prefix(size_type __n) noexcept
{__glibcxx_assert(this->_M_len >= __n);this->_M_str += __n;this->_M_len -= __n;
}constexpr void remove_suffix(size_type __n) noexcept
{this->_M_len -= __n;
}constexpr basic_string_view substr(size_type __pos = 0, size_type __n = npos) const noexcept(false)
{__pos = std::__sv_check(size(), __pos, "basic_string_view::substr");const size_type __rlen = std::min(__n, _M_len - __pos);return basic_string_view{_M_str + __pos, __rlen};
}
内部实现方面,std::string_view
的成员函数和操作符通常是非常轻量级的。底层实现原理相对简单,主要围绕着对指针和长度的操作展开。
六、总结
std::string_view
是C++17引入的一个非拥有的字符串视图类型,它提供了一种轻量级的方式来访问现有字符串数据。std::string_view
通过避免字符串复制和内存分配,它可以显著提高程序性能,并提供方便的字符串处理能力。但是,在使用过程中需要注意正确管理原始字符串的生命周期,以确保使用的字符串数据有效和安全。
相关文章:

深度解读C++17中的std::string_view:解锁字符串处理的新境界
深入研究C17中的std::string_view:解锁字符串处理的新境界 一、简介二、std::string_view的基础知识2.1、构造函数2.2、成员函数 三、std::string_view为什么性能高?四、std::string_view的使用陷阱五、std::string_view源码解析六、总结 一、简介 C中有…...

汇编基础-----常见命令基本使用
汇编基础-----常见命令基本使用 MOV:将数据从一个位置复制到另一个位置。 MOV destination, source例如: MOV RAX, RBX ; 将RBX寄存器中的值复制到RAX寄存器中ADD/SUB:将两个操作数相加或相减。 ADD destination, source SUB destinatio…...

科研学习|可视化——相关性结果的可视化
一、相关性分析介绍 相关性分析是指研究两种或者两种以上的变量之间相关关系的统计分析方法,一般分析步骤为: 1)判断变量间是否存在关联;2)分析关联关系(线性/非线性)、关联方向(正相…...

MapReduce过程解析
一、Map过程解析 Read阶段:MapTask通过用户编写的RecordReader,从输入的InputSplit中解析出一个个key/value。Map阶段:将解析出的key/value交给用户编写的Map()函数处理,并产生一系列的key/value。Collect阶段:在用户编…...

速看!这8道嵌入式面试题你都会吗?
大家好,我是知微! 正逢求职季,分享一些嵌入式面试当中经常会遇到的题目,希望这些干货对小伙伴们面试有用哦! 1、介绍一下static关键字的作用 在C语言中,static 关键字有几种不同的作用,根据其…...

基于SSM的电影网站(有报告)。Javaee项目。ssm项目。
演示视频: 基于SSM的电影网站(有报告)。Javaee项目。ssm项目。 项目介绍: 采用M(model)V(view)C(controller)三层体系结构,通过Spring SpringMv…...

SOCKS代理是如何提高网络性能和兼容性的?
SOCKS代理作为一种网络协议中间件,不仅在提升网络隐私和安全性方面发挥着重要作用,也在提高网络性能和兼容性方面有着不容忽视的影响🚀。本文将深入探讨SOCKS代理如何通过减少网络延迟🚀、优化数据传输🔄、提高跨平台兼…...

好菜每回味道不同--建造者模式
1.1 炒菜没放盐 中餐,老板需要每次炒菜,每次炒出来的味道都有可能不同。麦当劳、肯德基这些不过百年的洋快餐却能在有千年饮食文化的中国发展的那么好呢?是因为你不管何时何地在哪里吃味道都一样,而鱼香肉丝在我们中餐却可以吃出上…...

RuoYi-Cloud下载与运行
一、源码下载 若依官网:RuoYi 若依官方网站 鼠标放到"源码地址"上,点击"RuoYi-Cloud 微服务版"。 跳转至Gitee页面,点击"克隆/下载",复制HTTPS链接即可。 源码地址为:https://gitee.com/y_project/RuoYi-Cloud.git 点击复制 打开IDEA,选…...

Vue2.x计算属性
1.计算属性 在Vue 插值表达式内实现一些操作其实非常便利,但如果表达式的逻辑过于复杂,会让插值过于臃肿且难以维护。这时可以考虑使用Vue的计算属性 1.1 不使用计算属性的例子 <!DOCTYPE html> <html><head><meta charset"…...

Vue中使用require.context()自动引入组件和自动生成路由的方法介绍
目录 一、自动引入组件 1、语法 2、使用 2.1、在compoents文件下随便创建index.js文件 2.2、mian.js引入该js 二、自动生成路由 1、示例: 2、使用 2.1、在router文件下随便创建autoRouter.js文件 2.2、在router文件下index.js文件中引入autoRouter.js文件…...

【炒股Zero To Hero】MACD金叉死叉到底是否有效,加上这个指标回报率增加197倍
移动平均收敛散度(MACD - Moving Average Convergence Divergence)是一种趋势跟踪动量指标,显示了证券价格的两个移动平均之间的关系。它用于识别趋势的方向和强度,属于技术分析中振荡器的一类。 MACD如何衡量股票及其趋势 有两…...

Linux网络名称空间和虚拟机有何区别
在Linux系统中,网络名称空间和虚拟机都是实现资源隔离和虚拟化的技术,但它们在设计理念、实现机制、资源消耗、使用场景等方面存在着显著的区别。本文旨在全方位、系统性地分析这两种技术的区别。🔍 1. 设计理念与实现机制 1.1. 网络名称空…...

【UE Niagara】蓝图获取粒子数据
目录 效果 步骤 一、创建粒子 二、创建蓝图接收Niagara参数 效果 步骤 一、创建粒子 1. 新建一个Niagara发射器,使用Empty模板,打开后先添加“Spawn Rate”模块,这里设置粒子生成速率为0.7 在“Initialize Particle”模块中设置粒子颜色…...

更改el-cascade默认的value和label的键值
后端返回的树结构中,label的key不是el-cascade默认的label,我需要改成对应的字段,但是一直没有成功,我也在文档中找到了说明,但是我没注意这是在props中改,导致一直不成功 这是我一开始错误的写法…...

2024邮件工单系统排行揭晓:出海必备新宠
2024年各大榜单结果纷纷出炉,一起来看看2024十大邮件工单系统最新排行吧! 2024十大邮件工单系统 1、Zoho Desk;2、FreshDesk;3、Service Desk Plus;4、Help Scout;5、Helpshift;6、HongDans&am…...

java题目17:以m行n列二维数组为参数进行方法调用,分别计算二维数组各列元素之和,返回并输出计算结果(MethodCalls17)
每日小语 伟大企业的一项特质是“利润之上的追求”。——段永平 思考 方法调用 方法调用是通过在代码中使用方法名和参数列表来实现的。 public class MethodExample {public static void main(String[] args) {// 调用方法add,并传入两个参数int sum add(3, 5…...

Python中Python-docx 包的run介绍
先对run做一个简单地介绍。每个paragraph对象都包含一个run对象的列表。举例: 这是一个简短的段落。 from docx import Document doc Document("1.docx") #上面这段话保存在1.docx中 print("这一段的run个数是:",len(doc.paragr…...

vue2升级到vue3的一些使用注意事项记录(三)
更多ruoyi-nbcio功能请看演示系统 gitee源代码地址 前后端代码: https://gitee.com/nbacheng/ruoyi-nbcio 演示地址:RuoYi-Nbcio后台管理系统 http://122.227.135.243:9666/ 更多nbcio-boot功能请看演示系统 gitee源代码地址 后端代码:…...

SwiftUI Swift 显示隐藏系统顶部状态栏
Show me the code // // TestHideSystemTopBar.swift // pandabill // // Created by 朱洪苇 on 2024/4/1. //import SwiftUIstruct TestHideSystemTopBar: View {State private var isStatusBarHidden falsevar body: some View {Button {withAnimation {self.isStatusBa…...

PowerJob 分布式任务调度简介
目录 适用场景 设计目标 PowerJob 功能全景 任务调度 工作流 分布式计算 动态容器 什么是动态容器? 使用场景 可维护性和灵活性的完美结合 实时日志&在线运维 PowerJob 系统组件 PowerJob 应用场景 PowerJob 的优势 PowerJob(原OhMyScheduler&…...

Java——数组练习
目录 一.数组转字符串 二.数组拷贝 三.求数组中元素的平均值 四.查找数组中指定元素(顺序查找) 五.查找数组中指定元素(二分查找) 六.数组排序(冒泡排序) 七.数组逆序 一.数组转字符串 代码示例: import java.util.Arrays int[] arr {1,2,3,4,5,6}; String…...

波士顿房价预测案例(python scikit-learn)---多元线性回归(多角度实验分析)
波士顿房价预测案例(python scikit-learn)—多元线性回归(多角度实验分析) 这次实验,我们主要从以下几个方面介绍: 一、相关框架介绍 二、数据集介绍 三、实验结果-优化算法对比实验,数据标准化对比实验࿰…...

在 Queue 中 poll()和 remove()有什么区别?
在Java的Queue接口中,poll()和remove()方法都用于从队列中删除并返回队列的头部元素,但是它们在队列为空时的行为有所不同。 poll()方法:当队列为空时,poll()方法会返回null,而不会抛出异常。这是它的主要特点&#x…...

实现鼠标在页面点击出现焦点及大十字星
近段时间,在完成项目进度情况显示时候,用户在操作鼠标时候,显示当鼠标所在位置对应时间如下图所示 代码实现步骤如下: 1.首先引用 jquery.1.7.js 2.再次引用raphael.js 3.然后引用graphics.js 4.最后引用mfocus.js 其中mfocu…...

如何在 7 天内掌握C++?
大家好,我是小康,今天我们来聊下如何快速学习 C 语言。 本篇文章适合于有 C 语言编程基础的小伙伴们,如果还没有学习过 C,请看这篇文章先入个门:C语言快速入门 引言: C,作为一门集面向过程和…...

FineBI概述
FineBI是一种商业智能(BI)软件,旨在帮助企业从数据中获取见解并做出更明智的业务决策。以下是FineBI的详细概述: 功能特性: 数据连接与整合:FineBI可以连接到各种数据源,包括数据库、数据仓库、…...

百度Create AI开发者大会剧透丨用好三大AI神器 ,人人都是开发者
程序员会消失,真的吗?大模型的下一站是什么?开发者的机会在哪里?什么才是最好用的AI应用开发工具?在4月16日举办的2024百度Create AI开发者大会上,百度创始人、董事长兼首席执行官李彦宏将就这些备受瞩目的…...

外包干了17天,技术倒退明显
先说情况,大专毕业,18年通过校招进入湖南某软件公司,干了接近6年的功能测试,今年年初,感觉自己不能够在这样下去了,长时间呆在一个舒适的环境会让一个人堕落! 而我已经在一个企业干了四年的功能…...

Unity类银河恶魔城学习记录12-8 p130 Skill Tree UI源代码
Alex教程每一P的教程原代码加上我自己的理解初步理解写的注释,可供学习Alex教程的人参考 此代码仅为较上一P有所改变的代码 【Unity教程】从0编程制作类银河恶魔城游戏_哔哩哔哩_bilibili UI.cs using UnityEngine;public class UI : MonoBehaviour {[SerializeFi…...