c++面试:基础语法篇

个人总结的c++基础语法,有一些可能不重要…

int main(int argc, char argv)**:main函数也可以命令行传参;argc表示参数个数,argv表示所有的参数,可以用getopt函数解析:

#include <iostream>
#include <getopt.h>
#include <string>

int main(int argc, char** argv) {
	int opt = 0;
	std::string conf_a, conf_b, conf_c;
	//只能输入-a xx或-b xx或-c xx 
	while ((opt = getopt(argc, argv, "a:b:c:")) != -1) {
		switch (opt) {
			case 'a':
				conf_a = optarg;
				break;
			case 'b':
				conf_b = optarg;
				break;
			case 'c':
				conf_b = optarg;
				break;
		}
	}
	//这里用于判断必须项有没有被赋值(假设-a,-b是必须项,-c是可选项),注意这里只能保证conf_a和conf_b一定有值,其他的无法保证
	if (conf_a.empty() || conf_b.empty()) {
		std::cout << "format: command -a <file> -b <file> [-c <file>]" << std::endl;	
		exit(EXIT_FAILURE);
	}	
}

main函数执行之前后可能执行的代码是什么:创建全局对象,调用构造函数,之后全局对象的析构。
面向过程和面向对象的区别:面向过程:分析解决问题的步骤,然后一步一步函数实现。效率高,但难维护,复用性低。面向对象:把问题分解成各个对象来解决,每个对象有自己的方法和属性。便于维护,复用性高,但开销大。
变量的定义和声明的区别:声明就是告诉编译器变量存在,不会分配内存,可多次声明,定义会给变量分配内存,只能定义一次。
c和c++区别:c面向过程,c++面向对象;c++多一些关键字如new,delete取代malloc,free;c++支持重载,虚函数,引用。
C++中struct和class的区别:默认权限不同以及不能定义模板,其他基本可以互用。但与c的struct不同,c的struct作为一个数据集合,无成员函数,没有访问权限
全局变量和局部变量区别:全局变量存储在全局区,生命周期与程序一样,作用域是全局的,在编译时分配内存;局部变量存储在栈区,函数调用完就消失,作用域在函数内部,调用时才分配内存。
ifndef:预处理指令,检查一个宏是否未被定义,#elif, #else #endif;一般用于通过宏来设置编译不同的代码。而头文件包含用#progma once
static:修饰变量和函数:

修饰变量:不管什么变量,它就会被存储在全局区, 且只会被初始化一次。
修饰全局变量或全局函数的作用:只能在定义的文件中访问,即禁止extern
局部变量:为了让变量在上一次的基础上变化。
修饰类:静态成员和静态变量,都是全类共享。且静态成员函数只能访问静态成员变量,因为它没有this指针这个隐式形参。

那静态成员函数如何实现访问非静态成员
1、既然他没有this指针,那我们手动传一个对象做形参
2、函数内部创建对象来访问。

const:用于修饰变量或成员函数。

修饰变量,此变量在运行时值不能变。如修饰形参,函数内部就不能改变它。
修饰类:
修饰成员函数:即成为常函数,内部不能修改成员变量的值
修饰成员变量,则它只能调用常函数
const修饰的成员函数可以用于重载

constexpr: 用于编译时定义常量,const是运行时定义常量。
constexpr主要用于替换define宏定义
如:普通数组的大小,要在编译时已知,就可以用constexpr。(但gcc其实不需要了)
// 2个成员函数
void func() const; //常成员函数,常对象只能调用常函数
void func();

inline:修饰函数成为内联函数,编译时把函数调用的表达式替换成函数代码,避免了运行的函数调用的跳转开销,但耗内存,增加编译时间,所以要权衡。一般用于频繁调用的小型函数或短循环。内联声明只是建议,最终还是由编译器决定。
define:预处理指令:宏定义,预处理阶段进行文本替换,无类型检查,不遵守作用域规则的,因为在预处理阶段就替换了,所以慎用。const是在编译阶段定义常量,有类型检查,会分配内存。
定义长宏:如日志宏要替换成一个函数,使用do while(0),举例:

#define LOG(level, msg, ...)\
    do {\
        Loger& loger = Loger::GetInstance();\
        loger.SetLevel(level);\
        char buf[1024] = {0};\
        snprintf(buf, sizeof(buf), (std::string("[") + #level + "] " + msg + \
        " (" + __FILE__ + ":" + std::to_string(__LINE__) + ")").c_str(), ##__VA_ARGS__);\
        loger.Log(buf);\
    } while (0)

因为我们使用这种函数宏时会在后面加分号,while(0)正好可以接受分号。

typedef:定义一个类型,如:

typedef struct Send_Data{
    int a;
    string b;
} Send_Data;
也可用using
using InPut_Data = struct {
    int a;
    string b;
};

typedef,inline和const都在编译阶段执行,且都有类型检查;

volatile:我们知道编译器为了加快代码运行速度,会把内存的数据拷贝到寄存器,后续对变量的读取或者修改都通过寄存器这个中间人即cpu-寄存器-内存,这会导致内存变量和实际变量值不一致。加上volatile编译器就不会优化了,直接cpu-内存。它的作用:保证变量的可见性,对变量的修改所有线程会立即可见;保证代码执行的有序性:禁止指令重排序;
但不保证原子性(多线程共享变量,一个++,一个读取),所以需要保证原子性不能用它。举例:当用一个变量的改变标识程序的启动就需要可见性。当需要代码执行有序时a=1,b=2;
extern:对变量或函数声明,以便使用他们。

你想使用别人源文件里定义的变量或函数,要么包含对应的头文件,头文件里extern了。不然访问不了。
extern “C”声明下面引用的代码按c来编译,因为c++有重载,编译时会带上形参,c编译时不会带上形参,只包含函数名,作用就是让c++代码兼容c代码。注意:static修饰的全局变量或函数无法extern,它限制了可见域。
只有c++用到库代码采用extern “C”, 如:

extern "C" {
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
}

explicit:用于修饰构造函数,表示只能显示调用构造函数,如:
expilict test(const test& t) {}
A a; A b = a;这个隐式构造就不行
final:放在类名后面则类不能被继承;放在虚函数名后面则虚函数不能被重写。
override:显式表明该函数是重写父类的虚函数,会进行编译检查。
数组名与指向数组首元素的指针:数组名在sizeof和&操作与指针不同之外,可认为是个指针常量,指向首元素地址即a[0]&ap[0];但它作为形参就会退化成一般指针。
程序编译的4个阶段:

  1. 预处理:处理以#开头的预编译指令,如宏替换、头文件包含,得到预处理后的源代码
  2. 编译:语法检查,把源代码转换成汇编代码
  3. 汇编:把汇编代码转换成二进制代码即机器指令,生成二进制文件
  4. 链接:把所有目标文件链接到一起生成最终的可执行文件。

静态绑定和动态绑定:静态绑定在编译时根据变量类型确定要调用哪个方法,动态绑定在运行时根据对象类型确定调用哪个方法,只有虚函数才是动态绑定。
为什么只有引用和指针能实现动态绑定即动态多态:因为指针或引用在编译时不关心所指对象的类型,只要与指针或引用指向的类型兼容就行,而父类和子类类型是兼容的。所以这就使得父类的指针或引用可以在运行时根据所指对象类型动态绑定,而直接赋值在编译期就确定类型了。
静/动态链接
就是静/动态库的区别:

  1. 静态库:链接时把代码复制到对应的文件里了,运行时程序不依赖静态库。性能高,但耗内存,更新不便
  2. 动态库:链接时只把库的入口加载到程序中,应该叫dll,运行时时再调用具体函数。所以它内存占用小,性能慢一点,更新方便

指针和引用
1、 指针就是一个变量类型,他与int创建的变量没有区别,只不过存的值是地址;
2、引用是变量的别名或者叫小名,就是原变量,引用依附于原变量存在,所以必须在创建时就要绑定变量。
三种传参:
值传递、指针传递、引用传递
1、 值传递:形参拷贝实参,互不影响,一般用于不想改变实参的小型 数据。
2、 指针传递:本质也是值传递,拷贝的是指针,但可以通过解引用得到变量地址进行修改,所以可以认为指针传递间接改变实参值。
3、引用传递:形参就是实参别名,动形参就是动实参。
这2个如何选择:能用给引用传递就用引用传递(引用最初就是为了简化指针的写法),效率高(不需要解引用)、代码可读性高、不产生拷贝副本(指针还要拷贝8个字节呢)。但若要直接操作地址如链表,树,用指针更好。
引用作为成员变量占内存吗: 占用,大小等同指针8或4,要存储绑定对象的地址。
函数指针:指向函数的指针,因为函数也有地址,可用函数指针调用函数。语法int(*p)(int,int)=test;。用途:回调函数,把函数作参数:如创建线程时就得分配一个回调函数。
野指针和悬空指针:野指针是未初始化的指针;悬空指针:指向被释放了的内存的指针(浅拷贝易出现)。他俩都是不安全的,避免:养成初始化的习惯,delete后立即置空,使用智能指针自动管理。
++i与i++的区别:后置由于返回旧值,所以会调用拷贝构造;前置返回新值,所以直接返回的是引用。
strcpy和memcpy,snprintf:strcpy只能拷贝字符串,拷贝\0后结束,memcpy拷贝的是内存,所以不限类型,需要指定长度;snprintf用于把格式化的数据写入字符串,如

char a[10] = {0};
snprintf(a, sizeof(a), "hello, %d", 5); //指定了最多写入多少,避免了缓冲区溢出

strcpy:复制字符串到\0,若接不下缓冲区会溢出,所以要手动注意目标字符串长度要大于源字符串。考虑内存覆盖:

char* mystrcpy(char* s1, const char* s2) {
    assert(s1 && s2);
    int len = strlen(s2) + 1;//把\0也复制
    if (s1 < s2) {
        for (int i = 0; i < len; i++) *(s1 + i) = *(s2 + i);
    } else if (s1 > s2) {
        for (int i = len; i >= 0; i--) *(s1 + i) = *(s2 + i);
    }
    return s1;
}

void* mymemcpy(void* s1, const void* s2, int len) {//字节数;代码与strcpy相似。
void* memset(void* s, int c, int size) ;for循环赋值
memset也是直接操作内存,一般用于清结构体,类,但注意若类里有虚函数表会破坏虚函数表,因为虚表指针被清了。
strncpy:strcpy是一直拷贝完\0结束,有缓冲区溢出的风险,strncpy指定拷贝n个字符,相对安全,要手动加\0。
strlen和sizeof:sizeof是运算符,求任意类型/数据的大小,求字符串时会包括\0,strlen是库函数,用于求字符串长度,不包括\0
void*:void做形参,可接受任意类型指针/地址,void做形参,要传一级指针的地址再转(void),他俩在函数内部都要转换成具体类型再操作。
RTTI:运行时类型信息,依赖虚表实现。2个机制:typeid运算符:获取对象类型;dynamic_cast:用于安全转换。
C++四种强制转换:C的强制类型转换没有类型检查,所以C++引入4种,会做类型检查,避免明显的错误。
1、static_cast:用于基本类型、子类指针转父类指针,且父类指针转子类指针不要用它(没有动态类型检查);
2、 const_cast:去除指针的const属性
3、 reinterpret_cast: 不做任何检查,任意类型的指针直接转,要谨慎 使用。
4、dynamic_cast:用于虚基类指针转子类指针,失败返回nullptr,它在运行时有类型检查。为啥一定是虚基类:因为它是RTTI的体现,RTTI依赖虚表。
个人感觉:这4个不好用,要转换:直接(double)a;
且形参若是void
,则可接受任意类型的指针。

fork,wait,exec函数:fork函数用于拷贝父进程的一个副本创建子进程,在父进程中返回子进程的进程ID,在子进程中返回0。读时共享一块内存,写时才拷贝;wait函数用于父进程阻塞等待子进程的终止。exec函数在当前进程执行一个新程序,并取代进程内容,一般在子进程里使用exec,若在父进程使用的话就直接覆盖主程序了。
cout 和printf:printf(const char*, …),输出会解析字符串,遇到%就从可变参数中获取相应的值替换%最后输出。cout其实是ostream类的一个实例,使用cout会创建一个输出流对象,使用<<把数据插入到对象里,cout<<可以输出各种类型是因为<<已经重载了各种类型。他俩输出都是先输出到缓冲区在输出到屏幕,但c++有endl手动刷新缓冲区。
若想输出自定义类型如类对象需要重载<<:

class A{
private:
    int a;   
public:
    A(int b): a(b) {}
    friend ostream& operator<<(ostream& cout, const A& s) {
        cout << s.a;
        return cout;
    }
};
int main() {
    A s(1);
    cout << s;//s.a
}

判断2个浮点数是否相等:abs(a-b)<=定义的误差
函数调用的过程
首先我们要知道每次函数调用都会产生一个栈帧,保存函数调用的一些信息,如形参,局部变量,返回地址,寄存器,可以认为栈帧存储了函数调用的上下文。假设现在有个程序启动了,操作系统会分配一个栈给他,到了main函数:创建main栈帧,入栈;执行f1函数,创建f1栈帧入栈。而程序的执行上下文栈顶决定的,所以它来到了f1函数,执行完把返回值返回,f1栈帧出栈,程序回到main栈帧执行的上下文。

this指针:是非静态成员函数的隐式形参,谁调用函数,this就指向那个对象,本身是常指针T*const this。成员函数为啥能直接访问成员变量就是隐式调用了this指针。一般用于:返回对象本身,可return *this; 还有就是区分形参名和成员变量名。在成员函数中调用delete this就是释放对象内存。在析构函数delete this会栈溢出,因为delete this又会调用本对象的析构,就无限递归。
C++模板:泛型编程技术,允许编写代码时使用参数化类型,调用时按类型匹配,提高代码复用性。模板主要有函数模板和类模板,模板类创建时时要显示指定类型。底层原理:编译器进行2次编译,先是对定义的模板代码进行编译,遇到模板调用时进行模板实例化确定具体类型,得到具体代码再进行编译。
偏特化:给特定的类型开个小灶,给他一种特定的实现。

template<typename T>
T mycompare(T& a, T& b) {
    return a > b ? a : b;
}
Template <typename T1, typename T2> 
Class A{
Public:
T1 m_a;
T2 m_b;
A(T1 a, T2 b): m_a(a), m_b(b) {}
};

类模板使用要显示指定:A<int ,string> pa(1, “ad”);
template
T Add(T a, T b) {return a + b;}
运算符重载:本质就是通过定义运算符的重载函数,实现自定义数据类型的运算,如对象+数字;
实现:成员函数(一般的±=()用成员函数就行,this绑定运算符左侧对象,所以传参数少写一个,具体几个参数和你的运算符一致)或者友元(重载输入输出运算符,这个背下来)

class A {
public:
	int m_a;
	A(int a) : m_a(a) {}
	A() {}
	void operator+(int a) {
		m_a += a;
	}
};
int main() {
	A a(1);
a + 1;

异常处理:通常用try,throw,catch,先执行try包含的语句,若throw会抛出异常try的代码停止,catch捕获处理,处理完后程序继续运行。
int main() {
int a = 1;
try {
if (a == 1) throw -1;
} catch(…) { //捕获所有异常
cout << “未知异常”;
}
cout << 2;
}
但一般我们不主动抛出异常:只会去try判断是否有异常,如:

try {
	double num = std::stod(id);
} catch(...) {
	std::cout << stod failed! << std::endl;
}

C++11有哪些新特性:auto,decltype类型推导,智能指针,移动语义,lambda表达式等。
移动语义
1、 要说移动语义必须先谈右值引用:能取地址的是左值,反之就是右值,如临时对象。右值引用就是一个标识,标识对象的资源可以被移动。用于移动语义。
2、 举例:现在我有2个容器A,B,我要把A的一个元素移到B里,
我需要调用B的拷贝构造,创建一个元素,然后把A的元素销毁,实现移动。即需要一次拷贝一次析构实现移动。而移动语义是直接用A的那个元素的资源来创建B的元素,即不需要申请新资源,避免了不必要的资源拷贝。
3、 如何实现的:先把std::move把对象变成右值,然后调用移动构造。
4、 实际使用场景:如一个函数里,我创建了一个string,返回值就是返回这个string,那就可以用移动构造。不然你直接return string会调用拷贝构造。
5、std::move函数原理:就是通过static_cast<T&&>把对象转成了右值引用。

std::string func() {
	...
	std::stirng str = "aaa";
	//return test(str);
	return test(std::move(str));
}

完美转发:模板+forward实现,保证传递参数的左右值属性不变,让编译器自动推导是拷贝语义还是移动语义,避免了不必要的拷贝,如push_back()

void func(int& i) {cout << 1;}
void func(int&& i){cout << 2;}
template<typename T>
void test(T&& i) {func(forward<T>(i));} //T&&表示左右值都能接收
int main() {
    int a = 1;
    test(a);//传左值
    test(1);//传右值
}

智能指针
1、定义:是个模板类,遵循RAII原则,把资源的申请和释放放在了对象的构造和析构函数里,实现资源的自动释放,防止内存泄露。且它重载了->和*运算符,让我们使用它和使用普通指针一样。
2、创建指针语法:

unique_ptr<int> ptr = make_unique<int>(1);
shared_ptr<int> ptr = make_shared<int>(1);

3、三种类型:
(1)unique_ptr:独占指针,同一时间只能有一个指针指向该资源,内部原理:禁用了拷贝构造和拷贝复制。但可以移动(因为移动了还是只有独占的)。
实现:

template<typename T>
class unique_ptr {
public:
    unique_ptr(T* ptr): p(ptr) {}
    ~unique_ptr() {
        delete p; 
        p = nullptr;
    }
    unique_ptr(const unique_ptr& s) = delete;
    unique_ptr& operator=(const unique_ptr& s) = delete;
    //移动构造
    unique_ptr(unique_ptr&& s): p(s.p) {
        s.p = nullptr;
    }
    //移动赋值,让我换指向
    unique_ptr& operator=(unique_ptr&& s) {
        delete p;
        p = s.p;
        s.p = nullptr;
        return *this;
    }
    T* operator->() {
        return p;
    }
    T& operator*() {
        return *p;    
    }
private:
    T* p;
};

(2)shared_ptr:共享指针,同一时间可以有多个指针指向一个资源,采用引用计数机制,计数为0才会释放资源。且它的计数是线程安全的。
实现:

#include <atomic>
template<typename T>
class Shared_ptr {
public:
    Shared_ptr() : p(nullptr), cnt(nullptr) {}
    Shared_ptr(T* p) : p(p), cnt(new std::atomic<int>(1)) {}
    ~Shared_ptr() {
        release();
    }

    //拷贝构造
    Shared_ptr(const Shared_ptr& s): p(s.p), cnt(s.cnt) {
        if (p) {
            ++(*cnt);
        }
    }

    //拷贝赋值, 我本身指向的资源计数-1, 现在要指向的计数+1
    Shared_ptr& operator=(const Shared_ptr& s) {    
        release();
        p = s.p;
        cnt = s.cnt;
        if (p) {
            ++(*cnt);
        }
        return *this;
    }

    //移动构造: 计数不变
    Shared_ptr(Shared_ptr&& s): p(s.p), cnt(s.cnt) {
        s.p = nullptr;   
        s.cnt = nullptr;
    }

    //移动赋值,我要改变指向,原计数-1,新计数不变
    Shared_ptr& operator=(Shared_ptr&& s) {
        release();
        p = s.p;
        cnt = s.cnt;
        s.p = nullptr;
        s.cnt = nullptr;
        return *this;
    }

private:
    T* p;
    std::atomic<int>* cnt;

    void release() {
        if (p && --(*cnt) == 0) {
            delete p;
            delete cnt;
            p = nullptr;
            cnt = nullptr;
        }
    }
};

(3)weak_ptr:弱指针,为了解决2个共享指针相互引用导致死循环的问题,A类有B类的智能指针成员,B类有A类的智能指针成员,创建2个智能指针让他们相互指向,即计数都为2,2指针不管谁先出作用域计数也只能减到1,无法释放,他俩互相等对方释放资源,陷入死循环,此时只需要把其中一个改为weak_ptr即可,因为weak_ptr不会导致计数增加,且他能通过lock()函数提升为shared_ptr访问资源,若为空说明资源被释放了。即达到了循环引用的目的又不会形成死循环。

class B; // 前向声明

class A {
public:
    std::shared_ptr<B> b_ptr;
};

class B {
public:
    //std::shared_ptr<A> a_ptr; 
    std::weak_ptr<A> a_ptr;// 使用 std::weak_ptr 避免循环引用
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    a->b_ptr = b;
    b->a_ptr = a;
}

4、unique_ptr与shared_ptr如何选择:基本都是用unique_ptr, 除非一个资源确实要在多个部分共享(如多线程共享某个类对象),才会考虑shared_ptr,因为他要维护计数,开销更大。

自定义删除器
默认的删除器就是调用智能指针的析构,也可以自定义,在某些场景下,如连接池,连接用完不释放,放回队列,这就可以用。

unique_ptr(必须声明删除器类型):
    std::unique_ptr<MySql, std::function<void(MySql*)>> sp(connQue_.front(), [&](MySql* p){
        xxx
    });
    
 shared_ptr(不用声明):
    std::shared_ptr<MySql> sp(connQue_.front(), [&](MySql* p){
        xxx
    }

自动类型推导auto,必须要有初始值,一般用于比较复杂的类型;
delctype:返回表达式的类型,不会真正运行该表达式。
NULL和nullptr:nullptr只表示空指针,NULL即可表示空指针,又表示整数0。
Lambda表达式
1、定义:就地创建函数对象供我们使用,而不用特意创建一个函数或者类。它还可以捕获外部变量使用。
2、底层原理:编译器把他当成仿函数,重载了函数调用运算符,所以它可以像函数一样调用。而它的变量捕获,如值捕获就是成员变量拷贝外部的值,引用捕获就是外部变量的引用。
3、常用于如自定义排序:sort(a.begin(), a.end(), [](auto a, auto b){return a > b;}); sort第3个参数接收一个函数对象,可以就地使用lambda表达式而不需要特地去定义一个函数了;优先级队列priority_queue<int ,vector, decltype(f)> q(f);优先级队列接收函数对象的类型,且初始化函数对象。以后就这么写。
4、个人理解:最多的用于匿名回调和作为函数对象传参。

可变参数模板:普通的模板允许参数类型泛化,可变参数允许参数的个数泛化,可变参数模板2者结合:参数的个数、类型都泛化。
可变参数模板函数(代码背下来);接收任意类型和数量的参数。

void print() {} //上面要写一个无参版本,表示递归终止条件
template<typename T, typename... Args>
void print(T arg, Args... args) {//arg是本次展开的,args是剩余的参数包
    cout << arg <<endl;
    print(args...);//递归调用
}

//可变参数模板类,创建对象时可传入任意数量和类型的参数包,这里还打印了。

template<typename... Args>
class A{
public:
    A(Args... args) {myprintf(args...);}
    //下面的部分使可变参数模板函数了,通过构造函数调用的。
    void myprintf() {}
    template<typename T, typename... Rest>
    void myprintf(T num, Rest... rest) {
        cout << num <<endl;
        myprintf(rest...);
    }
};

a与&a的区别:&a得到数组地址,&a+1会偏移一整个数组

int main() {
    int a[] = {1,2,3};
    int* p1 = (int*)(&a + 1);
    cout << *(a + 1) << " " << *(p1 - 1); // 2 3
}

assert:断言,若断言的表达式为假,会终止程序。如assert(x==5); release版本会把他禁用掉
namespace:命名空间,一定要多用,少用using namespace std
uint8_t: uint8_t、uint16_t、uint32_t、 uint64_t、int8_t等,以后多用这些,#include <iostream>
能用double不用float,常量默认就是double
enum:枚举,使用枚举加上类型,更安全,且结构体和枚举的定义不用加typedef:

enum Status {
  OK, //不赋值默认是0,1,2,若赋值,则赋值后面的依次+1
  ERROR,
  UNKNOWN
};

//使用
func(Status::OK);

结构化绑定
用法1:遍历map

    std::unordered_map<int, std::string> map = {
        {1, "aa"}, 
        {2, "bb"}
    };
    for (const auto& [id, name]: map) {
        std::cout << id << ", " << name << std::endl;
    }

用法2:一次性初始化多个变量:

auto [id, name, res] = std::make_tuple<const uint8_t, std::string, std::vector<int>>(1, "123", {1});
  • 17
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值