C++基础知识-Day8
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 0; }
这样调用还是会出现死循环的情况,虽然其本意是在类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) 三角问题
多个父类中重名的成员,继承到子类中后,为了避免冲突,携带了各父类的作用域信息,子类中要访问继承下来的重名成员,则会产生二义性,为了避免冲突,访问时需要提供父类的作用域信息
构造器问题
下面我们用一个实际的例子来对其进行讲解
1 #include <iostream> 2 3 using namespace std; 4 5 class X 6 { 7 public: 8 X(int d) 9 { 10 cout<<"X()"<<endl; 11 } 12 protected: 13 int _data; 14 }; 15 16 class Y 17 { 18 public: 19 Y(int d) 20 { 21 cout<<"Y()"<<endl; 22 } 23 protected: 24 int _data; 25 }; 26 27 class Z:public X,public Y 28 { 29 public: 30 Z() 31 :X(1),Y(2) 32 { 33 34 } 35 void dis() 36 { 37 cout<<Y_data<<endl;39 } 40 }; 41 42 int main() 43 { 44 Z z; 45 z.dis(); 46 return 0; 47 }
直接这样的话会报错,因为_data会产生二义性,为了解决这个问题,我们可以在数据之前加上其父类作用域
1 void dis() 2 { 3 cout<<Y::_data<<endl; 4 cout<<X::_data<<endl; 5 }
下面我们看一个有趣的情况
#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(100,200); z.dis(); cout<<"================="<<endl; z.setData(1000000); cout<<z.getData()<<endl; cout<<"================="<<endl; z.dis(); return 0; }
在这里我们getData得到的数据仍然是200,并不是setData的1000000,原因如下
刚开始的时候,在类X和类Y中,都有一个_data,
当其继承在类Z中后
由于是重名的问题,setData设置的是类X中的数据,但是getData得到的是类Y中的数据,所以说会出现问题
那么我们应该怎么来解决这个问题呢
需要解决的问题:
数据冗余
访问方便
由此引发了一个三角转四角的问题
- 提取各父类中相同的成员,包括数据成员和函数成员,构成祖父类
- 让各父类,继承祖父类
- 虚继承是一种继承的扩展,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(100,200); z.dis(); cout<<"================="<<endl; z.setData(1000000); cout<<z.getData()<<endl; cout<<"================="<<endl; z.dis(); return 0; }
这样就带来了两个好处,解决了数据冗余的问题,并且为访问带来了便利,虚继承也是一种设计的结果,被抽象上来的类叫做虚基类。也可以说成:被虚继承的类称为虚基类
虚基类:被抽象上来的类叫做虚基类
虚继承:是一种对继承的扩展
那么虚继承就有几个问题需要我们来注意了,首先是初始化的顺序问题,为了测试初始化的顺序问题,因为上述都是构造器的默认情况,但是实际情况中,可能都会带参数,甚至是虚继承的祖父类也会带参数,那么构造器顺序又将是如何的呢?我们利用如下代码进行测试
1 #include <iostream> 2 3 using namespace std; 4 5 class A 6 { 7 public: 8 A(int i) 9 { 10 _data=i; 11 cout<<"A(int i)"<<endl; 12 } 13 protected: 14 int _data; 15 }; 16 class B:virtual public A 17 { 18 public: 19 B(int i) 20 :A(i) 21 { 22 _data=i; 23 cout<<"B(int i)"<<endl; 24 } 25 }; 26 27 class C:virtual public A 28 { 29 public: 30 C(int i) 31 :A(i) 32 { 33 _data=i; 34 cout<<"C(int i)"<<endl; 35 } 36 }; 37 38 class D:public C,B 39 { 40 public: 41 D() 42 :C(1),B(1),A(1) 43 { 44 cout<<"D(int i)"<<endl; 45 } 46 void dis() 47 { 48 cout<<_data<<endl; 49 } 50 }; 51 int main() 52 { 53 D d; 54 d.dis(); 55 return 0; 56 }
运行代码后我们可以得知,构造的顺序是从祖父类的构造器开始,按照顺序执行下来,最后到孙子类的构造器为止的
当然,上述只是一个测试,因为在实际过程中,祖父类是由父类抽象起来的,因此一般不会用祖父类生成对象
在实际过程中,在父类的构造器中我们常带默认参数,这样我们就可以不使得派生类的构造器如此复杂
实际例子,沙发床,除了上述之外,我们还需要增加颜色和重量,除此之外,我们还需要用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=0,int c=1) { _weight=w; _color=c; } void sit() { cout<<"take a sit and have a rest"<<endl; } }; class Bed:virtual public Furniture { public: Bed(float w=0,int c=1) { _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(1000,2); sb.sit(); sb.sleep(); sb.descript(); return 0; } int main1() { Sofa sf; sf.sit(); Bed bd; bd.sleep(); return 0; }
6. 多态
(1) 生活中的多态
如果有几个相似而不完全相同的对象,有时人们要求在向他们发出同一个消息时,他们的反应各不相同,分别执行不同的操作,这种情况就是多态现象
(2) C++ 中的多态
C++ 中的多态是指,由继承而产生的相关的不同的类,其对同一消息会做出不同的响应
比如,Mspaint中的单击不同图形,执行同一拖动动作而绘制不同的图形,就是典型的多态应用
多态性是面向对象程序设计的一个重要特征,能增加程序的灵活性,可以减轻系统的升级,维护,调试的工作量和复杂度
(3) 赋值兼容
赋值兼容是指,在需要基类对象的任何地方,都可以使用共有派生的对象来替代
只有在共有派生类中才有赋值兼容,赋值兼容是一种默认行为,不需要任何的显示的转化步骤
赋值兼容总结起来有以下三种特点
派生类的对象可以赋值给基类对象 |
派生类的对象可以初始化基类的引用 |
派生类对象的地址可以赋给指向基类的指针 |
下面我们将分别对其进行说明
- 派生类的对象可以赋值给基类对象
观察下面代码
1 #include <iostream> 2 3 using namespace std; 4 5 class Shape 6 { 7 public: 8 Shape(int x=0,int y=0) 9 :_x(x),_y(y){} 10 void draw() 11 { 12 cout<<"draw shape from"<<"("<<_x<<","<<_y<<")"<<endl; 13 } 14 protected: 15 int _x; 16 int _y; 17 }; 18 class Circle:public Shape 19 { 20 public: 21 Circle(int x=0,int y=0,int r=1) 22 :Shape(x,y),_radius(r){} 23 void draw() 24 { 25 cout<<"draw shape from"<<"("<<_x<<","<<_y<<")"<<"radius:"<<_radius<<endl; 26 } 27 protected: 28 int _radius; 29 }; 30 int main() 31 { 32 Shape s(1,2); 33 s.draw(); 34 Circle c(4,5,6); 35 c.draw(); 36 s=c; //派生类对象可以赋值给基类对象 37 s.draw(); 38 return 0; 39 }
有上述例子可以看出,派生类的对象是可以复制给基类对象的
- 派生类的对象可以初始化基类的引用
1 int main() 2 { 3 Shape s(1,2); 4 s.draw(); 5 Circle c(4,5,6); 6 Shape &rs=c; 7 rs.draw(); 8 return 0; 9 }
- 派生类的对象的地址可以赋给指向基类的指针
1 int main() 2 { 3 Shape s(1,2); 4 s.draw(); 5 Circle c(4,5,6); 6 Shape *ps=&c; 7 ps->draw(); 8 return 0; 9 }
在这三种情况中,使用的最多的是第三种,即派生类对象的地址可以赋给指向基类的指针
就如图示一样,假设左边的类是父类,右边的类是子类,,左边的指针是派生类的对象的地址赋给指向派生类的指针,那么其可访问的范围就是整个派生类,右边的指针是派生类的对象的地址赋给指向基类的指针,那么其访问范围就只有基类的那一部分
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; }
系统即会开始报错
- 子类覆写父类中的虚函数,子类中同名同参同函数,才能构成覆写
- 通过已被子类对象赋值的父类指针,调用虚函数,形成多态
1 #include <iostream> 2 #include <typeinfo> 3 using namespace std; 4 5 class Shape 6 { 7 public: 8 Shape(int x=0,int y=0) 9 :_x(x),_y(y) 10 { 11 cout<<"shape->this"<<this<<endl; 12 cout<<typeid(this).name()<<endl; 13 } 14 virtual void draw() 15 { 16 cout<<"draw shape from"<<"("<<_x<<","<<_y<<")"<<endl; 17 } 18 protected: 19 int _x; 20 int _y; 21 }; 22 class Circle:public Shape 23 { 24 public: 25 Circle(int x=0,int y=0,int r=1) 26 :Shape(x,y),_radius(r) 27 { 28 cout<<"shape->this"<<this<<endl; 29 cout<<typeid(this).name()<<endl; 30 } 31 void draw() 32 { 33 cout<<"draw shape from"<<"("<<_x<<","<<_y<<")"<<"radius:"<<_radius<<endl; 34 } 35 protected: 36 int _radius; 37 }; 38 39 40 class Rect:public Shape 41 { 42 public: 43 Rect(int x=0,int y=0,int w=0,int l=0) 44 :Shape(x,y),_width(w),_lenth(l){} 45 virtual void draw() 46 { 47 cout<<"draw Circle from"<<"("<<_x<<","<<_y<<")" 48 <<"width:"<<_width<<"lenth:"<<_lenth<<endl; 49 } 50 protected: 51 52 int _width; 53 int _lenth; 54 }; 55 56 57 int main() 58 { 59 Circle c(3,4,5); 60 Shape *ps=&c;//父类指针指向子类的对象 61 ps->draw(); 62 63 Rect r(6,7,8,9); 64 ps=&r; 65 ps->draw(); 66 return 0; 67 }
可以看出,利用virtual,可以实现多态
通过父类的指针调用父类的接口指向其本来应该指向的内容
1 int main() 2 { 3 Circle c(3,4,5); 4 Shape *ps=&c;//父类指针指向子类的对象 5 ps->draw(); 6 7 Rect r(6,7,8,9); 8 ps=&r; 9 ps->draw(); 10 while(1) 11 { 12 int choice; 13 cin>>choice; 14 switch(choice) 15 { 16 case 1: 17 ps=&c; 18 break; 19 case 2: 20 ps=&r; 21 break; 22 } 23 ps->draw(); 24 } 25 return 0; 26 }
一个接口呈现出不同的行为,其中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文件的内容如下
1 #ifndef ANIMAL_H 2 #define ANIMAL_H 3 class Animal 4 { 5 public: 6 Animal(); 7 ~Animal(); 8 virtual void voice()=0; 9 }; 10 #endif // ANIMAL_H
其.cpp文件中的内容如下
1 #include "animal.h" 2 #include <iostream> 3 using namespace std; 4 Animal::Animal() 5 { 6 cout<<"Animal::Animal()"<<endl; 7 } 8 9 Animal::~Animal() 10 { 11 cout<<"Animal::~Animal()"<<endl; 12 }
然后我们再生成Dog的.h文件
1 #ifndef DOG_H 2 #define DOG_H 3 #include "animal.h" 4 class Animal; 5 class Dog : public Animal 6 { 7 public: 8 Dog(); 9 ~Dog(); 10 11 virtual void voice(); 12 }; 13 #endif // DOG_H
然后我们再生成Dog的.cpp文件
1 #include "dog.h" 2 #include "animal.h" 3 #include <iostream> 4 using namespace std; 5 Dog::Dog() 6 { 7 cout<<"Dog::Dog()"<<endl; 8 } 9 10 Dog::~Dog() 11 { 12 cout<<"Dog::~Dog()"<<endl; 13 } 14 15 void Dog::voice() 16 { 17 cout<<"wang wang wang"<<endl; 18 }
然后我们生成Cat类
首先生成Cat的.h文件
1 #ifndef CAT_H 2 #define CAT_H 3 #include "animal.h" 4 class Cat : public Animal 5 { 6 public: 7 Cat(); 8 ~Cat(); 9 10 virtual void voice(); 11 }; 12 #endif // CAT_H
然后再生成cat的.cpp文件
1 #include "cat.h" 2 #include "animal.h" 3 #include <iostream> 4 using namespace std; 5 Cat::Cat() 6 { 7 cout<<"Cat::Cat()"<<endl; 8 } 9 Cat::~Cat() 10 { 11 cout<<"Cat::~Cat()"<<endl; 12 } 13 void Cat::voice() 14 { 15 cout<<"miao miao miao"<<endl; 16 }
最后,main函数如下
1 #include <iostream> 2 #include "animal.h" 3 #include "cat.h" 4 #include "dog.h" 5 using namespace std; 6 7 int main() 8 { 9 Cat c; 10 Dog d; 11 Animal *pa=&c; 12 pa->voice(); 13 return 0; 14 }
生成的结果为
可以看出其是析构完全了的
但是若为栈上的对象,即主函数改写为如下
1 #include <iostream> 2 #include "animal.h" 3 #include "cat.h" 4 #include "dog.h" 5 using namespace std; 6 7 int main() 8 { 9 Animal *pa=new Dog; 10 pa->voice(); 11 delete pa; 12 return 0; 13 }
得出的结果为
可以看出其是没有析构完全的,生成的Dog是没有析构的,因此对于堆上的对象,其是析构器有问题的
我们只需要解决如下
但凡类中含有虚函数(包括纯虚函数),将其虚构函数置为virtual ,这样即可以实现完整虚构
12.设计模式的原则:依赖倒置原则-核心思想:面向接口编程
传统的过程式设计倾向于使高层次的模块依赖于低层次的模块(自顶向下,逐步细化),而依据DIP的设计原则,将中间层抽象为抽象层,让高层模块和底层模块依赖于中间层
以一个例子来进行举例,用母亲给给孩子讲故事来进行举例
原本母亲给孩子讲故事是依赖于故事书上的内容,因此对于母亲给孩子讲故事我们可以写成如下代码
1 //Mother 依赖于 Book 依赖->耦合 -->低耦合 2 class Book 3 { 4 public: 5 string getContents() 6 { 7 return "从前有座山,山里有座庙,庙里有个小和尚." 8 "听老和尚讲故事,从前有座山"; 9 } 10 }; 11 class Mother 12 { 13 public: 14 void tellStory(Book &b) 15 { 16 cout<<b.getContents()<<endl; 17 } 18 };
在这里,母亲和书的关系是一种强耦合关系
即只要书的内容发生改变,Book,Mother等都需要发生改变,这样是很麻烦的
但是实际上,这种强耦合关系是我们所不希望的,为了解决这种强耦合关系,我们引入一个中间层
1 #include <iostream> 2 3 using namespace std; 4 5 //Mother 依赖于 Book 依赖->耦合 -->低耦合 6 7 class IReader 8 { 9 public: 10 virtual string getContents()=0; 11 }; 12 13 class Book:public IReader 14 { 15 public: 16 string getContents() 17 { 18 return "从前有座山,山里有座庙,庙里有个小和尚." 19 "听老和尚讲故事,从前有座山"; 20 } 21 }; 22 23 class NewsPaper:public IReader 24 { 25 public: 26 string getContents() 27 { 28 return "Trump 要在黑西哥边境建一座墙"; 29 } 30 }; 31 class Mother 32 { 33 public: 34 void tellStory(IReader *pi) 35 { 36 cout<<pi->getContents()<<endl; 37 } 38 }; 39 int main() 40 { 41 Mother m; 42 Book b; 43 NewsPaper n; 44 m.tellStory(&b); 45 m.tellStory(&n); 46 return 0; 47 }
这样的话,书改变时,Mother是不会发生改变的,只需要加一个新类就是可以的了,用户端接口不会发生改变
虚继承和虚函数总结
虚继承解决了多个父类中重名冗余的成员(包括数据成员和函数成员)
虚函数解决了多态的问题
被虚继承的类称为虚基类,含有纯虚函数的类称为抽象基类