【引用】——串讲(视频89-94)
Note:
i.视频为黑马程序员C++视频,系列文章为视频听课笔记;
ii.引用不仅包含定义及简单应用,在类与对象…中也有涉及;
iii.难度指数:+++
iv.不论变量、函数名、标识符形式怎样复杂,只要我们考虑编程的本质是对内存的操作,对内存进行分析,一切逻辑都会变得清晰。
1.引用的基本语法
1)含义:给变量起别名
2)code格式:数据类型 &别名 = 变量名
例如:int &b = a
,则b为变量a的引用,也是对引用b的初始化
3)内存图解:
由图可见,若定义b为变量a的引用,则b可对变量a中存储的值进行操作。
2.引用的注意事项
1)引用定义则必须进行初始化
2)引用在第一次初始化后不可更改
3.引用做函数参数(重点)
之前提到某封装函数被调用,传参时的传递方式分为按值传递或按地址传递,前者形参的改变不能改变实参的值,后者形参的改变可以改变实参的值;或者说前者形参不能修饰实参,后者形参可以修饰实参。
实际上,按地址传递实际上是传递的是指针,其记录实参的地址,因此可以实现对实参的修饰。引入“引用”的概念之后,可利用其作为函数参数实现对实参的修饰。
至此,函数被调用时,参数的传递方式有以下几种:
1)值传递
2)地址传递
3)引用传递
举例:以完成实参值的交换为例
1)值传递
#include<iostream>
#include<string>
using namespace std;
//定义值传递交换函数
void swap01(int a,int b)
{
//值传递交换函数无法实现实参的交换
int temp = a;
a = b;
tmep = b;
cout << "值传递形参a=" << a << endl;//a=20
cout << "值传递形参b=" << b << endl;//b=10
}
int main()
{
int a = 10;
int b = 20;
swap01(a, b);
cout << "值传递交换后实参a=" << a << endl;//a=10
cout << "值传递交换后实参b=" << b << endl;//b=20
}
依靠值传递交换函数,无法实现实参的交换。
2)地址传递
#include<iostream>
#include<string>
using namespace std;
//定义地址传递交换函数
void swap02(int *a,int *b)
{
//地址传递交换函数可以实现实参的交换
int temp = *a;
*a = *b;
tmep = *b;
cout << "地址传递形参a=" << a << endl;//a=20
cout << "地址传递形参b=" << b << endl;//b=10
}
int main()
{
int a = 10;
int b = 20;
swap02(&a, &b);
cout << "地址传递交换后实参a=" << a << endl;//a=20
cout << "地址传递交换后实参b=" << b << endl;//b=10
}
依赖地址传递交换函数,可以实现实参的交换。
3)引用传递
#include<iostream>
#include<string>
using namespace std;
//定义引用传递交换函数
void swap03(int &c,int &d)//定义形参c为a的引用,d为b的引用
{
//引用传递交换函数可以实现实参的交换
int temp = c;
c = d;
tmep = d;
cout << "引用传递形参a=" << c << endl;//a=20
cout << "引用传递形参b=" << d<< endl;//b=10
}
int main()
{
int a = 10;
int b = 20;
swap03(a, b);//注意这里的实参,不需要加任何东西符号
cout << "引用传递交换后实参a=" << a << endl;//a=20
cout << "引用传递交换后实参b=" << b << endl;//b=10
}
依赖引用传递交换函数,可以实现实参的交换,形参作了实参的引用。
【总结】:
1)封装函数在没有返回值的情况下,要想实现对main函数中实参的操作,必须在封装函数中对其内存进行操作。
2)引用类似于指针的简化版本,它不像指针一样类似于一种数据类型,需要定义,有固定的含义,即存储的是内存地址。它更像是变量的分身,代替变量完成不能完成的对内存的操作。
4.引用做函数返回值(重点、难点)
Note:
1)不能返回局部变量的引用,但可以返回静态变量的引用。
【碎碎念】:总之,所有的注意事项都跟内存息息相关。
代码示例:
#include<iostream>
#include<string>
using namespace std;
int& test01()//注意这里定义的函数返回值类型为引用
{
int a = 10;//a为局部变量,存储在栈区,函数调用完之后,变量内存就被释放。
return a;
}
int& test02()
{
static int a = 100;//a为静态变量,存储在全局区,程序全部执行完毕后释放
return a;
}
int main()
{
//创建局部变量引用
int &ref = test01();//ref为局部变量a的引用,但是由于a在函数执行完后即被释放,因此ref并没有可以操纵的内存
cout << "作局部变量引用ref的值=" << ref << endl;//10,之所以输出10,是由于编译器对a保留一次的操作
cout << "作局部变量引用ref的值=" << ref << endl;//乱码
//创建静态变量引用
int &ref2 = test02();
cout << "作静态变量引用ref的值="<< ref2 << endl;//100
cout << "作静态变量引用ref的值="<< ref2 << endl;//100
}
2)函数的调用可以作为左值
代码示例:
#include<iostream>
#include<string>
using namespace std;
int& test02()
{
static int a = 100;//a为静态变量,存储在全局区,程序全部执行完毕后释放
return a;
}
int main()
{
//创建静态变量引用
int &ref2 = test02();
cout << "ref2="<< ref2 << endl;//100
cout << "ref2="<< ref2 << endl;//100
//函数的调用作为左值
test02() = 1000;//test02()可以看成a的引用
cout << "ref2="<< ref2 << endl;//1000
cout << "ref2="< ref2 << endl;//1000
}
从上面的例子中可以看到,函数test02()作为左值可以实现对静态变量的赋值操作。
5.引用的本质
1)定义:引用的本质是一个指针常量。
涉及到指针常量:指针的指向不可更改,但指向的内存中的值可更改。
易混淆的常量指针:指针的指向可以更改,但指向的值不可更改。
2)前面在引用的注意事项中提到,引用一旦初始化就不能更改,这也是跟引用的本质有关系,引用初始化时就确定了其指向,而其指向是不可更改的。
3)之所以引用的定义形式等跟指针不同,是因为在C++中,编译器对引用进行了包装,代码示例如下:
#include<iostream>
#include<string>
using namespace std;
void func(int &ref)//等价于int * const ref = &a
{
//等价于*ref = 20
ref = 100;
}
int main()
{
int a = 10;
int &ref = a;//等价于int * const ref = &a
ref = 20;//ref为引用,内部编译器转换为*ref = 20
cout << "ref:" << ref << endl;//20
cout << "a:" << a << endl;//20
func(a);
cout << "ref:" << ref << endl;//100
cout << "a:" << a << endl;//100
return 0;
}
6.引用在类与对象中的应用(拓展、难点)
1)背景:创建类person
,已知其中包含age
属性以及personaddperson
成员函数,该成员函数可以实现两个实例对象age
属性的求和。
2)要求:创建实例对象p1
、p2
,实现p2.age
与若干个p1.age
的连续相加。
3.1)示例介绍:
如代码示例一所示,成员函数
personaddperson
的函数类型为person
,从函数的输出结果中可以看到,每次调用成员函数时,this指针都指向不同的实例对象(从输出的地址可以看出),因此,代码示例一不能实现p2.age
与若干个p1.age
的连续相加,仅且只能实现一次相加,下一次调用时,this指针即指向由成员函数创建的新的实例对象(非p2
)。
//代码示例一
class person
{
public:
//有参构造函数
person(int age)
{
this->age = age;
}
person personaddperson(person &p)//返回为“类”
{
this->age += p.age;
cout << "this指针" << this << endl;//输出每次成员函数调用时,this指向的对象地址
cout << "this指向的年龄为:" << this->age << endl;//输出每次成员函数调用时,this指向对象的年龄
return *this;
}
int age;
};
void test01()
{
person p1(10);
person p2(10);
p2.personaddperson(p1).personaddperson(p1).personaddperson(p1);
cout << "p2对象年龄为:" << p2.age << endl;
}
int main()
{
test01();
}
当成员函数返回为类,输出结果为:
3.2)示例介绍
如代码示例二所示,成员函数
personaddperson
的函数类型为person&
,从函数的输出结果中可以看到,每次调用成员函数时,this指针均指向实例对象p2
(从输出的地址可以看出)。这可以理解为:当利用类的引用作为函数返回类型时,所有创建的示例对象都是p2
的别名,因此实现了对p2
内存的连续操作。因此,示例二可以实现p2.age
与若干个p1.age
的连续相加。return *this
实际上返回的就是this指针指向的实例对象。
//代码示例2
person& personaddperson(person &p)//返回为“类”
{
this->age += p.age;
cout << "this指针" << this << endl;//输出每次成员函数调用时,this指向的对象地址
cout << "this指向的年龄为:" << this->age << endl;//输出每次成员函数调用时,this指向对象的年龄
return *this;
}
当成员函数返回为“类”的引用(代码示例如上),输出结果为:
【一点思考】:
1)在刚开始学习引用时,把引用单纯的勉强理解为变量的别名,当接触了引用在“类与对象”中的应用后,发现,引用可以是任何事物的别名,可以是类的别名,好像也可以是函数的别名等等。在本文6)的案例中,其作为p2实例对象的别名实现了对p2中age属性的连续操作。
2)之前我将指针可以看作一种数据类型,记为int/char... *
,即可套用其他数据类型定义的模板。
同理,引用也可以看作一种数据类型,记为int/char/person &
,这样可以帮助我们更好的理解代码。