类和对象
- 4.友元
- 4.1全局函数做友元
- 4.2类做友元
- 4.3成员函数做友元
- 5.运算符重载
- 5.1 加号运算符重载
- 5.1.1成员函数实现运算符重载
- 5.1.2全局函数实现运算符重载
- 5.2 左移运算符重载
- 5.2.1全局函数实现运算符重载
- 5.2.2成员函数实现运算符重载
- 5.3 递增/递减运算符重载
- 5.3.1 前置++
- 5.3.1.1成员函数实现运算符重载
- 5.3.1.2全局函数实现运算符重载
- 5.3.2 后置++
- 5.3.2.1成员函数实现运算符重载
- 5.3.2.2全局函数实现运算符重载
- 5.3.3 前置--
- 5.3.3.1成员函数实现运算符重载
- 5.3.3.2全局函数实现运算符重载
- 5.3.4 后置--
- 5.3.4.1成员函数实现运算符重载
- 5.3.4.2全局函数实现运算符重载
- 5.4 赋值运算符重载
- 5.5 关系运算符重载
- 5.5.1 关系运算符(==)重载
- 5.5.2 关系运算符(>)重载
- 5.6 函数调用运算符重载
- 6.继承
- 6.1 继承的基本语法
- 6.2继承的方式
- 6.3继承中的对象模型
- 6.4继承中的构造和析构顺序
- 6.5继承同名成员处理方式
- 6.5.1继承非静态同名成员处理方式
- 6.5.1.1非静态同名成员变量处理方式
- 6.5.1.2非静态同名成员函数处理方式
- 6.5.2继承同名静态成员处理方式
- 6.5.2.1同名静态成员变量处理方式
- 6.5.2.2同名静态成员函数处理方式
- 6.6多继承语法
- 6.7菱形继承
- 7.多态
- 7.1 多态的基本应用
- 7.1.1 多态的基本概念
- 7.1.2 多态的基本原理
- 7.2 纯虚函数和抽象类
- 7.3 虚析构和纯虚析构
4.友元
在程序里,有些私有属性也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术;
友元的目的就是让一个函数或者类访问另一个类中私有成员;
友元的关键字为 friend
友元的三种实现
- 全局函数做友元
- 类做友元
- 成员函数做友元
4.1全局函数做友元
class Building
{//告诉编译器 goodGay全局函数 是 Building类的好朋友,可以访问类中的私有内容friend void goodGay(Building* building);public:Building(){m_SittingRoom = "客厅";m_BedRoom = "卧室";}public://公共权限string m_SittingRoom; //客厅private://私有权限string m_BedRoom; //卧室
};//全局函数
void goodGay(Building* building)
{cout << "好朋友正在访问: " << building->m_SittingRoom << endl;cout << "好朋友正在访问: " << building->m_BedRoom << endl;
}void test01()
{Building b;goodGay(&b);
}
friend void goodGay(Building* building);
- 告诉编译器 goodGay全局函数 是 Building类的好朋友,可以访问类中的私有内容
可见,调用test01函数,当全局函数做友元时,该全局函数可以访问私有权限内容。
4.2类做友元
class Building;//先声明,防止goodGay类出错
class goodGay
{
public://类内声明,类外实现goodGay();void visitor();~goodGay();private:Building* building;
};class Building
{//告诉编译器 goodGay类是Building类的好朋友,可以访问到Building类中私有内容friend class goodGay;public:Building();public:string m_SittingRoom; //客厅
private:string m_BedRoom;//卧室
};//类外实现成员函数(注意加上所在类空间)
Building::Building()//Building类构造函数
{this->m_SittingRoom = "客厅";this->m_BedRoom = "卧室";
}
goodGay::goodGay()//goodGay类构造函数
{//堆区开辟空间,注意要释放(析构函数释放,delete)building = new Building;
}
goodGay::~goodGay()
{if(building!=NULL){delete building;building=NULL;}
}
void goodGay::visitor()
{cout << "好朋友正在访问" << building->m_SittingRoom << endl;cout << "好朋友正在访问" << building->m_BedRoom << endl;
}void test01()
{goodGay gg;gg.visitor();}
== friend class goodGay;==
- 告诉编译器 goodGay类是Building类的好朋友,可以访问到Building类中私有内容
可见,调用test01函数后,当类做友元时,该类内可以访问另一个类内的私有权限内容。
4.3成员函数做友元
class Building;//先声明,防止goodGay类出错
class goodGay
{
public:goodGay();void visitor1(); //只让visitor1函数作为Building的好朋友,可以发访问Building中私有内容void visitor2();private:Building* building;
};class Building
{//告诉编译器 goodGay类中的visitor1成员函数 是Building好朋友,可以访问私有内容friend void goodGay::visitor1();public:Building();public:string m_SittingRoom; //客厅
private:string m_BedRoom;//卧室
};Building::Building()
{this->m_SittingRoom = "客厅";this->m_BedRoom = "卧室";
}
goodGay::goodGay()
{building = new Building;
}void goodGay::visitor1()
{cout << "好朋友1正在访问" << building->m_SittingRoom << endl;cout << "好朋友1正在访问" << building->m_BedRoom << endl;
}
void goodGay::visitor2()
{cout << "好朋友2正在访问" << building->m_SittingRoom << endl;//cout << "好朋友正在访问" << building->m_BedRoom << endl;//无法访问
}void test01()
{goodGay gg;gg.visitor1();gg.visitor2();}
friend void goodGay::visitor1();
- 告诉编译器 goodGay类中的visitor1成员函数 是Building好朋友,可以访问私有内容
可见,调用test01函数后,当一个类内的成员函数做另一个类的友元时,该类内的成员函数可以访问另一个类内的私有权限内容。
5.运算符重载
运算符重载概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
5.1 加号运算符重载
作用:实现两个自定义数据类型相加的运算。
对于内置数据类型,编译器知道如何进行运算:
例如两个整型数据的相加,可以直接应用,但对于一些非内置数据类型,比如两个对象的相加,编译器内部没有相关运算方式,故需要我们自己对运算符进行补充。
如果不进行运算符重载,就会出现下面类似的错误:
5.1.1成员函数实现运算符重载
实例:实现两个对象的相加
class Person {
public:Person() {};//为了提供对无参构造函数的调用//若无,自己定义了有参构造函数,系统默认无无参构造函数//则Person temp;无法成立Person(int a, int b)//有参构造{this->m_A = a;this->m_B = b;}//成员函数实现 + 号运算符重载//函数名operator+系统默认,可以实现简化调用//自己定义函数名也可,但无法实现下面的简化调用Person operator+(const Person& p) {Person temp;//无参构造temp.m_A = this->m_A + p.m_A;temp.m_B = this->m_B + p.m_B;return temp;//值返回(重新创建新对象)}public:int m_A;int m_B;
};//运算符重载 可以发生函数重载
//同一函数名表示不同运算
Person operator+(const Person& p2, int val)
{Person temp;temp.m_A = p2.m_A + val;temp.m_B = p2.m_B + val;return temp;
}//测试函数
void test() {Person p1(10, 10);Person p2(20, 20);//成员函数方式//本质调用为://Person p3=p2.operaor+(p1)Person p3 = p2 + p1; cout << "mA:" << p3.m_A << " mB:" << p3.m_B << endl;//本质调用为://Person p4=operaor+(p3,10)Person p4 = p3 + 10; cout << "mA:" << p4.m_A << " mB:" << p4.m_B << endl;}
5.1.2全局函数实现运算符重载
示例:
class Person {
public:Person(int a, int b)//有参构造{this->m_A = a;this->m_B = b;}public:int m_A;int m_B;
};//全局函数实现 + 号运算符重载
//对象+对象
Person operator+(const Person & p1, const Person & p2) {Person temp(0, 0);temp.m_A = p1.m_A + p2.m_A;temp.m_B = p1.m_B + p2.m_B;return temp;
}
//运算符重载 可以发生函数重载
//对象+int
Person operator+(const Person& p2, int val)
{Person temp(0,0);temp.m_A = p2.m_A + val;temp.m_B = p2.m_B + val;return temp;
}void test() {Person p1(10, 10);Person p2(20, 20);//成员函数方式//本质实现:Person p3 = operator+ (p1, p2);//Person p3 = p2 + p1; cout << "mA:" << p3.m_A << " mB:" << p3.m_B << endl;//本质实现:Person p4 = operator+ (p3, 10);Person p4 = p3 + 10; cout << "mA:" << p4.m_A << " mB:" << p4.m_B << endl;
}
总结1:对于内置的数据类型的表达式的的运算符是不可能改变的;
总结2:不要滥用运算符重载。(即不可命名为加号运算符重载,实现用减法)
5.2 左移运算符重载
作用:可以输出自定义数据类型。
5.2.1全局函数实现运算符重载
示例:
class Person {//友元:实现对私有权限成员的访问friend ostream& operator<<(ostream& out, Person& p);public:Person(int a, int b){this->m_A = a;this->m_B = b;}private:int m_A;int m_B;
};//全局函数实现左移重载
//ostream对象只能有一个
ostream& operator<<(ostream& out, Person& p) {out << "a:" << p.m_A << " b:" << p.m_B;return out;
}void test() {Person p1(10, 20);cout << p1 << "hello world" << endl; //链式编程
}
5.2.2成员函数实现运算符重载
示例:
- 1.对象本身做形参
class Person {public:Person(int a, int b){this->m_A = a;this->m_B = b;}//成员函数 实现左移运算符重载,可以实现,但不是我们想要的效果ostream& operator<<(Person& p){cout << "a:" << p.m_A << " b:" << p.m_B;return cout;//链式编程}
private:int m_A;int m_B;
};void test() {Person p1(10, 20);p1.operator<<(p1)<<endl;//简化p1 << p1 << " hello world" <<endl;//与内置函数实现不一致
}
- 2.标准输出流做形参
示例:
class Person {public:Person(int a, int b){this->m_A = a;this->m_B = b;}//成员函数 实现左移运算符重载,可以实现,但不是我们想要的效果ostream& operator<<(ostream &out) {out << "a:" << this->m_A << " b:" << this->m_B;;return cout;//链式编程}private:int m_A;int m_B;
};void test() {Person p1(10, 20);//本质实现://p1.operator<<(cout);p1 << cout << " hello world" << endl;//可见与内置函数输出不一致
}
利用成员函数重载左移运算符,无法实现与内置输出一致的顺序(即cout<<p,cout在左侧),故不会利用成员函数重载<<运算符。
总结:重载左移运算符配合友元可以实现输出自定义数据类型
5.3 递增/递减运算符重载
作用: 通过重载递增运算符,实现自己的整型数据;
5.3.1 前置++
5.3.1.1成员函数实现运算符重载
示例:
class MyInteger {friend ostream& operator<<(ostream& out, MyInteger myint);public:MyInteger() {m_Num = 0;}//前置++//局部函数实现//返回引用MyInteger& operator++() {//先++m_Num++;//再返回return *this;//返回对象本身(引用),实现对一直对一个对象进行递增操作}private:int m_Num;
};//左移运算符重载:全局函数
ostream& operator<<(ostream& out, MyInteger myint) {out << myint.m_Num;return out;
}//前置++ 先++ 再返回
void test01() {MyInteger myInt;cout << ++(++myInt) << endl;cout << myInt << endl;
//本质实现:
//operator<<(cout, myInt.operator++())<<endl;//相当于cout << ++myInt << endl;
//cout << myInt.operator++().operator++() << endl;//相当于cout << ++(++myInt) << endl;
}
分析:++myInt:m_Num=1(返回对象本身);
++(++myInt)(对同一个对象++) :m_Num=2(返回对象本身);
- 如果将返回值改为值返回:即
//值返回
MyInteger operator++() {//先++m_Num++;//再返回return *this;//拷贝一个新对象
}
分析:++myInt(第一次:对象本身++):m_Num=1(创建新对象);
++(++myInt)(对新对象++):m_Num=2(返回新对象);
- 即cout << ++(++myInt) << endl中的++(++myInt)不再是原对象,而是创建的第二个新对象(其内容和返回对象本身结束的时候一样);
cout << myInt << endl;输出对象本身。
可见,返回类型不同,最后结果不同。
- 返回对象本身(引用):实现对一直对一个对象进行递增操作,每一次++都对同一个进行运算。
- 值返回方式:第一次调用++是对对象本身进行,但之后回利用拷贝构造函数创建一个新的对象,就形成了每次调用都会形成一个新对象,无法实现如同第一种(返回引用)的对同一个对象持续累加。
5.3.1.2全局函数实现运算符重载
MyInteger& operator++(MyInteger &myInt) {//先++myInt.m_Num++;//再返回return myInt;//返回对象本身(引用)
}
注:全局函数下,要实现链式访问只能返回对象本身(返回引用);
5.3.2 后置++
对于后置++,无法实现链式编程。故对于后置++的重载也无法实现链式编程。
报错原因:表达式必须是可修改的左值。
5.3.2.1成员函数实现运算符重载
示例:
class MyInteger {friend ostream& operator<<(ostream& out, MyInteger myint);
public:MyInteger() {m_Num = 0;}//后置++MyInteger operator++(int) {//先返回MyInteger temp = *this; //记录当前本身的值,然后让本身的值加1,但是返回的是以前的值,达到先返回后++;m_Num++;return temp;}private:int m_Num;
};
//左移运算符重载:全局函数
ostream& operator<<(ostream& out, MyInteger myint) {out << myint.m_Num;return out;
}//后置++ 先返回 再++
void test02() {MyInteger myInt;cout << myInt++ << endl;cout << myInt << endl;//本质实现://operator<<(cout, myInt.operator++(0))<<endl;//operator<<(cout, myInt)<<endl;
}
MyInteger operator++(int):int-占位参数
5.3.2.2全局函数实现运算符重载
示例:
//int为占位参数,为了和前置++区分;调用时需补占位参数(任意数都可)
MyInteger operator++(MyInteger& myInt,int) {//先返回MyInteger temp = myInt; //记录当前本身的值,然后让本身的值加1,但是返回的是以前的值,达到先返回后++;myInt.m_Num++;return temp;
}//后置++ 先返回 再++
void test02() {MyInteger myInt;cout << myInt++ << endl;cout << myInt << endl;// //本质实现://operator<<(cout, operator++(myInt,0)) << endl;//operator<<(cout, myInt)<<endl;}
5.3.3 前置–
5.3.3.1成员函数实现运算符重载
参考前置++:(前置–可以实现链式编程,只列写返回对象本身)
MyInteger& operator--() {//先--m_Num--;//再返回return *this;//返回对象本身(引用),实现对一直对一个对象进行递增操作
}
5.3.3.2全局函数实现运算符重载
MyInteger& operator--(MyInteger& myInt) {//先++myInt.m_Num--;//再返回return myInt;//返回对象本身(引用)
}
5.3.4 后置–
参见后置++:
5.3.4.1成员函数实现运算符重载
//后置--
MyInteger& operator--(int) {//先返回MyInteger temp = *this; //记录当前本身的值,然后让本身的值减1,但是返回的是以前的值,达到先返回后--;m_Num--;return temp;
}
5.3.4.2全局函数实现运算符重载
//int为占位参数,为了和前置--区分;调用时需补占位参数(任意数都可)
MyInteger operator--(MyInteger& myInt,int) {//先返回MyInteger temp = myInt; //记录当前本身的值,然后让本身的值减1,但是返回的是以前的值,达到先返回后--;myInt.m_Num--;return temp;
}
递增/递减运算符重载总结:
前置递增返回引用,后置递增返回值
.
5.4 赋值运算符重载
c++编译器至少给一个类添加4个函数:
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
- 赋值运算符 operator=, 对属性进行值拷贝
前三个之前介绍过,此处着重介绍第四个:
如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题 。
深浅拷贝
示例:
class Person
{
public:Person(int age){//将年龄数据开辟到堆区m_Age = new int(age);}//重载赋值运算符 Person& operator=(Person& p){//判断是否有属性在堆区,若有先释放干净,再进行深拷贝if (m_Age != NULL){delete m_Age;m_Age = NULL;}//编译器提供的代码是浅拷贝//m_Age = p.m_Age;//提供深拷贝 解决浅拷贝的问题m_Age = new int(*p.m_Age);//返回自身return *this;//链式编程}~Person(){//释放堆区空间if (m_Age != NULL){delete m_Age;m_Age = NULL;}}//年龄的指针int* m_Age;};
void test01()
{Person p1(18);Person p2(20);Person p3(30);p3 = p2 = p1; //赋值操作:链式编程cout << "p1的年龄为:" << *p1.m_Age << endl;cout << "p2的年龄为:" << *p2.m_Age << endl;cout << "p3的年龄为:" << *p3.m_Age << endl;
}
附:
参照内置类型赋值的链式程序。
int a = 10;
int b = 20;
int c = 30;c = b = a;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
5.5 关系运算符重载
作用:重载关系运算符,可以让两个自定义类型对象进行对比操作。
5.5.1 关系运算符(==)重载
示例:
class Person
{
public:Person(string name, int age){this->m_Name = name;this->m_Age = age;};bool operator==(Person& p){if (this->m_Name == p.m_Name && this->m_Age == p.m_Age){return true;}else{return false;}}string m_Name;int m_Age;
};void test01()
{Person a("Tom", 18);Person b("Tom", 25);if (a == b){cout << "a和b相等" << endl;}else{cout << "a和b不相等" << endl;}}
5.5.2 关系运算符(>)重载
示例:
class Person
{
public:Person(string name, int age){this->m_Name = name;this->m_Age = age;};int operator>(Person& p){//按字母进行比较return this->m_Name.compare(p.m_Name);//compare按照每一个字母的ASCII值进行比较,根据结果返回0,大于0,小于0}string m_Name;int m_Age;
};void test01()
{Person a("Tom", 18);Person b("Marry", 25);if ((a > b) == 0){cout << "a==b" << endl;}else if ((a > b) > 0){cout << "a>b" << endl;}elsecout << "a<b" << endl;
}
5.6 函数调用运算符重载
- 函数调用运算符 () 也可以重载;
- 由于重载后使用的方式非常像函数的调用,因此称为仿函数;
- 仿函数没有固定写法,非常灵活。
示例:
class MyAdd
{
public:int operator()(int v1, int v2){return v1 + v2;}
};void test01()
{MyAdd add;int ret = add(10, 10);//相当于:int ret = add.operator()(10, 10)cout << "ret = " << ret << endl;//匿名对象调用 cout << "MyAdd()(100,100) = " << MyAdd()(100, 100) << endl;
}
6.继承
继承是面向对象三大特性之一
有些类与类之间存在特殊的关系,例如下图中:
猫和狗都具备动物的属性,同时其又有很多品种(自己的属性)。
我们发现,定义这些类时,下级别的成员除了拥有上一级的共性,还有自己的特性。
这个时候我们就可以考虑利用继承的技术,减少重复代码。
6.1 继承的基本语法
继承的语法:class 子类 : 继承方式 父类
借助下面的事例,介绍继承的优势和语法:
例如:
我们看到很多网站(以某网站编程培训为例)中,都有公共的头部,公共的底部,甚至公共的左侧列表,只有中心内容不同;接下来我们分别利用普通写法和继承的写法来实现网页中的内容,看一下继承存在的意义以及好处:
普通实现:
//Java页面
class Java
{
public:void header(){cout << "首页、公开课、登录、注册...(公共头部)" << endl;}void footer(){cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;}void content(){cout << "JAVA学科视频" << endl;}
};
//Python页面
class Python
{
public:void header(){cout << "首页、公开课、登录、注册...(公共头部)" << endl;}void footer(){cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;}void content(){cout << "Python学科视频" << endl;}
};
//C++页面
class CPP
{
public:void header(){cout << "首页、公开课、登录、注册...(公共头部)" << endl;}void footer(){cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;}void content(){cout << "C++学科视频" << endl;}
};//测试函数
void test01()
{//Java页面cout << "Java下载视频页面如下: " << endl;Java ja;ja.header();ja.footer();ja.content();cout << "--------------------" << endl;//Python页面cout << "Python下载视频页面如下: " << endl;Python py;py.header();py.footer();py.content();cout << "--------------------" << endl;//C++页面cout << "C++下载视频页面如下: " << endl;CPP cp;cp.header();cp.footer();cp.content();}
可见,对于上述代码,有一部分代码多次重复引用,虽然结构清晰,但会造成代码冗余,内存浪费。
借助继承的特性可以实现简化:
继承实现:
//公共页面
class BasePage
{
public:void header(){cout << "首页、公开课、登录、注册...(公共头部)" << endl;}void footer(){cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;}};//Java页面
class Java : public BasePage
{
public:void content(){cout << "JAVA学科视频" << endl;}
};
//Python页面
class Python : public BasePage
{
public:void content(){cout << "Python学科视频" << endl;}
};
//C++页面
class CPP : public BasePage
{
public:void content(){cout << "C++学科视频" << endl;}
};
将普通实现的每个类内容替换,测试函数不变,会实现如上的结果。
总结:
继承的好处:可以减少重复的代码
class A : public B
A 类称为子类 或 派生类
B 类称为父类 或 基类
派生类中的成员,包含两大部分:
一类是从基类继承过来的,一类是自己增加的成员。
从基类继承过过来的表现其共性,而新增的成员体现了其个性。
6.2继承的方式
继承的语法:class 子类 : 继承方式 父类
继承方式一共有三种:
- 公共继承
- 保护继承
- 私有继承
不同继承方式下,对于父类的不同权限的内容的访问条件,可以用下面的图进行说明。
可联系,封装权限中的保护权限和私有权限的区别: - protected 保护权限 :类内可以访问 类外不可以访问(例如:儿子可以访问到父亲中的保护内容)
- private 私有权限 :类内可以访问 类外不可以访问(例如:儿子不可以访问到父亲中的私有内容)
6.3继承中的对象模型
问题:
从父类继承过来的成员,哪些属于子类对象中?
或者说子类的大小对父类中的继承权限有无关系?
示例:
class Base
{
public:int m_A;
protected:int m_B;
private:int m_C; //私有成员只是被隐藏了,但是还是会继承下去
};//公共继承
class Son :public Base
//对于父类中的公共权限和保护权限,可访问,不可访问私有权限
{
public:int m_D;
};void test01()
{cout << "sizeof Son = " << sizeof(Son) << endl;
}
可见,虽然子类无法访问父类中的私有权限内容,但子类大小是包含父类中的私有权限的。
下面借助VS自带的开发人员命令提示符窗口,对上述内容进行一个深入介绍:
由上图可知,对于Son类中,包含父类的全部内容(三种权限内容都被继承下来)和自己的特有内容,虽然对于公共继承而言,父类中私有权限无法访问,但其也被子类继承,只是被编译器隐藏。
6.4继承中的构造和析构顺序
子类继承父类后,当创建子类对象,也会调用父类的构造函数;
问题:父类和子类的构造和析构顺序是谁先谁后?
- 继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反。
示例:
class Base
{
public:Base(){cout << "Base构造函数!" << endl;}~Base(){cout << "Base析构函数!" << endl;}
};class Son : public Base
{
public:Son(){cout << "Son构造函数!" << endl;}~Son(){cout << "Son析构函数!" << endl;}};void test01()
{Son s;
}
总结:继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反。
(可对比类对象作为类成员的构造和析构函数调用顺序)2.7类对象作为类成员
6.5继承同名成员处理方式
6.5.1继承非静态同名成员处理方式
问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?
- 访问子类同名成员 直接访问即可;
- 访问父类同名成员 需要加作用域。
6.5.1.1非静态同名成员变量处理方式
示例:
//父类
class Base {
public:Base(){m_A = 100;}public:int m_A;
};//子类
class Son : public Base {
public:Son(){m_A = 200;}
public:int m_A;
};void test01()
{//子类和父类都有m_A成员变量Son s;cout << "Son类下m_A:" << s.m_A << endl;cout << "Base类下m_A:" << s.m_A << endl;cout << "Base类下m_A:" << s.Base::m_A << endl;
}
可见,子类和父类中有同名成员变量时,如要访问父类中成员变量需要加上父类所在作用域。
6.5.1.2非静态同名成员函数处理方式
当子类与父类拥有同名的成员函数,子类会隐藏父类中所有版本的同名成员函数;
如果想访问父类中被隐藏的同名成员函数,需要加父类的作用域.
示例:
//父类
class Base {
public:void func(){cout << "Base - func()调用" << endl;}void func(int a){cout << "Base - func(int a)调用" << endl;}public:int m_A;
};//子类
class Son : public Base {
public:void func(){cout << "Son - func()调用" << endl;}
public:int m_B;
};void test02()
{Son s;s.func();//s.func(10);//报错s.Base::func();s.Base::func(10);
}
总结:
- 子类对象可以直接访问到子类中同名成员
- 子类对象加作用域可以访问到父类同名成员
- 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数
6.5.2继承同名静态成员处理方式
问题:继承中同名的静态成员在子类对象上如何进行访问?
静态成员和非静态成员出现同名,处理方式一致
- 访问子类同名成员 直接访问即可;
- 访问父类同名成员 需要加作用域。
6.5.2.1同名静态成员变量处理方式
示例:
class Base {
public:static int m_A;
};//类内声明,类外初始化
int Base::m_A = 100;class Son : public Base {
public:static int m_A;
};
//类内声明,类外初始化
int Son::m_A = 200;//同名成员属性
void test01()
{//通过对象访问cout << "通过对象访问: " << endl;Son s;cout << "Son 下 m_A = " << s.m_A << endl;cout << "Base 下 m_A = " << s.Base::m_A << endl;//通过类名访问cout << "通过类名访问: " << endl;cout << "Son 下 m_A = " << Son::m_A << endl;cout << "Base 下 m_A = " << Son::Base::m_A << endl;
}
对于静态同名成员变量,子类和父类中成员变量的访问有两种方式:
- 通过对象访问:子类对象直接访问子类同名成员,父类成员访问需要子类对象加上父类作用域;
- 通过类名访问:子类对象可以直接在子类类名直接访问,父类成员需要在子类对象类名的基础上加上父类的作用域。
- Son::Base::m_A 中第一个::代表通过类名方式访问;第二个::代表访问父类作用域下。
6.5.2.2同名静态成员函数处理方式
同同名静态成员变量访问一样。
示例:
class Base {
public:static void func(){cout << "Base - static void func()" << endl;}static void func(int a){cout << "Base - static void func(int a)" << endl;}static int m_A;
};class Son : public Base {
public:static void func(){cout << "Son - static void func()" << endl;}static int m_A;
};//同名成员函数
void test02()
{//通过对象访问cout << "通过对象访问: " << endl;Son s;s.func();s.Base::func();s.Base::func(10);//通过类名访问cout << "通过类名访问: " << endl;Son::func();Son::Base::func();//出现同名,子类会隐藏掉父类中所有同名成员函数,需要加作作用域访问Son::Base::func(100);
}
总结:同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象 和 通过类名)
大总结:
- 访问子类同名成员 直接访问即可
- 访问父类同名成员 需要加作用域
6.6多继承语法
C++允许一个类继承多个类
语法:class 子类 :继承方式 父类1 , 继承方式 父类2...
多继承可能会引发父类中有同名成员出现,需要加作用域区分。
注:C++实际开发中不建议用多继承
示例:
//父类1
class Base1 {
public:Base1(){m_A = 100;}
public:int m_A;
};//父类2
class Base2 {
public:Base2(){m_A = 200; }
public://同名成员变量int m_A;
};//语法:class 子类:继承方式 父类1 ,继承方式 父类2
class Son : public Base2, public Base1
{
public:Son(){m_C = 300;m_D = 400;}
public:int m_C;int m_D;
};//多继承容易产生成员同名的情况
//通过使用类名作用域可以区分调用哪一个基类的成员
void test01()
{Son s;cout << "sizeof Son = " << sizeof(s) << endl;cout << s.Base1::m_A << endl;cout << s.Base2::m_A << endl;
}
下面借助VS自带的开发人员命令提示符窗口,对上述内容进行一个深入介绍:
总结: 多继承中如果父类中出现了同名情况,子类使用时候要加作用域
6.7菱形继承
菱形继承概念:
两个派生类继承同一个基类;
又有某个类同时继承者两个派生类;
这种继承被称为菱形继承,或者钻石继承。
典型的菱形继承案例:
在上图中,羊和驼都继承了动物中的属性,同时羊驼又分别继承两者的属性,就会造成羊驼中重复包含动物属性,造成浪费。即:
菱形继承问题:
- 羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性(不知道继承谁的)。
- 草泥马继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
普通示例:
class Animal
{
public:int m_Age;
};class Sheep : public Animal {};
class camel : public Animal {};//camel-骆驼
class alpaca : public Sheep, public camel {};//alpaca-羊驼void test01()
{alpaca st;st.Sheep::m_Age = 100;st.camel::m_Age = 200;cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl;cout << "st.camel::m_Age = " << st.camel::m_Age << endl;
}
对于,羊和驼中都含有动物中年龄成员变量,羊驼中就会含有两份年龄成员变量,就会造成以哪个为准呢?
虽然我们可通过添加作用域进行区分,但仍会造成内存浪费,毕竟羊驼只需要一份就可。这个问题可以通过下面的方式进行解决:
优化示例:
虚继承:
- 继承前加
virtual
关键字后,变为虚继承- 此时公共的父类Animal称为
虚基类
class Animal
{
public:int m_Age;
};class Sheep : virtual public Animal {};
class camel : virtual public Animal {};//camel-骆驼
class alpaca : public Sheep, public camel {};//alpaca-羊驼void test01()
{alpaca st;st.Sheep::m_Age = 100;st.camel::m_Age = 200;cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl;cout << "st.camel::m_Age = " << st.camel::m_Age << endl;cout << "st.m_Age = " << st.m_Age << endl;st.Sheep::m_Age = 300;cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl;cout << "st.camel::m_Age = " << st.camel::m_Age << endl;cout << "st.m_Age = " << st.m_Age << endl;
}
可以看出,采用虚继承方式,羊驼继承动物的属性就变成了一份,无论改变羊和驼中哪个属性,三者中的属性变量就都改变了。
下面借助VS自带的开发人员命令提示符窗口,对上述内容进行一个深入介绍:
- 普通示例:
- 优化示例:
对比两者,我们可以方向,参与虚继承方式下,动物类中属性(m_Age)只含有一份。
虚继承下,羊和驼类中包含一个指针(vbptr-virtual base pointer虚基类指针)指向vbtable(虚基类表-图中1和2的位置),羊和驼中的虚基类表包含各自的偏移量,用于找到动物类中的属性(m_Age),羊和驼就不再分别基础动物类中的属性了,只继承一个指针用于找到动物类的属性,减少空间浪费。
总结:
- 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义
- 利用虚继承可以解决菱形继承问题
7.多态
在编程语言和类型论中,多态(英语:polymorphism)指为不同数据类型的实体提供统一的接口。
多态类型(英语:polymorphic type)可以将自身所支持的操作套用到其它类型的值上。
7.1 多态的基本应用
7.1.1 多态的基本概念
多态是C++面向对象三大特性之一
多态分为两类:
- 静态多态: 函数重载和运算符重载属于静态多态,复用函数名;
- 动态多态: 派生类(子类)和虚函数实现运行时多态。
静态多态和动态多态区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址;
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址。
非多态示例:
class Animal
{
public:void speak(){cout << "动物在说话" << endl;}
};class Cat :public Animal
{
public:void speak(){cout << "小猫在说话" << endl;}
};class Dog :public Animal
{
public:void speak(){cout << "小狗在说话" << endl;}};//希望实现每次调用函数,给什么形参就调用谁的函数
//提供一个公共接口,否则就需要多个函数才可实现对每个动物叫的实现
void DoSpeak(Animal& animal)
{animal.speak();
}void test01()
{Cat cat;DoSpeak(cat);Dog dog;DoSpeak(dog);
}
但由输出来看与我们的想法不同,每次都调用动物类的说话函数(speak)。
原因:
由于doSpeak函数在编译阶段就确定了函数地址,即地址早绑定——静态多态
对于此种问题可以采用多态技术进行解决:
要实现调用谁,谁执行就需要使得doSpeak函数地址在运行阶段进行绑定,实现地址晚绑定——动态多态。
多态示例:
只需要在父类speak函数前添加关键字-virtual
,形成虚函数。
class Animal
{
public://Speak函数就是虚函数//函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。virtual void speak(){cout << "动物在说话" << endl;}
};
//我们希望传入什么对象,那么就调用什么对象的函数
//如果函数地址在编译阶段就能确定,那么静态联编
//如果函数地址在运行阶段才能确定,就是动态联编
多态满足条件
:
1、有继承关系
(继承是多态实现的基础);
2、子类重写
父类中的虚函数:
- 也就是与父类虚函数一样(关键字virtual可加可不加),
返回值类型 函数名 形参列表完全一致
。
多态使用:
父类指针或引用指向子类对象
,即:
void DoSpeak(Animal& animal)-父类引用;
void DoSpeak(Animal* animal)-父类指针。
7.1.2 多态的基本原理
class Animal
{
public://Speak函数就是虚函数//函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。virtual void speak(){cout << "动物在说话" << endl;}
};class Cat :public Animal
{
public:void speak(){cout << "小猫在说话" << endl;}
};class Dog :public Animal
{
public:void speak(){cout << "小狗在说话" << endl;}};//希望实现每次调用函数,给什么形参就调用谁的函数
//提供一个公共接口,否则就需要多个函数才可实现对每个动物叫的实现
void DoSpeak(Animal& animal)
{animal.speak();
}void test01()
{Cat cat;DoSpeak(cat);
}
对于多态来说,需要满足两个条件,继承和重写虚函数。
-
在子类未重写父类的虚函数时:
子类(猫类)由于继承父类,因此在子类未重写父类虚函数时,子类中将父类中的虚函数完全复制一份。
(图中,父类存储一个指针(即vfptr),其指向vftable-虚函数表,表中放着虚函数的地址)
下面借助VS自带的开发人员命令提示符窗口,对上述内容进行一个深入介绍:
-
子类重写父类虚函数
子类虚函数表内部就替换成子类的(虚)函数地址,子类就有了自己的(虚)函数,从而可以进行调用自己的函数。
下面借助VS自带的开发人员命令提示符窗口,对上述内容进行一个深入介绍:
-
父类指针或引用指向子类对象
void DoSpeak(Animal& animal)
{animal.speak();
}
//父类函数传入子类对象:DoSpeak(cat);
//Animal& animal=Cat;
//再进行调用父类函数:animal.speak();
//由于Animal& animal=Cat,指向子类对象(Cat),编译器就会在子类的虚函数表中去找内部的函数地址,从而调用子类函数。
7.2 纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容;
因此可以将虚函数改为纯虚函数。
纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;
当类中有了纯虚函数,这个类也称为抽象类
抽象类特点:
- 无法实例化对象;
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类。
示例:
class Base
{
public://纯虚函数//类中只要有一个纯虚函数就称为抽象类virtual void func() = 0;
};class Son :public Base
{
public://子类必须重写父类中的纯虚函数,否则也属于抽象类virtual void func(){cout << "func调用" << endl;};
};void test01()
{Base* base = NULL;//base = new Base; // 错误,抽象类无法实例化对象//父类指针指向子类对象base = new Son;base->func();//通过父类指针调用子类函数delete base;//记得销毁
}
7.3 虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
问题示例:
class Animal {
public:Animal(){cout << "Animal 构造函数调用!" << endl;}virtual void Speak() = 0;~Animal(){cout << "Animal析构函数调用!" << endl;}
};class Cat : public Animal {
public:Cat(string name){cout << "Cat构造函数调用!" << endl;m_Name = new string(name);}virtual void Speak(){cout << *m_Name << "小猫在说话!" << endl;}~Cat(){cout << "Cat析构函数调用!" << endl;if (this->m_Name != NULL) {delete m_Name;m_Name = NULL;}}public:string* m_Name;
};void test01()
{Animal* animal = new Cat("Tom");//父类指针指向子类对象animal->Speak();delete animal;//释放
}
由此可知,对于子类在堆区开辟的空间,对父类指针释放未能对子类空间释放,造成内存泄漏。(未能调用子类析构函数)
解决方式:
将父类中的析构函数改为虚析构或者纯虚析构
-
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象;
- 都需要有具体的函数实现;
-
虚析构和纯虚析构区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象。
虚析构语法:
virtual ~类名(){}
纯虚析构语法:(类内声明,类外实现)
类内声明: virtual ~类名() = 0;
类外实现:类名::~类名(){}
示例:
- 虚析构函数
class Animal {
public:Animal(){cout << "Animal 构造函数调用!" << endl;}//纯虚函数virtual void Speak() = 0;法一:析构函数加上virtual关键字,变成虚析构函数virtual~Animal(){cout << "Animal虚析构函数调用!" << endl;}
};
- 纯虚析构函数
class Animal {
public:Animal(){cout << "Animal 构造函数调用!" << endl;}//纯虚函数virtual void Speak() = 0;//法二:类内声明:纯虚析构函数//和包含普通纯虚函数的类一样,包含了纯虚析构函数的类也是一个抽象类。不能够被实例化。virtual ~Animal() = 0;
};//法二:类外实现:
Animal::~Animal()
{cout << "Animal 纯虚析构函数调用!" << endl;
}
注意:纯虚析构函数不要忘了类外实现,否则会出现下面的错误;
总结:
1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象;
2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构;
3. 拥有纯虚析构函数的类也属于抽象类。