C++ 学习笔记 -- 基础知识

当你知道的越多, 就觉得自己越无知

注释

//单行注释
/*
 * 多行注释
 */

标识符

标识符:

  • 不能是关键字
  • 不能以数字开头
  • 由数字, 字母, 下划线组成

匈牙利命名法:

  • 变量名前加类型, 如 iCount 代表 int 类型, cEnd 代表 char 类型

常量

  • 宏常量
#define CONST_ANME "constValue" 
//预处理指令, (编译器常量), 注意行末没有分号, 编译时执行替换, 可以是一个语句, 如下:
#define PRINT_HELLO std::cout<<"hello"<<endl
void main(){
	PRINT_HELLO;
	//打印结果 hello
}
  • const 常量(运行时), 其定义时必须赋值, 不能在定以后再次赋值(不同于Java)

字面值常量

整型字面值:

  • 十进制: 20
  • 八进制: 024
  • 十六进制: 0x14

指定字面值的类型:

  • L’a’ //宽字符型字面值, 类型是 wchar_t
  • u8"hi!" // utf-8 字符串字面值
  • 42ULL //无符号整型字面值, 类型是 unsigned long long
  • 1E-3F //单精度浮点型字面值, 类型是 float
  • 3.14159L //扩展精度浮点型字面值, 类型是 long double
    字符和字符串字面值前缀
前缀 含义 类型
u Unicode 16字符 char16_t
U Unicode 32字符 char32_t
L 宽字符 wchar_t
u8 UTF-8(仅用于字符串字面常量) char

整型字面值

后缀 最小匹配类型
u or U unsigned
l or L long
ll or LL long long

浮点型字面值

后缀 最小匹配类型
f or F float
l or L long double

预处理指令

  • #pragma once 保证头文件只会被编译一次

关键字

https://www.runoob.com/w3cnote/cpp-keyword-intro.html

关键字 描述
asm asm (指令字符串):允许在 C++ 程序中嵌入汇编代码。
auto auto(自动,automatic)是存储类型标识符,表明变量"自动"具有本地范围,块范围的变量声明(如for循环体内的变量声明)默认为auto存储类型。
bool bool(布尔)类型,C++ 中的基本数据结构,其值可选为 true(真)或者 false(假)。C++ 中的 bool 类型可以和 int 混用,具体来说就是 0 代表 false,非 0 代表 true。bool 类型常用于条件判断和函数返回值。
break break(中断、跳出),用在switch语句或者循环语句中。程序遇到 break 后,即跳过该程序段,继续后面的语句执行。
case 用于 switch 语句中,用于判断不同的条件类型。
catch catch 和 try 语句一起用于异常处理。
class class(类)是 C++ 面向对象设计的基础。使用 class 关键字声明一个类。
const const(常量的,constant)所修饰的对象或变量不能被改变,修饰函数时,该函数不能改变在该函数外面声明的变量也不能调用任何非const函数。在函数的声明与定义时都要加上const,放在函数参数列表的最后一个括号后。在 C++ 中,用 const 声明一个变量,意味着该变量就是一个带类型的常量,可以代替 #define,且比 #define 多一个类型信息,且它执行内链接,可放在头文件中声明;但在 C 中,其声明则必须放在源文件(即 .C 文件)中,在 C 中 const 声明一个变量,除了不能改变其值外,它仍是一具变量。
const_cast const_cast<type_id> (expression) 该运算符用来修改类型的 const 或 volatile 属性。除了 const 或 volatile 修饰之外, type_id 和 expression 的类型是一样的。常量指针被转化成非常量指针,并且仍然指向原来的对象;常量引用被转换成非常量引用,并且仍然指向原来的对象;常量对象被转换成非常量对象。
continue continue(继续)关键字用于循环结构。它使程序跳过代码段后部的部分,与 break 不同的是,continue 不是进入代码段后的部分执行,而是重新开始新的循环。因而它是"继续循环"之意,不是 break(跳出)。
default default(默认、缺省)用于 switch 语句。当 switch 所有的 case 都不满足时,将进入 default 执行。default 只能放在 switch 语句所有的 case 之后,并且是可选的。
delete delete(删除)释放程序动态申请的内存空间。delete 后面通常是一个指针或者数组 [],并且只能 delete 通过 new 关键字申请的指针,否则会发生段错误。
do do-while是一类循环结构。与while循环不同,do-while循环保证至少要进入循环体一次。
double double(双精度)类型,C++ 中的基本数据结构,以双精度形式存储一个浮点数。
dynamic_cast dynamic_cast(动态转换),允许在运行时刻进行类型转换,从而使程序能够在一个类层次结构安全地转换类型。dynamic_cast 提供了两种转换方式,把基类指针转换成派生类指针,或者把指向基类的左值转换成派生类的引用。
else else 紧跟在 if 后面,用于对 if 不成立的情况的选择。
enum enum(枚举)类型,给出一系列固定的值,只能在这里面进行选择一个。
explicit explicit(显式的)的作用是"禁止单参数构造函数"被用于自动型别转换,其中比较典型的例子就是容器类型。在这种类型的构造函数中你可以将初始长度作为参数传递给构造函数。
export 为了访问其他编译单元(如另一代码文件)中的变量或对象,对普通类型(包括基本数据类、结构和类),可以利用关键字 extern,来使用这些变量或对象时;但是对模板类型,则必须在定义这些模板类对象和模板函数时,使用标准 C++ 新增加的关键字 export(导出)。
extern extern(外部的)声明变量或函数为外部链接,即该变量或函数名在其它文件中可见。被其修饰的变量(外部变量)是静态分配空间的,即程序开始时分配,结束时释放。用其声明的变量或函数应该在别的文件或同一文件的其它地方定义(实现)。在文件内声明一个变量或函数默认为可被外部使用。在 C++ 中,还可用来指定使用另一语言进行链接,这时需要与特定的转换符一起使用。目前仅支持 C 转换标记,来支持 C 编译器链接。使用这种情况有两种形式:extern “C” 声明语句 extern “C” { 声明语句块 }
false false(假的),C++ 的基本数据结构 bool 类型的值之一。等同于 int 的 0 值。
float float(浮点数),C++ 中的基本数据结构,精度小于 double。
for for 是 C++ 中的循环结构之一。
friend friend(友元)声明友元关系。友元可以访问与其有 friend 关系的类中的 private/protected 成员,通过友元直接访问类中的 private/protected 成员的主要目的是提高效率。友元包括友元函数和友元类。
goto goto(转到),用于无条件跳转到某一标号处开始执行。
if if(如果),C++ 中的条件语句之一,可以根据后面的 bool 类型的值选择进入一个分支执行。
inline inline(内联)函数的定义将在编译时在调用处展开。inline 函数一般由短小的语句组成,可以提高程序效率。
int int(整型,integer),C++ 中的基本数据结构,用于表示整数,精度小于 long。
long long(长整型,long integer),C++ 中的基本数据结构,用于表示长整数。
mutable mutable(易变的)是 C++ 中一个不常用的关键字。只能用于类的非静态和非常量数据成员。由于一个对象的状态由该对象的非静态数据成员决定,所以随着数据成员的改变,对像的状态也会随之发生变化。如果一个类的成员函数被声明为 const 类型,表示该函数不会改变对象的状态,也就是该函数不会修改类的非静态数据成员。但是有些时候需要在该类函数中对类的数据成员进行赋值,这个时候就需要用到 mutable 关键字。
namespace namespace(命名空间)用于在逻辑上组织类,是一种比类大的结构。
new new(新建)用于新建一个对象。new 运算符总是返回一个指针。由 new 创建
operator operator(操作符)用于操作符重载。这是 C++ 中的一种特殊的函数。
private private(私有的),C++ 中的访问控制符。被标明为 private 的字段只能在本类以及友元中访问。
protected protected(受保护的),C++ 中的访问控制符。被标明为 protected 的字段只能在本类以及其继承类和友元中访问。
public public(公有的),C++ 中的访问控制符。被标明为 public 的字段可以在任何类
register register(寄存器)声明的变量称着寄存器变量,在可能的情况下会直接存放在机器的寄存器中;但对 32 位编译器不起作用,当 global optimizations(全局优化)开的时候,它会做出选择是否放在自己的寄存器中;不过其它与 register 关键字有关的其它符号都对32位编译器有效。
reinterpret_cast 用法:reinpreter_cast (expression) type-id 必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原先的指针值)。
return return(返回)用于在函数中返回值。程序在执行到 return 语句后立即返回,return 后面的语句无法执行到。
short short(短整型,short integer),C++ 中的基本数据结构,用于表示整数,精度小于 int。
signed signed(有符号),表明该类型是有符号数,和 unsigned 相反。数字类型(整型和浮点型)都可以用 signed 修饰。但默认就是 signed,所以一般不会显式使用。
sizeof 由于 C++ 每种类型的大小都是由编译器自行决定的,为了增加可移植性,可以用 sizeof 运算符获得该数据类型占用的字节数。
static static(静态的)静态变量作用范围在一个文件内,程序开始时分配空间,结束时释放空间,默认初始化为 0,使用时可改变其值。静态变量或静态函数,只有本文件内的代码才可访问它,它的名字(变量名或函数名)在其它文件中不可见。因此也称为"文件作用域"。在 C++ 类的成员变量被声明为 static(称为静态成员变量),意味着它被该类的所有实例所共享,也就是说当某个类的实例修改了该静态成员变量,其修改值为该类的其它所有实例所见;而类的静态成员函数也只能访问静态成员(变量或函数)。类的静态成员变量必须在声明它的文件范围内进行初始化才能使用,private 类型的也不例外。
static_cast 用法:static_cast < type-id > ( expression ) 该运算符把 expression 转换为 type-id 类型,但没有运行时类型检查来保证转换的安全性。它主要有如下几种用法:① 用于类层次结构中基类和子类之间指针或引用的转换。进行上行转换(把子类的指针或引用转换成基类表示)是安全的;进行下行转换(把基类指针或引用转换成子类表示)时,由于没有动态类型检查,所以是不安全的。② 用于基本数据类型之间的转换,如把 int 转换成 char,把 int 转换成 enum。这种转换的安全性也要开发人员来保证。③ 把空指针转换成目标类型的空指针。④ 把任何类型的表达式转换成void类?注意 static_cast 不能转换掉 expression 的 const、volitale、或者 __unaligned 属性。
struct struct(结构)类型,类似于 class 关键字,与 C 语言兼容(class 关键字是不与 C 语言兼容的),可以实现面向对象程序设计。
switch switch(转换)类似于 if-else-if 语句,是一种多分枝语句。它提供了一种简洁的书写,并且能够生成效率更好的代码。但是,switch 后面的判断只能是int(char也可以,但char本质上也是一种int类型)。switch 语句最后的 default 分支是可选的。
template template(模板),C++ 中泛型机制的实现。
this 返回调用者本身的指针。
throw throw(抛出)用于实现 C++ 的异常处理机制,可以通过 throw 关键字"抛出"一个异常。
true true(真的),C++ 的基本数据结构 bool 类型的值之一。等同于 int 的非 0 值。
try try(尝试)用于实现 C++ 的异常处理机制。可以在 try 中调用可能抛出异常的函数,然后在 try 后面的 catch 中捕获并进行处理。
typedef typedef(类型定义,type define),其格式为:typedef 类型 定义名;类型说明定义了一个数据类型的新名字而不是定义一种新的数据类型。定义名表示这个类型的新名字。
typeid 指出指针或引用指向的对象的实际派生类型。
typename typename(类型名字)关键字告诉编译器把一个特殊的名字解释成一个类型。在下列情况下必须对一个 name 使用 typename 关键字:1. 一个唯一的name(可以作为类型理解),它嵌套在另一个类型中的。2. 依赖于一个模板参数,就是说:模板参数在某种程度上包含这个name。当模板参数使编译器在指认一个类型时产生了误解。
union union(联合),类似于 enum。不同的是 enum 实质上是 int 类型的,而 union 可以用于所有类型,并且其占用空间是随着实际类型大小变化的。
unsigned unsigned(无符号),表明该类型是无符号数,和 signed 相反。
using 表明使用 namespace。
virtual virtual(虚的),C++ 中用来实现多态机制。
void void(空的),可以作为函数返回值,表明不返回任何数据;可以作为参数,表明没有参数传入(C++中不是必须的);可以作为指针使用。
volatile volatile(不稳定的)限定一个对象可被外部进程(操作系统、硬件或并发线程等)改变,声明时的语法如下:int volatile nVint;这样的声明是不能达到最高效的,因为它们的值随时会改变,系统在需要时会经常读写这个对象的值。因此常用于像中断处理程序之类的异步进程进行内存单元访问。
wchar_t wchar_t 是宽字符类型,每个 wchar_t 类型占 2 个字节,16 位宽。汉字的表示就要用到 wchar_t。

sizeof

语法:
sizeof(变量) 或者 sizeof(数据类型)

    short a = 10;
    int b = 10;
    long c = 10;
    long long d = 10;

    cout << sizeof(a) << endl;
    cout << sizeof(b) << endl;
    cout << sizeof(long) << endl;
    cout << sizeof(long long) << endl;

namespace & include 的区别与联系

  1. namespace 依赖于 include
  2. include导入的是文件, 可以是.h文件, 也可以是.cpp文件
  3. using namespace导入的是文件namespace块的中的函数
  4. namespace可以用于区分不同文件相同方法名的情况
  5. 例如:
    //NamespaceTest.cpp
    #include <iostream>
    namespace Main{
    
        int main(){
            std::cout << "NamespaceTest main() method running" << std::endl;
            return 0;
        }
    
        void test(){}
    }
    
    namespace Runnable{
        int run(){
            std:: cout << "NamespaceTest run() method running" << std::endl;
            return 0;
        }
        int main(){
            return 0;
        }
    }
    //main.cpp
    #include "NamespaceTest.cpp"
    
    int main() {
        Runnable::run();
        Runnable::main();
        Main::main();
        test();
        return 0;
    }
    

变量

变量声明

  • 使用括号声明: int i(1), j(1);
  • 使用等号声明: int i = 1, j = 1;

局部变量与全局变量相同时, 使用 :: 访问全局变量, 如:

int var;
int main(){
	int var = 1;
	cout << var << endl;//输出1
	cout << ::var << endl;//输出0
}

整型

short 通常占用两个字节
int 通常占用4个字节
long 跟int 占用字节相同或更大
long long 占用字节大于long , 跟平台编译器相关
可以使用 sizeof(int) sizeof(long long) 来获取数据类型的内存占用

静态变量

静态变量存放在全局区, 待程序结束后才释放

三元运算符

与java不同, 在C++中三元运算符返回结果是一个变量, 可以继续赋值(java中返回结果是值, 不能继续赋值)

int a = 10;
int b = 20;
(a > b ? a : b) = 100;//语法上没错但不建议这么写

数组

一维数组
  • 定义方式:
    int array_1[6];
    int array_2[6] = {1, 2, 3, 4, 5, 6};
    int array_3[] = {1, 2, 3, 4, 5, 6};
    int array_4[6]{1, 2, 3, 4, 5, 6};
    int array_5[]{1, 2, 3, 4, 5, 6};
    
  • 获取数组长度: sizeof(array_1) / sizeof(array_1[0])
  • 注意: 如果数组中的某个元素未赋值,请勿直接获取该元素,因为其初始值不确定,不一定为0(实测结果)
  • 遍历一维数组:
    for (int i = 0; i < sizeof(array_1) / sizeof(array_1[0]); ++i) {
       cout << array_1[i] << endl;
    }
    
二维数组
  • 二维数组定义方式:
    int array_1[2][3]{{1, 2 , 3},{1, 2, 3}};
    int array_2[2][3];
    int array_3[2][3] = {{1, 2 , 3},{1, 2, 3}};
    int array_4[][3] = {{1, 2 , 3},{1, 2, 3}};
    int array_5[2][3] = {1, 2, 3, 1, 2, 3};
    int array_6[2][3]{1, 2, 3, 1, 2, 3};
    int array_7[][3]{1, 2, 3, 1, 2, 3};
    
  • 获取数组长度: sizeof(array_1) / sizeof(array_1[0])
  • 注意: 如果数组中的某个元素未赋值,请勿直接获取该元素,因为其初始值不确定,不一定为0(实测结果)
  • 遍历二维数组:
    for (int i = 0; i < sizeof(array_1) / sizeof(array_1[0]); ++i) {
        for (int j = 0; j < sizeof(array_1[0]) / sizeof(array_1[0][0]); ++j) {
            cout << array_1[i][j] << endl;
        }
    }
    

指针

取址符

首先我们知道使用 & 可以获取一个变量的地址, 此地址通常是一个16进制的整数:

ing a = 10;
cout << "a的地址是: " << &a << endl;
//控制台输出: 0x61fe44

指针

指针是用来存储地址的一个变量, 通常的定义方式为 type* name , 其中 type 是地址指向变量的实际类型, name 是指针变量的变量名, 如 int* c 可以定义一个指向整型变量的指针, 通过*c 可以访问变量c, 进行赋值或取值操作。

int b = 10;
int* a;//定义指针变量
a = &b;//将地址赋值给指针变量
*a = 20;//操作指针指向的变量b , 给b赋值为20
cout << b << endl;
//控制台输出20

空指针

通常我们在定义指针变量的时候会给他赋初始值: int *b = NULL, 如果此时我们通过 *b 来访问指针变量指向的值, 程序将会崩溃, 我们称这个操作为发生了空指针。

int *b = NULL;
cout << *b << endl;
//程序发生崩溃

指针与数组

数组 --> 指针

int  var[MAX] = {10, 100, 200};
int  *ptr;
ptr = var;//此时如果想通过ptr来取数组中的元素, 必须使用指针自增运算

指针的算术运算

由于数组的内存地址是连续的, 所以可以通过指针自增来取数组元素

int  var[MAX] = {10, 100, 200};
int  *ptr;
ptr = var;
for(int i = 0; i < sizeof(var) / sizeof(var[0]);i++){
    cout << *ptr << endl;
    ptr++;
}

//输出结果:
// 10
// 100
// 200

指针数组

顾名思义, 就是存储指针类型的数组

int main ()
{
   int  var[MAX] = {10, 100, 200};
   int *ptr[MAX];
 
   for (int i = 0; i < MAX; i++)
   {
      ptr[i] = &var[i]; // 赋值为整数的地址
   }
   for (int i = 0; i < MAX; i++)
   {
      cout << "Value of var[" << i << "] = ";
      cout << *ptr[i] << endl;
   }
   return 0;
}

//输出结果:
//Value of var[0] = 10
//Value of var[1] = 100
//Value of var[2] = 200

指针指向指针

int main ()
{
   int  var;
   int  *ptr;
   int  **pptr;

   var = 3000;

   // 获取 var 的地址
   ptr = &var;

   // 使用运算符 & 获取 ptr 的地址
   pptr = &ptr;

   // 使用 pptr 获取值
   cout << "Value of var :" << var << endl;
   cout << "Value available at *ptr :" << *ptr << endl;
   cout << "Value available at **pptr :" << **pptr << endl;

   return 0;
}

const 修饰指针

//const 修饰指针
void constpoint(){
    int a = 10;
    int b = 10;

    //常量指针
    const int *p = &a;
    //*p = 11;
    //指针的指向可以更改
    //指针指向的值不可更改

    //指针常量
    int * const pt = &a;
    *pt = 11;
    //pt = &b;
    //指针指向的值可以更改
    //但指针的指向不可更改

    //const 修饰常量和指针
    const int * const ptr = &a;
//    ptr = &b;
//    *ptr = 11;
    //指针的指向不可更改
    //指针指向的值可以更改
}

结构体指针

//结构体指针
struct Student {
    string name;
    int age;
};

void structpointer(){
    Student student;
    student.name = "张三";
    student.age = 18;

    Student *_student = &student;
    _student->age = 19;//结构体指针访问属性使用操作符 '->'
    _student->name = "赵六";

    cout << _student->name << _student->age;
}

内存四区

  • 代码区
  • 全局区 由系统控制
  • 堆区 由开发者控制
  • 栈区 由系统控制

开辟内存&释放内存

开辟内存:
使用new关键字在中开辟一块内存, 使用指针来接收

int* t = new int(10);//为int开辟空间
int * array = new int[10];//为数组开辟空间

释放内存:

delete t;//释放int的内存
delete[] array;//释放数组的内存

引用

  • 引用是变量的别名
  • 引用不可为空(必须要初始化)
  • 引用初始化后不可修改
  • 不要返回局部变量的引用
  • 引用本质是一个指针常量
  • 如果函数返回值是引用, 则该返回值可以作为左值
    int& ref(int& a) {
    	a = 11;
    	return a;
    }
    int main() {
    	int a = 10;
    	ref(a) = 12;
    	cout << a << endl;//12
    	system("pause");
    	return 0;
    }
    
int a = 10;
int& b = a;//b是a的引用
b = 20;
cout << a << endl;//输出20

在函数参数中使用引用:

void swap(int& a, int& b) {
	int temp = a;
	a = b;
	b = temp;
}

int main() {

	int a = 10;
	int b = 20;
	swap(a, b);//实现数据的交换
	cout << a << endl;
	cout << b << endl;
	system("pause");
	return 0;
}

常量引用:
常量引用不可赋值:

int a = 10;
const int& b = a;
b = 11;//ERROR: 编译器将报错, 因为常量引用不可赋值

函数

函数参数默认值

  • 如果函数的某个参数有默认值, 那么此参数右边的所有参数都必须有默认值
  • 如果函数声明有默认参数, 其实现不能有默认参数
  • 如果函数实现有默认参数, 其声明不能有默认参数
int add(int a, int b = 0, int c = 0) {
	return a + b + c;
}

int result = add(10, 11, 12);
result = add(10, 11);
result = add(10);

函数占位参数

一般用于程序扩展和对C代码的兼容

void add(int a, int){
	...
}
int main(){
	add(10, 10);
	return 0;
}

函数重载

  • 在同一个作用域下
  • 函数名相同
  • 函数参数类型不同/个数不同/顺序不同

面向对象

  • 使用 class 定义类, 如下代码
    class Box {
    public: //权限修饰符-公有
    	double length;
    private: //权限修饰符-私有
    	double breadth;
    protected: //权限修饰符-受保护
    	double height;
    
    private:
    	int* ptr;
    	Box();//构造函数声明
    	//构造函数直接定义
    	Box(double length) {
    		this->length = length;
    	}
    	//使用初始化列表来初始化字段, 中间使用 "," 来分割
    	//格式: 构造函数(参数列表): 字段名(参数名), 字段名(参数名)
    	Box(double len, double height): length(len), height(height)
    	{
    		cout << "Object is being created, length = " << len << endl;
    	}
    	//析(xī)构函数: 类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。
    	//没有参数, 不可重载
    	~Box() {
    		cout << "Object is being deleted" << endl;
    	}
    
    	Box(const Box& obj);  // 拷贝构造函数
    
    private:
    	void setLength(double len);//函数声明
    public:
    	double getLength() {
    		return length;
    	};//类成员函数
    };
    
    // 构造函数定义
    Box::Box(void)
    {
    	cout << "Object is being created" << endl;
    }
    //成员函数可以定义在类定义内部,或者单独使用范围解析运算符 :: 来定义。在类定义中定义的成员函数把函数声明为内联的
    //使用范围解析运算符 :: 定义
    void Box::setLength(double len)
    {
    	length = len;
    }
    
    Box::Box(const Box& obj)
    {
    	cout << "Copy constructor allocating ptr." << endl;
    	ptr = new int;
    	*ptr = *obj.ptr; // copy the value
    }
    

访问权限

  • private 私有权限
    • 内部可访问
  • public 公共权限
    • 内部可访问, 子类可访问, 外部可访问
  • protected 受保护的权限
    • 内部可访问, 子类可访问

构造函数

  • 无参构造
    • 类名(); 初始化时调用
  • 有参构造
    • 类名(参数列表); 初始化时调用
  • 析构函数
    • ~类名(); 内存被释放时调用
  • 拷贝构造函数
    类名(const 类名 &参数名); 返回局部对象时或值传递传参时调用, 如:
class Person{
	int age;
	Person(int age){//构造函数
		this-> age = age;
	}

	Person(const Person &person){//拷贝构造
		age = person.age;
	}
};

int main(){
	Person p1(10);//创建p1对象
	Person p2 = Person(p1);//拷贝构造调用
	return 0;
}

构造函数调用:

class Person{
	int age;
	Person(int age){//构造函数
		this-> age = age;
	}
};

int main(){
	//构造函数调用
	Person p1(10);//括号法
	Person p2 = Person(10);//显示法
	Person p3 = 10;//隐式转换法
	return 0;
}

持续更新

猜你喜欢

转载自blog.csdn.net/qq_27070117/article/details/103365136