C++问题总结

文章目录

  • C++
    • 指针和引用的区别:
    • 悬空指针和野指针
    • 如何避免悬空指针和野指针
    • 多态
    • 析构函数可以为虚函数吗,为何建议为虚函数
    • C++程序的编译过程可以分为四个主要阶段
    • 线程池有死锁问题吗,死锁了解多少,递归锁知道吗
    • 常见的几种锁
  • 网络
    • 什么是TCP
    • TCP报文头部结构
    • TCP 三次握手和四次挥手过程
    • 为什么TCP连接的时候是3次?2次不可以吗?
    • 为什么客户端发出第四次挥手的确认报文后要等2MSL的时间才能释放TCP连接?
    • 为什么TCP连接的时候是3次,关闭的时候却是4次?
    • 在 TCP/IP 网络中,当发生丢包时,TCP 协议如何处理丢失的数据包?
  • QT
    • 信号和槽的机制
    • 信号和槽的优点

C++

指针和引用的区别:

1定义和初始化:

  • 指针(Pointer)是一个变量,用于存储另一个变量的内存地址。指针需要通过取地址操作(&)来获取变量的地址,并使用指针运算符(*)来访问该地址处的值。指针可以在声明时不进行初始化。
  • 引用(Reference)是一个别名,用于给已存在的变量起另一个名称。引用必须在声明时进行初始化,并且一经初始化后,无法再改变引用的绑定目标。

2空值:

  • 指针可以具有空值(null),即指向空地址或空对象的指针。可以将指针设置为 nullptr 或 0 来表示空指针状态。
    引用不能有空值,它必须始终引用一个有效的对象。

3内存管理:

  • 指针可以进行指针运算,可以通过指针进行内存的动态分配和释放,例如使用 new 和 delete 运算符。
  • 引用不进行指针运算,它只是给变量起了一个别名,不拥有自己的内存空间。引用的创建和销毁由编译器自动处理。

4重新绑定:

  • 指针可以重新指向不同的对象,可以通过改变指针的值来改变所指向的对象。
  • 引用一旦绑定到一个对象,就无法重新绑定到其他对象。它始终引用同一个对象,无法改变引用的绑定目标。

5 空间大小:

  • 指针本身需要占用一定的内存空间,通常是与系统的地址大小相关的固定大小。
  • 引用在内存中没有自己的存储空间,它只是作为已存在对象的别名,不占用额外的内存空间。

悬空指针和野指针

悬空指针和野指针都是指针的问题,但它们表示不同的情况。
1 悬空指针(Dangling Pointer)指的是指针仍然存在,但它所指向的对象已经被释放或销毁,因此指针指向的内存不再有效。这种情况可能发生在以下情况下:

在指针指向的对象被释放后,没有将指针置为 null 或重新指向其他有效的对象。
在指针指向的对象被销毁后,仍然使用指针进行访问。
悬空指针的使用是危险的,因为它们指向的内存可能已经被其他对象或数据所使用,访问悬空指针可能导致未定义行为或崩溃。

2 野指针(Wild Pointer)指的是指针没有被初始化或者指向了一个无效的、未知的内存地址。野指针可能发生在以下情况下:

-在声明指针后,没有给它一个有效的地址或初始化为 null。
在指针被释放后,没有将其置为 null 或重新初始化。

  • 野指针可能导致程序访问未知的内存区域,这可能会导致数据损坏、程序崩溃或安全漏洞。

如何避免悬空指针和野指针

  • 在指针被释放后,将其置为 null 或重新初始化,避免悬空指针的出现。
  • 在使用指针之前,始终确保它指向有效的内存区域,避免野指针的出现。
  • 使用智能指针(如 std::shared_ptr、std::unique_ptr)等 RAII 技术,可以更好地管理指针的生命周期,避免手动释放和悬空指针的问题。

多态

多态(Polymorphism)是面向对象编程的一个重要概念,它允许以统一的方式处理不同类型的对象,并根据对象的实际类型调用相应的方法。多态性使得代码更加灵活、可扩展和易于维护。

在多态中,一个基类可以有多个派生类,而这些派生类可以重写(override)基类的方法,以便根据自身的特性实现不同的行为。然后,通过基类的指针或引用,可以在运行时根据实际对象的类型来调用相应的方法。

多态的实现依赖于两个主要的概念:继承(Inheritance)和虚函数(Virtual Function)。

继承:
继承允许从一个基类派生出一个或多个派生类。派生类继承了基类的属性和方法,同时可以添加自己的特性。基类可以作为通用类型的抽象,而派生类可以提供具体的实现。

虚函数:
虚函数是在基类中声明为虚函数的成员函数。派生类可以重写基类的虚函数,以实现自己的特定行为。通过在基类中将函数声明为虚函数,可以实现动态绑定(Dynamic Binding),即在运行时根据对象的实际类型来调用相应的函数。

在 C++ 中,通过在基类中使用 virtual 关键字来声明虚函数,而在派生类中使用 override 关键字来重写虚函数。调用虚函数时,会根据对象的实际类型来确定要调用的实现。

多态的优势在于它可以通过统一的接口处理不同类型的对象,而无需关心具体的对象类型。这种灵活性和可扩展性使得代码更加可维护和可复用。多态性是面向对象编程中的一个重要特性,也是实现封装、继承和多态三大特性的基础之一。

析构函数可以为虚函数吗,为何建议为虚函数

是的,析构函数可以声明为虚函数。

建议将析构函数声明为虚函数的主要原因是确保在通过基类的指针或引用删除派生类对象时,能够正确调用派生类的析构函数。这是通过动态绑定(Dynamic Binding)来实现的。

当基类的析构函数被声明为虚函数时,派生类可以重写(override)该析构函数,并确保在对象销毁时,正确调用派生类的析构函数。这样做的好处是,当基类指针指向派生类对象时,将通过指针的静态类型找到适当的析构函数,并根据对象的实际类型来调用正确的析构函数。

下面是一个示例代码,展示了使用虚析构函数的情况:

class Base {
public:
    virtual ~Base() {
        // 基类的虚析构函数
    }
};

class Derived : public Base {
public:
    ~Derived() override {
        // 派生类的析构函数
    }
};

int main() {
    Base* ptr = new Derived();
    delete ptr;  // 调用派生类的析构函数
    return 0;
}

在上述示例中,基类 Base 的析构函数被声明为虚函数,而派生类 Derived 重写了该析构函数。当通过基类指针 ptr 删除对象时,会自动调用派生类 Derived 的析构函数,确保正确释放对象的资源。

如果基类的析构函数不是虚函数,而通过基类指针删除派生类对象,将只调用基类的析构函数,而不会调用派生类的析构函数。这可能导致派生类特定的资源无法正确释放,从而造成资源泄漏或行为不一致的问题。

因此,为了正确处理继承关系中的对象销毁,当有多态需求时,强烈建议将基类的析构函数声明为虚函数。这样可以确保在删除派生类对象时,适当调用派生类的析构函数。

C++程序的编译过程可以分为四个主要阶段

C++程序的编译过程可以分为四个主要阶段:预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)和链接(Linking)。
1 预处理(Preprocessing):
在这个阶段,预处理器会对源代码进行处理。它会执行以下操作:

  • 移除注释。
  • 处理预处理指令,如 #include、#define 等。
  • 展开宏定义。
  • 处理条件编译指令,如 #ifdef、#ifndef、#if 等。
  • 生成包含预处理结果的临时文件
    预处理后的代码将作为下一阶段编译器的输入。
    2 编译(Compilation):
    在编译阶段,编译器(如GCC、Clang等)将预处理后的代码进行语法分析和语义分析,并生成相应的目标代码。主要步骤包括:
  • 词法分析:将源代码分解为词法单元(tokens)。
  • 语法分析:使用语法规则将词法单元组织成语法树(parse tree)。
  • 语义分析:对语法树进行类型检查、符号解析等,并生成中间代码。
    编译器的输出是目标文件,通常是与特定平台相关的机器代码。

3汇编(Assembly):
汇编器(如GNU汇编器)将编译器生成的目标文件转换为可重定位的机器代码。汇编器的主要任务是将汇编指令与符号(如函数名、变量名)关联起来,并生成目标文件。

4链接(Linking):
链接器(如GNU链接器ld)将多个目标文件(以及库文件)合并为一个可执行文件。在链接阶段,主要完成以下任务:

  • 符号解析:将目标文件中的符号引用与符号定义进行匹配。
  • 地址重定位:根据需要,调整目标文件中的地址引用,以便在内存中正确加载和执行。
  • 符号表生成:生成可执行文件的符号表,以便在运行时进行符号解析。
    最终输出的可执行文件可以在操作系统上运行。

线程池有死锁问题吗,死锁了解多少,递归锁知道吗

死锁是指两个或多个线程相互等待对方释放资源而无法继续执行的情况,导致程序无法继续执行下去。死锁通常发生在多线程环境下,当线程获取了某个资源的锁,并试图获取其他线程占用的资源锁时,而其他线程也在等待当前线程占用的资源锁释放,从而形成循环等待,导致死锁的发生。
在线程池中,死锁问题可能出现在以下情况:

  • 任务之间存在依赖关系:如果线程池中的任务之间存在依赖关系,并且这些任务相互等待对方完成,就有可能导致死锁。例如,任务 A 等待任务 B 完成,而任务 B 又等待任务 A 完成,这样就形成了循环等待的死锁情况。

  • 锁的使用不当:如果在线程池中的任务中使用了不正确的锁机制,例如忘记释放锁或使用了错误的锁顺序,也可能导致死锁。

  • 资源竞争:如果线程池中的任务并发地竞争有限的资源,而没有合适的同步机制来保证资源的互斥访问,就可能导致死锁。

针对死锁问题,开发者可以采取一些预防和解决措施,例如:

  • 合理设计任务之间的依赖关系,避免形成循环等待。
  • 谨慎使用锁,确保正确地获取和释放锁,并避免死锁-prone 的锁顺序。
  • 使用同步机制(如互斥锁、条件变量)来保证对共享资源的互斥访问。
  • 使用工具和技术来检测和解决死锁问题,例如死锁检测工具、资源分配图等。

常见的几种锁

C++多线程中的锁主要有五类:互斥锁(信号量)、条件锁、自旋锁、读写锁、递归锁。
1 互斥锁(Mutex Lock):
互斥锁用于实现对共享资源的互斥访问,保证同一时间只有一个线程可以访问被保护的代码段或共享资源。

#include 

std::mutex mtx;

void protected_function() {
    std::lock_guard<std::mutex> lock(mtx);
    // 这里是需要互斥访问的代码段
    // ...
}

2递归锁(Recursive Mutex):
递归锁允许同一个线程多次获取同一个锁,以避免出现同一线程在嵌套调用中死锁的情况。
示例代码:

#include 

std::recursive_mutex mtx;

void protected_function() {
    std::lock_guard<std::recursive_mutex> lock(mtx);
    // 这里是需要互斥访问的代码段
    // ...
    protected_function(); // 可以在同一线程内递归调用
    // ...
}

3读写锁(Read-Write Lock):
读写锁允许多个线程同时对共享资源进行读取操作,但在有线程进行写入操作时,其他线程无法进行读取或写入操作,以保证数据的一致性和并发性。
示例代码:

#include 

std::shared_mutex mtx;

void read_operation() {
    std::shared_lock<std::shared_mutex> lock(mtx);
    // 这里是读取共享资源的代码段
    // ...
}

void write_operation() {
    std::unique_lock<std::shared_mutex> lock(mtx);
    // 这里是写入共享资源的代码段
    // ...
}

4自旋锁(Spin Lock):
自旋锁是一种忙等锁,线程在获取锁失败时会循环忙等,直到锁可用。适用于对共享资源的访问时间较短且线程数较少的情况。
示例代码:

#include 

std::atomic_flag flag = ATOMIC_FLAG_INIT;

void protected_function() {
    while (flag.test_and_set(std::memory_order_acquire)) {
        // 忙等,直到获取到锁
    }
    // 这里是需要互斥访问的代码段
    // ...
    flag.clear(std::memory_order_release);  // 释放锁
}

网络

什么是TCP

TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议,在发送数据前,通信双方必须在彼此间建立一条连接。所谓的“连接”,其实是客户端和服务端保存的一份关于对方的信息,如ip地址、端口号等。TCP可以看成是一种字节流,它会处理IP层或以下的层的丢包、重复以及错误问题。在连接的建立过程中,双方需要交换一些连接的参数。这些参数可以放在TCP头部。一个TCP连接由一个4元组构成,分别是两个IP地址和两个端口号。一个TCP连接通常分为三个阶段:连接、数据传输、退出(关闭)。通过三次握手建立一个链接,通过四次挥手来关闭一个连接。当一个连接被建立或被终止时,交换的报文段只包含TCP头部,而没有数据。

TCP报文头部结构

C++问题总结_第1张图片
上图中有几个字段需要重点介绍下:
(1)序号:seq序号,占32位,用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记。
(2)确认序号:ack序号,占32位,只有ACK标志位为1时,确认序号字段才有效,ack=seq+1。
(3)标志位:共6个,即URG、ACK、PSH、RST、SYN、FIN等,具体含义如下:

ACK:确认序号有效。FIN:释放一个连接。PSH:接收方应该尽快将这个报文交给应用层。RST:重置连接。SYN:发起一个新连接。URG:紧急指针(urgent pointer)有效。需要注意的是:不要将确认序号ack与标志位中的ACK搞混了。确认方ack=发起方seq+1,两端配对。

TCP 三次握手和四次挥手过程

三次握手
三次握手的本质是确认通信双方收发数据的能力首先,我让信使运输一份信件给对方,对方收到了,那么他就知道了我的发件能力和他的收件能力是可以的。于是他给我回信,我若收到了,我便知我的发件能力和他的收件能力是可以的,并且他的发件能力和我的收件能力是可以。然而此时他还不知道他的发件能力和我的收件能力到底可不可以,于是我最后回馈一次,他若收到了,他便清楚了他的发件能力和我的收件能力是可以的。这,就是三次握手。
C++问题总结_第2张图片
243 122

  • 第一次握手:客户端要向服务端发起连接请求,首先客户端随机生成一个起始序列号ISN(比如是100),那客户端向服务端发送的报文段包含SYN标志位(也就是SYN=1),序列号seq=100。
  • 第二次握手:服务端收到客户端发过来的报文后,发现SYN=1,知道这是一个连接请求,于是将客户端的起始序列号100存起来,并且随机生成一个服务端的起始序列号(比如是300)。然后给客户端回复一段报文,回复报文包含SYN和ACK标志(也就是SYN=1,ACK=1)、序列号seq=300、确认号ack=101(客户端发过来的序列号+1)。
  • 第三次握手:客户端收到服务端的回复后发现ACK=1并且ack=101,于是知道服务端已经收到了序列号为100的那段报文;同时发现SYN=1,知道了服务端同意了这次连接,于是就将服务端的序列号300给存下来。然后客户端再回复一段报文给服务端,报文包含ACK标志位(ACK=1)、ack=301(服务端序列号+1)、seq=101(第一次握手时发送报文是占据一个序列号的,所以这次seq就从101开始,需要注意的是不携带数据的ACK报文是不占据序列号的,所以后面第一次正式发送数据时seq还是101)。当服务端收到报文后发现ACK=1并且ack=301,就知道客户端收到序列号为300的报文了,就这样客户端和服务端通过TCP建立了连接。

四次挥手
四次挥手的目的是关闭一个连接
C++问题总结_第3张图片
比如客户端初始化的序列号ISN=100,服务端初始化的序列号ISN=300。TCP连接成功后客户端总共发送了1000个字节的数据,服务端在客户端发FIN报文前总共回复了2000个字节的数据。

2343 1222

  • 第一次挥手:当客户端的数据都传输完成后,客户端向服务端发出连接释放报文(当然数据没发完时也可以发送连接释放报文并停止发送数据),释放连接报文包含FIN标志位(FIN=1)、序列号seq=1101(100+1+1000,其中的1是建立连接时占的一个序列号)。需要注意的是客户端发出FIN报文段后只是不能发数据了,但是还可以正常收数据;另外FIN报文段即使不携带数据也要占据一个序列号。
  • 第二次挥手:服务端收到客户端发的FIN报文后给客户端回复确认报文,确认报文包含ACK标志位(ACK=1)、确认号ack=1102(客户端FIN报文序列号1101+1)、序列号seq=2300(300+2000)。此时服务端处于关闭等待状态,而不是立马给客户端发FIN报文,这个状态还要持续一段时间,因为服务端可能还有数据没发完。
  • 第三次挥手:服务端将最后数据(比如50个字节)发送完毕后就向客户端发出连接释放报文,报文包含FIN和ACK标志位(FIN=1,ACK=1)、确认号和第二次挥手一样ack=1102、序列号seq=2350(2300+50)。
  • 第四次挥手:客户端收到服务端发的FIN报文后,向服务端发出确认报文,确认报文包含ACK标志位(ACK=1)、确认号ack=2351、序列号seq=1102。注意客户端发出确认报文后不是立马释放TCP连接,而是要经过2MSL(最长报文段寿命的2倍时长)后才释放TCP连接。而服务端一旦收到客户端发出的确认报文就会立马释放TCP连接,所以服务端结束TCP连接的时间要比客户端早一些。

为什么TCP连接的时候是3次?2次不可以吗?

因为需要考虑连接时丢包的问题,如果只握手2次,第二次握手时如果服务端发给客户端的确认报文段丢失,此时服务端已经准备好了收发数(可以理解服务端已经连接成功)据,而客户端一直没收到服务端的确认报文,所以客户端就不知道服务端是否已经准备好了(可以理解为客户端未连接成功),这种情况下客户端不会给服务端发数据,也会忽略服务端发过来的数据。如果是三次握手,即便发生丢包也不会有问题,比如如果第三次握手客户端发的确认ack报文丢失,服务端在一段时间内没有收到确认ack报文的话就会重新进行第二次握手,也就是服务端会重发SYN报文段,客户端收到重发的报文段后会再次给服务端发送确认ack报文。

为什么客户端发出第四次挥手的确认报文后要等2MSL的时间才能释放TCP连接?

这里同样是要考虑丢包的问题,如果第四次挥手的报文丢失,服务端没收到确认ack报文就会重发第三次挥手的报文,这样报文一去一回最长时间就是2MSL,所以需要等这么长时间来确认服务端确实已经收到了。

MSL 是 Maximum Segment Lifetime(最大报文生存时间)的缩写,表示一个 TCP 报文在网络中的最长存活时间。在实际中,MSL 的具体值由操作系统决定,通常为2分钟。

为什么TCP连接的时候是3次,关闭的时候却是4次?

因为只有在客户端和服务端都没有数据要发送的时候才能断开TCP。而客户端发出FIN报文时只能保证客户端没有数据发了,服务端还有没有数据发客户端是不知道的。而服务端收到客户端的FIN报文后只能先回复客户端一个确认报文来告诉客户端我服务端已经收到你的FIN报文了,但我服务端还有一些数据没发完,等这些数据发完了服务端才能给客户端发FIN报文(所以不能一次性将确认报文和FIN报文发给客户端,就是这里多出来了一次)。

在 TCP/IP 网络中,当发生丢包时,TCP 协议如何处理丢失的数据包?

1 超时重传:当发送方发送一个数据包后,它会启动一个定时器来等待对应的确认(ACK)报文。如果在超时时间内未收到确认报文,发送方会假设该数据包已经丢失,并进行重传。这样可以确保数据包的可靠传输。

2快速重传:接收方在收到乱序的数据包时,会发送重复的 ACK 报文来通知发送方已经收到了中间的数据包。当发送方连续收到三个重复的 ACK 报文时,它会立即进行快速重传,即重传对应的丢失数据包,而不等待超时时间。

3滑动窗口调整:TCP 使用滑动窗口机制来调整发送方和接收方之间的数据流量。当发生丢包时,发送方会减小滑动窗口的大小,以降低发送速率,从而避免继续丢失更多的数据包。一旦网络恢复正常,发送方会逐渐增加滑动窗口的大小,恢复正常的数据传输。

4拥塞控制:当发生丢包时,TCP 协议会将其视为网络拥塞的信号。为了避免进一步加重网络拥塞,TCP 会启动拥塞控制机制,减少发送方的发送速率。这包括降低拥塞窗口大小、执行慢启动算法和拥塞避免算法等。

5FEC 编码:有些 TCP/IP 实现中使用前向纠错(Forward Error Correction,FEC)编码来减少丢包的影响。发送方在发送数据时,使用冗余数据对原始数据进行编码,接收方可以使用这些冗余数据来纠正部分丢失的数据,而无需进行重传。

QT

信号和槽的机制

Qt 的信号槽机制是一种用于对象间通信的机制,该机制允许一个对象(信号的发送者)在特定的事件发生时发出一个信号,而其他对象(槽的接收者)可以连接到该信号并在接收到信号时执行特定的操作。
通过信号槽机制,我们可以实现对象之间的松耦合,使得它们可以独立地进行交互和通信,而不需要直接引用或了解彼此的详细信息。
一个常见的用例是在用户界面编程中,当用户点击按钮时,按钮对象会发出一个 clicked() 信号,而我们可以将这个信号连接到一个槽函数,以执行相应的操作,比如更新界面或执行业务逻辑。

信号和槽的优点

Qt 的信号槽机制具有以下几个优点:

1 松耦合:信号槽机制使得对象之间的通信更加松耦合。信号的发送者和槽的接收者之间不需要直接引用或了解彼此的详细信息,它们只需要通过信号槽的连接建立通信关系。这样可以提高代码的可维护性和可扩展性。

2 灵活性:信号槽机制提供了一种灵活的方式来处理事件和消息的传递。一个信号可以连接到多个槽函数,而一个槽函数也可以连接到多个信号。这种灵活性使得开发者可以根据需要灵活地组织和处理对象之间的通信。

3 线程安全:Qt 的信号槽机制在多线程环境下是线程安全的。Qt 提供了线程间的信号槽连接和跨线程的信号槽调用机制,可以方便地在多线程应用程序中进行线程间的通信,而无需手动处理线程同步和互斥。

4 事件驱动:信号槽机制是一种事件驱动的编程范式,适用于 GUI 编程和事件处理。当发生特定的事件时,对象可以通过发出信号来通知其他对象进行相应的处理。这种事件驱动的方式使得程序逻辑更加清晰和可维护。

你可能感兴趣的:(C++学习,1024程序员节)