标签:
最近在看 C++,再细读关于面向对象的一些知识点,好记性不如烂笔头,就归类总结一下。
面向对象中,继承是非常重要的基础概念。从已有的类派生出新的类,就可对原有的类进行扩展和修改。
称已有的类为基类,继承出来的类叫做派生类。
什么时候使用继承呢? 如果两个对象是is-a关系,则可以用继承。
比如水果与苹果。苹果 (is-a) 水果,这时候 水果是基类,苹果则是水果的派生类。
如果不是(is-a)关系,最好不要用继承。比如下面的关系例子。
has-a 午餐 (has-a) 水果,但是午餐 !(is- a )水果。所以不合适继承
is-like-a 敌人(is-like-a)豺狼,但是敌人 !(is- a )豺狼。所以不适合继承。
is-implemented-as-a 数组(is-implemented-as-a)堆栈,但是 数组 !(is- a )堆栈。所以不适合继承。
uses-a 计算机(uses-a)打印机,但是 计算机 !(is- a )打印机。所以不适合继承。
继承实现很简单,但是要管理好继承类之间的关系,却是件比较复杂的事情。
涉及到构造析构函数,虚函数&虚函数表,静态联编&动态联编,抽象基类,动态内存分配等。
如果不了解这些机制,设计的类之间就会出现一些和自己原有意图违背的运行错误,以及内存泄漏。
继承的基本实现非常简单。语法就是定义类时,定义类class MyClass的后面 接上": 修饰符 基类名"即可。
修饰符有 public, private, protect 3种。这3个修饰符分别表示了C++的3种继承方式:公有继承,私有继承,保护继承。
比如SimpleClass继承了BaseClass, 「class SimpleClass : public BaseClass」并且修饰符为public,表明是公有继承。
公有继承也是最常用的方式。
关于修饰符,遵循一个基本规则,不论哪种继承方式,基类的私有成员,私有函数在派生类中都是不可见的。
想访问他们,只能通过基类提供公有或者保护方法去访问。
通过3种继承方式,原有基类的成员访问属性也会发生变化。
公有继承:派生类公有继承基类后,基类的成员和函数的访问限制在派生类中不变。
基类原来是public,到了派生类里还是public。
私有继承:派生类私有继承基类后,基类的成员和函数的访问限制在派生类全部变成私有。
所以如果私有继承一个基类,就相当于完全隐藏了基类中的所有成员和函数。
保护继承:派生类私有继承基类后,基类的成员和函数的public访问限制在派生类全部变成protect。
基类中的private还是继续保持private。
关于protect属性的注意点,类数据成员一般推荐用private,而不用protect或者public。
理由是用protect的话,派生类可以直接访问修改基类的数据成员。
成员函数用protect限定比较有用,使此成员函数只对派生类公开,对公众保持隐秘。
派生类需要有自己的构造函数,如果你没有看到构造函数,那只是编译器悄悄用默认的构造函数了。
派生类一定会调用基类的构造函数! 那么调用基类的那个构造函数呢? 答案很简单,你指定哪个就是哪个,如果你太懒不指定,那就调用默认的构造函数。
那如何指定呢?用成员初始化列表句法即可。
比如有一个艺术家类,记录艺术家的姓名,年龄。艺术家类派生出一个画家类。
画家除了有艺术家的姓名,年龄的基本属性外,还设定一个画作类别属性,表示画家最擅长的画是油画,中国画,水彩画之中的某一种。
#ifndef __CJiaJia__Artist__ #define __CJiaJia__Artist__ #include <iostream>
using namespace std; enum {LIM = 20}; class Artist { private: char firstname[LIM]; //所有艺术家都应该有一个姓名 char lastname[LIM]; unsigned int age; //所有艺术家都有自己的年龄 public: Artist (const char *fn = "none", const char *ln = "none", unsigned int age = 5); //构造函数 void showName() const; //显示艺术家的姓名 int getAge() const { return age; }; //获得艺术家的年龄 void setAge(int age) { this->age = age; }; //设定艺术家的年龄 }; class Painter : public Artist { private: char category[LIM]; //显示画家的画作种类。如水墨画,油画,水彩画等 public: Painter(const char *ct, const char *fn, const char *ln, unsigned int age); //构造函数1 Painter(const char *ct, const Artist & art); //构造函数2 char getCategory() const; //取得画家的绘画种类 void setCategory(const char *ct); //设定画家的绘画种类 };
#endif /* defined(__CJiaJia__Artist__) */
#include "Artist.h" Artist::Artist (const char *fn, const char *ln, unsigned int ag) { strncpy(firstname, fn, LIM - 1); firstname[LIM - 1] = ‘\0‘; strncpy(lastname, ln, LIM - 1); lastname[LIM - 1] = ‘\0‘; age = ag; } void Artist::showName() const { cout << firstname <<" "<< lastname<< endl; } Painter::Painter(const char *ct, const char *fn, const char *ln, unsigned int age): Artist( fn, ln, age) { strncpy(category, ct, LIM - 1); category[LIM - 1] = ‘\0‘; } Painter::Painter(const char *ct, const Artist & art): Artist(art) { strncpy(category, ct, LIM - 1); category[LIM - 1] = ‘\0‘; }
艺术家类的构造方法里,指定了艺术家的姓名,年龄。画家类同样拥有姓名,年龄,还特别记录了擅长的画作类别。
画家类的构造函数1里,用初始化成员列表显式调用艺术家类的构造函数 Artist( fn, ln, age )。
Painter::Painter(const char *ct, const char *fn, const char *ln, unsigned int age): Artist( fn, ln, age)
画家类的构造函数2里,将调用Artist类的复制构造函数。在这个例子里Artist类没有显式定义复制构造函数,所以调用其默认的复制构造函数进行浅拷贝即可。
关于复制构造函数,如果Artist类里面有成员变量指针通过new申请了内存,则需要显式定义Artist类的复制构造函数以对此成员进行深拷贝。
Painter::Painter(const char *ct, const Artist & art): Artist(art)
派生类可以使用基类的非私有方法。
Painter chinesePainter("中国画", "潘", "天寿", 74);
chinesePainter.showName();
基类指针可以在不进行显式类型转换的情况下指向派生类对象。基类引用可以在不进行显式类型转换的情况下引用派生类对象。
这叫向上强制转换。画家是艺术家,但是艺术家不是画家。这和现实逻辑是一样的。
Painter chinesePainter("中国画", "潘", "天寿", 74);
Artist & rt = chinesePainter;
Artist * pt = &chinesePainter;
rt.showName();pt->showName();
同一个方法在派生类和基类中可以有不同的行为,取决于调用该方法的对象。
C++有两种重要的机制实现多态公有继承。
- 在派生类中重新定义基类的方法
- 使用虚方法
比如上面的painter类,声明一个基类的方法。
void showName() const; //画家类也定义一个显示姓名的方法,同时显示画家的画作类别
void Painter::showName() const;
{
cout << firstname<< " "<< lastname<< "擅长" << category << endl;
}
如果不showName不指定为virtual 方法,那么下面代码运行情况如下:
Artist chineseArtist("齐", "白石", 93);
Painter chinesePainter("中国画", "潘", "天寿", 74);
Artist & art1_ref = chineseArtist;
Artist & art2_ref = chinesePainter;
//如果showName不是vitrual方法
art1_ref.showName(); //使用 Artist::showName()
art2_ref.showName(); //使用 Artist::showName()
//如果showName是vitrual方法
art1_ref.showName(); //使用 Artist::showName()
art2_ref.showName(); //使用 Painter::showName()
如果一个方法是virtual虚方法,程序会根据引用或者指针指向的对象的类型来选择方法。
如果一个方法不是virtual虚方法,程序会根据引用或者指针的类型来选择方法。
在基类中如果将方法声明为虚方法,在派生类中即使不明确指定该方法为虚方法,也是虚方法。
派生类方法中若要调用基类的方法,加上域限定修饰符和该方法名调用即可。
按照惯例,基类应该包含一个虚拟析构函数!理由是为了调用相应对象类型的析构函数,然后基类的析构函数会被自动调用。
如果派生类包含了执行某些操作的析构函数,则基类必须有虚拟析构函数,即使该析构函数不执行任何操作。
程序调用函数时,编译器决定使用哪个执行代码块。根据函数调用去执行特定的代码块被称为联编。
在编译过程中,就能完成的联编称为静态联编。
但是由于c++里有虚函数的存在,编译时期不能确定使用哪个函数,编译器必须生成在程序运行时选择正确的虚方法的代码,
这被称为动态联编。
什么时候用动态联编,什么时候用静态联编呢? 非虚方法用静态联编,虚方法用动态联编。
大多数时候,动态联编很好,它可以让程序选择为特定类型设计的方法。
但是编译器默认还是使用静态联编。理由在于效率,为了使程序在运行阶段决心决策,编译器必须采取一些方法来跟踪基类指针(虚函数表),增加额外的开销。
大神说C++的指导原则之一就是,不要为不使用的特性付出代价(内存或者处理时间)。
所以如果要在派生类中重新定义基类的方法,则将它设置为虚方法,否者则应该设为非虚方法。
编译器处理虚函数的方法,就是给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。
这个数组就称为虚函数表(virtual function table, vtbl)。虚函数表中存储了为类对象进行声明的虚函数的地址。
可以看出虚函数定义得越多,数组就越大。调用虚函数时,编译器会到表中去查找函数的地址。
如果在画家类中重新了如下的方法 void showName(int type) const ,那么Artist类的showName() 将被隐藏。
画家类调用showName()时,就会出现编译错误。
void showName(int type) const; //画家类也定义一个显示姓名的方法,同时显示画家的画作类别
void Painter::showName(int type) const;
{
cout << firstname<< " "<< lastname<< "擅长" << category << endl;
}
所以总结2条经验规则:
1. 重新定义继承的方法应该和基类的原型完全相同。 例外情况是函数的返回值如果是基类的引用或指针,将其修改为指向派生类的引用或指针是OK的。
这个特性叫返回类型协变( covariance of return type)。
2. 如果基类中的函数重载了,派生类中又想重新定义它们的实现,那么应在派生类中重新定义所有的基类版本。
如果偷懒只定义一个版本,另外的重载版本将被隐藏。
定义抽象类的原因是,一些虽然是is-a的关系通过继承出来,解决问题的效率却不高。
比如圆和椭圆。圆只需要半径值 就可以描述大小和形状,不需要长半轴a和短半轴b。当然也可以通过将同一个值赋给成员a和b来照顾这种情况,但是导致信息冗余没有必要。
这时候可以把圆和椭圆的共性放到一个ABC中,从该ABC派生出圆和椭圆类。
从语法上来说,至少有一个纯虚函数的类即为抽象类,不能创建抽象类的实例对象。
纯虚函数就是在虚函数结尾加个 =0 ,即表明该函数是纯虚函数。
比如virtual double Area() const = 0;
如果类成员通过new进行初始化,那么则要定义相关的复制构造函数,重载赋值操作符=。
暂时就写这么点了。。。
标签:
原文地址:http://www.cnblogs.com/jiulin/p/4528566.html