C++ 11 基础及新特性

1、原始字面量

原始字面量的作用

C++原始字面量(Raw Literals)是一种字面量表示方式,允许在字符串中包含特殊字符(如换行符、反斜杠)而不需要转义。在编写需要包含大量特殊字符的字符串时,原始字面量特别有用。

多行字符串:在不使用换行符转义的情况下编写多行字符串。

包含特殊字符:当字符串中包含大量特殊字符时,使用原始字面量可以避免频繁的转义。

正则表达式:编写复杂的正则表达式时,使用原始字面量可以提高代码的可读性。

生成代码或脚本:在生成代码或脚本的程序中,使用原始字面量可以更方便地包含代码片段。

原始字面量的定义

R"delimiter(raw_characters)delimiter"

解释:delimiter表示自定义的分隔符,可以默认为空,写为:R"(raw_characters)",但是如果raw_characters中包含括号特殊字符,可以自定义delimiter()delimiter

原始字面量的使用

简单示例:

#include <iostream>

int main() {
    const char* raw_str = R"(This is a raw string literal
that can span multiple lines without needing to escape newlines or "quotes".)";

    std::cout << raw_str << std::endl;
    return 0;
}

自定义分割符示例:

特殊情况,如下,不使用自定义分隔符,代码就会报错,因为它不知道如何解析:

	// 错误示例    
	const char * raw_str = R"(This is a)" raw string literal that can span multiple lines without needing to escape newlines or "quotes".)";

这个时候就体现自定义分割符的作用(注意自定义分隔符前后要一致`xxx()xxx`),如下代码又可以正常解析了

	// 使用自定义分隔符,解决上述问题
    const char * raw_str = R"xxx(This is a)" raw string literal that can span multiple lines without needing to escape newlines or "quotes".)xxx";

2、超长整型 long long

long long的定义与使用

在C++中,long long 是一种可以存储超长整型数据的内置数据类型。它通常至少是64位,这意味着它可以存储非常大的整数,范围约为-9,223,372,036,854,775,808到9,223,372,036,854,775,807。

#include <iostream>
using namespace std;

int main() {
    long long largeNumber = 9223372036854775807LL; // 使用LL后缀
    cout << "The large number is: " << largeNumber << endl;
    return 0;
}

long long也会存在溢出现象:

#include <iostream>
#include <limits>
using namespace std;

int main() {
    long long maxLongLong = numeric_limits<long long>::max();
    cout << "Maximum long long: " << maxLongLong << endl;

    // 溢出示例(不建议这样做)
    long long maxLong = numeric_limits<long long>::max();
    cout << "long long max number : " << maxLong << endl; // 9223372036854775807

    long long minLong = numeric_limits<long long>::min();
    cout << "long long min number : " << minLong << endl; // -9223372036854775808

    long long next = maxLong + 1; // 最大值 + 1,溢出
    cout << "max long long plus one : " << next << endl; // -9223372036854775808 存在溢出

    return 0;
}

int64_tlong long的关系

从下面的定义可以看到,int64_t就是由long long的typedef

/* 7.18.1.1  Exact-width integer types */
typedef signed char int8_t;
typedef unsigned char   uint8_t;
typedef short  int16_t;
typedef unsigned short  uint16_t;
typedef int  int32_t;
typedef unsigned   uint32_t;
__MINGW_EXTENSION typedef long long  int64_t;
__MINGW_EXTENSION typedef unsigned long long   uint64_t;

补充:long intlong long int

long intlong

最小大小:至少与int一样大。

  • 通常大小

    • 在许多32位和64位系统上,long int通常是32位。

    • 在一些64位系统上(如某些UNIX和Linux系统),long int可以是64位。

    • 如果是32位,范围为-21474836482147483647(即-2^312^31-1)。

    • 如果是64位,范围为-92233720368547758089223372036854775807(即-2^632^63-1)。

  • 取值范围

long long intlong long

最小大小:至少64位。

  • 通常大小

    • 在几乎所有现代系统上,long long int都是64位。

  • 取值范围-92233720368547758089223372036854775807(即-2^632^63-1)。

注意:long longlong long int 是完全等价的

3、类成员的快速初始化

在C++11中,引入了一种新的统一初始化语法,称为列表初始化或大括号初始化(braced-init-list)。这种语法不仅简化了变量初始化的过程,还解决了一些旧有初始化方式中的问题。例如:

  1. 防止隐式类型转换: 使用大括号初始化可以防止窄化转换(narrowing conversion),即从较大范围的类型转换到较小范围的类型,例如从doubleint,可能会导致数据丢失。

  2. 统一初始化语法: 可以用于初始化所有类型的对象,包括基本类型、复合类型(如数组和结构体)以及标准容器。

  3. 默认初始化和值初始化: 使用大括号初始化可以确保对象被正确初始化,避免了未定义行为。

{}初始化举例

基本类型的初始化

int main() {
    int a{5};            // 初始化为5
    double b{3.14};      // 初始化为3.14
    int c{};             // 默认初始化为0
    double d{};          // 默认初始化为0.0

    // int e{3.14};     // 编译错误,防止窄化转换
}

指针和数组的初始化

int main() {
    int* p{nullptr};     // 指针初始化为nullptr
    int arr[5]{1, 2, 3}; // 数组前3个元素初始化为1, 2, 3,后两个元素初始化为0

    // int arr2[3]{1, 2, 3, 4}; // 编译错误,元素数目超过数组大小
}

结构体和类的初始化

struct Point {
    int x;
    int y;
};

int main() {
    Point p1{10, 20};   // 结构体初始化为{x=10, y=20}
    Point p2{};         // 默认初始化为{x=0, y=0}

    // 类的初始化
    class Rectangle {
    public:
        int width;
        int height;
        Rectangle(int w, int h) : width{w}, height{h} {}
    };

    Rectangle r1{30, 40}; // 初始化为{width=30, height=40}
    // Rectangle r2{30};    // 编译错误,参数不足
}

容器初始化

#include <vector>
#include <map>
#include <string>

int main() {
    std::vector<int> vec{1, 2, 3, 4, 5}; // 初始化为包含5个元素的向量
    std::map<int, std::string> map{{1, "one"}, {2, "two"}, {3, "three"}}; // 初始化为包含3个键值对的映射
}

注意事项:

  1. 防止窄化转换: 窄化转换在传统的初始化方法中可能不会被捕获,但在列表初始化中会导致编译错误,确保数据不被无意中截断。

    int x = 3.14;    // 允许,会将3.14截断为3
    int y{3.14};     // 编译错误,防止窄化转换
  2. 统一的语法: 列表初始化可以用于初始化所有类型的变量,包括内置类型、结构体、类和标准容器。

  3. 默认初始化: 使用空的大括号{}进行初始化时,基本类型会被初始化为零值,类和结构体会调用默认构造函数。

    --javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown
    int a{};            // 默认初始化为0
    std::string s{};    // 调用默认构造函数,初始化为空字符串

补充:默认构造器和参构造器

//
// Created by zhangyl on 2024/6/23.
//
#include <iostream>

using namespace std;

class Animal {
public:
    Animal() = default;

    Animal(string name, int age, double price) {
        this->name = name;
        this->age = age;
        this->price = price;
    }

    string name{"rabbit"};
    int age{15};
    double price{3.25};
};


int main() {
    Animal a1{};
    Animal a2{"cat", 5, 500.00};

    cout << "name : " << a1.name << "; age : " << a1.age << "; price : " << a1.price << endl;
    cout << "name : " << a2.name << "; age : " << a2.age << "; price : " << a2.price << endl;
    return 0;
}

输出结果为:(在HW C++规范中,两种初始化方式不可以一起使用)

name : rabbit; age : 15; price : 3.25
name : cat; age : 5; price : 500

4、final 和 override

final的用法

在C++11及更高版本中,final关键字用于防止类被继承或虚函数被重写。它主要有两个用途:

  1. 防止类继承:当一个类被声明为final时,该类不能被继承。

  2. 防止虚函数重写:当一个虚函数被声明为final时,该虚函数不能在派生类中被重写。

防止类继承

当一个类被标记为final时,任何试图从这个类派生的行为都会导致编译错误。

class Base final {
    // Base类不能被继承
};

class Derived : public Base {
    // 编译错误:Base类是final,不能被继承
};
防止虚函数重写

当一个虚函数被标记为final时,任何试图在派生类中重写该函数的行为都会导致编译错误。

class Base {
public:
    virtual void func() final {
        // 这个函数不能在派生类中被重写
    }
};

class Derived : public Base {
public:
    void func() override {
        // 编译错误:Base::func是final,不能被重写
    }
};
使用final的原因
  1. 设计意图明确:通过使用final,可以明确表达设计意图,防止类被继承或函数被重写,避免意外的行为。

  2. 优化:编译器可以利用final关键字进行某些优化,例如减少虚函数调用的开销。

  3. 提高安全性:防止继承和重写可以减少潜在的错误和复杂性。

override的用法

override 关键字用于显式声明一个成员函数是从基类继承并覆盖了基类的虚函数。这种明确的声明不仅提高了代码的可读性和可维护性,还能帮助编译器捕捉一些常见的错误。

override的作用
  1. 明确意图:使用override关键字可以明确表示某个函数是用来重写基类的虚函数的。

  2. 编译器检查:如果函数**没有正确地覆盖基类中的虚函数,编译器会报错**。这有助于捕捉一些由于函数签名不匹配而导致的潜在错误。

示例代码:
#include <iostream>

class Base {
public:
    virtual void func() {
        std::cout << "Base::func" << std::endl;
    }
    
    virtual void func(int) {
        std::cout << "Base::func(int)" << std::endl;
    }
};

class Derived : public Base {
public:
    void func() override {  // 正确:覆盖Base::func()
        std::cout << "Derived::func" << std::endl;
    }
    
    // void func(double) override {  // 编译错误:没有匹配的基类函数
    //     std::cout << "Derived::func(double)" << std::endl;
    // }
    
    void func(int) override {  // 正确:覆盖Base::func(int)
        std::cout << "Derived::func(int)" << std::endl;
    }
};

int main() {
    Base* b = new Derived();
    b->func();       // 输出:Derived::func
    b->func(10);     // 输出:Derived::func(int)
    delete b;
    return 0;
}

假设我们在Derived类中试图重写Base类的func(int)函数,但由于签名不匹配(例如参数类型不同),未使用override时编译器不会提示错误:

class Derived : public Base {
public:
    void func(double) {  // 意图是覆盖Base::func(int),但签名不匹配
        std::cout << "Derived::func(double)" << std::endl;
    }
};

编译时不会报错,但运行时会导致错误行为,因为Derived::func(double)并没有覆盖Base::func(int)。如果我们使用override,编译器会报错:

class Derived : public Base {
public:
    void func(double) override {  // 编译错误:没有匹配的基类函数
        std::cout << "Derived::func(double)" << std::endl;
    }
};

5、模板的使用(部分非C++11新特性)

C++11支持Base<vector````<int>> b方式的写法,之前不支持。写为:`Base<vector<int> > b`。

函数模板

函数模板允许定义一个模板函数,该函数可以处理不同类型的参数。模板参数在函数调用时根据实际传入的参数类型自动推导。

#include <iostream>

// 函数模板
template <typename T>
T add(T a, T b) {
    return a + b;
}

int main() {
    std::cout << add(3, 4) << std::endl;         // 输出 7
    std::cout << add(3.14, 2.71) << std::endl;   // 输出 5.85
    return 0;
}

类模板

类模板允许定义一个模板类,该类可以处理不同类型的数据成员。模板参数在类实例化时指定。

#include <iostream>

// 类模板
template <typename T>
class Pair {
public:
    Pair(T first, T second) : first(first), second(second) {}

    T getFirst() const { return first; }
    T getSecond() const { return second; }

private:
    T first;
    T second;
};

int main() {
    Pair<int> intPair(1, 2);
    std::cout << intPair.getFirst() << ", " << intPair.getSecond() << std::endl;  // 输出 1, 2

    Pair<double> doublePair(3.14, 2.71);
    std::cout << doublePair.getFirst() << ", " << doublePair.getSecond() << std::endl;  // 输出 3.14, 2.71

    return 0;
}

模板使用案例

实现一个支持多态操作的队列类模板。这个队列允许存储基类指针,并通过虚函数实现多态行为。

#include <iostream>
#include <memory>
#include <queue>

// 基类
class Shape {
public:
    virtual void draw() const = 0;  // 纯虚函数
    virtual ~Shape() = default;
};

// 派生类
class Circle : public Shape {
public:
    void draw() const override {
        std::cout << "Drawing a circle" << std::endl;
    }
};

class Square : public Shape {
public:
    void draw() const override {
        std::cout << "Drawing a square" << std::endl;
    }
};

// 类模板
template <typename T>
class PolymorphicQueue {
public:
    void push(std::shared_ptr<T> obj) {
        queue.push(obj);
    }

    void pop() {
        if (!queue.empty()) {
            queue.pop();
        }
    }

    std::shared_ptr<T> front() const {
        return queue.front();
    }

    bool empty() const {
        return queue.empty();
    }

private:
    std::queue<std::shared_ptr<T>> queue;
};

int main() {
    PolymorphicQueue<Shape> shapeQueue;

    shapeQueue.push(std::make_shared<Circle>());
    shapeQueue.push(std::make_shared<Square>());

    while (!shapeQueue.empty()) {
        shapeQueue.front()->draw();
        shapeQueue.pop();
    }

    return 0;
}

6、数值类型和字符串之间的转换

C++11之前数值类型和字符串之间的转换

通过stringstream进行转换

#include <iostream>
#include <sstream>
#include <string>

int main() {
    // 数值转换为字符串
    int i = 42;
    double d = 3.14159;
    float f = 2.71828f;

    std::stringstream ss;
    
    ss << i;
    std::string str_i = ss.str();
	// 调用ss.str("")将字符串流的内容设置为空字符串,从而清空字符串流内部的缓冲区。
    // 如果不清空字符串流,后续的插入操作将会在现有内容后追加新内容,而不是覆盖现有内容。
    ss.str(""); // 这一步必须要加

    // ss.clear()用于清除字符串流的状态标志,包括错误标志。如果不调用这一步,可能会由于之前的操作设置了一些错误状态而导致后续操作失败。
    // 标志包括eofbit, failbit, 和 badbit,它们分别表示流到达末尾、读/写操作失败、以及流发生不可恢复的错误。
    ss.clear();

    ss << d;
    std::string str_d = ss.str();
    ss.str("");
    ss.clear();

    ss << f;
    std::string str_f = ss.str();
    ss.str("");
    ss.clear();

    std::cout << "Integer: " << str_i << std::endl;
    std::cout << "Double: " << str_d << std::endl;
    std::cout << "Float: " << str_f << std::endl;

    // 字符串转换为数值
    std::string str1 = "12345";
    std::string str2 = "67.89";
    std::string str3 = "45.67";

    int num1;
    double num2;
    float num3;

    ss << str1;
    ss >> num1;
    ss.str("");
    ss.clear();

    ss << str2;
    ss >> num2;
    ss.str("");
    ss.clear();

    ss << str3;
    ss >> num3;
    ss.str("");
    ss.clear();

    std::cout << "Converted int: " << num1 << std::endl;
    std::cout << "Converted double: " << num2 << std::endl;
    std::cout << "Converted float: " << num3 << std::endl;

    // 错误处理示例
    std::string invalid_str = "abc";
    int invalid_num;
    ss << invalid_str;
    ss >> invalid_num;
    if (ss.fail()) {
        std::cerr << "Conversion failed for invalid_str" << std::endl;
    }

    return 0;
}
C++11之后数值类型和字符串之间的转换

数值类型转字符串,直接使用to_string函数

#include <iostream>
#include <string>

int main() {
    int i = 42;
    double d = 3.14159;
    float f = 2.71828f;

    std::string str_i = std::to_string(i);
    std::string str_d = std::to_string(d);
    std::string str_f = std::to_string(f);

    std::cout << "Integer: " << str_i << std::endl;
    std::cout << "Double: " << str_d << std::endl;
    std::cout << "Float: " << str_f << std::endl;

    return 0;
}

字符串转数值类型,使用stox函数

#include <iostream>
#include <string>

int main() {
    std::string str_i = "42";
    std::string str_d = "3.14159";
    std::string str_f = "2.71828";

    int i = std::stoi(str_i);
    double d = std::stod(str_d);
    float f = std::stof(str_f);

    std::cout << "Integer: " << i << std::endl;
    std::cout << "Double: " << d << std::endl;
    std::cout << "Float: " << f << std::endl;

    return 0;
}
转换过程中的异常处理

std::invalid_argument:转换失败异常 和 std::out_of_range:超出范围异常。异常处理:

#include <iostream>
#include <string>
#include <stdexcept>

int main() {
    std::string invalid_str = "abc";
    std::string out_of_range_str = "99999999999999999999999999999999999999"; // 超出int范围

    try {
        int invalid_int = std::stoi(invalid_str);  // 会抛出std::invalid_argument异常
    } catch (const std::invalid_argument& e) {
        std::cerr << "Invalid argument: " << e.what() << std::endl;
    }

    try {
        int out_of_range_int = std::stoi(out_of_range_str);  // 会抛出std::out_of_range异常
    } catch (const std::out_of_range& e) {
        std::cerr << "Out of range: " << e.what() << std::endl;
    }

    return 0;
}

7、static_cast的使用

static_cast是C++中用于类型转换的操作符之一,主要用于显式类型转换。

#include <iostream>

class Base {
public:
    virtual void show() { std::cout << "Base class\n"; }
};

class Derived : public Base {
public:
    void show() override { std::cout << "Derived class\n"; }
};

enum Color { RED, GREEN, BLUE };

int main() {
    // 基本类型转换
    int i = 10;
    double d = static_cast<double>(i);
    std::cout << "Double: " << d << "\n";

    // 指针和引用转换
    Derived derivedObj;
    Base* basePtr = static_cast<Base*>(&derivedObj); // 向上转换
    basePtr->show();

    Derived* derivedPtr = static_cast<Derived*>(basePtr); // 向下转换
    derivedPtr->show();

    // 枚举和整数之间的转换
    Color color = GREEN;
    int colorValue = static_cast<int>(color);
    std::cout << "Color value: " << colorValue << "\n";
    
    Color newColor = static_cast<Color>(colorValue);
    std::cout << "New color: " << newColor << "\n"; // 1 (GREEN)

    return 0;
}
static_castdynamic_cast 的比较

static_cast

  1. 编译时转换:

    • static_cast 在编译时进行转换。

    • 它不会进行运行时类型检查。

  2. 用途:

    • 用于基本类型之间的转换,如 intfloat

    • 用于指针和引用之间的转换,特别是基类和派生类之间的转换。

    • 用于枚举类型和整数之间的转换。

  3. 性能:

    • static_cast 的性能通常优于 dynamic_cast,因为它没有运行时开销。

  4. 安全性:

    • 不安全的转换可能导致未定义行为(如从基类向下转换到派生类时,基类指针实际上指向一个不是该派生类的对象)。

dynamic_cast

  1. 运行时转换:

    • dynamic_cast 在运行时进行转换。

    • 它使用运行时类型信息(RTTI)来确保转换的安全性。

  2. 用途:

    • 主要用于多态类型的转换(即带有虚函数的类)。

    • 安全地将基类指针或引用转换为派生类指针或引用。

  3. 性能:

    • dynamic_cast 由于进行运行时检查,性能较 static_cast 低。

  4. 安全性:

    • 提供更高的安全性。

    • 如果转换失败,指针类型的 dynamic_cast 会返回 nullptr,引用类型的 dynamic_cast 会抛出 std::bad_cast 异常。

#include <iostream>
#include <typeinfo>

class Base {
public:
    virtual ~Base() {} // 必须有虚函数以使dynamic_cast工作
};

class Derived : public Base {
};

class AnotherDerived : public Base {
};

int main() {
    Base* base1 = new Derived();
    Base* base2 = new AnotherDerived();

    // 使用static_cast进行向上和向下转换
    Derived* derived1 = static_cast<Derived*>(base1); // 正确,base1实际指向Derived对象
    // Derived* derived2 = static_cast<Derived*>(base2); // 不安全,base2指向AnotherDerived对象

    if (derived1) {
        std::cout << "static_cast成功\n";
    } else {
        std::cout << "static_cast失败\n";
    }

    // 使用dynamic_cast进行向上和向下转换
    Derived* derived3 = dynamic_cast<Derived*>(base1); // 正确,base1实际指向Derived对象
    Derived* derived4 = dynamic_cast<Derived*>(base2); // 安全,返回nullptr

    if (derived3) {
        std::cout << "dynamic_cast成功\n";
    } else {
        std::cout << "dynamic_cast失败\n";
    }

    if (derived4) {
        std::cout << "dynamic_cast成功\n";
    } else {
        std::cout << "dynamic_cast失败\n";
    }

    delete base1;
    delete base2;

    return 0;
}

在C中,将派生类对象的指针或引用转换为基类对象的指针或引用被称为“向上转换”(upcasting);反之,则称为“向下转换”。这种转换在C中是安全且隐式允许的。无论是通过static_cast还是dynamic_cast进行向上转换,都是允许的,因为每个派生类对象都包含一个基类子对象。

为什么子类转父类可以(子类转父类可以,父类转子类会失败)

  1. 对象模型:

    • 在派生类对象中,基类部分总是存在的。换句话说,一个Derive对象总是包含一个Base对象的子部分。因此,将Derive*转换为Base*是安全的,因为派生类对象总是包含基类对象的所有成员。

  2. 向上转换:

    • 向上转换(upcasting)从派生类到基类的转换在编译时和运行时都是安全的,不需要进行任何特殊的检查。这种转换是隐式允许的,因为编译器知道派生类对象总是兼容基类类型。

    • static_castdynamic_cast 都可以用于向上转换。static_cast 用于编译时检查,而 dynamic_cast 可用于运行时检查和多态类型的向上转换。

8、noexcept

noexcept 是 C++11 引入的一种关键字,用于指定函数在运行时不会抛出异常。它可以提高代码的性能,并使代码更加健壮和清晰。

可以在函数声明中使用 noexcept,表明该函数不会抛出异常:

void myFunction() noexcept {
    // Function implementation
}

自动类型推导

基于范围的for循环

指针空值类型 - nullptr

lambda表达式

常量表达式修饰符 - constexpr

委托构造函数和继承构造函数

右值引用

转移和完美转发

列表初始化

using的使用

可调用对象包装器、绑定器

POD类型

默认函数控制 =default 与 =delete

扩展的friend语法

强类型枚举

非受限联合体

共享智能指针

独占智能指针

弱引用智能指针

补充

Single File Execution插件的使用

可以创建多个main函数,让每个项目独立执行

File -> Setting -> Plugins,在MarketPlace搜索`C/C++ Single File Execution`,安装。

报错:Non-const lvalue reference to type 'shared_ptr<Shape>' cannot bind to a temporary of type 'shared_ptr<Circle>'

这个错误是由于尝试将临时对象(例如通过std::make_shared<Circle>()创建的临时shared_ptr<Circle>)绑定到一个非const的左值引用shared_ptr<Shape>&。这是因为临时对象不能绑定到非const的左值引用。要解决这个问题,可以将参数类型改为右值引用或const引用。

为什么 临时对象不能绑定到非const的左值引用

一句话:临时对象生命周期短,临时对象在被引用被修改时,可能已经销毁(悬挂引用)。

  • 临时对象的生命周期

    • 临时对象(也称为右值)通常是在表达式求值过程中创建的,它们的生命周期很短,通常在表达式结束时就会被销毁。

    • 如果允许临时对象绑定到非const左值引用,那么在引用的生命周期内可能会对临时对象进行修改,但临时对象在表达式结束时就会被销毁,这可能导致悬挂引用(dangling reference)问题。

  • 修改临时对象的含义

    • const左值引用表示引用的对象可以被修改。然而,临时对象一般是不会被修改的,因为它们通常是一些计算结果或纯函数返回的值,程序员通常不期望这些临时结果会被修改。

    • 允许修改临时对象可能会导致难以理解的代码行为,增加代码错误的风险。

解决方案

1、使用const修饰(缺点会造成不必要的拷贝)

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

void push(const std::shared_ptr<T>& obj) {
        queue.push(obj);
    }

2、使用右值引用的方式传递临时变量

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

void push(std::shared_ptr<T>&& obj) {
        queue.push(std::move(obj));
    }

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值