目錄
前言
本期內(nèi)容介紹
一、什么是Bug?
二、調(diào)試以及調(diào)試的重要性
2.1什么是調(diào)試?
2.2調(diào)試的基本步驟
?三、Debug和Release介紹
Debug和Release
四、windows環(huán)境下的調(diào)試介紹
4.1調(diào)試環(huán)境
4.2一些調(diào)試常用的快捷鍵
4.3調(diào)試時查看當前程序的信息
a、查看臨時變量的值
b、查看程序的內(nèi)存信息
c、查看程序的調(diào)用堆棧
d、查看程序的匯編信息
e、查看寄存器信息
五、一些調(diào)試實例
六、如何寫出“好”代碼?
6.1什么是好代碼?
6.2如何寫出好代碼?
assert介紹
const介紹
七、常見的錯誤解析
7.1編譯型錯誤
7.2鏈接型錯誤
7.3運行時錯誤
前言
我們平時在寫代碼的時候會出現(xiàn)各種錯誤,面對程序出現(xiàn)的錯誤我們之前好像沒有介紹過如何高效、科學、系統(tǒng)地處理這種錯誤!您平時一般是如何說處理的呢?走讀?猜?還是???無論哪種方式其實都是不是最高效且合理的!本期小編將介紹VS常用調(diào)試技巧!
本期內(nèi)容介紹
什么是Bug?
調(diào)試及調(diào)試的重要性
Debug和Release介紹
windows環(huán)境調(diào)試介紹
一些調(diào)試實例
如何寫出"好"代碼?
常見錯誤解析
一、什么是Bug?
Bug的意思是:飛蛾或昆蟲!在計算機中用Bug來代指一些未被發(fā)現(xiàn)的或隱藏的錯誤或缺陷!
下面是百度百科的介紹以及歷史上第一個Bug的圖片:
這是關(guān)于Bug的更詳細的鏈接程序錯誤_百度百科 (baidu.com)感興趣的可以看看!
二、調(diào)試以及調(diào)試的重要性
2.1什么是調(diào)試?
調(diào)試又稱除錯!是當程序或電子儀器出現(xiàn)了問題之后對程序或電子儀器進行科學排查、尋找問題以及解決問題的過程!
2.2調(diào)試的基本步驟
OK,我們來寫個有問題程序來走一遍上面步驟:(求n的階乘和)
int main()
{
int n = 0;
scanf("%d", &n);
int sum = 0, ret = 1;//sum存儲n的階乘和 ret用于存儲每一個的階乘
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= i; j++)
{
ret *= j;
}
sum += ret;
}
printf("sum = %d",sum);
return 0;
}
我們知道3!和為:1!+ 2! + 3! = 1 + 2*1 + 3* 2*1 = 9 我們來看結(jié)果:
怎么是15???這和我們的預(yù)期不一樣,是不是就是出現(xiàn)了Bug了!我們就得想辦法來解決這個問題!第一步:發(fā)現(xiàn)問題,結(jié)果和預(yù)期不一樣,已經(jīng)發(fā)現(xiàn)問題!第二步:定位問題:我們這里對變量sum 和 ret分別監(jiān)視,一一比對每一次sum、ret的值是否與預(yù)期一樣!第三步:確定錯誤原因:
這里確定了問題的原因,怎么解決呢?我們發(fā)現(xiàn)這里應(yīng)該對每一個i的階乘都要單獨算,要就是說每一個ret都應(yīng)該獨立的,每一次的ret都應(yīng)該是沒有被用過的!那我們在每次計算每個i的時候?qū)et一開始先置 1即可!第四步:解決問題
int main()
{
int n = 0;
scanf("%d", &n);
int sum = 0, ret = 1;//sum存儲n的階乘和 ret用于存儲每一個的階乘
for (int i = 1; i <= n; i++)
{
ret = 1;//防止后面的值重復(fù)疊加
for (int j = 1; j <= i; j++)
{
ret *= j;
}
sum += ret;
}
printf("sum = %d",sum);
return 0;
}
OK,第五步:重新測試:
三、Debug和Release介紹
我們的VS編譯器上會經(jīng)??吹揭粋€東西是Debug!
其實不止有Debug還有一個是Release!
他們是什么呢?下面我們就來介紹一下Debug和Release:
Debug和Release
Debug:
Debug被稱為調(diào)試版本,它包含了調(diào)試信息,不做任何優(yōu)化,用于程序員調(diào)試解決問題!
Release:
Release被稱為發(fā)布版本,它往往是對程序進行了優(yōu)化,是的代碼在運行速度以及空間大小都是最優(yōu)的!用于用戶使用的!
無論是Debug還是Release只要編譯就會有相應(yīng)的文件包,(就和Java中的.class類似,一個類編譯一次有一個.class文件):
只在Debug編譯:
再在Release下編譯:
由于Release是用戶使用的所以不能用來調(diào)試,而Debug是對程序員的所以可以用來調(diào)試!
OK。我們來看一段代碼看看在兩個版本下的差異:
Debug:
int main()
{
int i = 0;
int arr[10] = { 0 };
for (i = 0; i <= 12; i++)
{
arr[i] = 0;
printf("hehe\n");
}
return 0;
}
我們先不管結(jié)果為什么是這樣!我們先來看看兩者結(jié)果的差別?。?/p>
這可不是結(jié)束了,這就是典型的死循環(huán)?。?!為什么死循環(huán)我們不管,待會分析!
再來看看Release版本下的結(jié)果:
這就很好的說明了release版本對程序的優(yōu)化!??!具體如何優(yōu)化的這個得問問微軟的開發(fā)工程師~
四、windows環(huán)境下的調(diào)試介紹
4.1調(diào)試環(huán)境
本期所有調(diào)試都是在VS2019的Debug下進行的?。。?/span>
4.2一些調(diào)試常用的快捷鍵
這里應(yīng)該最清楚的一個就是:ctrl+F5了我們執(zhí)行代碼就是這個,他其實是開始執(zhí)行不調(diào)試?。?!除此之外還有很多快捷鍵!我們下面介紹幾個最常用的!
F5
啟動調(diào)試,常用于直接跳到下一個斷點處?。ㄟ@個斷點是邏輯斷點,非物理斷點)
F9
創(chuàng)建和取消斷點,然后F5直接跳過去到當前斷點位置,然后就可以一步一步調(diào)試觀察是否和預(yù)期一樣!
所以,F(xiàn)5和F9是一起配合使用的!
F10
逐過程,通常是用來處理一個過程例如一次函數(shù)調(diào)用或一條語句!
F11
逐語句,每一次只能執(zhí)行一條語句,可以進到函數(shù)內(nèi)部!
如果您在調(diào)試的時候,上面的鍵按一下不起作用的話,有可能是Fn打開了,關(guān)掉Fn即可或者是上面的鍵+Fn配合使用即可?。。?/strong>
下面是知乎的一篇VS2019的調(diào)試技巧快捷鍵介紹:VS2019調(diào)試快捷鍵大全
4.3調(diào)試時查看當前程序的信息
a、查看臨時變量的值
在開始調(diào)試之后如何查看臨時變量的值!其實我們上面已經(jīng)用到了,只不過剛剛沒有介紹!
我們按下F10之后,啟動調(diào)試后,點擊調(diào)試,進去選擇窗口找到監(jiān)視,有四個窗口隨便一個都可以!
OK,我們在來一個看一個代碼順便調(diào)試看一下臨時變量!
void swap(int x, int y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 3;
int b = 5;
printf("交換前:a = %d b = %d\n", a, b);
swap(a, b);
printf("交換后:a = %d b = %d\n", a, b);
return 0;
}
這就是交換兩個數(shù)的代碼,我們先來看看結(jié)果!
似乎沒有交換,其實原因我們都清楚--》形參是實參的一份臨時拷貝,改變形參不改變實參!
這里我們假裝不知道,我們此時程序的結(jié)果和我們的預(yù)期不符合出了問題!就需要調(diào)試,我們來練習一下剛剛學的!
這就是F9打的斷點,我們分析程序是在swap函數(shù)出問題的,所以F9直接跳過沒問題的.然后F5到當前斷點來!
此時問題在函數(shù)里面,我們介紹過。F11是逐語句執(zhí)行,我們可以用它進入函數(shù)內(nèi)部,看看!
注意此時監(jiān)視x = 3, y = 5, a = 3, b = 5, 我們待會觀察a,b,x,y的變化情況!
這是調(diào)試結(jié)束的結(jié)果,此時x = 5, y = 3確實交換了,但a = 3, b = 5好像一點也沒變!我們結(jié)束打印的是a 和 b:
結(jié)果還是a = 3, b = 5;所以沒有交換成功!這里小編也就不廢話了,這里改變了形參形參是實參的一份拷貝,兩者的空間不同,改變形參實參不受影響!我們前面已經(jīng)不止一遍的介紹過這東西了,這里就不在嘮叨了!
b、查看程序的內(nèi)存信息
int main()
{
int a = 0x11223344;
return 0;
}
這段代碼的內(nèi)存布局以及a的值在內(nèi)存中是如何存的?我們一起來look一look:
還是先調(diào)試起來?。。?點調(diào)試--->窗口--->內(nèi)存-->4個窗口隨便一個!
這里輸入&a就可以看到,a的內(nèi)存布局以及a中的值在內(nèi)存中的存儲形式,我們前面介紹過倒著存是因為對當前平臺是小端存儲模式!
c、查看程序的調(diào)用堆棧
我們就用下面這個簡單的代碼演示一下:
void test2()
{
printf("hahaha\n");
}
void test1()
{
test2();
}
int main()
{
test1();
return 0;
}
這個代碼就是打印一個haha,但它是如何調(diào)用的?我們就可以來看看調(diào)用堆棧!
先調(diào)試起來?。?!再點擊調(diào)試-->窗口-->調(diào)用堆棧
一開始還沒有調(diào)用test1只有main函數(shù)的棧幀!
F11調(diào)用test1在main函數(shù)的上面開辟了test1的棧幀:
在調(diào)用test2就有在test1上面創(chuàng)建了test2的函數(shù)棧幀:
然后接下來打印完haha后就會把test2的函數(shù)棧幀銷毀,我們在函數(shù)棧幀的那一期介紹過,在調(diào)用一個函數(shù)的時候會把當前語句的下一條語句提前存起來,等棧幀銷毀的時候就直接執(zhí)行下一條語句了!所以test2帶哦萬就銷毀了,只剩test1和main函數(shù)的棧幀了!
然后會最后main函數(shù)的棧幀也會銷毀?。?!這里我又想起來了我在介紹函數(shù)棧幀的時候說過main函數(shù)是被其他函數(shù)調(diào)用的,但那時候操作不當在調(diào)用堆棧那里沒有找到!今天小編找到了!
在VS上調(diào)用main函數(shù)的函數(shù)是用C++寫的!當然這個具體實現(xiàn)得看編譯器,各個編譯器可能不一樣!
d、查看程序的匯編信息
先調(diào)試起來?。?!再點擊調(diào)試-->窗口-->反匯編
int maxNum(int x, int y)
{
return x > y ? x : y;
}
int main()
{
int a = 3;
int b = 5;
int ret = maxNum(a, b);
printf("max = %d", ret);
return 0;
}
紅色的框是在建立函數(shù)棧幀!這個我們函數(shù)棧幀介紹過!下面藍色的框是在創(chuàng)建變量!
這就是調(diào)用函數(shù)的指令!
這個就是maxNum的函數(shù)棧幀?。。?!的創(chuàng)建以及銷毀的反匯編!
e、查看寄存器信息
這個可能很多剛剛?cè)腴T的或者還沒入門的小伙伴都不知道!
寄存器(register)是cpu周圍的暫時放數(shù)據(jù)的地方,由于在cpu周圍所以訪問速度很快但空間不大!而且在寄存器上創(chuàng)建的變量是沒有地址的!?。?!
OK,我們來段代碼看看:
int main()
{
int a = 3;
register int b = 5;
return 0;
}
b就是一個寄存器的變量!他是沒有地址的!我們先對a取地址:
在對b 取地址:
在cpu周圍不需要開辟內(nèi)存直接可以使用?。?!
五、一些調(diào)試實例
(一)、代碼實現(xiàn):1! + 2! + ...+n!
int main()
{
int sum = 0;
int ret = 1;
int n = 0;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= i; j++)
{
ret *= j;
}
sum += ret;
}
printf("%d", sum);
return 0;
}
這段代碼我們其實一開始就介紹了,它的錯誤原因在于:每一次沒有對ret初始化為1,導致后面的ret把前面的結(jié)果疊加的乘了上去!??!我們上面也調(diào)試了,這里就不在調(diào)試了!正確代碼如下:
int main()
{
int sum = 0;
int ret = 1;
int n = 0;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
{
ret = 1;
for (int j = 1; j <= i; j++)
{
ret *= j;
}
sum += ret;
}
printf("%d", sum);
return 0;
}
(二)、下列程序打印機次haha?
int main()
{
int i = 0;
int arr[10] = { 0 };
for (i = 0; i <= 12; i++)
{
arr[i] = 0;
printf("haha\n");
}
return 0;
}
如果你以前沒有見過這道題或了解過地層相關(guān)的知識的話,這道題你可能100%會回答錯!你可能只會說這道題的問題不就是越界嘛!打印13個haha然后報錯!但事實真的是這樣嗎?我們看結(jié)果:
死循環(huán)了!??!問什么呢?我們先來找原因,其實如果之前看過函數(shù)棧幀的那一期的伙伴應(yīng)該會反映過來,在VS上,創(chuàng)建的變量一個里一個差兩行0hcccccccccc:
這里我們就不在用這種方式往下解釋了,我們用調(diào)試:
果然他也是差了兩行的ccccccccc,我們知道棧的使用規(guī)則是:先試用高地址,在使用低地址!而,數(shù)組的下標訪問是先用低地址,后用高地址!
所以數(shù)組訪問的地址路線如下:
會不會是在越界的時候把i改了呢?我們來監(jiān)視看看:
剛剛創(chuàng)建好變量和數(shù)組:
i==9是,馬上要越界了:
i == 12:
在i == 12,將arr[12] = 0;執(zhí)行完后i也變成了0,我們猜想會不是越界的時候到i=12時將i的值改為了0?換句話說i的地址和arr[12]是一樣的,他們是一塊空間?我們此時只需要看看他兩的地址即可:
果然一模一樣!?。?!我們上面的分析是正確的??!我在畫個圖來解釋一下上面:
那找到了問題,確定了原因,就得解決問題:
int main()
{
int arr[10] = { 0 };
for (int i = 0; i <= 12; i++)
{
arr[i] = 0;
printf("haha\n");
}
return 0;
}
這樣寫的話他i的創(chuàng)建在i的后面,前面的那塊空間一單越界就會報錯!
當然這種代碼在實踐中肯定是一般碰不到的!這個栗子僅僅是說明遇到問題了如何去解決!??!雖然在實踐中遇不到但不一定在筆試面試中碰不到:這道題曾經(jīng)就是一家叫Nice公司的筆試題:
這道提不就和剛剛這道題一樣嗎?只是換了數(shù)字而已!思路、問題、解決方法都是一樣的!?。?!
六、如何寫出“好”代碼?
6.1什么是好代碼?
我們經(jīng)常聽說好代碼,什么是好代碼呢?我們認為具備以下特征的代碼就是好代碼!
1、能正常運行
2、Bug很少
3、效率高
4、可維護性高
5、注釋清晰
6、可讀性高
7、文檔齊全
假設(shè)說你寫了個代碼,運行不了,或者即使運行起來了也全是Bug,你寫的代碼只有你自己你看的懂,不加注釋。你可能說只有自己看得懂那不就公司裁不了我了嗎?這樣想就太天真了!當代碼有幾十萬行時出了問題,你自己都控制不了的時候,,,你細品~。所以我們平時寫代碼要注重可讀性以及注釋!??!
6.2如何寫出好代碼?
這是一些寫好代碼的幾個tips:
1、使用assert
2、使用const
3、編碼風格規(guī)范
4、添加注釋
這里提到了assert和const,我們以前經(jīng)常用但沒有介紹過這兩個!下面我就先來介紹一下:
assert介紹
assert 中文意思就是斷言的意思。顧名思義就是判斷!他是一個宏!使用時要包含相應(yīng)的頭文件:#include <assert.h>
這是官網(wǎng)對他的介紹,人家明確說了這是一個宏?。?!長得像函數(shù)的宏!作用是斷言!如果它的判斷的那個表達式判斷失敗就會調(diào)用:abort這個函數(shù)!并終止程序!這個宏所顯示的消息與具體的實現(xiàn)庫有關(guān),一個編譯器和一個編譯器不一樣!但他至少顯示:斷言失敗的源文件名和對應(yīng)的行數(shù)!如果你包含了頭文件還是不能用就得添加上面我畫出來的#definede 那個了!加上就OK了!!
這里還提到了一個函數(shù):abort:
這個函數(shù)的作用是終止當前進程,進程是網(wǎng)絡(luò)那部分的東西,這里就不多介紹了!這里就理解為終止當前程序(異常終止)!調(diào)用這個函數(shù)時這個函數(shù)會捕捉去一個信號,如果沒有捕捉到,就會導致程序終止,并向平臺返回一個錯誤碼!程序被終止是不會破壞任何對象??!
OK!我們來寫個代碼用一下:
typedef struct Stu
{
char name[20];
int age;
}S;
void print(S* ps)
{
assert(ps);
printf("%d", ps->age);
}
void Modif(S* ps, int age)
{
assert(ps);
ps->age = age;
}
int main()
{
S s = { "張三",20 };
Modif(&s, 19);
print(&s);
return 0;
}
這就是assert的作用, 當然你可以按自己的需要判斷值是否符合預(yù)期~!
const介紹
const這是一個關(guān)鍵字,我們前面開始介紹常量和變量的時候說過,被const 修飾的變量是常變量!具有常量屬性但本質(zhì)是個變量(只不過這個變量被初始化以后就不能在被修改了)!例如:
#define M 10//#define定義的常量
int main()
{
int a = 3;//變量
const int b = 5;//const修飾的常變量
return 0;
}
因為b是被const修飾具有常量屬性不能被改,所以這里對他的值進行修改就會報錯:
而且也不可以用它來定義數(shù)組!我們知道定義數(shù)組時[ ]里面的是一個常量(變長數(shù)組除外),變量就不行,b是常變量所以他也不行?。?!
當然我們今天不是為了回憶以前學的,而是在此基礎(chǔ)上再進行拔高一層!比如const修飾指針的問題!先看如下代碼:
void test1()
{
int m = 3;
int n = 5;
int* p = &n;
*p = 20;
p = &m;
}
void test2()
{
int m = 3;
int n = 5;
const int* p = &n;
*p = 20;
p = &m;
}
void test3()
{
int m = 3;
int n = 5;
int* const p = &n;
*p = 20;
p = &m;
}
void test4()
{
int m = 3;
int n = 5;
const int* const p = &n;
*p = 20;
p = &m;
}
int main()
{
test1();
test2();
test3();
test4();
return 0;
}
上面的4個代碼分是對指針p的 * 兩邊都不用const修飾,在*左邊修飾右邊不修飾,右邊修飾左邊不修飾,以及兩邊都修飾!具體會出什么結(jié)果過呢?我們一個一個看一看!
test1兩邊都不修飾:
考慮此時的 m 和 n值是多少?
這個不怎么難一看就知道但我想說的是!這里有一行很不起眼的代碼你可能沒有注意到!就是倒數(shù)第二行!p = &m;你可能會說這不就是一行很簡單的賦值嗎?這有啥可介紹的!你這么想就可使有點外行了!這可是指針?。。?!有了指針可以直接修改的?。。±纾?/p>
void test1()
{
int m = 3;
int n = 5;
int* p = &n;
*p = 20;
p = &m;
printf("m = %d n = %d\n", m, n);
*p = 200;
printf("m = %d n = %d\n", m, n);
}
看結(jié)果:
這里你還覺得他僅僅賦值那么簡單嗎?是不是感覺他很不安全呀!的確指針使用不當很危險!所以在使用指針前對他進行檢查以及作相應(yīng)的修飾限制,例如const,下面就來看看被const此時的各種情況吧!
const在*左邊:
void test2()
{
int m = 3;
int n = 5;
const int* p = &n;
*p = 20;
p = &m;
}
這個就是const修飾*左邊的例子!如果被const修飾了左邊,會和不修飾有什么區(qū)別呢?我們想愛你編譯看看!
這貌似和const修飾一般的變量一樣!不能被修改,這個在左邊好像不能修改p指向那塊空間的值!那我們猜測:如果在右邊是不是不能改變p的值呢?我們來看看:
void test3()
{
int m = 3;
int n = 5;
int* const p = &n;
*p = 20;
p = &m;
}
還真和我們猜的一樣!再回到上面的栗子test2:
void test2()
{
int m = 3;
int n = 5;
const int* p = &n;
//*p = 20;
p = &m;
p = NULL;
}
既然不讓修改p指向的值的話,那我改變p的值,的確可以!
和這個類比一下,在右邊我不能改 p的值,那我可以改p指向的那塊空間的值吧!驗證一下:
果然!那我們可以再猜測一下是不是在兩邊修飾就是既不能修改p的值也不能修改p只向空間的值呢?試一試:
果然!這樣好像更安全了~的確是!我們現(xiàn)在就可以進行總結(jié)一下!
const 兩邊都不修飾
很不安全!??!只要拿到指針既可以修改指針也可以修改指針指向的空間的值!
const 修飾*左邊
不能對指針指向的那塊空間的值進行修改!但可以對指針的修改!
const 修飾*右邊
不能修改指針的值,但可以修改指針指向空間的值!
const 修飾兩邊
既不能修改指針的值,也不能修改指針指向空間的值!
OK,這三種修飾看具體情況使用?。。。?!我們剛剛上面的aeesrt的那個代碼是不是可以再來用優(yōu)化一下呢?
typedef struct Stu
{
char name[20];
int age;
}S;
void print(const S* const ps)
{
assert(ps);
printf("%d", ps->age);
}
void Modif(S* const ps, int age)
{
assert(ps);
assert(age);
ps->age = age;
}
int main()
{
S s = { "張三",20 };
Modif(&s, 19);
print(&s);
return 0;
}
print函數(shù)只是打印信息不需要改變ps的值以及ps指向空間的值!下面的修改函數(shù)是需要修改ps指向空間的值但不需要改變自身的值~,加上assert和const的代碼是不是更加健壯了!以后能用就多用?。?!另外多加注釋?。?!OK,下面我們就來完整的寫一個好代碼!模擬實現(xiàn)strcpy!這個函數(shù)不必多介紹了吧,我們已經(jīng)前面模擬實現(xiàn)了兩遍了!它的作用就是拷貝字符串!~函數(shù)原型如下:
兩個參數(shù),destination是目的地也即是要拷貝到的空間!另一個是,cosnt修飾的source是源頭,也就是要拷貝的數(shù)據(jù)!因為他只要求拷貝不讓修改,所以const修飾左邊!我們下面來實現(xiàn)一個:
char* MyStrcpy(char* dest, const char* src)
{
//檢查空指針
assert(dest);
assert(src);
//拷貝
char* ret = dest;
while (*dest++ = *src++)
;
return ret;
}
int main()
{
char dest[20] = { 0 };
char* src = "hello world!";
strcpy(dest, src);//庫函數(shù)
printf("%s\n", dest);
printf("-----------------------\n");
MyStrcpy(dest, src);//自己函數(shù)
printf("%s\n", dest);
return 0;
}
看結(jié)果:
OK!模擬完成!
七、常見錯誤解析
7.1編譯型錯誤
一般可以直接看到錯誤信息,憑借經(jīng)驗就可以解決!一半多為語法錯誤~
例如:
int main()
{
int a = 3
return 0;
}
這里是少個封號,導致語法錯誤!看編譯錯誤信息:
這種問題一般會很簡單,看看錯誤信息就可以解決!
7.2鏈接型錯誤
這種錯誤一般是由標識符錯誤火不存在導致的!看錯誤信息也很容易解決!
int Add(int a, int b)
{
return a + b;
}
int main()
{
int a = 3;
int b = 5;
int ret = add(a, b);
printf("%d", ret);
return 0;
}
這里add和Add不是一個東西就會表連接時錯誤!
這種找到改回來就好了!
7.3運行時錯誤
這是最麻煩的一種,語法沒有問題可是結(jié)果就是不符合預(yù)期,得一步一步調(diào)試~
調(diào)試也是程序員的內(nèi)功,非一日之功!~得多練習~,!文章來源:http://www.zghlxwxcb.cn/news/detail-692372.html
OK,好兄弟本期分享就到這里,我們下期再見~!文章來源地址http://www.zghlxwxcb.cn/news/detail-692372.html
到了這里,關(guān)于程序員必備技能之調(diào)試的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!