C++面试语法糖

一、关键字

static

作用一:修饰局部变量:一般情况下局部变量都放在栈区,并且局部变量生命周期会随着语句块的结束而结束,而static修饰的变量会放在静态数据区,其生命周期会持续到整个程序结束而结束。
作用二:修饰全局变量:对于全局变量,如果用static修饰,则可以在同一个工程其他源文件可以访问到。
作用三:修饰函数:类似于修饰全局变量一样,只是修改了其作用域而已。
作用四:修饰类:如果static修饰类中某个函数时,则表明其属于一个类而不是属于此类的任何特定对象,如果修饰一个成员变量,则表示变量对所有对象所有,存储空间只存在一个副本,可以通过类和对象调用。

  • static 成员函数不能被 virtual 修饰,static 成员不属于任何对象或实例,所以加上 virtual 没有任何实际意义;静态成员函数没有 this 指针,虚函数的实现是为每⼀个对象分配⼀个 vptr 指针,⽽ vptr 是通过 this 指针调⽤的,所以不能为 virtual;虚函数的调⽤关系,this->vptr->ctable->virtual function。

const

作用一:修饰基本类型数据:基本数据类型,修饰符 const 可以⽤在类型说明符前,也可以⽤在类型说明符后,其结果是⼀样的。在使⽤这些常量的时候,只要不改变这些常量的值即可。

  • 常量变量
const int myConstVal
  • 常量指针
const int* constPtr
  • 指向常量的指针
int* const ptrConst

作用二:修饰函数:表明函数不会修改类的成员变量。这在类的成员函数中经常使用,以确保这些函数不会对类的内部状态产生不可预知的副作用,而且只能调用其他 const 成员函数
作用三:const 修饰类对象,定义常量对象:常量对象只能调⽤常量函数,别的成员函数都不能调用。
补充:const 成员函数中如果实在想修改某个变量,可以使⽤ mutable 进⾏修饰。成员变量中如果想建⽴在整个类中都恒定的常量,应该⽤类中的枚举常量来实现或者 static const。

define

宏定义实际是在预处理阶段,仅仅做的是遇到宏定义进⾏字符串的展开,遇到多少次就展开多少次。因为 define 宏定义仅仅是展开,因此运⾏时系统并不为宏定义分配内存,但是从汇编 的角度来讲,define 却以立即数的方式保留了多份数据的拷贝。

volatile 和 extern 关键字

volatile 三个特性
易变性:在汇编层⾯反映出来,就是两条语句,下⼀条语句不会直接使⽤上⼀条语句对应的 volatile 变量的寄存器内容,而是重新从内存中读取。
不可优化性:volatile 告诉编译器,不要对我这个变量进⾏各种激进的优化,甚⾄将变量直接消除,保证程序员写在代码中的指令,⼀定会被执行。
顺序性:能够保证 volatile 变量之间的顺序性,编译器不会进⾏乱序优化

extern:通常是在编程中声明一个在其他源文件或模块中定义的变量、函数或对象。这是一种指示所声明实体的实际定义位于程序的其他地方或在单独的模块中的方式。
编译器实际的定义存在于其他地方,并且应在程序的链接阶段进行链接。这在项目中有多个源文件并且希望在一个文件中使用另一个文件中定义的函数或变量时非常有用。

malloc/free(不是关键字)和new/delete(操作符)

malloc底层原理:malloc是在程序执行期间动态分配指定数量的字节内存,malloc在申请内存时,有2种方式来申请堆内存

  • 通过brk()系统调用从堆分配内存
  • 通过mapp()系统调用在文件映射区分配内存
    malloc是分配的虚拟内存,如果没有被访问,就不会映射到物理内存上去,只有被访问之后,操作系统会查询页表,发现虚拟内存对应的页没有在物理内存中,就会发现缺页中断,就会建立映射关系。
  • 采用mmap分配内存不仅每次都会发生运行态的切换,还会发生缺页中断(在第一次访问虚拟地址后),这样会导致 CPU 消耗较大
  • 采用brk分配内存的化:等下次在申请内存的时候,就直接从内存池取出对应的内存块就行了,而且可能这个内存块的虚拟地址与物理地址的映射关系还存在,这样不仅减少了系统调用的次数,也减少了缺页中断的次数,这将大大降低 CPU 的消耗。
    free:而是一个函数,用于释放通过动态内存分配函数,如果用brk申请的内存是不会归还给操作系统的,而是缓存在内存池中,等待下次使用,而采用mapp申请的内存,会真正的归还给操作系统,得到真正的释放
    free传入一个内存地址,会查询到相应的内存块描述信息。free 会对传入进来的内存地址向左偏移 16 字节,然后从这个 16 字节的分析出当前的内存块的大小,自然就知道要释放多大的内存了。

New和DeleteNew和Delete详解
new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。
为啥有了malloc和free,还有new和delete

  • 类型安全:

new 和 delete 是面向对象的运算符,可以自动调用对象的构造函数和析构函数。这样确保了内存的分配和释放与对象的构造和析构是一致的,避免了因忘记调用构造函数或析构函数而导致的问题。
malloc 和 free 只是简单的内存分配和释放函数,不关心对象的构造和析构。

  • 面向对象的支持:

new 可以为类对象动态分配内存并调用构造函数,而 malloc 只是为一块内存分配空间,无法执行对象的构造过程。
delete 可以调用对象的析构函数,而 free 无法执行对象的析构过程。

  • 数组和对象数组:

new 可以方便地为单个对象或对象数组分配内存,而 malloc 和 free 需要手动计算分配的空间大小。
delete 能够正确释放对象数组的内存,并依次调用数组中每个对象的析构函数。

  • 异常处理:

new 在分配失败时会抛出 std::bad_alloc 异常,而 malloc 只是返回 NULL,需要手动检查。
delete 会根据对象是否有异常保证来决定是否抛出异常,而 free 不提供类似的异常机制。

  • 运算符重载:

new 和 delete 可以被用户重载,允许在内存分配和释放时添加特定的行为,例如内存池的使用。
malloc 和 free 无法被用户直接重载,只能通过调用系统提供的内存管理函数。

二、多态

多态是面向对象编程中的一个重要概念,它允许使用基类类型的指针或引用来操作派生类对象,从而实现代码的灵活性和可扩展性。多态性有两种主要形式:编译时多态性(静态多态性)和运行时多态性(动态多态性)
多态其实指的是继承加虚函数实现的多态,对于重载来说,实际的原理则是,编译器为函数生成符号表

  • 静态多态性:也就是重载,在编译期间决定了调用了哪个函数
  • 动态多态性:通过子类来重写父类的虚函数来实现的,在运行期间来决定调用哪个函数,这就是虚函数

虚函数
在基类的函数前加上virtual,在子类中重写该函数,运行时,会根据对象的实际类型来调用相应的函数。
实际上如果一个类中存在虚函数时,那么编译器会为该类生成虚函数表,保存虚函数的地址。派⽣类中⾃然⼀定有虚函数,所以编译器也会为派⽣类⽣成⾃⼰的虚函数表。当我们定义⼀个派⽣类对象时,编译器检测该类型有虚函数,所以为这个派⽣类对象⽣成⼀个虚函数指针,指向该类型的虚函数表,这个虚函数指针的初始化是在构造函数中完成的。

析构函数为啥可以成为虚函数,而构造函数为啥不能成为虚函数
答:析构函数之所以能成为能成为虚函数,是因为避免内存泄漏,因为如果一个父类的指针指向子类的对象时,使用完毕后准备销毁的时候,如果父类没有被定义为虚函数时候,那么编译器会根据指针类型就会认为当前对象是父类的指针,调用父类的析构函数,而子类无法析构,会导致内存泄漏。

构造函数不能成为虚函数的原因是:虚函数调用只需要知道部分,即函数接口,但是我们要创建一个对象时,需要知道完整的信息,因此构造函数不会成为虚函数。其次从编译器来看,多态的实现方式,虚函数的调用是通过实列化完成之后,根据虚函数表里的指针来查找哪个一个函数。如果虚函数表不存在,那么就不能实现多态了

纯虚函数
纯虚函数出现的目的是为了让继承出现多种情况

  • 有时候希望子类继承成员函数的接口
  • 有时我们希望派⽣类既继承成员函数的接口,继承成员函数的实现,而且可以在派⽣类中可以重写成员函
    数以实现多态
  • 在子类同时继承成员函数的接口和实现的情况下,不能缺省的实现
    哪些函数可以是虚函数
    构造函数,构造函数初始化对象,派⽣类必须知道基类函数⼲了什么,才能进⾏构造;当有虚函数时,每⼀个类有⼀个虚表,每⼀个对象有⼀个虚表指针,虚表指针在构造函数中初始化;
    内联函数,内联函数表示在编译阶段进⾏函数体的替换操作,⽽虚函数意味着在运⾏期间进⾏类型确定,所以内联函数不能是虚函数;
    静态函数,静态函数不属于对象属于类,静态成员函数没有this指针,因此静态函数设置为虚函数没有任何意义。
    友元函数,友元函数不属于类的成员函数,不能被继承。对于没有继承特性的函数没有虚函数的说法。
    普通函数,普通函数不属于类的成员函数,不具有继承特性,因此普通函数没有虚函数。

三、智能指针

智能指针相当于裸指针的封装。常见的智能指针有auto_ptr,unique_ptr,shard_ptr,weak_ptr
几种智能指针的优缺点和区别

  • auto_ptr已经在C++11中已经废除掉了,取而代之的是unique_ptr,相比之下,unique_ptr语义更加清晰,更加安全,不允许复制,更加高效,并且更好的支持数组,以及C++的新特性右值引用,因此auto_ptr逐渐被废除掉
  • weak_ptr本身不具备管理内存的能力,它主要解决的是shared_ptr循环引用的问题。weak_ptr的原理就是指向某个资源的时候,它不会增加该资源的引用计数
  • unique_ptr的主要是资源独占,它不可以复制。它通过析构函数来释放资源来管理对象的生命周期,来自动管理对象,可以防止多个智能指针指向同一个对象,更加方便管理,相比shard_ptr它的优点是更加高效,避免循环引用的问题
  • 当一个资源对象需要多个对象共享时,就无法使用unique_ptr。它通过循环引用计数器来对资源控制。

详解shared_ptr shared_ptr
shared_ptr内部维护了一个计数器,来跟踪多少个shared_ptr来指向资源,当计数器减少0时候,shared_ptr就会调用析构函数来释放资源

template<typename T>
class SharedPtr {
private:
	size_t* m_count_;
	T* m_ptr_;
public:
	//构造函数
	SharedPtr() : m_ptr_(nullptr), m_count_(new size_t) {}
	SharedPtr(T* ptr) : m_ptr_(ptr), m_count_(new size_t) { m_count_ = 1; }
	//析构函数
	~SharedPtr() {
		-- (*m_count_);
		if (*m_count_ == 0) {
			delete m_ptr_;
			delete m_count_;
			m_ptr_ = nullptr;
			m_count_ = nullptr;
		}
	}
	//拷⻉构造函数
	SharedPtr(const SharedPtr& ptr) {
		m_count_ = ptr.m_count_;
		m_ptr_ = ptr.m_ptr_;
		++(*m_count_);
	}
	//拷⻉赋值运算
	void operator=(const SharedPtr& ptr) { SharedPtr(std::move(ptr)); }
	//移动构造函数
	SharedPtr(SharedPtr&& ptr) : m_ptr_(ptr.m_ptr_), m_count_(ptr.m_count_) {
		++(*m_count_);
	}
	//移动赋值运算
	void operator=(SharedPtr&& ptr) { SharedPtr(std::move(ptr)); }
	//解引⽤
	T& operator*() { return *m_ptr_; }
	//箭头运算
	T* operator->() { return m_ptr_; }
	//᯿载bool操作符
	operator bool() { return m_ptr_ == nullptr; }
	T* get() { return m_ptr_; }
	size_t use_count() { return *m_count_; }
	bool unique() { return *m_count_ == 1; }
	void swap(SharedPtr& ptr) { std::swap(*this, ptr); }
};

循环引用是如何发生的,以及如何解决
在两个或多个相互引用,或者一些复杂的数据结构下,比如,在双向链表的情况下,存在多个引用路径的情况下,可能存在循环引用的问题,导致资源无法释放掉。这时候就需要weak_ptr来打破循环引用的问题,因为使用wea_ptr对同一个资源,不会增加同一个资源的引用计数。

weak_ptr弱在哪里
不影响对象生命周期: std::weak_ptr 不会增加对象的引用计数,因此不会影响对象的生命周期。如果只有 std::weak_ptr 引用一个对象,当所有 std::shared_ptr 都释放后,对象可能被销毁。

避免循环引用: 当两个对象相互引用,形成循环引用时,使用 std::shared_ptr 会导致内存泄漏。使用 std::weak_ptr 可以打破这种循环引用,避免资源无法释放。

shard_ptr是线程安全的吗?多线程使用智能指针来注意什么
智能指针并不是线程安全的。在多线程的环境下,如果多个线程同时访问和修改同一个智能指针,可能导致数据竞争和数据未定义的行为。我们使用智能指针的时候,应该多注意避免多个智能指针同时修改智能指针和所指向的对象,需要用线程同步的方法来保证线程安全。

四、移动语义

移动语义是C++11引入的一个重要特性,它通过右值引用(rvalue references)和移动构造函数(move constructors)以及移动赋值运算符(move assignment operators)实现。移动语义的目的是提高对资源管理的效率,减少不必要的拷贝操作,特别是对于大型对象或动态分配的资源。

什么是左值,什么是右值
左值是一个指向内存,具有具体名称的值,通常有个稳定的值,并且有段较长的生命周期。而右值通常是一个不指向稳定地址的匿名地址,声明周期较短。基于此特性,可以用取地址符来判断,能取到值的是左值,不能取到值的是右值。

前置++返回左值还是右值,后置++ 呢
前置++的实现直接对传入的对象自增,然后将此对象返回的值是一个稳定的值,而后置++的实现是创建一个临时的对象,然后传入对象自增,然后返回的是个临时对象,因此返回的是个右值

什么是左值引用,什么是右值引用

  • 左值引用:左值引用是指向左值的引用,左值引用用个单&符号表示。常量左值可以绑定左值,也可以绑定右值
  • 右值引用:右值引用是指向右值的引用,右值引用用&&符号表示。右值引用的主要作用是支持移动语义完美转发。通过移动语义可以减少不必要的拷贝操作,提高代码的性能。

深拷贝与浅拷贝

  • 深拷贝:会在堆上申请空间来存储数据,从而解决野指针的问题,在程序一个对象内容时,不影响另一个对象
  • 浅拷贝:仅仅复制对象的值,而不复制对象所指向的动态分配的内存,从而当对象快要结束的时候会调用两次析构函数

为什么拷贝构造函数的参数要使用引用传递?
在 C++ 中,使用引用方式传递参数是一种常见的做法,尤其是在拷贝构造函数的定义中。这种做法有一些重要的原因:

  1. 避免无限递归调用:
    • 如果使用值传递的方式,即在拷贝构造函数的参数中使用实际对象而不是引用,那么在调用拷贝构造函数时,会再次触发拷贝构造函数,导致无限递归调用,最终导致栈溢出。
    • 使用引用方式传递参数可以避免这种无限递归调用,因为引用只是传递对象的地址,而不是复制整个对象。
  2. 效率和性能:
    • 通过引用传递参数,避免了额外的内存拷贝操作。如果使用值传递,会创建对象的一个副本,这可能涉及到大量的内存拷贝操作,尤其是对于复杂的对象而言,会显著影响程序的性能。
    • 引用传递避免了这些拷贝开销,提高了程序的效率。

std::move()实现原理
std::move 是C++中的一个函数模板,位于 头文件中。它是用来将一个左值(lvalue)转换为右值引用(rvalue reference)的。std::move 并不实际移动任何数据,它只是告诉编译器你希望将一个对象视为右值,以便在需要时使用移动语义

template <typename T>
typename std::remove_reference<T>::type&& move(T&& t) noexcept {
    return static_cast<typename std::remove_reference<T>::type&&>(t);
}

  • typename std::remove_reference::type 用来移除传入类型 T 的引用。例如,如果 T 是 int&,那么 std::remove_reference::type 就是 int。

  • T&& 是一个右值引用类型,它表示对类型 T 的右值引用。

  • static_cast 用于执行类型转换,将左值引用转换为右值引用。

  • noexcept 是一个异常说明,表示这个函数不会抛出异常。

std::forward 的实现与 std::move 类似,它也是通过类型转换来实现的。以下是 std::forward 的简化版本实现:


template <typename T>
T&& forward(typename std::remove_reference<T>::type& t) noexcept {
    return static_cast<T&&>(t);
}

template <typename T>
T&& forward(typename std::remove_reference<T>::type&& t) noexcept {
    static_assert(!std::is_lvalue_reference<T>::value, "Can't forward an rvalue as an lvalue.");
    return static_cast<T&&>(t);
}

这里也使用了 std::remove_reference 来移除引用,然后通过 static_cast 将参数转换为相应的引用类型。std::forward 有两个版本,一个接受左值引用,一个接受右值引用。

重要的是,std::forward 的目的是在泛型代码中传递参数,确保它们的值类别和引用类型正确。在实现时,还添加了一个 static_assert,用于确保不能将一个右值引用作为左值引用传递,因为这可能导致悬空引用(dangling reference)的问题。

五、模板-template参数推导模板

模板(Templates)是C++中一种强大的机制,允许编写通用的代码,而不依赖于特定的数据类型。模板可以用于函数、类、和别的一些实体。这使得你可以编写一份代码,然后根据需要用不同的数据类型进行实例化。

  • 模板参数列表不能为空
  • 模板类型参数前必须使用关键字 class 或者 typename,在模板参数列表中这两个关键字含义相同,可互换使用。
template <typename T, typename U, ...>

** 函数模板和类模板的区别**

  • 实例化方式不同:函数模板实例化由编译程序在处理函数调用时自动完成,类模板实例化需要在程序中显式指定。
  • 实例化的结果不同:函数模板实例化后是一个函数,类模板实例化后是一个类。
  • 默认参数:类模板在模板参数列表中可以有默认参数。
  • 特化:函数模板只能全特化;而类模板可以全特化,也可以偏特化。
  • 调用方式不同:函数模板可以隐式调用,也可以显式调用;类模板只能显式调用。

什么是模板特化?为什么特化
模板特化的原因:模板并非对任何模板实参都合适、都能实例化,某些情况下,通用模板的定义对特定类型不合适,可能会编译失败,或者得不到正确的结果。因此,当不希望使用模板版本时,可以定义类或者函数模板的一个特例化版本。

模板特化:模板参数在某种特定类型下的具体实现。分为函数模板特化和类模板特化

函数模板特化:将函数模板中的全部类型进行特例化,称为函数模板特化。
类模板特化:将类模板中的部分或全部类型进行特例化,称为类模板特化。
特化分为全特化和偏特化:

全特化:模板中的模板参数全部特例化。
偏特化:模板中的模板参数只确定了一部分,剩余部分需要在编译器编译时确定。
说明:要区分下函数重载与函数模板特化
定义函数模板的特化版本,本质上是接管了编译器的工作,为原函数模板定义了一个特殊实例,而不是函数重载,函数模板特化并不影响函数匹配。

#include <iostream>
#include <cstring>
using namespace std;
//函数模板
template <class T>
bool compare(T t1, T t2){
    cout << "通用版本:";
    return t1 == t2;
}

template <> //函数模板特化
bool compare(char *t1, char *t2){
    cout << "特化版本:";
    return strcmp(t1, t2) == 0;
}在LINUX中我们可以使用mmap用来在进程虚拟内存地址空间中分配地址空间,创建和物理内存的映射关系。

int main(int argc, char *argv[]){
    char arr1[] = "hello";
    char arr2[] = "abc";
    cout << compare(123, 123) << endl;
    cout << compare(arr1, arr2) << endl;

    return 0;
}
/*
运行结果:
通用版本:1
特化版本:0

面试题整理

memcpy和strcpy整理

  • 复制方法的不同
    strcpy在复制字符时,直到遇到空字符’\0’为止
    memcpy则是直接按字节进行内存块的复制
  • 用途不同
    由于方法和复制的内容不同,strcpy主要用于字符串的复制,而memcpy更通用,可以复制任意类型的数据
    strcpy的缺点
    strcpy 函数的缺陷:strcpy 函数不检查目的缓冲区的大小边界,而是将源字符串逐一的全部赋值给目的字符串地址起始的一块连续的内存空间,同时加上字符串终止符,会导致其他变量被覆盖。

说明:从上述代码中可以看出,变量 var 的后六位被字符串 “hello world!” 的 “d!\0” 这三个字符改变,这三个字符对应的 ascii 码的十六进制为:\0(0x00),!(0x21),d(0x64)。
原因:变量 arr 只分配的 10 个内存空间,通过上述程序中的地址可以看出 arr 和 var 在内存中是连续存放的,但是在调用 strcpy 函数进行拷贝时,源字符串 “hello world!” 所占的内存空间为 13,因此在拷贝的过程中会占用 var 的内存空间,导致 var的后六位被覆盖。
C++中迭代器失效原因及有哪种情况
是的,迭代器是 C++ 中的一个模板类。C++ 标准库定义了多种类型的迭代器,以适应不同种类的容器和不同的需求。这些迭代器包括但不限于:

  1. 正向迭代器 (forward_iterator): 从容器的起始位置向结束位置移动。
  2. 双向迭代器 (bidirectional_iterator): 可以在正向迭代器的基础上向前移动。
  3. 随机访问迭代器 (random_access_iterator): 具有在常数时间内跳跃到序列中任何位置的能力。
  4. 输入迭代器 (input_iterator): 仅支持读操作。
  5. 输出迭代器 (output_iterator): 仅支持写操作。

在使用迭代器时,要注意迭代器的有效性。迭代器可能会在以下情况失效:

  1. 容器的修改: 如果在使用迭代器的过程中对容器进行了修改,迭代器可能会失效。这包括插入、删除等会影响容器大小的操作。

  2. 容器的重新分配: 对于某些容器,当元素数量达到一定阈值时,可能会发生重新分配内存的操作。这会导致之前获取的迭代器失效。

  3. 使用过程中删除迭代器指向的元素: 如果在迭代器指向的元素被删除之后仍然尝试使用该迭代器,可能会导致未定义的行为。

  4. 插入或删除元素: 当在容器中插入或删除元素时,可能会导致迭代器失效。这是因为插入或删除元素可能会导致容器重新分配内存,使原来的迭代器指向的地址无效。

    cppCopy codestd::vector<int> myVector = {1, 2, 3, 4, 5};
    auto it = myVector.begin();
    myVector.erase(it); // 这里it迭代器已经失效
    
  5. 容器扩容: 对于需要动态分配内存的容器(例如 std::vectorstd::string),在元素数量超过当前容量时,可能会触发容器的重新分配内存。这会导致所有先前的迭代器失效。

    cppCopy codestd::vector<int> myVector;
    myVector.push_back(1);
    auto it = myVector.begin();
    myVector.push_back(2); // 这里可能会导致myVector重新分配内存,使it失效
    
  6. 清空容器: 当使用 clear() 函数清空整个容器时,所有的迭代器都会失效,因为容器被清空了。

    cppCopy codestd::vector<int> myVector = {1, 2, 3};
    auto it = myVector.begin();
    myVector.clear(); // 这里it迭代器已经失效
    
  7. std::list 迭代器的赋值操作:std::list 中,迭代器的赋值操作可能导致其他迭代器失效。

    cppCopy codestd::list<int> myList = {1, 2, 3};
    auto it1 = myList.begin();
    auto it2 = myList.begin();
    ++it2;
    myList.insert(it2, 4); // 这里it1迭代器已经失效
    
  8. 超出生命周期: 如果迭代器指向的容器在迭代器生命周期结束后被销毁,那么迭代器就失效了。

    cppCopy codestd::vector<int>* pVector = new std::vector<int>{1, 2, 3};
    auto it = pVector->begin();
    delete pVector; // 这里it迭代器已经失效
    

在任何情况下,如果迭代器失效,继续使用它可能导致未定义行为,因此在进行潜在导致迭代器失效的操作后,最好重新评估和更新迭代器。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

我只爱炸鸡

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

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

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

打赏作者

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

抵扣说明:

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

余额充值