您的位置:首页 > 编程语言 > C语言/C++

C++中的临时对象

2012-12-04 11:39 357 查看



关于引用和指针的区别的文章很多很多,但是总是找不到他们的根本区别,偶然在codeproject上看到这篇文章,觉得讲的挺好的,

所以翻译了下,希望对大家有帮助。

原文地址: http://www.codeproject.com/KB/cpp/References_in_c__.aspx
引言

我选择写 C++ 中的引用是因为我感觉大多数人误解了引用。而我之所以有这个感受是因为我主持过很多 C++的面试,并且我很少从面试者中得到关于 C++ 引用的正确答案。

那么 c++ 中引用到底意味这什么呢?通常一个引用让人想到是一个引用的变量的别名,而我讨厌将 c++ 中引用定义为变量的别名。这篇文章中,我将尽量解释清楚, c++ 中根本就没有什么叫做别名的东东。

背景

在 c/c++ 中,访问一个变量只能通过两种方式被访问,传递,或者查询。这两种方式是:

1. 通过值 访问 / 传递变量

2. 通过地址 访问 / 传递变量 – 这种方法就是指针

除此之外没有第三种访问和传递变量值的方法。引用变量也就是个指针变量,它也拥有内存空间。最关键的是引用是一种会被编译器自动解引用的指针。很难相信么?让我们来看看吧。。。

下面是一段使用引用的简单 c++ 代码

[cpp:nogutter] view plaincopyprint?

#include <iostream.h>
int main()
{
int i = 10; // A simple integer variable
int &j = i; // A Reference to the variable i
j++; // Incrementing j will increment both i and j.
// check by printing values of i and j
cout<< i << j <<endl; // should print 11 11
// Now try to print the address of both variables i and j
cout<< &i << &j <<endl;
// surprisingly both print the same address and make us feel that they are
// alias to the same memory location.
// In example below we will see what is the reality
return 0;
}

引用其实就是 c++ 中的常量指针。表达式 int &i = j; 将会被编译器转化成 int *const i = &j; 而引用之所以要初始化是因为 const 类型变量必须初始化,这个指针也必须有所指。下面我们再次聚焦到上面这段代码,并使用编译器的那套语法将引用替换掉。

[cpp:nogutter] view plaincopyprint?

#include <iostream.h>
int main()
{
int i = 10; // A simple integer variable
int *const j = &i; // A Reference to the variable i
(*j)++; // Incrementing j. Since reference variables are
// automatically dereferenced by compiler
// check by printing values of i and j
cout<< i << *j <<endl; // should print 11 11
// A * is appended before j because it used to be reference variable
// and it should get automatically dereferenced.
return 0;
}

读者一定很奇怪为什么我上面这段代码会跳过打印地址这步。这里需要一些解释。因为引用变量时会被编译器自动解引用的,那么一个诸如 cout << &j << endl; 的语句,编译器就会将其转化成语句 cout << &*j << endl; 现在 &* 会相互抵消,这句话变的毫无意义,而 cout 打印的 j 值就是 i 的地址,因为其定义语句为 int *const j = &i;

所以语句 cout << &i << &j << endl; 变成了 cout << &i << &*j << endl; 这两种情况都是打印输出 i 的地址。这就是当我们打印普通变量和引用变量的时候会输出相同地址的原因。

下面给出一段复杂一些的代码,来看看引用在级联 (cascading) 中是如何运作的。

[cpp:nogutter] view plaincopyprint?

#include <iostream.h>
int main()
{
int i = 10; // A Simple Integer variable
int &j = i; // A Reference to the variable
// Now we can also create a reference to reference variable.
int &k = j; // A reference to a reference variable
// Similarly we can also create another reference to the reference variable k
int &l = k; // A reference to a reference to a reference variable.
// Now if we increment any one of them the effect will be visible on all the
// variables.
// First print original values
// The print should be 10,10,10,10
cout<< i << "," << j << "," << k << "," << l <<endl;
// increment variable j
j++;
// The print should be 11,11,11,11
cout<< i << "," << j << "," << k << "," << l <<endl;
// increment variable k
k++;
// The print should be 12,12,12,12
cout<< i << "," << j << "," << k << "," << l <<endl;
// increment variable l
l++;
// The print should be 13,13,13,13
cout<< i << "," << j << "," << k << "," << l <<endl;
return 0;
}

下面这段代码是将上面代码中的引用替换之后代码,也就是说明我们不依赖编译器的自动替换功能,手动进行替换也能达到相同的目标。

[cpp:nogutter] view plaincopyprint?

#include <iostream.h>
int main()
{
int i = 10; // A Simple Integer variable
int *const j = &i; // A Reference to the variable
// The variable j will hold the address of i
// Now we can also create a reference to reference variable.
int *const k = &*j; // A reference to a reference variable
// The variable k will also hold the address of i because j
// is a reference variable and
// it gets auto dereferenced. After & and * cancels each other
// k will hold the value of
// j which it nothing but address of i
// Similarly we can also create another reference to the reference variable k
int *const l = &*k; // A reference to a reference to a reference variable.
// The variable l will also hold address of i because k holds address of i after
// & and * cancels each other.
// so we have seen that all the reference variable will actually holds the same
// variable address.
// Now if we increment any one of them the effect will be visible on all the
// variables.
// First print original values. The reference variables will have * prefixed because
// these variables gets automatically dereferenced.
// The print should be 10,10,10,10
cout<< i << "," << *j << "," << *k << "," << *l <<endl;
// increment variable j
(*j)++;
// The print should be 11,11,11,11
cout<< i << "," << *j << "," << *k << "," << *l <<endl;
// increment variable k
(*k)++;
// The print should be 12,12,12,12
cout<< i << "," << *j << "," << *k << "," << *l <<endl;
// increment variable l
(*l)++;
// The print should be 13,13,13,13
cout << i << "," << *j << "," << *k << "," << *l <<endl;
return 0;
}

我们通过下面代码可以证明 c++ 的引用不是神马别名,它也会占用内存空间的。

[cpp:nogutter] view plaincopyprint?

#include <iostream.h>
class Test
{
int &i; // int *const i;
int &j; // int *const j;
int &k; // int *const k;
};
int main()
{
// This will print 12 i.e. size of 3 pointers
cout<< "size of class Test = " << sizeof(class Test) <<endl;
return 0;
}

结论

我希望这篇文章能把 c++ 引用的所有东东都解释清楚,然而我要指出的是 c++ 标准并没有解释编译器如何实现引用的行为。所以实现取决于编译器,而大多数情况下就是将其实现为一个 const 指针。

引用支持 c++ 虚函数机制的代码

[cpp:nogutter] view plaincopyprint?

#include <iostream.h>
class A
{
public:
virtual void print() { cout<<"A.."<<endl; }
};
class B : public A
{
public:
virtual void print() { cout<<"B.."<<endl; }
};

class C : public B
{
public:
virtual void print() { cout<<"C.."<<endl; }
};
int main()
{
C c1;
A &a1 = c1;
a1.print(); // prints C
A a2 = c1;
a2.print(); // prints A
return 0;
}

上述代码使用引用支持虚函数机制。如果引用仅仅是一个别名,那如何实现虚函数机制,而虚函数机制所需要的动态信息只能通过指针才能实现,所以更加说明引用其实就是一个 const 指针。

程序员间交谈时,经常把仅仅需要一小段时间的变量称为临时变量。例如在下面这段swap(交换)例程里:

template<class T>

void swap(T& object1, T& object2)

{

T temp = object1;

object1 = object2;

object2 = temp;

}

通常把temp叫做临时变量。不过就C++而言,temp根本不是临时变量,它只是一个函数的局部对象。

在C++中真正的临时对象是看不见的,它们不出现在我们的源代码中。建立一个没有命名的非堆(non-heap)对象会产生临时对象。这种未命名的对象通常在两种条件下产生:为了使函数成功调用而进行隐式类型转换和函数返回对象时。理解如何和为什么建立这些临时对象是很重要的,因为构造和释放它们的开销对于程序的性能来说有着不可忽视的影响。

首先考虑为使函数成功调用而建立临时对象这种情况。当传送给函数的对象类型与参数类型不匹配时会产生这种情况。例如一个函数,它用来计算一个字符在字符串中出现的次数:

// 返回ch在str中出现的次数

size_t countChar(const string& str, char ch);

char buffer[MAX_STRING_LEN];

char c;

// 读入到一个字符和字符串中,用setw避免缓存溢出,当读取一个字符串时

cin >> c >> setw(MAX_STRING_LEN) >> buffer;

cout << "There are " << countChar(buffer, c)

<< " occurrences of the character " << c

<< " in " << buffer << endl;

看一下countChar的调用。第一个被传送的参数是字符数组,但是对应函数的正被绑定的参数的类型是const string&。仅当消除类型不匹配后,才能成功进行这个调用,编译器很乐意替你消除它,方法是建立一个string类型的临时对象。通过以buffer做为参数调用string的构造函数来初始化这个临时对象。countChar的参数str被绑定在这个临时的string对象上。当countChar返回时,临时对象自动释放。

这样的类型转换很方便(尽管很危险),但是从效率的观点来看,临时string对象的构造和释放是不必要的开销。通常有两个方法可以消除它。一种是重新设计代码,不让发生这种类型转换。另一种方法是通过修改软件而不再需要类型转换。

仅当通过传值(by value)方式传递对象或传递常量引用(reference-to-const)参数时,才会发生这些类型转换。当传递一个非常量引用(reference-to-non-const)参数对象,就不会发生。考虑一下这个函数:

void uppercasify(string& str); // 把str中所有的字符

// 改变成大写

在字符计数的例子里,能够成功传递char数组到countChar中,但是在这里试图用char数组调用upeercasify函数,则不会成功:

char subtleBookPlug[] = "Effective C++";

uppercasify(subtleBookPlug); // 错误!

没有为使调用成功而建立临时对象,为什么呢?

假设建立一个临时对象,那么临时对象将被传递到upeercasify中,其会修改这个临时对象,把它的字符改成大写。但是对subtleBookPlug函数调用的真正参数没有任何影响;仅仅改变了临时从subtleBookPlug生成的string对象。无疑这不是程序员所希望的。程序员传递subtleBookPlug参数到uppercasify函数中,期望修改subtleBookPlug的值。当程序员期望修改非临时对象时,对非常量引用(references-to-non-const)进行的隐式类型转换却修改临时对象。这就是为什么C++语言禁止为非常量引用(reference-to-non-const)产生临时对象。这样非常量引用(reference-to-non-const)参数就不会遇到这种问题。

建立临时对象的第二种环境是函数返回对象时。例如operator+必须返回一个对象,以表示它的两个操作数的和。例如给定一个类型Number,这种类型的operator+被这样声明:

const Number operator+(const Number& lhs,const Number& rhs);

这个函数的返回值是临时的,因为它没有被命名;它只是函数的返回值。必须为每次调用operator+构造和释放这个对象而付出代价。

通常我们不想付出这样的开销。对于这种函数,可以切换到operator=,而避免开销。不过对于大多数返回对象的函数来说,无法切换到不同的函数,从而没有办法避免构造和释放返回值。至少在概念上没有办法避免它。然而概念和现实之间又一个黑暗地带,叫做优化,有时能以某种方法编写返回对象的函数,以允许编译器优化临时对象。这些优化中,最常见和最有效的是返回值优化。

综上所述,临时对象是有开销的,所以应该尽可能地去除它们,然而更重要的是训练自己寻找可能建立临时对象的地方。在任何时候只要见到常量引用(reference-to-const)参数,就存在建立临时对象而绑定在参数上的可能性。在任何时候只要见到函数返回对象,就会有一个临时对象被建立(以后被释放)。学会寻找这些对象构造,就能显著地增强透过编译器表面动作而看到其背后开销的能力。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: