c++引用相关
左值
具名内存 —>也就是能够取该值取对象地址
分类
非常左值
无 const 修饰
常左值
有 const 修饰
举例
int a = 10; // a 为非常左值
&a; //语法正确
a = 20;// 语法正确
const int b = 10; // b 为常左值
&b; // 语法正确
b = 20; // 语法错误,因为被 const 修饰,无法修改 b 的值
生命周期
当前作用域的生命周期
右值
匿名内存—>不能对该对象取地址
举例
# 常量
10;
&10;//err,编译器会报错:lvalue required as unary '&' operand(需要左值作为一元 '&' 的操作数)
# 函数返回值
int foo() {}
int main() {
foo(); // 执行流程,1.会在 mian 的函数栈中分配一块匿名内存,用于接收该函数的返回值。2.生成跳转指令
&foo(); // 对函数调用取地址,仍然报错:err,编译器会报错:lvalue required as unary '&' operand(需要左值作为一元 '&' 的操作数)
10 = 20; // 编译器报错:lvalue required as left operand of assignment 10 = 20(赋值操作的左操作数必须是左值))
// 类似的,函数调用的值也不能修改
// foo() = 20; //error
}
生命周期
语句级别的生命周期。(直白点就是分号结束)
备注
原因是98/03标准标注更改右值毫无意义,所以编译器对于更改右值的操作会报错。
引用
- 引用即内存的别名
# 示例
int a = 10;
int& b = a; // b 为 a 的引用
- 引用本身不占用内存,并非实体,对引用的所有操作都是对目标内存进行操作
#include <iostream>
using namespace std;
int main() {
int a = 10;
int& b = a; // 应该理解为 b是 a所代表的内存的别名
b = 20; // 实际修改的是 a 的内存的值
cout << "a = " << a << endl;
cout << "b = " << b << endl; // 读取的是引用 b的目标内存的值,也就是 a 内存的值
// 如果此时对 a,b 同时进行取地址,会发现是同一个地址,佐证上面的内容,此处省略。
}
# 输出结果
# a = 20
# b = 20
- 引用必须初始化,且不能更换目标
int& c;// 未初始化引用,编译器会报错:'c' declared as reference but not initialized(c 作为引用但是没有被初始化)
- 不存在引用的引用
- 引用的常属性应该和目标的常属性保持“一致”
const int a = 10;
const int& b = a; // ok
int& c = a; // err
# 因为原值不能修改,所以也不能通过引用对原值进行修改
- 引用的限定可以比原值更加严格
int a = 10;
const int& b = a; // ok
- 引用可以延长右值的生命周期
如果一个右值被声明了引用,则会将右值的生命周期提高到引用的作用域
int& a = 10; //err,编译器会报错,invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int' (无效的初始化:不能将 int 类型的右值绑定到 int& 类型的非 const 引用)
const int& b = 10; //ok,ps. 10不让改是因为标准规定修改右值无意义,引用 b 不让修改是因为加了常属性 const,最终达到引用 b 和 10 都不能修改
// 同理,函数返回值也可以做类似操作
int& c = foo();//err
const int& d = foo(); //ok
- 常引用即万能引用
可以引用常左值,非常左值,右值
ps:如果常引用引用的是非常左值,那么通过常引用将失去对目标内存的修改权限
- 常指针即万能指针
可以指向常左值,非常左值,右值
ps:如果常指针指向的是非常左值,那么通过常指针将失去对目标内存的修改权限
- 引用的生命周期不能长于目标
应用
在 c 语言中的数据传递(初始化,赋值,传参,返回值。。。)都是值传递。(将数据复制一份)
example:
int a = 10;
a = 30;
foo(a);
int b = foo(a);
在 c++中可以进行引用传递
引用型参数
函数的形参是实参的别名,避免对象复制带来的开销。
非常引用型形参
函数内部可以修改实参
#include <iostream>
using namespace std;
// 如果调用此函数,则原数的值不会发生更改,因为是值传递
void swap(int x,y) {
int z = y;
y = x;
x = z;
}
// 调用此函数,原数的值会发生更改,但是注意,此时还是进行值传递,只不过传递的值是参数的地址,通过修改地址指向的值进行数据修改
void swap(int* x,y) {
int z = *y;
*y = *x;
*x = z;
}
// 调用此函数,原数的值会发生更改,此时是直接将引用传递到函数中进行数值修改
void swap(int& x,y) {
int z = x;
x = y;
y = z;
}
int main() {
int a = 10,b = 20;
// 引用修改
swap(a,b);
// 指针修改
swap(&a,&b);
}
常引用型实参
防止对实参的意外修改
void print1(int& x,int& y) {
// 此时可能发生对实参进行修改
}
void print2(const int& x,const int& y) { // 常引用型形参
x = 888;// 在函数形参前添加 const 修饰,可以避免函数内部对实参进行修改,如果意外发生修改操作,则编译器会报错:assignment of read-only referen 'x'(只读的引用赋值)
}
print1(444,555); // err
print2(444,555); // ok
引用型返回值
从函数中返回引用,一定要保证在函数返回以后,该引用的目标依然有效
返回全局,静态变量的引用
# include <iostream>
using namespace std;
int g_value = 0;
// 引用型返回值
int& foo() {
return g_value;
}
int& bar(){
static int s_value = 0;
cout << s_value <<endl;
return s_value;
}
int main() {
foo() = 100; // 此处不会生成匿名内存来保存返回值,因为此处 foo()函数所代表的含义为 g_value 的引用,所以此处可以进行赋值操作。同时 g_value 的值也会被修改为 100.
bar() = 200; //同上,s_value 的值会被修改为 200。
bar(); // 因为 s_value 为静态局部变量,所以只会初始化一次,第二次调用可以通过输出语句查看 s_value 的值是否被修改(此处被修改)
return 0;
}
返回在堆中动态创建的对象的引用
int& hum() {
int* pn = new int; // 在堆中申请 4 个字节的堆内存
return *pn;
}
int main() {
hum(); // 此处返回的类型为*pn 的引用
}
返回引用型参数本身
int& fun(int& x) {
return x; // 返回引用型参数本身
}
int main() {
int a = 10;
fun(a) = 200;// 返回值还是代表了 a 的引用,也就是说 a 的值还是被修改为了200
}
不能返回局部变量的引用
int& boo() {
int m = 100;
return m;
}
int main() {
boo(); // 此处 boo 函数调用结束跳转回来后,m 对象已经被销毁,此时引用指向的目标无效,编译器会报警告(严重!)
}
非常引用型返回值
通过引用可以修改目标
常引用型返回值
不能通过引用修改目标
指针引用对比
在实现层面(汇编),引用就是指针,但是在 c++语言层面,引用不是实体类型
- 指针可以不做初始化,但是引用必须初始化
- 指针的目标可以在初始化后随意变更(除非是指针常量),但是引用一旦初始化后就无法变更
- 存在空指针,但是不存在空引用
- 存在二级指针(指针的指针),但是不存在二级引用(引用的引用)
- 存在指针的引用,但是不存在引用的指针
- 存在指针数组,但是不存在引用数组,存在数组引用