cpp八股文
public、protected、private三者有什么联系、区别?
public(公有):
成员可以在类的外部和内部访问。
成员在类的继承链中保持其可见性,派生类可以访问基类的公有成员。
成员通常用于定义类的公共接口,即其他代码可以访问的类成员。protected(保护):
成员可以在类的内部和派生类中访问,但不能在类的外部访问。
成员通常用于实现继承的接口和内部实现细节,以便在派生类中重用。
成员对于类的用户来说是不可见的,只有派生类可以访问它们。private(私有):
成员只能在类的内部访问,不能在类的外部或派生类中访问。
成员通常用于类的实现细节,以隐藏内部数据和实现细节。
成员对于类的用户和派生类都是不可见的。
类的静态成员函数有什么特性?
- 静态成员函数不依赖于类的实例,因此可以在没有类对象的情况下调用。它们属于类本身而不是类的实例。这意味着你可以通过类名直接调用静态成员函数,而不需要创建类的对象。
- 静态成员函数只能访问类的静态成员(静态数据成员和静态函数),不能访问非静态成员(普通数据成员和普通成员函数)。这是因为静态成员函数不关联于任何特定的类对象,因此无法访问实例相关的数据。
- 静态成员函数可以具有公有、保护或私有的访问级别,就像普通成员函数一样。这意味着它们可以被外部代码访问,也可以在派生类中被重写。
- 静态成员函数通常与静态数据成员一起使用,因为它们都与类本身相关,而不是与类的实例相关。静态成员函数可以用于操作和管理静态数据成员
空类的大小?
空类的话就是类中没有任何数据成员和成员函数。空类的大小取决于编译器和平台,但通常情况下,空类的大小为1字节。
这1字节的大小通常用于标识对象在内存中的位置,以确保不同的对象具有不同的地址。
所以即使是空类,也必须在内存中占用至少一个字节。
空类默认生成哪几个成员函数?
- 默认构造函数:如果你没有显式定义构造函数,编译器会为你生成一个默认构造函数。这个构造函数不接受任何参数,用于创建类的对象。默认构造函数会执行默认的对象初始化。
- 析构函数:如果你没有显式定义析构函数,编译器会为你生成一个默认的析构函数。这个析构函数用于销毁类的对象,释放对象所占用的资源。默认析构函数通常是空的,不执行特定的清理工作。
- 拷贝构造函数:如果你没有显式定义拷贝构造函数,编译器会为你生成一个默认的拷贝构造函数。这个构造函数用于创建一个对象作为另一个对象的副本。默认的拷贝构造函数执行成员逐一拷贝,适用于大多数情况,但可能不适用于包含动态分配内存的类。
- 拷贝赋值运算符:如果你没有显式定义拷贝赋值运算符,编译器会为你生成一个默认的拷贝赋值运算符。这个运算符用于将一个对象的内容复制到另一个对象,通常与拷贝构造函数类似,执行成员逐一拷贝。
c++ 5种构造函数
- 默认构造
Student();//没有参数 - 普通构造函数
Student(int num,int age);//有参数 - 复制(拷贝)构造函数
Student(Student&);//形参是本类对象的引用 - 转换构造函数
Student(int r) ;//形参时其他类型变量,且只有一个形参 - 移动构造函数
Str(Str &&s)//移动构造函数
下面这样是只是调用了”=”的重载,并没有调用拷贝构造函数
1 |
|
结构体和类有什么区别?
- 默认访问控制:
- 结构体:结构体的成员默认为公有(public),这意味着结构体的数据成员可以在结构体外部直接访问。
- 类:类的成员默认为私有(private),这意味着类的数据成员默认情况下不能在类外部直接访问。
- 成员函数的默认属性:
- 结构体:结构体可以包含成员函数,但这些成员函数默认为公有(public)。
- 类:类中的成员函数默认为私有(private)。
- 设计目的:
- 结构体:结构体通常用于组织一组相关的数据,以便轻松地进行数据的打包和传递。结构体的主要目的是数据的聚合。
- 类:类用于封装数据和操作数据的方法,实现面向对象编程的概念。类的主要目的是数据和行为的封装,支持数据隐藏和信息隐藏。
- 默认构造函数:
- 结构体:如果你没有显式定义构造函数,结构体会有一个默认构造函数,但不会初始化成员变量。
- 类:如果你没有显式定义构造函数,类会有一个默认构造函数,但不会初始化成员变量。不过,如果类包含了非静态的 const 数据成员,那么默认构造函数会对这些成员进行初始化。
- 默认析构函数:
- 结构体:如果你没有显式定义析构函数,结构体会有一个默认析构函数,但不执行任何清理工作。
- 类:如果你没有显式定义析构函数,类会有一个默认析构函数,但不执行任何清理工作。不过,如果类包含了资源(如动态分配内存),你可能需要自定义析构函数来正确释放这些资源。
map的[]和find两者有什么区别?
- 返回值类型:
[] 运算符:返回与指定键相关联的值,如果键不存在,则会插入一个新键值对,该值的类型为该 map 的值类型,并且可以用于读取和写入值。
find 函数:返回一个迭代器,指向与指定键关联的元素(键值对)或者指向 map 的 end() 迭代器,如果键不存在。 - 行为差异:
[] 运算符:如果使用 [] 访问一个不存在的键,则会在 map 中插入一个新键值对,键的值由值类型的默认构造函数确定(通常为零初始化)。如果键已存在,则会返回与该键相关联的值,并且可以使用 [] 来修改该值。
find 函数:find 函数只用于查找元素,如果键不存在,它会返回 map 的 end() 迭代器,不会插入新元素。 - 异常差异:
[] 运算符:不会引发异常,如果键不存在,它会插入一个新元素。
find 函数:不会引发异常,如果键不存在,它会返回 map 的 end() 迭代器。 - 使用场景:
[] 运算符通常用于在明确知道键存在的情况下访问元素,或者在需要插入新元素并设置其值时使用。
find 函数通常用于检查键是否存在,然后根据情况采取进一步的操作。
迭代器中如何删除一个迭代器又能保证后续迭代器不失效呢?
- 使用迭代器的返回值:在执行删除操作后,将迭代器的返回值(指向被删除元素的下一个元素的迭代器)赋值给原始迭代器,从而更新它。这样做可以确保后续迭代器不会失效。
1 |
|
示例中,如果满足某个条件,我们使用 erase 函数删除了元素,并将返回的迭代器赋值给 it,以确保后续迭代器仍然有效。
- 使用后缀自增运算符:可以使用后缀自增运算符 it++ 而不是前缀自增运算符 ++it 来移动迭代器,因为后缀自增运算符返回的是迭代器的副本,而不是引用,这样可以避免潜在的问题。
1 |
|
示例中,我们在删除元素之后使用 it++ 来继续移动迭代器,确保后续迭代器仍然有效。
shared_ptr内部的实现原理
- 引用计数:shared_ptr 内部维护一个引用计数,用于跟踪有多少个 shared_ptr 共享同一个堆内存资源。每当创建一个新的 shared_ptr 对象来管理特定的堆内存资源时,引用计数就会增加1。当 shared_ptr 被销毁或者不再引用这个堆内存资源时,引用计数会减少1。当引用计数变为0时,表示没有任何 shared_ptr 对象在管理这个堆内存资源,此时会自动释放(delete)这个资源,避免内存泄漏。
- 资源管理:shared_ptr 不仅维护了引用计数,还在内部持有一个指向堆内存资源的指针。这个指针指向被管理的对象或数组。当 shared_ptr 的构造函数被调用时,它会接收一个裸指针(通常是通过 new 创建的)或另一个 shared_ptr,并将这个指针保存在内部。
- 拷贝构造和赋值操作:当你将一个 shared_ptr 赋值给另一个 shared_ptr 或者使用拷贝构造函数创建一个新的 shared_ptr 时,引用计数会增加1,并且这两个 shared_ptr 对象都指向相同的堆内存资源。这是因为它们共享同一个引用计数。当其中一个 shared_ptr 被销毁或不再引用资源时,引用计数会减少1。
- 释放资源:当 shared_ptr 的引用计数减少到0时,表示没有任何 shared_ptr 对象再引用这个堆内存资源。此时,shared_ptr 内部会自动释放这个资源,调用 delete(或者适用于数组的 delete[])来销毁对象或释放数组。这确保了资源的正确释放,避免了内存泄漏。
- 弱引用计数:除了引用计数,shared_ptr 还维护了一个弱引用计数。弱引用计数用于 weak_ptr,它是另一种智能指针,允许观察(但不拥有)与 shared_ptr 共享的资源。弱引用计数跟踪有多少个 weak_ptr 共享相同的资源,但不影响资源的生命周期。只有当所有的 shared_ptr 都销毁时,资源才会被释放。
引用和指针有什么区别?
语法和声明:
引用:引用是一个别名,使用 & 符号声明,通常在初始化时就要指定引用的目标对象,且一旦引用被初始化,就无法再重新绑定到其他对象。1
2int x = 10;
int &ref = x; // 引用ref绑定到x指针:指针是一个变量,用于存储另一个对象的内存地址,使用 * 符号声明,可以在任何时候改变指针所指向的对象。
1
2int x = 10;
int *ptr = &x; // ptr指向x的地址空引用和空指针:
- 引用:引用在声明时必须初始化,不存在空引用的概念。如果试图创建一个未初始化的引用,会导致编译错误。
- 指针:指针可以声明而不初始化,形成空指针。空指针的值通常为 nullptr 或 NULL。
- 对象绑定:
- 引用:引用在初始化时必须绑定到一个对象,并且一旦绑定,无法再更改其绑定对象。这使得引用在函数参数传递和返回值上非常有用,因为它们可以提供更直观的语法。
- 指针:指针可以在任何时候指向不同的对象,因此更灵活。这也使得指针在动态内存分配和数组操作等场景中非常有用。
- 操作符和语法:
- 引用:引用使用 . 运算符来访问对象的成员(对于类和结构体),并且不需要使用间接解引用运算符 *。
- 指针:指针使用 -> 运算符来访问对象的成员,或者使用 * 运算符来间接访问指向的对象。
- 空值处理:
- 引用:引用不能表示空值或空对象,因为引用必须在初始化时绑定到一个有效的对象。
- 指针:指针可以为空(nullptr 或 NULL),表示不指向任何有效的对象,这在处理可能不存在的对象时很有用。
- 传递方式
- 引用:通过引用传递参数可以实现按引用传递,允许函数修改原始对象的值。这通常用于避免拷贝大型对象。
- 指针:通过指针传递参数可以实现按指针传递,允许函数修改原始对象的值。指针传递需要显式地使用指针运算符 * 来访问对象。
POSIX线程库(pthread)
POSIX线程库,通常简称为pthread,是一套用于多线程编程的标准接口,最初定义在POSIX.1标准中。它提供了一组函数和数据类型,用于创建、管理和同步线程,允许程序在多个并发执行的线程中进行任务分配和协调。
使用pthread库可以实现多线程的目标,如提高程序的性能、增加并发性和响应能力、利用多核处理器等。下面是pthread库的一些主要特点和功能:
- 线程创建和管理:pthread库提供了函数,如pthread_create()和pthread_join(),用于创建和管理线程。开发人员可以使用这些函数来创建新的线程,并等待线程的结束。
- 线程同步:pthread库提供了一系列的同步原语,如互斥锁(mutex)、条件变量(condition variable)和信号量(semaphore)。通过这些同步机制,可以实现线程之间的互斥、同步和协作,以避免竞态条件和其他并发问题。
- 线程控制:pthread库支持线程的属性设置和控制。例如,可以设置线程的优先级、栈大小和调度策略等。此外,还可以通过函数pthread_exit()和pthread_cancel()来终止线程的执行。
- 线程局部存储:pthread库允许每个线程具有自己独立的线程局部存储(thread-local storage)。这意味着每个线程都有自己的变量副本,这些变量对于其他线程是不可见的,可以提供线程间的数据隔离。
- 线程间通信:pthread库提供了一些函数用于线程间的通信。例如,通过条件变量(condition variable)可以实现线程的等待和唤醒操作,通过互斥锁(mutex)可以实现线程的互斥访问共享资源。
总体而言,POSIX线程库(pthread)是一个功能强大且广泛支持的多线程编程接口,被广泛应用于各种操作系统和编程语言中。它为开发人员提供了一套标准化的工具和机制,以便高效地编写并发程序,并处理多线程之间的同步和通信问题。
1 |
|
线程注意事项
- 其他线程保留,主线程退出用pthread_exit;
- 避免僵尸线程考虑使用:
pthread_join
pthread_detach
pthread_create指定分离属性; - malloc和mmap的内存其他线程可以使用;
- 应多避免多线程调用fork,除非马上exec,子进程中只有调用fork的线程存在,其他线程都已退出;
- 信号的复杂语义很难和多线程共存,应避免多线程引入信号机制,因为信号来了哪个线程先抢到就是它处理,各线程有独立的mask屏蔽字,处理方式是共享的,很难指定哪个线程处理。
什么时候用条件变量,什么时候用线程锁?
条件变量(Condition Variable):条件变量用于线程之间的协作,它允许线程等待某个条件的发生,然后在条件满足时被唤醒。条件变量通常与线程锁一起使用,以实现更复杂的同步需求。条件变量主要用于以下情况:
- 等待事件发生:当一个线程需要等待某个条件变为真时,它可以进入等待状态,释放锁,然后等待其他线程通过条件变量发出信号通知它条件已经满足。
- 通知其他线程:当一个线程完成某个任务并且条件已满足时,它可以通过条件变量发送信号通知其他线程,以唤醒它们并使它们能够执行相关操作。
- 避免忙等待:条件变量允许线程进入休眠状态,而不是忙等待某个条件的满足,这可以节省CPU资源。
- 线程间的协作:条件变量常用于多线程之间的协作,例如生产者-消费者问题,其中生产者线程通知消费者线程何时可以消费数据。
线程锁(Mutex):线程锁是一种互斥机制,用于保护共享资源,确保一次只有一个线程可以访问共享资源。线程锁主要用于以下情况:
- 保护共享资源:当多个线程需要访问共享资源(如共享变量或数据结构)时,可以使用线程锁来确保在任何给定时间只有一个线程可以修改或访问资源,从而防止竞态条件。
- 临界区保护:临界区是一段代码,只能由一个线程同时执行。线程锁可以用来标记临界区,以确保同一时刻只有一个线程可以进入临界区执行代码。
- 避免数据竞争:线程锁可以防止数据竞争,当多个线程尝试同时写入或读取共享数据时,可以使用锁来协调它们的操作,避免数据不一致性。
区别和使用场景:
- 使用线程锁是为了保护共享资源的互斥访问,以防止数据竞争和并发问题。线程锁通常与临界区一起使用,以限制同时访问共享资源的线程数量。
- 使用条件变量是为了实现线程之间的协作和等待,以满足某个特定条件。条件变量通常与线程锁一起使用,以确保在等待条件时释放锁,以允许其他线程修改共享资源。
进程间的通信方式?
- 管道(Pipe):管道是一种半双工的IPC方式,用于在父进程和子进程之间传输数据。管道通常用于具有父子关系的进程之间,其中一个进程将数据写入管道,而另一个进程从管道中读取数据。
- 命名管道(Named Pipe,FIFO):命名管道是一种允许不具有亲缘关系的进程之间通信的方式。它是一种特殊的文件,可以在文件系统中创建,并通过文件名来访问。
- 消息队列(Message Queue):消息队列是一种进程间通信方式,允许不同进程之间发送和接收消息。消息队列通常用于实现进程之间的异步通信。
- 共享内存(Shared Memory):共享内存允许多个进程访问相同的物理内存区域,因此它是一种高效的IPC方式。但需要谨慎处理同步问题,以避免数据竞争。
- 信号(Signal):信号是一种轻量级的IPC方式,用于通知进程发生了某个事件。信号通常用于处理异步事件,如进程终止或某个条件的发生。
- 套接字(Socket):套接字是一种网络编程中常用的IPC方式,允许不同主机上的进程之间进行通信。套接字通常用于实现分布式应用程序。
- 文件锁(File Lock):文件锁是一种通过文件系统进行IPC的方式,允许多个进程协调对共享文件的访问。
- 信号量(Semaphore):信号量是一种计数器,用于控制多个进程对共享资源的访问。它可以用于解决资源分配和同步的问题。
- 共享文件(Shared File):多个进程可以通过共享文件来进行通信。这通常需要一种协议来确保对文件的互斥访问。
- RPC(远程过程调用)和RMI(远程方法调用):RPC和RMI允许一个进程调用另一个进程中的函数或方法,从而实现远程通信。
tcp和udp有什么区别?
- 连接性:
TCP是面向连接的协议。在建立通信之前,TCP会建立一个连接,确保数据的可靠传输,然后在通信结束后关闭连接。这种连接性保证了数据的可靠性,但会引入一定的延迟。
UDP是无连接的协议。它不会建立连接,直接将数据包发送到目标,不保证数据的可靠性,但具有低延迟的优势。 - 数据可靠性:
TCP提供数据的可靠传输。它使用确认机制和重传来确保数据的完整性和可靠性。如果数据包在传输过程中丢失或损坏,TCP会重新发送丢失的数据。
UDP不提供数据的可靠性。它将数据包发送出去,但不保证它们的到达。丢失、重复或无序的数据包在UDP中是常见的,应用程序需要自行处理。 - 流量控制:
TCP具有流量控制机制,可以根据接收端的处理能力来调整数据的发送速率,以避免过载。
UDP没有流量控制机制,发送方将数据包发送出去,不考虑接收方的处理速度,可能导致网络拥塞。 - 顺序性:
TCP保持数据的顺序性。发送的数据包将按照发送顺序在接收端被重建。
UDP不保证数据包的顺序性。数据包可能以不同的顺序到达接收端。 - 头部开销:
TCP头部较大,包含许多控制信息,这增加了数据包的大小。
UDP头部较小,只包含少量的必要信息。 - 适用场景:
TCP适用于需要可靠数据传输的应用程序,如网页浏览、电子邮件、文件传输等。
UDP适用于对数据传输延迟要求较高的应用程序,如音频和视频流、在线游戏等。
当网络不稳定的时候对网络数据包有什么影响?
- 丢包(Packet Loss):网络不稳定时,数据包可能会丢失,这意味着它们在传输过程中消失了。丢包可以导致数据的不完整性,需要在应用层进行处理,通常通过协议的重传机制来恢复丢失的数据。
- 延迟(Latency):网络不稳定可能导致数据包的传输延迟增加。延迟是数据从发送端到接收端所需的时间,高延迟可能会影响实时性要求高的应用,如实时通信或在线游戏。
- 抖动(Jitter):抖动是指数据包在传输过程中的延迟不稳定,导致数据包以不规则的时间间隔到达。抖动对音视频流或实时通信应用尤其有害,因为它会导致声音或图像的不连贯性。
- 带宽限制(Bandwidth Limitation):网络不稳定可能导致带宽限制,即可用带宽变小。这会影响数据传输速度,导致数据包传输变得更慢。
- 重排序(Packet Reordering):在不稳定的网络中,数据包的到达顺序可能会被打乱,需要在接收端进行重排序,以确保数据的正确顺序。
- 连接中断(Connection Drops):网络不稳定可能导致连接中断,即通信双方之间的连接被中断。这需要重新建立连接,并可能导致数据丢失。
- 冲突和碰撞(Collisions):在某些情况下,网络不稳定可能导致数据包冲突和碰撞,这会损坏数据包并导致丢失。
- 协议调整(Protocol Adaptation):有些应用程序和协议可以通过自适应机制来适应网络不稳定,例如调整数据传输速率或采用错误纠正机制来处理丢失的数据包。