2024-01-08 20:13 星期一
博客内容来自相关书籍和网站内容总结,仅供个人参考使用:笔者@StuBoo
使用目录快速转到技术面试问题汇总、算法笔记
1.C++语言基础
1.1 语言特性
面向对象编程(OOP):C++ 支持面向对象编程,包括封装、继承和多态。通过类和对象,可以将数据和方法组织成单个单元,提高了代码的重用性和可维护性。
标准模板库(STL):C++ 提供了 STL,它包含了一组模板类和函数,例如容器(如vector、list、map)、算法(如排序、搜索等)和迭代器等,提供了高效的数据结构和算法。
泛型编程:C++ 具有模板(Templates)特性,使得可以编写通用代码,使代码可以适用于不同的数据类型,实现了泛型编程。
内存管理:C++ 具有直接的内存管理能力,可以使用指针、引用和动态内存分配等机制来精确地控制内存的分配和释放。
多线程编程:通过标准库中的thread头文件可以创建和管理线程。这个头文件包含了用于操作线程的类和函数。C++11 引入了跨平台的线程库,允许开发者在不同的操作系统上使用相同的线程接口。
网络编程:C++ 通过各种库和框架(如Boost.Asio、Poco、CppNetLib等)提供了强大的网络编程支持。它允许开发者创建客户端和服务器应用程序,进行网络通信,支持各种网络协议(如TCP、UDP、HTTP等)
1.2 OPP
C++ 是一种支持面向对象编程(OOP)的语言,它的三大面向对象特性包括:封装、继承和多态。
- 封装(Encapsulation):
封装是一种将数据和操作(方法或函数)捆绑在一起并限制外部访问的机制。通过使用类,C++ 允许将数据(成员变量)和方法(成员函数)组合成一个单一的单元,这样可以控制数据的访问权限。
类中的数据成员可以被标记为私有(private)或受保护(protected),从而限制对它们的直接访问,而对外提供公共接口(公有成员函数)来间接操作这些数据,这就是封装的体现。 - 继承(Inheritance):
继承是一种允许一个类(派生类/子类)从另一个类(基类/父类)继承属性和行为的机制。派生类可以重用基类的成员变量和成员函数,并且可以添加自己的特定成员。
通过继承,可以构建出一个类层次结构,使得代码的重用性更强,同时也能更好地组织和管理类之间的关系。 - 多态(Polymorphism):
多态性允许一个函数在不同的对象上表现出不同的行为。它可以通过函数重载(函数名相同,参数列表不同)、运算符重载、虚函数和抽象类(接口)实现。
最常见的多态性形式是运行时多态(动态多态),通过虚函数和继承实现。在运行时,根据实际对象的类型调用相应的函数,这种机制称为动态绑定。
1.2.1 多态
静态多态
静态多态是指在编译时就能确定要调用的函数,它主要通过函数重载和运算符重载来实现。编译器在编译阶段确定调用的函数,因此也称为早期绑定。
实现原理:
函数重载:函数名称相同,但参数类型或个数不同。
运算符重载:重载了类的成员函数或全局函数,使其具有不同的行为。
代码示例:
#include <iostream>
class StaticPolymorphism {
public:
// 函数重载实现静态多态
void print(int value) {
std::cout << "Integer value: " << value << std::endl;
}
void print(double value) {
std::cout << "Double value: " << value << std::endl;
}
};
int main() {
StaticPolymorphism obj;
obj.print(5); // 调用 print(int)
obj.print(3.14); // 调用 print(double)
return 0;
}
动态多态
动态多态是指在运行时根据对象的实际类型来确定调用的函数。主要通过虚函数和继承来实现。在运行时,根据对象的类型确定调用的函数,因此也称为晚期绑定。
实现原理:
虚函数:在基类中使用 virtual 关键字声明的函数,可以被派生类重写。
继承:派生类可以覆盖基类中的虚函数,根据实际对象的类型进行调用。
代码示例:
#include <iostream>
class Base {
public:
// 虚函数实现动态多态
virtual void print() {
std::cout << "Base class print() called" << std::endl;
}
};
class Derived : public Base {
public:
// 覆盖基类的虚函数
void print() override {
std::cout << "Derived class print() called" << std::endl;
}
};
int main() {
Base* basePtr;
Base baseObj;
Derived derivedObj;
basePtr = &baseObj;
basePtr->print(); // 调用 Base::print()
basePtr = &derivedObj;
basePtr->print(); // 调用 Derived::print()
return 0;
}
静态多态和动态多态的区别:
绑定时间 | 实现方式 | 适用性 | 效率 | |
---|---|---|---|---|
静态多态 | 在编译时确定调用的函数,早期绑定 | 通过函数重载和运算符重载实现 | 适用于函数重载和运算符重载,但不适用于类的层次结构 | 效率更高,因为函数调用在编译时确定 |
动态多态 | 在运行时确定调用的函数,晚期绑定 | 通过虚函数和继承实现 | 适用于类的层次结构,通过派生类重写基类的虚函数来实现多态 | 运行时需要额外的虚函数查找,可能会有些许性能损失 |
1.2.2 动态多态
在 C++ 中,多态性的主要实现依赖于虚函数(Virtual Functions)和虚函数(vtable)。
虚函数:
虚函数是在基类中声明并使用 virtual 关键字标记的成员函数。它允许在派生类中进行覆盖(重写),并在运行时根据对象的实际类型调用相应的函数。
在基类中声明为虚函数的函数可以在派生类中被重写。如果在派生类中使用 override 关键字重新定义了基类的虚函数,编译器会检查其覆盖关系,从而提高代码的可读性和安全性。
虚函数允许基类指针或引用指向派生类对象,并且在运行时根据实际对象的类型来决定调用的函数,这就是多态性的体现。
虚函数表(vtable):
虚函数表是用来实现动态多态性的关键机制之一。对于每个含有虚函数的类,编译器会生成一个对应的虚函数表。
虚函数表是一个存储虚函数地址的表格,每个对象都有一个指向其类的虚函数表的指针。该指针通常称为虚指针(vptr),它指向对象所属类的虚函数表。
在运行时,当调用虚函数时,实际上是通过对象的虚指针找到对应的虚函数表,然后根据函数在虚函数表中的索引调用正确的函数。
如果派生类重写了基类的虚函数,那么虚函数表中的对应位置会被新的函数地址所覆盖,确保调用时能够执行派生类的函数。
在 C++ 中,虚函数指针(vptr)是实现动态多态性的关键。在继承体系中,子类继承了基类的虚函数并且可以覆盖(重写)这些虚函数。
虚函数指针(vptr):
存储虚函数表地址:每个类(含有虚函数的类)的对象都包含一个指向其类的虚函数表(vtable)的指针,称为虚函数指针(vptr)。
动态绑定:在运行时,通过虚函数指针可以动态定位到对象的虚函数表,并根据实际对象的类型来确定调用的函数。
多态实现:通过虚函数指针实现运行时多态性,当基类指针指向派生类对象时,调用虚函数会根据对象的实际类型在虚函数表中找到正确的函数进行调用,这就是多态性的体现。
子类继承虚函数:
覆盖基类的虚函数:派生类可以重新定义(覆盖)基类中的虚函数,如果派生类中重新定义了基类的虚函数,那么派生类的虚函数表会覆盖基类相应位置的函数地址。
动态绑定:子类继承了基类的虚函数,如果子类重新定义了基类的虚函数,那么在使用派生类对象调用该虚函数时,根据派生类对象的实际类型确定调用的是派生类的虚函数还是基类的虚函数。
代码示例:
#include <iostream>
class Base {
public:
virtual void show() {
std::cout << "Base::show()" << std::endl;
}
};
class Derived : public Base {
public:
void show() override {
std::cout << "Derived::show()" << std::endl;
}
};
int main() {
Base* ptr;
Base baseObj;
Derived derivedObj;
ptr = &derivedObj; // 基类指针指向派生类对象
ptr->show(); // 调用派生类的虚函数
return 0;
}
基类指针 ptr 指向派生类对象 derivedObj 时,调用 ptr->show() 时,根据 ptr 所指向的实际对象类型(派生类对象),会动态地调用派生类 Derived 中的 show() 函数,进行动态绑定,从而实现多态。
1.3 STL
1.3.1STL六大组件
STL大体分为六大组件,分别是:容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器
1.容器:各种数据结构,如vector、list, deque,set、map等,用来存放数据。
2.算法:各种常用的算法,如sort、find、copy、for_each等
3.迭代器:扮演了容器与算法之间的胶合剂。
4.仿函数:行为类似函数,可作为算法的某种策略。
5.适配器:一种用来修饰容器或者仿函数或迭代器接口的东西。
6.空间配置器:负责空间的配置与管理。
容器
STL容器就是将运用最广泛的一些数据结构实现出来。
常用的数据结构:数组,链表,树,栈,队列,集合,映射表等这些容器分为序列式容器和关联式容器两种:
- 关联式容器:二叉树结构,各元素之间没有严格的物理上的顺序关系。
- 序列式容器:强调值的排序,序列式容器中的每个元素均有固定的位置。
算法
有限的步骤,解决逻辑或数学上的问题,这一门学科我们叫做算法(Algorithms)
算法分为:质变算法和非质变算法。
- 质变算法:是指运算过程中会更改区间内的元素的内容。例如拷贝,替换,删除等等。
- 非质变算法:是指运算过程中不会更改区间内的元素内容,例如查找、计数、遍历、寻找极值等等。
迭代器
提供一种方法,便之能够依序寻访某个容器所含的各个元素,而又无需暴露该容器的内部表示方式。
每个容器都有自己专属的迭代器。
迭代器使用非常类似于指针,初学阶段我们可以先理解迭代器为指针。
迭代器种类:
种类 | 功能 | 支持运算 |
---|---|---|
输入迭代器 | 对数据的只读访问 | 只读,支持++、==、! = |
输出迭代器 | 对数据的只写访问 | 只写,支持++ |
前向迭代器 | 读写,并能向前推进迭代器 | 读写,支持++、==、!= |
双向迭代器 | 读写,并能向前和向后操作 | 读写,支持++、-, |
随机访问迭代器 | 读写,可以跳跃访问任意数据,功能最强的迭代器 | 读写,支持++、-、[n]、-n、<、<=、>、>= |
常用的容器中迭代器种类为双向迭代器,和随机访问迭代器。
容器迭代器示例
- 最常见容器Vector,可以理解为数组。
vector存放内置数据类型
容器:vector 算法:for_each 迭代器: vector\<int>::iterator
1.3.2STL-常用容器
1.3.2.1string容器
- string基本概念
本质:string是C++风格的字符串,而string本质上是一个类。
string和char* 区别: char*是一个指针,string是一个类,类内部封装了char*,管理这个字符串,是一个char*型的容器。
string类内部封装了很多成员方法,例如:查找find,拷贝copy,删除delete,替换replace,插入insert。
string管理char*所分配的内存,不用担心复制越界和取值越界等,由类内部进行负责。
- string构造函数
构造函数原型:
string(); //创建一个空的字符串例如:string str;
string(const char* s); //使用字符串s初始化
string(const string& str); //使用一个string对象初始化另一个string对象
string(int n, char c); //使用n个字符c初始化
string赋值操作
功能描述:
·给string字符串进行赋值
赋值的函数原型:
string& operator=(const char* s); //char*类型字符串赋值给当前的字符串
string& operator=(const string &s); //把字符串s赋给当前的字符串
string& operator=(char c); //字符赋值给当前的字符串
string& assign(const char *s); //把字符串s赋给当前的字符串
string& assign(const char *s,int n); //把字符串s的前n个字符赋给当前的字符串
string& assign(const string &s); //把字符串s赋给当前字符串
string& assign(int n,char c); //用n个字符c赋给当前字符串
string字符串拼接
功能描述:实现在字符串末尾拼接字符串
函数原型:
string& operator+=(const char* str); //重载+=操作符
string& operator+=(const char c); //重载+=操作符
string& operator+=(const string& str); //重载+=操作符
string& append(const char *s); //把字符串s连接到当前字符串结尾
string& append(const char *s, int n); //把字符串s的前n个字符连接到当前字符串结尾
string& append(const string &s); //同operator+=(const string& str)
string& append(const string &s, int pos, int n); //字符串s中从pos开始的n个字符连接到字符串结尾
- string查找和替换
功能描述:
- 查找:查找指定字符串是否存在
- 替换:在指定的位置替换字符串 I
函数原型:
int find(const string& str, int pos =0) const; //查找str第一次出现位置,从pos开始查找
int find(const char* s, int pos =0) const; //查找s第一次出现位置,从pos开始查找
int find(const char* s, int pos, int n) const; //从pos位置查找s的前n个字符第一次位置
int find(const char c, int pos =0) const; //查找字符C第一次出现位置
int rfind(const string& str, int pos = npos) const; //查找str最后一次位置,从pos开始查找
int rfind(const char* s, int pos = npos) const; //查找s最后一次出现位置,从pos开始查找
int rfind(const char* s, int pos, int n) const; //从pos查找s的前n个字符最后一次位置
int rfind(const char c, int pos=0)const; //查找字符C最后一次出现位置
string& replace(int pos, int n, const string& str); //替换从pos开始n个字符为字符串str
string& replace(int pos, int n,const char* s); //替换从pos开始的n个字符为字符串s
- string字符串比较
功能描述:
- 字符串之间的比较
字符串比较是按字符的ASCII码进行对比
=返回 0
>返回 1
<返回-1
函数原型:
int compare(const string &s) const; //与字符串s比较
int compare(const char *s) const; //与字符串s比较
- string字符串存取
char& operator[](int n); //通过[]方式取字符
char& at(int n); //通过at()方式取字符
- string插入和删除
功能描述:
- 对string字符串进行插入和删除字符操作
函数原型:
string& insert(int pos, const char*s); //插入字符串
string& insert(int pos, const string& str); //插入字符串
string& insert(int pos, int n,char c); //在指定位置插入n个字符C
string& erase(int pos, int n= npos); //删除从Pos开始的n个字符
- string子串
string substr(int pos = 0, int n= npos) const; //返回由pos开始的n个字符组成的字符串
1.3.2.2vector容器
1 vector基本概念
vector数据结构和数组非常相似,也称为单端数组
vector与普通数组区别:数组是静态空间,而vector可以动态扩展
- 动态扩展:并不是在原空间之后续接新空间,而是找更大的内存空间,然后将原数据拷贝新空间,释放原空间
2 vector构造函数
vector<T> v; //采用模板实现类实现,默认构造函数
vector(v.begin(), v.end()); //将v[begin(),end())区间中的元素拷贝给本身。
vector(n, elem); //构造函数将n个elem拷贝给本身。
vector(const vector &vec); //拷贝构造函数。
3 vector赋值操作
vector& operator=(const vector &vec); //重载等号操作符
assign(beg,end); //将[beg,end)区间中的数据拷贝赋值给本身。
assign(n, elem); //将n个elem拷贝赋值给本身。
4 vector容量和大小
empty(); //判断容器是否为空
capacity(); //容器的容量
size(); //返回容器中元素的个数
resize(int num); //重新指定容器的长度为num,若容器变长,则以默认值填充新位置。如果容器变短,则末尾超出容器长度的元素被删除。
resize(int num,elem); //重新指定容器的长度为num,若容器变长,则以elem值填充新位置。如果容器变短,则末尾超出容器长度的元素被删除
5 vector插入和删除
push_back(ele); //尾部插入元素ele
pop_back(); //删除最后一个元素
insert(const_iterator pos, ele); //迭代器指向位置pos插入元素ele
insert(const_iterator pos,int count,ele); //迭代器指向位置pos插入count个元素
eleerase(const_iterator pos); //删除迭代器指向的元素
erase(const_iterator start,const_iterator end); //删除迭代器从start到end之间的元素clear(); //删除容器中所有元素
6 vector数据存取
at(iht idx); //返回索引idx所指的数据
operator[]; //返回索引idx所指的数据
front(); //返回容器中第一个数据元素
back(); //返回容器中最后一个数据元素
7 vector互换容器
swap(vec); //将vec与本身的元素互换
8 vector预留空间
reserve(int len);//容器预留len个元素长度,预留位置不初始化,元素不可访问。
1.3.2.3deque容器(daike)
1 deque容器基本概念
双端数组,可以对头端进行插入删除操作
deque与vector区别:
- vector对于头部的插入删除效率低,数据量越大,效率越低
- deque相对而言,对头部的插入删除速度回比vector快
- vector访问元素时的速度会比deque快,这和两者内部实现有关
deque内部工作原理:
deque内部有个中控器维护每段缓冲区中的内容,缓冲区中存放真实数据中控器维护的是每个缓冲区的地址,使得使用deque时像一片连续的内存空间。
deque容器的迭代器也是支持随机访问的
deque<T> deqT; //默认构造形式
deque(beg, nd); //构造函数将[beg, end)区间中的元素拷贝给本身。
deque(n, elem); //构造函数将n个elem拷贝给本身。
deque(const deque &deq); //拷贝构造函数
2 deque赋值操作
deque&roperator=(const deque&deq); //重载等号操作符
assign(beg,end); //将[beg,end)区间中的数据拷贝赋值给本身。
assign(n,elem); //将n个elem拷贝赋值给本身。
3 deque大小操作
deque.empty(); //判断容器是否为空
deque.size(); //返回容器中元素的个数
deque.resize(num); //重新指定容器的长度为num,若容器变长,则以默认值填充新位置。
//如果容器变短,则末尾超出容器长度的元素被删除。
deque.resize(num, elem); //重新指定容器的长度为num,若容器变长,则以elem值填充新位置。
//如果容器变短,则末尾超出容器长度的元素被删除。
4 deque插入和删除
push_back(elem); //在容器尾部添加一个数据
push_front(elem); //在容器头部插入一个数据
pop_back(); //删除容器最后一个数据
pop_front(); //删除容器第一个数据
insert(pos,elem); //在pos位置插入一个elem元素的拷贝,返回新数据的位置。
insert(pos,n,elem); //在pos位置插入n个elem数据,无返回值。
insert(pos,beg,end); //在pos位置插入[beg,end)区间的数据,无返回值。
clear(); //清空容器的所有数据erase(beg,end);//删除[beg,end)区间的数据,返回下一个数据的位置。
erase(pos); //删除pos位置的数据,返回下一个数据的位置。
5 deque数据存取
at(int idx); //返回索引idx所指的数据
operator[]; //返回索引idx所指的数据
front(); //返回容器中第一个数据元素
back(); //返回容器中最后一个数据元素
6 deque排序
sort(iterator beg,iterator end) //对beg和end区间内元素进行排序
1.3.2.4stack 常用接口
构造函数:
stack<T> stk; //stack采用模板类实现,stack对象的默认构造形式
stack(const stack &stk); //拷贝构造函数
赋值操作:
stack& operator=(const stack &stk); //重载等号操作符
数据存取:
push(elem); //向栈顶添加元素
pop(); //从栈顶移除第一个元素
top(); //返回栈顶元素
大小操作:
empty(); //判断堆栈是否为空
size(); //返回栈的大小
1.3.2.5queue 常用接口
构造函数:
queue<T> que; //queue采用模板类实现,queue对象的默认构造形式
queue(const queue &que); //拷贝构造函数
赋值操作: I
queue& operator=(const queue &que); //重载等号操作符
数据存取:
push(elem); //往队尾添加元素
pop(); //从队头移除第一个元素
back(); //返回最后一个元素
front(); //返回第一个元素
大小操作:
empty(); //判断堆栈是否为空
size(); //返回栈的大小
1.3.2.6list容器
1 list基本概念
链表(list):是一种物理存储单元上非连续的存储结构,数据元素的逻辑顺序是通过链表中的指针链接来实现的。
- 链表的组成:链表由一系列结点组成。
- 结点的组成:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
- SLT中的链表是一个双向循环链表。
由于链表的存储方式并不是连续的内存空间,因此链表list中的迭代器只支持前移和后移,属于双向迭代器。
list的优点:
- 采用动态存储分配,不会造成内存浪费和溢出
- 链表执行插入和删除操作十分方便,修改指针即可,不需要移动大量元素
list的缺点:
- 链表灵活,但是空间(指针域)和时间(遍历)额外耗费较大。
- list有一个重要的性质,插入操作和删除操作都不会造成原有list迭代器的失效,这在vector是不成立的。
2 list构造函数
list<T> lst; //list采用采用模板类实现,对象的默认构造形式:
list(beg,end); //构造函数将[beg,end)区间中的元素拷贝给本身。//构造函数将n个elem拷贝给本身。
list(n,elem);
list(const list &lst); //拷贝构造函数。
3 list赋值和交换
assign(beg, end); //将[beg,end)区间中的数据拷贝赋值给本身。
assign(n, elem); //将n个elem拷贝赋值给本身。
list& operator=(const list &lst); //重载等号操作符
swap(lst); //将lst与本身的元素互换。
4 list大小操作
size(); //返回容器中元素的个数
empty(); //判断容器是否为空
resize(num); //重新指定容器的长度为num,若容器变长,则以默认值填充新位置。//如果容器变短,则末尾超出容器长度的元素被删除。
resize(num, elem); //重新指定容器的长度为num,若容器变长,则以elem值填充新位置。//如果容器变短,则末尾超出容器长度的元素被删除。
5 list插入和删除
push_back(elem);//在容器尾部加入一个元素
pop_back();//删除容器中最后一个元素
push_front(elem);//在容器开头插入一个元素
pop_front();//从容器开头移除第一个元素
insert(pos,elem);//在pos位置插elem元素的拷贝,返回新数据的位置。
insert(pos,n,elem);//在pos位置插入n个elem数据,无返回值。
insert(pos,beg,end);//在pos位置插入[beg,end)区间的数据,无返回值。
clear();//移除容器的所有数据
erase(beg,end);//删除[beg,end)区间的数据,返回下一个数据的位置。
erase(pos);//删除pos位置的数据,返回下一个数据的位置。
remove(elem);//删除容器中所有与elem值匹配的元素。
6 list数据存取
front(); /返回第一个元素。
back(); //返回最后一个元素。
7 list反转和排序
reverse();//反转链表
sort(); //链表排序
1.3.2.7set/multiset容器
1 set基本概念
所有元素都会在插入时被自动排序。
set/multiset属于关联式容器,底层结构是用二叉树实现。
set和multiset区别:
- set不允许容器中有重复的元素。
- multiset允许容器中有重复的元素
2 set构建和赋值
set<T> st; //默认构造函数:
set(const set &st); //拷贝构造函数
set& operator=(const set &st); //重载等号操作符
3 set大小和交换
sized; //返回容器中元素的数目
empty(); //判断容器是否为空
swap(st); //交换两个集合容器
4 set插入和删除
insert(elem); //在容器中插入元素。
clear(); //清除所有元素
erase(pos); //删除pos迭代器所指的元素,返回下一个元素的迭代器。
erase(beg, end); //删除区间[beg,end)的所有元素,返回下一个元素的迭代器。erase(elem); //删除容器中值为elem的元素。
5 set查找和统计
find(key); //查找key是否存在,若存在,返回该键的元素的迭代器;若不存在,返回set.end(;
count(key); 11统计key的元素个数
6 set和multiset区别
区别:
- set不可以插入重复数据,而multiset可以
- set插入数据的同时会返回插入结果,表示插入是否成功
- multiset不会检测数据,因此可以插入重复数据
总结:
- 如果不允许插入重复数据可以利用set
- 如果需要插入重复数据利用multiset
1.3.2.8 pair
C++ STL 标准库提供了 pair 类模板,其专门用来将 2 个普通元素 first 和 second(可以是 C++ 基本数据类型、结构体、类自定的类型)创建成一个新元素 <first, second>。
pair将2个数据组合成一组数据,当需要这样的需求时就可以使用pair,如stl中的map就是将key和value放在一起来保存。另一个应用是,当一个函数需要返回2个数据的时候,可以选择pair。 pair的实现是一个结构体,主要的两个成员变量是first,second 因为是使用struct不是class,所以可以直接使用pair的成员变量。
pair以类模板的形式定义在 utility 头文件,并位于命名空间 std 中,在 C++ 11 标准之前,pair 类模板中提供了以下 3 种构造函数:
// 默认构造函数,即创建空的 pair 对象
pair();
// 直接使用 2 个元素初始化成 pair 对象
pair (const first_type& a, const second_type& b);
// 拷贝(复制)构造函数,即借助另一个 pair 对象,创建新的 pair 对象
template<class U, class V> pair (const pair<U,V>& pr);
在 C++ 11 标准中,在引入右值引用的基础上,pair 类模板中又增添了如下 2 个构造函数:
// 移动构造函数
template<class U, class V> pair (pair<U,V>&& pr);
// 使用右值引用参数,创建 pair 对象
template<class U, class V> pair (U&& a, V&& b);
成员变量:
first: 用于访问 pair 中的第一个元素。
second: 用于访问 pair 中的第二个元素。
比较运算符:
==, !=, <, <=, >, >=: 比较两个 pair 对象。按照字典序,先比 first ,然后比 second 。
交换函数:
swap(pair<T1, T2>& x, pair<T1, T2>& y): 用于交换两个 pair 对象的值。
make_pair 函数:
make_pair(T1&& x, T2&& y): 创建并返回一个 pair 对象,使用给定的参数初始化。
示例:
#include <iostream>
#include <utility>
int main() {
// 构造函数
std::pair<int, double> myPair(10, 3.14);
// 访问成员变量
std::cout << "第一个元素: " << myPair.first << std::endl;
std::cout << "第二个元素: " << myPair.second << std::endl;
// 比较运算符
std::pair<int, double> anotherPair(20, 6.28);
if (myPair == anotherPair) {
std::cout << "Pairs 相等。" << std::endl;
} else {
std::cout << "Pairs 不相等。" << std::endl;
}
// 交换函数
std::swap(myPair, anotherPair);
// make_pair 函数
auto newPair = std::make_pair(42, 7.0);
return 0;
}
通过tie获取pair元素值
在某些清况函数会以pair对象作为返回值时,可以直接通过std::tie进行接收。
std::pair<std::string, int> getPreson() {
return std::make_pair("Sven", 25);
}
int main(int argc, char **argv) {
std::string name;
int ages;
std::tie(name, ages) = getPreson();
std::cout << "name: " << name << ", ages: " << ages << std::endl;
return 0;
}
1.3.2.9 map/multimap容器
1 map基本概念
map中所有元素都是pair
pair中第一个元素为key(键值),起到索引作用,第二个元素为value(实值)
所有元素都会根据元素的键值自动排序
map/multimap属于关联式容器,底层结构是用二叉树实现,可以根据key值快速找到value值
区别:
- map和multimap区别
- map不允许容器中有重复key值元素
- multimap允许容器中有重复key值元素
3 map构造和赋值
map<T1,T2> mp; //map默认构造函数:
map(const map &mp); //拷贝构造函数
map&operator=(const map &mp); //重载等号操作符
4 map大小和交换
size(); //返回容器中元素的数目
empty(); //判断容器是否为空
swap(st); //交换两个集合容器
5 map插入和删除
insert(elem); //在容器中插入元素。
clear(); //清除所有元素
erase(pos); //删除pos迭代器所指的元素,返回下一个元素的迭代器。
erase(beg, end); //删除区间[beg,end)的所有元素,返回下一个元素的迭代器。
erase(key); //删除容器中值为key的元素。
6 map查找和统计
find(key); //查找key是否存在,若存在,返回该键的元素的迭代器;若不存在,返回set.end();
count(key); //统计key的元素个数
1.3.2.10 优先队列Priority_queue
1.简介
优先队列是一种极其特殊的队列,他与标准的队列使用线性结构进行计算不同,优先队列的底层是以散列的状态(非线性)表现的,
它与标准的队列有如下的区别,标准的队列遵从严格的先进先出,优先队列并不遵从标准的先进先出,而是对每一个数据赋予一个权值,
根据当前队列权值的状态进行排序,使得权值最大(或最小)的永远排在队列的最前面。
2.头文件
属于队列的一种,直接使用队列的头文件 #include<queue>
3.优先队列的初始化
priority_queue<T, Container, Compare>
priority_queue<T> //直接输入元素则使用默认容器和比较函数
与往常的初始化不同,优先队列的初始化涉及到一组而外的变量,这里解释一下初始化:
a) T就是Type为数据类型
b) Container是容器类型(Container必须是用数组实现的容器,比如vector,deque,不能用 list,默认vector)
c) Compare是比较方法,类似于sort第三个参数那样的比较方式,对于自定义类型,需要我们手动进行比较运算符的重载。
与sort直接Bool一个函数来进行比较的简单方法不同,Compare需要使用结构体的运算符重载完成,直接
bool cmp(int a,int b){ return a>b; } //这么写是无法通过编译的。
struct cmp
{ //这个比较要用结构体表示
bool operator()(int &a, int &b) const
{
return a > b;
}
};
priority_queue<int,vector<int>,cmp> q; //使用自定义比较方法
priority_queue<int> pq;
4.常用接口
我们预先通过priority_queue <int> q
创建了一个队列,命名为q,方便举例。
a)大小size()
返回队列元素的个数
函数原型:size_type size() const;
cout<<q.size()<<endl; //直接返回队列中元素的个数
b) 入队push()
进行入队操作,在队尾处进行插入
函数原型:void push (const value_type& val);
q.push(100);
c) 出队pop()
进行出队操作,在对头出进行弹出
函数原型:void pop();
q.pop();
d) 访问队头元素top()
与标准队列不同,优先队列只允许访问队头元素,不允许访问其余的数据,由于堆的特殊性质,堆顶元素的优先权最高(或者最低),
访问其余元素没有意义,因此,优先队列只允许访问队头元素,这和栈的访问类型类似所以使用栈访问栈顶的命名top
函数原型是:
reference& top();
const_reference& top() const;
cout<<q.top()<<endl;
e) 判空empty()
返回一个bool类型的值,只存在真和假,当队列为空时为真,不为空时为假
函数原型
bool empty() const;
可以利用empty()进行队列的遍历操作,这里建议先使用初始化函数将队列进行复制,否则遍历之后队列就为空了。
while(q.empty()){
cout<<q.pop()<<endl;
q.pop();
}
1.3.3STL函数对象(仿函数)
1 函数对象
2 谓词
3内建函数对象
1.4 泛型编程
C++ 中的泛型编程主要依赖于模板(Templates)和泛型算法,模板是实现泛型编程的关键组成部分。
- 模板(Templates):
函数模板(Function Templates):允许定义通用的函数,可以用于多种数据类型,通过模板参数来实现参数化的数据类型。
类模板(Class Templates):类似于函数模板,允许定义通用的类,通过模板参数化的数据类型。
模板允许编写通用的代码,通过在编译时进行模板实例化来生成特定类型的代码。编译器在编译阶段根据实际使用的数据类型生成模板函数或模板类的实例。
- 泛型算法:
泛型算法是针对不同数据类型的通用算法。C++ 标准模板库(STL)提供了大量的泛型算法,如排序、查找、操作容器等,它们使用模板来实现对不同数据类型的通用处理。
泛型算法使用模板来实现通用性。例如,对于排序算法,通过使用模板参数传递比较函数,可以针对不同的数据类型和排序要求实现通用的排序算法。
示例:
- 函数模板:
#include <iostream>
template <typename T>
T maximum(T x, T y) {
return (x > y) ? x : y;
}
int main() {
std::cout << "Max of 3 and 5 is: " << maximum(3, 5) << std::endl; // 调用模板函数
std::cout << "Max of 3.14 and 6.28 is: " << maximum(3.14, 6.28) << std::endl; // 调用模板函数
return 0;
}
- 类模板:
#include <iostream>
template <typename T>
class Pair {
private:
T first;
T second;
public:
Pair(T a, T b) : first(a), second(b) {}
T getFirst() { return first; }
T getSecond() { return second; }
};
int main() {
Pair<int> intPair(10, 20); // 使用模板类实例化出一个具体类型的类
std::cout << "First value: " << intPair.getFirst() << std::endl;
std::cout << "Second value: " << intPair.getSecond() << std::endl;
Pair<double> doublePair(3.14, 6.28);
std::cout << "First value: " << doublePair.getFirst() << std::endl;
std::cout << "Second value: " << doublePair.getSecond() << std::endl;
return 0;
}
1.5 内存管理
1.5.1 概念
栈内存和堆内存
栈内存(Stack Memory):
- 由编译器自动分配和释放。
- 存储局部变量和函数调用信息。
- 内存分配和释放都是自动的,遵循先进后出(FILO)原则。
堆内存(Heap Memory):
- 由程序员手动分配和释放。
- 存储动态分配的变量和对象。
- 内存分配和释放需要程序员负责,需要调用 new 和 delete 或 malloc 和 free 等函数。
new 和 delete 操作符
new 操作符:
用于动态分配内存,返回指向分配内存的指针。
int* dynamicInt = new int;
delete 操作符:
用于释放通过 new 分配的内存。
delete dynamicInt;
malloc 和 free 函数
malloc 函数:
用于动态分配内存,返回 void* 指针。
int* dynamicIntArray = static_cast<int*>(malloc(5 * sizeof(int)));
free 函数:
用于释放通过 malloc 分配的内存。
free(dynamicIntArray);
智能指针
智能指针是C++11引入的一种机制,用于自动管理动态内存。主要有 std::unique_ptr 和 std::shared_ptr。
std::unique_ptr:
拥有独占的所有权,不能被复制。
当 std::unique_ptr 超出范围时,其拥有的内存会被自动释放。
std::unique_ptr<int> uniqueInt = std::make_unique<int>(42);
std::shared_ptr:
允许多个 std::shared_ptr 共享同一块内存。
内部使用引用计数,当最后一个 std::shared_ptr 被销毁时,内存会被释放。
std::shared_ptr<int> sharedInt = std::make_shared<int>(42);
RAII(资源获取即初始化)
RAII 是一种C++编程范式,通过在对象的构造函数中分配资源,而在析构函数中释放资源,从而保证资源的正确管理。智能指针的使用就是一种典型的RAII实践。
class MyResource {
public:
MyResource() {
// 资源分配
resourcePtr = new int;
}
~MyResource() {
// 资源释放
delete resourcePtr;
}
private:
int* resourcePtr;
};
int main() {
MyResource myResource; // RAII确保在作用域结束时释放资源
return 0;
}
1.5.2 智能指针(C++11)
C++11引入了智能指针,这是一种用于自动管理动态分配内存的机制,有助于避免内存泄漏和悬空指针等问题。主要的智能指针包括 std::unique_ptr 和 std::shared_ptr。
std::unique_ptr
std::unique_ptr 是一种独占所有权的智能指针,它确保只有一个指针能够拥有动态分配的内存。
创建和使用 std::unique_ptr:
#include <iostream>
#include <memory>
int main() {
std::unique_ptr<int> uniqueInt = std::make_unique<int>(42);
std::cout << "Value: " << *uniqueInt << std::endl;
// unique_ptr 离开作用域时,自动释放内存
return 0;
}
移动语义:
std::unique_ptr 支持移动语义,可以通过 std::move 将所有权转移到另一个 std::unique_ptr。
std::unique_ptr<int> sourcePtr = std::make_unique<int>(42);
std::unique_ptr<int> destPtr = std::move(sourcePtr);
std::shared_ptr
std::shared_ptr 是一种共享所有权的智能指针,它允许多个指针同时拥有同一块内存。
创建和使用 std::shared_ptr:
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> sharedInt = std::make_shared<int>(42);
std::cout << "Value: " << *sharedInt << std::endl;
// 当最后一个 shared_ptr 离开作用域时,内存被释放
return 0;
}
循环引用问题:
std::shared_ptr 内部包含一个引用计数,用于记录有多少个 std::shared_ptr 共享同一块内存。每当一个新的 std::shared_ptr 对象拥有这块内存时,引用计数就会增加;当 std::shared_ptr 对象销毁或不再拥有这块内存时,引用计数就会减少。
循环引用是指一组对象,它们相互引用,导致它们的引用计数永不为零,因此资源永远不会被释放。这可能导致内存泄漏,因为 std::shared_ptr 的引用计数永不为零,对象也永不被销毁。
为了解决循环引用问题,引入了 std::weak_ptr。std::weak_ptr 是一种弱引用,它不改变引用计数,允许对被 std::shared_ptr 管理的资源进行观察,而不拥有资源的所有权。
#include <iostream>
#include <memory>
class Node;
class Edge {
public:
std::shared_ptr<Node> destination;
};
class Node {
public:
std::vector<std::shared_ptr<Edge>> edges;
};
int main() {
std::shared_ptr<Node> node1 = std::make_shared<Node>();
std::shared_ptr<Node> node2 = std::make_shared<Node>();
// 创建循环引用
node1->edges.push_back(std::make_shared<Edge>());
node1->edges[0]->destination = node2;
node2->edges.push_back(std::make_shared<Edge>());
node2->edges[0]->destination = node1;
// 使用 std::weak_ptr 打破循环引用
std::weak_ptr<Node> weakNode1 = node1;
std::weak_ptr<Node> weakNode2 = node2;
node1->edges.push_back(std::make_shared<Edge>());
node1->edges[1]->destination = weakNode2.lock();
node2->edges.push_back(std::make_shared<Edge>());
node2->edges[1]->destination = weakNode1.lock();
return 0;
}
std::weak_ptr
std::weak_ptr 可以通过 lock() 函数获取对应的 std::shared_ptr,如果资源已经释放或者不存在,则返回一个空的 std::shared_ptr。
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> sharedInt = std::make_shared<int>(42);
std::weak_ptr<int> weakInt = sharedInt;
// 使用 lock() 获取 shared_ptr,若对象存在
if (auto lockedInt = weakInt.lock()) {
std::cout << "Value: " << *lockedInt << std::endl;
} else {
std::cout << "Pointer is expired." << std::endl;
}
return 0;
}
通过 std::weak_ptr,即可解决 std::shared_ptr 循环引用的问题。循环引用中的一个对象可以使用 std::weak_ptr 来观察另一个对象,而不引起循环引用问题。当需要访问共享资源时,可以通过 lock() 获取对应的 std::shared_ptr,有效避免了循环引用导致的内存泄漏。
总结:
- std::unique_ptr 用于独占所有权的情况,具有移动语义。
- std::shared_ptr 用于共享所有权的情况,使用引用计数,有循环引用问题。
- std::weak_ptr 用于解决 std::shared_ptr 的循环引用问题,不改变引用计数。
1.5.3 RAII
RAII(Resource Acquisition Is Initialization)是一种C++编程范式,旨在通过对象的构造和析构来管理资源的获取和释放。该范式的核心思想是,在对象的构造函数中获取资源,而在析构函数中释放资源,利用对象的生命周期来确保资源的正确管理。
RAII的基本原理:
- 资源获取:在对象的构造函数中获取需要管理的资源,这可以是动态分配的内存、文件句柄、网络连接等。
- 资源释放:在对象的析构函数中释放已获取的资源。这确保了当对象离开作用域时,资源会被正确释放。
- 生命周期管理:通过对象的生命周期,确保资源的获取和释放是自动进行的,无需手动管理。
RAII的优势:
- 自动管理资源:无需手动调用函数来获取和释放资源,资源的管理隐含在对象的生命周期中。
- 异常安全性:在面对异常时,对象的析构函数会被调用,确保资源的释放,避免资源泄漏。
- 可读性和可维护性:通过RAII,代码更加清晰、简洁,降低了出错的可能性。
RAII的应用场景:
智能指针: std::shared_ptr 和 std::unique_ptr 是RAII的典型应用,它们在对象的析构函数中自动释放内存。
文件操作: 使用RAII管理文件资源,确保文件在作用域结束时被正确关闭。
class File {
public:
File(const char* filename) : fileHandle(std::fopen(filename, "r")) {
if (!fileHandle) {
// 处理文件打开失败的情况
}
}
~File() {
if (fileHandle) {
std::fclose(fileHandle);
}
}
private:
FILE* fileHandle;
};
数据库连接: 通过RAII管理数据库连接,确保在对象生命周期结束时关闭连接。
class DatabaseConnection {
public:
DatabaseConnection() {
// 打开数据库连接
}
~DatabaseConnection() {
// 关闭数据库连接
}
};
锁的管理: 使用RAII管理互斥锁,确保在离开作用域时自动释放锁。
class MutexLock {
public:
MutexLock(std::mutex& mutex) : lock(mutex) {
// 获取互斥锁
}
~MutexLock() {
// 释放互斥锁
}
private:
std::unique_lock<std::mutex> lock;
};
1.5.4 自定义内存分配器和内存池
Custom Allocators:
自定义分配器是一种允许开发者定制内存分配和释放策略的机制。以下是一个简单的自定义分配器示例:
#include <iostream>
template <typename T>
class MyAllocator {
public:
T* allocate(size_t size) {
return static_cast<T*>(malloc(size * sizeof(T)));
}
void deallocate(T* ptr) {
free(ptr);
}
};
int main() {
MyAllocator<int> myAllocator;
int* dynamicIntArray = myAllocator.allocate(5);
for (int i = 0; i < 5; ++i) {
dynamicIntArray[i] = i * 10;
}
for (int i = 0; i < 5; ++i) {
std::cout << dynamicIntArray[i] << " ";
}
myAllocator.deallocate(dynamicIntArray);
return 0;
}
在上面的示例中,MyAllocator 是一个简单的模板类,包含了 allocate 和 deallocate 成员函数,用于分配和释放内存。这里使用了 malloc 和 free,但实际的分配和释放策略可以根据需求进行定制。
Memory Pools:
内存池是一种将多个小块内存一次性分配并汇总管理的机制。以下是一个简单的内存池示例:
#include <iostream>
#include <vector>
class MemoryPool {
public:
MemoryPool(size_t blockSize, size_t poolSize) : blockSize(blockSize), poolSize(poolSize) {
allocatePool();
}
~MemoryPool() {
for (void* block : memoryBlocks) {
free(block);
}
}
void* allocate() {
if (freeBlocks.empty()) {
allocatePool();
}
void* block = freeBlocks.back();
freeBlocks.pop_back();
return block;
}
void deallocate(void* block) {
freeBlocks.push_back(block);
}
private:
void allocatePool() {
size_t totalSize = blockSize * poolSize;
void* block = malloc(totalSize);
for (size_t i = 0; i < poolSize; ++i) {
void* addr = static_cast<char*>(block) + i * blockSize;
freeBlocks.push_back(addr);
}
memoryBlocks.push_back(block);
}
size_t blockSize;
size_t poolSize;
std::vector<void*> memoryBlocks;
std::vector<void*> freeBlocks;
};
int main() {
MemoryPool memoryPool(sizeof(int), 5);
int* intArray = static_cast<int*>(memoryPool.allocate());
for (int i = 0; i < 5; ++i) {
intArray[i] = i * 10;
}
for (int i = 0; i < 5; ++i) {
std::cout << intArray[i] << " ";
}
memoryPool.deallocate(intArray);
return 0;
}
MemoryPool 是一个简单的内存池类,使用 malloc 和 free 进行内存分配和释放。内存池一次性分配了多个小块内存,并在需要时分配和释放这些小块。这种机制可以提高内存分配的效率。
1.6 并发和多线程
1.6.1 线程池
#include <iostream>
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <memory>
#include <future>
class ThreadPool {
public:
explicit ThreadPool(size_t numThreads)
: stop(false) {
for (size_t i = 0; i < numThreads; ++i) {
threads.emplace_back([this] {
while (true) {
std::shared_ptr<std::function<void()>> task;
{
std::unique_lock<std::mutex> lock(queueMutex);
condition.wait(lock, [this] { return stop || !tasks.empty(); });
if (stop && tasks.empty()) {
return;
}
task = std::move(tasks.front());
tasks.pop();
}
(*task)();
}
});
}
}
~ThreadPool() {
{
std::unique_lock<std::mutex> lock(queueMutex);
stop = true;
}
condition.notify_all();
for (std::thread& thread : threads) {
thread.join();
}
}
template <class F, class... Args>
auto enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type> {
using return_type = typename std::result_of<F(Args...)>::type;
auto task = std::make_shared<std::packaged_task<return_type()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...));
std::future<return_type> result = task->get_future();
{
std::unique_lock<std::mutex> lock(queueMutex);
if (stop) {
throw std::runtime_error("enqueue on stopped ThreadPool");
}
tasks.emplace(task);
}
condition.notify_one();
return result;
}
private:
std::vector<std::thread> threads;
std::queue<std::shared_ptr<std::function<void()>>> tasks;
std::mutex queueMutex;
std::condition_variable condition;
bool stop;
};
int main() {
ThreadPool pool(4);
// 示例任务
auto task1 = pool.enqueue([] {
std::cout << "Task 1 executed." << std::endl;
});
auto task2 = pool.enqueue([] {
std::cout << "Task 2 executed." << std::endl;
});
task1.wait();
task2.wait();
return 0;
}
这线程池的实现思路:
- 任务队列和线程池: 使用std::queue存储任务(std::function<void()>)以及std::vector<std::thread>表示线程池。
- 同步机制: 使用std::mutex和std::condition_variable实现对任务队列的同步访问,确保线程池的安全运行。
- 线程执行循环: 每个线程在一个无限循环中等待任务,一旦有任务就执行,然后继续等待下一个任务。这样的设计保证了线程的复用。
- 任务生命周期管理: 使用std::shared_ptr管理任务生命周期,确保任务在执行过程中不被提前销毁,避免悬垂指针问题。
- 条件变量通知: 使用条件变量std::condition_variable实现线程的等待和唤醒,以避免忙等待,提高效率。
- 线程池的停止: 在线程池析构时,通过设定stop标志并通知所有线程退出,等待线程池中的所有任务执行完毕。
1.6.2 IOSP
IOCP(Input/Output Completion Ports)是一种在Windows平台上用于异步I/O操作的机制。它主要用于提高I/O密集型应用程序的性能,允许同时处理多个I/O操作而不阻塞线程。
原理:Completion Port(完成端口):IOCP通过创建一个完成端口来实现异步I/O操作的通知机制。完成端口是一个内核对象,用于管理I/O操作的完成状态。
- 创建套接字: 应用程序创建一个套接字,并将其关联到IOCP。
- 提交I/O操作: 应用程序使用WSARecv、WSASend等异步I/O函数向IOCP提交I/O操作。每个I/O操作都关联一个与之相关的完成例程。
- 线程池: 系统维护一个线程池,线程池中的线程会等待I/O操作完成的通知。
- I/O操作完成: 当一个I/O操作完成时,系统将相关信息传递给IOCP,并触发与该I/O操作关联的完成例程。
- 线程执行完成例程: 线程池中的空闲线程会执行与I/O操作关联的完成例程。这样,应用程序能够异步地处理I/O完成的事件。
用法:
创建IOCP:
使用CreateIoCompletionPort函数创建IOCP对象。HANDLE hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
将套接字关联到IOCP:
使用CreateIoCompletionPort函数将套接字与IOCP关联。CreateIoCompletionPort((HANDLE)socket, hIOCP, (ULONG_PTR)socket, 0);
提交I/O操作:
使用WSARecv、WSASend等函数提交I/O操作。WSARecv(socket, &wsaBuffer, 1, &dwBytes, &dwFlags, &overlapped, NULL);
等待I/O完成:
使用GetQueuedCompletionStatus函数等待I/O操作完成。GetQueuedCompletionStatus(hIOCP, &dwBytes, &key, &overlapped, INFINITE);
处理I/O完成事件:
在完成例程中处理I/O操作完成的事件。void CompletionRoutine(DWORD dwBytes, ULONG_PTR key, LPOVERLAPPED overlapped) { // 处理I/O完成事件 }
IOCP的优势在于能够高效地处理大量的并发I/O操作,避免了传统同步I/O模型中线程阻塞的问题,提高了系统的性能和响应速度。
示例:
#include <iostream>
#include <thread>
#include <WinSock2.h>
#include <MSWSock.h>
#pragma comment(lib, "ws2_32.lib")
const int MAX_CLIENTS = 100;
const int PORT = 8080;
// 定义完成端口相关数据结构
struct PerHandleData {
SOCKET socket;
SOCKADDR_STORAGE clientAddr;
OVERLAPPED overlapped;
char buffer[1024];
};
// 函数声明
void WorkerThread(HANDLE completionPort);
int main() {
// 初始化Winsock
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
std::cerr << "WSAStartup failed." << std::endl;
return -1;
}
// 创建IOCP
HANDLE completionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, 0);
if (completionPort == nullptr) {
std::cerr << "CreateIoCompletionPort failed." << std::endl;
return -1;
}
// 创建工作线程
std::thread workerThread(WorkerThread, completionPort);
// 创建监听套接字
SOCKET listenSocket = WSASocket(AF_INET, SOCK_STREAM, 0, nullptr, 0, WSA_FLAG_OVERLAPPED);
SOCKADDR_IN serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = INADDR_ANY;
serverAddr.sin_port = htons(PORT);
bind(listenSocket, reinterpret_cast<SOCKADDR*>(&serverAddr), sizeof(serverAddr));
listen(listenSocket, SOMAXCONN);
// 关联监听套接字到IOCP
CreateIoCompletionPort(reinterpret_cast<HANDLE>(listenSocket), completionPort, 0, 0);
// 接受客户端连接
for (int i = 0; i < MAX_CLIENTS; ++i) {
SOCKET clientSocket = WSASocket(AF_INET, SOCK_STREAM, 0, nullptr, 0, WSA_FLAG_OVERLAPPED);
PerHandleData* perHandleData = new PerHandleData;
perHandleData->socket = clientSocket;
// 提交异步接受操作
DWORD bytesReceived;
perHandleData->overlapped.hEvent = reinterpret_cast<HANDLE>(perHandleData);
AcceptEx(listenSocket, clientSocket, perHandleData->buffer, sizeof(perHandleData->buffer) - (2 * (sizeof(sockaddr_in) + 16)),
sizeof(sockaddr_in) + 16, sizeof(sockaddr_in) + 16, &bytesReceived, &perHandleData->overlapped);
}
// 等待工作线程结束
workerThread.join();
// 清理资源
closesocket(listenSocket);
WSACleanup();
return 0;
}
void WorkerThread(HANDLE completionPort) {
while (true) {
DWORD bytesTransferred;
ULONG_PTR completionKey;
LPOVERLAPPED overlapped;
DWORD waitResult = GetQueuedCompletionStatus(completionPort, &bytesTransferred, &completionKey, &overlapped, INFINITE);
if (!waitResult || bytesTransferred == 0) {
// 处理错误或连接断开的情况
PerHandleData* perHandleData = reinterpret_cast<PerHandleData*>(completionKey);
closesocket(perHandleData->socket);
delete perHandleData;
continue;
}
// 处理接收到的数据
PerHandleData* perHandleData = reinterpret_cast<PerHandleData*>(completionKey);
perHandleData->buffer[bytesTransferred] = '\0';
std::cout << "Received from client: " << perHandleData->buffer << std::endl;
// 继续提交异步接收操作
DWORD bytesReceived;
WSARecv(perHandleData->socket, &perHandleData->wsaBuffer, 1, &bytesReceived, 0, &perHandleData->overlapped, nullptr);
}
}
1.7 异常处理
基本概念
C++ 提供了异常处理机制,用于处理程序执行期间可能发生的异常情况。异常是在程序执行过程中遇到错误或意外情况时引发的一种事件。
try、catch 和 throw:
try 块用于包含可能引发异常的代码块。catch 块用于捕获和处理异常。每个 catch 块可以处理特定类型的异常。throw 用于在 try 块中引发异常。
try {
// 代码块可能引发异常
throw MyException("This is an exception");
}
catch (MyException& e) {
// 捕获并处理 MyException 类型的异常
std::cerr << "Caught an exception: " << e.what() << std::endl;
}
标准异常类:
C++ 提供了一些标准异常类,这些类位于stdexcept 头文件中,派生自 std::exception。
常见的标准异常类包括:
std::runtime_error:表示运行时错误。
std::logic_error:表示逻辑错误。
std::out_of_range:表示索引超出范围。
#include <stdexcept>
try {
// 代码块可能引发异常
throw std::runtime_error("This is a runtime error");
}
catch (const std::exception& e) {
// 捕获并处理 std::exception 及其派生类的异常
std::cerr << "Caught an exception: " << e.what() << std::endl;
}
自定义异常类:
程序员可以定义自己的异常类,只需继承自exception 类或其派生类,并提供适当的构造函数和实现 what 方法。
#include <stdexcept>
#include <string>
class MyException : public std::runtime_error {
public:
MyException(const std::string& message) : std::runtime_error(message) {}
};
try {
// 代码块可能引发 MyException 类型的异常
throw MyException("This is a custom exception");
}
catch (const std::exception& e) {
// 捕获并处理 std::exception 及其派生类的异常
std::cerr << "Caught an exception: " << e.what() << std::endl;
}
std::exception 类的 what 方法:
所有的异常类都应该覆盖 std::exception 类的 what 方法,以提供有关异常的描述信息。
const char* what() const noexcept override {
return "My custom exception occurred";
}
异常规范(Exception Specification):
C++ 支持异常规范,但在现代 C++ 中,通常不推荐使用异常规范。异常规范在函数声明中指定了函数可能抛出的异常类型,但这已经在 C++11 之后逐渐被弃用。
// 不推荐使用异常规范
void myFunction() throw(MyException) {
// ...
}
C++11/14/17中的异常处理
C++11:
异常规范(Exception Specification)的变更:
C++11 中不再推荐使用异常规范 throw(Type),因为它在实践中往往不起作用,并且可能引起程序终止。
// 不推荐使用异常规范
void myFunction() throw(MyException) {
// ...
}
nothrow:引入了 std::nothrow,可以在动态内存分配中使用,避免抛出异常,而是返回一个空指针。
nt* ptr = new(std::nothrow) int[100];
if (ptr == nullptr) {
// 内存分配失败
}
C++14:
make_exception_ptr:
引入了 std::make_exception_ptr 函数,用于创建 std::exception_ptr 对象,允许将异常对象传递给异步任务。
try {
// 代码块可能引发异常
}
catch (...) {
std::exception_ptr eptr = std::current_exception();
// 将异常传递给异步任务
// std::async([=] { someAsyncFunction(eptr); });
}
C++17:
uncaught_exceptions:
引入了 std::uncaught_exceptions 函数,用于获取当前未捕获的异常数量,可用于判断是否在析构期间发生了异常。
int count = std::uncaught_exceptions();
std::nested_exception:引入了 std::nested_exception 类,允许在异常处理块中保存原始异常信息。cpptry {
// 代码块可能引发异常
}
catch (const std::exception& e) {
std::throw_with_nested(MyException("Nested exception occurred"));
}
异常的解包(Exception Decomposition):
C++17允许使用结构化绑定从异常中提取信息。
try {
// 代码块可能引发异常
}
catch (const std::exception& e) {
auto [name, message] = extractExceptionInfo(e);
std::cout << "Exception Name: " << name << ", Message: " << message << std::endl;
}
1.8 网络编程
1.8.1 Socket编程
C++ Socket编程中客户端和服务器端的主要流程以及涉及的关键函数:
服务器端流程:
创建Socket:使用 socket 函数创建套接字。
int serverSocket = socket(AF_INET, SOCK_STREAM, 0);
绑定Socket到地址:使用 bind 函数将套接字绑定到服务器地址。
bind(serverSocket, (struct sockaddr*)&serverAddress, sizeof(serverAddress));
监听连接:使用 listen 函数开始监听客户端的连接请求。
listen(serverSocket, 10);
接受连接:使用 accept 函数接受客户端的连接。
int clientSocket = accept(serverSocket, nullptr, nullptr);
接收数据:使用 recv 函数从客户端接收数据。
recv(clientSocket, buffer, sizeof(buffer), 0);
关闭Socket:使用 close 函数关闭套接字。
close(clientSocket);
客户端流程:
创建Socket:使用 socket 函数创建套接字。
int clientSocket = socket(AF_INET, SOCK_STREAM, 0);
连接到服务器:使用 connect 函数连接到服务器。
connect(clientSocket, (struct sockaddr*)&serverAddress, sizeof(serverAddress));
发送数据:使用 send 函数向服务器发送数据。
send(clientSocket, message, strlen(message), 0);
关闭Socket:使用 close 函数关闭套接字。
close(clientSocket);
关键函数参数列表和取值解释:
socket 函数:
int socket(int domain, int type, int protocol);
domain:指定协议族,常用的是 AF_INET 表示IPv4。
type:指定套接字类型,常用的是 SOCK_STREAM 表示TCP套接字。
protocol:指定协议,一般设为0,表示使用默认协议。
bind 函数:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:套接字描述符。
addr:指向 sockaddr 结构的指针,包含服务器的地址信息。
addrlen:sockaddr 结构的长度。
listen 函数:
int listen(int sockfd, int backlog);
sockfd:套接字描述符。
backlog:等待连接队列的最大长度。
accept 函数:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd:套接字描述符。
addr:指向 sockaddr 结构的指针,用于存储客户端地址信息。
addrlen:指向 socklen_t 的指针,用于存储 sockaddr 结构的长度。
recv 函数:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
sockfd:套接字描述符。
buf:接收数据的缓冲区。
len:缓冲区的长度。
flags:标志,通常设为0。
send 函数:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
sockfd:套接字描述符。
buf:要发送的数据。
len:要发送的数据长度。
flags:标志,通常设为0。
connect 函数:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:套接字描述符。
addr:指向 sockaddr 结构的指针,包含服务器的地址信息。
addrlen:sockaddr 结构的长度。
close 函数:
int close(int sockfd);
sockfd:套接字描述符。
TCP服务器示例:
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
int main() {
// 创建Socket
int serverSocket = socket(AF_INET, SOCK_STREAM, 0);
if (serverSocket == -1) {
std::cerr << "Error creating socket" << std::endl;
return -1;
}
// 设置服务器地址信息
sockaddr_in serverAddress;
serverAddress.sin_family = AF_INET;
serverAddress.sin_addr.s_addr = INADDR_ANY;
serverAddress.sin_port = htons(8080);
// 绑定Socket到地址
if (bind(serverSocket, (struct sockaddr*)&serverAddress, sizeof(serverAddress)) == -1) {
std::cerr << "Error binding socket to address" << std::endl;
close(serverSocket);
return -1;
}
// 监听连接
if (listen(serverSocket, 10) == -1) {
std::cerr << "Error listening for connections" << std::endl;
close(serverSocket);
return -1;
}
std::cout << "Server listening on port 8080..." << std::endl;
// 接受连接
int clientSocket = accept(serverSocket, nullptr, nullptr);
if (clientSocket == -1) {
std::cerr << "Error accepting connection" << std::endl;
close(serverSocket);
return -1;
}
// 从客户端接收数据
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
if (recv(clientSocket, buffer, sizeof(buffer), 0) == -1) {
std::cerr << "Error receiving data from client" << std::endl;
} else {
std::cout << "Received from client: " << buffer << std::endl;
}
// 关闭Socket
close(clientSocket);
close(serverSocket);
return 0;
}
TCP客户端示例:
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
int main() {
// 创建Socket
int clientSocket = socket(AF_INET, SOCK_STREAM, 0);
if (clientSocket == -1) {
std::cerr << "Error creating socket" << std::endl;
return -1;
}
// 设置服务器地址信息
sockaddr_in serverAddress;
serverAddress.sin_family = AF_INET;
serverAddress.sin_addr.s_addr = INADDR_ANY;
serverAddress.sin_port = htons(8080);
// 连接到服务器
if (connect(clientSocket, (struct sockaddr*)&serverAddress, sizeof(serverAddress)) == -1) {
std::cerr << "Error connecting to server" << std::endl;
close(clientSocket);
return -1;
}
// 发送数据到服务器
const char* message = "Hello, Server!";
if (send(clientSocket, message, strlen(message), 0) == -1) {
std::cerr << "Error sending data to server" << std::endl;
} else {
std::cout << "Sent to server: " << message << std::endl;
}
// 关闭Socket
close(clientSocket);
return 0;
}
1.9 C++11/14新特性
1 左值右值
在C++中,左值(Lvalue)和右值(Rvalue)是用于描述表达式的两种基本类别。这与C++11引入的右值引用(Rvalue Reference)密切相关,它改变了对值的引用和传递的方式。
左值(Lvalue):
左值是可以标识并且具有内存位置的表达式。通常,左值是可以取地址的表达式。以下是一些左值的例子:
int x = 42; // x 是左值
int* ptr = &x; // &x 是左值
int arr[5]; // arr 是左值
左值可以出现在赋值语句的左侧或右侧。
右值(Rvalue):
右值是在表达式中使用的临时或不具有具名内存位置的值。右值通常表示临时的中间结果或即将被移动的对象。以下是一些右值的例子:
int result = 2 + 3; // 2 + 3 是右值
int&& rvalueRef = 42; // 42 是右值
int* ptr = new int(10); // new int(10) 是右值
右值可以出现在赋值语句的右侧。
左值引用和右值引用:
C++11引入了右值引用,通过使用&&符号,它使得可以直接绑定到右值。左值引用(使用&符号)仍然绑定到左值。右值引用的主要用途是实现移动语义和完美转发。
移动语义(Move Semantics):
原理: 移动语义允许将资源(如堆上的内存)的所有权从一个对象转移到另一个对象,而不进行深层次的复制。这是通过使用右值引用和移动构造函数/移动赋值运算符实现的。
好处: 移动语义的主要好处在于避免不必要的数据拷贝,提高了程序的性能。特别是对于临时对象或即将销毁的对象,可以通过移动而非复制的方式转移资源,避免额外的开销。
示例:
class MyString {
public:
// 移动构造函数
MyString(MyString&& other) noexcept
: data(other.data) {
other.data = nullptr;
}
// 移动赋值运算符
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
other.data = nullptr;
}
return *this;
}
private:
char* data;
};
// 使用移动语义
MyString createString() {
MyString str;
// 构建字符串
return str; // 返回右值
}
int main() {
MyString str1 = createString(); // 调用移动构造函数
MyString str2;
str2 = createString(); // 调用移动赋值运算符
return 0;
}
C++11引入了移动语义,其中的移动构造函数和移动赋值函数允许对象的资源(例如动态分配的内存)在转移所有权时进行高效的移动,而不是进行昂贵的复制。这可以提高程序性能,特别是在处理临时对象或需要动态分配内存的对象时。
移动构造函数:
移动构造函数是一种特殊的构造函数,接受一个右值引用作为参数,通常被用于将资源从一个对象“移动”到另一个对象,避免资源的深拷贝。
class MyString {
public:
// 移动构造函数
MyString(MyString&& other) noexcept
: data(other.data) {
other.data = nullptr;
}
private:
char* data;
};
// 使用移动构造函数
MyString createString() {
MyString str;
// 构建字符串
return str; // 返回右值
}
MyString(MyString&& other) 是移动构造函数,它接受一个右值引用 MyString&& 作为参数,并将资源从 other 移动到新的对象。
移动赋值函数:
移动赋值函数是一种特殊的赋值运算符重载,也接受一个右值引用作为参数,用于在对象已经存在的情况下转移资源。
class MyString {
public:
// 移动赋值运算符
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
other.data = nullptr;
}
return *this;
}
private:
char* data;
};
// 使用移动赋值运算符
MyString str1, str2;
// ...
str1 = std::move(str2); // 使用 std::move 调用移动赋值运算符
operator=(MyString&& other) 是移动赋值函数,它接受一个右值引用 MyString&& 作为参数,将资源从 other 移动到当前对象。
好处:
移动构造函数和移动赋值函数避免了不必要的资源复制,提高了程序性能。
通常与右值引用结合使用,特别适用于处理临时对象、容器的动态内存分配等情况。
在使用移动语义时,需要小心确保在移动之后的对象状态是有效的,并且资源的所有权已经正确转移。使用 std::move 可以将对象转换为右值引用,用于显式地表示将要进行移动操作。
emplace() 系列函数是C++11引入的一组用于在STL容器中进行插入操作的函数,与原有的STL插入函数有一些区别。这些函数包括 emplace_back()、emplace_front()、emplace() 等。
区别:
构造方式:
- 原有插入函数: 使用原有的插入函数时,需要使用对象的拷贝构造函数,将对象的副本插入容器。
- emplace() 系列函数: 使用 emplace() 系列函数时,可以直接在容器中构造对象,而不需要额外调用拷贝构造函数。这是通过在容器内部直接构造元素,而不是传递元素副本来实现的。
参数传递:
- 原有插入函数: 需要传递对象的副本或引用。
- emplace() 系列函数: 直接在函数参数中传递构造元素所需的参数,无需额外创建对象。这允许在构造元素时避免不必要的临时对象创建。
性能优势:
- 原有插入函数: 涉及拷贝构造函数和析构函数,可能导致性能开销。
- emplace() 系列函数: 直接在容器内部构造对象,减少了不必要的拷贝和析构操作,提高了性能。
示例:
#include <iostream>
#include <vector>
#include <string>
class MyClass {
public:
MyClass(int value, const std::string& text)
: value_(value), text_(text) {
std::cout << "Constructor called." << std::endl;
}
private:
int value_;
std::string text_;
};
int main() {
std::vector<MyClass> vec;
// 使用原有插入函数
MyClass obj1(1, "Object1");
vec.push_back(obj1); // 拷贝构造函数被调用
// 使用 emplace() 系列函数
vec.emplace_back(2, "Object2"); // 直接在容器内构造对象,避免了拷贝构造函数的调用
return 0;
}
vec.push_back(obj1) 使用了原有的插入函数,导致了拷贝构造函数的调用。而 vec.emplace_back(2, "Object2") 则直接在容器内部构造对象,避免了拷贝构造函数的调用,提高了性能。emplace() 系列函数的使用方式使得代码更简洁,同时能够更直接地传递构造对象所需的参数。
原有插入函数: 使用传统的插入函数(如 push_back()、insert())时,需要创建对象的副本,涉及一次拷贝构造或移动构造。
emplace() 系列函数: 使用 emplace() 系列函数时,直接在容器内部构造对象,避免了创建对象的副本,因此通常可以减少一次拷贝构造的操作。
通过使用 emplace() 系列函数,可以在插入元素时减少一次拷贝构造的开销,提高程序性能。这样的优势在处理大型对象、需要动态分配内存或构造复杂对象时尤为显著。
完美转发(Perfect Forwarding):
原理: 完美转发允许在函数中保持参数的原始类型(左值或右值性质),并将其传递给其他函数,同时保留参数的引用性质。这是通过使用通用引用和std::forward实现的。
好处: 完美转发在实现泛型代码时非常有用,特别是当你希望传递参数给其他函数,但不希望丢失参数的引用性质。它允许函数在传递参数时保留参数的原始性质,使得能够正确处理左值和右值。
示例:
#include <utility>
#include <iostream>
// 模板函数使用完美转发
template <typename T>
void forwarder(T&& arg) {
otherFunction(std::forward<T>(arg));
}
// 接受左值或右值引用的函数
void otherFunction(int& x) {
std::cout << "Lvalue reference: " << x << std::endl;
}
void otherFunction(int&& x) {
std::cout << "Rvalue reference: " << x << std::endl;
}
int main() {
int value = 42;
forwarder(value); // 保留左值性质
forwarder(123); // 保留右值性质
return 0;
}
在上述例子中,forwarder函数使用了完美转发,允许正确地传递参数给otherFunction,并保留了参数的原始引用性质。这样,otherFunction可以正确地处理左值和右值。
2 自动类型推导
auto 和 decltype 是C++11引入的两个关键字,用于提高代码的灵活性和可读性。它们用于自动类型推导,避免手动指定变量或表达式的类型,从而使代码更容易维护和适应变化。
auto 关键字:
使用情况: auto 用于声明变量时,编译器会根据变量的初始化表达式自动推导出变量的类型。
好处:
简化代码,减少冗余,提高可读性。
更好地支持泛型编程,使得代码更具通用性。
示例:
#include <iostream>
#include <vector>
int main() {
// 自动类型推导
auto x = 42; // x 被推导为 int
auto y = 3.14; // y 被推导为 double
// 遍历容器
std::vector<int> numbers = {1, 2, 3, 4, 5};
for (auto it = numbers.begin(); it != numbers.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
return 0;
}
auto&&总是推断出引用类型
decltype 关键字:
使用情况: decltype 用于获取表达式的类型,而不是变量的类型。它通常在需要推导表达式的类型时使用。
好处:
提供更灵活的类型推导,特别在模板编程和泛型编程中使用频繁。
用于获取表达式的确切类型。
示例
#include <iostream>
#include <vector>
int main() {
// decltype 获取表达式的类型
int x = 42;
decltype(x) y = 100; // y 的类型与 x 相同,为 int
// 结合模板
std::vector<int> numbers = {1, 2, 3, 4, 5};
decltype(numbers.size()) size = numbers.size(); // size 的类型为 std::vector<int>::size_type
return 0;
}
decltype((x))形式的表达式获得计算结果的引用类型,类似auto&&的效果。
总体而言,auto 和 decltype 可以使代码更灵活,减少类型声明的冗余,并提高代码的可读性。它们特别在现代C++中广泛用于迭代容器、模板编程、lambda 表达式等场景。
decltype(auto) 是C++14引入的一项新特性,它结合了 decltype 和 auto 的优点,用于声明变量或函数返回类型,并保留初始化表达式的引用性质。它通常在泛型编程和模板元编程中使用,提供更强大的类型推导。
使用情况:
int x = 42;
decltype(auto) y = x; // y 的类型为 int,保留了 x 的引用性质
函数返回类型:
auto getValue() -> decltype(auto) {
int x = 42;
return x; // 返回 x 的引用
}
示例:
#include <iostream>
int main() {
int x = 42;
const int& cx = x;
// decltype(auto) 用于变量声明
decltype(auto) a = x; // a 是 int,保留了 x 的引用性质
decltype(auto) b = cx; // b 是 const int,保留了 cx 的引用性质
// decltype(auto) 用于函数返回类型
auto getValue() -> decltype(auto) {
return x; // 返回 x 的引用
}
const int& result = getValue();
std::cout << result << std::endl; // 输出 42
return 0;
}
decltype(auto) 被用于变量声明和函数返回类型。它保留了被初始化表达式的引用性质,允许在泛型代码中更灵活地处理各种类型。decltype(auto) 特别适用于处理模板函数或返回表达式类型未知的场景,同时减少了代码的冗余。
3 面向过程编程
C++11引入了一些新的语法和特性,包括 nullptr、列表初始化(list initialization)、范围-based for 循环等,它们在代码编写和可读性上提供了一些便利和优势。
- nullptr:
nullptr 是 C++11 引入的空指针常量,用于替代传统的 NULL 或 0,以提供更明确和类型安全的空指针表示。
int* ptr = nullptr;
优势:
明确表示空指针,避免了 NULL 或 0 的二义性。
类型安全,可以避免一些潜在的错误。
- 列表初始化:
列表初始化使用花括号 {} 进行初始化,它可以用于初始化各种数据结构,如数组、类对象等。
int arr[] = {1, 2, 3};
std::vector<int> vec = {1, 2, 3};
优势:
统一了初始化语法,简化了代码。
避免了隐式类型转换,提高了类型安全性。
防止窄化转换,提高代码的健壮性。
- 范围-based for 循环:
范围-based for 循环用于遍历容器、数组等可迭代的对象。
std::vector<int> numbers = {1, 2, 3, 4, 5};
for (const auto& num : numbers) {
std::cout << num << " ";
}
优势:
语法简洁,避免了手动管理索引的麻烦。
避免了越界访问的风险,提高代码安全性。
范围-based for 循环简化了遍历容器的操作,减少了代码的复杂度。
- 新式函数声明:
C++11引入了一种新式的函数声明语法,被称为“函数声明尾置返回类型”(trailing return type)。这种语法允许将函数返回类型放在函数参数列表之后,使用 -> 运算符指定返回类型。
新式函数声明的语法如下:
auto function_name(parameters) -> return_type {
// 函数体
}
其中,auto 关键字表示编译器将根据函数体中的返回语句推导出返回类型。
示例:
#include <iostream>
// 传统函数声明
int add(int a, int b) {
return a + b;
}
// 新式函数声明
auto multiply(int a, int b) -> int {
return a * b;
}
int main() {
int result_add = add(3, 4);
int result_multiply = multiply(3, 4);
std::cout << "Addition result: " << result_add << std::endl;
std::cout << "Multiplication result: " << result_multiply << std::endl;
return 0;
}
multiply 函数使用了新式函数声明,将返回类型放在参数列表之后。auto 关键字用于表示编译器将推导出实际的返回类型。
新式函数声明的优势在于可以更清晰地表达函数的返回类型,特别是在涉及复杂模板或嵌套类型的场景下。此外,它避免在函数声明时提前指定返回类型可能导致的代码可读性问题。
4 面型对象编程
C++11引入了一些新的关键字和语法,用于增强面向对象编程的灵活性和安全性。
- default 和 delete:
default:使用 default 关键字可以为特殊成员函数(如默认构造函数、拷贝构造函数、赋值运算符等)生成默认实现。
struct MyClass {
MyClass() = default; // 默认构造函数的默认实现
MyClass(const MyClass& other) = default; // 拷贝构造函数的默认实现
};
delete:使用 delete 关键字可以阻止编译器生成某个特殊成员函数。
struct NoCopy {
NoCopy(const NoCopy&) = delete; // 禁止拷贝构造函数的生成
NoCopy& operator=(const NoCopy&) = delete; // 禁止拷贝赋值运算符的生成
};
- override:
override关键字用于显式声明成员函数为覆盖基类虚函数,提高代码的可读性和安全性。
class Base {
public:
virtual void foo();
};
class Derived : public Base {
public:
void foo() override; // 显式声明覆盖基类虚函数
};
- final:
final 关键字用于阻止类的继承和虚函数的进一步覆盖。
class Base {
public:
virtual void foo() final; // 声明虚函数为 final
};
class Derived : public Base {
public:
// 尝试覆盖 foo 会导致编译错误
};
- 成员初始化:
C++11允许在类的声明中直接初始化成员变量。
class MyClass {
public:
int x = 42; // 直接初始化成员变量
};
这样可以提高代码的简洁性,避免在构造函数中显式初始化。
- 委托构造:
C++11引入了委托构造函数,允许一个构造函数调用同一类的其他构造函数。
class MyClass {
public:
MyClass(int x, int y) : x_(x), y_(y) {}
MyClass(int z) : MyClass(z, 0) {} // 委托构造函数
private:
int x_;
int y_;
};
这减少了构造函数之间的代码重复,提高了代码的可维护性。
优化:
- 更安全的默认和删除: default 和 delete 关键字提供了更细粒度的控制,避免了意外生成或禁止特殊成员函数。
- 更清晰的虚函数覆盖: 使用 override 关键字能够明确地声明虚函数的覆盖关系,减少错误发生的可能性。
- 继承和虚函数的安全限制: final 关键字增加了对继承和虚函数覆盖的安全限制,避免滥用和误用。
- 简洁的成员初始化: 直接在类的声明中初始化成员变量提高了代码的简洁性。
- 委托构造提高可维护性: 委托构造函数减少了构造函数之间的重复代码,提高了代码的可维护性。
5 泛型编程
在C++11和C++14中,引入了一些新的特性和关键字,包括 using、const 与 constexpr、assert 和 static_assert,用于增强泛型编程的灵活性和安全性。
- using:
using 关键字用于类型别名的声明,用于简化复杂的类型名称。
using MyInt = int;
优势:
提高代码的可读性,减少冗长的类型名称。
- const 与 constexpr:
const: 用于声明常量,表示该变量的值不能被修改。
const int myConst = 42;
constexpr: 用于在编译时求值的常量表达式,提供了更广泛的编译时计算能力。
constexpr int myConstExpr = 2 * 21;
优势:
const 提供了运行时常量,保护数据免受意外修改。
constexpr 在编译时进行计算,可以用于更多的上下文,提高了性能和灵活性。
const 和 constexpr 提供了更灵活的常量声明和编译时计算的能力。
- assert:
assert 宏用于在运行时检查断言是否为真,如果为假,则终止程序。
#include <cassert>
int main() {
int x = 42;
assert(x == 42);
return 0;
}
优势:
在调试阶段提供了一种简单的方法来检查代码的正确性。
在遇到问题时提供了及早终止程序的机制,帮助定位错误。
- static_assert:
static_assert 用于在编译时进行静态断言检查,如果表达式为假,则导致编译错误。
static_assert(sizeof(int) == 4, "int must be 4 bytes");
优势:
在编译时提供了一种机制来检查条件,有助于提前发现一些错误。
提供了更灵活的编译时断言,可以用于各种复杂的条件检查。
assert 和 static_assert 提供了在运行时和编译时进行断言检查的机制,有助于代码的调试和稳定性。
- 可变参数模板
C++11引入了可变参数模板(Variadic Templates)的特性,允许定义接受任意数量和类型参数的函数或类模板。这使得泛型编程更加灵活,适用于处理不定数量的参数。
函数模板的可变参数:
#include <iostream>
// 基本情况:终止递归
void print() {
std::cout << std::endl;
}
// 递归情况:打印第一个参数,然后继续打印剩余参数
template<typename T, typename... Args>
void print(T first, Args... rest) {
std::cout << first << " ";
print(rest...);
}
int main() {
print(1, 2, 3, "Hello", 3.14);
return 0;
}
在上述示例中,print 函数使用可变参数模板,接受任意数量和类型的参数,并递归地打印这些参数。
类模板的可变参数:
#include <iostream>
template<typename... Args>
class Tuple;
// 基本情况:终止递归
template<>
class Tuple<> {};
// 递归情况:Tuple 包含一个元素和剩余的 Tuple
template<typename First, typename... Rest>
class Tuple<First, Rest...> {
public:
Tuple(First first, Rest... rest)
: head(first), tail(rest...) {}
First head;
Tuple<Rest...> tail;
};
int main() {
Tuple<int, double, std::string> myTuple(42, 3.14, "Hello");
return 0;
}
在上述示例中,Tuple 类模板使用可变参数模板,允许定义包含任意数量和类型的元素的元组。
可变参数模板提供了一种灵活的方式,使得可以处理不定数量的参数,适用于各种泛型编程场景,如元组、参数包展开等。
6 函数式编程
C++11引入了lambda表达式,允许在代码中定义匿名函数,提高了代码的灵活性和可读性。C++14进一步扩展了lambda表达式,引入了泛型lambda,允许lambda函数使用auto参数,提供更强大的泛型支持。
Lambda表达式:
#include <iostream>
int main() {
// 普通lambda表达式
auto add = [](int x, int y) { return x + y; };
std::cout << "Sum: " << add(3, 4) << std::endl;
// 带捕获列表的lambda表达式
int offset = 10;
auto addWithOffset = [offset](int x) { return x + offset; };
std::cout << "Sum with offset: " << addWithOffset(5) << std::endl;
return 0;
}
上述代码中,第一个lambda表达式 add 接受两个整数参数,返回它们的和。第二个lambda表达式 addWithOffset 捕获了外部变量 offset,并在其基础上进行加法操作。
优点:
- 可读性和简洁性: Lambda表达式允许在使用时定义匿名函数,减少了代码中不必要的命名,提高了可读性和简洁性。
- 方便的捕获机制: Lambda表达式支持捕获外部变量,提供了方便的闭包机制,使得在函数对象中可以使用外部变量。
- 适用于STL算法: Lambda表达式常用于STL算法,使得代码更为紧凑。
Lambda表达式的捕获列表用于指定在lambda函数体中可以访问的外部变量。捕获可以按值捕获、按引用捕获,或者混合使用。以下是捕获列表的详细解释:
- 按值捕获:
#include <iostream>
int main() {
int x = 42;
auto lambda = [x]() {
std::cout << "Captured by value: " << x << std::endl;
};
x = 100; // 不影响lambda中的捕获值
lambda(); // 输出: Captured by value: 42
return 0;
}
在上述例子中,[x] 表示按值捕获变量 x,lambda函数体中的 x 是捕获时的值的拷贝。
- 按引用捕获:
#include <iostream>
int main() {
int x = 42;
auto lambda = [&x]() {
std::cout << "Captured by reference: " << x << std::endl;
};
x = 100; // 影响lambda中的捕获引用
lambda(); // 输出: Captured by reference: 100
return 0;
}
在上述例子中,[&x] 表示按引用捕获变量 x,lambda函数体中的 x 是捕获时的引用,因此会反映最新的值。
- 混合捕获:
#include <iostream>
int main() {
int x = 42, y = 100;
auto lambda = [x, &y]() {
std::cout << "Captured by value: " << x << std::endl;
std::cout << "Captured by reference: " << y << std::endl;
};
x = 200;
y = 500;
lambda();
// 输出:
// Captured by value: 42
// Captured by reference: 500
return 0;
}
[x, &y] 表示按值捕获变量 x,按引用捕获变量 y。
- 隐式捕获:
可以使用 = 或 & 进行隐式捕获,表示按值或按引用捕获所有可见的外部变量。
#include <iostream>
int main() {
int x = 42, y = 100;
auto lambda = [=, &y]() {
std::cout << "Captured by value: " << x << std::endl;
std::cout << "Captured by reference: " << y << std::endl;
};
x = 200;
y = 500;
lambda();
// 输出:
// Captured by value: 42
// Captured by reference: 500
return 0;
}
[=, &y] 表示按值捕获所有可见的外部变量,同时按引用捕获变量 y。
- mutable修饰符:
使用 mutable 修饰符可以在lambda函数体中修改按值捕获的变量。
#include <iostream>
int main() {
int x = 42;
auto lambda = [x]() mutable {
std::cout << "Before modification: " << x << std::endl;
x = 100; // 合法,因为使用了 mutable
std::cout << "After modification: " << x << std::endl;
};
lambda();
// 输出:
// Before modification: 42
// After modification: 100
return 0;
}
mutable 允许在lambda函数体中修改按值捕获的变量 x
泛型Lambda表达式(C++14):
#include <iostream>
int main() {
auto genericLambda = [](auto x, auto y) { return x + y; };
std::cout << "Sum (int): " << genericLambda(3, 4) << std::endl;
std::cout << "Sum (double): " << genericLambda(3.14, 2.71) << std::endl;
return 0;
}
在上述代码中,genericLambda 是一个泛型lambda表达式,可以接受任意类型的参数,提供了更强大的泛型支持。
优点:
- 通用性: 泛型Lambda表达式允许在匿名函数中使用auto参数,提供了更通用、灵活的编程方式。
- 减少代码重复: 可以在同一lambda中处理不同类型的参数,减少了代码的重复性。
- 更强大的适用性: 适用于处理多种类型的情况,特别是在模板编程和泛型编程中有很大的帮助。
7 并发编程
在C++11和C++14中,extern、static、thread_local 是用于修饰变量的关键字,它们影响变量的链接性(extern)、生命周期(static)、线程局部性(thread_local)等方面。
- extern:
extern 用于声明一个全局变量,表示该变量是在其他文件中定义的。它仅用于声明,而非定义。使用 extern 可以引用其他文件中定义的全局变量,而不会在当前文件中分配存储空间。
// File1.cpp
int globalVar = 42;
// File2.cpp
extern int globalVar; // 声明全局变量,表示在其他文件中定义
- static:
static 有不同的作用,具体取决于它在不同上下文中的使用:
在全局变量中: 表示限制变量的链接性,使其在当前文件中可见,不可被其他文件引用。
// File1.cpp
static int localVar = 42; // 限制链接性,只在当前文件中可见
在局部变量中: 使得局部变量在程序生命周期内保持存在,而不是在离开其定义的作用域时销毁。
void myFunction() {
static int count = 0; // 保持存在,不会在函数调用结束后销毁
count++;
}
- thread_local:
thread_local 用于修饰变量,表示该变量具有线程局部存储,每个线程都有一份独立的副本,互不影响。
#include <iostream>
#include <thread>
thread_local int threadVar = 0; // 线程局部变量
void myThreadFunction() {
threadVar++;
std::cout << "Thread ID: " << std::this_thread::get_id() << ", threadVar: " << threadVar << std::endl;
}
int main() {
std::thread t1(myThreadFunction);
std::thread t2(myThreadFunction);
t1.join();
t2.join();
return 0;
}
在上述例子中,threadVar 是一个线程局部变量,每个线程都有一份独立的副本,互不影响。
总结特征:
extern:
用于声明全局变量,表示在其他文件中定义。
不分配存储空间,仅作为声明使用。
static:
在全局变量中表示限制链接性,使其在当前文件中可见。
在局部变量中表示在程序生命周期内保持存在。
thread_local:
用于修饰变量,表示线程局部存储。
每个线程有独立的副本,互不影响。
8 面向安全编程
在C++11和C++14中,noexcept、namespace、enum 和 enum class 是用于定义和组织代码的关键字,具有不同的用法和特征。
- noexcept:
noexcept 用于指示函数是否抛出异常。当在函数声明或定义中使用 noexcept 时,表示该函数不会抛出异常。这对于优化、异常安全性和异常规范等方面都有影响。
void myFunction() noexcept {
// 函数体
}
- namespace:
namespace 用于创建命名空间,将代码组织成逻辑上的分组。它可以包含变量、函数、类等。
namespace MyNamespace {
int myVariable;
void myFunction() {
// 函数体
}
}
- enum:
enum 用于定义枚举类型,枚举成员的值是整数。
enum Color {
RED,
GREEN,
BLUE
};
- enum class:
enum class 是C++11中引入的一种强类型的枚举,它解决了传统枚举的一些问题,如命名冲突、不安全等。
enum class Status {
OK,
ERROR
};
noexcept:
- 用于指示函数是否抛出异常。
- 提高代码的异常安全性,对编译器优化有影响。
namespace:
- 用于创建命名空间,避免命名冲突,将代码组织成逻辑上的分组。
- 提高代码的可维护性和可读性。
enum:
- 用于定义简单的枚举类型。
- 枚举成员的作用域为整个enum,可能引起命名冲突。
enum class:
- 用于定义强类型的枚举。
- 枚举成员的作用域限定在enum class内部,避免了命名冲突。
- 提供更强的类型安全性,不会隐式地转换为整数类型。
1.10 模板元编程
C++ 模板元编程(Template Metaprogramming,TMP)是一种利用模板特化、递归和编译时计算等技术在编译期进行计算的编程范式。模板元编程的目标是在编译时生成代码,以提高程序性能和灵活性。
模板元函数是使用模板进行元编程的一种形式,其中函数的计算发生在编译时而不是运行时。通过递归、特化和模板参数推断等技术,可以在编译时生成复杂的计算。
示例:计算斐波那契数列
#include <iostream>
template <unsigned n>
struct Fibonacci {
static const unsigned value = Fibonacci<n - 1>::value + Fibonacci<n - 2>::value;
};
template <>
struct Fibonacci<0> {
static const unsigned value = 0;
};
template <>
struct Fibonacci<1> {
static const unsigned value = 1;
};
int main() {
// 在编译时计算斐波那契数列
const unsigned result = Fibonacci<5>::value;
// 输出结果
std::cout << "Fibonacci(5) is: " << result << std::endl;
return 0;
}
SFINAE(Substitution Failure Is Not An Error)SFINAE 是一种编译时技术,指的是在模板参数推断和实例化过程中,如果推断失败,不会导致编译错误,而是选择其他匹配的模板。这使得我们可以通过模板元编程实现条件分支。
示例:SFINAE 判断类型是否有 value 成员
#include <iostream>
template <typename T>
struct has_value_member {
private:
typedef char yes[1];
typedef char no[2];
template <typename C>
static yes& test(decltype(&C::value));
template <typename>
static no& test(...);
public:
static const bool value = sizeof(test<T>(0)) == sizeof(yes);
};
struct WithValue {
static const int value = 42;
};
struct WithoutValue {};
int main() {
std::cout << std::boolalpha;
// 测试是否有 value 成员
std::cout << "WithValue has value member: " << has_value_member<WithValue>::value << std::endl;
std::cout << "WithoutValue has value member: " << has_value_member<WithoutValue>::value << std::endl;
return 0;
}
元编程的条件语句和循环通过使用 SFINAE 和递归等技术,可以实现元编程中的条件语句和循环。
示例:元编程的条件语句
#include <iostream>
template <bool Condition, typename T, typename F>
struct Conditional {
typedef T type;
};
template <typename T, typename F>
struct Conditional<false, T, F> {
typedef F type;
};
int main() {
// 条件语句的使用
typedef Conditional<true, int, double>::type ResultType;
// 输出结果
std::cout << "ResultType is: " << typeid(ResultType).name() << std::endl;
return 0;
}
1.11 C++知识补充
内联函数
在C++中,内联函数是一种用于提高程序性能的机制。内联函数的基本思想是在调用函数的地方直接将函数的代码插入,而不是通过常规的函数调用过程。这样可以减少函数调用的开销,提高执行效率。
内联函数的声明和定义:
要声明一个内联函数,可以在函数声明或定义前面加上 inline 关键字。
// 声明内联函数
inline int add(int a, int b);
// 定义内联函数
inline int add(int a, int b) {
return a + b;
}
内联函数的使用:
内联函数通常适用于短小的函数,因为将大型复杂的函数插入多个调用点可能会导致代码膨胀。
#include <iostream>
// 内联函数的声明和定义
inline int add(int a, int b) {
return a + b;
}
int main() {
int result = add(3, 4); // 内联函数的调用
std::cout << "Sum: " << result << std::endl;
return 0;
}
内联函数的优缺点:
优点:
- 减少函数调用开销: 内联函数会在调用点直接插入代码,减少了函数调用的开销。
- 提高执行速度: 对于短小的函数,内联可以提高程序的执行速度。
- 避免函数调用栈: 内联函数不需要在函数调用栈中保存和恢复局部变量,可以减小栈的开销。
缺点:
- 可能导致代码膨胀: 内联函数在每个调用点都会插入代码,可能导致生成的机器代码变得更大。
- 不适合复杂函数: 对于复杂的函数,内联可能不会带来性能的显著提升,而且可能会增加代码的大小。
注意事项:
- 内联函数的定义通常应该放在头文件中,以便在多个编译单元中都能够看到它,从而实现内联。
- 编译器对于是否真正内联函数的决定可能是基于启发式算法,可以使用 inline 关键字作为建议,但不是强制性的。
2.算法笔记
《算法笔记》详见:
https://segmentfault.com/a/1190000044568329
力扣刷题链接地址:
https://github.com/StuBoo3i/LeetCode
已停止更新,推荐Cline/VS leetcode插件
3.操作系统
4.计算机网络
5.数据库
6.QT开发
概述
- Qt 的基础知识:
Qt 是什么?它的主要特点是什么?
Qt 是一个跨平台的 C++ 框架,用于开发图形用户界面(GUI)和非 GUI 程序。其主要特点包括跨平台性、信号和槽机制、丰富的类库和模块、多线程支持等。
Qt 信号和槽是如何工作的?
信号和槽是 Qt 中用于实现事件处理和通信的机制。信号是某个特定事件的发射者,槽是接收并响应这个事件的函数。通过 connect 函数将信号与槽关联,当信号发生时,关联的槽函数将被调用。
Qt 中的事件系统是什么?
事件系统用于处理与用户交互和系统事件相关的任务。每个 Qt 对象都能够接收和处理事件。事件通过事件队列进行分发,对象可以通过重写事件处理函数来响应事件。
Qt 中的元对象系统是用于什么的?
元对象系统是 Qt 的一部分,用于提供反射机制。它允许在运行时获取类的信息,包括类的成员、信号和槽等。元对象系统使得信号和槽机制能够在运行时动态工作。
- Qt 的核心类:
QWidget 和 QMainWindow 的区别是什么?
QWidget 是所有用户界面对象的基类,而 QMainWindow 是主窗口的特殊类型,通常包含菜单栏、工具栏和状态栏等。
QList、QMap、QVector 分别是什么,有什么区别?
QList 是动态数组,QMap 是键值对的关联容器,QVector 是高效的动态数组。它们的区别在于存储和访问的方式。
Qt 中的模型/视图架构是什么?
模型/视图架构是用于显示数据的一种结构。模型负责管理数据,视图负责显示数据,二者通过代理(Delegate)进行交互。QAbstractItemModel 是模型的抽象基类。
Qt 的多线程支持是如何实现的?
Qt 提供了 QThread 类和一套信号槽机制来实现多线程。线程通过继承 QThread 类,实现 run 函数来定义线程的执行逻辑。多线程中,需要注意线程间数据共享的同步问题。
- Qt 的界面设计:
Qt Designer 是什么,有什么作用?
Qt Designer 是一个可视化界面设计工具,用于创建 Qt 界面。它允许开发者通过拖放和连接操作创建用户界面,生成 UI 文件以及相应的代码。
如何使用 Qt Creator 创建一个新的 Qt 项目?
打开 Qt Creator,选择 File -> New File or Project,选择适当的项目模板(如 Qt Widgets Application),设置项目名称和路径,点击 Next,然后按照向导完成项目配置。
Qt 中的布局管理器有哪些,各自的作用是什么?
常见的布局管理器有 QHBoxLayout、QVBoxLayout、QGridLayout 等。它们用于管理和自动调整界面上的控件的布局,确保在不同尺寸的窗口中能够自适应。
- Qt 的数据库支持:
Qt 中的 QSqlDatabase 是用于什么的?
QSqlDatabase 用于表示和管理数据库连接。通过它,可以连接到数据库、执行 SQL 查询、处理结果等。
Qt 中如何执行数据库查询?
使用 QSqlQuery 类来执行数据库查询。通过创建 QSqlQuery 对象,设置 SQL 查询语句,然后执行 exec 函数即可执行查询。
Qt 支持哪些数据库后端?
Qt 支持多种数据库后端,包括 SQLite、MySQL、PostgreSQL 等。具体支持的数据库取决于编译 Qt 时所使用的数据库驱动。
- Qt 的图形编程:
Qt 中的 QPainter 是用于什么的?
QPainter 是用于进行图形绘制的类。它可以在 QWidget 或 QImage 上进行绘制,实现自定义的图形输出。
Qt 中如何进行图形绘制?
通过在 paintEvent 函数中创建 QPainter 对象,然后调用相关的绘制函数进行图形绘制。
Qt 中的 QGraphicsView 和 QGraphicsScene 是用于什么的?
QGraphicsView 和 QGraphicsScene 是用于实现图形项的图形视图框架。它们支持图形项的添加、移动、缩放等操作,用于构建复杂的图形界面。
- Qt 的网络编程:
Qt 中的网络模块是什么?
Qt 的网络模块包括 QTcpSocket、QTcpServer、QUdpSocket 等类,用于实现 TCP 和 UDP 网络通信。
Qt 中如何进行网络编程?
使用 QTcpSocket 和 QTcpServer 来实现基于 TCP 的网络通信,使用 QUdpSocket 来实现基于 UDP 的网络通信。通过信号和槽机制来处理网络事件。
Qt 的网络模块支持哪些协议?
Qt 的网络模块支持 TCP、UDP、HTTP 等多种协议,通过不同的类来实现。
- Qt 的性能优化:
如何进行 Qt 项目的性能优化?
可以通过合理使用布局管理器、避免不必要的刷新、使用合适的数据结构等方式进行性能优化。使用 QPainter 进行绘制时,可以使用双缓冲等技术。
Qt 的资源管理和加载优化是什么?
使用 Qt 的资源系统,将程序所需的资源文件(如图像、样式表)打包成二进制资源文件,以减少文件读取和提高加载速度。
在 Qt 中如何处理大量数据的显示?
使用模型/视图架构,尽量避免在主线程中进行耗时的数据处理,考虑使用多线程来异步加载和处理大量数据。
- Qt 的其他相关问题:
Qt 中的国际化和本地化支持是什么?
Qt 提供了国际化和本地化的支持,可以通过 tr 函数进行字符串翻译,使用 .ts 文件管理翻译。
Qt 中的单元测试是如何进行的?
使用 Qt Test 框架进行单元测试,编写测试用例,使用 QTest 类提供的断言和测试函数。
Qt 的开发模型和编译过程是怎样的?
Qt 采用基于事件的编程模型。在编译过程中,Qt 使用 MOC(Meta-Object Compiler)处理带有元对象宏的源文件,生成元对象代码。
Qt 中如何处理异常?
Qt 中使用异常处理机制,可以使用 try、catch 和 throw 关键字来捕获和抛出异常。不过在 Qt 中,更推荐使用返回错误码的方式来处理错误。
核心机制
Qt的核心机制包括以下几个重要的概念和机制,它们共同构成了 Qt 框架的基础,支撑着 Qt 应用程序的开发和运行:
信号和槽机制:
概念:信号和槽是 Qt 中用于实现事件处理和对象间通信的机制。一个对象发射信号,而其他对象通过连接到这个信号的槽来响应这个事件。这种机制实现了松耦合和灵活的对象通信。
使用方式:QObject::connect(sender, SIGNAL(signalName()), receiver, SLOT(slotName()));
- 元对象系统(Meta-Object System):
概念: 元对象系统是 Qt 的一个关键组成部分,用于提供反射机制。它允许在运行时获取类的信息,包括类的成员、信号和槽等。元对象系统使得信号和槽机制能够在运行时动态工作。
使用方式:使用 Q_OBJECT 宏来声明一个类,以启用元对象系统。使用 QObject::metaObject() 获取类的元对象。 - 事件系统:
概念: Qt 的事件系统用于处理与用户交互和系统事件相关的任务。每个 Qt 对象都能够接收和处理事件。事件通过事件队列进行分发,对象可以通过重写事件处理函数来响应事件。
使用方式:重写对象的事件处理函数,如 QWidget::event()。 - 模型/视图架构:
概念: 模型/视图架构是 Qt 用于显示数据的一种结构。模型负责管理数据,视图负责显示数据,二者通过代理进行交互。QAbstractItemModel 是模型的抽象基类。
使用方式:创建自定义的模型类继承自 QAbstractItemModel,并实现相关接口。使用视图类如 QListView、QTableView 来显示数据。 - 事件驱动编程:
概念: Qt 采用事件驱动的编程模型,应用程序的执行流程主要由事件和信号槽的响应来驱动。当某个事件发生时,Qt 会调用相关的事件处理函数或槽函数。
使用方式:在应用程序中重写事件处理函数或使用信号槽机制来响应事件。 - 布局管理器:
概念: Qt 提供了一系列布局管理器,用于自动调整和管理界面上控件的布局。布局管理器确保在不同尺寸的窗口中能够自适应。
使用方式:使用 QHBoxLayout、QVBoxLayout、QGridLayout 等布局管理器。 - 多线程支持:
概念: Qt 提供了多线程支持,允许开发者创建和管理多个线程。Qt 的线程模型使用了信号槽机制,使得线程之间的通信更为方便。
使用方式:继承自 QThread 类,实现 run 函数来定义线程的执行逻辑。使用信号槽机制进行线程间通信。 - 对象树和父子关系:
概念: Qt 中的对象形成了一个层次结构,构成了对象树。每个对象都有一个父对象,通过父子关系,实现对象的内存管理和生命周期的管理。
使用方式:通过构造函数或 setParent 函数设置对象的父对象。当父对象销毁时,会自动销毁其子对象。
信号和槽机制
信号和槽机制是 Qt 框架中一种用于实现对象间通信和事件处理的重要机制。这种机制实现了松耦合和灵活的对象通信,是 Qt 框架的核心之一。
信号和槽的基本概念:
信号(Signal): 信号是一种对象发出的通知,表示某个事件发生了。信号可以没有参数,也可以带有参数。
槽(Slot): 槽是用于响应信号的函数。槽可以是普通的成员函数、静态函数,也可以是全局函数。
连接(Connect): 通过 connect 函数将信号和槽连接起来,使得当信号发出时,相关的槽函数被调用。
信号和槽的语法:
QObject::connect(sender, SIGNAL(signalName()), receiver, SLOT(slotName()));
sender:信号的发出者对象。
SIGNAL(signalName()):用于指定信号的宏,包含了信号的名称和参数。
receiver:信号的接收者对象。
SLOT(slotName()):用于指定槽的宏,包含了槽的名称。- 信号和槽的使用示例:
例子1:无参数的信号和槽
// 定义一个发出信号的类
class Sender : public QObject {
Q_OBJECT
public:
Sender() {}
signals:
void simpleSignal(); // 无参数的信号
public slots:
void simpleSlot() {
qDebug() << "Slot called!";
}
};
// 在其他地方连接信号和槽
Sender* sender = new Sender();
QObject::connect(sender, SIGNAL(simpleSignal()), sender, SLOT(simpleSlot()));
// 发出信号
emit sender->simpleSignal(); // 输出: Slot called!
例子2:带参数的信号和槽
// 定义一个发出带参数信号的类
class SenderWithArgs : public QObject {
Q_OBJECT
public:
SenderWithArgs() {}
signals:
void signalWithArgs(int value); // 带参数的信号
public slots:
void slotWithArgs(int value) {
qDebug() << "Slot called with value:" << value;
}
};
// 在其他地方连接带参数信号和槽
SenderWithArgs* sender = new SenderWithArgs();
QObject::connect(sender, SIGNAL(signalWithArgs(int)), sender, SLOT(slotWithArgs(int)));
// 发出带参数信号
emit sender->signalWithArgs(42); // 输出: Slot called with value: 42
- 自定义信号和槽:
除了使用内建的信号和槽外,Qt 还支持自定义信号和槽。在类中使用 signals 和 public slots 关键字定义自己的信号和槽,并在需要的时候使用 emit 关键字来发出信号。
class MyObject : public QObject {
Q_OBJECT
signals:
void customSignal(int value);
public slots:
void customSlot(int value) {
qDebug() << "Custom slot called with value:" << value;
}
void triggerSignal() {
emit customSignal(123);
}
};
MyObject* obj = new MyObject();
QObject::connect(obj, SIGNAL(customSignal(int)), obj, SLOT(customSlot(int)));
// 发出自定义信号
obj->triggerSignal(); // 输出: Custom slot called with value: 123
- 信号和槽的线程安全性:
默认情况下,信号和槽是线程安全的。如果信号和槽连接的是同一个对象,那么它们在同一线程执行。如果连接的是不同线程的对象,Qt 会自动使用线程安全的方式进行信号和槽的调用。但是,跨线程连接时需要注意线程安全性和使用 Qt::QueuedConnection 进行连接。
// 跨线程连接
QThread* thread = new QThread();
Sender* sender = new Sender();
sender->moveToThread(thread);
QObject::connect(sender, SIGNAL(simpleSignal()), sender, SLOT(simpleSlot()), Qt::QueuedConnection);
信号和槽机制是 Qt 中强大而灵活的通信机制,为开发者提供了一种优雅的方式来处理对象之间的交互和事件响应。
元对象系统
元对象系统是 Qt 框架中的一个重要组成部分,用于提供反射(reflection)和运行时类型信息(RTTI)的支持。元对象系统允许在运行时获取类的信息,包括类的成员、信号和槽等,从而实现一些高级的特性,如信号和槽机制、动态属性和动态对象特性。以下是元对象系统的核心概念和使用方法:
Q_OBJECT 宏:在使用元对象系统之前,需要在类的声明中使用 Q_OBJECT 宏,以启用元对象系统的支持。这个宏会在编译时由元对象编译器(MOC,Meta-Object Compiler)处理,生成与类相关的元对象信息。
class MyClass : public QObject { Q_OBJECT public: // 类的声明... };
- 元对象的获取:使用 QObject::metaObject() 函数可以获取一个类的元对象。元对象是 QMetaObject 类的实例,包含了类的各种信息,如类名、信号、槽等。cppconst QMetaObject* metaObject = QObject::metaObject();
类名的获取:使用 metaObject 的 className() 函数可以获取类的名称。
const char* className = metaObject->className();
- 信号和槽的元对象信息:元对象系统使得信号和槽机制在运行时能够动态工作。通过元对象,可以获取类中定义的信号和槽的信息。
获取信号的索引:
int signalIndex = metaObject->indexOfSignal("mySignal()");
获取槽的索引:
int slotIndex = metaObject->indexOfSlot("mySlot()");
动态属性的支持:元对象系统支持动态属性,允许在运行时为对象添加新的属性。通过 Q_PROPERTY 宏可以定义动态属性。
class MyClass : public QObject { Q_OBJECT Q_PROPERTY(int myDynamicProperty READ getMyDynamicProperty WRITE setMyDynamicProperty) public: int getMyDynamicProperty() const; void setMyDynamicProperty(int value); // 其他成员函数... };
类的实例化和元对象的使用:使用元对象系统可以在运行时动态创建类的实例,并利用元对象进行一些动态操作。
const QMetaObject* metaObject = MyClass::staticMetaObject; QObject* myObject = metaObject->newInstance();
动态调用成员函数:通过元对象系统,可以在运行时动态调用对象的成员函数,包括信号和槽。
const QMetaObject* metaObject = myObject->metaObject(); int methodIndex = metaObject->indexOfMethod("mySlot()"); metaObject->invokeMethod(myObject, "mySlot");
元对象系统为 Qt 提供了很多灵活性和动态性,使得开发者可以在运行时获取和操作类的信息,从而实现一些高级的特性。其中,信号和槽机制的实现就依赖于元对象系统,使得 Qt 拥有了强大的事件处理和对象通信的能力。
事件系统
框架中的事件系统用于处理与用户交互和系统事件相关的任务。每个 Qt 对象都能够接收和处理事件。事件通过事件队列进行分发,对象可以通过重写事件处理函数来响应事件。事件系统是 Qt 中实现事件驱动编程的关键机制之一。以下是事件系统的核心概念和使用方法:
- 事件基类:事件基类是 QEvent,它定义了所有事件的基本结构。派生自 QEvent 的类代表了不同类型的事件,如鼠标事件、键盘事件、定时器事件等。常见的事件类型包括:QMouseEvent:鼠标事件QKeyEvent:键盘事件QTimerEvent:定时器事件QCloseEvent:窗口关闭事件
事件处理函数:每个继承自 QObject 的类都可以重写一个或多个事件处理函数,以响应特定类型的事件。这些事件处理函数的命名规则为 event + 事件类型,如 mousePressEvent、keyPressEvent 等。
class MyWidget : public QWidget { Q_OBJECT protected: void mousePressEvent(QMouseEvent* event) override { // 处理鼠标点击事件的逻辑 } };
事件过滤器:事件过滤器是一种机制,允许对象监视和处理其他对象的事件。通过实现 eventFilter 函数,可以在一个对象上过滤和处理其他对象的事件。
class EventFilterObject : public QObject { Q_OBJECT public: bool eventFilter(QObject* watched, QEvent* event) override { if (event->type() == QEvent::MouseButtonPress) { // 处理鼠标按钮按下事件 return true; // 表示事件已被处理 } return QObject::eventFilter(watched, event); } };
事件发送和接收:事件通过 sendEvent 函数或 postEvent 函数发送给对象。sendEvent 是同步的,而 postEvent 是异步的,将事件放入对象的事件队列中等待处理。
QMouseEvent* mouseEvent = new QMouseEvent(QEvent::MouseButtonPress, QPointF(10, 10), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); QCoreApplication::sendEvent(widget, mouseEvent); // 或者使用 postEvent QCoreApplication::postEvent(widget, new QMouseEvent(QEvent::MouseButtonPress, QPointF(10, 10), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier));
自定义事件:除了使用已有的事件类型,还可以通过派生自 QEvent 类来定义自己的事件类型。这样可以在应用程序中发送和处理自定义的事件。
class CustomEvent : public QEvent { public: static const QEvent::Type CustomEventType = QEvent::Type(QEvent::User + 1); CustomEvent() : QEvent(CustomEventType) { // 自定义事件的构造逻辑 } };
事件循环:Qt 应用程序通常通过事件循环来处理事件。主要的事件循环是在 QCoreApplication 或 QApplication 中运行的。事件循环会不断地从事件队列中取出事件,并将其发送给对应的对象进行处理。
int main(int argc, char *argv[]) { QApplication app(argc, argv); // 创建窗口等对象... return app.exec(); }
事件系统使得 Qt 应用程序能够响应用户输入、处理系统事件,并通过信号和槽机制实现对象间的通信,是构建灵活、交互式应用程序的重要组成部分。
模型/视图架构
模型/视图架构是 Qt 框架中用于显示数据的一种结构,它提供了一种有效的方式来组织和显示数据,使得数据与界面的展示能够更加灵活和可扩展。这个架构基于以下几个关键组件:模型(Model)、视图(View)、代理(Delegate)和项目(Item)。
- 模型(Model):模型是数据的抽象,负责存储、检索和操作应用程序的数据。在 Qt 中,模型通常继承自 QAbstractItemModel 类,它是模型的抽象基类。
常见的模型包括: - QStandardItemModel: 提供了一个简单的表格模型,适用于小型数据集。
- QSqlQueryModel: 从数据库查询结果中生成的模型。
自定义模型:
通过继承 QAbstractItemModel 来实现自定义的模型。 - 视图(View):视图是用于显示模型中数据的界面元素。在 Qt 中,常见的视图包括:QListView: 显示一个项目列表。QTableView: 以表格形式显示数据。QTreeView: 用于显示树形结构的数据。视图是通过模型-视图架构中的模型提供的数据进行显示的,视图并不直接存储数据,而是通过模型来获取和呈现数据。
- 代理(Delegate):代理用于控制模型中数据在视图中的展示和编辑方式。在 Qt 中,代理通常继承自 QStyledItemDelegate 类,它提供了对项的外观和编辑的自定义能力。通过代理,可以实现对特定项的定制显示和编辑,而不改变模型的数据结构。
- 项目(Item):项目是模型中的基本数据单元。在 Qt 的模型中,每个项目都有一个唯一的索引(QModelIndex),通过这个索引可以唯一确定模型中的某个数据项。项目的数据和元数据(如显示状态、编辑状态等)由模型负责管理。
- 模型/视图架构的基本流程:
- 创建模型:实现一个继承自 QAbstractItemModel 的自定义模型,定义数据结构和提供相应的接口。
- 创建视图:选择适当的视图组件(如 QListView、QTableView)来显示数据。
- 设置模型:将创建的模型设置给视图,建立模型和视图之间的关联。
- 创建代理(可选):如有需要,可以创建一个继承自 QStyledItemDelegate 的代理,以控制数据在视图中的显示和编辑方式。
- 显示数据:视图通过模型获取数据并显示在界面上,每个数据项都对应一个视图项。
- 处理用户交互:视图接收用户的交互操作,如点击、编辑等,通过模型更新数据。
通过这种架构,实现了数据与界面的分离,使得对数据的操作更加方便灵活。模型/视图架构提供了一种强大的方式来组织和管理复杂的数据,适用于各种不同的数据展示需求。
事件驱动编程
事件驱动编程是一种基于事件的软件设计范式,它的核心思想是程序的执行流程主要由事件和事件处理器(或回调函数)的响应来驱动。在事件驱动模型中,程序等待并响应外部事件的发生,而不是通过顺序执行代码来控制程序的执行流程。
在 Qt 框架中,事件驱动编程是一种重要的设计模式。以下是事件驱动编程的关键概念和特点:
- 事件:事件是指在程序执行过程中发生的特定事情,可以是用户输入、系统消息、定时器事件等。每个事件都与一个特定的事件类型相关联,例如鼠标点击、按键、窗口关闭等。
- 信号和槽机制:Qt 使用信号和槽机制来实现事件的处理和对象之间的通信。对象可以发出信号,而其他对象可以连接到这些信号的槽函数,从而在特定事件发生时执行相应的操作。这种机制实现了对象之间的松耦合,提高了代码的灵活性和可维护性。
- 事件循环:事件循环是一个主要的执行结构,负责等待和分发事件。在 Qt 中,事件循环通常由 QCoreApplication(或其派生类如 QApplication)提供。事件循环会不断地从事件队列中取出事件,并将其分发给对应的对象进行处理。
事件处理函数:对象可以通过重写特定的事件处理函数来响应特定类型的事件。例如,QWidget 类中包含一系列的事件处理函数,如 mousePressEvent、keyPressEvent 等,用于处理鼠标事件、键盘事件等。
class MyWidget : public QWidget { Q_OBJECT protected: void mousePressEvent(QMouseEvent* event) override { // 处理鼠标点击事件的逻辑 } };
事件过滤器:事件过滤器允许一个对象监视并过滤另一个对象的事件。通过实现 eventFilter 函数,可以在一个对象上过滤和处理其他对象的事件。
class EventFilterObject : public QObject { Q_OBJECT public: bool eventFilter(QObject* watched, QEvent* event) override { if (event->type() == QEvent::MouseButtonPress) { // 处理鼠标按钮按下事件 return true; // 表示事件已被处理 } return QObject::eventFilter(watched, event); } };
定时器事件:定时器事件是一种周期性的事件,通过定时器可以在一定时间间隔内执行特定的操作。在 Qt 中,定时器通过 QTimer 类实现。
class MyWidget : public QWidget { Q_OBJECT public: MyWidget() { QTimer* timer = new QTimer(this); connect(timer, SIGNAL(timeout()), this, SLOT(handleTimer())); timer->start(1000); // 每隔1秒触发一次定时器事件 } public slots: void handleTimer() { // 处理定时器事件的逻辑 } };
- 事件驱动编程的优势:
响应性: 事件驱动模型使得程序能够实时响应用户输入和系统事件,提高了程序的响应性和用户体验。
可扩展性: 通过信号和槽机制,对象之间的通信更加灵活,使得系统的扩展和修改更加容易。
松耦合: 事件驱动编程使得不同组件之间能够解耦,降低了组件之间的依赖性,提高了代码的可维护性。
Qt 框架通过事件驱动编程模型,使得开发者能够轻松构建交互式和响应性强的应用程序,同时保持代码的清晰结构和可维护性。
多线程支持
Qt 提供了强大的多线程支持,使得在 Qt 应用程序中轻松使用多线程变得容易。
Qt 中多线程支持的主要特点和使用方法:
QThread 类:QThread 类是 Qt 中用于创建和管理线程的主要类。通过继承 QThread 类,可以在子类中实现线程的执行逻辑。通常,将需要在线程中执行的任务封装在 run 函数中。
class MyThread : public QThread { Q_OBJECT protected: void run() override { // 线程执行的逻辑 } };
信号和槽在多线程中的使用:在多线程环境中,Qt 的信号和槽机制可以用于线程间通信。通过使用 Qt::QueuedConnection 或 Qt::BlockingQueuedConnection,可以实现在不同线程中的对象之间进行异步或同步的信号和槽连接。
class Worker : public QObject { Q_OBJECT public slots: void doWork() { // 执行耗时操作 } signals: void workDone(); }; // 在主线程中创建对象 Worker* worker = new Worker(); // 在子线程中创建 QThread 对象 QThread* thread = new QThread(); worker->moveToThread(thread); // 在主线程中连接信号和槽 connect(worker, SIGNAL(workDone()), this, SLOT(handleWorkDone())); // 在子线程中通过信号触发槽的执行 emit worker->workDone();
使用 QtConcurrent:QtConcurrent 命名空间提供了一组用于在多线程中执行并行操作的函数和类。它通过 QtConcurrent::run 函数可以方便地在新线程中运行函数。
#include <QtConcurrent/QtConcurrentRun> void myFunction() { // 执行耗时操作 } // 在新线程中执行 myFunction QtConcurrent::run(myFunction);
线程安全的数据共享:Qt 提供了一些线程安全的数据类型,如 QMutex、QReadWriteLock 等,以便在多线程环境中实现数据共享的安全访问。
QMutex mutex; void MyThread::run() { QMutexLocker locker(&mutex); // 线程安全的操作 }
事件循环和事件处理:每个线程都可以有自己的事件循环,通过 QCoreApplication 或 QEventLoop 类可以在线程中实现事件处理。
class MyThread : public QThread { Q_OBJECT protected: void run() override { QEventLoop loop; connect(this, SIGNAL(quit()), &loop, SLOT(quit())); loop.exec(); } };
线程池:Qt 提供了 QThreadPool 类,可以用于管理和调度多个线程,实现线程的池化。线程池可以通过 QtConcurrent::run、QRunnable 等方式进行任务的分发和执行。
QThreadPool threadPool; // 在线程池中执行任务 threadPool.start(new MyRunnable());
Qt 的多线程支持使得开发者能够更轻松地处理并行任务、实现多线程程序,同时通过信号和槽机制在不同线程间进行通信,提高了程序的响应性和并发性。
对象树和父子关系
在 Qt 中,对象树和父子关系是一种组织对象的机制,用于管理对象的生命周期、内存管理以及事件传递。每个 QObject(Qt 中的基类)都可以有一个父对象,形成对象树结构。以下是对象树和父子关系的关键概念:
- 对象树结构:
根对象(Root Object): 没有父对象的对象称为根对象,通常是通过 new 运算符创建的顶层对象。
父对象(Parent Object): 通过 setParent 函数设置的对象称为父对象。父对象拥有其子对象,并负责管理其生命周期。
子对象(Child Object): 通过构造函数或 setParent 函数设置的对象称为子对象。子对象的生命周期由父对象管理。
对象树(Object Tree): 由对象及其子对象构成的层次结构,形成了对象树。对象树有助于管理对象的生命周期,当父对象被销毁时,它会自动销毁其所有子对象。 对象的创建和销毁:动态创建对象: 使用 new 运算符动态创建对象时,可以指定对象的父对象。
QObject* parentObj = new QObject(); QObject* childObj = new QObject(parentObj);
静态创建对象: 通过栈上的对象或类成员变量创建对象时,可以在构造函数中指定父对象。
class MyWidget : public QWidget { Q_OBJECT public: MyWidget(QWidget* parent = nullptr) : QWidget(parent) { // 构造函数中指定父对象 } };
销毁对象:当父对象被销毁时,它会自动销毁其所有子对象。也可以通过 delete 运算符手动销毁对象,但这会破坏父子关系。
delete parentObj; // 父对象及其所有子对象都会被销毁
父子关系的作用:
内存管理: 父对象负责管理其子对象的内存,子对象会在父对象销毁时自动销毁。
事件传递: 当对象接收到事件时,如果它没有处理该事件,事件将被传递给其父对象,形成事件传递链。
对象查找: 通过 QObject 的 findChild 和 findChildren 函数可以在对象树中查找子对象。QObject* child = parent->findChild<QObject*>(childObjectName);
- QObject 属性:QObject 类提供了一些属性用于管理对象的父子关系,包括:QObject::parent():返回对象的父对象。QObject::children():返回对象的直接子对象的列表。QObject::setParent(QObject* parent):设置对象的父对象。
示例:
QObject* parentObj = new QObject(); QObject* childObj1 = new QObject(parentObj); QObject* childObj2 = new QObject(parentObj); delete parentObj; // 父对象及其所有子对象都会被销毁
例中,parentObj 是根对象,childObj1 和 childObj2 是其子对象。当 parentObj 被销毁时,它会自动销毁其所有子对象。
Qt 的对象树和父子关系提供了一种方便的机制来管理对象的生命周期,确保对象在正确的时机被销毁,同时还支持事件传递和对象查找等功能。
面试Q&A
链接地址:https://segmentfault.com/a/1190000044573375
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。