1、权限
1.1 权限修饰符
三种权限,一共对应九种场景。要做到心中有表,遇到任何一种场景都能直接反映出是否能访问。
类内 | 派生类中 | 全局 | |
private | √ | × | × |
protected | √ | √ | × |
public | √ | √ | √ |
#include <iostream>
using namespace std;
class Base
{
protected:
//类内 派生类内访问,
string s = "保护权限";
public:
Base()
{
cout << s << endl;
}
};
class Son:public Base
{
public:
Son()
{
cout << s << endl;
}
};
int main()
{
// Base b1;
Son s1;
// cout << s1.s << endl; // 错误 s是保护权限
return 0;
}
1.2 不同权限的继承
1.2.1 公有继承 public
上面的代码中一直使用的就是公有继承,公有继承也是使用的最多的一种继承方式。
在共有继承当中,派生类可以继承基类的成员,但是不可访问基类的私有成员,基类的公有成员与保护成员在派生类中权限不变。
#include <iostream>
using namespace std;
class Base
{
private:
string str1 = "私有成员";
protected:
string str2 = "保护成员";
public:
string str3 = "公有成员";
};
class Son:public Base
{
public:
Son()
{
// cout << str1 << endl; // 错误 str1为私有成员
cout << str2 << endl;
cout << str3 << endl;
}
};
int main()
{
Son s1;
return 0;
}
1.2.2 保护继承 protected
在保护继承中,派生类可以继承基类的成员,不可访问基类的私有成员,基类的公有成员与保护成员在派生类中的权限都是保护权限。(只能在基类与派生类中访问,外部无法访问)。
#include <iostream>
using namespace std;
class Base
{
private:
string str1 = "私有成员";
protected:
string str2 = "保护成员";
public:
string str3 = "公有成员";
};
// 保护继承
class Son:protected Base
{
public:
Son()
{
// cout << str1 << endl; // 错误 str1为私有成员
cout << str2 << endl;
cout << str3 << endl;
}
};
int main()
{
Son s1;
// s1.str3; //错误,str3在保护继承的派生类中为保护成员,外部无法访问
return 0;
}
1.2.3 私有继承 private
在私有继承中,派生类可以继承基类的成员,但是不可以访问基类的私有成员,基类的公有成员与保护成员在派生类中的权限都是私有权限。
#include <iostream>
using namespace std;
class Base
{
private:
string str1 = "私有成员";
protected:
string str2 = "保护成员";
public:
string str3 = "公有成员";
};
// 私有继承
class Son:private Base
{
public:
Son()
{
// cout << str1 << endl; // 错误 str1为私有成员
cout << str2 << endl;
cout << str3 << endl;
}
};
int main()
{
Son s1;
// s1.str3; //错误,str在私有继承的派生类中为私有成员
return 0;
}
2、多态
2.1 函数覆盖 override
函数覆盖与函数隐藏比较相似,但是函数隐藏不支持多态,而函数覆盖是多态的必备条件。
(函数覆盖是指在父类和子类之间存在继承关系时,子类定义了与父类中同名的函数,并且函数的参数类型、返回值类型必须与父类中的相应函数一致。当子类对象调用该同名函数时,会自动调用子类中的函数,而不是父类中的函数。这种机制就是函数覆盖。)
(函数隐藏是指在派生类中定义了与基类中同名的函数,从而隐藏了基类中的函数。当通过派生类的对象调用该函数时,实际上调用的是派生类中定义的函数,而不是基类中的函数。函数隐藏发生在不同作用域,即派生类的作用域中隐藏了基类的函数。)
在编程方式上,函数覆盖与函数隐藏有以下几点区别:
- 被覆盖的基类函数必须是虚函数。
- 在C++中,可以在派生类中新覆盖的函数上使用override 关键字验证覆盖是否成功。
一个函数使用virtual关键字修饰,就是虚函数。虚函数是函数覆盖的前提。在Qt Creator中函数名称使用斜体字。
虚函数具有以下性质:
- 虚函数具有传递性,基类中被覆盖的函数是虚函数,派生类中新覆盖的函数也是虚函数。
- 只有普通成员函数与析构函数可以声明为虚函数。
- 如果虚函数的声明与定义分离,virtual关键字只需要修饰到声明处。不能写到定义处。
#include <iostream>
using namespace std;
class Animal
{
public:
// 虚函数
virtual void eat();
void test()
{
}
};
void Animal::eat()
{
cout << "动物爱吃饭" << endl;
}
class Dog:public Animal
{
void eat() override
{
cout << "狗爱吃骨头" << endl;
}
// void test() override
// {
// }
};
int main()
{
Dog d1;
return 0;
}
2.2 多态的概念
多态可以理解为“一种接口,多种状态”,只需要编写一个函数接口,根据传入的参数类型,执行不同的策略代码。
多态的使用需要具备三个前提条件:
- 公有继承
- 函数覆盖(在派生类内声明一个与基类公共成员函数函数名及参数一致的函数)
- 基类的引用/指针指向派生类对象
#include <iostream>
using namespace std;
class Animal
{
public:
// 虚函数
virtual void eat()
{
cout << "动物爱吃饭" << endl;
}
};
class Dog:public Animal
{
public:
void eat() override
{
cout<<"狗爱吃骨头"<<endl;
}
};
class Cat:public Animal
{
public:
void eat()
{
cout<<"猫爱吃鱼"<<endl;
}
};
void animal_eat(Animal *al)
{
al->eat();
}
void animal_eat2(Animal &al)
{
al.eat();
}
int main()
{
//堆区开辟
Dog *d1=new Dog;
Cat *c1=new Cat;
animal_eat(d1);
animal_eat(c1);
//栈区定义
Dog d2;
Cat c2;
animal_eat2(d2);
animal_eat2(c2);
return 0;
}
2.3 多态的原理
具有虚函数的类会存在一张虚函数表。这张表被当前类所有对象共用。每个类的对象内部都会有一个隐藏的虚函数指针成员,指向当前类的虚函数表。
在代码运行时,通过对象的虚函数指针找到对应虚函数表,在表中定位到虚函数的调用地址,执行对应的虚函数的内容。
因此使用多态会产生一些额外的开销。优点是代码编写更加的灵活高效,缺点是会降低代码的执行速度,代码可读性降低。
2.4 虚析构函数
(析构函数:当对象销毁时被调用,走完花括号“{}”,或delate释放堆空间)
如果不使用虚析构函数,且基类指针或引用指向派生类对象,使用delete销毁对象时,
析构函数不会被继承,但虚函数表会被继承,虚析构函数不会被覆盖,会自动生成一个新的对应派生类虚函数的虚析构函数,原虚析构函数对应基类虚函数。
只会触发基类的析构函数,如果在派生类中申请了内存资源,则会导致无法释放,出现内存泄漏的问题。
#include <iostream>
using namespace std;
class Animal
{
public:
// 虚函数
virtual void eat()
{
cout << "动物爱吃饭" << endl;
}
~Animal()
{
cout << "Animal 析构函数被调用了" << endl;
}
};
class Dog:public Animal
{
public:
void eat() override
{
cout << "狗爱吃骨头" << endl;
}
~Dog()
{
cout << "Dog 析构函数被调用了" << endl;
}
};
int main()
{
Animal *al = new Dog;
al->eat();//狗爱吃骨头
delete al;//Animal 析构函数被调用了,只调用这一个
return 0;
}
解决方法是给基类的析构函数使用virtual关键字修饰为虚析构函数,通过传递性可以把各个派生类的析构函数都变为虚析构函数。因此建议给一个可能为基类的类中的析构函数设置为虚析构函数。
#include <iostream>
using namespace std;
class Animal
{
public:
// 虚函数
virtual void eat()
{
cout << "动物爱吃饭" << endl;
}
virtual ~Animal()
{
cout << "Animal 析构函数被调用了" << endl;
}
};
class Dog:public Animal
{
public:
void eat() override
{
cout << "狗爱吃骨头" << endl;
}
~Dog()
{
cout << "Dog 析构函数被调用了" << endl;
}
};
int main()
{
Animal *al = new Dog;
al->eat();//狗爱吃骨头,函数执行完后执行该函数对应的析构函数。
//Dog 析构函数被调用了
delete al;//Animal 析构函数被调用了
return 0;
}
- 抽象类
如果基类只表达一些抽象的概念,并不与实际的对象相关联,这时候就可以使用抽象类。
如果一个类中有纯虚函数,这个类就是一个抽象类。
如果一个类是抽象类,则这个类中一定有纯虚函数。
纯虚函数是虚函数的一种,这种函数只有声明没有定义。不能实例化对象·
virtual 返回值类型 函数名(参数列表) = 0;
不能直接使用抽象类作为类型声明,因为不存在抽象类类型的对象。
抽象类作为基类时,具有两种情况:
- 派生类继承抽象类,覆盖并实现所有的纯虚函数,此时派生类可以作为普通类使用,即不再是抽象类。
- 派生类继承抽象类,没有把抽象类中所有的纯虚函数覆盖并实现,此时派生类也变为了抽象类,等待他的派生类覆盖并实现剩余的纯虚函数。(需要完全覆盖基类所有抽象函数)
抽象类,无法实例化对象,但是可以创建指针和引用。
#include <iostream>
using namespace std;
// 抽象类:形状
class Shape
{
public:
// 纯虚函数
virtual void area() = 0; // 面积
virtual void perimeter() = 0; // 周长
};
// 圆形
class Circle:public Shape
{
public:
void area()
{
cout << "圆形计算面积" << endl;
}
void perimeter()
{
cout << "圆形计算周长" << endl;
}
};
// 多边形
class Polygon:public Shape
{
public:
void perimeter()
{
cout << "多边形计算周长" << endl;
}
};
// 矩形
class Rectangle:public Polygon
{
public:
void area()
{
cout << "矩形计算面积" << endl;
}
};
int main()
{
// Shape s; // 错误抽象类无法实例化对象
Circle c;
c.area();
c.perimeter();
// Polygon p; // 错误,没有完全覆盖基类抽象函数,抽象类无法实例化对象
Rectangle r;
r.area();
r.perimeter();
return 0;
}
使用抽象类注意以下几点:
- 抽象类的析构函数必须是虚析构函数
- 抽象类支持多态,可以存在指针或引用的声明格式
- 因为抽象类的作用就是指定算法框架,因此在一个继承体系中,抽象类的内容相对丰富且重要。
2.5、纯虚析构(熟悉)
纯虚析构函数的定义:
纯虚析构的本质:是析构函数,作用是各个类的回收工作。而且析构函数不能被继承。
必须要为纯虚析构函数提供一个函数体。
纯虚析构函数,必须在类外实现。
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void speak()
{
}
Animal()
{
cout << "基类的构造函数被调用了" << endl;
}
// 纯虚析构
virtual ~Animal() = 0;
};
// 纯虚析构类外实现
Animal::~Animal()
{
cout << "基类析构函数被调用了" << endl;
}
class Dog:public Animal
{
public:
void speak()
{
cout << "狗会汪汪汪" << endl;
}
Dog()
{
cout << "dog类构造函数被调用了" << endl;
}
~Dog()
{
cout << "Dog类的析构函数被调用了" << endl;
}
};
int main()
{
// Animal al; // 错误基类是纯虚析构,无法实例化对象
Animal *al = new Dog();
al->speak();
delete al;
return 0;
}
虚析构与纯虚析构的区别:
- 虚析构:virtual关键字修饰,有函数体,不会导致基类为抽象类。
- 纯虚函数:virtual关键字修饰,结果=0,函数体需要类外实现,会导致基类是抽象类。