C++好难(1):C++的入门

目录

前言:

C++的历史:

c++的领域

1.C++的关键字:

2.命名空间

2.1命名空间的定义:

1)命名空间的普通定义:

2)命名空间的嵌套定义:

3)命名空间相同的处理:

2.2命名空间的使用说明:

1)符号  : :  的使用

2)使用  using namespace   的全部内容引入

3)使用  using    的部分内容引入

3.C++的输入输出

4.缺省参数

4.1缺省参数的概念

4.2缺省参数的分类:

1)全缺省参数

2)半缺省参数

4.3缺省参数注意事项

1)缺省必须从左往右依次来缺省,不能间隔着缺省

2)缺省参数不能在函数声明和函数实现中同时出现

3)缺省值必须是常量或者变量

5.函数重载

5.1函数重载概念

1)参数类型不同

2)参数个数不同

3)参数顺序不同

5.2函数重载实现原理(了解)

6.引用:

6.1引用的概念

 6.2引用的特性

 6.3常引用

*临时变量

 总结

6.4使用场景

6.5引用和指针的区别

6.6 引用和指针的区别

7.内敛函数

7.1概念

 7.2内联函数的特性

8.auto关键字

8.1 auto的引入

8.2 auto简介

8.3 auto的使用 

8.4 auto不能自动推导的场景

9.基于范围的for循环

9.1范围for语法:

9.2范围for的使用条件

10.指针的空值nullptr


前言:

一些c++发展的介绍和c++面向岗位的介绍

C++的历史:

c++的领域

我们要学习c++首先要知道c++学好之后能干什么?

1.操作系统以及大型系统软件开发

所有操作系统几乎都是C/C++写的,许多大型软件背后几乎都是C++写的,比如:
Photoshop、Office、JVM(Java虚拟机)等,究其原因还是性能高,可以直接操控硬件。

2.服务器端开发

后台开发:主要侧重于业务逻辑的处理,即对于前端请求后端给出对应的响应,现在主流采
用java,但内卷化比较严重,大厂可能会有C++后台开发,主要做一些基础组件,中间件、
缓存、分布式存储等。服务器端开发比后台开发跟广泛,包含后台开发,一般对实时性要求
比较高的,比如游戏服务器、流媒体服务器、网络通讯等都采用C++开发的

3.游戏开发

PC平台几乎所有的游戏都是C++写的,比如:魔兽世界、传奇、CSgo、等,市面上相当多的游戏引擎都是基于C++开发的,比如:Cocos2d、虚幻4、DirectX等。三维游戏领域计算量非常庞大,底层的数学全都是矩阵变换,想要画面精美、内容丰富、游戏实时性搞,这些高难度需求无疑只能选C++语言。

比较知名厂商:腾讯、网易、米哈游等。

4. 嵌入式和物联网领域

嵌入式:就是把具有计算能力的主控板嵌入到机器装置或者电子装置的内部,能够控制这些
装置。比如:智能手环、摄像头、扫地机器人、智能音响等。


谈到嵌入式开发,大家最能想到的就是单片机开发(即在8位、16位或者32位单片机产品或者
裸机上进行的开发),嵌入式开发除了单片机开发以外,还包含在soc片上、系统层面、驱动
层面以及应用、中间件层面的开发。


常见的岗位有:嵌入式开发工程师、驱动开发工程师、系统开发工程师、Linux开发工程
师、固件开发工程师等。

5. 数字图像处理

数字图像处理中涉及到大量数学矩阵方面的运算,对CPU算力要求比较高,主要的图像处理
算法库和开源库等都是C/C++写的,

比如:OpenCV、OpenGL等,大名鼎鼎的Photoshop就是C++写的。

6. 人工智能

一提到人工智能,大家首先想到的就是python,认为学习人工智能就要学习python,这个
是误区,python中库比较丰富,使用python可以快速搭建神经网络、填入参数导入数据就
可以开始训练模型了。但人工智能背后深度学习算法等核心还是用C++写的。

7. 分布式应用

后端架构要不断提高性能和并发能力才能应对大信息时代的来临。在分布式领域,好些分布式框架、文件系统、中间组件等都是C++开发的。对分布式计算影响极大的Hadoop生态的几个重量级组件:HDFS、zookeeper、HBase等,也都是基于Google用C++实现的GFS、Chubby、BigTable。

包括分布式计算框架MapReduce也是Google先用C++实现了一套,之后才有开源的java版本。

1.C++的关键字:

 c++总共有63个关键字,其中包含c语言的32个(圈出来的)

这些关键字不需要特意去记,在我们日后写代码的过程中会慢慢用到并记住。

2.命名空间

命名空间是什么?

我们在学校学习c++的时候,一般都会在开头写上:using namespace std 这个就是一个命名空间

只不过老师很少讲这个是用来干什么的,这里我们就来详细的说一下:

在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。

使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染namespace关键字的出现就是针对这种问题的

命名空间就是一个新的作用域,命名空间中所有的内容都会局限在该空间内

2.1命名空间的定义:

定义命名空间,需要使用到  namespace  关键字,后面根命名空间的名字,然后接一对 {  } 即可
{  }  中包含的就是命名空间的成员

1)命名空间的普通定义:

//普通的命名空间
namespace A  //A为空间的名称
{
	//命名空间中的内容,既可以是变量,也可以是函数
	int a;
	int Sum(int x, int y)
	{
		return x + y;
	}
}

2)命名空间的嵌套定义:

命名空间可以嵌套使用,(无限套娃)

// 命名空间可以嵌套
namespace A // 定义一个名为A的命名空间
{
	int a;
	int b;
	namespace B // 嵌套定义另一个名为B的命名空间
	{
		int c;
		int d;
	}
}

3)命名空间相同的处理:

同一个工程里面,如果存在多个相同的命名空间,编译器会在最后将其整合为一个命名空间

// 定义一个A
namespace A
{
	int a;
	int Add(int x, int y) {
		return x + y;
	}
}
// 再定义一个A
namespace A
{
	int Mul(int left, int right)
	{
		return left * right;
	}
}

2.2命名空间的使用说明:

命名空间的使用分为三种:

  1. 符号  : :   在C++中叫做作用域限定符
  2. 用  using namespace  关键字将命名空间里面的全部内容引入
  3. 用  using  关键字将命名空间里面的部分内容引入

1)符号  : :  的使用

 : :  叫做:作用域限定符,我们通过  命名空间::命名空间成员   来访问命名空间里面的内容

// 加命名空间名称及作用域限定符
namespace A
{
	int a = 10;
	float b;
}

int main()
{
	A::b = 3.6;//引入命名空间b,并赋值

	printf("%d\n", A::a); // 打印a
	printf("%.2f\n", A::b); // 打印b

	return 0;
}

结果:

2)使用  using namespace   的全部内容引入

// 使用 using namespace 命名空间名称引入
namespace A
{
	int a;
	float b;
}

using namespace A; // 将命名空间A的所有成员引入

int main()
{
	a = 10; // 将命名空间中的成员a赋值为10

	printf("%d\n", a); // 打印命名空间中的成员a
	return 0;
}

3)使用  using    的部分内容引入

// 使用using将命名空间中的成员引入
namespace A
{
	int a;
	float b;
	char c;
}

using A::a; // 将命名空间中的成员a引入
using A::b; // 再将b引入

int main()
{
	a = 10; // 将命名空间中的成员a赋值为10
	b = 3.69; // 将b赋值为3.69

	printf("%d\n", a); // 打印成员a
	printf("%.2f\n", b); // 打印成员b

	return 0;
}

结果:

3.C++的输入输出

C语言的输入输出用到的符号是 printf scanf  那C++用到的是什么呢?

我们先打一个hello world

#include<iostream>
using namespace std;

int main()
{
	cout << "hello world" << endl;
	
	return 0;
}

结果: 

解释:

  • c语言的标准输入输出是 printf 和  scanf 
  • c++的是:cout(流插入) 和 cin(流提取),
  • <<流插入的指令,>>流提取的指令
  • 在使用  cout   和   cin    时,需要包含头文件   <iostream>   以及   std 标准命名空间
  • endl  相当时换行的意思,相当于 ' \n '

我们在写C语言的输入输出时,需要加数据类型来进行控制,如整型:%d,字符:%c

而在C++的输入输出中,我们不用增加数据类型的控制,cout 和 cin  会自动识别

4.缺省参数

4.1缺省参数的概念

缺省参数是声明或定义函数时为函数的参数指定一个缺省值

在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。

// a = 0 ,0就是a的默认值
void Func(int a = 0)
{
	cout << a << endl;
}
int main()
{
	Func();     // 没有传参时,使用参数的默认值(打印0)
	Func(10);   // 传参时,使用指定的实参(打印10)
	return 0;
}

 第一个Func输出默认值0,第二个Func输出结果时指定值10

4.2缺省参数的分类:

缺省参数分为:

  1. 全缺省
  2. 半缺省

1)全缺省参数

全缺省参数的所有参数都有默认值,如果没有手动传参,那么编译器会使用默认参数列表中的参数

注意:如果只传了部分参数,那么这个值会被从左到右匹配 

2)半缺省参数

void Func1(int a, int b = 2, int c = 3)
{
    cout << "a = "<< a << "   b = " << b << "   c = " << c << endl;
}

void Func2(int a, int b = 2, int c = 3)
{
    cout << "a = " << a << "   b = " << b << "   c = " << c << endl;
}

其中Func1至少需要传一个参数,Func2至少需要传两个参数

4.3缺省参数注意事项

1)缺省必须从左往右依次来缺省,不能间隔着缺省

// 错误示例
void Func(int a, int b = 20, int c)
{
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
}

// 正确示例
void Func(int a, int b = 20, int c = 30)
{
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
}

2)缺省参数不能在函数声明和函数实现中同时出现

错误示例:
// Test.h(函数声明)
void Test(int a, int b, int c = 30);

// Test.c(函数定义)
void TestFunc(int a, int b, int c = 30) {
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
}

-------------------------------------------

正确示例:
// Test.h(函数声明)
void Test(int a, int b, int c = 30);

// Test.c(函数定义)
void TestFunc(int a, int b, int c) {
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
}

如果 声明 定义 同时出现缺省的默认参数,编译器将无法确定到底该用哪个缺省数

一般把缺省的默认参数写在函数声明中

3)缺省值必须是常量或者变量

// 正确示例

int x = 30; //全局变量
void Func(int a, int b = 20, int c = x)
{
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
}

5.函数重载

5.1函数重载概念

函数重载是指:在同一作用域内,可以有一组具有相同函数名,不同参数列表的函数,这组函数被称为重载函数。

重载函数通常用来命名一组功能相似的函数,这样做减少了函数名的数量,避免了名字空间的污染,对于程序的可读性有很大的好处

函数重载的三种方法:

  1. 参数类型不同
  2. 参数个数不同
  3. 参数顺序不同

1)参数类型不同

int Add(int left, int right)
{
	cout << "int Add(int left, int right)" << endl;
	return left + right;
}

double Add(double left, double right)
{
	cout << "double Add(double left, double right)" << endl;
	return left + right;
}

2)参数个数不同

void Func(int a)
{
	cout << "Func(int a)" << endl;
}

void Func(int a, int b)
{
	cout << "Func(int a, int b)" << endl;
}

3)参数顺序不同

void Func(int a, char b)
{
 cout << "Func(int a,char b)" << endl;
}

void Func(char b, int a)
{
 cout << "Func(char b, int a)" << endl;
}

5.2函数重载实现原理(了解)

为什么C++可以做到重载,而C语言不行呢?

一个程序运行起来需要经历以下几个阶段:预处理、编译、汇编、链接

c语言中,在汇编阶段进行符号汇总时,一个函数汇总后的符号就是其函数名,所以当汇总时发现多个相同函数的函数符号时,编译器就会报错

c++中,进行汇编阶段进行符号汇总时,对函数的名字修饰做了改动,函数汇总的符号不单单只是函数的函数名,而是通过其函数的类型,个数,顺序,等信息汇总层一个符号

这样,就算是函数名相同的函数,只要其参数的类型或参数的个数或参数的顺序不同,那么汇总出来的符号也就不同了。

6.引用:

6.1引用的概念

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空 间,它和它引用的变量共用同一块内存空间

比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"。

引用的使用:类型& 引用变量名(对像名)= 引用实体

注意:引用类型必须和引用实体是同种类型

 6.2引用的特性

  1. 引用在定义时必须初始化
  2. 一个变量可以有多个引用
  3. 引用一旦引用一个实体,在不能引用其他的实体

1)引用在定义时必须初始化

错误用法:

int a = 10;
int& b;
b = a;

正确用法:

int a = 10;
int& b = a; // 定义时必须初始化

2)一个变量可以有多个引用

int a = 10;
int& b = a;
int& c = a;
int& d = a;

b、c、d都是变量a的引用

3)引用一旦引用一个实体,在不能引用其他的实体

int a = 10;
int& b = a;

int c = 20;
b = c;

此时,b已经是a的引用了,b不能再引用其他实体,

这里b=c的意思是将c的值赋给b,也就是将b和a的内容改为了20

 6.3常引用

引用类型必须和引用实体是同种类型的。

但如果一个普通引用去引用被const所修饰的类型,那么是不被允许的

这是因为我们对引用别名是有原则的:对原变量的引用,权限只能缩小,不能放大

1)权限不变的情况:

 2)权限缩小的情况:

a变量可读可写的,它的权限是最大的,我们可以控制b的权限使得b的权限降低

 可以看到缩小b的权限后,b不能被改变了

3)权限放大

上面代码中:a 变量用 const 修饰,说明变量不能被修改,是只读的,那么我们定义别名的时候,也必须拿 const 修饰

*临时变量

还有一种情况也必须用 const,下面代码中,d 是 double 类型而 a 和 b 都是 int 类型,但是如果想要为 d 取一个别名必须加上引用,因为把 double 类型赋值给 int 类型会涉及到隐式类型转换,那么 d 在此时就是一个临时变量临时变量具有常性是只读的,所以要加 const

这里的d是一个临时变量,具有常性,必须要用const进行修饰

 总结

  • const引用是为了保护实参,避免被修改,
  • 函数传参的时候,不想原变量被改变,这个参数可以使用const引用传参

6.4使用场景

1)做参数:

先看一段代码:

//C语言的传址调用
void Swap1(int* p1, int* p2) {
	int temp = *p1;
	*p1 = *p2;
	*p2 = temp;
}

//C++的引用调用
void Swap2(int& rx, int& ry) {
	int temp = rx;
	rx = ry;
	ry = temp;
}

int main()
{
	int x = 3, y = 6;

	Swap1(&x, &y); // C传参
	Swap2(x, y); // C++传参

	return 0;
}

这里引用就相当于再swap2的函数定义是,给参数用一个引用(起一个别名),引用的改变,原来的数也会改变

能明显发现,使用引用的方法,来替换指针的使用,能更方便一点

2)做返回值

下面这段代码中,我们再func里面定义了一个变量,出了作用域,n会销毁,编译器这时会用一个临时变量来记录n的值,编译器创建的临时变量时被const所修饰的,最会将n的值赋值给ret

int func() 
{
	int n = 0;
	n++;
	return n;
}
int main()
{
	int ret = func();
	cout << ret << endl;

	ret = Cout();
	cout << ret << endl;
	return 0;
}

但如果我们返回的数据必须是被 static 修饰,或者是 动态开辟 的,再或者是 全局变量 等一些不会随着函数调用的结束而被销毁的数据,就可以使用引用做返回值

int& func() 
{
	static int n = 0;
	n++;
	return n;
}
int main()
{
	int ret = func();
	cout << ret << endl;

	ret = Cout();
	cout << ret << endl;
	return 0;
}

那么用引用返回有什么好处呢?

用引用返回,就会避免编译器产生临时变量,如果你返回的是一个结构一个类,那么这个临时变量会变得很大,会降低代码的效率

总结:

  • 如果函数返回时,出了函数作用域,返回对象未销毁,则可以使用引用返回;
  • 如果已经还给系统了,则必须使用传值返回。

6.5引用和指针的区别

在语法上,引用就是一个别名,没有独立空间,和其引用实体公用同一块空间

而指针变量时开辟一块空间,存储变量的地址

 可以看到,a和它的引用b的地址是一样的

但其实,在底层的实现上实际是有开辟新空间的,因为引用的底层逻辑时按照指针方式来实现的

来看看  汇编代码,就可以看的很明白 

6.6 引用和指针的区别

1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
2. 引用在定义时必须初始化,指针没有要求
3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
4. 没有NULL引用,但有NULL指针
5. 在sizeof中含义不同引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小

7. 有多级指针但是没有多级引用
8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
9. 引用比指针使用起来相对更安全

7.内敛函数

在程序中,大量的重复的建立函数栈帧会造成很大的性能开销

在 C 语言可以用宏函数来代替函数,使之不会开辟栈帧,
虽然宏的优点多,但也有不少的缺点(比如使用起来麻烦,需要注意的细节多),
这时 内联函数 就可以针对这种场景解决问题 (内联函数对标宏函数)。

7.1概念

以 inline修饰 的函数叫做 内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。

没有使用内敛函数之前,汇编的时候,需要进行函数调用,传递的是函数的地址

用了内联函数之后,可以看到汇编的时候是直接展开的

 7.2内联函数的特性

  • inline 是一种 以空间换时间 的做法,在编译阶段,会用函数体替换函数调用,省去调用函数额开销。所以 代码很长 或者 有循环 或者 有递归 的函数不适宜使用作为内联函数。
  • inline 对于编译器而言只是一个建议,编译器会自动优化,如果定义为 inline 的函数体内 有循环 或者 有递归 等等,编译器优化时会忽略掉内联。
  • inline 不建议声明和定义分离,分离会导致链接错误。因为 inline 被展开,就没有函数地址了,链接就会找不到。

8.auto关键字

8.1 auto的引入

auto是自动识别类型的关键字,经常使用在:

  • 类型难拼写
  • 含义不明确导致容易出错
#include <string>
#include <map>

//auto关键字
int main()
{
	std::map<std::string, std::string> m{ { "apple", "苹果" }, { "orange", "橙子" },{"pear","梨"} };
	std::map<std::string, std::string>::iterator it = m.begin();
	while (it != m.end())
	{
		//....
	}
	return 0;
}

对于这段代码,大家不用在意它是什么意思,

其中   std::map<std::string, std::string>::iterator   这个是一个类型,但是他太长了,容易写错,而且麻烦。

这时,我们大多数人会想到用   typedef   给他区别名,比如:

#include <string>
#include <map>

typedef std::map<std::string, std::string> Map;
int main()
{
	Map m{ { "apple", "苹果" },{ "orange", "橙子" }, {"pear","梨"} };
	Map::iterator it = m.begin();
	while (it != m.end())
	{
		//....
	}
	return 0;
}

使用typedef给类型取别名确实可以简化代码,但是typedef有会遇到新的问题:

typedef char* pstring;
int main()
{
	const pstring p1;    // 编译成功还是失败?
	const pstring* p2;   // 编译成功还是失败?
	return 0;
}

8.2 auto简介

在早期 C/C++ 中 auto 的含义是:使用 auto 修饰的变量,是具有自动存储器的局部变量

C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。

 注意:

  • 使用 auto 定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导 auto 的实际类型。
  • 因此 auto 并非是一种 “类型” 的声明,而是一个类型声明时的 “占位符”,编译器在编译期会将 auto 替换为变量实际的类型。

8.3 auto的使用 

1)auto与指针的引用结合起来使用

用 auto 声明指针类型时,用  auto  和  auto*  没有任何区别,
但用auto声明引用时,必须加&,否则城建的知识与实体类型相同的不同变量 

2)在同一行定义多个变量

int main()
{
	auto a = 1, b = 2;	// 正常通过

	auto c = 3, d = 3.14; // 编译器报错:“auto”必须始终推导为同一类型

	return 0;
}

8.4 auto不能自动推导的场景

  1. auto 不能作为函数的参数
    // 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
    void TestAuto(auto a) 
    {
    	//....
    }
    
  2. auto 不能直接用来声明数组
    void TestAuto() 
    {
     	int a[] = {1, 2, 3};
     	auto b[] = {4,5,6}; // 此处编译失败,错误写法
    }

  3.  为了避免与 C++98 中的 auto 发生混淆,C++11 只保留了 auto 作为类型指示符的用法
  4.  auto 在实际中最常见的优势用法就是跟 C++11 提供的新式 for 循环,还有 lambda 表达式等进行配合使用。

9.基于范围的for循环

9.1范围for语法:

我们遍历数组的方法如下:

int main()
{
	int arr[] = { 1,2,3,4,5 };

	// 打印数组中的所有元素
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); ++i) 
	{
		cout << arr[i] << " ";
	}

	cout << endl;

	return 0;
}

基于范围的for循环引入了auto,会自动判断结束标志

 for 循环后的括号由冒号 : 分为两部分:
第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围

和普通循环类似,可以用  continue  和  break  来进行控制

9.2范围for的使用条件

1)for循环迭代的范围必须是确定的

  • 对于数组而言,就是数组中第一个元素和最后一个元素的范围;
  • 对于类而言,应该提供 begin和end的方法,begin和end就是for循环迭代的范围。

以下代码就是有问题的代码,因为它for的范围不能确定,函数调用拿到的时array的首元素地址

void TestFor(int array[])
{
    for(auto& e : array)
        cout<< e <<endl;
}

2)迭代的对象要实现++和==的操作

(需要其他知识的补充才能理解到位,放到以后会讲)

10.指针的空值nullptr

在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现 不可预料的错误,比如未初始化的指针。

如果一个指针没有合法的指向,我们基本都是按照如下 方式对其进行初始化:

void TestPtr() 
{
 	int* p1 = NULL;
 	int* p2 = 0;
}

NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:

#ifndef NULL
#ifdef __cplusplus
#define NULL   0
#else
#define NULL   ((void *)0)
#endif
#endif

可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:

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

程序本意是想通过f(NULL)调用指针版本的f(int*)函数,

但是由于NULL被定义成0,因此与程序的初衷相悖。

注意:

  1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
  2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
  3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。


c++第一篇就到这里学完了,可以看到c++相比c有很多补充,也有更多的细节需要记,

前路漫漫还得继续努力了!

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值