C++——面向对象
三大特性
继承:让某种类型对象获得另⼀个类型对象的属性和⽅法,它可以使⽤现有类的所有功能,并在⽆需重新编写原来的类的情况下对这些功能进⾏扩展
封装:把客观事物封装成抽象的类,并且类可以把⾃⼰的数据和⽅法只让可信的类或者对象操作,对不可信的进⾏信息隐 藏
多态:同⼀事物表现出不同事物的能⼒,即向不同对象发送同⼀消息,不同的对象在接收时会产⽣不同的⾏为(重载实现 编译时多态,虚函数实现运⾏时多态)
实现多态有两种⽅式
- 重写(override): 是指⼦类重新定义⽗类的虚函数的做法
- 重载(overload): 是指允许存在多个同名函数,⽽这些函数的参数表不同(或许参数个数不同,或许参数 类型不同,或许两者都不同)
- public 修饰符⽤于指定类中的成员可以被类的外部代码访问。公有成员可以被类外部的任何代码(包括类的实 例)访问。
- private 修饰符⽤于指定类中的成员只能被类的内部代码访问。私有成员对外部代码是不可⻅的,只有类内部的 成员函数可以访问私有成员。
- protected 修饰符⽤于指定类中的成员可以被类的派⽣类访问。受保护成员对外部代码是不可⻅的,但可以在派 ⽣类中被访问。
重载与重写
- 重载:重载是指在同⼀作⽤域内,使⽤相同的函数名但具有不同的参数列表或类型,使得同⼀个函数名可以有多个版 本。
- 重写:指派⽣类(⼦类)重新实现(覆盖)基类(⽗类)中的虚函数,以提供特定于派⽣类的实现。重写是⾯ 向对象编程中的多态性的⼀种体现,主要涉及基类和派⽣类之间的关系,⽤于实现运⾏时多态。
1、override是重写(覆盖)了⼀个⽅法
以实现不同的功能,⼀般是⽤于⼦类在继承⽗类时,重写⽗类⽅法。
规则:
- 重写⽅法的参数列表,返回值,所抛出的异常与被重写⽅法⼀致
- 被重写的⽅法不能为private
- 静态⽅法不能被重写为⾮静态的⽅法
- 重写⽅法的访问修饰符⼀定要⼤于被重写⽅法的访问修饰(public>protected>default>private)
2、overload是重载,这些⽅法的名称相同⽽参数形式不同
⼀个⽅法有不同的版本,存在于⼀个类中。
规则:
- 不能通过访问权限、返回类型、抛出的异常进⾏重载
- 不同的参数类型可以是不同的参数类型,不同的参数个数,不同的参数顺序(参数类型必须不⼀样)
- ⽅法的异常类型和数⽬不会对重载造成影响
使⽤多态是为了避免在⽗类⾥⼤量重载引起代码臃肿且难于维护。
重写与重载的本质区别是,加⼊了override的修饰符的⽅法,此⽅法始终只有⼀个被你使⽤的⽅法。
多态如何实现
C++中的多态性是通过虚函数(virtual function)和虚函数表(vtable)来实现的。多态性允许在基类类型的指针 或引⽤上调⽤派⽣类对象的函数,以便在运⾏时选择正确的函数实现。
- 基类声明虚函数:在基类中声明虚函数,使⽤ virtual 关键字,以便派⽣类可以重写(override)这些函数。
class Shape { public:virtual void draw() const {// 基类的默认实现} };
- 派⽣类重写虚函数:在派⽣类中重写基类中声明的虚函数,使⽤override 关键字
class Circle : public Shape { public:void draw() const override {// 派⽣类的实现} };
- 使⽤基类类型的指针或引⽤指向派⽣类对象。
Shape* shapePtr = new Circle();
- 调⽤虚函数:通过基类指针或引⽤调⽤虚函数。在运⾏时,系统会根据对象的实际类型来选择调⽤正确的函数 实现。
shapePtr->draw(); // 调⽤的是 Circle 类的 draw() 函数
- 虚函数表:编译器在对象的内存布局中维护了⼀个虚函数表,其中存储了指向实际函数的指针。这个表在运⾏ 时⽤于动态查找调⽤的函数。
虚函数和虚函数表
1. 虚函数
C++中的虚函数的作⽤主要是实现了多态的机制。虚函数允许在派⽣类中重新定义基类中定义的函数,使得通过基 类指针或引⽤调⽤的函数在运⾏时根据实际对象类型来确定。这样的机制被称为动态绑定或运⾏时多态。
在基类中,通过在函数声明前⾯加上 virtual 关键字,可以将其声明为虚函数。派⽣类可以重新定义虚函数,如 果派⽣类不重新定义,则会使⽤基类中的实现。
class Base {
public:virtual void virtualFunction() {// 虚函数的实现}
};
class Derived : public Base {
public:void virtualFunction() override {// 派⽣类中对虚函数的重新定义}
};
2. 虚函数表
虚函数的实现通常依赖于⼀个被称为虚函数表(虚表)的数据结构。每个类(包括抽象类)都有⼀个虚表,其中包 含了该类的虚函数的地址。每个对象都包含⼀个指向其类的虚表的指针,这个指针被称为虚指针(vptr)。
当调⽤⼀个虚函数时,编译器会使⽤对象的虚指针查找虚表,并通过虚表中的函数地址来执⾏相应的虚函数。这就 是为什么在运⾏时可以根据实际对象类型来确定调⽤哪个函数的原因。
3. 纯虚函数
纯虚函数是在抽象类中声明的虚函数,它没有具体的实现,只有函数的声明。通过在函数声明的末尾使⽤ 可以将虚函数声明为纯虚函数。派⽣类必须实现抽象类中的纯虚函数,否则它们也会成为抽象类。
纯虚函数没有函数体,只有函数声明,即没有提供默认的实现。派⽣类必须提供纯虚函数的具体实现,否则它们也会成为抽象类。包含纯虚函数的类⽆法被实例化,只能⽤于派⽣其他类。纯虚函数使⽤ = 0 在函数声明末尾进⾏声明。通过纯虚函数,抽象类提供⼀种接⼝规范,要求派⽣类提供相关实现。
class AbstractBase {
public:// 纯虚函数,没有具体实现virtual void pureVirtualFunction() = 0;// 普通成员函数可以有具体实现void commonFunction() {// 具体实现}
};
4. 抽象类
抽象类是不能被实例化的类,它存在的主要⽬的是为了提供⼀个接⼝,供派⽣类继承和实现。抽象类中可以包含普 通的成员函数、数据成员和构造函数,但它必须包含⾄少⼀个纯虚函数。即在声明中使⽤ 函数⼀个 = 0 的纯虚函数。
5. 虚析构函数
虚析构函数是⼀个带有 virtual 关键字的析构函数。 主要作⽤是确保在通过基类指针删除派⽣类对象时,能够正确调⽤派⽣类的析构函数,从⽽释放对象所占⽤的资源。
通常,如果⼀个类可能被继承,且在其派⽣类中有可能使⽤该基类的析构函数应该声明为虚析构函数。
class Base {
public:// 虚析构函数virtual ~Base() {// 基类析构函数的实现}
};class Derived : public Base {
public:// 派⽣类析构函数,可以覆盖基类的虚析构函数~Derived() override {// 派⽣类析构函数的实现}
};
6. 为什么没有虚构造函数
构造函数在对象的创建阶段被调⽤,对象的类型在构造函数中已经确定。因此,构造函数调⽤不涉及多态性,也就 是说,在对象的构造期间⽆法实现动态绑定。虚构造函数没有意义,因为对象的类型在构造过程中就已经确定,不 需要动态地选择构造函数。
不能被声明为虚函数的函数:
- 构造函数:
- 构造函数在对象的创建期间调⽤,对象的类型在构造期间已经确定。因此,构造函数不能是虚函数,因为虚函数的 动态绑定是在运⾏时实现的,⽽构造函数在对象还未创建完全时就会被调⽤。
- 普通函数
- 普通函数(⾮成员函数)只能被overload,不能被override,声明为虚函数也没有什么意思,因此编译器会在编译 时绑定函数。
- 静态成员函数
- 静态成员函数对于每个类来说只有⼀份代码,所有的对象都共享这⼀份代码,他也没有要动态绑定的必要性。
- 友元函数
- 因为C++不⽀持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。
- 内联成员函数
- 内联函数就是为了在代码中直接展开,减少函数调⽤花费的代价,虚函数是为了在继承后对象能够准确的执⾏⾃⼰ 的动作,这是不可能统⼀的。(再说了,inline函数在编译时被展开,虚函数在运⾏时才能动态的绑定函数) 内联函数是在编译时期展开,⽽虚函数的特性是运⾏时才动态联编,所以两者⽭盾,不能定义内联函数为虚函
成员函数、成员变量、静态成员函数、静态成员变量
1. 成员函数
- 成员函数是属于类的函数,它们可以访问类的成员变量和其他成员函数。
- 成员函数可以分为普通成员函数和静态成员函数。
- 普通成员函数使⽤对象调⽤,可以访问对象的成员变量。
- 普通成员函数的声明和定义通常在类的内部,但定义时需要使⽤类名作为限定符。
2. 成员变量
- 成员变量是属于类的变量,存储在类的每个对象中。
- 每个对象拥有⼀份成员变量的副本,它们在对象创建时分配,并在对象销毁时释放。
- 成员变量的访问权限可以是 public 、 private 或 protected 。
3. 静态成员函数
- 静态成员函数属于类⽽不是对象,因此可以直接通过类名调⽤,⽽不需要创建类的实例。
- 静态成员函数不能直接访问普通成员变量,因为它们没有隐含的 this 指针。
- 静态成员函数的声明和定义也通常在类的内部,但在定义时需要使⽤类名作为限定符。
4. 静态成员变量
- 静态成员变量是属于类⽽不是对象的变量,它们在所有对象之间共享。
- 静态成员变量通常在类的声明中进⾏声明,但在类的定义外进⾏定义和初始化。
- 静态成员变量可以通过类名或对象访问。
构造函数、析构函数
1. 构造函数
构造函数是在创建对象时⾃动调⽤的特殊成员函数。它的主要⽬的是初始化对象的成员变量,为对象分配资源,执⾏必要的初始化操作。
构造函数的特点包括:
- 函数名与类名相同: 构造函数的函数名必须与类名相同,且没有返回类型,包括 void。
- 可以有多个构造函数: ⼀个类可以有多个构造函数,它们可以根据参数的类型和数量不同⽽重载。
- 默认构造函数: 如果没有为类定义任何构造函数,编译器会⾃动⽣成⼀个默认构造函数。默认构造函数没有 参数,也可能执⾏⼀些默认的初始化操作。
2. 析构函数
析构函数是在对象⽣命周期结束时⾃动调⽤的特殊成员函数。它的主要⽬的是释放对象占⽤的资源、执⾏必要的清 理操作。析构函数的特点包括:
- 函数名与类名相同,前⾯加上波浪号 ~: 析构函数的函数名为 ~ClassName ,其中 ClassName 是类名。
- 没有参数: 析构函数没有参数,不能重载,每个类只能有⼀个析构函数。
- 默认析构函数: 如果没有为类定义任何析构函数,编译器会⾃动⽣成⼀个默认析构函数,执⾏简单的清理操 作。
构造函数种类
1. 默认构造函数:没有任何参数的构造函数。如果⽤户没有为类定义构造函数,编译器会⾃动⽣成⼀个默认构造函数。默认构造函数⽤于创建对象时的初始化,当⽤户不提供初始化值时,编译器将调⽤默认构造函数。
class MyClass {
public:// 默认构造函数MyClass() {// 初始化操作}
};
2. 带参数的构造函数:接受⼀个或多个参数,⽤于在创建对象时传递初始化值。可以定义多个带参数的构造函 数,以⽀持不同的初始化⽅式。
class MyClass {
public:
// 带参数的构造函数MyClass(int value) {// 根据参数进⾏初始化操作}
};
3. 拷贝构造函数:⽤于通过已存在的对象创建⼀个新对象,新对象是原对象的副本。参数通常是对同类型对象的 引⽤。
class MyClass {
public:// 拷⻉构造函数MyClass(const MyClass &other) {// 进⾏深拷⻉或浅拷⻉,根据实际情况}
};
4.委托构造函数:在⼀个构造函数中调⽤同类的另⼀个构造函数,减少代码重复。通过成员初始化列表或构造函 数体内部调⽤其他构造函数。
class MyClass {
public:// 委托构造函数MyClass() : MyClass(42) {// 委托给带参数的构造函数}MyClass(int value) {// 进⾏初始化操作}
};
深拷贝与浅拷贝
主要区别在于如何处理对象内部的动态分配的资源。
1. 深拷⻉
深拷⻉是对对象的完全独⽴复制,包括对象内部动态分配的资源。
在深拷⻉中,不仅复制对象的值,还会复制对象 所指向的堆上的数据。
主要特点:
- 复制对象及其所有成员变量的值。
- 动态分配的资源也会被复制,新对象拥有⾃⼰的⼀份资源副本。
- 深拷⻉通常涉及到⼿动分配内存,并在拷⻉构造函数或赋值操作符中进⾏资源的复制。
#include <cstring>class MyClass {
priavte:char* data;
public:MyClass(const char* inputData) {data = new char[std::strlen(inputData + 1];std::strcpy(data, inputData);}// 深拷贝构造函数MyClass(const MyClass& other) {data = new char[std::strlen(other.data) + 1];std::strcpy(data, other.data);}// 另外一种写法,用赋值操作符重载(operator=)MyCLass& operator = (const MyClass& other) {if(this != &other) {delete[] data;data = new char[std::strlen(other.data) + 1];std::strcpy(data, other.data);}return *this;}~MyClass() {delete[] data;}void printData() {std::cout << data << std::endl;}
};int main() {MyClass obj1("Hello");MyClass obj2 = obj1;obj1.printData();obj2.printData();return 0;
}
2. 浅拷⻉
浅拷⻉仅复制对象的值,⽽不涉及对象内部动态分配的资源。在浅拷⻉中,新对象和原对象共享相同的资源,⽽不 是复制⼀份新的资源。
主要特点:
- 复制对象及其所有成员变量的值。
- 对象内部动态分配的资源不会被复制,新对象和原对象共享同⼀份资源。
- 浅拷⻉通常使⽤默认的拷⻉构造函数和赋值操作符,因为它们会逐成员地复制原对象的值。
运算符重载
重载运算符函数,本质还是函数调⽤
通过运算符重载,我们可以定义如何使用运算符来操作你的自定义类型。