1、类的继承
1.1、概念
类的继承(Inheritance)是面向对象编程(OOP)中的一种机制,允许一个类(子类)继承另一个类(父类)的属性和方法,子类可以扩展父类的功能,并且可以添加新的属性和方法。继承可以提高代码的重用性和灵活性。
在继承关系中,子类是父类的扩展和修改,子类继承了父类的所有属性和方法,并且可以在父类的基础上添加新的属性和方法或覆盖父类的方法。
1.2、语法
class Derived : public/protected/private Base {// 继承的内容
};
其中:
Derived
是子类的名称Base
是父类的名称public
/protected
/private
是继承方式的访问控制修饰符
访问控制修饰符的意思是:
public
:继承的内容将是公共的,可以在任何地方访问protected
:继承的内容将是保护的,只能在本类和其子类中访问private
:继承的内容将是私有的,只能在本类中访问
例如:
class Animal {
public:void eat() {cout << "Animal is eating" << endl;}
};class Dog : public Animal {
public:void bark() {cout << "Woof!" << endl;}
};
在上面的例子中,Dog
类继承自 Animal
类,并使用 public
访问控制修饰符。这样,Dog
类可以继承 Animal
类的公共成员变量和函数,并且可以在 Dog
类中添加新的成员变量和函数。
如果你想使用 protected
访问控制修饰符,可以像下面这样:
class Animal {
protected:void eat() {cout << "Animal is eating" << endl;}
};class Dog : protected Animal {
public:void bark() {cout << "Woof!" << endl;}
};
在上面的例子中,Dog
类继承自 Animal
类,并使用 protected
访问控制修饰符。这样,Dog
类可以继承 Animal
类的保护成员变量和函数,但是不能在外部访问这些成员变量和函数。
如果你想使用 private
访问控制修饰符,可以像下面这样:
class Animal {
private:void eat() {cout << "Animal is eating" << endl;}
};class Dog : private Animal {
public:void bark() {cout << "Woof!" << endl;}
};
在上面的例子中,Dog
类继承自 Animal
类,并使用 private
访问控制修饰符。这样,Dog
类可以继承 Animal
类的私有成员变量和函数,但是不能在外部访问这些成员变量和函数。
需要注意的是,在 C++ 中,继承关系可以是多继承的,也就是一个类可以继承多个父类。例如:
class Animal {
public:void eat() {cout << "Animal is eating" << endl;}
};class Mammal {
public:void walk() {cout << "Mammal is walking" << endl;}
};class Dog : public Animal, public Mammal {
public:void bark() {cout << "Woof!" << endl;}
};
在上面的例子中,Dog
类继承自 Animal
和 Mammal
两个类,并使用公共继承方式。这样,Dog
类可以继承 Animal
和 Mammal
两个类的公共成员变量和函数。
1.3、单继承
单继承(Single Inheritance)是指一个子类继承自一个父类的关系。在C++中,单继承的语法如下:
class Derived : public Base {// 继承的内容
};
其中,Derived
是子类的名称,Base
是父类的名称,public
是继承方式的访问控制修饰符。
构造函数
在C++中,构造函数是用于初始化对象的特殊函数。对于单继承,构造函数的执行顺序是:
- 父类的构造函数被调用
- 子类的构造函数被调用
例如:
class Base {
public:Base() {cout << "Base constructor" << endl;}
};class Derived : public Base {
public:Derived() {cout << "Derived constructor" << endl;}
};int main() {Derived d;return 0;
}
输出结果:
Base constructor
Derived constructor
可以看到,先调用了父类的构造函数,然后调用了子类的构造函数。
析构函数
析构函数是用于释放对象资源的特殊函数。在C++中,析构函数的执行顺序是:
- 子类的析构函数被调用
- 父类的析构函数被调用
例如:
class Base {
public:~Base() {cout << "Base destructor" << endl;}
};class Derived : public Base {
public:~Derived() {cout << "Derived destructor" << endl;}
};int main() {Derived d;return 0;
}
输出结果:
Derived destructor
Base destructor
可以看到,先调用了子类的析构函数,然后调用了父类的析构函数。
成员函数
成员函数是对象中的一部分函数。对于单继承,成员函数的执行顺序是:
- 父类的成员函数被调用
- 子类的成员函数被调用
例如:
class Base {
public:void foo() {cout << "Base::foo()" << endl;}
};class Derived : public Base {
public:void foo() {cout << "Derived::foo()" << endl;}
};int main() {Derived d;d.foo();return 0;
}
输出结果:
Base::foo()
Derived::foo()
可以看到,先调用了父类的成员函数,然后调用了子类的成员函数。
总的来说,单继承在C++中是一种常见的继承方式,可以用于实现对象之间的关系。构造函数、析构函数和成员函数的执行顺序都遵循一定的规则,可以帮助开发者更好地理解和使用继承关系。
1.4、多重继承
多重继承(Multiple Inheritance)是指一个子类继承自多个父类的关系。在C++中,多重继承的语法如下:
class Derived : public Base1, public Base2, ... {// 继承的内容
};
其中,Derived
是子类的名称,Base1
、Base2
等是父类的名称。
多重继承的优点
多重继承可以实现以下几个优点:
- 代码复用:多重继承可以复用多个父类的代码,减少代码的重复。
- flexibility:多重继承可以实现更加灵活的继承关系,满足不同的需求。
- 扩展性:多重继承可以扩展类的功能,实现更加复杂的继承关系。
多重继承的缺点
多重继承也存在以下几个缺点:
- complexity:多重继承可以增加类的复杂性,难以理解和维护。
- ambiguity:多重继承可能引起名称冲突和 ambiguity,需要使用using指示符来解决。
- override:多重继承可能会出现 override 问题,即子类的成员函数可能会被多个父类的成员函数override。
多重继承的示例
以下是一个多重继承的示例:
class Animal {
public:void eat() {cout << "Animal is eating" << endl;}
};class Mammal {
public:void walk() {cout << "Mammal is walking" << endl;}
};class Dog : public Animal, public Mammal {
public:void bark() {cout << "Woof!" << endl;}
};
在上面的示例中,Dog
类继承自 Animal
和 Mammal
两个父类。这样,Dog
类可以继承 Animal
和 Mammal
两个父类的成员变量和函数。
多重继承的注意事项
在使用多重继承时,需要注意以下几点:
- using 指示符:使用using指示符可以解决名称冲突和 ambiguity。
- override 问题:需要确保子类的成员函数不会被多个父类的成员函数override。
- 继承顺序:需要确保继承顺序正确,避免继承错误的父类。
总的来说,多重继承可以实现更加灵活和复杂的继承关系,但是需要注意相应的缺点和注意事项。
1.5、多层继承
多层继承(Multilevel Inheritance)是指一个类继承自另一个类,而这个子类又继承自另一个类,形成一个继承链条。它是一种特殊的继承方式,与多重继承不同,多层继承只涉及一个父类在每一层。
语法:
多层继承没有特殊的语法,它仅仅是继承关系的层层嵌套。例如:
class Animal {
public:void eat() { std::cout << "Animal is eating" << std::endl; }
};class Mammal : public Animal {
public:void giveBirth() { std::cout << "Mammal is giving birth" << std::endl; }
};class Dog : public Mammal {
public:void bark() { std::cout << "Woof!" << std::endl; }
};
在这个例子中,Dog
继承自 Mammal
,而 Mammal
继承自 Animal
。这就是多层继承。 Dog
继承了 Mammal
和 Animal
的所有公共成员(除非被隐藏或重写)。
构造函数和析构函数的调用顺序:
在多层继承中,构造函数和析构函数的调用顺序遵循自底向上的原则:
- 构造函数: 最底层的类的构造函数先被调用,然后是其父类的构造函数,依次向上调用到最顶层的父类构造函数。
- 析构函数: 与构造函数相反,最顶层的类的析构函数先被调用,然后是其子类的析构函数,依次向下调用到最底层的子类析构函数。
示例:
#include <iostream>class Animal {
public:Animal() { std::cout << "Animal constructor called" << std::endl; }~Animal() { std::cout << "Animal destructor called" << std::endl; }
};class Mammal : public Animal {
public:Mammal() { std::cout << "Mammal constructor called" << std::endl; }~Mammal() { std::cout << "Mammal destructor called" << std::endl; }
};class Dog : public Mammal {
public:Dog() { std::cout << "Dog constructor called" << std::endl; }~Dog() { std::cout << "Dog destructor called" << std::endl; }
};int main() {Dog myDog;return 0;
}
输出结果将是:
Animal constructor called
Mammal constructor called
Dog constructor called
Dog destructor called
Mammal destructor called
Animal destructor called
优缺点:
优点:
- 代码复用: 可以最大限度地复用代码,减少冗余。
- 层次清晰: 继承关系清晰,易于理解和维护(相较于多重继承)。
缺点:
- 继承链过长: 如果继承层次过深,可能会导致代码难以维护和理解。 过长的继承链也可能导致代码过于复杂,难以调试。
- 脆弱性: 对父类的修改可能会影响到所有子类,这需要谨慎处理。
总而言之,多层继承是一种强大的工具,但需要谨慎使用。 在设计类层次结构时,应尽量保持继承层次的简洁和清晰,避免过度使用多层继承,以提高代码的可维护性和可读性。 如果继承层次过深,可能需要重新考虑设计,例如使用组合代替继承。
多重与多层:
多重继承与多层继承其实比较混乱,可能我在这里说的事多层,别人觉得是多重,但是我们自己分清除就好
1.6、最远派生类
在多层继承或多重继承中,“最远派生类”(most derived class)指的是继承链条中最底层的那个类,也就是没有其他类再继承自它的那个类。 它继承了所有祖先类(父类、祖父类等等)的公共成员(除非被隐藏或重写)。
例如,考虑以下继承关系:
class A {};
class B : public A {};
class C : public B {};
在这个例子中,C
是最远派生类。它继承了 A
和 B
的所有公共成员。
再看一个多重继承的例子:
class A {};
class B {};
class C : public A, public B {};
这里 C
也是最远派生类,它继承了 A
和 B
的成员。
最远派生类的重要性:
最远派生类的概念在以下几个方面很重要:
-
构造函数和析构函数的调用顺序: 在多层或多重继承中,构造函数的调用顺序是从最基类到最远派生类,析构函数的调用顺序则相反,从最远派生类到最基类。理解最远派生类对于理解这种调用顺序至关重要。
-
虚函数的调用: 如果涉及虚函数,运行时多态性将根据对象的实际类型(最远派生类类型)来决定调用哪个版本的虚函数。
-
对象大小: 最远派生类对象的大小取决于它所有祖先类成员的大小之和(考虑内存对齐)。
-
成员访问: 虽然最远派生类可以访问其所有祖先类的公共成员,但访问方式可能需要使用作用域解析运算符
::
来避免歧义,尤其是在多重继承的情况下。
总而言之,理解最远派生类的概念对于正确理解和使用C++的继承机制至关重要,特别是涉及到多层继承和多重继承时。 它是继承体系中最终的、完整的类,包含了所有祖先类的特性。
2、多态
2.1、3个主要的概念
多态(Polymorphism)是指在编译时或运行时根据对象的实际类型来选择方法或函数的不同实现。多态可以分为以下几个方面:
- 函数重载(Function Overloading):指的是在同一个类中拥有多个同名函数,但它们的参数列表不同。编译器根据函数的参数列表来选择哪个函数被调用。
- 函数重写(Function Overriding):指的是在继承关系中,子类重新定义了父类的同名函数。编译器根据对象的实际类型来选择哪个函数被调用。
- 函数隐藏(Function Hiding):指的是在继承关系中,子类定义了一个同名函数,但父类中已经有了同名函数。这种情况下,子类的函数将隐藏父类的函数。
函数重载:
函数重载是一种编译时多态性,它根据函数的参数列表来选择哪个函数被调用。例如:
class Calculator {
public:int add(int a, int b) { return a + b; }
};class AdvancedCalculator : public Calculator {
public:int add(int a, int b, int c) { return a + b + c; }
};int main() {AdvancedCalculator calc;int result = calc.add(1, 2); // 调用 AdvancedCalculator 的 add 函数return 0;
}
在上面的例子中,AdvancedCalculator
类重载了 Calculator
类的 add
函数,添加了一个新的参数。
函数重写:
函数重写是一种运行时多态性,它根据对象的实际类型来选择哪个函数被调用。例如:
class Animal {
public:virtual void sound() { std::cout << "Animal makes a sound" << std::endl; }
};class Dog : public Animal {
public:void sound() { std::cout << "Dog barks" << std::endl; }
};int main() {Animal* animal = new Dog();animal->sound(); // 调用 Dog 的 sound 函数return 0;
}
在上面的例子中,Dog
类重写了 Animal
类的 sound
函数,提供了一个新的实现。
函数隐藏:
函数隐藏是一种编译时多态性,它根据函数的名称和参数列表来选择哪个函数被调用。例如:
class Animal {
public:void sound() { std::cout << "Animal makes a sound" << std::endl; }
};class Dog : public Animal {
public:void sound() { std::cout << "Dog barks" << std::endl; }
};int main() {Animal* animal = new Dog();animal->sound(); // 调用 Dog 的 sound 函数return 0;
}
在上面的例子中,Dog
类隐藏了 Animal
类的 sound
函数,提供了一个新的实现。
总之,函数重载、重写和隐藏都是多态性中的重要概念,它们可以帮助我们创建更加灵活和可扩展的代码。
补充一点
多态是什么??
在面试的时候,如果是与C++相关的岗位,多态是什么应该是出现频率最高的问题了
个人观点:
在C++中,多态性可以分为两种:
- 编译时多态(Compile-time polymorphism):指的是在编译时根据函数的参数列表来选择哪个函数被调用。例如,函数重载(Function Overloading)。
- 运行时多态(Run-time polymorphism):指的是在运行时根据对象的实际类型来选择哪个函数被调用。例如,函数重写(Function Overriding)和虚函数(Virtual Function)。
如果你觉得麻烦也可以是一句话:相同接口的不同实现
2.2、虚函数
虚函数(Virtual Function)
虚函数是一种面向对象编程(OOP)的概念,在C++中,它允许子类重新定义父类的函数。虚函数是实现多态性的一个重要手段。
什么是虚函数?
虚函数是一种函数,它可以被子类重新定义,以便在子类中提供不同的实现。虚函数的主要特点是:
- 重写:子类可以重新定义父类的虚函数,以提供不同的实现。
- 多态性:虚函数可以根据对象的实际类型来选择哪个函数被调用。
虚函数的定义
虚函数的定义语法如下:
virtual return-type function-name (parameter-list);
其中,return-type
是函数的返回类型,function-name
是函数的名称,parameter-list
是函数的参数列表。
虚函数的使用
虚函数可以在父类中定义,并在子类中重新定义。以下是一个简单的示例:
class Animal {
public:virtual void sound() { std::cout << "Animal makes a sound" << std::endl; }
};class Dog : public Animal {
public:void sound() { std::cout << "Dog barks" << std::endl; }
};int main() {Animal* animal = new Dog();animal->sound(); // Output: Dog barksreturn 0;
}
在上面的示例中,Animal
类定义了一个虚函数 sound()
,Dog
类继承自 Animal
类,并重新定义了 sound()
函数。编译器根据对象的实际类型来选择哪个函数被调用。
虚函数表
虚函数表(VTable)是一个对象中存储虚函数的表。每个对象都有一个虚函数表,它包含了对象中所有虚函数的地址。虚函数表的主要作用是:
- 存储虚函数的地址:虚函数表存储了每个虚函数的地址,以便在运行时根据对象的实际类型来选择哪个函数被调用。
- 实现多态性:虚函数表实现了多态性,即根据对象的实际类型来选择哪个函数被调用。
虚函数的注意事项
- 父类中必须声明虚函数:在父类中必须声明虚函数,以便子类可以重新定义它。
- 子类中必须重新定义虚函数:在子类中必须重新定义父类的虚函数,以便提供不同的实现。
- 虚函数的返回类型必须相同:虚函数的返回类型必须相同,以便在子类中重新定义时能够正确地调用父类的实现。
- 虚函数的参数列表必须相同:虚函数的参数列表必须相同,以便在子类中重新定义时能够正确地调用父类的实现。
总之,虚函数是一个重要的面向对象编程概念,它允许子类重新定义父类的函数,以实现多态性。
2.3、类的大小
类的实际大小并非简单地将所有成员变量的大小相加。它受到多种因素的影响,理解这些因素对于编写高效的C++代码至关重要。 以下详细解释影响类大小的因素:
1. 成员变量的大小: 这是最直接的因素。 每个成员变量都会占用一定大小的空间,这个大小取决于其数据类型(例如,int
通常是4字节,double
通常是8字节,char
是1字节等等)。 类的大小至少要能容纳所有成员变量。
2. 成员变量的排列顺序和对齐方式: 编译器为了优化内存访问效率,会对成员变量进行对齐。 对齐规则通常是:成员变量的起始地址必须是其大小的整数倍。 例如,如果int
是4字节对齐,那么int
类型的成员变量的地址必须是4的倍数。 如果成员变量的排列顺序导致需要填充一些字节来满足对齐要求,那么类的大小就会增加。
3. 继承: 如果一个类继承自另一个类,那么子类的大小通常会包含父类的大小。 但是,如果父类包含虚函数,情况会变得复杂(见下文)。
4. 虚函数: 如果类包含虚函数,编译器会在类中添加一个指向虚函数表的指针(vptr)。 这个指针通常是4或8字节(取决于系统是32位还是64位)。 这意味着即使类没有其他成员变量,它的大小至少也是指针的大小。 需要注意的是,这个vptr只在类本身以及其派生类中各存在一个,而不是每个对象都拥有一个vptr。
5. 虚继承: 如果一个类使用虚继承,情况会更加复杂。 虚继承是为了解决菱形继承问题(多个类继承同一个基类)中出现的数据冗余。 虚继承会引入额外的指针成员,以确保每个子类只有一个基类的副本。 这会增加类的大小。
6. 静态成员变量: 静态成员变量不属于类的对象,而是属于类本身。 它们不占用类对象的空间,因此不会影响类的大小。
7. 空类的大小: 一个空类的大小不为零,通常为1字节。 这是为了保证不同对象在内存中具有不同的地址。
8. 编译器和平台的差异: 不同的编译器和平台可能有不同的对齐规则和优化策略,这会导致相同代码在不同环境下产生不同大小的类。
示例:
假设一个简单的类:
class MyClass {
public:int a;char b;double c;
};
在这个例子中,MyClass
的大小并非简单的 4 + 1 + 8 = 13 字节。 编译器可能会为了对齐而添加填充字节。 例如,double
通常是8字节对齐,所以 c
可能会被放在地址为8的倍数的位置,导致 a
和 b
之间有填充字节。 最终的大小可能大于13字节。 实际大小需要使用 sizeof(MyClass)
来确定。
总结:
准确计算类的实际大小需要考虑上述所有因素,并且最好通过 sizeof
运算符来实际测量。 不要试图手动计算,因为编译器的优化和对齐规则可能会让你得到错误的结果。 理解这些影响因素有助于你编写更高效的代码,并避免一些潜在的内存管理问题。
2.4、多态的用法
多态主要用于以下场景:
-
消除类型检查的if-else语句: 在没有多态的情况下,你可能需要编写大量的
if-else
语句来处理不同类型的对象。 多态可以简化这种代码,使其更易于阅读和维护。 -
编写更通用的代码: 多态允许你编写能够处理多种对象类型的代码,而无需知道对象的具体类型。 这使得代码更具可重用性和可扩展性。
-
在运行时确定对象类型: 多态允许你在运行时确定对象的实际类型,并根据实际类型执行相应的操作。 这在处理动态数据时非常有用。
-
设计灵活的框架: 多态是设计灵活框架的关键技术,它允许你轻松地添加新的对象类型,而无需修改现有的代码。
2.5、多态的实现
C++中主要通过以下两种方式实现多态:
-
编译时多态(静态多态): 主要通过函数重载和运算符重载实现。 编译器在编译时根据函数的参数类型或运算符选择合适的函数版本。 这是一种静态绑定,在编译时就确定了调用哪个函数。
-
运行时多态(动态多态): 主要通过虚函数实现。 虚函数允许子类重写基类的函数,在运行时根据对象的实际类型选择调用哪个函数。 这是一种动态绑定,在运行时才确定调用哪个函数。
示例(运行时多态):
#include <iostream>class Animal {
public:virtual void makeSound() {std::cout << "Generic animal sound" << std::endl;}virtual ~Animal() = default; // 重要:析构函数也应该声明为虚函数,避免内存泄漏
};class Dog : public Animal {
public:void makeSound() override {std::cout << "Woof!" << std::endl;}
};class Cat : public Animal {
public:void makeSound() override {std::cout << "Meow!" << std::endl;}
};int main() {Animal* animal1 = new Dog();Animal* animal2 = new Cat();animal1->makeSound(); // 输出:Woof!animal2->makeSound(); // 输出:Meow!delete animal1;delete animal2;return 0;
}
在这个例子中,makeSound
是一个虚函数。 animal1
和 animal2
指向不同的对象,但都通过 makeSound()
方法进行调用。 程序在运行时根据对象的实际类型(Dog
或 Cat
)选择正确的 makeSound()
实现。 如果没有 virtual
关键字,则只会调用 Animal
类的 makeSound()
方法。
总结:
多态性是面向对象编程的核心概念,它极大地提高了代码的可重用性、可扩展性和可维护性。 理解编译时多态和运行时多态的区别,以及虚函数在实现运行时多态中的作用,对于编写高质量的C++代码至关重要。 记住在基类中使用虚函数,并在派生类中使用 override
关键字来明确重写基类的虚函数,这有助于提高代码的可读性和可维护性,并避免潜在的错误。
2.6、抽象类
抽象类是面向对象编程中的一个重要概念,主要用于定义接口或规范,而不是提供具体的实现。抽象类不能直接实例化,它们的主要目的是作为其他类的基类,提供一些必须由派生类实现的抽象方法。
抽象类的特点:
-
包含纯虚函数:抽象类至少包含一个纯虚函数。纯虚函数是使用
= 0
定义的,表示该函数没有实现,必须由派生类提供实现。 -
不能实例化:抽象类不能直接创建对象,只能通过派生类创建对象。
-
可以包含普通成员函数和成员变量:除了纯虚函数,抽象类还可以包含普通的成员函数和成员变量,这些成员可以在派生类中使用。
-
提供接口规范:抽象类通常用于定义接口或规范,强制派生类实现某些方法,从而确保一致的行为。
抽象类的用法:
抽象类常用于以下场景:
-
定义接口或规范:当你希望定义一个接口或规范,并强制派生类实现某些方法时,可以使用抽象类。
-
提供部分实现:抽象类可以包含一些通用的实现,这些实现可以被派生类继承和使用。
-
多态性:抽象类可以作为基类,通过指向派生类对象的基类指针或引用,实现多态性,从而在运行时根据对象的实际类型调用相应的方法。
示例:
#include <iostream>// 抽象类
class Shape {
public:// 纯虚函数virtual void draw() = 0;// 普通成员函数void setColor(const std::string& color) {this->color = color;}std::string getColor() const {return color;}private:std::string color;
};// 派生类
class Circle : public Shape {
public:// 实现纯虚函数void draw() override {std::cout << "Drawing a circle with color " << getColor() << std::endl;}
};// 派生类
class Rectangle : public Shape {
public:// 实现纯虚函数void draw() override {std::cout << "Drawing a rectangle with color " << getColor() << std::endl;}
};int main() {Shape* shape1 = new Circle();Shape* shape2 = new Rectangle();shape1->setColor("Red");shape2->setColor("Blue");shape1->draw(); // 输出:Drawing a circle with color Redshape2->draw(); // 输出:Drawing a rectangle with color Bluedelete shape1;delete shape2;return 0;
}
在这个例子中,Shape
是一个抽象类,包含一个纯虚函数 draw()
和一些普通成员函数。Circle
和 Rectangle
是派生类,分别实现了 draw()
方法。通过基类指针调用 draw()
方法,实现了多态性。
总结:
抽象类是定义接口或规范的有力工具,通过包含纯虚函数,强制派生类实现某些方法,从而确保一致的行为。理解抽象类的用法和实现,对于编写高质量的面向对象代码至关重要。
2.7、虚继承
虚继承是一种继承方式,在C++中它允许一个类继承另一个类的部分成员变量和方法,而不需要继承整个类。虚继承使用 virtual
关键字来实现。
虚继承的优点:
-
减少代码重复:虚继承可以减少代码重复,因为一个类可以继承另一个类的部分成员变量和方法,而不需要继承整个类。
-
提高灵活性:虚继承可以提高类的灵活性,因为一个类可以继承多个基类的部分成员变量和方法,从而实现更加复杂的继承关系。
-
提高可维护性:虚继承可以提高类的可维护性,因为一个类可以继承多个基类的部分成员变量和方法,从而使得代码更加易于维护和修改。
虚继承的实现:
虚继承使用 virtual
关键字来实现。在定义基类时,使用 virtual
关键字来声明虚继承关系。例如:
class Base {
public:virtual void foo() { }
};class Derived : public virtual Base {
public:void foo() override { }
};
在上面的示例中,Base
是一个基类,Derived
是一个派生类,它继承自 Base
并override了 foo()
方法。
虚继承的注意事项:
-
虚拟继承关系:虚继承关系只能存在于基类之间,而不能存在于派生类之间。
-
多重继承:虚继承可以实现多重继承,即一个类可以继承多个基类的部分成员变量和方法。
-
虚拟析构函数:如果一个类使用虚继承关系继承了另一个类的析构函数,需要在派生类中override析构函数,以确保正确的析构顺序。
示例:
#include <iostream>class Base {
public:virtual ~Base() { std::cout << "Base destructor" << std::endl; }virtual void foo() { std::cout << "Base foo" << std::endl; }
};class Middle : public virtual Base {
public:virtual ~Middle() { std::cout << "Middle destructor" << std::endl; }void foo() override { std::cout << "Middle foo" << std::endl; }
};class Derived : public Middle {
public:virtual ~Derived() { std::cout << "Derived destructor" << std::endl; }void foo() override { std::cout << "Derived foo" << std::endl; }
};int main() {Derived* derived = new Derived();derived->foo(); // 输出:Derived foodelete derived;return 0;
}
在上面的示例中,Base
是一个基类,Middle
是一个中间类,它继承自 Base
并override了 foo()
方法。Derived
是一个派生类,它继承自 Middle
并override了 foo()
方法。通过输出结果可以看到,虚继承关系正确地调用了析构函数。
2.8、虚析构
虚析构函数是C++中的一个重要概念,用于确保在删除派生类对象时,正确地调用基类和派生类的析构函数。虚析构函数的主要目的是防止内存泄漏和资源未释放的问题。
虚析构函数的作用:
-
确保正确的析构顺序:当你通过基类指针删除派生类对象时,虚析构函数可以确保派生类的析构函数和基类的析构函数都被正确调用。
-
防止内存泄漏:在多态类层次结构中,如果没有虚析构函数,可能会导致派生类的资源未被正确释放,从而引发内存泄漏。
虚析构函数的声明:
在基类中声明虚析构函数的方法是在析构函数前加上 virtual
关键字。例如:
class Base {
public:virtual ~Base() {std::cout << "Base destructor" << std::endl;}
};
示例:
#include <iostream>class Base {
public:virtual ~Base() {std::cout << "Base destructor" << std::endl;}
};class Derived : public Base {
public:~Derived() {std::cout << "Derived destructor" << std::endl;}
};int main() {Base* basePtr = new Derived();delete basePtr; // 输出:Derived destructor Base destructorreturn 0;
}
在这个示例中,Base
类有一个虚析构函数,Derived
类继承自 Base
类。当通过基类指针 basePtr
删除派生类对象时,虚析构函数确保了 Derived
类的析构函数和 Base
类的析构函数都被正确调用。
注意事项:
-
虚析构函数的性能开销:虚析构函数会增加一些性能开销,因为它需要通过虚函数表(vtable)来调用正确的析构函数。
-
非虚析构函数的风险:如果基类没有虚析构函数,而派生类有自己的析构函数,通过基类指针删除派生类对象时,只会调用基类的析构函数,导致派生类的资源未被正确释放。
-
虚析构函数和内存管理:在使用智能指针(如
std::unique_ptr
和std::shared_ptr
)时,虚析构函数也是必须的,以确保正确的内存管理。
总结:
虚析构函数是确保在多态类层次结构中正确释放资源的关键。通过在基类中声明虚析构函数,可以确保派生类的析构函数和基类的析构函数都被正确调用,从而防止内存泄漏和资源未释放的问题。
2.9、限制构造
限制构造是C++中的一个概念,用于限制一个类的实例化过程。限制构造可以通过使用 private
关键字来声明构造函数,使得外部无法访问该构造函数,从而限制实例化该类的对象。
限制构造的方式:
1、private 构造函数:使用 private
关键字来声明构造函数,使得外部无法访问该构造函数。
class MyClass {
private:MyClass() {} // private 构造函数
};
2、prootected 构造函数:使用 protected
关键字来声明构造函数,使得继承类可以访问该构造函数,但外部无法访问。
class MyClass {
protected:MyClass() {} // protected 构造函数
};
3、friend 函数:使用 friend
关键字来声明一个函数,使得该函数可以访问私有构造函数。
class MyClass {
private:MyClass() {}friend MyClass createMyClass(); // friend 函数
};MyClass createMyClass() {return MyClass();
}
限制构造的优点:
-
控制实例化:限制构造可以控制实例化该类的对象,从而避免了未经控制的实例化。
-
提高安全性:限制构造可以提高安全性,因为外部无法访问私有构造函数,从而避免了非法的实例化。
-
简化代码:限制构造可以简化代码,因为不需要在类外部定义构造函数。
限制构造的注意事项:
-
继承:如果一个类继承了另一个类,并且继承类需要访问私有构造函数,需要使用
protected
关键字来声明构造函数。 -
friend 函数:如果使用
friend
关键字来声明一个函数,使得该函数可以访问私有构造函数,需要确保该函数是安全的,以免导致非法的实例化。 -
单例模式:限制构造可以用于实现单例模式,而不是使用
static
关键字来实现单例模式。
示例:
#include <iostream>class MyClass {
private:MyClass() {std::cout << "MyClass constructor" << std::endl;}friend MyClass createMyClass(); // friend 函数
};MyClass createMyClass() {return MyClass();
}int main() {MyClass obj = createMyClass(); // 输出:MyClass constructorreturn 0;
}
在这个示例中,MyClass
类使用私有构造函数来限制实例化过程,并使用 friend
关键字来声明一个函数,使得该函数可以访问私有构造函数。通过使用 createMyClass
函数来实例化 MyClass
对象,可以控制实例化过程。
3、C++设计模式
C++设计模式是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。 它们并非是现成的代码,而是解决特定问题的思路和方案。 以下是一些常见的设计模式,并按类别进行分类:
一、创建型模式 (Creational Patterns): 关注对象的创建方式。
-
单例模式 (Singleton): 保证一个类只有一个实例,并提供一个访问它的全局访问点。 常用在需要全局唯一资源的场景,例如数据库连接池。
-
工厂模式 (Factory Method): 定义一个用于创建对象的接口,让子类决定实例化哪一个类。 这将创建过程延迟到子类。
-
抽象工厂模式 (Abstract Factory): 提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
-
建造者模式 (Builder): 将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。 适合创建复杂对象,步骤繁多。
-
原型模式 (Prototype): 通过复制现有对象来创建新的对象。 适合创建复杂对象,且创建过程耗时。
二、结构型模式 (Structural Patterns): 关注类和对象的组合。
-
适配器模式 (Adapter): 将一个类的接口转换成客户希望的另一个接口。 适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
-
桥接模式 (Bridge): 将抽象部分与它的实现部分分离,使它们都可以独立地变化。
-
组合模式 (Composite): 将对象组合成树形结构以表示“部分-整体”的层次结构。 组合模式使得用户对单个对象和组合对象的使用具有一致性。
-
装饰器模式 (Decorator): 动态地给一个对象添加一些额外的职责。 装饰模式比继承更灵活。
-
外观模式 (Facade): 为子系统中的一组接口提供一个一致的界面,Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
-
享元模式 (Flyweight): 运用共享技术有效地支持大量细粒度的对象。
三、行为型模式 (Behavioral Patterns): 关注类和对象之间的交互模式。
-
策略模式 (Strategy): 定义一系列算法,并将每个算法封装起来,使它们可以相互替换。 策略模式让算法的变化独立于使用算法的客户。
-
模板方法模式 (Template Method): 定义一个操作中的算法骨架,而将一些步骤延迟到子类中。 模板方法模式使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
-
观察者模式 (Observer): 定义对象间的一种一对多依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动刷新。
-
迭代器模式 (Iterator): 提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。
-
责任链模式 (Chain of Responsibility): 使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它为止。
-
命令模式 (Command): 将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。
-
状态模式 (State): 允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
-
访问者模式 (Visitor): 表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作。
-
中介者模式 (Mediator): 定义一个对象,该对象封装一系列对象的交互。中介者模式使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
-
解释器模式 (Interpreter): 给定一个语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。
这并非所有设计模式的完整列表,但涵盖了最常用和最重要的模式。 选择哪种模式取决于具体的应用场景和需求。 理解这些模式的优缺点,以及它们之间的关系,对于编写高质量的C++代码至关重要。 记住,设计模式是指导原则,而不是死板的规则,需要根据实际情况灵活运用。