通过地址传参(第二部分)

本课是《通过地址传参》的延续。

通过地址传递“可选”参数

地址传参的常见用途之一是允许函数接收“可选”参数。通过一个例子比通过文字描述更直观:

#include <iostream>

void printIDNumber(const int *id = nullptr)  // 默认为 nullptr
{
    if (id)
        std::cout << "Your ID number is " << *id << ".\n";
    else
        std::cout << "Your ID number is not known.\n";
}

int main()
{
    printIDNumber(); // 传入 nullptr(无实参)

    int userid{ 34 };
    printIDNumber(&userid); // 传入用户 ID

    return 0;
}

输出:

Your ID number is not known.
Your ID number is 34.

printIDNumber() 的形参 id 是一个通过地址传递的参数,默认值为 nullptr。在 main() 函数中,我们两次调用该函数。

  • 第一次调用时,我们不知道用户的 ID,因此不传递任何实参,id 参数默认为 nullptr,函数输出“Your ID number is not known.”。
  • 第二次调用时,我们已知用户的 ID,因此传入 &userid,函数输出“Your ID number is 34.”。

虽然这看起来是一个可行的解决方案,但在许多情况下,函数重载是实现相同效果的更好选择:

#include <iostream>

void printIDNumber()  // 重载版本 1:无参数
{
    std::cout << "Your ID is not known\n";
}

void printIDNumber(int id)  // 重载版本 2:接受 ID
{
    std::cout << "Your ID is " << id << "\n";
}

int main()
{
    printIDNumber(); // 无参数调用

    int userid{ 34 };
    printIDNumber(userid); // 传入用户 ID

    printIDNumber(62); // 也可传入字面量

    return 0;
}

通过函数重载,我们有以下优点:

  1. 不再需要担心空指针解引用的问题。
  2. 可以直接传入字面量或其他右值作为参数。

改变指针形参所指向的地址

当我们将一个地址传递给函数时,该地址会从参数复制到指针形参中(因为复制地址很快)。现在,考虑以下程序:

#include <iostream>

void nullify([[maybe_unused]] int* ptr2)  // [[maybe_unused]] 避免编译器警告
{
    ptr2 = nullptr; // 将函数形参置为 nullptr
}

int main()
{
    int x{ 5 };
    int* ptr{ &x }; // ptr 指向 x

    std::cout << "ptr is " << (ptr ? "non-null\n" : "null\n");

    nullify(ptr);

    std::cout << "ptr is " << (ptr ? "non-null\n" : "null\n");
    return 0;
}

输出:

ptr is non-null
ptr is non-null

正如您所见,改变指针形参所持有的地址对实参所持有的地址没有影响(ptr 仍然指向 x)。当函数 nullify() 被调用时,ptr2 接收了传入地址的副本(在此例中,是 ptr 所持有的地址,即 x 的地址)。当函数改变 ptr2 所指向的内容时,这只会影响 ptr2 所持有的副本。

如果要让函数改变指针参数所指向的内容,该如何操作?

通过引用传递地址
是的,这是可行的。正如我们可以按引用传递普通变量一样,我们也可以按引用传递指针。以下是上述程序的修改版本,将 ptr2 改为对地址的引用:

#include <iostream>

void nullify(int*& refptr)  // refptr 现在是对指针的引用
{
    refptr = nullptr; // 将函数形参置为 nullptr
}

int main()
{
    int x{ 5 };
    int* ptr{ &x }; // ptr 指向 x

    std::cout << "ptr is " << (ptr ? "non-null\n" : "null\n");

    nullify(ptr);

    std::cout << "ptr is " << (ptr ? "non-null\n" : "null\n");
    return 0;
}

输出:

ptr is non-null
ptr is null

由于 refptr 现在是对指针的引用,当 ptr 作为参数传递时,refptrptr 绑定在一起。这意味着对 refptr 的任何修改都会影响到 ptr

(注:由于指针的引用相对较少见,容易搞混语法。不过,如果你写反了(比如写成 int&* 而不是 int*&),编译器会报错,因为不能存在指向引用的指针。)

为什么不再推荐使用 0NULL(可选)

在这一小节中,我们将解释为何不再推荐使用 0NULL

整数字面量 0 可以被解释为整数字面量,也可以被解释为空指针字面量。在某些情况下,这可能导致歧义 —— 有时编译器可能假设我们想用一个,而实际上我们想用另一个,从而导致程序行为不符合预期。

预处理器宏 NULL 的定义在语言标准中没有明确规定。它可以被定义为 00L((void*)0) 或其他内容。

在 11.1 课《函数重载简介》中,我们提到函数可以重载(多个函数可以有相同的名称,只要它们可以通过参数的数量或类型区分)。编译器可以根据函数调用中传递的参数来确定您想要调用哪一个重载函数。

当使用 0NULL 时,这可能会导致问题:

#include <iostream>
#include <cstddef> // 用于 NULL

void print(int x)  // 接受整数
{
    std::cout << "print(int): " << x << '\n';
}

void print(int* ptr)  // 接受整数指针
{
    std::cout << "print(int*): " << (ptr ? "non-null\n" : "null\n");
}

int main()
{
    int x{ 5 };
    int* ptr{ &x };

    print(ptr);  // 始终调用 print(int*),因为 ptr 是 int* 类型(好)  
    print(0);    // 始终调用 print(int),因为 0 是整数字面量(希望这是我们的预期)  

    print(NULL); // 这条语句可能会有以下几种行为:
    // 调用 print(int)(Visual Studio 会这样做)  
    // 调用 print(int*)  
    // 导致模糊的函数调用编译错误(gcc 和 Clang 会这样做)  

    print(nullptr); // 始终调用 print(int*)

    return 0;
}

在作者的机器上(使用 Visual Studio),这会打印:

print(int*): non-null
print(int): 0
print(int): 0
print(int*): null

当将整数值 0 作为参数传递时,编译器会优先选择 print(int) 而不是 print(int*)。这可能会导致意外的结果,因为我们原本希望调用 print(int*) 并将 nullptr 作为参数。

NULL 被定义为值 0 时,print(NULL) 也会调用 print(int),而不是像您可能期望的那样调用 print(int*)(因为它是一个空指针字面量)。当 NULL 不被定义为 0 时,可能会发生其他行为,比如调用 print(int*) 或导致编译错误。

使用 nullptr 可以消除这种歧义(它将始终调用 print(int*)),因为 nullptr 只会匹配指针类型。

std::nullptr_t(可选)

由于 nullptr 可以在函数重载中与整数值区分开来,因此它必须有不同的类型。nullptr 的类型是什么?答案是 nullptr 的类型是 std::nullptr_t(在头文件 <cstddef> 中定义)。std::nullptr_t 只能保存一个值:nullptr!虽然这看起来有点多余,但在一种情况下很有用。如果我们想编写一个函数,

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

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

公众号二维码

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