C++ 指针、引用

个人学习笔记

引用、指针的区别

符号

引用:&

指针:->

赋值:

改变原对象的值:指针、引用都会同步改变

改变指针:会指向新值,原对象不会改变

改变引用:原对象也会同步改变

左值、右值

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++中,引用是现有对象的别名。定义引用后,对引用的任何操作都将应用于被引用的对象。

关键点

引用本质上与被引用的对象相同。

左值引用不能绑定到不可修改的左值或右值

在大多数情况下,引用的类型必须与被引用的类型匹配

提示

&符号往往会引起混淆,因为它根据上下文具有不同的含义:

  1. 当跟在类型名称后面时,& 表示左值引用:int& ref。
  2. 在表达式中的一元上下文中使用时,& 是取地址操作:std::cout « &x。
  3. 在表达式的二进制上下文中使用时,& 是逐位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;
}

参考资料:

左值引用 – C++学习网

const的左值引用 – C++学习网

通过左值引用传递函数参数

值复制传递成本很高

关键点

通过引用,我们可以将原始对象的别名传递给函数,而无需在每次调用函数时复制它。

通过传递非常量引用作为参数,允许我们在函数中修改原始对象的值。

#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;
}

参考资料:

通过左值引用传递函数参数 – C++学习网

混合传入参数

#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

参考资料:

指针简介 – C++学习网

指针->

指针是保存内存地址(通常是另一个变量的地址)作为其值的对象。这允许我们存储其他对象的地址以供以后使用。

与使用与号(&)字符声明引用类型很相似,指针类型使用星号(*)声明:

结论

指针是保存内存地址的变量。可以使用解引用操作符(*)来解引用它们,以检索它们所持有的地址处的值。解引用野指针或悬空指针将导致未定义的行为,并可能导致应用程序崩溃。

指针比引用更灵活,也更危险。

警告

无论何时使用指针,都需要格外小心,不要让代码解引用空指针或悬空指针,因为这将导致未定义的行为(可能是应用程序崩溃)。

指针:理解为原对象的地址,通过指针获取原对象的值:

        1、改变原对象,指针也会同步改变

        2、改变指针指向对象,原对象不会改变

关键点

当使用没有解引用(ptr)的指针时,访问指针持有的地址。修改它(ptr=&y)将更改指针所指向的地址。

当解引用指针(*ptr)时,访问所指向的对象。修改它(*ptr=6;)将更改所指向对象的值。

我们可以以两种不同的赋值方式来使用指针:

  1. 改变指针所指向的对象(为指针赋值一个新的所指向的地址)
  2. 改变指针指向对象的值(通过解引用操作,为所指对象赋值)

最佳实践

声明指针类型时,请将星号放在类型名称旁边。

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;
}

指针和引用

指针和引用之间还有一些其他差异值得一提:

  1. 引用必须初始化,指针不需要初始化。
  2. 引用不是一个单独的对象,指针是。
  3. 无法重新设置引用(更改为引用其他内容),指针可以更改它们所指向的内容。
  4. 引用必须始终绑定到对象,指针可以指向空。
  5. 引用是“安全的”(除了悬空引用之外),指针本身就是危险的。

#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

参考文档

复合数据类型简介 – C++学习网

指针简介 – C++学习网

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值