一、一些常见的类型和零值的比较
整形(int, short, long, long long)
零值:0
无符号整形(unsigned int, unsigned short, unsigned long, unsigned long long)
零值:0
字符类型(char, unsigned char, signed char)
零值:'\0'
浮点数(float, double, long double)
零值:0.0
布尔类型(bool)
零值:false
指针类型(任何指针类型)
零值:nullptr
数组类型
零值:每一个数组元素初始化为0
结构体类型:
零值:所有成员的零值
类类型的零值:
类类型的零值依赖与类的构造函数,如果没有自定义构造函数,类对象的成员将使用默认的初始化方式。
零值:类成员使用默认初始化值,通常是通过默认构造函数实现的。
总结:
类型 | 零值 |
int | |
unsigned int | |
char | |
float | |
double | |
bool | |
指针类型 | |
数组类型 | |
结构体类型 | |
类类型 |
空指针和nullptr的区别
在 C++ 中,nullptr是一个特殊的空指针常量,表示指针类型的零值。与NULL或者0传统表示的空指针相比,nullptr更安全,因为他是类型安全的。
空值与零值的差异
零值:通常是某一个数据类型的初始值(比如:0,false,'\0'等)
空值:对于指针类型来说,空指针通常表示没有指向任何有效内存位置,通常通过nullptr或者NULL来表示
总结:
不同类型的零值取决于他们的定义,基本数据类型通常有明确的零值,像0,false,'\0'等。复杂类型如数组和结构体的零值是由其每一个成员的零值决定的。对于指针类型,零值是nullptr。
二、malloc的几个常见的没有释放内存的错误
2.1 忘记调用 free
当我们通过malloc或者其他内存分配函数分配内存时,忘记调用free来释放分配的内存,导致内存泄露。
int* arr = (int*)malloc(10 * sizeof(int));
// 操作完 arr 后,忘记调用 free(arr) 来释放内存
2.2 指针被覆盖后丢失内存
如果在没有释放之前,指针指向新的内存块发生变化,那么之前分配的内存就无法被访问,也就无法释放,造成内存泄露。
int* arr = (int*)malloc(10 * sizeof(int));
arr = (int*)malloc(20 * sizeof(int)); // 原来的内存地址丢失,未释放
// 忘记释放原来指向的内存
2.3 在函数中分配内存,但是未释放
如果在函数中使用malloc分配了内存,而函数结束后没有相应的free,会导致内存泄露。
void foo() {int* arr = (int*)malloc(10 * sizeof(int));// 这里没有 free(arr)
}
// 当 foo() 返回时,arr 指向的内存没有释放
2.4 条件判断中的内存分配
在某些情况下,如果在不同的条件路径下调用malloc,但是没有保证每一个路径都正确释放内存,可能会导致内存泄露。
int* arr = (int*)malloc(10 * sizeof(int));
if (some_condition) {// 做一些操作,但没有释放 arr
}
// 如果 some_condition 为假,arr 仍未被释放
2.5 多次分配内存,但未释放
如果程序多次调用malloc进行内存分配,但没有对应的free来释放这些内存,就会发生内存泄露。
int* arr1 = (int*)malloc(10 * sizeof(int));
int* arr2 = (int*)malloc(20 * sizeof(int));
// 没有调用 free(arr1) 和 free(arr2),造成内存泄漏
2.6 程序异常退出时内存未释放
如果程序因异常终止(例如崩溃或者调用exit)而没有正常退出,这时分配的内存可能无法释放,造成内存泄露。
int* arr = (int*)malloc(10 * sizeof(int));
exit(1); // 程序直接退出,arr 指向的内存没有释放
2.7 在循环中分配内存但没有释放
如果在循环中多次调用malloc进行内存分配,而没有在每次迭代后释放之前的内存,就会造成内存泄露。
for (int i = 0; i < 100; i++) {int* arr = (int*)malloc(10 * sizeof(int));// 没有 free(arr),每次都会分配新内存,而旧的内存没有释放
}
总结:
为了避免这些错误,使用malloc时应该确保:
- 每次分配的内存都在不在使用时通过free正确释放。
- 不要丢失指向动态分配内存的指针,或者在失去指针之前释放内存。
- 适时检查代码路径,确保每一条路径都正确释放内存。
- 可以使用工具(比如Valgrind)检查和调试内存泄露问题。
三、const 和 extern 关键字
3.1 const关键字
const 关键字用于声明变量,表示某一个变量的值不能被修改。使用const可以保护数据不被意外修改。
用法:
- 修饰变量:const用于定义常量变量,一旦初始化后,不能修改其值
- 修饰指针:const还可以修饰指针,表示指针指向的内容不可改变,或者指针本身不可以改变,具体取决于const的位置
- 常量指针:指针本身可以修改,但是指向的内容不可改变
- 指向常量的指针:指针不可改变,但是指向的内容可以修改
- 常量指针常量:指针本身不可改变,指向的内容也不可以改变
- 常量参数:const也可以用来修饰函数的参数,表示该参数在函数内部不可修改
const int x = 5; // 定义常量 x,值为 5,不能修改
int x = 5;
const int* ptr = &x; // ptr 可以指向其他地址,但不能修改 *ptr 的值
int x = 5;
int* const ptr = &x; // ptr 不可改变,但可以修改 *ptr 的值
int x = 5;
const int* const ptr = &x; // ptr 和 *ptr 都不能修改
void foo(const int x) {// x 在此函数内不可修改
}
3.2 extern关键字
extern 关键字用于声明一个变量或者函数是在其他文件中定义的,告诉编译器这个变量或者函数的定义是在其他地方,而不是当前文件中。他通常用于跨文件共享数据或者函数。
用法:
- 用于变量:如果在一个文件中定义了一个全局变量,在另一个文件中使用该变量时,可以使用extern来声明该变量。
- 定义文件:
- 声明文件:
// file1.c
int x = 10; // 定义了全局变量 x// file2.c
extern int x; // 声明在 file1.c 中定义的变量 x
- 用于函数:如果在一个文件中定义了一个函数,在另一个文件中使用该函数时,也需要使用extern来声明该函数
四、条件编译
条件编译是指在编译时,根据不同的条件来决定是否编译某部分代码的机制。这在跨平台开发和调试过程中尤其有用,可以根据不同的操作系统、编译器版本或者编译配置来启用或者禁用某些代码。
在C++中,条件编译主要通过预处理指令来实现,常用的指令包括#if,#ifdef,#ifndef,#else,#elif,#endif等。
常用的条件编译指令:
- #if:根据条件表达式的值来决定是否编译代码块
- #ifdef:如果宏已经被定义,则编译代码块
- #ifndef:如果宏没有被定义,则编译代码块
- #else:在#if或者#ifdef条件失败时,编译#else后面的代码块
- #elif:#else if,用于在多个条件中选择其中一个
- #endif:结束一个条件编译块
条件编译的常见用途:
- 平台特定的代码:根据不同操作系统或者平台来编译不同的代码。例如,windows下的API可能与Linux下的API不同
- 调试模式:通过定义DEBUG宏来开启或者禁用调试信息。发布版本中通常会禁用调试代码,而开发版本中会启用它。
- 优化代码:根据编译器或者CPU特性选择不同的优化策略。比如某些处理器支持特定的指令集,可以根据编译器支持的标志启用或者禁用特定代码。
- 编译选项配置:根据不同的编译选项(例如#ifdef NODEBUG)来决定会否启用断言等调试工具。
五、模拟实现 strcpy
strcpy函数用于将源字符串复制到目标字符串
char* my_strcpy(char* desc, const char* src)
{char* desc_ptr = desc;while (*src != '\0'){*desc_ptr = *src;desc_ptr++;src++;}*desc_ptr = '\0';return desc;
}
六、qt的信号与槽函数
在qt中,信号与槽机制是一种核心的通信机制,用于对象之间的通信。信号与槽机制使得不同的对象能够在不直接耦合的情况下进行交互。你可以将信号看做是对象发出的“通知”,而槽则是接收并处理这些通知的函数。
6.1 信号与槽的工作原理
- 信号:当某一个事件发生时,对象发出一个信号。这通常是通过成员函数 emit 发出的
- 槽:槽是一个普通的成员函数,他用于响应信号。槽函数可以与一个或者多个信号关联。
当信号发出时,Qt会自动调用与该信号连接的槽函数,完成响应的操作。
6.2 基本概念
信号:是一个没有实现的函数声明,通常在类中定义,但是不需要实现。
槽:是一个普通的成员函数,可以通过Qt的QObject::connect()来与信号连接。
6.3 使用信号与槽的步骤
- 定义信号:在类中使用signals关键字定义信号
- 定义槽:在类中使用 public slots 或者 private slots 关键字定义槽
- 连接信号与槽:通过QObject::connect() 函数将信号和槽连接起来
6.4 连接信号与槽的其他方式
在Qt中,信号与槽的连接不仅仅限于直接的函数指针连接,还支持使用Lambda函数等方式:
6.4.1 使用Lambda函数作为槽
QObject::connect(&obj, &MyClass::mySignal, [](int value) {qDebug() << "Received value via lambda:" << value;
});emit obj.mySignal(42);
6.4.2 连接跨线程的信号与槽
Qt的信号与槽机制也支持跨线程通信。连接信号与槽时,如果信号发送者和槽接受者位于不同的线程,Qt会自动处理线程同步,并根据需要使用队列。
6.5 信号与槽的类型匹配
Qt在进行信号与槽连接时会进行类型匹配,确保信号的参数与槽的参数类型一致。如果不一致,Qt会尝试进行类型转换(例如从int到double)但是,如果没有合适的转换或者类型不兼容,Qt会发出警告并且信号不会连接成功。
6.6 信号与槽的参数个数
在Qt中,信号和槽可以具有不同的参数个数,但是需要保证信号和槽的参数个数和类型匹配。下面是一些关键点和实例,帮助你理解Qt中信号和槽的参数个数如何工作。
6.6.1 信号和槽的参数个数
- 信号:可以带有任意数量的参数,信号的参数个数决定了他能够传递的信息。
- 槽:槽函数也可以接受与信号相同数量和类型的参数。也就是说,槽的参数个数和类型必须与信号一致,才能正确连接。
6.6.2 信号与槽的参数匹配
当你连接信号与槽时,Qt会检查信号和槽的参数个数和类型是否匹配。如果信号有多个参数,槽也需要有相同个数的参数,并且类型必须相匹配(能够进行隐式转换)
6.6.3 信号和槽的实例
信号和槽有相同个数和类型的参数
#include <QCoreApplication>
#include <QDebug>
#include <QObject>class MyClass : public QObject
{Q_OBJECTpublic:MyClass() {}~MyClass() {}signals:void mySignal(int value1, double value2); // 信号有两个参数public slots:void mySlot(int value1, double value2) { // 槽也有两个参数qDebug() << "Received values:" << value1 << value2;}
};int main(int argc, char *argv[])
{QCoreApplication a(argc, argv);MyClass obj;// 连接信号与槽QObject::connect(&obj, &MyClass::mySignal, &obj, &MyClass::mySlot);// 发出信号emit obj.mySignal(42, 3.14); // 发送信号,两个参数return a.exec();
}
信号和槽的参数个数不匹配
信号和槽的参数个数不匹配,Qt会发出警告,并且不会成功连接信号和槽。
#include <QCoreApplication>
#include <QDebug>
#include <QObject>class MyClass : public QObject
{Q_OBJECTpublic:MyClass() {}~MyClass() {}signals:void mySignal(int value); // 信号只有一个参数public slots:void mySlot(int value1, double value2) { // 槽有两个参数qDebug() << "Received values:" << value1 << value2;}
};int main(int argc, char *argv[])
{QCoreApplication a(argc, argv);MyClass obj;// 连接信号与槽QObject::connect(&obj, &MyClass::mySignal, &obj, &MyClass::mySlot); // 参数个数不匹配// 发出信号emit obj.mySignal(42); // 发送信号,只有一个参数return a.exec();
}
信号和槽参数个数不同,但是可以通过默认参数来匹配
可以通过给槽函数的某些参数设置默认值来实现参数个数不匹配的情况。例如:
#include <QCoreApplication>
#include <QDebug>
#include <QObject>class MyClass : public QObject
{Q_OBJECTpublic:MyClass() {}~MyClass() {}signals:void mySignal(int value1); // 信号有一个参数public slots:void mySlot(int value1, double value2 = 3.14) { // 槽有两个参数,其中第二个参数有默认值qDebug() << "Received values:" << value1 << value2;}
};int main(int argc, char *argv[])
{QCoreApplication a(argc, argv);MyClass obj;// 连接信号与槽QObject::connect(&obj, &MyClass::mySignal, &obj, &MyClass::mySlot); // 参数个数匹配// 发出信号emit obj.mySignal(42); // 只发送一个参数return a.exec();
}
支持可变参数的信号和槽
Qt也支持可变参数的信号和槽,可以使用QVariant来传递不同类型的参数
#include <QCoreApplication>
#include <QDebug>
#include <QObject>
#include <QVariant>class MyClass : public QObject
{Q_OBJECTpublic:MyClass() {}~MyClass() {}signals:void mySignal(QVariant value); // 信号参数使用 QVariantpublic slots:void mySlot(QVariant value) { // 槽参数使用 QVariantqDebug() << "Received value:" << value;}
};int main(int argc, char *argv[])
{QCoreApplication a(argc, argv);MyClass obj;// 连接信号与槽QObject::connect(&obj, &MyClass::mySignal, &obj, &MyClass::mySlot);// 发出信号emit obj.mySignal(42); // 发送一个整数emit obj.mySignal(3.14); // 发送一个浮点数emit obj.mySignal("Hello"); // 发送一个字符串return a.exec();
}
总结
- 信号和槽的参数个数必须一致。如果信号有多个参数,槽也必须有相同的参数个数。
- 如果参数个数不匹配,Qt 会发出警告并且不会成功连接信号与槽。
- 可以通过默认参数、
QVariant
或者 Lambda 函数来处理不同数量的参数。 - 参数的类型必须匹配,或者能够进行隐式转换。
七、多态由哪几种组成?
7.1 静态多态
静态多态是通过编译时绑定来实现的。在C++中,静态多态通过函数重载和运算符重载来实现的。
函数重载:允许在同一个作用域内定义多个同名但是参数不同的函数
运算符重载:允许自定义运算符的行为,使得运算符可以作用于自定义的数据类型
7.2 动态多态
动态多态是在程序运行时实现的,他是通过虚函数和继承来实现的。动态多态的核心是基类指针或者引用指向派生类对象,调用虚函数时,根据对象的实际类型来决定调用哪一个版本的函数。C++使用虚函数和继承来实现动态多态。
虚函数:在基类中声明为virtual的成员函数,可以被派生类重写。在运行时,C++会根据对象的实际类型来动态绑定函数调用。
继承:通过继承,子类可以重写基类的虚函数,实现不同的行为。
#include <iostream>class Animal {
public:virtual void makeSound() { // 虚函数std::cout << "Animal makes a sound." << std::endl;}
};class Dog : public Animal {
public:void makeSound() override { // 重写虚函数std::cout << "Dog barks." << std::endl;}
};class Cat : public Animal {
public:void makeSound() override { // 重写虚函数std::cout << "Cat meows." << std::endl;}
};int main() {Animal* animal;Dog dog;Cat cat;animal = &dog;animal->makeSound(); // 调用 Dog::makeSound()animal = &cat;animal->makeSound(); // 调用 Cat::makeSound()return 0;
}
7.3 模版多态
模版多态是C++中的一种编程特性,他利用模版来实现泛型编程,通过这种方式,C++可以根据传递给模版的类型,生成不同版本的代码。模版多态和传统的多态(比如通过虚函数实现的动态多态)不同,他是在编译时进行类型推导的,而不是运行时进行的。
模版多态允许一个函数或者类接受不同类型的参数,从而在多个类型之间共享相同的实现,这是通过模版函数和模版类来实现的。
7.3.1 模版函数的多态
模版函数是可以接受不同类型参数的函数,在编译时根据实际传入的类型来生成对应的函数。
#include <iostream>
using namespace std;// 模板函数
template <typename T>
T add(T a, T b) {return a + b;
}int main() {cout << add(3, 4) << endl; // 使用 int 类型cout << add(3.5, 4.5) << endl; // 使用 double 类型return 0;
}
7.3.2 模版类的多态
模版类允许我们创建一个类模版,后续实例化时可以根据类型生成特定类型的类。这也是模版多态的一种体现。
#include <iostream>
using namespace std;// 模板类
template <typename T>
class Box {
private:T value;
public:Box(T val) : value(val) {}T getValue() {return value;}
};int main() {Box<int> intBox(10); // 使用 int 类型Box<double> doubleBox(3.14); // 使用 double 类型cout << "Value in intBox: " << intBox.getValue() << endl;cout << "Value in doubleBox: " << doubleBox.getValue() << endl;return 0;
}
总结:
模版多态利用模版的泛型特性,使得用一段代码可以作用域不同类型的对象,从而避免了重复代码的编写。模版多态的主要特点是:
编译时多态:模版多态是在编译时根据类型推导生成不同的代码,而不像虚函数那样在运行时根据对象的类型选择函数
类型无关:通过模版,代码可以适应多种类型,而无需为每一个类型写多个版本的函数或者类
性能优化: 模版会在编译时生成特定的代码,因此不会引入运行时的开销
八、为什么做内存对齐??
为了提高程序的效率,特别是访问内存时,提高CPU的性能
8.1 CPU内存访问的限制
大多数现代计算机架构都有内存访问的对齐要求。通常,处理器在访问内存时,要求数据的地址是某一个特定字节边界的倍数。例如:
- 对于 32 位系统,可能要求整型数据(
int
)必须存储在 4 字节的边界上。 - 对于 64 位系统,要求长整型(
long
)存储在 8 字节的边界上。
这意味着,如果你在内存中存储的一个数据类型(比如int)的地址没有按照该数据类型的对齐要求进行对齐,处理器可能需要进行额外的内存访问来读取或者写入该数据,这样就会导致性能下降。
8.2 内存对齐的原因
内存对齐的主要目的是提高数据访问的效率:
8.2.1 性能提升
在不对齐的情况下,处理器可能需要执行多个内存操作来获取一个完整的数据单元。例如:假设我们有一个4字节的数据(比如int)如果他没有对齐在4字节边界上,处理器可能需要两次内存访问来获取完整的4字节,而不是一次。如果数据按要求对齐,处理器可以一次性读取完整数据,从而提高效率。
8.2.2 避免硬件异常
某些处理器架构可能会在访问未对齐的数据时抛出硬件异常或者产生错误。因此,进行内存对齐时避免这些问题的一个重要原因。
8.2.3 减少额外的计算和指令
如果数据没有对齐,处理器可能需要执行额外的计算或者使用更多的指令来处理数据。例如,处理器可能需要在访问数据前执行一些位移或者掩码操作,增加CPU负担
8.3 如何对齐
基本对齐:处理器会要求数据存储在某一个特定的字节边界上。例如,一个4字节的int类型存储在4字节边界上,一个8字节的double类型必须存储在8字节边界上。
结构体内存对齐:在结构体(或类)中,成员的顺序和对齐也会影响内存的布局。如果一个结构体的成员没有按照合适的对齐顺序排列,可能会在结构体的内部插入额外的填充字节来保证每一个成员正确对齐。
8.4 内存对齐的好处和影响
好处:
性能优化:合适的对齐能让CPU高效地访问内存。
避免错误:避免了不对齐时可能出现的硬件异常或者错误。
影响:
内存浪费:有时为了满足对齐要求,结构体或者类的大小可能会增加,导致内存浪费,尤其是在存储大量小数据结构
额外的填充字节:为了确保成员的对齐,编译器可能会在数据结构之间插入填充字节,这样会增加内存的占用
九、TCP粘包有哪几种方式
9.1 固定长度的协议
通过规定每一个数据包的固定长度
9.2 消息头加消息体
9.3 特殊分隔符方式
9.4 基于时间的方式
9.5 TCP流控制与应用协议结合
十、Qt中的connect()函数
在Qt中,connect()函数是一个非常重要的函数,用于将信号(signal)与槽(slot)连接起来。当信号发射时,相应的槽函数会被调用,从而实现事件驱动的编程。
10.1 connect()函数的基本语法
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *slot, Qt::ConnectionType type = Qt::AutoConnection
);
sender(const QObject* sender)
发送信号的对象,也就是说,信号是有哪一个对象发射的
通常这是一个QObject的派生类的实力(比如按钮,窗口等)
signal(const char* signal)
信号的名称,是一个字符串,表示发送信号的名称
格式为SIGNAL(signal_name)例如:
SIGNAL(clicked())
注意:signal字符串表示的是一个类中定义的信号方法
receiver(const QObject* receiver)
接收信号的对象,也就是,当信号发射时,哪一个对象的槽函数应该被调用。
这是接收信号的对象,一般是一个QObject派生类的实例
slot(const char* slot)
槽函数的名称:是一个字符串,表示接收信号并处理的函数
格式为SLOT(slot_name),例如:
SLOT(onButtonClicked())
注意:slot字符串表示的是一个类中定义的槽函数
type(Qt::ConnectionType type = Qt::AutoConnection)
- 这个参数是一个枚举类型,表示连接的方式。其默认值是
Qt::AutoConnection
,根据情况自动选择连接方式。 - 它的常用值有:
Qt::AutoConnection
:默认连接方式,Qt会根据信号和槽在同一线程或不同线程之间选择合适的连接方式。Qt::DirectConnection
:信号和槽在同一线程时,直接调用槽函数。Qt::QueuedConnection
:信号和槽在不同线程时,会将槽函数调用放入事件队列,等待接收线程处理。Qt::BlockingQueuedConnection
:信号和槽在不同线程时,发出信号的线程会等待槽函数执行完毕,才继续执行后面的代码。Qt::ConnectionType
还可以有更多的选项,但Qt::AutoConnection
和Qt::DirectConnection
是最常见的。
10.2 connect() 函数的作用
connect()用于建立信号和槽的连接,使得某一个对象的信号触发时,可以调用另一个对象的槽函数来处理相关的逻辑,这个连接的建立是异步的,信号发送后,槽函数的调用通常是异步处理的,Qt会负责管理调用的顺序和线程间的通信。
通过 connect()
的最后一个参数 Qt::ConnectionType
,你可以控制信号和槽连接的方式。下面是每种类型的解释:
-
Qt::AutoConnection(默认值):
- 如果信号和槽在同一线程中,使用
Qt::DirectConnection
,直接调用槽函数; - 如果信号和槽在不同线程中,使用
Qt::QueuedConnection
,将槽函数调用放入事件队列。
- 如果信号和槽在同一线程中,使用
-
Qt::DirectConnection:
- 信号发射时,直接调用槽函数。这通常在信号和槽在同一线程时使用。
-
Qt::QueuedConnection:
- 如果信号和槽在不同线程,信号发射时,槽函数调用会被放入接收线程的事件队列,接收线程会在其事件循环中执行槽函数。
-
Qt::BlockingQueuedConnection:
- 与
Qt::QueuedConnection
类似,但发射信号的线程会等待槽函数执行完成后再继续执行后续代码。通常用于需要同步执行的情况。
- 与
-
Qt::UniqueConnection:
- 确保信号和槽的连接是唯一的,避免重复连接相同的信号和槽。
十一、C++面向对象特性思想的理解
C++是一种支持面向对象编程的语言,其面向对象的思想主要包括以下几个核心特性:封装、继承、多态和抽象。
封装是面向对象编程的核心思想之一,他指的是将数据(成员变量)和操作数据的函数(成员函数)组合在一起,形成一个对象。封装的目的是隐藏实现细节,只暴漏必要的接口,确保对象的内部数据不能被随意访问和修改,从而提高了安全性和可维护性。
继承是面向对象的另一重要特性,他允许一个继承另一个类的成员和行为,从而实现代码重用。通过继承、一个类可以扩展或者修改其父类的行为,这是创建层次化对象模型的基础。
多态是面向对象编程中允许对象在不同情况下表现出不同行为的能力。C++中有三种多态:静态多态,动态多态,模版多态。
抽象是将对象的复杂性隐藏在一个简单的接口后面,只暴露出用户需要关心的信息,他有助于减少复杂性并增强代码的可维护性。抽象通常通过抽象类来实现,抽象类通常包含纯虚函数,无法直接实例化,只能通过继承来实现。
十二、vector和list的区别
他们用于存储和管理一系列数据,虽然他们都实现了容器的基本功能,比如存储元素。提供访问等,内部实现和性能特点存在一些显著差异。下面是主要区别:
12.1 内部实现
std::vector
是一个动态数组
在内存中是连续存储的,也就是说,所有元素在内存中的地址是连续的
这种连续的内存分配使得元素访问速度非常快,因为可以通过指针偏移直接访问任意位置的元素
std::list
是一个双向链表
他的元素在内存中不是连续存储的,而是分散的,每一个元素都包含指向前一个和后一个元素的指针
因为每一个元素都有指针,他的内存开销比std::vector大
12.2 随机访问
std::vector 支持随机访问,因为在内存时连续存储的,可以直接通过索引直接访问任意元素
std::list 不支持随机访问,要访问某一个元素,必须从头或者尾来时遍历链表
12.3 插入和删除操作
-
std::vector
:- 在末尾插入元素:平均是 O(1) 时间复杂度,因为它可以在数组末尾添加元素,且不需要移动其他元素。但在极少数情况下,可能会因为需要扩展内存而导致 O(n) 的时间复杂度。
- 在中间或前面插入元素:需要移动元素以保持数组的连续性,时间复杂度是 O(n)。
- 删除元素:同样需要移动元素,时间复杂度是 O(n)。
-
std::list
:- 插入和删除元素:在链表的头部或尾部插入和删除元素的时间复杂度是 O(1),因为链表的节点只是简单地通过指针连接,不需要移动其他元素。
- 在中间插入或删除元素:需要遍历链表找到指定位置,时间复杂度是 O(n),但一旦找到位置,插入或删除本身是 O(1) 操作。
12.4 内存开销
-
std::vector
:- 由于是动态数组,内存是连续分配的。它的内存开销比较低,仅仅需要为存储元素分配内存。
- 当元素个数超过当前容量时,
std::vector
会重新分配内存并复制元素,通常会分配比实际需要更多的内存,以减少频繁重新分配的开销。
-
std::list
:- 由于每个元素都包含两个指针(前向指针和后向指针),
std::list
的内存开销较大。 - 这种指针的开销使得
std::list
相比std::vector
在存储相同数量的元素时需要更多的内存。
- 由于每个元素都包含两个指针(前向指针和后向指针),
12.5 适用场景
-
std::vector
:- 适用于需要频繁随机访问、较少插入和删除操作的场景。
- 适用于元素个数变化较小或大多数操作发生在数组末尾的情况。
- 因为它在内存中是连续存储的,所以如果你需要大量的访问操作并且能接受较少的插入/删除操作,
std::vector
是一个更好的选择。
-
std::list
:- 适用于需要频繁插入和删除元素的场景,尤其是当这些操作发生在容器的头部或中间时。
- 适用于不需要随机访问元素,而是逐步遍历容器的情况。
12.6 缓冲友好性
std::vector
:由于元素在内存中是连续存储的,std::vector
对 CPU 缓存更友好。现代处理器的缓存行优化使得访问std::vector
时能够更高效地利用 CPU 缓存,从而提高性能。std::list
:由于std::list
的元素不在内存中连续存储,它的性能通常会受到缓存未命中的影响,尤其是在需要频繁访问元素时,可能会表现较差。
12.7 常见操作对比
操作 | std::vector | std::list |
随机访问元素 | O(1) | O(n) |
在尾部插入元素 | O(1)(均摊) | O(1) |
在中间插入元素 | O(n) | O(n)(需要找到位置) |
删除元素 | O(n) | O(n)(需要找到位置) |
内存开销 | 较低 | 较高 |
支持的操作类型 | 随机访问,尾部操作快 | 频繁插入删除操作更快 |
十三、了解过线程安全的问题
互斥锁
读写锁
原子操作
线程局部存储
条件变量
十四、tcp分包和粘包,丢包和抓包
1. tcp的分包和粘包
分包
分包指的是,在发送数据时,由于某些原因(例如数据较大,网络带宽或者传输延迟),TCP会将一个大的数据包拆分为多个小的数据包进行发送,这就会导致接收方接收到的多个小数据包并不是按照发送方发送的原始数据的边界进行接收的。
- 网络传输限制:由于 MTU(最大传输单元)的限制,数据包必须分片传输。
- 应用层数据过大:应用层发送的数据太大,无法一次传送,TCP 会自动进行分片。
粘包
粘包是指多个应用层数据包被合并成一个TCP数据包发送,接收端接收到的数据流没有明确的分隔标识,导致接收方无法知道每一个数据包的边界,无法准确解析每一个数据包。
- TCP 数据流特性:TCP 是一个面向字节流的协议,它不会维护应用层数据的边界。
- 网络传输的合并:如果两个小的数据包在传输过程中合并在一起,接收方就会读取到多个应用层的数据包在一起的情况。
2. tcp的丢包的抓包
丢包是指在网络通信过程中,由于各种原因(比如网络故障,拥堵,错误等)某些数据报未能到达接收方,尽管TCP协议提供可靠的数据传输,但仍然存在丢包的情况。
网络质量差:例如,网络链路不稳定、带宽不足、网络拥堵等。
路由器、交换机的缓存溢出:当网络设备的缓存满时,可能会丢弃一些数据包。
数据包的处理延迟:长时间的网络延迟或丢失的 ACK(确认包)可能导致丢包。
TCP如何处理丢包
重传机制:TCP使用超时重传和快速重传机制来处理丢包,丢失的数据包会被标记,并且接收方通过ACK确认收到的包,发送方会根据重传计时器或接收到的丢失确认来重发修饰的数据包。
抓包:
抓包是指通过一些工具捕获网络中的数据包,从而分析网络通信中的问题或者调试应用程序。常见的抓包工具有 Wireshark、tcpdump、Fiddler 等。
抓包的用途:
- 网络故障排查:帮助开发者查看网络请求是否成功,数据是否完整,响应时间是否符合预期。
- 性能分析:通过抓包分析,可以检测网络中的延迟、带宽使用情况等。
- 安全分析:抓包可以帮助检查是否存在不安全的通信,比如明文传输密码等。
总结
- 分包 和 粘包 问题是由于 TCP 的字节流特性导致的,可以通过数据包头部长度字段、分隔符等方式来解决。
- 丢包 可能是由于网络拥堵或设备故障引起的,但 TCP 提供了重传机制来确保数据的可靠性。
- 抓包 是通过网络抓包工具捕获并分析网络流量,用于调试、性能优化或安全分析。
十五、new申请对象导致的异常问题
在C++中,使用new操作符来动态分配内存时,可能会遇到异常的问题。通常,new用于分配内存,创建对象或者数组。然而,如果内存不足,或者出现了其他资源限制,new可能会抛出异常。
1. 内存不足导致的异常
当系统的内存资源不足时,new可能无法分配所请求的内存块,在这种情况下,new会抛出 std::bad_alloc 异常,表示分配内存失败。
try {int* p = new int[100000000000]; // 请求分配大量内存
} catch (const std::bad_alloc& e) {std::cout << "内存分配失败: " << e.what() << std::endl;
}
std::bad_alloc:这是一个标准库异常,表示内存分配失败,new在内存分配失败时会抛出这个异常。
2. 如何避免异常
如果你想避免new引发异常,有两种常见的方法:
使用nothrow:可以通过new(std::nothrow)来避免new抛出异常,而是返回一个nullptr
int* p = new(std::nothrow) int[100000000000];
if (!p) {std::cout << "内存分配失败" << std::endl;
}
检查内存分配:另外,在进行new时,可以提前检查内存大小,避免尝试分配过大的内存
3. new 和 new[ ] 的区别
在 C++ 中,new可以用来分配单个对象或者数组
new:用来分配单个对象
new[ ]:用来分配数组
这两者在内存分配方式上有所不同,但它们都可能因为内存不足而引发异常。
4. 如何处理异常
在C++中,new抛出的异常应该被正确处理。一般情况下,可以通过try和catch语句来捕获异常,避免长须崩溃。
try {int* p = new int[1000000]; // 假设分配大量内存
} catch (const std::bad_alloc& e) {std::cerr << "内存分配失败: " << e.what() << std::endl;// 执行必要的错误处理
}
捕获std::bad_alloc异常后,程序可以做适当的错误处理,比如释放已经分配的资源、记录日志、提示用户等。
5. delete 和 delete[ ] 配对使用
使用 new
或 new[]
动态分配内存时,必须使用 delete
或 delete[]
进行释放,以避免内存泄漏。注意:
- 如果使用
new
分配的内存,应该使用delete
来释放: - 如果使用
new[]
分配的内存,应该使用delete[]
来释放:
使用错误的释放方式(比如 delete[]
释放单个对象,或者 delete
释放数组)会导致未定义行为,甚至程序崩溃。
总结:
new
操作符在内存分配失败时,会抛出std::bad_alloc
异常,表示内存不足。- 使用
std::nothrow
可以让new
返回nullptr
,而不会抛出异常。 - 必须在使用完动态分配的内存后,及时调用
delete
或delete[]
来释放资源,防止内存泄漏。 - 使用
try-catch
块来捕获std::bad_alloc
异常,可以帮助更好地处理内存分配失败的情况,确保程序稳定运行。
十六、qt的信号槽实现原理
Qt的信号和槽机制是Qt的核心特性之一,他用于对象之间的通信,他使得对象之间的通信需要直接依赖彼此,从而实现了松耦合,信号和槽机制的实现原理主要依赖与Qt的源对象系统和事件驱动模型。
Qt 实现信号槽的机制主要依赖于 元对象编译器(MOC) 和 事件系统。这个机制让 Qt 的信号槽功能和标准的 C++ 方法调用机制区别开来。
3.1 MOC(Meta-Object Compiler)
MOC 是 Qt 的一个关键部分,它是一个工具,用来处理 Qt 特有的扩展,例如信号和槽的声明。
- 当你声明一个信号或槽时,你实际上是在声明一个特殊的元对象系统功能。
- MOC 会扫描你的头文件,查找
Q_OBJECT
宏,并生成一个包含信号和槽的元信息的代码文件。 - MOC 会为每个对象创建一个内部的 元对象(Meta-Object),包含类的信息、信号、槽等。
MOC 会为 valueChanged
信号生成相关的代码,使得该信号能够通过 connect()
与槽函数连接。
3.2 信号和槽的连接
在 Qt 中,信号和槽的连接是通过 QObject::connect()
函数完成的。Qt 使用函数指针和内部机制来实现信号与槽的绑定。
十七、右值引用
右值引用是C++11引入的一种新类型,他使得C++语言具有了更好的资源管理和性能优化,尤其是在移动语义和完美转发方面。
右值引用的基本概念
左值:可以作为赋值运算符左边的对象(有持久地址的对象)
右值:通常是一个临时对象或者是表达式的结果(没有持久地址的对象)
右值引用的语法
右值引用通过双引号&&表示,区别于左值引用的单括号&
右值引用的用途
移动语义:通过右值引用,可以在不进行拷贝的情况下转移资源,从而提高性能。例如:std::vector的元素在移动时会把资源从一个对象转移到另一个对象,而不是复制。
std::vector<int> vec1 = {1, 2, 3};
std::vector<int> vec2 = std::move(vec1); // 通过移动语义,vec1的资源被转移到vec2
完美转发:当传递参数时,右值引用可以用于完美转发,即既能传递左值,也能传递右值
template <typename T>
void f(T&& arg) { /* 处理传入的参数 */ }int x = 10;
f(x); // 传递左值
f(20); // 传递右值
左值引用和右值引用的区别
左值引用:可以绑定在左值
右值引用:可以绑定到右值,但不能绑定到左值
注意点:
std::move :std::move是一个强制类型转换,他将左值转换为右值引用,从而允许进行资源的移动操作。
18、列表初始化和放在构造函数体中的初始化有什么区别??
在C++中,列表初始化和在构造函数体中的初始化有一些重要的区别,让我们分别讨论一下:
列表初始化
列表初始化是通过构造函数的初始化列表来初始化对象的成员,通常在构造函数的参数列表后面跟上一对花括号{}进行赋值。列表初始化可以用于所有类型的成员变量。
class MyClass {
public:int x;double y;MyClass(int x_val, double y_val) : x(x_val), y(y_val) {}
};
在构造函数体中初始化
在构造函数体内,你可以通过赋值操作来初始化成员变量:
class MyClass {
public:int x;double y;MyClass(int x_val, double y_val) {x = x_val;y = y_val;}
};
区别:
效率:
- 列表初始化通常在比构造函数体中的初始化更加高效,因为他直接在对象的构造阶段初始化成员变量,而不是先构造对象再赋值。
- 在构造函数体中,成员变量在默认构造后会被赋值,这会导致一次不必要的默认构造和赋值操作。
初始化顺序:
- 在构造函数的初始化列表中,成员变量会按声明顺序初始化(而非参数顺序)。这对于避免依赖于成员初始化顺序的错误非常重要。
- 在构造函数体内的赋值初始化会按照代码的顺序执行,这可能会导致不可预期的结果,特别是当你有依赖顺序的初始化时。
常量和引用的初始化
- 常量成员变量和引用类型的成员变量只能通过初始化列表进行初始化,如果你试图在构造函数体内初始化常量成员或者引用变量,编译器会报错。
避免隐式调用默认构造函数
- 对于某些类型的成员变量,构造函数体中的赋值语句可能会先调用默认构造函数,在通过赋值语句进行初始化,这样可能会导致不必要的默认构造函数调用。
- 列表初始化避免了这些问题,直接调用对应的构造函数。
推荐使用哪一种??
推荐使用列表初始化:他通常更简洁、高效、避免了一些潜在的错误。
只有当成员变量是常量或者引用类型时,必须使用列表初始化。
什么时候只能使用列表初始化?
常量成员变量:常量成员必须子啊初始化列表中初始化,不能在构造函数体内初始化。
引用成员变量: 引用类型的成员变量也必须通过初始化列表进行初始化。