《C++ Primer》导学系列:第 12 章 - 动态内存

12.1 动态内存与智能指针

动态内存管理是C++编程中的重要部分。使用动态内存时,程序员需要手动分配和释放内存,这可能导致内存泄漏和其他问题。为了解决这些问题,C++11引入了智能指针(smart pointers),它们自动管理动态内存的生命周期。本节将介绍动态内存管理的基本概念以及智能指针的使用。

12.1.1 直接管理内存

在C++中,可以使用newdelete操作符来分配和释放动态内存。

  • new:分配动态内存并返回指向该内存的指针。
  • delete:释放先前使用new分配的动态内存。
示例代码
#include <iostream>

int main() {
    // 使用new分配动态内存
    int* p = new int(42);

    // 输出动态内存中的值
    std::cout << "Value: " << *p << std::endl;

    // 使用delete释放动态内存
    delete p;

    return 0;
}

直接管理内存的缺点包括需要手动释放内存,容易导致内存泄漏和悬空指针(dangling pointer)的问题。因此,使用智能指针来管理动态内存是一个更好的选择。

12.1.2 shared_ptr

shared_ptr是C++标准库中的智能指针类型之一,用于共享所有权。多个shared_ptr实例可以共享同一个对象,当最后一个shared_ptr被销毁时,对象的内存会被自动释放。

示例代码
#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> p1 = std::make_shared<int>(42);
    std::cout << "Shared_ptr value: " << *p1 << std::endl;
    std::shared_ptr<int> p2 = p1; // p1和p2共享同一个对象
    std::cout << "Use count: " << p1.use_count() << std::endl;

    return 0;
}

12.1.3 shared_ptrnew结合使用

虽然推荐使用std::make_shared来创建shared_ptr,但也可以直接与new结合使用。这种方式需要注意,在创建shared_ptr时要确保对象的内存被正确管理。

示例代码
#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> p1(new int(42));
    std::cout << "Shared_ptr value: " << *p1 << std::endl;
    std::shared_ptr<int> p2 = p1; // p1和p2共享同一个对象
    std::cout << "Use count: " << p1.use_count() << std::endl;

    return 0;
}

12.1.4 unique_ptr

unique_ptr是独占所有权的智能指针,不能被复制,只能被移动。这意味着在同一时间只能有一个unique_ptr指向特定对象。使用unique_ptr可以确保动态内存不会被多个指针同时管理,从而避免了双重释放的问题。

示例代码
#include <iostream>
#include <memory>

void useUniquePtr() {
    std::unique_ptr<int> p1(new int(42));
    std::cout << "Value: " << *p1 << std::endl;

    // 将所有权转移给另一个unique_ptr
    std::unique_ptr<int> p2 = std::move(p1);
    if (!p1) {
        std::cout << "p1 is null after move" << std::endl;
    }
    std::cout << "Value after move: " << *p2 << std::endl;
}

int main() {
    useUniquePtr();
    return 0;
}

12.1.5 weak_ptr

weak_ptr是一种弱引用智能指针,不影响shared_ptr的引用计数。它通常与shared_ptr一起使用,以避免循环引用导致的内存泄漏。weak_ptr不能直接访问对象,需要先将其转换为shared_ptr

示例代码
#include <iostream>
#include <memory>

void useWeakPtr() {
    std::shared_ptr<int> sp = std::make_shared<int>(42);
    std::weak_ptr<int> wp = sp; // wp是sp的弱引用
    std::cout << "Use count before reset: " << sp.use_count() << std::endl;

    sp.reset(); // 释放sp持有的对象

    if (wp.expired()) {
        std::cout << "Weak pointer is expired" << std::endl;
    } else {
        std::shared_ptr<int> sp2 = wp.lock(); // 将wp转换为shared_ptr
        std::cout << "Value: " << *sp2 << std::endl;
    }
}

int main() {
    useWeakPtr();
    return 0;
}

12.1.6 智能指针与异常

智能指针在异常处理时尤为有用。由于智能指针会自动释放所管理的内存,因此可以防止因异常抛出导致的内存泄漏。

示例代码
#include <iostream>
#include <memory>

void mayThrow() {
    throw std::runtime_error("An error occurred");
}

void useSmartPointerWithException() {
    std::shared_ptr<int> sp = std::make_shared<int>(42);
    try {
        mayThrow();
    } catch (const std::exception& e) {
        std::cout << "Exception caught: " << e.what() << std::endl;
    }
    std::cout << "Value: " << *sp << std::endl;
}

int main() {
    useSmartPointerWithException();
    return 0;
}

在上述代码中,即使mayThrow函数抛出异常,智能指针sp也会确保在其作用域结束时正确释放内存。

重点与难点分析

重点

  1. 理解动态内存分配与释放:掌握newdelete操作符的使用。
  2. 掌握智能指针的基本概念和使用方法:了解std::unique_ptrstd::shared_ptrstd::weak_ptr的特点和用法。
  3. 避免内存泄漏和双重释放:通过使用智能指针自动管理内存,减少内存管理错误。

难点

  1. 智能指针的所有权管理:理解std::unique_ptr的独占所有权和std::shared_ptr的共享所有权。
  2. 循环引用问题的解决:了解如何使用std::weak_ptr避免std::shared_ptr之间的循环引用。

练习题解析

  1. 练习12.1:编写一个程序,使用std::unique_ptr管理动态分配的整数,并输出其值。
    • 示例代码
#include <iostream>
#include <memory>

int main() {
    std::unique_ptr<int> p(new int(42));
    std::cout << "Value: " << *p << std::endl;

    return 0;
}
  1. 练习12.2:编写一个程序,使用std::shared_ptr管理动态分配的字符串,并输出其值和引用计数。
    • 示例代码
#include <iostream>
#include <memory>
#include <string>

int main() {
    std::shared_ptr<std::string> p1 = std::make_shared<std::string>("Hello, World!");
    std::shared_ptr<std::string> p2 = p1; // p1和p2共享同一个字符串

    std::cout << "Value: " << *p1 << std::endl;
    std::cout << "Use count: " << p1.use_count() << std::endl;

    return 0;
}
  1. 练习12.3:编写一个程序,使用std::weak_ptrstd::shared_ptr避免循环引用,验证std::weak_ptr的有效性。
    • 示例代码
#include <iostream>
#include <memory>

struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;
};

int main() {
    std::shared_ptr<Node> node1 = std::make_shared<Node>();
    std::shared_ptr<Node> node2 = std::make_shared<Node>();

    node1->next = node2;
    node2->prev = node1;

    std::cout << "Node1 use count: " << node1.use_count() << std::endl;
    std::cout << "Node2 use count: " << node2.use_count() << std::endl;

    node1.reset();
    node2.reset();

    return 0;
}

总结与提高

本节总结

  1. 了解了动态内存分配与释放的基本概念和操作,掌握了newdelete的使用方法。
  2. 理解了智能指针的基本概念,掌握了std::unique_ptrstd::shared_ptrstd::weak_ptr的使用方法。
  3. 通过智能指针的使用,能够有效避免内存泄漏和双重释放问题,提高了程序的可靠性和稳定性。

提高建议

  1. 多练习智能指针的使用:通过编写更多涉及智能指针的程序,熟悉各种智能指针的用法,提高对动态内存管理的能力。
  2. 深入理解智能指针的原理:通过阅读文档和相关书籍,深入理解智能指针的实现原理和使用场景,提高编写高效代码的能力。
  3. 避免常见内存管理错误:在实际项目中,尽量使用智能指针管理动态内存,避免手动管理内存带来的错误,提高代码的可读性和可维护性。

12.2 动态数组

动态数组是指在运行时动态分配内存来存储数组元素的技术。在C++中,动态数组通常使用newdelete[]操作符来管理其内存。尽管可以直接使用这些操作符进行内存管理,但标准库提供的容器类(如std::vector)更为方便和安全。

12.2.1 使用newdelete[]管理动态数组

可以使用new操作符为动态数组分配内存,并使用delete[]操作符释放该内存。下面是一个基本示例,展示了如何分配和释放动态数组:

示例代码
#include <iostream>

int main() {
    // 使用new分配动态数组
    int* arr = new int[5];

    // 初始化数组
    for (int i = 0; i < 5; ++i) {
        arr[i] = i * 2;
    }

    // 输出数组元素
    for (int i = 0; i < 5; ++i) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;

    // 使用delete[]释放动态数组
    delete[] arr;

    return 0;
}

12.2.2 使用智能指针管理动态数组

智能指针可以用来管理动态数组的生命周期,以避免手动释放内存带来的问题。C++11引入的std::unique_ptr可以用来管理动态数组。

示例代码
#include <iostream>
#include <memory>

int main() {
    // 使用unique_ptr管理动态数组
    std::unique_ptr<int[]> arr(new int[5]);

    // 初始化数组
    for (int i = 0; i < 5; ++i) {
        arr[i] = i * 2;
    }

    // 输出数组元素
    for (int i = 0; i < 5; ++i) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;

    // 无需手动释放内存,unique_ptr会自动管理

    return 0;
}

12.2.3 使用std::vector管理动态数组

虽然可以直接使用newdelete[]来管理动态数组,但使用标准库中的std::vector通常是更好的选择。std::vector是一个动态数组容器,提供了许多方便的成员函数来管理数组元素的添加、删除和访问。

示例代码
#include <iostream>
#include <vector>

int main() {
    // 使用vector管理动态数组
    std::vector<int> vec(5);

    // 初始化数组
    for (int i = 0; i < 5; ++i) {
        vec[i] = i * 2;
    }

    // 输出数组元素
    for (int i = 0; i < 5; ++i) {
        std::cout << vec[i] << " ";
    }
    std::cout << std::endl;

    // vector会自动管理内存,无需手动释放

    return 0;
}

12.2.4 allocator

C++标准库提供了allocator类,用于更灵活和低级别的内存管理。allocator类允许我们分配未初始化的内存,并在其中构造对象。

示例代码
#include <iostream>
#include <memory>

int main() {
    std::allocator<int> alloc;  // 创建allocator对象

    int* arr = alloc.allocate(5);  // 分配未初始化的内存

    for (int i = 0; i < 5; ++i) {
        alloc.construct(&arr[i], i * 2);  // 在分配的内存中构造对象
    }

    for (int i = 0; i < 5; ++i) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;

    for (int i = 0; i < 5; ++i) {
        alloc.destroy(&arr[i]);  // 销毁对象
    }
    alloc.deallocate(arr, 5);  // 释放内存

    return 0;
}

重点与难点分析

重点

  1. 动态数组的基本操作:掌握使用newdelete[]分配和释放动态数组内存的方法。
  2. 智能指针的应用:理解如何使用std::unique_ptr管理动态数组的内存。
  3. 标准库容器的使用:熟悉std::vector的用法,理解其在动态数组管理中的优势。
  4. allocator类的使用:学习使用allocator类进行更灵活的内存管理。

难点

  1. 手动内存管理的注意事项:在使用newdelete[]时,确保正确匹配,避免内存泄漏和未定义行为。
  2. 智能指针与原生指针的区别:理解智能指针如何自动管理内存,避免手动释放带来的问题。
  3. 选择合适的工具:在实际编程中,根据具体需求选择使用原生指针、智能指针或标准库容器。
  4. 低级内存管理:理解和使用allocator类进行手动内存管理。

练习题解析

  1. 练习12.4:编写一个程序,使用newdelete[]管理动态分配的数组,并输出其元素。
    • 示例代码
#include <iostream>

int main() {
    int* arr = new int[5];
    for (int i = 0; i < 5; ++i) {
        arr[i] = i * 2;
    }
    for (int i = 0; i < 5; ++i) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
    delete[] arr;
    return 0;
}
  1. 练习12.5:编写一个程序,使用std::unique_ptr管理动态分配的数组,并输出其元素。
    • 示例代码
#include <iostream>
#include <memory>

int main() {
    std::unique_ptr<int[]> arr(new int[5]);
    for (int i = 0; i < 5; ++i) {
        arr[i] = i * 2;
    }
    for (int i = 0; i < 5; ++i) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
    return 0;
}
  1. 练习12.6:编写一个程序,使用std::vector管理动态分配的数组,并输出其元素。
    • 示例代码
#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec(5);
    for (int i = 0; i < 5; ++i) {
        vec[i] = i * 2;
    }
    for (int i = 0; i < 5; ++i) {
        std::cout << vec[i] << " ";
    }
    std::cout << std::endl;
    return 0;
}
  1. 练习12.7:编写一个程序,使用allocator类管理动态分配的数组,并输出其元素。
    • 示例代码
#include <iostream>
#include <memory>

int main() {
    std::allocator<int> alloc;
    int* arr = alloc.allocate(5);
    for (int i = 0; i < 5; ++i) {
        alloc.construct(&arr[i], i * 2);
    }
    for (int i = 0; i < 5; ++i) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
    for (int i = 0; i < 5; ++i) {
        alloc.destroy(&arr[i]);
    }
    alloc.deallocate(arr, 5);
    return 0;
}

总结与提高

本节总结

  1. 了解了动态数组的基本概念和操作,掌握了使用newdelete[]管理动态数组的方法。
  2. 理解了智能指针在动态数组管理中的应用,掌握了使用std::unique_ptr管理动态数组的方法。
  3. 熟悉了std::vector的用法,理解了其在动态数组管理中的优势。
  4. 学习了allocator类的使用,掌握了更灵活的内存管理技术。

提高建议

  1. 多练习动态数组的管理:通过编写更多涉及动态数组管理的程序,熟

悉各种管理方法的用法,提高对动态内存管理的能力。
2. 深入理解智能指针的原理:通过阅读文档和相关书籍,深入理解智能指针的实现原理和使用场景,提高编写高效代码的能力。
3. 优先使用标准库容器:在实际项目中,尽量使用std::vector等标准库容器管理动态数组,以减少手动内存管理带来的错误,提高代码的可读性和可维护性。
4. 掌握低级内存管理技巧:通过练习使用allocator类,掌握更加灵活和低级的内存管理技巧,以应对复杂的内存管理需求。

12.3 使用标准库:文本查询程序

这一小节将通过一个实际的文本查询程序示例,来演示如何使用C++标准库中的容器和算法处理动态内存。我们将实现一个简单的文本查询程序,能够读取一个文件,并允许用户查询文件中的某个单词在文中出现的次数及其所在行。

12.3.1 程序概述

文本查询程序将分为以下几个部分:

  1. 读取文件:读取文件内容并存储每行文本。
  2. 构建索引:创建一个索引,将每个单词映射到它出现的行号集合。
  3. 查询单词:允许用户输入一个单词,程序返回该单词出现的次数及其所在行。
示例代码
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <string>
#include <map>
#include <set>

class TextQuery {
public:
    TextQuery(std::ifstream &infile);
    std::set<int> query(const std::string &word) const;
    void display_results(const std::set<int> &lines, const std::string &word) const;
private:
    std::vector<std::string> file;  // 存储文件每行文本
    std::map<std::string, std::set<int>> word_map;  // 单词到行号的映射
};

TextQuery::TextQuery(std::ifstream &infile) {
    std::string line;
    int line_number = 0;
    while (std::getline(infile, line)) {
        file.push_back(line);
        std::istringstream iss(line);
        std::string word;
        while (iss >> word) {
            word_map[word].insert(line_number);
        }
        ++line_number;
    }
}

std::set<int> TextQuery::query(const std::string &word) const {
    auto it = word_map.find(word);
    if (it != word_map.end()) {
        return it->second;
    } else {
        return std::set<int>();
    }
}

void TextQuery::display_results(const std::set<int> &lines, const std::string &word) const {
    std::cout << "The word \"" << word << "\" occurs " << lines.size() << " times:" << std::endl;
    for (int num : lines) {
        std::cout << "\t(line " << num + 1 << ") " << file[num] << std::endl;
    }
}

int main() {
    std::ifstream infile("text.txt");
    if (!infile) {
        std::cerr << "Could not open the file!" << std::endl;
        return 1;
    }

    TextQuery tq(infile);
    while (true) {
        std::cout << "Enter a word to search for, or q to quit: ";
        std::string word;
        if (!(std::cin >> word) || word == "q") break;
        std::set<int> results = tq.query(word);
        tq.display_results(results, word);
    }
    return 0;
}

12.3.2 详细分析

读取文件

程序首先打开一个文件,并逐行读取文件内容,将每行文本存储在一个std::vector<std::string>中。这使得我们能够在查询时轻松访问和显示每行内容。

构建索引

在读取每行文本时,程序使用std::istringstream将行文本分解为单词,并将每个单词插入到一个std::map<std::string, std::set<int>>中,其中std::string是单词,std::set<int>是单词出现的行号集合。使用std::set可以确保每个行号只出现一次。

查询单词

查询功能通过查找std::map中的单词来实现。如果找到单词,则返回其对应的行号集合;否则,返回一个空的std::set<int>

显示结果

显示结果的函数接受一个行号集合和一个单词,然后输出该单词出现的次数及其所在行的内容。

重点与难点分析

重点

  1. 使用标准库容器:熟悉std::vectorstd::mapstd::set的用法,并理解它们在不同场景下的作用。
  2. 文件输入输出:掌握如何使用std::ifstream读取文件内容,并使用std::istringstream解析每行文本。
  3. 查询与显示:理解如何高效地进行单词查询,并显示查询结果。

难点

  1. 索引构建:确保每个单词正确映射到其出现的所有行号,并处理重复单词和行号。
  2. 查询效率:利用std::mapstd::set的特性,确保查询操作的高效性。
  3. 动态内存管理:虽然标准库容器自动管理内存,但理解其背后的动态内存分配机制对于写出高效的代码仍然重要。

练习题解析

  1. 练习12.8:编写一个程序,读取一个文本文件,并输出文件中每个单词出现的次数。
    • 示例代码
#include <iostream>
#include <fstream>
#include <sstream>
#include <map>

int main() {
    std::ifstream infile("text.txt");
    if (!infile) {
        std::cerr << "Could not open the file!" << std::endl;
        return 1;
    }

    std::map<std::string, int> word_count;
    std::string line;
    while (std::getline(infile, line)) {
        std::istringstream iss(line);
        std::string word;
        while (iss >> word) {
            ++word_count[word];
        }
    }

    for (const auto &pair : word_count) {
        std::cout << pair.first << ": " << pair.second << std::endl;
    }

    return 0;
}
  1. 练习12.9:修改文本查询程序,使其能够忽略大小写。
    • 示例代码
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <string>
#include <map>
#include <set>
#include <algorithm>

class TextQuery {
public:
    TextQuery(std::ifstream &infile);
    std::set<int> query(const std::string &word) const;
    void display_results(const std::set<int> &lines, const std::string &word) const;
private:
    std::vector<std::string> file;
    std::map<std::string, std::set<int>> word_map;

    static std::string to_lower(const std::string &str);
};

TextQuery::TextQuery(std::ifstream &infile) {
    std::string line;
    int line_number = 0;
    while (std::getline(infile, line)) {
        file.push_back(line);
        std::istringstream iss(line);
        std::string word;
        while (iss >> word) {
            word = to_lower(word);
            word_map[word].insert(line_number);
        }
        ++line_number;
    }
}

std::set<int> TextQuery::query(const std::string &word) const {
    auto it = word_map.find(to_lower(word));
    if (it != word_map.end()) {
        return it->second;
    } else {
        return std::set<int>();
    }
}

void TextQuery::display_results(const std::set<int> &lines, const std::string &word) const {
    std::cout << "The word \"" << word << "\" occurs " << lines.size() << " times:" << std::endl;
    for (int num : lines) {
        std::cout << "\t(line " << num + 1 << ") " << file[num] << std::endl;
    }
}

std::string TextQuery::to_lower(const std::string &str) {
    std::string lower_str = str;
    std::transform(lower_str.begin(), lower_str.end(), lower_str.begin(), ::tolower);
    return lower_str;
}

int main() {
    std::ifstream infile("text.txt");
    if (!infile) {
        std::cerr << "Could not open the file!" << std::endl;
        return 1;
    }

    TextQuery tq(infile);
    while (true) {
        std::cout << "Enter a word to search for, or q to quit: ";
        std::string word;
        if (!(std::cin >> word) || word == "q") break;
        std::set<int> results = tq.query(word);
        tq.display_results(results, word);
    }
    return 0;
}
  1. 练习12.10:编写一个程序,允许用户查询多个单词,并显示每个单词出现的次数及其所在行。
    • 示例代码
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <string>
#include <map>
#include <set>

class TextQuery {
public:
    TextQuery(std::ifstream &infile);
    std::map<std::string, std::set<int>> query(const std::vector<std::string> &words) const;
    void display_results(const std::map<std::string, std::set<int>> &results) const;
private:
    std::vector<std::string> file;
    std::map<std::string, std::set<int>> word_map;
};

TextQuery::TextQuery(std::ifstream &infile) {
    std::string line;
    int line_number = 0;
    while (std::getline(infile, line)) {
        file.push_back(line);
        std::istringstream iss(line);
        std::string word;
        while (iss >> word) {
            word_map[word].insert(line_number);
        }
        ++line_number;
    }
}

std::map<std::string, std::set<int>> TextQuery::query(const std::vector<std::string> &words) const {
    std::map<std::string, std::set<int>> results;
    for (const auto &word : words) {
        auto it = word_map.find(word);
        if (it != word_map.end()) {
            results[word] = it->second;
        }
    }
    return results;
}

void TextQuery::display_results(const std::map<std::string, std::set<int>> &results) const {
    for (const auto &pair : results) {
        std::cout << "The word \"" << pair.first << "\" occurs " << pair.second.size() << " times:" << std::endl;
        for (int num : pair.second) {
            std::cout << "\t(line " << num + 1 << ") " << file[num] << std::endl;
        }
    }
}

int main() {
    std::ifstream infile("text.txt");
    if (!infile) {
        std::cerr << "Could not open the file!" << std::endl;
        return 1;
    }

    TextQuery tq(infile);
    while (true) {
        std::cout << "Enter words to search for, separated by spaces, or q to quit: ";
        std::string line;
        if (!std::getline(std::cin, line) || line == "q") break;
        std::istringstream iss(line);
        std::vector<std::string> words;
        std::string word;
        while (iss >> word) {
            words.push_back(word);
        }
        auto results = tq.query(words);
        tq.display_results(results);
    }
    return 0;
}

总结与提高

本节总结

  1. 通过实现一个实际的文本查询程序,了解了如何使用C++标准库中的容器和算法处理动态内存。
  2. 掌握了文件输入输出、字符串处理、容器的使用和查询结果的显示。
  3. 理解了如何高效地构建索引,以便快速查询和显示结果。

提高建议

  1. 多练习实际项目:通过更多实际项目练习,巩固对标准库容器和算法的使用,提高代码编写能力。
  2. 优化程序性能:在实际项目中,考虑程序的性能优化,如使用更高效的数据结构和算法。
  3. 深入理解标准库:通过阅读文档和相关书籍,深入理解C++标准库的各个组件及其使用场景,提高编写高效代码的能力。

本主页会定期更新,为了能够及时获得更新,敬请关注我:点击左下角的关注。也可以关注公众号:请在微信上搜索公众号“iShare爱分享”并关注,或者扫描以下公众号二维码关注,以便在内容更新时直接向您推送。 

  • 47
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

AI与编程之窗

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

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

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

打赏作者

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

抵扣说明:

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

余额充值