【C++引用超详细笔记, 引用、指针和值传递的汇编级辨析,对象、继承和引用】

【C++引用超详细笔记, 引用、指针和值传递的汇编级辨析,对象、继承和引用】,第1张

文章目录
  • 引用变量
    • 1. 什么是引用、如何创建一个引用变量?
    • 2. 引用的具体使用方法
      • 2.1 引用做函数的形参
      • 2.2 引用参数的const用法
      • 左值、右值是什么?
      • 2.3 结构体的引用
      • 2.4 引用做函数返回值
      • 2.5 将引用用于类对象
    • 3 引用的本质是什么? 引用,指针和值传递的辨析
    • 4 什么时候使用引用参数?

参考文献:
C++ primer plus
对象、继承和引用
C++的左值和右值
黑马程序员:引用的本质

引用变量 1. 什么是引用、如何创建一个引用变量?

要搞清楚什么是引用,首先我们要清楚变编程语言的变量名或者叫标识符究竟是什么,比如int a; int *p; 这里面的a或者p,究竟是个什么呢? ———— 这些标识符本质上就是用来代指一个内存单元的.在汇编语言中,比如[0x1101]这就是一个内存单元,里面的0x1101是这个内存单元的地址或者说编号,外面的方括号用来表明直接寻址方式,即表示这个地址所代表的的内存单元。


而用一长串地址加一个方括号表示内存单元太过麻烦,我们就给这个[0x1101]这个内存单元起了一个好听的名字a,这个符号a经过编译器翻译和分配内存单元之后,所形成的的汇编指令,就被翻译成[0x1101]了,我们可以叫这个内存单元a,当然我们也可以叫它b或者dog随便什么名字都可以,一个内存单元当然可以拥有多个名字,至于叫什么其实无所谓,因为指的都是这个内存单元。

这个小名b就是引用啦。


int a = 0;
int &b = a;
这个b就是给a这个内存起了个小名b而已,并没有什么特别的。


&在C++中有两种用法,一种是引用,一种是取地址。

同样,指针int *p = &a; 中的p,所代表的也不过是一个普通的内存单元,我们就叫它[0x1102]

1、 我们&p ,也就是取这个内存单元的地址,也就是0x1102这个地址值。


2、我们输出p ,得到的就是这个内存单元里的内容,也就是a的地址呗,就是0x1101
3、 我们输出*p,这是解引用 *** 作,得到的就是a所代表的内存单元存的的内容,或者说是指针p指向的内存单元的内容,就是0

那么我们既然知道了变量a引用变量b指的都是同一个内存单元[0x1101],那呢我们无论或者a或者b中的任何一个进行 *** 作,[0x1101]这个内存单元都会发生相应的改变。

2.引用的注意事项:
1、引用必须初始化: int &b; b = a;是错误的,必须是int &b = a,引用在初始化后,不可以改变:
2、一个别名不允许更改成另一个变量的别名,起别名后,别名只能代表一块内存,不能改变成另一块内存

示例代码

//1. 引用的基本语法
int a = 10;
int &b = a;   //给a对应的内存起个别名b,用b也可以 *** 作这款块内存
b = 20;
cout <<"a= "<< a << endl;   // 20
cout << "b= "<<b << endl;   // 20

b = 100;

cout << "a= " << a << endl;  //100
cout << "b= " << b << endl;  //100

//2 引用的注意事项:必须初始化,不可改变内存的指向
int c = 10;
int &d = c;   

int g = 20;
d = g;  //赋值从 *** 作,而不是更改引用
// int& d = g;  //这也不是更改引用,这是多次初始化了,也是错的,别名就不能更改

cout << "c= " << c << endl;  //c和d的内容一样,因为这就是一块相同的内存区域
cout << "d= " << d << endl;
cout << "g= " << g << endl;

2. 引用的具体使用方法 2.1 引用做函数的形参

函数形参的形式一般分为三种:

  • 值传递方式:形参不能改变实参,形参和实参相当于是两块独立的内存,形参内存区数值的变化对于实参的内存单元中的数值是没有影响的。

  • 地址传递方式:即指针方式,通过另外一块内存单元(即指针变量)来指向实参的内存区,通过改变这个指针的指向,来直接对实参内存区中不同的单元进行 *** 作。

    所以指针做函数形参是可以改变实参的数值的。

  • 引用方式:引用做函数形参时,就是相当于对这个实参的内存单元起了一个别名,实际上直接 *** 作的就是实参的内存单元区域,所以引用方式也可以改变函数实参。


以常见的交换函数为例来说明三种形参的差别

#include

//值传递
oid mySwap01(int a, int b)
{
	int temp = a;
	a = b;
	b = temp;
}

//地址传递
oid mySwap02(int* a, int* b)
{
	int temp = *a;
	*a = *b;
	*b = temp;
}

//引用传递
oid mySwap03(int &a, int &b)  //相当于 int &a_2 = a; 给实参a起了个别名形参a, 形参a *** 作的还是实参a所代表的的那块内存单元,所以可以形参改变实参
{
	int temp = a;
	a = b;
	b = temp;
}

int main(){
    using namespace std;
    
	int a = 10;
	int b = 20;
	mySwap01(a, b);     //值传递的方式形参不会改变实参
	cout << "a = " << a << endl;  //10
	cout << "b = " << b << endl;  //20

	a = 10;
	b = 20;
	mySwap02(&a, &b);     //地址传递的形参可以改变实参
	cout << "a = " << a << endl;   //20
	cout << "b = " << b << endl;   //10

	a = 10;
	b = 20;
	mySwap03(a, b);     //引用时形参可以改变实参
	cout << "a = " << a << endl;   //20
	cout << "b = " << b << endl;   //10

    return 0;
}

引用做形参的注意事项
值传递时,实参时允许传入一个常数或者表达式的,比如

double z = cube(x + 2.0);
z = cube(8.0);
z = cube(k);

但是传递引用的限制更严格,实参只能是变量名,而不能是一个常数或者表达式
如下都是错误的

double refcube(double &ra)
{
    ra = ra*ra;
    return ra;
}

int main()
{
    double z = refcube(3.0);  //错误 不能double &ra = 3.0

    double x = 10; 
    double z = refcube(x + 3.0); //错误 不能double &ra = x + 3.0

    double z = refcube(x); //正确 ,相当于 double &ra = x 
}
2.2 引用参数的const用法

常量引用: const
作用:

  1. 常量引用用来修饰形参,防止误 *** 作,
  2. 在函数形参列表中,可以加const修饰形参,防止形参改变实参

注意:仅当参数为const引用时,如果实参与引用参数不匹配,C++将生成临时变量,可以传递参数的种类更丰富一些,而常规变量引用是不允许的。

double refcube(const double &ra)
{
    return ra*ra*ra;
}

double side = 3.0;
double *pd = &side;
double &rd = side;
long edge = 5l;
double lens[4] = {2.0, 5.0, 10.0, 12.0};

//side,len[2],rd,*pd都是有名称的1,double类型的数据对象,因此可以欸其创建引用,而不需要临时变量
double c1 = refcube(side);
double c2 = refcube(lens[2]);
double c3 = refcube(rd);
double c4 = refcube(*pd);

//以下三种i情况,数据类型问题或者是表示式、常数,没有名称,都是需要创建临时变量,但不会报错,与常规变量引用不同。

//edge虽然是变量,但是类型却不正确,double不能指向long double c5 = refcube(edge); //7.0和side+10.0都是double类型的数据,但是没有名称 double c6 = refcube(7.0); double c7 = refcube(side+10.0);

一般来说const int &a = 10; 有问题,
但编译器会这样处理:
int tenmp = 10;
int &a = temp ;
这样就正确了

需要注意的是编译器只会对const引用做临时变量处理,而常规变量引用则不会

究其原因是因为常规引用中形参是要改变实参,而创建临时变量则会阻止这种改变,引起错误,所以方法就是直接禁止创建临时变量。


而对于const引用目的则是不想改变实参,只是想用引用方便一些,不需要像值传递时需要复制很大一块空间,所以加个const不允许改变参数值,如果不小心改变了,编译器会报错提醒程序员。

所以这个时候允许创建临时变量,使用起来可以更方便,传入参数类型限制可以不用那么严格。

使用const的好处

  • 可以避免无意修改数据的编程错误
  • 能处理左值数据和右值数据,否则只能接受左值数据
  • 能够使函数正确生成并使用临时变量
左值、右值是什么?

左值:左值是一个可被引用的数据对象,例如变量、数组元素、结构成员、引用和解除引用的指针都是左值。

(实际占用内存空间的数据),对cpu来说对左值可以进行读和写 *** 作。

(可以写在左边或右边)
右值:包括常量和包含多项的表达式(用引号扩起的字符串除外,他们由地址表示)。

基本不占用内存,一般在寄存器里或者是立即数(汇编概念),只能进行读 *** 作。


常规变量和const变量都可视为左值,因为可通过地址访问他们,但常规变量属于可修改的左值,而const变量属于不可修改的左值。

左值引用(使用&声明)是指向左值的,加了const之后也可以指向右值(实际上是创建了个临时变量)
C++11新增了右值引用,是用来指向右值的,用&&声明

double && rref = std::sqrt(36.00);   //左值引用就不允许这样做
double j = 15.0;
double && jref = 2.0*j+18.5;  //右值引用
std::cout << rref <<'\n';   //6.0
std::cout << jref <<'\n';  //48.5;
2.3 结构体的引用

引用非常适用于结构和类,引入引用机制主要就是为了用于这些类型的,而不是基本的内置类型。


使用方法与使用基本变量引用相同

struct free_throws
{
    std::string name;
    int made;
    int attempts;
    float persent;
};
//修改传入的结构
void set_pc(free_throws &ft);
//或者是不修改传入的结构
void set_pc( const free_throws &ft);
2.4 引用做函数返回值
  • 作用:引用是可以作为函数的返回值存在的,返回引用的函数实际上是被引用变量的一个别名
free_throws& accumulate(free_throws& target, const free_throws& source)
{
    target.attempts += source.attempts;
    target.made += source.made;
    set_pc(target);
    return target;
}

函数accumulate(a,b) 实际上就是a这个地址空间的一个别名,可以用来作为左值

  • 用法:函数的调用可以作为左值
   display(accumulate(team, two));  

   accumulate(accumulate(team, three), four);
   

   dup = accumulate(team, five);

   free_throws &a =  accumulate(team, five);
   
   accumulate(dup, five) = four;
   // 这条语句更详细的内容见下面的,const用于返回值类型
  • 注意:不要返回局部变量引用
//不要返回局部变量引用
int& test01()
{
	int a = 10;  //局部变量存放在栈区,出了函数之后就被编译器释放负掉了
	return a;  
}

//返回静态变量引用 
int& test02()
{
	static int a = 10;  //静态变量存放在全局区,整个程序运行结束之后才由 *** 作系统释放
	return a; 
}


int main()
{
    int &ref = test01();
    cout << "ref = " <<ref<< endl;    //第一次结果正确,是因为编译器做了一次保留
    cout << "ref = " << ref<<endl;    //第二次结果错误,是因为a的内存已经释放

    int& ref2 = test02();
    cout << "ref2 = " << ref2 << endl; //两次都正确,因为这块内存一直没有释放
    cout << "ref2 = " << ref2 << endl;
    return 0;
}

解决方法:

  1. 函数返回引用时,返回一个传入函数的参数。

    形参引用将指向实参数据区,返回引用也可以指向这个实参数据区,不去出现问题(比如上面accumulate( , )采取的方法)

  2. 用new来分配新的存储空间,

free_throws& clone2(free_throws& ft)
{
    free_throws newguy;   
    newguy = ft;
    return newguy;  //返回的是一个局部变量,在栈区,函数结束后这段内存空间就被释放了
}



 free_throws& clone(free_throws& ft)
{
     free_throws* pt = new free_throws{}; //使用new分配新的存储空间,由程序员自己释放,用一个指针指向这个空间
    *pt = ft;   //向这个空间写入数据
    return *pt;  //相当于 free_throws &clone(free_throws& ft) = *pt,返回这个*pt所代表的空间的一个引用  即clone(one)就是这个空间的第一个单元一个别名
}

int main()
{
    free_throws& jolly = clone(one);
    display(jolly);   //每次显示结果都一样,因为堆空间是由程序员自己释放的。

display(jolly); display(jolly); return 0}

但是这个方法也有一点问题,就是调用clone()函数时看不到new,程序员可能会忘了delete这段内存空间。

结构与返回的引用变量应用示例

//strc_ref.cpp -- using structure references
#include 
#include 
struct free_throws
{
    std::string name;
    int made;
    int attempts;
    float percent;
};


void display(const free_throws& ft);
void set_pc(free_throws& ft);
free_throws& accumulate(free_throws& target, const free_throws& source);

int main()
{
    //初始化结构体成员变量,指定的初始值(3个)比成员数(4个)少,剩下的成员(precent)将被设置为0
    free_throws one = { "Ifelsa Branch", 13, 14 };
    free_throws two = { "Andor Knott", 10, 16 };
    free_throws three = { "Minnie Max", 7, 9 };
    free_throws four = { "Whily Looper", 5, 9 };
    free_throws five = { "Long Long", 6, 14 };
    free_throws team = { "Throwgoods", 0, 0 };
    free_throws dup;


    set_pc(one);
    display(one);
    accumulate(team, one);
    display(team);

    // use return value as argument
    display(accumulate(team, two));             //返回引用的函数实际上是返回这个结构的别名,这个别名还可以作为左值,供左值引用来使用
    //相当于这两句:
    // accumulate(team, two)   返回team对象
    // display(team);         ft指向team
    
    accumulate(accumulate(team, three), four);
    display(team);

    //这里是赋值 *** 作,相当于将team结构复制了一份赋值给dup (发生了拷贝)
    dup = accumulate(team, five);
    std::cout << "Displaying team:\n";
    display(team);
    std::cout << "Displaying dup after assignment:\n";
    display(dup);
    set_pc(four);

    //下面这条语句如果是按值返回是不能写在左边的,因为返回的是一个右值
    //但是用返回引用的方式就可以了,因为返回值是一个左值,可以对这个左值进行写 *** 作(赋值)
    accumulate(dup, five) = four;
    //上面的两条语句就相当于:
    //accumulate(dup, five);     dup = dup + five
    // dup = five;               dup = five
    // 其中第二条语句消除了第一条语句的工作,所以这种方法虽然能通过编译,但是不太好。

std::cout << "Displaying dup after ill-advised assignment:\n"; display(dup); // std::cin.get(); return 0; } void display(const free_throws& ft) //形参为结构体常量引用,不能修改实参 ,只是用来打印结构体所有成员变量 { using std::cout; cout << "Name: " << ft.name << '\n'; cout << " Made: " << ft.made << '\t'; cout << "Attempts: " << ft.attempts << '\t'; cout << "Percent: " << ft.percent << '\n'; } void set_pc(free_throws& ft) //形参为结构体左值引用 ,用于设置分数 { if (ft.attempts != 0) ft.percent = 100.0f * float(ft.made) / float(ft.attempts); else ft.percent = 0; } free_throws& accumulate(free_throws& target, const free_throws& source) //target可修改 , source不可修改 ,返回target的引用 { target.attempts += source.attempts; target.made += source.made; set_pc(target); return target; }

  • const应用于返回类型

accumulate(dup, five) = four;
这条语句是可以通过编译的,因为函数返回指向dup的引用,其实是标识了dup这个内存块的,它是由地址的实实在在的一块内存空间的别名,是可以作为左值的。


而常规的函数返回类型返回的是右值,即不能通过地址访问的值,(比如 10 这种常数或者 x + y这种表达式),这种返回值只是位于临时的内存单元中,运行到下一条语句时,他们可能就不存在了

但是很明显,这里的赋值 *** 作four ,覆盖了函数 *** 作的dup空间,所以当你想想要使用引用,又不允许给返回的左值进行赋值 *** 作时,可以使用const修饰
const free_throws & accumulate(free_throws& target, const free_throws& source);
相当于返回一个不可更改的左值(这块内存只能读不能写)。

使用const之后,

   display(accumulate(team, two));  
//这里仍然可以使用,因为display()的形参也是const free_throws & 类型
   accumulate(accumulate(team, three), four);
   //这里会报错,因为第一个参数是非const类型
   //但这里影响不大
  
   accumulate(team, three);
   accumulate(team, four);
  //这样就可以了

   dup = accumulate(team, five); //从内存读数据没问题
   
  const free_throws &a =  accumulate(team, five);
   //接收也要加一个const
   accumulate(dup, five) = four; //报错

2.5 将引用用于类对象

1. 使用引用方式将类的对象作为函数参数

#include
#include
using namespace std;

//使用引用方式将类的对象作为函数参数
string version1(const string& s1, const string& s2);
const string& version2(string& s1, const string& s2);  //has side effect
const string& version3(string& s1, const string& s2);  //bad design

int main()
{
	string input;
	string result;
	string copy;   //副本,后面用来恢复input

	cout << "Enter a string: ";
	getline(cin, input);
	copy = input;

	cout << "Your string as entered: " << input << endl;
	result = version1(input, "***");
	cout << "Your string enhanced: " << result << endl;
	cout << "Your string original:" << input << endl;   //值传递原始内容不变

	result = version2(input, "###");
	cout << "Your string enhanced: " << result << endl;
	cout << "Your string original:" << input << endl;   //引用传递原始内容改变了

	//恢复input的内容
	cout << "Reseting original string\n";
	input = copy;

	result = version3(input, "@@@");   //这里程序会崩溃,这块内存都没了,还返回啥
	cout << "Your string enhanced: " << result << endl;
	cout << "Your string original:" << input << endl;

	

	return 0;
}


string version1(const string& s1, const string& s2)  //有const时 ,"***"是char*类型,和引用类型不匹配时,程序能够创建一个正确类型的临时变量,使其能正确传参
{
	string temp;
	temp = s2 + s1 + s2;
	return temp;  //这里的temp s1 s2都是一个string类的对象 ,返回一个string类的对象
}

const string& version2(string& s1, const string& s2)
{
	s1 = s2 + s1 + s2; //这里string类对象s1是可以修改的
	return s1;  //返回一个不可修改的string类的对象s1的引用 
}

const string& version3(string& s1, const string& s2)
{
	string temp;
	temp = s2 + s1 + s2;
	return temp;  //!!!注意这里返回了一个局部变量的引用,函数运行结束之后,temp这块内存就没有了,所以不能发这么写。

}

2. 在函数里,通过基类的引用,来指向派生类的对象,而无需强制类型转换

//filefunc.cpp -- function with ostream & parameter
/*
  这个程序要求用户输入望远镜的物镜和目镜的焦距,然后计算每个目镜的放大倍数,放大倍数等于物镜的焦距除以目镜的焦距
*/



#include   //和控制台有关
#include    //和文件有关
#include 
using namespace std;

//在函数里,通过基类的引用,来指向派生类的对象,而无需强制类型转换

void file_it(ostream& os, double fo, const double fe[], int n);
//定义的这个基类的对象的引用ostream& os 
//由于ostream是基类,而ofstream是派生类,
// 1. 派生类继承了基类的方法,派生类可以使用基类的特性
// 2. 基类的引用也可以指向派生类的对象!!!!!!!!!!
//这里的意思就是基类引用形参ostream & os  ,这里传递进去一个派生类ofstream创立的对象fout作为函数参数也是可以的






const int LIMIT = 5;
int main()
{
    fstream fout;  //用fstream这个类定义一个对象fout
    const char* fn = "ep-data.txt";

    fout.open(fn); //用open方法打开文档
    if (!fout.is_open())
    {
        cout << "Can't open " << fn << ". Bye." << endl;
        exit(EXIT_FAILURE); //这个宏在#include 
    }

    double objective;
    cout << "Enter the focal length of your telescope objective in mm:"; //输入物镜焦距
    cin >> objective;

    double eps[LIMIT]; 
    for (int i = 0; i < LIMIT; i++)      //目镜有好几个呢
    {
        cout << "EyePieces #" << i + 1 << ": ";
        cin >> eps[i];
    }

   
    

    file_it(cout,objective,eps,LIMIT);  //cout是终端控制台的对象,终端输出显示
    file_it(fout, objective, eps, LIMIT);  //fout是文件的一个对象,文件输出显示 ,这里派生类对象fout也可以作为形参传入基类引用中

    cout << "Done." << endl;


    return 0;
}

void file_it(ostream& os, double fo, const double fe[], int n)  //ostream类对象 , 物镜焦距,多个目镜焦距的数组,目镜个数
{
   
    os << "Focal length of objective: " << fo << endl;  //输出物镜倍数
    os << "f.1. eyepieces" << " magnification" << endl; //输出目镜倍数
    for (int i = 0; i < n; i++)
    {
        os <<"  "<< fe[i] << " \t " << int(fo / fe[i] + 0.5) << endl;  //放大倍数取整

    }
   
}

3 引用的本质是什么? 引用,指针和值传递的辨析

/普通变量、指针和引用的汇编实现*/

/*
总结:C++中的变量名,不论是int a 的a,还是int* p 的p,其本质都是一个计算机分配的内存单元的标识符,用来指代某个特定的内存单元
变量名本是是一个数值,并不占用内控空间来存储这个数值,而是直接在汇编指令中编译为 [a] 或[1122H],用直接寻址的方式来代表某一块内存

int *p = &a; 指针则是一个实际的内存单元,这个内存单元中存放了[a]这个内存单元的地址值,也就是变量名a被翻译成汇编指令后对应的数值
指针变量名p也是一个数值,被汇编程序翻译成指针变量的内存单元的地址值,[p]这个单元就是指针变量
[p]直接寻址,可以找到指针变量,[p]单元中存放了[a]这个单元的地址,也就是变量名a的数值

指针变量初始化的过程:
int *p = &a;
lea      eax, [a]           
mov      dword ptr[p], eax
取[a]的单元的地址,经寄存器eax中转,放入[p]单元中

指针变量解引用,即用*p来代表a单元,
*p = 4;
mov      eax, dword ptr[p]    从指针变量的内存单元中获取内存单元a(变量)的地址
mov      dword ptr[eax], 4    寄存器间接寻址在找到[a]这个内存单元,进行赋值

变量赋值
a = 5;
mov      dword ptr[a]	a直接就代表一块特定的内存单元的地址,这里用的是直接寻址方式

引用
int &q = a;
lea         eax, [a]
mov         dword ptr[q], eax

指针常量
int* const r = &a;
lea         eax, [a]
mov        dword ptr[r], eax

可以见到,,引用的本质在汇编层面就是一个指针常量,他也是一个指针,只不过是一个只能指向[a]内存单元的指针,他的指向不可以改变
所以可以避免指针指向其他位置造成混乱,[q] 和[a]是一个东西,q = a都是那个内存单元的地址的数值,两者是相等的
引用就是一个功能简化后的指针,用起来更方便。

或者直接当成变量的别名即可,一码事。

int* p = &a;        // 指针初始化
// lea         eax, [a]              &a *** 作就是将a的地址放入寄存器中
 // mov         dword ptr[p], eax     在将这个地址放到指针变量的内存单元中
int& q = a;   //引用初始化
// lea         eax, [a]
 // mov         dword ptr[q], eax
	
int* s;  //这个指针变量的声明没有赋值,所以此时海内有开辟对应的指针变量的内存空间,没有相应的汇编指令
s = &a;
// lea         eax, [a]
 // mov         dword ptr[s], eax


*p = 4;                              //给指针指向的内存单元赋值
// mov         eax, dword ptr[p]    *p解引用: 将指针变量的内存单元存放的内容(即变量a的地址)送到寄存器中,解引用就是获取指针变量中存放的地址
// mov         dword ptr[eax], 4    再采用寄存器间接寻址的方式将4放入变量a的内存单元中,先*p,再赋值,将4送到这个指针指向的内存单元中区
a = 5;
// mov         dword ptr[a], 5
a = b;
// mov         eax, dword ptr[b]
// mov         dword ptr[a], eax

p = &b;                    //给指针改变指向
// lea         eax, [b]
// mov         dword ptr[p], eax   

int t = b; //常规变量赋值
// mov         eax, dword ptr[b]
// mov         dword ptr[t], eax

	
int* const r = &a;     //指针常量初始化
// lea         eax, [a]
// mov        dword ptr[r], eax

q = 6;
// mov         eax, dword ptr[q]
 // mov         dword ptr[eax], 6

4 什么时候使用引用参数?

引用:

  1. 形参可以修改实参
  2. 本质是个常量指针,直接指向参数内存,提高程序运行速度,节省内存空间。

对于传递的值不修改的函数

  • 数据将对象
  • 很小(内置类型或小型结构)———— 值传递
  • 数组 ———— 使用const指针
  • 数据对象较大 ———— const指针或const引用
  • 类对象 ———— const引用 ,传递类对象的标准方式是按引用传递

修改传递的值的函数

  • 内置数据类型 ———— 指针
  • 数组 ———— 只能指针
  • 结构 ———— 引用或指针
  • 类对象 ———— 引用

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/673376.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-19
下一篇 2022-04-19

发表评论

登录后才能评论

评论列表(0条)

保存