C++11新特性学习

1.auto关键字

1.1 自动推断变量类型

  • auto关键字是c++11推出的在编译器编译期间自动推导类型的辅助关键字,包含以下条件。①auto必须要初始化r
  • 支持普通变量自动推导
auto x = 10;
auto y = 3.14;
auto z = true;
  • 支持模板代码简化
template<typename T>
void test(T t)
{
    auto x = t + 1}
  • 支持迭代器类型自动推断
std::vector<int> test = { 1,2,3 };
for (auto it = test.begin(); it != test.end(); ++it)
{
    //
}
  • 支持普通函数和lambda函数返回类型推导
auto func = [](int a, int b) { return a + b; };

struct T{
int _x, _y;
};
auto func()
{
	return T{1, 2};
}

auto不能使用的地方:

  • 不能在函数参数中使用
  • 不能定义数组
  • 不能用于模板参数
  • 不能用于类的非静态成员变量

1.2 auto与模板函数集合使用

template<typename T>
auto max(T a, T b) -> decltype (a > b ? a : b)
{
    return a > b ? a : b;
}

  • decltype关键字用于选择并返回操作数的数据类型,编译器会分析表达式并得到它的类型,却并不实际计算表达式的值。

2.列表初始化

2.1 内置类型初始化

  • C++11相比于C++98来说允许支持使用花括号{}对元素或者数组进行统一初始化
void test1()
{
    // 普通变量初始化
	int x{ 0 };
	// 数组初始化
	int arr1[]{ 1,2,3,4,5 };
	int arr2[5]{ 0 };
	// new表达式初始化
	int* p = new int[3] {1, 2, 3};
}

2.2 自定义类型和标准容器初始化

  • C++对struct进行了C兼容,因此即便没有定义相应的构造函数,也可以使用C风格进行初始化,因此使用new初始化也可以通过。
  • 对于class来说,需要定义带参构造函数,因为列表初始化会执行带参构造函数。test2相比于test1来说会多出临时构造和拷贝构造的过程会造成浪费,可以使用explicit 关键字防止隐式转换发生。
struct Point {
	int _x;
	int _y;
};

class Test {
private:
	int _x;
	int _y;
public:
	explicit Test(int _x, int _y) {
		this->_x = _x;
		this->_y = _y;
	}
};

void test2()
{
    // 结构体初始化
	Point point1{ 1,2 };
	Point point2[3]{ {1,2},{2,3},{3,4} };
	Point *p = new Point[2]{{1,2}, {3,4}};
	Point p3 = {4,5};
	// 自定义类初始化,会调用构造函数
	Test test1{ 1,2 };
	// 编译报错,使用explicit关键字不允许隐式转换
	// Test test2 = {3,4};
	// 标准容器初始化
	std::vector<int> v1{ 1,2,3,4,5 };
	std::map<int, int> { {1, 2}, { 2,3 }, { 3,4 }};
	std::vector<int>v2{ v1 };
}

2.3 std::initializer_list

文档链接:initializer介绍

  • 标准容器之所以可以使用初始化列表进行元素赋值,主要是因为其构造函数添加了std::initializer_list轻量级类模板来实现这个功能。
void Test()
{
	// 数组
	int arr1[]{ 1,2,3 };
	// map初始化列表
	std::map<std::string, int> mm{ {"1",1},{"2",2},{"3",3} };
	// set初始化列表
	std::set<int> ss = { 1,2,3 };
	// vector初始化列表
	std::vector<int> arr2 = { 1,2,3,4,5 };
	// 赋值,重载了operator=
	arr2 = { 6,7,8,9,10 };
}
	
// map构造函数示例
map(initializer_list<value_type> _Ilist) : _Mybase(key_compare()) {
        insert(_Ilist);
}
// set 构造函数示例
 set(initializer_list<value_type> _Ilist) : _Mybase(key_compare()) {
        this->insert(_Ilist);
}  
// vector沟槽函数示例
vector(initializer_list<_Ty> _Ilist, const _Alloc& _Al = _Alloc())
        : _Mypair(_One_then_variadic_args_t{}, _Al) {
        _Construct_n(_Convert_size<size_type>(_Ilist.size()), _Ilist.begin(), _Ilist.end());
}
  • 通过自定义类的构造函数添加对std::initializer_list 的支持也能使其拥有初始化任意长度的能力。
  • 此外也可以在自定义函数中传递同类型的对象集合
template<class T>
class Test {
public:
    Test(std::initializer_list<T> list)
    {
        for (auto it = list.begin(); it != list.end(); ++it)
        {
            _content.push_back(*it);
        }
    }

    int size() {
        return _content.size();
    }

    friend std::ostream& operator<<(std::ostream& out, const Test<T>& t) {
        for (int i = 0; i < t._content.size(); i++) {
            out << t._content[i] << " ";
        }
        return out;
    }

private:
    std::vector<T> _content;
};

void main()
{
	Test<int> test = { 1,2,3,4,5,6 };
	std::cout << test << std::endl;
	return;
}
  • 需要注意的是std::initializer_list只是存储了对象的引用,并不负责保存或者拷贝初始化列表中的元素
std::initializer_list<int> func(void)
{
    int a = 1, b = 2;
    return { a, b }; // a、 b 在返回时并没有被拷贝
}
  • 这种情况下应该使用具有拷贝和转移的语义代替std::initializer_list,如
std::vector<int> func(void)
{
    int a = 1, b = 2;
    return { a, b };
}

3.nullpt空指针

  • nullptr是c++11引入的新特性,代表一个空指针常量,比NULL0更加安全。
void foo(int);
void foo(char*);
// 在c++98中,下面由于NULL存在类型转换的问题,会编译出错
foo(NULL);
// c++11引入nullptr关键字比NULL更安全,并确定编译器调用的是foo(char*)
foo(nullptr);

4.移动语义

4.1 移动语义基础

  • 移动语义是c++11提出的新特性,可以实现快速资源转移。
  • c++的赋值和传递参数操作一般会伴随着大量的内容复制过程,期间多次拷贝函数的调用使得内存和时间消耗较多,导致较大的性能损失。
std::vector<int> v1 = { 1,2,3 };
std::vector<int> v2(v1);
std::vector<int> v3 = move(v1);
  • 这段代码中v1是源对象,v2v3是目标对象,其中v2会调用拷贝构造函数使用v1来初始化自己,而v3则可以通过移动语法快速将v1的资源转移给自己,从而提高了性能。
std::vector<int> v1 = { 1,2,3 };
std::cout << "移动前: " << v1.size() << std::endl;
std::vector<int> v2(v1);
std::vector<int> v3 = move(v1);

std::cout << "移动后: " << v1.size() << std::endl;
std::cout << v2.size() << std::endl;
std::cout << v3.size() << std::endl;

在这里插入图片描述
可以发现,移动后,v1的资源转移给了v3

4.2 移动语义的实现

c++移动语义是通过重载"="操作符移动构造函数来实现的。

4.3 完美转发

完美转发的目的是如果函数收到左值就触发左值函数,接收到右值就触发右值函数,但事实上由于退化现象的存在,右值会退化车成左值,导致无法触发右值。

void Func(int& x) {
    std::cout << "左值引用" << std::endl;
}
void Func(const int& x) {
    std::cout << "const 左值引用" << std::endl;
}
void Func(int&& x) {
    std::cout << "右值引用" << std::endl;
}
void Func(const int&& x) {
    std::cout << "const 右值引用" << std::endl;
}

template<typename T>
void PefectForward(T&& t) {
    Func(t);
}

void main()
{
    PefectForward(10);
    int a;
    PefectForward(a);
    PefectForward(std::move(a));
    const int b = 8;
    PefectForward(b);
    PefectForward(std::move(b));
    return;
}

在这里插入图片描述
为了解决这一现象,c++11提供std::forward<T>(t)来进行完美转发,保持右值属性不变。

//...
// 修改为如下
template<typename T>
void PefectForward(T&& t) {
    Func(std::forward<T>(t));
}

在这里插入图片描述

5.委托构造函数

class Test {
public:
    Test() : Test(0, 0) {} // 委托构造函数
    Test(int x, int y) : _x(x), _y(y){}
    
private:
    int _x, _y;
};

  • 委托构造函数通过在一个构造函数中调用另一个构造函数来实现此功能,这一点有点类似于C#中的构造函数调用静态构造一样,方便初始化。

6.类别名

6.1 typedef

typedef int mInt;
mInt i = 10;

6.2 using

using mInt = int;
mInt i = 10;

7.范围for

  • 给出一个容器,传统遍历方法使用迭代器,比如
void main()
{
    // c++98遍历迭代器方法
    std::vector<int> vec1 = { 1,2,3,4,5 };
    for(auto it = vec1.begin();it != vec1.end();it++)
        // ...
    

    // c++11简化for方法
    for(auto it : vec1)
        // ...
    return;
}

8.lambda表达式

[capture list](parameter list) mutable exception->
     return type { function body }
  • capture list: 用于捕获外部变量的列表,可以省略。
  • paramter list: 函数参数列表,可以省略。
  • mutable: 可选项,用于指定能否修改捕获的变量。
  • exception: 可选项,用于指定能抛出的异常。
  • return type: 可选,指定返回值类型。
  • function body: 函数主体。
[capture list] (parameters) -> return type {function body}

示例代码: addsub函数

auto add = [](int a, int b) -> int {return a + b; };
auto sub = [](int a, int b) -> int {return a - b; };
std::cout << add(1, 2) << std::endl;
std::cout << sub(4, 3) << std::endl;

稍微复杂一点

struct Got {
    std::string key;
    int price;
};


void main()
{
 
    auto up = [](const Got& left, const Got& right) -> bool {return left.price > right.price; };
    auto down = [](const Got& left, const Got& right) -> bool {return left.price < right.price; };

    Got gots[] = { {"Alpha", 10}, {"Beta", 11}, {"Cycle", 12}, {"Delete", 13} };
    std::sort(gots, gots + sizeof(gots) / sizeof(gots[0]),
        up);
    for (auto& got : gots) {
        std::cout << got.key << ":" << got.price << std::endl;
    }
    std::cout << "----------------------------------------" << std::endl;
    std::sort(gots, gots + sizeof(gots) / sizeof(gots[0]),
        down);
    for (auto& got : gots) {
        std::cout << got.key << ":" << got.price << std::endl;
    }
    return;
}

在这里插入图片描述

  • 通过捕获列表交换两个变量的值
int a = 10, b = 20;
std::cout << "a: " << a << " " << "b: " << b << std::endl;
auto swap = [&a, &b]()mutable
{
	int c = a;
	a = b;
	b = c;
};
swap();
std::cout << "a: " << a << " " << "b: " << b << std::endl;

在这里插入图片描述

9.强制默认生成default和禁止默认生成delete

  • 使用default关键字显式提供默认构造函数
class Test {
public:
	// 不写defult就会报错,因为提供了构造函数,那么默认构造就会失效,除非使用default关键字
	Test() = default;
	Test(const char* name, int age = 0) :_name(name), _age(age) {
		
	}
private:
	std::string _name;
	int _age;
};


void main()
{

	Test t1("test", 10);
	Test t2;

	return;
}
  • 使用delete关键字删除Test的默认拷贝构造函数
class Test {
public:
	// 不写defult就会报错,因为提供了构造函数,那么默认构造就会失效,除非使用default关键字
	Test() = default;
	Test(const Test& t) = delete;
	Test(const char* name, int age = 0) :_name(name), _age(age) {
		
	}
private:
	std::string _name;
	int _age;
};


void main()
{

	Test t1("test", 10);
	Test t2;
	// 报错,因为默认拷贝构造函数使用delete关键字删除了
	// Test t3(t1);
	return;
}

10.右值引用

在以往中,我们一般常用的是左值引用,c++11引入了一种新的引用类型称之为右值引用,右值引用主要包括以下两种情况

  1. 由函数或语句返回的将亡的临时对象
  2. std::move标记的非const对象
  • 右值引用的出现是为了减少局部对象在将亡时发生多次拷贝构造造成的内存和时间消耗而出现的
  • 使用右值引用和移动语义可以减少不必要的拷贝,但并不能消除从临时对象到传递的实参之间的拷贝
std::string getName() {
    return "Test";
}

void printName(std::string&& name) {
    std::cout << "Name: " << name << std::endl;
}

int main() {
    std::string name = getName();                // 获取临时对象
    printName(std::move(name));                   // 将临时对象的所有权转移给右值引用
    return 0;
}
  • 上述getName()返回的临时对象仍然会触发外部name的拷贝构造,而printName中的传参时产生的拷贝构造确实被减少了拷贝次数

另一个例子

class MyString {
public:
    // 默认构造函数
    MyString() : data(nullptr), length(0) {}

    // 带参构造函数
    MyString(const char* str) {
        length = std::strlen(str);
        data = new char[length + 1];
        std::strcpy(data, str);
    }

    // 移动构造函数
    MyString(MyString&& other) noexcept {
        length = other.length;
        data = other.data;

        // 将临时对象置于有效但未指向任何资源的状态
        other.length = 0;
        other.data = nullptr;
        std::cout << "触发移动构造" << std::endl;
    }

    // 析构函数
    ~MyString() {
        delete[] data;
    }

    // 获取字符串长度
    int getLength() const {
        return length;
    }

    // 获取字符串内容
    const char* getData() const {
        return data;
    }

private:
    char* data;
    int length;
};

MyString createString() {
    MyString str("Hello World");
    return str;
}

int main() {
    MyString str = std::move(createString()); // 接收返回的将亡的临时对象

    std::cout << "Length: " << str.getLength() << std::endl;
    std::cout << "Data: " << str.getData() << std::endl;

    return 0;
}
  • 上述情况下,使用移动构造将createString返回的临时对象使用移动构造赋值给了str

11.静态断言

静态断言是C++11引入的一种编译期断言。它在编译期间评估一个常量表达式,并在表达式为false时生成编译错误。静态断言的语法是:

static_assert(expression, message);
  • 编译错误
int main() {
    const int a = 10;
    const int b = 20;
    static_assert(a + b == 32, "4 + 5 should be equals 9");

    return 0;
}
  • 编译通过
int main() {
    const int a = 10;
    const int b = 20;
    static_assert(a + b == 30, "4 + 5 should be equals 9");

    return 0;
}

断言通过表达语句和结果是否匹配进行判断,当不匹配时,会引发编辑器编译错误

12.智能指针

智能指针的出现是为了方便管理程序在整个生命周期中的内存分配,因为传统的非智能指针很容易引发内存泄露的问题,这是一个及其严重的问题,会导致系统崩溃或者系统假死。

避免方法:

  • 确定好内存的使用计划,确保单元模块执行完毕后,内存得到释放
  • 使用内存管理工具,比如智能指针来管理内存,确保规范
  • 使用内存检测工具,实时关注内存的动态变化,在程序运行期间防止内存泄露
  • 在代码测试和审查阶段尤其关注内存的管理

12.1 shared_ptr

12.1.1 原理和代码

12.2 unique_ptr

12.2.1 原理和代码

12.3 weak_ptr

12.3.1 原理和代码

13.并发线程库

c++11重要以及大多数现在仍在使用的特性就是支持了线程,使得c++在并行编程时不需要依赖与第三方库,并从操作系统中引入了原子类的概念,条件是必须包含<thread>头文件->thread头文件查看
线程库中使用的最多的就是多线程条件变量互斥锁原子操作

函数名称对应功能
thread()构造一个线程对象,没有关联任何线程函数,即没有启动任何线程
thread(fn,args1, args2,…)构造一个线程对象,并关联线程函数fn,args1,args2,…为线程函数的线程
get_id()获取线程id
jionable()线程是否还在执行,joinable代表的是一个正在执行中的线程
join()该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行
detach()在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程变为后台线程,创建的线程的"死活"就与主线程无关

需要注意的是

  • 真正的线程数量和CPU的核心数量有关,超出这个数量的线程不一定会让程序处理速度变快
  • 创建一个线程时需要提供一个可执行的线程函数。如果没有提供线程函数,则不会创建新的线程,当前线程也不会被占用。
  • 线程函数一般通过函数指针lambda表达式函数对象三种方式提供
  • thread类时防拷贝的,不允许拷贝构造以及赋值,可以进行移动构造和移动赋值,可以将当前线程对象的关联状态转移给其他线程对象,转移期间不影响线程的执行。

一个简单的实例,创建t1线程后,提供CirclePrint给该函数,然后执行循环打印的功能

void CirclePrint(int x) {
    for (int i = 0; i < x; i++) {
        std::cout << i << std::endl;
    }
}
int main() {
    

    int a = 10;
    std::thread t1(CirclePrint, a);
    t1.join();

    return 0;
}

通过引用传值来修改某些变量的值一定要使用std::ref关键字,我们无法直接修改主线程中a变量的值,因为a在传递给线程函数之前就已经被复制了一份到新线程中,该线程有独立的栈,为了解决这个问题,需要使用std::ref关键字指明该变量传递的是在主线程中的引用而不是副本。

std::ref将一个对象转换为对该变量转换为引用类型,并将其传递给线程函数,以便在协程函数中访问原变量,防止编译器复制该对象的副本,实现多线程并发时的数据共享。

  • 需要注意的是,使用std::move一定要确保生命周期准确,防止出现垂悬引用的问题,在创建新线程的时候使用std::ref,避免主线程访问已经被销毁的对象。
  • 另一种解决办法是使用指针作为参数,传主线程中变量的地址过去
void Change(int& x) {
    x += 12;
}
void Change(int* a) {
    *a += 10;
}
int main() {
    

    int a = 10;
    int b = 10;
    // 使用std::ref
    std::thread t1(Change, std::ref(a));
    // 使用&传递参数的地址
    std::thread t2(Change, &b);
    t1.join();
    std::cout << a << std::endl;
    return 0;
}
  • 使用容器存放多个线程
void CirclePrint(int x) {
    for (int i = 0; i < x; i++) {
        std::cout << i << std::endl;
    }
}

int main() {
    int n = 10;
    std::vector<std::thread> v_threads(n);
    for (auto& t : v_threads) {
        t = std::thread(CirclePrint, 10);
        std::cout << "线程id为" << t.get_id() << std::endl;
    }
    for (auto& t : v_threads) {
        t.join();
    }
    return 0;
}
  • 使用两个线程对变量进行++
int x = 0;
void Func(int number) {
    for (int i = 0; i < number; i++) {
        ++x;
    }
}

int main() {
    
    
    std::thread t1 = std::thread(Func, 10);
    std::thread t2 = std::thread(Func, 20);
    t1.join();
    t2.join();

    std::cout << x << std::endl;

    return 0;
}

在这里插入图片描述
看似这段代码没问题,当增加的数字达到一定级别的数量时,会引发线程安全问题。

int x = 0;
void Func(int number) {
    for (int i = 0; i < number; i++) {
        ++x;
    }
}

int main() {
    
    
    std::thread t1 = std::thread(Func, 100000);
    std::thread t2 = std::thread(Func, 200000);
    t1.join();
    t2.join();

    std::cout << x << std::endl;

    return 0;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
如结果所示,多次执行的记过不同,这是因为当某一时刻线程t1拿到x进行增加操作后,还没来得及放回数据,线程t2就直接拿数据了进行增加了,这时虽然触发了两次增加操作,但是实际上x只增加了一次,这样就引出了线程安全问题。

void Func(int number) {
    for (int i = 0; i < number; i++) {
   
        if (i % 1000 == 0) {
            std::cout << std::this_thread::get_id() << "->" << x << std::endl;
        }
        ++x;
    }
}

在这里插入图片描述
打印线程id后,发现有多次两个线程同时打印的情况,为解决这一问题需要进行加锁。

13.1 加锁解决

传统c++98解决这一问题的方法是使用互斥量库<mutex>
`c++```包括四个互斥量的种类
请添加图片描述

  • mutex: 互斥锁
  • timed_mux: 带超时机制的互斥锁
  • recursive_mutex: 递归互斥锁
  • recursive_timed_mutex: 带超时机制的递归互斥锁

mutex 是最常用的基本互斥量,包括三个常用的函数

函数函数功能
lock()上锁:锁住互斥量 如果互斥锁是未锁定状态,调用lock()成员函数的线程会得到互斥锁的所有权,并将其上锁。如果互斥锁是锁定状态,调用lock()成员函数的线程就会阻塞等待,直到互斥锁变成未锁定状态。
unlock()解锁:释放对互斥量的所有权
try_lock()上锁:如果互斥锁是未锁定状态,则加锁成功,函数返回true, 如果互斥锁是锁定状态,则加锁失败,函数立即返回false。(线程不会阻塞等待)

使用std::mutex创建一个互斥量mtx后,在Func中使用lock()unlock()进行加锁和解锁保证线程安全

int x = 0;
std::mutex mtx;
void Func(int number) {
    for (int i = 0; i < number; i++) {
        mtx.lock();
        if (i % 1000 == 0) {
            std::cout << std::this_thread::get_id() << "->" << x << std::endl;
        }
        ++x;
        mtx.unlock();
    }
}

int main() {
    
    
    std::thread t1 = std::thread(Func, 1000000);
    std::thread t2 = std::thread(Func, 2000000);
    t1.join();
    t2.join();

    std::cout << x << std::endl;

    return 0;
}

在这里插入图片描述
可以发现结果没有出现两个线程竞争统一资源的情况,加锁的目的是防止线程对共享资源同时进行操作,而如果只是操作则不一定需要加锁。

  • 加锁时要考虑锁的细粒度,否则会出现消耗较多时间的情况
int x = 0;
std::mutex mtx;
void Func(int number) {
    mtx.lock();
    for (int i = 0; i < number; i++) {
        ++x;
    }
    mtx.unlock();
}

int main() {
    
    clock_t start = clock();
    
    std::thread t1 = std::thread(Func, 1000000);
    std::thread t2 = std::thread(Func, 2000000);
    t1.join();
    t2.join();

    clock_t end = clock();
    std::cout << x << std::endl;
    std::cout << "消耗的时间为" << (double)(end - start) / CLOCKS_PER_SEC << std::endl;
    return 0;
}

在这里插入图片描述

int x = 0;
std::mutex mtx;
void Func(int number) {
   
    for (int i = 0; i < number; i++) {
        mtx.lock();
        ++x;
        mtx.unlock();
    }
    
}

int main() {
    
    clock_t start = clock();
    
    std::thread t1 = std::thread(Func, 1000000);
    std::thread t2 = std::thread(Func, 2000000);
    t1.join();
    t2.join();

    clock_t end = clock();
    std::cout << x << std::endl;
    std::cout << "消耗的时间为" << (double)(end - start) / CLOCKS_PER_SEC << std::endl;
    return 0;
}

在这里插入图片描述
对比可以发现,锁加载不同位置所消耗的时间是巨大的

13.2 原子操作

  • 加锁的缺点是当一个线程拿到资源后,其他线程就会阻塞影响了程序运行的效率,而且如果锁加不好会引发死锁问题。为解决这一现象c++11引入了原子操作来解决这一问题。

c++11同样提供了原子操作,对原子类型的操作进行了抽象,定义了统一的接口,并要求编译器产生平台相关的原子操作的具体实现。c++11标准对原子操作定义为aumic模板类的成员函数,包括读(load)写(store)交换(exchange)等。主要是通过重载一些全局操作符来完成的。比如对上文total+=i的原子加操作,是通过对operator+=重载来实现的。
请添加图片描述
请添加图片描述
未使用atomic的结果

#include <atomic>
#include <thread>
#include <iostream>
using namespace std;
int64_t total = 0;       //atomic_int64_t相当于int64_t,但是本身就拥有原子性
//线程函数,用于累加
void threadFunc(int64_t endNum)
{
	for (int64_t i = 1; i <= endNum; ++i)
	{
		total += 1;
	}
}
int main()
{
	int64_t endNum = 10000000;
	thread t1(threadFunc, endNum);
	thread t2(threadFunc, endNum);

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

	cout << "total=" << total << endl;    //10100
}

在这里插入图片描述
使用了atomic的结果

// int64_t total = 0;
// 使用了atomic的结果
atomic_int64_t total = 0; 

在这里插入图片描述

参考链接

https://bobowen.blog.csdn.net/article/details/128696518?spm=1001.2014.3001.5502
https://blog.csdn.net/weixin_44120785/article/details/128816083
https://zhuanlan.zhihu.com/p/455848360
https://blog.csdn.net/qq_56044032/article/details/125230984
【重点】https://blog.csdn.net/qq_52433890/article/details/127231352
https://blog.csdn.net/u011942101/article/details/124069208

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值