C++ 的引用类型

在翻旧文的时候,发现这么一篇文章:关于一道C++笔试题的纠结,学计算机的伤不起啊。当时可能是觉得 Placement new 的语法[1]比较新鲜,所以印象比较深刻。现在则是觉得那篇文章中的笔试题挺有水准的,于是记一篇文章,特别说明这一问题——C++ 的引用类型。

C++ 的类型默认是值类型

C++ 的 class 默认是值类型的。我们常常会从内存布局的角度来看待一个值类型。默认情况下,值类型可以复制,也就是说,值类型具有一个拷贝构造函数以及一个拷贝赋值操作符。值类型对象反映的是其内容,当拷贝发生时,总是会产生两个相互独立的对象,修改其中的一个对象不会改变另一个对象的内容。

C++ 也可以定义引用类型

我们可以通过一定的设置,构造出 C++ 的引用类型,这样的类型具有多态的行为方式,从而可以支持面向对象编程。我们总是以实现多态行为为目的来看待引用类型的,比如说其基类是什么,有没有虚函数,等等。我们应当禁用一个引用类型的拷贝构造函数以及拷贝赋值操作符,并且使用虚析构函数。引用类型的对象反映的是其身份——这是对什么对象的引用?因此,引用类型也常被成为多态类型。

使用 C++ 定义引用类型

虽然C#里面的引用类型用的都是指针或者是引用,但是 C++ 中的引用类型并不受此限制。但是在传递一个引用类型的对象时,只能传递引用或指针,而不能传递值,否则与引用类型的意图不符合。

因此我们需要做的就是:

  1. 禁用拷贝构造函数。

  2. 禁用拷贝赋值操作符。

  3. 显式提供一个构造函数。

  4. 显式提供一个析构函数。

示例代码如下所示:

class MyRefType
{
private:
    MyRefType(const MyRefType &);
    MyRefType & operator=(const MyRefType &);
public:
    MyRefType() { }
    virtual ~MyRefType() { }
};

这样一来,我们就定义了一个引用类型的对象MyRefType。注意,MyRefType可以分配在栈上,也可以分配在堆上,这一点是与 C#、Java 等语言不同的。但是,如果需要传递`MyRefType`的对象,则必须传递引用或者是指针,否则将会出现编译错误:(错误信息依编译器不同而变化)

Cannot access private member declared in class MyRefType.

测试用例如下:

MyRefType CreateMyRefTypeInstance();
int main(void)
{
    MyRefType instance1;
    MyRefType instance2 = instance1;
    MyRefType instance3 = CreateMyRefTypeInstance();
    return 0;
}

其中第 19 行和第 20 行编译时期会报错。

C++11 的新语法

C++11 中提出了新的语法用于删除默认函数或者是显式提供默认函数。[2]使用了新语法的示例如下:[3]

class MyRefType
{
    MyRefType(const MyRefType &) = delete;
    MyRefType & operator=(const MyRefType &) = delete;
public:
    MyRefType() = default;
    virtual ~MyRefType() = default;
};

个人认为,使用这种方式来实现引用对象,代码可读性要上一个台阶。

但是 C++11 提出了一个叫做 Move 的新语义,可以用于转移对象的所有权。对此有何影响,还不可知。Bjarne Stroustrup 的 C++11 FAQ 中的 control of defaults: move and copy 中提到了一些这方面的 建议

禁止使用栈分配对象

下面绕来绕去又回到了那个 笔试题,即如何禁止使用栈分配对象?一般说来有两种方法:限制访问构造函数或者限制访问析构函数。并且同时,我们应该把这个对象视为一个引用类型的对象,否则我们就可以合理的创建一个在栈上的拷贝。印象中 Bjarne Stroustrup 在 The Design and Evolution of C++ 中曾经提到过,将析构函数设为 protected(未求证),就像这样:

class HeapOnlyRefType1
{
private:
    HeapOnlyRefType1(const HeapOnlyRefType1 &);
    HeapOnlyRefType1 & operator=(const HeapOnlyRefType1 &);
public:
    HeapOnlyRefType1() { }
    void destory(void) { delete this; }
protected:
    virtual ~HeapOnlyRefType1() { }
};

这样一来,由于不能使用析构函数,自然就不能在栈上创建该对象了。需要注意的是,该类型的全局变量和临时对象也不能创建,因为它们最终还是要被销毁的,但是编译器却不能使用析构函数。为了避免内存泄漏,我们使用`destory`方法来销毁这一类型的对象。

另一种方式,具有更高的灵活性,也是我比较习惯使用的一种方式,即通过禁用构造函数来限制这一对象,就像这样:

class HeapOnlyRefType2
{
private:
    HeapOnlyRefType2(const HeapOnlyRefType2 &);
    HeapOnlyRefType2 & operator=(const HeapOnlyRefType2 &);
protected:
    HeapOnlyRefType2() { }
public:
    static HeapOnlyRefType2 * CreateInstance(void) { return new HeapOnlyRefType2(); }
    virtual ~HeapOnlyRefType2() { }
};

这样一来,今后我们还可以通过修改实例方法,来控制该类型对象的创建。比如说使用单例模式(Singleton Pattern)或者其他创建模式,或者使用一些对象策略,比如说缓存,等等。

参考资料


2012/12/3 补充:现在最新版本的Visual Studio 2012 Update 1 已经支持[3]中所提到的语法了。


3. Visual Studio 的最新版本 2012 仍然不支持这一语法,但是GCC从4.4开始就支持这一语法了。因此请使用GCC4.4以上的版本来实践这一语法。