个人学习笔记
引用、指针的区别
符号
引用:&
指针:->
赋值:
改变原对象的值:指针、引用都会同步改变
改变指针:会指向新值,原对象不会改变
改变引用:原对象也会同步改变
左值、右值
C++中,表达式语句可以分为左值与右值。
左值、右值是指花括号内的值{} : int x { 5 };
左值:
1、是内存中持续储存的数据,表达式执行完毕后会持久存在;
2、左值表达式计算为可识别的对象。
3、左值表达式是那些求值为变量或其他可识别对象的表达式,这些变量或对象持续存在于表达式末尾之后。
右值:
1、是临时储存的计算结果,表达式执行完毕后就不再存在。
2、右值表达式计算为一个值。
3、右值表达式计算为函数/运算符返回的值,并且在表达式结束后丢弃。
左值
int main()
{
int x { 5 };
int y { x }; // x 是左值表达式
int a{};
const double d{};
int b { a }; // a 是可修改的左值表达式
const double e { d }; // d 是不可修改的左值表达式
return 0;
}
右值
int return5()
{
return 5;
}
int main()
{
int x{ 5 }; // 5 是右值表达式
const double d{ 1.2 }; // 1.2 是右值表达式
int y { x }; // x 是可修改的左值表达式
const double e { d }; // d 是不可修改的左值表达式
int z { return5() }; // return5() 是右值表达式 expression (因为它的结果是按值返回)
int w { x + 1 }; // x + 1 是右值表达式
int q { static_cast<int>(d) }; // static_cast的结果是右值表达式
return 0;
}
提示
如果您不确定表达式是左值还是右值,请尝试使用运算符&获取其地址,这要求其操作数是左值。如果 “&表达式;“编译通过,表达式一定是左值:
int foo()
{
return 5;
}
int main()
{
int x { 5 };
&x; // 可以编译: x 是左值
&5; // 无法编译: 5 是右值
&foo(); // 无法编译: foo() 是右值表达式
}
左值到右值的转换
int main()
{
int x { 5 };
int y { x }; // x 是左值表达式
return 0;
}
参考资料
值类别(左值 lvalue 和右值 rvalue) – C++学习网
引用&
左值引用
对原对象生成一个别名
改变原对象:引用也会改变
改变引用:原对象也会改变
在C++中,引用是现有对象的别名。定义引用后,对引用的任何操作都将应用于被引用的对象。
关键点
引用本质上与被引用的对象相同。
左值引用不能绑定到不可修改的左值或右值
在大多数情况下,引用的类型必须与被引用的类型匹配
提示
&符号往往会引起混淆,因为它根据上下文具有不同的含义:
- 当跟在类型名称后面时,& 表示左值引用:int& ref。
- 在表达式中的一元上下文中使用时,& 是取地址操作:std::cout « &x。
- 在表达式的二进制上下文中使用时,& 是逐位AND运算符:std::cout « x & y。
int // 普通的int类型
int& // int类型对象的左值引用
double& // double类型对象的左值引用
案例
#include <iostream>
int main()
{
int x{ 5 }; // x 是普通的int类型变量
int& ref{ x }; // ref 是int类型的左值引用变量,充当变量x的别名
std::cout << x << '\n'; // 打印 x (5)
std::cout << ref << '\n'; // 打印 x 通过 ref (5)
ref = 3;// 修改引用,x的值也一起改变
std::cout << x << '\n'; // 打印 x (5)
std::cout << ref << '\n'; // 打印 x 通过 ref (5)
return 0;
}
左值修改引用
#include <iostream>
int main()
{
int x { 5 }; // 普通int变量
int& ref { x }; // ref 现在是 x 的别名
std::cout << x << ref << '\n'; // 打印 55
x = 6; // x 现在值是 6
std::cout << x << ref << '\n'; // 打印 66
ref = 7; // 对引用的对象(x)现在值是 7
std::cout << x << ref << '\n'; // 打印 77
return 0;
}
左值引用必须绑定到可修改的左值。
int main()
{
int x { 5 };
int& ref { x }; // 有效: 左值引用绑定到可修改的左值
const int y { 5 };
int& invalidRef { y }; // 无效: 不能绑定到不可修改的左值
int& invalidRef2 { 0 }; // 无效: 不能绑定到右值
return 0;
}
左值引用的作用域
#include <iostream>
int main()
{
int x { 5 }; // 普通变量
int& ref { x }; // 变量x的引用
return 0;
} // x 和 ref 在这里失效
const的左值引用
int main()
{
const int x { 5 }; // x 是不可修改的左值
const int& ref { x }; // okay: ref 可以引用到x
return 0;
}
const的右指引用
#include <iostream>
int main()
{
const int& ref { 5 }; // okay: 5 是一个 右值
std::cout << ref << '\n'; // 打印 5
return 0;
}
参考资料:
通过左值引用传递函数参数
值复制传递成本很高
关键点
通过引用,我们可以将原始对象的别名传递给函数,而无需在每次调用函数时复制它。
通过传递非常量引用作为参数,允许我们在函数中修改原始对象的值。
#include <iostream>
void printValue(int y)
{
std::cout << y << '\n';
} // y 在这里被销毁
int main()
{
int x { 2 };
printValue(x); // x 按值拷贝到 y (如果x是比较大的对象,会非常消耗资源)
return 0;
}
#include <iostream>
#include <string>
void printValue(std::string y)
{
std::cout << y << '\n';
} // y 在这里被销毁
int main()
{
std::string x { "Hello, world!" }; // x 是 std::string
printValue(x); // x 按值拷贝到 y (复制成本高)
return 0;
}
用指针传递参数
#include <iostream>
void addOne(int& y) // y 被绑定到原始的 x
{
++y; // 修改的实际是 x
}
int main()
{
int x { 5 };
std::cout << "value = " << x << '\n';
addOne(x);
std::cout << "value = " << x << '\n'; // x 被修改了
return 0;
}
按引用传递参数只能接受可修改的左值
#include <iostream>
void printValue(int& y) // y 只能接受可修改的左值
{
std::cout << y << '\n';
}
int main()
{
int x { 5 };
printValue(x); // ok: x 是可修改的左值
const int z { 5 };
printValue(z); // error: z 是不可修改的左值
printValue(5); // error: 5 是右值
return 0;
}
参考资料:
混合传入参数
#include <string>
void foo(int a, int& b, const std::string& c)
{
}
int main()
{
int x { 5 };
const std::string s { "Hello, world!" };
foo(5, x, s);
return 0;
}
取地址操作(&)
考虑一个普通的变量,如下所示:
char x {}; // char 使用1字节内存
简单来说,当执行该定义生成的代码时,会将一块内存分配给该对象。假设变量x被分配了内存地址140。每当我们在表达式或语句中使用变量x时,程序将访问存储在内存地址140的值。
关于变量的好处是,不需要担心分配了什么特定的内存地址,或者需要多少字节来存储对象的值。只需要通过给定的标识符来使用变量,编译器将该名称转换为适当分配的内存地址。编译器负责寻址工作。
#include <iostream>
int main()
{
int x{ 5 };
std::cout << x << '\n'; // 打印变量 x 的值
std::cout << &x << '\n'; // 打印变量 x 的地址
return 0;
}
打印
5
000000165B11F744
解引用运算符(*)
关键点
给定一个内存地址,我们可以使用解引用操作符(*)来获取该地址的值(作为左值)。
运算符(&)和解引用运算符(*)是相反的:& 获取对象的地址,而解引用获取地址处的对象。
提示
尽管解引用运算符看起来就像乘法运算符,但可以区分它们,因为解引用运算符是一元的,而乘法运算符是二元的。
#include <iostream>
int main()
{
int x{ 5 };
std::cout << x << '\n'; // 打印变量 x 的值
std::cout << &x << '\n'; // 打印变量 x 的地址
std::cout << *(&x) << '\n'; // 打印变量x的存储地址内的值 (括号不是必须的, 只是为了可读性)
return 0;
}
打印结果
5
000000290B6FFC84
5
参考资料:
指针->
指针是保存内存地址(通常是另一个变量的地址)作为其值的对象。这允许我们存储其他对象的地址以供以后使用。
与使用与号(&)字符声明引用类型很相似,指针类型使用星号(*)声明:
结论
指针是保存内存地址的变量。可以使用解引用操作符(*)来解引用它们,以检索它们所持有的地址处的值。解引用野指针或悬空指针将导致未定义的行为,并可能导致应用程序崩溃。
指针比引用更灵活,也更危险。
警告
无论何时使用指针,都需要格外小心,不要让代码解引用空指针或悬空指针,因为这将导致未定义的行为(可能是应用程序崩溃)。
指针:理解为原对象的地址,通过指针获取原对象的值:
1、改变原对象,指针也会同步改变
2、改变指针指向对象,原对象不会改变
关键点
当使用没有解引用(ptr)的指针时,访问指针持有的地址。修改它(ptr=&y)将更改指针所指向的地址。
当解引用指针(*ptr)时,访问所指向的对象。修改它(*ptr=6;)将更改所指向对象的值。
我们可以以两种不同的赋值方式来使用指针:
- 改变指针所指向的对象(为指针赋值一个新的所指向的地址)
- 改变指针指向对象的值(通过解引用操作,为所指对象赋值)
最佳实践
声明指针类型时,请将星号放在类型名称旁边。
int; // 普通的 int
int&; // int的左值引用
int*; // 指向int的指针 (保存对应的内存地址)
警告
尽管通常不应在一行上声明多个变量,但如果您这样做,则每个变量都必须包含星号。
int* ptr1, ptr2; // 不正确: ptr1 是int指针, 但 ptr2 只是一个 int!
int* ptr3, * ptr4; // 正确: ptr3 和 ptr4 都是int指针
为了避免误用,有人习惯将星号与变量名放在一起,而不是类型名放在一起。 但是更好的做法是避免在同一语句中定义多个变量。
注
关于指针命名法的注释:“X指针”(X是某种类型)是“指向X的指针”的常用缩写。所以当我们说“整数指针”时,我们实际上是指“指向整数的指针”。当我们谈论常量指针时,这种理解将很有价值。
指针初始化
与普通变量一样,指针在默认情况下不会初始化。尚未初始化的指针有时称为野指针。野指针包含垃圾地址,解引用野指针将导致未定义的行为。因此,您应该始终初始化指针。
int main()
{
int x{ 5 };
int* ptr; // 未初始化的指针 (包含一个垃圾地址)
int* ptr2{}; // 空指针
int* ptr3{ &x }; // 指向变量 x 地址的指针
return 0;
}
由于指针保存地址,因此当我们初始化指针或将值赋给指针时,该值必须是地址。通常,指针用于保存另一个变量的地址(可以使用操作符(&)的地址来获得)。
一旦有一个指针保存另一个对象的地址,就可以使用解引用操作符(*)来访问该地址上的值。例如:
#include <iostream>
int main()
{
int x{ 5 };
std::cout << x << '\n'; // 打印变量 x 的值
int* ptr{ &x }; // ptr 保存 x 的地址
std::cout << *ptr << '\n'; // 使用解引用操作获取ptr保存的地址上的值 (ptr保存的是x的地址)
std::cout << ptr << '\n';
return 0;
}
5
5
00000022C677F514
指针和赋值
#include <iostream>
int main()
{
int x{ 5 };
int* ptr{ &x }; // ptr 初始化指向 x
std::cout << x << '\n'; // 打印 x 的值 5
std::cout << *ptr << '\n'; // 打印ptr指向的值 (x) 5
x = 8; // 改变x变量的值
std::cout << x << '\n'; // 打印 x 的值 8
std::cout << *ptr << '\n'; // 打印ptr指向的值 (x) 8
int y{ 9 };
ptr = &y;
std::cout << x << '\n'; // 打印 x 的值 8
std::cout <<y << '\n'; // 打印 y 的值 9
std::cout << *ptr << '\n'; // 打印ptr指向的值 (y) 9
*ptr = 6; // ptr 保存的地址对应的对象 (y) 设置为 6 (注意这里ptr被解引用)
std::cout << x << '\n'; // 打印 x 的值 8
std::cout << y << '\n'; // 打印 y 的值 6
std::cout << *ptr << '\n'; // 打印ptr指向的值 (x) 6
return 0;
}
指针和引用
指针和引用之间还有一些其他差异值得一提:
- 引用必须初始化,指针不需要初始化。
- 引用不是一个单独的对象,指针是。
- 无法重新设置引用(更改为引用其他内容),指针可以更改它们所指向的内容。
- 引用必须始终绑定到对象,指针可以指向空。
- 引用是“安全的”(除了悬空引用之外),指针本身就是危险的。
#include <iostream>
int main()
{
int x{ 5 };
int& ref { x }; // ref 是 x 的别名
int* ptr { &x }; // ptr 指向 x
std::cout << x;
std::cout << ref; // 使用引用打印 x 的值 (5)
std::cout << *ptr << '\n'; // 使用指针打印 x 的值 (5)
ref = 6; // 使用引用修改 x
std::cout << x;
std::cout << ref; // 使用引用打印 x 的值 (6)
std::cout << *ptr << '\n'; // 使用指针打印 x 的值 (6)
*ptr = 7; // 使用指针修改 x
std::cout << x;
std::cout << ref; // 使用引用打印 x 的值 (7)
std::cout << *ptr << '\n'; // 使用指针打印 x 的值 (7)
return 0;
}
悬空指针
关键点
解引用无效指针将导致未定义的行为。无效指针值的任何其他使用都是由实现定义的。
#include <iostream>
int main()
{
int x{ 5 };
int* ptr{ &x };
std::cout << *ptr << '\n'; // 有效
{
int y{ 6 };
ptr = &y;
std::cout << *ptr << '\n'; // 有效
} // y 被销毁, ptr现在是悬空指针
std::cout << *ptr << '\n'; // 解引用悬空指针,未定义的行为
return 0;
}
空指针
除了实际内存地址,指针还可以保存一个特定的值:空值。空值(通常缩写为null)是一个特殊的值,表示某物没有值。当指针持有空值时,这意味着指针没有指向任何东西。这样的指针称为空指针。
最佳实践
如果不是用有效对象的地址初始化指针,则将其置空。
创建空指针的最简单方法是使用值初始化:
int main()
{
int* ptr {}; // ptr 是一个空指针,不指定任何对象
return 0;
}
#include <iostream>
int main()
{
int* ptr {}; // ptr 是空指针
int x { 5 };
ptr = &x; // ptr 现在指向 x
std::cout << *ptr << '\n'; // 解引用ptr,打印x
return 0;
}
nullptr关键字
就像关键字true和false表示布尔字面值一样,nullptr关键字表示空指针字面值。可以使用nullptr显式地初始化指针或为指针分配null值。
int main()
{
int* ptr { nullptr }; // 使用nullptr,显示声明ptr是空指针
int value { 5 };
int* ptr2 { &value }; // ptr2 是有效的指针
ptr2 = nullptr; // 将 ptr2 设置为nullptr
someFunction(nullptr); // 可以将nullptr作为函数参数传递
return 0;
}
解引用空指针会导致未定义的行为
就像取消对悬挂(或野生)指针的引用导致未定义的行为一样,取消对空指针的引用也会导致未定义行为。在大多数情况下,它会使应用程序崩溃。
下面的程序说明了这一点,当您运行应用程序时,它可能会异常崩溃或终止(继续,尝试它,您不会伤害您的机器):
意外地解引用空指针和悬空指针是C++程序员最常见的错误之一,并且可能是C++程序在实践中崩溃的最常见原因。
#include <iostream>
int main()
{
int* ptr {}; // Create a null pointer
std::cout << *ptr << '\n'; // Dereference the null pointer
return 0;
}
检查空指针
我们注意到整数值会隐式转换为布尔值:整数值0转换为布尔值false,任何其他整数值转换为布尔值true。
警告
条件语句只能用于区分空指针和非空指针。没有方便的方法来确定非空指针是指向有效对象还是悬空(指向无效对象)。
类似地,指针也将隐式转换为布尔值:空指针转换为布尔值false,非空指针转换成布尔值true。这允许我们跳过对nullptr的显式测试,可以只使用bool值的隐式转换来检查是否为空指针。以下程序等价于前一个程序:
#include <iostream>
int main()
{
int x { 5 };
int* ptr { &x };
if (ptr == nullptr) // 显式检查
std::cout << "ptr is null\n";
else
std::cout << "ptr is non-null\n";
int* nullPtr {};
std::cout << "nullPtr is " << (nullPtr==nullptr ? "null\n" : "non-null\n"); // 显式检查
return 0;
}
打印:
ptr is non-null
nullPtr is null
参考文档