muduo C++网络库的学习笔记
文章目錄
- 10.3.C++鏈接linking
- 10.3.2 inline函數
- 10.3.3 模板
- 10.3.4 虛函數
- 10.4 工程項目中頭文件的使用規則
- 10.4.2 頭文件的使用規則
- 11.1 樸實的C++設計
- 11.2 程序庫的二進制兼容性
- 11.3 避免使用虛函數作為庫的接口
- 11.4 動態庫接口的推薦做法
- 11.5 std::function和std::bind取代虛函數
- 11.5.1基本用途
- 11.5.2 對程序庫的影響
- 11.5.5 對面向對象程序設計的影響
- 11.6 iostream的用途與局限
- 11.6.1 stdio格式化輸入輸出的缺點
- 11.6.2 iostream設計初衷
- 11.6.3 iostream與標準庫其他組件的交互
- 11.6.4 iostream在使用方面的缺點
- 11.6.5 iostream在設計方面的缺點
- 11.6.6 一個300行的memory buffer output stream
- 11.6.7 現實中的C++程序如果做文件IO
- 11.7 值語義與數據抽象
- 11.7.1 什么是值語義
- 11.7.2 值語義與生命期
- 11.7.3 值語義與標準庫
- 11.7.4 值語義與C++語言
- 11.7.5 什么是數據抽象
- 11.7.6 數據抽象所需的語言措施
- 11.7.7 數據抽象的例子
- 12 C++經驗談
- 12.1 用異或來交換變量是錯誤的
- 12.2 不要重載全局::operator new()
- 12.2.1 內存管理的基本要求
- 12.2.2 重載::operator new()的理由
- 12.2.3 ::operator new()的兩種重載方式
- 12.2.4 現實的開發環境
- 12.2.5 重載::operator new()的困境
- 12.2.6 解決辦法:替換malloc
- 12.2.7 為單獨的class重載::operator new()有問題嗎?
- 12.2.8 有必要自行定制內存分配器嗎?
- 12.3 帶符號整數的除法與余數
- 12.4 在單元測試中mock系統調用
- 12.4.1 系統函數的依賴注入
- 12.4.2 鏈接器墊片link seam
- 12.5 慎用匿名namespace
- 12.5.1 C語言的static關鍵字的兩種用法
- 12.5.2 C++語言的static關鍵字的四種用法
- 12.5.3 匿名namespace的不利之處
- 12.6 采用有利于版本管理的代碼格式
- 12.6.1 對diff友好的代碼格式
- 12.6.2 對grep友好的代碼風格
- 12.6.3 一切為了效率
- 12.7 再探std::string
- 12.7.1 直接拷貝eager copy
- 12.7.2 寫時賦值copy on write
- 12.7.3 短字符串優化
- 12.8 用STL algorithm輕松解決算法面試題
10.3.C++鏈接linking
10.3.2 inline函數
inline關鍵字在源文件中不是必須的,編譯器可以自動判斷;
inline在頭文件中還是需要的,可以防止鏈接器重復定義(multipe definition);
然后判斷一個C++可執行文件是debug build還是release build?即判斷:一個可執行文件是-O0編譯的還是-O2編譯的?
- eg:
my_course/course/12/01_math/01/run.sh
#!/bin/bash set -erm -rf build cmake -B build cmake --build buildmy_course/course/12/01_math/01/CMakeLists.txt
cmake_minimum_required(VERSION 3.15) project(hellocmake)add_executable(main main.cpp)- 測試:
debug
wangji@DESKTOP-QNG23J0:~/test/test/my_course/course/12/01_math/01$ g++ -Wall main.cpp wangji@DESKTOP-QNG23J0:~/test/test/my_course/course/12/01_math/01$ nm ./a.out |grep size|c++filt 000000000000142c W std::vector<int, std::allocator<int> >::size() constrelease
wangji@DESKTOP-QNG23J0:~/test/test/my_course/course/12/01_math/01$ g++ -Wall -O2 main.cpp wangji@DESKTOP-QNG23J0:~/test/test/my_course/course/12/01_math/01$ nm ./a.out |grep size|c++filt10.3.3 模板
- my_course/course/12/01_math/01/Request.h
- my_course/course/12/01_math/01/Printer.h
- my_course/course/12/01_math/01/Printer.cc
C++11 extern template特性
- 阻止隱式模板具體化,使得std::string和std::
10.3.4 虛函數
每個多態class都有一份vtable
- 定義或者繼承了虛函數的對象中會有一個隱含成員:指向vtable的指針vptr
- 在構造和析構對象的適合,編譯器生成的代碼會修改這個vptr成員,會用到vtable的定義(使用其他地址)
- eg:
- 測試:出現這種錯誤的根本原因是:程序中某個虛函數沒有定義。雖然報錯顯示:找不到虛函數表的定義
10.4 工程項目中頭文件的使用規則
頭文件通常分為:C語言系統頭文件,C++標準庫頭文件,C++第三方庫頭文件,本公司的基礎庫頭文件,本項目的頭文件
10.4.2 頭文件的使用規則
- eg:查找頭文件包含的小技巧。eg:一個程序只包含了<iostream>,但是卻能使用std:string,這個<string>是如何被引入的?
11.1 樸實的C++設計
11.2 程序庫的二進制兼容性
11.3 避免使用虛函數作為庫的接口
11.4 動態庫接口的推薦做法
1.暴露的接口里面不要有虛函數,要顯式聲明構造函數、析構函數,并且不能inline
- graphics.h
2.這部分代碼位于so中,隨著庫的升級一起變化
graphics.cc
3.增加新的功能,不必通過繼承的方式,就可以原地修改,容易保持二進制兼容性
graphics.h
graphics.cc
#include "graphics.h"class Graphics::Impl { public:void drawLine(int x0, int y0, int x1, int y1);void drawLine(double x0, double y0, double x1, double y1); };//編譯器可以看到Impl的定義,編譯通過 Graphics::Graphics() : impl(new Impl) {}//析構函數是空的,也必須放到這里定義。 Graphics::~Graphics() {}void Graphics::drawLine(int x0, int y0, int x1, int y1) {impl->drawLine(x0, y0, x1, y1); }void Graphics::drawLine(double x0, double y0, double x1, double y1) {impl->drawLine(x0, y0, x1, y1); }pimpl C語言的庫同樣可以用,eg:libevent2中的struct event_base
- 為什么非虛函數比虛函數更健壯?
因為虛函數是虛表指針+offset來決定虛函數的,而非虛函數是通過名字找到對應的函數的
11.5 std::function和std::bind取代虛函數
11.5.1基本用途
#include <functional> #include <string>class Foo { public:void methodA();void methodInt(int a);void methodString(std::string const &str); };class Bar { public:void methodB(); };void main() {// 無參,無返回值std::function<void()> f1;std::function<void(int)> f2;Foo foo;f1 = std::bind(&Foo::methodA, &foo);f1();Bar bar;f1 = std::bind(&Bar::methodB, std::ref(bar));f1();f1 = std::bind(&Foo ::methodInt, &foo, 42);f1();f1 = std::bind(&Foo::methodString, &foo, "hello");f1(); //調用foo.methodString("hello")// 要留意bind的實參(const char*)的生命期,她不應該短于f1的生命期// 必要時,可通過f1 = std::bind(&Foo::methodString, &foo, "hello”_s);來保證安全f2 = std::bind(&Foo::methodInt, &foo, std::placeholders::_1);f2(53); }11.5.2 對程序庫的影響
程序庫的設計不應該給使用者帶來不必要的耦合限制,而繼承是第二強的一種耦合,最強的耦合是友元;
常規OO設計:虛函數+派生覆寫;
基于std::function的設計:以std::function作為接口
- eg:線程庫
- eg:網絡庫
11.5.5 對面向對象程序設計的影響
用std::function代替虛函數,OO設計模式:行為模式、Factory Method創建型模式、Stratery模式、Command模式、Template Method等都可以不用了
- 以上述EchoService為例,EchoService需要一個函數原型滿足SendMessageCallback的東西來發送消息,而并不關心數據發送到網絡上還是Mock上
面向對象的接口與實現分離:
先寫一個AbstructDataSink interface,包含sendMessage()這個虛函數,然后派生出兩個class:NetDataSink和MockDataSink;
EchoService的構造函數應該以AbstructDataSink*為參數
基于對象的接口與實現分離:
直接傳入一個SendMessageCallback對象
什么時候使用繼承?
OO中的public繼承,即為了實現接口與實現分離,muduo只會在派生類的數目和功能完全確定的情況下使用;
- eg:IO multiplexing在不同操作系統下不同有不同的實現方法,數目固定,且功能完全確定。用多態來代替switch-case可以達到簡化代碼的目的;
11.6 iostream的用途與局限
11.6.1 stdio格式化輸入輸出的缺點
#include <stdio.h> #define __STDC_FORMAT_MACROS #include <inttypes.h> #include <iostream>int main() {//緩沖區溢出危險:輸入的name沒有指定大小// char name[80];// scanf("%s\n", name);// // 安全做法:// constexpr int max_name = 80;// char myname[max_name];// char fmt[10];// sprintf(fmt, "%%%%ds", max_name - 1);// scanf(fmt, name);// int64_t在32bit和64bit平臺上是不同的類型int64_t x = 100;printf("%" PRIo64 "\n", x); //輸出的是8進制printf("%06" PRIo64 "\n", x);std::cout << std::dec << x << std::endl;// 等價于printf("%""ld""\n",x); // 64bit OSprintf("%""lld""\n",x); // 32bit OS// 等價于// printf("%ld\n", x); // 64bit OS// printf("%lld", x); // 32bit OSstd::size_t i = 1;printf("%zd\n", i);return 0; }11.6.2 iostream設計初衷
#include <ostream> #include <iostream>class Date { public:Date(int year, int month, int day) : year_(year), month_(month), day_(day) {}void writeTo(std::ostream &out) const{out << year_ << "-" << month_ << "-" << day_;}friend std::ostream &operator<<(std::ostream &out, const Date &date){out << date.year_ << date.month_ << date.day_ << std::endl;}private:int year_;int month_;int day_; };std::ostream &operator<<(std::ostream &out, const Date &date) {date.writeTo(out);return out; }int main() {Date date{2022, 11, 3};std::cout << date << std::endl;return 0; }11.6.3 iostream與標準庫其他組件的交互
11.6.4 iostream在使用方面的缺點
#include <ostream> #include <iostream> #include <iomanip> //操作子格式化需要該頭文件 class Date { public:Date(int year, int month, int day) : year_(year), month_(month), day_(day) {}// 輸出2022-11-03// iostream輸出格式繁瑣void writeTo(std::ostream &out) const{out << year_ << "-"<< std::setw(2) << std::setfill('0') << month_ << "-"<< std::setw(2) << std::setfill('0') << day_;}void writeTo(std::ostream &out){out << year_ << "-" << month_ << "-" << day_;char buf[32];snprintf(buf, sizeof(buf), "%d-%02d-%02d", year_, month_, day_);out << buf;}private:int year_;int month_;int day_; };std::ostream &operator<<(std::ostream &out, const Date &date) {date.writeTo(out);return out; }int main() {Date date{2022, 11, 3};std::cout << date << std::endl;const char *name = "wangji";int age = 30;// 注:數字表示替換字符串中要替換的位置,若一個字符串要替換兩個int類型,在替換位置分別 寫%1$d和%2$d.printf("My name is %1$s, I am %2$d years old.\n", name, age);std::cout << "My name is " << name << ", I am " << age << " years old." << std::endl;// 將整數轉為十六進制,會持續影響ostream的狀態int x = 8888;std::cout << std::hex << x << std::endl; // 0x22b8std::cout << 123 << std::endl; // 0x7bdouble d = 123.45;// C風格的格式輸出都不會影響,很舒服// 一般情況考慮用snprintf()打印到棧上緩沖,再用ostream輸出// stdio函數時線程安全的,iostream不是線程安全的:cout.operator<<(a).operator<<(b)兩次調用期間可能會被打斷,造成輸出不連續// fprintf(stdout, "%s %d", a, b);打印的內容不會受其他線程影響printf("\n%8.3f\n", d);std::cout << d << std::endl;using namespace std;cout << d << endl;// setprecision會影響后續輸出的精度,setw則不會cout << setw(8) << fixed << setprecision(3) << d << endl;cout << d << endl;return 0; }總結
輸入:istream不適合輸入帶格式的數據;推薦做法:std::getline讀入一行數據到std::string,然后用正則表達式判斷正誤,并作分組,最后用strtod,strtol,或者std::string相關函數做類型轉換
輸出:ostream僅做簡單的無格式輸出
不要用ostream來寫log
ostringstream會動態分配內存,不適合性能較高的場合
文件IO,如果用作文本文件的輸入或者輸出,fstream也有上述的缺點
iostream在某些場合比stdio快,某些又慢,對于高性能而言,需自己實現字符串轉換
11.6.5 iostream在設計方面的缺點
面向對象中的public繼承需要滿足Liskov替代原則,就是OO繼承強調的是可替代性,派生類的對象可以替換基類對象;
只有真正的is-a關系采用public繼承,其他均以組合替代;
11.6.6 一個300行的memory buffer output stream
LogStream接口定義見muduo
其他程序如何使用LogStream作為輸出呢?
#include <ostream> #include <iostream> #include <iomanip> //操作子格式化需要該頭文件 class Date { public:Date(int year, int month, int day) : year_(year), month_(month), day_(day) {}// 輸出2022-11-03// iostream輸出格式繁瑣void writeTo(std::ostream &out) const{out << year_ << "-"<< std::setw(2) << std::setfill('0') << month_ << "-"<< std::setw(2) << std::setfill('0') << day_;}template <typename OStream>void writeTo(OStream &out){out << year_ << "-" << month_ << "-" << day_;char buf[32];snprintf(buf, sizeof(buf), "%d-%02d-%02d", year_, month_, day_);out << buf;}private:int year_;int month_;int day_; };template <typename OStream> OStream& operator<<(OStream& out, const Date &date) {date.writeTo(out);return out; }11.6.7 現實中的C++程序如果做文件IO
在C++項目中,自己寫個File class,把項目用到的文件IO功能簡單封裝一下,通常就能滿足需要。
11.7 值語義與數據抽象
11.7.1 什么是值語義
指的是對象的拷貝與原對象無關,就像拷貝int一樣,eg:
- C++內置類型bool、int、double、
- 標準庫:pair<>、vector<>、map<>、string
對象語義:對象拷貝是禁止的,eg:
- muduo的Thread的拷貝是禁止的,因為Thread代表一個線程,拷貝一個Thread對象并不能讓系統增加一個一模一樣的線程
11.7.2 值語義與生命期
- eg:若Parent擁有Child,Child的生命期由其Parent控制
采用std::shared_ptr寫法:
#include <memory> class Parent; using ParentPtr = std::shared_ptr<Parent>;class Child { public:explicit Child(const ParentPtr &myParent) : myParent_(myParent) {}private:std::weak_ptr<Parent> myParent_; };using ChildPtr = std::shared_ptr<Child>; class Parent : public std::enable_shared_from_this<Parent> { public:Parent() {}void addChild(){myChild.reset(new Child(shared_from_this()));}private:ChildPtr myChild; };int main() {ParentPtr p(new Parent());p->addChild(); }- eg:Child持有mom和dad的parents,一個parent持有一個或者多個child;mom知道她的配偶spouse,dad知道她的配偶spouse
11.7.3 值語義與標準庫
C++編譯器會為class默認提供copy constructor和assignment operator,所以在寫一個C++ class的時候,讓他默認繼承boost::noncopyable,幾乎總是正確的。
11.7.4 值語義與C++語言
C++的設計初衷是讓用戶定義的類型class能像內置類型int一樣工作。
11.7.5 什么是數據抽象
數據抽象data abstraction是與面向對象object oriented并列的一種編程范式。
11.7.6 數據抽象所需的語言措施
11.7.7 數據抽象的例子
12 C++經驗談
12.1 用異或來交換變量是錯誤的
- eg:將“12345”反轉為“54321”
12.2 不要重載全局::operator new()
12.2.1 內存管理的基本要求
既不重復delete,也不漏掉delete
12.2.2 重載::operator new()的理由
12.2.3 ::operator new()的兩種重載方式
//方式1 #include <new> void *operator new(size_t size); void operator delete(void *p);//方式2 void *operator new(size_t size, const char *file, int line); void operator delete(void *p, const char *file, int line);Foo *p = new (__FILE, __LINE__) Foo;12.2.4 現實的開發環境
12.2.5 重載::operator new()的困境
12.2.6 解決辦法:替換malloc
12.2.7 為單獨的class重載::operator new()有問題嗎?
12.2.8 有必要自行定制內存分配器嗎?
重載::operator new()或許在某些臨時的場合能應急,但是不應該作為一種策略來使用。
如果需要,可以從malloc層面入手,徹底替換內存分配器。
12.3 帶符號整數的除法與余數
12.4 在單元測試中mock系統調用
12.4.1 系統函數的依賴注入
方法1:
采用傳統的面向對象的手法,借助運行期的遲綁定實現注入與替換。自己寫一個System interface,把程序里面的open,close,write等函數用虛函數封裝一層。
方法2:采用編譯器或者鏈接期的遲綁定,因此程序只會用到一個implementation object,為此虛函數調用的代價有些不值得。(與系統調用相比,虛函數這點開銷可以忽略不計)。
- 在一個system namespace頭文件,在其中聲明read()和write()等普通函數,然后在.cc文件里轉發給對應系統函數::read()和::write()。
- 無需用到虛函數,代碼寫起來也比較簡潔,只用前綴sockets::即可。
12.4.2 鏈接器墊片link seam
- 一開始沒有考慮單元測試,如何注入mock系統調用?
12.5 慎用匿名namespace
一些小的helper函數會放到匿名namespace中
12.5.1 C語言的static關鍵字的兩種用法
函數內的靜態變量
- 使用靜態變量的函數是不可重入的,也不是線程安全的
函數體之外修飾變量或者函數
- 僅對本文件可見
- 匿名namespace可以達到相同的效果
12.5.2 C++語言的static關鍵字的四種用法
除了以上C語言的兩種用法,有定義了兩種新用法:
- 修飾class的數據成員
- 修飾class的成員函數
12.5.3 匿名namespace的不利之處
anon.cc
namespace {void foo() {} } // namespaceint main() {foo(); }anonlib.cc
namespace {void foo() {} } // namespace上述兩個文件都定義了匿名空間中的foo()函數,那么gdb則無法區分這兩個函數
- 匿名namespace中的函數是weak text,鏈接的時候若發生重名,linker不會報錯
解決辦法:
- 調試時使用文件名:行號
- 使用具體的namespace名字,Boost中就常用boost::detail來存放不應該暴露給用戶,但又不得不放到頭文件里面的函數或者class
12.6 采用有利于版本管理的代碼格式
C和C++代碼中的換行符都被編譯器(預處理之后)當做white space來對待。
- eg:等價寫法:
12.6.1 對diff友好的代碼格式
不適用/**/來注釋多行代碼
12.6.2 對grep友好的代碼風格
12.6.3 一切為了效率
12.7 再探std::string
12.7.1 直接拷貝eager copy
12.7.2 寫時賦值copy on write
12.7.3 短字符串優化
12.8 用STL algorithm輕松解決算法面試題
生成N個不同元素的全排列
#include <algorithm> #include <vector> #include <iostream> #include <iterator>using namespace std; int main() {std::vector<int> vec = {1, 2, 3, 4};int count = 0;do{std::cout << ++count << ": ";std::copy(vec.begin(), vec.end(), std::ostream_iterator<int>(std::cout, ","));cout << endl;} while (next_permutation(vec.begin(), vec.end()));return 0; }生成從N個元素中取出M個的所有組合
- 對序列{1,1,1,0,0,0,0}做全排列。對于每個排列,輸出數字1對應的位置上的元素。
- eg:
用unique()去除連續的重復空白
- 給定一個字符串,要求原地把相鄰的多個空格替換為一個,例如a__b,輸出為a_b。
- 所有針對于STL algorithm都只能調整區間內元素的順序,不能真正刪除容器內的元素
用一臺4GiB內存的機器對磁盤上的單個100GB文件排序
- 假設要歸并從小到大排序好的32個文件
- 標準思路是:先分塊排序,然后多路歸并成輸出文件。多路歸并使用heap排序。
- 用{make,push,pop}_heap實現多路歸并
總結
以上是生活随笔為你收集整理的muduo C++网络库的学习笔记的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 单片机介绍与内部结构
- 下一篇: s3c2440移植MQTT