继承
1.继承的概念
概念:继承(inheritace)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称之为派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。继承是类设计层次的复用。
// 派生类 继承方式 基类
class Student:public Person{
public:int _name;int _sex;
}
- 代码复用:通过继承,派生类可以复用基类的代码,减少重复编写相同功能的需要。
- 扩展性:派生类可以在基类的基础上添加新的成员或方法,扩展功能。
- 层次结构:继承建立了类之间的“is-a”关系,形成了一个类层次结构,有助于组织和理解代码。
2.继承方式和访问限定符
特征 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员变成 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员变成 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员变成 | 只能通过基类接口访问,派生类中不可见 | 只能通过基类接口访问,派生类中不可见 | 只能通过基类接口访问,派生类中不可见 |
能否隐式向上转换 | 是 | 是(但只能在派生类中) | 否 |
注意:
- 基类的
private
成员派生类不可见(无法直接允许访问),但可以使用基类public
或protected
成员函数间接访问。- 使用关键字
class
时默认继承方式是private
;使用struct
时默认继承方式是public
。最好显示的写出继承方式,以提高代码的可读性和可维护性。- public > protected > private,继承方式权限只能缩小不能放大,如基类的public成员的遇到protected继承方式就成了派生类的protected成员。
- 访问限定符在基类中没有体现,在派生类中才产生区别,这也就是访问限定符产生的原因。
struct BaseStruct {
public:int publicMember;
protected:int protectedMember;
private:int privateMember;
};class DerivedClass : BaseStruct { // 默认 private 继承
public:void accessMembers() {publicMember = 1; // private 继承,基类的 public 成员变为 privateprotectedMember = 2; // private 继承,基类的 protected 成员变为 private// privateMember = 3; // 无法访问}
};class DerivedPublic : public BaseStruct { // public 继承
public:void accessMembers() {publicMember = 1; // 保持 publicprotectedMember = 2; // 保持 protected// privateMember = 3; // 无法访问}
};
3.基类和派生类对象赋值转换
3.1 赋值兼容规则:
- 子类对象可以赋值给父类对象、指针或引用(称为”切割“或”切片“)。在这种赋值过程中,派生类特有的成员将被忽略,只保留基类部分。
- 基类对象不能直接赋值给派生对象,因为基类对象不包含派生类新增的成员。
class Person{
public:string _name;int _sex;int _age;
};
class Student : public Person{
public:int _No;
};
int main(){Student s;s.name = "Alice";s.age = 20;s._No = 12345;Person p;p = s; // 切割,只复制基类部分std::cout << p.name << ", " << p.age << std::endl; // 输出:Alice, 20Person* ptr = &s; // 多态,ptr 指向 Student 对象Person& ref = s; // 引用,ref 引用 Student 对象 return 0;
}
3.2 指针和引用的转换:
-
指针转换:
-
向上转型(Upcasting):将派生类指针转换为基类指针,是隐式且安全的。
-
向下转型(Downcasting):将基类指针转换为派生类指针,需要使用
dynamic_cast
进行类型检查,确保转换的安全性。
-
-
引用转换:
-
类似于指针转换,向上转型是隐式的,而向下转型需要显式的类型转换。
int main() {Student s;s.name = "Bob";s.age = 22;s.studentID = 67890;Person* basePtr = &s; // 向上转型,隐式转换basePtr->introduce(); // 调用基类方法// 向下转型,需要使用 dynamic_castStudent* derivedPtr = dynamic_cast<Student*>(basePtr);if (derivedPtr) {derivedPtr->study(); // 调用派生类方法}return 0;
}
4.继承作用域与成员隐藏
- 作用域独立:继承中的基类和派生类都有各自独立的作用域,成员隐藏仅在派生类作用域内有效。
- 成员隐藏:只要成员名称相同,无论类型或参数列表,派生类成员都会重定义(隐藏)基类的成员。
解决函数的隐藏与重载
class A {
public:virtual void fun() {cout << "A::fun()" << endl;}
};class B : public A {
public:void fun(int i) { // 重载,不隐藏基类的 fun()cout << "B::fun(int): " << i << endl;}void callBaseFun() {A::fun(); // 显式调用基类的 fun()}void fun() override { // 重定义cout << "B::fun()" << endl;}
};int main() {B b;b.fun(); // 调用 B::fun()b.fun(10); // 调用 B::fun(int)b.callBaseFun(); // 调用 A::fun()return 0;
}
B::fun(int i)
:重载了A::fun()
,但不隐藏基类的fun()
,在同一作用域内(派生类B
中)。B::fun()
:重定义了A::fun()
,提供了新的实现。
也可以通过using A::fun
引入基类的fun()
,从而调用A类的fun方法。
重载、重写和重定义的区别:
特性 重载(Overloading) 重写(Overriding) 重定义(Hiding) 作用域 同一类内 派生类和基类之间 派生类和基类之间 是否需要继承 否 是 是 关键字 无 virtual
、override
无 参数列表 必须不同 必须相同 可以不同 返回类型 可以不同 必须相同(或协变) 可以不同 调用时间 编译时决定 运行时决定 编译时决定 用途 提供同名函数的不同版本 实现多态性 派生类中隐藏基类同名函数
5.派生类的默认成员函数
当创建派生类时,编译器会自动为其生成一些默认的成员函数,包括构造函数、拷贝构造函数、赋值运算符和析构函数。这些默认成员函数在大多数情况下是足够的,但在特定需求下,程序员可以显式地定义或删除它们。
默认成员函数的行为:
- 构造函数:
- 默认构造函数:派生类的构造函数必须调用基类的构造函数以初始化基类部分。如果基类没有默认构造函数,派生类构造函数必须在初始化列表中显式调用基类的其他构造函数。
- 拷贝构造函数:自动调用基类的拷贝构造函数,完成基类部分的拷贝初始化。
- 赋值运算符(
operator=
):- 自动调用基类的赋值运算符,完成基类部分的赋值。
- 析构函数:
- 派生类的析构函数在执行完自己的清理工作后,自动调用基类的析构函数,确保基类部分的资源得到正确释放。
- 虚析构函数:如果基类的析构函数是虚函数(
virtual
),可以确保通过基类指针删除派生类对象时,派生类的析构函数被正确调用,避免资源泄漏。
#include <iostream>
using namespace std;class Base {
public:Base() { cout << "Base Constructor" << endl; }Base(const Base&) { cout << "Base Copy Constructor" << endl; }Base& operator=(const Base&) { cout << "Base Assignment Operator" << endl; return *this; }virtual ~Base() { cout << "Base Destructor" << endl; }
};class Derived : public Base {
public:Derived() { cout << "Derived Constructor" << endl; }Derived(const Derived& d) : Base(d) { cout << "Derived Copy Constructor" << endl; }Derived& operator=(const Derived& d) { Base::operator=(d); cout << "Derived Assignment Operator" << endl; return *this; }~Derived() { cout << "Derived Destructor" << endl; }
};int main() {Derived d1;Derived d2 = d1; // 拷贝构造d2 = d1; // 赋值运算符return 0;
}/*
输出:
Base Constructor
Derived Constructor
Base Copy Constructor
Derived Copy Constructor
Base Assignment Operator
Derived Assignment Operator
Derived Destructor
Base Destructor
Derived Destructor
Base Destructor
*/
6.友元与静态成员
- 友元关系不能继承:基类的友元在派生类中不是友元,派生类需要重新声明友元关系。
class Base {
private:int secret;friend class FriendClass;
};class Derived : public Base {// FriendClass 不是 Derived 的友元
};
- 静态成员:基类定义了static静态成员,则整个继承体系里面只有一个这样的成员,所有派生类共享这一个静态成员。
class Base {
public:static int staticValue;
};int Base::staticValue = 0;class Derived1 : public Base {};
class Derived2 : public Base {};int main() {Derived1::staticValue = 5;cout << Derived2::staticValue << endl; // 输出:5return 0;
}
7.多继承
优点:
- 功能整合:允许派生类同时拥有多个基类的功能,适用于需要结合多种特性的复杂类。
- 灵活性:提供了更大的设计灵活性,适用于多维度的类层次结构。
缺点:
- 复杂性增加:类层次结构更加复杂,增加了理解和维护的难度。
- 名称冲突:多个基类中可能存在同名成员,导致名称冲突和二义性。
- 菱形继承问题:多个基类继承自同一个祖先类,导致数据冗余和二义性。
7.1 单继承与多继承
- 单继承:一个子类只有一个直接父类时这个继承关系为单继承。
- 多继承:一个子类有两个或以上的直接父类时成为多继承。
7.2 菱形继承
菱形继承:菱形继承是多集成的一种特殊情况。
菱形继承的问题:
- 数据冗余:派生类中存在多个基类的副本,导致数据冗余。例如,
Assistant
类中会有两份Person
的成员。 - 二义性:当访问祖先类的成员时,编译器无法确定访问哪一个基类的成员,导致二义性错误。
class Person {
public:string _name; // 姓名
};
class Student : public Person {
protected:int _num; //学号
};
class Teacher : public Person {int _id; //职工号
};
class Assistant : public Student, public Teacher {
protected:string _majorCourse; // 主修课程
};
void Test() {// 这样会有二义性无法明确知道访问的是哪一个Assistant a;// 编译错误:二义性,无法确定是 Student::Person::name 还是 Teacher::Person::name//a._name = "peter";// 需要显示指定访问那个父类的成员可以解决二义性问题// 但是数据冗余问题无法解决a.Student::_name = "XXX";a.Teacher::_name = "YYY";
}
8.虚拟继承
为了解决菱形继承带来的数据冗余和二义性问题,C++ 引入了虚继承(Virtual Inheritance)。通过虚继承,派生类共享基类的唯一实例,消除数据冗余和二义性。
如在上面Student和Teacher在继承Person时使用虚继承即可解决问题:
class Person {
public:std::string name;
};class Student : virtual public Person {
protected:int studentID;
};class Teacher : virtual public Person {
protected:int employeeID;
};class Assistant : public Student, public Teacher {
protected:std::string majorCourse;
};int main() {Assistant a;a.name = "Charlie"; // 唯一的 Person::name 成员return 0;
}
下面是一个直接继承的例子:
注:由于x86指针采用4字节对齐方式,x64采用8字节对齐方式。方便起见以下全用x86为例
class A {
public:int _a;
};
class B : public A {
public:int _b;
};
class C : public A {
public:int _c;
};
class D : public B, public C {
public:int _d;
};
void Test(){D d;// sizeof(B)=8; sizeof(C)=8// sizeof(D) = sizeof(B)+sizeof(C)+sizeof(_d) = 20cout << "sizeof(d)=" << sizeof(d) << endl;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;// 编译报错:_a 不明确,需要指明是那个父类下连带的属性,如上// d._a = 6;
}
从图中可以见得数据中存在两个_a
,造成了数据的冗余和二义性;
下面看一下虚继承的示例:
class A {
public:int _a;
};
class B : virtual public A {
public:int _b;
};
class C : virtual public A {
public:int _c;
};
class D : public B, public C {
public:int _d;
};
void Test(){D d;cout << "sizeof(d)=" << sizeof(d) << endl;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;d._a = 6;
}
8.1 虚拟继承工作原理
- 虚基表指针(VBPtr):编译器为每个类对象添加一个虚基表指针,用于指向虚基类的信息。这与虚函数表指针(Vptr)不同,用于虚函数的多态性。
- 虚基表(VBTable):虚基表记录虚基类的位置信息和偏移量,确保在派生类中正确访问共享基类成员。
虚拟指针的“三板斧”
由上面例子我们能够发现:但虚拟继承时,原本的父类成员会被替换为一个虚基表指针,这个指针指向一张虚基表,虚基表里存放虚基类与虚基表指针的偏移量。要注意和虚函数表区分开来。
正常继承:(下图中展示的是x64环境下的)
虚拟继承:
编译器处理虚继承的方法是:编译器在处理虚继承(虚函数)时,会给每个派生类添加一个隐藏成员类似:
class A{
public:void *vptr;// 续集表指针 --> 4个字节...
}
同时在编译器会给向派生类的构造函数中安插一条赋值语句 来为vptr
赋值,类似:
A(){vptr = &A::vftable;
}
虚基类存储在最后继承它的派生类。
9.继承与组合
继承和组合都是实现类复用和构建复杂对象的手段,但它们在设计哲学和应用场景上有所不同。
9.1 继承(Inheritance)
- 关系:表示“is-a”关系,即派生类是基类的一种。例如,
Student
是Person
。 - 复用方式:派生类通过继承基类的成员,实现代码的复用和扩展。
- 继承是一种白箱复用,父类对子类基本是透明的,但是它一定程度破坏了父类的封装
- 优点:
- 简单直接,适用于明确的类型层次结构。
- 允许派生类直接访问基类的
public
和protected
成员。
- 缺点:
- 高耦合,派生类依赖基类的实现细节。
- 破坏基类的封装性,基类的改变可能影响派生类。
8.2 组合(Composition)
- 关系:表示“has-a”关系,即一个类包含另一个类的对象。例如,
Car
有一个Engine
。 - 复用方式:通过在类中包含成员对象,实现功能的复用和扩展。
- 组合式一种黑箱复用,C对D是不透明的,C保持着他的封装。
- 优点:
- 低耦合,类之间的依赖关系较弱。
- 保持了各自类的封装性和独立性。
- 更加灵活,可以在运行时动态组合不同的组件。
- 缺点:
- 需要通过成员对象的接口间接访问功能,可能增加代码复杂性。
示例:
- 继承示例:
// 继承示例
class Engine {
public:void start() {std::cout << "Engine started." << std::endl;}
};class Car : public Engine { // Car is-a Engine(不符合实际逻辑,仅为示例)
public:void drive() {start(); // 直接访问 Engine 的成员std::cout << "Car is driving." << std::endl;}
};int main() {Car car;car.start(); // 直接调用 Engine 的方法car.drive();return 0;
}
/*
输出:
Engine started.
Engine started.
Car is driving.
*/
- 组合示例:
// 组合示例
class Engine {
public:void start() {std::cout << "Engine started." << std::endl;}
};class Car {
private:Engine engine; // Car has-a Engine
public:void drive() {engine.start(); // 通过 Engine 的接口访问std::cout << "Car is driving." << std::endl;}
};int main() {Car car;// car.start(); // 错误,Engine 的 start() 是私有的car.drive();return 0;
}
/*
输出:
Engine started.
Car is driving.
*/
组合的类耦合度更低,而继承的类是一种高耦合。
最佳实践:
- 优先使用组合:当类之间的关系不明确或“has-a”关系更符合实际需求时,优先选择组合。
- 谨慎使用继承:仅在明确需要“is-a”关系并且派生类确实需要基类的功能时,才使用继承。
面试题
C++的缺陷有哪些?
- 复杂性:
- C++ 的语法和特性非常丰富,这使得学习曲线陡峭,容易出错。
- 复杂的模板和泛型编程可能导致编译错误难以理解。
- 内存管理:
- 手动管理内存(如使用
new
和delete
)容易导致内存泄漏和野指针问题。 - 虽然 C++11 引入了智能指针(如
unique_ptr
和shared_ptr
),但仍然需要开发者谨慎使用。
- 手动管理内存(如使用
- 多继承:
- 多继承可能导致复杂的对象模型和潜在的二义性问题。
- 菱形继承问题是一个典型的多继承问题,需要使用虚继承来解决。
- 性能和效率:
- 虽然 C++ 通常被认为是高性能的语言,但某些高级特性(如虚函数、RTTI)可能会引入额外的开销。
- 优化代码需要深入理解编译器和硬件特性。
- 缺乏内置的垃圾回收机制:
- C++ 没有内置的垃圾回收机制,需要手动管理内存,增加了开发复杂度。
- 标准库的局限性:
- 标准库虽然强大,但某些领域(如网络编程、GUI 开发)的支持相对薄弱。
- 第三方库的质量和兼容性参差不齐。
什么是菱形继承?菱形继承的问题是什么?如何解决?虚继承的原理是什么?
-
什么是菱形继承?
菱形继承是一种多继承的情况,其中派生类从两个基类派生,而这两个基类又共同派生自同一个基类。这种继承结构形成了一个菱形的形状。
class A {
public:int value;
};
class B : public A {};
class C : public A {};
class D : public B, public C {};
-
菱形继承的问题是什么?
(1) 二义性:
- 由于
D
从B
和C
继承,而B
和C
都有一个A
的子对象,D
会有两个A
的子对象。 - 当尝试访问
A
的成员时,编译器无法确定应该使用哪个A
的子对象,导致二义性问题。
(2) 对象模型复杂:
- 菱形继承会导致对象模型变得复杂,增加内存开销和管理难度。
- 由于
-
如何解决菱形继承的问题?
使用虚继承可以解决菱形继承带来的二义性和对象模型复杂的问题。
class A {
public:int value;
};
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};
-
虚继承的原理?
(1) 单一子对象:
- 虚继承确保派生类中只有一个基类的子对象,而不是多个。
- 在上述例子中,
D
只有一个A
的子对象,而不是两个。
(2) 初始化顺序:
- 虚基类的构造函数会在最派生类的构造函数中被调用,而不是在中间派生类的构造函数中被调用。
- 这确保了虚基类的子对象在所有派生类的子对象之前被初始化。
(3) 内存布局:
- 虚继承会导致对象的内存布局更加复杂,因为编译器需要在对象中添加额外的指针来管理虚基类的子对象。
- 这可能会引入一定的性能开销。
模版
1.概念和工作原理
1.1 概念:
模版(Template)是C++中实现泛型变成的核心机制。它运行程序员编写与类型无关的代码,通过实例化时指定具体类型,从而实现代码的复用和灵活性。模版主要分为函数模版和类模版。模版的设计旨在在不牺牲性能的前提下,实现代码的高度复用。通过在编译时生成针对不同类型的代码,模版避免了运行时的多态开销,同时保持了类型安全。这种编译时多态性与运行时多态性(如虚函数)形成鲜明对比,各有优劣。
1.2 工作原理:
模版在C++中通过实例化机制工作。当编译器遇到模板的使用时,会根据提供的类型参数生成具体的函数或类。这一过程发生在编译期,确保了生成的代码在类型上是正确的。
编译器在编译阶段根据模板参数生成响应的代码,这意味着每个不同的模板参数组合都会生成独立的代码实例。这种机制带了以下优点:
- 类型安全: 所有类型检查在编译期完成,避免了运行时错误。
- 性能优化:生成的代码针对特定类型进行了优化,消除了不必要的抽象层。
但同时也存在一些缺点:
- 编译时间增加:大量的模版实例化可能导致编译时间显著增加。
- 代码膨胀:每个模版实例化都会生成独立的代码,可能导致可执行文件体积增大。
2.函数模板
2.1 定义与使用
函数模板允许编写与类型无关的函数,通过模板参数在调用时指定具体类型。函数模板的语法以template
关键字开头,紧随其后的是模板参数列表。
// 函数模版
template <typename T>
T getMax(T a, T b) {return (a > b) ? a : b;
}int main() {cout << getMax<int>(3, 7) << endl; // 输出:7cout << getMax<double>(3.5, 2.5) << endl; // 输出:3.5cout << getMax<char>('g', 'e') << endl; // 输出:greturn 0;
}
2.2 模版参数推导
在大多数情况下编译期能够根据函数参数自动推导处模板类型,此时可以不用显式的指定类型。,因此上面对模版的使用还可以这样
int main() {cout << getMax(3, 7) << endl; // 自动推导为 getMax<int>cout << getMax(3.5, 2.5) << endl; // 自动推导为 getMax<double>cout << getMax('g', 'e') << endl; // 自动推导为 getMax<char>return 0;
}
注意事项:
- 如果模版参数无法从函数参数中推导出来,必须显式指定类型。
- 模版参数推导在函数重载解析中起重要作用,可能影响函数的选择。
2.3 多参数模板
函数模板可以接收多个类型参数,以处理不同类型的参数组合。这为函数的通用性提供了更大的灵活性:
// 多参数函数模版
template <typename T1, typename T2>
auto add(T1 a, T2 b) -> decltype(a + b) {// 根据传入值a、b相加的结果推导类型return a + b;
}int main() {cout << add(3, 4.5) << endl; // 输出:7.5cout << add(2.3f, 4) << endl; // 输出:6.3return 0;
}
拓展分析:
decltype
关键字:是C++11新增的一个关键字,和auto的功能一样,用来编译时期进行自动类型推导,确保返回类型与操作符的实际结果类型一致。它的引入很好的弥补了auto不适用或跟不无法使用的场景。
decltype
基本语法:int a = 10; decltype(a) b = a; // b的类型与a相同,即int
3.类模板
3.1 定义与使用
#include <iostream>
#include <string>
using namespace std;// 类模版
template <typename T>
class MyContainer {
private:T element;
public:MyContainer(T elem) : element(elem) {}void display() const {cout << "Element: " << element << endl;}
};int main() {MyContainer<int> intObj(42);intObj.display(); // 输出:Element: 42MyContainer<string> strObj("Hello");strObj.display(); // 输出:Element: Helloreturn 0;
}
3.2 模板类的默认参数
类模板可以为模板参数指定默认类型,简化实例化时的类型指定。允许在需要时覆盖默认类型,保持代码的通用性。
// 类模版,默认类型为int
template <typename T = int>
class MyContainer {
private:T element;
public:MyContainer(T elem) : element(elem) {}void display() const {cout << "Element: " << element << endl;}
};int main() {MyContainer<> defaultObj(100); // 默认类型为intdefaultObj.display(); // 输出:Element: 100MyContainer<double> doubleObj(99.99);doubleObj.display(); // 输出:Element: 99.99return 0;
}
4.模板特化
模板特化允许为特定类型或类型组合提供专门的实现,以满足特殊需求。模板特化分为全特化和偏特化。
4.1 全特化
全特化为模板所有参数指定具体类型,提供专门的实现。这在处理特定类型时非常有用,例如针对指针类型或某些自定义类型提供不同的行为。
#include <iostream>
using namespace std;// 原始类模版
template <typename T>
class MyContainer {
private:T element;
public:MyContainer(T elem) : element(elem) {}void display() const {cout << "Generic Element: " << element << endl;}
};// 全特化,针对char*类型
template <>
class MyContainer<char*> {
private:char* element;
public:MyContainer(char* elem) : element(elem) {}void display() const {cout << "Specialized Element: " << element << endl;}
};int main() {MyContainer<int> intObj(10);intObj.display(); // 输出:Generic Element: 10char msg[] = "Hello, World!";MyContainer<char*> charPtrObj(msg);charPtrObj.display(); // 输出:Specialized Element: Hello, World!return 0;
}
- 限制: 全特化允许部分模板参数进行特化,适用于部分类型参数的特殊实现。
4.2 偏特化
偏特化允许部分模板参数进行特化,实现更加灵活和细粒度的模板行为。适用于部分类型的特殊实现。偏特化主要用于类模板,函数模板不支持偏特化。
#include <iostream>
#include <string>
using namespace std;// 原始类模版
template <typename T1, typename T2>
class Pair {
private:T1 first;T2 second;
public:Pair(T1 a, T2 b) : first(a), second(b) {}void display() const {cout << "Pair: (" << first << ", " << second << ")" << endl;}
};// 偏特化,当T2为char*时
template <typename T1>
class Pair<T1, char*> {
private:T1 first;char* second;
public:Pair(T1 a, char* b) : first(a), second(b) {}void display() const {cout << "Specialized Pair: (" << first << ", " << second << ")" << endl;}
};int main() {Pair<int, double> p1(1, 3.14);p1.display(); // 输出:Pair: (1, 3.14)char msg[] = "C++ Templates";Pair<string, char*> p2("Topic", msg);p2.display(); // 输出:Specialized Pair: (Topic, C++ Templates)return 0;
}