去年年底的因為業務需要需要在使用tk.mybaits框架的系統中實現指定欄位的更新,可是tk.mybaits框架本身並不支持這個功能,我翻遍了CSDN和其他相關的技術相關的網站都沒有找到相關的解決方法。於是我通過幾天的翻閱相關資料和摸索後終於實現了這個功能。最近事情不是很多,想到又想到了去年解決 ...
本章中,你將學習對象、函數和類型。我們將研究如何聲明變數(有標識符的對象)和函數,獲取對象的地址,並對這些對象指針的解引用。你已經看到了C語言程式員可用的一些類型, C語言中的類型不是對象就是函數。
對象、函數、類型和指針
對象是你可以表示數值的存儲。準確地說,C標準(ISO/IEC 9899:2018)將對象定義為 "執行環境中的數據存儲區域,其內容可以代表數值",並補充說明,"當被引用時,對象可以被解釋為具有特定類型"。變數是對象的例子。
變數會聲明的類型,告訴你它的值代表哪種對象。例如類型為int的對象包含整數值。類型很重要,因為代表一種類型的對象的比特集合,如果被解釋為不同類型的對象,可能會有不同的值。例如,數字1在IEEE 754(IEEE浮點運算標準)中由比特模式0x3f800000(IEEE 754-2008)表示。但是,如果你把這個相同的比特模式解釋為一個整數,你會得到1,065,353,216的值,而不是1。
函數不是對象,但確實有類型。
C語言也有指針,它在地址--記憶體中存儲對象或函數的位置。指針類型是由引用類型的函數或對象類型派生出來的。從被引用類型T派生出來的指針類型被稱為對T的指針。
聲明變數
聲明變數時,需要指定類型,並提供名稱用來引用該變數。
可以一行聲明多個變數,但如果變數是指針或數組,或者變數是不同的類型,這樣做就會引起混亂。下麵的聲明都是正確的。
char *src, c;
int x, y[5];
int m[12], n[15][3], o[21];
第一行聲明瞭兩個變數,src和c,它們的類型不同。src變數的類型為char *,而c的類型為char。第二行再次聲明瞭兩個類型不同的變數,x和y。變數x的類型是int,y是由五個元素組成的數組,類型是int。第三行聲明瞭三個數組-m、n和o-具有不同的尺寸和元素數。
一行一種類型的聲明可讀性會更好:
char *src; // src has a type of char *
char c; // c has a type of char
int x; // x has a type int
int y[5]; // y is an array of 5 elements of type int
int m[12]; // m is an array of 12 elements of type int
int n[15][3]; // n is an array of 15 arrays of 3 elements of type int
int o[21]; // o is an array of 21 elements of type int
實例:交換值1
在{ }字元之間有代碼塊稱為複合語句。我們在主函數中定義了兩個變數,a和b。我們聲明這些變數的類型為int,並將它們分別初始化為21和17。每個變數都必須有一個聲明。然後主函數調用swap函數來嘗試交換這兩個整數的值。
#include <stdio.h>
void swap(int a, int b) {
int t = a;
a = b;
b = t;
printf("swap: a = %d, b = %d\n", a, b);
}
int main(void) {
int a = 21;
int b = 17;
swap(a, b);
printf("main: a = %d, b = %d\n", a, b);
return 0;
}
局部變數,如清單2-1中的a和b,具有自動存儲期限,這意味著它們一直存在,直到執行離開它們被定義的塊。我們將嘗試交換存儲在這兩個變數中的值。
swap函數聲明瞭兩個參數,a和b,你用它們來向這個函數傳遞參數。我們還在交換函數中聲明瞭int類型的臨時變數t,並將其初始化為a的值。這個變數用於臨時保存a中存儲的值,以便在交換過程中不會丟失。
執行結果
$ ./a.out
swap: a = 17, b = 21
main: a = 21, b = 17
變數a和b分別被初始化為21和17。在swap函數中對printf的第一次調用顯示這兩個值被交換了,但在main中對printf的第二次調用顯示原始值沒有變化。
C語言是傳值的語言,傳參時參數的值被覆制到一個單獨的變數中,以便在函數中使用。swap函數將你作為參數傳遞的對象的值分配給各自的參數。當函數中的參數值發生變化時,調用方的值不會受到影響,因為它們是不同的對象。因此,在第二次調用printf時,變數a和b在main中保留了它們的原始值。
實例:交換值2
我們使用指示符(*)來聲明指針
#include <stdio.h>
int swap(int *_a, int *_b) {
int tmp = *_a;
*_a = *_b;
*_b = tmp;
}
int main(void) {
int a = 21;
int b = 17;
swap(&a, &b);
printf("a = %d, b = %d\n", a, b);
return 0;
}
清單2-3:修改後的使用指針的交換函數
當在函數聲明或定義中使用時,作為指針聲明器的一部分,表示參數是指向特定類型的對象或函數的指針。註意_a表示指針,_a表示指針指向的值。&獲取操作符的地址。
執行結果
$ ./a.out
a = 17, b = 21
變數a和b分別被初始化為21和17。然後代碼將這些對象的地址作為參數傳遞給交換函數。
在swap函數中,參數_a和_b現在都被聲明為int的指針類型,並且包含了從調用函數(在本例中,main)傳遞給swap的參數的副本。這些地址副本仍然指向完全相同的對象,所以當它們所引用的對象的值在交換函數中被交換時,在main中聲明的原始對象的內容也被訪問並被交換。這種方法通過生成對象地址,通過值傳遞這些地址,然後通過地址來訪問原始對象,即傳址。
範圍
對象、函數、巨集和其他C語言標識符都有範圍,它限定了它們可以被訪問的連續區域。C語言有四種類型的範圍:文件、塊、函數原型和函數。
對象或函數標識符的範圍是由它的聲明位置決定的。如果聲明在任何塊或參數列表之外為文件範圍;如果聲明出現在塊內或參數內,只能在該塊內訪問。
如果聲明出現在函數原型的參數聲明列表中(不是函數定義的一部分),那麼標識符具有函數原型作用域,它終止於函數聲明末端; 函數範圍是指函數定義的開頭{和結尾}之間的區域。標簽名是唯一一種函數作用域的標識符。標簽是標識符,後面有一個冒號,用來標識函數中的一個語句,控制權可以被轉移到這個語句中。
作用域可以被嵌套,有內部和外部作用域。內層作用域可以訪問外層作用域,但反之不行。如果你在內層作用域和外層作用域中都聲明瞭同一個標識符,那麼在外層作用域中聲明的標識符會被內層作用域中的標識符所隱藏,後者具有優先權。
存儲期限
有四種存儲期限可供選擇:自動、靜態、線程和分配。你已經看到,自動存儲期限的對象被聲明在塊中或作為函數參數。這些對象的生命周期從它們被聲明的塊開始執行時開始,到塊的執行結束時結束。
範圍和壽命是完全不同的概念。範圍適用於標識符,而壽命適用於對象。標識符的範圍是代碼區域,在這個區域中,標識符所表示的對象可以通過它的名字被訪問。對象的生命周期是該對象存在的時間段。
在文件作用域中聲明的對象具有靜態存儲期限。這些對象的生命期是程式的整個執行過程,它們的存儲值在程式啟動前被初始化。你也可以通過使用存儲類指定符static,將塊作用域中的變數聲明為具有靜態存儲期限,如清單2-6中的計數例子所示。這些對象在函數退出後持續存在。
#include <stdio.h>
void increment(void) {
static unsigned int counter = 0;
counter++;
printf("%d ", counter);
}
int main(void) {
for (int i = 0; i < 5; i++) {
increment();
}
return 0;
}
這個程式輸出1 2 3 4 5。我們在程式啟動時將靜態變數counter初始化為0,併在每次調用increment函數時將其遞增。計數器的生命周期是程式的整個執行過程,它將在整個生命周期內保留其最後存儲的值。你可以通過用文件範圍聲明計數器來實現同樣的行為。然而在可能的情況下,限制對象的範圍是一種良好的軟體工程實踐。
靜態對象必須用常量值而不是變數來初始化。
int *func(int i) {
const int j = i; // ok
static int k = j; // error
return &k;
}
常量值指的是字面常量(例如,1、'a'或0xFF)、枚舉成員以及alignof或sizeof等運算符的結果。
線程存儲持續時間用於併發編程,動態分配的記憶體。
對齊
對象類型有對齊要求,對象可能被分配的地址進行限制。對齊代表了給定對象可以被分配的連續地址之間的位元組數。CPU在訪問對齊的數據(例如,數據地址是數據大小的倍數)和未對齊的數據時可能有不同的行為。
一些機器指令可以在非字的邊界上執行多位元組訪問,但可能會有性能上的損失。字是自然的、固定大小的數據單位,由指令集或處理器的硬體處理。一些平臺不能訪問未對齊的記憶體。對齊要求可能取決於CPU字的大小(通常為16、32或64位)。
一般來說,C語言程式員不需要關心對齊要求,因為編譯器為其各種類型選擇合適的對齊方式。對於所有的標準類型,包括數組和結構,從malloc動態分配的記憶體都需要充分對齊。然而,在極少數情況下,你可能需要覆蓋編譯器的預設選擇;例如,在必須從二冪地址邊界開始的記憶體緩存行邊界上對齊數據,或者滿足其他系統特定的要求。傳統上,這些要求是通過linker命令來滿足的,或者通過malloc對記憶體進行整體分配,然後將用戶地址向上舍入,或者通過涉及其他非標準設施的類似操作。
C11引入了一種簡單的、向前相容的機制來指定對齊方式。對齊是以size_t類型的值表示的。每個有效的對齊值都是的2整數次方。每個對象都有預設的對齊要求:更嚴格的對齊(更大的2次方)可以通過對齊指定器(_Alignas)來請求。你可以在聲明的指定器中包個對齊方式的指定器。清單2-7使用對齊指定符來確保good_buff是正確對齊的(bad_buff對於成員訪問表達式可能有不正確的對齊)。
struct S {
int i; double d; char c;
};
int main(void) {
unsigned char bad_buff[sizeof(struct S)];
_Alignas(struct S) unsigned char good_buff[sizeof(struct S)];
struct S *bad_s_ptr = (struct S *)bad_buff; // wrong pointer alignment
struct S *good_s_ptr = (struct S *)good_buff; // correct pointer alignment
}
對齊是按從弱到強(也叫嚴格)的順序排列的。
對象類型
我們將介紹布爾類型、字元類型和數字類型(包括整數和浮點類型)。
布爾類型
聲明為_Bool的對象只能存儲0和1的值。這種布爾類型是在C99中引入的,並以下劃線開始,以便在已經聲明瞭自己的標識符名為bool或boolean的現有程式中加以區分。以下劃線和大寫字母或另一個下劃線開頭的標識符總是被保留。
如果你包含頭文件<stdbool.h>,你也可以把這個類型拼成bool,並給它賦值為true(擴展為整數常數1)和false(擴展為整數常數0)。在這裡,我們使用兩種類型名稱的拼寫來聲明兩個布爾變數:
#include <stdbool.h>
_Bool flag1 = 0;
bool flag2 = false;
兩種拼法都可以使用,但最好使用bool,因為這是語言的長期方向。
字元類型
C語言定義了三種字元類型:char、signed char和unsigned char。每個編譯器的實現都會將char定義為具有相同的對齊方式、大小、範圍、表示方式和行為,即signed char或nsigned char。無論做出什麼樣的選擇,char都是獨立的類型,與其他兩種類型都不相容。
char類型通常用於表示C語言程式中的字元數據。特別是,char類型的對象必須能夠表示執行環境中所需要的最小字元集(稱為基本執行字元集),包括大寫和小寫字母、10位小數、空格字元以及各種標點符號和控制字元。char類型不適合整數數據;使用signed char來表示小的有符號整數值,使用unsigned char來表示小的無符號值,是比較安全的。
基本的執行字元集適合許多傳統的數據處理應用的需要,但它缺乏非英文字母是國際用戶接受的障礙。為瞭解決這一需要,C標準委員會指定了一種新的、寬的類型,以允許大型字元集。你可以通過使用wchar_t類型將大字元集的字元表示為寬字元,它通常比基本字元占用更多空間。通常情況下,實現者選擇16或32位來表示一個寬字元。C標準庫提供了同時支持窄字元和寬字元類型的函數。
數值類型
C提供了幾種數字類型,可以用來表示整數、枚舉器和浮點值。第3章更詳細地介紹了其中的一些類型,但這裡是簡單的介紹。
- 整數類型
有符號的整數類型可以用來表示負數、正數和零。有符號的整數類型包括signed char、short int、int、long int和long long int。
除了int本身,在這些類型的聲明中可以省略關鍵字int,所以你可以,例如,用long long而不是long long int來聲明一個類型。
對於每個有符號的整數類型,都有相應的無符號整數類型,使用相同的存儲量:unsigned char、unsigned short int、unsigned int、unsigned long int和unsigned long long int。無符號類型只能用來表示正數和零。
有符號和無符號整數類型用於表示各種大小的整數。每個平臺(當前或歷史)都決定了這些類型的大小,給定了一些約束條件。每個類型都有最小的可表示範圍。這些類型按寬度排序,保證較寬的類型至少和較窄的類型一樣大,這樣,long long int類型的對象可以表示long int類型的對象可以表示的所有值,long int類型的對象可以表示int類型的對象可以表示的所有值,等等。各種整數類型的實際大小可以從<limits.h>頭文件中指定的各種整數類型的最小和最大可表示值推斷出來。
int類型通常具有執行環境的架構所建議的自然大小,因此在16位架構上,其大小為16位寬,在32位架構上為32位寬。你可以通過使用<stdint.h>或<inttypes.h>頭文件的類型定義來指定實際寬度的整數,如uint32_t。這些頭文件還為最寬的可用整數類型提供了類型定義:uintmax_t和intmax_t。
第3章詳細介紹了整數類型。
- 枚舉類型
枚舉,或稱enum,允許你定義一個類型,在具有可枚舉的常量值集的情況下,為整數值分配名稱(枚舉器)。下麵是枚舉的例子:
enum day { sun, mon, tue, wed, thu, fri, sat };
enum cardinal_points { north = 0, east = 90, south = 180, west = 270 };
enum months { jan = 1, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec };
如果你沒有用=操作符給第一個枚舉器指定值,那麼它的枚舉常量的值就是0,而後面每個沒有=的枚舉器都會在前面的枚舉常量的值上加1。因此,day枚舉中sun的值是0,mon是1,以此類推。
你也可以給每個枚舉器分配特定的值,如cardinal_points枚舉所示。與枚舉器一起使用=可能會產生具有重覆值的枚舉常數,如果你錯誤地認為所有的值都是唯一的,這可能是一個問題。months枚舉將第一個枚舉器設置為1,每個後續的枚舉器如果沒有被特別指定一個值,將被遞增1。
枚舉常量的實際值必須可以作為int來表示,但是它的類型是由實現定義的。例如,Visual C++使用有符號的int,而GCC使用無符號的int。
- 浮點類型
C語言支持三種浮點類型:float、double和long double。浮點運算類似於實數運算,並經常被用作實數運算的模型。C語言支持多種浮點表示法,包括在大多數系統上支持IEEE浮點算術標準(IEEE 754-2008)。浮點表示法的選擇取決於實現。第3章詳細介紹了浮點類型。
- void類型
關鍵字void(本身)的意思是 "不能容納任何值"。例如,你可以用它來表示函數不返回值,或者作為函數的唯一參數來表示該函數不接受參數。另一方面,派生類型void *意味著指針可以引用任何對象。我將在本章後面討論派生類型。
函數類型
函數類型是派生類型。在這種情況下,該類型是由返回類型和其參數的數量和類型衍生出來的。函數的返回類型不能是數組類型。
當你聲明函數時,你使用函數聲明器來指定函數的名稱和返回類型。如果聲明器包括參數類型列表和定義,每個參數的聲明必須包括標識符,除了只有void類型參數的參數列表,它不需要標識符。
下麵是幾個函數類型的聲明:
int f(void);
int *fip();
void g(int i, int j);
void h(int, int);
首先,我們聲明沒有參數的函數f,返回int。接下來,我們聲明一個沒有指定參數的函數fip,它返回指向int的指針。最後,我們聲明兩個函數,g和h,每個函數都返回void,並接受兩個int類型的參數。
如果標識符是巨集,用標識符來指定參數(如這裡的g)可能會有問題。然而,提供參數名稱是自我記錄代碼的良好做法,所以通常不建議省略標識符(如對h的做法)。
在函數聲明中,指定參數是可選的。然而,不這樣做偶爾也會有問題。如果你用C++寫fip的函數聲明,它將聲明不接受任何參數的函數,並返回int *。在C語言中,fip聲明的是接受任何類型參數的函數,並返回一個int *。在C語言中,你不應該用空參數列表來聲明函數。首先,這是語言的廢棄功能,將來可能會被刪除。其次,這段代碼可能會被移植到C++中,所以要明確地列出參數類型,在沒有參數的時候使用void。
帶有參數類型列表的函數類型被稱為函數原型。函數原型告知編譯器一個函數所接受的參數的數量和類型。編譯器使用這些信息來驗證在函數定義和對函數的任何調用中是否使用了正確的參數數量和類型。
函數定義提供了該函數的實際實現。請看下麵的函數定義:
int max(int a, int b)
{ return a > b ? a : b; }
返回類型指定為int;函數聲明器為max(int a, int b);而函數主體為{ return a > b ? a : b; }。函數類型的指定不能包括任何類型限定符(參見第32頁 "類型限定符")。函數體本身使用了條件運算符(?