系统学习C++,本章将记录类继承、类多态的相关概念
方便自己日后复习,错误的地方希望积极指正
往期文章:
C++基础从0到1入门编程(一)
C++基础从0到1入门编程(二)
C++基础从0到1入门编程(三)
C++基础从0到1入门编程(四)
C++基础从0到1入门编程(五)
参考视频:
1.黑马程序员匠心之作|C++教程从0到1入门编程,学习编程不再难
2.系统化学习C++
1 类继承
1.1 基本概念
继承:一个类从另一个类获取成员变量和成员函数的过程
语法:
class 派生类名: [继承方式] 基类名
{
派生类新增加的成员
}
被继承的类称为基类或父类
;继承的类称为派生类或子类
使用继承的场景
(1)新创建的类和现有的类相似,只是多出若干成员变量或成员函数,可以使用继承
#include <iostream>
using namespace std;
class CallComers
{
public:
string name_;
string tel_;
CallComers() { name_ = "Big"; tel_ = "123"; }
void sing() { cout << "I am a bird\n"; }
void setname(const string& name) { name_ = name; }
void settel(const string& tel) { tel_ = tel; }
};
class CGirl : public CallComers
{
public:
int bh_;
CGirl() { bh_ = 8; }
void show() { cout << bh_ << ' ' << name_ << ' ' << tel_ << endl; }
};
int main()
{
CGirl g;
g.name_ = "Small";
g.show(); // 8 Small 123
}
(2)当需要创建多个类时,如果拥有很多相似的成员变量或成员函数,可以将这些类提取出来,定义为基类,然后从基类继承
class Sort
{
int data[30];
void print()
};
class BubbleSort :public Sort
{
void sort();
};
class ShellSort :public Sort
{
void sort();
};
1.2 继承方式
类成员访问权限:public - > protected - > private
public成员在类外可以访问,private成员只能在类的成员函数中访问
不考虑继承关系,protected成员和private成员一样,类外不能访问。当存在继承关系,基类的protected成员可以在派生类中访问,而基类的private成员不能在派生类中访问
继承方式:public、protected、private。如果不写,默认为private
(1)基类成员在派生类中的访问权限不得高于继承方式中指定的权限。继承方式中的public、protected、private是用来指明基类成员在派生类中的最高访问权限的
(2)不管继承方式如何,基类中的private成员在派生类中始终不能使用,不能在派生类的成员函数中访问或调用
(3)如果希望基类的成员能够被派生类继承并且毫无障碍地使用,那么这些成员只能声明为public 或protected;只有那些不希望在派生类中使用的成员才声明为private
(4)如果希望基类的成员既不向外暴露(不能通过对象访问),还能在派生类中使用,那么只能声明为 protected
由于private和protected继承方式会改变基类成员在派生类中的访问权限,导致继承关系复杂,在实际开发中,一般使用public
(5)在派生类中,可以通过基类的公有成员函数间接访问基类的私有成员
(6)使用 using 关键字可以改变基类成员在派生类中的访问权限
using只能改变基类中public和protected成员的访问权限,不能改变private成员的访问权限,因为基类中的private成员在派生类中是不可见的,根本不能使用
#include <iostream>
using namespace std;
class A
{
public:
int a_ = 10;
protected:
int b_ = 20;
private:
int c_ = 30;
};
class B :public A
{
public:
using A::b_;
// using A::c_; // 报错
private:
using A::a_;
};
int main()
{
B b;
// b.a_ = 10;
b.b_ = 21;
}
1.3 继承的对象模型
- 创建派生类对象,先调用基类的构造函数,再调用派生类的构造函数。销毁派生类对象,先调用派生类的析构函数,再调用基类的析构函数。
如果手工调用派生类的析构函数,也会调用基类的析构函数
基类(构造函数)-> 派生类(构造函数) -> 派生类(析构函数) -> 基类(析构函数) - 创建派生类对象只会申请一次内存,派生类对象包含了基类对象的内存空间,this指针是相同的
- 创建派生类对象,先初始化基类对象,再初始化派生类对象
- 对派生类对象用sizeof得到的是基类所有成员(包括私有成员)+ 派生类对象所有成员的大小
- 在C++中,不同继承方式的访问权限只是语法上的处理
- 对派生类对象用memset()会清空基类私有成员
memset(p, 0, sizeof(B));
- 用指针可以访问到基类中的私有成员(内存对齐)
*((int*)p + 2) = 123;
#include <iostream>
#include <cstring>
using namespace std;
void* operator new(size_t size)
{
void* ptr = malloc(size);
cout << ptr << ' ' << size << endl;
return ptr;
}
void operator delete(void* ptr)
{
if (ptr == 0) return;
free(ptr);
cout << "Free\n";
}
class A
{
public:
int a_ = 10;
protected:
int b_ = 20;
private:
int c_ = 30;
public:
A()
{
cout << this << endl;
cout << &a_ << endl;
cout << &b_ << endl;
cout << &c_ << endl;
}
void func()
{
cout << a_ <<' ' << b_ << ' ' << c_ << endl;
}
};
class B:public A
{
public:
int d_ = 40;
B()
{
cout << this << endl;
cout << &a_ << endl;
cout << &b_ << endl;
cout << &d_ << endl;
}
void func1() { cout << d_ << endl; }
};
int main()
{
cout << sizeof(A) << endl; // 12
cout << sizeof(B) << endl; // 16
B* p = new B;
p->func(); p->func1();
// memset(p, 0, sizeof(B));
// p->func(); p->func1();
*((int*)p + 2) = 123;
p->func(); p->func1();
delete p;
}
1.4 构造基类
派生类构造函数的要点:
(1)创建派生类对象,程序先调用基类构造函数,然后再调用派生类构造函数
(2)没有指定基类构造函数,将使用基类的默认构造函数
(3)可以用初始化列表指明要使用的基类构造函数
(4)基类构造函数负责初始化被继承的数据成员;派生类构造函数用于初始化新增的数据成员
(5)派生类的构造函数总是调用一个基类构造函数,包括拷贝构造函数
#include <iostream>
using namespace std;
class A
{
public:
int a_;
private:
int b_;
public:
A() : a_(0), b_(0)
{
cout << "Base class MorenGouzao\n";
}
A(int a, int b) : a_(a), b_(b)
{
cout << "Base class Gouzao\n";
}
A(const A &a) : a_(a.a_), b_(a.b_)
{
cout << "Base class KaoBeiGouzao\n";
}
void showA()
{
cout << a_ << ' ' << b_ << endl;
}
};
class B:public A
{
public:
int c_;
B() : c_(0), A()
{
cout << "Moren GouzaoB()\n";
}
B(int a,int b, int c) : A(a,b), c_(c)
{
cout << "B(int a,int b, int c)\n";
}
B(const A& a, int c) : A(a), c_(c)
{
cout << "B(const A& a, int c)\n";
}
void showB()
{
cout << c_ << endl;
}
};
int main()
{
B b1;
b1.showA(); // 0 0
b1.showB(); // 0
B b2(1, 2, 3);
b2.showA(); // 1 2
b2.showB(); // 3
A a(10, 20);
B b3(a, 20);
b3.showA(); // 10 20
b3.showB(); // 20
}
1.5 名字遮蔽与类作用域
如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,通过派生类对象或者在派生类的成员函数中使用该成员时,将使用派生类新增的成员,而不是基类的
#include <iostream>
using namespace std;
class A
{
public:
int a_ = 30;
void func() { cout << "A func()\n"; }
};
class B:public A
{
public:
int a_ = 80;
void func() { cout << "B func()\n"; }
};
int main()
{
B b;
cout << b.a_ << endl; // 80
b.func(); // B func()
}
Tip:基类的成员函数和派生类的成员函数不会构成重载,如果派生类有同名函数,那么就会遮蔽基类中的所有同名函数(因为作用域)
在成员名前面加类名和域解析符可以访问对象的成员
如果不存在继承关系,类名和域解析符可以省略不写
#include <iostream>
using namespace std;
class A
{
public:
int a_ = 30;
void func() { cout << "A func()\n"; }
void func(int a) { cout << "A func(int a)\n"; }
};
class B:public A
{
public:
int a_ = 80;
//void func() { cout << "B func()\n"; }
};
int main()
{
B b;
cout << b.a_ << endl; // 80
b.func(); // B func()
b.func(1); // 把派生类的func()注释掉可以运行
}
当存在继承关系时,基类的作用域嵌套在派生类的作用域中。如果成员在派生类的作用域中已经找到,就不会在基类作用域中继续查找;如果没有找到,则继续在基类作用域中查找
在成员的前面加上类名和域解析符,就可以直接使用该作用域的成员
#include <iostream>
using namespace std;
class A
{
public:
int a_ = 10;
void func() { cout << "A func()\n"; }
};
class B : public A
{
public:
int a_ = 20;
void func() { cout << "B func()\n"; }
};
class C : public B
{
public:
int a_ = 30;
void func() { cout << "C func()\n"; }
void show()
{
cout << C::a_ << ' ' << B::a_ << ' ' << B::A::a_ << endl;
}
};
int main()
{
C c;
cout << c.C::a_ << endl; // 30
cout << c.B::a_ << endl; // 20
cout << c.B::A::a_ << endl;// 10
c.C::func(); // C func()
c.B::func(); // B func()
c.B::A::func();// A func()
c.show(); // 30 20 10
}
1.6 继承的特殊关系
派生类和基类之间的特殊关系
(1)如果继承方式是公有的,派生类对象可以使用基类成员
(2)派生类对象赋值给基类对象(包括私有成员),会舍弃非基类的成员
(3)基类指针可以在不显示转换的情况下指向派生类对象
(4)基类引用可以在不显示转换的情况下指向派生类对象
Tip:
(1)基类指针或引用只能调用基类的方法,不能调用派生类的方法
(2)用派生类构造基类
(3)如果函数形参是基类,实参可以用派生类
(4)C++要求指针和引用类型与赋给的类型匹配,这一规则对继承来说是例外。但是,这种例外只是单向的,不可以将基类对象和地址赋给派生类引用和指针(没有价值)
#include <iostream>
using namespace std;
class A
{
public:
int a_ = 0;
private:
int b_ = 0;
public:
void show()
{
cout << a_ << ' ' << b_ << endl;
}
void setb(int b) { b_ = b; }
};
class B:public A
{
public:
int c_ = 0;
void show() { cout << a_ << ' ' << c_ << endl; }
};
int main()
{
B b;
A* a = &b;
b.a_ = 10;
b.setb(20);
b.c_ = 30;
b.show(); // 10 30
a->a_ = 11;
a->setb(12);
a->show(); // 11 12
}
1.7 类继承 - 多继承与虚继承
多继承语法:
class B : public A, public C
{
int a_ = 10;
}
菱形继承:
虚继承可以解决菱形继承的二义性
、数据冗余
问题
有了多继承,就存在菱形继承,有了菱形继承就有虚继承,变得更复杂
不提倡使用多继承,只有在比较简单和不出现二义性的情况下才使用多继承,能用单一继承解决的问题就不要用多继承
多继承:
#include <iostream>
using namespace std;
class A1 {
public:
int a_ = 10;
};
class A2 {
public:
int a_ = 20;
};
class B :public A1, public A2 {
public:
int a_ = 30;
};
int main()
{
B b;
cout << b.a_ << endl; // 30
cout << b.A1::a_ << endl;// 10
cout << b.A2::a_ << endl;// 20
}
菱形继承:
#include <iostream>
using namespace std;
class A
{
public:
int a_ = 10;
};
class B : virtual public A {};
class C : virtual public A {};
class DD : public B, public C {};
int main()
{
DD d;
d.a_ = 20;
cout << d.a_ << endl; // 20
cout << &d.B::a_ << ' ' << &d.C::a_ << endl; // 0x47d1ffc90 0x47d1ffc90
}
2 类多态
2.1 多态的基本概念
基类指针只能调用基类的成员函数,不能调用派生类的成员函数。
如果在基类的成员函数前加virtual关键字,把它声名为虚函数,基类指针就可以调用派生类中同名的成员函数,通过派生类中的成员函数,就可以访问派生对象的成员变量
有了虚函数,基类指针指向基类对象时就使用基类的成员函数和数据,指向派生类对象时就使用派生类的成员函数和数据,基类指针表现出了多种形式,这种现象称为多态
基类引用也可以使用多态
Tip:
(1)只需要在函数声名的时候加上virtual关键字,函数定义时不能加
(2)派生类中重定义虚函数,函数特征要相同
(3)基类中定义了虚函数时,如果派生类没有重定义该函数,那么将使用基类的虚函数
(4)在派生类中重定义了虚函数的情况下,如果想使用基类的虚函数,可以加类名和域解析符
(5)如果要在派生类中重新定义基类的函数,则将它设置为虚函数;否则,不要设置为虚函数
有两方面好处:(1)普通函数效率更高(2)指出不要重新定义该函数
#include <iostream>
using namespace std;
class CAllComers{
public:
int bh_ = 0;
virtual void show() { cout << "CAllComers::show() " << bh_ << endl; }
virtual void show(int a) { cout << "CAllComers::show(int a) " << bh_ << endl; }
};
class CGirl : public CAllComers {
public:
int age_ = 0;
void show() { cout << "CGirl::show() " << bh_ << ' ' << age_ << endl; }
void show(int a) { cout << "CGirl::show(int a) " << bh_ << ' ' << age_ << endl; }
};
int main()
{
CAllComers a;
a.bh_ = 3; // 创建基类对象并对成员赋值
CGirl g;
g.bh_ = 8; g.age_ = 23;// 创建派生类对象并对成员赋值
CAllComers* p;// 声明基类指针
// p = &a;
// p->show(); //CAllComers::show() 3 // 让基类指针指向基类对象,并调用虚函数
// p->show(5);
p = &g;
p->show(); // CGirl::show() 8 23 // 让基类指针指向派生类对象,并调用虚函数
p->show(5);
p->CAllComers::show(5);
}
2.2 多态的应用场景
(1)基类的虚函数实现基本功能
(2)派生类重新定义虚函数,扩展功能、提升功能
(3)实现个性化功能
#include <iostream>
using namespace std;
class Hero
{
public:
int viability;
int attack;
virtual void skill1() { cout << "One skill1" << endl; }
virtual void skill2() { cout << "Two skill2" << endl; }
virtual void uskill() { cout << "uskill" << endl; }
};
class XS : public Hero
{
public:
void skill1() { cout << "XS skill1" << endl; }
void skill2() { cout << "XS skill2" << endl; }
void uskill() { cout << "XS uskill" << endl; }
};
class HX : public Hero
{
public:
void skill1() { cout << "HX skill1" << endl; }
void skill2() { cout << "HX skill2" << endl; }
void uskill() { cout << "HX uskill" << endl; }
};
int main()
{
int id = 0;
cout << "Please input hero: " << endl;
cin >> id;
// 创建基类指针,让它指向派生类对象,用基类指针调用派生类的成员函数
Hero* ptr = nullptr;
if (id == 1)
{
ptr = new XS;
}
else if (id == 2)
{
ptr = new HX;
}
if (ptr != nullptr)
{
ptr->skill1();
ptr->skill2();
ptr->uskill();
delete ptr;
}
}
2.3 多态的对象类型
类的普通成员函数地址是静态的,在编译阶段指定
(1)如果基类中有虚函数,对象的内存模型中有一个虚函数表,表中存放了基类的函数名和地址
(2)如果派生类中重定义了基类的虚函数,创建派生对象时,将用派生类的函数取代虚函数表中基类的函数
C++中的多态分为两种:静态多态与动态多态
静态多态:编译时的多态,在编译时期就已经确定要执行了的函数地址了;主要有函数重载和函数模板
动态多态:动态绑定,在运行时才去确定对象类型和正确选择需要调用的函数,一般用于解决基类指针或引用派生类对象调用类中重写的方法(函数)时出现的问题
调用普通成员函数的效率比调用虚函数的效率更高,所以如果不考虑多态,不要把普通成员函数设置为虚函数
2.4 如何析构派生类
(1)构造函数不能继承,创建派生类对象,先执行基类构造函数,再执行派生类构造函数
(2)析构函数不能继承,销毁派生类对象时,先执行派生类析构函数,再执行基类析构函数
(3)派生类析构函数执行完,会自动调用基类的析构函数
(4)如果手工的调用派生类的析构函数,也会自动调用基类函数
析构派生类
:
1.析构派生类对象时,会自动调用基类的析构函数。与构造函数不同的是,在派生类的析构函数中不用显式地调用基类的析构函数,因为每个类只有一个析构函数,编译器知道如何选择,无需程序员干涉
2.析构函数可以手工调用,如果对象中有堆内存
delete ptr;
ptr = nullptr;
3.用基类指针指向派生类对象时,delete基类指针调用的时基类的析构函数,不是派生类的,如果希望调用派生类的析构函数,就要把基类的析构函数设置为虚函数
4.C++编译器对虚析构函数做了特别的处理
5.对于基类,即使它不需要析构函数,也应该提供一个空虚析构函数,不然析构派生类对象不执行
6.赋值运算符函数不能继承,派生类继承的函数的特征标与基类完全相同,但赋值运算符函数的特征标随类而异,它包含了一个类型为其所属类的形参
7.友元函数不是类成员,不能继承
#include <iostream>
using namespace std;
class AA
{
public:
AA() { cout << "Base Gouzao\n"; }
virtual void func() { cout << "Base func()\n"; }
virtual ~AA() { cout << "Base Xigou\n"; }
};
class BB : public AA
{
public:
BB() { cout << "Pai Gouzao\n"; }
void func() { cout << "Pai func()\n"; }
~BB() { cout << "Pai Xigou\n"; }
};
int main()
{
AA *a = new BB;
a->func();
delete a;
}
2.5 纯虚函数和抽象类
纯虚函数是一种特殊的虚函数,在某些情况,基类中不能对虚函数给出有意义的实现,把它声名为纯虚函数
语法:virtual 返回值类型 函数名(参数列表) = 0
纯虚函数在基类中为派生类保留一个函数的名字,以便派生类进行重定义,如果在基类中没有保留函数名字,则无法支持多态性
含有纯虚函数的类被称为抽象类,
不能实例化对象
,可以创建指针和引用
派生类必须重定义抽象类中的纯虚函数,否则也属于抽象类(不能实例化对象)
基类的纯虚析构函数也需要实现,为啥声名析构函数为纯虚析构函数?
有时候,想使一个类成为抽象类,刚好没有任何纯虚析构函数,在想要成为抽象类中声名一个纯虚析构函数
#include <iostream>
using namespace std;
class AA
{
public:
AA() { cout << "Base Gouzao\n"; }
virtual void func() = 0;// { cout << "Base func()\n"; }
virtual ~AA() { cout << "Base Xigou\n"; }
};
class BB : public AA
{
public:
BB() { cout << "Pai Gouzao\n"; }
void func() { cout << "Pai func()\n"; }
~BB() { cout << "Pai Xigou\n"; }
};
int main()
{
AA *a = new BB;
a->func();
delete a;
}