《Essential C++》读书笔记(六)

来源:互联网 发布:淘宝动态导航怎么做 编辑:程序博客网 时间:2024/05/01 05:25

第五章 面向对象编程风格

上一章是基于对象,而这一章是面向对象了。基于对象只是单独的类,无法指出类间的关系,类间的关系有赖于“面向对象编程模型”加以设定。
当书中提到“是一种”的时候,我想到了“门、纲、目、科、属、种”,若我们的知识仅限于上一章的话,那么我们对于具有不同属性的同一类事物的class的编写将会十分麻烦,不论它们是否有相同的属性或行为,我们都要写其独立的代码。若是通过继承机制将一组各自都具有的属性与行为继承下来,这样我们的代码编写便不会那么费劲了。

5.1 面向对象编程概念

主要特性:继承、多态
继承:使我们得以将一群相关的类组织起来,并让我们得以分享其间的共通数据和操作行为。
我感觉书上说的都很重要,但是我不想抄书啊。
父类定义了所用子类的共通的对外接口和私有实现内容。每个子类都可以增加或改写继承而来的东西,以实现它自身的独特行为。
抽象基类应该是一个继承体系中最根本的类。它应该定义的是各派生类的共通操作行为,其可能并不代表一个实际存在的对象,仅仅是为了设计上的需要而存在。但这个抽象的添加物十分关键。
设计类时应该把继承体系的图标画出来,以便查看。
c++中类的多态于动态绑定机制依赖于指针和引用,而不是对象。
静态绑定和动态绑定是有很大差别的。

5.2 漫游:面向对象编程思维

书永远都是最重要的啊。
书中的例子这次是很好,反正我看的是十分明白。
默认情况下,member function的决议程序皆在编译时期静态进行。若要令其在执行期动态进行,就得在它的声明式前加上virtual关键字。
当程序定义出一个派生对象时,基类和派生类的constructor都会被执行起来。当派生对象被摧毁时,基类和派生类的destructor也都会被执行(但次序相反)。
当时看到这里的时候有了点疑问,我把当时的笔记都写下了:
书中的例子是一个3层的类体系。以LibMat做根本类,派生出Book,从Book派生出AudioBook。
class LibMat{
public:
LibMat(){ cout<<"LibMat::libMat() default constructor!\n";}
virtual ~LibMat(){ cout<<"LibMat::~LibMat() destructor!\n"; }
virtual void print() const{ cout<<"LibMat::print()--I am a LibMat object!\n"; }
};
//首先一个疑问是:为什么构造函数不virtual而析构函数virtual了?
测试函数non-member function print()
void print (const LibMat &mat){
cout<<"int global print()::about to print mat.print()\n";
//下一行回一句mat实际指向的对象决议该执行哪个print()member function
mat.print();
}
//第二个疑问,若将参数中的LibMat改为Book,而后传递的实参是AudioBook,应该也可以吧?
调试代码:
cout<<"\n"<<"Creatirng a LibMat object to print()\n";
LibMat libmat;
print(lib mat);
追踪结果:
Creating a LibMat object to print()
//建构LibMat libmat
LibMat::LibMat() default constructot;
//处理print(libmat)
in global print():about to print mat.print()
LabMat::print()--I am a LibMat object!
//析构LibMat libmat
LibMat::~LibMat() destructor!
书中提了一句Default constructor 的调用乃是紧跟在libmat 的定义行为之后。
下面来定义Book类,为了清楚表示这个新类乃是继承自一个已存在的类,其名称之后必须接着一个冒号:,然后紧跟着关键词public和积累的名称(public表示公有继承,另两种继承方式,private和protected这里没讲)。
class Book:public LibMat{
public:
Book(const string &title,const string &author):_title(title),_author(author){
cout<<"Book::Book("<<_title<<","<<_author<<")constructor\n";
}
virtual ~Book(){
cout<<"Book::~Book()destructor!\n";
}
virtual void print()const{
cout<<"Book::print()--I am a Book object!\n"<<"My title is :"<<_title<<'\n'<<"My author is :"<<_author<<endl;
}
const string& title()const{return _title;}
const string& author()const{return _author;}
protected:
string _title;
string _author;
};
//被声明为protected的所用成员都可以被派生类直接取用,除派生类之外,都不得直接取用protected成员。
测试代码:
cout<<"\n"<<"Creating a Book object to print()\n";
Book b("The Castle ","Franz kafka");
print(b);
追踪结果:
Creating a Book object to print()
//建构Book b
LibMat::LibMat() default constructor!
Book::Book(The Castle,Franz Kafka) constructor
//处理print(b)
in global print():about to print mat.print()
Book::print()--I am a Book object!
My title is:The Castle
My author is:Franz Kafka
//析构Book b
Book::~Book() destructor
LibMat::~LibMat() destructor!
书上说当程序定义出一个派生对象时,基类和派生类的constructor都会被执行起来,当派生对象被摧毁时,基类和派生类的的destructor也都会被执行起来(但次序颠倒)。
既然都执行,为啥一个virtual一个没有呢?
以下是AudioBook的定义:
class AudioBook:public Book{
public:
AudioBook(const string &title,const string &author,const string &narrator):Book(title,author),_narrator(narrator){
cout<<"AudioBook:AudioBook("<<_title<<","<<_author<<","<<_narrator<<")constructor\n";
}
~AudioBook()
{  cout<<"AudioBook::~AudioBook()destructor!\n";}
virtual void print () const{
cout<<"AudioBook::print()--I am an AudioBook object!\n"
//注意以下直接取用继承而来的data member:_title和_author
<<"My title is:"<<_title<<'\n'<<"My author is:"<<_author<<'\n'<<"My narrator is:"<<_narrator<<endl;
}
const string& narrator() const{return _narrator;}
protected:
string _narrator;
};
检测代码:
cout<<"\n"<<"Creating an AudioBook object to print()\n";
AudioBook ab("Man Without Qualities","Robert Musil","kenneth Meyer");
print(ab);
追踪结果:
Creating an AudioBook object to print()
//构造AudioBook ab
LibMat::LibMat() default constructor!
Book::Book(Man Without Qualities ,Robert Musil)constructor
AudioBook::AudioBook (Man Without Qualities,Robert Musil,Kenneth Meyer)constructor
//print(ab)的决议过程
in global print():about to print mat.print()
//
AudioBook::print()--I am an AudioBook object!
My title is:Man Without Qualities
My author is:Robert Musil
My narrator is:Kenneth Meyer
//析构
AudioBook::~AudioBook() destructor!
Book::~Book() destructor!
LibMat::~LibMat() destructor!
使用派生类时不需刻意区分“继承而来的成员”和“自身定义的成员”,两者的使用完全透明。

实在忍不住看了一眼《c++ primer》,其中有句话“除了构造函数之外,任意非static成员函数都可以是虚函数”,很好,虽然没理解原因,但是知道这个规定了。要搁置争议,共同发展啊。
对于派生类构造函数的运用机制,我有一点猜想,当定义一个派生类对象时,对于其基类的构造函数和自身的构造函数都会调用,类必须有构造函数,不论是自己定义的还是编译器帮着定义的,因为数据成员是在构造函数里定义,才能在内存中开辟空间。若基类的数据成员是private,那么派生类的构造函数无法将其定义,只能是基类自己的构造函数定义之。所以定义派生类时,编译器一定会调用基类的构造函数和派生类的构造函数。若派生类的构造函数未调用基类的构造函数,则编译器在派生类的构造函数前调用基类的默认构造函数。若是派生类的构造函数调用了其基类的构造函数,编译器便不会再调用基类的constructor function。
写了一小段代码看了下,有点乱,看不看都行,反正跟上面的结果一样:
#include<iostream>
using namespace std;
calss a{
public:
a(int i=0,string s="s"):_val(i),_str(s){
cout<<"a的默认构造函数 _val="<<_val<<"_str="<<_str<<endl;
}
(virtual) ~a(){    //表示virtual加或不加
cout<<"a的析构函数”<<endl;
}
protected:
int _val;
private:
string _str;
};

class b:public a{
public:
b(int i,string s):_val(i),_str(s) (,a(i,s)){   //表示 ,a(i,s)  加或不加
cout<<"b的构造函数 _val="<<_val<<"_str="<<_str<<endl;
}
~b(){
cout<<"b的析构函数"<<endl;
protected:
int _val;
private:
string _str;
};



1.测试代码  当析构函数不加virtual时,  ,a(i,s)也不加时
b kk(19,"kk");
b *tt=new b(9,"oo");
delete tt;
跟踪:
a的默认构造函数_val=10 _str=s
b的构造函数 _val=19 _str=kk
a的默认构造函数 _val=0 _str=kk
b的构造函数 _val=9 _str=oo
b的析构函数
a的析构函数
b的析构函数
a的析构函数

证明了一点,派生类对象定义时会先调用其基类的默认构造函数

2.测试代码  当析构函数不加virtual,加 ,a(i,s)  时
b  kk(19,"kk");
跟踪:
a的默认构造函数 _val=19 _str=kk
b的构造函数 _val=19 _str=kk
b的析构函数
a的析构函数

说明了当派生类的构造函数显示调用基类的构造函数时,覆盖了其类默认构造函数的调用,必然的么。

3.测试代码 当析构函数不加virtual,不加 ,a(i,s) 时
a *tt=new b(9,"oo");
delete tt;
跟踪:
a的默认构造函数 _val=10 _str=s
b的构造函数 _val=9 _str=oo
a的析构函数

4.测试代码  当析构函数加virtual,不加 ,a(i,s) 时
a *tt=new b(9,"oo");
delete tt;
跟踪:
a的默认构造函数 _val=10 _str=s
b的构造函数  _val=9  _str=oo
b的析构函数
a的xigouhs

结合1,3,4 对于delete tt; 的结果发现:若析构函数不加virtual,那么会根据指针的类型运行析构函数,因为此时析构函数不会发生执行期决议了,而是编译时就决议好的了。virtual关键字声明该函数应该在执行期被决议。

5.3不带继承的多态

这是通过编程技巧获得的,不是由程序语言先天赋予的。作者都说了,这样是不好的,这个破例子当初就看的我头疼。

5.4定义一个抽象基类

我对于抽象基类的理解是 其应该包含的是子类所具有的共通操作与子类所具有的相同属性。而属性一定有可变的和不可变的。对于可变的应该为protected。而对于不可变得应设为private,定义一个public返回其值,不可变得尽可查询不可更改。
书中给了几个定义抽象类的步骤:
定义抽象类的第一个步骤就是找出所有子类共通的操作行为:
第二步便是设法找出哪些操作行为与型别相依——也就是说,有哪些操作行为必须根据不同的派生类而又不同的实现方式。这些操作行为应该成为整个类继承体系中的虚拟函数。
注意static member function 无法被声明为虚拟函数。
第三步,便是试着找出每个操作行为的存取层级。
说过抽象基类很多时候只是提供一个共通的接口,而对于该接口的不同定义应由不同的派生类实现,此时该虚函数应设定为纯虚函数。
virtual void gen_elems(int pos)=0;
对于基类含有纯虚函数的派生类,若想定义对象,必须为纯虚函数提供定义。书中提了个”派生类子对象”,真是让我浮想联翩啊。
根据一般规则,凡基类定义有一个(或多个)虚拟函数,应该要将其destructor声明为virtual。
还举了个例子,说明了一下:
num_sequence *ps=new Fibonacci(12);
delete ps;
下面是书中的解释:
ps是基类num_sequence的指针,但它实际上指向派生类Fibonacci对象。当delete表达式被施行于该指针身上时,destructor会先施行于指针所指的对象身上,于是将此对象占用的内存空间归还给程序的自由区域。还记得吗,non-virtual函数在编译期便已完成决议,根据该对象被调用是的型别来判断。
于是,这个例子中,通过ps调用的destructor一定是Fibonacci destructor,不是num-sequence destructor。正确的情况应该是“根据实际对象的型别选择调用哪个destructor”,而此决议操作应该在执行期进行。为了促成正确行为的发生,我们必须将destructor声明为virtual。

5.5定义一个派生类

本节第一段便让我浮想联翩。
“派生类由两部分组成:一是基类所构成的子对象,由基类的non-static data member——如果有的话——组成,二是派生类的部分(由派生类的non-static data member 组成)。”
发人深思啊,第一个强调了只有data member,第二个让我想到了构造函数。基类部分由基类的构造函数构造,派生类部分由派生类构造函数构造。
当我们通过基类实现动态绑定时,只能调用基类提供之接口。
在基类中指明为virtual的函数会一直保留其virtual的特性。
派生类的虚拟函数必须精确吻合基类中的函数原型。在类本身之外对虚拟函数进行定义时,不需指明关键词virtual。
一般来说,继承而来的public成员和protected成员,不论在继承体系中的深度为何,都可被视为派生类自身拥有的成员。以下是primer中的说明:
如果是共有继承,基类成员保持自己的访问级别:基类的public成员为派生类的public成员,基类的protected成员为派生类的protected成员。
如果是受保护继承,基类的public和protected成员在派生类中为protected成员。
如果是私有继承,基类的所有成员在派生类中为private成员。
无论派生列表中是什么访问标号,所有继承父类的类对父类中的成员具有相同的访问。派生访问标号将控制派生类的用户对从父类继承而来的成员的访问。
当我们清楚要调用哪个类的函数时,不必等到执行期再确认,调用时可在函数名前加上class scope。这样在编译时就会决定了。
每当派生类有某个member与其基类的member同名时,便会遮蔽住基类的那份member,此时对于该名字的使用依赖于指针或引用的类型。
non-virtual member function中含有virtual member function,同样可实现动态绑定,依赖于指针所指对象而不是指针型别。
验证代码如下:
#include<iostream>
using namespace std;
class a{
public:
virtual void ret_str(){
cout<<"aaaaaa"<<endl;
}
void bb(){
ret_str();
}
};
class b:public a{
public:
virtual void ret_str(){
cout<<"bbbbbbbb"<<endl;
}
};

int main(){
a *kk=new b;
kk->bb();
return 0;
}

5.6运用继承体系

过~~~~~

5.7基类应该多么抽象

本节的思想与我的一样就是抽取共通的行为和属性放于基类中。

5.8初始化、析构、复制

这一节很好,完全解释了我前几节的疑问,感觉这一节都很重要,我只记那些我感觉格外重要的吧。
若我们没定义,编译器便会为类定义默认构造函数、默认的复制构造函数、默认的复制操作符。派生类对于上三个的使用相同。
派生类对象的初始化行为,包含调用其基类之constructor,然后再调用派生类自己的constructor,这个过程有助于我们了解,派生类对象之中其实含有多个子对象:由基类constructor初始化的“基类子对象”,以及由派生类constructor所初始化的“派生类子对象”。

5.9在派生类中定义一个虚拟函数

如果我们决定改写基类所提供的虚拟函数,那么派生类所提供的新定义,其函数型别必须完全符合基类所声明的函数原型。若不然,将视为派生类新定义的函数。而并未用来覆写基类所提供的同名函数。这一规则有个例外:当基类的虚拟函数返回某个基类形式(pointer或reference)时:派生类中的同名函数可以返回该基类所派生出来的型别。

虚拟函数的静态决议:
在两种情况下,虚拟函数机制不会出现预期的行为:(1)在基类的constructor和destructor内,此时的派生类的data members尚未初始化或已被释放,所以在基类的constructor和destructor中,派生类的虚拟函数绝对不会被调用。(2)当我们使用的是基类的对象,而非基类对象的pointer或reference时。谨记c++中的多态依赖于基类的pointer和reference。若将派生类对象传给一个基类对象,只会把派生类对象的基类子对象传递过去,此时通过基类对象调用的virtual member function不会发出动态绑定,而只是调用基类的virtual member function。

5.10执行期的型别鉴定机制

这一节的内容在《c++ primer》中是特殊工具里的,没看过。
typeid运算符是RTTI的一部分(另一个是dynamic_cast< >),由程序语言本事支持。它让我们得以查询多态化的class pointer 或class reference,获得其所指对象的实际型别。
#include<typeinfo>
inline const char* num_sequence::what_am_i()const
{ return typed(*this).name();}
typeid是c++中的关键字,其会返回一个type_info对象。其中存储着与型别相关的种种信息。该对象的name()函数会返回一个const char*,用以表示类名称,而该类名称的具体表示法由编译器的不同而不同。我试了下:
typeid(int).name();
在我的GNU编译器下返回i。
dynamic_cast也是一个RTTI运算符,它会进行执行期检验操作,如:dynamic_cast<Fibonacci*>(ps);检验ps所指对象是否属于Fibonacci类,如果是转换操作便会发生,若不是,dynamic_cast运算符返回0.

好累啊!