1. 三大特性
1.1 封装
把客观事物封装成抽象的类,并且类可以把⾃⼰的数据和⽅法只让可信的类或者对象操作,对不可信的进⾏信息隐藏
1.2 继承
它可以使⽤现有类的所有功能,并在⽆需重新编写原来的类的情况下对这些功能进⾏扩展 常见继承的三种方式: 实现继承:指使⽤基类的属性和⽅法⽽⽆需额外编码 接⼝继承:指仅使⽤属性和⽅法的名称、但是⼦类必须提供实现 可视继承:指⼦窗体(类)使⽤基窗体(类)的外观和实现代码
1.3 多态
允许将⼦类类型的指针赋值给⽗类类型的指针 实现多态的两种方式: 覆盖(override): 是指⼦类重新定义⽗类的虚函数的做法 重载(overload): 是指允许存在多个同名函数,⽽这些函数的参数表不同
2. 访问权限问题
在类的内部(定义类的代码内部),⽆论成员被声明为 public、protected 还是 private,都是可以互相访问的, 没有访问权限的限制。 在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员。 ⽆论共有继承、私有和保护继承,私有成员不能被“派⽣类”访问,基类中的共有和保护成员能被“派⽣类”访问。 对于共有继承,只有基类中的共有成员能被“派⽣类对象”访问,保护和私有成员不能被“派⽣类对象”访问。对于私有和保护继承,基类中的所有成员不能被“派⽣类对象”访问。
3. 成员函数/成员变量/静态成员函数/静态成员变量的区别
成员函数
成员函数是属于类的函数,它们可以访问类的成员变量和其他成员函数。 成员函数可以分为普通成员函数和静态成员函数。 普通成员函数使⽤对象调⽤,可以访问对象的成员变量。 普通成员函数的声明和定义通常在类的内部,但定义时需要使⽤类名作为限定符
成员变量
成员变量是属于类的变量,存储在类的每个对象中。 每个对象拥有⼀份成员变量的副本,它们在对象创建时分配,并在对象销毁时释放。 成员变量的访问权限可以是 public 、 private 或 protected
静态成员函数
静态成员函数属于类⽽不是对象,因此可以直接通过类名调⽤,⽽不需要创建类的实例。 静态成员函数不能直接访问普通成员变量,因为它们没有隐含的 this 指针。 静态成员函数的声明和定义也通常在类的内部,但在定义时需要使⽤类名作为限定符。
静态成员变量
静态成员变量是属于类⽽不是对象的变量,它们在所有对象之间共享。 静态成员变量通常在类的声明中进⾏声明,但在类的定义外进⾏定义和初始化。 静态成员变量可以通过类名或对象访问。
4. 构造函数
默认构造函数 带参数的构造函数 拷贝构造函数 移动构造函数 委托构造函数:在⼀个构造函数中调⽤同类的另⼀个构造函数,减少代码重复
MyClass(int value){ ... }
MyClass() : MyClass(42){ ... }
5. 深拷贝和浅拷贝的区别
主要区别在于如何处理对象内部动态分配的资源 深拷贝: 对对象的完全独⽴复制,包括对象内部动态分配的资源。在深拷⻉中,不仅复制对象的值,还会复制对象所指向的堆上的数据。 特点: 复制对象及其所有成员变量的值; 动态分配的资源也会被复制,新对象拥有⾃⼰的⼀份资源副本。 深拷⻉通常涉及到⼿动分配内存,并在拷⻉构造函数或赋值操作符中进⾏资源的复制。
class DeepCopy{
public:int *data;DeepCopy(const DeepCopy &other){data = new int(*(other.data));}~DeepCopy(){delete data;}DeepCopy& operator = (const DeepCopy &other){if(this != &other){delete data;data = new int (*(other.data));}return *this;}
};
浅拷贝: 仅复制对象的值,⽽不涉及对象内部动态分配的资源。在浅拷⻉中,新对象和原对象共享相同的资源 特点: 复制对象及其所有成员变量的值。 对象内部动态分配的资源不会被复制,新对象和原对象共享同⼀份资源。 浅拷⻉通常使⽤默认的拷⻉构造函数和赋值操作符,因为它们会逐成员地复制原对象的值。
6. 什么是多重继承?
⼀个类可以从多个基类(⽗类)继承属性和⾏为,⼀个派⽣类可以同时拥有多个基类 多重继承可能引⼊⼀些问题,如菱形继承问题 当⼀个类同时继承了两个拥有相同基类的类,⽽最终的派⽣类⼜同时继承了这两个类时, 可能导致⼆义性和代码设计上的复杂性 C++ 提供了虚继承, 通过在继承声明中使⽤ virtual 关键字,可以避免在派⽣类中⽣成多个基类的实例,从⽽解决了菱形继承带来的⼆义性
7. C++ 的重载和重写的区别
重载是指在同⼀作⽤域内,使⽤相同的函数名但具有不同的参数列表或类型,使得同⼀个函数名可以有多个版本 重写是指派⽣类(⼦类)重新实现(覆盖)基类(⽗类)中的虚函数,以提供特定于派⽣类的实现。 重写⽅法的参数列表,返回值,所抛出的异常与被重写⽅法⼀致 被重写的⽅法不能为private 静态⽅法不能被重写为⾮静态的⽅法 重写⽅法的访问修饰符⼀定要⼤于被重写⽅法的访问修饰符(public>protected>default>private)
8. C++多态如何实现
多态字面意思为多种状态。在面向对象语言中,一个接口,多种实现即为多态。C++中的多态性具体体现在编译和运行两个阶段。编译时多态是静态多态,在编译时就可以确定使用的接口。运行时多态是动态多态,具体引用的接口在运行时才能确定。 静态多态和动态多态的区别其实只是在什么时候将函数实现和函数调用关联起来,是在编译时期还是运行时期,即函数地址是早绑定还是晚绑定的。 静态多态是指在编译期间就可以确定函数的调用地址,并生产代码,这就是静态的,也就是说地址是早绑定; 动态多态则是指函数调用的地址不能在编译器期间确定,需要在运行时确定,属于晚绑定。 动态多态是通过虚函数(virtual function)和虚函数表(vtable)来实现的。多态性允许在基类类型的指针或引⽤上调⽤派⽣类对象的函数,以便在运⾏时选择正确的函数实现 在基类中声明虚函数,使⽤ virtual 关键字,以便派⽣类可以重写(override)这些函数 在派⽣类中重写基类中声明的虚函数,使⽤ override 关键字 使⽤基类类型的指针或引⽤指向派⽣类对象 通过基类指针或引⽤调⽤虚函数。在运⾏时,系统会根据对象的实际类型来选择调⽤正确的函数实现 编译器在对象的内存布局中维护了⼀个虚函数表,其中存储了指向实际函数的指针。这个表在运⾏时⽤于动态查找调⽤的函数。(虚函数表指针在32为下为4字节)
class Shape{
public:virtual void draw() const{ ... }
};class Circle : public Shape{
public:void draw() const override{ ... }
};int main(){Shape* shapePtr = new Circle();shapePtr->draw(); // 调⽤的是 Circle 类的 draw() 函数
}
9. 虚函数和虚函数表
虚函数: 虚函数的作⽤主要是实现了多态的机制。虚函数允许在派⽣类中重新定义基类中定义的函数,使得通过基 类指针或引⽤调⽤的函数在运⾏时根据实际对象类型来确定。这样的机制被称为动态绑定或运⾏时多态。 虚函数表: 虚函数的实现通常依赖于⼀个被称为虚函数表(虚表)的数据结构。每个类(包括抽象类)都有⼀个虚表,其中包含了该类的虚函数的地址。每个对象都包含⼀个指向其类的虚表的指针,这个指针被称为虚指针(vptr) 当调⽤⼀个虚函数时,编译器会使⽤对象的虚指针查找虚表,并通过虚表中的函数地址来执⾏相应的虚函数。这就是为什么在运⾏时可以根据实际对象类型来确定调⽤哪个函数的原因。
10. 虚函数和纯虚函数
虚函数: 有实现: 虚函数有函数声明和实现,即在基类中可以提供默认实现。 可选实现: 派⽣类可以选择是否覆盖虚函数。如果派⽣类没有提供实现,将使⽤基类的默认实现。 允许实例化: 虚函数的类可以被实例化。即你可以创建⼀个虚函数的类的对象。 调⽤靠对象类型决定: 在运⾏时,根据对象的实际类型来决定调⽤哪个版本的虚函数。 ⽤ virtual 关键字声明: 虚函数使⽤ virtual 关键字声明,但不包含 = 0 。 纯虚函数: 没有实现: 纯虚函数没有函数体,只有函数声明,即没有提供默认的实现。 强制覆盖: 派⽣类必须提供纯虚函数的具体实现,否则它们也会成为抽象类。 禁⽌实例化: 包含纯虚函数的类⽆法被实例化,只能⽤于派⽣其他类。 ⽤ = 0 声明: 纯虚函数使⽤ = 0 在函数声明末尾进⾏声明。 为接⼝提供规范: 通过纯虚函数,抽象类提供⼀种接⼝规范,要求派⽣类提供相关实现。
11. 抽象类和纯虚函数
抽象类是不能被实例化的类,它存在的主要⽬的是为了提供⼀个接⼝,供派⽣类继承和实现。 抽象类中可以包含普通的成员函数、数据成员和构造函数,但它必须包含⾄少⼀个纯虚函数。即在声明中使⽤ virtual 关键字并赋予函数⼀个 = 0 的纯虚函数。 纯虚函数是在抽象类中声明的虚函数,它没有具体的实现,只有函数的声明。 通过在函数声明的末尾使⽤ = 0 , 可以将虚函数声明为纯虚函数。派⽣类必须实现抽象类中的纯虚函数,否则它们也会成为抽象类。
12. 虚析构函数(虚构造函数没有意义)
确保在通过基类指针删除派⽣类对象时,能够正确调⽤派⽣类的析构函数,从⽽释放对象所占⽤的资源。 如果基类的析构函数不是虚的,当通过基类指针删除指向派⽣类对象的对象时,只会调⽤基类的析构函数,⽽不会调⽤派⽣类的析构函数。这可能导致派⽣类的资源未被正确释放,造成内存泄漏。 如果⼀个类可能被继承,且在其派⽣类中有可能使⽤ delete 运算符来删除通过基类指针指向的对象,那么该基类的析构函数应该声明为虚析构函数 虚构造函数没有意义: 从存储空间角度来看: 虚函数对应一个vtable,可是这个vtable其实是存储在对象的内存空间的。如果构造函数是虚的,就需要通过vtable来调用,可是对象还没有实例化,也就是内存空间还没有,就无法找到vtable 从多态角度来看:构造函数不被继承;构建派生类的时候,也会构建基类;所以虚构造不会构成多态 从使用角度来看:创建一个对象时我们总是要明确指定对象的类型,如果是虚构造,编译器就不会知道要构建哪个类型
13. 哪些函数不能被声明为虚函数
构造函数:构造函数不涉及多态性,在对象构造期间无法实现动态绑定 普通函数:只能被overload,不能被override 静态成员函数:所有的对象都共享这⼀份代码,没有动态绑定的必要性 友元函数:C++不⽀持友元函数的继承 内联成员函数:inline函数在编译时被展开,虚函数在运⾏时才能动态的绑定函数