日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

C/C++ 踩过的坑和防御式编程

發布時間:2025/3/12 c/c++ 18 豆豆
生活随笔 收集整理的這篇文章主要介紹了 C/C++ 踩过的坑和防御式编程 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

相信你或多或少地用過或者了解過 C/C++,盡管今天越來越少地人直接使用它,但今天軟件世界大多數軟件都構筑于它,包括編譯器和操作系統。因此掌握一些 C/C++ 技能的重要性不言而喻。

這場 Chat 本人將從小處入手,以親身踩過的坑作為示例,講述一下 C++ 的常見的坑,以及其防御方法——防御式編程。主要內容包括:

  • C/C++ 基礎知識簡介

  • C/C++ 常見問題復現示例

  • 內存泄露問題排查

  • 防御式編程理論

  • 防御式編程實踐


大家好,我是林奇思妙想。很高興能夠和各位在Gitchat上交流一些平常用C/C++, 寫 C/C++ 的經驗分享。

為了表明這是一場嚴肅的、有深度的交流,我們先不走偏,先回顧一下經典教材上關于C/C++的基礎知識。盡量保持簡單,點到為止,希望你沒有被嚇走。作為一枚典型猿族,我就話不多說,直接帶領大家入坑(笑~)。

注:所有的程序示例在MS VS2017下調試的。

C/C++ 基礎知識簡介

我們說到 C/C++ 一般指兩部分:

  • C++兼容 C 的部分

  • C++獨有部分

先看一下C/C++共有部分(有基礎的同學可以略過這一部分, 或者可以跟我一起溫習)。

C/C++ 共有部分

常量

常量是指運行時值不能改動的一類“變量”,他們的值是編譯進目標程序中的。

1)立即數

如字面常量 12, 123.5f, "abc".

2)常量對象

const int SIZE_A = 11; const Mat MAT_A(12,22,-1);

變量在運行時占有內存地址空間,且它的值可以在運行時被更改

變量

1)普通值變量

float a = 9.1f; Mat mat_a(1,2,-1);

2)指針變量 p_a

int *p_a = &a;

表達式

表達式是指能被編譯器編譯為指令的語句,通常以“;”結束。

表達式分以下幾種:

1)賦值

int a = 4;

2)逗號

a = 9, b = 11;

3)判斷

if (a > b){do_something();}

4)循環

for(int i=0; i<MAX; i++){do_something(); }

5)函數調用

do_something();

其它數據結構

結構體等

struct st_a{int x; int y; };

通常是 public 的,即沒有封裝性。

C++獨有

類是C++所獨有,也稱對象。通常我們用一個類來表示一類客觀事物的層次、繼承關系。

如下圖所示:

圖 0.1 類的繼承關系

1)繼承

繼承就如0.1所示,從父類到子類,越來越具體。

2)封裝

封裝是指對于某一子類,可以控制哪些信息能被外部看到,如控制我們能獲取手機的大小,顏色等屬性,對用戶隱藏手機串號等信息。

3)多態

多態是C++精華所在,但也是C++的難點所在。一些教材常常用高大上的描述把人搞昏。譬如我手邊這本書里面是這樣講多態的(C++程序設計教程——錢能版):

“ 多態是基于類的層次結構的,當指針飄忽不定地可能指向類層次中的上下不同對象時,以指針間訪的形式實施的操作便是表現多態的條件。”

這段文字真是一騎絕塵,不食人間煙火,難免把人帶到小黑屋子——關著。

用通俗的話說,多態是指多個子類有一個共有操作,我們在父類中定義一個統一的抽象虛接口,然后各個子類分別實現。這樣子,運行時,依據子類是什么,動態選擇子類的方法。 這樣子描述,我們又不可避免地走入了經典教材的巨梗——不懂啊! 不如直接看如下代碼吧。

如下代碼所示:

// code start /* B is base class, A -- C is sub-class*/class B {public:virtual void do_sth() = 0;};class A : public B {public:void do_sth() {cout << "- A do_sth()\n"; }};class C : public B {public:void do_sth() {cout << "- C do_sth()\n";}};void do_sth(B *id_b) {id_b->do_sth(); }int main(){A* id_a = new A(); C* id_c = new C(); do_sth(id_a); do_sth(id_c); return 0; }// code end

函數運行輸出結果是:

A do_sth()C do_sth()

注意其中的粗體代碼。

看“ void dosth(B *idb)” , 我們用的基類指針作為函數的接口參數,但是“dosth(ida); ” 傳遞參數時,我們傳的是A 或者 C 對象的指針! 多態使得調用的接口一致,更利于抽象和簡化。

C/C++ 常見問題復現示例

如果算上最新接觸C到現在,已經有9年。寫過無數的C/C++代碼。有些坑是自己挖的,有些則是語言層面上的“陷阱” 。

坑1

立即數左移越出范圍。先看一段代碼:

assert((1 << 3) == pow(2, 3));assert((1 << 30) == pow(2, 30));assert((1 << 62) == pow(2, 62));

先不運行,猜測一下問題在哪一行?

運行結果如下:

Assertion failed: (1 << 62) == pow(2, 62), file

我們再進一步看一下他們的值:

cout << pow(2, 62) << endl; cout << (1 << 62) << endl; 4.61169e+18 0

可知前者是對的,后者是錯的,在C語言中,左移結果最大是 32 位。

為了驗證,我們再看一下:

cout << pow(2, 62) << endl; cout << (1 << 62) << endl; cout << (4.61169e+18) << endl;

運行結果是:

4.61169e+18 0 4.61169e+18

符合我們的預期。

坑2

sprintf()越界問題。

char buf[10]; float x = 1/3.0f; sprintf(buf, "cols = %f", x); printf(buf);

運行后,buf會越界,出現地址異常!正確的做法是給 buf 一個更大的地址。 但是這類棧溢出在大型的工程中,防不勝防。其實可以在 C++ 中,考慮用更一種更安全的方式。

float f = 1 / 3.0f;ostringstream ss;ss << "num is " << f << endl;cout << ss.str();

坑 3

case語句,不打括號。

// 示例代碼K_0 :

int num = 2; switch (num){case 0:do_0(); break; case 1:do_1();do_xx();do_xxx(); do_xxxx();break;default:do_default(); }// code end

這段代碼運行起來沒有問題,

邏輯沒有問題,還不至于成坑,但是在實際的大型項目中,一個項目因需求變化可能需要頻繁改動。如上述代碼,有人在"do_xxxx()"后加上另外一個分支。變成:

do_xx();do_xxx(); do_xxxx(); case 2:do_2(); do_2_x(); do_2_xx();break;

這個時候邏輯就會出問題。而且這種錯誤很難調試,最好的辦法是預防。 預防的方法也很簡單,給case分支加上大括號。如下:

switch (num){case 0:{ do_0();break;}case 1:{do_1();do_xx();do_xxx();do_xxxx();break;}default:{do_default();}}

這種方法看起來很笨,但比后期要發布前出bug, 把班加個昏天黑地要強。

坑4

sizeof()用于一個結構體時其值不是絕對的。與平臺相關,也與編譯指令相關??蠢?#xff1a;

struct pack_struct{char t;uint32_t x; };cout << sizeof(pack_struct);

輸出是什么?

有很多同學一看,說這個容易,?char?占用一個字節,?uint32_t?占用 4 個字節,所以共占用 5 個字節。還有同學想到了4字節對齊,說應該是4的倍數,所以應該是8。那正確的結果又是什么呢?

其實答案是:兩者都有可能。與編譯控制有關。要想結果得到“5”, 用下面的:

#pragma pack(1)struct pack_struct{char t;uint32_t x; };#pragma pack()

要想得到“8”, 把pack(1)?改成pack(4)。

一般默認的編譯參數,不同的平臺是不一樣的。所以要求我們不能寫硬編碼的代碼。例如下面的代碼將得不到我們想要的結果:

pack_struct arr_pack[8]; char *p_tmp = (char*)arr_pack;pack_struct *pack_idx_1 = (pack_struct*) (p_tmp + 5);

示例有點兒繞,我們j是想要通過指針的方式得到數組arr_pack的第一個指針。這里用了硬編碼,寫成了5。這里硬編碼的問題可能是,在其它的平臺上不一定能正確工作!

為了方便移植,應該改寫成:

pack_struct arr_pack[8]; char *p_tmp = (char*)arr_pack; pack_struct *pack_idx_1 = (pack_struct*) (p_tmp + sizeof(pack_struct));

坑5

浮點數的比較。某君寫了如下代碼:

float x = 1.333 - 1; cout << x << endl; if (x == 0.333){cout << "x = " << 0.333 << endl; }else{cout << "x != " << 0.333 << endl;}

請問一下輸出是什么?

不用想,筆者在這里把它們寫出來,肯定是有坑的,所以輸出結果也是“驚世駭俗”的,輸出為:

0.333 x != 0.333

看起來不可思議!!!問題來了,那怎樣才能正確地作浮點數相等判斷呢?也很簡單,一般用偏離一個中心的距離小于某個精度來判斷相等:

if (fabs(f1 - f2) < 預先指定的精度){//...}

實際一般推薦用 1e-5 作為精度(看各項目的精度要求啰)。

坑6:模版的使用

首先來看一下一個正常的模版類使用方法:

`// ### main.cpp ###`#include <iostream>using namespace std; template<class T> void disp(T t);template<class T>void disp(T t){cout << t << endl;}int main(){disp(8); disp<float>(8.8f);return 0; }


這個能正常編譯運行,但是把這些模版細節都放到?main.cpp?中,看得很昏。于是我們想要把他們移出main.cpp。

假如我們有一個頭文件test.h?和一個實現文件?test.cpp。

我們把模版的聲明移到test.h,模版的實現移至main.cpp, 此時三個文件內容分別如下:

// ### test.h ###

template<class T> void disp(T t);// ### test.cpp ###template<class T>void disp(T t){cout << t << endl;}


/ ### main.cpp ###

#include <iostream>using namespace std; int main(){disp(8); disp<float>(8.8f);return 0; }

編譯運行,發現Linker出錯:

error LNK2019: unresolved external symbol "void __cdecl disp<int>(int)" (??$disp@H@@YAXH@Z) referenced in function main error LNK2019: unresolved external symbol "void __cdecl disp<float>(float)" (??$disp@M@@YAXM@Z) referenced in function main fatal error LNK1120: 2 unresolved externals -- FAILED.

錯誤出現了,我們想知道“為什么”?

這是因為目前C++還不支持模版分開編譯。分開會導致模版函數在具化過程中找不到外部符號。通常地做法是把聲明和實現全部放在頭文件中。下面是正確的版本:

// ### test.h ###

template<class T> void disp(T t);template<class T>void disp(T t){cout << t << endl;}

// ### test.cpp ###

// test.cpp file content...

// ### main.cpp ###

#include <iostream>using namespace std; int main(){disp(8); disp<float>(8.8f);return 0; }

這回終于對了!

坑7:棧被意外修改

看下面這段代碼。你覺得Line 28會異常嗎? 實際結果是”會“!

這個就是我在工作中遇到的一個實際問題,當時一直監控一個變量,這個變量總是莫名其妙被更改了, 最后挖出來罪魁禍首就是一個把棧破壞了。結果我的 “N” 成了一個無意義的超大數。

所以一定要嚴格控制好指針的越界行為,編譯器無法知道你的意圖,這個只有靠自己來把控,但也有一些方法來排查和防衛這種問題的發生。這是接下來要講的內容。

內存泄露問題排查

內存泄露問題是大型 C++項目中最棘手的問題之一。有人會想,嘿,反正我內存多,不用擔心用完,讓它去泄露吧, 但我只能告訴你,too young , too simple, sometimes naive。

內存只有有哪怕一個地址的泄露,都可能導致嚴重的宕機事故,特別是在一些重要的嵌入式領域,如醫療,核電,飛行控制器軟件中。

有很多工具可以幫忙排查內存泄露問題。從懷疑內存泄露到證實一般要經過以下的步驟:

懷疑

開機后一切正常,系統運行一段時間后,越來越卡。此時需要排除是不是 CPU 溫度過高,軟件處理數據量是不是變大了。如果都正常,那就著手懷疑是內存泄露問題。

如果在Linux上,可以通過 free -m 命令看到剩余的內存。如果運行長時間后,剩余內存變得越來越小。則大概率是內存泄露。

驗證

下一步可以通過一些工具去協助監控內存的分配和釋放, 如:coverity (力薦,因為是我的前東家新思科技產的,當然也是最好用的,Valgrind(開源,免費)等。

防御式編程理論

講了這么多坑,終于要講一些理論總結了,應該松口氣還是憋口氣呢(笑)?

我們編程最終的目標是讓產品能平穩運行,而且要”不以人的意志為轉移“式地平穩運行。

這其中包括兩點:

  • 產品代碼要邏輯正確、完備;

  • 代碼能夠讓人讀而知其義,能盡量避免他人犯錯;

防御式編程就是在做到 1 后,再把第 2 條做好。

在這里,一定要看清楚,做到1后再做2。 有些軟件管理可能過分強調防御,在功能,邏輯都還沒有完備時,大搞特搞防御,其實并不可取。其實,本人傾向于當功能雛形做好后,再防御。畢竟過早防御會使得代碼過分臃腫。

說了這么久”防御“,讀者可能已經昏了。那到底什么是防御? 別急,讓我們先來看一個例子。

如取上面的例子N, 防御式編程應該是:

FILE *fp = fopen("main.cpp", "rb"); const int MAX_BUF = 8;char *buf = new char[MAX_BUF];int N = 99; int i_to_read = 1024; assert(MAX_BUF - i_to_read > 0); fread(buf, 1, i_to_read, fp);assert(N == 99); fclose(fp); delete[] buf;

assert那一行就是防止別人犯錯,并把錯誤盡早地暴露出來的防御代碼。

防御式編程實踐

其實防御式編程理論遠比這個要深。在實踐中,各聰明的大師們總結了一套規律來嚴防死守內存越界,數組越界,值為負,除數為零, 野指針等。

下面分別說明。

防內存越界。我是懶人,還是取上面的例子:)

assert(MAX_BUF - i_to_read > 0);fread(buf, 1, i_to_read, fp);

防數組越界

如下,假如我們要寫一個函數,返回數組的索引為?idx?的元素值。

int get_arr_by_idx(int *arr, int len, int idx);int get_arr_by_idx(int *arr, int len, int idx) {assert(idx <= len - 1);return arr[idx];}

防值為負

char buf[9] = 0;int sz_to_set = 4;assert(sz_to_set >= 0);memset(buf, 0xAF, sz_to_set);

防被除數為零

double t = 9.9f; for (int i = -2; i < 2; i++) {assert(i != 0);cout << t / i << endl; }

野指針

char *buf = NULL; assert(NULL != buf);printf(buf);

當然,說了這么多的防御技巧,其實最好的防御是命名。(驚訝的表情)。名命得好,可以讓人”望文生義“。筆者就常常用下面這幾個命名:

  • idx?: 表元素索引, 是大于等于 0 的;
  • sz, len?: 表元素長度;
  • p_xx?: 表示 xx 的一個指針;
  • 全大寫:?表示常量;
  • m_xx :?表示成員變量;

當然,關于命名上筆者不是能手,想成為能手,就去讀一些成熟的優秀的開源框架中的命名。 相信我,你會受益良多的。推薦boost, Linux Kernel, Google Protobuf.

上面說了這么多, 如果你有幸從中學到一些,一定要告訴筆者,讓筆者欣慰欣慰。

最后, 謝謝你的閱讀。

林奇思妙想 于 深圳

2017/11/24

總結

以上是生活随笔為你收集整理的C/C++ 踩过的坑和防御式编程的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。