【C++初阶】小白入门C++

前言:

从本篇文章开始就正式进入C++的学习了,C++在C语言的基础上增加了面向对象的思想,还有许多库,在使用上相比C语言大大提高了效率,同时在语法上也填上C语言的坑。本篇文章是介绍C++入门的基础语法知识,接下来我们一起往下看吧~
在这里插入图片描述

1、C++关键字

C++总共有63个关键字,C语言有32个。毕竟是从C语言 “进化” 出来的,补充的东西肯定比较多。有些关键字我们是已经知道的,还有一小部分本文后面会有介绍,其他的以后会重点讲解的,这里只是看看C++有哪些关键字。
在这里插入图片描述

2、命名空间

2.1命名空间是什么

命名空间是C++标准库中一个概念,顾名思义,简单来说就是定义某个变量或者函数的名称放在指定的空间里,这个空间也有它的名字,编程者要可以通过命名空间找到要使用的变量或者函数。例如 std 就是C++标准库定义的命名空间名,要使用cout、cin等这些函数不仅要有对应的头文件,还要对命名空间进行展开(这个后面也会具体讲)才能使用,因为C++将标准库的定义实现都放在这个命名空间里。

2.2为什么要有命名空间

我们之前学习C语言时会发现一个问题,或者说这是C语言的语法规则。C语言语法规则:在全局作用域中,两个变量不能有相同的名称,函数也一样。

可是如果在某些场景下必须用到相同的名称怎么办,不得已修改变量名或者是函数名容易造成混乱。

所以,C++补充了命名空间这个新语法,可以支持在全局作用域中出现两个甚至多个名称相同的变量或者函数。只要在命名空间中定义某个变量,另一个变量是全局变量(或者是在另一个命名空间),我们只要在去相应的命名空间里找那个变量就可以使用,两个名称相同也不会有命名冲突。

举个栗子:小明的爷爷在山上种了一片果林,小明去山上摘果子,但是好巧不巧,山上还有另一片果林,于是小明懵了,不知道哪个果林是爷爷种的,万一摘错了岂不是自找麻烦,小明只好下山。后来,爷爷在自己的果林建了围墙,并且门口标注着爷爷的名字,小明再次上山就找到了爷爷的果林,可以进入摘果子了。

看下面这段代码:

#include <stdio.h>
#include <stdlib.h>

int rand = 10;
int main()
{
    
    
	printf("%d\n", rand);
	return 0;
}

我们定义了一个全局变量rand,可是有#include <stdlib.h>这个头文件,在这个头文件里rand是一个函数名,也就是说我们定义的这个全局变量的名称与函数的名称重了,所以编译器报错。
在这里插入图片描述

2.3命名空间怎么使用

2.3.1命名空间的写法

定义一个命名空间,需要命名空间关键字——namespace,然后在命名空间后面就是这个命名空间的名字,名字是自定义的,一般习惯写成自己姓名的首字母。命名空间里可以定义变量、函数、类型。

// 关键字+命名空间名
namespace yss
{
    
    
	int Add(int a, int b)//定义函数
	{
    
    
		int c = a + b;
		return c;
	}
}

定义完这个函数,我们在main函数里要使用这个函数,还要了解一个符号

域作用限定符—— ::

域作用限定符由两个冒号组成,可以通过域作用限定符访问命名空间里的函数。

//   命名空间名 + 域作用限定符 + 函数名
int ret = yss::Add(1, 3);

注意:只要定义的不管是变量、函数还是类型在命名空间里,都要通过这种方式访问,否则找不到该变量/函数/类型

2.3.2命名空间是可以嵌套的

namespace yss
{
    
    
	namespace yss1
	{
    
    
		int Add1(int a, int b)
		{
    
    
			int c = a + b;
			return c;
		}
	}
	int Add(int a, int b)
	{
    
    
		int c = a + b;
		return c;
	}
}

int main()
{
    
    
	int ret = yss::Add(1, 3);
	int ret1 = yss::yss1::Add1(2, 4);
	return 0;
}

要找到Add1这个函数先要进入yss这个命名空间,然后进入yss1这个命名空间。

注意:如果多个文件中有相同的命名空间,也就是这些命名空间的名字相同,那么编译器就会把它们整合成一个整体;同理,在一个文件中有多个相同的命名空间也是这样的。

2.3.3使用命名空间的三种方式

第一种:全部展开

namespace yss
{
    
    
	int Add(int a, int b)
	{
    
    
		int c = a + b;
		return c;
	}
}
using namespace yss;//
int main()
{
    
    
	int ret = Add(1, 3);
	printf("%d %d", ret);
	return 0;
}

用这种方法就可以不加域作用限定符。

第二种:局部展开

namespace yss
{
    
    
	int Add(int a, int b)
	{
    
    
		int c = a + b;
		return c;
	}
}
using yss::Add;
int main()
{
    
    
	int ret = Add(1, 3);
	printf("%d %d", ret);
	return 0;
}

第三种:带上命名空间名 + 域作用限定符(前面的就是)

3、C++输入和输出

3.1初识cout和cin

刚学习C语言,我们输出的第一个程序是Hello World! ;初学C++,我们也要知道它的输出函数,以及使用它打印出Hello World!

#include <iostream>
using namespace std;
int main()
{
    
    
	cout << "Hello World!" << endl;
	return 0;
}

在这里插入图片描述
我们先不管cout、endl以及<<是什么,再来第一个C++程序的输入并输出:

#include <iostream>
using namespace std;
int main()
{
    
    
	int a = 0;
	cin >> a;
	cout << a;
	return 0;
}

在这里插入图片描述
看完C++的输入和输出,我们发现这可比C语言的输入和输出好写多了。但是,我们要了解下C++的输入和输出。

cout是标准输出对象;cin是标准输入对象;endl是特殊的C++符号,表示换行。它们都包含在 < iostream > 这个头文件里,并且按照命名空间的使用方法使用std

<<是流插入运算符;>>是流提取运算符

3.2C++的输入输出可以自动识别变量类型

#include <iostream>
using namespace std;
int main()
{
    
    
	float f = 0;
	cin >> f;
	cout << f << endl;
	int i = 0;
	cin >> i;
	cout << i << endl;
	return 0;
}

在这里插入图片描述
那么又有一个问题,假如要保留一位小数怎么办?其实很简单,因为C++兼容C,所以C里面的东西C++也能用。

控制小数点:

printf("%.1f", f);

补充:
std是C++标准库的命名空间,使用using namespace std展开标准库就会全部暴露,如果定义的变量/函数/类型与库里面的重名就会有冲突,这种问题一般在例如项目开发这些代码较多,规模较大的情况下会发生,所以在项目开发中,一般都采用局部展开。我们在日常练习中很少使用那些库里面的函数,代码也不是很多,为了方便操作和练习,所以在日常练习中可以采用全部展开。

4、缺省参数

4.1缺省参数是什么

缺省参数是在声明或者定义函数时为函数的参数指定一个或者多个缺省值。调用这个函数,如果实参没有值传过去,就由形参的缺省值来充当参数,否则使用指定的实参。C语言不支持缺省参数。

比如,我们正常调用一个函数是这样的:

void Func(int a, int b)
{
    
    
	cout << a + b << endl;
}
int main()
{
    
    
	Func(1, 2);
	return 0;
}

形参的部分是类型+变量名

调用带有缺省参数的函数:

void Func(int a = 10, int b = 20)
{
    
    
	cout << a + b << endl;
}
int main()
{
    
    
	Func();
	return 0;
}

在这里插入图片描述
当没有传参时,使用参数的默认值,即缺省参数。

注意:缺省值必须是常量或者全局变量

4.2缺省参数的分类和使用

全缺省参数:

void Func(int a = 1, int b = 2, int c = 3)
{
    
    
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
}
int main()
{
    
    
	Func();
	return 0;
}

在这里插入图片描述
半缺省参数:

void Func(int a, int b = 2, int c = 3)
{
    
    
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
}
int main()
{
    
    
	Func(10);
	Func(10, 20);
	Func(10, 20, 30);
	return 0;
}

在这里插入图片描述

半缺省参数必须从右往左依次来给出,不能间隔着给

int a, int b = 2, int c
int a = 1, int b = 2, int c

以上这些是错误的!

缺省参数不能在函数声明和定义中同时出现
因为当两个同时出现缺省参数时,如果两边的缺省值相同那倒还没什么,如果不相同调用这个函数不知道使用哪边的缺省值,产生歧义。所以,函数声明和定义分离的情况下,在声明时给缺省参数较好。

5、函数重载

5.1函数重载的定义和使用

在C++中,允许同一作用域中出现名字相同但类型/参数个数/类型顺序不同的函数,这类函数叫做重载函数。常用来解决功能类似数据类型不同的问题。

类型不同:

void Add(int a, int b)
{
    
    
	cout << a + b << endl;
}
void Add(double a, double b)
{
    
    
	cout << a + b << endl;
}
int main()
{
    
    
	Add(1, 3);
	Add(1.1, 3.3);
	return 0;
}

在这里插入图片描述
参数个数不同:

void Add(int a, int b)
{
    
    
	cout << a <<endl << b << endl;
}
void Add(int a)
{
    
    
	cout << a << endl;
}
int main()
{
    
    
	Add(1, 3);
	Add(6);
	return 0;
}

在这里插入图片描述

类型顺序不同:

void Func(int a, char c)
{
    
    
	cout << a << endl;
	cout << c << endl;
}
void Func(char a, int c)
{
    
    
	cout << a << endl;
	cout << c << endl;
}
int main()
{
    
    
	Func(1, 'y');
	Func('u', 2);
	return 0;
}

在这里插入图片描述

5.2重载二义性区分

一:函数参数类型不同

void Add(int a, int b)
{
    
    
	cout << a + b << endl;
}
void Add(double a, double b)
{
    
    
	cout << a + b << endl;
}
int main()
{
    
    
	Add(1, 3);
	Add(1.1, 3.3);
	Add(1, 3.3);///
	return 0;
}

第三个Add函数一个参数的类型是整型,另一个是浮点型,那么这时这个Add函数不知道应该调用上面两个中的哪一个。如果是第一个,就是double转int ,如果是第二个,就是int转double,所以这里产生了歧义,编译器会报错。
在这里插入图片描述
二:无传参时形参是缺省参数
先来看一个正常运行的代码:

void Func()
{
    
    
	cout << "func" << endl;
}
void Func(int a)
{
    
    
	cout << "func" << endl;
}
int main()
{
    
    
	Func();
	Func(1);
	return 0;
}

无传参的调用上面没有形参的函数,有传参的调用上面有形参的函数。接下来让形参 a = 10

void Func()
{
    
    
	cout << "func" << endl;
}
void Func(int a = 10)
{
    
    
	cout << "func" << endl;
}
int main()
{
    
    
	Func();
	Func(1);
	return 0;
}

我们可以发现这样的错误:
在这里插入图片描述
因为当调用没有传参的函数时,既可以是对应最上面的函数,也可以是下面那个函数,前面的缺省参数学习过,当没有传参时,默认使用缺省值,所以产生了歧义。

三:函数名、参数类型都相同,返回值不同
刚学习重载函数时,我们知道重载函数的函数名相同,类型不同,或者参数个数不同。但是为什么不能返回值不同呢?
看下面代码:

int Func()
{
    
    
	return 2;
}
double Func()
{
    
    
	return 2.3;
}int main()
{
    
    
	Func();
	return 0;
}

错误如下:
在这里插入图片描述
我们调用一个函数,但是不知道是上面的哪个函数返回,所以返回值不同不能构成重载。

5.3为什么C++支持重载,而C不能

要了解C++支持重载的原因,我们得先了解程序运行起来的流程

一个项目的规模往往是比较大的,所以不可能所有代码都挤在一个文件里写,分文件写代码是必需的。那么,当程序运行起来,大致需要经历这4个步骤:

预处理——>编译——>汇编——>链接

假设 a.cpp 里调用 b.cpp中定义的函数,编译后还未链接,a.o 的目标文件没有函数的地址,因为函数是在 b.cpp 中定义的,所以函数的地址在 b.o 里。接下来的链接就起作用了,a.o调用函数却没有函数的地址,链接器就会去 b.o 的符号表中找函数的地址,然后链接起来。

而这里链接器又会怎么找呢?答案是通过函数名字去找,但是不同的编译器找法不同,也就是函数名修饰规则不同。

这里我们通过Linux分别采用gcc和g++来看下(Windows比较复杂)
gcc编译器:
在这里插入图片描述
g++编译器:
在这里插入图片描述
可以看出,Linux环境下,gcc编译器完成后函数名字没有发生改变;而g++编译器完成后修饰过的函数名是【_Z+函数长度+函数名+类型首字母】。这就可以解释为什么C语言不支持重载,因为修饰过后的函数名依然是不变的,没办法区分开来;而C++编译器通过函数名修饰规则只要函数参数个数不同或者参数类型不同就能够区分开,也就支持重载了。

6、引用

6.1什么是引用

引用不是新增加的一个变量,而是给已经存在的变量起的别名。引用与引用的对象共用一块内存空间。

例如,铁牛是李逵的别名,铁牛和李逵是同一个人。

int main()
{
    
    
	int a = 1;
	int& b = a;
	return 0;
}

a是已经存在的变量,b是a的引用或者说是别名,b不单独占一块空间,而是与a共用一块内存空间,所以改变b就是改变a。

6.2为什么有引用

我们知道,C语言中的指针操作是比较危险的,如果使用不当,会有非常严重的后果。所以C++新增了引用这个概念,它可以修改引用的对象,而且也使程序的可读性较好,安全性更高。

6.3引用的使用规范

6.3.1引用的写法

类型& 引用变量名(对象名) = 引用实体;

	int a = 3;
	int& b = a;

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

6.3.2引用必须初始化

引用必须有所引用的对象,即初始化。

int main()
{
    
    
	int a = 3;
	int& b;
	return 0;
}

没有初始化是错误的,可看编译器显示:
在这里插入图片描述

6.3.3引用不能改变指向

以前我们学习过链表,链表中指针改变指向的内容来实现链表的增删查改,但是,C++规定,引用不能改变指向。
例如:

int main()
{
    
    
	int a = 2;
	int b = 3;
	int& c = a;
	int& c = b;
	return 0;
}

在这里插入图片描述

c是a的别名,又是b的别名,如果对c进行修改,那么到底是改a还是改b呢?所以引用不能改变指向。

6.3.4一个对象可有多个别名

前面的是多个对象一个别名,这里是一个对象多个别名。就好比一个人有多个外号一样。

int main()
{
    
    
	int a = 10;
	int& b = a;
	int& c = a;
	int& d = a;
	return 0;
}

在这里插入图片描述

6.4引用的如何使用

6.4.1引用做参数

实现两数交换,看以下代码:

void Swap(int& a, int& b)
{
    
    
	int tmp = a;
	a = b;
	b = tmp;
}
int main()
{
    
    
	int a = 3;
	int b = 5;
	Swap(a, b);
	cout << a << endl << b << endl;
	return 0;
}

在这里插入图片描述
我们再来看以前写法,用指针:

void Swap(int* pa, int* pb)
{
    
    
	int tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}
int main()
{
    
    
	int a = 4;
	int b = 7;
	Swap(&a, &b);
	cout << a << endl << b << endl;
}

在这里插入图片描述
通过比对,是不是很容易看出引用做参数更简洁,而且程序可读性更好。

6.4.2引用做返回值

第一种写法:引用返回值

int& Func(int a, int b)
{
    
    
	int c = a + b;
	return c;
}
int main()
{
    
    
	int ret = Func(1, 2);
	cout << ret << endl;
	return 0;
}

在这里插入图片描述
程序有以下错误:
在这里插入图片描述
分析:
该代码是两数之和返回c,返回的是c的别名。
在这里插入图片描述
我们再来看一段代码:

int& Func(int a, int b)
{
    
    
	int c = a + b;
	return c;
}
int main()
{
    
    
	int ret = Func(1, 2);
	cout << ret << endl;

	Func(3, 4);
	cout << ret << endl;

	return 0;
}

在这里插入图片描述
变量ret它接收的是返回值c的别名,c的别名是什么,这是编译器处理的,我们不需要管。但是我们知道,函数结束后,这个作用域就会被销毁。而这个c的别名返回给ret具体等于多少,取决于编译器是否清理完。如果清理完,就是随机值,否则就是a+b的大小,这是不确定的值,在VS下是还未清理完。再次调用这个函数,因为变量ret是有单独占一块空间的,它前面接收第一次调用的返回值就是3,而第二次调用函数完这个作用域销毁了,ret也没有去接收第二次调用的返回值,所以ret的内容不改变,还是3

第二种写法:接收的变量也是引用对象,ret前加&

int& Func(int a, int b)
{
    
    
	int c = a + b;
	return c;
}
int main()
{
    
    
	int& ret = Func(1, 2);
	cout << ret << endl;

	Func(3, 4);
	cout << ret << endl;

	return 0;
}

运行结果:
在这里插入图片描述
分析:
在这里插入图片描述
此时ret也是一个别名,是返回值的别名,与前面有所不同的是,ret不单独占内存空间,直接来说ret就是c的别名。前面提过,c的别名传过去是不确定的值,取决于编译器是否清理完。所以第一次调用在VS下是3,第二次调用在VS下是7

第三种写法:加static修饰
前面两种写法都是错误的,因为不同的编译器下实现的结果是不同的。所以这里得用static,可以使修饰的变量生命周期变长。

int& Func(int a, int b)
{
    
    
	static int c = a + b;
	return c;
}
int main()
{
    
    
	int& ret = Func(1, 2);
	cout << ret << endl;

	Func(3, 4);
	cout << ret << endl;

	return 0;
}

运行结果:
在这里插入图片描述
为什么两个都是3呢?因为static修饰的静态变量只能被初始化一次,也就是说函数第一次调用时被初始化为3(a+b=3),后面第二次调用时,它的初始值就是3了,如果再次static int c = a + b 相当于第二次初始化,这是不行的。

稍微调整下:

int& Func(int a, int b)
{
    
    
	static int c;
	c = a + b;
	return c;
}
int main()
{
    
    
	int& ret = Func(1, 2);
	cout << ret << endl;

	Func(3, 4);
	cout << ret << endl;

	return 0;
}

运行结果:
在这里插入图片描述
初始化后面为 c = a+ b就可以对C 进行修改

6.4.3常引用

const 是修饰常变量,引用的对象前面加const修饰或者引用前加const会怎样呢?

int main()
{
    
    
	const int a = 1;
	int& b = a;
	return 0;
}

在这里插入图片描述
const变量a,说明变量a变成了常变量,不能改变了。而b是a的别名,如果改变b就相当于把a改了,这就把权限放大了。

权限不可以放大,但可以缩小或者平移
权限缩小:

int main()
{
    
    
	int a = 1;
	const int& b = a;
	return 0;
}

注意:此时b就不能再改变了。

权限平移:两个都加上const

int main()
{
    
    
	const int a = 1;
	const int& b = a;
	return 0;
}

6.4.4常引用和类型转换

我们知道,引用必须两者是同类型的才行,如果不同编译器会报错

int main()
{
    
    
	int a = 3;
	double& b = a;
	return 0;
}

在这里插入图片描述
这个错误提示,发生类型转换时,引用的对象必须是常量。

增加const 就没问题了

	const double& b = a;

因为只要发生类型转换,都会产生一个临时变量,这个临时变量就是a要转换的结果,a本身是不变的,然后临时变量赋给b。为什么加const就可以了呢?因为产生的临时变量具有常属性,所以要加const。

6.5引用与指针

6.5.1引用和指针的异同

语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
看以下汇编代码:
在这里插入图片描述

6.5.2不同点

  1. 引用是一个变量的别名,指针存储一个变量的地址
  2. 引用必须初始化,指针可以不用
  3. 引用不能改变指向的对象,指针可以
  4. 有多级指针,没有多级引用
  5. 没有NULL引用,有NULL指针
  6. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
  7. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
  8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
  9. 引用使用起来比指针更安全

7.内联函数

7.1什么是内联函数

用关键字inline修饰的函数叫内联函数,编译时会在调用内联函数的地方展开,不用建立栈帧,提高了程序运行效率。

// inline 关键字
inline int Add(int a, int b)
{
    
    
	int c = a + b;
	return c;
}
int main()
{
    
    
	int ret = Add(3, 5);
	cout << ret << endl;
	return 0;
}

7.2为什么要有内联函数

普通函数会建立栈帧的开销,不用函数,改成宏虽然不用建立栈帧,但是宏的缺点也显而易见。

宏的缺点:
语法细节多,容易出错
不能调试
没有安全类型的检查

所以C++引入了内联这个概念,可提高程序的效率。

7.3内联函数的特性

7.3.1提高程序运行效率

如果编译器将函数当成内联函数来处理,在编译阶段,会用函数体替换函数调用。少了调用开销,提高效率;但缺点是可能会使目标文件变大。

7.3.2inline 只是一个建议

当我们使用inline时,只是给编译器发送这么一个请求而已,不同编译器的处理结果不同,也就是说是否会将函数转换成内联函数取决于编译器。一般建议,如果函数规模较小、不是递归且频繁调用可以使用inline修饰。
在这里插入图片描述

7.3.3内联不能声明和调用分离

分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。

#include <iostream>
using namespace std;
inline int Add(int a, int b);
//
#include "List.h"
int Add(int a, int b)
{
    
    
	int c = a + b;
	return c;
}
/
int main()
{
    
    
	int ret = Add(3, 5);
	cout << ret << endl;
	return 0;
}

可看以下错误提示:
在这里插入图片描述

8.auto关键字

8.1介绍auto

简单来说,auto 就是拿来作类型推导,也可以叫做是类型的别名。

int a = 10;
auto b = a;

auto推导出来的类型是int

8.2为什么有auto

在编程时,我们返回一个函数的返回值,要知道它的返回类型。可是如果一个函数的返回类型很长,写起来很繁琐怎么办,所以用auto可以代替长的返回类型。

vector<string> v;
vector<string>::iterator it = v.begin();//写法1
auto it = v.begin();//写法2

8.3 auto的使用规则

1.auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&

    int x = 10;
    auto a = &x;
    auto* b = &x;
    auto& c = x;

2.在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。

auto a = 1, b = 2; 
auto c = 3, d = 4.0;  // 该行代码会编译失败,因为c和d的初始化表达式类型不同

注意:auto不能作为函数的参数;不能直接用来声明数组

9.指针空值nullptr(C++11)

在C语言中,我们学习过NULL是一个空指针。NULL实际是一个宏,可看以下代码:

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

在编程时,如果没有给一个指针具体指向,那么就用NULL作为初始化的值。但是,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,因此与程序的初衷相悖。

在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。

注意:

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

猜你喜欢

转载自blog.csdn.net/2301_77459845/article/details/133905194
今日推荐