首页 > 代码库 > C++必知必会(3)

C++必知必会(3)

条款26操作符函数查找

class X

{

       public:

              Xoperator %( const X& ) const;             //二元取余操作

              XmemFunc1( const X&);

              voidmemFunc2();

};

可以采用中缀或函数调用语法来调用这个重载的操作符:如下

X a,b,c;

a = b%c;        //采用中缀语法调用成员操作符%

a = b.operator %(c);   //成员函数调用语法

a = b.memFunc1(c);   //另一个成员函数调用

然而,对重重爱操作符中的中缀调用的处理机制与此不同:

X operaator %(const X &, int);        //非成员操作符声明

//…

void X::memFunc2()

{

       *this% 12;           //调用非成员操作符%

       operator%(*this, 12);       //错误!实参太多

}

第一个对operator %的中缀调用,将会匹配非成员的那一个。这不是一个重载的例子,而是编译器在两个不同的地方查找候选函数。第二个对operator %的非中缀调用准许标准的函数查找规则,因而匹配那个成员函数,但是这里我们会遇到一个错误,因为我们试图将三个实参传递给一个二元函数。(记住,对成员函数的调用存在一个隐式的实参this!)

条款27 能力查询

能力查询通常是通过对不相关的类型进行dynamic_cast转换而表达的。

Class Shape{

};

class Rollable{

};

class Square : public Shape{

};

class Wheel : public Rollable, publicRollable{

};

Shape* s = getSomeShape();

Rollable *roller = dynamic_cast<Rollable*>(s);

       这种dynamic_cast用法通常称为很像转型,因为它视图在一个类成次结果中执行横向转换。如果s所指对象时一个Square,那么dynamic_cast将会失败,从而知道s所指向的Shape不是Rollable。如果s指向的是一个Circle或从Rollable派生下来的其他Shape,那么转型将会成功。

条款28 指针比较的含义

在C++中,一个对象可以有多个有效的地址,因此指针比较不是地址问题,而是对象同一性问题。请看以下例子。

#include <stdio.h>
 
class Shape{
public:
    int a;
};
class Subject{
public:
    int b;
};
class ObservedBlob : public Shape,public Subject
{
};
int main()
{
    ObservedBlob* ob = newObservedBlob();
    Shape* s = ob;
    Subject* subj = ob;
    void* v =subj;
    printf("%d\n",ob);
    printf("%d\n",s);
    printf("%d\n",subj);
 
    if(subj ==ob)
       printf("subj= ob\n");
    else
       printf("subj!= ob\n");
 
    if(subj == v)
       printf("subj= v\n");
    else
       printf("subj!= v\n");
 
    if(ob == v )
       printf("ob= v\n");
    else
       printf("ob!= v\n");
 
    return 0;
}


运行结果如下:

3759208

3759208

3759212

subj = ob

subj = v

ob != v

请按任意键继续. . .

由于ob,s,subj都指向同一个对象,因此编译器必须区别ob与s和subj的比较结果均为true。编译器通过将参与比较的指着值之一调整一定的偏移量来完成这种比较。例如,表达式:

ob == subj;

可能被转化翻译为

ob ? (ob+delta ==ubj) : (subj == 0)

所以本例结果为ob =subj。

 

但一般而言,当我们处理指向对象的指针或引用时,必须小心避免丢失类型信息。指向void的指针是常见的错误。如上,一旦通过将对象的指针复制到void*将会去掉对象中包含的地址类型信息,编译器别无他法,只好求助于原始地址比较。所以才会出现以上结果。
subj = v

ob != v

条款29虚构造函数与Prototype模式

在点餐时,想点一份别人再吃的事物,可用如下程序实现:

class Meal{
public:
       virtual~meal();
virtual voideat() = 0;
virtual Meal*clone() const = 0;
       //…
};
class Spaghetti : publicMeal{
public:
       Spaghetti(const Spaghetti &);        //复制构造函数
       voideat();
       Spaghetti*clone() const
{
       return new Spaghetti( *this );  //调用复制构造函数
}     
};
 
constMeal* m = thatGuyMeal();   //不管他正在吃什么
Meal*myMeal = m->clone();        //我都要一份和他一样的!


事实上,我们完全有可能所点的事务没有一点了解。

这里实际上使用了Prototype模式,对一个类型的存在不知情并不会对创建该类型的对象造成任何障碍。

而所谓的虚构造函数,并不是真正的虚构造函数,但是生成对象的一份复制品通常涉及到通过对一个虚函数对其类的构造函数的间接调用,效果上也算得上是虚构造函数了。

条款30 Factory Method模式

Factory Method的本质在于,基类提供一个虚函数挂钩,用于产生适当的产品。每一个派生类可以重写继承的虚函数,为自己产生适当的产品。实际上,我们具备了使用一个位置类型的对象来产生另一个未知类型对象的能力。

条款31协变返回类型

一般来说,一个重写的函数必须与被它重写的函数具有相同的返回类型。

但是,这个规则对于“协变返回类型”不严格。也就说,如果B是一个类类型,并且一个基类虚函数返回B*,那么一个重写的派生类函数可以返回D*,其中的D公有派生于B。如果基类虚函数返回B&,那么一个重写的派生类函数可以返回一个D&。

条款32禁止复制

可以通过将复制操作(复制构造函数和复制赋值函数)声明为private同时不为之提供定义而做到。

条款33制造抽象基类

抽象基类用于目标问题领域的抽象概念,创建这种类型的喜爱那个是没有什么意义的。我们通过至少声明一个纯虚函数使得一个基类成为抽象的,编译器将会确保无人能够创建该抽象基类的任何对象。例如:

class ABC{

public:

       virtual ~ABC();

       virtual void anOperation() = 0;              //纯虚函数

};

然而,有时找不到一个可以成为纯虚函数的合理候选者,但仍希望类的行为像个抽象类。可用如下方法实现。

方法1:可以通过确保类中不存在公有构造函数来模拟抽象基类的性质。这需要我们显式地声明默认构造函数和复制构造函数,并且这两个构造函数应该是受保护的,这是为了既允许派生类的构造函数使用它们,同时阻止创建独立的ABC对象。

class ABC

{

public:

       virtual ~ABC();

protected:

       ABC();

       ABC( const ABC& );

};

 

class D : publicABC

{

//…

};

 

void func1(ABC);

void func2(ABC&);

ABC a;   //错误!默认构造函数是受保护的

D d;        //OK

func1(d);       //错误!复制构造函数是受保护的、

func2(d);       //ok,不涉及复制构造函数

方法2:另一种使一个类成为抽象基类的方式是将该类的一个虚函数指定为纯虚函数。通常来说,析构函数是一个不错的选择:

class ABC{

public:

       virtual ~ABC() = 0;

};

ABC::~ABC(){…}

注意,在这个例子中,很有必要为该纯虚函数提供一个实现,因为派生类的析构函数将会隐式地调用其基类的析构函数(注意,从一个派生类析构函数内部对一个基类析构函数的隐式调用,总是非虚拟的)。
       方法3:受保护的析构函数和受保护的构造函数发挥的效果基本相同,不过前者的报错发生于对象离开作用域时或被显示销毁时,而不是在对象创建时。

class ABC{

protected:

       ~ABC();

public:

       //…

};

 

void someFunc()

{

       ABC a;          //此时不会报错

       ABC *P = new ABC;    //此时不会报错

       delete p;       //错误!析构函数时受保护的

              //错误!隐式调用a的析构函数

}

条款34禁止或强制使用堆分配

有时候,指明一些特定类的对象不应该被分配到堆上是个好主意。通常这是为了确保该对象的析构函数会得到调用。维护对本体对象的引用计数的句柄对象就属于这种对象。具有自动存储区的类的局部对象,其析构函数会被中调用,具有静态存储区的类的对象亦然。

指明对象不应该被分配到堆上的方法之一,是将其对内存分配定义不合法:

class NoHeap

{

public:  

       //….

protected:

       void* operator new(size_t){return 0;}

       void operator delete(void*){}

};

任何在堆上分配一个NoHeap对象的习惯性尝试,都会产生编译器错误。

NoHeap * nh =new NoHeap;  //错误!NoHeap::operatornew是受保护的

//…

delete nh;     //错误!NoHeap::operatordelete是受保护的

之所以给出operator new和operator的定义,是因为在一些平台上它们可能会被构造和析构函数调用。处于同样原因,我们将其声明为protected是因为它们可能被派生类的构造函数和析构函数隐式的调用。如果NoHeap不打算用作基类,那么这两个函数也可以声明为private。

同时,还要注意阻止堆上分配NoHeap对象的数组。在这种情况下,只要将array new和array delete声明为private且不予定义即可,类似于禁止复制操作的方式。

class NoHeap{

public:

       //…

protected:

       void* operator new(size_t ){return 0;}

       void operatoe delete(void*){}

private:

       void* operator new[](size_t);

       void operator delete[](void*);

};

当然,在某些场合下,我们可能鼓励使用对分配,只需要将析构函数声明为private即可。

class OnHeap{

       ~OnHeap();

public:

       void destroy()

{

       delete this;

}

};

当对象的名字离开其作用域时,任何一个声明自动或静态OnHeap对象的尝试,都会产生一个隐式的析构函数调用:

OnHeap oh1;       //错误!隐式调用私有析构函数

void aFunc()

{

       OnHeap oh2;

       //…

       //错误!隐式调用oh2析构函数

}