1、位拷贝
拷贝构造函数,它常被称为X(X&)(“X引用的X”),为了理解拷贝构造函数的需要,看一下C语言在调用函数式处理通过按值传递和返回变量的方法。如果声明了一个函数并调用它:
int f(int x, char c);
int g = f(a, b);
从产生的汇编代码中可以看出,f()的返回值放在寄存器中,编译器知道返回值的类型,因为这个类型是内置于语言中的,于是编译器可以通过把返回值放在寄存器中返回它。在C的基本数据类型中,拷贝这个值的位的行为就等同于拷贝这个对象。
问题是当寄存器没有用于存放返回值的足够大小时该怎么做?答案是把返回值的地址像一个函数一样压栈,让函数直接把返回值信息拷贝到目的地。
下面来考虑一个简单地例子:一个类在任何时候都知道它存在多少个对象。可以通过一个静态数据成员的方法来做到这点。
#include <iostream>
#include <string>
using namespace std;
class HowMany
{
static int objectCount;
public:
HowMany()
{
objectCount++;
}
static void print(const string& msg = "")
{
if (msg.size() != 0)
{
cout << msg << ": ";
cout << "objectCount = " << objectCount << endl;
}
}
~HowMany()
{
objectCount--;
print("~HowMany()");
}
};
int HowMany::objectCount = 0;
HowMany f(HowMany x) //位拷贝
{
x.print("x argument inside f()");
return x;
}
void main()
{
HowMany h;
HowMany::print("after construction of h");
HowMany h2 = f(h);
HowMany::print("after call to f()");
}
HowMany类包括一个静态变量objectcount和一个用于报告这个变量的静态成员函数print(),每当一个对象产生时,构造函数增加计数,而对象销毁时,析构函数减小计数。然而,输出并不是所期望的那样:
after construction of h: objectCount = 1
x argument inside f(): objectCount = 1
~HowMany(): objectCount = 0
after call to f(): objectCount =0
~HowMany(): objectCount = -1
~HowMany(): objectCount = -2
让我们来看一下函数f()通过按值传递方式传入参数哪一处,原来的对象h存在于函数体之外,同时在函数体内又增加了一个对象,这个对象是通过传值方式传入对象的拷贝,然而,参数的传递是使用C的原始的位拷贝概念,所以,构造函数并没有调用。
在对f()调用的最后,当局部对象出了其范围时,析构函数就被调用,析构函数使objectCount减小,就使objectCount值变为负值。
2、拷贝构造函数
出现上述问题是因为编译器假定我们想使用位拷贝来创建对象。在许多情况下这是可行的。但在HowMany类中就行不通,因为初始化不是简单地拷贝。如果类中含有指针又将出现另一个问题:他们指向什么内容,是否拷贝他们或他们是否与一些新的内存块相连?
幸运的是,可以防止编译器进行位拷贝。每当编译器需要从现有的对象创建新对象时,可以通过定义自己的函数做这些事。因为是在创建对象,所以,这个函数应该是构造函数,但是,这个对象不能通过按值传递方式传入构造函数,这里,引用就起作用了,可以使用原对象的引用。这个函数被称为拷贝构造函数,他经常被称为X(X&)。
2.1 默认拷贝构造函数
因为拷贝构造函数实现按值传递方式的参数传递和返回,如果没有创建拷贝构造函数,C++编译器将自动的创建拷贝构造函数。然而,默认的都是原始行为:位拷贝。
为了对使用组合(和继承的方法)的类创建拷贝构造函数,编译器递归的为所有的成员对象和基类调用拷贝构造函数。
2.2 防止按值传递
有一个简单地技术防止通过按值传递方式传递:声明一个私有拷贝构造函数。甚至不必去定义它,除非成员函数或友元函数需要执行按值传递方式的传递。
3、指向成员的指针
指针式指向一些内存地址的变量,既可以是数据的地址也可以是函数的地址。所以,可以在运行时改变指针指向的内容。C++的成员指针遵从这样的概念,除了所选择的内容是在类中之内的成员指针。
这里麻烦的是所有的指针需要地址,但在类内部是没有地址的;选择一个类的成员意味着在类中偏移。只有把这个偏移和具体对象的开始地址结合,才能得到实际地址。成员指针的语法要求选择一个对象的同时间接引用成员指针。
考虑如果有一个指向一个类对象的指针,如果假设它代表对象内一定的偏移,将会发生什么?为了取得指针指向的内容,必须用*号间接引用。但是,它只是一个对象内的偏移,所以必须也要指向那个对象。因此,*号要和间接引用的对象结合。所以对于指向一个对象的指针,新的语法变为->*,对于一个对象或引用,则为.*,如下所示:
objectPointer->*pointerToMember = 47;
Object.*pointerToMember = 47;
现在,让我们看看定义pointerToMember的语法是什么?其实它像任何一个指针,必须说出它指向什么类型,并且,在定义中也要使用一个‘*’号。因此,可表示如下:
int ObjectClass::*pointerToMember;
定义一个名字为pointerToMember的成员指针,该指针可以指向在ObjectClass类中的任一int类型的成员,还可以在定义的时候初始化这个成员指针。
int ObjectClass::*pointerToMember = &ObjectClass::a;
另外,成员指针是受限制的,他们仅能被指定给在类中的确切的位置。例如,我们不能像使用普通指针那样增加或比较成员指针。
3.1 函数
为了定义和使用成员函数的指针,圆括号扮演同样重要的角色。假设在一个结构内有一个函数,通过给普通函数插入类名和作用域运算符就可以定义一个指向成员函数的指针。
class Simple
{
public:
int f(float)const
{
return 1;
}
};
int (Simple::*fp)(float)const;
int (Simple::*fp2)(float)const = &Simple::f;
void main()
{
Simple s, *sp = &s;
(sp->*fp2)(1);
}
不像非成员函数,当获取成员函数的地址时,符号&不是可选的。在程序运行时,我们可以改变指针所指的内容,因此在运行时就可以通过指针选择或改变我们的行为,成员指针也一样,它允许在运行时选择一个成员。
当然期望一般用户创建如此复杂的表达式不是很合乎情理的。如果用户必须直接操作成员指针,那么typedef是适合的。现在回到先前的那个在类中使用成员指针的例子上来。用户所要做的是传递一个数字以选择一个函数。
#include <iostream>
using namespace std;
class Widget
{
void f(int)const
{
cout << "Widget::f()\n";
}
void g(int)const
{
cout << "Widget::g()\n";
}
void h(int)const
{
cout << "Widget::h()\n";
}
void i(int)const
{
cout << "Widget::i()\n";
}
enum{ cnt = 4 };
void (Widget::*fptr[cnt])(int)const;
public:
Widget()
{
fptr[0] = &Widget::f;
fptr[1] = &Widget::g;
fptr[2] = &Widget::h;
fptr[3] = &Widget::i;
}
void select(int i, int j)
{
if (i < 0 || i >= cnt)
{
return;
}
(this->*fptr[i])(j);
}
int count()
{
return cnt;
}
};
void main()
{
Widget w;
for (size_t i = 0; i < w.count(); ++i)
{
w.select(i, 47);
}
}
在类接口和main()函数里,可以看到,包括函数本身在内的整个实现被隐藏了。代码甚至必须请求对函数的Count()。用这个方法,类执行者可以在内部执行时改变函数的数量而不影响使用这个类的代码。
在构造函数中,成员指针的初始化似乎过分指定了。是否可以这样写:
fptr[1] = &g;
因为名字g在成员函数中出现,这是否可以自动的认为在这个类范围内呢?问题是这不符合成员函数的语法,它的语法要求每个人尤其编译器能够判断将要进行什么。相似的,当成员指针被间接引用时,他看起来像这样:
(this->*fptr[i])(j);
它仍是过分指定的,this似乎多余,正如前面所讲,当他被间接引用时,语法也需要成员指针总是和一个对象绑定在一起。