使用类对象的指针和引用——特别是引用,在面向对象编程和函数形参说明方面——特别是后者,非常重要。类对象可能涉及相当多的数据,因此使用按值传递机制(将函数形参指定为对象)可能非常耗时和低效,因为需要复制每一个实参对象。还有一些对类的某些操作而言必不可少的技术也需要使用引用。比如稍后将看到的那样,如果不使用引用形参,我们将不能编写复制构造函数。
类对象的指针
我们以声明其他指针的相同方式,声明指向类对象的指针。例如,下面这条语句声明了一个指向CBox类对象的指针:
CBox* pBox = 0; // Declare a pointer to CBox
现在,我们可以在赋值语句中以通常的方式,使用该指针来存储CBox对象的地址:
pBox = &cigar; // Store address of CBox object cigar in pBox
正如在Compare()成员函数的定义中使用this指针时所看到的那样,我们可以使用对象的指针来调用函数。例如,我们可以像下面语句中那样通过指针pBox调用函数Volume():
cout << pBox->Volume(); // Display volume of object pointed to by pBox
该语句再次使用了间接成员访问运算符。大多数程序员在这种情况下通常都使用该运算符,因此从现在开始,本书也将大量使用。
试一试:类对象的指针
让我们稍微深入地试着练习一下间接成员访问运算符的用法。我们将使用Ex7_10.cpp作为基础,但需要作一些修改。
// Ex7_13.cpp
// Exercising the indirect member access operator
#include <iostream>
using std::cout;
using std::endl;
class CBox // Class definition at global scope
{
public:
// Constructor definition
CBox(double lv = 1.0, double bv = 1.0, double hv = 1.0)
{
cout << endl << "Constructor called.";
m_Length = lv; // Set values of
m_Width = bv; // data members
m_Height = hv;
}
// Function to calculate the volume of a box
double Volume() const
{
return m_Length*m_Width*m_Height;
}
// Function to compare two boxes which returns true (1)
// if the first is greater that the second, and false (0) otherwise
int Compare(CBox* pBox) const
{
return this->Volume() > pBox->Volume();
}
private:
double m_Length; // Length of a box in inches
double m_Width; // Width of a box in inches
double m_Height; // Height of a box in inches
};
int main()
{
CBox boxes[5]; // Array of CBox objects declared
CBox match(2.2, 1.1, 0.5); // Declare match box
CBox cigar(8.0, 5.0, 1.0); // Declare cigar Box
CBox* pB1 = &cigar; // Initialize pointer to cigar object address
CBox* pB2 = 0; // Pointer to CBox initialized to null
cout << endl
<< "Address of cigar is " << pB1 // Display address
<< endl
<< "Volume of cigar is "
<< pB1->Volume(); // Volume of object pointed to
pB2 = &match;
if(pB2->Compare(pB1)) // Compare via pointers
cout << endl
<< "match is greater than cigar";
else
cout << endl
<< "match is less than or equal to cigar";
pB1 = boxes; // Set to address of array
boxes[2] = match; // Set 3rd element to match
cout << endl // Now access thru pointer
<< "Volume of boxes[2] is " << (pB1 + 2)->Volume();
cout << endl;
return 0;
}
如果运行该示例,则输出看起来与下面显示的大致相似:
Constructor called.
Constructor called.
Constructor called.
Constructor called.
Constructor called.
Constructor called.
Constructor called.
Address of cigar is 0012FE20
Volume of cigar is 40
match is less than or equal to cigar
Volume of boxes[2] is 1.21
注意:
当然,您的PC上对象cigar的地址值有可能不同。
示例说明
对类定义的修改不是重要的实质性修改。我们只是将Compare()函数改为接受CBox对象的指针作为实参。另外,我们现在知道了关于const成员函数的信息,又因为Compare()函数也不修改对象,所以将其声明为const。main()函数只是以各种相当随意的方式,练习使用CBox对象的指针而已。
在main()函数中,我们在声明过数组Boxes以及CBox对象cigar和match之后,声明了两个CBox对象的指针。第一个指针pB1被初始化为对象cigar的地址,第二个指针pB2被初始化为NULL。这两条语句使用指针的方式与我们使用基本类型的指针时的方式完全相同。我们在使用自定义类型的指针这一事实没有造成任何差别。
我们使用pB1与间接成员访问运算符,获得被指向对象的体积,并将结果显示出来。然后,将match的地址赋给pB2,并在调用比较函数时使用了pB1和pB2两个指针。因为Compare()函数的实参是CBox对象的指针,所以该函数为实参对象调用Volume()函数时使用了间接成员访问运算符。
为了证实使用指针pB1选择成员函数时可以执行地址算术,我们将pB1设定为CBox类型数组boxes中第一个元素的地址。之后,选择数组的第三个元素,并计算其体积。结果与match的体积相同。
从输出可以看出,共有7次对CBox对象构造函数的调用,其中5次是创建数组Boxes,另外创建对象cigar和match各需要一次。
总之,使用类对象的指针与使用基本类型(比如double类型)的指针之间实质上没有任何区别。
类对象的引用
当随同类一起使用时,引用才能真正获得应该得到的荣誉。如同指针的情况一样,在声明和使用类对象的引用与声明和使用基本类型变量的引用之间,实质上没有任何区别。例如,为了声明对象cigar的引用,我们可以这样写:
CBox& rcigar = cigar; // Define reference to object cigar
为了使用引用计算对象cigar的体积,我们只需在应该出现对象名的位置使用引用名即可:
cout << rcigar.Volume(); // Output volume of cigar thru a reference
我们可能还记得,引用扮演着被引用对象别名的角色,因此其用法完全与使用原来的对象名相同。
1. 实现复制构造函数
引用的重要性实际体现在函数(特别是类的成员函数)的实参和返回值等上下文中。让我们首先返回到复制构造函数的问题。目前,我们暂且回避何时需要编写自己的复制构造函数这个问题,而全神贯注于如何编写一个复制构造函数的问题。我们将使用CBox类,这仅仅是为了使讨论更具体。
复制构造函数是通过用同类的现有对象进行初始化,从而创建新对象的构造函数,因此需要接受同类的对象作为实参。我们经过考虑,可能写出下面的原型:
CBox(CBox initB);
现在,考虑调用该构造函数时将发生什么事情。如果我们写出
CBox myBox = cigar;
这样一条声明语句,那么将生成如下所示对复制构造函数的调用:
CBox::CBox(cigar);
在意识到实参是通过按值传递机制传递的之前,该语句似乎没有任何问题。在可以传递对象cigar之前,编译器需要安排创建该对象的副本。因此,编译器为了处理复制构造函数的这条调用语句,需要调用复制构造函数来创建实参的副本。遗憾的是,因为是按值传递,第二次调用同样需要创建实参的副本,因此还得调用复制构造函数,就这样持续不休。我们最终得到的是对复制构造函数的无穷调用。
您肯定已经猜到了,解决方法是使用const引用形参。我们可以将复制构造函数的原型写成下面这样:
CBox(const CBox& initB);
现在,计算机再不需要复制复制构造函数的实参。实参是用来初始化引用形参的,因此没有复制发生。我们记得前面关于引用的讨论中说过,如果函数的形参是引用,则调用该函数时不需要复制实参。函数直接访问调用函数中的实参变量。const限定符用来确保该函数不能修改实参。
注意:
这是const限定符的另一个重要用途。我们应该总是将函数的引用形参声明为const,除非该函数将修改实参。
我们可以像下面这样实现这个复制构造函数:
CBox::CBox(const CBox& initB)
{
m_Length = initB.m_Length;
m_Width = initB.m_Width;
m_Height = initB.m_Height;
}
该复制构造函数的定义假定自己位于类定义外部,因此构造函数名用类名和作用域解析运算符加以限定。被创建对象的各个数据成员用传递给构造函数的实参对象的对应成员进行初始化。当然,我们也可以使用初始化列表来设定对象的值。