EffectiveModernC++读书笔记

 

本篇博客是Scott Meyers的EffectiveModernC++读书笔记.刚写了几章笔记,还没写完.可以这里下载本书pdf,密码6aoo.本博客的目的是1)方便大家快速刷一下本书;2)博主备忘.  

 

第一章 Deducing Types.

Item 1: Understand template type deduction

观察下面的例子,在调用*cout<<f1(a)<<endl;*的时候程序员不需要显示标明类型,C++进行了类型自动推理

template <typename T>
T f1(T p)
{
	p = p + 1;
	return p;
}

int main()
{
	double a = 3;
	cout << f1(a) << endl;
	return 0;
}

本章讲了一些推理的情况,属于手册形式.推理大部分是符合直觉的,不过如果你的直觉和设计C++的大神不一样,就得改一下直觉,哈哈.书中有这几种情况

1) param type is reference or point, but not universal reference.

例子1

template<typename T>
void f(T& param); // param is a reference

int x = 27; f(x); //推理成 int&
const int cx = x;f(cs); //推理成 const int&
const int& rx = x;f(rx); //推理成 const int&

例子2

template<typename T>
void f(const T& param); // param is now a ref-to-const
int x = 27; f(x);// 推理成 const int&
const int cx = x; f(cs); // 推理成 const int&
const int& rx = x; f(rx); // 推理成 const int&

例子3

template<typename T>
void f(T* param); // param is now a pointer
int x = 27; f(&x);// 推理成 int*
const int *px = &x; f(px) // 推理成 const int*

2) param type is universal reference, universal reference是这个样子 T&&, 是右值引用的意思,参考stl中move的方法.

例子1

template<typename T>
void f(T&& param); // param is now a universal reference
int x = 27; // as before
const int cx = x; // as before
const int& rx = x; // as before
f(x); // x是左值,推理成 int&
f(cx); // cx是左值,推理成 const int
f(rx); // rx是左值,推理成 const int&
f(27); // 27是右值,推理成 int&&

3) ParamType is Neither a Pointer nor a Reference

例子1

template<typename T>
void f(T param); // param is now passed by value

int x = 27; // as before
const int cx = x; // as before
const int& rx = x; // as before
f(x); // 推理成 int
f(cx); // 推理成 int
f(rx); // 推理成 int

ps:这里有些反直觉

例子2

template<typename T>
void f(T param); // param is still passed by value
const char* const ptr = "Fun with pointers";// ptr is const pointer to const object
f(ptr); // 推理成const char* 

ps: const让人头疼,不过这里感觉确实应该这样推理.

4) array

例子1

template<typename T>
void f(T param); // template with by-value parameter

const char name[] = "J. P. Briggs"; // name's type is const char[13]
f(name);  //推理成 const char*

例子2

template<typename T>
void f(T& param); // template with by-reference parameter

const char name[] = "J. P. Briggs"; // name's type is const char[13]
f(name);  //推理成 const char& [13]

ps:如果我们想的数组的长度,可以:
 

int name[10];

//方法1:
int length_name = sizeof(name)/sizeof(name[0]);
//方法2
int length_name2 = std::size(name);

//这个std::size的源码是这样
template<class _Ty,size_t _Size> 
inline constexpr size_t size(const _Ty(&)[_Size]) _NOEXCEPT
{    
    // get dimension for array
    return (_Size);
}

5) Function Arguments

例子1

void someFunc(int, double); // someFunc is a function;type is void(int, double)
template<typename T>
void f1(T param); // in f1, param passed by value
template<typename T>
void f2(T& param); // in f2, param passed by ref
f1(someFunc);  //推理成 void (*)(int, double)
f2(someFunc); // 推理成 void (&)(int, double)

本节总结:感觉是一些C++冷知识.

Item 2: Understand auto type deduction

auto用法

auto x = 3; 
//等价于
int x = 3;

1)

auto可以用上一节的原理来推

auto x = 27;
//相当于这种写法
template<typename T> 
void func_for_x(T param); 
func_for_x(27); 
				
const auto cx = x;
//相当于这种写法
template<typename T> 
void func_for_cx(const T param); 
func_for_cx(x); 
				
const auto& rx = x;
//相当于这种写法
template<typename T> 
void func_for_rx(const T& param); 
func_for_rx(x); 

例子1

	auto x = 27; //等价于 int x = 27;
	const auto cx = x; //等价于 const int cx = x;
	const auto& rx = x; //等价于 const int& rx = x;

	auto&& uref1 = x; //等价于 int& uref1 = x;
	auto&& uref2 = cx; //等价于 const int& uref2 = x;
	auto&& uref3 = 27; //等价于int&& uref3 = 27;

	const char name[] = "R. N. Briggs";
	auto arr1 = name; //等价于 const char*
	auto& arr2 = name; // 等价于 const char (&)[13]

	void someFunc(int, double); // someFunc is a function type is void(int, double)
	auto func1 = someFunc; // 等价于 void (*)(int, double)
	auto& func2 = someFunc; // 等价于 void (&)(int, double)

Item 3: Understand decltype

decltype这个关键字这样用

decltype(auto) f1()
{
	int x = 0;
	return x; // decltype(x) is int, so f1 returns int
}
decltype(auto) f2()
{
	int x = 0;
	return (x); // decltype((x)) is int&, so f2 returns int&
}

decttype返回变量的类型, 下面是举例

const int i = 0;			// decltype(i) is const int
bool f(const Widget& w);	// decltype(w) is const Widget& ,decltype(f) is bool(const Widget&), decltype(f(w)) is bool
struct Point {
int x, y; // decltype(Point::x) is int
}; 
Widget w; // decltype(w) is Widget
vector<int> v; // decltype(v) is vector<int>
if (v[0] == 0) // decltype(v[0]) is int&

返回模板类型

template<typename Container, typename Index> 
auto authAndAccess(Container& c, Index i) 
{ 
	return c[i]; 
}

体会一下这句话

*auto specifies that the type is to be deduced, and decltype says that decltype rules should be used during the deduction.*

看一下下面这个例子

template<typename Container, typename Index> 
auto authAndAccess_rvalue(Container& c, Index i) 
{
	return c[i];
}

template<typename Container, typename Index> 
decltype(auto) authAndAccess_ref(Container& c, Index i) 
{ 
	return c[i]; 
}

int main()
{
	int w;
	vector<int> buf(3);
	for (auto &value : buf) value = 1;
	int a = authAndAccess_rvalue<vector<int>, int>(buf, 1);  //这个是右值
	authAndAccess_ref<vector<int>, int>(buf, 1) = 3;    //这个是引用
	return 0;
}

 看一下这个例子

Widget w;
const Widget& cw = w;
auto myWidget1 = cw; // auto type deduction,myWidget1's type is Widget
decltype(auto) myWidget2 = cw; // decltype type deduction,myWidget2's type is const Widget&

ps: decltype有些反直觉,用的时候还是自己测试一下左值还是右值 

Item 4: Know how to view deduced types.

IDE Editors

下面例子推理成什么呢

const int theAnswer = 42;
auto x = theAnswer;
auto y = &theAnswer;

这是我用vs2015提示的结果

const int theAnswer = 42;
auto x = theAnswer;    //提示为int
auto y = &theAnswer;    //提示为 const int*

据说有些太复杂的逻辑IED也会推理错,所以建议太复杂自己也弄不清的时候还是换个写法吧,要是领导也看不懂说你dull怎么办呢.

如果编译器弹出问题,可以分析下也可以改一个简单的写法

实在不行,可以打印一下类型

#include <iostream>
#include <vector>

using namespace std;

int main()
{
	int x = 0;
	cout << typeid(x).name() << endl;
	return 0;
}

书中还说有时候typeid也不好使,推荐用boost库的方法

#include <boost/type_index.hpp>
template<typename T>
void f(const T& param)
{
	using std::cout;
	using boost::typeindex::type_id_with_cvr;
	// show T
	cout << "T = "<< type_id_with_cvr<T>().pretty_name()<< '\n';
	// show param's type
	cout << "param = "<< type_id_with_cvr<decltype(param)>().pretty_name()<< '\n';
}


 

第二章 auto

Item 5: Prefer auto to explicit type declarations.

atuo的变量必须进过初始化

int x1; // 未初始化
auto x2; // error! initializer required
auto x3 = 0; // fine, x's value is well-define

auto 和 function比较,auto速度快一些

auto derefLess = // C++14 comparison
    [](const auto& p1, // function for
    const auto& p2) // values pointed
    { return *p1 < *p2; }; // to by anything pointer-like
std::function<bool(const std::unique_ptr<Widget>&,
    const std::unique_ptr<Widget>&)>
    derefUPLess = [](const std::unique_ptr<Widget>& p1,
                        const std::unique_ptr<Widget>& p2)
                        { return *p1 < *p2; };

例子1 std::vector<int>::size_type在32位系统写时32位,在64位系统下是64位

#include <iostream>
#include <vector>

using namespace std;

int main()
{
	std::vector<int> v;
	unsigned sz = v.size();  //这种写法如果数组的内容条数超时32位的范围,在64位系统就会产生错误
    auto asz = v.size(); //这种写法看起来高端一些
	return 0;
}

 例子2:下边例子写法1没有写法2好的原因

#include <iostream>
#include <unordered_map>

using namespace std;

int main()
{
	std::unordered_map<std::string, int> m;
	
	//写法1
	for (const std::pair<std::string, int>& p : m)
	{
	}

	//写法2
	for (const auto& p : m)
	{
	}
	return 0;
}

unordered_map的key是 const std::string, 但是循环中是std::string, 所以变态的编译器会偷偷的创建一个临时变量,这样就浪费性能了,如果用auto,编译器和人类都省事了.书中在这个小结中说到

相比python,c++是有些难用,但是对于高手来说是没啥问题的.我看到想说,python真香.作者: 如果代码看起来不直观,你就看一下IDE的提示,保持一些抽象还是必要滴,重构的时候也方便.^_^也许高手和我们不是同一种生物吧.

Item 6: Use the explicitly typed initializer idiom when auto deduces undesired types

例子1

class Widget {/*Some features*/};
std::vector<bool> features(const Widget& w);// Define priority function
bool highPriority = features(w)[5];  // Get priority
processWidget(w, highPriority);      // According to priority, carry out relevant processing

但是如果写成auto, 我在vs2015下测试就没有得到预期结果

#include <iostream>
#include <vector>

using namespace std;

std::vector<bool> features(const int& w)
{
	vector<bool> buf(10);
	for (auto &value : buf) value = true;
	return buf;
}

int main()
{
	int w;
	auto highPriority = features(w)[5];  //注意,highPriority会赋值为false,下面的写法会改正确
	return 0;
}

书中说应该写成下面这种写法就对了

#include <iostream>
#include <vector>

using namespace std;

std::vector<bool> features(const int& w)
{
	vector<bool> buf(10);
	for (auto &value : buf) value = true;
	return buf;
}

int main()
{
	int w;
	auto highPriority = static_cast<bool>(features(w)[5]);  //这里highPriority是true了
	return 0;
}

*bool highPriority = features(w)[5];* 这行代码  features(w)[5] 会返回引用对象,我们用bool去接,编译器会自动把bool值copy过来.

但是如果改用auto, features(w)[5] 是一个 tmp object 的 index 5, 返回的是 *dangling pointer*, c++标注对这种情况是*未定义*.

例子2

Matrix sum = m1 + m2 + m3 + m4;

每一个 + 的结果不直接回 Matrix type,而是类似 Sum<Matrix, Matrix> ,整个则是 Sum<Sum<Sum<Matrix, Matrix>,Matrix>,Matrix>。

这样的 invisible proxy classes 通常和 ‘auto’ 处得不好。所以要避免用 auto 去接。若真的要用 ‘auto’ 去接也要使用 static_cast<type> 来避免 dangling pointer。例如:

auto sum = static_cast<Matrix>(m1 + m2 + m3 + m4);

 

第三章 moving to modern C++

如果本书只看一张的话,那就是本章了.赶紧看一下吧.

Item 7: Distinguish between () and {} when creating objects.

c++的初始化居然可以四种写法,这里有孔乙己的画面.

int x(0); // initializer is in parentheses
int y = 0; // initializer follows "="
int z{ 0 }; // initializer is in braces
int z = { 0 }; // initializer uses "=" and braces

注意这几个的不同

Widget w1; // call default constructor
Widget w2 = w1; // not an assignment; calls copy ctor
w1 = w2; // an assignment; calls copy operator=

 估计你也不知道 constructor/copy constructor/copy operator=的不同

#include<iostream>  
#include<stdio.h>  
  
using namespace std;  
  
class Test  
{  
    public:  
    Test() {}  //this is default constructor
    Test(const Test &t)  
    {  
        cout<<"Copy constructor called "<<endl;  
    }  
      
    Test& operator = (const Test &t) 
    { 
        cout<<"Assignment operator called "<<endl; 
        return *this; 
    }  
};  
  
// Driver code 
int main()  
{  
    Test t1, t2;  
    t2 = t1;  
    Test t3 = t1;  
    getchar();  
    return 0;  
}  

这几种初始化方法还不能混用

class Widget {
private:
    int x{ 0 }; // fine, x's default value is 0
    int y = 0; // also fine
    int z(0); // error!
};

std::atomic<int> ai1{ 0 }; // fine
std::atomic<int> ai2(0); // fine
std::atomic<int> ai3 = 0; // error!

 作者说如果这时你傻掉了,那就用{}, 他把{}称为*Uniform initialization*. {}, 英文叫 braced initialization, 它的作用是禁止narrowing conversions among built-in types.

double x, y, z;
int sum1{ x + y + z }; // error! sum of doubles may not be expressible as int
int sum2(x + y + z); // okay (value of expression truncated to an int)
int sum3 = x + y + z; // okay

 c++有一个vexing(烦人)的地方(作者在书中用这个词)

Widget w1(10); // call Widget constructor with argument 10
Widget w2(); //???, 是调用无参数构造函数,还是调动一个叫w2的函数
Widget w3{}; //这种写法就好了,只能是调用无参数构造函数

使用 brace initialization有个需要注意的地方是,如果类定义了initializer_list, 就会优先调用initilazer_list的函数.比如这个

class Widget {
public:
	Widget(int i, bool b); // as before
	Widget(int i, double d); // as before
	Widget(std::initializer_list<long double> il); // added
};

Widget w1(10, true); // uses parens and, as before,calls first ctor
Widget w2{ 10, true }; // uses braces, but now calls std::initializer_list ctor	(10 and true convert to long double)
Widget w3(10, 5.0); // uses parens and, as before,calls second ctor
Widget w4{ 10, 5.0 }; // uses braces, but now calls std::initializer_list ctor (10 and 5.0 convert to long double)

如果你对上面有疑问,说明你对 initializer_list这个关键字不清楚. initializer_list是一种构造函数, 例如这样构造

vector<int> v1 = {1,2,3,4}; 

上面的内部逻辑是:

1 创建临时变量 std::initializer_list<T> l = {1,2,3,4}

2 v1.insert(v1.end(), l.begin(), l.end())

例子1

#include <iostream>
#include <vector>
#include <initializer_list>

template <class T>
struct S {
	std::vector<T> v;
	S(std::initializer_list<T> l) : v(l) {
		std::cout << "constructed with a " << l.size() << "-element list\n";
	}
};

int main()
{
	S<int> s = { 1, 2, 3, 4, 5 }; // copy list-initialization
}

输出: constructed width a 5 element list

例子2

struct myclass {
  myclass (int,int);
  myclass (initializer_list<int>);
  /* definitions ... */
};

myclass foo {10,20};  // calls initializer_list ctor
myclass bar (10,20);  // calls first constructor

 参数列表只要可以记性 narrowing conversions, 编译器就会调用 initializer_list, 如果initializer_list是string, 你传int或double,编译器就能避免再调用string的initializer_list.

class Widget {
public:
	Widget(int i, bool b); // as before
	Widget(int i, double d); // as before
							 // std::initializer_list element type is now std::string
	Widget(std::initializer_list<std::string> il);
	
}; // conversion funcs
Widget w1(10, true); // uses parens, still calls first ctor
Widget w2{ 10, true }; // uses braces, now calls first ctor
Widget w3(10, 5.0); // uses parens, still calls second ctor
Widget w4{ 10, 5.0 }; // uses braces, now calls second ctor

 如果传一个空的{}, 这时也有initilizer_list, C++规定此时调用默认构造函数

class Widget {
public:
	Widget(); // default ctor
	Widget(std::initializer_list<int> il); // std::initializer_list ctor
}; // conversion funcs

int main()
{
	Widget w1; // calls default ctor
	Widget w2{}; // also calls default ctor
	Widget w3(); // most vexing parse! declares a function!
}

如果我此时就是想调用initializer_list constructor, 可以这样

Widget w4({}); // calls std::initializer_list ctor with empty list
Widget w5{{}}; // calls std::initializer_list ctor with empty list

 关于vector用法的举例

std::vector<int> v1(10, 20); // 插入10个int,值为20
std::vector<int> v2{10, 20}; // 插入10和20这两个int

书中在这里提醒: 如果你重构的时候增加一个intializer_list constructor, 就会产生一些问题,要注意. 

Things to Remember


• Braced initialization is the most widely usable initialization syntax, it prevents narrowing conversions, and it’s immune to C++’s most vexing parse.
• During constructor overload resolution, braced initializers are matched to std::initializer_list parameters if at all possible, even if other constructors offer seemingly better matches.
• An example of where the choice between parentheses and braces can make a significant difference is creating a std::vector<numeric type> with two arguments.
• Choosing between parentheses and braces for object creation inside templates can be challenging.

 

Item 8: Prefer nullptr to 0 and NULL.

本章作者推荐使用关键字 nullptr, 能用nullptr就不用0或NULL

Things to Remember
• Prefer nullptr to 0 and NULL.
• Avoid overloading on integral and pointer types

Item 9: Prefer alias declarations to typedefs.

新的概念*alias declarations*

typedef void (*FP)(int, const std::string&); // typedef 
using FP = void (*)(int, const std::string&); // alias declaration

但是 *alias declarations*支持模板用法

template<typename T> // MyAllocList<T>
using MyAllocList = std::list<T, MyAlloc<T>>; // is synonym for std::list<T,MyAlloc<T>>
MyAllocList<Widget> lw; // client code

//使用typedef就比较麻烦了
template<typename T> 
struct MyAllocList { 
	typedef std::list<T, MyAlloc<T>> type; 
}; 
MyAllocList<Widget>::type lw; // client code

Things to Remember
• typedefs don’t support templatization, but alias declarations do.
• Alias templates avoid the “::type” suffix and, in templates, the “typename”
prefix often required to refer to typedefs.
• C++14 offers alias templates for all the C++11 type traits transformations.
 

Item 10: Prefer scoped enums to unscoped enums

 

推荐使用新的enum语法,新的enum语法有范围

enum Color { black, white, red };
auto white = false; // error! white already declared in this scope

enum Class Color { black, white, red };
auto white = false; // fine, no other "white" in scope
Color c = white; // error! no enumerator named "white" is in this scope
Color c = Color::white; // fine
auto c = Color::white;  // also fine 

Things to Remember
• C++98-style enums are now known as unscoped enums.
• Enumerators of scoped enums are visible only within the enum. They convert to other types only with a cast.
• Both scoped and unscoped enums support specification of the underlying type. The default underlying type for scoped enums is int. Unscoped enums have no default underlying type.
• Scoped enums may always be forward-declared. Unscoped enums may be forward-declared only if their declaration specifies an underlying type
 

Item 11: Prefer deleted functions to private undefined ones.

对容易混淆的参数调用,可以使用delete关键字

class Man {
public:
	bool isLucky(int number) { return true; }; // original function
	bool isLucky(char) = delete; // reject chars
	bool isLucky(bool) = delete; // reject bools
}; 

int main()
{
	Man lili;
	lili.isLucky(1);
	lili.isLucky(true); //编译错误
}

可以对模板中具体的类进行delete

class Widget {
public:
	
	template<typename T>
	void processPointer(T* ptr)
	{
		…
	}
};
template<> 
void Widget::processPointer<void>(void*) = delete; 

Things to Remember

• Prefer deleted functions to private undefined ones.
• Any function may be deleted, including non-member functions and templateinstantiations.
 

Item 12: Declare overriding functions override

 

推荐使用overrdid关键字,如果自己继承错误了,会收到编译器的警告

class Base {
public:
	virtual void mf1() const;
	virtual void mf2(int x);	
};

class Derived : public Base {
public:
	virtual void mf1() const override;
	virtual void mf2(double x) override;  //这里会有警告,编译器找不到能继承的函数
}; 


Item 13: Prefer const_iterators to iterators

 

在使用iterator的地方,要考虑一下是不是可以用const_iterator.只有好处没有坏处.

例子: 在一个vector中找到20, 如果找到了,就再它前面插入0,如果没有找到,就再vector最后插入0

#include <iostream>
#include <vector>

using namespace std;

int main()
{
	vector<int> vec_buf = { 1,2,3,4 };
	vector<int>::const_iterator it = find(vec_buf.cbegin(), vec_buf.cend(), 20);
	vec_buf.insert(it, 0);
}


Item 14: Declare functions noexcept if they won’t emit exceptions.

使用noexcept关键字,编译器会给额外的优化,函数速度会更快一点点.

int f(int x) throw(); // no exceptions from f: C++98 style
int f(int x) noexcept; // no exceptions from f: C++11 style

增加noexcept关键字,在设计上也看起来高大上了,调用者也会感觉nice. 注意用上这个关键字不是说编译器就不会产生exception了,而是开发者告诉编译器,我保证这个函数不会有异常,你给我再优化一下吧. 

Things to Remember
• noexcept is part of a function’s interface, and that means that callers may depend on it.
• noexcept functions are more optimizable than non-noexcept functions.
• noexcept is particularly valuable for the move operations, swap, memory deallocation functions, and destructors.
• Most functions are exception-neutral rather than noexcept.
 

Item 15: Use constexpr whenever possible.

constexpr 关键字可以让函数变成宏,在编译期算出结果

constexpr int foo(int i)
{
	return i * 5.0;
}

int main()
{
	foo(5)  //返回常量,编译时计算
	int i = 10;
	foo(i); //返回非常量,运行时计算
}


Item 16: Make const member functions thread safe

 

mutable 关键字

如果一个类是const的,类的成员就不能改变了,如果我想让类的其中一个成员改变,就可以把这个成员声明成mutable

#include <iostream>
using namespace std;
code
class Test {
   public:
      int a;
   mutable int b;
   Test(int x=0, int y=0) {
      a=x;
      b=y;
   }
   void seta(int x=0) {
      a = x;
   }
   void setb(int y=0) {
      b = y;
   }
   void disp() {
      cout<<endl<<"a: "<<a<<" b: "<<b<<endl;
   }
};
int main() {
   const Test t(10,20);
   cout<<t.a<<" "<<t.b<<"\n";
   // t.a=30; //Error occurs because a can not be changed, because object is constant.
   t.b=100; //b still can be changed, because b is mutable.
   cout<<t.a<<" "<<t.b<<"\n";
   return 0;
}

atomic 关键字

一个比mutex更底层的关键字,比mutex底层,不blockthread, 没有上下文切换,速度会快一点点.

#include <iostream>
#include <vector>
#include <thread>
#include <atomic>

using namespace std;

atomic<int> g_sum = 0;
void SumNumbers(const vector<int>& toBeSummed, const int idxStart, const int idxEnd)
{
	for (int i = idxStart; i <= idxEnd; ++i)
	{
		g_sum += toBeSummed[i];
	}
}

int main()
{
	vector<int> toBeSummed;
	for (int i = 0; i < 30000; ++i)
	{
		toBeSummed.push_back(rand());
	}

	int sum = 0;
	for (int i = 0; i < toBeSummed.size(); ++i)
	{
		sum += toBeSummed[i];
	}

	cout << "sum = " << sum << endl;

	thread t1(SumNumbers, toBeSummed, 0, 9999);
	thread t2(SumNumbers, toBeSummed, 10000, 19999);
	thread t3(SumNumbers, toBeSummed, 20000, 29999);

	t1.join();
	t2.join();
	t3.join();

	cout << "sum = " << g_sum << endl;
	return 0;
	
}


Item 17: Understand special member function generation.
 

下面是这本书的目录,我还没有看后边的.希望我不要太监.

第四章 Smart Pointers

第五章 Rvalue References, Move Semantics, and Perfect Forwarding

第六章 Lambda Expressions

第七章 The Concurrency API

第八章 Tweaks

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值