C++面向对象包含三大特性:封装、继承、多态,下面对知识点进行一一介绍。
一、封装
封装是指类中的所有数据成员都封装在类的成员函数中,只能通过类的成员函数进行访问。
1、访问限定符(public, protected, private)
使用类实例化一个对象,对于类中的 public 成员,对象可以直接访问;对于类中的 private 成员,对象只能通过类的成员函数进行访问,这也是 C++ 封装特性的体现。
2、内存占用情况
一个类在未实例化之前,不占用存储空间,在实例化之后才占用存储空间,虽然一个类会实例化很多个对象,但是类的代码只有一份保存在代码区。
3、一个空类默认产生哪些函数?
- 构造函数
- 析构函数
- 拷贝构造函数
- 赋值函数
4、构造函数
- 若类的构造函数使用初始化列表方式进行初始化,则初始化变量顺序是根据数据成员的定义顺序进行的;
- 当成员变量为 const 类型时,只能通过初始化列表方式进行初始化;
- 构造函数在类的实例化时被调用;
- 与类同名,可以重载
- 未定义构造函数,系统会自动生成默认构造函数;
5、拷贝构造函数
student(const student &s )
{
... //类名为 student
}
- 拷贝构造函数的参数为引用类型,为什么呢?是为了防止拷贝构造函数的无限递归导致内存溢出;
- 当使用直接初始化或者复制初始化方式实例化对象时,会调用拷贝构造函数;
- 拷贝构造函数的参数是确定的,不能重载;
- 使用拷贝构造函数时要注意深拷贝和浅拷贝的区别,具体介绍参看这里;
6、析构函数
- 在对象销毁时调用析构函数,释放系统空间;
- ~类名(),没有任何参数,不能重载;
7、this 指针:在类中,程序编译时会自动为成员函数增加 this 指针,当类的数据成员和类的成员函数同名时,会在成员函数中使用 this.xx 来指明为数据成员的值而非成员函数的参数值。
8、class 和 struct 的区别
实际上,class 和 struct 的意义是一样的,struct 也可以有构造函数和析构函数,也可以继承,唯一的区别是在 class 中,默认的访问控制是 private,但在 struct 中,默认的访问控制是 public。
二、继承
什么是继承?
类 A 的属性类 B 都有,类 B 从类 A 那里继承过来使得简化对类 B 的定义。
1、继承方式
class B: public A
此时类 A 中的 public 成员会继承到类 B 的 public 中,类 A 中的 protected 成员会继承到类 B 的 protected 中,类 A 中的 private 成员类 B 无法访问
class B: protected A
此时类 A 中的 public 成员会继承到类 B 的 protected 中,类 A 中的 protected 成员会继承到类 B 的 protected 中,类 A 中的 private 成员类 B 无法访问
class B: private A
此时类 A 中的 public 成员会继承到类 B 的 private 中,类 A 中的 protected 成员会继承到类 B 的 private 中,类 A 中的 private 成员类 B 无法访问
2、实例化对象时的过程
- 若实例化子类的对象,则调用父类的构造函数,再调用子类的构造函数;
- 在对象销毁时,先调用子类的析构函数,再调用父类的析构函数;
3、隐藏
在父类和子类中定义了同名的数据成员或者成员函数,在实例化子类的对象时,父类中同名的成员就会被隐藏;若此时使用子类的对象访问该同名的成员,则访问到的是子类自身的成员,不是父类的成员;若想使用子类的对象访问父类的成员函数 fun(),则使用 b.A::fun() 的方式进行。
4、多重继承
class B: public A
class C: public B
这种就属于多重继承,A->B->C,即类 B 继承于类 A,类 C 继承于类 B。
5、多继承
class B: public A
class C: public A
这种就属于多继承,即即类 B 继承于类 A,类 C 继承于类 A。
6、菱形继承
当既有多继承又有多重继承时,就会出现菱形继承的情况,于是会出现如下问题:此时的类 D 中会包含两份类 Base,那么如何让解决呢?答案是使用虚继承。
7、虚继承
class B: virtual public A
class C: virtual public A
使用 virtual 关键字进行修饰,即可解决。
三、多态
简单来说,多态就是一种接口,多种实现,多态的实现是基于虚函数的,多态是基于继承的。
1、虚函数
使用父类的指针实例化子类的对象时,要在父类和子类的成员函数前使用 virtual 关键字修饰,这样才能使得使用父类调用子类同名成员函数时返回的是子类的成员函数,从而实现多态。
2、虚析构函数
当使用父类的指针指向子类的对象时,对象在销毁时由于只执行了父类的析构函数,所以会造成内存泄漏,解决方法是使用 virtual 关键字修饰父类和子类的析构函数
3、virtual的使用限制
- 普通函数不能使用虚函数,否则会出现编译错误。虚函数必须是某个类的成员函数。
- 静态成员函数不能是虚函数,否则会出现编译错误。因为静态成员函数(使用static修饰)不属于任何一个对象,它与类同生共死。
- 内联函数不能是虚函数。当内联函数使用virtual修饰时,系统会自动忽略inline关键字,而把该函数完全当成是虚函数。
- 构造函数不能是虚函数,否则会出现编译错误。那么为什么呢?因为虚函数采用一种虚调用的方法,虚调用是一种在部分信息已知的情况下工作你的机制,是当我们只知道接口不知道准确对象类型时使用的,但是构造函数一旦创建对象,对象的类型就是已知的,所以构造函数不能是虚函数。
4、覆盖
若子类中未定义与父类同名的虚函数,那么在使用父类的指针实例化子类的对象时,子类的虚函数表中存放的是父类的虚函数入口地址;但是,若子类中定义了与父类同名的虚函数,那么在子类的虚函数表中存放的便是子类自身的虚函数的入口地址,这种情况成为覆盖。
5、纯虚函数
由于在C++中关于虚继承、虚函数、纯虚函数的概念容易混淆,在此给出纯虚函数的定义:纯虚函数无函数体,函数名=0,如下:
virtual void fun() = 0;
纯虚函数是某个类的成员函数,包含纯虚函数的类称为抽象类,只包含纯虚函数而不包含其他任何数据成员及成员函数的类称为接口类,接口类通常用于表达一种能力或者协议。