天天看點

如何寫出好的代碼?詳解const和assert斷言如何寫出好的并且易于調試的代碼

文章目錄

  • 如何寫出好的并且易于調試的代碼
    • 一、 什麼是好的代碼?
    • 二、如何寫出好的代碼?
      • 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的代碼片段。

這段代碼包括了詳細的注釋,以及功能的實作。注釋非常詳細的闡述了,函數的功能、要求以及例外情況

如果編寫的代碼可以滿足以下要求,我認為可以稱為好的代碼:

  1. 代碼運作正常
  2. bug少
  3. 效率高
  4. 可讀性高
  5. 可維護性高
  6. 注釋清晰
  7. 文檔齊全

二、如何寫出好的代碼?

通過觀察上述庫函數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賦為空指針,函數入口斷言,判斷參數的有效性。當程式執行時,就會報出這樣的錯誤:

如何寫出好的代碼?詳解const和assert斷言如何寫出好的并且易于調試的代碼

從報錯中我們可以直覺的看出,在哪個檔案多少行,什麼錯誤。

建議

  1. 每個assert隻檢驗一個條件(參數),因為同時檢驗多個條件時,如果斷言失敗,無法直覺的判斷是哪個條件(參數)錯誤。
  2. 不能使用改變環境的語句,因為assert隻在Debug環境生效,如果這麼做,會使程式在真正運作時(Release環境)遇到問題。
  3. assert和後面的語句應該空一行,以形成邏輯和視覺上的一緻感。
  4. 使用斷言捕捉不應該發生的非法情況。不要混淆非法情況和錯誤情況之間的差別,後者是必然存在的并且是一定要作出處理的。
  5. 在函數的入口處,使用斷言檢查函數參數的有效性(合法性)。
  6. 在編寫函數時,要反複進行考察,并且自問:“我打算做哪些假定?”,一旦确定了假定,就要使用斷言對假定進行檢查。
  7. 一般教科書都鼓勵程式員們進行防錯設計,但要記住這種程式設計風格可能會隐瞞錯誤。當進行防錯設計時,如果“不可能發生”的事情确實發生了,則要使用斷言進行報警。

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和assert斷言如何寫出好的并且易于調試的代碼

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來定義常量.但是前者比後者有更多的優點:

  1. const常量有資料類型,而宏常量沒有資料類型。編譯器可以對前者進行類型安全檢查。而對後者隻進行字元替換,沒有類型安全檢查,并且在字元替換時可能會産生意料不到的錯誤。
  2. 有些內建化的調試工具可以對const常量進行調試,但是不能對宏常量進行調試。

總結

  1. 使用assert
  2. 盡量使用conset
  3. 養成良好的編碼風格
  4. 添加必要的注釋
  5. 避免編碼陷阱

繼續閱讀