cpp►全面分析官网stringstream类

stringstream类:http://www.cplusplus.com/reference/sstream/stringstream
类层次关系:

ios_base
ios
istream
ostream
ifstream
ofstream
iostream
stringstream

class std::stringstream
typedef basic_stringstream<char> stringstream;
我们通过VS2019找到定义std这个命名空间的地方,有很多文件(如下图底部文件列表),比如D:\ProgramFiles…\include\string,我们知道使用string类对象时,类型为std::string。在这里就只看看std<iostream>部分源代码,其他类似:

其中,_STD_BEGIN_STD_END的定义如下:

#define _STD_BEGIN namespace std {
#define _STD_END   }

可见,cin、cout、cerr…都是在std名称空间下的。所以用它们时得加前缀std::,避免命名冲突的产生。
关于__PURE_APPDOMAIN_GLOBAL的注释:

process-global is the default for code built with /clr or /clr:oldSyntax.
appdomain-global is the default for code built with /clr:pure.
Code in MSVCM is built with /clr, but is used by user code built with /clr:pure so it must conform to the expectations of /clr:pure clients.
Use __PURE_APPDOMAIN_GLOBAL when a global needs to be appdomain-global for pure clients and process-global for mixed clients.
————————————————————————————
process-global是使用/clr/clr:oldSyntax构建的代码的默认值。
appdomain-global是使用/clr:pure构建的代码的默认值。
MSVCM中的代码是用/clr构建的,但是被用/clr:pure构建的用户代码使用,所以它必须符合/clr:pure客户端的期望。
对于纯客户端需要全局变量是appdomain-global,对于混合客户端需要的是process-global,这时使用__PURE_APPDOMAIN_GLOBAL。

extern关键字:
可置于变量或者函数前,以表示变量或者函数的定义在别的文件中。提示编译器遇到此变量或函数时,在其它模块中寻找其定义,另外,extern也可用来进行链接指定。extern用在变量声明中常常有这样一个作用:你要在.c文件中引用另一个文件中的一个全局的变量,那就应该放在.h中用extern来声明这个全局变量。声明可以多次,定义只能一次。声明变量时不会分配内存,定义变量时才会分配内存。

extern int a;//声明一个全局变量a
int a; //定义一个全局变量a

extern int a = 0;//定义一个全局变量a,并给初值。
int a = 0;// 同上

对于函数而言,性质一样:

// X.cpp文件
static unsigned long int next = 1;// 本文件内可见
int rand(void) {
	next = next * 1103515245 + 12345;
    return (unsigned int) (next/65536) % 32768;
}

// Y.cpp文件
#include <xxx>
extern unsigned int rand(void);
int main() { ...

在链接期,extern的含义则是告诉链接器(Linker):我这里使用了一个不是自己定义的变量,老兄你得把它找出来并且让我的代码可以使用这个变量。那么这时链接器会干什么呢?它会在你指定的范围里到处寻找这个变量的具体定义,找到这家伙被真正分配的存储空间,然后让所有通过extern声明使用它的家伙都得到这个空间的地址,从而能够真正的使用到它。比如在M.cpp中有代码extern typename X;并且在函数中使用了此X,则链接器会找到定义X的地方,比如在N.cpp或P.h文件中。
对于__PURE_APPDOMAIN_GLOBAL extern ostream cout;就是说cout是在别的文件中定义的对象。

在VS2019里面点击声明的stringstream对象,查看源码,部分源代码如下:

using关键字:
想象下面这个场景:

typedef std::map<std::string, int> map_int_t;
typedef std::map<std::string, std::string> map_str_t;

我们需要的其实是一个固定以 std::string 为 key 的 map,它可以映射到 int 或另一个 std::string。然而这个简单的需求仅通过 typedef 却很难办到。因此,在 C++98/03 中往往不得不这样写(模板结构体):

template <typename Val>
struct str_map
{
    typedef std::map<std::string, Val> type;
};
//...
str_map<int>::type map1;
str_map<std::string>::type map2;

一个虽然简单但却略显烦琐的 str_map 外敷类是必要的。这明显让我们在复用某些泛型代码时非常难受。
现在,在 C++11 中终于出现了可以重定义一个模板的语法。如下示例:

template <typename Val>
using str_map_t = std::map<std::string, Val>;
//...
str_map_t<int> map1;
str_map_t<std::string> map2;

实际上,using 的别名语法覆盖了 typedef 的全部功能。
使用using与使用typedef对比示例如下:

typedef unsigned int uint_t;// 重新定义unsigned int
using uint_t = unsigned int;

typedef void (*func_t)(int, int);// 函数指针
using func_t = void (*)(int, int);

对于源代码中的:

using stringstream  = basic_stringstream<char, char_traits<char>, allocator<char>>;

stringstreambasic_stringstream<...>的类型别名。
先看basic_stringstream<...>,有尖括号说明它是一个模板类,尖括号里面是这个类的成员要绑定的类型,此类成员要绑定的类型有三个:charchar_traits<char>allocator<char>,其中后两个类型本身也是模板类型。
关于模板,可以先阅读《C Primer Plus》Blog之“函数探幽””,了解基本概念。
模板函数
我们知道对于std::stringstd::wstring,它们的定义如下:

using string  = basic_string<char, char_traits<char>, allocator<char>>;
using wstring = basic_string<wchar_t, char_traits<wchar_t>, allocator<wchar_t>>;

所以basic_stringstream的类型绑定列表与string是一模一样的。
继续阅读源码:

// STRUCT char_traits<char> (FROM <string>)
template <>
struct char_traits<char> : _Narrow_char_traits<char, int> {}; // properties of a string or stream char element

可以看到结构体char_traits继承结构体_Narraw_char_traits。C++中的struct对C中的struct进行了扩充,它已经不再只是一个包含不同数据类型的数据结构了,它已经获取了太多的功能。struct能包含成员函数吗? 能!struct能继承吗?能!!struct能实现多态吗?能!!!
_Narrow_char_traits
可以看到_Narrow_char_traits是一个模板结构体。大致可以窥探它的内容,如下:
char_traits 字符特征类:
1)意义:包装特定串元素的通用行为界面,以便容器实现时依据特征信息而执行特定行为。
2)定义了通用类型名:
typedef _Elem char_type;
typedef int int_type;
typedef streampos pos_type;
typedef streamoff off_type;
typedef mbstate_t state_type;
其中 int_type 表示字符元素转换到特定编码时的整型表示,pos_type, off_type 分别作为字符串索引和字符串元素偏移的类型,类似容器迭中的指针,迭代类型和指针,迭代器的偏移类型。最后的 state_type 用于存储流状态,如出错,格式控制等等。
3)定义了字符 / 字符串操作的包装界面,以便通用算法的调用:
assign(a, b) 定义将 b 字符赋值给 a 字符的过程,实现 a.operator = 的行为。
eq(a, b) 定义 a 字符和 b 字符的相等关系,实现 a.operator == 的行为。
lt(a, b) 定义 a 小于 b 的关系,实现 a.operator < 的行为。
compare(a_ptr, b_ptr, cnt) 定义两组字符串的比较,返回 int 类型,实现类似 memcmp 的行为。
length(ptr) 定义取字符串长度,实现类似 strlen 的行为。
copy(a_ptr, b_ptr, cnt) 定义两组字符串的复制,实现类似 memcpy 的行为。
move(a_ptr, b_ptr, cnt) 定义两组字符串的不重叠复制,实现类似 memmove 的行为。
assign(ptr, cnt, ch) 定义了填充字符串的过程,实现类似 memset 的行为。
to_int_type(ch) 定义了 char_type 到 int_type 整型的转换过程。
to_char_type(n) 定义了 int_type 到 char_type 字符型的转换过程。
eq_int_type(a, b) 定义两个和当前 char_type 类型对应的 int_type 的相等关系。
eof() 定义字符串结尾符,使用整型表示。
not_eof(n) 定义非字符串结尾符,若输入结尾符,则返回 1,其他输入返回原值,即总是不返回 eof()。
4)int_type 类型应是当前字符类型的整型编码。

我们再来看看allocator<char>

分配器(allocator)是定义由标准库的某些部分使用的内存模型的类,尤其是STL容器使用的内存模型。
本节描述默认分配器模板分配器。如果没有指定最后一个(可选的)模板参数,这是所有标准容器都将使用的分配器,也是标准库中唯一的预定义分配器。
————————————————————
还可以定义其他分配器。任何allocator_traits为其生成有效实例化的类Alloc都可以用作标准容器上的分配器(Alloc可以通过成员函数实现,也可以不实现)。
除了析构函数之外,标准默认allocator类模板的任何成员都不应引入数据竞争。对分配或释放存储的成员函数的调用应该以单一的总顺序发生,而且每次这样的释放都应该在这个顺序中的下一次分配(如果有的话)之前发生。
————————————————————
从技术上讲,分配器描述的内存模型可能专用于要分配的每种类型的对象,甚至可以存储与它们一起使用的每个容器的本地数据。尽管使用默认分配器不会发生这种情况。
可以阅读Blog:“标准库 STL :Allocator能做什么?”

最后,直接在VS2019里面代码跳进basic_stringstream,直接看看代码片段,不作解释:
basic_stringstream以上就理清了大致的std::stringstream代码流。

接着,看看官网对std::stringstream的描述:

操作字符串的流类
该类的对象使用包含字符序列的字符串缓冲区(string buffer)。这个字符序列可以通过成员str作为string对象直接访问。
可以使用输入流(input)和输出流(output)上允许的任何操作插入和/或提取字符。

以下是一个具有的模板参数的basic_stringstream的实例化:

模板参数定义
charT(成员别名char_type)char
traits(成员别名traits_type)char_traits< char>
Alloc(成员别名allocator_type)allocator< char>

除了内部字符串缓冲区之外,这些类的对象还保留一组从ios_base、ios和istream继承来的内部字段:

字段成员函数描述
格式格式标志flags、setf、unsetf一组内部标记,影响某些输入/输出操作的解释或生成方式。具体查看fmtflags
字段位宽width要插入的下一个格式化元素的宽度。
显示精度precision插入的下一个浮点值的十进制精度。
本地化getloc、imbue函数用于受本地化属性影响的格式化输入/输出操作的区域设置对象。
填充字符fill将格式化字段填充到字段宽度(width)的字符。
状态错误状态rdstate、setstate、clear流的当前错误状态。单个值可以通过调用good、eof、fail和bad来获得。具体查看iostate
异常掩码exceptions抛出失败异常的状态标志。具体查看iostate
其他回调栈register_callback指向某些事件发生时调用的函数的指针的堆栈。
扩展数组iword、pword、xalloc用于存储long和void*类型对象的内部数组。
绑定流tie指向输出流的指针,在该流上的每个I/O操作之前刷新。
字符流rdbuf指向关联的streambuf对象的指针,该对象负责所有输入/输出操作。
字符数gcount上次未格式化输入操作读取的字符数。

成员类型:

成员类型定义
char_typechar
traits_typechar_traits< char>
allocator_typeallocator< char>
int_typeint
pos_typestreampos
off_typestreamoff

这些成员类型都是从它的基类istream、ostream、ios_base继承而来:

成员类型描述
event表明事件类型(public member type)
event_callback函数事件回调类型(public member type)
failure流异常的基类(public member class)
fmtflags流格式标示类型(public member type)
Init初始化标准流对象(public member class)
iostate流状态标志类型(public member type)
openmode流打开模式标志类型(public member type)
seekdir流寻位置方向的标志类型(public member type)
sentry(istream)准备的输入流(public member class)
sentry(ostream)准备的输出流(public member class)

公有成员函数:

成员函数描述
(constructor)构造函数
strget/set内容
operator=(C++11)移动赋值运算符
swap(C++11)内部交换

继承来的函数非成员函数重载(C++11):

非成员函数描述
swap交换字符串流(模板函数)

我们每个列表的成员/函数大至选几个描述一下:
————————————————————
public member type(公共成员类型
std::ios_base::event
enum event;
此成员枚举类型在ios_base中定义为:

enum event { erase_event, imbue_event, copyfmt_event };

在用ios_base::register_callback注册的函数中用作第一个形参的类型。此参数标识触发函数调用的事件的类型。

触发事件
copyfmt_event在调用ios::copyfmt时(此时所有格式标志都已复制,但在异常掩码之前)。
erase_event在调用流析构函数时调用(也在ios::copyfmt开头调用)。
imbue_event调用ios_base::imbue(就在函数返回之前)。

——————————
public member type(公共成员类型
std::ios_base::event_callback
为成员register_callback注册的回调函数类型。
它被定义为ios_base的成员类型:

typedef void (*event_callback) (event ev, ios_base& obj, int index);

成员类型而非成员函数或类:(event_callback是一个ios_base类的一个成员:函数指针对象)
member type
——————————
public member function(公共成员函数
std::ios_base::register_callback
void register_callback (event_callback fn, int index);
注册fn作为回调函数,当流事件发生时,使用index作为参数自动调用。
如果注册了多个回调函数,它们都将被调用,顺序与注册的顺序相反。
回调函数的类型应该可以转换为event_callback。调用它的表达式等价于:

(*fn)(ev,stream,index)

——————————
public member class(成员类
std::ios_base::failure
class failure;
这个内嵌类继承自system_error,并作为标准输入/输出库元素抛出异常的基类。
它的定义如下:

class ios_base::failure : public system_error {
public:
  explicit failure (const string& msg, const error_code& ec = io_errc::stream);
  explicit failure (const char*   msg, const error_code& ec = io_errc::stream);
}

这些错误通常被分类在iostream_category(如果它们与库的操作相关)或system_category(如果错误来自于系统)中。尽管细节是由实现定义的。
库实现可以使用io_errc类型的值来方便地标识iostream_category的错误条件。
成员方法(C++11):
code:获取错误码;
what:获取异常相关信息。

#include <iostream>
#include <fstream>
int main() {
    std::ifstream f("doesn't exist");
    try {
        f.exceptions(f.failbit);
    } catch (const std::ios_base::failure& e) {
        std::cout << "Caught an ios_base::failure.\n"
                  << "Explanatory string: " << e.what() << '\n'
                  << "Error code: " << e.code() << '\n';
    }
}
Caught an ios_base::failure.
Explanatory string: ios_base::clear: unspecified iostream_category error
Error code: iostream:1

———————————————
std::ios_base::openmode
表示流打开模式标志的位掩码类型。此类型的值可以是以下成员常量的任何有效组合:

成员常量文件打开模式
app(append)在每次输出操作之前,将流的位置指示器设置到流的末尾。
ate(at end)将流的位置指示器设置为打开时流的末尾。
binary(binary)将流视为二进制而不是文本。
in(input)允许在流上进行输入操作。
out(output)允许在流上进行输出操作。
trunc(truncate)假设打开时长度为零,则丢弃任何当前内容。

这些常量在ios_base类中定义为公共成员。因此,可以通过名称将它们直接称为ios_base的成员(例如ios_base::in),也可以使用它们的任何继承的类或实例化的对象(例如ios::atecout.out)来引用它们。
———————————————
std::istream::sentry
class sentry; (sentry:[ˈsentri]:哨兵)
在每个输入操作之前和之后执行一系列操作的成员类。
它的构造函数对作为其实参传递的流对象执行以下操作(顺序相同):
如果它的任何内部错误标志(internal error flags)被设置,函数将设置它的failbit标志并返回。
如果是绑定流(tied stream),函数将刷新绑定到它的流(如果输出缓冲区不为空)。类可以为库函数实现一些方法,将此刷新延迟到其关联的流缓冲区的下一个调用溢出。
如果设置了它的skipws格式标志,并且构造函数没有作为第二个参数传递true (noskipws),则提取并丢弃所有前导空白字符(特定于区域设置)。如果该操作耗尽了字符源,该函数将同时设置failbit和eofbit内部状态标志。
如果在构建过程中出现故障,它可以设置流的failbit标志。
它的析构函数不需要执行任何操作。但是实现可以使用sentry对象的构造和销毁在所有输入操作通用的流上执行额外的初始化或清除操作。
所有执行输入操作的成员函数都会自动构造该类的对象,然后对其求值(如果没有设置状态标志,则返回true)。只有当该对象的计算结果为true时,函数才尝试输入操作(否则,它将不执行该操作而返回)。在返回之前,函数破坏了sentry对象。
操作符>>格式的输入操作通过将false作为第二个参数(跳过前导空格)来构造sentry对象。所有其他构造sentry对象的成员函数都将true作为第二个参数传递(不会跳过前导空格)。
它的定义如下:

// C++98
class sentry {
public:
  explicit sentry (istream& is, bool noskipws = false);
  ~sentry();
  operator bool() const;
private:
  sentry (const sentry&);             // not defined
  sentry& operator= (const sentry&);  // not defined
};

// C++11
class sentry {
public:
  explicit sentry (istream& is, bool noskipws = false);
  ~sentry();
  explicit operator bool() const;
  sentry (const sentry&) = delete;
  sentry& operator= (const sentry&) = delete;
};
// istream::sentry example
#include <iostream>     // std::istream, std::cout
#include <string>       // std::string
#include <sstream>      // std::stringstream
#include <locale>       // std::isspace, std::isdigit
struct Phone {
  std::string digits;
};
// custom extractor for objects of type Phone
std::istream& operator>>(std::istream& is, Phone& tel) {
    std::istream::sentry s(is);
    if (s) while (is.good()) {
      char c = is.get();
      if (std::isspace(c,is.getloc())) break;
      if (std::isdigit(c,is.getloc())) tel.digits+=c;
    }
    return is;
}
int main () {
  std::stringstream parseme ("   (555)2326");
  Phone myphone;
  parseme >> myphone;
  std::cout << "digits parsed: " << myphone.digits << '\n';
  return 0;
}

digits parsed: 5552326

——————————————————————————————
std::stringstream::str
string str() const; // get
void str(const string& s); // set

// stringstream::str
#include <string>       // std::string
#include <iostream>     // std::cout
#include <sstream>      // std::stringstream, std::stringbuf
int main () {
  std::stringstream ss;
  ss.str("Example string");
  std::string s = ss.str();
  std::cout << s;
  return 0;
}
Example string

————————————————————
std::istream::seekg
istream& seekg(streampos pos);
istream& seekg(streamoff off, ios_base::seekdir way);

// 在本例中,seekg用于将位置移动到文件的末尾,然后返回到开始。
#include <iostream>     // std::cout
#include <fstream>      // std::ifstream
int main () {
  std::ifstream is ("test.txt", std::ifstream::binary);
  if (is) {
    // get length of file:
    is.seekg(0, is.end);
    int length = is.tellg();
    is.seekg(0, is.beg);
    // allocate memory:
    char * buffer = new char[length];
    // read data as a block:
    is.read(buffer, length);
    is.close();
    // print content:
    std::cout.write(buffer, length);
    delete[] buffer;
  }
  return 0;
}

————————————————————
std::ostream::operator<<
arithmetic types (1) :
ostream& operator<< (bool val);
ostream& operator<< (short val);
ostream& operator<< (unsigned short val);
ostream& operator<< (int val);
ostream& operator<< (unsigned int val);
ostream& operator<< (long val);
ostream& operator<< (unsigned long val);
ostream& operator<< (long long val);
ostream& operator<< (unsigned long long val);
ostream& operator<< (float val);
ostream& operator<< (double val);
ostream& operator<< (long double val);
ostream& operator<< (void
val);
stream buffers (2) :
ostream& operator<< (streambuf* sb );
manipulators (3) :
ostream& operator<< (ostream& (*pf)(ostream&));
ostream& operator<< (ios& (*pf)(ios&));
ostream& operator<< (ios_base& (pf)(ios_base&));

应用于输出流的这个操作符(<<)称为插入操作符。它作为成员函数重载如下:

(1)算术类型
生成一个表示val的字符序列,根据区域设置和流中选择的其他格式化设置进行正确格式化,并将它们插入到输出流中。
在内部,该函数通过首先构造一个sentry对象来访问输出序列。然后(如果合适的话),它调用num_put::put(使用流选择的语言环境)来执行格式化和插入操作,相应地调整流的内部状态标志。最后,它在返回之前销毁了sentry对象。
(2)流缓冲区
从sb所指向的流缓冲区对象控制的输入序列中检索尽可能多的字符(如果有的话),并将它们插入流中,直到输入序列耗尽或函数无法插入流。
在内部,该函数通过首先构造一个sentry对象来访问输出序列。然后(如果合适的话),将字符插入到其关联的流缓冲区对象中,就像调用其成员函数sputc一样,并最终在返回之前销毁sentry对象。
(3)操纵符
调用pf(*this),其中pf可能是一个操纵符。
操作符是专门设计用于与该操作符一起使用时调用的函数。
此操作对输出序列没有影响,也不插入任何字符(除非操纵符本身会这样做,如endl或ends)。

// example on insertion
#include <iostream>     // std::cout, std::right, std::endl
#include <iomanip>      // std::setw
int main () {
  int val = 65;
  std::cout << std::right;       // right-adjusted (manipulator)
  std::cout << std::setw(10);    // set width (extended manipulator)
  std::cout << val << std::endl; // multiple insertions
  return 0;
}
        65

————————————————————
std::ios::good
bool good const;
函数定义:

bool ios::good() const {
  return rdstate() == goodbit;
}
// error state flags
#include <iostream>     // std::cout, std::ios
#include <sstream>      // std::stringstream
void print_state(const std::ios& stream) {
  std::cout << " good()=" << stream.good();
  std::cout << " eof()=" << stream.eof();
  std::cout << " fail()=" << stream.fail();
  std::cout << " bad()=" << stream.bad();
}
int main () {
  std::stringstream stream;
  // void clear(iostate state = goodbit);
  // Sets a new value for the stream's internal error state flags.
  stream.clear(stream.goodbit);
  std::cout << "goodbit:"; print_state(stream); std::cout << '\n';
  stream.clear(stream.eofbit);
  std::cout << " eofbit:"; print_state(stream); std::cout << '\n';
  stream.clear(stream.failbit);
  std::cout << "failbit:"; print_state(stream); std::cout << '\n';
  stream.clear(stream.badbit);
  std::cout << " badbit:"; print_state(stream); std::cout << '\n';
  return 0;
}
goodbit: good()=1 eof()=0 fail()=0 bad()=0
 eofbit: good()=0 eof()=1 fail()=0 bad()=0
failbit: good()=0 eof()=0 fail()=1 bad()=0
 badbit: good()=0 eof()=0 fail()=1 bad()=1

————————————————————
std::ios_base::flags
fmtflags flags() const; // get
fmtflags flags(fmtflags fmtfl);
//set

// modify flags
#include <iostream>     // std::cout, std::ios
int main () {
  std::cout.flags(std::ios::right | std::ios::hex | std::ios::showbase);
  std::cout.width(10);
  std::cout << 100 << '\n';
  return 0;
}
      0x64

————————————————————
function template(函数模板)
std::stringstream::swap
void swap (stringstream& x);
在x和*this之间交换所有内部数据。
在内部,函数调用iostream::swap,然后调用关联的stringbuf对象的成员swap。

// swapping stringstream objects
#include <string>       // std::string
#include <iostream>     // std::cout
#include <sstream>      // std::stringstream
int main () {
  std::stringstream foo;
  std::stringstream bar;
  foo << 100;// 将int类型的值插入到输出流中,形成流缓冲
  bar << 200;
  swap(foo, bar);
  int val;
  foo >> val;// 从stringstream中抽取前面插入的int类型的流缓冲中的值,赋给int类型
  std::cout << "foo: " << val << '\n';
  bar >> val; 
  std::cout << "bar: " << val << '\n';
  return 0;
}
foo: 200
bar: 100

stringstream构造函数如下:

default (1) :
explicit stringstream (ios_base::openmode which = ios_base::in | ios_base::out);
initialization (2):
explicit stringstream (const string& str,
ios_base::openmode which = ios_base::in | ios_base::out);
copy (3)
stringstream (const stringstream&) = delete;
move (4)
stringstream (stringstream&& x);

最后看一个使用stringstream的例子:

#include <iostream>
#include <string>
#include <sstream>
using namespace std;
int main () {
  string mystr;
  float price = 0;
  int quantity = 0;
  cout << "Enter price: ";
  getline(cin, mystr);
  stringstream(mystr) >> price;
  cout << "Enter quantity: ";
  getline(cin, mystr);
  stringstream(mystr) >> quantity;
  cout << "Total price: " << price*quantity << endl;
  return 0;
}
Enter price: |22.25
Enter quantity: |7
Total price: 155.75

在本例中,我们间接地从标准输入中获取数值:我们不是直接从cin中提取数值,而是将其中的行提取到字符串对象(mystr)中,然后将该字符串中的值提取到变量price和quantity中。一旦这些是数值,就可以对它们执行算术运算,例如将它们相乘以获得总价格。
使用这种方法的整个线路和提取它们的内容,我们分开的过程从其解释用户输入数据,允许用户输入过程预计,同时获得更多的控制其内容转换成有用的数据的程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

itzyjr

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

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

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

打赏作者

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

抵扣说明:

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

余额充值