目录
1.1当你需要表示两个类之间的“是一个”(IS-A)关系时,继承是合适的选择。例如:
1.3继承与虚函数结合使用,可以实现多态性,这使得你可以通过基类指针或引用调用派生类的函数。例如:
1.什么情况下使用继承方式
1.1当你需要表示两个类之间的“是一个”(IS-A)关系时,继承是合适的选择。例如:
- 动物类和具体动物类:假设你有一个基类
Animal
,它有一些通用的属性和方法(如eat()
和sleep()
)。然后你可以创建派生类,如Dog
和Cat
,它们是Animal
的特殊化。
class Animal {
public:
void eat() { /* 实现 */ }
void sleep() { /* 实现 */ }
};
class Dog : public Animal {
public:
void bark() { /* 狗的吠叫实现 */ }
};
class Cat : public Animal {
public:
void meow() { /* 猫的叫声实现 */ }
};
1.2代码重用
如果多个类有相同的代码或功能,你可以将这些共同的功能放在一个基类中,其他类继承这个基类,以重用代码。例如:
- 形状类和具体形状类:假设你有一个基类
Shape
,它包含公共的绘制功能和计算面积的函数,具体形状(如Circle
、Rectangle
)可以继承Shape
,重用公共功能,并实现特定的计算方法。
class Shape {
public:
virtual void draw() = 0; // 纯虚函数
virtual double area() const = 0; // 纯虚函数
};
class Circle : public Shape {
public:
Circle(double r) : radius(r) {}
void draw() override { /* 绘制圆形 */ }
double area() const override { return 3.14 * radius * radius; }
private:
double radius;
};
class Rectangle : public Shape {
public:
Rectangle(double w, double h) : width(w), height(h) {}
void draw() override { /* 绘制矩形 */ }
double area() const override { return width * height; }
private:
double width, height;
};
1.3继承与虚函数结合使用,可以实现多态性,这使得你可以通过基类指针或引用调用派生类的函数。例如:
- 基类指针调用派生类方法:如果你有一个基类
Shape
和多个派生类,你可以通过Shape
指针调用派生类的实现,而不需要知道具体的派生类。
void drawShape(const Shape& shape) {
shape.draw(); // 调用派生类的 draw 方法
}
int main() {
Circle c(5);
Rectangle r(4, 6);
drawShape(c); // 绘制圆形
drawShape(r); // 绘制矩形
return 0;
}
1.4扩展现有类
当你需要在现有类的基础上增加功能而不改变原有类的实现时,继承非常有用。例如:
- 扩展功能:如果你有一个已有的
Logger
类,你可以创建一个派生类FileLogger
,在Logger
的基础上增加文件日志记录的功能。
class Logger {
public:
virtual void log(const std::string& message) = 0;
};
class FileLogger : public Logger {
public:
void log(const std::string& message) override {
// 将日志写入文件
}
};
1.5实现接口
在 C++ 中,接口通常是通过包含纯虚函数的基类来实现的。派生类必须实现这些纯虚函数。这种方法可以确保派生类遵循某种协议或接口。
class IRenderable {
public:
virtual void render() const = 0; // 纯虚函数
};
class Button : public IRenderable {
public:
void render() const override { /* 渲染按钮 */ }
};
class TextBox : public IRenderable {
public:
void render() const override { /* 渲染文本框 */ }
};
1.6扩:继承与派生的区别
“继承”和“派生”通常指的是同一个概念,派生类从基类继承属性和方法。然而,在不同的上下文中,这两个术语可能有细微的区别:
继承(Inheritance):是一个更广泛的概念,指的是类从另一类继承功能。它允许派生类复用基类的代码并扩展或修改其行为。
派生(Derivation):通常指的是从基类创建新类的过程。它强调的是从基类中衍生出新的类。这是继承的具体应用,常用于描述继承关系的具体实现或子类的创建过程。
2.如何理解多态?
多态——允许不同类的对象通过相同的接口实现不同的操作
从而提高代码的灵活性和可扩展性。
- 编译时多态(静态多态):包括函数重载和类模板函数模板。
- 运行时多态(动态多态):通过虚函数和继承实现。
2.1静态多态
2.1.1函数重载
同一作用域声明多个功能类似的重名函数,具有不同的参数列表
注:不能通过返回值来区别重载
原因:编译器通过看参数类型确定调用func(int)
的哪一个版本,而不考虑返回类型。
2.1.2运算符重载
为自定义类定义运算符的行为
class Complex {
public:
Complex(double r, double i) : real(r), imag(i) {}
Complex operator + (const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}
void display() const {
std::cout << "Complex number: " << real << " + " << imag << "i" << std::endl;
}
private:
double real, imag;
};
Complex c1(1.0, 2.0);
Complex c2(3.0, 4.0);
Complex c3 = c1 + c2;
c3.display(); // Output: Complex number: 4.0 + 6.0i
2.1.3 类模板和函数模板
函数模板:
定义一个通用的函数形式,在调用时编译器根据实际类型的参数,生成具体函数
//定义通用模板 template <typename T> T add(T a, T b) { return a + b; } //使用 int result1 = add(5, 10); // 整数相加 double result2 = add(5.5, 2.3); // 浮点数相加
类模板://定义通用模板 template <typename T> class Box { public: Box(T value) : value(value) {} T getValue() const { return value; } private: T value; }; //使用 Box<int> intBox(123); Box<std::string> strBox("Hello");
2.1.4扩: 编译过程
预编译:头文件中的函数声明拷贝到源文件,避免编译过程找不到函数名
编译:语法分析+符号汇总(函数名)
汇总:生成函数名到函数地址的映射,方便通过函数名找到函数定义位置
链接:多个文件表中的符号汇总
2.2运行时多态
2.2.1通过虚函数和基类指针\引用 实现的
基类中声明虚函数,函数前+virtual,使得子类可以重写这些函数,实现动态绑定。在编译过程生成他们的虚函数表,在运行时通过基类指针或引用调用子类中重写的方法。
class Base {
public:
virtual void show() const {
std::cout << "Base class" << std::endl;
}
virtual ~Base() = default; // 虚析构函数,确保正确删除派生类对象
};
class Derived : public Base {
public:
void show() const override {
Base::show();//子类调用父类方法
std::cout << "Derived class" << std::endl;
}
};
int main() {
Base* b = new Derived();
b->show(); // Output: Derived class
delete b; // 正确调用 Derived 的析构函数
return 0;
}
3.虚函数、纯虚函数、虚函数表、虚函数指针
3.1虚函数(多态中说了)
3.2纯虚函数
是一个在基类中声明但没有实现的虚函数,其目的是为了让基类成为抽象类,不能被实例化。它通过在函数声明的末尾添加
= 0
来实现。纯虚函数要求派生类必须实现这个函数才能实例化派生类对象
问题:让基类成为抽象类,不能被实例化,这样做的目的是什么?
将基类定义为抽象类的主要目的是为了创建一个接口或蓝图,强制派生类实现特定的功能。这种设计确保了:
- 接口定义: 抽象类定义了一组必须由派生类实现的方法,确保一致的接口。
- 代码重用: 基类可以包含共享的代码和数据,供派生类复用,而派生类实现具体细节。
- 设计灵活性: 抽象类允许你定义通用行为和强制具体实现,从而灵活地扩展系统。
3.3虚函数表
3.3.1创建
- 每个包含虚函数的类都有一个虚函数表。这个表格存储了该类的所有虚函数的地址(指针)。
- 当一个对象的虚函数被调用时,程序通过对象的虚函数表来找到实际应该调用的函数地址。
- 虚函数表通常存储在程序的全局或静态内存区域。它们在程序加载时创建,并在程序运行期间保持存在。
- 虚函数表的内容是在编译时生成的,通常被放在数据段或只读数据段中,因为虚函数表是只读的,不会在运行时改变。
- 每个对象实例包含一个指向虚函数表的指针(通常称为 vptr)。这个指针在对象的内存布局中存储,通常在对象的开头或紧接在对象的其他数据成员之后。
- vptr 指向与对象的实际类型对应的虚函数表,使得对象能够通过虚函数表找到实际的虚函数实现。
3.3.2内容
- 虚函数表是一个数组,其中每个元素是一个指向虚函数的指针。对于每个类,虚函数表会按照该类定义的虚函数的顺序存储这些指针。
- 如果类重写了基类的虚函数,那么虚函数表中的相应条目会更新为指向派生类的实现。
3.3.3vptr
- 每实例化一个对象,生成一个vptr,指向虚函数表。
//实例化 Drived *d = new Drived; //子类指针接收对象,就会去子类对象对应的虚函数表中找到函数去调用 Base *b = new Drived; //创建的还是子类对象,还是去子类对象对应的虚函数表中找到函数去调用
- 和对象存放在一起
Box b; //栈 通过new->堆上 C++内存划分: 栈 堆 全局静态区 只读数据段 代码区
- 当调用虚函数时,程序通过对象的 vptr 查找对应的虚函数表,并使用虚函数表中的地址调用实际的函数。
3.3.4优点和局限性
优点:
- 支持多态: 虚函数表机制允许基类指针或引用在运行时调用派生类的函数实现,从而支持运行时多态性。
- 灵活性: 允许在程序运行时动态决定调用哪个函数实现,从而提供了更大的灵活性和扩展性。
局限性:
- 开销: 虚函数表机制引入了一定的开销,包括额外的内存开销(存储虚函数表和 vptr)和运行时开销(通过 vptr 查找函数地址)。
- 性能: 虚函数调用比普通函数调用慢,因为需要通过 vptr 查找函数地址。
3.4为什么构造函数不能是虚函数?
构造函数不能是虚函数是因为虚函数的机制依赖于对象的完整构造和虚表的建立。
构造函数在对象的创建过程中被调用,此时对象还未完全构造完成,虚表还未设置。因此,虚函数的机制无法正常工作。
虚函数需要一个完整的对象实例来进行动态绑定,而构造函数是在虚表建立之前调用的,这使得在构造函数中使用虚函数不切实际。
3.5析构函数为什么是虚函数
析构函数是虚函数以确保在通过基类指针删除派生类对象时,派生类的析构函数也能被正确调用。这是关键的,
如果析构函数不是虚函数,基类指针在删除对象时只会调用基类的析构函数,从而导致派生类的资源无法正确释放,可能导致资源泄漏或未定义行为。通过将析构函数声明为虚函数,可以确保正确的析构顺序,先调用派生类的析构函数,再调用基类的析构函数。
4.进程、线程
4.1进程线程区别
定义:
进程:资源分配
线程:调度
开销:
进:创建、销毁开销大,间通信复杂
线:小、简单
内存空间:
- 多进程:每个进程有自己的独立内存空间。
- 多线程:所有线程共享同一进程的内存空间。
通信机制:
- 多进程:需要通过 IPC 机制进行通信。
- 多线程:可以通过共享内存直接通信。
资源隔离:
- 多进程:资源隔离较好,一个进程崩溃不会影响其他进程。
- 多线程:资源共享较多,一个线程崩溃可能会影响整个进程。
适用场景:
- 多进程:适用于需要高度隔离的任务,如分布式系统、服务端程序等。
- 多线程:适用于需要紧密协作的任务,如桌面应用、游戏等。
4.2进程间通信方式
1. 管道(Pipes)
无名管道(Anonymous Pipe):
- 用于父子进程之间的通信。
- 管道在创建时提供一个读端和一个写端,数据从写端写入,可以从读端读取。
- 无名管道的生命周期与创建它的进程相关。
命名管道(Named Pipe):
- 允许不相关的进程之间的通信。
- 使用一个名称来标识管道,进程可以通过这个名称打开管道进行通信。
- 命名管道在系统中存在,并且可以在进程之间共享。
2. 消息队列(Message Queues)
- 允许进程以消息的形式进行通信。
- 消息队列是由内核管理的,进程可以向队列中写入消息,也可以从队列中读取消息。
- 支持消息的优先级,可以实现异步通信。
3. 共享内存(Shared Memory)
- 允许多个进程直接访问同一块内存区域。
- 进程需要先创建一个共享内存区域,然后通过映射到各自的地址空间来访问。
- 共享内存通常配合信号量等同步机制来协调对共享内存的访问,避免竞争条件。
4. 信号量(Semaphores)
- 用于进程间同步,以控制对共享资源的访问。
- 信号量可以用来实现进程间的排队和互斥机制。
- 在使用共享内存时,通常配合信号量使用,以防止数据竞争和不一致。
————————————————————————————————5. 套接字(Sockets)
- 套接字是网络编程中常用的通信机制,也可以用于同一台机器上的进程间通信。
- 支持不同主机和不同协议(如TCP/IP、UDP)的通信。
- 可以用来实现网络通信,也可用于本地进程间通信。
6. 信号(Signals)
- 用于向进程发送异步通知。
- 信号可以用于通知进程发生了某种事件,但不用于传递大量数据。
- 常用于进程间的简单控制和状态通知。
4.3线程间通信方式
1. 共享变量
- 线程可以直接读取和写入共享内存中的变量。
- 需要使用同步机制(如互斥锁、读写锁)来避免竞争条件和数据不一致。
2. 互斥锁(Mutexes)
- 用于保护共享资源,防止多个线程同时访问。
- 确保在任意时刻只有一个线程可以访问共享资源。
3. 条件变量(Condition Variables)
- 允许线程在特定条件下进行等待和通知。
- 线程可以在条件变量上等待,直到另一个线程发出通知或信号。
4. 读写锁(Read-Write Locks)
- 允许多个线程同时读取共享资源,但在写操作时会进行互斥。
- 提高了读操作的并发性,适用于读多写少的场景。
5. 信号量(Semaphores)
- 用于控制对共享资源的访问,通过设置计数器来管理线程的访问权限。
- 可用于实现互斥和同步机制。
6. 线程安全队列
- 提供线程安全的队列结构,允许线程间的安全数据传递。
- 常用于生产者-消费者模式。
7. 事件(Events)
- 用于线程间的信号传递和同步。
- 一个线程可以设置事件,其他线程可以等待这个事件的触发。
8. 消息传递
- 线程通过消息队列或其他消息传递机制进行通信。
- 可以实现异步通信和解耦。
9. 原子操作(Atomic Operations)
- 支持对基本数据类型的原子操作,避免使用传统的锁机制。
- 适用于需要简单、快速同步的场景,如计数器的增加或减少。
4.4信号和信号量的区别
都是用于进程或线程间同步和通信的机制
信号(Signals):
- 概念: 信号是一种异步事件通知机制。它允许进程或线程在接收到特定的事件时被通知。
- 用途: 主要用于通知进程或线程某种事件的发生,例如中断、定时器到期、非法操作等。信号一般不用于数据传输,而是用于进程间的简单控制和状态通知。
- 应用场景: 可以用于处理进程的终止请求、执行定时任务、处理特定的异常等。
信号量(Semaphores):
- 概念: 信号量是一种用于协调对共享资源访问的同步机制。它基于计数器实现,防止竞争条件和死锁,实现互斥和同步方面进行控制。
- 应用场景: 适用于实现生产者-消费者模式、读写锁、资源池等。