使用 C 和 C++ 的程式語言概念/動態記憶體管理
當被要求定義一個概念時,數學家往往不會將他們的答案建立在日常生活中的侷限性上。集合僅僅是不同元素的集合,不會在它們之間強加隱含的順序。空集和包含三個或五個元素的集合一樣“真實”。可以向集合中新增無限數量的元素。事實上,可以談論無限集合。
不幸的是,計算機的有限性並沒有給計算機科學家帶來做出這種大膽主張的奢侈。你不能假設你的資料結構具有無限的大小。更糟糕的是,對時間和/或空間效能的關注通常會迫使你穿上靜態資料結構的緊身衣:你的資料結構的大小不能超過某個預定義的值。但是,如果你做了一個糟糕的預測怎麼辦?簡單來說:你的程式崩潰;你用不同的值重新構建程式,並祈禱它不會再次發生。或者,以一種自毀的方式,你的程式分配了從未使用過的記憶體。
不過,情況並非如此糟糕。你有一個選擇:動態記憶體管理。使用動態記憶體管理函式,可以根據需要動態地增加和縮減資料結構的大小。這些函式透過影響一個稱為 *堆* 或 *自由儲存區* 的記憶體區域來完成它們的工作。
增加資料結構的大小是透過諸如 malloc 或 new 之類的函式完成的。這些函式基本上會向語言執行時的一部分(稱為記憶體分配器)請求記憶體。如果記憶體分配器可以滿足請求,它將返回指向分配區域的指標(或控制代碼)。否則,將返回一個特殊值(如 NULL 或 nil)或一個異常物件,指示分配請求失敗。
縮減資料結構更有趣。雖然一些程式語言提供了顯式釋放記憶體的函式,但一些語言會承擔責任並代表程式設計師進行釋放。後一組被稱為具有 [自動] *垃圾收集* ([自動] GC)。屬於第一組的程式語言的記憶體分配器期望程式設計師透過呼叫諸如 free 或 delete 之類的函式來採取行動。這對馬虎的程式設計師來說是個壞訊息。不釋放未使用的記憶體等同於浪費記憶體,最終會導致崩潰;隨著越來越多的記憶體被分配而沒有被回收,可用記憶體池會縮小,分配器會發現自己處於無法滿足分配請求的情況,這不可避免地會導致上述結果。這種未使用的記憶體被稱為垃圾。相反,過早地釋放記憶體同樣是災難性的。你現在面臨著使用不指向有效資料的指標的危險。這樣的指標被稱為懸空指標。
那麼,為什麼人們——至少有些人——堅持使用沒有垃圾收集的語言?或者,他們不能簡單地將這種功能新增到 C 之類的語言中?第一個問題的答案是效能。垃圾收集器作為後臺執行緒(或程序)執行,將在所有其他執行緒處於等待狀態或程式記憶體不足並且下一個分配請求只能透過一些尚未回收的垃圾來滿足時接管。[1] 這是一個耗時的過程,不會在幾個機器週期內完成;作為其執行的一部分,垃圾收集器將掃描整個堆記憶體並將未使用的區域標記為垃圾。這意味著呼叫垃圾收集器將導致效能大幅下降。另一方面,透過諸如 free 或 delete 之類的函式顯式地解除分配,使程式設計師能夠在記憶體變得未使用時將其返回。雖然它仍然是一個選項,但程式中沒有專門的階段用於以批發的方式專門花費週期來返回未使用的記憶體。下圖顯示了這一點。

至於第二個問題,答案是否定的。能夠透過不同的指標引用相同的記憶體區域,C 中的自由使用強制轉換使得分配器無法弄清楚記憶體的內容。如果此區域最初包含其他指標,現在被視為原始位元組的集合怎麼辦?因此,透過記憶體跟蹤指標以找到未使用的區域,從而進行垃圾收集變得不可能。[2]
在這個例子中,我們為字元字串提供了一個不透明型別的實現。在前面的介紹(基於物件的程式設計)之上,該型別表示中的兩個指標級別為我們提供了強調正確分配和解除分配順序的機會。
#ifndef STRING_H
#define STRING_H
#define SUBSTR_NOT_FOUND -1
#define OUT_OF_MEMORY –2
另一個前向宣告及其伴侶 typedef!前者用於預先通知編譯器關於我們將要處理的記錄型別的存在,而後者提供對這種記錄型別例項的控制代碼。以這種方式操作的型別稱為 *不透明型別*;透過指標來訪問其例項。
觀察到原型中提供的所有形式引數都是“'[常量] 指向 struct _STR'”,而不是“struct _STR”。這樣做的原因如下:作為資訊隱藏原則的自然結果,我們應該將記錄型別表示保留為實現細節:設施的使用者無需關心對“如何”(實現)問題的詳細答案;她最好集中精力找到對“什麼”(介面)問題的答案。這可以透過給她不會改變的東西來實現;可能會改變的東西應該作為實現的一部分,而不是介面的一部分。並且指向記錄型別的指標的大小保持不變,即使記錄型別本身的大小可能發生變化。
struct _STR;
typedef struct _STR *String;
extern String String_Create(const char*);
extern void String_Destroy(String *this);
extern int String_Compare(const String this, const String);
extern String String_Concat(const String this, const String);
extern int String_Contains(const String this, const String);
extern int String_Containsr(const String this, const String);
extern unsigned int String_Length(const String this);
extern String String_Substring(const String this, unsigned int start, unsigned int length);
extern char* String_Tocharp(const String this);
#endif
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "General.h"
#include "utility/String.h"
除了字串 [字元] 之外,我們的表示還提供了字串的長度作為成員欄位。透過這樣做,我們在空間和時間之間進行權衡:當被問到字串的長度時,我們不是遍歷所有字元直到看到終止的 NULL 字元,而是返回此成員欄位的值。換句話說,我們更喜歡 *空間中的計算* 而不是 *時間中的計算*。
struct _STR {
unsigned int length;
char *str;
};
static String String__Reverse(const String this);
String String_Create(const char* source) {
String this;
unsigned int i;
除了靜態資料區,資料在程式執行期間始終處於活動狀態,以及執行時堆疊,區域性於塊的變數位於其中;我們提供了一個記憶體池,我們可以在程式執行期間使用。這個記憶體區域稱為程式的自由儲存區或堆。與其他兩個區域不同,這個區域由程式設計師透過動態記憶體管理函式來管理。
自由儲存區記憶體的一個方面是它是無名的。在自由儲存區上分配的物件是透過指標間接操作的。自由儲存區的另一個方面是分配的記憶體是未初始化的。
在執行時分配記憶體稱為動態記憶體分配;透過諸如 malloc 之類的函式分配的記憶體被稱為動態分配的。但是,這並不意味著指標本身的儲存空間是動態分配的。它可能很可能是靜態分配的。[3]
定義:物件的生存期——程式執行期間儲存繫結到物件的這段時間——被稱為物件的 *範圍*。
在檔案範圍內定義的變數被稱為具有 *靜態範圍*。儲存在程式啟動之前分配,並在整個程式執行過程中保持繫結到變數。在區域性範圍內定義的變數被稱為具有區域性範圍。儲存在進入區域性範圍時在執行時堆疊上分配;退出時,此儲存空間被釋放。另一方面,靜態區域性變量表現出靜態範圍。
在自由儲存區上分配的物件被稱為具有 *動態範圍*。透過使用諸如 C 中的 malloc 之類的函式分配的儲存空間將一直繫結到物件,直到由程式設計師顯式地解除分配。
malloc 返回一個通用指標,void*<>/source. Such a pointer cannot be dereferenced with the <syntaxhighlight lang="c" enclose="none">* 或下標運算子。為了將此指標用於生產性目的,我們必須將返回值強制轉換為某個特定的指標型別。假設 malloc 可以滿足請求,區域性變數 this 包含一個地址值,該值將被解釋為指示 struct _STR 物件的起始地址。否則,它包含一個多型值,NULL,表示 malloc 的失敗。
| 問題 |
|---|
假設使用“資料級結構”一章中介紹的方案,您認為由 malloc 分配的區域所需的對齊方式是多少? |
this = (String) malloc(sizeof(struct _STR));
如果堆中可能沒有足夠的記憶體,NULL 將由 malloc 函式返回。我們應該讓使用者知道這一點。
成功分配用於元資料的記憶體後,該元資料是字串內容的指標及其長度的組合,我們必須為字串內容分配空間。我們透過第二次使用 malloc 函式來實現這一點。最終,如果一切順利,我們將得到下面給出的數字。[4]
if (this == NULL) {
fprintf(stderr, "Out of memory...\n");
return(NULL);
} /* end of if(this == NULL) */
this->length = strlen(source);
this->str = (char *) malloc(this->length + 1);
if (this->str == NULL) {
free(this);
fprintf(stderr, "Out of memory...\n");
return(NULL);
} /* end of if (this->str == NULL) */
下一個迴圈以及接下來的兩行用於將一串字元複製到另一個記憶體區域。我們首先在 for 迴圈中複製與字串長度一樣多的字元,然後追加終止的 NULL 字元。最後,現在我們已在過程中移動了指標,並且它指向字串末尾的 NULL 字元,我們使用最後一行中的指標運算使其指向該字元的開頭。
請注意,我們可以使用標準字串函式 strcpy 來實現這一點。除了 strcpy 之外,我們還提供了一系列字串函式。除了這些之外,由於一串字元是以“\0”作為哨兵值的記憶體區域,因此我們可以使用適當的記憶體函式處理這樣的實體。下面列出了一些此類函式的示例。
朋友們 strcpy | |
|---|---|
char* strcat(char *dest, const char *src)
|
將 src 追加到 dest 並返回累積在 dest 中的值作為其結果。 |
char* strncat(char *dest, const char *src, size_t len)
|
與 strcat 相同,但附加條件是將累積的字串長度限制為 len。 |
int strcmp(const char *s1, const char *s2)
|
返回按字典順序比較其引數的結果。如果兩個字串相等,則返回值為 0。如果 s1 在 s2 之前,則返回值小於 0。否則,將返回正值。 |
int strncmp(const char *s1, const char *s2, size_t len)
|
與 strcmp 相同,但附加條件是將比較限制為引數字串的前 len 個字元。 |
char* strcpy(char *dest, const char *src)
|
將 src 複製到 dest 並返回 dest 的最終值作為其結果。 |
char* strncpy(char *dest, const char *src, size_t len)
|
與 strncpy 相同,但附加條件是將複製的字元數限制為 len。 |
size_t strlen(const char *s)
|
返回其引數的長度。 |
char* strchr(const char* s, int c):
|
在字串 s 中搜索 c 的首次出現。如果成功,將返回指向此位置的指標。否則,將返回一個空指標。 |
char* strrchr(const char* s, int c):
|
反向搜尋字串 s 中 c 的首次出現。也就是說,它找到 s 中的最後一個 c。需要注意的一點是:strchr 和 strrchr 認為終止的空字元是 s 的一部分,用於搜尋目的。 |
char* strstr(const char* str, const char* sub):
|
在 str 中找到 sub 的首次出現並返回指向此出現位置的開頭的指標。如果 sub 不出現在 str 中,則返回空指標。 |
int memcmp(const void *ptr1, const void *ptr2, size_t len)
|
/* 使用 set 中的字元作為分隔符對 str 進行標記 */ |
void* memcpy(void *dest, const void *src, size_t len)
|
/* 使用 set 中的字元作為分隔符對 str 進行標記 */ |
void* memchr(const void *ptr, int val, size_t len )
|
/* 使用 set 中的字元作為分隔符對 str 進行標記 */ |
for (i = 0; i < this->length; i++)
*this->str++ = *source++;
*this->str = '\0';
this->str -= this->length;
return(this);
} /* end of String String_Create(const char*) */
現在是時候釋放由 *this 指向的記憶體區域並將它返回到空閒儲存中以供重複使用。在 C 中,這可以通過幾個函式來完成,其中之一是 free。
我們的解構函式誠然非常簡單。事實證明,堆記憶體是 String 資料型別使用的唯一資源。對於其他資料型別可能並非如此。它們可能儲存對檔案、訊號量等資源的控制代碼。
給定struct _STR的定義,我們有以下記憶體佈局
*this被稱為控制代碼。使用指標(struct _STR *),換句話說,間接定址,是必要的,以便將使用者與表示的潛在變化隔離開。即使實現者改變了底層表示,使用者也不會受到影響。[5] 因為她無法直接訪問表示。她擁有的只是一個“String物件的控制代碼”,而不是String物件本身。
指標指向的區域只有在該區域中所有指標指向的內容都被釋放後才能被釋放。這就是為什麼我們應該首先釋放(*this)->str,然後釋放*this。有一點需要注意:被釋放的是指標指向的區域,而不是指標本身!
當控制到達 return 語句時,我們得到下面的圖。陰影區域表示返回給分配器的記憶體;這些區域可以在隨後的分配請求中被重新使用。因此,嘗試使用已釋放的記憶體是不明智的。
觀察解構函式的簽名:唯一的引數型別是String *,而不是String。這種修改是為了確保將NULL賦值給已刪除物件的控制代碼是永久性的。我們在解構函式中進行此賦值的原因是,以減輕使用者(s)在呼叫解構函式後必須進行此賦值的負擔,因為 - 由於有許多使用者關心模組提供的功能,而只有一個實現者應該關心模組的實現方式 - 這無疑會更容易出錯。[6]
void String_Destroy(String* this) {
if (*this == NULL) return;
free((*this)->str);
(*this)->str = NULL;
free(*this);
*this = NULL;
} /* end of void String_Destroy(String*) */
int String_Compare(const String this, const String str2) {
return(strcmp(this->str, str2->str));
} /* end of int String_Compare(const String, const String) */
String String_Concat(const String this, const String str2) {
String res_str;
res_str = (String) malloc(sizeof(struct _STR));
if (!res_str) {
fprintf(stderr, "Out of memory...\n");
return(NULL);
} /* end of if (!res_str) */
我們的表示有兩層:一層用於底層字串的資訊,一層用於字串本身。這意味著我們需要發出兩個獨立的記憶體分配命令,每個層級一個。因此,在程式中的這個點,我們有上面的記憶體佈局。length 和 str 欄位中存在隨機值,這些值可能是之前使用分配給它們的區域時留下的。
res_str->length = this->length + str2->length;
res_str->str = (char *) malloc(res_str->length + 1);
if (!res_str->str) {
free(res_str);
fprintf(stderr, "Out of memory...\n");
return(NULL);
} /* end of if (!res_str->str)*/
當控制到達此點時,我們將已經為 length 欄位分配了準確的值,並分配了足夠多的記憶體來儲存兩個引數的連線。注意,str 指向的內容是隨機的。但這並不意味著它們不能被使用。將 strlen 函式應用於 str 仍然會產生合法的返回值,具體取決於第一個“/0”在記憶體中出現的位置。
while (*this->str) *res_str->str++ = *this->str++;
最後,我們將第一個引數複製到結果String中。好吧,幾乎!每次我們將一個字元從this->str複製到res_str->str時,我們都會將指標向前移動,使其指向下一個要複製的字元。當我們到達源String 的末尾 (this->str) 時,這兩個指標都將比它們最初的內容大源字串的長度。對於res_str->str 來說,這是可以的,因為還有第二個字串要附加到它。但是,this->str 的原始值需要被恢復,這可以透過以下複合賦值語句來完成。
如果我們使用一個臨時指標指向與this->str 相同的位置,並使用這個臨時指標複製源String,我們就可以免去這種重新調整。此程式碼如下所示
String String_Concat(const String this, const String str2) { String res_str; char* temp_str = this->str; ... ... while (*temp_str) *res_str->str++ = *temp_str++; ... ... } /* end of String String_Concat(const String, const String) */
this->str -= this->length;
while (*str2->str) *res_str->str++ = *str2->str++;
str2->str -= str2->length;
*res_str->str = '\0';
res_str->str -= res_str->length;
return(res_str);
將引數Strings 複製到結果String 中後,我們現在返回到呼叫者。請注意,我們返回指向堆中某個物件的指標。這使我們有機會在函式呼叫之間共享同一個物件,而如果我們選擇使用駐留在執行時棧中的物件,我們將無法做到這一點:駐留在執行時棧中的物件只能在建立它的函式以及從該函式直接或間接呼叫的函式中訪問。
這可能會讓我們認為,返回指向靜態資料區域或主函式幀中某個物件的指標是安全的選擇。就物件的生命週期而言,這是正確的。[7] 但現在我們面臨著靜態分配帶來的限制:在這些區域中分配的物件不能動態地改變大小。在靜態資料區域中建立的物件的大小在編譯時是固定的,而執行時棧中建立的物件的大小必須在它的定義被展開時固定。這意味著,實現動態資料結構的唯一方法是使用堆記憶體。[8], [9]
} /* end of String String_Concat(const String, const String) */
int String_Contains(const String this, const String substr) {
int i;
unsigned int j;
for(i = 0; i <= this->length - substr->length; i++) {
for (j = 0; j < substr->length; j++)
if (substr->str[j] != this->str[i + j]) break;
if (j == substr->length) return(i);
} /* end of outer for loop */
return(SUBSTR_NOT_FOUND);
} /* end of int String_Contains(const String, const String) */
int String_Containsr(const String this, const String substr) {
String this_rv = String__Reverse(this);
String sub_rv = String__Reverse(substr);
int where;
if (this_rv == NULL || sub_rv == NULL) {
fprintf(stderr, "Out of memory...\n");
return(OUT_OF_MEMORY);
} /* end of if (this_rv == NULL || sub_rv == NULL) */
反向搜尋可以用正向搜尋來表達。這正是我們在這裡所做的。我們在反轉後的字串中對反轉後的子字串進行正向搜尋,並根據搜尋結果返回一個值。
where = String_Contains(this_rv, sub_rv);
String_Destroy(&sub_rv);
String_Destroy(&this_rv);
if (where >= 0) return(this->length - substr->length - where);
else return(SUBSTR_NOT_FOUND);
} /* end of int String_Containsr(const String, const String) */
unsigned int String_Length(const String this) {
return(this->length);
} /* end of unsigned int String_Length(const String) */
String String_Substring(const String this, unsigned int start, unsigned int len) {
String res_str;
unsigned int i;
if (start >= this->length || len == 0) return(String_Create(""));
if (start + len > this->length) len = this->length - start;
res_str = (String) malloc(sizeof(struct _STR));
if (res_str == NULL) {
fprintf(stderr, "Out of memory...\n");
return(NULL);
} /* end of if (res_str == NULL) */
res_str->str = (char *) malloc(len + 1);
if (res_str->str == NULL) {
free(res_str);
fprintf(stderr, "Out of memory...\n");
return(NULL);
} /* end of if (res_str->str == NULL) */
for (i = 0; i < len; i++)
res_str->str[i] = this->str[start + i];
res_str->str[i] = '\0';
res_str->length = len;
return(res_str);
} /* end of String String_Substring(const String, unsigned int, unsigned int) */
以下是我們所說的使用者定義的轉換函式:它將String 轉換為char*。與 C++ 不同,C++ 中編譯器會隱式呼叫此類函式,此轉換函式必須由程式設計師顯式呼叫。
char* String_Tocharp(const String this) {
char *res_str = (char *) malloc(this->length + 1);
if (!res_str) {
fprintf(stderr, "Out of memory...\n");
return(NULL);
} /* end of if(!res_str) */
strcpy(res_str, this->str);
return(res_str);
} /* end of char* String_Tocharp(const String) */
static String String__Reverse(const String this) {
String str_reverse;
unsigned int i;
str_reverse = (String) malloc(sizeof(struct _STR));
if (!str_reverse) {
fprintf(stderr, "Out of memory...\n");
return(NULL);
} /* end of if (!str_reverse) */
str_reverse->str = (char *) malloc(this->length + 1);
if (!str_reverse->str) {
free(str_reverse);
fprintf(stderr, "Out of memory...\n");
return(NULL);
} /* end of if (!str_reverse) */
str_reverse->length = this->length;
for (i = 0; i < this->length; i++)
str_reverse->str[i] = this->str[this->length - 1 - i];
str_reverse->str[this->length] = '\0';
return(str_reverse);
} /* end of String String__Reverse(const String) */
測試程式
[edit | edit source]#include <stdio.h>
#include <stdlib.h>
#include "General.h"
#include "utility/String.h"
int main(void) {
char *sztmp;
int loc;
String first_name, last_name, name;
String strtmp, strtmp2;
first_name = String_Create("Tevfik");
為了列印 String 變數的內容,我們將它轉換為 C 風格的字元字串,並將該字串傳遞給 printf 函式。但我們必須注意避免建立垃圾。如果我們在不將其儲存在一個變數中,直接將 String_Tocharp 的返回值(指向 char 的指標)傳遞給 printf,那麼當 printf 返回時,該指標就會丟失。這將使此指標指向的記憶體區域變成垃圾。這是我們不希望看到的!出於這個原因,我們首先將返回值賦值給某個臨時變數,然後將其傳送給 printf 函式。一旦 printf 返回,我們就會使用 free 釋放臨時變數指向的區域。
printf("First name: %s", (sztmp = String_Tocharp(first_name)));
printf("\tLength: %d\n", String_Length(first_name));
free(sztmp);
last_name = String_Create("Aktuglu");
printf("Last name: %s", (sztmp = String_Tocharp(last_name)));
printf("\tLength: %d\n", String_Length(last_name));
free(sztmp);
printf("Forward search for u in the last name: ");
loc = String_Contains(last_name, strtmp = String_Create("u"));
if (loc == SUBSTR_NOT_FOUND)
printf("u not found...Sth wrong with the String_Contains function!!!\n");
else printf("u found at location %d\n", loc);
String_Destroy(&strtmp);
printf("Backward search for u in the last name: ");
loc = String_Containsr(last_name, strtmp = String_Create("u"));
if (loc == SUBSTR_NOT_FOUND)
printf("u not found...Sth wrong with the String_Containsr function!!!\n");
else printf("u found at location %d\n", loc);
String_Destroy(&strtmp);
name = String_Concat((strtmp = String_Concat(first_name, (strtmp2 = String_Create(" ")))), last_name);
String_Destroy(&strtmp); String_Destroy(&strtmp2);
printf("Name: %s", (sztmp = String_Tocharp(name)));
printf("\tLength: %d\n", String_Length(name));
free(sztmp);
strtmp = String_Substring(name, 0, String_Length(first_name));
printf("Comparing first name with the first %d characters of the name...", String_Length(first_name));
if (String_Compare(first_name, strtmp) == 0) printf("Equal\n");
else printf("Not equal\n");
String_Destroy(&strtmp); String_Destroy(&first_name);
String_Destroy(&last_name); String_Destroy(&name);
exit(0);
} /* end of int main(void) */
使用 GDB 除錯程式
[edit | edit source]一個編譯並連結沒有錯誤的程式並不一定意味著一切正常。 邏輯錯誤可能已經潛入程式,並且我們的可執行檔案可能會產生錯誤的結果。 在這種情況下,我們應該返回原始碼並嘗試修復故障。 這種方法的問題在於難以確定從何處開始搜尋惡意錯誤。 如果我們正在處理一個涉及數百個函式互動的大型專案,而所討論的邏輯錯誤出現在遠離其起源的地方怎麼辦? 我們最好找到一個更簡單的方法!
偵錯程式是我們解決問題的答案。 它允許我們逐步執行有問題的軟體,動態修改程式碼,並查明潛在的故障點。 以下是 GNU 偵錯程式 GDB 的介紹。
編譯用於除錯的源程式
[edit | edit source]為了除錯程式,您需要在編譯程式時生成除錯資訊。 此除錯資訊儲存在目標檔案中; 它包含描述每個變數和函式的型別、原始碼行號與可執行程式碼中的地址之間的對應關係等元資料。
要請求將除錯資訊插入到目的碼中,您必須在執行編譯器時指定“-g”選項。 因此,String_Test.c 將由以下命令執行
gcc –c –g –ID:\include String.c↵[10]gcc –o String_Test.exe –g –ID:\include String_Test.c↵- String_Test↵
[在繼續之前,應該強調的是,由於某些最佳化會涉及程式碼消除/修改,這意味著您除錯的原始碼與偵錯程式顯示的內容可能不匹配,因此啟用最佳化以及除錯選項不是一個好主意。 因此,建議您首先關閉最佳化(並開啟除錯開關),然後,當您確信程式碼經過全面測試並且可以上市時,開啟最佳化。]
如果程式中可能出現問題,請發出下一行給出的命令。
gdb String_Test.exe↵
此命令將使您進入 GDB 環境,並等待您發出 GDB 命令。 您還可以除錯崩潰的程序,並嘗試透過傳遞程序的 core dump[11] 作為額外引數來找出問題所在。 還可以監控正在執行的程序。 因此,gdb 命令可以概括如下
gdb [''executable_file'' [''core_dump'' | ''process_id'']]↵
從上述規範可以看出,gdb 可以不帶任何引數發出。 在這種情況下,您必須透過 GDB 命令提供引數值。
gdb String_Test.exe↵- ≡
gdb↵- GNU gdb 5.0
- Copyright 2000 Free Software Foundation. Inc.
- ...
- (gdb)file String_Test.exe↵
類似地,可以使用core-file 命令提供 core dump,GDB 可以使用attach 命令附加到程序。
主要 GDB 命令
[edit | edit source]命令是單行輸入,沒有長度限制。 它以命令名稱開頭,後面跟著引數,引數的含義取決於命令名稱。
GDB 命令名稱始終可以縮寫,只要該縮寫是明確的。 對於常用的命令,一個沒有歧義解釋的縮寫肯定會執行預期命令。 [12]
將空行作為輸入輸入到 GDB(只輸入 RET)通常意味著重複上一個命令。 # 用於開始單行註釋; 從 # 到行尾的任何內容都被視為註釋,因此被丟棄。
從 GDB 獲取幫助
[edit | edit source]在 GDB 中有如此多的命令,人們可能難以記住某個特定的命令。 在這種情況下,您可以向 GDB 詢問其命令的資訊。 您需要了解的是help[13] 命令,它有三種形式
- help
- 顯示命令類的簡短列表。
- help command_class
- 顯示在特定command_class 中找到的命令列表。
- help command
- 顯示有關如何使用特定命令的簡短段落。
- apropos reg_exp
- 在所有 GDB 命令及其文件中搜索reg_exp 中指定的正則表示式。 例如,
- (gdb) apropos local var*↵
- backtrace -- 列印所有堆疊幀的回溯
- bt -- 列印所有堆疊幀的回溯
- info locals -- 當前堆疊幀的區域性變數
- info locals -- 當前堆疊幀的區域性變數
- where -- 列印所有堆疊幀的回溯
- complete command_prefix
- 列出所有以command_prefix 開頭的可能命令。
GDB 可以為您在命令中填入單詞的其餘部分,如果只有一種可能性; 它還可以隨時向您顯示命令中下一個單詞的可能性。 為了讓 gdb 進行命令補全,您必須按 TAB 鍵。 如果沒有歧義,gdb 將填入單詞並等待您完成命令。 否則,它將發出警報,告訴您可能性。 在這一點上,您可以要麼提供更多字元並再次嘗試,要麼只按兩次 TAB 鍵。 按兩次 TAB 鍵將顯示以您輸入的字首開頭的所有命令名稱,這實際上是 complete 命令提供的。 例如,
- (gdb) h TAB TAB
- handle hbreak help
- (gdb) he TAB → (gdb) help
停止執行
[edit | edit source]除非您另行說明,否則程式啟動後將執行至完成。 對於正在除錯的程式,這種行為不太有幫助。 我們應該能夠在某些點停止以檢查程式狀態,遍歷程式語句,並可能透過干擾程式的控制流來修改程式碼。
可以在程式的某些點停止,方法是設定斷點。 在 GDB 中,這是透過break 命令完成的。 break 可以以多種不同的方式使用。 [14]
- break (func_name | filename:func_name)
- 在名為func_name 的函式的入口處設定斷點 [檔名為filename]。
- break (line_num | filename:line_num)
- 在第line_num 行設定斷點 [檔名為filename]。
- break *address
- 在地址address 處設定斷點。 * 字首用於表示後面的是要解釋為地址值。
- break (+ | -) offset
- 使用引數值作為偏移量,此命令在當前原始碼行相對於當前原始碼行的位置設定斷點。
- break
- 在要執行的下一條指令處設定斷點。 等同於break +0.
- break location if condition
- 在某個位置設定斷點,該位置可以使用不同的方式提供,如上所述。 只有當condition 評估為非零值時,GDB 才會接管程式(即程式將停止)。 請注意,斷點的無條件版本可以使用始終評估為非零值的條件來表示。 也就是說,以前的斷點命令等同於break location if n,其中n 是一個非零值。
上述一組替代用法可以簡潔地定義為
- break [location] [if condition]
其中 [ 和 ] 用於表示封閉的語法單元的零個或一個例項。
- tbreak location [if condition]
- 設定一個臨時斷點,該斷點將在程式第一次在指定為引數的位置停止後被刪除。 通常,您會在程式的入口點設定這樣的斷點。
- hbreak location [if condition]
- 設定硬體輔助斷點。 由於缺乏硬體支援,GDB 可能無法設定此類斷點。 即使支援,用於此目的的暫存器數量可能不足以滿足您的需求。 在這種情況下,您必須刪除或停用未使用的斷點,並將其重新用於新的斷點。
- thbreak location [if condition]
- 設定一個臨時的硬體輔助斷點。
- rbreak reg_exp
- 在與正則表示式reg_exp 匹配的所有函式上設定斷點。 請記住,您提供的正則表示式有一個隱式的“.*”前導和尾隨。 因此,在除錯 String_Test.exe 時,
- rbreak .*Cont.*↵ 和 rbreak Cont↵
- 是等價的,它們都將在
String_Contains處設定斷點,並且.String_Containsr
- 是等價的,它們都將在
觀察點是一種特殊的斷點,當表示式的值更改時,它會停止程式。 這使您免受預測此類更改可能發生的場所的負擔。 它有三種形式
- watch expr
- 設定一個觀察點,只要程式寫入expr 並更改其值,它就會中斷。
- rwatch expr
- 設定一個觀察點,只要程式讀取expr,它就會中斷。
- awatch expr
- 設定一個監視點,當程式訪問(讀取或寫入)expr 時,它將中斷。
捕獲點 是一種特殊的斷點,當發生特定型別的事件時,它會停止您的程式,例如丟擲 C++ 異常或載入庫。
無條件斷點,無論是斷點、監視點還是捕獲點,都可以透過condition命令轉換為條件斷點。
- condition breakpoint_num [expression]
- 將expression 作為條件新增到由breakpoint_num 指定的斷點。如果沒有表示式部分,則將刪除為斷點設定的任何條件。也就是說,它將成為一個普通的無條件斷點。
這裡,斷點號是一個索引,用於引用特定的斷點。可以透過發出info breakpoints命令找到它。
可以為任何斷點提供一系列命令,在程式因該斷點而停止時執行。
- commands [breakpoint_num]
- command1
- command2
- ...
- commandn
- end
- 缺少斷點號會導致命令附加到最後設定的斷點(不是遇到的斷點!)。在commands 和斷點號後加上 end 可以輕鬆地從斷點中刪除命令列表。
commands 的效果可以透過display 命令部分獲得,該命令將它的引數表示式新增到一個稱為自動顯示列表中。每次程式停止時都會列印此列表中的所有專案。
在確定了程式中的問題後,我們可能希望刪除斷點。這可以透過使用clear 和delete 命令來完成。
- clear
- 刪除在即將執行的指令處設定的斷點。
- clear (function | filename:function)
- 刪除在作為引數傳遞的函式入口處設定的斷點。
- clear (line_num | filename:line_num)
- 刪除在指定行程式碼處或程式碼內設定的任何斷點。
- delete [breakpoints] [list_of_breakpoints]
- 刪除作為引數傳遞的斷點。如果沒有引數,它將刪除所有斷點。
與其刪除斷點,我們可能會選擇忽略或停用它。這樣的斷點將存在但無效,等待恢復。
- ignore breakpoint_num ignore_count
- 導致由breakpoint_num 引用斷點被繞過ignore_count 次。
- disable [breakpoints] [list_of_breakpoints]
- 導致給定的斷點被忽略,直到相關聯的啟用命令。如果沒有提供列表,則所有斷點都將被停用。
- enable [breakpoints] [list_of_breakpoints]
- 啟用之前停用的斷點列表。
- enable [breakpoints] once list_of_breakpoints
- 僅啟用給定的斷點列表一次。
- enable [breakpoints] delete list_of_breakpoints
- 啟用給定的斷點列表以工作一次,然後停止。一旦程式在該斷點處停止,GDB 將刪除列表中的任何斷點。
恢復執行
[edit | edit source]一旦程式停止,它可以透過不同的方式恢復。以下是這些命令的列表
- next [no_of_repetitions]
- 繼續到下一行原始碼。如果要執行的當前行包含函式呼叫,則它將在單個步驟中執行,而不會進入它。換句話說,它永遠不會增加執行時堆疊的深度。提供的引數告訴 GDB 執行next 命令的次數。
- step [no_of_repetitions]
- 繼續到下一行原始碼。與next 不同,如果要執行的當前行包含函式呼叫,則將插入一個新的堆疊幀,並且控制權將流入被呼叫函式中的第一個可執行語句。
- nexti [no_of_repetitions]
- 執行下一條機器指令並返回偵錯程式。如果下一條指令是函式呼叫,則它會執行整個函式,然後返回。
- stepi [no_of_repetitions]
- 執行下一條機器指令並返回偵錯程式。
- continue [ignore_counts]
- 繼續執行程式直到下一個斷點。當傳遞一個引數時,它意味著偵錯程式將忽略在最近遇到的位置設定的斷點ignore_count - 1 次。因此,continue 等效於continue 1。
- until [location]
- 當傳遞一個引數時,until 會繼續執行程式,直到達到指定的 location 或當前函式結束。當不帶引數使用時,location 被假定為當前行。它非常有用,可以避免逐步遍歷迴圈。
- finish
- 繼續運行當前函式直到完成。
- return [ret_value]
- 立即返回呼叫方,而不執行被呼叫方(即當前函式)中剩餘的語句。如果它是一個返回值函式,則ret_value 被解釋為被呼叫方的返回值。
- call expr
- 評估作為其唯一引數傳遞的表示式,而不改變當前位置。請注意,由於評估導致的副作用是永久性的。
- jump (line_no | *address)
- 在引數中指定的行或地址處恢復執行。
下圖顯示了這些命令的效果。虛線用於表示狀態發生變化,其中狀態轉換的效果是透過訪問所有中間狀態來實現的。例如,“完成 f1”表示執行 f1 中剩餘的語句,然後返回,這由虛線表示。“從 f2 返回”由一條直線表示,表示它跳過 f2 中的所有語句並將控制權返回到 main。

跟蹤正在執行的程式
[edit | edit source]有時,停止程式的行為本身可能會影響程式的正確性,或者可能會降低服務質量。前者的例子是即時程式,例如嵌入式系統中的程式;後者的例子是應該全天候執行的伺服器。在這種情況下,我們可能會選擇使用 GDB 中的trace 和collect 命令。使用這些命令和其他一些命令,我們可以指定程式中的位置,並導致在這些位置發生某些操作,例如資料收集。這些位置稱為跟蹤點。稍後檢查在跟蹤點收集的資料,以檢視程式中出現了什麼問題。
- trace location
- 在指定位置設定一個跟蹤點。設定跟蹤點或更改其命令直到下一個tstart 命令才生效。
- tstart
- 啟動跟蹤實驗並開始收集資料。
- tstatus
- 顯示當前跟蹤資料收集的狀態。
- info tracepoints [tracepoint_num]
- 顯示有關特定跟蹤點的資訊。如果沒有引數,它將顯示有關迄今為止定義的所有跟蹤點的資訊。在其他資訊項中,它提供了每個跟蹤點的系統分配號。
- actions [tracepoint_num]
- 定義在命中編號為tracepoint_num 的跟蹤點時要執行的操作列表。如果沒有引數,將使用最近定義的跟蹤點。
- action 塊有一些特殊的命令。這些命令是collect 和while-stepping。
- collect expr [, expression_list]
- 命中跟蹤點時收集給定表示式(s)的值。除了根據源語言語法規則形成的表示式外,還可以使用以下之一。
- $regs 收集所有暫存器。
- $args 收集所有函式引數。
- $locals 收集所有區域性變數。
- while-stepping n
- 在跟蹤點之後執行n 次單步跟蹤,在每一步收集新資料。
一旦收集到足夠的資料,我們可以顯式地停止實驗(而不是程式),或者讓 GDB 使用每個跟蹤點的透過次數自行停止。
- tstop
- 結束跟蹤實驗並停止收集資料。
- passcount [n [tracepoint_num]]
- 設定跟蹤點的透過次數。透過次數是跟蹤點可以擊中的最大次數。因此,將跟蹤點的透過次數設定為n 將在第n 次命中特定跟蹤點時或之前停止跟蹤實驗。其他跟蹤點被擊中的次數以及它們的透過次數是多少,第一個被擊中的跟蹤點,其次數與其透過次數一樣多,將停止跟蹤實驗。缺少跟蹤點號假定最近定義的跟蹤點。如果沒有透過次數值,跟蹤實驗將繼續進行,直到使用者顯式停止。
每次命中跟蹤點時收集的資料稱為快照。這些快照從零開始連續編號,並進入緩衝區,以便您稍後檢查它們。
- tfind snaphot_num
- 檢索編號為snapshot_num 的跟蹤快照。引數可以透過以下幾種方式指定。
- start
- 查詢緩衝區中的第一個快照。它是 0 的同義詞。
- none
- 停止除錯快照並恢復即時除錯。
- end
- 與 none 相同。
- ENTER
- 查詢緩衝區中的下一個快照。
- -
- 查詢緩衝區中的上一個快照。
- tracepoint_num
- 查詢與tracepoint_num 關聯的下一個快照。
- pc addr
- 查詢其 pc(程式計數器)值為addr 的下一個快照。
- outside addr1, addr2
- 查詢其 pc 在給定範圍之外的下一個快照。
- line [filename
- ]n
- 查詢與特定原始碼行關聯的下一個快照。
跟蹤實驗通常涉及以下步驟。
- 使用trace 命令設定跟蹤點。
- 使用actions,將操作列表附加到跟蹤點。
- 如果需要,為跟蹤點設定透過次數。
- 對所有其他資料收集點重複 1-3。
- 透過使用以下方法啟動跟蹤實驗tstart.
- 使用tstop停止跟蹤實驗,或者等待跟蹤點到達其(跟蹤點)的計數來停止跟蹤實驗。
- 使用tfind檢查實驗期間收集的值。
每次被除錯的程式停止時,gdb都會列印要執行的行。這可能不足以讓你瞭解上下文。在這種情況下,你需要獲取當前行周圍的程式行的列表。你可以透過使用list命令來實現這一點。
- list [行號]
- 當傳遞引數時,list會列印以引數指定的行號為中心的幾行。這個值可以透過給出[特定檔案中的]行號、[特定檔案中的]函式名、地址或偏移量來指定。[15] 不帶引數時,list列印最後列印的行之後的幾行。傳遞+作為引數與不傳遞引數的效果相同。傳遞-作為引數會列出最後列印的行之前的幾行。
- list 起始行 , 結束行
- 當傳遞兩個引數時,list會列印起始行和結束行之間的原始碼行。[16] 如果其中一個引數缺失,且逗號仍然存在,則該缺失的值將由另一個引數確定。
除了列出原始檔的某些部分之外,你可能還希望檢視變數的值或評估表示式。
- print [/格式] 表示式
- 評估表示式並使用格式列印結果。格式規範可以是以下任一格式
- d 將值解釋為整數並以有符號十進位制形式列印。
- u 將值解釋為整數並以無符號十進位制形式列印。
- x 將值解釋為整數並以十六進位制形式列印。
- o 將值解釋為整數並以八進位制形式列印。
- t 將值解釋為整數並以二進位制形式列印。
- c 將值解釋為整數並以字元常量形式列印。
- a 將值視為地址並列印。
- f 作為浮點值列印。
- 如果缺少格式規範,gdb將使用適當的型別。inspect是該命令的同義詞。
- 表示式可以根據源語言的語法規則來形成。我們可以使用某些運算子來擴充套件這種語法。@將記憶體的一部分視為陣列;::允許你指定識別符號的作用域。例如,假設
seq是int*,
- 表示式可以根據源語言的語法規則來形成。我們可以使用某些運算子來擴充套件這種語法。@將記憶體的一部分視為陣列;::允許你指定識別符號的作用域。例如,假設
- print *sum_all::seq@10↵
- 將
seq指向的記憶體區域(它屬於名為sum_all的函式)視為大小為10的陣列並列印該陣列。
- 將
每次表示式被評估和列印時,它也會被儲存到一個稱為值歷史記錄的地方。值歷史記錄中的值可以透過在列印順序之前加上$符號來引用。第一個被列印的值是$1;第二個值是$2等等。單個$引用最近列印的值,而$$引用前一個值。$$n引用從末尾開始的第n個值。因此,$$0等效於$。需要記住的是,值歷史記錄儲存表示式的值,而不是表示式本身。也就是說,假設*name的值為"Mete",那麼在列印*name後儲存到值歷史記錄中的是"Mete",而不是"*name"。
可以使用values子命令來檢查值歷史記錄中的值,該子命令屬於show命令。
- show values [ n | + ]
- 列印以n為中心的十個值歷史記錄條目。如果未傳遞引數,則列印最後十個值。如果提供+作為引數,則該命令會列印最後列印的值之後的十個歷史記錄值。
如果你不想將評估結果記錄到值歷史記錄中,應該使用x命令。
- x [[/重複次數 格式 單位] 地址]
- 檢查從地址開始的記憶體,獨立於底層實際資料型別。除了在print中使用的那些之外,格式可以是s(表示字串)或i(表示機器指令)。重複次數和單位用於確定要顯示的記憶體量:重複次數 * 單位位元組。重複次數是一個十進位制值,而單位可以是以下任一單位
- b 位元組。
- h 半字(兩個位元組)。
- w 字(四個位元組)。
- g 巨字(八個位元組)。
- 注意,任何一個引數都可能缺失。格式最初預設為x,並在每次使用x或print時更改。重複次數預設為1。為特定值指定的單位大小將被接受為下次使用相同格式時的預設值。
當你的程式停止時,你可能想知道它在哪裡停止以及它是如何到達那裡的。這需要有關執行時堆疊的資訊,這些資訊由以下命令提供。注意,這些命令不會更改程式的控制流。也就是說,在執行以下命令組合之後發出next仍然會從最近命中的斷點繼續執行。換句話說,以下命令都不會修改指令指標。
- frame [幀號 | 地址]
- 將你從一個堆疊幀移動到另一個堆疊幀,並列印有關你選擇的堆疊幀的資訊。可以透過傳遞幀的地址或編號來選擇堆疊幀。幀號是一個非負數,其中當前執行的幀為0,它的呼叫者為1,依此類推。
- backtrace [[(- | +)] 幀數]
- 列印程式到達當前位置的摘要。不帶引數使用時,該命令會列印整個堆疊的回溯;帶正數時,它會列印最內層(最近)的n個堆疊幀;帶負數作為引數時,它會列印最不最近的n個堆疊幀。該命令的同義詞是info stack。
- up [n]
- 向上移動n個幀。也就是說,它向較不最近建立的幀移動。
- down [n]
- 向下移動n個幀。也就是說,它向較最近的堆疊幀移動。
除了這些之外,我們還可以透過使用info子命令來獲取更多資訊。
- info frame [地址]
- 列印幀的描述,而不選擇它。
- info args
- 列印所選幀的引數。
- info locals
- 列印所選幀的區域性變數。
- info variables [正則表示式]
- 顯示程式中使用的所有全域性變數和靜態變數名稱。如果傳遞引數,它會顯示所有匹配正則表示式的這些資訊項。
- info scope (函式名 | *地址 | 行號)
- 列出某個作用域的區域性變數。此列表包含引數和區域性變數相對於幀基址的地址。
如果你不想使用上一節中介紹的[不完整!]命令列表帶來的額外靈活性,你可以嘗試使用以下偵錯程式之一,它們基本上是在gdb之上添加了一個圖形介面。
| 偵錯程式 | 描述 |
|---|---|
| rhide | 這個開發環境可以在djgpp和cygwin中使用。在任何一個環境的命令提示符下鍵入rhide,會進入一個dos視窗。注意,這是一個完整的IDE,它恰好包含一個基於gdb的偵錯程式。 |
| gnu ddd | 除了linux發行版之外,這個偵錯程式還可以從cygwin中使用。要從cygwin命令提示符啟動這個偵錯程式,你需要先發出一個xinit命令。之後,輸入ddd會開啟一個圖形偵錯程式視窗。 |
| insight | 隨紅帽linux發行版一起提供,這個偵錯程式可以透過在命令提示符下輸入insight來啟動。 |
| kdbg | 是suse linux發行版的一部分,這個偵錯程式可以透過在命令提示符下輸入kdbg來啟動。 |
除了這些之外,你還可以嘗試使用gnu emacs、xemacs、eclipse、kdevelop、netbeans和ms visual studio,它們也提供整合除錯支援。最後需要記住的一點是,並非所有偵錯程式/環境都支援所有除錯格式。例如,包含gdb除錯資訊的執行檔案對於ms visual studio來說毫無意義。
- ↑ 有一個很少使用的選項,可以顯式呼叫垃圾收集器。
- ↑ 但是,有諸如lint或splint之類的工具可以令人滿意地用於此目的。
- ↑ 例如,靜態分配的指標可以考慮列表的頭部。 動態分配的指標的示例可以考慮同一列表中的下一個節點連結資訊。 但是,無論指標是靜態分配還是動態分配,以及它指向的是靜態資料區域還是執行時堆疊,有一點是肯定的:動態分配的記憶體是透過指標進行操作的。
- ↑ 請注意,傳遞給
String_Create函式的引數可能指向三個區域中的任何一個區域的記憶體。 - ↑ 假設您透過函式使用底層物件。
- ↑ 有關後一種方法的示例,請參閱基於物件的程式設計章節。
- ↑ 畢竟,前者具有靜態範圍,而後者在進入main時分配並在退出時解除分配,這實際上等同於第一種情況。
- ↑ 我們仍然可以透過定義非常大的靜態資料結構並使用此[靜態結構]作為我們的堆來模擬動態資料結構。 實際上,這種方法用於在諸如90年前的FORTRAN和COBOL之類的程式語言中實現動態結構。 除了無法提供100%的溢位免疫力外,這種方法對記憶體的浪費相當大。
- ↑ 請注意,該圖未準確反映C呼叫約定。 由於執行時堆疊中的區域將被呼叫者丟棄,因此現在就劃掉下半部分為時過早。
- ↑ 應該注意的是,此命令和接下來的兩個命令可能是由兩個不同的方發出的:分別為實現者和使用者。 不預測除錯的可能性,或者[更可能]不願意提供有關實現的資訊,實現者可能不會為您提供物件模組的除錯版本。 在這種情況下,當控制流指向模組中找到的某個函式時,程式的原始碼級除錯是不可能的。
- ↑ 核心轉儲是包含崩潰時隨機存取記憶體內容的磁碟檔案,該檔案可以稍後用於找出故障原因。
- ↑ 在我們的演示中,命令縮寫將透過下劃線相關命令的部分來表示。
- ↑ 在本講義的其餘部分,我們將對命令的一部分進行下劃線,以表示代表該命令的最短可能字首。
- ↑ ( , ), [, ], | 是元字元;它們不是用於形成傳遞給命令的引數的語法的一部分。 ( 和 ) 用於分組;[ 和 ] 用於表示選項;| 用於分隔備選方案。
- ↑ 偏移值將新增到列印的最後一行。
- ↑ 與偏移值一起使用時,第二個引數將新增到將第一個引數新增到要列印的行中獲得的值中。