十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C++学习笔记(聊天服务器)

C++学习笔记(聊天服务器) 虚函数虚函数 给基类留一个 “接口空位”让子类可以替换掉这个函数的实现调用的时候自动跑子类的代码而不是基类的。这个行为叫多态。如果不加virtual调用哪个函数看指针 / 引用的类型编译阶段就定死。如果加virtual调用哪个函数看对象实际是什么类型运行时才决定。什么时候要用虚函数什么时候要重写✅场景 1基类定义接口子类提供不同实现最常见就是 boost::system::error_category 的场景error_category是抽象基类它只定规则必须实现name()返回类别名字必须实现message(int ev)把错误码转文字。系统自带系统错误类别、通用 POSIX 错误类别。你自己要新增业务错误就继承这个基类重写 name ()、message ()。框架只操作基类error_category不需要关心是系统类别还是你自定义业务类别自动调用你写的 name/message。框架 / 库预留抽象接口用户继承重写是虚函数最大用途。asio、Qt、很多库都这么设计。虚函数就是用来解决“同一个调用不同对象干不一样的活” 这件事。举个生活例子有个工具叫【遥控器】遥控器上有个按键按下按键→执行动作。遥控器本身不知道你接的是什么设备可能是电视机、空调、投影仪。接电视按按键 → 切换频道接空调按按键 → 调温度接投影仪按按键 → 切换输入源遥控器只是规定必须有这个按键但具体按下去干什么由你插上去的设备自己决定。 遥控器 基类父类 按键 虚函数 电视、空调、投影仪 子类派生类#includeiostreamusingnamespacestd;classRemote{public:virtualvoidpress(){//虚函数这里只定义按键这个行为不写死逻辑cout切换电视频道\n;}};classAirCondition:publicRemote{public:voidpress()override{cout调节空调温度\n;}};intmain(){AirCondition ac;Remote*pac;p-press();return0;}只有基类加上virtual其他子类才能重写override纯虚函数virtual void press() 0;遥控器干脆自己不实现只规定“继承我的设备你必须实现 press 按键”。遥控器本身不能拿来直接用只能当接口。就像 boost::system::error_category库只规定接口具体逻辑交给你写的子类实现。假设不用虚函数想要实现 “电视按按键换台空调按按键调温”你只能这么干if(对象是电视){tv.press();}elseif(对象是空调){ac.press();}elseif(对象是投影仪){pro.press();}设备越多代码越乱库的作者写代码的时候根本不知道未来用户会新增什么设备没法提前写 if 判断。static_castC静态强制类型转换编译期转换安全的类型转换。final写在类名后面禁止别的类再继承这个类。constexpr 构造函数只要传入的是编译期常量对象可以编译期直接算出来不占用运行时开销。因为 file /function 都是字符串字面量指针编译期常量line 是数字适合 constexpr。templateclassT// 模板声明T是类型参数比如int、std::stringusingresultboost::system::resultT;template告诉编译器这是一个模板T是占位类型。以后你写 result那么这里 T 就替换成int写resultstd::stringT 替换成std::string。RAII资源获取即初始化Resource Acquisition Is Initialization把资源的申请放在类的构造函数把资源释放放在类的析构函数。对象生命周期结束自动释放资源不用你手动写释放代码。资源是什么堆内存、文件句柄、socket 套接字、锁、数据库连接。痛点如果手动申请资源很容易忘记释放或者中途 return / 异常跳出漏掉释放 → 内存泄漏、句柄泄漏、死锁。RAII 靠 C 的规则局部对象离开作用域析构函数一定会自动执行。#includecstdioclassFileGuard{private:FILE*m_fp;public://构造函数获取资源打开文件explicitFileGuard(constchar*path,constchar*mode):m_fp(fopen(path,mode)){}//析构函数释放资源自动调用~FileGuard(){if(m_fp!nullptr){fclose(m_fp);}}//对外提供访问FILE*get(){returnm_fp;}//禁止拷贝文件不能被复制FileGuard(constFileGuard)delete;FileGuardoperator(constFileGuard)delete;};实际调用voidgood_func(){FileGuardfg(test.txt,r);//构造打开文件RAII获取资源//不管这里 return;//不管这里发生异常抛出//只要离开这个大括号fg局部对象销毁自动调用 ~FileGuard()执行fcloseFILE*fpfg.get();if(fp){//读文件}}// ✅出大括号fg析构自动关闭文件一定释放不会漏智能指针智能指针 用 RAII 封装的普通裸指针。普通裸指针int* p new int;需要你手动delete p;忘了就内存泄漏抛异常、提前 return 也容易漏掉释放。智能指针利用RAII对象生命周期结束离开大括号析构函数自动帮你执行delete不用手动释放堆内存。std::unique_ptr 独占所有权特点独占所有权同一时间只能有一个unique_ptr持有这块堆内存。不允许拷贝只能std::move转移所有权。开销几乎为 0性能接近裸指针。出作用域析构自动 delete。std::shared_ptr 共享所有权引用计数共享所有权多个shared_ptr可以指向同一块堆内存。内部维护一个引用计数器记录现在有多少个 shared_ptr 在引用这块内存。拷贝一次计数 1一个 shared_ptr 销毁计数‑1计数降到 0才执行 delete 释放内存。有额外开销要维护引用计数。std::weak_ptr 配合 shared_ptr解决循环引用不增加 shared_ptr 的引用计数只是 “旁观” 这块内存不拥有所有权。只能从std::shared_ptr构造出来。不能直接*解引用要调用.lock()得到一个临时shared_ptr。.expired() 判断对象是否已经被释放。#includememory#includeiostreamstructB;structA{std::shared_ptrBb_ptr;~A(){std::coutA 析构\n;}};structB{// std::shared_ptrA a_ptr; // ❌循环引用内存泄漏std::weak_ptrAa_weak;// ✅用weak_ptr不增加计数~B(){std::coutB 析构\n;}};voiddemo_weak(){autoastd::make_sharedA();autobstd::make_sharedB();a-b_ptrb;b-a_weaka;// weak不增加计数// 尝试获取对象if(autotempb-a_weak.lock()){// temp是shared_ptr对象还活着可以使用}}// 出作用域a、b计数归零A B都会正常析构释放定义C函数的格式【A】 返回类型 函数名(参数) 【B】 { … }普通函数A 区前面static只在当前.cpp文件内部能用别的文件看不见inline建议编译器把函数直接展开constexpr可以编译期算[[nodiscard]]调用不拿返回值就警告B 区括号后面普通函数只有这一个noexcept承诺这个函数不会抛异常成员函数A 区函数前面static静态成员函数没有thisvirtual虚函数用于多态inline、constexpr同上B 区括号后面重点普通函数不能写这些const这个函数不会修改对象的成员变量只能左值对象调用只能右值对象调用noexcept不抛异常override重写父类虚函数写错编译报错final不让子类再重写这个函数const / / / override / final 只能类成员函数写普通函数后面不能写static成员函数不能写 const没有 this 指针public不是写在函数上是类里面单独一行guard守卫structguard_deleter{voidoperator()(async_mutex*self)constnoexcept{self-unlock();}};operator()是什么C 语法重载圆括号运算符。写了它以后这个结构体的对象可以像函数一样调用。这个结构体本身不存任何成员变量是空结构体只存一段行为拿到锁指针就解锁。C 的类里面默认自带 6 个特殊函数叫六大默认函数默认构造函数拷贝构造函数移动构造函数C11 新增拷贝赋值运算符移动赋值运算符C11 新增析构函数public:explicitasync_mutex(boost::asio::any_io_executor ex):chan_(std::move(ex)){}// 用户自定义构造async_mutex(constasync_mutex)delete;// 拷贝构造删掉禁用async_mutex(async_mutex)default;// 移动构造让编译器生成默认async_mutexoperator(constasync_mutex)delete;// 拷贝赋值删掉禁用async_mutexoperator(async_mutex)default;// 移动赋值编译器生成默认~async_mutex()default;// 析构编译器生成默认boollocked()constnoexcept{returnlocked_;}// 普通业务函数查询锁状态函数 作用 为什么要这么设置async_mutexexplicit async_mutex(ex) 构造函数创建锁对象 创建锁必须传入执行器 executor不能随便造空锁explicit 防止偷偷转换async_mutex(const async_mutex)delete 拷贝构造用旧对象复制出新对象 ❌delete禁止复制锁。锁和等待队列不能复制复制就会出现两把锁逻辑崩坏async_mutex(async_mutex)default 移动构造把对象整体搬家旧对象作废 ✅default允许搬家。协程中锁对象需要移动让编译器帮我写搬家逻辑async_mutex operator(const async_mutex)delete 拷贝赋值ab复制 ❌禁止m2m1复制锁和上面理由一样async_mutex operator(async_mutex)default 移动赋值astd::move(b)搬家 ✅允许搬家赋值编译器生成默认代码~async_mutex()default 析构对象死亡时清理 不用自己手写编译器自动清理成员locked_、chan_bool locked() const noexcept 普通成员查询锁状态 给外面用看看锁现在有没有被占delete告诉编译器这个函数不要生成任何人调用就编译报错。用来禁止某些危险操作。default告诉编译器这个函数我自己不写按编译器默认逻辑帮我生成一份。impl的作用把类所有真正的成员变量、内部数据全部挪到 cpp 文件里藏起来头文件只留一个unique_ptrimpl impl_指针。不用 impl 会怎么样所有成员变量ws、write_mtx_、buffer、reading 标记全部写在头文件 hpp 里。头文件必须#include一大堆 boost/beast/asio 头文件。只要改一个内部成员所有包含这个头的代码全部重新编译编译很慢。头文件暴露全部内部细节。类对象本身很大。无impl的情况classWebSocket{private:websocket_type ws;async_mutex write_mtx_;flat_buffer read_buffer;boolreading{false};public:error_codeaccept(yield_context yield);error_codewrite(std::string_view buff,yield_context yield);};只要我修改reading或者增加一个成员所有#include websocket.hpp的文件全部重编译。使用impl的情况头文件hpp包含以下内容structimpl;classWebSocket{std::unique_ptrimplimpl_;//就一个指针public:error_codeaccept(yield_context yield);error_codewrite(std::string_view buff,yield_context yield);};.cpp文件structWebSocket::impl{websocket_type ws;async_mutex write_mtx_;flat_buffer read_buffer;boolreading{false};};修改 impl 里面任意成员只编译 cpp 本身其他文件不用重新编译。头文件完全不动。C中this指针的用法总结先建立直觉this 就是我假设你写了一个 Car 类classCar{intspeed_;public:voidsetSpeed(ints){speed_s;}};当你这样调用时Car carA;Car carB;carA.setSpeed(10);// 这里在改谁的速度carB.setSpeed(20);// 这里呢setSpeed 这个函数代码只有一份但它怎么知道改的是 carA 的 speed_ 还是 carB 的 speed_答案就是编译器偷偷给每个成员函数传了一个隐藏参数 this指向正在被操作的那个对象。carA.setSpeed(10);// 实际相当于 setSpeed(carA, 10)this carAcarB.setSpeed(20);// 实际相当于 setSpeed(carB, 20)this carB所以 this 本质上就是一个指针指向当前函数所属的那个对象。2. this 的三个核心用法配项目代码用法一区分成员变量和同名的参数/局部变量这是最常见、最重要的用法。当参数名和成员变量同名时用this-明确我要的是成员变量。项目里MainWindow构造函数就是这样参数名和成员变量同名// src/gui/MainWindow.cpp:42-46MainWindow::MainWindow(constQStringconfigPath,constQStringdataPath,QWidget*parent):QMainWindow(parent),configPath_(configPath)// 初始化列表把参数 configPath 赋给成员 configPath_,dataPath_(dataPath){这里成员变量叫configPath_带下划线参数叫configPath不带下划线故意用下划线区分开就是为了避免混淆。如果不加下划线就得靠this-来区分// 假设成员变量也叫 configPath就必须这样写this-configPathconfigPath;// 左边是成员右边是参数你的项目里选择成员变量加下划线configPath_的风格这样就不用到处写this-代码更清爽。两种风格都常见。用法二把我自己传给别的函数Qt 里最常用Qt 里到处都在把 this 当参数传出去主要是两处① 传 parent —— 建立对象树// src/gui/MainWindow.cpp:104view2D_newView2D(this);// 告诉 View2D你的父对象是我 MainWindow这里的this是把我自己MainWindow作为 parent 传进去。作用是MainWindow 销毁时会自动连带销毁view2D_对象树机制见《QT技术知识总结》第 14 节。② 传 receiver —— 指定信号槽的接收者// src/gui/MainWindow.cpp:329connect(dataLoader_,DataLoader::vehiclePositionReady,this,MainWindow::onVehicleDataUpdated);// 信号发出者 ↑ this 接收者是我 MainWindow这里的this是告诉connect“信号的接收者是我MainWindow”这样onVehicleDataUpdated才会在this这个对象上被调用。用法三Lambda 里捕获 this在 lambda 表达式中想访问当前对象的成员成员变量、成员函数就要把this捕获进去// src/gui/MainWindow.cpp:362-367connect(dataLoader_,DataLoader::totalFramesLoaded,this,[this](inttotal){intmaxFrame(total0)?total-1:0;timeSlider_-setMaximum(maxFrame);// ← 这里的 timeSlider_ 其实是 this-timeSlider_timeLabel_-setText(QString(0 / %1).arg(maxFrame));});[this]是干嘛的Lambda 默认不能访问外部对象的成员。[this]是把 this 指针抓进来这样 lambda 里面才能用timeSlider_、timeLabel_这些成员变量否则编译器会报错未定义的标识符。3. 一个你可能没意识到的事你一直在隐形地用它在成员函数里访问成员变量/成员函数时this-是可以省略的编译器自动帮你补上。比如这段代码// src/communication/Socket.cpp:43-46if(QThread::currentThread()!this-thread()){LOG_ERROR(架构违规尝试跨线程启动 TCP Server);returnfalse;}这里this-thread()的this-其实可以省略写成thread()也完全一样。但这里作者故意写出来是为了强调我要比较的是’我这个对象所属的线程’。而在项目里更常见的是省略形式比如// src/gui/MainWindow.cpp:47setWindowTitle(ADASim - 自动驾驶算法仿真平台 v1.0.0);这行代码表面看没有this但它实际是this-setWindowTitle(...)——在成员函数里调用另一个成员函数编译器自动补上了this-。总结用法什么时候用项目里的例子this-成员区分成员变量和同名参数构造函数虽然项目用下划线风格避开了new Xxx(this)把当前对象作为父对象传出去new View2D(this)connect(..., this, ...)指定信号槽的接收者connect(dataLoader_, ..., this, ...)[this]让 lambda 能访问成员[this](int total){ ... }省略this-成员函数里访问成员最常用setWindowTitle(...)一句话记住this就是当前这个对象自己的地址它存在的意义是让你能在成员函数里指代我自己——不管是访问自己的成员还是把自己传给别人。
返回列表