当前位置:嗨网首页>书籍在线阅读

07-将引用用于结构

  
选择背景色: 黄橙 洋红 淡粉 水蓝 草绿 白色 选择字体: 宋体 黑体 微软雅黑 楷体 选择字体大小: 恢复默认

8.2.4 将引用用于结构

引用非常适合用于结构和类(C++的用户定义类型)。确实,引入引用主要是为了用于这些类型的,而不是基本的内置类型。

使用结构引用参数的方式与使用基本变量引用相同,只需在声明结构参数时使用引用运算符&即可。例如,假设有如下结构定义:

struct free_throws
{
    std::string name;
    int made;
    int attempts;
    float percent;
};

则可以这样编写函数原型,在函数中将指向该结构的引用作为参数:

void set_pc(free_throws & ft); // use a reference to a structure

如果不希望函数修改传入的结构,可使用const:

void display(const free_throws & ft); // don't allow changes to structure

程序清单8.6中的程序正是这样做的。它还通过让函数返回指向结构的引用添加了一个有趣的特点,这与返回结构有所不同。对此,有一些需要注意的地方,稍后将进行介绍。

程序清单8.6 strc_ref.cpp

//strc_ref.cpp -- using structure references
#include <iostream>
#include <string>
struct free_throws
{
    std::string name;
    int made;
    int attempts;
    float percent;
};
void display(const free_throws & ft);
void set_pc(free_throws & ft);
free_throws & accumulate(free_throws & target, const free_throws & source);
int main()
{
// partial initializations – remaining members set to 0
    free_throws one = {"Ifelsa Branch", 13, 14};
    free_throws two = {"Andor Knott", 10, 16};
    free_throws three = {"Minnie Max", 7, 9};
    free_throws four = {"Whily Looper", 5, 9};
    free_throws five = {"Long Long", 6, 14};
    free_throws team = {"Throwgoods", 0, 0};
// no initialization
    free_throws dup;
    set_pc(one);
    display(one);
    accumulate(team, one);
    display(team);
// use return value as argument
    display(accumulate(team, two));
    accumulate(accumulate(team, three), four);
    display(team);
// use return value in assignment
    dup = accumulate(team,five);
    std::cout << "Displaying team:\n";
    display(team);
    std::cout << "Displaying dup after assignment:\n";
    display(dup);
    set_pc(four);
// ill-advised assignment
    accumulate(dup,five) = four;
    std::cout << "Displaying dup after ill-advised assignment:\n";
    display(dup);
    return 0;
}
void display(const free_throws & ft)
{
    using std::cout;
    cout << "Name: " << ft.name << '\n';
    cout << " Made: " << ft.made << '\t';
    cout << "Attempts: " << ft.attempts << '\t';
    cout << "Percent: " << ft.percent << '\n';
}
void set_pc(free_throws & ft)
{
    if (ft.attempts != 0)
        ft.percent = 100.0f *float(ft.made)/float(ft.attempts);
    else
        ft.percent = 0;
}
free_throws & accumulate(free_throws & target, const free_throws & source)
{
    target.attempts += source.attempts;
    target.made += source.made;
    set_pc(target);
    return target;
}

下面是该程序的输出:

Name: Ifelsa Branch
  Made: 13 Attempts: 14 Percent: 92.8571
Name: Throwgoods
  Made: 13 Attempts: 14 Percent: 92.8571
Name: Throwgoods
  Made: 23 Attempts: 30 Percent: 76.6667
Name: Throwgoods
  Made: 35 Attempts: 48 Percent: 72.9167
Displaying team:
Name: Throwgoods
  Made: 41 Attempts: 62 Percent: 66.129
Displaying dup after assignment:
Name: Throwgoods
  Made: 41 Attempts: 62 Percent: 66.129
Displaying dup after ill-advised assignment:
Name: Whily Looper
  Made: 5 Attempts: 9 Percent: 55.5556

1.程序说明

该程序首先初始化了多个结构对象。本书前面说过,如果指定的初始值比成员少,余下的成员(这里只有percent)将被设置为零。第一个函数调用如下:

set_pc(one);

由于函数set_pc()的形参ft为引用,因此ft指向one,函数set_pc()的代码设置成员one.percent。就这里而言,按值传递不可行,因此这将导致设置的是one的临时拷贝的成员percent。根据前一章介绍的知识,另一种方法是使用指针参数并传递地址,但要复杂些:

set_pcp(&one); // using pointers instead - &one instead of one
...
void set_pcp(free_throws * pt)
{
    if (pt->attempts != 0)
        pt->percent = 100.0f *float(pt->made)/float(pt->attempts);
    else
        pt->percent = 0;
}

下一个函数调用如下:

display(one);

由于display()显示结构的内容,而不修改它,因此这个函数使用了一个const引用参数。就这个函数而言,也可按值传递结构,但与复制原始结构的拷贝相比,使用引用可节省时间和内存。

再下一个函数调用如下:

accumulate(team, one);

函数accumulate()接收两个结构参数,并将第二个结构的成员attempts和made的数据添加到第一个结构的相应成员中。只修改了第一个结构,因此第一个参数为引用,而第二个参数为const引用:

free_throws & accumulate(free_throws & target, const free_throws & source);

返回值呢?当前讨论的函数调用没有使用它;就目前而言,原本可以将返回值声明为void,但请看下述函数调用:

display(accumulate(team, two));

上述代码是什么意思呢?首先,将结构对象team作为第一个参数传递给了accumulate()。这意味着在函数accumulate()中,target指向的是team。函数accumulate()修改team,再返回指向它的引用。注意到返回语句如下:

return target;

光看这条语句并不能知道返回的是引用,但函数头和原型指出了这一点:

free_throws & accumulate(free_throws & target, const free_throws & source)

如果返回类型被声明为free_throws而不是free_throws &,上述返回语句将返回target(也就是team)的拷贝。但返回类型为引用,这意味着返回的是最初传递给accumulate()的team对象。

接下来,将accumulate()的返回值作为参数传递给了display(),这意味着将team传递给了display()。display()的参数为引用,这意味着函数display()中的ft指向的是team,因此将显示team的内容。所以,下述代码:

display(accumulate(team, two));

与下面的代码等效:

accumulate(team, two);
display(team);

上述逻辑也适用于如下语句:

accumulate(accumulate(team, three), four);

因此,该语句与下面的语句等效:

accumulate(team, three);
accumulate(team, four);

接下来,程序使用了一条赋值语句:

dup = accumulate(team,five);

正如您预期的,这条语句将team中的值复制到dup中。

最后,程序以独特的方式使用了accumulate():

accumulate(dup,five) = four;

这条语句将值赋给函数调用,这是可行的,因为函数的返回值是一个引用。如果函数accumulate()按值返回,这条语句将不能通过编译。由于返回的是指向dup的引用,因此上述代码与下面的代码等效:

accumulate(dup,five); // add five's data to dup
dup = four;           // overwrite the contents of dup with the contents of four

其中第二条语句消除了第一条语句所做的工作,因此在原始赋值语句使用accumulate()的方式并不好。

2.为何要返回引用

下面更深入地讨论返回引用与传统返回机制的不同之处。传统返回机制与按值传递函数参数类似:计算关键字return后面的表达式,并将结果返回给调用函数。从概念上说,这个值被复制到一个临时位置,而调用程序将使用这个值。请看下面的代码:

double m = sqrt(16.0);
cout << sqrt(25.0);

在第一条语句中,值4.0被复制到一个临时位置,然后被复制给m。在第二条语句中,值5.0被复制到一个临时位置,然后被传递给cout(这里理论上的描述,实际上,编译器可能合并某些步骤)。

现在来看下面的语句:

dup = accumulate(team,five);

如果accumulate()返回一个结构,而不是指向结构的引用,将把整个结构复制到一个临时位置,再将这个拷贝复制给dup。但在返回值为引用时,将直接把team复制到dup,其效率更高。

注意: 返回引用的函数实际上是被引用的变量的别名。

3.返回引用时需要注意的问题

返回引用时最重要的一点是,应避免返回函数终止时不再存在的内存单元引用。您应避免编写下面这样的代码:

const free_throws & clone2(free_throws & ft)
{
    free_throws newguy; // first step to big error
    newguy = ft;        // copy info
    return newguy;      // return reference to copy
}

该函数返回一个指向临时变量(newguy)的引用,函数运行完毕后它将不再存在。第9章将讨论各种变量的持续性。同样,也应避免返回指向临时变量的指针。

为避免这种问题,最简单的方法是,返回一个作为参数传递给函数的引用。作为参数的引用将指向调用函数使用的数据,因此返回的引用也将指向这些数据。程序清单8.6中的accumulate()正是这样做的。

另一种方法是用new来分配新的存储空间。前面见过这样的函数,它使用new为字符串分配内存空间,并返回指向该内存空间的指针。下面是使用引用来完成类似工作的方法:

const free_throws & clone(free_throws & ft)
{
    free_throws * pt;
    *pt = ft;     // copy info
    return *pt;  // return reference to copy
}

第一条语句创建一个无名的free_throws结构,并让指针pt指向该结构,因此*pt就是该结构。上述代码似乎会返回该结构,但函数声明表明,该函数实际上将返回这个结构的引用。这样,便可以这样使用该函数:

free_throws & jolly = clone(three);

这使得jolly成为新结构的引用。这种方法存在一个问题:在不再需要new分配的内存时,应使用delete来释放它们。调用clone()隐藏了对new的调用,这使得以后很容易忘记使用delete来释放内存。第16章讨论的auto_ptr模板以及C++11新增的unique_ptr可帮助程序员自动完成释放工作。

4.为何将const用于引用返回类型

程序清单8.6包含如下语句:

accumulate(dup,five) = four;

其效果如下:首先将five的数据添加到dup中,再使用four的内容覆盖dup的内容。这条语句为何能够通过编译呢?在赋值语句中,左边必须是可修改的左值。也就是说,在赋值表达式中,左边的子表达式必须标识一个可修改的内存块。在这里,函数返回指向dup的引用,它确实标识的是一个这样的内存块,因此这条语句是合法的。

另一方面,常规(非引用)返回类型是右值——不能通过地址访问的值。这种表达式可出现在赋值语句的右边,但不能出现在左边。其他右值包括字面值(如10.0)和表达式(如x + y)。显然,获取字面值(如10.0)的地址没有意义,但为何常规函数返回值是右值呢?这是因为这种返回值位于临时内存单元中,运行到下一条语句时,它们可能不再存在。

假设您要使用引用返回值,但又不允许执行像给accumulate()赋值这样的操作,只需将返回类型声明为const引用:

const free_throws &
    accumulate(free_throws & target, const free_throws & source);

现在返回类型为const,是不可修改的左值,因此下面的赋值语句不合法:

accumulate(dup,five) = four; // not allowed for const reference return

该程序中的其他函数调用又如何呢?返回类型为const引用后,下面的语句仍合法:

display(accumulate(team, two));

这是因为display()的形参也是const free_throws &类型。但下面的语句不合法,因为accumulate()的第一个形参不是const:

accumulate(accumulate(team, three), four);

这影响大吗?就这里而言不大,因为您仍可以这样做:

accumulate(team, three);
accumulate(team, four);

另外,您仍可以在赋值语句右边使用accumulate()。

通过省略const,可以编写更简短代码,但其含义也更模糊。

通常,应避免在设计中添加模糊的特性,因为模糊特性增加了犯错的机会。将返回类型声明为const引用,可避免您犯糊涂。然而,有时候省略const确实有道理,第11章将讨论的重载运算符<<就是一个这样的例子。