1定义和初始化:
2空值:
3内存管理:
4重新绑定:
5 空间大小:
悬空指针和野指针都是指针的问题,但它们表示不同的情况。
1 悬空指针(Dangling Pointer)指的是指针仍然存在,但它所指向的对象已经被释放或销毁,因此指针指向的内存不再有效。这种情况可能发生在以下情况下:
在指针指向的对象被释放后,没有将指针置为 null 或重新指向其他有效的对象。
在指针指向的对象被销毁后,仍然使用指针进行访问。
悬空指针的使用是危险的,因为它们指向的内存可能已经被其他对象或数据所使用,访问悬空指针可能导致未定义行为或崩溃。
2 野指针(Wild Pointer)指的是指针没有被初始化或者指向了一个无效的、未知的内存地址。野指针可能发生在以下情况下:
-在声明指针后,没有给它一个有效的地址或初始化为 null。
在指针被释放后,没有将其置为 null 或重新初始化。
多态(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++程序的编译过程可以分为四个主要阶段:预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)和链接(Linking)。
1 预处理(Preprocessing):
在这个阶段,预处理器会对源代码进行处理。它会执行以下操作:
3汇编(Assembly):
汇编器(如GNU汇编器)将编译器生成的目标文件转换为可重定位的机器代码。汇编器的主要任务是将汇编指令与符号(如函数名、变量名)关联起来,并生成目标文件。
4链接(Linking):
链接器(如GNU链接器ld)将多个目标文件(以及库文件)合并为一个可执行文件。在链接阶段,主要完成以下任务:
死锁是指两个或多个线程相互等待对方释放资源而无法继续执行的情况,导致程序无法继续执行下去。死锁通常发生在多线程环境下,当线程获取了某个资源的锁,并试图获取其他线程占用的资源锁时,而其他线程也在等待当前线程占用的资源锁释放,从而形成循环等待,导致死锁的发生。
在线程池中,死锁问题可能出现在以下情况:
任务之间存在依赖关系:如果线程池中的任务之间存在依赖关系,并且这些任务相互等待对方完成,就有可能导致死锁。例如,任务 A 等待任务 B 完成,而任务 B 又等待任务 A 完成,这样就形成了循环等待的死锁情况。
锁的使用不当:如果在线程池中的任务中使用了不正确的锁机制,例如忘记释放锁或使用了错误的锁顺序,也可能导致死锁。
资源竞争:如果线程池中的任务并发地竞争有限的资源,而没有合适的同步机制来保证资源的互斥访问,就可能导致死锁。
针对死锁问题,开发者可以采取一些预防和解决措施,例如:
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是一种面向连接的、可靠的、基于字节流的传输层通信协议,在发送数据前,通信双方必须在彼此间建立一条连接。所谓的“连接”,其实是客户端和服务端保存的一份关于对方的信息,如ip地址、端口号等。TCP可以看成是一种字节流,它会处理IP层或以下的层的丢包、重复以及错误问题。在连接的建立过程中,双方需要交换一些连接的参数。这些参数可以放在TCP头部。一个TCP连接由一个4元组构成,分别是两个IP地址和两个端口号。一个TCP连接通常分为三个阶段:连接、数据传输、退出(关闭)。通过三次握手建立一个链接,通过四次挥手来关闭一个连接。当一个连接被建立或被终止时,交换的报文段只包含TCP头部,而没有数据。
上图中有几个字段需要重点介绍下:
(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,两端配对。
三次握手
三次握手的本质是确认通信双方收发数据的能力首先,我让信使运输一份信件给对方,对方收到了,那么他就知道了我的发件能力和他的收件能力是可以的。于是他给我回信,我若收到了,我便知我的发件能力和他的收件能力是可以的,并且他的发件能力和我的收件能力是可以。然而此时他还不知道他的发件能力和我的收件能力到底可不可以,于是我最后回馈一次,他若收到了,他便清楚了他的发件能力和我的收件能力是可以的。这,就是三次握手。
243 122
四次挥手
四次挥手的目的是关闭一个连接
比如客户端初始化的序列号ISN=100,服务端初始化的序列号ISN=300。TCP连接成功后客户端总共发送了1000个字节的数据,服务端在客户端发FIN报文前总共回复了2000个字节的数据。
2343 1222
因为需要考虑连接时丢包的问题,如果只握手2次,第二次握手时如果服务端发给客户端的确认报文段丢失,此时服务端已经准备好了收发数(可以理解服务端已经连接成功)据,而客户端一直没收到服务端的确认报文,所以客户端就不知道服务端是否已经准备好了(可以理解为客户端未连接成功),这种情况下客户端不会给服务端发数据,也会忽略服务端发过来的数据。如果是三次握手,即便发生丢包也不会有问题,比如如果第三次握手客户端发的确认ack报文丢失,服务端在一段时间内没有收到确认ack报文的话就会重新进行第二次握手,也就是服务端会重发SYN报文段,客户端收到重发的报文段后会再次给服务端发送确认ack报文。
这里同样是要考虑丢包的问题,如果第四次挥手的报文丢失,服务端没收到确认ack报文就会重发第三次挥手的报文,这样报文一去一回最长时间就是2MSL,所以需要等这么长时间来确认服务端确实已经收到了。
MSL 是 Maximum Segment Lifetime(最大报文生存时间)的缩写,表示一个 TCP 报文在网络中的最长存活时间。在实际中,MSL 的具体值由操作系统决定,通常为2分钟。
因为只有在客户端和服务端都没有数据要发送的时候才能断开TCP。而客户端发出FIN报文时只能保证客户端没有数据发了,服务端还有没有数据发客户端是不知道的。而服务端收到客户端的FIN报文后只能先回复客户端一个确认报文来告诉客户端我服务端已经收到你的FIN报文了,但我服务端还有一些数据没发完,等这些数据发完了服务端才能给客户端发FIN报文(所以不能一次性将确认报文和FIN报文发给客户端,就是这里多出来了一次)。
1 超时重传:当发送方发送一个数据包后,它会启动一个定时器来等待对应的确认(ACK)报文。如果在超时时间内未收到确认报文,发送方会假设该数据包已经丢失,并进行重传。这样可以确保数据包的可靠传输。
2快速重传:接收方在收到乱序的数据包时,会发送重复的 ACK 报文来通知发送方已经收到了中间的数据包。当发送方连续收到三个重复的 ACK 报文时,它会立即进行快速重传,即重传对应的丢失数据包,而不等待超时时间。
3滑动窗口调整:TCP 使用滑动窗口机制来调整发送方和接收方之间的数据流量。当发生丢包时,发送方会减小滑动窗口的大小,以降低发送速率,从而避免继续丢失更多的数据包。一旦网络恢复正常,发送方会逐渐增加滑动窗口的大小,恢复正常的数据传输。
4拥塞控制:当发生丢包时,TCP 协议会将其视为网络拥塞的信号。为了避免进一步加重网络拥塞,TCP 会启动拥塞控制机制,减少发送方的发送速率。这包括降低拥塞窗口大小、执行慢启动算法和拥塞避免算法等。
5FEC 编码:有些 TCP/IP 实现中使用前向纠错(Forward Error Correction,FEC)编码来减少丢包的影响。发送方在发送数据时,使用冗余数据对原始数据进行编码,接收方可以使用这些冗余数据来纠正部分丢失的数据,而无需进行重传。
Qt 的信号槽机制是一种用于对象间通信的机制,该机制允许一个对象(信号的发送者)在特定的事件发生时发出一个信号,而其他对象(槽的接收者)可以连接到该信号并在接收到信号时执行特定的操作。
通过信号槽机制,我们可以实现对象之间的松耦合,使得它们可以独立地进行交互和通信,而不需要直接引用或了解彼此的详细信息。
一个常见的用例是在用户界面编程中,当用户点击按钮时,按钮对象会发出一个 clicked() 信号,而我们可以将这个信号连接到一个槽函数,以执行相应的操作,比如更新界面或执行业务逻辑。
Qt 的信号槽机制具有以下几个优点:
1 松耦合:信号槽机制使得对象之间的通信更加松耦合。信号的发送者和槽的接收者之间不需要直接引用或了解彼此的详细信息,它们只需要通过信号槽的连接建立通信关系。这样可以提高代码的可维护性和可扩展性。
2 灵活性:信号槽机制提供了一种灵活的方式来处理事件和消息的传递。一个信号可以连接到多个槽函数,而一个槽函数也可以连接到多个信号。这种灵活性使得开发者可以根据需要灵活地组织和处理对象之间的通信。
3 线程安全:Qt 的信号槽机制在多线程环境下是线程安全的。Qt 提供了线程间的信号槽连接和跨线程的信号槽调用机制,可以方便地在多线程应用程序中进行线程间的通信,而无需手动处理线程同步和互斥。
4 事件驱动:信号槽机制是一种事件驱动的编程范式,适用于 GUI 编程和事件处理。当发生特定的事件时,对象可以通过发出信号来通知其他对象进行相应的处理。这种事件驱动的方式使得程序逻辑更加清晰和可维护。