面向对象的三大特性
封装
封装是将数据和操作这些数据的函数(方法)组合在一个类中的过程。
封装的主要目的是隐藏类的内部实现细节,仅暴露必要的接口给外部,通过控制类成员的访问级别,可以限制对类内部数据的直接访问,确保数据的完整性和安全性。
继承
继承是一个类(派生类)从另一个类(基类)哪里获得其属性和方法的过程。
C++中 public继承表示派生类 is-a (是一个)基类。
pricate继承表示派生类 包含(has a)基类。
多态*
定义:同一事物表现出不同事物的能力,即向不同对象发送同一消息,不同的对象在接受时会产生不同的行为(重载实现编译时多态,虚函数实现运行时多态)
功能:多态允许不同类的对象使用相同的接口名字,但具有不同实现的特性,允许将子类类型的指针赋值给父类类型的指针,
实现多态有两种方法:
1. 覆盖(override):是指子类重新定义父类的虚函数的做法
2. 重载(overload):是指允许存在多个同名函数,而这些函数的参数表不同,或许参数个数不同,或许参数类型不同,或许两者都不同
重载overload
重载是指相同作用域(比如命名空间或者同一个类)内拥有相同的方法名,但具有不同的参数类型和/或参数数量的方法。 重载允许根据所提供的参数不同来调用不同的函数。它主要在以下情况下使用:
方法具有相同的名称。
方法具有不同的参数类型或参数数量。
返回类型可以相同或不同。
同一作用域,比如都是一个类的成员函数,或者都是全局函数。
重写overriding
重写是指在派生类中重新定义基类中的方法。
当派生类需要改变或扩展基类方法的功能时,就需要用到重写。
重写的条件包括:
方法具有相同的名称。
方法具有相同的参数类型和数量。
方法具有相同的返回类型。
重写的基类中被重写的函数必须有virtual修饰。
重写主要在继承关系的类之间发生。
隐藏Hiding
隐藏是指派生类的函数屏蔽了与其同名的基类函数。注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。
类对象的初始化和析构顺序
初始化顺序
在C++中,类对象的初始化顺序遵循以下规则:
1. 基类初始化顺序:如果当前类继承自一个或多个基类,它们将按照声明顺序进学校初始化,但是在有虚继承和一般继承存在的情况下,优先虚继承。
2. 成员变量初始化顺序:类的成员变量按照它们在类定义中的声明顺序进行初始化
3. 执行构造函数
类的析构顺序
类的析构顺序和构造顺序完全相反
多态的实现方式:虚函数、纯虚函数和模板函数
虚函数、纯虚函数实现多态
虚函数是指在基类中声明的函数,它在派生类中可以被重写。
当我们使用基类指针或引用指向派生类对象时,通过虚函数的机制,可以调用到派生类中重写的函数,从而实现多态。
因此C++的多态必须满足两个条件:
1. 必须通过基类的指针或者引用调用虚函数
2. 被调用的函数是虚函数,且必须完成对基类虚函数的重写
虚函数表
虚函数是通过一张虚函数表来实现的,在这个表中,存放的是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,确定其调用的函数。
当一个类中的成员函数有虚函数时,这个类的实例内存中都有一个虚函数表的指针(vptr),虚函数表指针指向一个数组,该数组的元素就是各个虚函数的地址,通过函数的索引,我们就能直接访问对应的虚函数。
对于派生类,其虚函数表通常是在基类的虚函数表的基础上扩展而来的,当我们在子类中重写一个父类的虚函数时,在子类的虚函数表中用子类重写的函数地址去取代父类的函数地址。当调用一个虚函数时,编译器会通过对象的虚函数指针查找到该对象所属的类的虚函数表,并根据函数的索引值来找到对应的虚函数地址,然后将控制转移到该地址,实际执行该函数的代码。
所以,当我们用父类的指针来操作有一个子类的时候,程序会根据子类的续函数表调用所指的函数。
静态绑定:当我们使用派生类的指针去访问虚函数时,实际上并未发生多态,因为编译时就能够确定对象类型为派生类型,然后直接生成调用派生类虚函数的代码即可。
动态绑定:通过基类的指针或引用调用虚函数才能构成多态,因为这种情况下运行时才能确定对象的实际类型。
纯续函数
纯虚函数是一种在基类中声明但没有实现的虚函数。它的作用是定义了一种接口,这个接口需要由派生类来实现
包含纯虚函数的类被称为抽象类:抽象类仅仅提供了一些接口,但是没有实现具体的功能,作用就是制定各种接口,通过派生类来实现不同的功能,从而实现代码的复用和可扩展性。
为什么基类析构函数需要是虚函数
析构函数是进行类的清理工作,比如释放内容、关闭文件等。
为了实现多态,C++可以通过基类的指针或引用访问派生类的成员
那么,如果一个基类的析构函数不是虚函数时,则该类的虚函数表中就不会有这个析构函数的地址,当我们通过基类的指针来访问派生类且结束时,派生类的析构函数不会被调用,只会调用基类的析构函数,因为这里并没有使用到多态(析构函数不在虚函数表中),这可能会导致资源泄露。
例如:
Class Base1{
public:
~Base(){ cout << "Base1 destructor" <<endl;}
}
Class derived1 : public Base1{
public:
~Derived(){ cout << "Derived1 destructor" << endl;}
}
Class Base2{
public:
virtual ~Base(){ cout << "Base2 destructor" <<endl;}
}
Class derived2 : public Base2{
public:
~Derived(){ cout << "Derived2 destructor" << endl;}
}
int main()
{
Base1 *ptr1 = new Derived1();
delete ptr1;
Base1 *ptr2 = new Derived2();
delete ptr2;
return 0;
}
该程序的运行结果为:
Base1 destructor
Derived2 destructor
Base2 destructor
模板函数多态
模板函数可以根据传递参数的不同类型,自动生成相应类型的函数代码,模板函数可以用来实现多态,例如
template <class T>
T max(T a, T b){return (a>b? a:b)}
int main()
{
int i=1, j = 2;
char a = 's', b = 'w';
cout << max(i,j) << max(a,b) << endl;
return 0;
}
为什么C++的成员模板函数不能是virtual
因为C++的编译与链接模型是分离的
1. 从Unix/C开始,一个C/C++程序就可以被分开编译,然后用一个linker链接起来。这种模型有一个问题,就是各个编译单元可能对另一个编译单元一无所知。
2. 一个function template 最后到底会被instantiate为多少个函数,要等整个程序(所有编译单元)全部被编译完成才知。
3. virtual function的实现利用了虚函数表,这种实现中,一个类的内存布局需要在这个类编译完成的时候就被完全确定。
所以当一个虚函数是模板函数时,编译器在编译时无法为其生成一个确定的虚函数表条目,因为模板函数可以有无数个实例。但是编译时无法确定需要调用那个特定的模板实例。因此,C++规定成员函数不能既是模板函数又是虚函数。