C++基本功和 Design Pattern系列(4-6)

来源:互联网 发布:python 继承 编辑:程序博客网 时间:2024/05/17 22:11
======================================================
 大家请把我的文章当参考,详细内容  还请参照 权威书籍 
 <c++ programming language>如果文中有错误和遗漏,
 请指出,Aear会尽力更正, 谢谢!
======================================================
最 近实在是太忙了,无工夫写呀。只能慢慢来了。呵呵,今天Aear讲的是class.ctor 也就是constructor, 和  class.dtor, destructor. 相信大家都知道constructor 和 destructor是做什么用的,基本功能我就不废话 了。下面先说效率的问题,让我们看个简单的例子:

class SomeClass;   // forward declaration

class AnotherClass {
private:
    SomeClass SomeClassInstance;
public:
    AnotherClass(const SomeClass Para) SomeClassInstance Para; };
    ~AnotherClass();
};

也许这是很多初学者经常写出来的代码,Aear以前也写过。让我们来看看这段代码有什么问题。

首 先需要说明的是,在一个class实例化之前,所有的member都会被初始化,如果member是个class,那么那个class的 constructor就会被调用。也就是说,在运行AnotherClass的constructor之前,SomeClass的 constructor就已经运行了。接下来的代码里,SomeClassInstance又被重新执行了次 操作。也就是说,我们在给 SomeClassInstance附初值的时候,调用了2次SomeClass的method. 这个浪费也太大了,比较标准的方式是使用初始化列表, 如下:

    AnotherClass (const SomeClass Para): SomeClassInstance(Para) {};
如果有多个类成员,可以用","来分割,如:

    AnotherClass (const SomeClass Para1, UINT32 Para2):
                   SomeClassInstance(Para1),
                   SecondAttr(Para2),
                   ThirdAttr(Para3) {};
值 得注意的是, 类成员的初始化顺序和在类中的声明顺序应该一致。这个是有compiler来控制的,并不根据你在AnotherClass的 constructor中提供的初始化顺序来进行。所以,如果你想先初始化ThirdAttr,然后把ThirdAttr传到SecondAttr作为初 始化参数,是会失败的。只有改变声明顺序才会成功。

同理,在声明类变量被附初值的时候,使用拷贝构造函数,效率更高:

=====错误=====
class x1;
x1 x2;

=====正确=====
class x1(x2);

===================分割线===================

从 上面的例子可以看到,几乎所有的class,都需要提供拷贝构造函数,也就是 className(const className &)。同时 值得注意的是,如果提供了拷贝构造函数,一般也就需要提供 "="操作,也就是 className operator  (const className &),说到 operator =, 也有必要强调下implicit type conversion的问 题,这将会在以后的章节张有详细描述。至于为什么要提供 operator =,举个简单的例子:
 
class1 {
public:
    class1() new int[100]; };
    ~class1() delete[] p; };
private:
    char* p;
x1, x2;

如 果class1不提供operator =, 那么运行 x1 x2的时候,C++会运行最基本的拷贝操作,也就是 x1.p x2.p,那么在 x1被释放的时候,delete p;被执行。这时候 x2再要访问p,p已经变成非法指针了。 也许有人会说,我才不会用x1 x2这么危险的操 作,那让我们看看更加隐性的操作吧,例子如下:

void func(class1 Para) {...};

func(x1);

这 时候,c++会调用class1的拷贝构造函数,来把参数从x1里拷贝到Para,如果class1没有提供copy constructor,那么c+ +就执行简单拷贝工作,也就是 Para.p x1。当func返回的时候,Para被释放,调用 Para.~class1(),并且 delete p;那么x1.p就变成非法指针了。

这样大家就知道为什么要同时提供copy constructor和 operator =了吧。特别是在class里有指针的情况下,必须提供以上2个method。如果不想提供,可以把他们设为private,代码如下:

class1 {
...
private:
    class1 (const class1 &);
    class1 operator (const class1 &);
}
这样别人在执行 和 func()的时候就会报错了。

还有,在声明构造函数的时候,单参数的构造函数,最好都用explicit来声明,例如:

class1 {
public:
    class1(int Para) {...}
    ...
};

其中class1(int Para)是个单参数的构造函数,如果执行下列操作,如:

class1 x1 2;

的 时候,因为2不是class1,所以c++会用隐性的类型转换,也就是把2转换成class1,因此会调用class1(2),然后用operator  符值给 x1. 这种操作经常会产生很多问题。比如如果我们提供了 operator == ,那么 在 if(x1 == 2)的时候,c++也会 进行类似的操作,可能会产生我们不需要的结果。所以,对于这种单参数的constructor 最好做如下声明:

explicit class1 (int Para) {...}

这样做再执行 class1 x1 2;的时候就会报错了,explicit的意思就是C++ 的compiler不能做隐性类型转换,必须由程序员做type cast,比如:

class1 x1 static_cast<class1>(2) 才会成功。

===================分割线===================
在运行constructor的时候,值得注意的一点就是,如果在constructor里,要初始化会throw exception的代码,一定要在constructor里catch。比如:

class1 {
    class1()
    {
       pInt new int[100];
       try {
           pClass2 new pClass2;
       }catch(...)
       delete pInt; throw; };
     }
}

大家看的明白了吧,如果不catch pClass2的exception,pInt分配的内存就不会释放,因为constructor如果失败,c++是不会调用destructor的。

===================分割线===================
最后关于destructor,需要注意的是,如果是被继承的base class,destructor一定要是virtual。比如:

BaseClass ()
{
public:
    BaseClass();
    virtual ~BaseClass();
}

DerivedClass public BaseClass()
{
public:
    DerivedClass();
    ~DerivedClass();
}

BaseClass pBase static_cast<BaseClass *>(new DerivedClass());
delete pBase;

如果BaseClass的destructor是virtual,那么正确的ctor dtor调用顺序是:

BaseClass();
DerivedClass();
~DerivedClass();
~BaseClass();

如果不是Virtual,调用顺序是:

BaseClass();
DerivedClass();
~BaseClass();

也 就是说,DerivedClass的派生类不能被正确调用,这主要是因为在delete的时候c++并不知道你delete的是  DerivedClass, 因此需要把BaseClass的 dtor 设置成 virtual, 这样可以使用 vptr在 vtbl中查找  destructor,从而能够正确的调用destructor。

===================分割线===================
从上面的例子大家也看出来了,如果是派生类,那么就要调用基类的constructor,在多层次的派生类创建过程中,所以基类的constructor都要被调用。 destructor同理。因此要想提高效率,可以在关键代码短使用非派生类。

也 许有人会说,所有的constructor和destructor都被compiler inline了,但是即使是inline并且 base class的constructor中不进行任何操作,c++也要为每个类设置vptr,也是有不需要的overhead。当然,我们得到效率 的同时,失去的是可扩展性,良好的程序层次结构等等,大家要根据具体情况来权衡。

 

======================================================
 大家请把我的文章当参考,详细内容  还请参照 权威书籍 
 <c++ programming language>如果文中有错误和遗漏,
 请指出,Aear会尽力更正, 谢谢!
======================================================

继续上一章的内容,下面是经过调整后的Test Class代码:

class Test {
private:
    int internalData;
public:
    // constructor and destructor
    Test(int data 0) internalData(data) {};
    Test(const Test Para) internalData(Para.internalData) {};
    ~Test() {};
  
    // Operator overlording
    Test operator += (const Test Para1);
    Test operator (const Test Para1); 
};

Test Test::operator += const Test Para1 )
{
    internalData += Para1.internalData;
    return this;
}

Test Test::operator (const Test Para1)
{
    return Test(*this) += Para1;
}

下面我们假设要给这个Test Class添加一种新的功能,让Test Class 和 Integer之间能够进行加法操作。 也就是说可以执行下列代码:

Test x1(10);
x1 = x1 + 5;
x1 += 5;

实际上,我们不需要进行任何修改,上面的代码就能够正确执行。因为我们提供的构造函数Test(int data 0) 能够隐性的 (implicit type conversion) 把一个integer 转换成一个Temporary Test Object,然后掉用Test Test::operator (const Test Para1)。因此,上面的代码等同于:

x1 = x1.operator + (Test(5));
x1 = x1.operator += (Test(5));

Implicit Type Conversion 实际上会带来很多的麻烦,要想避免潜在的危险,最好在
Test(int data 0)前面加上explicit,表示如果对interger转换成Test,必须由程序员来控制,compiler不得进行隐性的操作。因此,要想似的 x1 = x1 + 5能够正常运行,有2种方法:

x1 = x1 + static_cast<Test>(5);

x1 = x1 + Test(5);

还有一点需要注意的是,如果不用explicit type conversion,可以运行:

x1 = x1 + 5;

但是在编译:

x1 = 5 + x1

的时候就会报错了,除非使用一个Temporary Object ,如:

x1 = Test(5) + x1;

要想使Test Class 支持 x1 = 5 + x1,最好的方法就是用helper function. 下面我们来看看Operator的另外一中定义方式。

==================分割线
==================

我们可以使用friend function 来定义Test Class 的加法运算,代码如下:

class Test {
    Test(int data = 0) : internalData(data) {};
    ...
    // 针对这个Test Class, 并不需要下面这行。
    friend Test operator + ( const Test & Para1, const Test & Para2);
};

Test operator + ( const Test & Para1, const Test & Para2)
{
    return Test(Para1) += Para2;
}

首先我们需要注意的是,Test(int data = 0)没有用explicit,也就是说可以进行隐性的类型转换,因此无论是运行:
    x1 = x1 + 5;
还是:
    x1 = 5 + x1;
都能够编译通过。

解决了基本的功能问题,让我们继续考虑一下效率。无论是在x1 = x1 + 5,还是在x1 = 5 + x1,都至少会掉用额外的constructor和destructor把5转换成Test Object,这种浪费是很没有必要的。其次允许compiler进行implicit type conversion并不是一个良好的习惯。解决这些问题的方法,就是提供专用的 operator + 来进行integer和Test object之间的加法操作,具体代码如下:

========== 支持 x1 + 5 ==========
Test operator + ( const Test & Para1, int Para2)
{
    return Test(Para2) += Para1;
}

========== 支持 5 + x1 ==========
Test operator + ( int Para1, const Test & Para2 )
{
    return Test(Para1) += Para2;
}

同时还要在class Test中加如下面2行(对于此例子并不需要,不过正常情况是需要的):

friend Test operator + ( int Para1, const Test & Para1 );
friend Test operator + ( const Test & Para1, int Para2 );

并且在constructor前面加上 explicit。当然,你也可以用Template进行定义,如下:

========== 支持 x1 + 5 ==========
template <class T>
T operator + ( const T & Para1, int Para2)
{
    return T(Para2) += Para1;
}

实际上对于 template的定义,我个人并不推荐. 首先是因为namespace的问题,到底是global namespace呢?还是一个local namespace?如果是global namespace,那不一定所有的global class 都需要 operator +,这样就提供了多余的class操作。local namespace倒是可以用,前提是所有的class都定义了 +=. 也许对于大多数class来讲,并不需要operator + 的操作。所以我觉得对于 operator 的定义,尽量少用 template (个人观点).

==================分割线==================

下面说说关于类型转换的operator. 对于一个Abstract Data Type来说,类型转换是经常用到的,比如我们前面提到的从 integer转换成 Test, 可以使用implicit type conversion 和 explicit type conversion. 但是如果我们想从Test 转换成 integer,compiler无法支持自动的类型转换,因此需要我们提供相应的operator:

class Test {
    ...
    // Type converstion from Test to int
    operator int() { return internalData; };
}

那么我们就可以执行:
    int i = Test(10);

实际上,operator int()又是一种implicit type conversion,这并是收程序员的控制。良好的程序设计,是programmer能够精确的控制每一个细微的操作。因此并不推荐使用 operator int(),好的方法是按照 < effective c++ > 中给出的那样,提供一个asInt() method,来做explicti type conversion:

============ explicti ============
class Test {
    ...
    // Type converstion from Test to int
    int asInt() { return internalData; };
}

================== Test++ & ++Test ==================

相信大家都知道 Prefix ++ 和 Postfix ++的区别是什么,下面是代码:

// Prefix
Test& operator++()
{
   
 ++internalData;
    return (*this);
}

// Postfix
Test operator++(int)
{
   
 ++*this;
    return --Test(*this); // 为了使用返回值优化,需要定义 --Test
}

我们只是简单的看下效率问题,在 Prefix中也就是 ++ 返回的是reference,没有temporary object,在 Postfix中返回的是个object,使用了Temporary。相信大家都知道了,能不使用 Test++的地方就不要使用,尽量使用 ++Test。

比如:

for( iterator i = XXX; XXX; ++i) // 不要使用 i++

对于postfix, compiler并不能保证肯定会优化成 prefix,所以写代码的时候尽量注意。

================== 其他关于Operator ==================

有些operator,并不推荐进行overload,因为会出现无法预料的情况。这些operator 包括:

&&, || , & , | , == , != , ","

举个简单的例子,如果你overload了",",那么有一个for循环如下:

for( Test x1 = x2,i = 0; ; ) {....}

到底是x1 = x2 和 i = 0呢?还是 x1 = x2.operator , (i) = 0 呢?如果overload了 & ,对于逻辑判断,x1 && x2,到底是  x1 && x2呢?还是 x1.operator & (&x2)呢?因此这些overload都会产生很多让人费解的问题。

其次,很多operator overload需要很小心的对待,这些operator 如下:

new new[] delete delete[] -> [] ()

请仔细阅读 C++ 标准,了解详细内容后,再对这些operator进行overload,不然很容易造成程序的不稳定。

好了,关于operator 就说这么多了,欢迎大家有空去我的Blog坐坐http://blog.sina.com.cn/u/1261532101下次见。

 

======================================================
 大家请把我的文章当参考,详细内容  还请参照 权威书籍 
 <c++ programming language>如果文中有错误和遗漏,
 请指出,Aear会尽力更正, 谢谢!
======================================================

今天讲的是 public inheritance, protected inheritance & private inheritance,内容不多,但是非常重要。基本的类的继承,也就是inheritance的概念大家都清楚,明确的定义不再详细说明了。先面举个例子来说明:

class People {
    ...
    Walk();
    Eat();
};

class Student : public People{
   ...
   Study();
};

注意这行:
    class Student : public People {
中的public,表明是public inheritance,如果换成protected,就是protected inheritance, private就是private inhertance. 首先需要说明的是3种inheritance在语法上相似,但是在语意上完全不同。我们先从public inheritance说起。

=====================public inheritance
=====================

public inheritance最基本的概念就是"isa" ( is a )。 简单的说,继承类也就是Derived Class "is a" Base Class. 用上面的例子来说,People是base class, Student是 derived class,所以能够推导出: “student is a people” 这句话。如果你无法推导出 "isa"的关系,那么就不应该使用public inheritance.

其次,即使是能推导出 "isa" 的关系,也必须满足2个条件,才能使用 public inheritance. 这2个条件是:

    1. 所有Base Class的属性,也就是 attribute,Derived Class都有。
    2. 所有Base Class的方法,Derived Class都应该包含。

在上面的例子中,student也是个people,所以能够Walk() 和 Eat(),因此public inheritance 是合理的。
如果满足 "isa" 但是不满足上述条件,建议使用 Delegation/Composition,具体关于Delegation和Composition,在"C++基本功和 Design Pattern系列(1)" 中有说明。让我们看下在《Effective C++》中的一个例子来说明这种情况:

class Rectangle {
    ...
    SetWidth();
    SetHeight();
};

class Square : public Rectangle {
    ...
    SetLength();
};

我们大家都知道,一个正方形Square,一定是一个长方形Rectangle,所以满足"isa"的条件。我们给Rectangle提供了SetWidth()和SetHeight()的方法。如果不考虑上面2条,只考虑 "isa",那么这个 public inheritance是合理的,但是让我们看看会出现什么问题。

在Square中我们要求长和宽必须相等,因此我们提供了SetLength(),来同时设置正方形的长和宽。但是有一位Bill小朋友无法分辨长方形和正方形,因此写出了如下代码:

    Square MySquare;
    MySquare.SetWidth(100);
    MySquare.SetLenght(200);

那么问题出现了,MySquare并不是一个Square。相信大家都明白了吧。语言的不精确性导致在设计过程中出现的错误是屡见不鲜的。因此,在public inheritance的时候要特别注意。也许有人会说,我们把SetHeight 和 SetWidth设置成Virtual然后在Square Class中重载不就可以了吗? 如果Rectangle和Square 2个class都是你来写,那么也许不会出现问题。但是如果一个非常复杂的class,包含几十个方法和几十个属性,并且由别人来写,那么你会不会仔细的阅读代码并且overlord每一个需要的方法呢?即使你这样做了,也许会带来更多的麻烦。因为有可能破坏内部数据的一致性。

让我们来看看interface inheritance的例子:

    Class Bird {
       ...
       virtual Fly() = 0;
    };

    Class Turkey : public Bird {
       ...
       Fly() { cout << "I cannt fly! Jessus....." <<endl; };
    };

    Turkey Bird0;
    ...
    Bird0.Flg();   // runtime error

首先,鸟能飞,这个没有问题,火鸡是一种鸟,这也没有问题,但是: 火鸡不能飞。问题出现了,client能够调用Turkey的Fly()方法,但是得到的确是一个 RunTime Error! 这里必须强调下:"RUNTIME ERROR!",对于游戏程序来说,一个"RUNTIME ERROR"基本上就等于程序崩溃。和out of memory同等性质。如果你玩WOW做7个小时中间不能间断的任务,然后出现一只火鸡给个RUNTIME ERROR....我想是人都会崩溃吧。

所以对于这种错误,我们要在编译的时候尽量查出来,也就是 Prefer Compile Error over Runtime Error. 通过更改类的设计,我们可以避免类似的runtime error:
    Class Bird {
       ...
    };

   
    Class UnflyableBird : public Bird{
       ...
       // no fly() here
    };

    Class Turkey : public UnflyableBird {
       ...
    };

    Turkey Bird0;
    ...
    Bird0.Flg();   // compile error....
  
所以,要想使用public inheritance,必须满足:

    1. "ISA"
    2. 所有Base Class的属性,也就是 attribute,Derived Class都有。
    3. 所有Base Class的方法,Derived Class都应该包含。


=====================private inheritance=====================

private inheritance和public inheritance最大的区别就在于,private inheritance不满足"isa"的关系。举个例子:

class People {
    ...
    Walk();
    Eat();
};

class ET: private People{
   ...
};

外星人ET是一种类似人的生物,能做一些类似人的动作,但是并不是人。从C++的语法上面来讲,下面的代码是错误的:

    People*  p = new ET();   // ERROR, ET is not a People

使用private inheritance的目的只是简单的为了代码重用。因此如果不满足public inheritance的条件,可以使用 Delegation/composition 和 Private Inheritance。 那么在什么情况下使用 private inheritance,什么情况下使用
Delegation/Composition 呢?

有2种情况是推荐使用 private inheritance的,其他的情况下,推荐使用Delegation/Composition.

情况1: 需要对Base Class中的 private/protect virtual 进行重载。比如类似Draw() 等等。

情况2: 不希望一个Base class被 client使用。

关于情况2,举个简单的例子:
如果我们不希望Base Class被别人直接使用,有2种方法,第一是:把它设置成为abstract class, 也就是包含pure virtual function. 第2种方法是把constructor 和 descturctor设置成 protected.代码如下:

class Base {
protected:
   Base(); 
   virtual ~Base();
};

class Derived : private Base {
    ...
};

Base n; // Error, Base() cannot be called
Derived m; // ok, Derived can call Base()

这样我们又可以保证n的代码可以被m使用,又可以防止 client直接调用 Base进行我们不希望的操作。

=====================protected inheritance=====================

protected inheritance和 private inheritance没有本质的区别,但是如果我们希望的 Derived Class 能够作为其他 class的基类,那么就应该使用 protected inheritance.

今天就说这么多,有空来我的Blog做客: http://blog.sina.com.cn/u/1261532101 ,下次见!

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 自考挂科10门了怎么办 高中的会考没过怎么办 毕业证和学位证丢了怎么办 大学毕业证学位证丢了怎么办 大学毕业证和学位证丢了怎么办 毕业证是士官证号码怎么办 自考本科准考证丢了怎么办 自考档案搞丢了怎么办 自考本科档案在自己手里怎么办? 本科自考准考证丢了怎么办 大学团员证丢了怎么办 大学开学团员证丢了怎么办 研究生开学没有团员证怎么办 研究生开学已经不是团员了怎么办 毕业了要搬宿舍怎么办 中专学历认证已停止怎么办 中专不做学历认证考试怎么办 大学生欠学费被扣毕业证怎么办 考警校体检没过怎么办 美国签证申请预约名字写错怎么办 当兵不从学校走怎么办 门牙崩了一小块怎么办 遇到很难过的事情怎么办 小孩子上课精力不集中怎么办 每天工作都很累压力大怎么办 重体力活搬不动怎么办 大学没参加体测怎么办 英文写的很丑怎么办 患有勃起障碍应该怎么办较好 运动过度小腿肌肉酸痛怎么办 高考有纹身是字怎么办 新生儿测听力没过关怎么办 色弱高考体检时没查出来怎么办 公司福利体检查二对半怎么办 高考体检表复印件丢了怎么办 高考体检表身高填错了怎么办 大学档案高考体检表丢了怎么办 工厂组织体检我有乙肝怎么办 我有乙肝单位组织体检怎么办? 矮腰袜子老掉怎么办 短腰袜子老下滑怎么办