小结与测验:复合数据类型、引用与指针

快速回顾

复合数据类型(也称为组合数据类型)是可以从基本数据类型(或其他复合数据类型)构建的数据类型。

表达式的值类别表明该表达式是否解析为值、函数或某种对象。

**左值(lvalue)**是评估为具有身份的函数或对象的表达式。具有身份的对象或函数具有标识符或可识别的内存地址。左值分为两种子类型:可修改的左值是可以修改的左值,不可修改的左值是不能修改的左值(通常因为它们是 constconstexpr)。

**右值(rvalue)**不是左值的表达式。这包括字面量(除字符串字面量外)以及函数或运算符的返回值(按值返回时)。

引用是现有对象的别名。一旦定义了引用,对该引用的任何操作都应用于被引用的对象。C++ 包含两种引用:左值引用和右值引用。左值引用(通常简称为引用)是现有左值(如变量)的别名。左值引用变量是作为左值引用的变量(通常是另一个变量)。

当引用用对象(或函数)初始化时,我们说它绑定到该对象(或函数)。被引用的对象(或函数)有时称为被引用对象。

左值引用不能绑定到不可修改的左值或右值(否则你将能够通过引用修改这些值,这将违反它们的常量性)。因此,左值引用有时也被称为非 const 左值引用(有时简称为非 const 引用)。

一旦初始化,C++ 中的引用不能重新绑定,即不能改变为引用另一个对象。

如果被引用的对象在引用之前被销毁,引用将引用一个不存在的对象。这样的引用称为悬垂引用。访问悬垂引用会导致未定义行为。

通过在声明左值引用时使用 const 关键字,我们告诉左值引用将其引用的对象视为 const。这样的引用称为对 const 值的左值引用(有时称为对 const 的引用或 const 引用)。const 引用可以绑定到可修改的左值、不可修改的左值和右值。

临时对象(也称为无名对象或匿名对象)是为临时使用而创建(然后销毁)的对象,仅在单个表达式内有效。

当使用引用传递时,我们将函数参数声明为引用(或 const 引用),而不是普通变量。调用函数时,每个引用参数都绑定到相应的实参。由于引用是实参的别名,因此不会创建实参的副本。

取地址运算符(&)返回其操作数的内存地址。解引用运算符(*)返回给定内存地址处的值作为左值。

指针是一个保存内存地址(通常是另一个变量的地址)的对象。这使我们能够存储某个对象的地址以便后续使用。与普通变量一样,指针默认未初始化。未初始化的指针有时称为野指针。悬垂指针是指保存无效对象(例如,已被销毁的对象)地址的指针。

除了内存地址外,指针还可以保存一个额外的值:空值。空值(通常简称为 null)是一个特殊的值,表示没有值。当指针保存空值时,意味着指针不指向任何内容。这样的指针称为空指针nullptr 关键字表示空指针字面量。我们可以使用 nullptr 显式初始化或分配指针为空值。

指针应该保存有效对象的地址,或者设置为 nullptr。这样我们只需要测试指针是否为空,就可以假设任何非空指针都是有效的。

指向 const 值的指针(有时简称为指向 const 的指针)是一个指向常量值的非 const 指针。

const 指针是一个初始化后地址不能改变的指针。

指向 const 值的 const 指针不能改变其地址,也不能通过指针改变其指向的值。

通过地址传递时,调用者不是提供一个对象作为参数,而是提供对象的地址(通过指针)。该指针(保存对象的地址)被复制到被调用函数的指针参数中(该指针现在也保存对象的地址)。然后函数可以通过解引用该指针来访问传递地址的对象。

通过引用返回会返回一个绑定到被返回对象的引用,从而避免复制返回值。使用通过引用返回有一个主要的注意事项:程序员必须确保被引用的对象在返回引用的函数之后仍然存在。否则,返回的引用将变成悬垂引用(引用了一个已经被销毁的对象),使用这个引用将导致未定义行为。如果参数是通过引用传递到函数中的,那么通过引用返回该参数是安全的。

如果函数返回一个引用,并且该引用用于初始化或分配给非引用变量,则返回值将被复制(就好像它是按值返回的一样)。

通过 auto 关键字进行变量的类型推导会丢弃推导出的类型中的任何引用或顶级 const 限定符。如果需要,可以在变量声明中重新应用这些限定符。

通过地址返回几乎与通过引用返回完全相同,只是返回的是指向对象的指针,而不是对象的引用。

测验时间

问题 1

对于以下 << 运算符右侧的每个表达式,指出该表达式是左值还是右值:

a) std::cout << 5;
答:右值

b)

int x{ 5 };
std::cout << x;

答:左值

c)

int x{ 5 };
std::cout << x + 1;

答:右值

d)

int foo() { return 5; }
std::cout << foo();

答:右值

e)

int& max(int &x, int &y) { return x > y ? x : y; }
int x{ 5 };
int y{ 6 };
std::cout << max(x, y);

答:左值

问题 2

这个程序的输出是什么?

#include <iostream>

int main()
{
    int x{ 4 };
    int y{ 6 };

    int& ref{ x };
    ++ref;
    ref = y;
    ++ref;

    std::cout << x << ' ' << y;

    return 0;
}

答:6 6

问题 3

列举两个原因,说明为什么我们尽可能优先使用通过 const 引用传递参数,而不是通过非 const 引用传递参数。

答:

  1. 通过 const 引用传递可以避免修改实参的值。
  2. const 引用可以绑定到右值,而非常量引用则不能。

问题 4

const 指针和指向 const 的指针有什么区别?

答:
const 指针是一个初始化后地址不能改变的指针。
指向 const 的指针是一个指向常量值的非 const 指针。

问题 5

编写一个名为 sort2 的函数,允许调用者将两个 int 变量作为参数传递。当函数返回时,第一个参数应保存两个值中较小的那个,第二个参数应保存较大的那个。

提示:可以使用 <algorithm> 头文件中的 std::swap() 函数来交换两个变量的值。例如,std::swap(x, y) 会交换变量 xy 的值。

以下代码应能运行并打印注释中指出的值:

#include <iostream>

int main()
{
    int x{ 7 };
    int y{ 5 };

    std::cout << x << ' ' << y << '\n'; // 应该打印 7 5

    sort2(x, y); // 确保当需要交换值时排序工作正常
    std::cout << x << ' ' << y << '\n'; // 应该打印 5 7

    sort2(x, y); // 确保当不需要交换值时排序工作正常
    std::cout << x << ' ' << y << '\n'; // 应该打印 5 7

    return 0;
}

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"