多态:
简单地说就是不同对象去完成时,会出现不同的结果。
比如说买票,普通人买票,全价;学生买票,半价。
多态构成的条件:
1、必须通过基类的指针或引用的方式调用虚函数;
2、被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
友元(friend):
允许外部函数、其他类或其他类的成员函数访问当前类的私有(private) 和保护(protected) 成员。
友元函数:
在类中通过 friend 关键字声明的外部函数(可以是全局函数,或其他类的成员函数),被声明为友元后,该函数可以直接访问类的私有 / 保护成员。
#include <iostream>
using namespace std;
class Student {
private:
string name;
int score;
public:
Student(string n, int s) : name(n), score(s) {}
// 声明全局函数printScore为友元
friend void printScore(const Student& s);
};
// 全局函数:作为友元,可直接访问Student的私有成员
void printScore(const Student& s) {
// 访问私有成员name和score(非友元函数无法做到)
cout << s.name << "的成绩:" << s.score << endl;
}
int main() {
Student stu("小明", 90);
printScore(stu); // 输出:小明的成绩:90
return 0;
}
- 友元函数的声明需在类内部,用
friend修饰,但函数定义在类外部(与普通函数相同); - 友元函数不属于类的成员函数,因此没有
this指针,访问类成员时需通过对象参数; - 友元声明的位置(
public/private/protected段)不影响其权限。
友元类:
如果类 A 声明类 B 为友元(friend class B;),则类 B 的所有成员函数都可以访问类 A 的私有 / 保护成员。
友元类的关系是单向的(A 是 B 的友元 ≠ B 是 A 的友元),且不可传递(A 是 B 的友元,B 是 C 的友元 ≠ A 是 C 的友元)。
#include <iostream>
using namespace std;
class A {
private:
int secret = 100; // 私有成员
// 声明类B为友元:B的所有成员函数可访问A的私有成员
friend class B;
};
class B {
public:
// B的成员函数访问A的私有成员
void getASecret(A& a) {
cout << "A的私有数据:" << a.secret << endl; // 合法
}
};
int main() {
A a;
B b;
b.getASecret(a); // 输出:A的私有数据:100
return 0;
}
- 类 B 作为 A 的友元,无需在 B 中做任何额外声明,即可直接访问 A 的私有成员;
- 反之,A 不能访问 B 的私有成员(除非 B 也声明 A 为友元)。
友元成员函数:
仅允许另一个类的特定成员函数访问当前类的私有成员(而非整个类的所有成员函数)。
#include <iostream>
using namespace std;
// 提前声明类B(因为A中需要用到B)
class B;
class A {
private:
int value = 200;
public:
// 声明B的成员函数printAValue为友元
friend void B::printAValue(A& a);
};
class B {
public:
// B的这个成员函数是A的友元,可访问A的私有成员
void printAValue(A& a) {
cout << "A的私有值:" << a.value << endl; // 合法
}
// B的其他成员函数不是A的友元,无法访问A的私有成员
void otherFunc(A& a) {
// cout << a.value << endl; // 错误:无访问权限
}
};
int main() {
A a;
B b;
b.printAValue(a); // 输出:A的私有值:200
return 0;
}
- 需提前声明类 B(
class B;),否则 A 中无法识别 B; - 仅指定的成员函数(
B::printAValue)有访问权限,其他成员函数无权限,比友元类更灵活。
模板:
函数模板:
函数模板不允许自动类型转化,普通函数则能进行自动类型转换
类模板:
用于创建通用类,最典型的是 STL 容器(如vector、map),可指定元素类型。核心逻辑:类定义前用template <typename T>声明,类内成员变量 / 函数可使用T。
容器:
string容器(字符串):
string是C++风格的字符串,而string本质上是一个类
string和char * 区别:
char * 是一个指针
string是一个类,类内部封装了char*,管理这个字符串,是一个char*型的容器。
特点:
string 类内部封装了很多成员方法
查找find,拷贝copy,删除delete ,替换replace,插入insert
vector容器(动态数组):
std::vector 是一个动态数组,支持自动扩展和随机访问,适用于需要频繁随机访问的场景。
特点
- 动态扩展:std::vector 的大小会根据需求动态调整,当元素数目超过当前容量时,它会自动分配更多的内存来容纳新元素。
- 连续存储:数据存储在连续的内存块中,因此访问性能高,遍历时效率优于链表等非连续存储容器。
- 尾部操作高效:在尾部添加或删除元素的时间复杂度是 O(1),非常高效。
中间插入或删除效率低:由于 vector 是连续存储,插入或删除中间元素时,需移动大量元素,因此效率为 O(n)。
| 容器类型 | 底层结构 | 核心特性 | 适用场景 |
|---|---|---|---|
vector<T> | 动态数组 | - 随机访问快(O (1))- 尾部插入 / 删除快(O (1))- 中间插入 / 删除慢(O (n))- 内存连续 | 需频繁随机访问、尾部操作,如存储列表数据、动态数组(最常用) |
deque<T> | 双端数组 | - 双端插入 / 删除快(O (1))- 随机访问较快(O (1))- 中间操作较慢(O (n)) | 需频繁在头部和尾部操作,如队列、双端队列 |
list<T> | 双向链表 | - 任意位置插入 / 删除快(O (1),需迭代器定位)- 无随机访问(O (n))- 内存不连续 | 需频繁在中间插入 / 删除,如频繁修改的链表(如链表排序、频繁增删的场景) |
forward_list<T> | 单向链表 | - 比list更节省空间- 仅支持单向遍历- 头部插入 / 删除快(O (1)) | 内存受限,且只需单向遍历、频繁头部操作的场景(较少用) |
array<T, N> | 固定大小数组 | - 编译期固定大小- 随机访问快(O (1))- 不可动态扩容 | 已知固定大小,需高效访问(替代 C 风格数组,更安全) |
容器array(静态数组):
std::array 是固定大小的静态数组,大小在编译时确定。
特点
- 轻量高效:
std::array是静态分配的,因此不涉及动态内存分配,这使得它非常高效。 - 固定大小:数组大小在编译时确定,因此不支持动态扩展,适合已知大小的数据集合。
- 随机访问高效:访问任意元素的时间复杂度为 (O(1)),类似普通数组。
| 操作 | 方法 | 描述 |
|---|---|---|
| 访问元素 | operator[] 或 at() | 随机访问元素 |
| 获取大小 | size() | 返回固定大小 |
| 获取头尾元素 | front() / back() | 获取第一个或最后一个元素 |
| 填充所有元素 | fill(value) | 用指定值填充整个数组 |
容器list(双向链表):
双向链表,适用于频繁的中间插入和删除操作。在链表中,每个元素都有一个指向前后元素的指针,这使得在任何位置进行插入和删除都非常高效
特点
- 高效插入和删除:在链表中的插入和删除时间复杂度为 (O(1)),不需要像
vector一样移动其他元素。 - 随机访问效率低:由于链表没有连续存储,不能通过索引直接访问某个元素,必须从头或尾遍历,因此随机访问的效率很低。
| 操作 | 方法 | 描述 |
|---|---|---|
| 添加元素 | push_front() / push_back() | 在头部或尾部添加元素 |
| 删除元素 | pop_front() / pop_back() | 删除头部或尾部元素 |
| 插入元素 | insert(iterator, value) | 在指定位置插入元素 |
| 删除指定元素 | erase(iterator) | 删除指定位置的元素 |
容器deque(双端数组):
双端队列,支持在头部和尾部快速插入和删除。
| 操作 | 方法 | 描述 |
|---|---|---|
| 添加元素 | push_front() / push_back() | 在头部或尾部添加元素 |
| 删除元素 | pop_front() / pop_back() | 删除头部或尾部元素 |
| 随机访问元素 | operator[] 或 at() | 随机访问元素 |
stack容器(栈):
stack是一种先进后出的数据结构,它只有一个出口
queue容器(队列):
Queue是一种先进先出的数据结构,它有两个出口,
单例模式:
核心目标:是确保一个类仅有一个实例,并提供一个全局访问点来获取该实例。
要实现单例模式,需满足以下条件:
- 私有构造函数:禁止外部通过
new直接创建实例; - 私有拷贝构造函数和赋值运算符:禁止实例被复制(避免通过拷贝生成新实例);
- 静态成员变量:存储唯一实例的指针或引用;
- 静态成员函数:提供全局访问点,负责创建 / 返回唯一实例。
1. 饿汉式(Eager Initialization)
特点:程序启动时(类加载阶段)就创建实例,确保 “饿” 着加载,提前初始化。
优势:实现简单,天然线程安全(C++ 中全局 / 静态变量的初始化在主线程执行,无多线程竞争)。
劣势:如果实例占用资源大,会导致程序启动变慢;若实例从未被使用,会浪费资源。
2. 懒汉式(Lazy Initialization)
特点:实例在第一次被使用时才创建(“懒” 加载),避免资源浪费。
优势:节省资源,仅在需要时初始化。
劣势:多线程环境下可能创建多个实例(线程不安全),需额外处理线程同步。
虚函数和纯虚函数:
虚函数的核心目的是:允许通过基类指针 / 引用调用派生类对象的重写函数,且调用的函数版本由对象的实际类型(而非指针 / 引用的声明类型)决定(即 “运行时绑定”)。
虚函数:是用virtual关键字修饰,且必须有具体实现
纯虚函数:用virtual关键字修饰,没有具体实现,而是在声明末尾加上= 0,表示 “该函数需要由派生类实现”。
| 特性 | 虚函数(Virtual Function) | 纯虚函数(Pure Virtual Function) |
|---|---|---|
| 定义形式 | virtual 返回类型 函数名(...) { ... } | virtual 返回类型 函数名(...) = 0; |
| 是否有实现 | 必须有(基类中提供具体代码) | 无(基类中不提供实现,由派生类完成) |
| 基类性质 | 基类可实例化(非抽象类) | 基类为抽象类,不可实例化 |
| 派生类要求 | 可重写,也可继承基类实现 | 必须重写,否则派生类仍为抽象类 |
| 设计目的 | 提供可修改的默认行为 | 定义必须实现的接口规范 |
虚函数表(vtable):每个包含虚函数的类(包括其派生类)都会在编译期生成一个全局唯一的虚函数表。这是一个函数指针数组,存储该类所有虚函数的地址(包括从父类继承的未被重写的虚函数,以及自身新增或重写的虚函数)。
虚表指针(vptr):每个该类的对象在创建时,会自动包含一个指向所属类虚函数表的指针(vptr)。vptr 是对象内存的一部分,由编译器自动添加和初始化(通常在构造函数中完成)。
介绍c++的RAII机制,如何实现自动释放内存的原理,动态管理内存的底层逻辑?
核心逻辑:
- 资源获取:在对象的构造函数中完成资源的申请(如
new分配内存、fopen打开文件); - 资源释放:在对象的析构函数中完成资源的释放(如
delete释放内存、fclose关闭文件); - 生命周期绑定:当对象超出作用域(如函数返回、代码块结束)时,C++ 会自动调用其析构函数,从而强制释放资源,无需手动干预。
重写和重载的区别:
重载:在同一作用域内,允许定义多个同名的函数或运算符,但它们的参数列表(数量、类型、顺序)必须不同。
重写:在继承关系中,子类重新定义父类中已声明为虚函数(virtual) 的函数,且函数名、参数列表、返回值类型必须与父类完全一致(协变返回类型除外)。实现 “多态”—— 让父类指针 / 引用能根据指向的实际对象类型,调用对应的子类实现(运行时动态绑定)。
虚继承:
虚继承的核心是解决多继承中的 “菱形继承” 问题,让多个派生类共享同一基类实例,从而避免基类成员的冗余存储和访问二义性。
virtual关键字的位置:仅需在 “中间类继承顶层基类” 时添加,最终子类继承中间类时无需加virtual。- 虚基类构造函数的调用顺序:虚基类(如 A)的构造函数由最终派生类(如 D)直接调用,而非中间类(B、C)。即使中间类的构造函数显式初始化 A,也会被编译器忽略。
class A {
public:
A(int x) : a(x) {} // 带参构造
int a;
};
class B : virtual public A {
public:
B() : A(10) {} // 中间类的初始化会被忽略
};
class C : virtual public A {
public:
C() : A(20) {} // 同样被忽略
};
// 最终子类D必须直接初始化虚基类A
class D : public B, public C {
public:
D() : A(30) {} // 正确:直接调用A的构造函数
};
左值引用和右值引用:
左值:有明确内存地址、可被修改(除非是 const)、能出现在赋值符号=左侧的表达式。
右值:无持久内存地址、临时存在(表达式结束后销毁)、只能出现在赋值符号=右侧的表达式。
左值引用(&)
左值引用是最常用的引用类型,用&声明,只能绑定到左值(除非是const左值引用,可特殊绑定右值)。
- 绑定对象:
- 非
const左值引用(T&):只能绑定到非const左值(如普通变量)。 const左值引用(const T&):可绑定到非const左值、const左值,甚至右值(临时对象)。
- 非
- 本质:左值的 “别名”,与被引用对象共享同一块内存,修改引用会影响原对象。
- 生命周期:
const左值引用绑定右值时,会延长临时对象的生命周期(与引用同生命周期)。
右值引用(&&)
- 绑定对象:
- 非
const右值引用(T&&):只能绑定到非const右值(如临时对象、表达式结果)。 const右值引用(const T&&):实际中极少使用,可绑定const右值,但失去移动语义的意义(右值不可修改)。
- 非
- 本质:右值的 “别名”,但右值本身是临时的,右值引用的出现让我们能 “捕获” 并利用这些临时对象的资源(而非拷贝)。
- 右值引用本身是左值:虽然绑定的是右值,但右值引用变量自身有名字和地址,因此它是左值。
| 维度 | 左值引用(T& / const T&) | 右值引用(T&&) |
|---|---|---|
| 绑定对象 | 非const左值(T&);左值 / 右值(const T&) | 仅非const右值 |
| 核心用途 | 避免拷贝(函数传参、返回引用)、修改原对象 | 移动语义(接管临时对象资源)、完美转发(保持值类别) |
| 与原对象的关系 | 共享内存,修改引用影响原对象 | 绑定临时对象,可接管其资源(原对象生命周期结束) |
是否能绑定std::move后的左值 | const T&可以,但无移动意义 | 可以(std::move将左值转为右值引用) |
为什么要区分左值引用右值引用?
区分左值引用和右值引用的核心目的是:在不破坏原有安全性的前提下,通过 “资源所有权转移” 替代 “冗余拷贝”,提升程序性能,并通过语义明确性让代码意图更清晰。
右值引用本身就带了资源转交吗?还是说要调用函数去实现呢?
右值引用本身不会自动完成资源转交,它仅仅是一种 “类型标识”,告诉编译器 “当前引用的对象是右值(临时对象或可被转移资源的对象)”。真正的资源转交逻辑需要通过移动构造函数或移动赋值运算符来手动实现。
move这个操作资源会不会转交资源?
std::move 的核心价值是:通过将左值转为右值引用,允许其资源被安全转移,从而在不影响程序正确性的前提下,用 “移动” 替代 “拷贝”,减少资源浪费,提升性能。
注意:std::move 本身不直接移动资源,它只是一个 “类型转换工具”。资源是否真的被转移,取决于目标类型是否定义了移动构造函数或移动赋值运算符:
- 若定义了移动函数:资源会被转移(浅拷贝指针,原对象资源被置空)。
- 若未定义移动函数:
std::move后的对象会退化为拷贝(仍深拷贝资源)。
智能指针:
智能指针不是指针,是一个管理指针的类,用来存储指向动态分配对象的指针,负责自动释放动态分配的对象,防止堆内存泄漏和空悬指针等等问题。
unique_ptr:
核心特性:独占所有权,即同一时间只能有一个 unique_ptr 指向某个对象,不允许拷贝。
管理单个对象或动态数组的内存,适用于 “一对一” 的资源。
shared_ptr:
核心特性:共享所有权,多个 shared_ptr 可指向同一对象,通过 引用计数 跟踪对象被引用的次数,当最后一个 shared_ptr 销毁时,自动释放对象。
用于需要多个所有者共享资源的场景。
weak_ptr:
核心特性:弱引用,不拥有对象的所有权,仅作为 shared_ptr 的 “观察者”,不影响引用计数
- 核心作用是打破
shared_ptr的循环引用,避免内存泄漏; - 次要作用是临时访问共享资源,且不延长对象的生命周期。
enable_shared_from_this()、shared_from_this():
作用:enable_shared_from_this 配合 shared_from_this(),允许类内部安全地获取指向自身的 shared_ptr,且与外部 shared_ptr 共享引用计数,避免内存错误。
原理:通过基类内部的 weak_ptr 关联外部 shared_ptr,shared_from_this() 基于此 weak_ptr 生成有效的 shared_ptr。
lambda:
lambda 表达式(也称为 “匿名函数”)是 C++11 引入的特性,用于在代码中快速定义短小的函数,无需像普通函数那样单独声明和定义。
| 捕获方式 | 含义 | 示例 |
|---|---|---|
[] | 不捕获任何外部变量 | - |
[x] | 值捕获:复制外部变量 x 到 lambda 内 | int x=10; auto f = [x](){ return x; }; |
[&x] | 引用捕获:通过引用访问外部变量 x | int x=10; auto f = [&x](){ x++; }; |
[=] | 隐式值捕获:捕获所有外部变量的副本 | int a=1, b=2; auto f = [=](){ return a+b; }; |
[&] | 隐式引用捕获:通过引用访问所有外部变量 | int a=1; auto f = [&](){ a++; }; |
[=, &x] | 混合捕获:默认值捕获,除 x 用引用 | int x=1, y=2; auto f = [=, &x](){ x++; return y; }; |
[&, x] | 混合捕获:默认引用捕获,除 x 用值 | int x=1, y=2; auto f = [&, x](){ y++; return x; }; |
inline关键字用法:
内联函数修饰符,意味着无需调用,直接衔接在函数后。广泛代替#define,因为#define不具备检查语法错误,仅为简单替换。
深拷贝和浅拷贝:
浅拷贝:只复制对象的 “表层结构”:
对于基本类型属性:直接复制值,原对象和拷贝对象的基本类型属性相互独立;
对于引用类型属性:仅复制引用地址(栈内存中的地址),不复制堆内存中的实际数据。因此,原
对象和拷贝对象的引用类型属性会指向同一块堆内存,修改其中一个会影响另一个。
深拷贝:会递归复制对象的所有层级:
不仅复制表层的基本类型属性,还会对嵌套的引用类型属性(如对象中的对象、数组中的数组)进行 “深层复制”,即重新在堆内存中创建一份完全相同的数据,并让拷贝对象的引用指向新内存。
最终,原对象和拷贝对象完全独立,修改任何一方的属性都不会影响另一方。
| 维度 | 浅拷贝 | 深拷贝 |
|---|---|---|
| 复制深度 | 仅复制表层 | 递归复制所有嵌套层级 |
| 引用类型处理 | 共享同一块堆内存 | 重新创建堆内存,完全独立 |
| 相互影响 | 引用类型属性修改会相互影响 | 任何修改都不相互影响 |
| 性能 | 效率高(复制少) |
效率低(复制多,递归耗时) |
指针和引用的区别:
指针:是一个变量,专门用于存储另一个变量的内存地址。通过指针可以访问其指向的内存中的数据。
引用:是变量的别名,它与被引用的变量共享同一块内存空间,本身不占用额外内存(逻辑层面)。
| 特性 | 指针 | 引用 |
|---|---|---|
| 本质 | 存储地址的变量 | 变量的别名 |
| 初始化 | 可延迟初始化,可指向nullptr | 必须在声明时初始化,不能为nullptr |
| 指向可修改 | 可随时改变指向的对象 | 一旦初始化,指向不可改变 |
| 操作符 | 需要*解引用、&取地址 | 直接使用,无需解引用 |
| 多级访问 | 支持多级指针(如**) | 不支持多级引用 |
| 安全性 | 较低(可能出现野指针、空指针) | 较高(无空引用,需避免悬垂引用) |
引用类型:指存储在堆内存中的数据(如对象、数组、函数等),变量中仅存储其内存地址(引用);
基本类型:指存储在栈内存中的数据(如数字、字符串、布尔值等),变量直接存储值。
new和malloc的区别
new:C++操作符,
- 不需要显式指定内存大小,编译器会根据目标类型自动计算所需字节数。
- 返回的是对应类型的指针(无需强制类型转换)。
- 语法:
类型* 指针 = new 类型(初始化值);(单个对象)或类型* 指针 = new 类型[数量]。
malloc:库函数,
- 需要显式指定分配的字节数(需手动计算,如
sizeof(类型) * 数量)。 - 返回的是
void*指针,必须强制类型转换为目标类型才能使用。 - 语法:
类型* 指针 = (类型*)malloc(字节数)。
函数指针有什么用
函数指针的核心价值在于将函数作为 “数据” 来传递和使用。
1、实现回调函数
#include <iostream>
using namespace std;
// 排序函数:接收数组、元素个数、元素大小、比较函数指针
void mySort(void* base, int num, int size,
int (*cmp)(const void*, const void*)) {
// 简单冒泡排序(核心:用cmp函数指针比较元素)
for (int i = 0; i < num - 1; i++) {
for (int j = 0; j < num - i - 1; j++) {
// 计算第j和j+1个元素的地址
void* elem1 = (char*)base + j * size;
void* elem2 = (char*)base + (j + 1) * size;
// 通过函数指针调用比较函数:若返回值>0,则交换
if (cmp(elem1, elem2) > 0) {
// 交换两个元素(按字节复制)
char temp[size];
memcpy(temp, elem1, size);
memcpy(elem1, elem2, size);
memcpy(elem2, temp, size);
}
}
}
}
// 比较int的回调函数:升序(a > b返回1,否则返回-1)
int cmpInt(const void* a, const void* b) {
int* p1 = (int*)a;
int* p2 = (int*)b;
return (*p1 > *p2) ? 1 : -1;
}
// 比较double的回调函数:降序(a < b返回1,否则返回-1)
int cmpDouble(const void* a, const void* b) {
double* p1 = (double*)a;
double* p2 = (double*)b;
return (*p1 < *p2) ? 1 : -1;
}
int main() {
int arr1[] = {3, 1, 4, 2};
mySort(arr1, 4, sizeof(int), cmpInt); // 传入int比较函数
for (int x : arr1) cout << x << " "; // 输出:1 2 3 4
double arr2[] = {3.2, 1.5, 4.7, 2.1};
mySort(arr2, 4, sizeof(double), cmpDouble); // 传入double比较函数
for (double x : arr2) cout << x << " "; // 输出:4.7 3.2 2.1 1.5
return 0;
}
2、实现状态机机制
#include <iostream>
#include <string>
using namespace std;
// 命令处理函数类型:接收字符串参数,返回是否成功
typedef bool (*CmdHandler)(const string&);
// 具体命令处理函数
bool handleAdd(const string& args) {
cout << "执行添加操作,参数:" << args << endl;
return true;
}
bool handleDel(const string& args) {
cout << "执行删除操作,参数:" << args << endl;
return true;
}
bool handleQuit(const string& args) {
cout << "退出程序" << endl;
exit(0); // 退出程序
return true;
}
int main() {
// 函数表:命令名 -> 处理函数指针(可扩展更多命令)
struct {
string cmd;
CmdHandler handler;
} cmdTable[] = {
{"add", handleAdd},
{"del", handleDel},
{"quit", handleQuit}
};
int tableSize = sizeof(cmdTable) / sizeof(cmdTable[0]);
// 模拟命令输入
string input;
while (true) {
cout << "请输入命令(add/del/quit):";
cin >> input;
// 遍历函数表,找到对应处理函数并调用
bool found = false;
for (int i = 0; i < tableSize; i++) {
if (cmdTable[i].cmd == input) {
cmdTable[i].handler("参数示例"); // 调用函数指针
found = true;
break;
}
}
if (!found) cout << "未知命令" << endl;
}
return 0;
}
3、模拟多态(C 语言中)
#include <stdio.h>
// 定义“动物”结构体:包含“叫”的函数指针
typedef struct Animal {
void (*speak)(); // 函数指针:指向具体动物的“叫”函数
} Animal;
// 狗的“叫”实现
void dogSpeak() {
printf("汪汪叫\n");
}
// 猫的“叫”实现
void catSpeak() {
printf("喵喵叫\n");
}
// 创建狗对象
Animal createDog() {
Animal dog;
dog.speak = dogSpeak; // 绑定狗的函数
return dog;
}
// 创建猫对象
Animal createCat() {
Animal cat;
cat.speak = catSpeak; // 绑定猫的函数
return cat;
}
int main() {
Animal animal1 = createDog();
Animal animal2 = createCat();
animal1.speak(); // 调用狗的speak:汪汪叫
animal2.speak(); // 调用猫的speak:喵喵叫
return 0;
}
4、QT信号槽机制
线程同步和线程异步:
同步:规定顺序执行,可以通过互斥锁,条件变量,信号量等进行同步。
异步:讲究的是独立性,每个线程互不干扰。
| 维度 | 同步(Synchronous) | 异步(Asynchronous) |
|---|---|---|
| 执行方式 | 线程需等待其他线程完成,顺序执行 | 线程独立执行,无需等待,并行处理 |
| 阻塞性 | 可能阻塞(如等待锁释放) | 通常非阻塞(发起任务后立即返回) |
| 核心目的 | 保证共享资源安全,避免数据竞争 | 提高效率和响应性,不阻塞主线程 |
| 适用场景 | 多线程操作共享资源、有依赖的任务 | 耗时操作(IO、网络)、无依赖的任务 |
| 实现关键 | 依赖锁、信号量等同步工具 | 依赖回调、Future、事件循环等机制 |
TCP和UDP的区别
1、连接方式不同
TCP:面向连接,通过三次握手建立连接,通过四次挥手断开连接;
UDP:不需要预先建立连接,直接发送数据。
2、可靠性
TCP:可靠传输,通过确认应答(ACK)、超时重传、丢包重发等机制确保数据有序;
UCP:不可靠传输,不保证数据是否到达和顺序是否正确。
3、数据顺序
TCP:通过序列号和确认机制保证数据按发送数据到达;
UDP:不保证传输顺序,即使数据乱序也不会重新排序。
4、流量控制
TCP:通过滑动窗口机制动态调整发送速率,避免接收方缓冲区溢出。
UDP:无流量控制,可能因发送过快导致丢包。
5. 拥塞控制
TCP:通过慢启动、拥塞避免等算法(如Reno、CUBIC)避免网络拥堵。
UDP:无拥塞控制,可能加剧网络拥堵。
6. 传输效率
TCP:因连接管理、重传等机制,头部开销大(20字节以上),传输效率较低。
UDP:头部仅8字节,无额外控制机制,传输效率高。
7. 数据边界
TCP:基于字节流,无明确消息边界,需应用层自行处理(如添加分隔符)。
UDP:保留数据报边界,每次发送/接收均为独立报文。
8. 多播/广播支持
TCP:仅支持单播(一对一通信)。
UDP:支持单播、多播(一对多)和广播(一对所有)。
9. 适用场景
TCP:要求可靠传输的场景(如网页浏览、文件传输、电子邮件)。
UDP:实时性优先的场景(如视频流、语音通话、在线游戏、DNS查询)。
10. 首部大小
TCP:首部至少20字节(包含选项字段可更长)。
UDP:固定8字节首部(源端口、目的端口、长度、校验和)。
相关的API:
1. 通用基础函数(TCP 和 UDP 共需)
-
socket():创建套接字(传输层端点)。int socket(int domain, int type, int protocol);domain:协议族(AF_INET表示 IPv4);type:套接字类型(SOCK_STREAM对应 TCP,SOCK_DGRAM对应 UDP);protocol:协议(通常为 0,自动匹配type对应的协议)。- 返回值:套接字描述符(
int,类似文件描述符),失败返回 - 1。
-
bind():将套接字绑定到本地 IP 和端口(服务器端必需,客户端可选)。int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);sockfd:socket()返回的套接字描述符;addr:指向本地 IP 和端口的结构体(struct sockaddr_in,需转换为struct sockaddr*);addrlen:addr的长度(sizeof(struct sockaddr_in))。
2. TCP 专用 API(面向连接的流程)
TCP 通信需先建立连接(服务器监听→客户端发起连接→双方确认),再传输数据,最后断开连接。
(1)服务器端流程
-
listen():将套接字设为监听状态(仅服务器端调用)。int listen(int sockfd, int backlog);backlog:未完成连接队列的最大长度(等待三次握手的连接数)。
-
accept():阻塞等待并接受客户端的连接请求(仅服务器端调用)。int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);- 返回值:新的套接字描述符(用于与该客户端通信,原
sockfd继续监听新连接); addr:输出参数,存储客户端的 IP 和端口。
- 返回值:新的套接字描述符(用于与该客户端通信,原
-
send()/recv():已连接状态下发送 / 接收数据(基于accept()返回的新套接字)。ssize_t send(int sockfd, const void *buf, size_t len, int flags); // 发送 ssize_t recv(int sockfd, void *buf, size_t len, int flags); // 接收
(2)客户端流程
-
connect():向服务器发起连接请求(三次握手由内核完成)。int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);addr:服务器的 IP 和端口;- 成功返回 0(连接建立),失败返回 - 1。
-
连接建立后,客户端也通过
send()/recv()与服务器通信。
(3)关闭连接
close():关闭套接字,触发四次挥手断开连接。int close(int fd); // fd为套接字描述符
3. UDP 专用 API(无连接的流程)
UDP 无需建立连接,直接发送 / 接收数据,核心是通过 “目标地址” 指定接收方。
-
sendto():发送数据到指定的目标 IP 和端口(客户端和服务器端均可用)。ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);dest_addr:目标主机的 IP 和端口;- 无需先建立连接,直接发送。
-
recvfrom():接收数据,并获取发送方的 IP 和端口(客户端和服务器端均可用)。ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);src_addr:输出参数,存储发送方的 IP 和端口;- 每次接收都需明确来源(因无连接状态)。
| 角色 | TCP 流程 | UDP 流程 |
|---|---|---|
| 服务器 | socket() → bind() → listen() → accept() → send()/recv() → close() | socket() → bind() → recvfrom()/sendto() → close() |
| 客户端 | socket() → connect() → send()/recv() → close() | socket()(可选bind()) → sendto()/recvfrom() → close() |
UDP是一个面向无连接的,不安全的,报式传输层协议,udp的通信过程默认也是阻塞的。
UDP通信不需要建立连接 ,因此不需要进行connect()操作
UDP通信过程中,每次都需要指定数据接收端的IP和端口,和发快递差不多
UDP不对收到的数据进行排序,在UDP报文的首部中并没有关于数据顺序的信息
UDP对接收到的数据报不回复确认信息,发送端不知道数据是否被正确接收,也不会重发数据。
如果发生了数据丢失,不存在丢一半的情况,如果丢当前这个数据包就全部丢失了
TCP三次握手:
ping的工作原理:
ping 利用 ICMP 协议发送回显请求,通过目标主机的回显应答判断连通性,并计算往返时间和丢包率,核心是 “请求 - 回复” 的简单交互机制。
2304

被折叠的 条评论
为什么被折叠?



