本章节书里讲的比较杂,索性用自己的方式总结了一波,有些没有实质内容的章节就跳过了
面向对象编程概念最主要特质:继承和多态
class 新类的名字:继承方式 继承类的名字{};
// 基类
class Animal {
// eat() 函数
// sleep() 函数
};
//派生类
class Dog : public Animal {
// bark() 函数
};
① 访问权限
访问 | public | protected | private |
---|---|---|---|
同一个类 | yes | yes | yes |
派生类 | yes | yes | no |
外部的类 | yes | no | no |
② 继承后的子类成员访问权限
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类不可见 | 在派生类不可见 | 在派生类不可见 |
③ 派生类继承了什么?
一个派生类继承了所有的基类方法,但下列情况除外:
④ 同名的成员变量和成员函数
在有些时候,父类和子类中出现了同一个成员变量,如下name
,这个时候编译器是以子类为优先。想要访问父类的name
成员变量,就需要加上修饰
cout << people::name<< endl;
同样一个函数print
在父类和子类中都存在,编译器会默认调用子类中匹配的函数,(函数重载是在同一个作用域,这里父类和子类是两个作用域)
构造函数调用顺序:基类 > 成员类 > 派生类
析构函数和构造函数相反,编译器默认先调用子类的析构函数,再调用父类的析构函数。
#include
using namespace std;
class A {
public:
A() {
cout << "call A()" << endl;
}
~A() {
cout << "call ~A()" << endl;
}
};
class B : A {
public:
B(int val) : A(), value(val) {
val = 0; // 重新赋值
cout << "call B()" << endl;
}
~B() {
cout << "call ~B()" << endl;
}
private:
int value;
};
int main() {
B b(10);
return 0;
}
/*
* 结果如下
* call A()
* call B()
* call ~B()
* call ~A()
* 说明放在初始化列表的部分在构造函数之前执行
*/
多态字面意思就是多种形态。
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
举个例子:买票
普通人全价,学生半价,军人全价但优先购票…
这就是对买票这一个具体行为,不同人去完成时产生的不同结果。
C++的多态必须满足两个条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
#include
using namespace std;
class Person
{
public:
virtual void display()
{
cout << "全价票" << endl;
}
};
class Student :public Person
{
public:
virtual void display() // //子类完成对父类虚函数的重写
{
cout << "半价票" << endl;
}
};
void BuyTicket(Person* p)
{
p->display();
}
int main(){
Student st;
Person p;
BuyTicket(&st); //半价票
BuyTicket(&p); //全价票
system("PAUSE");
return 0;
}
/* 将传地址改为传对象,不满足多态条件后,调用的都是父类的函数
void BuyTicket(Person p)
{
p.display();
}
int main(){
Student st;
Person p;
BuyTicket(st); //全价票
BuyTicket(p); //全价票
system("PAUSE");
return 0;
}
*/
如果满足多态,编译器会调用指针指向对象的虚函数,而与指针的类型无关。如果不满足多态,编译器会直接根据指针的类型去调用虚函数。
用virtual
修饰的关键字就是虚函数,且虚函数只能是类中非静态的成员函数。
子类和父类中的虚函数拥有相同的名字,返回值,参数列表,那么称子类中的虚函数重写了父类的虚函数
虚函数可以不实现(定义)。不实现(定义)的虚函数是纯虚函数。 virtual int area() = 0;` = 0 告诉编译器,函数没有主体。那么纯虚函数有什么用呢?
1,强制子类重写虚函数,完成多态。
2,表示某些抽象类。
虚函数重写的两个例外:
- 协变:子类的虚函数和父类的虚函数的返回值可以不同,也能构成重载。但对返回值有要求:需要子类的返回值是一个子类的指针或者引用,父类的返回值是一个父类的指针或者引用,且返回值代表的两个类也成继承关系。这个叫做协变。
- 析构函数的重写:析构函数名字天生不一样,怎么实现多态?
/*test.1 B继承了A,他们的析构函数没有重写。 */ class A { public: ~A() { cout << "~A()" << endl; } }; class B : public A { public: ~B() { cout << "~B()" << endl; } }; A* a = new B; //把B的对象切片给A类型的指针。 delete a; //实际调用的是A的析构函数 /*test.2 B继承了A,他们的析构函数没有重写。 */ class A { public: virtual ~A() { cout << "~A()" << endl; } }; class B : public A { public: ~B() { cout << "~B()" << endl; } }; A* a = new B; //把B的对象切片给A类型的指针。 delete a; //有进行了重写,会先调用B的析构函数,后调用A的析构函数
override
检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。(定义在子类)
感觉没多大意义,有点画蛇添足,既然都想到了要写override
,那还能忘记要重写基类的虚函数?或者说写错?
final
用
final
修饰的虚函数无法重写。用final
修饰的类无法被继承。final
像这个单词的意思一样,这就是最终的版本,不用再更新了。(定义在父类)
子类对有final
修饰的虚函数进行重写,会报错
作用域 | 组成 | 要求 | |
---|---|---|---|
重载 | 两者处在同一作用域 | 函数名相同,参数、返回值不同 | |
覆盖(重写) | 两者分别在基类和派生类的作用域 | 函数名、参数、返回值相同(协变除外) | 两者都是虚函数 |
重定义(隐藏) | 两者分别在基类和派生类的作用域 | 函数名相同,参数、返回值不同 | 子类和父类的同名函数不是重定义就是重写 |
普通函数的继承就是实现继承,虚函数的继承就是接口继承。子类继承了函数的实现,可以直接使用。虚函数重写后只会继承接口,重写实现。所以如果不用多态,不要把函数写成虚函数。
#include
using namespace std;
class A
{
public:
virtual void fun(int val = 0)
{
cout << "A->val = " << val << endl;
}
void Fun()
{
fun();//传过来一个子类指针调用fun()
}
};
class B:public A
{
public:
virtual void fun(int val = 1)
{
cout << "B->val = " << val << endl;
}
};
int main(){
B b;
b.Fun();
system("PAUSE");
return 0;
}
定义一个抽象基类
所有子类共通的操作行为,这些行为代表的是基类的公有接口
找出哪些操作行为(基类成员函数)必须根据不同的派生类而有不同的实现方式。
如果某操作行为(基类成员函数)必须根据不同的派生类而有不同的实现方式,这个操作行为就要成为整个类继承体系的虚函数,即该成员函数(至少在基类中)的声明前加virtual。
①某个操作行为(类成员函数)能让一般程序(在该类主体和该类的派生类如果有派生类的话外)访问这个成员函数,那么我们将这个成员函数的声明/定义放进基类的public:里。
②某个操作行为(类成员函数))在基类之外不需要被用到,自然不用在基类之外访问这个成员函数。我们就把这个成员函数的声明/定义放进基类的private:里
③某个操作行为(类成员函数)在该类的派生类(如果有的话)主体/对象可以被访问,却不许一般程序访问。 那么我们就把这个成员函数的声明/定义放进基类的protected:里。
#include
inline const char* num_sequence::what_am_i()const{return typeid(*this).name();}
num_sequence *ps=&fib;
//...
if(typeid(*ps)==typeid(Fibonacci))
{}
//...
这一堆代码是测试ps这个基类指针是否指向某个派生类类对象。
用static_cast运算符来强制转换(无条件转换)基类指针变为派生类指针。用法举例:
if(typeid(*ps)==typeid(Fibonacci))
{
Fibonacci *pf=static_cast(*ps);
pf->gen_elems(64);//经过static_cast运算符转换指针变为派生类指针
//这种写法得以被编译器承认
}
不过static_cast有个潜在危险,就是编译器无法确定转换类指针从基类到派生类是不是转换对了,所以我们加了if(typeid(*ps)==typeid(Fibonacci)){}
如果typeid运算符(这里是==和())运算结果为真,就执行类指针的无条件转换。
dynamic_cast运算符没有这种潜在危险。其用法举例:
if(Fibonacci *pf=dynamic_cast(ps))
{
pf->gen_elems(64);//经过dynamic_Cast运算符转换指针变为派生类指针
//这种写法得以被编译器承认
}