C++_高阶
异常处理
异常提供了一种转义控制权的方式,控制权转移的本质:
当程序中发生异常时,它会中断当前的正常执行流程,将程序的控制权(即代码执行权)从当前点转移到专门处理错误的异常处理代码块。
C++ 异常处理涉及到三个关键字:try、catch、throw
-
throw:
当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
-
-
double division(int a, int b) { if( b == 0 ) { throw "Division by zero condition!"; } return (a/b); }
-
-
catch:
在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常。
-
-
try { // 保护代码 }catch( ExceptionName e ) { // 处理 ExceptionName 异常的代码 }
上面的代码会捕获一个类型为 ExceptionName 的异常。如果您想让 catch 块能够处理 try 块抛出的任何类型的异常,则必须在异常声明的括号内使用省略号 ...,如下所示:
-
try { // 保护代码 }catch(...) { // 能处理任何异常的代码 }
-
-
try:
try 块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个 catch 块。
-
-
#include <iostream> using namespace std; double division(int a, int b) { if( b == 0 ) { throw "Division by zero condition!"; } return (a/b); } int main () { int x = 50; int y = 0; double z = 0; try { z = division(x, y); cout << z << endl; }catch (const char* msg) { cerr << msg << endl; } return 0; }
-
-
C++ 标准的异常
C++ 提供了一系列标准的异常,定义在 <exception> 中,我们可以在程序中使用这些标准的异常。它们是以父子类层次结构组织起来的,如下所示: -
![image]()
-
定义新的异常
- 您可以通过继承和重载 exception 类来定义新的异常。下面的实例演示了如何使用 std::exception 类来实现自己的异常:
在这里,what() 是异常类提供的一个公共方法,它已被所有子异常类重载。这将返回异常产生的原因。
#include <iostream> #include <exception> using namespace std; struct MyException : public exception { const char * what () const throw () { return "C++ Exception"; } }; int main() { try { throw MyException(); } catch(MyException& e) { std::cout << "MyException caught" << std::endl; std::cout << e.what() << std::endl; } catch(std::exception& e) { //其他的错误 } }
- 您可以通过继承和重载 exception 类来定义新的异常。下面的实例演示了如何使用 std::exception 类来实现自己的异常:
动态内存
-
new与malloc的区别
-
概念本质与所属范畴
malloc:malloc是 C 标准库函数,在 C 和 C++ 代码中均可使用。它的主要功能是从堆中分配指定字节数的内存空间,其函数原型为void* malloc(size_t size).new:new是 C++ 关键字,是 C++ 面向对象编程中内存分配机制的核心部分。new不仅负责分配内存,还会根据对象类型进行初始化,这与 C++ 的对象模型和构造函数概念紧密相连.
-
-
-
类型处理机制
malloc:malloc不具备类型感知能力,它只关注分配的字节数。无论要存储何种类型的数据,malloc返回的都是void*类型指针,意味着在使用这个指针之前,程序员必须手动进行类型转换
-
int* numPtr = (int*)malloc(sizeof(int));
-
new:new在类型处理上更为智能和安全。它会根据所创建对象的类型自动计算所需内存大小,并返回正确类型的指针,无需手动类型转换。-
int* numPtr = new int;
-
-
内存初始化方式
malloc:malloc分配的内存不会自动初始化,内存中的内容是未定义的。这意味着如果分配的内存用于存储对象,对象的成员变量值将是不确定的。
-
struct MyClass { int data; MyClass() : data(0) {} }; MyClass* objPtr1 = (MyClass*)malloc(sizeof(MyClass)); // objPtr1->data的值是未定义的
-
-
new操作符在分配内存后,会自动调用对象的构造函数进行初始化。对于内置类型,如int,若使用new int,会进行默认初始化(对于int等算术类型,默认初始化为 0);若使用new int(),则会进行值初始化(同样,对于int会初始化为 0)。对于自定义类型,会调用其对应的构造函数进行全面初始化。
-
struct MyClass { int data; MyClass() : data(0) {} }; MyClass* objPtr2 = new MyClass; // objPtr2->data的值为0,因为调用了构造函数进行初始化
-
-
内存释放机制
-
malloc:使用malloc分配的内存,必须使用free函数来释放。free函数原型为void free(void* ptr),其中ptr是malloc、calloc或realloc返回的指针。如果传递给free的指针不是这些函数返回的,或者该指针已经被释放过,程序会出现未定义行为
-
int* numPtr = (int*)malloc(sizeof(int)); free(numPtr);
-
new:使用new分配的内存,要使用delete关键字来释放(对于数组,使用delete[])。delete不仅会释放内存,还会在释放前调用对象的析构函数,以清理对象占用的其他资源。-
int* numPtr = new int; delete numPtr; int* arrPtr = new int[10]; delete[] arrPtr;
-
-
-
异常处理与错误反馈
malloc:malloc分配内存失败时,返回NULL。调用者需要在每次调用malloc后手动检查返回值,以判断内存分配是否成功。
-
int* numPtr = (int*)malloc(sizeof(int)); if (numPtr == NULL) { // 处理内存分配失败的情况,如提示用户、记录日志等 }
-
new在分配内存失败时,默认会抛出std::bad_alloc异常。在 C++ 中,可以使用try - catch块来捕获并处理这个异常,使错误处理代码更加集中和结构化-
try { int* numPtr = new int; } catch (const std::bad_alloc& e) { // 处理内存分配失败的异常,如提示用户、记录日志等 }
-
-
预处理器
预处理器是一些指令,指示编译器在实际编译之前所需完成的预处理。
所有的预处理器指令都是以井号(#)开头,只有空格字符可以出现在预处理指令之前。预处理指令不是 C++ 语句,所以它们不会以分号(;)结尾。
-
#include
- 这个宏用于把头文件包含到源文件中
-
#define
#define 预处理指令用于创建符号常量。该符号常量通常称为宏
-
-
参数宏
-
#include <iostream> using namespace std; #define MIN(a,b) (a<b ? a : b) int main () { int i, j; i = 100; j = 30; cout <<"较小的值为:" << MIN(i, j) << endl; return 0; }
-
-
条件编译
用来有选择地对部分程序源代码进行编译。这个过程被称为条件编译。
-
#ifdef NULL #define NULL 0 #endif 您可以只在调试时进行编译,调试开关可以使用一个宏来实现,如下所示: #ifdef DEBUG cerr <<"Variable x = " << x << endl; #endif 如果在指令 #ifdef DEBUG 之前已经定义了符号常量 DEBUG,则会对程序中的 cerr 语句进行编译。您可以使用 #if 0 语句注释掉程序的一部分,如下所示: #if 0 不进行编译的代码 #endif
-
-
-
# 和 ## 运算符
-
#
-
运算符会把 replacement-text 令牌转换为用引号引起来的字符串。
-
#include <iostream> using namespace std; #define MKSTR( x ) #x int main () { cout << MKSTR(HELLO C++) << endl; return 0; }
RE:HELLO C++
-
-
##
-
运算符用于连接两个令牌。当 CONCAT 出现在程序中时,它的参数会被连接起来,并用来取代宏。例如,程序中 CONCAT(HELLO, C++) 会被替换为 "HELLO C++"
-
-
-
#include <iostream> using namespace std; #define concat(a, b) a ## b int main() { int xy = 100; cout << concat(x, y); return 0; }
-
-
-
C++ 中的预定义宏
| 描述 | |
|---|---|
| __LINE__ | 这会在程序编译时包含当前行号。 |
| __FILE__ |
|
| __DATE__ | 这会包含一个形式为 month/day/year 的字符串,它表示把源文件转换为目标代码的日期。 |
| __TIME__ | 这会包含一个形式为 hour:minute:second 的字符串,它表示程序被编译的时间。 |
-
-
#include <iostream> using namespace std; int main () { cout << "Value of __LINE__ : " << __LINE__ << endl; cout << "Value of __FILE__ : " << __FILE__ << endl; cout << "Value of __DATE__ : " << __DATE__ << endl; cout << "Value of __TIME__ : " << __TIME__ << endl; return 0; } Value of __LINE__ : 6 Value of __FILE__ : test.cpp Value of __DATE__ : Feb 28 2011 Value of __TIME__ : 18:52:48
-
多线程
-
基本概念
- 线程:程序执行中的单一顺序控制流,线程共享进程的地址空间、文件描述符、堆和全局变量等资源,但每个线程有自己的栈、寄存器和程序计数器。
- 并行:多个任务在多个处理器或处理器核上同时执行。
- 并发:多个任务在时间片段内交替执行,表现出同时进行的效果。
-
创建线程
-
语法
-
#include<thread> std::thread thread_object(callable, args...); callable:可调用对象,可以是函数指针、函数对象、Lambda 表达式等。 args...:传递给 callable 的参数列表。
-
-
使用函数指针
-
#include <iostream> #include <thread> void printMessage(int count) { for (int i = 0; i < count; ++i) { std::cout << "Hello from thread (function pointer)!\n"; } } int main() { std::thread t1(printMessage, 5); // 创建线程,传递函数指针和参数 t1.join(); // 等待线程完成 return 0; }
-
-
使用函数对象
-
通过类中的 operator() 方法定义函数对象来创建线程
-
-
-
#include <iostream> #include <thread> class PrintTask { public: void operator()(int count) const { for (int i = 0; i < count; ++i) { std::cout << "Hello from thread (function object)!\n"; } } }; int main() { std::thread t2(PrintTask(), 5); // 创建线程,传递函数对象和参数 t2.join(); // 等待线程完成 return 0; }
-
-
使用 Lambda 表达式
Lambda 表达式可以直接内联定义线程执行的代码:
-
#include <iostream> #include <thread> int main() { std::thread t3([](int count) { for (int i = 0; i < count; ++i) { std::cout << "Hello from thread (lambda)!\n"; } }, 5); // 创建线程,传递 Lambda 表达式和参数 t3.join(); // 等待线程完成 return 0; }
-
-
-
线程管理
-
join
- join() 用于等待线程完成执行。如果不调用 join() 或 detach() 而直接销毁线程对象,会导致程序崩溃。
-
detach
- detach() 将线程与主线程分离,线程在后台独立运行,主线程不再等待它。
-
-
线程的传参
- 值传递
- std::thread t(func, arg1, arg2);
- 引用传递
-
#include <iostream> #include <thread> void increment(int& x) { ++x; } int main() { int num = 0; std::thread t(increment, std::ref(num)); // 使用 std::ref 传递引用 t.join(); std::cout << "Value after increment: " << num << std::endl; return 0; }
-
- 值传递
-
线程同步和互斥
在C++中,当两个或更多的线程需要访问共享数据时,就会出现线程安全问题。这是因为,如果没有适当的同步机制,一个线程可能在另一个线程还没有完成对数据的修改就开始访问数据,这将导致数据的不一致性和程序的不可预测性。为了解决这个问题,C++提供了多种线程同步和互斥的机制。
-
-
互斥量
-
互斥量是一种同步机制,用于防止多个线程同时访问共享资源。在C++中,可以使用std::mutex类来创建互斥量。
-
#include <thread> #include <mutex> std::mutex mtx; // 全局互斥量 int shared_data = 0; // 共享数据 void thread_func() { for (int i = 0; i < 10000; ++i) { mtx.lock(); // 获取互斥量的所有权 ++shared_data; // 修改共享数据 mtx.unlock(); // 释放互斥量的所有权 } } int main() { std::thread t1(thread_func); std::thread t2(thread_func); t1.join(); t2.join(); std::cout << shared_data << std::endl; // 输出20000 return 0; }
我们创建了一个全局互斥量
mtx和一个共享数据shared_data。然后,我们在thread_func函数中使用mtx.lock()和mtx.unlock()来保护对shared_data的访问,确保在任何时候只有一个线程可以修改shared_data。
-
-
锁
-
C++还提供了std::lock_guard和std::unique_lock两种锁,用于自动管理互斥量的所有权。
-
-
-
#include <thread> #include <mutex> std::mutex mtx; // 全局互斥量 int shared_data = 0; // 共享数据 void thread_func() { for (int i = 0; i < 10000; ++i) { std::lock_guard<std::mutex> lock(mtx); // 创建锁,自动获取互斥量的所有权 ++shared_data; // 修改共享数据 // 锁在离开作用域时自动释放互斥量的所有权 } } int main() { std::thread t1(thread_func); std::thread t2(thread_func); t1.join(); t2.join(); std::cout << shared_data << std::endl; // 输出20000 return 0; }
使用
std::lock_guard来自动管理互斥量的所有权。当创建std::lock_guard对象时,它会自动获取互斥量的所有权,当std::lock_guard对象离开作用域时,它会自动释放互斥量的所有权。这样,我们就不需要手动调用mtx.lock()和mtx.unlock(),可以避免因忘记释放互斥量而导致的死锁
-
-
条件变量
-
条件变量是一种同步机制,用于在多个线程之间同步条件的变化。在C++中,可以使用std::condition_variable类来创建条件变量。
-
-
-
#include <thread> #include <mutex> #include <condition_variable> std::mutex mtx; // 全局互斥量 std::condition_variable cv; // 全局条件变量 bool ready = false; // 共享条件 void print_id(int id) { std::unique_lock<std::mutex> lock(mtx); // 创建锁,自动获取互斥量的所有权 while (!ready) { // 如果条件不满足 cv.wait(lock); // 等待条件变量的通知 } // 当收到条件变量的通知,且条件满足时,继续执行 std::cout << "thread " << id << '\n'; } void go() { std::unique_lock<std::mutex> lock(mtx); // 创建锁,自动获取互斥量的所有权 ready = true; // 修改共享条件 cv.notify_all(); // 通知所有等待的线程 } int main() { std::thread threads[10]; for (int i = 0; i < 10; ++i) threads[i] = std::thread(print_id, i); std::cout << "10 threads ready to race...\n"; go(); // 开始比赛 for (auto& th : threads) th.join(); return 0; }
在上述代码中,我们创建了一个全局互斥量
mtx、一个全局条件变量cv和一个共享条件ready。然后,我们在print_id函数中使用cv.wait(lock)来等待条件变量的通知,当收到条件变量的通知,且条件满足时,继续执行。在go函数中,我们修改共享条件,并使用cv.notify_all()来通知所有等待的线程
-
-
原子操作(Atomic Operation)
原子操作是一种特殊的操作,它可以在多线程环境中安全地对数据进行读写,而无需使用互斥量或锁。在C++中,可以使用
std::atomic模板类来创建原子类型。-
#include <thread> #include <atomic> std::atomic<int> shared_data(0); // 共享数据 void thread_func() { for (int i = 0; i < 10000; ++i) { ++shared_data; // 原子操作 } } int main() { std::thread t1(thread_func); std::thread t2(thread_func); t1.join(); t2.join(); std::cout << shared_data << std::endl; // 输出20000 return 0; }
创建了一个原子类型的共享数据
shared_data。然后,我们在thread_func函数中使用++shared_data来进行原子操作,这样,我们就不需要使用互斥量或锁,也可以保证在任何时候只有一个线程可以修改shared_data
-
-
异步编程
-
概述
异步编程是一种编程范式,允许程序在等待某些操作完成的时候继续执行其他的任务,而不是阻塞/等待这些操作完成。
在传统的同步编程里,代码是按顺序执行的,每个操作必须等待前一个操作完成,这种方式在处理I/O操作,网络请求或计算密集型任务的时候可能会导致程序的性能瓶颈。比如说:当你想要获取数据的时候,他会等待数据返回后执行,这期间CPU处在了空闲状态,浪费了资源。
-
future
std::future是一个模板类,用于表示一个异步操作的结果,它给你提供了一个机制,允许你在将来某个时间内获得操作的结果。
-
-
启动异步任务
-
std::future<int> f1 = std::async(func1, 20, 30);
-
std::future<int> f1 = std::async(std::launch::async, func1, 20, 30);//启动异步任务
-
-
-
-
-
- std::launch::async:必须在新线程中启动任务,当你希望任务在单线程中执行且不希望他阻塞当前线程的时候使用。
- std::launch::deferred:在调用get和wait时执行任务,如果任务没被查询过,那它就永远不会执行。
- std::launch::async | std::launch::deferred:系统自动选择执行方式。
-
#include <iostream> #include <thread> #include <future> int func1(int a, int b){ std::cout << "This is sub function : " << std::this_thread::get_id() << std::endl; return a + b; } int main(void){ std::future<int> f1 = std::async(std::launch::async, func1, 20, 30);//启动异步任务 std::cout << "This is main thread id : " << std::this_thread::get_id() << f1.get() << std::endl; f1 = std::async(std::launch::deferred, func1, 20, 30);//启动异步任务 std::cout << "This is main thread id : " << std::this_thread::get_id() << f1.get() << std::endl; return 0; }
-
-
异步操作结果
-
-
-
get:获取异步操作的结果。如果结果没准备好,就阻塞等待直到操作完成。一旦调用get它就会返回一个异步结果,并且std::future对象不会再与任何共享状态关联。get只允许调用一次,如果多次调用就会发生std::future_error异常
-
-
-
- wait:等待异步操作完成,但不获取结果。调用wait会阻塞当前的线程,直到异步操作完成。和get用完即失效不同,调用wait后std::future对象依旧有效。可以继续使用get获取结果
- wait_for:等待异步操作完成,但只会在一个时间段内等待,如果在等待时间段内完成就返回std::future_status::ready;如果超时了就返回std::future_status::timeout;如果没准备(尚未启动)就返回std::future_status::deferred。
- wait_until:和wait_for差不多,但它指定的是时间点。
-
-
-
-
#include <iostream> #include <thread> #include <future> int func1(int a, int b){ std::cout << "This is sub function" << std::endl; return a + b; } int func2(int a, int b){ return a * b; } int main(void){ std::future<int> f1 = std::async(func1, 20, 30);//启动异步任务 std::cout << f1.get() << std::endl;//获得结果 std::future<int> f2 = std::async(func2, 10, 30);//启动异步任务 f2.wait();//等待结果 std::cout << f2.get() << std::endl;//获得结果 return 0; }
-
-
-
设置异步任务:promise
promise是一个用于设置异步操作结果的机制。它允许你在一个线程中设置值或异常,然后在另一个线程中通过 std::future对象检索这些值或异常
-
-
#include <iostream> #include <future> #include <thread> #include <chrono> void async_task(std::promise<int> prom){ std::this_thread::sleep_for(std::chrono::seconds(2)); prom.set_value(15); } int main(void){ std::promise<int> prom; std::future<int> f = prom.get_future();//获得promise和关联的future std::thread thr(async_task, std::move(prom));//启动异步任务,将promise传给它 //在线程里等待异步任务完成并获得结果 std::cout << f.get() << std::endl; thr.join(); return 0; }
-
-
- get_future() :返回一个 std::future对象,该对象与std::promise对象共享状态。你可以通过这个std::future对象来检索异步操作的结果。
-
- set_value(T value):设置异步操作的结果。调用此方法后,与std::promise关联的 std::future对象将变为 fulfilled 状态,并且可以通过调用std::future::get()来检索结果。
-
- set_exception(std::exception_ptr p):设置异步操作中抛出的异常。调用此方法后,与std::promise关联的std::future对象将变为rejected状态,并且可以通过调用std::future::get() 来重新抛出异常。
-
包装可调用对象
package_task是一个模板类,用于将一个可调用的对象包装起来,以便于异步执行,它和std::future,std::thread密切相关,经常用于多线程。
-
-
工作流程如下:
- 创建:在创建package_task的时候,你要把可调用对象传给他,这个可调用对象会被package_task包装起来,成为一个异步任务。
- 获取future对象:使用get_future可以获取一个和package_task关联的std::future对象,后者可以异步获得任务结果。
- 执行:任务可以通过operator()或者在一个新线程里用std::thread来执行。这里的package_task请一定要用std::move传递。
- 获得结果:使用get()获得。
- 包装普通包
-
#include <iostream> #include <future> #include <thread> // 普通函数 int add(int a, int b) { return a + b; } int main() { // 1. 创建packaged_task,包装add函数 std::packaged_task<int(int, int)> task(add); // 2. 获取关联的future对象 std::future<int> result = task.get_future(); // 3. 在新线程中执行任务(必须用std::move转移所有权) std::thread t(std::move(task), 10, 20); // 4. 等待结果并获取 std::cout << "10 + 20 = " << result.get() << std::endl; t.join(); return 0; }
-
- 包装lambda表达式
-
#include <iostream> #include <future> #include <thread> #include <string> int main() { // 1. 创建packaged_task,包装lambda表达式 std::packaged_task<std::string(std::string)> task( [](std::string name) { return "Hello, " + name + "!"; } ); // 2. 获取future std::future<std::string> result = task.get_future(); // 3. 在新线程执行 std::thread t(std::move(task), "World"); // 4. 获取结果 std::cout << result.get() << std::endl; // 输出 "Hello, World!" t.join(); return 0; }
-
-
-
异步编程具有以下三个优点:
- 高效执行:并发多个任务,高效利用CPU的资源。
- 提高响应的速度:可以使程序在等待某个操作完成的同时继续响应其他的操作,提高用户体验。
- 简化I/O操作:异步编程特别适合处理I/O密集型操作,如读取文件,网络请求等
智能指针
智能指针是一种特殊的对象,它封装了原始指针,并通过重载运算符(如*和->)来模拟原始指针的行为。智能指针的主要目的是自动管理内存,确保动态分配的内存能够在适当的时候被释放,从而避免手动管理内存带来的风险。。
-
unique_ptr 独占指针
std::unique_ptr 是独占式智能指针,其管理的资源只能被一个 unique_ptr 拥有,不允许拷贝(避免多个指针同时管理同一资源),但允许移动(转移所有权)。
-
-
基本用法
-
#include <memory> #include <iostream> using namespace std; class MyClass { public: MyClass(int id) : id(id) { cout << "MyClass(" << id << ") 构造" << endl; } ~MyClass() { cout << "MyClass(" << id << ") 析构" << endl; } void print() { cout << "MyClass id: " << id << endl; } private: int id; }; int main() { // 1. 创建 unique_ptr(管理动态对象) unique_ptr<MyClass> ptr1(new MyClass(1)); // 直接初始化(不推荐,可能抛异常) auto ptr2 = make_unique<MyClass>(2); // 推荐:make_unique(更安全,避免内存泄漏) // 2. 访问资源(重载 * 和 ->) (*ptr1).print(); // 等价于 ptr1->print() ptr2->print(); // 3. 转移所有权(只能通过 move,原指针变为空) unique_ptr<MyClass> ptr3 = move(ptr1); // ptr1 失去所有权,变为空 if (ptr1 == nullptr) { cout << "ptr1 已为空" << endl; } ptr3->print(); // 仍可访问资源 // 4. 主动释放资源(reset()) ptr3.reset(); // 释放资源,ptr3 变为空 if (ptr3 == nullptr) { cout << "ptr3 已释放资源" << endl; } return 0; // ptr2 离开作用域,自动释放资源 }
-
-
注意事项
推荐使用make_unique:make_unique<T>(args)直接在内部构造对象。避免裸new可能导致的异常安全问题(如new后未传给智能指针前抛异常,导致内存泄漏)。unique_ptr的拷贝构造和拷贝赋值被删除(= delete),只能通过std::move转移所有权。
-
-
shared_ptr
多个 shared_ptr 可以共享同一个对象的所有权。通过引用计数机制来跟踪有多少个 shared_ptr 指向同一个对象。当最后一个 shared_ptr 被销毁时,对象才会被删除。
-
-
基本用法
-
#include <memory> #include <iostream> using namespace std; int main() { // 1. 创建 shared_ptr(推荐用 make_shared) shared_ptr<MyClass> ptr1 = make_shared<MyClass>(1); cout << "引用计数: " << ptr1.use_count() << endl; // 1 // 2. 拷贝 shared_ptr(引用计数 +1) shared_ptr<MyClass> ptr2 = ptr1; // 拷贝,计数变为 2 cout << "ptr1 计数: " << ptr1.use_count() << endl; // 2 cout << "ptr2 计数: " << ptr2.use_count() << endl; // 2 // 3. 转移部分指针的所有权(计数不变) shared_ptr<MyClass> ptr3 = move(ptr1); // ptr1 变为空,计数仍为 2(ptr2 和 ptr3 共享) cout << "ptr1 是否为空: " << (ptr1 == nullptr ? "是" : "否") << endl; // 是 cout << "ptr3 计数: " << ptr3.use_count() << endl; // 2 // 4. 释放部分指针(计数 -1) ptr2.reset(); // ptr2 释放,计数变为 1 cout << "ptr3 计数: " << ptr3.use_count() << endl; // 1 return 0; // ptr3 离开作用域,计数变为 0,资源释放 }
-
注意事项
- 推荐使用
make_shared:make_shared<T>(args)一次性分配对象和引用计数的内存,效率高于shared_ptr<T>(new T(args))(后者可能分配两次内存) - 循环引用问题:两个
shared_ptr互相引用会导致引用计数无法归零,造成内存泄漏(需用weak_ptr解决
-
-
weak_ptr
std::weak_ptr 是一种弱引用智能指针,它指向 shared_ptr 管理的资源,但不增加引用计数,主要用于解决 shared_ptr 的循环引用问题。
-
-
基本使用
-
struct B; struct A { std::shared_ptr<B> b_ptr; ~A() { std::cout << "A destroyed\n"; } }; struct B { std::shared_ptr<A> a_ptr; // 如果这里是shared_ptr,会产生循环引用 ~B() { std::cout << "B destroyed\n"; } }; // 循环引用导致内存泄漏: { auto a = std::make_shared<A>(); auto b = std::make_shared<B>(); a->b_ptr = b; // b的引用计数为2 b->a_ptr = a; // a的引用计数为2 } // 离开作用域,a和b的引用计数都减为1,永远不会变为0,对象无法被销毁! // 使用weak_ptr打破循环引用: struct B; struct A { std::shared_ptr<B> b_ptr; ~A() { std::cout << "A destroyed\n"; } }; struct B { std::weak_ptr<A> a_weak_ptr; // 使用weak_ptr而不是shared_ptr ~B() { std::cout << "B destroyed\n"; } }; { auto a = std::make_shared<A>(); auto b = std::make_shared<B>(); a->b_ptr = b; b->a_weak_ptr = a; // 这里不会增加a的引用计数!(a的计数仍为1) } // 离开作用域,a的计数变为0,先被销毁。然后b的计数变为0,也被销毁。 // 输出: A destroyed \n B destroyed
-
-
注意事项
- 不能直接访问资源:
weak_ptr没有重载*和->,必须通过lock()获取shared_ptr后才能访问资源(避免访问已释放的资源)。 - 检查资源有效性:
expired()方法可判断资源是否已释放(true表示已释放):-
weak_ptr<MyClass> wp; { auto sp = make_shared<MyClass>(1); wp = sp; // wp 指向 sp 管理的资源 cout << "资源是否有效: " << (!wp.expired() ? "是" : "否") << endl; // 是 } // sp 销毁,资源释放 cout << "资源是否有效: " << (!wp.expired() ? "是" : "否") << endl; // 否
-
- 不能直接访问资源:
-
-
常见问题
std::unique_ptr和std::shared_ptr的根本区别是什么?
根本区别在于所有权语义。unique_ptr表示独占所有权,一个对象只能由一个unique_ptr拥有,它不能被拷贝,只能移动。shared_ptr表示共享所有权,多个shared_ptr可以共享同一个对象的所有权,通过引用计数机制管理生命周期。-
为什么更推荐使用
make_shared而不是直接new来创建shared_ptr?
主要有两个原因:
性能:make_shared 通常只进行一次内存分配,同时为对象和控制块分配内存,而 new 需要两次分配。
异常安全:make_shared 避免了在函数参数求值过程中可能发生异常而导致的内存泄漏问题。
-
- 什么是循环引用?
weak_ptr是如何解决它的?
- 什么是循环引用?
循环引用是指两个或多个对象通过 shared_ptr 互相持有,导致它们的引用计数永远无法降为 0,从而无法被析构,产生内存泄漏。weak_ptr 通过提供一种不增加引用计数的“弱引用”来打破这种循环。它将循环中的某一个 shared_ptr 替换为 weak_ptr,这样就不会阻止所指向对象的销毁。
-
-
能否从一个
weak_ptr直接访问资源?如果不能,应该如何做?
不能。因为weak_ptr不拥有资源,它不知道资源是否已被释放。必须使用lock()方法,它会返回一个shared_ptr。如果资源还存在,这个shared_ptr是有效的(并且会增加引用计数);如果资源已被释放,则返回一个空的shared_ptr。必须检查lock()的返回值。 -
智能指针能否用于管理数组?
可以。
-
std::unique_ptr 完全支持数组:std::unique_ptr<T[]>,并且重载了 operator[]。使用 make_unique<T[]>(size) (C++17)。
std::shared_ptr 对数组的支持不如 unique_ptr 直接。在 C++17 及以后,可以用 std::make_shared<T[]>(size),但 API 不如 unique_ptr 完善。在 C++17 之前,通常需要为 shared_ptr 指定一个删除器,如 std::shared_ptr<int> sp(new int[10], std::default_delete<int[]>());。
-
- 智能指针的大小是多少?开销有多大?
std::unique_ptr:大小通常等于一个指针(例如 8 字节 on x64),开销极小,几乎就是封装了一个原生指针和一些编译期决定的逻辑。std::shared_ptr:大小通常是两个指针(例如 16 字节 on x64)。一个指向管理的对象,另一个指向包含引用计数等的控制块。其开销包括额外的内存分配(控制块)和维护引用计数的原子操作。std::weak_ptr:大小通常和shared_ptr一样(两个指针),它也需要访问控制块。
- 智能指针的大小是多少?开销有多大?
socket编程
-
概念
-
定义
-
Socket(套接字)是计算机网络中进程间通信的一种机制。Socket本质上是一个编程接口(API),封装了TCP/IP协议栈的复杂实现,使开发者能便捷地实现网络通信。
-
-
核心作用
-
-
-
- 实现跨进程/跨主机的数据传输
- 提供可靠的字节流传输(TCP)或无连接的数据报传输(UDP)
- 支持多种网络协议(TCP、UDP、ICMP等)
- 允许应用程序通过端口号区分不同服务
-
-
-
主要类型
-
| 类型 | 协议 | 特点 | 应用场景 |
|---|---|---|---|
| SOCK_STREAM | TCP | 面向连接、可靠传输、字节流、双工通信 | HTTP/HTTPS、文件传输、邮件 |
| SOCK_DGRAM | UDP | 无连接、不可靠、数据报、效率高 | 视频流、语音通话、DNS查询 |
| SOCK_RAW | 原始协议 | 直接访问IP层,用于协议开发 | 网络诊断工具、自定义协议 |
-
-
通信模型
-
Socket通信基于客户端-服务器(C/S)模型:
-
-
- 服务器:创建Socket → 绑定端口 → 监听连接 → 接受请求 → 数据交互
- 客户端:创建Socket → 连接服务器 → 数据交互
-
-
示例
-
TCP服务器端实现
-
#include <iostream> #include <cstring> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> const int PORT = 8080; const int BUFFER_SIZE = 1024; int main() { // 1. 创建TCP套接字 int server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd == -1) { std::cerr << "Socket creation failed: " << strerror(errno) << std::endl; return -1; } // 2. 设置地址重用,避免端口占用问题 int opt = 1; if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) { std::cerr << "Setsockopt failed: " << strerror(errno) << std::endl; close(server_fd); return -1; } // 3. 绑定地址和端口 sockaddr_in address; int addrlen = sizeof(address); address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; // 监听所有网络接口 address.sin_port = htons(PORT); // 转换为网络字节序 if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) { std::cerr << "Bind failed: " << strerror(errno) << std::endl; close(server_fd); return -1; } // 4. 监听连接请求(最大等待队列长度为3) if (listen(server_fd, 3) < 0) { std::cerr << "Listen failed: " << strerror(errno) << std::endl; close(server_fd); return -1; } std::cout << "Server listening on port " << PORT << std::endl; // 5. 接受客户端连接 int new_socket; sockaddr_in client_addr; int client_addr_len = sizeof(client_addr); if ((new_socket = accept(server_fd, (struct sockaddr *)&client_addr, (socklen_t*)&client_addr_len)) < 0) { std::cerr << "Accept failed: " << strerror(errno) << std::endl; close(server_fd); return -1; } char client_ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN); std::cout << "Accepted connection from " << client_ip << ":" << ntohs(client_addr.sin_port) << std::endl; // 6. 数据交互 char buffer[BUFFER_SIZE] = {0}; ssize_t valread = read(new_socket, buffer, BUFFER_SIZE); std::cout << "Received: " << buffer << std::endl; const char *response = "Hello from TCP server"; send(new_socket, response, strlen(response), 0); std::cout << "Response sent" << std::endl; // 7. 关闭连接 close(new_socket); close(server_fd); return 0; }
-
-
TCP客户端实现
-
#include <iostream> #include <cstring> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> const char* SERVER_IP = "127.0.0.1"; const int PORT = 8080; const int BUFFER_SIZE = 1024; int main() { // 1. 创建TCP套接字 int sock = socket(AF_INET, SOCK_STREAM, 0); if (sock == -1) { std::cerr << "Socket creation failed: " << strerror(errno) << std::endl; return -1; } // 2. 设置服务器地址 sockaddr_in serv_addr; serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(PORT); // 转换IP地址为二进制格式 if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) { std::cerr << "Invalid address or address not supported" << std::endl; close(sock); return -1; } // 3. 连接服务器 if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) { std::cerr << "Connection failed: " << strerror(errno) << std::endl; close(sock); return -1; } std::cout << "Connected to server " << SERVER_IP << ":" << PORT << std::endl; // 4. 发送数据 const char *message = "Hello from TCP client"; send(sock, message, strlen(message), 0); std::cout << "Message sent" << std::endl; // 5. 接收响应 char buffer[BUFFER_SIZE] = {0}; ssize_t valread = read(sock, buffer, BUFFER_SIZE); std::cout << "Received: " << buffer << std::endl; // 6. 关闭连接 close(sock); return 0; }
-
-
-
UDP服务器端实现
-
-
-
#include <iostream> #include <cstring> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> const int PORT = 8080; const int BUFFER_SIZE = 1024; int main() { // 1. 创建UDP套接字 int server_fd = socket(AF_INET, SOCK_DGRAM, 0); if (server_fd == -1) { std::cerr << "Socket creation failed: " << strerror(errno) << std::endl; return -1; } // 2. 设置地址重用 int opt = 1; if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) { std::cerr << "Setsockopt failed: " << strerror(errno) << std::endl; close(server_fd); return -1; } // 3. 绑定地址和端口 sockaddr_in address; int addrlen = sizeof(address); address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(PORT); if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) { std::cerr << "Bind failed: " << strerror(errno) << std::endl; close(server_fd); return -1; } std::cout << "UDP Server listening on port " << PORT << std::endl; // 4. 接收和发送数据 char buffer[BUFFER_SIZE] = {0}; sockaddr_in client_addr; int client_addr_len = sizeof(client_addr); while (true) { // 接收客户端消息 ssize_t len = recvfrom(server_fd, buffer, BUFFER_SIZE, 0, (struct sockaddr *)&client_addr, (socklen_t*)&client_addr_len); if (len < 0) { std::cerr << "Recvfrom failed: " << strerror(errno) << std::endl; continue; } buffer[len] = '\0'; // 添加字符串结束符 char client_ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN); std::cout << "Received from " << client_ip << ":" << ntohs(client_addr.sin_port) << ": " << buffer << std::endl; // 发送响应 const char *response = "Hello from UDP server"; sendto(server_fd, response, strlen(response), 0, (struct sockaddr *)&client_addr, client_addr_len); } // 注意:UDP服务器通常不会主动关闭,此处为示例完整性添加 close(server_fd); return 0; }
-
UDP客户端实现
-
#include <iostream> #include <cstring> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> const char* SERVER_IP = "127.0.0.1"; const int PORT = 8080; const int BUFFER_SIZE = 1024; int main() { // 1. 创建UDP套接字 int sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock == -1) { std::cerr << "Socket creation failed: " << strerror(errno) << std::endl; return -1; } // 2. 设置服务器地址 sockaddr_in serv_addr; serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(PORT); if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) { std::cerr << "Invalid address or address not supported" << std::endl; close(sock); return -1; } // 3. 发送数据 const char *message = "Hello from UDP client"; sendto(sock, message, strlen(message), 0, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); std::cout << "Message sent to " << SERVER_IP << ":" << PORT << std::endl; // 4. 接收响应 char buffer[BUFFER_SIZE] = {0}; sockaddr_in server_response_addr; int server_addr_len = sizeof(server_response_addr); ssize_t len = recvfrom(sock, buffer, BUFFER_SIZE, 0, (struct sockaddr *)&server_response_addr, (socklen_t*)&server_addr_len); if (len < 0) { std::cerr << "Recvfrom failed: " << strerror(errno) << std::endl; close(sock); return -1; } buffer[len] = '\0'; std::cout << "Received: " << buffer << std::endl; // 5. 关闭套接字 close(sock); return 0; }
-
-
隐式类型转换
在C++中,隐式类型转换(Implicit Type Conversion),也称为“类型提升”或“自动类型转换”,是编译器在不需要显式指示的情况下,将一种数据类型自动转换为另一种数据类型的过程。
-
基本类型转换
-
整型提升
整型提升是指在表达式中,所有比int类型小的整数类型(如char、short、bool等)都会被提升为int类型。
-
#include <iostream> using namespace std; int main() { char c = 'A'; // 'A'的ASCII值为65 int i = c + 1; // char类型c被提升为int类型 cout << "i = " << i << endl; // 输出: i = 66 return 0; }
-
-
算术转换
-
当表达式中包含多种不同的数据类型时,C++会自动将这些数据类型转换为一种统一的类型,以进行计算。
-
-
-
#include <iostream> using namespace std; int main() { int i = 5; double d = 2.5; double result = i * d; // int类型i被转换为double类型 cout << "result = " << result << endl; // 输出: result = 12.5 return 0; }
-
-
-
类类型的隐式转换
类类型的隐式转换通过构造函数或转换函数来实现。
-
-
通过单参数构造函数进行隐式转换
-
如果一个类具有单参数构造函数,编译器会将该构造函数视为隐式转换操作符。这意味着,当需要该类类型的对象时,可以通过传递构造函数所需的参数来自动创建对象。
-
-
-
#include <iostream> using namespace std; class Complex { public: double real, imag; // 单参数构造函数 Complex(double r) : real(r), imag(0) {} void display() const { cout << "实部: " << real << ", 虚部: " << imag << endl; } }; int main() { Complex c1 = 3.5; // 隐式调用 Complex(double) 构造函数 c1.display(); // 输出: 实部: 3.5, 虚部: 0 return 0; }
在这个例子中,
Complex类有一个单参数构造函数,该构造函数将double类型转换为Complex对象。c1对象通过隐式类型转换从double类型的3.5构造而来
-
-
通过转换函数进行隐式转换
-
C++类可以定义转换函数,将类对象转换为另一种类型。这些转换函数通常使用operator关键字定义
-
-
-
#include <iostream> using namespace std; class Complex { public: double real, imag; Complex(double r, double i) : real(r), imag(i) {} // 转换函数,将 Complex 转换为 double operator double() const { return real; } }; int main() { Complex c1(3.5, 4.5); double d = c1; // 隐式调用 Complex::operator double() cout << "d = " << d << endl; // 输出: d = 3.5 return 0; }
在这个例子中,
Complex类定义了一个转换函数operator double(),允许将Complex对象隐式转换为double类型。在main函数中,c1被转换为double类型,其值为复数的实部
-
-
网络IO模型
-
概述
网络IO,会涉及到两个系统对象,一个是用户空间调用IO的进程或线程,另一个是内核空间的内核系统,比如发生IO操作read时,它会经历两个阶段。
-
- 等待数据准备就绪
- 将数据从内核拷贝到进程或者线程
因为在以上两个阶段上各有不同的情况,所以出现了多种网络 IO 模型

-
分类
-
阻塞 IO(blocking IO)
-
-
![image]()
-
理解:
-
-
-
当用户进程调用了 read 这个系统调用,kernel 就开始了 IO 的第一个阶段:准备数据。对于 network io 来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的数据包)这个时候 kernel 就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当 kernel 一直等到数据准备好了,它就会将数据从 kernel 中拷贝到用户内存,然后 kernel 返回结果,用户进程才解除 block 的状态,重新运行起来。
第一次接触到的网络编程都是从 listen()、send()、recv() 等接口开始的,这些接口都是阻塞型的。使用这些接口可以很方便的构建服务器/客户机的模型。下面是一个简单地“一问一答”的服务器。
考虑使用“线程池”或“连接池”。“线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。“连接池”维持连接的缓存池,尽量重用已有的连接、减少创建和关闭连接的频率。这两种技术都可以很好的降低系统开销,都被广泛应用很多大型系统,如 websphere、tomcat 和各种数据库等。但是,“线程池”和“连接池”技术也只是在一定程度上缓解了频繁调用 IO 接口带来的资源占用。而且,所谓“池”始终有其上限,当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有池的时候效果好多少。所以使用“池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小。
对应上例中的所面临的可能同时出现的上千甚至上万次的客户端请求,“线程池”或“连接池”或许可以缓解部分压力,但是不能解决所有问题。总之,多线程模型可以方便高效的解决小规模的服务请求,但面对大规模的服务请求,多线程模型也会遇到瓶颈,可以用非阻塞接口来尝试解决这个问题。
-
-
-
优缺点
-
-
-
-
-
- 优点:开发简单,容易入门。在阻塞等待期间,用户线程挂起,在挂起期间不会占用 CPU 资源
- 缺点:一个线程维护一个 IO ,不适合大并发,在并发量大的时候需要创建大量的线程来维护网络连接,内存、线程开销非常大
-
-
-
-
非阻塞 IO(non-blocking IO)
-
![image]()
-
理解
-
-
当用户进程发出 read 操作时,如果 kernel 中的数据还没有准备好,那么它并不会 block 用户进程,而是立刻返回一个 error。从用户进程角度讲 ,它发起一个read 操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个 error时,它就知道数据还没有准备好,于是它可以再次发送 read 操作。一旦 kernel 中的数据准备好了,并且又再次收到了用户进程的 system call,那么它马上就将数据拷贝到了用户内存,然后返回,所以,在非阻塞式 IO 中,用户进程其实是需要不断的主动询问 kernel数据准备好了没有。
-
-
-
非阻塞 IO优缺点:
-
-
同步非阻塞 IO 优点:每次发起 IO 调用,在内核等待数据的过程中可以立即返回,用户线程不会阻塞,实时性好
同步非阻塞 IO 缺点:多个线程不断轮询内核是否有数据,占用大量 CPU 资源,效率不高。一般 Web 服务器不会采用此模式
-
-
多路复用 IO(IO multiplexing)
-
异步 IO(Asynchronous I/O)
-
信号驱动 IO(signal driven I/O, SIGIO)
-
五种网络 IO 模型对比
-




浙公网安备 33010602011771号