大-杂-烩
計算機網絡
在利用網絡socket進行通信時,選擇tcp協議進行交互,利用send函數進行數據發送,我們都知道send函數的返回結果為0時代表對端關閉了連接,那么如果發送0字節數據時是不是也會返回0?這樣會不會有歧義呢?
答:調用send函數發送0字節數據時,send(,0,) 第三個參數代表發送的字節數,本端的操作系統的協議棧并不會把數據發送出去(這里可以通過tcpdump截包佐證只有tcp建鏈的包,后續沒有任何數據包),但是返回的結果依然是0,所以這里確實是會有一點歧義。對于接收端而言,并不會有任何響應,另外如果是阻塞式的socket,那么接收端的緩沖區并沒有數據收到,所以程序會阻塞在recv處(可以通過gdb運行程序查看)。另外發送0字節的數據是沒有任何意義的,所以盡量避免。
2.陳碩關于網絡編程時提出的幾個問題
程序在本機測試正常,放到網絡運行上就經常出現數據收不全的情況?
TCP 協議真的有所謂的“粘包問題”嗎?該如何設計打包拆包的協議?又該如何編碼實現才不會掉到陷阱里?
帶外數據(OOB)、信號驅動IO這些高級特性到底有沒有用?
網絡協議格式該怎么設計?發送 C struct 會有對齊方面的問題嗎?對方不用 C/C++ 怎么通信? 將來服務端軟件升級,需要在協議中增加一個字段,現有的客戶端就必須強制升級?
要處理幾千上萬的并發連接,似乎書上講的傳統 fork() 模型應付不過來,該用哪種并發模型呢? 試試 select、poll、epoll 這種 IO 復用模型吧,又感覺非阻塞IO陷阱重重,怎么程序的 CPU 使用率一直是100%?
要不改用現成的 libevent 網絡庫吧,怎么查詢一下數據庫就把其他連接上的請求給耽誤了?
再用個線程池吧。萬一發回響應的時候對方已經斷開連接了怎么辦?會不會串話?
讀過《UNIX 環境高級編程》,想用多線程來發揮多核 CPU 的效率, 但對程序該用哪種多線程模型感到一頭霧水? 有沒有值得推薦的適用面廣的多線程 IO 模型? 互斥器、條件變量、讀寫鎖、信號量這些底層同步原語哪些該用哪些不該用? 有沒有更高級的同步設施能簡化開發? 《UNIX 網絡編程(第二卷)》介紹的那些琳瑯滿目的IPC機制到底用哪個才能兼顧開發效率與可伸縮性?
網絡編程和多線程編程的基礎打得差不多,開始實際做項目了,更多問題撲面而來:
網上聽人說服務端開發要做到 7x24 運行,為了防止內存碎片連動態內存分配都不能用, 那豈不是連 C++ STL 也一并禁用了?硬件的可靠性高到值得去這么做嗎?
傳聞服務端開發主要通過日志來查錯,那么日志里該寫些什么?日志是寫給誰看的?怎樣寫日志才不會影響性能?
分布式系統跟單機多進程到底有什么本質區別?心跳協議為什么是必須的,該如何實現?
C++ 的大型工程該如何管理?庫的接口如何設計才能保證升級的時候不破壞二進制兼容性?
這本《Linux 多線程服務端編程》中,作者憑借多年的工程實踐經驗試圖解答以上疑問。當然,內容還遠不止這些……
—摘自陳碩博客
操作系統
數據結構
GDB調試
1.編譯時增加編譯選項 -fstack -protector 可以在檢測到緩沖區溢出時,立刻終止正在執行的程序,并提示其檢測到緩沖區存在的溢出問題。
C++語法
1.多繼承時,子類的構造函數應該怎么寫?
如果父類里面有顯示的聲明構造函數,一般來說子類不需要再顯示的聲明構造函數,會生成一個默認構造函數。當多繼承時可能會存在多義性,其實也很好理解,比如C繼承自類A和類B,而類A和類B都有相同的構造函數(參數列表一樣),那么這個時候如果不在類C里面顯示的聲明一個新的構造函數,則編譯器會提示“C::C”不明確,即系統不知道去默認調用A還是B的構造函數。
2.類中的靜態成員到底是什么屬性?
常常會見到,類里面有一些靜態成員的聲明,其實static作用在類里面,說明這個成員是屬于類的,使用時,需要加域名空間才能使用。與不在類中的static類似,比如在.cpp中定義一個static的成員,不管是靜態變量,還是靜態函數,都只能在本cpp里面可見。有一點區別是,類中的靜態成員,在使用之前需要使用 如下方法初始化
3.派生類的構造函數與析構函數的調用順序
class A { //構造和析構在此省略 } class B { //構造和析構在此省略 } class C:public A,public B { //子類的構造和析構 }利用斷點調式逐步執行就能發現執行順序是
A的構造函數執行
B的構造函數執行
C的構造函數執行
C的析構函數執行
B的析構函數執行
A的析構函數執行
規律是,無論繼承是什么順序,衍生類的構造函數都是最后執行,A和B的構造函數是誰繼承在前誰先執行
4.多態(類型轉換)
基類的指針可以指向派生類的對象,編譯器會隱式的執行這種派生類到基類的轉換。之所以能轉換是因為每一個派生類都包含一個基類的全部。
5.虛繼承
考慮一種情況,基類A被A1和A2同時繼承,B繼承了A1和A2,此時A就會同時被繼承兩次(這樣說可能不妥),所以在訪問變量時,可能會存在一些二意性,而且兩個基類也比較占空間。這時候就需要虛基類的出現了。虛基類無論被何種方式繼承,都只會被初始化一次,且基類的初始化需要孫子類或者更下級的類來完成初始化。
6.類型轉換構造函數
簡單點來說,類型轉換構造函數就是帶有一個參數的構造函數,具體實現例子如下
如果構造函數前面加上explicit(明確的)可以阻止系統進行改類型轉換。
7.成員/虛/靜態函數指針 成員/靜態變量指針
class MyT { public:void Test1(){cout << "普通成員函數調用" << endl;}static void Test2(){cout << "靜態成員函數調用" << endl;}virtual void Test3(){cout << "虛成員函數調用" << endl;}public:int m_a; //普通成員變量static int m_b; };int MyT::m_b = 1;int main() { MyT t;MyT * tp = &t;//1.普通成員函數//普通成員函數是跟著類走的,不是跟著對象走,只要有類,就能取到類成員函數的指針void (MyT::*myp)(void);myp = &MyT::Test1; (t.*myp)(); //前面必須拿圓括號括上,因為后面的括號優先級高!(tp->*myp)();//2.虛函數 與成員函數一致 必須綁定到類對象上void (MyT::*myp2) (void);myp2 = &MyT::Test3; (t.*myp2)();//3.靜態成員函數//不需要綁定到類對象上,可直接使用void (*myp3) (void);myp3 = &MyT::Test2;(*myp3)();//4.成員變量指針 不是某個地址 而是該成員變量與該類對象之間的偏移量int MyT::*myp4 = &MyT::m_a;//當生成類對象時,如果這個類中有虛函數表,就會有一個指向這個虛函數表的指針,這個指針占四個字節//所以這里看到的m_a的偏移是4,如果把 virtual 函數注釋后調試模式下查看myp4的值就會是0而不是0x4t.*myp4 = 111; // 等價于 t.m_a = 111;std::cout << t.m_a << endl;//靜態成員變量int *myp5 = &MyT::m_b;*myp5 = 111;return 0; }8.字節對齊問題
定義一個結構體如下
按照常規的理解,其sizeof的結果應該是11個字節,但是我在64位機器上sizeof出來是24?為什么呢?看一下內存分布
0x0113FBFC是對象的初始地址,可以看到A和B分別占用了一個字,D占用了八個字節,后面的C也占用了8個字節,最后末尾還有7個字節。首先看看百度百科對于字節對齊規則的描述
1) 結構體變量的首地址能夠被其最寬基本類型成員的大小所整除;
2) 結構體每個成員相對于結構體首地址的偏移量都是成員大小的整數倍,如有需要編譯器會在成員之間加上填充字節(?);
3) 結構體的總大小為結構體最寬基本類型成員大小的整數倍,如有需要編譯器會在最末一個成員之后加上填充字節。(最后7個字節的來源)
在這個結構體的定義前后加上以下定義,就會發現sizeof大小為11,說明取消了默認的字節對齊。這里這個語句的意思就是,接下來的結構體定義,都按照n字節對齊,除去所有的padding字節。使內存更加緊湊,節約內存。
#pragma pack(push,1) typedef struct Student {char A; char B; int64_t D;char C; }; #pragma pack(pop)9.malloc庫函數可能出現的問題以及改進方法?
malloc是線程不安全的,有可能出現分配異常的問題,自定義一個函數
10.new 和 delete的使用
new和delete成對使用
且 new [] 與delete[]應該對應使用
如果是內部數據類型 如 int char操作時不會內存泄漏,但是是自定義的類如果在new時用new[] 但是delete時用delete,則會出現內存泄漏。多出的內存是用來存儲在初始化時分配了幾個數組元素,一般多出的內存是4個字節。
自定義的類如果沒有自定義的析構函數,那么用delete也不會有內存泄漏。
為什么像 int 這種內部數據類型不用額外的4字節?
11.智能指針自定義刪除器
//shared_ptr
自定義刪除器方式1
std::shared_ptr<Socket> ptsocket(new Socket(),[](Socket * socket){socket->close();delete socket;})自定義刪除其方式2
std::unique_ptr<Socket, void(*)(Socket * socket)> ptsocket(new Socket(), [](Socket * socket) { //to do });自定義刪除器方式3
auto deletor = [](Socket* pSocket) {//關閉句柄pSocket->close();//TODO: 你甚至可以在這里打印一行日志...delete pSocket;};//std::unique_ptr<Socket, void(*)(Socket * pSocket)> spSocket(new Socket(), deletor);std::unique_ptr<Socket, decltype(deletor)> spSocket(new Socket(), deletor);后面的delete函數用Lambda表達式實現了
shared_ptr和unique_ptr的刪除器實現方式不一樣,在傳遞模板參數時,unique_ptr還需要傳遞一個刪除器的類型,因此類實現也就不一樣,會導致不同刪除器類型的unique_ptr不能放置于同一個容器中。而shared_prt則不一樣,只要指向的對象一樣,即使刪除器不一樣,也能放置于同一個容器中。
詳解:王建偉C++ 5-7 00:30:32
12.對象的生命周期
以前一直覺得,函數執行結束了,對象的生命周期才會結束,但是最近因為看一個bug才尼瑪知道,if while do while () 這些東西里面聲明的對象在出其作用域的時候都會釋放掉。
以上程序在作用外均無法使用對象,對象與函數出棧時一樣,都已經被釋放掉了…
13.指針的長度
普通的裸指針是 四字節
shared_ptr 與 weak_ptr 是八個字節,多出來的四個字節是用來存放指向控制塊的指針(包括 強引用計數 弱引用計數 自定義刪除器指針),實際上智能指針也就是存放了兩個裸指針。
另外 unique_ptr 的尺寸與前兩個都不一樣
unique_ptr p1(new string(“hello world!”));
sizeof(p1) // 輸出結果是4 。。
為什么unique_ptr是四個字節呢?因為沒有自定義刪除器,如果增加了自己的刪除器,則 unique_ptr 的尺寸可能增加,比如傳遞函數指針類型時
14.裸指針初始化只能指針陷阱
利用裸指針同時初始化兩個智能指針,會發現p1和p2的強引用都是1,釋放時會釋放兩次。
15.循環引用導致的智能指針無法釋放
new了兩個對象,等于申請了兩塊內存,但是pca的成員pcb是利用pcb創建的,會導致pca的強引用加1,同理 pcb的成員pca是利用pca創建的,會導致pcb的強引用也加1,所以在pca和pcb退出作用域時,不會釋放其指向的內存。
15.智能指針設計的目的以及auto_ptr被放棄的原因
防止內存泄漏,休業式RAII的一種提現。
auto_ptr使用缺陷是
這里定義會導致p1自動被釋放,造成不可預計的問題
雖然unique_ptr也是獨占式的指針,但是unique_ptr會在編譯時報錯,編譯器不允許存在這種操作,避免后續使用p1導致的程序崩潰問題。
16.死鎖的出現以及規避的方法
同一個線程存在對兩個及兩個以上的鎖同時操作時,有可能出現死鎖的情況。比較簡單的方案就是保證調用的順序來保證不出現死鎖的情況,另外,std下提供了lock方法,如
這個方法會一直嘗試獲取1鎖與2鎖,如果獲取不到又會釋放等待下一次獲取。
上述方法有一點不好的就是沒有用到lock_guard,如下可以解決該問題。
使用adopt_lock關鍵字會使得guard對象在生成時不直接調用加鎖,而且后面也不用手動釋放鎖,脫離作用域自動釋放。
17.為什么字符串在網絡傳輸不需要大小端轉換,而像int/long…等卻需要轉換?
首先
大端模式,是指數據的高字節保存在內存的低地址中
小端模式,是指數據的高字節保存在內存的高地址中
大端模式與人類的識別方式是相似的,從左往右,從高到底開始寫
大小端是指在一個字節內,數據存儲的不同順序,以32位小端為例
char a = 65; (字符A)a 在內存中的存儲形式是
0x1000 0010 0000 0000
如果是大端模式,那么在內存中的存儲形式是
0x0000 0000 0100 0001
//未驗證 待驗證
18.dynamic_cast 和 static_cast 的區別
static_cast屬于靜態類型的轉換,會在編譯的時候進行類型檢查。
可以用于相關類型的轉換,比如int和double之間的轉換,子類轉成父類,void*和其他類型之間的轉換,一般不能用于指針類型之間的互轉。從我的角度來看,static_cast就比C的強制類型轉換的作用稍微弱一點,加了一些類型之間轉換的限制。
dynamic_cast主要是具有運行時檢查功能,轉換也是在運行時轉換的,比如有三個類 基類是 human man 和women類分別繼承自 human
有如下轉換,而且human必須具有虛函數,否則dynamic_cast無法正常使用,只有虛函數的存在,才使用指針或引用所指向對象的動態類型。基類有虛函數之后,會有一個指針指向基類的虛函數表,
以上代碼在轉換時,將A轉換成 women* 時,因為dynamic_cast 屬于具有運行時檢查功能,他發現A 指針實際上不是指向的一個women類時,轉換就會失敗,得到的C是一個空指針。
另外還有兩種強制類型轉換
const_cast 和 reinterpret_cast
const_cast 用于去除指針或者引用的常量屬性,屬于編譯時類型轉換。
常見錯誤使用
reinterpret_cast 編譯時進行的類型轉換
重新解釋,把操作數的類型解釋為另外一種類型,處理無關類型的轉換。
比如下面這個轉換,除了C里面的強制類型轉換能做到,其他的dynamic_cast static_cast const_cast均無法實現轉換,編譯無法通過。
19.引用和指針有何區別?怎么拿到引用變量的地址?
引用和指針本無區別,只是編譯器為了方便設置的語法糖,引用本身也是存儲了指向對象的指針變量,和 利用 & 取到的變量地址沒有任何差別,只是看起來不一樣。
至于怎么拿地址?似乎引用變量沒有占內存啊?
參考文
引用比指針安全么?也不是,一樣也存在安全問題,僅僅就是一個語法糖而已
為了進一步驗證引用與指針在本質上的相同,我們看當引用作為函數參數傳遞時,編譯器的行為:
1 void Swap(int& v1, int& v2); 2 void Swap(int* v1, int* v2);3 4 int var1 = 1; 5 00A64AF8 mov dword ptr [var1],1 6 int var2 = 2; 7 00A64AFF mov dword ptr [var2],2 8 Swap(var1,var2); 9 00A64B06 lea eax,[var2] 10 00A64B09 push eax 11 00A64B0A lea ecx,[var1] 12 00A64B0D push ecx 13 00A64B0E call Swap (0A6141Fh) 14 00A64B13 add esp,8 15 Swap(&var1, &var2); 16 00A64B16 lea eax,[var2] 17 00A64B19 push eax 18 00A64B1A lea ecx,[var1] 19 00A64B1D push ecx 20 00A64B1E call Swap (0A61424h) 21 00A64B23 add esp,8上面代碼再次證明了,引用與指針的行為完全一致,只是編譯器在編譯時對引用作了更嚴格的限制。
不要用匯編結果來替代概念,引用不占空間意思就是不占對象空間,不表示不占指針的少量空間。實際上指針是匯編工具實現引用的一種方式而已,而有的優化結果可能沒有代表自己的指針。
總而言之,引用就是引用,是這種概念,它為方便程序員使用,和方便匯編工具優化而產生。匯編怎么實現和優化是匯編的事,至于出了什么違反該概念的結果,是匯編的錯,而不是定義的錯,不要本末倒置。
你可以通過匯編來了解編譯器怎樣實現引用
引用 卻不應該用匯編來解釋 它只是一個概念
贊同,引用只是編譯器之上,給出來的一個抽象定義。接口的實現,由編譯器來決定!
仔細想想,確實如此,引用只是一個概念,為我們提供了一個接口。怎么實現,由編譯器自己決定。
20.RTTI是什么東西?
run time type identification :運行時類型檢查
利用dynamic_cast將基類的指針或引用安全的轉換成子類的指針或者引用,并用來調用子類的某個重寫的虛函數。
21.獲取開機到現在的時間函數
extern int clock_gettime (clockid_t __clock_id, struct timespec *__tp) __THROW;22.git相關
撤銷已add的單個文件修改到modified
撤銷已commit的某個文件的修改
首先查詢這個文件的log:
其次查找到這個文件的上次commit id xxx,并對其進行reset操作:
git reset <commit-id> <fileName>再撤銷對此文件的修改:
git checkout <fileName>最后amend一下,再push上去:
總結
- 上一篇: 首届中国云南普洱茶茶王获奖名单及常识
- 下一篇: Spark机器学习实验