文章目录
- [c++ 基础](#c++ 基础)
-
- [1. C++ 中的 final](#1. C++ 中的 final)
- [2. 虚函数和纯虚函数](#2. 虚函数和纯虚函数)
-
- [2.1 虚函数(Virtual Function)](#2.1 虚函数(Virtual Function))
- [2.1 纯虚函数(Pure Virtual Function)](#2.1 纯虚函数(Pure Virtual Function))
- [2.3 虚函数和纯虚函数的区别](#2.3 虚函数和纯虚函数的区别)
-
- [2.4 总结](#2.4 总结)
- [3. 抽象类](#3. 抽象类)
-
- [3.1 抽象类的定义](#3.1 抽象类的定义)
- [3.2 抽象类的特点](#3.2 抽象类的特点)
- [3.3 抽象类的用途](#3.3 抽象类的用途)
- [3.4 抽象类可以有非虚函数。](#3.4 抽象类可以有非虚函数。)
- [3.4 抽象类的总结](#3.4 抽象类的总结)
- [4. 非虚函数、虚函数和重写、重载、覆盖的关系?](#4. 非虚函数、虚函数和重写、重载、覆盖的关系?)
-
- [4.1 非虚函数](#4.1 非虚函数)
- [4.2. 虚函数](#4.2. 虚函数)
- [4.3. 重写(Override)](#4.3. 重写(Override))
- [4.4. 重载(Overload)](#4.4. 重载(Overload))
- [4.5. 覆盖(Hiding)](#4.5. 覆盖(Hiding))
- [4.6 总结](#4.6 总结)
- [5. 重载和覆盖似乎是一回事儿?](#5. 重载和覆盖似乎是一回事儿?)
c++ 基础
1. C++ 中的 final
在 C++ 中,final
是一个关键字,用于防止类或虚函数被进一步继承或重写。
用法
-
用于类 :如果你希望某个类不能被继承,你可以在类声明时使用
final
关键字。cppclass Base final { // 类定义 }; // 错误:无法继承标记为 final 的类 class Derived : public Base { };
在上面的例子中,
Base
类使用了final
关键字,因此任何试图继承它的行为都会导致编译错误。 -
用于虚函数 :你也可以使用
final
关键字来阻止某个虚函数在派生类中被重写。cppclass Base { public: virtual void foo() final { // 函数实现 } }; class Derived : public Base { // 错误:无法重写标记为 final 的函数 void foo() override { // 派生类的实现 } };
在这个例子中,
Base::foo()
函数被标记为final
,因此它不能在Derived
类中被重写。
适用场景
- 当你希望确保某个类不能被继承时,使用
final
关键字。 - 当你希望确保某个虚函数在派生类中不能被重写时,可以使用
final
来强制这一点。
final
的作用主要是为了增强代码的安全性和设计的一致性,避免不必要的继承和重写。
在 C++ 中,final
关键字只能用于修饰类和虚函数。具体来说:
- 修饰类:防止类被继承。
- 修饰虚函数:防止虚函数在派生类中被重写。
无法修饰其他实体
- 非虚函数 :
final
不能用于非虚函数,因为非虚函数本身就没有重写的概念。 - 成员变量 :
final
不能修饰成员变量。 - 普通函数或全局函数 :同样,
final
也不能修饰普通函数或全局函数。
扩展说明
final
关键字设计的主要目的就是为了控制类的继承关系以及虚函数的重写行为。
2. 虚函数和纯虚函数
在 C++ 中,虚函数和纯虚函数是实现多态性和抽象类的关键概念。它们之间有一些重要的区别。
2.1 虚函数(Virtual Function)
虚函数是在基类中声明并且可能会在派生类中被重写的函数。虚函数的主要目的是允许通过基类指针或引用调用派生类的函数实现,从而实现运行时的多态性。
定义:
cpp
class Base {
public:
virtual void display() {
std::cout << "Base display" << std::endl;
}
};
- 特性:虚函数在基类中可以有一个具体的实现。派生类可以选择重写该函数,也可以不重写。如果派生类没有重写虚函数,那么调用该函数时会使用基类的实现。
- 多态性:通过基类的指针或引用调用虚函数时,实际调用的是派生类中重写后的函数。
示例:
cpp
class Derived : public Base {
public:
void display() override {
std::cout << "Derived display" << std::endl;
}
};
Base* basePtr = new Derived();
basePtr->display(); // 输出: Derived display
在这个例子中,虽然 basePtr
是一个指向 Base
类型的指针,但由于 display()
是虚函数,实际调用的是 Derived
类中的 display()
函数,这就是多态的实现。
2.1 纯虚函数(Pure Virtual Function)
纯虚函数是一种特殊的虚函数,它在基类中没有具体的实现。纯虚函数的目的是让基类成为一个抽象类,强制派生类提供该函数的实现。
定义:
cpp
class Base {
public:
virtual void display() = 0; // 纯虚函数
};
- 特性:纯虚函数没有函数体,只有函数声明。在派生类中,必须提供该函数的具体实现,否则派生类也会变成抽象类,无法实例化。
- 抽象类:包含一个或多个纯虚函数的类被称为抽象类,抽象类不能被实例化。其主要目的是作为接口或基类,供其他类继承并实现特定的功能。
示例:
cpp
class Derived : public Base {
public:
void display() override {
std::cout << "Derived display" << std::endl;
}
};
Base* basePtr = new Derived();
basePtr->display(); // 输出: Derived display
在这个例子中,Base
类是抽象类,因为它包含了一个纯虚函数 display()
。Derived
类必须重写这个函数才能实例化。
2.3 虚函数和纯虚函数的区别
- 实现 :
- 虚函数在基类中可以有一个具体的实现,派生类可以选择是否重写它。
- 纯虚函数在基类中没有实现,派生类必须重写它,否则无法实例化派生类。
- 类的类型 :
- 包含虚函数的类不一定是抽象类,可以被实例化。
- 包含纯虚函数的类是抽象类,不能被实例化。
2.4 总结
- 虚函数:允许在基类中提供默认实现,派生类可以重写也可以不重写,虚函数支持多态。
- 纯虚函数:没有实现,派生类必须重写纯虚函数,使基类成为抽象类,用于强制派生类实现某些功能。
纯虚函数常用于设计抽象接口,而虚函数用于实现多态性。
3. 抽象类
抽象类 是一种不能直接实例化的类,它通常用于作为基类,为派生类提供接口和基本的功能框架。抽象类的主要特点是包含至少一个纯虚函数(即函数声明后带有 = 0
),强制派生类必须实现这些函数。
3.1 抽象类的定义
抽象类至少包含一个纯虚函数。一个纯虚函数是没有定义函数体的虚函数,它的声明以 = 0
结尾。
示例:
cpp
class AbstractClass {
public:
virtual void pureVirtualFunction() = 0; // 纯虚函数
};
在这个例子中,AbstractClass
是一个抽象类,因为它包含了一个纯虚函数 pureVirtualFunction()
。这个类不能被实例化。
3.2 抽象类的特点
-
不能被实例化 :由于抽象类包含未实现的纯虚函数,因此它不能直接创建对象。
cppAbstractClass obj; // 错误:不能实例化抽象类
-
用于接口定义:抽象类通常作为接口,定义派生类必须实现的功能。例如,所有派生类都必须提供纯虚函数的具体实现。
-
可以包含普通成员函数 :抽象类可以包含非纯虚函数和成员变量。这些成员函数可以在派生类中被继承或重写,但类仍然是抽象类,只要它包含至少一个纯虚函数。
cppclass AbstractClass { public: virtual void pureVirtualFunction() = 0; // 纯虚函数 void regularFunction() { std::cout << "This is a regular function." << std::endl; } };
-
派生类实现纯虚函数 :派生类继承自抽象类时,必须提供所有纯虚函数的实现,否则派生类也将成为抽象类,无法实例化。
cppclass ConcreteClass : public AbstractClass { public: void pureVirtualFunction() override { std::cout << "Pure virtual function implemented." << std::endl; } }; ConcreteClass obj; // 可以实例化派生类
3.3 抽象类的用途
-
接口定义 :抽象类常被用来定义类的接口,确保所有派生类都实现特定的功能。例如,你可以定义一个包含各种形状(如
Circle
、Square
)的抽象类Shape
,并且让所有具体的形状类实现draw()
函数。cppclass Shape { public: virtual void draw() = 0; // 纯虚函数 }; class Circle : public Shape { public: void draw() override { std::cout << "Drawing a circle." << std::endl; } }; class Square : public Shape { public: void draw() override { std::cout << "Drawing a square." << std::endl; } };
-
代码重用:抽象类可以包含一些普通成员函数和成员变量,供派生类直接使用或重写,实现代码重用。
-
多态性:抽象类允许使用多态性,在程序中通过基类指针或引用操作派生类对象。
cppShape* shape = new Circle(); shape->draw(); // 输出: Drawing a circle.
3.4 抽象类可以有非虚函数。
虽然抽象类通常包含一个或多个纯虚函数,使其不能被实例化,但它仍然可以包含普通的非虚函数、成员变量、构造函数和析构函数等。这些非虚函数可以在派生类中直接使用或者被重写。
示例:
cpp
class AbstractClass {
public:
virtual void pureVirtualFunction() = 0; // 纯虚函数
void nonVirtualFunction() { // 非虚函数
std::cout << "This is a non-virtual function in the abstract class." << std::endl;
}
};
在这个例子中,AbstractClass
是一个抽象类,因为它包含了纯虚函数 pureVirtualFunction()
。但是它也包含了一个非虚函数 nonVirtualFunction()
。这个非虚函数在派生类中可以被直接使用,而不需要重写。
非虚函数的作用
-
提供默认行为:抽象类的非虚函数可以提供一些通用的默认行为,派生类可以直接继承这些行为而无需重写。
-
代码复用:非虚函数可以实现抽象类中的公共逻辑,这样可以避免在每个派生类中重复编写相同的代码,从而提高代码的复用性。
-
组合虚函数和非虚函数:你可以在抽象类中定义一些非虚函数来调用纯虚函数或虚函数,从而通过这种方式在抽象类中实现部分功能。
cppclass AbstractClass { public: virtual void pureVirtualFunction() = 0; void templateMethod() { // 非虚函数调用纯虚函数 std::cout << "Before pure virtual function" << std::endl; pureVirtualFunction(); // 派生类实现的函数将会被调用 std::cout << "After pure virtual function" << std::endl; } };
3.4 抽象类的总结
- 抽象类 是一种包含至少一个纯虚函数的类,不能被实例化。
- 它通常用来定义接口,迫使派生类实现特定的功能。
- 抽象类是面向对象编程中用来实现多态和接口统一的强大工具。
- 抽象类 可以包含非虚函数。这些非虚函数在派生类中可以直接使用。
- 非虚函数提供了一种方法,可以在抽象类中实现部分通用行为,帮助实现代码复用和默认行为。
- 非虚函数不会被派生类重写,除非派生类明确地重写它们。
4. 非虚函数、虚函数和重写、重载、覆盖的关系?
在 C++ 中,非虚函数、虚函数、重写(override)、重载(overload)和覆盖(覆盖)之间的关系涉及到函数的不同行为和应用场景。以下是对这些概念及其关系的详细说明:
4.1 非虚函数
非虚函数 是默认的普通成员函数,它们没有虚函数的动态多态性特性。非虚函数的行为是根据编译时的类型确定的,无论派生类是否定义了同名函数,调用时都会使用编译时已知的类型的函数。
特性:
- 非虚函数在基类和派生类中可以定义相同的名称和参数,但它们不会参与运行时多态。
- 函数调用是静态绑定 的,即在编译时决定。
示例:
cpp
class Base {
public:
void show() { std::cout << "Base show" << std::endl; }
};
class Derived : public Base {
public:
void show() { std::cout << "Derived show" << std::endl; }
};
Base* basePtr = new Derived();
basePtr->show(); // 输出: "Base show"
即使 basePtr
实际指向 Derived
,调用的仍然是 Base::show()
,因为 show()
不是虚函数。
4.2. 虚函数
虚函数 是使用 virtual
关键字声明的成员函数,它允许通过基类指针或引用调用派生类的实现。虚函数支持运行时的动态多态性,调用的是实际对象的类型对应的函数,而不是编译时已知类型的函数。
特性:
- 虚函数支持动态绑定(运行时绑定)。
- 派生类可以重写基类的虚函数,实现多态行为。
示例:
cpp
class Base {
public:
virtual void show() { std::cout << "Base show" << std::endl; }
};
class Derived : public Base {
public:
void show() override { std::cout << "Derived show" << std::endl; }
};
Base* basePtr = new Derived();
basePtr->show(); // 输出: "Derived show"
在这个例子中,通过基类指针调用虚函数 show()
,会调用派生类 Derived
中的实现,因为 show()
是虚函数。
4.3. 重写(Override)
重写(Override) 指的是在派生类中重新定义基类中的虚函数。重写的函数必须具有与基类中虚函数相同的函数签名(参数类型、参数数量、返回值类型等)。override
关键字可以显式标注重写行为,以防止函数签名不匹配的问题。
特性:
- 重写只适用于虚函数。
- 基类和派生类中的函数签名必须完全一致。
- 使用
override
关键字可以帮助编译器检查重写是否正确。
示例:
cpp
class Base {
public:
virtual void show() { std::cout << "Base show" << std::endl; }
};
class Derived : public Base {
public:
void show() override { std::cout << "Derived show" << std::endl; }
};
Derived::show()
重写了 Base::show()
,并且由于使用了 override
,编译器会检查函数签名是否与基类一致。
4.4. 重载(Overload)
重载(Overload) 指的是在同一个类
中定义多个同名函数
,但参数的数量或类型必须不同
。重载与虚函数无关,且函数的重载是在编译时决定的。
特性:
- 函数名称相同,但参数列表不同。
- 函数调用根据传递的参数来确定使用哪个函数。
- 重载发生在同一个作用域内,和多态无关。
示例:
cpp
class Base {
public:
void show() { std::cout << "Base show" << std::endl; }
void show(int a) { std::cout << "Base show with int: " << a << std::endl; }
};
这里的 show()
函数被重载,调用时会根据参数来选择对应的函数版本。
4.5. 覆盖(Hiding)
覆盖(Hiding) 是指派生类
中的非虚函数或虚函数隐藏了基类中具有相同名称的非虚函数或虚函数,但它们的函数签名不完全一致。例如,基类中有一个非虚函数或虚函数,派生类定义了一个具有相同名称但不同参数列表的函数,此时基类的函数就会被隐藏。
特性:
- 函数签名不同,函数在派生类中隐藏基类中的同名函数。
- 覆盖可以发生在非虚函数和虚函数之间。
- 使用
using
关键字可以显式地引入基类的被隐藏函数。
示例:
cpp
class Base {
public:
virtual void show(int a) { std::cout << "Base show with int: " << a << std::endl; }
};
class Derived : public Base {
public:
void show() { std::cout << "Derived show" << std::endl; }
};
Derived d;
d.show(); // 输出: "Derived show"
d.show(10); // 错误:`Base::show(int)` 被 `Derived::show()` 隐藏
在这个例子中,Derived::show()
隐藏了 Base::show(int)
。如果要使用 Base::show(int)
,可以在 Derived
类中使用 using
关键字显式地引入它。
4.6 总结
- 非虚函数:静态绑定,在编译时决定调用哪个函数。
- 虚函数:动态绑定,通过基类指针或引用在运行时调用实际对象的函数实现。
- 重写 :派生类中重定义基类的虚函数,函数签名必须完全一致,使用
override
关键字标注。 - 重载:同一作用域内,同名函数根据参数列表不同而具有多个版本,与虚函数无关。
- 覆盖:派生类中定义了与基类同名但不同参数的函数,隐藏了基类中的函数。
这些概念有助于理解函数行为在继承和多态中的不同表现形式。
5. 重载和覆盖似乎是一回事儿?
重载(Overloading) 和 覆盖(Hiding) 听起来可能类似,但它们在 C++ 中有着非常不同的含义和使用场景。两者的主要区别在于发生的范围、影响的函数、以及它们是如何处理函数调用的。
5.1 重载(Overloading)
重载 是在同一个类 中使用相同的函数名称 ,但使用不同的参数列表 。重载完全与继承无关,发生在编译时。它是静态多态性的一部分。
重载的特性:
- 只在同一个作用域内进行。
- 函数的名称相同,但参数的数量或类型必须不同。
- 重载函数根据调用时提供的参数类型和数量决定使用哪一个函数。
- 与继承和派生类无关。
示例:
cpp
class Example {
public:
void show() { std::cout << "No arguments" << std::endl; }
void show(int a) { std::cout << "One int argument: " << a << std::endl; }
};
在这里,show()
函数被重载。根据传递的参数类型和数量,编译器会选择相应的函数。
5.2 覆盖(Hiding)
覆盖 是发生在继承层次结构中的。它指的是派生类中定义的函数 (虚函数或非虚函数)隐藏了基类中具有相同名称的函数 ,但它们的参数列表或返回类型不同。在这种情况下,基类中的同名函数在派生类中被"隐藏"了。
覆盖的特性:
- 发生在继承层次中。
- 派生类中的函数和基类中的函数名称相同,但参数或返回类型不同。
- 基类的同名函数被"隐藏",无法通过派生类对象直接调用。
- 与虚函数没有必然关系,非虚函数也可以被覆盖。
示例:
cpp
class Base {
public:
void show(int a) { std::cout << "Base show with int: " << a << std::endl; }
};
class Derived : public Base {
public:
void show() { std::cout << "Derived show" << std::endl; }
};
Derived d;
d.show(); // 输出: "Derived show"
d.show(10); // 错误:Base::show(int) 被隐藏
在这个例子中,Derived::show()
隐藏了 Base::show(int)
,因为派生类定义了同名但不同参数的函数。
5.2 重载和覆盖的区别
-
发生范围:
- 重载:发生在同一个类或同一个作用域内。
- 覆盖:发生在继承层次结构中,派生类隐藏基类的同名函数。
-
函数参数:
- 重载:函数名相同,但参数列表不同(数量或类型)。
- 覆盖:函数名相同,但参数列表或返回类型不同。
-
影响:
- 重载:不影响继承关系。编译器根据参数列表选择合适的重载函数。
- 覆盖:派生类中的函数会隐藏基类中的同名函数,导致通过派生类对象无法访问基类的被隐藏函数。
-
与继承的关系:
- 重载:与继承无关,完全发生在同一作用域内。
- 覆盖:与继承相关,发生在基类和派生类之间。
5.2.1 解决隐藏问题
如果你在派生类中想要同时使用基类的隐藏函数,可以使用 using
关键字显式引入基类的函数。
cpp
class Derived : public Base {
public:
using Base::show; // 引入基类的 show(int)
void show() { std::cout << "Derived show" << std::endl; }
};
Derived d;
d.show(); // 输出: "Derived show"
d.show(10); // 输出: "Base show with int: 10"
通过 using Base::show
,你可以在派生类中继续使用基类的 show(int)
函数。
5.3 覆盖的函数签名可以一致吗?
覆盖(Hiding) 中,函数签名通常是不一致的(参数或返回类型不同),因为这会导致基类的函数被隐藏。然而,在 C++ 中,如果函数签名一致 ,并且是在虚函数的情况下发生的,这种行为被称为重写(Overriding),而不是覆盖。
-
覆盖(Hiding) :通常发生在函数签名不同的情况下,基类函数被派生类的同名函数隐藏。覆盖和虚函数无关,任何非虚函数或虚函数都可能被隐藏。
-
重写(Overriding) :发生在函数签名相同 且基类函数是虚函数的情况下。重写是虚函数机制的一部分,支持动态多态性。
签名一致的覆盖
虽然在大多数情况下,覆盖 是函数签名不同的情况,但在极少数情况下,C++ 允许基类的非虚函数与派生类的函数签名一致,这时它也会被视为覆盖 ,而不是重写。这种情况下,派生类的非虚函数会完全隐藏基类的同名非虚函数。
示例:
cpp
class Base {
public:
void show() { std::cout << "Base show" << std::endl; }
};
class Derived : public Base {
public:
void show() { std::cout << "Derived show" << std::endl; }
};
Derived d;
d.show(); // 输出: "Derived show"
Base* basePtr = &d;
basePtr->show(); // 输出: "Base show" (非虚函数调用)
在这里,Derived::show()
完全隐藏了 Base::show()
,即使它们的签名一致。这种情况仍然被视为覆盖,而不是重写,因为 Base::show()
不是虚函数。
- 覆盖(Hiding) 和 重写(Overriding) 的关键区别在于基类函数是否是虚函数。
- 覆盖:发生在基类函数和派生类函数的签名不同或基类函数不是虚函数的情况下,基类函数被隐藏。
- 重写:发生在基类虚函数和派生类函数的签名一致的情况下,支持动态多态性。
当签名一致时,如果基类函数是虚函数,那么派生类函数将重写基类函数;如果基类函数不是虚函数,那么派生类函数将覆盖基类函数。
总结
- 重载 和 覆盖 并不是一回事。
- 重载 是同一类中通过不同的参数签名实现的多态,与继承无关。
- 覆盖 是派生类隐藏基类的同名函数,发生在继承链中。
每个概念在不同的上下文中都有其特定的用途和意义。