引言
突然感覺要出去走走了, 醒了後 刷完牙就在聯系coding, 不知不覺到了 黃昏.
看看天, 打開燈. 又感覺到了 夜夜夜夜 .
13年到北京務工, 遇到一批批NB的同齡人物. 一塊工作, 一塊喜歡鍛煉, 一塊默默的學習.
從他(她)們身上發現一個事實.
假如我們一樣聰明,
當你抱怨自己為什麼努力了, 確還是 這麼水的時候 ; 其實他(她)們在拼命. 而你只是在努力 ,
假如我們不一樣聰明,
如果還不能開掛, 那會是怎麼樣精彩 x x x x.
前言 - 內存越界處理
我們先看設計圖. 內存越界檢查原理如下
上面原理是不是很簡單. 而這恰恰是最通用的做法. 美的東西不負責. 美很重要.
那我們按照上面設計思路. 首先構建 接口文件 checkmem.h
#ifndef _H_MEMCHECK_CHECKMEM #define _H_MEMCHECK_CHECKMEM #include <stddef.h> /* * 對malloc進行的封裝, 添加了邊界檢測內存塊 * (inline 原本分_DEBUG有宏處理, 後面沒加等於沒用) * sz : 申請內存長度 * : 返回得到的內存首地址 */ extern inline void* mc_malloc(size_t sz); /* * 對calloc進行封裝, 添加邊界檢測內存塊 * cut : 申請的個數 * sz : 每個的大小 */ extern inline void* mc_calloc(size_t cut, size_t sz); /* * 對relloc進行了封裝, 同樣添加了邊間檢測內存塊 */ extern inline void* mc_realloc(void* ptr, size_t sz); /* * 對內存檢測, 看是否出錯, 出錯直接打印錯誤信息 * 只能檢測, check_* 得到的內存 */ extern inline void mc_check(void* ptr); #endif // !_H_MEMCHECK_CHECKMEM
主要是對 malloc, calloc, realloc 進行添加尾部和頭部的內存塊處理. 就這麼簡單一步. 假如能看懂上面設計思路圖.
這些代碼都可以跳過了. 思路比代碼重要. 好那我們繼續展現實現部分. checkmem.c
#include "checkmem.h" #include <stdio.h> #include <errno.h> #include <string.h> #include <stdlib.h> // 控制台打印錯誤信息, fmt必須是雙引號括起來的宏 #define CERR(fmt, ...) \ fprintf(stderr,"[%s:%s:%d][error %d:%s]" fmt "\r\n",\ __FILE__, __func__, __LINE__, errno, strerror(errno), ##__VA_ARGS__) //控制台打印錯誤信息並退出, t同樣fmt必須是 ""括起來的字符串常量 #define CERR_EXIT(fmt,...) \ CERR(fmt,##__VA_ARGS__),exit(EXIT_FAILURE) // 插入字節塊的個數 #define _INT_CHECK (1<<4) /* * 對malloc進行的封裝, 添加了邊界檢測內存塊 * sz : 申請內存長度 * : 返回得到的內存首地址 */ inline void* mc_malloc(size_t sz) { // 頭和尾都加內存檢測塊, 默認0x00 char* ptr = calloc(1, sz + 2 * _INT_CHECK); if (NULL == ptr) { CERR_EXIT("malloc sz + sizeof struct check is error!"); } //前四個字節保存 最後一個內存塊地址 大小 size_t* iptr = (size_t*)ptr; *iptr = sz + _INT_CHECK; return ptr + _INT_CHECK; } /* * 對calloc進行封裝, 添加邊界檢測內存塊 * cut : 申請的個數 * sz : 每個的大小 */ inline void* mc_calloc(size_t cut, size_t sz) { return mc_malloc(cut*sz); } /* * 對relloc進行了封裝, 同樣添加了邊間檢測內存塊 */ inline void* mc_realloc(void* ptr, size_t sz) { // 先檢測一下內存 mc_check(ptr); // 重新申請內存 char* cptr = (char*)ptr - _INT_CHECK; char* nptr = calloc(1, sz + 2 * _INT_CHECK); if (NULL == nptr) { CERR_EXIT("realloc is error:%p.", ptr); } // 內存移動 size_t* bsz = (size_t*)cptr; memcpy(nptr, cptr, *bsz < sz ? *bsz : sz); *bsz = sz; free(cptr); return nptr; } // 檢測內存是否錯誤, 錯誤返回 true, 在控制台打印信息 static void _iserror(char* s, char* e) { while (s < e) { if (*s) { CERR_EXIT("Need to debug test!!! ptr is : (%p, %p).check is %d!",s, e, *s); } ++s; } } /* * 對內存檢測, 看是否出錯, 出錯直接打印錯誤信息 * 只能檢測, check_* 得到的內存 */ inline void mc_check(void* ptr) { char *sptr = (char*)ptr - _INT_CHECK; //先檢測頭部 char* s = sptr + sizeof(size_t); char* e = sptr + _INT_CHECK; _iserror(s, e); //後檢測尾部 size_t sz = *(size_t*)sptr; s = sptr + sz; e = s + _INT_CHECK; _iserror(s, e); }
代碼實現都很中規中矩, 比較容易. 也就百行. 按照接口文件一個個看實現. 很容易學到開發中技巧. 提高實戰技巧.
扯一點, C, C++ 老開發人員水平都比較高, 不喜歡寫注釋. 這個強烈推薦不是大牛的選手一定要多寫注釋.
不要扯 什麼 <代碼即注釋> . 多寫注釋容易加深自己二次思考, 加快自己的成長. 不要和老開發人學這個 , 如果你跳槽, 遇到一個大項目
注釋等價無, 你是什麼感受. 為我們多留條後路, 多寫注釋.
好 看測試代碼 main.c
#include <stdio.h> #include <stdlib.h> #include "checkmem.h" /* * 演示一種檢測內存越界的辦法 * 添加上下限方式 */ int main(int argc, char* argv[]) { // 實驗步驟是, 是申請內存, 在操作內存 char* as = mc_malloc(16); mc_check(as); // 內存越界了 //as[16] = 18; //mc_check(as); // 重新分配內存, 再次越界 as = mc_realloc(as, 15); as[15] = 44; mc_check(as); free(as); return 0; }
測試結果
到這裡內存越界的思路和實現都已經完畢了.歡迎思考嘗試.
正文 - 內存全局監測
內存全局檢測思路更簡單. 采用引用'計數方式'處理. 扯一點很多自動垃圾回收機制都采用了引用計數方式.
包括內核層例如 文件描述符, IPC 共享內存, 消息機制等. 先看接口 memglobal.h
#ifndef _H_MEMGLOBAL_MEMGLOBAL #define _H_MEMGLOBAL_MEMGLOBAL #include <stddef.h> #include <stdlib.h> /* * 全局啟動內存簡單監測 */ extern inline void mg_start(void); /* * 增加的全局計數的 malloc * sz : 待分配內存大小 * : 返回分配的內存首地址 */ extern void* mg_malloc(size_t sz); /* * 增加了全局計數的 calloc * sc : 分配的個數 * sz : 每個分配大小 * : 返回分配內存的首地址 */ extern inline void* mg_calloc(size_t sc, size_t sz); /* * 增加了計數的 realloc * ptr : 上一次分配的內存地址 * sz : 待重新分配的內存大小 * : 返回重新分配好的內存地址 */ extern void* mg_realloc(void* ptr, size_t sz); /* * 增加了計數處理的內存 free * ptr : 上面函數返回地址的指針 */ extern inline void mg_free(void* ptr); // 在測試模式下開啟 全局內存使用計數 #if defined(_DEBUG) # define malloc mg_malloc # define calloc mg_calloc # define realloc mg_realloc # define free mg_free #else # define malloc malloc # define calloc calloc # define realloc realloc # define free free #endif #endif // !_H_MEMGLOBAL_MEMGLOBAL
還是比較優美的. 再看 memglobal.c
#include "memglobal.h" #include <stdio.h> #include <errno.h> #include <string.h> #include <stdlib.h> // 取消內置宏, 防止遞歸 #undef malloc #undef calloc #undef realloc #undef free // 控制台打印錯誤信息, fmt必須是雙引號括起來的宏 #define IOERR(io, fmt, ...) \ fprintf(io,"[%s:%s:%d][error %d:%s]" fmt "\r\n",\ __FILE__, __func__, __LINE__, errno, strerror(errno), ##__VA_ARGS__) // 全局內存計數, 系統第一次構造的時候為0 static int _mct; #define _STR_MGTXT "checkmem.log" // mg內存監測退出時候, 記錄一些信息 static void _mg_exit(void) { if (_mct == 0) return; // 先打印信息到控制台 IOERR(stderr, "Detect memory leaks _mct = %d!!", _mct); //輸出到文件 FILE* txt = fopen(_STR_MGTXT, "a"); if (txt == NULL) { IOERR(stderr, "fopen " _STR_MGTXT " a is error!"); return; } IOERR(txt, "Detect memory leaks _mct = %d!!", _mct); fclose(txt); } /* * 全局啟動內存簡單監測 */ inline void mg_start(void) { // 注冊退出監測事件 atexit(_mg_exit); } /* * 增加的全局計數的 malloc * sz : 待分配內存大小 * : 返回分配的內存首地址 */ void* mg_malloc(size_t sz) { void* ptr = malloc(sz); if (!ptr) return NULL; ++_mct; memset(ptr, 0x00, sz); return ptr; } /* * 增加了全局計數的 calloc * sc : 分配的個數 * sz : 每個分配大小 * : 返回分配內存的首地址 */ inline void* mg_calloc(size_t sc, size_t sz) { return mg_malloc(sc*sz); } /* * 增加了計數的 realloc * ptr : 上一次分配的內存地址 * sz : 待重新分配的內存大小 * : 返回重新分配好的內存地址 */ void* mg_realloc(void* ptr, size_t sz) { if (!ptr) return mg_malloc(sz); return realloc(ptr, sz); } /* * 增加了計數處理的內存 free * ptr : 上面函數返回地址的指針 */ inline void mg_free(void* ptr) { if (!ptr) return; --_mct; free(ptr); }
中間用了
// 取消內置宏, 防止遞歸 #undef malloc #undef calloc #undef realloc #undef free
這個主要為了解決 引用了 頭文件 memglobal.h 會造成遞歸調用. Linux上還有一種思路, 不包含這個頭文件
鏈接時候gcc 指定就可以. 但是 vs 是自動推導編譯, 如果不引入它推導不出來. 後面就采用了上面通用的做法.
上面思路是, 先啟動 全局內存監測功能, 再通過特殊宏,替代原先的申請和釋放函數. 來達到目的.
測試文件 main.c
#include <stdio.h> #include <stdlib.h> #include "memglobal.h" /* * 內存全局計數, 檢測內存是否越界 */ int main(int argc, char* argv[]) { // 開啟內存全局計數 mg_start(); int *p = malloc(16); p = calloc(12, 2); *p = 154; puts("就這樣!"); p = realloc(NULL, 6); puts("測試這樣行!"); return 0; }
測試運行結果如下
最終打印日志是
好. 到這裡 關於內存全局檢測的技巧解釋和實現完畢. 很簡單很好用.
重點是理解上面兩種方式思路. 哈哈, 是不是發現 好神奇的內存洩露, 內存越界, 內存洩露監測也不過如此.
開發, 寫代碼很簡單, 但化為生產力就很難了, 也許需要更多有能力的一起轉換.
後記
錯誤是難免, 歡迎吐槽交流, 拜~~. 希望早睡早起.