Essential c++ 第五章
这一章,主要介绍了怎么进行各个类的高级管理
文章目录
1. 概述
第四章主要讲了一个类都有什么内容,第五章主要介绍如果出现多个有关系的类,应该如果写程序,如果更加高效的管理类。面向对象的编程中,最关键的有两点,就是继承和多态。继承是将所有类共有的东西抽象出来,放到上一层,然后下一层次放个性的东西,使得各个类之间层次分明。多态是提高类活力的方法,能够将一个函数,根据各个类的需要,进行特化,通过同一个函数,却能够使得每一个类都有自己的实现方法。
2. 继承
2.1 基类和派生类
当我们定义两个类的时候,两个类有互相包含的关系,我们就可以使用继承的方法来构造这些类。比如我们有一个类叫做人,有另外一个类叫做学生,学生是属于人的,必定含有人的所有共性,但是学生又有自己的特性,并不是所有的人都有学生的特性。这种关系就是继承的关系。
更加共性的那个类被叫做父类也叫做基类,具有自己特性的那个类,叫做子类或者叫做派生类。子类具有父类的所有成员,并且可以定义自己的成员变量和成员函数。
下面来写一下继承的例子。继承通过:表示继承关系
//比如我们定义了一个类叫book,所有的书都有书名和作者
#include<iostream>
using namespace std;
class book
{
public:
string _author;
string _name;
};
//我们定义了一种类别的书叫做audiobook,audiobook是book类的子类,通过继承符号:表示二者的关系
//有了继承关系之后,audiobook就默认包括了book类的书名和作者两个成员变量,同时又定义了自己的成员变量,讲述人。
class audiobook :public book //这里使用public对父类进行修饰,是一种访问权限
{
public:
string _narrator;
};
int main()
{
//我们可以看到,虽然audiobook中并没有直接定义_name和_author,但是因为父类有,所以仍然可以使用,这是他继承来的成员
audiobook abk;
abk._author = "herry";
abk._name = "king";
abk._narrator = "ross";
return 0;
}
2.2 类的访问权限
其实,子类并不能够访问父类的全部成员,这要看子类对父类的访问权限是怎么样的。访问权限有public/protected/private三个级别
class book
{
public:
string _author;
string _name;
protected:
string _ID;
private:
int _a;
};
就像上面定义的这个样子。public的类表示,我们在程序中定义了一个对象以后,可以通过这个对象直接访问类中的这个成员变量;protected表示,我们在main函数中是不能通过实例对象访问这个变量的,但是可以在类本身和他继承的类中使用这个对象;private表示,这个成员变量只有类本身可以访问,他的子类也不能访问
class audiobook:book
{
void f()
{
//这句话可以的,因为继承的类可以访问父类public和protected级别的成员
cout << _ID << endl ;
//这句话是不可以的,因为子类不能访问父类private级别的变量
//cout <<_a << endl ;
}
}
int main()
{
book bk;
//这句话是可以的,因为实例对象可以访问public级别的成员函数或者成员变量
cout<<bk._author;
//这两句话是不可以的,因为实例对象不可以访问private和protected级别的成员函数和成员变量
// cout<<bk._ID;
//cout<<bk._a;
}
2.3 父类和子类的关系
2.3.1 构造和析构
下面我们通过一个简单的程序来看一下,继承的过程到底是一个怎么样的过程。我们定义这样的两个类
#include<iostream>
using namespace std;
class LibMat
{
public:
LibMat()
{
cout<<"这是libmat的构造函数"<<endl;
}
~LibMat()
{
cout<<"这是libmant的析构函数"<<endl;
}
};
class book: public LibMat
{
public:
book()
{
cout<<"这是book的构造函数"<<endl;
}
~book()
{
cout<<"这是book的析构函数"<<endl;
}
};
int main()
{
{ //小括号表示生存域,出了括号,book的实例对象bk就不存在了
book bk;
}
return 0;
}
运行这段代码之后,我们发现显示父类LibMat构造,然后是book构造;析构的顺序相反,book先析构,LibMat再析构。于是我们就大概理解了继承是个怎么样的过程。每次构造子类的时候,先把父类有的东西复制到子类,因此会调用父类的构造函数。然后再回产生子类自己的东西,于是子类的构造函数被调用。析构的时候,先移去子类自己的东西,再清理父类的东西。
2.3.2 初始化
使用类一般需要对这些类进行初始化,那么当子类使用父类的成员变量的时候,如何完成初始化这个过程呢?我们可以使用父类的构造函数完成初始化过程。
#include<iostream>
using namespace std;
//定义了父类
class book
{
public:
book(string author, string name) :_author(author), _name(name)
{
}
protected:
string _author;
string _name;
};
//定义了子类,使用父类的构造函数完成子类的构造
class audiobook :public book
{
public:
audiobook(string author, string name, string narrator) :book(author, name), _narrator(narrator)
{
}
protected:
string _narrator;
};
int main()
{
audiobook abk("me", "mybook", "he");
return 0;
}
2.3.3 同名函数
这里要说明的一点是,如果父类和子类具有同名的函数,他们之间不会构成重载的关系,子类的函数会把父类的函数给掩盖掉
#include<iostream>
using namespace std;
class book
{
public:
book(string author, string name) :_author(author), _name(name)
{
}
void f(int a)
{
cout << "book" << endl;
}
protected:
string _author;
string _name;
};
class audiobook :public book
{
public:
audiobook(string author, string name, string narrator) :book(author, name), _narrator(narrator)
{
}
void f()
{
cout << "audiobook" << endl;
}
protected:
string _narrator;
};
int main()
{
audiobook abk("me", "mybook", "he");
abk.f();
//abk.f(1); 这个函数并不能够被识别。所以父类和子类的同名函数不构成重载关系,而是被隐藏了
return 0;
}
2.4 关于继承的一个小建议
如果说,我们把一些类的共有特征抽象成了一个基类,我们不需要创建基类的实例对象,但是我们在基类里面放置了一些重要的成员。这个时候,我们可以把基类的构造函数定义为protected的。这么做的话,用户就不能使用构造函数定义这个抽象类,同时它的子类可以通过构造函数来定义这个抽象类,往里面给重要成员变量赋值。
比如我们把所有的书抽象为LibMat,然后发现所有的书都有作者和名字,我们可以把这两个成员函数放到最抽象的libMat类中,然后我们不希望用户定义这个类,就把他的构造函数定义为protected的,只能通过他的子类来调用构造函数并赋值。比如book类。
3. 多态
3.1 造型
造型简单来说,就是把父类当子类用,把子类当父类用。比如把子类的指针指向父类,叫做向上造型;把父类的指针指向子类,叫做向下造型。向下造型是安全的,因为父类中所有的成员子类都有,不存在丢失东西的问题;而向上造型就是不安全的了,因为父类不能调用子类的成员。
#include<iostream>
using namespace std;
class book
{
public:
book(string author, string name) :_author(author), _name(name)
{
}
void f()
{
cout << "book" << endl;
}
protected:
string _author;
string _name;
};
class audiobook :public book
{
public:
audiobook(string author, string name, string narrator) :book(author, name), _narrator(narrator)
{
}
void f()
{
cout << "audiobook" << endl;
}
protected:
string _narrator;
};
int main()
{
audiobook abk("me", "mybook", "he");
//这个过程就叫做向上造型,意思就是把子类当成父类使用
book* bk = &abk;
bk->f();
}
3.2 静态绑定
通过3.1的函数运行实例,我们发现,当我们用向上造型的方法,把子类当做父类使用,并且调用了一个父类和子类都有的函数,其运行结果并不是子类的函数,而是使用了父类的函数。
为什么会这样呢?这是因为,类中的函数在程序编译的时候就以及确定好了要使用哪个了,哪个类使用哪个函数是被提前绑定好了的,所以,即使把子类的指针交给父类,仍不能够改变父类所绑定的成员函数,所以在调用函数f的时候,运行的是父类的函数。这种成员函数的运行机制,就叫做静态绑定。
3.3 虚函数与动态绑定
既然有静态绑定,必然有一种机制叫做动态绑定。我们要先引入一个概念,叫虚函数。虚函数是对函数使用关键词virtual进行定义。虚函数的运行机制是,在一个类定义的时候,在类的最前端,产生一个指针,这个指针用来表示类中的虚函数具体是指向哪个内存空间,这个区域也叫做虚函数表。在程序编译的时候,并没有指定类中函数调用的时候要调用哪一个,而是程序运行的时候根据虚函数表的指向才确定。这是一种动态绑定的机制。基于动态绑定之后,我们再运行3.1的程序,这个时候使用向上造型的方法,把子类的指针交给父类的时候,实际上也把子类的虚函数表给复制了过来,所以程序再运行的时候,同名函数就调用的是子类的了。
#include<iostream>
using namespace std;
class book
{
public:
book(string author, string name) :_author(author), _name(name)
{
}
virtual void f()
{
cout << "book" << endl;
}
virtual ~book(){}
protected:
string _author;
string _name;
};
class audiobook :public book
{
public:
audiobook(string author, string name, string narrator) :book(author, name), _narrator(narrator)
{
}
virtual void f()
{
cout << "audiobook" << endl;
}
virtual ~audiobook(){}
protected:
string _narrator;
};
int main()
{
audiobook abk("me", "mybook", "he");
book* bk = &abk;
bk->f();
}
所有类中的虚函数都应该具有相同的函数名和函数声明,但是具体函数定义可以不相同。每个子类中都应该具有相对应的虚函数,否则运行的时候没有办法找到对应的运行函数。
3.4 构造函数和析构函数与虚函数
普通函数的虚函数我们已经看到了,那么构造函数和析构函数是否存在虚函数呢?
对于构造函数而言,由于函数定义的时候必须要进行初始化,所以这个调用的函数必须在编译的时候就明确好了,因此构造函数是不允许做成虚函数的,事实上,大部分编译器也认为这是个错误的。
而对于析构函数来说,就必须要使用虚函数了,因为有时候你使用的一些资源是来自子类的,比如申请的内存空间等等,必须要用子类的析构函数才能释放这些资源,这个时候如果析构函数不是虚函数,只能调用父类的析构函数,很可能会出现资源无法释放的问题。
3.5 多态
知道了各种基础知识以后,我们其实也就对多态这个名词比较了解了。多态就是向上构造和动态绑定的组合。这样使得我们定义函数的时候,只要写一个基类的指针就行了,所有的派生类都可以通过这个基类的指针被送入函数,同时由于动态绑定,我们可以让那些指针去调用派生类的函数,最大程度上简化了类的设计。
3.6 静态函数与虚函数
由于静态函数是在程序运行之前被定义在全局变量区的,虚函数是在运行之后才确定的,这两类函数有着本质的区别,因此静态函数是不能够被定义为虚函数的。
#include<iostream>
using namespace std;
class LibMat
{
public:
LibMat()
{
cout << "这里是libmat的构造函数!" << endl;
}
virtual void print()
{
cout << "我是libmat" << endl;
}
virtual ~LibMat()
{
cout << "这里是libmat的析构函数!" << endl;
cout << endl;
}
private:
};
class book: public LibMat
{
public:
book(string title, string author) :_title(title), _author(author)
{
cout << "这里是book的构造函数!" << endl;
}
virtual void print()
{
cout << "我是book" << endl;
cout << "我的title是" << _title << endl;
cout << "我的author是" << _author << endl;
}
virtual ~book()
{
cout << "这里是book的析构函数!" << endl;
cout << endl;
}
string& title(){ return _title; }
string& author(){ return _author; }
protected:
string _title;
string _author;
};
class audiobook :public book
{
public:
//配合注解2,派生类构造函数要给父类赋值,如果要用父类定义的对象的话
audiobook(string title, string author, string narrator) :book(title, author), _narrator(narrator)
{
cout << "这里是audiobook的构造函数!" << endl;
}
virtual void print()
{
cout << "我是audiobook" << endl;
cout << "我的title是" << _title << endl;
cout << "我的author是" << _author << endl;
cout << "我的narrator是" << _narrator << endl;
}
virtual ~audiobook()
{
cout << "这里是audiobook的析构函数!" << endl;
cout << endl;
}
string& narrator() { return _narrator; }
protected:
string _narrator;
};
void print(LibMat& lib)
{
lib.print();
}
int main()
{
{
book bk("my book", "me");
print(bk);
}
{
audiobook abk("my audiobook", "me", "he");
abk.author() = "he";
abk.narrator() = "me";
print(abk);
}
return 0;
}
//注解1:这个程序演示了继承和多态的使用方法。基类为libmat,book是libmat的派生类,audiobook是book的派生类
//注解2:从里面看出,使用继承,必须要给他的基类进行赋值,使用初始化列表就行。同时父类的所有对象,都可以被
//派生类使用,只要父类的对象是public或者protected的
//注解3:从外部函数print的实现情况来看,虚函数是一种动态绑定,只要运行时候才会去判断自己要调用哪个函数
//注解4:如果类中有虚函数,析构函数必须也是虚函数,否则派生类多的一些对象没有相对应的析构函数去析构
4.如何定义一个类
- 确定哪些操作是共有的,构成基类
- 确定哪些操作是特有的,定义为虚函数
- 根据需要对成员变量和成员函数设定访问级别
5.什么情况下虚函数会变成静态调用
在有些时候,虚函数也会变成静态调用。可能是我们刻意为之,也可能是错误,下面就谈一下虚函数哪些情况下会变成静态调用
5.1 使用 class scope
要调用的虚函数前加入class scope,可以明确的表示你要调用哪个函数,通过这种方式以后,这个虚函数就变成确定的了,就变成静态绑定的了。一般应用场景是,你定义的这个虚函数需要能够特化基类的虚函数,同时这个类中还有其他函数要调用这个虚函数,于是就在其他函数调用虚函数的地方,给他加一个class scope,举例如下:
#include<iostream>
using namespace std;
class LibMat
{
public:
LibMat()
{
cout << "这里是libmat的构造函数!" << endl;
}
virtual void print()
{
cout << "我是libmat" << endl;
}
virtual ~LibMat()
{
cout << "这里是libmat的析构函数!" << endl;
}
void P()
{
print();
}
void Q()
{
LibMat::print();
}
};
class book : public LibMat
{
public:
book(string title, string author) :_title(title), _author(author)
{
cout << "这里是book的构造函数!" << endl;
}
virtual void print()
{
cout << "我是book" << endl;
cout << "我的title是" << _title << endl;
cout << "我的author是" << _author << endl;
}
virtual ~book()
{
cout << "这里是book的析构函数!" << endl;
}
string& title() { return _title; }
string& author() { return _author; }
protected:
string _title;
string _author;
};
class audiobook :public book
{
public:
audiobook(string title, string author, string narrator) :book(title, author), _narrator(narrator)
{
cout << "这里是audiobook的构造函数!" << endl;
}
virtual void print()
{
cout << "我是audiobook" << endl;
cout << "我的title是" << _title << endl;
cout << "我的author是" << _author << endl;
cout << "我的narrator是" << _narrator << endl;
}
virtual ~audiobook()
{
cout << "这里是audiobook的析构函数!" << endl;
}
string& narrator() { return _narrator; }
protected:
string _narrator;
};
int main()
{
audiobook abk("my audiobook", "me", "he");
abk.P();
cout << endl;
abk.Q();
cout << endl;
return 0;
}
//注解1:有时候,我们可能会在类中的其他函数中调用虚函数,但是我们不希望这个虚函数是运行时候才确定是哪个,
//因为我们只想调用自己这个类的函数,而不希望他动态绑定到其他地方。所以前面要使用class scope符号,来明确
//调用哪个函数,这相当于隐藏了虚函数的运行机制。比如函数P,就调用的是audiobook的print,而Q调用的是libmat的print
5.2 虚函数不完全匹配
5.2.1 一个典型的错误
如果两个虚函数不完全匹配,是不能够形成多态的效果的,最典型的就是把函数定义为const,但是另外一个没有定义为const,这样虽然编译能过,程序能够正常运算,但是运行结果却是静态绑定的结构
#include<iostream>
using namespace std;
class num_sequence
{
public:
virtual const char* what_am_i()const
{
return "num_sequence";
}
};
class Fibnoacci: public num_sequence
{
public:
virtual const char* what_am_i()
{
return "Fibnoacci";
}
};
int main()
{
num_sequence p;
Fibnoacci q;
num_sequence& pp = q;
cout << pp.what_am_i() << endl;;
return 0;
}
5.2.2 特例
一种虚函数不完全匹配,但是仍然是动态匹配结果的例子是,函数返回值是类本身。这样仍然是动态绑定的效果,可以看做是一种特例
#include<iostream>
using namespace std;
class num_sequence
{
public:
virtual num_sequence& f()
{
cout << "num_sequence" << endl;
return *this;
}
};
class Fibonacci :public num_sequence
{
public:
virtual Fibonacci & f()
{
cout << "Fibonacci" << endl;
return *this;
}
};
int main()
{
Fibonacci b;
num_sequence p;
num_sequence* pp = &b;
pp->f();
return 0;
}
5.3 没有通过索引和指针的方式进行访问
多态构成的核心条件就是,子类把指针交给父类,所以,如果在写函数的参数列表时,把类定义了不是地址类型的,那么自然也就无法构成多态,结果也就是一种静态绑定的结果了。
#include<iostream>
using namespace std;
class LibMat
{
public:
LibMat()
{
cout << "这里是libmat的构造函数!" << endl;
}
virtual void print()
{
cout << "我是libmat" << endl;
}
virtual ~LibMat()
{
cout << "这里是libmat的析构函数!" << endl;
cout << endl;
}
private:
};
class book : public LibMat
{
public:
book(string title, string author) :_title(title), _author(author)
{
cout << "这里是book的构造函数!" << endl;
}
virtual void print()
{
cout << "我是book" << endl;
cout << "我的title是" << _title << endl;
cout << "我的author是" << _author << endl;
}
virtual ~book()
{
cout << "这里是book的析构函数!" << endl;
cout << endl;
}
string& title() { return _title; }
string& author() { return _author; }
protected:
string _title;
string _author;
};
class audiobook :public book
{
public:
audiobook(string title, string author, string narrator) :book(title, author), _narrator(narrator)
{
cout << "这里是audiobook的构造函数!" << endl;
}
virtual void print()
{
cout << "我是audiobook" << endl;
cout << "我的title是" << _title << endl;
cout << "我的author是" << _author << endl;
cout << "我的narrator是" << _narrator << endl;
}
virtual ~audiobook()
{
cout << "这里是audiobook的析构函数!" << endl;
cout << endl;
}
string& narrator() { return _narrator; }
protected:
string _narrator;
};
void print(LibMat lib) //函数传递参数没有使用引用或者指针的方式,即使传入book,也不调用book的虚函数
{
lib.print();
}
int main()
{
{
book bk("my book", "me");
print(bk);
}
return 0;
}
6. 多态类的类型转换
6.1 鉴定多态类
c++中有一个函数,能够鉴别程序运行过程中,当前的多态类绑定到了哪个类中,需要头文件#include
依旧使用前面的libMat,book和audiobook三个类,只是更新一个函数print
void print(LibMat &lib)
{
lib.print();
cout << typeid(lib).name() << endl; //鉴别当前的多态类与谁绑定
cout << (typeid(lib) == typeid(book) )<< endl;//同时这个多态类还支持比较运算符
}
6.2 静态转换
我们可以使用多态类调用其他类中的成员函数,但是必须要把多态类进行类型转换才能调用。如果我们不进行类型转换,即使编译器知道它就是那个类型,也是不能运行的。比如
book bk("me","mybook");
LibMat *lib = &bk;
//lib->book::author();
//这句话是不能调用的,即使我们知道确实当前多态类指代的就是book,不经过类型转换,我们仍然不能使用book的成员函数
静态类型转换static_cast有些不安全,因为他是无条件的转换,即使转换条件不成了,所以我们必须要判断一下,当前的多态类指代是谁
//使用静态类型转换,为了确保安全,要先保证该多态是否是我们要的那个样子
if (typeid(*lb) == typeid(book))
{
book* b = static_cast<book*>(lb);
cout << b->book::author() << endl;//不进行类型转换直接用会报错的
}
6.3 动态转换
动态类型转换dynamic_cast只有符号条件才能转换,只能从宽类型往窄类型转换,所以不需要判断当前多态类指代是谁,直接用即可
//动态类型转换,只有能转,才会转
if (book *b1 = dynamic_cast<book*>(lb))
{
cout << "b1" << endl;
cout << b1->book::author() << endl;//不进行类型转换直接用会报错的
}
6.4 完整代码
多态类型转换的代码贴在这里,虽然目前并不是很清楚,对多态类做类型转换有什么意义。
#include<iostream>
#include<typeinfo>
using namespace std;
class LibMat
{
public:
LibMat()
{
cout << "这里是libmat的构造函数!" << endl;
}
virtual void print()
{
cout << "我是libmat" << endl;
}
virtual ~LibMat()
{
cout << "这里是libmat的析构函数!" << endl;
cout << endl;
}
private:
};
class book : public LibMat
{
public:
book(string title, string author) :_title(title), _author(author)
{
cout << "这里是book的构造函数!" << endl;
}
virtual void print()
{
cout << "我是book" << endl;
cout << "我的title是" << _title << endl;
cout << "我的author是" << _author << endl;
}
virtual ~book()
{
cout << "这里是book的析构函数!" << endl;
cout << endl;
}
string& title() { return _title; }
string& author() { return _author; }
protected:
string _title;
string _author;
};
class audiobook :public book
{
public:
//配合注解2,派生类构造函数要给父类赋值,如果要用父类定义的对象的话
audiobook(string title, string author, string narrator) :book(title, author), _narrator(narrator)
{
cout << "这里是audiobook的构造函数!" << endl;
}
virtual void print()
{
cout << "我是audiobook" << endl;
cout << "我的title是" << _title << endl;
cout << "我的author是" << _author << endl;
cout << "我的narrator是" << _narrator << endl;
}
virtual ~audiobook()
{
cout << "这里是audiobook的析构函数!" << endl;
cout << endl;
}
string& narrator() { return _narrator; }
protected:
string _narrator;
};
void print(LibMat& lib) //函数传递参数没有使用引用或者指针的方式,即使传入book,也不调用book的虚函数
{
lib.print();
cout << typeid(lib).name() << endl;
cout << (typeid(lib) == typeid(book)) << endl;
}
int main()
{
LibMat libmat;
book bk("my book", "me");
audiobook abk("my audiobook", "me", "he");
//LibMat* lb = &libmat; //这句话两个函数都不会执行,因为窄类型变不成宽类型
//LibMat* lb = &bk; //这句话两个函数都会执行
LibMat* lb = &abk; //这句话,动态类型转换可以,静态类型转换不可以,动态类型转换能把宽类型变成窄类型
//使用静态类型转换,为了确保安全,要先保证该多态是否是我们要的那个样子
if (typeid(*lb) == typeid(book))
{
book* b = static_cast<book*>(lb);
cout << b->book::author() << endl;//不进行类型转换直接用会报错的
}
//动态类型转换,只有能转,才会转
if (book *b1 = dynamic_cast<book*>(lb))
{
cout << "b1" << endl;
cout << b1->book::author() << endl;//不进行类型转换直接用会报错的
}
return 0;
}