c++知识盲点总结

1:字节对齐

(1)自身对齐值:每种数据类型,都有自己的对齐值,基础数据类型的对齐值就是其字节数,结构体类型的对齐值是其成员变量中对齐值最大的那个。
(2)指定对齐:使用 #pragma pack (value) 可以指定对齐值value。
举个例子:

struct B{
 7     char   b;
 8     int    a;
 9     short  c;
10 };

这个结构体一共占12个字节,为什么呢?
b的对齐值为1,因此占用第一个字节,a的对齐值为4,地址起始值为4的倍数,因此从第4个字节存起到4+4=8,c的对齐值为2,8是二的倍数,8+2=10。B本身的对齐值是其中对齐值最大的元素a的对齐值4。因此B最终占用的字节数要是4的倍数,且大于10,就是12了。

2:内存拷贝

这个题之前头条问过,当时没有考虑内存重叠的情况。
这里注意几个点:

  1. 是src前要加const,属于好的编码习惯。
  2. void*,万能指针,转化为char*之后,加1时一次移动一个字节。
  3. 内存根据不同的重叠方式,选择从后往前或从前往后拷贝。
void* memcpy(void* des,const void* src,size_t count)
{
	if (!src || count <= 0) return des;
	char* td = (char*)des;
	const char* ts = (const char*)src;
	//如果源地址 + count 小于目的地址或者目标在复制过程中可能会和原地址重叠,正向拷贝
	if (ts + count < td || td + count > ts)
	{
		while (count--) *td++ = *ts++;
	}
	//否则进行逆向拷贝
	else
	{
		char* ttd = td + count - 1;
		const char* tts = ts + count - 1;
		while (count--) *ttd-- = *tts--;
	}
	return des;
}
 
3:static关键字

主要注意点三条

  1. 无论在函数外部还是内部,都只会被初始化一次
int add(){
    static int a = 0;
    return ++a;
}
int main(){
    cout << add() << add() << add() << endl;
}

输出 1 2 3

  1. 无法被其他文件直接使用,如果需要,只能借助于非static类型变量
    文件A中:
static char C = 'A';
char c = C;
static int F(){
	return 5;
}
int f(){
	return F();
}

文件B中:通过f和c,间接引用F和C

int main(){
	extern int f();
	extern char c;
	cout << f() << endl;
	cout << c << endl;
	return 0;
}
  1. 最后这条就简单了,类中的static类型属于类
4: 继承、虚函数、纯虚函数

c++继承和其他语言有个很大的区别在于,他的继承分为 private public protect。
(1)子类会继承父类的所有非静态成员。
(2)继承成员的暴露情况依据继承方式而定,如父类是private,继承成员都是private,如父类是protect,继承成员最高为protect。
(3)成员变量寻找,遵循 子类->子类中的父类属性 的顺序,在子类中找到,即使为private,不能访问,也不会再去父类中查找了。

虚函数和多态
多态,往简单的说,就是父类指针调用子类对象。
不使用虚函数的情况下,c++是没有多态的,比如这个例子。

class Person{
public:
    void say(){
        cout << "person" << endl;
    }
};
class Student:public Person{
    void say(){
        cout << "student" << endl;
    }
};
int main(){
    Student st;
    Person& p = st;
    p.say();
    return 0;
}

输出 person。这是因为c++静态语言的特性,编译的时候就已经确定好了。
c++中虚函数的作用一共有两个:
(1)实现多态的特性。

class Person{
public:
    virtual void say(){
        cout << "person" << endl;
    }
};

class Student:public Person{
    void say(){
        cout << "student" << endl;
    }
};

int main(){
    Student st;
    Person& p = st;
    p.say();
    return 0;
}

由于父类的say是虚函数,在运行时,会先执行子类的say方法,如果子类没有say,才会去虚函数表中查找。
关于虚函数表,具体可以参考这篇文章:
https://blog.csdn.net/haoel/article/details/1948051
其中有提到,如果子类的虚函数覆盖了父类的虚函数,使用子类调用时,子类虚函数的优先级也在父类之上。

(2)纯虚函数和抽象类。
纯虚函数就是没有函数体的虚函数,具体声明方式如下:

class Base{
public:
    virtual void f() = 0;
};

具体实现就是虚函数表中,指向函数体的指针为0,空的。
包含纯虚函数的类,就是抽象类,无法被实例化。

但基本上,只有成员函数才能够声明为虚函数,也只有如此才有意义,以下几种函数是无法声明为虚函数的:

普通函数(非成员函数)
静态成员函数
内联成员函数
构造函数
友元函数。

(3) 虚析构

class Person{
public:
    virtual ~Person(){
        cout << "person die" << endl;
    }
};

class Student:public Person{
public:
    ~Student(){
        cout << "student die" << endl;
    }
};

在父类中加上virtual,父类和子类的析构函数都变成虚析构了,那么虚析构有什么用呢?
在多态下,使用父类指针指向了子类对象,如:

Person *p = new Student;
delete p;

释放p的时候,如果父类不是虚析构,只会调用父类的析构函数,但由于真实指向的是子类,这么做难免清理不干净内存。如果使用虚析构,由于c++的动态联调,会同时调用子类和父类的析构函数,达到清理的目的。
如上面的例子,最终会打印:

student die
person die

c++中有两种多态,静态多态在编译时确认,用的泛型,动态多态在调用时确认,用的就是虚函数了。

5: 棱形继承问题和虚继承
class A{
protected:
    int a;
};

class B : virtual public A{
    int b;
};

class C : virtual public A{
    int c;
};

class D : public B,public C{
    int d;
public:
    void f(){
        cout << this->a << endl;
    }
};

B C继承于A,于是A的成员变量a在B中有一份,在C中也有一份,D继承了B和C,那么D中的a是用哪个B中的还是C中的呢?这就出现了棱形继承问题。解决的办法就是B C虚继承于A。
另外c++默认继承方式是私有的。

6: 不能重载的几个运算符
.(成员访问运算符)
->(成员指针运算符)	
::(作用域解析运算符)
?(条件运算符)
sizeof运算符
7: #include 后面跟引号与尖括号的区别?

引号编译器会搜索当前工作目录下的头文件,尖括号会搜索安装目录下的头文件。

类成员函数的重载、覆盖(重写)区别

重载: 发生在同一个类中,或者同一个文件中,函数名称相同,参数不同,就会发生重载,具体调用哪个,根据参数决定。
覆盖:发生在父类和其子类之间,子类如果有方法的方法名、参数、返回值都和父类相同,就会出现覆盖。

c语言数组初始化问题

在c语言中,下面这种写法是错误的,因为数组不能使用变量初始化,否则编译器在编译的时候,不知道给数组分配多少的内存空间。

void f(int size){
    int arr[size] = {0};
}

但是,下面这种写法是正确的,使用模板。

template <class T,int size>
void f(){
    int arr[size] = {0};
}
8: c++中没有super关键字

java或者objective c中,都有super关键字,用于指明调用父类的某个方法,但c++中,由于支持多继承,没有super关键字,但可以【父类名::方法名】来调用。

9: extern关键字

文件A中声明了全局变量num

int num = 20;

main.cpp中要使用这个全局变量20

int main(){
	extern int num;
	cout << num << endl;
}

就这么简单,关于有些博客说如下方法会报错

void f();
int main(){
	f();
}
int num = 20;
void f(){
	cout << num << endl;
}

因为编译器在执行main的时候,并不知道num的存在,需要在main前用 extern num 声明。这个要看编译器,xcode的llvm不会出现这样的错误。

10: 左值、右值、左值引用、右值引用、move()

c++中所有的值必属于左值、右值两者之一,两者简单的区别就是:
可以取地址的,有名字的就是左值,反之,不能取地址的,没有名字的就是右值(将亡值或纯右值)。
比如 int a = b + c,a就是左值,变量名为a,通过&a可以获取该变量名地址。表达式 b+c、 函数 int func() 的返回值是右值,在其赋值给某一变量前,不能通过变量名找到它, &(b+c) 是无法通过编译的。

关于右值(将亡值):

int val = 10;
(val + 5); <- 这就是一个右值,因为它只是计算的周期驻留在临时的寄存器中,没有固定的内存地址,给右值赋值就会出现错误。

通常来说,函数返回值是右值,如

int& f(){
    return 2;
}
f() = 3; <- 报错,Non-const lvalue reference to type 'int' cannot bind to a temporary of type 'int'

f返回的是一个右值,变为左值就会报错,但如下情况是可以的

int global_val = 10;
int& f(){
	return global_val;
}
f() = 20;

函数返回的 global_val 是一个全局变量,有自己的内存地址,因此是一个左值。
这种性质在运算符重载的时候,其实很有作用,入

vector<int> v(10,1);
v[2] = 10;

这里的 v[2] 实际上调用的就是运算符重载函数,这里给运算符重载函数赋值了。
左值和右值还有意思的一点就是 a++ 和 ++a,其中 a++ 会创建一个a的拷贝,然后返回的拷贝是一个临时对象,因此是右值,而++a最后返回是是a本身,因此是左值。

左值引用和右值引用
先说概念

int b = 10;
int& a = b;	//左值引用,用&表示
int&& c = (b + 1);	//右值引用,用&&表示

b+1的值是保存在寄存器中的,右值引用可以让我们直接使用寄存器的值。

move() : 作用是将左值变为右值,先看一下使用方式

string str = "abc";
vector<string>v;
v.push_back(str); //这里会触发拷贝构造,str中使用char[]来保存 “abc”,在拷贝构造中,"abc"会被复制一份。
//但如果str使用完了,就没用了,"abc"的复制就显得多余,这时候就可以使用 移动拷贝构造

//move将str变为了右值,因此调用移动拷贝构造,这样str中的char[]数组的所有权就被转交给了vector中的元素,原先的str中的char[]就变成了空。
//变成了右值,str自然就要消失了
v.push_back(move(str));

移动拷贝构造相当于

class string{
private:
	char c[];
public:
	//移动拷贝构造,接收值是一个右值引用
	string(const string&& str){
		this->c = str.c;
		str.c = NULL;
	}
	~string(){
		if(this->c != NULL){
			delete c[];
		}
	}
}

move的转换应该还有其他的使用方式,先看到这儿了。

11: NULL和nullptr的区别

应该使用nullptr而不是NULL,因为NULL是0,在函数调用的时候会有二义性问题。
NULL在c中的定义是:

#define NULL ((void *)0)

也就是说NULL实际上是一个void *的指针,然后吧void *指针赋值给类似 int * 的指针的时候,隐式转换成相应的类型。而如果换做一个C++编译器来编译的话是要出错的,因为C++是强类型的,因此c++中使用 0 来表示 NULL。

#define NULL 0 

但这种做法,在下面这种情况就会有二义性问题

void f(int a){
    cout << a << endl;
};
void f(int* a){
    cout << "*" << endl;
};
int main() {
    f(NULL);
}

提示:

Call to ‘f’ is ambiguous 编译器不知道该调用哪个函数,因为两个都符合。
如果使用 nullptr就不会,它的类型为 nullptr_t ,一个模板类型的空指针,可以指向任何类型的对象。下面的函数会调用 f (int* a) 函数。

int main() {
    f(nullptr);
}
12: 悬空指针

其实就是野指针的一种,指向的对象被delete,但是指针没有设置为nullptr,仍然指向那块内存。

13: 运行时类型识别 dynamic_cast

首先要说的是,c++中无论使用虚函数实现多态还是 dynamic_cast 都需要共有继承。
有如下继承关系:

class Base{
};
class Child : public Base{
};

下面的写法默认是对的

Base* b = new Child();

但这么写就是错的

Base* b = new Base();
Child* c = b; 	//错误,子类可能包含父类没有的方法或成员变量,因此这么做是不安全的。

如果一定要将一个派生类指针或引用转化为基类的指针或引用,就需要使用 dynamic_cast

Child* c = dynamic_cast<Child*>(b);
14: 编译过程
  1. 预处理 -> 主要就是宏定义替换,预处理命令,c++还有const类型的替换等,生成.i© .II(c++)
  2. 编译 -> 将c源码编译成汇编,这一步生成的文件后缀 .s
  3. 汇编 -> 编译成机器指令,这一步生成 .o
  4. 链接,将编译后的文件,彼此链接。
15: list的使用

c++中的双向链表,如果需要在线性adt中插入数据,使用list是比较合适的,特别是最近常遇见的围着一个桌子报数的问题。如果两端需要插入就用deque。
stack、queue、deque、vector 都支持随机访问,但list是不支持的,++ – 运算符在list中都被重载了,访问上一个或者下一个节点。
基本使用:

list<int> l = {1,2,3,4,5,6};	//使用{}的形式初始化。
auto p = find(l.begin(), l.end(), 3);
l.erase(p);	//删除p指向的元素,但是erase的返回值指向下一个可用的迭代器。
p = find(l.begin(), l.end(), 4);
l.insert(p, 99);	//向p之后插入一个元素。
for(auto i=l.begin(); i!=l.end(); i++){	//++运算符,在这里并不是地址加1的意思,而是向后挪了一个节点,--就是向前移了。
	cout << *l << endl;
}
16: value_type

stl中每一种容器都有自己的value_type,使用迭代器取值的时候,值的类型就是value_type,例如

map<int,string> m;
m::value_type(); //这里的变量类型就是 pair<int,string>
17: vector、map 越界访问下标

使用迭代器和 [] 访问下标的时候,不会做边界检查。也就是说,即使越界了,也不会报错。如下面的代码

vector<int> v = {1,2,3};
v[4]; 
17: 三个内存分配函数 malloc、relloc、calloc
//分配内存,但不初始化
void* p = malloc(10);

//分配 n块大小为size的内存,并初始化为0
void* sp = calloc(10, sizeof(int));

//改变内存大小(扩大或缩小),如果原内存后面没有足后可以使用的内存,就会寻找一块新的内存,因此新内存的起始位置可能和原内存不同
//所以使用realloc扩容就要使用它的返回值
p = realloc(p, 20);
18:c++ cast汇总

c++中一共有四种cast,用于类型转换,分别是 static_cast、const_cast、dynamic_cast、reinpreter_cast
static_cast: 就是普通的强制类型转换,比如

void* p;

//下面两种方式是一个意思
char* c = (char*)p;
char* c2 = static_cast<char*>p

是一个意思,但不能将const转非const,也不能将基类指针或引用转换为派生类指针或引用,volitale,__unaligned 也不能用。
如果想将基类指针转换为派生类指针,就要使用 dynamic_cast,前面有说。
如果想将const类型的指针或引用转换为非const类型的指针或引用,就要用 const_cast

const char* c = "zifuchuan";
const int& i = 0;
char* c2 = const_cast<char *>(c);
int& i2 = const_cast<int&>(i);

reinpreter_cast 只能转换指针、引用(转的是引用的那个变量)、算数类型,转换的前提是所占内存相同

int val = 10;
int* a = &val;
long b = reinterpret_cast<long>(a);

这里64位操作系统,指针占8个字节,因此只能转为long,而不是int。

19: allocator、deallocator
  1. 第一级配置器直接使用malloc()、free()和relloc(),第二级配置器视情况采用不同的策略:当配置区块超过128bytes时,视之为足够大,便调用第一级配置器;当配置器区块小于128bytes时,为了降低额外负担,使用复杂的内存池整理方式,而不再用一级配置器;
  2. 第二级配置器主动将任何小额区块的内存需求量上调至8的倍数,并维护16个free-list,各自管理大小为8~128bytes的小额区块;
  3. 空间配置函数allocate(),首先判断区块大小,大于128就直接调用第一级配置器,小于128时就检查对应的free-list。如果free-list之内有可用区块,就直接拿来用,如果没有可用区块,就将区块大小调整至8的倍数,然后调用refill(),为free-list重新分配空间;
  4. 空间释放函数deallocate(),该函数首先判断区块大小,大于128bytes时,直接调用一级配置器,小于128bytes就找到对应的free-list然后释放内存。
20: 函数参数管理 __stdcall、__cdcel、__fastcall
  1. __stdcall:
    函数参数从右向左入栈,由被调用者清除栈内参数,windows api默认方式。
  2. __cdcel:
    函数参数从右向左入栈,由调用者清除栈内参数,c/c++默认方式。
  3. __fastcall:
    从左开始,小于4字节放入cpu寄存器中,其余参数由右向左入栈。
    另外,三种参数,编译后,函数修饰的名字不同。
21: 智能指针和实现

c++中的智能指针一共有3个 shared_ptrunique_ptrweak_ptr(伴随类),其中shared_ptr和weak_ptr的使用,和oc中strong、weak类型指针很像,一个会让引用计数器+1,一个不会,但oc中的引用计数是编译器特性,shared_ptr则是用自己的构造函数和析构函数完成的。
shared_ptr:允许多个智能指针指向一个对象,每多一个指针,引用计数器+1,引用计数器为0时,释放对象。

Class Obj{};
shared_ptr<Obj> s = make_shared<Obj>(); //创建一个Obj对象,并使用智能指针进行管理。
shared_ptr<Obj> s2(s);	//拷贝构造,s和s2指向同一个对象,因此引用计数为2。
Obj* p = new Obj;
shared_ptr<Obj> s(p);	//也可以使用普通指针初始化智能指针

但是如下方式是错的, 因为智能指针
shared_ptr<Obj>s = p;

当然,一般方便使用,指针类型直接使用auto就可以了。
自己实现智能指针:

template<class T>
class Shared_ptr{
private:
    unsigned int* _count;
    T* _p;
public:
    Shared_ptr():_count(new unsigned int(0)),_p(nullptr){};
    Shared_ptr(T* p):_count(new unsigned int(1)),_p(p){};
    Shared_ptr(Shared_ptr& other):_count(other._count),_p(other._p){};
    ~Shared_ptr(){
        if (_p && --*_count==0) {
            delete _count;
            delete _p;
        }
    };
    T& operator*(){
        if (_count == 0) {
            return nullptr;
        }
        return *_p;
    };
    T* operator->(){
        if (_count == 0) {
            return nullptr;
        }
        return _p;
    };
    //=运算符有返回值,主要是为了连续赋值
    //other指向的内存多个一个指针,引用数+1
    //this类指向发生了改变,因此原先计数器-1
    Shared_ptr& operator=(Shared_ptr& other){
        other._count++;
        if (--_count == 0) {
            delete _p;
            delete _count;
        }
        this->_count = other._count;
        this->_p = other._p;
        return *this;
    };
};
22: lambda函数的实现

以 [] 开头的就是 lambda函数,也就是匿名函数,不需要写返回值。

cout << [](int a,int b){ return a<b; } << endl;
23: hello world程序执行过程
  1. 通过可执行文件首部信息计算出代码和数据在可执行文件中的位置,并找到相应的磁盘块。
  2. 生成一个进程,将代码和数据映射到进程中(并不是读入)。
  3. 设置cpu上下文环境,并跳到程序开始处。
  4. 执行第一条指令,发生缺页码异常。
  5. 分配一页物理内存,将代码读入内存中。
  6. 执行puts函数,在显示器上写一串字符串。
  7. 内核调用显示进程,进程调用输出设备输出字符串。
24: explicit 禁止隐式类型转换

先看一下隐式类型转换

class Obj{
public:
    Obj(int val){};
};

int main() {
    Obj o = 10;	//会直接调用一个参数的构造函数
}

使用explicit 禁止掉隐式类型转换

class Obj{
public;
	explicit Obj(int val){};
}
25: 如果在类的成员函数中调用delete this?
  1. c++的对象模型只包含数据成员和指向虚函数表的指针 vptr,成员函数在代码段中,由所有对象公用。
  2. 为了知道是那个对象调用了成员函数,在调用成员函数的时候,会传递一个this指针,指向调用对象。
  3. delete this后,成员函数只要不涉及对象内容,仍然可以正常运行,如果设计对象内容,由于对象已经被释放,就会出现不可预期的问题,因为要调用的内存已经被回收了,this变成了野指针。
26: 动态联编和静态联编
  • 静态联编:在编译阶段确定调用和代码的映射关系,优点是效率比较高。
  • 动态联编:在程序运行时,确定调用和代码的映射关系,优点是比较灵活,缺点是效率较低。
    c++中,动态联编是通过虚函数实现的。
27: 静态编译和静态编译

主要针对程序中使用的动态连接库
静态编译: 将动态链接库中需要的部分,提取出来,连接到可执行文件中,优点是可执行文件执行时不需要依赖动态链接库,缺点是编译速度慢,可执行文件体积大。

动态编译: 可执行文件附带一个动态连接库,需要执行时,直接调用动态连接库中的命令。优点是编译速度快,缺点是缺少动态连接库,程序就无法运行,因此需要附带庞大的动态连接库。

28: 动态连接库和静态连接库

但如果是动态编译,动态连接库只会在需要的时候,调用,而静态连接库会被包含到可执行文件中。
如果是静态编译,动态链接库也只会包含需要的部分。
动态连接库可以包含静态连接库,但反过来就不行。

29: 经典算法,不使用额外空间交换两个数?

这道题考的比较少了,但还是写一下吧。

a = a + b;
b = a - b;
a = a - b;
30: main(int argc, char* argv[])

main里的参数主要用于接收程序启动时的命令参数,其中argc是参数的个数,argv是一个char**的二纬数组,里面就是参数,第一个参数是应用程序的名称。
之前面试时问过一个经典问题,iOS在外部使用scheme跳转到应用程序内的时候,如果这时候程序没有启动,如何知道要跳转到那个页面呢?
跳转页面就是以启动参数的形式传递进来的,在argv里。

31: struct、union

c和c++中struct的区别主要就是多了封装和方法了,c++中,struct除了默认访问权限是public,和class没有什么区别。
c中的union比较简单,主要就是一种类型复合,内存占用是其中最长的成员变量的数值。

union u{
	int a;
	double c;
	char c[];
}

但c++中的union就比较神奇了,可以添加方法和权限,默认访问权限是public,但不能派生和继承。

union u{
	int a;
	double c;
	void say(){
		cout << c << endl;
	};
}

union有一个用途就是用来测试大端序和小端序。

union test{
    char b;
    short a;
};
int main() {
    test t;
    t.a = 0x1234;
    if (t.b == 0x12) {	//高位在内存第位
        cout << "big" << endl;
    }else if(t.b == 0x34){	//低位在内存低位
        cout << "small" << endl;
    };
}
32: volatile关键字的作用

使用这个关键字修饰的变量,编译器对访问该变量的代码就不再进行优化,可以提供对特殊地址的稳定访问。
一般来说,下面几种情况需要加上violate

  1. 多任务下,各任务之间的共享标志应该加volatile
  2. 中断服务中修改供其他程序检测的变量
  3. 存储映射的寄存器
33: new、operator new、placement new、operator delete、delete

new:创建并初始化对象
operator new:分配内存,但不初始化对象
placement new:初始化对象
因此 new = operator new + placement new
delete:删除对象,并置空指针
operator delete:类似free

34: 什么时候会有合成构造函数、合成析构函数?

四种情况
1: 父类有构造函数
2: 成员对象有构造函数
3: 有虚函数
4: 有虚基类

35:何时必须使用初始化成员列表?

1: 成员变量为引用类型的时候
2: 成员变量为const类型的时候

36: assert 和 NDEBUG

assert定义在assert.h中,如果它的条件返回错误,则终止程序执行。

#include "assert.h"
void assert(int expression);

NDEBUG 如果定义了,assert就不会被执行。

37:strcpy和strnpy

先说结论:strnpy更安全,排除了strcpy可能会遇到的内存重叠和目标串空间不足的问题。

strcpy(char *des, const char *src);
strnpy(char *des, const char *src, int pos);	//复制pos长度的字符,如果字符重叠或者空间不够就会报错。
38:c++中新参命名问题

c++中形参在是可以不命名的,这一点一般用在声明的时候,比如:

int f(int, float)

当然,实现的时候,也可以不给形参命名,但就没有什么意义了,因为无法使用传入的参数。

int f(int, float){}

如果将这种写法用于函数指针上,就可以写出如下的代码,接收参数是一个函数指针,这个指针指向一个传入值为空,但返回值为int的变量。

int f(int());

这种写法用于某些对象时,就会出现二义性问题,比如下面的原函数调用,第一眼看上去是调用了函数f,接收参数是一个Obj对象(调用了默认构造函数生成的临时变量)。
但实际上,编译器会理解为,我们传入了一个函数指针,这个指针的返回值是Obj对象,传入值为空。

class Obj{
}
int f(Obj());

在c++多线程编程时,调用函数对象也很容易出现类似的问题

class Task{
	void operator()() const{
		cout << "do something" << endl;
	}
}
//这里想要表达的意思,是使用默认构造函数生成了一个函数对象,然后调用拷贝构造传进去,但是编译器会理解为传入了一个函数指针,指向一个返回值为Task但传入值为空的函数。
thread t(Task());

如果要消除这种二义性,可以在外面加一组小括号

thread t((Task()));

更好的是使用新的统一的初始化语法:

thread t{Task()};
39: bind和ref
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值