• Index

函数参数使用引用

Reads: 13

函数参数使用左值引用

先看个函数声明:

unsigned int count_if(std::u32string text, std::function<bool(char32_t)> pred);

这个函数声明是不是有点熟悉,哈哈,就是在函数类型写的例子。现在,假设调用这个函数的时候,第一个参数传了一篇100万字的长篇小说,那么你会惊讶的发现(或者不惊讶的发现)它会卡一下(电脑渣的话),而且内存也会稍稍地升一下。以下是完整代码:

#include <iostream> // std::cout std::endl
#include <string> // std::u32string
#include <functional> // std::function

unsigned int count_if(std::u32string text, std::function<bool(char32_t)> pred)
{
    unsigned int count = 0;
    for (auto ch : text)
    {
        if (pred(ch))
        {
            ++count;
        }
    }
    return count;
}

int main(void)
{
    auto ishey = [](auto c)
    {
        return c == U'嘿';
    };
    std::u32string novel = U"嘿嘿嘿,假设这里是从txt里读出来的100万字小说";
    auto count = count_if(novel, ishey); // 这里赋值会进行复制
    std::cout << count << std::endl;
    return 0;
}

之前说过,参数的传值就是一个赋值行为。将novel传给函数参数里的texttext首先会创建保存100万个字符的内存(即3.8M左右的内存,向系统申请内存需要一点点时间),然后将novel中的字符一个个复制到text中(复制大量数据耗时)。当处理完后,text离开作用域而释放内存,释放内存也需要一点时间(申请内存和释放内存的处理速度一般比较快)。

基础示例

为了避免这种创建内存然后复制的情况,引用提供了非常有用的帮助。以上代码可以写成这样:

#include <iostream> // std::cout std::endl
#include <string> // std::u32string
#include <functional> // std::function

unsigned int count_if(const std::u32string &text, std::function<bool(char32_t)> pred)
{
    unsigned int count = 0;
    for (auto ch : text)
    {
        if (pred(ch))
        {
            ++count;
        }
    }
    return count;
}

int main(void)
{
    auto ishey = [](auto c)
    {
        return c == U'嘿';
    };
    std::u32string novel = U"嘿嘿嘿,假设这里是从txt里读出来的100万字小说";
    auto count = count_if(novel, ishey); // 这里赋值会进行复制
    std::cout << count << std::endl;
    return 0;
}

基础讲解

只需要函数count_if()的第一个参数从std::u32string text改成const std::u32string &text就可以避免上面的创建内存然后复制的情况。因为参数text只是传进来的变量的引用。

函数count_if()中的第二个参数没有使用引用是因为:类型std::function<bool(char32_t)>的复制并不怎样耗时而且占用内存也不大,而引用本身也是会占用小小内存,所以这个情况没必要使用引用。

因为函数count_if()不需要改变传入的实际参数的值,所以应该加上const来限制函数不能改变它的值。在上面代码中,假设novelconst限定并且text没有const限定,那么像上面这样调用函数将会报错,因为编译器认为函数count_if()会改变实参novel的值,而novel是不能被修改的。所以在设计函数的时候,如果你的形式参数使用了引用并且不需要修改它的值时,请务必加上const限定。

还有就是:当不需要改变传入的实参值时,基本数据类型(包括为基本数据类型重新命名的类型)不需要使用引用,因为它们的内存占用大小和引用的一样或者更小。

函数参数使用右值引用

函数参数使用右值引用,一般用作重载函数的区分,区分出左值引用和右值引用,然后分别作优化:

std::u32string copy(const std::u32string &text); // 函数1
std::u32string copy(std::u32string &&text); // 函数2

函数实现复制字符串的功能,是不需要改变实参的,所以左值引用加上const防止改变实参保存的值;而右值引用的实参肯定是一个值,所以没有必要加const,除非你在函数定义中不想改变text保存的值。

再看下面的调用:

std::u32string text = U"小古银很帅气";
auto copytext1 = copy(text); // 变量名text是左值, 调用函数1
auto copytext2 = copy(std::u32string(U"小古银不是死肥宅")); // 没有变量名, 调用函数2

基础示例

以下是完整代码:

#include <iostream> // std::cout std::endl
#include <string> // std::u32string

std::u32string copy(const std::u32string &text);
std::u32string copy(std::u32string &&text);

int main(void)
{
    std::u32string text = U"小古银很帅气";
    auto copytext1 = copy(text); // 调用左值引用
    std::cout << copytext1.size() << std::endl;

    auto copytext2 = copy(std::u32string(U"小古银不是死肥宅")); // 调用右值引用
    std::cout << copytext2.size() << std::endl;

    return 0;
}

std::u32string copy(const std::u32string &text)
{
    // 为了方便查看究竟调用哪一个函数而加上输出
    // 实际工程中复制功能就只有复制, 不应该有其他不相关的代码
    std::cout << "调用左值引用的重载函数来复制字符串" << std::endl;

    // std::u32string temp = text; // 复制
    // return temp; // 返回局部变量
    // 以上两行代码可以简写成下面这一行
    // return std::u32string(text);
    // 由于返回的是右值而不是引用, 上面这行代码可以进一步简写成下面这一行
    return text; // 复制出一个没有变量名称的字符串, 然后返回这个无名的字符串
}

std::u32string copy(std::u32string &&text)
{
    // 为了方便查看究竟调用哪一个函数而加上输出
    // 实际工程中复制功能就只有复制, 不应该有其他不相关的代码
    std::cout << "调用右值引用的重载函数, 完美转发" << std::endl;
    return text; // text相当于局部变量, 编译器会启用完美转发的优化, 不会复制
}

输出结果:

调用左值引用的重载函数来复制字符串
6
调用右值引用的重载函数, 完美转发
8

基础讲解

右值引用的重载中,由于用了转移,所以形参不能加const限定。

以下代码中,text是通过右值传进来的,所以就相当于局部变量。

std::u32string copy(std::u32string &&text)
{
    return text;
}

以下代码调用右值的copy(),由于函数copy()里的text是局部变量,所以编译器启用完美转发优化机制,将局部变量text的内存空间直接转移到copytext2中,这样就省去了复制过程,提高运行速度。

auto copytext2 = copy(std::u32string(U"小古银不是死肥宅"));

另外,可能你会对这个的返回值有所纠结:

std::u32string copy(const std::u32string &text)
{
    return text;
}

注意这个函数的返回类型不是一个引用,所以就算直接返回引用text,返回的也不是一个引用。实际上它内部的操作是return std::u32string(text);,而它可以简写成上面代码的样子。如果想返回引用,可以将返回类型改成引用类型,详细请看下一篇教程。

基础拓展 1

假设只有左值引用参数的函数,没有右值引用参数的函数,就是下面只有函数1没有函数2:

std::u32string copy(const std::u32string &text); // 函数1
// std::u32string copy(std::u32string &&text); // 函数2

这个时候的调用如下:

std::u32string text = U"小古银很帅气";
auto copytext1 = copy(text); // 变量名text是左值, 调用函数1
auto copytext2 = copy(std::u32string(U"小古银不是死肥宅")); // 调用函数1, 因为没有函数2

这里有个左值引用作为函数参数和普通的变量的区别:一般的引用只能以变量作为初始值;而作为参数的引用可以以值作为初始值。

基础拓展 2

假设只有右值引用参数的函数,没有左值引用参数的函数,就是下面只有函数2没有函数1:

// std::u32string copy(const std::u32string &text); // 函数1
std::u32string copy(std::u32string &&text); // 函数2

这个时候的调用如下:

std::u32string text = U"小古银很帅气";
// auto copytext1 = copy(text); // 去除该行开头的注释将会报错, 因为没有函数1
auto copytext2 = copy(std::u32string(U"小古银不是死肥宅")); // 调用函数2

就像注释所说的一样,去除第二行开头的注释,编译器就会报错说找不到左值的重载。不要问为什么,编译器就是酱紫处理的。

基础拓展 3

或许,喜欢思考的你,现在应该发现:

std::u32string copy(const std::u32string &text); // 函数1
std::u32string copy(std::u32string &&text); // 函数2

为什么是上面酱紫,而不是下面酱紫:

std::u32string copy(const std::u32string &text); // 函数1
std::u32string copy(std::u32string text); // 函数3

现在进行说明:

像上面所说的,当只有函数1的时候,无论使用左值还是右值都可以调用函数1,传进去后参数都是左值引用。

而当只有函数3的时候,无论使用左值还是右值都可以调用函数3,左值传进去后参数就会进行复制操作,右值传进去后参数会进行转移或者完美转发。

当函数1和函数3同时出现时,因为这两个函数都可以接收左值和右值,这样造成了歧义,编译器不知道调用哪个函数。当函数1和函数2同时出现时,因为函数2只接收右值,所以当有右值参数时优先传给函数2;当有左值参数时,只能传给函数1。

在函数2和函数3同时出现的情况:因为函数2不接收左值,所以当有左值参数时,只能传给函数3;因为右值引用就是只能接收右值的普通变量,所以当传入右值时,就会造成歧义。

这里就不上代码了,你可以在上面的例子中改改参数,实践一下。

基础拓展 4

注意:如果使用非常量左值引用保存右值时,编译将会报错。

因为非常量左值引用意味着:传进来的实际参数肯定是可以修改保存数据变量,所以是不能传入右值或者字面量,因为它们都是值。如:

void reset(int &variable)
{
    variable = 0;
}

以下代码是没有问题的:

int value = 111;
reset(value);

以下代码编译将不通过:

reset(123456); // 编译时将会报错

Comments

Make a comment

  • Index

WARNING: You are using an old browser that does not support HTML5. Please choose a modern browser (Chrome / Microsoft Edge / Firefox / Sarafi) to get a good experience.