章节目录

通过指针传递函数参数(第2部分)

本节阅读量:

传递地址的“可选”参数

传递地址的一个更常见的用法是允许函数接受“可选”参数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <string>

void greet(std::string* name=nullptr)
{
    std::cout << "Hello ";
    std::cout << (name ? *name : "guest") << '\n';
}

int main()
{
    greet(); // 这里还不知道用户名

    std::string joe{ "Joe" };
    greet(&joe); // 现在知道用户是Joe

    return 0;
}

此示例打印:

1
2
Hello guest
Hello Joe

在这个程序中,greet()函数有一个由地址传递的参数,默认为nullptr。在main()中,调用该函数两次。第一次调用时,不知道用户是谁,因此在没有参数的情况下调用greet()。name参数默认为nullptr,greet函数打印“guest”。对于第二个调用,现在有一个有效的用户,因此调用greet(&joe)。name参数接收joe的地址,并可以使用它来打印名称“joe”。

然而,在许多情况下,函数重载是实现相同结果的更好的替代方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <string>
#include <string_view>

void greet(std::string_view name)
{
    std::cout << "Hello " << name << '\n';
}

void greet()
{
    greet("guest");
}

int main()
{
    greet(); // 这里还不知道用户名

    std::string joe{ "Joe" };
    greet(joe); // 现在知道用户是Joe

    return 0;
}

这有许多优点:不再需要担心解引用空指针,并且如果需要,可以传入字符串字面值。


更改指针参数指向的内容

当向函数传递地址时,该地址将复制到指针参数中(这很好,因为复制地址很快)。现在考虑以下程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

// ptr被赋值但未被使用,[[maybe_unused]]让编译器不要告警
void nullify([[maybe_unused]] int* ptr2) 
{
    ptr2 = nullptr; // 将ptr指向空指针
}

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

该程序打印:

1
2
ptr is non-null
ptr is non-null

正如所看到的,更改指针参数ptr2存储的地址不会影响原来指针ptr所保存的地址(ptr仍然指向x)。当调用函数nullify()时,ptr2接收传入地址的副本(在本例中,是ptr持有的地址,即x的地址)。当函数更改ptr2指向的内容时,这仅影响ptr2持有的副本。

那么,如果想允许函数改变原来的指针ptr所指向的地址,该怎么办?


通过引用传递指针?

就像可以通过引用传递普通变量一样,也可以通过引用来传递指针。下面是与上面类似的程序,ptr2更改为对指针的引用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#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;
}

该程序打印:

1
2
ptr is non-null
ptr is null

因为refptr现在是指针的引用,所以当ptr传递给函数时,refptr绑定到ptr。这意味着对refptr的任何更改都是对ptr的更改。


为什么不再推荐使用0或NULL表示空指针(可选)

0可以解释为整数,也可以解释为空指针。具体哪一个可能是模棱两可的——在一些情况下,编译器可能会假设我们是指一个,而我们是本意是另一个,程序可能会因此产生预期外的执行结果。

语言标准未定义预处理器宏NULL的定义。它可以定义为0、0L、((void*)0) 或完全其它的东西。

在函数重载简介中,讨论了函数可以重载(多个函数可以具有相同的名称,只要它们可以通过参数的数量或类型来区分)。编译器通过实际函数调用时传入的值来区分具体调用哪个函数。

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <cstddef> // for NULL

void print(int x) // 这个函数接收int参数
{
	std::cout << "print(int): " << x << '\n';
}

void print(int* ptr) // 这个函数接收int指针
{
	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是int (希望这是我们预期的行为)

	print(NULL); // 这个语句可能有如下的行为:
	// 调用 print(int) (Visual Studio 上)
	// 调用 print(int*)
	// 编译报错,无法明确匹配函数 (gcc 与 Clang 上)

	print(nullptr); // 永远调用 print(int*)

	return 0;
}

在作者的计算机上(使用Visual Studio),将打印:

1
2
3
4
print(int*): non-null
print(int): 0
print(int): 0
print(int*): null

当将整数值0作为参数传递时,编译器会选择print(int)而不是print(int*)。当我们打算用NULL作为空指针参数调用print(int*)时,这可能有意外的结果。

在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!虽然这看起来有点傻,但它在一种情况下是有用的。如果希望编写只接受nullptr参数的函数,可以将该参数类型设置为std::nullptr_t。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <cstddef> // for std::nullptr_t

void print(std::nullptr_t)
{
    std::cout << "in print(std::nullptr_t)\n";
}

void print(int*)
{
    std::cout << "in print(int*)\n";
}

int main()
{
    print(nullptr); // 调用 print(std::nullptr_t)

    int x { 5 };
    int* ptr { &x };

    print(ptr); // 调用 print(int*)

    ptr = nullptr;
    print(ptr); // 调用 print(int*) (因为 ptr 类型是 int*)

    return 0;
}

在上面的示例中,函数调用print(nullptr)解析为函数print(std::nullptr_t)而不是 print(int*),因为它不需要转换。

一种可能有点令人困惑的情况是,当ptr持有nullptr值时调用print(ptr)。记住,函数重载匹配的是类型,而不是值,并且ptr的类型是int*。因此,print(int*)将被匹配。在这种情况下,不会考虑print(std::nullptr_t),因为指针类型不会隐式转换为std::nullptr_t。

您可能永远不需要使用这个,但知道这一点很好,以防万一。


也许所有的参数都只是传递值

既然您已经理解了通过引用传递、地址和值之间的基本区别,那么让我们来了解一下简化主义者。:)

虽然编译器通常可以完全优化引用,但在某些情况下,这是不可能的,并且实际上需要引用。引用通常由编译器使用指针来实现。这意味着在幕后,按引用传递本质上只是一个传递指针(对引用的访问执行隐式指针解引用)。

在上一课中,提到了传递地址只是将地址从调用者复制到被调用的函数——它只是按值传递地址。

因此,我们可以得出结论,C++确实按值传递所有内容!传递地址(和引用)的能力在于,可以解引用传递的地址来更改参数指向的对象,这是使用普通值作为参数所不能做到的!


12.9 通过指针传递函数参数

上一节

12.11 引用或指针作为函数返回值

下一节