2.类的作用域运算符

shadow

在我们之前讲的内容中,我们会发现一种情况,就是在我们在不同类中的打印函数我们都是尽量让其名字不同,那么为什么会有这种情况呢?首先我们来看一个函数

void func()
{
cout<<"B::void func()"<<endl;
func();
}

运行程序会发现这是一个死循环,因为其存在自己调用自己的情况,那么放在类中会是什么样子的呢

#include <iostream>

using namespace std;
class A
{
public:
void foo()
{
cout<<"A::void foo()"<<endl;
}
};
class B:public A
{
public:
void foo()
{
cout<<"B::void foo()"<<endl;
foo();//实际上这里是有一个this指针指向foo的
}
};
int main()
{
B b;
b.foo();
return ;
}

这样调用还是会出现死循环的情况,虽然其本意是在类B中的foo调用类A中的foo,但是由于this指针指向foo并且由于类中的两个函数重名,因此会出现死循环,为了解决这个问题,引入类的作用域运算符,将类B中的foo函数写成如下形式

void foo()
{
cout<<"B::void foo()"<<endl;
A::foo();
}

shadow产生机理

(1)  在父子类中出现重名的标识符(函数成员和数据成员),就会构成shadow,如果想访问被shadow的成员,加上父类的命名空间

(2)  shadow在父子类中的标识符只有一个,就是重名,不论返回值,参数不同什么

3. 继承的方式详解

继承的方式有三种:public,protected和private,但是我们一般都用public

所有的继承必须是public的,如果想私有继承的话,应该采用将基类实例作为成员的方式作为替代

一般情况下,在一个类中,public常用于接口,protected常用于数据,private常用于隐私

那么为什么public是用的最多的呢

如果多级派生中,均采用public,直到最后一级,派生类中均可访问基类的public,protected,很好的做到了接口的传承,保护数据以及隐私的保护

protected:封杀了对外的接口,保护数据成员,隐私保护

public:传承接口,间接地传承了数据(protected)

protected:传承数据,间接封杀了对外接口(public)

private:统杀了数据和接口

4. 类的作用域运算符

shadow产生机理

(1)  在父子类中出现重名的标识符(函数成员和数据成员),就会构成shadow,如果想访问被shadow的成员,加上父类的命名空间

(2)  shadow在父子类中的标识符只有一个,就是重名,不论返回值,参数不同什么

5. 多重继承

从继承类别来说,继承可以分为单继承和多继承

多继承的意义:

俗话讲,鱼和熊掌不可兼得,而在计算机中可以实现,生成一种新的对象,叫熊掌鱼,多继承自鱼和熊掌即可

继承语法:

派生类名:public 基类名1,public 基类名2,…,protected 基类名n

构造器格式

派生类名:派生类名(总参列表)

:基类名1(参数表1),基类名2(参数名2),…基类名n(参数名n),

内嵌子对象1(参数表1),内嵌子对象2(参数表2)…内嵌子对象n(参数表n)

{

派生类新增成员的初始化语句

}

多继承可能存在的问题

(1)  三角问题

多个父类中重名的成员,继承到子类中后,为了避免冲突,携带了各父类的作用域信息,子类中要访问继承下来的重名成员,则会产生二义性,为了避免冲突,访问时需要提供父类的作用域信息

构造器问题

下面我们用一个实际的例子来对其进行讲解

 #include <iostream>

 using namespace std;

 class X
{
public:
X(int d)
{
cout<<"X()"<<endl;
}
protected:
int _data;
}; class Y
{
public:
Y(int d)
{
cout<<"Y()"<<endl;
}
protected:
int _data;
}; class Z:public X,public Y
{
public:
Z()
:X(),Y()
{ }
void dis()
{
cout<<Y_data<<endl; }
}; int main()
{
Z z;
z.dis();
return ;
}

直接这样的话会报错,因为_data会产生二义性,为了解决这个问题,我们可以在数据之前加上其父类作用域

 void dis()
{
cout<<Y::_data<<endl;
cout<<X::_data<<endl;
}

下面我们看一个有趣的情况

#include <iostream>

using namespace std;

class X
{
public:
X(int d)
{
cout<<"X()"<<endl;
_data=d;
}
void setData(int d)
{
_data=d;
}
protected:
int _data;
}; class Y
{
public:
Y(int d)
{
cout<<"Y()"<<endl;
_data=d;
}
int getData()
{
return _data;
}
protected:
int _data;
}; class Z:public X,public Y
{
public:
Z(int i,int j)
:X(i),Y(j)
{ }
void dis()
{
cout<<X::_data<<endl;
cout<<Y::_data<<endl;
}
}; int main()
{
Z z(,);
z.dis();
cout<<"================="<<endl;
z.setData();
cout<<z.getData()<<endl;
cout<<"================="<<endl;
z.dis();
return ;
}

在这里我们getData得到的数据仍然是200,并不是setData的1000000,原因如下

刚开始的时候,在类X和类Y中,都有一个_data,

当其继承在类Z中后

由于是重名的问题,setData设置的是类X中的数据,但是getData得到的是类Y中的数据,所以说会出现问题

那么我们应该怎么来解决这个问题呢

需要解决的问题:

数据冗余

访问方便

由此引发了一个三角转四角的问题

  1. 提取各父类中相同的成员,包括数据成员和函数成员,构成祖父类
  2. 让各父类,继承祖父类
  3. 虚继承是一种继承的扩展,virtual

首先解决初始化问题,

祖父类的好处是,祖父类是默认的构造器,因此在父类中,并不需要显示地调用,按道理说,Z中有类X,Y,只需要管X,Y的初始化就可以了

#include <iostream>

using namespace std;

//祖父类
class A
{
protected:
int _data;
};
//父类继承祖父类
class X:virtual public A
{
public:
X(int d)
{
cout<<"X()"<<endl;
_data=d;
}
void setData(int d)
{
_data=d;
} };
//各父类继承祖父类
class Y:virtual public A
//虚继承
{
public:
Y(int d)
{
cout<<"Y()"<<endl;
_data=d;
}
int getData()
{
return _data;
}
}; class Z:public X,public Y
{
public:
Z(int i,int j)
:X(i),Y(j)
{ }
void dis()
{
cout<<_data<<endl;
}
}; int main()
{
Z z(,);
z.dis();
cout<<"================="<<endl;
z.setData();
cout<<z.getData()<<endl;
cout<<"================="<<endl;
z.dis();
return ;
}

这样就带来了两个好处,解决了数据冗余的问题,并且为访问带来了便利,虚继承也是一种设计的结果,被抽象上来的类叫做虚基类。也可以说成:被虚继承的类称为虚基类

虚基类:被抽象上来的类叫做虚基类

虚继承:是一种对继承的扩展

那么虚继承就有几个问题需要我们来注意了,首先是初始化的顺序问题,为了测试初始化的顺序问题,因为上述都是构造器的默认情况,但是实际情况中,可能都会带参数,甚至是虚继承的祖父类也会带参数,那么构造器顺序又将是如何的呢?我们利用如下代码进行测试

 #include <iostream>

 using namespace std;

 class A
{
public:
A(int i)
{
_data=i;
cout<<"A(int i)"<<endl;
}
protected:
int _data;
};
class B:virtual public A
{
public:
B(int i)
:A(i)
{
_data=i;
cout<<"B(int i)"<<endl;
}
}; class C:virtual public A
{
public:
C(int i)
:A(i)
{
_data=i;
cout<<"C(int i)"<<endl;
}
}; class D:public C,B
{
public:
D()
:C(),B(),A()
{
cout<<"D(int i)"<<endl;
}
void dis()
{
cout<<_data<<endl;
}
};
int main()
{
D d;
d.dis();
return ;
}

运行代码后我们可以得知,构造的顺序是从祖父类的构造器开始,按照顺序执行下来,最后到孙子类的构造器为止的

当然,上述只是一个测试,因为在实际过程中,祖父类是由父类抽象起来的,因此一般不会用祖父类生成对象

在实际过程中,在父类的构造器中我们常带默认参数,这样我们就可以不使得派生类的构造器如此复杂

实际例子,沙发床,除了上述之外,我们还需要增加颜色和重量,除此之外,我们还需要用descript函数来对其进行描述

#include <iostream>

using namespace std;

class Furniture
{
public:
void descript()
{
cout<<"_weight:"<<_weight<<endl;
cout<<"_color :"<<_color<<endl;
}
protected:
float _weight;
int _color;
};
class Sofa:virtual public Furniture
{
public:
Sofa(float w=,int c=)
{
_weight=w;
_color=c;
}
void sit()
{
cout<<"take a sit and have a rest"<<endl;
}
}; class Bed:virtual public Furniture
{
public:
Bed(float w=,int c=)
{
_weight=w;
_color=c;
}
void sleep()
{
cout<<"have a sleep ......."<<endl;
} }; class SofaBed:public Sofa,public Bed
{
public:
SofaBed(float w,int c)
{
_weight=w;
_color=c;
}
}; int main()
{
SofaBed sb(,);
sb.sit();
sb.sleep();
sb.descript();
return ;
} int main1()
{
Sofa sf;
sf.sit();
Bed bd;
bd.sleep();
return ;
}

6. 多态

(1)  生活中的多态

如果有几个相似而不完全相同的对象,有时人们要求在向他们发出同一个消息时,他们的反应各不相同,分别执行不同的操作,这种情况就是多态现象

(2)  C++ 中的多态

C++ 中的多态是指,由继承而产生的相关的不同的类,其对同一消息会做出不同的响应

比如,Mspaint中的单击不同图形,执行同一拖动动作而绘制不同的图形,就是典型的多态应用

多态性是面向对象程序设计的一个重要特征,能增加程序的灵活性,可以减轻系统的升级,维护,调试的工作量和复杂度

(3)  赋值兼容

赋值兼容是指,在需要基类对象的任何地方,都可以使用共有派生的对象来替代

只有在共有派生类中才有赋值兼容,赋值兼容是一种默认行为,不需要任何的显示的转化步骤

赋值兼容总结起来有以下三种特点

派生类的对象可以赋值给基类对象

派生类的对象可以初始化基类的引用

派生类对象的地址可以赋给指向基类的指针

下面我们将分别对其进行说明

  • 派生类的对象可以赋值给基类对象

观察下面代码

 #include <iostream>

 using namespace std;

 class Shape
{
public:
Shape(int x=,int y=)
:_x(x),_y(y){}
void draw()
{
cout<<"draw shape from"<<"("<<_x<<","<<_y<<")"<<endl;
}
protected:
int _x;
int _y;
};
class Circle:public Shape
{
public:
Circle(int x=,int y=,int r=)
:Shape(x,y),_radius(r){}
void draw()
{
cout<<"draw shape from"<<"("<<_x<<","<<_y<<")"<<"radius:"<<_radius<<endl;
}
protected:
int _radius;
};
int main()
{
Shape s(,);
s.draw();
Circle c(,,);
c.draw();
s=c; //派生类对象可以赋值给基类对象
s.draw();
return ;
}

有上述例子可以看出,派生类的对象是可以复制给基类对象的

  • 派生类的对象可以初始化基类的引用
 int main()
{
Shape s(,);
s.draw();
Circle c(,,);
Shape &rs=c;
rs.draw();
return ;
}
  • 派生类的对象的地址可以赋给指向基类的指针
 int main()
{
Shape s(,);
s.draw();
Circle c(,,);
Shape *ps=&c;
ps->draw();
return ;
}

在这三种情况中,使用的最多的是第三种,即派生类对象的地址可以赋给指向基类的指针

就如图示一样,假设左边的类是父类,右边的类是子类,,左边的指针是派生类的对象的地址赋给指向派生类的指针,那么其可访问的范围就是整个派生类,右边的指针是派生类的对象的地址赋给指向基类的指针,那么其访问范围就只有基类的那一部分

7. 多态

多态分为静多态和动多态

静多态,就是我们说的函数重载,表面上,是由重载规则来限定的,内部实现却是Namemangling,此种行为,发生在编译期,故称为静多态

(动)多态,不是在编译阶段决定,而是在运行阶段决定,故称动多态,动多态的形成条件如下

多态实现的条件

父类中有虚函数(加virtual,是一个声明型关键字,即只能在声明中有,在实现中没有),即公用接口

子类override(覆写)父类中的虚函数

通过已被子类对象赋值的父类指针,调用共有接口

下面分别对这些条件进行讲解

  • 父类中有虚函数(加virtual,是一个声明型关键字,即只能在声明中有,在实现中没有),即公用接口

virtual函数是一个声明型关键字,只能在声明中有,在实现中没有

class A
{
public:
A(){};
virtual void draw();
private:
int _x;
}
void A::draw()
{
cout<<_x<<endl;
}

假设在实现的过程中也加入virtual关键字,即

virtual void A::draw()
{
cout<<_x<<endl;
}

系统即会开始报错

  • 子类覆写父类中的虚函数,子类中同名同参同函数,才能构成覆写
  • 通过已被子类对象赋值的父类指针,调用虚函数,形成多态
 #include <iostream>
#include <typeinfo>
using namespace std; class Shape
{
public:
Shape(int x=,int y=)
:_x(x),_y(y)
{
cout<<"shape->this"<<this<<endl;
cout<<typeid(this).name()<<endl;
}
virtual void draw()
{
cout<<"draw shape from"<<"("<<_x<<","<<_y<<")"<<endl;
}
protected:
int _x;
int _y;
};
class Circle:public Shape
{
public:
Circle(int x=,int y=,int r=)
:Shape(x,y),_radius(r)
{
cout<<"shape->this"<<this<<endl;
cout<<typeid(this).name()<<endl;
}
void draw()
{
cout<<"draw shape from"<<"("<<_x<<","<<_y<<")"<<"radius:"<<_radius<<endl;
}
protected:
int _radius;
}; class Rect:public Shape
{
public:
Rect(int x=,int y=,int w=,int l=)
:Shape(x,y),_width(w),_lenth(l){}
virtual void draw()
{
cout<<"draw Circle from"<<"("<<_x<<","<<_y<<")"
<<"width:"<<_width<<"lenth:"<<_lenth<<endl;
}
protected: int _width;
int _lenth;
}; int main()
{
Circle c(,,);
Shape *ps=&c;//父类指针指向子类的对象
ps->draw(); Rect r(,,,);
ps=&r;
ps->draw();
return ;
}

可以看出,利用virtual,可以实现多态

通过父类的指针调用父类的接口指向其本来应该指向的内容

 int main()
{
Circle c(,,);
Shape *ps=&c;//父类指针指向子类的对象
ps->draw(); Rect r(,,,);
ps=&r;
ps->draw();
while()
{
int choice;
cin>>choice;
switch(choice)
{
case :
ps=&c;
break;
case :
ps=&r;
break;
}
ps->draw();
}
return ;
}

一个接口呈现出不同的行为,其中virtual是一个声明型关键字,用来声明一个虚函数,子类覆写了的函数,也是virtual

虚函数在子函数中的访问属性并不影响多态,要看子类

虚函数和多态总结

(1)virtual是声明函数的关键字,他是一个声明型关键字

(2)override构成的条件,发生在父子类的继承关系中,同名,同参,同返回

(3)虚函数在派生类中仍然为虚函数,若发生覆写,最好显示的标注virtual

(4)子类中覆写的函数,可以为任意的访问类型,依子类需求决定

8. pure virtual function

纯虚函数,指的是virtual修饰的函数,没有实现体,被初始化为0,被高度抽象化的具有纯接口类才配有纯虚函数,含有纯虚函数的类称为抽象基类

抽象基类不能实例化(不能生成对象),纯粹用来提供接口用的

子类中若无覆写,则依然为纯虚,依然不能实例化

9. 总结

(1)纯虚函数只有声明,没有实现,被“初始化”为0

(2)含有纯虚函数的类,称为Abstract Base Class(抽象基类),不能实例化,即不能创造对象,存在的意义就是被继承,而在派生类中没有该函数的意义

(3)如果一个中声明了纯虚函数,而在派生类中没有该函数的定义,则该虚函数在派生类中仍然为虚函数,派生类仍然为纯虚基类

10. 析构函数

含有虚函数的类,析构函数也应该声明为虚函数

这是为了保证对象析构的完整性,具体的情况就是父类的指针指向子类的堆对象,此时通过父类指针去析构子类堆对象时就会虚构不完整,为了保证析构的完整性,含有虚函数的类将其析构函数也声明为虚函数(virtual)

对比栈对象和对对象在多态中销毁的不同

首先我们来看位于栈上的对象

在这里,我们生成了几个类,一个是抽象基类,一个是Dog类,一个是Cat类,我们分别在class中去构造这几个类

首先生成Animal类

其.h文件的内容如下

 #ifndef ANIMAL_H
#define ANIMAL_H
class Animal
{
public:
Animal();
~Animal();
virtual void voice()=;
};
#endif // ANIMAL_H

其.cpp文件中的内容如下

 #include "animal.h"
#include <iostream>
using namespace std;
Animal::Animal()
{
cout<<"Animal::Animal()"<<endl;
} Animal::~Animal()
{
cout<<"Animal::~Animal()"<<endl;
}

然后我们再生成Dog的.h文件

 #ifndef DOG_H
#define DOG_H
#include "animal.h"
class Animal;
class Dog : public Animal
{
public:
Dog();
~Dog(); virtual void voice();
};
#endif // DOG_H

然后我们再生成Dog的.cpp文件

 #include "dog.h"
#include "animal.h"
#include <iostream>
using namespace std;
Dog::Dog()
{
cout<<"Dog::Dog()"<<endl;
} Dog::~Dog()
{
cout<<"Dog::~Dog()"<<endl;
} void Dog::voice()
{
cout<<"wang wang wang"<<endl;
}

然后我们生成Cat类

首先生成Cat的.h文件

 #ifndef CAT_H
#define CAT_H
#include "animal.h"
class Cat : public Animal
{
public:
Cat();
~Cat(); virtual void voice();
};
#endif // CAT_H

然后再生成cat的.cpp文件

 #include "cat.h"
#include "animal.h"
#include <iostream>
using namespace std;
Cat::Cat()
{
cout<<"Cat::Cat()"<<endl;
}
Cat::~Cat()
{
cout<<"Cat::~Cat()"<<endl;
}
void Cat::voice()
{
cout<<"miao miao miao"<<endl;
}

最后,main函数如下

 #include <iostream>
#include "animal.h"
#include "cat.h"
#include "dog.h"
using namespace std; int main()
{
Cat c;
Dog d;
Animal *pa=&c;
pa->voice();
return ;
}

生成的结果为

可以看出其是析构完全了的

但是若为栈上的对象,即主函数改写为如下

 #include <iostream>
#include "animal.h"
#include "cat.h"
#include "dog.h"
using namespace std; int main()
{
Animal *pa=new Dog;
pa->voice();
delete pa;
return ;
}

得出的结果为

可以看出其是没有析构完全的,生成的Dog是没有析构的,因此对于堆上的对象,其是析构器有问题的

我们只需要解决如下

但凡类中含有虚函数(包括纯虚函数),将其虚构函数置为virtual ,这样即可以实现完整虚构

12.设计模式的原则:依赖倒置原则-核心思想:面向接口编程

传统的过程式设计倾向于使高层次的模块依赖于低层次的模块(自顶向下,逐步细化),而依据DIP的设计原则,将中间层抽象为抽象层,让高层模块和底层模块依赖于中间层

以一个例子来进行举例,用母亲给给孩子讲故事来进行举例

原本母亲给孩子讲故事是依赖于故事书上的内容,因此对于母亲给孩子讲故事我们可以写成如下代码

 //Mother 依赖于 Book  依赖->耦合    -->低耦合
class Book
{
public:
string getContents()
{
return "从前有座山,山里有座庙,庙里有个小和尚."
"听老和尚讲故事,从前有座山";
}
};
class Mother
{
public:
void tellStory(Book &b)
{
cout<<b.getContents()<<endl;
}
};

在这里,母亲和书的关系是一种强耦合关系

即只要书的内容发生改变,Book,Mother等都需要发生改变,这样是很麻烦的

但是实际上,这种强耦合关系是我们所不希望的,为了解决这种强耦合关系,我们引入一个中间层

 #include <iostream>

 using namespace std;

 //Mother 依赖于 Book  依赖->耦合    -->低耦合

 class IReader
{
public:
virtual string getContents()=;
}; class Book:public IReader
{
public:
string getContents()
{
return "从前有座山,山里有座庙,庙里有个小和尚."
"听老和尚讲故事,从前有座山";
}
}; class NewsPaper:public IReader
{
public:
string getContents()
{
return "Trump 要在黑西哥边境建一座墙";
}
};
class Mother
{
public:
void tellStory(IReader *pi)
{
cout<<pi->getContents()<<endl;
}
};
int main()
{
Mother m;
Book b;
NewsPaper n;
m.tellStory(&b);
m.tellStory(&n);
return ;
}

这样的话,书改变时,Mother是不会发生改变的,只需要加一个新类就是可以的了,用户端接口不会发生改变

虚继承和虚函数总结

虚继承解决了多个父类中重名冗余的成员(包括数据成员和函数成员)

虚函数解决了多态的问题

被虚继承的类称为虚基类,含有纯虚函数的类称为抽象基类

最新文章

  1. [Android] HttpURLConnection &amp; HttpClient &amp; Socket
  2. 使用lftp传输文件的shell脚本
  3. canvas基本画图
  4. 修改hive内存限制
  5. hihocoder 1237 Farthest Point
  6. 02安卓用户界面优化之(二)SlidingMenu使用方法
  7. java 解析国密SM2算法证书
  8. 悟透JavaScript (一)
  9. php日志
  10. 洛谷P3376 【模板】网络最大流
  11. 16.IO之其他流
  12. Vue 项目骨架屏注入与实践
  13. MIME sniffing攻击
  14. 查看postgre都有哪些语句占用CPU,以及对应的sql语句
  15. js点击页面其他地方如何隐藏div元素菜单
  16. 百度地图sdk问题 error inflating class com.baidu.mapapi.map.mapview
  17. Codeforces Round #390 (Div. 2) D. Fedor and coupons(区间最大交集+优先队列)
  18. &lt;Android 应用 之路&gt; 百度地图API使用(4)
  19. Excel 导出指定行为txt文件(VBA,宏)
  20. 剑指Offer——矩阵中的路径

热门文章

  1. SSM+shiro及相关插件的整合maven所有依赖,详细注释版,自用,持续更新
  2. WebStorm开发React项目,修代码之后运行的项目不更新
  3. Announcing Windows Template Studio in UWP
  4. spring 自己创建配置类
  5. FTP登录不上 显示“找不到元素”
  6. [十]SpringBoot 之 普通类获取Spring容器中的bean
  7. POJ 3349-Snowflake Snow Snowflakes-字符串哈希
  8. 「CF838B」 Diverging Directions
  9. 【模板】可持久化文艺平衡树-可持久化treap
  10. 洛谷P3602 Koishi Loves Segments(贪心,multiset)