C++深入理解 面向对象部分
一、面向对象深入部分
接下来接续深入 c++ 面向对象部分知识。
1. 继承和派生
1.1 继承和派生
继承:在定义一个新的类 B 时,如果该类与某个已有的类 A 相似(指的是 B 拥有 A 的全部特点),那么就可以把 A 作为一个基类,把 B 作为基类的一个派生类(也称子类)。派生类是通过基类进行修改和扩充得到的。在派生类中,可以扩充新的成员变量和成员函数。派生类一旦定义,就可以使用,不依赖基类。
派生类拥有基类的全部成员函数,不论 private, protected, public 。在派生类的各个成员函数中,不能访问基类的 private 成员。写法为
class 派生类名:public 基类名{};
class Student{
private:
string Name;
int age;
public:
bool IsThreeGood(){};
void setName(const string & name);
{Name = name;}
// ...
};
class UndergraduateStudent:public Student{
private:
int Department;
public:
bool IsThreeGood(){......}; // 覆盖
bool CanBaoYan(){......};
}; // 派生类写法:类名 puclic 基类名
class GraduateStudent:public Student{
private:
int Department;
char mentorName[20];
public:
int CountSalary(){};
};
派生类对象的体积,等于基类对象的体积,再加上派生类对象自己的成员变量的体积。派生类对象中,包含基类对象,而且基类对象的存储位置位于派生类对象新增的成员变量之前。例如,
#include <iostream>
#include <string>
using namespace std;
class Student{
private:
string name;
string id;
char gender; // "F"代表女,"M"代表男
int age;
public:
void printInfo();
void setInfo(const string & name_, const string & id_,
int age_, char gender_);
string getName(){return name;}
};
void Student::printInfo(){
cout << "name:" << name << endl;
cout << "id:" << id << endl;
cout << "gender:" << gender << endl;
cout << "age:" << age << endl;
}
void Student::setInfo(const string & name_, const string & id_,
int age_, char gender_){
name = name_;
id = id_;
age = age_;
gender = gender_;
}
class UndergraduateStudent:public Student{ // 本科生类,继承了Student类
private:
string department; // 学生所属的系的名称
public:
void qualifiedForBaoyan(){ // 给予保研资格
cout << "qulified for baoyan" << endl;
}
void printInfo(){
// 不写的话,就会当作派生类自己的函数
Student::printInfo(); // 调用基类的 printInfo
cout << "Department:" << department << endl;
}
void setInfo(const string & name_, const string & id_,
int age_, char gender_, const string & department_){
Student::setInfo(name_, id_, age_, gender_); // 调用基类的 setInfo
department = department_;
}
};
int main(){
UndergraduateStudent s2;
s2.setInfo("Harry Potter", "118829212", 19, 'M', "Computer Science");
cout << s2.getName() << " ";
s2.qualifiedForBaoyan();
s2.printInfo();
return 0;
}
视频里有两个函数没有写,所以运行出来有问题,后来我自己补充上运行得到了结果,如下,
1.2 继承关系和复合关系
继承是“是”的关系,复合是“有”的关系。举个例子,要写一个圆和点的类。圆里面有点,所以要写成复合关系,如下
class Point{
double x, y;
friend class Circle; // 因为 x,y是私有的
}
class Circle{
double r;
Point center; // 复合类
}
复合关系的使用:比如有一个业主和狗管理信息情况,那就只能用复合关系来写,其中每个主人最多有10条狗,所以定义如下,
// 为“狗”类写一个“业主”类的对象指针;
// 为“业主”类设一个“狗”类的对象指针数组。
class Master; // Master 必须提前声明,不能先写 Master 类后写 Dog 类
class Dog{
Master * pm;
}
class Master{
Dog * dogs[10];
}
// 这样两个类可以相互独立,也可以找到他们的关系
1.3 覆盖和保护成员
覆盖:派生类可以定义一个和基类成员同名的成员,这叫覆盖。在派生类中访问这类成员时,缺省情况是访问派生类定义的成员。要在派生类中访问基类同名成员时,要使用作用域::。
一般来说,基类和派生类不定义同名成员变量。
类的保护成员,汇总如下,
举个例子,
#include <iostream>
#include <string>
using namespace std;
class Father{
private: int Private;
public: int Public;
protected: Protected;
};
class Son:Father{
void accessFather(){
Public = 1; // 可以
Private = 1; // 不可以
Protected = 1; // 可以,访问从基类继承的 protected 成员
Son f; // 没太理解
f.Protected = 1; // 编译出错, f 不是当前对象
}
}
int main(){
Father f;
Son s;
f.Public = 1; // 可以
f.Private = 1; // 不可以
f.Protected = 1; // 不可以
s.Public = 1; // 可以
s.Protected = 1; // 不可以
s.Private = 1; // 不可以
return 0;
}
1.4 派生类的构造函数
举个例子,
#include <iostream>
#include <string>
using namespace std;
class Bug{
private:
int legs;
int color;
public:
int type;
Bug(int nlegs, int ncolor);
void printBug(){};
};
class flyBug: public Bug{
int wings;
public:
flyBug(int nlegs, int ncolor, int nwings);
};
Bug::Bug(int nlegs, int ncolor){
legs = nlegs;
color = ncolor;
}
// 错误的 flyBug 构造函数
/*
flyBug::flyBug(int nlegs, int ncolor, int nwings){
legs = nlegs; // 不能访问
color = ncolor; // 不能访问
type = 1; // 可以
wings = nwings; // 可以
}*/
// 正确的 flyBug 构造函数
flyBug::flyBug(int nlegs, int ncolor, int nwings):Bug(nlegs, ncolor){
wings = nwings; // 可以
}
int main(){
flyBug fb(2, 3, 4);
fb.printBug();
fb.type = 1;
// fb.Legs = 2; // 错误,私有的
return 0;
}
在创建派生类的对象时,需要调用基类的构造函数:初始化派生类对象中从基类继承的成员。在执行一个派生类的构造函数之前,总是先执行基类的构造函数。
调用基类构造函数的两种方式,
显示方式:在派生类的构造函数中,为基类的构造函数提供参数;
derived::derived(arg_derived-list):base(arg_base-list)
隐式方式:在派生类的构造函数中,省略基类构造函数时,派生类的构造函数则自动调用基类的默认构造函数。
派生类的析构函数被执行时,执行完派生类的析构函数后,自动调用基类的构造函数。例如
#include <iostream>
#include <string>
using namespace std;
class Base{
public:
int n;
Base(int i):n(i){
cout << "Base " << n << " constructed" << endl;
}
~Base(){
cout << "Base " << n << " destructed" << endl;
}
};
class Derived: public Base{
public:
Derived(int i):Base(i){
cout << "Derived constructed" << endl;
}
~Derived(){
cout << "Derived destructed" << endl;
}
};
int main(){
Derived Obj(3);
return 0;
}
结果如下,
包含成员对象的派生类的构造函数写法,如下
#include <iostream>
#include <string>
using namespace std;
class Bug{
private:
int legs;
int color;
public:
int type;
Bug(int nlegs, int ncolor);
void printBug(){};
};
class Skill{
public:
Skill(int n){}
};
class flyBug:public Bug{
int wings;
Skill sk1, sk2; // 包含成员对象的派生类
public:
flyBug(int nlegs, int ncolor, int nwings);
};
flyBug::flyBug(int nlegs, int ncolor, int nwings):
Bug(legs, color), sk1(5), sk2(color), wings(nwings){
}
1.5 公有继承的赋值兼容规则
public 继承的赋值兼容规则,
class base{};
class derived:public base{};
base b;
derived d;
// 1) 派生类的对象可以赋值给基类对象
b = d; // d 里面有的内容拷贝到 b 中
// 2) 派生类的对象可以初始化基类引用
base & br = d; // 基类引用派生类包含的那一个部分
// 3) 派生类的对象地址可以赋值给基类指针
base * bp = & d; // 指针指向派生类包含的那一个部分(先存基类,再存派生类)
- 派生类的对象可以赋值给基类对象
b = d; // d 里面有的内容拷贝到 b 中 - 派生类的对象可以初始化基类引用
base & br = d; // 基类引用派生类包含的那一个部分 - 派生类的对象地址可以赋值给基类指针
base * bp = & d; // 指针指向派生类包含的那一个部分(先存基类,再存派生类)
如果不是public 就不成立了
直接基类和间接基类的理解。B:A, C:B, D:C,多个继承,比较容易理解。
----------》派生类沿着类的层次自动向上继承它的间接基类。
----------》派生类成员包括:派生类自己的成员,直接基类的所有成员,所有间接基类的全部成员
----------》在声明派生类时,只需要列出它的直接基类就行
----------》构造函数执行从基类到最底类,由顶层开始。析构函数执行时,由底至顶
2. 虚函数和多态
2.1 虚函数和多态定义
在类的定义中,前面有 virtual 关键字的成员函数就是虚函数。virtual 关键字只用在类定义里的函数声明时,写函数体时不用。构造函数和静态成员函数不能是虚函数。例如
// 虚函数可以参与多态,其他函数不能
class base{
virtual int get();
};
int base::get(){}
多态的表现形式一,
----》派生类的指针可以赋给基类指针;
----》通过基类指针调用基类和派生类中的同名虚函数时:1) 若该指针指向一个基类的对象时,那么被调用是基类的虚函数;2) 若该指针指向一个派生类的对象,那么被调用的是派生类的虚函数。**这种机制就叫做多态。**举个例子,
class Base{
public:
virtual void someVirtualFunc(){}
};
class Derived: public Base{
public:
virtual void someVirtualFunc(){}
};
int main(){
Derived derived;
Base *p = & derived;
// 调用哪个虚函数取决于 p 指向哪种类型的对象
// 指向的是 Derived 类对象的虚函数
p -> someVirtualFunc();
return 0;
}
多态的表现形式二,
----》派生类的对象可以赋给基类引用
----》通过基类引用调用基类和派生类中的同名虚函数时:1) 若该引用引用的是一个基类的对象,那么被调用的是基类的虚函数;2) 若该引用引用的是一个派生类的对象,那么被调用的是派生类的虚函数。这种机制也叫做多态。 举个例子,
class Base{
public:
virtual void someVirtualFunc(){}
};
class Derived: public Base{
public:
virtual void someVirtualFunc(){}
};
int main(){
Derived derived;
Base & r = derived;
// 调用哪个虚函数取决于 r 引用哪种类型的对象
// 指向的是 Derived 类对象的虚函数
r.someVirtualFunc();
return 0;
}
多态的简单示例,
#include <iostream>
#include <string>
using namespace std;
class A{
public:
virtual void print(){
cout << "A::print"<<endl;
}
};
class B: public A{
public:
virtual void print(){
cout << "B::print"<<endl;
}
};
class D: public A{
public:
virtual void print(){
cout << "D::print"<<endl;
}
};
class E: public B{
public:
virtual void print(){
cout << "E::print"<<endl;
}
};
int main(){
A a; B b; E e; D d;
A * pa = & a; B * pb = & b;
D * pd = & d; E * pe = & e;
pa -> print(); // pa 是基类指针,多态
pa = pb;
pa -> print();
pa = pd;
pa -> print();
pa = pe;
pa -> print();
return 0;
}
多态的作用: 在面向对象的程序设计中使用多态,能够增强程序的可扩充性,即程序需要修改或增加功能的时候,需要改动和增加的代码较少。
2.2 多态实例:《魔法门之英雄无敌》
游戏中有很多种怪物,每种怪物都有一个类与之对应,每个怪物就是一个对象。怪物能够互相攻击,攻击敌人和被攻击时都有相应的动作,动作是通过对象的成员函数实现的。
游戏版本升级时,要增加新的怪物–雷鸟。如何编程才能使升级时的代码改动和增加量较小?
基本思路:
----》为每个怪物类编写 Attack,FightBack 和 Hurted 成员函数
----》Attack 函数表现攻击动作,攻击某个怪物,并调用被攻击怪物的 Hurted 函数,以减少被攻击怪物的生命值,同时也调用被攻击怪物的 FightBack 成员函数,遭受被攻击怪物的反击。
----》设置基类 Creature, 使用怪物类都从基类派生而来
非多态写法:
#include <iostream>
#include <string>
using namespace std;
class Creature{
protected:
int power; // 代表生命力
int lifeValue; // 代表生命值
};
class Deagon: public Creature{
public:
void Attack(Wolf* pWolf){
// ... 表现攻击动作的代码
pWolf -> Hurted(power);
pWolf -> FightBack(this);
}
void Attack(Ghost* pGhost){
// ... 表现攻击动作的代码
pGhost -> Hurted(power);
pGhost -> FightBack(this); // 指向攻击发起者
}
void Hurted(int power){
// ... 表现受伤的动作
lifeValue -= power;
}
void FightBack(Wolf* pWolf){
// ... 表现反击的动作
pWolf -> Hurted(power / 2);
}
void FightBack(pGhost* pGhost){
// ... 表现反击的动作
pGhost -> Hurted(power / 2);
}
};
这样,有 n 中怪物,Deagon 类中就会有 n 个 Attack 成员函数,以及 n 个 FightBack 成员函数。对于其他类也是一样的。
多态写法:
#include <iostream>
#include <string>
using namespace std;
class Creature{
protected:
int power; // 代表生命力
int lifeValue; // 代表生命值
public:
virtual void Attack(Creature * pCreature){}
virtual void Hurted(int power){}
virtual void FightBack(Creature * pCreature){}
};
class Dragon: public Creature{
public:
virtual void Attack(Creature * pCreature);
virtual void Hurted(int power);
virtual void FightBack(Creature * pCreature);
};
void Dragon::Attack(Creature * pCreature){
// ... 表现攻击动作的代码
pCreature -> Hurted(power); // 多态
pCreature -> FightBack(this); // 多态
}
void Dragon::Hurted(int power){
// ... 表现受伤的动作
lifeValue -= power;
}
void Dragon::FightBack(Creature * pCreature){
// ... 表现反击的动作
pCreature -> Hurted(power / 2); // 多态
}
如果游戏升级,增加了新怪物。只要编写新类怪物,不需要在已有的类里为新怪物增加成员函数,已有的类可以原封不动。原理如下,
#include <iostream>
#include <string>
using namespace std;
// 例如
Dragon dragon; Wolf wolf; Ghost ghost;
thunderBird Bird;
Dragon.Attack(& wolf); // (1)
Dragon.Attack(& ghost); // (2)
Dragon.Attack(& Bird); // (3)
根据多态的规则,上面的 (1), (2), (3) 进入到 Dragon::Attack 函数以后,能分别调用:Wolf::Hurted, Ghost::Hurted, thunderBird::Hurted.
2.3 多态实例:几何形体程序
几何形体处理程序:输入若干个几何形体的参数,要求按面积排序输出。输出时要指明形状,
Input:
第一行是几何形体数目 n (不超过100).下面有 n 行,每行以一个字母 c 开头。
如 c 是 "R", 则代表一个矩形,本行后面跟着两个整数,分别是矩形的宽和高;
如 c 是 "C", 则代表一个圆形,本行后面跟着一个整数代表其半径;
如 c 是 "T", 则代表一个三角形,本行后面跟着三个整数,代表三条边长度;
Output:
按面积从大到小依次输出每个几何形体的种类及面积。每行一个几何形体,输出格式为:
形体名称:面积
Sample Input:
3
R 3 5
C 9
T 3 4 5
Sample Output:
Triangle: 6
Rectangle: 15
Circle: 254.32
实现如下,
#include <iostream>
#include <stdlib.h>
#include <math.h>
using namespace std;
class Shape{
public:
virtual double Area() = 0; // 纯虚函数
virtual void printInfo() = 0;
};
class Rectangle: public Shape{
public:
double w, h;
virtual double Area();
virtual void printInfo();
};
class Circle: public Shape{
public:
double r;
virtual double Area();
virtual void printInfo();
};
class Triangle: public Shape{
public:
double a, b, c;
virtual double Area();
virtual void printInfo();
};
double Rectangle::Area(){
return w * h;
}
void Rectangle::printInfo(){
cout << "Rectangle:" << Area() << endl;
}
double Circle::Area(){
return 3.14 * r * r;
}
void Circle::printInfo(){
cout << "Circle:" << Area() << endl;
}
double Triangle::Area(){
double p = (a + b + c) / 2.0;
return sqrt(p * (p - a)*(p - b)*(p - c));
}
void Triangle::printInfo(){
cout << "Triangle:" << Area() << endl;
}
// 进入的是数组元素指针
int myCompare(const void * s1, const void * s2){
double a1, a2;
Shape ** p1; // s1, s2 是 void*, 不可写"*s1"来取得s1指向的内容
Shape ** p2;
p1 = (Shape **)s1; // s1, s2 指向 Shape 数组中的元素,数组元素的类型是 Shape*
p2 = (Shape **)s2; // p1, p2 都是指向指针的指针,类型为 Shape**
a1 = (*p1) -> Area(); // *p1 的类型是 Shape*, 是基类指针,故此句为多态
a2 = (*p2) -> Area();
if(a1 < a2)return -1;
else if(a2 < a1)return 1;
else return 0;
}
Shape * pShapes[100];
int main(){
int i; int n;
Rectangle * pr; Circle * pc; Triangle * pt;
cin >> n;
for(i = 0; i < n; i++){
char c;
cin >> c;
switch(c){
case 'R':
pr = new Rectangle();
cin >> pr->w >> pr->h;
pShapes[i] = pr;
break;
case 'C':
pc = new Circle();
cin >> pc->r;
pShapes[i] = pc;
break;
case 'T':
pt = new Triangle();
cin >> pt->a >> pt->b >> pt->c;
pShapes[i] = pt;
break;
}
}
qsort(pShapes, n, sizeof(Shape*), myCompare);
for(i = 0; i < n; i++)
pShapes[i]->printInfo();
return 0;
}
结果如下,
2.4 多态实例:Base 例子
接下来对这个例子进行理解,
#include <iostream>
#include <stdlib.h>
#include <math.h>
using namespace std;
class Base{
public:
void func1(){func2()};
/* 解释如下
void func1(){this->func2()}; // this 是基类函数,fun2是虚函数,所以是多态 */
virtual void fun2(){cout << "Base::func2()" << endl};
};
class Derived: public Base{
public:
virtual void func2(){cout << "Derived: fun2()" << endl;}
};
int main(){
Derived d;
Base *pBase = &d;
pBase -> fun1();
return 0;
}
接下来对这个例子进行理解,不过真想感叹一句,这个老师喝水是真滴 LiuPi,一大瓶水真的喝完了,一大瓶。。。看下面,
吐槽完了继续理解,为什么输出的是 “Derived: fun2()”,注意 this 指针。注意,在非构造函数,非析构函数的成员函数中调用虚函数,是多态。
构造函数和析构函数中调用虚函数。在构造函数和析构函数中调用虚函数,不是多态。编译时即可确定,调用的函数是自己的类或者基类中定义的函数,不会等到运行时才决定调用自己的还是派生类的函数。例如,
注意:派生类中和基类中虚函数同名同参数表的函数,不加 virtual 也自动成为虚函数。
#include <iostream>
#include <stdlib.h>
#include <math.h>
using namespace std;
class myclass{
public:
virtual void hello(){cout << "hellp from myclass" << endl;};
virtual void bye(){cout << "bye from myclass" << endl;}
};
class son:public myclass{
public:
// 注意:派生类中和基类中虚函数同名同参数表的函数,不加 virtual 也自动成为虚函数
void hello(){cout << "hello from son" << endl;};
// 注意 在构造函数和析构函数中调用虚函数,不是多态。
son(){hello();}; // 虚函数
~son(){bye();}; // 虚函数
};
class grandson: public son{
public:
// 虚函数,注意 在构造函数和析构函数中调用虚函数,不是多态。
void hello(){cout << "hello from grandson" << endl;};
void bye(){cout << "bye from grandson" << endl;};
grandson(){cout << "constructing grandson" << endl;};
~grandson(){cout << "distructing grandson" << endl;};
};
int main(){
grandson gson;
son * pson;
pson = &gson;
pson -> hello(); // 多态
return 0;
}
结果如下,
3. 多态实现原理及其一些细节
3.1 多态实现原理
“多态”的关键字在于通过基类指针或引用调用一个虚函数时,编译时不确定到底调用的是基类还是派生类的函数,运行时才确定,这叫 “动态联编”。有一个例子
class Base{
public:
int i;
virtual void print(){
cout << "Base:print" << endl;
}
};
class Derived: public Base{
public:
int n;
virtual void print(){
cout << "Derived:print" << endl;
}
};
int main(){
Derived d;
cout << sizeof(Base) << ", " << sizeof(Derived);
}
// 输出结果为: 8, 12 为什么都多出了四个字节
多态实现的关键,虚函数表
每一个有虚函数的类(或有虚函数的类的派生类)都有一个虚函数表,该类的任何对象中都存放虚函数表的指针。虚函数表中列出了该类的虚函数地址。多出来的四个字节就是用来放虚函数表的地址的。
多态的函数调用语句被编译成一系列根据基类指针所指向的(或基类引用所引用的)对象中存在的虚函数表的地址,在虚函数表中查找虚函数地址,并调用虚函数。在理解,举个例子,
#include <iostream>
#include <stdlib.h>
#include <math.h>
using namespace std;
class A{
public:
virtual void Func(){
cout << "A::Func" << endl;
}
};
class B: public A{
public:
virtual void Func(){
cout << "B::Func" << endl;
}
};
int main(){
A a;
A * pa = new B();
pa -> Func();
// 64 位程序指针为 8 字节
long long * p1 = (long long *) & a;
long long * p2 = (long long *) pa;
* p2 = * p1; // a 虚函数表的地址覆盖掉 B 虚函数表的地址
pa -> Func(); // 查虚函数表才知道,
return 0;
}
结果如下,
指针赋予了程序访问任意地址空间的能力,对函数的任意字节进行修改,有助于提升使用效率。
3.2 虚析构函数、纯虚函数和抽象类
虚析构函数:
通过基类的指针删除派生类对象时,通常情况下只调用基类的析构函数。
------》但是,删除一个派生类的对象时,应该先调用派生类的析构函数,然后调用基类的析构函数。
解决方法:把基类的析构函数声明为 virtual
------》派生类的析构函数可以 virtual 不声明
------》通过基类的指针删除派生类对象时,首先调用派生类的析构函数,然后调用基类的析构函数。
一般来说,一个类如果定义了虚函数,则应该将析构函数也定义成虚函数。或者,一个类打算作为基类使用,也应该将析构函数定义成虚函数。
注意:不允许以虚函数作为构造函数。举个例子,
class son{
public:
~son{
cout << "bye from son" << endl;
}
};
class grandson: public son{
public:
~grandson{
cout << "bye from grandson" << endl;
}
};
int main(){
son *pson;
pson = new grandson();
delete pson;
return 0;
}
输出:bye from son // 没有执行 grandson::~grandson(). 因此解决方法为,
class son{
public:
virtual ~son{
cout << "bye from son" << endl;
}
};
class grandson: public son{
public:
~grandson{
cout << "bye from grandson" << endl;
}
};
int main(){
son *pson;
pson = new grandson();
delete pson;
return 0;
}
输出:bye from grandson \r\n bye from son // 先执行 grandson::~grandson(),引起执行 son:: ~ son() .
纯虚函数:没有函数体的函数:
例如,
class A{
private: int a;
public:
virtual void print() = 0; // 纯虚函数
void func(){cout << "func"; }
};
抽象类:包含纯虚函数的类
----------》 抽象类只能作为基类来派生新类使用,不能创建抽象类的对象
----------》 抽象类的指针和引用可以指向由抽象类派生出来的类的对象
例如,
A a; // 错误的, A 是抽象类,不能创建对象
A * pa; // 可以的,可以定义抽象类的指针和引用
pa = new A; // 错误, A 是抽象类,不能创建对象
----------》 在抽象类的成员函数内可以调用纯虚函数,但是在构造函数或析构函数内部不能调用纯虚函数。(注意多态在不同形式下调用的情况)
----------》 如果一个类从抽象类派生而来,那么当且仅当它实现了基类中的所有纯虚函数,它才能成为非抽象类。
具体例子,
#include <iostream>
#include <stdlib.h>
#include <math.h>
using namespace std;
class A{
public:
virtual void f() = 0; // 纯虚函数
void g(){this -> f(); // 可以的,是多态
}
A(){ // f()}; // 错误的,在构造函数调用虚函数不是多态
}
};
class B: public A{
public:
void f(){
cout << "B:f()" << endl;
}
};
int main(){
B b;
b.g();
return 0;
}
结果如下,
4. 总结
这个老师是讲的真不错,逻辑清晰,按照说的复现不卡 BUG,不用浪费时间调,直接理解就行,真推荐想学习的也看看他的视频。