1、权限

1.1权限修饰符

三种权限,一共对应九种场景。要做到心中有表,遇到任何一种场景都能直接反映出是否能访问。

类内

派生类中

全局

private

×

×

protected

×

public

#includeusing namespacestd;class Base{protected://类内派生类内访问,strings= "保护权限";public:Base(){cout<<s<<endl;}};class Son:publicBase{public:Son(){cout<<s<<endl;}};int main(){//Baseb1;Sons1;//cout<<s1.s<<endl;//错误s是保护权限return 0;}

1.2不同权限的继承

1.2.1公有继承public

上面的代码中一直使用的就是公有继承,公有继承也是使用的最多的一种继承方式。

共有继承当中,派生类可以继承基类的成员,但是不可访问基类的私有成员,基类的公有成员与保护成员在派生类中权限不变。

#includeusing namespacestd;class Base{private:stringstr1= "私有成员";protected:stringstr2= "保护成员";public:stringstr3= "公有成员";};class Son:publicBase{public:Son(){//cout<<str1<<endl;//错误str1为私有成员cout<<str2<<endl;cout<<str3<<endl;}};int main(){Sons1;return 0;}

1.2.2保护继承protected

保护继承中,派生类可以继承基类的成员,不可访问基类的私有成员,基类的公有成员与保护成员在派生类中的权限都是保护权限。(只能在基类与派生类中访问,外部无法访问)。

#includeusing namespacestd;class Base{private:stringstr1= "私有成员";protected:stringstr2= "保护成员";public:stringstr3= "公有成员";};//保护继承class Son:protectedBase{public:Son(){//cout<<str1<<endl;//错误str1为私有成员cout<<str2<<endl;cout<<str3<<endl;}};int main(){Sons1;//s1.str3;//错误,str3在保护继承的派生类中为保护成员,外部无法访问return 0;}

1.2.3私有继承private

私有继承中,派生类可以继承基类的成员,但是不可以访问基类的私有成员,基类的公有成员与保护成员在派生类中的权限都是私有权限。

#includeusing namespacestd;class Base{private:stringstr1= "私有成员";protected:stringstr2= "保护成员";public:stringstr3= "公有成员";};//私有继承class Son:privateBase{public:Son(){//cout<<str1<<endl;//错误str1为私有成员cout<<str2<<endl;cout<<str3<<endl;}};int main(){Sons1;//s1.str3;//错误,str在私有继承的派生类中为私有成员return 0;}

2、多态

2.1函数覆盖override

函数覆盖与函数隐藏比较相似,但是函数隐藏不支持多态,而函数覆盖是多态的必备条件。

函数覆盖是指在父类和子类之间存在继承关系时,子类定义了与父类中同名的函数,并且函数的参数类型、返回值类型必须与父类中的相应函数一致。当子类对象调用该同名函数时,会自动调用子类中的函数,而不是父类中的函数。这种机制就是函数覆盖。

(函数隐藏是指在派生类中定义了与基类中同名的函数,从而隐藏了基类中的函数。当通过派生类的对象调用该函数时,实际上调用的是派生类中定义的函数,而不是基类中的函数。函数隐藏发生在不同作用域,即派生类的作用域中隐藏了基类的函数。)

在编程方式上,函数覆盖与函数隐藏有以下几点区别:

  • 被覆盖的基类函数必须是虚函数。
  • 在C++中,可以在派生类中新覆盖的函数上使用override关键字验证覆盖是否成功

一个函数使用virtual关键字修饰,就是虚函数。虚函数是函数覆盖的前提。在QtCreator中函数名称使用斜体字。

虚函数具有以下性质:

  • 虚函数具有传递性,基类中被覆盖的函数是虚函数,派生类中新覆盖的函数也是虚函数。
  • 只有普通成员函数与析构函数可以声明为虚函数。
  • 如果虚函数的声明与定义分离,virtual关键字只需要修饰到声明处。不能写到定义处。

#includeusing namespacestd;class Animal{ public://虚函数virtual void eat();void test(){}};void Animal::eat(){cout<< "动物爱吃饭" <<endl;}class Dog:publicAnimal{void eat() override{cout<< "狗爱吃骨头" <<endl;}//voidtest()override//{//}};int main(){Dogd1;return 0;}

2.2多态的概念

多态可以理解为“一种接口,多种状态”,只需要编写一个函数接口,根据传入的参数类型,执行不同的策略代码。

多态的使用需要具备三个前提条件:

  • 公有继承
  • 函数覆盖(在派生类内声明一个与基类公共成员函数函数名及参数一致的函数)
  • 基类的引用/指针指向派生类对象

#includeusing namespacestd;class Animal{ public://虚函数virtual void eat(){cout<< "动物爱吃饭" <<endl;}};class Dog:publicAnimal{public:void eat() override{cout<<"狗爱吃骨头"<<endl;}};class Cat:publicAnimal{public:void eat(){cout<<"猫爱吃鱼"<<endl;}};void animal_eat(Animal*al){al->eat();}void animal_eat2(Animal&al){al.eat();}int main(){//堆区开辟Dog*d1=newDog;Cat*c1=newCat;animal_eat(d1);animal_eat(c1);//栈区定义Dogd2;Catc2;animal_eat2(d2);animal_eat2(c2);return 0;}

2.3多态的原理

具有虚函数的类会存在一张虚函数表。这张表被当前类所有对象共用。每个类的对象内部都会有一个隐藏的虚函数指针成员,指向当前类的虚函数表。

在代码运行时,通过对象的虚函数指针找到对应虚函数表,在表中定位到虚函数的调用地址,执行对应的虚函数的内容。

因此使用多态会产生一些额外的开销。优点是代码编写更加的灵活高效,缺点是会降低代码的执行速度,代码可读性降低。

2.4虚析构函数

(析构函数:当对象销毁时被调用,走完花括号“{}”,或delate释放堆空间)

如果不使用虚析构函数,且基类指针或引用指向派生类对象,使用delete销毁对象时,

析构函数不会被继承,但虚函数表会被继承,虚析构函数不会被覆盖,会自动生成一个新的对应派生类虚函数的虚析构函数,原虚析构函数对应基类虚函数。

只会触发基类的析构函数,如果在派生类中申请了内存资源,则会导致无法释放,出现内存泄漏的问题。

#includeusing namespacestd;class Animal{ public://虚函数virtual void eat(){cout<< "动物爱吃饭" <<endl;}~Animal(){cout<< "Animal析构函数被调用了" <<endl;}};class Dog:publicAnimal{public:void eat() override{cout<< "狗爱吃骨头" <<endl;}~Dog(){cout<< "Dog析构函数被调用了" <<endl;}};int main(){Animal*al= newDog;al->eat();//狗爱吃骨头deleteal;//Animal析构函数被调用了,只调用这一个return 0;}

解决方法是给基类的析构函数使用virtual关键字修饰为虚析构函数,通过传递性可以把各个派生类的析构函数都变为虚析构函数。因此建议给一个可能为基类的类中的析构函数设置为虚析构函数。

#includeusingnamespacestd;classAnimal{public://虚函数virtualvoideat(){cout<<"动物爱吃饭"<<endl;}virtual~Animal(){cout<<"Animal析构函数被调用了"<<endl;}};classDog:publicAnimal{public:voideat()override{cout<<"狗爱吃骨头"<<endl;}~Dog(){cout<<"Dog析构函数被调用了"<<endl;}};intmain(){Animal*al=newDog;al->eat();//狗爱吃骨头,函数执行完后执行该函数对应的析构函数。//Dog析构函数被调用了deleteal;//Animal析构函数被调用了return0;}

  1. 抽象类

如果基类只表达一些抽象的概念,并不与实际的对象相关联,这时候就可以使用抽象类。

如果一个类中有纯虚函数,这个类就是一个抽象类。

如果一个类是抽象类,则这个类中一定有纯虚函数。

纯虚函数是虚函数的一种,这种函数只有声明没有定义。不能实例化对象·

virtual返回值类型函数名(参数列表) = 0;

不能直接使用抽象类作为类型声明,因为不存在抽象类类型的对象。

抽象类作为基类时,具有两种情况:

  • 派生类继承抽象类,覆盖并实现所有的纯虚函数,此时派生类可以作为普通类使用,即不再是抽象类。
  • 派生类继承抽象类,没有把抽象类中所有的纯虚函数覆盖并实现,此时派生类也变为了抽象类,等待他的派生类覆盖并实现剩余的纯虚函数。(需要完全覆盖基类所有抽象函数

抽象类,无法实例化对象,但是可以创建指针和引用。

#includeusing namespacestd;//抽象类:形状class Shape{public://纯虚函数virtual void area() = 0; //面积virtual void perimeter() = 0; //周长};//圆形class Circle:publicShape{public:void area(){cout<< "圆形计算面积" <<endl;}void perimeter(){cout<< "圆形计算周长" <<endl;}};//多边形class Polygon:publicShape{public:void perimeter(){cout<< "多边形计算周长" <<endl;}};//矩形class Rectangle:publicPolygon{public:void area(){cout<< "矩形计算面积" <<endl;}};int main(){//Shapes;//错误抽象类无法实例化对象Circlec;c.area();c.perimeter();//Polygonp;//错误,没有完全覆盖基类抽象函数,抽象类无法实例化对象Rectangler;r.area();r.perimeter();return 0;}

使用抽象类注意以下几点:

  • 抽象类的析构函数必须是虚析构函数
  • 抽象类支持多态,可以存在指针或引用的声明格式
  • 因为抽象类的作用就是指定算法框架,因此在一个继承体系中,抽象类的内容相对丰富且重要。

2.5、纯虚析构(熟悉)

纯虚析构函数的定义:

纯虚析构的本质:是析构函数,作用是各个类的回收工作。而且析构函数不能被继承。

必须要为纯虚析构函数提供一个函数体。

纯虚析构函数,必须在类外实现。

#includeusing namespacestd;class Animal{public:virtualvoid speak(){}Animal(){cout<< "基类的构造函数被调用了" <<endl;}//纯虚析构virtual ~Animal() = 0;};//纯虚析构类外实现Animal::~Animal(){cout<< "基类析构函数被调用了" <<endl;}class Dog:publicAnimal{public:void speak(){cout<< "狗会汪汪汪" <<endl;}Dog(){cout<< "dog类构造函数被调用了" <<endl;}~Dog(){cout<< "Dog类的析构函数被调用了" <<endl;}};int main(){//Animalal;//错误基类是纯虚析构,无法实例化对象Animal*al= new Dog();al->speak();deleteal;return 0;}

虚析构与纯虚析构的区别:

  • 虚析构:virtual关键字修饰,有函数体,不会导致基类为抽象类。
  • 纯虚函数:virtual关键字修饰,结果=0,函数体需要类外实现,会导致基类是抽象类。