it’s class Nothing {}

一、继承与派生

1.概念

令A为基类,B作为A的 派生类 拥有(继承)A的全部特点并对其进行修改和扩充,称B 继承 A

实例:学生基类及其派生

class student {
private :
string name;
int age, grade; // 会包含
...
void SetInfo(); // 会包含
bool CheckGood();
}
class Senior_student : public student { // 派生类
private :
int School;
public:
Senior_student( ... ) : student(...) { School = school; }
// 写派生类的构造函数时不能访问到基类的Private,
// 需要调用基类的构造函数.
School = school;
void SetInfo() {
student::SetInfo();
// 在派生类访问基类定义的同名成员时,应使用作用域符号 `::`。
}
bool CheckGood(); //覆盖
bool JIngSai(); //扩充
}

其中,对于封闭派生类对象的构造函数,其执行顺序应为:

  • 先执行基类的构造函数,初始化派生类对象中继承的成员;
  • 再执行成员对象的构造函数,初始化自己派生的成员对象;
  • 执行派生类自己的构造函数。

析构函数反之;

2. 复合关系

在设计两个有关系的类时要注意,并非两个类有共同点,就可以让它们成为继承关系。

让类 B 继承类 A,必须满足 类 B 所代表的事物也是类 A 所代表的事物 这个命题从逻辑上是成立的。 例如,写一个平面上的点类 CPoint:

class CPoint{
double x, y; //点的坐标
};

又要写一个圆类 CCircle。CCircle 类有圆心,因而 CCircle 类和 CPoint 类似乎有相同的成员变量。如果因此就让 CCircle 类从 CPoint 类派生而来,即采用如下写法:

class CCircle: public CPoint{
double radius; //半径
};

这是不正确的。因为圆也是点这个命题是不成立的。 正确的做法是利用类的复合关系,即在 CCircle 类中引入 CPoint 成员变量,代表圆心

class CCircle {
Cpoint center;
double radius;
}

同时仍需要注意复合关系的循环定义,这是应该极力避免的。

class CMaster;
class CDog {
CMaster* pm; // 狗只有一个主人,指向一个对象
};
class CMaster
{
CDog *dogs[10]; // 主人有多条狗,同样用指针指向
int dogNum;
};

3. 类的保护成员

protected 成员:可以被基类的成员/友元函数,以及派生类的成员函数访问。

4. 赋值兼容

  • 派生类对象可以赋值给基类对象,亦可初始化基类的引用。
  • 由于派生类的起始地址即为其中基类对象的起始地址,因此可以直接将派生对象的地址赋值给基类指针。

二、多态

1. 虚函数

虚函数可以参与多态,构造函数和静态成员函数不能是虚函数。

class base {
virtual int get();
}
int base::get() {}

2. 概念

由于派生类的指针/引用可以赋给基类指针/引用,通过基类指针/引用调用同名虚函数时:

  • 若指针指向/引用的是基类对象,则被调用的是基类虚函数;
  • 若指向/引用的是派生类对象,则被调用的是派生类虚函数。

我们称这种机制为多态

实例:《英雄无敌》中的小怪类

class Creature {
protected:
int hp, at;
public:
virtual void attack(Creature *pC) {}
virtual void hurted(int power) {}
virtual void FightBack(Creature *pC) {}
virtual void Charact() = 0;
// 纯虚函数,即对于基类不存在这种定义。
};

// 派生类
class Dragon : public Creature {
public :
void double_attack(Creature *pC) { attack(pC), attack(pC); }
// (2)
virtual void attack(Creature *pC) {}
virtual void hurted(int npower) {}
virtual void FightBack(Creature *pC) {}
}

void Dragon::attack(Creature *p) {
p->Hurted(at); // 多态
p->FightBack(this); // 多态
}
void Dragon::hurted(int pw) { hp-=pw; }
void Dragon::FightBack(Creature *p)
{
p->hurted(at/2); // 多态
}

... // 省略其他功能

Dragon dragon; Wolf wolf;
dragon.attack( &wolf );

Creature* enemy[maxn]; // 一个敌对生物大军的所有信息
for (int i=0;i<n;i++) {
pr = new Dragon(); cin >> pr.hp >> pr.at;
enemy[i] = pr; ...
}
for (int i=0;i<n;i++)
enemy[i] -> print(); // (1) 打印不同怪物的所有属性,属于多态。

各个行为函数会调用其对应对象的成员函数,提高了程序的 可扩充性

(1)让基类指针数组存放指向各种派生类对象的指针,遍历数组就能对各派生类对象做各种操作。若要访问某个派生类对象的成员变量,由于指针数组指向的是指向对象的指针,所以应使用 Creatrue** 类型。

(2)在非构造函数中调用虚函数,等价于 this->func, 也属于多态。

(3)多态只存在于虚函数中,构造函数和析构函数调用虚函数不属于多态。编译时即可确定调用的虚函数是自己的类或基类中定义的函数R,不会等到运行时才决定。

3. 原理

多态实现的原理在于通过基类指针或引用一个虚函数时,编译不确定调用函数而直到运行时才确定 — — 我们称其为 动态联编

而实现的关键在于,每一个有虚函数的类都有一个虚函数表,而该类任何对象中都存放着虚函数表的指针,指向对应虚函数的地址。

多态函数调用语句首先被编译成基类指针所指向的、虚函数表的地址。之后在虚函数表中查找虚函数地址,调用对应的指令。

4. 抽象类 及 纯虚函数

没有函数体的函数称为纯虚函数;包含了纯虚函数的类即为抽象类,只能作为基类进行派生而不能创建独立的对象。

class A {
private : int a;
public :
virtual void print() = 0;
void func() { cout<<"Func"; }
};

A a; // Error
A* a_point; // OK
  • 由于多态,抽象类的成员函数可以调用纯虚函数;
  • 如果一个类由抽象类派生而来,当且仅当它实现了基类所有的纯虚函数,它才能称为非抽象类

三、C++ 中的 IO 模板

  1. iostream 类函数

1. istream &getline(char *buf, int Size, char delim);

从输入流中读取 Size-1 个字符到缓冲区,或碰到 delim 字符为止(缺省时为\n)。
之所以只能读 Size-1 个字符,因为会自动添加 \0 于最后一位参数。
也可用 cin.getline() 判断输入是否结束。

2. bool eof(); 判断输入流是否结束。

  1. 流操纵算子:#include <iomanip> 所扩展的流操作函数:
  • 整数流的基数(进制):dec, oct, hex, serbase()
  • 浮点数精度:cout.precision, setprecision()
  • 设置域宽:cin.width(), setw()

此外,我们还可自定义流操纵算子:

ostream &tab(ostream &o) { return o << '\t'; }
cout << 1 << tab << 2; // 输出: 1 2

由于 iostream 对 << 左移操作进行了重载:

ostream & operator << ( ostream &(*p)(ostream &) );

因此对于形式符合 参数是ostream引用且返回值也是ostream引用 的函数,课直接调用 p 所指向的函数并以 *this 作为参数。

四、 泛型程序设计 及 模板

1. 函数模板

所谓函数模板,实际上是建立一个 通用函数,它所用到的数据的类型(包括返回值类型、形参类型、局部变量类型)可以不具体指定,而是用一个 虚拟的类型 来代替(实际上是用一个标识符来占位),等发生函数调用时再根据传入的实参来逆推出真正的类型。编译器由模板生成函数的过程称为 模板实例化

template < class 类型1, class 类型2, ... > 是定义函数模板的类型参数表。

以下是一个经典函数模板例子:实现 Swap 函数

template <class T>  // T 是一个类型参数
void Swap(T &a, T &b)
{
T tmp = x; x = y; y = tmp;
}

int main()
{
int n = 1, m = 2;
Swap(n, m); // 编译器生成了一个 void Swap(int &a,int &b)
return 0;
}

也可以不通过参数,实例化函数模板:

template <class T> 
T inc(T n) { return 1+n; }
int main()
{
cout << inc<double>(4)/2; // 输出 2.5
return 0;
}

你甚至可以用函数模板,实例化函数指针:

int a[3]={1,2,3}, b[3];
int suqare(int x) { return x*x; }
int cube(int x) { return x*x*x; }

template<class T, class pred>

void Map(T s,T t, T x, pred op)
{ for (; s!=t; ++s,++x) *x = op(*s); }

int main()
{
Map(a, a+5, b, suqare);
Map(a, a+5, b, cube);
// 此时前三个参数是 int*,最后一个参数是函数指针类型
// 即 pred = int (*op)(int)
}

函数模板可以重载,只要它们的形参表或类型参数表不同

需要注意的是,匹配模板函数不会进行类型自动转换。

template<class T1, class T2>
void print(T1 x1, T2 x2) { cout<< x1 << x2; }

template<class T>
void print(T x1, T x2) { cout<< x1 << x2; }

template<class T1, class T2>
void print(T1 x1, T1 x2) { cout<< x1 << x2; }

2. 类模板

对于一批操作相似的类,可以定义一个类模板然后由此生成不同的类。定义类模板时要求加上若干个类型参数,并在类的声明中给出各个参数替换的具体类型。

编译器由模板类生成出类的过程叫做类模板的实例化,由类模板实例化的类叫做模板类。(什么绕口令)

类模板实例: pair类模板

template<class T, class U>
class Pair {
public :
T x; U y;
Pair(T xx, U yy) : x(xx), y(yy) {};
Pair<T, U> operator+(const Pair<T, U> &p)
{ return Pair(x+p.x, y+p.y); }
};

template<class T, class U>
ostream& operator<<(ostream& os, const Pair<T, U>& p)
// 相当于重载 ostream 类的函数,因此得写全局函数
// 也可以在 Pair 类搞个友元函数啥的
{
os << "(" << p.x << ", " << p.y << ")";
return os;
}


int main()
{
Pair<int, int> x(114, 514);
Pair<int, int> y(1919, 810);
cout << x+y << '\n';
return 0;
}

3. 类模板的派生

本质上就是参数类型的套娃。

template<class T, class U>
class A { // 定义了一个_类模板_A
T v1; U v2;
};

template <class T, class U>
class B : public A<U, T> { // 整蛊,_类模板_B 的基类是 A 但参数反过来
T v3; U v4;
}

class C : public B<int,int> {
int v5; // 一个由 _模板类_ 派生而来的普通类;
}

int main()
{
B<int,double> ob1;
// 编译器产生两个类:A<double,int> , B<int,double>
C ob2;
// 编译器产生两个类:A<int,int>, B<int,int>
}

4. 类模板与友元

首先,任何函数、类及其成员函数都可以作为类模板的友元;
其次,函数模板也可以做类模板的友元,比如pair进行重载流输出时候。