1、简单的基类
从一个类派生出令一个类时,原始类称为基类,继承类称为派生类。
class TableTennisPlayer {
private:
string firstname;
string lastname;
bool hasTable;
public:
TableTennisPlayer(const string &fn = "none", const string &ln = "none", bool ht = false);
void Name() const;
inline bool HasTable() const { return hasTable; };
inline void ResetTable(bool v) { hasTable = v; };
};
TableTennisPlayer::TableTennisPlayer(const string &fn, const string &tn, bool ht)
:firstname(fn),
lastname(tn),
hasTable(ht)
{
}
void TableTennisPlayer::Name() const {
std::cout << lastname << "," << firstname << endl;
}
TableTennisPlayer player1("tom", "clus", true);
最后在创建TableTenisPlayer对象时,传入参数是const char*,类型和const string&并不匹配,但是string类中有一个const char*作为参数的构造函数,使用c风格字符串初始化string对象时将自动调用这个构造函数。
class RatedPlayer :public TableTennisPlayer {
private:
unsigned int rating;
public:
RatedPlayer(unsigned int r = 0, const string &fn = "none", const string &ln = "none", bool ht = false);
RatedPlayer(unsigned int r, const TableTennisPlayer &tp);
unsigned int Rating() const { return rating; }
void ResetRating(unsigned int r) { rating = r; }
};
TableTennisPlayer是一个公有基类,被称为公有派生。派生类对象包含基类对象。使用公有派生,基类的公有成员将称为派生类的公有成员,基类的私有部分也成为派生类的一部分,但是只能通过基类的公有和保护方法访问。
2、构造函数
派生类不能直接访问基类的私有成员,必须通过基类方法进行访问。派生类构造函数必须使用基类构造函数。
创建派生类对象时,程序首先创建基类对象。这意味着基类对象应当在程序进入派生类构造函数之前被创建。C++使用成员初始化列表语法来完成这个工作:
RatedPlayer::RatedPlayer(unsigned int r, const string &fn, const string &ln, bool ht)
:TableTennisPlayer(fn, ln, ht)
{
rating = r;
}
:TableTennisPlayer(fn, ln, ht)被称为成员初始化列表,将调用TableTennisPlayer的构造函数。必须首先创建基类对象,如果不调用基类构造函数,程序将使用默认的基类构造函数。
RatedPlayer::RatedPlayer(unsigned int r, const string &fn, const string &ln, bool ht)
{
rating = r;
}
可以看到上面这种写法也可以,但是其实它默认调用了带默认参数的构造函数。
RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer &tp)
:TableTennisPlayer(tp), rating(r)
{
}
第二个构造函数参数类型为TableTennisPlayer&,明显是要调用复制构造函数,但是复制构造函数没有定义,编译器将自动生成一个。执行成员复制的隐式复制构造函数是合适的,因为类没有使用动态内存分配。虽然string用到了动态内存分配,但是复制string用的是string的复制构造函数。
释放对象的顺序与创建对象的顺序相反,首先执行派生类的析构,再自动调用基类的析构
3、派生类和基类之间的关系
派生类可以使用基类的非private方法;基类指针可以在不进行显式类型转换的情况下指向派生类对象;基类引用可以在不进行显式类型转换的情况下引用派生类对象。
RatedPlayer p1(10, "rong", "ks");
TableTennisPlayer p2 = p1;
TableTennisPlayer &p3 = p1;
TableTennisPlayer *p4 = &p1;
基类指针或者引用只能用于调用基类方法,不能调用派生类的方法。不可以将基类对象和地址赋给派生类的引用和指针。
4、继承
C++有三种继承方式:公有、私有、保护继承。公有继承是最常用的,它建立一个is-a关系,即派生类也是一个基类对象,可以对基类对象执行的操作,也可以对派生类对象执行。
4.1、多态公有继承
如果希望同一个方法在派生类和基类中的行为是不同的,方法的行为应取决于调用该方法的对象。这种行为称为多态。有两种方法可以实现多态公有继承:在派生类中重新定义基类的方法;使用虚方法。
通过引用或指针而不是对象调用方法,如果没有使用关键字virtual,程序将根据引用类型或指针类型选择方法;如果使用了virtual,程序将根据引用或者指针指向的对象的类型来选择方法。
class Base
{
public:
Base(int w, int h);
virtual ~Base();
virtual void getSize() const;
void printName() { cout << "Base" << endl; }
protected:
int width;
int height;
};
Base::Base(int w, int h)
:width(w), height(h)
{
}
Base::~Base()
{
}
void Base::getSize() const
{
cout << "width : " << width << " height : " << height << endl;
}
class Box : public Base
{
public:
Box(int w, int h, int l);
virtual ~Box();
virtual void getSize() const override;
void printName() { cout << "Box" << endl; }
private:
int length;
};
Box::Box(int w, int h, int l)
:Base(w, h), length(l)
{
}
Box::~Box()
{
}
void Box::getSize() const
{
cout << "width : " << width << " height : " << height << " length : " << length << endl;
}
int main()
{
Base a(10, 20);
Box b(20, 30, 40);
Base &c = a;
Base &d = b;
c.printName(); // Base
d.printName(); // Base
c.getSize(); // width : 10 height : 20
d.getSize(); // width : 20 height : 30 length : 40
getchar();
return 0;
}
如上述例子,c和d都调用了printName方法,但是printName方法并不是虚函数,调用的是引用类型的方法。getSize方法是虚函数,程序将根据引用指向的类型来选择方法。使用指针代替引用时,行为将与之类似。
方法在基类中被声明为虚函数后,它在派生类中将自动称为虚方法,也可以在派生类中使用关键字virtual来指出那些函数是虚函数。
在基类中声明一个虚析构函数,这样可以确保释放派生对象时,按照正确的顺序调用析构函数。b析构时将会先调用自身的析构函数,然后自动调用基类的构造函数。
指针和引用类型的兼容性:将派生类引用或指针转换为基类引用或指针被称为向上强制转换,这使得公有继承不需要进行显式类型转换。相反的将基类指针或引用转换为派生类指针或引用称为向下强制转换,如果不用显式类型转换,则向下强制转换时不允许的
有关虚函数的注意事项:
构造函数不能是虚函数,析构函数应当是虚函数(除非其不用做基类);
友元不能是虚函数;
如果派生类没有重新定义函数,将使用该函数的基类版本;
在派生类中重新定义不会生成函数的两个重载版本,而是会隐藏同名的基类方法;所以如果重新定义继承的方法,应确保与原来的类型完全相同;但如果返回的类型是基类引用或者指针,则可以修改为指向派生类的引用或指针,这成为返回类型协变。
4.2、访问控制:protected
最好对类数据成员采用私有访问控制,不要使用保护访问控制,同时通过基类方法使派生类能访问基类数据。
4.3、抽象基类
假设有一个椭圆类,数据成员包括中心坐标,长轴,短轴,方向角、移动椭圆、返回椭圆面积、旋转椭圆、以及缩放长半轴和短半轴的方法:
class Ellipse
{
private:
double x;
double y;
double a;
double b;
double ange;
public:
void move(int nx, int ny) { x = nx; y = ny; };
virtual double Area() const { return 3.14* a*b; }
virtual void Rotate(double nang) { ange += nang; }
virtual void Sacle(double sa, double sb) { a *= sa; b *= sb; }
};
圆是椭圆的一个特殊情况,现在假设从椭圆派生出一个Circle类。但是这种派生是笨拙的,因为圆只需要一个半径就可以描述大小和形状,并不需要长轴和短轴,Circle构造函数可以将同一个值赋给a 和 b来处理这种情况,但是会导致信息冗余。另外angle参数和Rotate方法对于圆来说没有实际意义,可以使用一些技巧来修正这些问题,例如在circle类中四有部分中心定义Rotate方法,使Rotate方法不能以公有方式用于圆。总的来说不使用继承,直接定义Circle更简单。
class Circle
{
private:
double x;
double y;
double r;
public:
void move(int nx, int ny) { x = nx; y = ny; };
double Area() const { return 3.14* r*r; }
void Sacle(double sr) { r *= sr; }
};
Circle和Ellipse有很多共同点,将他们分别定义则忽略了这一事实。还有一种解决方法就是从这两个类中抽象出他们的共性,放到一个基类中。这个例子中共同点是中心坐标,move和area方法,但是不能在基类中实现area方法,因为没有包含必要的数据成员。c++通过纯虚函数提供未实现的函数。
class BaseEllipse {
private:
double x;
double y;
public:
BaseEllipse(double x0 = 0, double y0 = 0) { x = x0; y = y0; }
virtual ~BaseEllipse() {};
void move(int nx, int ny) { x = nx; y = ny; }
virtual double Area() const = 0;
};
当类声明中包含纯虚函数时就不能创建该类的对象。
class BaseEllipse {
private:
double x;
double y;
public:
BaseEllipse(double x0 = 0, double y0 = 0) { x = x0; y = y0; }
virtual ~BaseEllipse() {};
void move(int nx, int ny) { x = nx; y = ny; }
virtual double Area() const = 0;
};
class Ellipse : public BaseEllipse
{
private:
double a;
double b;
double angle;
public:
Ellipse(double x, double y, double a, double b, double angle)
:BaseEllipse(x, y), a(a), b(b), angle(angle)
{}
virtual ~Ellipse(){}
virtual double Area() const override{ return 3.14 * a * b; }
void Rotate(double nang) { angle += nang; }
void Sacle(double sa, double sb) { a *= sa; b *= sb; }
};
class Circle : public BaseEllipse
{
private:
double r;
public:
Circle(double x, double y, double r)
:BaseEllipse(x, y), r(r) {}
virtual ~Circle() {}
virtual double Area() const override{ return 3.14 * r * r; }
void Sacle(double sr) { r *= sr; }
};
4.4、继承和动态内存分配
派生类不使用new:
class BaseDMA
{
private:
char *lable;
int rating;
public:
BaseDMA(const char *l = "null", int r = 0);
BaseDMA(const BaseDMA& rs);
virtual ~BaseDMA();
BaseDMA& operator=(const BaseDMA& rs);
};
class lackDMA :public BaseDMA
{
private:
char color[40];
public:
};
基类声明中包含了构造函数使用new需要的特殊方法:析构函数、复制构造函数和重载赋值运算符。从基类派生出lackDMA,后者不使用new,也未包含其他一些不常用的、需要特殊处理的特性。这时候可以不用为派生类lackDMA显式定义析构函数、复制构造函数和赋值运算符。
如果没有定义析构函数,编译器将定义一个不执行任何操作的默认构造函数(执行自身代码后调用基类析构函数)
如果没有定义复制构造函数,复制lackDMA时将使用默认复制构造函数复制lackDMA,复制基类时使用显式的复制构造函数复制BaseDMA。
赋值运算符和复制构造函数类似,类的默认赋值运算符将自动使用基类的赋值运算符来对基类组件进行赋值。
派生类使用new:
class hasDMA :public BaseDMA
{
private:
char *style;
public:
};
这种情况下,必须为派生类定义显式析构函数、复制构造函数和赋值运算符。
派生类的析构函数自动调用基类的析构函数,故其自身的职责是对派生类构造函数执行工作的清理。
hasDMA的复制构造函数只能访问hasDMA的数据,因此它必须调用BaseDMA的复制构造函数来处理共享的BaseDMA数据:
hasDMA(const hasDMA& hd)
:BaseDMA(hd)
{}
派生类的赋值运算符只能访问hasDMA的数据,可以通过显式调用基类赋值运算符来完成基类对象BaseDMA的赋值:
hasDMA& operator=(const hasDMA& hd)
{
BaseDMA::operator=(hd);
}