文章目錄
- 如何寫出好的并且易于調試的代碼
-
- 一、 什麼是好的代碼?
- 二、如何寫出好的代碼?
-
- assert()
-
- 介紹
- 優缺點與特點
- 執行個體
- 建議
- const
-
- 介紹
- 初始化指派
- 生命周期
- 記憶體存儲
- const與數組
- const與指針
- const與函數形參
- const與define
- 總結
如何寫出好的并且易于調試的代碼
一、 什麼是好的代碼?
我們可以看一段代碼,來體會什麼是好的代碼:
/***
*strlen.c - contains strlen() routine
*
* Copyright (c) Microsoft Corporation. All rights reserved.
*
*Purpose:
* strlen returns the length of a null-terminated string,
* not including the null byte itself.
*
*******************************************************************************/
#include <string.h>
#pragma function(strlen)
/***
*strlen - return the length of a null-terminated string
*
*Purpose:
* Finds the length in bytes of the given string, not including
* the final null character.
*
*Entry:
* const char * str - string whose length is to be computed
*
*Exit:
* length of the string "str", exclusive of the final null byte
*
*Exceptions:
*
*******************************************************************************/
size_t __cdecl strlen (
const char * str
)
{
const char *eos = str;
while( *eos++ ) ;
return( eos - str - 1 );
}
這是由微軟公司提供的庫函數strlen的代碼片段。
這段代碼包括了詳細的注釋,以及功能的實作。注釋非常詳細的闡述了,函數的功能、要求以及例外情況
如果編寫的代碼可以滿足以下要求,我認為可以稱為好的代碼:
- 代碼運作正常
- bug少
- 效率高
- 可讀性高
- 可維護性高
- 注釋清晰
- 文檔齊全
二、如何寫出好的代碼?
通過觀察上述庫函數strlen的代碼,我們可以發現在聲明以及實作功能時,有兩個我們所沒有見過的
assert
宏(函數)和
const
關鍵字。
size_t __cdecl strlen (
const char * str
)
{
const char *eos = str;
while( *eos++ ) ;
return( eos - str - 1 );
}
assert()
程式一般分為Debug版本和Release版本,Debug版本用于内部調試,Release版本用于給測試人員測試或者發行給使用者使用。
斷言assert是僅在Debug版本起作用的宏,其實在大部分編譯器下,assert是一個宏,在少數編譯器下,assert是一個函數。我們無需關心這些差異,隻管把assert()當做函數使用即可。
介紹
頭檔案:
<assert.h>
描述:
允許診斷資訊被寫入到标準錯誤檔案中,簡單來說,它可以用在C程式中添加診斷。
聲明:
參數:
expression – 這個參數可以是一個變量或者任何C語言表達式。如果expression結果為ture,assert()不執行任何動作。如果expression結果為false,assert()會在标準錯誤上顯示錯誤資訊,并中止執行,避免程式運作引起更大的錯誤。
了解:
assert()函數,其實相當于一個if else語句
if(expression) //expression結果為ture
{
//程式正常執行!
}
else
{
//報錯并且終止程式!
}
如果直接使用if else語句的話,就會出現許多個if語句,顯得比較啰嗦。而且大多數情況才,我們要驗證的假設,隻是屬于偶然性事件,又或者我們僅僅想測試一下,一些最壞情況是否會發生,是以有了assert()的存在。
優缺點與特點
作用:
assert()的作用是先計算表達式expression,如果結果為假(0),那麼先向stderr列印一條出錯資訊,然後通過調用abort來終止程式。
缺點:
使用assert的缺點是,頻繁的調用會極大的影響程式的性能,增加額外的開銷。
特點:
assert隻有在Debug版本中才會有效,如果環境為Release版本則會被忽略。
執行個體
#include<stdio.h>
#include<assert.h>
char* my_strcpy(char* dest, const char* src)
{
assert(dest);//斷言指針的有效性
assert(src);
char* ret = dest;
while (*dest++ = *src++)
{
;
}
return ret;
}
int main()
{
char arr1[20] = { 0 };
//char* p = "hello";
char* p = NULL;
my_strcpy(arr1, p);
//printf("%s\n", my_strcpy(arr1, p));
return 0;
}
在這段程式中指針變量p賦為空指針,函數入口斷言,判斷參數的有效性。當程式執行時,就會報出這樣的錯誤:
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5iYzUzMihzYwY2YhVzYhRWN3YWMyYmNzgjYhFmN3QWNm9CX0JXZ252bj91Ztl2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
從報錯中我們可以直覺的看出,在哪個檔案多少行,什麼錯誤。
建議
- 每個assert隻檢驗一個條件(參數),因為同時檢驗多個條件時,如果斷言失敗,無法直覺的判斷是哪個條件(參數)錯誤。
- 不能使用改變環境的語句,因為assert隻在Debug環境生效,如果這麼做,會使程式在真正運作時(Release環境)遇到問題。
- assert和後面的語句應該空一行,以形成邏輯和視覺上的一緻感。
- 使用斷言捕捉不應該發生的非法情況。不要混淆非法情況和錯誤情況之間的差別,後者是必然存在的并且是一定要作出處理的。
- 在函數的入口處,使用斷言檢查函數參數的有效性(合法性)。
- 在編寫函數時,要反複進行考察,并且自問:“我打算做哪些假定?”,一旦确定了假定,就要使用斷言對假定進行檢查。
- 一般教科書都鼓勵程式員們進行防錯設計,但要記住這種程式設計風格可能會隐瞞錯誤。當進行防錯設計時,如果“不可能發生”的事情确實發生了,則要使用斷言進行報警。
const
有時候我們希望定義這樣一種變量,它的值不能被改變,在整個作用域中都保持固定。例如:用一個變量來表示班級的最大人數,為了滿足這一要求,可以使用const關鍵字來對變量加以限定。
介紹
const 是constant的縮寫,意思是“恒定不變的”。它是定義隻讀變量的關鍵字,或者說const是定義常變量的關鍵字。
隻讀:隻讀取變量記憶體儲的資料,不對其進行修改。
常變量:const定義的是一個變量,但是定義之後這個變量就相當于一個常量;const定義的是一個常量,但是這個常量又具有變量的屬性,是以叫做常變量。
使用:
使用const定義常變量的方法很簡單,隻需要在定義普通變量時的基礎上在最前面加上const即可。
初始化指派
使用const定義的變量的值是不允許被修改的,即不允許給它重新指派,即使是賦相同的值也是不可以的。是以說它定義的是隻讀變量。這也就是意味着必須在定義的時候就給它賦初值。
如果在定義的時候沒有進行初始化,對于未初始化的局部變量,當程式執行的時候會自動把一個很小的負數存放進去。這樣後面再給它賦初值的話就是“改變了它的值”,即發生了文法錯誤。
int a;
const int b;
a = 10; // 可以
b = 10; // 報錯
生命周期
用const修飾的變量,無論是全局變量還是局部變量,生命周期都是程式運作的整個過程。全局變量的生命周期位程式運作的整個過程這個理所當然。而室友const修飾過的局部變量就有了靜态特性,它的生命周期也是程式運作的整個過程。我們知道全局變量是靜态的,靜态的生命周期就是程式運作的整個過程。但是用cunst修飾過的局部變量隻是有了靜态特性,并沒有說它變成了靜态變量。
記憶體存儲
局部變量存儲在棧中,靜态變量存儲在靜态存儲區中,而經過const修飾過的變量存儲在記憶體中的“隻讀資料段”中。隻讀資料段中存放着常量和隻讀變量等不可修改的量。
const與數組
數組的長度不能是變量,雖然const定義的是隻讀變量(常變量),相當于定義了一個常量,但是隻讀變量(常變量)也是變量,它也有變量的屬性,是以const修飾的變量仍然不能作為數組的長度。
const與指針
const也可以和指針變量一起使用,這樣可以限制指針變量本身,也可以限制指針指向的資料。
const修飾指針變量的時候有以下幾種不同的順序:
#include <stdio.h>
int main()
{
const int* p1;
int const* p2;
int *const p3;
const int* const p4;
}
const如果放在 * 的左邊,修飾的是指針指向的内容,保證桌子很指向的内容不能通過指針來改變。但是指針變量本身的内容可變。
const如果放在 * 的右邊,修飾的是指針變量本身,保證了指針變量的内容不能修改,但是指針指向的内容,可以通過指針改變。
即:
p3本身的值是不能被修改的。
p1和p2,指針所指向的資料是隻讀的,但是p1和p2本身的值是可以修改的(指向不同的記憶體空間),但他們指向的資料不能被修改。
const離變量名近就是用來修飾指針變量的,離變量名遠就是用來修飾指針指向的資料,如果遠的近的都有,那麼就是同時修飾指針變量以及它指向的資料。
const與函數形參
在C語言中,單獨定義const變量沒有明顯的優勢,完全可以使用#define指令來代替。const通常用在函數形參中,如果形參是一個指針,為了防止在函數内部修改指針指向的資料,就可以使用const來限制。
例如C語言标準庫中的strlen函數的形參就是被const來進行限制的。下面是strlen函數的原型:
size_t __cdecl strlen (const char * str)
{
const char *eos = str;
while( *eos++ ) ;
return( eos - str - 1 );
}
strlen函數的作用是計算字元串長度,不應該對字元串str進行修改,是以使用const來進行限制。可以防止由于操作失誤引起的字元串修改。
const與define
#define
是預編譯指令,而caonst是普通變量的定義。#define定義的宏是在預處理階段展開的,而const定義的隻讀變量是在編譯運作階段使用的。
在C語言中可以使用const來定義常量,也可以使用#define來定義常量.但是前者比後者有更多的優點:
- const常量有資料類型,而宏常量沒有資料類型。編譯器可以對前者進行類型安全檢查。而對後者隻進行字元替換,沒有類型安全檢查,并且在字元替換時可能會産生意料不到的錯誤。
- 有些內建化的調試工具可以對const常量進行調試,但是不能對宏常量進行調試。
總結
- 使用assert
- 盡量使用conset
- 養成良好的編碼風格
- 添加必要的注釋
- 避免編碼陷阱