如何在eclipse上做網站網絡營銷推廣價格
C語言技能數(知識點匯總)
- C 語言概述
- 特點
- 不足之處
- 標準
- 編程機制
- 數據類型
- 變量
- 數據類型
- 字符類型
- 整數類型
- 符號位
- 二進制的原碼、反碼和補碼
- 浮點類型
- 布爾類型
- 常量
- 字面常量
- const 修飾的常變量
- #define定義的標識符常量
- 枚舉常量
- sizeof
- sizeof(結構體)
- 不要對 void 類型使用 sizeof
- 不要在子函數中對指針類型使用 sizeof
- sizeof 的結果取決于環(huán)境
- 運算符與表達式
- 語句與控制流
- 函數與程序結構
- 函數的聲明和定義
- 函數的參數
- 形式參數和實際參數
- 數組作為實參進行傳遞
- 函數的返回和嵌套
- 函數的遞歸
- 變量的作用域
- 局部變量
- 全局變量
- 注意事項
- 頭文件
- \#include
- 頭文件的引用
- 頭文件的內容
- 示例
- 內部函數和外部函數
- 外部函數 (extern)
- 內部函數 (static)
- 數組
- 數組的創(chuàng)建
- 數組的使用
- 二維數組
- 變長數組
- 指針
- 指針的聲明
- 指針的簡單應用
- NULL 指針
- 指針的算術運算
- 指針數組
- 數組指針
- 指向指針的指針
- 字符串
- 字符
- C 的字符串
- 字符串輸入與輸出
- scanf 函數
- gets 函數
- fgets 函數
- scanf 和 gets 的區(qū)別
- printf 函數
- puts 函數
- fputs 函數
- 字符串函數
- strcpy
- strcat
- strlen
- strcmp
- strchr
- strstr
- C++ 的字符串
- 枚舉類型
- 結構體
- 聲明與定義
- 結構體作為參數
- 指向結構體的指針
- 結構體占用的內存空間大小
- 位域
- 結構體小案例
- 聯合體
- 定義聯合體與使用
- 聯合體作為參數
- 指向聯合體的指針
- 位運算
- 位和字節(jié)
- 進制數
- 十進制轉二進制
- 二進制轉十進制
- 八進制
- 十六進制
- 位邏輯運算符
- 與運算:&
- 或運算:|
- 非運算:~
- 異或運算:^
- 位移運算:<< 或 >>
- 位運算賦值運算符
- 預處理器
- #include
- #define
- #ifndef & #ifdef & #endif
- #if & #elif & #else
- #undef
- 文件
- 內存管理
- 存儲類別
- 自動變量 (auto)
- 外部變量
- 靜態(tài)變量(static)
- 寄存器變量(register)
- 相關概念
- 作用域(scope)
- 鏈接(linkage)
- 存儲期(storage duration)
- 存儲類別小結
- 內存動態(tài)管理
- 棧上開辟空間
- 動態(tài)內存函數
- 常見動態(tài)內存錯誤
- 標準函數庫
- 數學庫
- 三角函數(Trigonometric functions)
- 指數函數(Exponential functions)
- 對數函數(Logarithmic functions)
- 冪函數(Power functions)
- 平方根(square root)
- 立方根(cube root)
- 求斜邊(hypotenuse)
- 取整函數
- 絕對值和最值
- 通用工具庫
C 語言概述
特點
- C語言簡潔、緊湊、靈活。C語言的核心內容很少,只有
32
個關鍵字,9
種控制語句; - 表達方式簡練、實用。C語言有一套強有力的運算符,達44種
- 具有豐富的數據類型。
- 具有低級語言的特點。具有與匯編語言相近的功能和描述方法,如地址運算、二進制數位運算等,對硬件端口等資源直接操作,可充分使用計算機資源。
- 是一種結構化語言,適合于大型程序的模塊化設計。
- 預處理命令和預處理程序。
- 可移植性。
- 生成的目標代碼質量高。
- C語言語法限制不嚴,程序設計自由度大。
不足之處
- C程序的錯誤更隱蔽
- C程序有時會難以理解
- C程序有時會難以修改
標準
- 1978年,丹尼斯·里奇(Dennis Ritchie)和布萊恩·科爾尼干(Brian Kernighan) 發(fā)布了
K&R C
. - 1989年,美國國家標準局為其指定標準,稱為
ANSI C
,也稱C89
. - 1990年,國際化標準組織為其指定標準,稱為
ISO C
,也稱C90
. - 1999年,發(fā)布的ISO/IEC 9899:1999標準,稱為
C99
. - 2011年,美國國家標準局采納了ISO/IEC 9899:2011標準,稱為
C11
. - 2018年,發(fā)布的ISO/IEC 9899:2018標準,稱為
C18
.
編程機制
一個典型的C程序編譯管道,包含預處理、編譯、匯編、鏈接四個環(huán)節(jié)。
- 輸入
.c
和.h
等源碼文件 - 【預處理】宏展開(#include/#define/…)
- 【編譯器】編譯成匯編代碼,得到
.s
文件 - 【匯編器】輸出機器代碼,得到
.o
或.obj
文件 - 【鏈接器】輸出可執(zhí)行文件,得到
.exe
文件
數據類型
變量
變量相當于內存中一個數據存儲空間的表示,通過變量名可以訪問到變量(值)??梢园炎兞靠醋鍪且粋€房間的門牌號,通過門牌號我們可以找到房間。
變量使用注意事項
- 變量表示內存中的一個存儲區(qū)域(不同的數據類型,占用的空間大小不一樣)
- 該區(qū)域有自己的名稱和類型
- 變量必須先聲明,后使用
- 該區(qū)域的數據可以在同一類型范圍內不斷變化
- 變量在同一個作用域內不能重名
- 變量三要素 (變量名+值+數據類型) 。
數據類型
每一種數據都定義了明確的數據類型,在內存中分配了不同大小的內存空間(使用字節(jié)多少表示)。
注意:
- 在c中沒有字符串類型,使用字符數組
char[]
表示字符串 - 在不同系統(tǒng)上,部分數據類型字節(jié)長度不一樣,舉例:
int
可以占兩個字節(jié)或四個字節(jié)
字符類型
字符類型可以表示單個字符,字符類型是
char
,char
是1個字節(jié)(可以存字母或者數字),多個字符稱為字符串, 在C語言中使用char
數組 表示,數組不是基本數據類型,而是構造類型。
注意
- 字符常量是用單引號括起來的單個字符。 例如:
char c1 ='a'; char c3 = '9'
; - C中還允許使用轉義字符
‘\’
來將其后的字符轉變?yōu)樘厥庾址统A?。例?#xff1a;char c3 = '\n';
- 在C中, char的本質是一個整數。字符型存儲到計算機中,需要將字符對應的碼值(整數)找出來。字符和碼值的對應關系是通過字符編碼表決定的。
- 可以直接給char賦一個整數,然后輸出時,會按照對應的ASCII 字符輸出。
- char類型是可以進行運算的,相當于一個整數,因為它都對應有Unicode碼。
整數類型
對于整型和字符型,可以使用
signed
和unsigned
進行修飾。默認是signed
。
類型 | 字節(jié) | 取值下限 | 取值上限 |
---|---|---|---|
signed char | 1 | -2^-7即 -128 | 2^7-1 即 127 |
unsigned char | 1 | 0 | 2^8-1 即 255 |
signed short | 2 | -2^-15 即 -32768 | 2^15-1即 32767 |
unsigned short | 2 | 0 | 2^16-1 即 65535 |
signed int | 2或4 | -2^-15 或 -2^-31 | 215-1或231-1 |
unsigned int | 2或4 | 0 | 216-1或232-1 |
signed long | 4 | -2^-31 | 2^31-1 |
unsigned long | 4 | 0 | 2^32-1 |
符號位
- 存放signed類型的存儲單元中,左邊第一位表示符號位。
- 如果該位為0,表示該整數是一個正數;如果該位為1,表示該整數是一個負數。
- 一個32位的整型變量,除去左邊第一位符號位,剩下表示值的只有31個比特位。
注意
-
各種類型的存儲大小與操作系統(tǒng)、 系統(tǒng)位數和編譯器有關 ,目前通用的以64位系統(tǒng)為主。
-
C語言的整型類型, 分為有符號 (signed) 和無符號 (unsigned) 兩種, 默認是有符號的。
-
C程序中整型常聲明為
int
型, 除非不足以表示大數, 才使用long long
。 -
一個 B 等于 8 個 b
- B 是
Byte
的縮寫,意思是字節(jié),是計算機存儲容量的最基本的存儲單元。 - b 是
bit
的縮寫,意思是比特位,是計算機中的存儲數據的最小單位。指的是二進制中的一個位數。
- B 是
-
一個字節(jié)有八位二進制組成,即
1 Byte = 8 bit
。- 通常用
Bit
來作數據傳輸的單位,因為物理層、數據鏈路層的傳輸對于用戶是透明的。 - 通常用
Byte
來作應用層的單位,比如表示文件的大小,在用戶看來就是可見的數據大小。
- 通常用
二進制的原碼、反碼和補碼
原碼
將最高位作為符號位(0表示正,1表示負),其它數字位代表數值本身的絕對值的數字表示方式
反碼
- 如果是正數,則表示方法和原碼一樣;
- 如果是負數,符號位不變,其余各位取反,則得到這個數字的反碼表示形式。
補碼
- 如果是正數,則表示方法和原碼一樣;
- 如果是負數,則將數字的反碼加上1(相當于將原碼數值位取反然后在最低位加1)。
浮點類型
類型 | 存儲大小 | 值范圍 | 精度 |
---|---|---|---|
float 單精度 | 4 字節(jié) | 1.2E-38 到 3.4E+38 | 6 位小數 |
double 雙精度 | 8 字節(jié) | 2.3E-308 到 1.7E+308 | 15 位小數 |
注意
- 關于浮點數在機器中存放形式的簡單說明,浮點數=符號位+指數位+尾數位 , 浮點數是近視值
- 尾數部分可能丟失,造成精度損失。
浮點型使用細節(jié)
- 浮點型常量默認為
double
型 , 聲明float
型常量時, 須后加f
或F
。 - 浮點型常量有兩種表示形式
- 十進制數形式:如:
5.12 512.0f .512
(必須有小數點) - 科學計數法形式:如:
5.12e2
、5.12E-2
- 十進制數形式:如:
- 通常情況下,應該使用
double
型,因為它比float型更精確。 - 格式化輸出時,
%f
默認輸出 6 位小數,%.2f
可指定輸出 2 位小數。
布爾類型
布爾類型并非是基本類型, C語言標準(C89)沒有定義布爾類型,所以C語言判斷真假時以0為假,非0為真
C語言標準(C99)提供了_Bool
型, _Bool
仍是整數類型,但與一般整型不同的是,_Bool
變量只能賦值為0或1,非0的值都會被存儲為1。
C99還提供了一個頭文件 <stdbool.h>
定義了 bool
代表 _Bool
, true
代表1, false
代表0
只要導入 stdbool.h
,就能方便的操作布爾類型了 , 比如 bool flag = false;
常量
C編程中的常量是一些固定的值,它在整個程序運行過程中無法被改變。
字面常量
字面常量是直接寫出的固定值,它包含C語言中可用的數據類型,可分為整型常量,字符常量等。
如:
9.9
,“hello”
等就屬于這一類常量。
const 修飾的常變量
C語言標準提供了
const
關鍵字。在定義變量的同時,可在變量名之前加上const
修飾。
const int n = 10;
int arr[n] = { 0 };
const
修飾的常變量,本質上是變量。- 但具有常屬性,不能被修改。
- 在C99標準之前,數組的大小只能是常量修飾,不支持變長數組。
#define定義的標識符常量
C語言提供了
#define
命令定義標識符常量,該標識符常量在程序中是個定值,通常用于代表數組容量或涉及數學的常量等。
#define PI 3.14159
#define SIZE 10
#define
又稱宏定義,標識符為所定義的宏名,簡稱宏。- 對宏定義而言,預編譯的時候會將程序中所有出現“標識符”的地方全部用這個“常量”替換,稱為“宏替換”或“宏展開”。
- 被定義的標識符不占內存,只是一個臨時的符號,預編譯后這個符號就會被宏替換。
- 宏所表示的常量可以是數字、字符、字符串、表達式。其中最常用的是數字。
枚舉常量
語言提供了一種 枚舉(Enum)類型,能夠列出所有可能會用到的取值,并給它們取一個名字
enum Gender { Male, Female, Secret, Unknown };
在使用枚舉常量的時候,需要注意以下幾點:
- 不能對枚舉常量賦值,只能將它們的值賦給其他的變量。
- 不能再定義與枚舉常量名字相同的變量。
- 不能用
&
取得它們的地址。
sizeof
sizeof 是關鍵字,操作符,不是函數,用于獲取操作數被分配的內存空間,以字節(jié)單位表示。
基本使用
- sizeof(object);
- sizeof object
- sizeof(type_name);
例如:
int n = 10;
printf("sizeof(n)=%d\n", sizeof(n));
printf("sizeof n =%d\n", sizeof n );
printf("sizeof(int)=%d\n", sizeof(int));
sizeof(結構體)
- 理論上講結構體的各個成員在內存中是連續(xù)存放的,和數組非常類似,但是,結構體占用內存的總大小不一定等于全部成員變量占用內存大小之和。
- 在編譯器的具體實現中,為了提高內存尋址的效率,各個成員之間可能會存在縫隙。
- 用
sizeof
可以得到結構體占用內容在總大小。 sizeof(結構體名)
或sizeof(結構體變量名)
都可以。
不要對 void 類型使用 sizeof
void是無值型或空類型,不知道存儲空間大小的類型,編譯器也不能確定它的大小。
#include <stdio.h>void func() {}int main() {printf("sizeof(func)=%d\n", sizeof(func));// 輸出結果:sizeof(func)=1return 0;
}
void不能聲明變量,但是可以聲明指針
void *pv;
printf("sizeof(pv)=%d\n",sizeof(pv));
printf("sizeof(void*)=%d\n",sizeof(void *));
// 輸出結果:
// sizeof(pv)=4
// sizeof(void*)=4
不要在子函數中對指針類型使用 sizeof
如果把一個字符串的地址傳給子函數,子函數用一個字符指針(如char *pstr)來存放傳入的字符串的地址,如果在子函數中用 sizeof(pstr)
,得到的不是字符串占用內存的字節(jié)數,而是字符指針變量占用內存的字節(jié)數。
所以,不能在子函數中對傳入的字符串進行初始化,除非字符串的長度也作為參數傳入到了子函數中。
#include <stdio.h>void func(char * pstr) {// sizeof(pstr)=4printf("sizeof(pstr)=%d\n", sizeof(pstr));
}int main() {func("hello world!");return 0;
}
同理,也不要在子函數中對結構體指針用 sizeof,如果把一個結構體(如struct Human h)的地址傳給子函數,子函數用一個結構體指針(如struct Human *h)來存放傳入的結構體的地址,如果在子函數中用 sizeof(h),得到的不是結構體占用內存的字節(jié)數,而是結構體指針變量占用內存的字節(jié)數。
正確的用法是用 sizeof(struct Human)
。
#include <stdio.h>struct Human {char * name; int age;};void func(struct Human *h) {// 錯誤的輸出:sizeof(h)=4printf("sizeof(h)=%d\n", sizeof(h));// 正確的輸出:sizeof(*h)=8printf("sizeof(*h)=%d\n", sizeof(*h));// 正確的輸出:sizeof(struct Human)=8printf("sizeof(struct Human)=%d\n", sizeof(struct Human));
}int main() {struct Human human = {"tom", 23};func(&human);return 0;
}
sizeof 的結果取決于環(huán)境
在不同的 GCC 環(huán)境中,得到的結果是不同的。
通過 systeminfo
指令可以獲取當前系統(tǒng)的信息,部分如下:
主機名: THINKPADX1
OS 名稱: Microsoft Windows 11 專業(yè)版
OS 制造商: Microsoft Corporation
OS 配置: 獨立工作站
系統(tǒng)制造商: LENOVO
系統(tǒng)類型: x64-based PC
可以看出,設備是基于 x64
的芯片,也就是收機器字長是 64。通過下面這段源代碼觀察不同 gcc
下的結果
#include<stdio.h>int main() {char c = 'A';short s = 10; int i = 10; long l = 10L;float f = 3.14F; double d = 3.14D;int *p = &i;printf("char c = %c\t", c);printf("short s = %d\t", s);printf("int s = %d\t", i);printf("long s = %d\n", l);printf("float s = %f\t", f);printf("double s = %f\n", d);printf("*p = %d\n", *p);printf(" p = %p\n", p);printf("&p = %p\n", &p);printf("sizeof(char) = %d\n", sizeof(char));printf("sizeof(short) = %d\n", sizeof(short));printf("sizeof(int) = %d\n", sizeof(int));printf("sizeof(long) = %d\n", sizeof(long));printf("sizeof(float) = %d\n", sizeof(float));printf("sizeof(double) = %d\n", sizeof(double));printf("sizeof(size_t) = %d\n", sizeof(size_t));// size_t是一種機器相關的無符號類型,它被設計的足夠大以便能表示內存中任意對象的大小。return 0;
}
我已經在本機的 D:\Program\MinGW
和 D:\Program\MinGW64
分別安裝了 gcc
的32位版本和 64位版本。
在命令行中臨時修改環(huán)境變量,用以切換 gcc
的尋址路徑:
> SET PATH=C:\Windows\System32\;D:\Program\MinGW\bin
> WHERE gcc
D:\Program\MinGW\bin\gcc.exe
也可以通過 gcc -v
指令來查看當前 gcc
的版本,接下來使用 gcc
對上面的程序進行編譯和執(zhí)行,如下:
gcc app.c -o app.exe && app.exe
執(zhí)行結果:
char c = A short s = 10 int s = 10 long s = 10
float s = 3.140000 double s = 3.140000
*p = 10p = 0061FF04
&p = 0061FF00
sizeof(char) = 1
sizeof(short) = 2
sizeof(int) = 4
sizeof(long) = 4
sizeof(float) = 4
sizeof(double) = 8
sizeof(size_t) = 4
然后在命令行中通過再次修改環(huán)境變量,來臨時切換下 gcc
的尋址位置,如下所示:
> SET PATH=C:\Windows\System32\;D:\Program\MinGW64\bin
> WHERE gcc
D:\Program\MinGW64\bin\gcc.exe
再次使用 gcc
命令對上面的程序進行編碼和執(zhí)行,執(zhí)行結果如下:
char c = A short s = 10 int s = 10 long s = 10
float s = 3.140000 double s = 3.140000
*p = 10p = 000000000061FE04
&p = 000000000061FDF8
sizeof(char) = 1
sizeof(short) = 2
sizeof(int) = 4
sizeof(long) = 4
sizeof(float) = 4
sizeof(double) = 8
sizeof(size_t) = 8
在基于 x64 的操作系統(tǒng)中安裝兩個不同版本的 gcc
,使用 sizeof
對基礎類型測量的占用空間,得到結論如下:
type | GCC x32 | GCC x64 |
---|---|---|
char | 1 | 1 |
short | 2 | 2 |
int | 4 | 4 |
long | 4 | 4 |
float | 4 | 4 |
double | 8 | 8 |
pointer | 8 | 16 |
size_t | 4 | 8 |
可以明顯看到:
- 基礎數據類型占用的空間不會因為
gcc
的版本不同而不同的 - 即便是基于 x64 的平臺,在
gcc
32 位的環(huán)境下,指針的長度也為 8
運算符與表達式
- 賦值運算符號
=、+=、-=、*=、/=、%=
- 算術運算符號
+、-、*、/、%
-
自運算符號:
++
和--
- 前綴:先運算后賦值
- 后綴:先賦值后運算
-
關系運算符號
<、<=、>、>=、==、!=
- 邏輯運算符號
與(&&)、或(||)、非(!)
- 逗號運算符號
一個句子就會像一個函數一樣有返回值,如果用逗號隔開, 這個【返回值】就會變成最后那個表達式的值!
#include <stdio.h>int main() {3, 4, 5;//這是一條語句int n = (3, 4, 5); // n 的值 5printf("n=%d\n", n);int a=3, b=4, c=5;int x=0;int y=((x=a+b),(b+c));// 等價于 y=b+c 致使 y 的值是 9printf("x=%d,y=%d\n", x, y);y=(x=a+b),(b+c);// 等價于 y=x=a+b 致使 y 的值是 7printf("x=%d,y=%d\n", x, y);return 0;
}
- 條件運算符號
int num = 2024;
printf("num is %s\n", num % 2 == 0 ? "even" : "odd");
語句與控制流
- 控制語句
// for 循環(huán)
for(int i = 0; i < 5; i++) {printf("%d\t", i);
}
printf("for end\n");// do 循環(huán)
int i = 0;
do {printf("%d\t", i);i++;
} while(i < 5);
printf("do end\n");// while 循環(huán)
i = 0;
while(i < 5) {printf("%d\t", i);i++;
}
printf("while end\n");
- 函數返回(return)
int main() {return 0;
}
- 轉向語句
// for 循環(huán)
for(int i = 0; i < 5; i++) {printf("%d\t", i);if(i == 2) {goto end_flag;}
}
printf("for end\n");end_flag:
printf("finish!\n");
- 函數調用語句
printf(“Hello world.”);
- 表達式語句
i++;
- 空語句
直接只有一個分號的一行語句。
- 復合語句,也稱為代碼塊或者語句塊
// 代碼塊
{int i = 10;printf("i=%d\n", i);
}
// 復合語句
{int i = 20;printf("i=%d\n", i);
}
函數與程序結構
被調用的函數必須在調用行之前被定義
函數的聲明和定義
在 C 語言中,一般地,一個函數由函數頭和函數體兩部分組成。一般形式如下:
返回值類型 函數名 (參數1, 參數2, ...) {// 代碼
}
上述形式中,大括號部分就是函數體,除了大括號的那一部分就被稱之為函數頭。
如果只有函數的聲明,而沒有函數的定義,那么程序將會在鏈接時出錯
void fun1() {} // 該行是對函數 fun1 進行的定義
void fun2(); // 該行只是函數聲明,并沒有對該函數進行定義int main() {return 0;
}// 如下代碼是對 fun2 函數進行具體的定義,如果不定義 fun2, 會在鏈接環(huán)節(jié)出現錯誤。
void fun2() {// statements here...
}
函數的參數
在定義函數的時候,可以在小括號中定義一系列變量,這些變量被稱之為函數的參數。
這一系列變量就構成了函數的參數列表,它定義了該函數可以接納那些參數的輸入。
參數列表和函數名一起構成了函數簽名。
形式參數和實際參數
形式參數
形參出現在被調函數當中,在整個函數體內都可以使用。
形參在定義時編譯系統(tǒng)并不分配存儲空間,只有在調用該函數時才分配內存單元。
調用結束內存單元被釋放,故形參只有在函數調用時有效,調用結束時不能再使用。
實際參數
實參出現在主調函數當中,當函數調用時,主調函數把實參的值傳送給被調函數的形參,從而實現函數間的數據傳遞。
- 實參與形參必須個數相同
- 對應的形參和實參的類型必須一致
實參的傳遞方式
在調用函數的時候,數據從實參傳遞給形參,可以使得函數在內部讀取或使用函數外部的變量的數據。傳遞的方式主要有兩種:傳值和傳址
傳值(值傳遞)
值傳遞的特點是將實參的數據拷貝給形參,形參的修改并不會影響到實參本身,因為修改的是不同的地址。
傳址(址傳遞)
址傳遞的特點是將實參的地址傳遞給形參,形參的修改會同步影響到實參本身,因為修改的是相同的地址。
數組作為實參進行傳遞
傳遞數組元素
數組元素(也稱之為下標變量)作為函數的參數進行的數據傳遞是值傳遞方式。
傳遞數組名稱
數組名(即數組首地址)、數組元素的地址(
&arr[0]
)作為函數參數進行的數據傳遞是地址傳遞方式。
#include <stdio.h>void func(int *p) {printf("p=%p\n", p);
}
int main() {int ns[] = {3,4,5};func(ns);func(&ns); // warning: expected 'int *' but argument is of type 'int (*)[3]'func(&ns[0]);return 0;
}
上述三種傳遞的方式,形參 p
接收到的是同一個地址,如果 p
修改了地址中的值,ns
中的值也會跟著變化。
函數的返回和嵌套
返回值類型:如果函數需要返回一個值,則需要為其指定具體的類型
函數的嵌套:定義函數時不能定義另一個函數,但是可以進行嵌套調用函數。
- 如果需要從調用函數帶回一個函數值(供主函數使用),被調函數中需包含return語句
- 函數的返回值通過函數中的
return
語句傳遞到調用行。 - 在定義函數時要指定函數值的類型,函數類型決定返回值的類型
int func(int a, int b) {return a + b;
}
函數的遞歸
函數的遞歸調用是指:一個函數在他的函數體內直接或間接地調用它自身。
分為:直接遞歸(函數直接調用自身)和間接遞歸(函數通過其他函數調用自身)。
可分為“回溯”和“遞推”兩個階段。
#include<stdio.h>// 階乘:一個正整數的階乘是所有小于及等于該數的正整數的積,并且0的階乘為1。自然數n的階乘寫作n!。
int factorial(int a) {if(a < 2) {return 1;}return a * factorial(a - 1);
}int main() { printf("5!=%d\n", factorial(5));return 0;
}
變量的作用域
局部變量
首先,它是一個變量,其次,這個變量只是在程序的局部范圍內有效;局部變量定義可以定義在如下位置:
- 函數的開頭;
- 函數內的復合語句內定義;
- 形式參數;
- 函數中間(非開頭);
程序執(zhí)行到某個函數時,這個函數內部的局部變量將會被分配內存空間;局部變量在函數執(zhí)行結束后,變量所占內存將會被釋放;
全局變量
首先,它是變量,其次,它可以在全局范圍內是有意義的變量;
所謂全局也并不是真正的全局,而是在定義處以下的范圍內才是有效的;全局變量定義的位置:
- 文件開頭;
- 函數前;
- 函數后;
- 文件結尾;
注意事項
-
為了區(qū)別全局變量和局部變量,往往大家在寫程序的時候都喜歡將全局變量的首字母大寫,而局部變量的首字母小寫;
-
全局變量的優(yōu)點和缺點:
**優(yōu)點:**C語言的函數,每次最多只能返回一個值,但是如果定義了全局變量,那么在這個變量的有效范圍內,很多函數都能改變這個變量的值,所以增加了函數之間的聯系,通過函數的調用可以得到一個或一個以上的值;
缺點:(大量使用全局變量的情況下)- 占內存:全局變量所占的內存空間不會像局部變量一樣會被釋放;
- 降低程序清晰性:無法隨時確定定義的全局變量的值的大小;
- 降低通用性:程序設計時要求函數的“內聚性”強,函數與函數之間“耦合性”弱;定義全局變是一定要注意在有效范圍內變量不能重名,并且當全局變量被跨文件調用的函數調用時,不能出現全局變量與所跨文件中存在重名變量,否則有可能會出錯;所以,為了提高程序的可靠性,可移植性和可讀性等,全局變量盡量少用;
頭文件
#include
#include
是C語言的預處理指令之一,在編譯之前做的處理,預處理指令一般以#
開頭#include
指令后面會跟著一個文件名,預處理器發(fā)現#include
指令后,就會根據文件名去查找文件,并把這個文件的內容包含到當前文件中。- 被包含文件中的文本將替換源文件中的
#include
指令,就像是把被包含文件中的全部內容拷貝到這個#include
指令所在的位置一樣。 - 所以第一行的
#include <stdio.h>
指令的作用是將stdio.h
文件里面的所有內容拷貝到第一行中。 - 如果被包含的文件拓展名為
.h
,我們稱之為 頭文件 (Header File) - 頭文件可以用來聲明函數,要想使用這些函數,就必須先用
#include
指令包含函數所在的頭文件 #include
指令不僅僅限于.h
文件,可以包含任何編譯器能識別的 C/C++ 代碼文件,如.c
、.hpp
、.cpp
等,甚至.txt
、.abc
等文本文件都可以#include
使用的是相對路徑,也可以使用絕對路徑。比如#include "/Users/apple/Desktop/my.txt"
#include <>
和 #include ""
的區(qū)別
二者的區(qū)別在于:當被include的文件路徑不是絕對路徑的時候,有不同的搜索順序。
- 對于使用雙引號
""
來include
文件,搜索的時候按以下順序:
- 先在這條
include
指令所在的文件的所在文件夾內搜索 - 如果上一步找不到,則在該文件的父級目錄內搜索;
- 如果上一步找不到,則在編譯器設置的
include
路徑內搜索; - 如果上一步找不到,則在系統(tǒng)的
INCLUDE
環(huán)境變量內搜索
- 對于使用尖括號
<>
來include
文件,搜索的時候按以下順序:
- 在編譯器設置的
include
路徑內搜索; - 如果上一步找不到,則在系統(tǒng)的
INCLUDE
環(huán)境變量內搜索
頭文件的引用
一般地,系統(tǒng)提供的頭文件用
< >
引用, 自己寫的用" "
引用。
include
是可以包含.c
源文件的,在某些工程里可以看到,但是這樣的做法不常見也不推薦;include
關鍵字包含.c
源文件和.h
頭文件,理解都是一樣的,在原地將引用的文件展開;
頭文件的內容
頭文件里一般包括宏定義, 全局變量, 函數原型聲明。
頭文件名的格式為 "_頭文件名_"
,注意要大寫
#ifndef 頭文件名
#define 頭文件名頭文件內容#endif
示例
頭文件:
app.h
#ifndef _APP_H
#define _APP_Hvoid func(int n);#endif
源文件:
app.c
#include <stdio.h>
#include "app.h"int main() {func(10);return 0;
}void func(int n) {printf("n=%d\n", n);
}
可以使用
gcc app.c
即可對其進行編譯,#include
指令會自動找到同目錄下的.h
文件
內部函數和外部函數
函數的調用,一般是對同一個源文件中的其他函數進行調用的,也可以對另外一個源文件中的函數進行調用
C語言中,根據函數能否被其他源文件調用,分為內部函數和外部函數
外部函數,可以被其他源文件調用的函數
內部函數,只在定義的文件中有效
外部函數 (extern)
定義外部函數的方式,在函數的返回值類型前面添加 extern
關鍵字
extern int add(int x,int y);
舉例來說外部函數:
- 第一個源文件
other.c
#include <stdio.h>int add(int a, int b) {int c = a + b;printf("call add(%d,%d)=%d\n", a, b, c);return a + b;
}
- 第二個源文件
app.c
// 該行的 extern 可省略
// 該行也可以直接刪除,但不顯式聲明 add 函數的話,編譯器會報警告
extern int add(int x,int y); int main() {add(3,4);return 0;
}
在調用方的源文件中,可以不用 #include
指令來引入定義方的源文件,使用 gcc
對這兩個文件進行編譯即可:
gcc other.c app.c
編譯器通過 extern
關鍵字會明確地知道,add
函數是定義在其他文件中的外部函數。
在本例中,app.c
作為調用方,省略 extern
關鍵字也可以正常運行。但是必須在調用方的源文件中聲明需要調用的函數的聲明,否則會在編譯時報警告,但依然能輸出調用結果,如下所示:
app.c: In function 'main':
app.c:6:2: warning: implicit declaration of function 'add' [-Wimplicit-function-declaration]add(3,4);^~~
內部函數 (static)
只在定義的文件中有效,這類函數稱為內部函數。
在定義內部函數時,需要在函數的返回值類型前面添加
static
關鍵字,也稱靜態(tài)函數。
舉個例子來說明內部函數:
- 第一個源文件
other.c
#include <stdio.h>void func() {printf("func in other.c\n");
}
- 第二個源文件
app.c
#include <stdio.h>static void func() {printf("func in app.c\n");
}int main() {func();return 0;
}
在調用方的源文件中,可以不用 #include
指令來引入定義方的源文件,使用 gcc
對這兩個文件進行編譯即可:
gcc other.c app.c
編譯器通過 static
關鍵字會明確地知道,func
函數是僅供內部使用的。故而在 main
方法中調用 func
函數的時候,輸出的結果自然是 func in app.c
。
如果將第二個文件中的 static 去掉,會在編譯過程中拋出 multiple definition
的錯誤信息。致使無法得到最后的目標程序。
如果在 app.c
中直接刪除 func
函數的定義,輸出的結果就會是 func in other.c
。但同時也會出現警告,因為 main
中的 func
是作為外部函數來進行調用的,而 app.c
中又不存在該函數的聲明。
思考:如果在 other.c
中定義一個函數,默認是可以在 app.c
中作為外部函數進行調用的,那使用 static
修飾后,在 app.c
中又顯示注明需要調用外部函數,會出現什么結果呢?
- 第一個源文件
other.c
#include <stdio.h>static void func() {printf("func in other.c\n");
}
- 第二個源文件
app.c
extern void func();int main() {func();return 0;
}
執(zhí)行編譯后,出現 undefined reference to func'
的異常,編譯不通過,刪除 other.c
中的 static
關鍵字刪除后,就可以正常編譯通過了。編譯器通過 extern
關鍵字知道調用方源文件中需要使用外部函數 func
,但是能夠在 other.c
中被找到的 func
函數使用了 static
關鍵字修飾,也就不能允許外部源文件調用。故而將該函數視作是未定義進而拋出編譯時異常。
數組
數組是一組相同類型元素的集合。
若將有限個類型相同的變量的集合命名,那么這個名稱為數組名。
組成數組的各個變量稱為數組的分量,也稱為數組的元素,有時也稱為下標變量。
用于區(qū)分數組的各個元素的數字編號稱為下標,下標從零開始。
數組的創(chuàng)建
一維數組創(chuàng)建示例:
int ns[5];
// 數組創(chuàng)建,[]中要給一個常量才可以,不要使用變量。
const int N = 10;
int ms[N];
數組在創(chuàng)建的時候如果想不指定數組的確定的大小就得初始化。數組的元素個數根據初始化的內容來確定。
char cs1[] = "abc";
char cs2[3] = {'a','b','c'};
數組的使用
使用下標引用操作符
[]
能對數組中的元素進行索引(下標)訪問。使用 sizeof 可以量出數組的長度和某個元素的長度,以此來計算數組的長度。
int ns[] = {2, 5, 4};
int len = sizeof(ns) / sizeof(ns[0]);
printf("sizeof(ns)=%d\n", sizeof(ns));
printf("sizeof(ns[0])=%d\n", sizeof(ns[0]));
printf("len=%d\n", len);
- 數組的大小可以通過計算得到。
- 數組在內存中是連續(xù)存放的。
二維數組
二維數組可以視作是一個一維數組嵌套一個一維數組。表現形式上可以理解為是一個矩陣。
int ns1[3][4];
// ns1 可視作 3x4 的矩陣,如下所示:
// □ □ □ □
// □ □ □ □
// □ □ □ □
int ns2[2][3] = {3,5,7};
// ns2 可視作 2x3 的矩陣,如下所示:
// 3 5 7
// □ □ □
int ns3[3][3] = {{2,4},{6}};
// ns3 可視作 3x3 的矩陣,如下所示:
// 2 4 □
// 6 □ □
// □ □ □// 雙層循環(huán)以遍歷二維數組
for(int i=0;i<3;i++) {for(int j=0;j<3;j++) {printf("(%d,%d)=%d\t", i, j, ns3[i][j]);}printf("\n");
}
數組越界
- 數組的下標是有范圍限制的。
- 數組的下規(guī)定是從0開始的,如果輸入有n個元素,最后一個元素的下標就是n-1。
- 所以數組的下標如果小于0,或者大于n-1,就是數組越界訪問了,超出了數組合法空間的訪問。
- C語言本身是不做數組下標的越界檢查,編譯器也不一定報錯,但是編譯器不報錯,并不意味著程序就是正確的,所以程序員寫代碼時,最好自己做越界的檢查。
- 二維數組的行和列也可能存在越界。
變長數組
數組變量本身表達的就是地址。在 C 中,數組變量就是一個特殊常量指針,也稱之為數組指針。
可以利用 malloc
進行動態(tài)創(chuàng)建指定長度的數組,如下所示:
#include<stdio.h>
#include<stdlib.h>int main(void) {int size = 12, i=0;int* const p = (int*)malloc(size*(sizeof(int)));// 為所有下標位置賦值for (i=0;i<size; i++) {p[i] = i;}// 輸出所有下標位置的值for (i = 0; i <size; i++) {printf("%d,", p[i]);}free(p);return 0;
}
指針
在計算機科學中,指針(Pointer)是編程語言中的一個對象。
它的值直接指向(points to)存在電腦存儲器中另一個地方的值。
由于通過地址能找到所需的變量單元,可以說,地址指向該變量單元。
因此,將地址形象化的稱為“指針”。意思是通過它能找到以它為地址的內存單元。
指針的聲明
指針是一個變量,其值是另一個變量的地址,即,內存位置的直接地址。就像其他變量或常量一樣,必須在使用指針存儲其他變量地址之前,對其進行聲明。指針變量聲明的一般形式為:type* var-name;
如下例所示:
#include<stdio.h>int main() {// 聲明一個 int 類型的指針int* ip;// 聲明一個 long 類型的指針long *lp;// 聲明一個 double 類型的指針double* dp;// 聲明一個 float 類型的指針float* fp;printf("size of int pointer is %d.\n", sizeof(ip));printf("size of long pointer is %d.\n", sizeof(lp));printf("size of double pointer is %d.\n", sizeof(dp));printf("size of float pointer is %d.\n", sizeof(fp));return 0;
}
執(zhí)行結果:
size of int pointer is 4.
size of long pointer is 4.
size of double pointer is 4.
size of float pointer is 4.
在聲明的時候,*
無論是偏向數據類型的關鍵字還是偏向變量名稱,都是被認為是合法的。也就是說:int *ip;
和 int* ip;
都是正確的聲明方式。但是當多個變量在同一個聲明語句時候,需要使用 *ip
的形式。例如:
int *a, *b; // 聲明兩個 int 類型的指針變量,分別是 a 和 b
int a, *b, c; // 聲明一個 int 類型的指針變量 b,變量 a 和 c 都是普通的 int 變量。
因為指針變量存儲的值是一個地址值,所以,無論什么類型的指針,都不會影響其本身需要占用內存的空間。由于指針變量接受的是地址值,所以,在給指針變量賦值的時候需使用到取址符 &
,如下例所示:
#include<stdio.h>int main() {int number = 10;int* ip = &number;printf("number is %d\n", number);printf("&number is %d\n", &number);printf("ip is %d\n", ip);printf("&ip is %d\n", &ip);printf("*ip is %d\n", *ip);return 0;
}
執(zhí)行結果:
number is 10
&number is 2293564
ip is 2293564
&ip is 2293560
*ip is 10
既然指針也是變量,那根據上面得到的結果對照到表格中來看:
變量名稱 | 變量的類型 | 變量的值 | 變量的地址 | 為變量賦值 | |
---|---|---|---|---|---|
int number = 10; | number | int | 10 | 2293564 | number = 10 |
int* ip = &number; | ip | int* | 2293564 | 2293560 | ip = &number |
指針是一種特殊的變量,其存儲的數據不是一個可以直接被人識別的數字,或者文本,而是某個其他變量的內存地址值。
指針的簡單應用
借助指針,可以對該內存地址的數據進行讀寫。如如下例:
#include<stdio.h>int main() {int a = 10;int* ip = &a;printf("a is %d,\t &a is %d\n", a, &a);printf("ip is %d \t &ip is %d \t *ip is %d\n", ip, &ip, *ip);// 修改指針所指向的內存地址(2293564)中的值*ip = 100;printf("a is %d,\t &a is %d\n", a, &a);printf("ip is %d \t &ip is %d \t *ip is %d\n", ip, &ip, *ip);return 0;
}
執(zhí)行結果:
a is 10, &a is 2293564
ip is 2293564 &ip is 2293560 *ip is 10
a is 100, &a is 2293564
ip is 2293564 &ip is 2293560 *ip is 100
這樣不需要變量 a 就能實現修改變量 a 所存儲的值。
在執(zhí)行過程中,也可以修改指針的存儲的地址值為其他的變量。如下所示:
#include<stdio.h>int main() {int a = 10, b = 20;int* ip = &a;printf("a is %d,\t &a is %d\n", a, &a);printf("b is %d,\t &b is %d\n", b, &b);printf("ip is %d \t &ip is %d \t *ip is %d\n", ip, &ip, *ip);*ip *= 2;printf("a is %d,\t &a is %d\n", a, &a);printf("b is %d,\t &b is %d\n", b, &b);printf("ip is %d \t &ip is %d \t *ip is %d\n", ip, &ip, *ip);ip = &b;*ip *= 3;printf("a is %d,\t &a is %d\n", a, &a);printf("b is %d,\t &b is %d\n", b, &b);printf("ip is %d \t &ip is %d \t *ip is %d\n", ip, &ip, *ip);return 0;
}
執(zhí)行結果:
a is 10, &a is 2293564
b is 20, &b is 2293560
ip is 2293564 &ip is 2293556 *ip is 10
a is 20, &a is 2293564
b is 20, &b is 2293560
ip is 2293564 &ip is 2293556 *ip is 20
a is 20, &a is 2293564
b is 60, &b is 2293560
ip is 2293560 &ip is 2293556 *ip is 60
這里的指針 ip 先指向的是變量 a 的地址,在對該地址的數據進行累乘操作后,指向了變量 b 的地址,又對變量 b 的地址中的值進行了累乘操作。最終 a 的值被乘以 2,b 的值被乘以3。
NULL 指針
如果在聲明一個指針的時候,沒有為其賦予確切的地址值,那指針可能會指向一個未知的地址。如下例所示:
#include<stdio.h>int main() {int *a, *b;printf("a=%d, *a=%d, b=%d, *b=%d\n", a, *a, b, *b);return 0;
}
執(zhí)行結果:
a=2293540, *a=4200720, b=2147344384, *b=0
在沒有為指針變量 a 和 b 賦予地址值的時候,既然還能有值,并且能讀取到該地址里的值。因為一旦指針存儲了地址值后,可以對該地址進行修改操作,可能會出現一些不該出現的現象,比如某個地方的變量的值因為這個指針的原因被修改了之類的。
如果沒有確切的地址可以賦值,為指針變量賦一個 NULL 值作為初始值是一個良好的編程習慣。賦為 NULL 值的指針被稱為空指針。
因為在 C 中,把任何非零和非空的值假定為 true,把零或 null 假定為 false,所以,當一個指針是空指針的時候,是可以被判斷的。這也有利于后面更加順利地操作指針。代碼如下:
#include<stdio.h>int main() {int a = 10;int* ip = NULL;printf("a is %d,\t &a is %d\n", a, &a);if(ip) {printf("ip is not null pointer.\n");} else {printf("ip is null pointer.\n");ip = &a;}*ip *= 2;printf("ip is %d \t &ip is %d \t *ip is %d\n", ip, &ip, *ip);return 0;
}
執(zhí)行結果:
a is 10, &a is 2293564
ip is null pointer.
ip is 2293564 &ip is 2293560 *ip is 20
當然,在該程序中 ip 自然是為空指針,但當 ip 作為一個函數參數的時候,對指針判空的處理就很重要了,因為不知道外部傳遞的指針到底是否是空的。
指針的算術運算
指針是一種存儲其他變量地址值的特殊變量,同時也能進行簡單的算數運算和邏輯運算。運用自運算遍歷數組一種常見的指針應用。
#include<stdio.h>int main() {int i, size = 7;int nums[7] = {101, 202, 303, 404, 505, 606, 707};int *p = NULL;p = nums;for(i = 0; i < size; i++) {printf("nums[%d] = %d, p==%d, *p=%d\n", i, nums[i], p, *p);p++;}return 0;
}
執(zhí)行結果:
nums[0] = 101, p==2293528, *p=101
nums[1] = 202, p==2293532, *p=202
nums[2] = 303, p==2293536, *p=303
nums[3] = 404, p==2293540, *p=404
nums[4] = 505, p==2293544, *p=505
nums[5] = 606, p==2293548, *p=606
nums[6] = 707, p==2293552, *p=707
因為數組變量在內存中是以連續(xù)的內存空間來存儲數據的,故而,p = nums;
等價于 p = &nums[0];
也就是說,將數組 nums
的地址值賦予給指針,等價于將數組中第一個元素的地址值賦值給指針。
從輸出的結果上來看也會發(fā)現,連續(xù)的每個元素的地址相差 4 位,這個值正是在開篇使用 sizeof 量出來的指針的大小是一致的。
那既然 p = nums;
等價于 p = &nums[0];
那將數組中最后一個元素的地址值賦予給指針做自減運算輸出會怎么樣。代碼如下:
#include<stdio.h>int main() {int i, size = 7;int nums[7] = {101, 202, 303, 404, 505, 606, 707};int *p = NULL;p = &nums[size - 1];for(i = size - 1; i > -1; i--, p--) {printf("nums[%d] = %d, p==%d, *p=%d\n", i, nums[i], p, *p);}return 0;
}
執(zhí)行結果:
nums[6] = 707, p==2293552, *p=707
nums[5] = 606, p==2293548, *p=606
nums[4] = 505, p==2293544, *p=505
nums[3] = 404, p==2293540, *p=404
nums[2] = 303, p==2293536, *p=303
nums[1] = 202, p==2293532, *p=202
nums[0] = 101, p==2293528, *p=101
因為數組中每個元素的地址是連續(xù)的,當指針指向第一個元素的時候,只要指針指向的地址沒有超過最后一位,就能進行循環(huán)取值。這里就需要對指針中存儲的地址值進行比較,示例代碼如下:
#include<stdio.h>int main() {int size = 7;int nums[7] = {101, 202, 303, 404, 505, 606, 707};int *p = nums; // 等價于 &nums[0]while(p <= &nums[size - 1]) {printf("%d ", *p);p++;}return 0;
}
執(zhí)行結果:
101 202 303 404 505 606 707
指針數組
指針數組往往是用于存儲一系列相同類型的指針的集合。其聲明的一般形式為:type* var-name[size];
,例如:
int* ps[3];
在理解指針的時候,可以類比變量,同樣的,理解指針數組可以類比數組。
數組名稱 | 數組中每個元素的類型 | 獲取下標為0的元素的值 | 獲取下標為0的元素的地址值 | 為下標為0的元素賦值 | |
---|---|---|---|---|---|
int nums[3]; | nums | int | nums[0] | &nums[0] | nums[0] = 10; |
int* ps[3]; | ps | int* | ps[0] | &ps[0] | ps[0] = &nums[0] |
這樣一來,很容易理解指針數組了,它就是一系列指針的集合。他們不一定是連續(xù)的,就像數組中的數據不一定是連續(xù)的數字的道理一樣。這里使用一個案例來對指針數組和普通數組進行對比:
#include<stdio.h>int main() {int i, a = 101, b = 202, c = 303, size = 3;int nums[3];nums[0] = a;nums[1] = b;nums[2] = c;int* ps[3];ps[0] = &a;ps[1] = &b;ps[2] = &c;// 輸出各個變量的值for(i = 0; i < size; i++) {printf("nums[%d]=%d, &nums[%d]=%d\n", i, nums[i], i, &nums[i]);} printf("\na=%d, &a=%d, b=%d, &b=%d, c=%d, &c=%d\n\n", a, &a, b, &b, c, &c);for(i = 0; i < size; i++) {printf("ps[%d]=%d, *ps[%d]=%d, &ps[%d]=%d\n", i, ps[i], i, *ps[i], i, &ps[i]);}// 修改指針指向的地址中的值for(i = 0; i < size; i++) {*ps[i] *= 10;}// 再次輸出各個變量的值printf("\n------------------\n");for(i = 0; i < size; i++) {printf("nums[%d]=%d, &nums[%d]=%d\n", i, nums[i], i, &nums[i]);} printf("\na=%d, &a=%d, b=%d, &b=%d, c=%d, &c=%d\n\n", a, &a, b, &b, c, &c);for(i = 0; i < size; i++) {printf("ps[%d]=%d, *ps[%d]=%d, &ps[%d]=%d\n", i, ps[i], i, *ps[i], i, &ps[i]);}return 0;
}
執(zhí)行結果:
nums[0]=101, &nums[0]=2293536
nums[1]=202, &nums[1]=2293540
nums[2]=303, &nums[2]=2293544a=101, &a=2293556, b=202, &b=2293552, c=303, &c=2293548ps[0]=2293556, *ps[0]=101, &ps[0]=2293524
ps[1]=2293552, *ps[1]=202, &ps[1]=2293528
ps[2]=2293548, *ps[2]=303, &ps[2]=2293532------------------
nums[0]=101, &nums[0]=2293536
nums[1]=202, &nums[1]=2293540
nums[2]=303, &nums[2]=2293544a=1010, &a=2293556, b=2020, &b=2293552, c=3030, &c=2293548ps[0]=2293556, *ps[0]=1010, &ps[0]=2293524
ps[1]=2293552, *ps[1]=2020, &ps[1]=2293528
ps[2]=2293548, *ps[2]=3030, &ps[2]=2293532
從執(zhí)行結果來看,指針數組中存儲的三個指針,修改這三個指針對應地址的值,會影響變量 a, b, c,但是不影響數組 nums。因為在給數組賦值的時候,是將變量 a, b, c 的值賦予了數組 nums,也就是 傳值。給指針數組賦值的時候,是將變量 a, b, c 的地址值賦予了 ps,也就是 傳址。這樣就更加好理解指針數組了,數組是一系列相同數據類型的數據的集合,而指針數組是一系列相同數據類型的指針的集合。
數組指針
在 C 中,數組到底是什么?
現象一:
在上文中的 指針的算術運算 段落中的內容可以知道:p = nums;
等價于 p = &nums[0];
。
分析:
賦值運算符 =
兩邊的數據類型在一致的時候才能將右值賦予左邊的變量,如果不是因為數據類型發(fā)生了隱式轉換,那就是符號兩邊的數據類型本就是一致的。這是不是能說明數組變量 nums 就是一個指針?如果是這樣,那這個指針存儲的值難道是數組中第一個元素的地址?如果是這樣,那就能解釋為什么在給指針 p 賦值的時候,nums 不需要 &
作為前綴了。
現象二:
回顧在之前的變量學習中,可以知道,變量的賦值可以是這樣的:
int a = 10; int b = 20; b = a;
最終變量 b 的值變成了 20,那同樣的代碼,數組能這樣使用嗎?比如:
int a[] = {1, 2}; int b[] = {3, 4}; b = a;
程序在編譯的時候會拋出一個錯誤信息:
error: assignment to expression with array typeb = a;^
分析:
為什么這里會提示數組類型的變量在聲明之后不允許使用賦值表達式?這個現象和常量很相似,常量在定義完成之后,也是不允許為其賦值。這是不是能說明數組是一個常量?
解釋:
在 C 中,數組變量就是一個特殊常量指針,也稱之為數組指針。它有如下特點:
- 數組變量本身表達的就是地址。所以
nums == &nums[0]
- 運算符
[]
可以對數組做運算,也可以對指針做運算。所以p[0]
等價于nums[0]
- 運算符
*
可以對指針做運算,也可以對數組做運算。所以*nums
是被允許的 - 數組變量是 const 的指針,所以不允許被賦值。也就是說
int nums[]
等價于int * const nums
實例:
#include<stdio.h>#define SIZE 6
int main() {int nums[SIZE] = {101, 202, 303, 404, 505, 606};int *p = nums;// nums 和 p 都能使用運算符 *printf("*p=%d, *nums=%d\n", *p, *nums);printf(" p=%p, nums=%p\n", p, nums);printf("&p=%p, &nums=%p\n", &p, &nums);// nums 和 p 都能使用邏輯運算符printf("p <= &nums[1] is %s\n", p <= &nums[1] ? "true" : "false");printf("nums < &nums[1] is %s\n", nums < &nums[1] ? "true" : "false");// nums 和 p 都能使用下標來操作元素for(int i = 0; i < SIZE; i++) {printf("nums[%d]=%d, &nums[%d]=%p, p[%d]=%d, &p[%d]=%p\n", i, nums[i], i, &nums[i], i, p[i], i, &p[i]);}// nums 是常量指針,故而不能做自運算while(p < &nums[SIZE - 1]) {p++;// error: lvalue required as increment operand// nums++;printf("p=%p, *p=%d, nums=%p, *nums=%d\n", p, *p, nums, *nums);}return 0;
}
執(zhí)行結果:
*p=101, *nums=101p=0022FF24, nums=0022FF24
&p=0022FF20, &nums=0022FF24
p <= &nums[1] is true
nums < &nums[1] is true
nums[0]=101, &nums[0]=0022FF24, p[0]=101, &p[0]=0022FF24
nums[1]=202, &nums[1]=0022FF28, p[1]=202, &p[1]=0022FF28
nums[2]=303, &nums[2]=0022FF2C, p[2]=303, &p[2]=0022FF2C
nums[3]=404, &nums[3]=0022FF30, p[3]=404, &p[3]=0022FF30
nums[4]=505, &nums[4]=0022FF34, p[4]=505, &p[4]=0022FF34
nums[5]=606, &nums[5]=0022FF38, p[5]=606, &p[5]=0022FF38
p=0022FF28, *p=202, nums=0022FF24, *nums=101
p=0022FF2C, *p=303, nums=0022FF24, *nums=101
p=0022FF30, *p=404, nums=0022FF24, *nums=101
p=0022FF34, *p=505, nums=0022FF24, *nums=101
p=0022FF38, *p=606, nums=0022FF24, *nums=101
指向指針的指針
我們知道指針存儲的是其他變量的的地址值,而指針本身也是一個變量,那一個指針指向的變量正好也是一個指針變量呢?這種情況被稱之為指向指針的指針。
指向指針的指針在聲明的時候必須比被指向的指針變量多一個星號,如下所示:
#include<stdio.h>int main() {int num = 10;int *p = #int **ip = &p;int ***ipp = &ip;printf("num=%d, &num=%d\n", num, &num);printf("p=%d, *p=%d, &p=%d\n", p, *p, &p);printf("ip=%d, *ip=%d, &ip=%d\n", ip, *ip, &ip);printf("ipp=%d, *ipp=%d, &ipp=%d\n", ipp, *ipp, &ipp);return 0;
}
執(zhí)行結果:
num=10, &num=2293564
p=2293564, *p=10, &p=2293560
ip=2293560, *ip=2293564, &ip=2293556
ipp=2293556, *ipp=2293560, &ipp=2293552
如果上例中的指針 ipp 指向指針 p 會在編譯的時候拋出一個警告:
warning: initialization of 'int ***' from incompatible pointer type'int **' [-Wincompatible-pointer-types]int ***ipp = &p;^
但是還是會編譯通過,執(zhí)行結果是:
num=10, &num=2293564
p=2293564, *p=10, &p=2293560
ip=2293560, *ip=2293564, &ip=2293556
ipp=2293560, *ipp=2293564, &ipp=2293552
指針 ipp 最終還是成功指向了指針 p,雖然這樣做是可行的,但不建議這樣去寫。
如果按照正常的方式去編寫,這種指向指針的指針能最多能寫多少個星號呢?目前貌似沒有找到相關資料來解釋這個問題。就下面的案例來看,能寫到至少 6 顆星。
#include<stdio.h>int main() {int num = 10;int *p = #int **ip = &p;int ***ipp = &ip;int ****ippp = &ipp;int *****ipppp = &ippp;int ******ippppp = &ipppp;printf("num=%d, &num=%d\n", num, &num);printf("p=%d, *p=%d, &p=%d\n", p, *p, &p);printf("ip=%d, *ip=%d, &ip=%d\n", ip, *ip, &ip);printf("ipp=%d, *ipp=%d, &ipp=%d\n", ipp, *ipp, &ipp);printf("ippp=%d, *ippp=%d, &ippp=%d\n", ippp, *ippp, &ippp);printf("ipppp=%d, *ipppp=%d, &ipppp=%d\n", ipppp, *ipppp, &ipppp);printf("ippppp=%d, *ippppp=%d, &ippppp=%d\n", ippppp, *ippppp, &ippppp);return 0;
}
執(zhí)行結果:
num=10, &num=2293564
p=2293564, *p=10, &p=2293560
ip=2293560, *ip=2293564, &ip=2293556
ipp=2293556, *ipp=2293560, &ipp=2293552
ippp=2293552, *ippp=2293556, &ippp=2293548
ipppp=2293548, *ipppp=2293552, &ipppp=2293544
ippppp=2293544, *ippppp=2293548, &ippppp=2293540
字符串
在 C 語言中,字符串實際上是使用 null 字符 ‘\0’ 終止的一維字符數組。
因此,一個以 null 結尾的字符串,包含了組成字符串的字符。
字符
關鍵字 char 可以用來聲明一個字符變量。char 變量既是一種整數(最小的整數類型),也是一種特殊的字符類型。如下所示:
#include<stdio.h>int main() {char c = 'A';printf("c=%d, c=%c", c, c);return 0;
}
執(zhí)行結果:
c=65, c=A
可以發(fā)現,字符 A
在輸出的時候,既可以使用數字格式輸出,也可以使用字符格式輸出,原因是它的數據類型是 char 類型,是一種既是整型又是字符的數據類型。既然是整型,那就能進行算數運算。如下例:
#include<stdio.h>int main() {char c = 'A' + 1;printf("c=%d, c=%c", c, c);return 0;
}
執(zhí)行結果:
c=66, c=B
實際上,char 能將字符 A
進行算數運算的原因是它的每個字符都有對應的無符號整型的數據與之對應。從這里的例子中可以知道,字符 A
對應的整數是 65,字符 B
對應的整數就是 66,如果進行 'Z' - 'A'
的運算,能得到與整數 26對應的字符。
在計算機早期,所有的數據在存儲和運算時都要使用二進制數表示(因為計算機用高電平和低電平分別表示1和0),例如,像a、b、c、d這樣的52個字母(包括大寫)以及0、1等數字還有一些常用的符號(例如*、#、@等)在計算機中存儲時也要使用二進制數來表示。
而具體用哪些二進制數字表示哪個符號的問題,美國國家標準學會(American National Standard Institute , ANSI)制定了一套標準的單字節(jié)字符編碼方案,用于基于文本的數據。
它最初是美國國家標準,供不同計算機在相互通信時用作共同遵守的西文字符編碼標準,后來它被國際標準化組織(International Organization for Standardization, ISO)定為國際標準,稱為ISO 646標準。適用于所有拉丁文字字母。
—— 摘自百度百科
這種字符和整數對應關系的標準被稱之為 ASCII (American Standard Code for Information Interchange),即美國信息交換標準代碼,ASCII 碼表的具體內容如下:
二進制 | 八進制 | 十進制 | 十六進制 | 縮寫/字符 | 解釋 |
---|---|---|---|---|---|
0000 0000 | 0 | 0 | 0x00 | NUL(null) | 空字符 |
0000 0001 | 1 | 1 | 0x01 | SOH(start of headline) | 標題開始 |
0000 0010 | 2 | 2 | 0x02 | STX (start of text) | 正文開始 |
0000 0011 | 3 | 3 | 0x03 | ETX (end of text) | 正文結束 |
0000 0100 | 4 | 4 | 0x04 | EOT (end of transmission) | 傳輸結束 |
0000 0101 | 5 | 5 | 0x05 | ENQ (enquiry) | 請求 |
0000 0110 | 6 | 6 | 0x06 | ACK (acknowledge) | 收到通知 |
0000 0111 | 7 | 7 | 0x07 | BEL (bell) | 響鈴 |
0000 1000 | 10 | 8 | 0x08 | BS (backspace) | 退格 |
0000 1001 | 11 | 9 | 0x09 | HT (horizontal tab) | 水平制表符 |
0000 1010 | 12 | 10 | 0x0A | LF (NL line feed, new line) | 換行鍵 |
0000 1011 | 13 | 11 | 0x0B | VT (vertical tab) | 垂直制表符 |
0000 1100 | 14 | 12 | 0x0C | FF (NP form feed, new page) | 換頁鍵 |
0000 1101 | 15 | 13 | 0x0D | CR (carriage return) | 回車鍵 |
0000 1110 | 16 | 14 | 0x0E | SO (shift out) | 不用切換 |
0000 1111 | 17 | 15 | 0x0F | SI (shift in) | 啟用切換 |
0001 0000 | 20 | 16 | 0x10 | DLE (data link escape) | 數據鏈路轉義 |
0001 0001 | 21 | 17 | 0x11 | DC1 (device control 1) | 設備控制1 |
0001 0010 | 22 | 18 | 0x12 | DC2 (device control 2) | 設備控制2 |
0001 0011 | 23 | 19 | 0x13 | DC3 (device control 3) | 設備控制3 |
0001 0100 | 24 | 20 | 0x14 | DC4 (device control 4) | 設備控制4 |
0001 0101 | 25 | 21 | 0x15 | NAK (negative acknowledge) | 拒絕接收 |
0001 0110 | 26 | 22 | 0x16 | SYN (synchronous idle) | 同步空閑 |
0001 0111 | 27 | 23 | 0x17 | ETB (end of trans. block) | 結束傳輸塊 |
0001 1000 | 30 | 24 | 0x18 | CAN (cancel) | 取消 |
0001 1001 | 31 | 25 | 0x19 | EM (end of medium) | 媒介結束 |
0001 1010 | 32 | 26 | 0x1A | SUB (substitute) | 代替 |
0001 1011 | 33 | 27 | 0x1B | ESC (escape) | 換碼(溢出) |
0001 1100 | 34 | 28 | 0x1C | FS (file separator) | 文件分隔符 |
0001 1101 | 35 | 29 | 0x1D | GS (group separator) | 分組符 |
0001 1110 | 36 | 30 | 0x1E | RS (record separator) | 記錄分隔符 |
0001 1111 | 37 | 31 | 0x1F | US (unit separator) | 單元分隔符 |
0010 0000 | 40 | 32 | 0x20 | (space) | 空格 |
0010 0001 | 41 | 33 | 0x21 | ! | 嘆號 |
0010 0010 | 42 | 34 | 0x22 | " | 雙引號 |
0010 0011 | 43 | 35 | 0x23 | # | 井號 |
0010 0100 | 44 | 36 | 0x24 | $ | 美元符 |
0010 0101 | 45 | 37 | 0x25 | % | 百分號 |
0010 0110 | 46 | 38 | 0x26 | & | 和號 |
0010 0111 | 47 | 39 | 0x27 | ’ | 閉單引號 |
0010 1000 | 50 | 40 | 0x28 | ( | 開括號 |
0010 1001 | 51 | 41 | 0x29 | ) | 閉括號 |
0010 1010 | 52 | 42 | 0x2A | * | 星號 |
0010 1011 | 53 | 43 | 0x2B | + | 加號 |
0010 1100 | 54 | 44 | 0x2C | , | 逗號 |
0010 1101 | 55 | 45 | 0x2D | - | 減號/破折號 |
0010 1110 | 56 | 46 | 0x2E | . | 句號 |
0010 1111 | 57 | 47 | 0x2F | / | 斜杠 |
0011 0000 | 60 | 48 | 0x30 | 0 | 字符0 |
0011 0001 | 61 | 49 | 0x31 | 1 | 字符1 |
0011 0010 | 62 | 50 | 0x32 | 2 | 字符2 |
0011 0011 | 63 | 51 | 0x33 | 3 | 字符3 |
0011 0100 | 64 | 52 | 0x34 | 4 | 字符4 |
0011 0101 | 65 | 53 | 0x35 | 5 | 字符5 |
0011 0110 | 66 | 54 | 0x36 | 6 | 字符6 |
0011 0111 | 67 | 55 | 0x37 | 7 | 字符7 |
0011 1000 | 70 | 56 | 0x38 | 8 | 字符8 |
0011 1001 | 71 | 57 | 0x39 | 9 | 字符9 |
0011 1010 | 72 | 58 | 0x3A | : | 冒號 |
0011 1011 | 73 | 59 | 0x3B | ; | 分號 |
0011 1100 | 74 | 60 | 0x3C | < | 小于 |
0011 1101 | 75 | 61 | 0x3D | = | 等號 |
0011 1110 | 76 | 62 | 0x3E | > | 大于 |
0011 1111 | 77 | 63 | 0x3F | ? | 問號 |
0100 0000 | 100 | 64 | 0x40 | @ | 電子郵件符號 |
0100 0001 | 101 | 65 | 0x41 | A | 大寫字母A |
0100 0010 | 102 | 66 | 0x42 | B | 大寫字母B |
0100 0011 | 103 | 67 | 0x43 | C | 大寫字母C |
0100 0100 | 104 | 68 | 0x44 | D | 大寫字母D |
0100 0101 | 105 | 69 | 0x45 | E | 大寫字母E |
0100 0110 | 106 | 70 | 0x46 | F | 大寫字母F |
0100 0111 | 107 | 71 | 0x47 | G | 大寫字母G |
0100 1000 | 110 | 72 | 0x48 | H | 大寫字母H |
0100 1001 | 111 | 73 | 0x49 | I | 大寫字母I |
1001010 | 112 | 74 | 0x4A | J | 大寫字母J |
0100 1011 | 113 | 75 | 0x4B | K | 大寫字母K |
0100 1100 | 114 | 76 | 0x4C | L | 大寫字母L |
0100 1101 | 115 | 77 | 0x4D | M | 大寫字母M |
0100 1110 | 116 | 78 | 0x4E | N | 大寫字母N |
0100 1111 | 117 | 79 | 0x4F | O | 大寫字母O |
0101 0000 | 120 | 80 | 0x50 | P | 大寫字母P |
0101 0001 | 121 | 81 | 0x51 | Q | 大寫字母Q |
0101 0010 | 122 | 82 | 0x52 | R | 大寫字母R |
0101 0011 | 123 | 83 | 0x53 | S | 大寫字母S |
0101 0100 | 124 | 84 | 0x54 | T | 大寫字母T |
0101 0101 | 125 | 85 | 0x55 | U | 大寫字母U |
0101 0110 | 126 | 86 | 0x56 | V | 大寫字母V |
0101 0111 | 127 | 87 | 0x57 | W | 大寫字母W |
0101 1000 | 130 | 88 | 0x58 | X | 大寫字母X |
0101 1001 | 131 | 89 | 0x59 | Y | 大寫字母Y |
0101 1010 | 132 | 90 | 0x5A | Z | 大寫字母Z |
0101 1011 | 133 | 91 | 0x5B | [ | 開方括號 |
0101 1100 | 134 | 92 | 0x5C | \ | 反斜杠 |
0101 1101 | 135 | 93 | 0x5D | ] | 閉方括號 |
0101 1110 | 136 | 94 | 0x5E | ^ | 脫字符 |
0101 1111 | 137 | 95 | 0x5F | _ | 下劃線 |
0110 0000 | 140 | 96 | 0x60 | ` | 開單引號 |
0110 0001 | 141 | 97 | 0x61 | a | 小寫字母a |
0110 0010 | 142 | 98 | 0x62 | b | 小寫字母b |
0110 0011 | 143 | 99 | 0x63 | c | 小寫字母c |
0110 0100 | 144 | 100 | 0x64 | d | 小寫字母d |
0110 0101 | 145 | 101 | 0x65 | e | 小寫字母e |
0110 0110 | 146 | 102 | 0x66 | f | 小寫字母f |
0110 0111 | 147 | 103 | 0x67 | g | 小寫字母g |
0110 1000 | 150 | 104 | 0x68 | h | 小寫字母h |
0110 1001 | 151 | 105 | 0x69 | i | 小寫字母i |
0110 1010 | 152 | 106 | 0x6A | j | 小寫字母j |
0110 1011 | 153 | 107 | 0x6B | k | 小寫字母k |
0110 1100 | 154 | 108 | 0x6C | l | 小寫字母l |
0110 1101 | 155 | 109 | 0x6D | m | 小寫字母m |
0110 1110 | 156 | 110 | 0x6E | n | 小寫字母n |
0110 1111 | 157 | 111 | 0x6F | o | 小寫字母o |
0111 0000 | 160 | 112 | 0x70 | p | 小寫字母p |
0111 0001 | 161 | 113 | 0x71 | q | 小寫字母q |
0111 0010 | 162 | 114 | 0x72 | r | 小寫字母r |
0111 0011 | 163 | 115 | 0x73 | s | 小寫字母s |
0111 0100 | 164 | 116 | 0x74 | t | 小寫字母t |
0111 0101 | 165 | 117 | 0x75 | u | 小寫字母u |
0111 0110 | 166 | 118 | 0x76 | v | 小寫字母v |
0111 0111 | 167 | 119 | 0x77 | w | 小寫字母w |
0111 1000 | 170 | 120 | 0x78 | x | 小寫字母x |
0111 1001 | 171 | 121 | 0x79 | y | 小寫字母y |
0111 1010 | 172 | 122 | 0x7A | z | 小寫字母z |
0111 1011 | 173 | 123 | 0x7B | { | 開花括號 |
0111 1100 | 174 | 124 | 0x7C | | | 垂線 |
0111 1101 | 175 | 125 | 0x7D | } | 閉花括號 |
0111 1110 | 176 | 126 | 0x7E | ~ | 波浪號 |
0111 1111 | 177 | 127 | 0x7F | DEL (delete) | 刪除 |
所以,char 類型的數據對應的整型數據的取值范圍應該在 [0, 127] 之間。使用循環(huán)可以將其全部輸出并查看:
#include<stdio.h>int main() {for(char c = 0; c < 128; c++) {printf("c=%d, c=%c\n", c, c);if(c < 0) {break;}}return 0;
}
因為 char 的最大值是 127, 如果當變量 c 是 127 的時候,再執(zhí)行自增操作,則 c 的最高位會被進位為 1,就被解釋為負數了,故而,需要在循環(huán)體中做判斷 c < 0
就跳出循環(huán)的操作,否則該循環(huán)將成為無限循環(huán)。
C 的字符串
很多資料在描述 C 的字符串的時候,說在 C 中,字符數組就是字符串,其實不完全正確。根據定義,字符串實際上是使用 null 字符 ‘\0’ 終止的一維字符數組。換句話說,如果一個字符數組中的最后一個元素不是 NULL 字符,那這個字符數組不能稱之為字符串。示例如下:
#include<stdio.h>int main() {char a[] = {'H', 'e', 'l', 'l', 'o'};char b[] = {'H', 'e', 'l', 'l', 'o', '\0'};char c[] = "Hello";printf("sizeof(a) is %d, &a=%p, a=%s\n", sizeof(a), &a, a);printf("sizeof(b) is %d, &b=%p, b=%s\n", sizeof(b), &b, b);printf("sizeof(c) is %d, &c=%p, c=%s\n", sizeof(c), &c, c);return 0;
}
執(zhí)行結果:
sizeof(a) is 5, &a=0022FF3B, a=Hello
sizeof(b) is 6, &b=0022FF35, b=Hello
sizeof(c) is 6, &c=0022FF2F, c=Hello
這里的變量 a, b, c 都是字符數組,但變量 a 是不能被稱之為是 C 的字符串的,因為根據定義,它的最后一個元素的不是 NULL 字符。
那為什么變量 c 能被稱之為是字符串呢?原因是在編寫源程序的時候,不需要把 null 字符放在字符串常量的末尾。編譯器會在初始化數組時,自動把 '\0'
放在字符串的末尾。也就是說,C 在編譯的時候,將字符串字面量 "Hello"
分解成了一個以 NULL 字符結尾的字符數組。這也就是為什么使用 sizeof 運算變量 c 的時候,得到的值是 6 的原因,因為在字符 o
的后面還有一個 NULL 字符??梢院唵蔚睦斫鉃?"Hello"
等價于 {'H', 'e', 'l', 'l', 'o', '\0'}
。
在之前以 指針 為主題的文章中說:在 C 中,數組變量就是一個特殊常量指針,也稱之為數組指針。那也就是說在上面的例子中,變量 a, b, c 也都是指針。換句話說,字符串變量也是一個指針變量。
同樣的,把之前數組指針的案例修改下,更改數據類型為 char,然后增加輸出格式 %c
。得到如下代碼:
#include<stdio.h>#define SIZE 6int main() {char cs[SIZE] = {'H', 'e', 'l', 'l', 'o', '\0'};char *p = cs;// cs 和 p 都能使用運算符 *printf("*p=%d, *cs=%d, *cs=%c\n", *p, *cs, *cs);printf(" p=%p, cs=%p\n", p, cs);printf("&p=%p, &cs=%p\n", &p, &cs);// cs 和 p 都能使用邏輯運算符printf("p <= &cs[1] is %s\n", p <= &cs[1] ? "true" : "false");printf("cs < &cs[1] is %s\n", cs < &cs[1] ? "true" : "false");// cs 和 p 都能使用下標來操作元素for(int i = 0; i < SIZE; i++) {printf("cs[%d]=%d, cs[%d]=%c, &cs[%d]=%p, p[%d]=%d, &p[%d]=%p\n", i, cs[i], i, cs[i], i, &cs[i], i, p[i], i, &p[i]);}// cs 是常量指針,故而不能做自運算while(p < &cs[SIZE - 1]) {p++;// error: lvalue required as increment operand// cs++;printf("p=%p, *p=%d, cs=%p, *cs=%d, *cs=%c\n", p, *p, cs, *cs, *cs);}return 0;
}
執(zhí)行結果:
*p=72, *cs=72, *cs=Hp=0022FF26, cs=0022FF26
&p=0022FF20, &cs=0022FF26
p <= &cs[1] is true
cs < &cs[1] is true
cs[0]=72, cs[0]=H, &cs[0]=0022FF26, p[0]=72, &p[0]=0022FF26
cs[1]=101, cs[1]=e, &cs[1]=0022FF27, p[1]=101, &p[1]=0022FF27
cs[2]=108, cs[2]=l, &cs[2]=0022FF28, p[2]=108, &p[2]=0022FF28
cs[3]=108, cs[3]=l, &cs[3]=0022FF29, p[3]=108, &p[3]=0022FF29
cs[4]=111, cs[4]=o, &cs[4]=0022FF2A, p[4]=111, &p[4]=0022FF2A
cs[5]=0, cs[5]= , &cs[5]=0022FF2B, p[5]=0, &p[5]=0022FF2B
p=0022FF27, *p=101, cs=0022FF26, *cs=72, *cs=H
p=0022FF28, *p=108, cs=0022FF26, *cs=72, *cs=H
p=0022FF29, *p=108, cs=0022FF26, *cs=72, *cs=H
p=0022FF2A, *p=111, cs=0022FF26, *cs=72, *cs=H
p=0022FF2B, *p=0, cs=0022FF26, *cs=72, *cs=H
從這個簡單的案例中證明了,字符串變量也是一個指針變量。原因是因為數組是一個常量指針。那也就是說可以使用聲明指針的形式來聲明一個字符串。示例如下:
#include<stdio.h>int main() {char *a = "Hello";printf("sizeof(a) is %d, &a=%p, a=%s\n", sizeof(a), &a, a);return 0;
}
執(zhí)行結果:
sizeof(a) is 4, &a=0022FF3C, a=Hello
那如果聲明兩個相同的字符串指針變量,他們的值相同嗎?代碼如下:
#include<stdio.h>
#include<string.h>int main() {char *a = "Hello";char *b = "Hello";if(a == b) {printf("a == b.\n");} else {printf("a != b.\n");}printf("a=%s, a=%p, &a[0]=%p, *a=%c, &a=%p\n", a, a, &a[0], *a, &a);printf("b=%s, b=%p, &b[0]=%p, *b=%c, &b=%p\n", b, b, &b[0], *b, &b);return 0;
}
執(zhí)行結果:
a == b.
a=Hello, a=00405044, &a[0]=00405044, *a=H, &a=0022FF3C
b=Hello, b=00405044, &b[0]=00405044, *b=H, &b=0022FF38
從這個例子中可以知道的是,兩個字符指針指向的地址是同一個地址,也就是說源程序中的兩個字面量 Hello
在內存中使用同一個字符數組來進行存儲的。換句話講,這個例子中的變量 a 和變量 b 不是兩個長得一樣,而是它們就是同一個字符串。
綜上可知,字符串的特點有:
- 字符串變量是使用 NULL 字符作為最后一個元素的一維字符數組
- 字符串變量也是一個指針變量
- 字符串指針指向的是字符數組的第一個元素的地址
- 兩個相同的字符串字面量在內存中使用同一個字符數組來進行存儲
字符串輸入與輸出
如果想把一個字符串讀取到程序中,必須首先預留存儲字符串的空間,然后使用輸入函數來獲取這個字符串,C 在
stdio.h
中提供了三個讀取字符串的函數:scanf
、gets
和fgets
。
scanf 函數
scanf
函數可以讀取輸入的信息到指定的地址值中,配合變量的取值符可以更有效的讀入數據:
int age = 1;
scanf("%d", &age);
printf("age = %d\n", age);
如果是指針變量,可以很容易的將地址傳遞給 scanf
函數,如下:
int *p;
scanf("%d", p);
printf("*p = %d\n", *p);
但是讀入數據到指針指定位置的時候,會覆蓋所指向的數據,并可能導致程序異常終止。
char *name = "hello";
scanf("%s", name);
printf("name = %s\n", name);
這個是 因為 scanf
把信息復制到由name指定的地址中,而在這種情況下,參數是個未被初始化的指針,name可能指向任何地方。
gets 函數
gets
函數對于交互式程序非常方便,它從系統(tǒng)的標準輸入設備(通常是鍵盤)獲得一個字符串。
因為字符串沒有預定的長度,所以 gets
函數通過判斷遇到的第一個換行符 \n
結束輸入,按回車鍵可以產生這個字符。它讀取換行符之前(不包括換行符)的所有字符,并在這些字符后添加一個空字符(\0
)。
char n[12];
char *p;
p = gets(n);
printf("n=%s\n", n);
printf("p=%p\n", p);
printf("n=%p\n", n);
如果在 gets
函數在讀取字符串時出錯或者遇到文件結尾,它就返回一個空(或0)地址,這個空地址被稱為空指針,并且 stdio.h
里面定義的常量 NULL
來表示,可以用下面的代碼來進行一些錯誤檢測。
char name[1024];
while(get(name) != NULL) {// ... do something here ....
}
也可以通過 getchar
函數來完成上面的錯誤檢測。
char ch;
while((ch = getchar()) != EOF) {// ... do something ...
}
fgets 函數
fgets
函數也可以讀入字符數據到變量中,它和 gets
函數的區(qū)別主要有:
fgets
需要第二個參數來說明最大讀入字符數。如果這個參數值為n
,fgets
就會讀取最多n-1
個字符或者讀完一個換行符為止(因為會自動添加一個空字符(\n)),由這兩者中最先滿足的那個結束輸入。- 如果
fgets
讀取到換行符,就會把它存到字符串里,而不是像gets
那樣丟棄。 fgets
還需要第三個參數來說明讀哪一個文件,從鍵盤上讀取數據時,可以使用stdin(代表standard input)作為參數,這個標識符在stdio.h
中被定義。
#include<stdio.h>
#define MAX 1024int main() {char n[MAX];char *p;printf("input:");p = gets(n);printf("n=%s\n", n);printf("p=%p\n", p);printf("n=%p\n", n);printf("input:");p = fgets(n, MAX, stdin); printf("n=%s\n", n);printf("p=%p\n", p);printf("n=%p\n", n);return 0;
}
執(zhí)行結果:
input:hello
n=hello
p=0061FB1C
n=0061FB1C
input:hello
n=hellop=0061FB1C
n=0061FB1C
明顯看到第二次輸入的 hello 后面會帶有一個回車符號。
scanf 和 gets 的區(qū)別
scanf
函數和 gets
函數的主要區(qū)別在于如何決定字符串何時結束。scanf
函數可以使用 %s
來讀入一個單詞,而 gets
則是讀入一個長字符串,以回車鍵結束。如下所示代碼:
char n[1024];
char *p;printf("input:");
p = gets(n);
printf("n=%s\n", n);printf("input:");
scanf("%s", n);
printf("n=%s\n", n);
如果輸入均為 hello world,則輸出結果如下:
input:hello world
n=hello world
input:hello world
n=hello
這兩種方法都是以遇到的第一個非空白字符開始的,針對 scanf
函數的結束,可以歸納為:
- 如果使用
%s
格式,字符串讀取到(但不包括)下一個空白字符(比如空格、制表符或換行符)結束 - 如果指定了字段寬度,比如
%10s
,scanf
函數就會讀取10個字符或者直到遇到第一個空白字符,由二者最先滿足的那一個終止輸入
printf 函數
printf
函數接受一個格式控制字符串,格式控制字符串是用雙引號括起來的字符串,包括三類信息:
格式字符:格式字符由 %
引導,如 %d
、%f
等。它的作用是控制輸出字符的格式。
轉義字符:格式控制字符串里的轉義字符按照轉義后的含義輸出,如換行符\n
,即輸出回車。
普通字符:普通字符即需要在輸出時原樣輸出的字符,如漢字或者其他普通單詞。
格式字符 | 說明 |
---|---|
d | 輸出帶符號的十進制整數,正數的符號省略 |
u | 以無符號的十進制整數形式輸出 |
o | 以無符號的八進制整數形式輸出,不輸出前導符0 |
x | 以無符號十六進制整數形式(小寫)輸出,不輸出前導符0x |
X | 以無符號十六進制整數形式(大寫)輸出,不輸出前導符0X |
f | 以小數形式輸出單、雙精度數,隱含輸出6位小數 |
e | 以指數形式(小寫e表示指數部分)輸出實數 |
E | 以指數形式(大寫E表示指數部分)輸出實數 |
g | 自動選取f或e中輸出寬度較小的一種使用,且不輸出無意義的0 |
c | 輸出一個字符 |
s | 輸出字符串 |
puts 函數
puts
函數接受一個字符串參數的地址,它遇到空字符(\0
)就會結束輸出(所以必須要有空字符)。puts
函數在顯示字符串的時候,會自動在其后添加一個換行符(\n
)。
#include<stdio.h>
#define STR "learn C++ together!"int main() {char cs[] = "defined by array!";char *cp = "defined by pointer!";puts("puts string!");puts(cs);puts(cp);puts(STR);puts(&cs[3]);puts(cs+4);puts(&cp[3]);puts(cp+4);return 0;
}
fputs 函數
fputs
函數 puts
函數面向文件版本,兩者主要的區(qū)別是:
fputs
函數需要第二個參數來說明要寫的文件,可以使用stdout
(standard output) 作為參數來進行輸出顯示。- 與
puts
函數不同,fputs
函數并不為輸出自動添加換行符。
讀取一行并把它回顯在下一行,用下面的兩種循環(huán)都可以辦到
// get & puts
{char line[81];while(gets(line))puts(line);
}
// fgets & fputs
{char line[81];while(fgets(line,81,stdin))fputs(line,stdout);
}
字符串函數
在 C 的標準庫中,提供了關于字符串的函數,在使用之前需要引入頭文件 string.h
。關于這個頭文件中包含的函數說明請參考:http://www.cplusplus.com/reference/cstring/
strcpy
函數簽名: char * strcpy ( char * destination, const char * source );
函數描述:將 source 所指的 C 字符串復制到 destination 所指的數組中,包括終止的空字符(并在該點停止)。
名稱來源: Copy string
使用示例:
#include<stdio.h>
#include<string.h>int main() {char a[] = "Hello";char b[] = "World";// strcpy(a, b) 等價于 a = bstrcpy(a, b);printf("a=%s\n", a);printf("b=%s\n", b);return 0;
}
執(zhí)行結果:
a=World
b=World
因為數組是一個常量指針,所以它不能像普通變量那樣直接將變量 b 的值賦予給變量 a。這里使用到 C 標準庫中提供的 strcpy 函數將 b 的值賦予給 a。當然,如果使用指針變量聲明的字符串完成就可以不需要 strcpy 函數了。示例如下:
#include<stdio.h>
#include<string.h>int main() {char *a = "Hello";char *b = "World";a = b;printf("a=%s\n", a);printf("b=%s\n", b);return 0;
}
執(zhí)行結果:
a=World
b=World
結果雖然一樣,但是使用指針的做法實際上就是將 b 的指針指向的地址值賦予了 a,使得 a 的指向發(fā)生變化而已。
strcat
函數簽名: char * strcat ( char * destination, const char * source );
函數描述: 將 source 字符串的副本追加到 destination 字符串。destination 中的終止空字符被 source 的第一個字符覆蓋,并且在由 destination 中的兩個字符串聯而成的新字符串的末尾包含一個空字符。
名稱來源: Concatenate strings
使用示例:
#include<stdio.h>
#include<string.h>int main() {char a[] = "Hello";char b[] = "World";strcat(strcat(a, " "), b);printf("a=%s\n", a);printf("b=%s\n", b);return 0;
}
執(zhí)行結果:
a=Hello World
b=World
strlen
函數簽名: size_t strlen ( const char * str );
函數描述: 返回 C 字符串 str 的長度。
名稱來源: Get string length
使用示例:
#include<stdio.h>
#include<string.h>int main() {char a[] = "Hello";char b[] = "World";printf("a=%s, a's length is %d\n", a, strlen(a));printf("b=%s, b's length is %d\n", b, strlen(b));return 0;
}
執(zhí)行結果:
a=Hello, a's length is 5
b=World, b's length is 5
strcmp
函數簽名: int strcmp ( const char * str1, const char * str2 );
函數描述: 如果每個字符都一致,則返回零,否則比較第一個不同的字符的 ASCII 值。
名稱來源: Compare two strings
使用示例:
#include<stdio.h>
#include<string.h>int main() {char a[] = "Hello";char b[] = "World";int c = strcmp(a, b);printf("strcmp(\"%s\", \"%s\") is %d\n", a, b, c);return 0;
}
執(zhí)行結果:
strcmp("Hello", "World") is -1
函數 strcmp 比較 Hello
和 World
的結果是 -1,也就是 a < b
。那是因為 World
的第一個字符的 ASCII 值比 Hello
的大。如果將 World
更改為 Happy
,那就是比較第二個字符的 ASCII 值,結果就是 a > b
了。關于它的更多介紹,可以參考 http://www.cplusplus.com/reference/cstring/strcmp/
strchr
函數簽名: char * strchr ( const char * str, int character);
函數描述: 返回指向 C 字符串 str 中 character 的第一個匹配項的指針。
名稱來源: Locate first occurrence of character in string
使用示例:
#include<stdio.h>
#include<string.h>int main() {char a[] = "Hello World!";char *c = strchr(a, 'l');printf("c=%p, c=%c, c=%d, c=%s", c, *c, *c, c);return 0;
}
執(zhí)行結果:
c=0022FF31, c=l, c=108, c=llo World!
返回的指針 c 指向的是原字符串的一個第一個 l 的字符,可以使用指針運算,統(tǒng)計出該字符串中某個字符的總數。如下例所示:
#include<stdio.h>
#include<string.h>int main() {char a[] = "Hello World!";char *c = a, ch = 'l';int count = 0;while(c) {c = strchr(c, ch);if(!c) {// 如果找不到對應的字符,則返回空指針break;}// printf("c=%p, c=%c, c=%d, c=%s\n", c, *c, *c, c);c++;count++;}printf("count of '%c' is %d.\n", ch, count);return 0;
}
執(zhí)行結果:
count of 'l' is 3.
strstr
函數簽名: char * strstr ( char * str1, const char * str2 );
函數描述: 返回指向 str1 中第一個 str2 的指針,如果 str2 不是 str1 的一部分,則返回空指針。
使用示例:
#include<stdio.h>
#include<string.h>int main() {char a[] = "The matching process does not include the terminating null-characters, but it stops there.";char *c = strstr(a, "null");if(c) {// c is not null pointerprintf("c=%p, c=%c, c=%d, c=%s\n", c, *c, *c, c);}return 0;
}
執(zhí)行結果:
c=0022FF07, c=n, c=110, c=null-characters, but it stops there.
C++ 的字符串
在 C++ 中,字符串的聲明和使用都比 C 中來得簡單。示例代碼如下:
#include<iostream>
#include<string>
using namespace std;int main() {string str = "Hello World";cout << "str's length is " << str.size() << endl;str += "!";cout << "str's length is " << str.size() << endl;cout << str << endl;// 在字符串從左開始查找第一次出現指定字符串的位置,并返回下標int index = str.find("l");cout << "index is " << index << endl;// 在字符串從右開始查找第一次出現指定字符串的位置,并返回下標index = str.rfind(" ");cout << "index is " << index << endl;// 從指定下標開始截取字符串,截取指定的長度string substring = str.substr(index + 1, 2);cout << "substring is " << substring << endl;// 從指定下標開始截取字符串,截取到末尾substring = str.substr(index + 1);cout << "substring is " << substring << endl;substring = str.replace(index + 1, 5, "C++");cout << "substring is " << substring << endl;return 0;
}
執(zhí)行結果:
str's length is 11
str's length is 12
Hello World!
index is 2
index is 5
substring is Wo
substring is World!
substring is Hello C++!
枚舉類型
枚舉是 C 語言中的一種基本數據類型,它可以讓數據更簡潔,更易讀。
使用 enum 關鍵字定義一系列枚舉值,表示星期,例如:
enum WEEK { MON, TUE, WED, THU, FRI, SAT, SUN };
接下來就能在程序中使用這些枚舉值表示星期了
#include<stdio.h>enum WEEK { MON, TUE, WED, THU, FRI, SAT, SUN };int main() {enum WEEK w = SAT;switch(w) {case MON:case TUE:case WED:case THU:case FRI:printf("today is week. ");break;case SAT:case SUN:printf("today is weekend. ");break;} return 0;
}
執(zhí)行結果:
today is weekend.
非常簡單,如果嘗試將其以數字的形式輸出呢?代碼如下:
#include<stdio.h>enum WEEK { MON, TUE, WED, THU, FRI, SAT, SUN };int main() {enum WEEK w = SAT;printf("today is %d. ", w); return 0;
}
執(zhí)行結果:
today is 5.
會發(fā)現,輸出的 5 正好與 SAT 在聲明枚舉的語句中所在的下標是一致的,于是,全部將其輸出,查看結果:
printf("MON is %d.\n", MON);
printf("TUE is %d.\n", TUE);
printf("WED is %d.\n", WED);
printf("THU is %d.\n", THU);
printf("FRI is %d.\n", FRI);
printf("SAT is %d.\n", SAT);
printf("SUN is %d.\n", SUN);
其結果是:
MON is 0.
TUE is 1.
WED is 2.
THU is 3.
FRI is 4.
SAT is 5.
SUN is 6.
因為枚舉值實際上就是一系列連續(xù)的數字,默認從零開始。當然,可以在定義的時候手動更改他們的值。例如:
enum WEEK { MON, TUE, WED=8, THU, FRI, SAT, SUN };
輸出對應的數字值分別是:
MON is 0.
TUE is 1.
WED is 8.
THU is 9.
FRI is 10.
SAT is 11.
SUN is 12.
當然,故意將其設置為相同的值是被允許的,但不建議這樣做:
反例1:
enum WEEK { MON, TUE, WED=8, THU=8, FRI, SAT, SUN };
反例2:
enum WEEK { MON=5, TUE, WED, THU=3, FRI, SAT, SUN };
這樣寫都會導致枚舉值出現重復的數值,也不利于程序的維護。
結構體
在C語言中,結構體(struct)指的是一種數據結構,是C語言中聚合數據類型(aggregate data type)的一類。
結構體可以被聲明為變量、指針或數組等,用以實現較復雜的數據結構。
結構體同時也是一些元素的集合,這些元素稱為結構體的成員(member),且這些成員可以為不同的類型,成員一般用名字訪問。
—— 摘自百度百科
聲明與定義
在 C 語言提供的基本數據類型中,只能表示單一的類型的數據類型,如果需要將多個數據類型組合成為一個整體的作為新的類型的話,就需要使用到結構體。
在表達式 int a = 10;
中,變量 a 被定義為了 int 類型,無論如何,變量 a 都表示的是一個數。
而實際運用中,需要依賴變量某個變量來表示更多的內容。
例如,需要表示某個人的信息,包括姓名,年齡,體重等。那每個信息可以使用一個基本數據類型來表示。如下代碼所示:
#include<stdio.h>int main() {char name[50] = "tom";unsigned int age = 27;double weight = 57.9;printf("name is %s, age is %d, weight is %f\n", name, age, weight);return 0;
}
執(zhí)行結果:
name is tom, age is 27, weight is 57.900000
但是作為某個人的信息,它們應該是一個整體。這時候可以使用結構體定義一個新的類型,這個類型將包含需要表示的某個人的信息的基本類型。如下代碼所示:
/*** 人*/
struct Person {/*** 姓名*/char name[50];/*** 年齡*/unsigned int age;/*** 體重*/double weight;
};
這里定義了一個 Person 類型,該類型包含了 C 提供的三個基本類型。而作為整體,該類型將表示某個人的數據。
結構體的定義語法:
struct 結構體名稱 {member-list
}
定義好的結構體與基本數據類型的使用方式是一樣的。如下所示:
char name[50];
unsigned int age;
double weight;
struct Person man;
或者可以在聲明的時候為其賦予初始值:
char name[50] = "Tom";
unsigned int age = 27;
double weight = 57.9;
struct Person man = {"Mark", 28, 60.2};
也可以在聲明之后,使用運算符 .
來為結構體變量賦值,完整代碼如下所示:
#include<stdio.h>
#include<string.h>/*** 人*/
struct Person {/*** 姓名*/char name[50];/*** 年齡*/unsigned int age;/*** 體重*/double weight;
};int main() {// 聲明普通的變量char name[50];unsigned int age;double weight;// 為普通的變量賦值strcpy(name, "Tom");age = 27;weight = 57.9;// 輸出變量的值printf("name is %s, age is %d, weight is %f\n", name, age, weight);// 聲明結構體變量struct Person man;// 為結構體變量的內部成員賦值strcpy(man.name, "Mark");man.age = 28;man.weight = 60.2;// 輸出結構體變量的內部成員的值printf("name is %s, age is %d, weight is %f\n", man.name, man.age, man.weight);return 0;
}
執(zhí)行結果:
name is Tom, age is 27, weight is 57.900000
name is Mark, age is 28, weight is 60.200000
可以將結構體看作是將多個基礎數據類型的變量進行打包的容器,使得這些只能表示單一信息的單元組合成一個整體,無論是取值還是儲值都需要通過變量 man
使用運算符 .
來操作。在閱讀代碼的時候,可以將運算符 .
理解為漢字 的,比如 man.age = 28
可以理解為 man 的 age 是 28
<==> man's age is 28
。也加強了代碼的可讀性。
當然,結構體的內部成員除了基礎數據類型之外,還可以是指針變量,枚舉變量和其他結構體,例如:
/*** 性別*/
enum Gender {Girl, Boy};/*** 成績*/
struct Score {/*** 語文成績*/int Chinese;/*** 數學成績*/int Math;/*** 英語成績*/int English;
};
/*** 人*/
struct Person {/*** 姓名*/char name[50];/*** 年齡*/unsigned int age;/*** 性別*/enum Gender gender;/*** 體重*/double weight;/*** 成績*/struct Score score;
};
結構體作為參數
因為結構體類型中包含多個數據類型的成員,以結構體作為函數參數能減少函數的參數列表的長度,使得代碼更加簡潔。示例如下:
#include<stdio.h>enum Gender {Girl=0, Boy=1};/*** 人*/
struct Person {/*** 姓名*/char name[50];/*** 年齡*/unsigned int age;/*** 性別*/enum Gender gender;/*** 體重*/double weight;
};/*** 打印 Person 結構體的成員變量*/
void print(struct Person person) {char *gender = person.gender ? "Boy" : "Girl";printf("Person{name:%s, age:%d, gender:%s, weight:%f}\n", person.name, person.age, gender, person.weight);}int main() {struct Person man = {"Mary", 16, Girl, 48.2};print(man);return 0;
}
執(zhí)行結果:
Person{name:Mary, age:16, gender:Boy, weight:48.200000}
如果結構體中的成員變量較多,使用這種方式傳遞數據相比較把所有的成員變量列舉到形參列表中而言顯得更加方便簡潔。
指向結構體的指針
同樣的,聲明一個指向結構體的指針,也能作為函數參數進行數據的傳遞。但是作為指針變量,需要訪問結構體中的成員變量時,需要使用 ->
運算符。如下所示:
#include<stdio.h>enum Gender {Girl=0, Boy=1};/*** 人*/
struct Person {/*** 姓名*/char name[50];/*** 年齡*/unsigned int age;/*** 性別*/enum Gender gender;/*** 體重*/double weight;
};/*** 打印 Person 結構體的成員變量*/
void print(struct Person *p) {char *gender = p->gender ? "Boy" : "Girl";printf("Person{name:%s, age:%d, gender:%s, weight:%f}\n", p->name, p->age, gender, p->weight);
}int main() {struct Person man = {"Mary", 16, Girl, 48.2};struct Person *p = &man;printf("this person's name is %s\n", p->name);print(p);return 0;
}
執(zhí)行結果:
this person's name is Mary
Person{name:Mary, age:16, gender:Girl, weight:48.200000}
結構體占用的內存空間大小
使用 sizeof
能夠量出某個變量或者類型的占用空間的字節(jié)數,那結構體占用多大的內存空間呢?首先來看一段代碼:
#include<stdio.h>int main() {printf("sizeof(char) is %d\n", sizeof(char));printf("sizeof(int) is %d\n", sizeof(int));printf("sizeof(double) is %d\n", sizeof(double));printf("========================\n");struct A{int a; int b;};printf("sizeof(A) is %d\n", sizeof(struct A));struct B{int a; double b;};printf("sizeof(B) is %d\n", sizeof(struct B));struct C{double a; double b;};printf("sizeof(C) is %d\n", sizeof(struct C));struct D{double a; double b; int c;};printf("sizeof(D) is %d\n", sizeof(struct D));struct E{double a; double b; int c; int d;};printf("sizeof(E) is %d\n", sizeof(struct E));struct F{double a; double b; int c; int d; char e;};printf("sizeof(F) is %d\n", sizeof(struct F));return 0;
}
執(zhí)行結果:
sizeof(char) is 1
sizeof(int) is 4
sizeof(double) is 8
========================
sizeof(A) is 8
sizeof(B) is 16
sizeof(C) is 16
sizeof(D) is 24
sizeof(E) is 24
sizeof(F) is 32
結構體 A, C, E 的大小正好是所有的內部成員的 size 之和。而其他的結構體的 size 都比內部成員的 size 的和還要大些。由此可知:結構體占用的內存空間大小不是簡單的幾個類型的 size 的和。
從結果上看,結構體的 size 貌似是以 8 為單位擴張的。例如當成員中已經有兩個 double 成員的時候,再增加一個 int 成員,就將自身擴張 8 個字節(jié),于是 size 為 24。
實際上,結構體的 size 在擴張的時候是取決于 C 中的 #pragma pack(n)
指令。缺省情況下,n 的值為8。這也就是該例中擴張 8 個字節(jié)的原因。因為 n 的合法的數值分別是1、2、4、8、16。若將該例中增加該指令并設置 n 的值為 1,結構體就會以 1 位單位進行擴張。執(zhí)行結果如下:
sizeof(char) is 1
sizeof(int) is 4
sizeof(double) is 8
========================
sizeof(A) is 8
sizeof(B) is 12
sizeof(C) is 16
sizeof(D) is 20
sizeof(E) is 24
sizeof(F) is 25
這樣就能看到結構體的 size 就正好是各個成員的 size 之和了。
編譯器中提供了#pragma pack(n)來設定變量以n字節(jié)對齊方式。n字節(jié)對齊就是說變量存放的起始地址的偏移量有兩種情況:
- 如果n大于等于該變量所占用的字節(jié)數,那么偏移量必須滿足默認的對齊方式
- 如果n小于該變量的類型所占用的字節(jié)數,那么偏移量為n的倍數,不用滿足默認的對齊方式。
結構的總大小也有個約束條件,分下面兩種情況:如果n大于所有成員變量類型所占用的字節(jié)數,那么結構的總大小必須為占用空間最大的變量占用的空間數的倍數;否則必須為n的倍數。
—— 摘自百度百科
位域
除了使用指令能約束結構體的 size 之外,使用位域也能約束某個成員變量占用的內存大小,進而調整 結構體的 size。在結構內聲明位域的形式如下:
struct 結構體標識符
{type [member_name] : width ;
};
在使用位域的時候,需要注意以下幾個問題:
- 成員類型只能為 int(整型),unsigned int(無符號整型),signed int(有符號整型) 三種類型,決定了如何解釋位域的值。
- 位域中位的數量。寬度必須小于或等于指定類型的位寬度。
示例代碼如下:
#include<stdio.h>int main() {struct A { int a:1; int b:1; }; printf("sizeof(A) is %d\n", sizeof(struct A));return 0;
}
執(zhí)行結果:
sizeof(A) is 4
因為結構體 A 內部成員使用的 int 型,其 size 為 4,也就是 32 個 bit 位。所以位域的寬度不能超過 int 的寬度,也就是不能超過 32。在本例中,兩個 int 類型的位域寬度值都是 1 ,在實際存儲中,它只占用 1 個 bit 位,所以結構體的 size 就是一個 int 類型的 size。換句話將,結構體中所有 int 類型的成員的位域值之和不超過 32 的話,結構體 A 的 size 就為 4。如下例所示:
#include<stdio.h>int main() {struct A {int a0:1; int a1:1; int a2:1; int a3:1; int a4:1;int a5:1; int a6:1; int a7:1; int a8:1; int a9:1;int b0:1; int b1:1; int b2:1; int b3:1; int b4:1;int b5:1; int b6:1; int b7:1; int b8:1; int b9:1;int c0:1; int c1:1; int c2:1; int c3:1; int c4:1;int c5:1; int c6:1; int c7:1; int c8:1; int c9:1;int d0:1; int d1:1;};printf("sizeof(A) is %d\n", sizeof(struct A));return 0;
}
執(zhí)行結果:
sizeof(A) is 4
該例中結構體 A 的成員變量有 32 個,但是結構體 A 的 size 仍然是 4,因為沒有超出一個 int 的位寬。如果在增加一個相同的成員,則超出了一個 int 的位寬,其 size 就會擴張為 8。
結構體小案例
結構體能夠表示很多場景的實體,比如訂單,商品等。在程序中,如果需要使用某個數據類型來表示一個訂單信息,或者一個商品信息。使用結構體是一個不錯的選擇。
下面是一個模擬了一個簡單的訂單實體的數據小案例。本例中,聲明了三個結構體,一個枚舉類型;運用隨機數并結合之前的文章的內容。具體代碼如下所示:
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<string.h>// 定義存貨量的上限值
#define INVENTORY_QUANTITY 9/*** 支付方式枚舉*/
enum PayWay {Alipay, WeChatPay, ApplePay, UnionPay, CreditCard};/*** 商品詳情*/
struct Goods {/*** 商品名稱*/char name[50];/*** 商品價格*/double price;/*** 商品折扣*/unsigned short int discount;
};/*** 訂單詳情*/
struct OrderDetail {/*** 該商品購買的數量*/int quantity;/*** 小計*/double subtotalAmount;/*** 該商品的詳細信息*/struct Goods goods;
};/*** 訂單*/
struct Order {/*** 總計*/double totalAmount;/*** 訂單支付方式*/enum PayWay payway;/*** 訂單詳情*/struct OrderDetail details[100];
};int main() {// 定義多個商品集合用于后面模擬存貨struct Goods stocks[INVENTORY_QUANTITY] = {{"鍵盤", 199.0, 9},{"鼠標", 129.5, 8},{"電源", 109.0, 10},{"音響", 699.0, 9},{"耳機", 169.0, 10},{"插板", 19.0, 10},{"電腦", 7899.0, 10},{"手機", 4999.0, 10},{"平板", 299.0, 10}}; // 使用隨機數模擬購物需要的數據int count = 0;int indexs[INVENTORY_QUANTITY];struct Order order = {0.0, Alipay};{// 初始化已購清單的索引for(int k = 0; k < INVENTORY_QUANTITY; k++) {indexs[k] = -1;}// 使用當前時間作來初始化隨機函數種子srand((unsigned)time(NULL));// 隨機一個數字,這個數字不能超過 stocks 的長度count = rand() % INVENTORY_QUANTITY;for(int i = 0; i < count; i++) {// 隨機一個商品int index = -1, exist = 0;while (index < 0) {index = rand() % INVENTORY_QUANTITY;for(int j = 0; j < INVENTORY_QUANTITY; j++) {if(indexs[j] == index) {exist = 1;break;}}if(exist == 1) {// 已經在買過該商品, 重新選擇新的下標exist = 0;index = -1;}}for(int k = 0; k < INVENTORY_QUANTITY; k++) {if(indexs[k] < 0) {indexs[k] = index;break;}}struct Goods goods = stocks[index];// 隨機一個數量,表示購買該商品的數量int quantity = rand() % 50;// 計算小計double subtotalAmount = quantity * goods.price * goods.discount / 10.0;order.totalAmount += subtotalAmount;// 生成一個訂單詳情信息struct OrderDetail detail = {quantity, subtotalAmount, goods};order.details[i] = detail;}}// 打印訂單信息if(count > 0) {printf("\n==========================================================\n");{char way[11] = "";switch(order.payway) {case Alipay: strcpy(way, "支付寶"); break;case WeChatPay: strcpy(way, "微信支付"); break;case ApplePay: strcpy(way, "蘋果支付"); break;case UnionPay: strcpy(way, "銀聯支付"); break;case CreditCard: strcpy(way, "信用卡支付"); break;}printf("訂單總額:%f, 支付方式:%s", order.totalAmount, way);}printf("\n----------------------------------------------------------\n");printf("序號 \t 商品 \t 數量 \t 單價 \t\t 小計.\n");for(int i = 0; i < count; i++) {struct OrderDetail detail = order.details[i];char *name = detail.goods.name;int quantity = detail.quantity;double price = detail.goods.price;int discount = detail.goods.discount;double subtotalAmount = detail.subtotalAmount;printf(" %d. \t %s \t %d \t %.2f \t %.2f\n", (i + 1), name, quantity, price, subtotalAmount);if(discount > 0 && discount < 10) {double subPrice = price * discount / 10.0;printf(" \t \t \t 折后價 -> %.2f\n", subPrice);}}printf("\n==========================================================\n");} else {printf("沒有查詢到有效的訂單信息");}return 0;
}
執(zhí)行結果:
==========================================================
訂單總額:53539.800000, 支付方式:支付寶
----------------------------------------------------------
序號 商品 數量 單價 小計.1. 鼠標 44 129.50 4558.40折后價 -> 103.602. 音響 25 699.00 15727.50折后價 -> 629.103. 鍵盤 19 199.00 3402.90折后價 -> 179.104. 平板 15 299.00 4485.005. 插板 13 19.00 247.006. 電源 47 109.00 5123.007. 手機 4 4999.00 19996.00==========================================================
聯合體
在進行某些算法的C語言編程的時候,需要使幾種不同類型的變量存放到同一段內存單元中。
也就是使用覆蓋技術讓幾個變量互相覆蓋。這種幾個不同的變量共同占用一段內存的結構,在C語言中,被稱作“共用體”類型結構,簡稱共用體,也叫聯合體。
—— 摘自百度百科
定義聯合體與使用
聯合體是一種特殊的數據類型,允許在相同的內存位置存儲不同的數據類型。程序中可以定義帶有多個成員的聯合體,但是任何時候只能有一個成員帶有值。共用體提供了一種使用相同的內存位置的有效方式。用到的關鍵字是 union
,其格式如下:
union 聯合體名稱 {member-list
}
實例代碼如下:
#include<stdio.h>
#include<string.h>union Person {char name[50];unsigned int age;double weight;
};int main() {union Person person;strcpy(person.name, "tom");person.age = 27;person.weight = 46.9;printf("person.name = %s, person.age = %d, person.weight = %f\n", person.name, person.age, person.weight);return 0;
}
執(zhí)行結果:
person.name = 33333sG@諏鱱AQ, person.age = 858993459, person.weight = 46.900000
可以看到,只有成員 weight 的值是被正確的輸出,原因是這三個成員變量在儲值的時候占用的是同一個內存空間,相當于這三個成員指向的是同一塊內存空間,當 weight 被賦值之后,其他的成員的值就被損壞。使用 &
可以查看各個成員指向的地址值:
union Person up;
printf("&up=%p, &up.name=%p, &up.age=%p, &up.weight=%p\n", &up, &up.name, &up.age, &up.weight);
執(zhí)行結果如下:
&up=0022FF08, &up.name=0022FF08, &up.age=0022FF08, &up.weight=0022FF08
如果需要在聯合體變量被聲明的時候賦予初始值的話,因為它們共用同一個內存空間,故而,不需要將每個值都賦值,而且編譯器只會接收第一個值作為有效值填入合適的成員中。例如:
#include<stdio.h>
#include<string.h>union Person {char name[50];unsigned int age;double weight;
};int main() {union Person up1 = {"Tom", 12, 4.6};union Person up2 = {12};union Person up3 = {4.6};union Person up4 = {"Tom"};printf("name=%s, age=%d, weight=%f\n", up1.name, up1.age, up1.weight);printf("name=%s, age=%d, weight=%f\n", up2.name, up2.age, up2.weight);printf("name=%s, age=%d, weight=%f\n", up3.name, up3.age, up3.weight);printf("name=%s, age=%d, weight=%f\n", up4.name, up4.age, up4.weight);return 0;
}
該源程序在編譯的時候給出警告和提示:
warning: excess elements in union initializerunion Person up1 = {"Tom", 12, 4.6};^~
note: (near initialization for 'up1')
warning: excess elements in union initializerunion Person up1 = {"Tom", 12, 4.6};^~~
note: (near initialization for 'up1')
執(zhí)行結果變成了:
name=Tom, age=7171924, weight=0.000000
name=, age=12, weight=0.000000
name=, age=4, weight=0.000000
name=Tom, age=7171924, weight=0.000000
從本例中可以看到,聯合體 up1 和 up4 接受的值是字符串 Tom
,聯合體 up2 接受的值是 12,聯合體 up3 將傳遞的小數直接轉換為了整型賦值給了 age 成員,接受的值是 4。故而,在給聯合體變量聲明的時候不需要為每個成員變量賦值,只需要賦予一個合適類型的值即可。
聯合體作為參數
將聯合體作為參數傳遞到函數中的操作方式與結構體一樣。但是因為共享內存的原因,在函數內,僅接受一個有效值。代碼如下:
#include<stdio.h>
#include<string.h>union Person {char name[50];unsigned int age;double weight;
};void printPerson(union Person p) {printf("Person{name:%s, age:%d, weight:%f}", p.name, p.age, p.weight);
}int main() {union Person up = {12};printPerson(up);return 0;
}
執(zhí)行結果:
Person{name:, age:12, weight:0.000000}
指向聯合體的指針
指向聯合體的指針在使用上與結構體一致,主要使用的運算符是 ->
。具體代碼如下:
#include<stdio.h>
#include<string.h>union Person {char name[50];unsigned int age;double weight;
};void printPerson(union Person *p) {printf("Person{name:%s, age:%d, weight:%f}", p->name, p->age, p->weight);
}int main() {union Person up = {12};union Person *p = &up;printPerson(p); return 0;
}
執(zhí)行結果:
Person{name:, age:12, weight:0.000000}
位運算
位和字節(jié)
1字節(jié)(byte) = 8 比特(bit)
字長指的是 CPU 一次性能夠運算的數據的位數,不同的計算機可能不一樣。
一個英文字符和英文標點占用一個字節(jié),一個中文字符和中文標點占用兩個字節(jié)。
計算機中的位
- 二進制數系統(tǒng)中,每個0或1就是一個位(bit),位是數據存儲的最小單位。
- 其中8 bit就稱為一個字節(jié)(Byte)。
- 計算機中的CPU位數指的是CPU一次能處理的最大位數,
- 例如32位計算機的CPU一次最多能處理32位數據,計算機中的CPU位數也成為機器字長和數據總線(CPU與內部存儲器之間連接的用于傳輸數據的線的根數)的概念是統(tǒng)一的。
比特
- 計算機專業(yè)術語,是信息量單位,是由英文 bit 音譯而來。
- 二進制數的一位所包含的信息就是 1bit,如二進制數
0101
就是 4bit。 - 二進制數字中的位,信息量的度量單位,為信息量的最小單位。
- 數字化音響中用電脈沖表達音頻信號,
1
代表有脈沖,0
代表脈沖間隔。 - 如果波形上每個點的信息用四位一組的代碼表示,則稱4比特,比特數越高,表達模擬信號就越精確,對音頻信號信號還原能力越強。
進制數
在實際開發(fā)中,我們可以用0和1的字符串來表達信息,例如某設備有八個傳感器,每個傳感器的狀態(tài)用1表示正常,用0表示故障,用一個二進制的字符串表示它們如01111011,用一個字符或整數表示它就是123。
十進制轉二進制
- 把十進數除以2,記下余數(余數保存在字符串中),現用商除以2,再記下余數,如此循環(huán),直到商為0。
- 把保存余數的字符串反過來,就是結果。
例如123轉化成二進制:
123 ÷ 2 = 61 ... 1
61 ÷ 2 = 30 ... 1
30 ÷ 2 = 15 ... 0
15 ÷ 2 = 7 ... 1
7 ÷ 2 = 3 ... 1
3 ÷ 2 = 1 ... 1
1 ÷ 1 = 0 ... 1
于是,十進制的 123 的用二進制表示就是:1111011
,代碼實現如下:
#include<stdio.h>
#include<stdlib.h>// 十進制轉換為二進制:輾轉相除法
void dec2bin(int decimal, char * bstr) {int quotient = decimal;char cs[32];int i = -1;// 初始化 cs 全部使用 \0 填充for(int j = 0; j < 32;j++) {cs[j] = '\0';}do {// div 函數來自 stdlib.h, 可以通過該函數得到商(quotient)和余數(remainder)div_t r = div(quotient, 2);quotient = r.quot;cs[++i] = (char)(48 + r.rem);} while(quotient > 0);// 反轉 cs 中的字符,得到二進制的字符串表達形式for(int j = 0; j < i;j++) {char c = cs[i-j];bstr[j]=c;}
}int main() {int n = 123;char bstr[32];dec2bin(n, bstr);printf("%d --> %s\n", n, bstr);return 0;
}
二進制轉十進制
把二進制數從右往左依次與2的次冪數相乘,最右邊的為0次冪,往左次冪依次升高,將每位的乘積相加即可:
例如1111011轉化成十進制:
1*2^6 + 1*2^5 + 1*2^4 + 1*2^3 + 0*2^2 + 1*2^1 + 1*2^0
結果是123。代碼實現如下:
#include<stdio.h>
#include<string.h>
#include<math.h>// 二進制轉換為十進制
int bin2dec(char * bstr) {int len = strlen(bstr);int sum = 0;for(int i = 0; i < len; i++) {char c = bstr[len-i-1];int b = (int)c - 48;sum += b * exp2(i);}return sum;
}int main() {char bstr[] = "1111011";int n = bin2dec(bstr);printf("%s --> %d\n", bstr, n);return 0;
}
八進制
一種以8為基數的計數法,采用0,1,2,3,4,5,6,7八個數字,逢八進1。
一些編程語言中常常以數字0開始表明該數字是八進制。
八進制的數和二進制數可以按位對應(八進制一位對應二進制三位),因此常應用在計算機語言中。
十六進制
在數學中是一種逢16進1的進位制。一般用數字0到9和字母A到F(或af)表示,其中:AF表示10~15。
位邏輯運算符
位運算的運算分量只能是整型或字符型數據。
位運算把運算對象看作是由二進位組成的位串信息,按位完成指定的運算,得到位串信息的結果。
位運算符有:
&
(按位與)、|
(按位或)、^
(按位異或)、~
(按位取反)。優(yōu)先級從高到低依次為:
~
、&
、^
、|
可以將位運算中的 1 和 0 理解為是邏輯中的真和假,或者為電路中的閉路和開路。
與運算:&
同真為真,存假則假
printf("(1 & 1) = %d\n", (1 & 1)); // 1
printf("(1 & 0) = %d\n", (1 & 0)); // 0
printf("(0 & 1) = %d\n", (0 & 1)); // 0
printf("(0 & 0) = %d\n", (0 & 0)); // 0
或運算:|
同假為假,存真為真
printf("(1 | 1) = %d\n", (1 | 1)); // 1
printf("(1 | 0) = %d\n", (1 | 0)); // 1
printf("(0 | 1) = %d\n", (0 | 1)); // 1
printf("(0 | 0) = %d\n", (0 | 0)); // 0
非運算:~
真即是假,假即是真
異或運算:^
相同為假,相異為真
printf("(1 ^ 1) = %d\n", (1 ^ 1)); // 0
printf("(1 ^ 0) = %d\n", (1 ^ 0)); // 1
printf("(0 ^ 1) = %d\n", (0 ^ 1)); // 1
printf("(0 ^ 0) = %d\n", (0 ^ 0)); // 0
位移運算:<< 或 >>
左移運算符
左移運算符(<<):是用來將一個數的各二進制位左移若干位,移動的位數由右操作數指定(右操作數必須是非負值),其右邊空出的位用0填補,高位左移溢出則舍棄該高位。
// 128 << 1 ? 256 × 2^1
printf("(128 << 1) = %d\n", (128 << 1)); // 256
// 64 << 2 ? 64 × 2^2
printf("(64 << 2) = %d\n", (64 << 2)); // 256
// 32 << 3 ? 32 × 2^3
printf("(32 << 3) = %d\n", (32 << 3)); // 256
右移運算符
右移運算符(>>):是用來將一個數的各二進制位右移若干位,移動的位數由右操作數指定(右操作數必須是非負值),移到右端的低位被舍棄,對于無符號數,高位補0。對于有符號數,某些機器將對左邊空出的部分用符號位填補(即“算術移位”),而另一些機器則對左邊空出的部分用0填補(即“邏輯移位”)。
注意:
-
對于無符號數,右移時左邊高位移入0
-
對于有符號的值
- 如果原來符號位為 0(即正數),則左邊也是移入0
- 如果符號位原來為 1(即負數),則左邊移入0還是1,要取決于所用的計算機系統(tǒng)
- 移入0的稱為“邏輯移位”,即簡單移位;
- 移入1的稱為“算術移位”。
// 256 >> 1 ? 256 ÷ 2^1
printf("(256 >> 1) = %d\n", (256 >> 1)); // 128
// 256 >> 2 ? 256 ÷ 2^2
printf("(256 >> 2) = %d\n", (256 >> 2)); // 64
// 256 >> 3 ? 256 ÷ 2^3
printf("(256 >> 3) = %d\n", (256 >> 3)); // 32
位運算賦值運算符
&=, |=, >>=, <<=, ∧=
int a = 256;
printf("a = %d\n", a);
a >>= 1;
printf("a = %d\n", a);
a >>= 1;
printf("a = %d\n", a);
a >>= 1;
printf("a = %d\n", a);
a <<= 1;
printf("a = %d\n", a);
a <<= 1;
printf("a = %d\n", a);
a <<= 1;
printf("a = %d\n", a);
預處理器
預處理器是在真正的編譯開始之前由編譯器調用的獨立程序。預處理器可以刪除注釋、包含其他文件以及執(zhí)行宏(宏macro是一段重復文字的簡短描寫)替代。
#include
包含一個源代碼文件,例如:
#include <stdio.h>
#define
定義宏,例如:
#define IDC_STATIC -1
#ifndef & #ifdef & #endif
#ifndef 與 #endif 聯合使用,表示如果某個宏未定義,則執(zhí)行相應的邏輯。例如:
#ifndef IDC_STATIC#define IDC_STATIC -1
#endif
#ifdef 與 #endif 聯合使用,表示如果某個宏已定義,則執(zhí)行相應的邏輯。例如:
#ifdef IDC_STATIC// do something here.
#endif
#if & #elif & #else
預處理器中的條件結構,示例代碼:
#if IDC_STATIC > 0#define IDC_STATIC -1
#elif !defined(MSG)#define MSG "MSG is undefined!"
#elif defined(MSG)#define MSG "MSG is defined!"
#else#define MSG "invalid code is here"
#endif
#undef
取消已定義的宏,例如:
#ifdef IDC_STATIC#undef IDC_STATIC
#endif
文件
C 的標輸出庫 stdio 中提供了對文件進行讀寫的函數。
打開文件
函數簽名:FILE * fopen ( const char * filename, const char * mode );
返回說明:如果成功打開該文件,則返回指向該文件的 FILE
對象的指針
參數說明:
- filename:需要打開的文件名
- mode:打開文件的模式
關于參數 mode 的取值:
值 | 說明 |
---|---|
"r" | read |
"w" | write |
"a" | append |
"r+" | read/update |
"w+" | write/update |
"a+" | append/update |
詳細說明請參考:http://www.cplusplus.com/reference/cstdio/fopen/
關閉文件
函數簽名:int fclose ( FILE * stream );
返回說明:如果成功關閉該文件,則返回 0
參數說明:
- stream:指向該文件的
FILE
對象的指針
詳細說明請參考:http://www.cplusplus.com/reference/cstdio/fclose/
寫入文件
函數簽名:int fputs ( const char * str, FILE * stream );
返回說明:成功時返回非負值
參數說明:
- str:需要寫入的字符串常量
- stream:指向該文件的
FILE
對象的指針
詳細說明請參考:http://www.cplusplus.com/reference/cstdio/fputs/
讀取文件
函數簽名:char * fgets ( char * str, int num, FILE * stream );
返回說明:成功時傳入的參數 str 的值
參數說明:
- str:指向在其中復制字符串讀取的字符數組的指針
- num:要復制到str中的最大字符數(包括終止的空字符)
- stream:指向該文件的
FILE
對象的指針
詳細說明請參考:http://www.cplusplus.com/reference/cstdio/fgets/
簡單示例
#include<stdio.h>int main() {const char *filename = "/io.txt";// 寫{FILE *f = fopen("/io.txt", "w+");int r = fputs("Hello World.\n —— marvelousness", f);if(r > -1) {r = fclose(f);if(r == 0) {// printf("寫入完成.\n");}}}// 讀{FILE *f = fopen("/io.txt", "r");char buff[1024];while(fgets(buff, 1024, f) != NULL) {printf("%s", buff);}int r = fclose(f);}return 0;
}
執(zhí)行結果:
Hello World.—— marvelousness
內存管理
了解變量的存儲類別以及動態(tài)內存分配的知識
存儲類別
C語言中的四種存儲類別: 自動變量 (auto)、靜態(tài)變量(static)、寄存器(register)、外部變量 (extern)。
自動變量 (auto)
通常在自定義函數內或代碼段中(用
{}
括起來的)定義的變量,都是自動變量,除了加了static關鍵字修飾的變量,也稱為局部變量。
自動變量都是動態(tài)地分配存儲空間的,數據存儲在動態(tài)存儲區(qū)中。
函數中的形參和在函數中定義的變量(包括在復合語句中定義的變量)都屬于這個分類,在調用該函數時系統(tǒng)會給它們分配存儲空間,在函數調用結束時就自動釋放這些存儲空間。
自動變量用關鍵字 auto 進行存儲類別的聲明,例如聲明一個自動變量:
void func() {auto int a = 10;
}
外部變量
外部變量(即全局變量)是在函數的外部定義的,它的作用域為從變量定義處開始,到本程序文件的末尾。
int a = 10;int main() {return 0;
}
或者通過 extern
指定某個全局變量來自外部文件,例如在 other.c
中定義的全局變量在 app.c
中使用:
第一個源文件
other.c
靜態(tài)變量(static)
有時希望函數中的局部變量的值在函數調用結束后不消失而保留原值,這時就應該指定局部變量為靜態(tài)局部變量,用關鍵字
static
進行聲明。
#include<stdio.h>void func(int i) {static int a=0;a += i;printf("a=%d\n", a);
}int main() {func(1);func(1);func(1);return 0;
}
執(zhí)行結果:
a=1
a=2
a=3
如果將程序中的 static
去掉,則輸出的結果將會恒為 a=1
。
寄存器變量(register)
為提高效率,C 語言允許將局部變量的值存放在 CPU 的寄存器中,這種變量叫做寄存器變量。
用關鍵字 register 聲明。
使用寄存器變量需要注意以下幾點:
- 只有局部自動變量和形式參數可以作為寄存器變量。
- 一個計算機系統(tǒng)中的寄存器數目有限,不能定義任意多個寄存器變量。
- 不能使用取地址運算符
&
求寄存器變量的地址。
void func(register int i) {register int a = 10;printf("a=%d,i=%d\n", a, i);
}
相關概念
了解變量的作用域、鏈接、存出期
作用域(scope)
作用域描述了程序中合法訪問一個標識符的區(qū)域。
一個C變量的作用域可以是:
- 代碼塊作用域(block scope)
- 函數原型作用域(function prototype scope)
- 文件作用域(file scope)
鏈接(linkage)
跟作用域類似,變量的鏈接是一個空間概念,描述了程序中合法訪問一個標識符的區(qū)域。
一個C變量的鏈接類型可以是:
- 外部鏈接(external linkage)
- 內部鏈接(internal linkage)
- 空鏈接(no linkage)
存儲期(storage duration)
變量的聲明周期,描述了一個C變量在程序執(zhí)行期間的存在時間。
一個C變量的存儲期可以是:
- 靜態(tài)存儲期(static storage duration)
- 自動存儲期(automatic storage duration)
- 動態(tài)存儲期(dynamic storage duration)
存儲類別小結
存儲類 | 時期 | 作用域 | 鏈接 | 聲明方式 |
---|---|---|---|---|
自動 | 自動 | 代碼塊 | 空 | 代碼塊內 |
寄存器 | 自動 | 代碼塊 | 空 | 代碼塊內 Register |
具有外部鏈接的靜態(tài) | 靜態(tài) | 文件 | 外部 | 所有函數之外 |
具有內部鏈接的靜態(tài) | 靜態(tài) | 文件 | 內部 | 所有函數之外 static |
空鏈接的靜態(tài) | 靜態(tài) | 代碼塊 | 空 | 代碼塊之內 static |
內存動態(tài)管理
C 語言為內存的分配和管理提供了幾個函數。這些函數可以在
<stdlib.h>
頭文件中找到。
棧上開辟空間
默認情況下,C 語言會根據基礎類型為變量在棧上開辟空間。
int num = 20;//在??臻g上開辟四個字節(jié)
char ns[4] = {1, 2, 3, 4};//在??臻g上開辟10個字節(jié)的連續(xù)空間
特點如下:
- 空間開辟大小是固定的。
- 數組在聲明的時候,必須指定數組的長度,它所需要的內存在編譯時分配。
動態(tài)內存函數
malloc
用來開辟動態(tài)內存
void * malloc(size_t size);
這個函數向內存申請一塊連續(xù)可用的空間,并返回指向這塊空間的指針。
- 如果開辟成功,則返回一個指向開辟好空間的指針。
- 如果開辟失敗,則返回一個NULL指針,因此malloc的返回值一定要做檢查。
- 返回值的類型是
void*
,所以malloc
函數并不知道開辟空間的類型,在使用的時候使用者需要手動轉換 - 如果參數 size 為 0,
malloc
的行為是標準是未定義的,取決于編譯器。
int n = 26;
// 開辟一塊內存,占 n 個 char 類型的大小
char *p = malloc(n * sizeof(char));
for(int i = 0; i < n; i++) {p[i] = (char)(i + 97);
}
printf("p=%s\n", p);
free(p);
free
函數用來釋放動態(tài)開辟的內存
void free (void* ptr);
- 如果參數
ptr
指向的空間不是動態(tài)開辟的,那free函數的行為是未定義的。 - 如果參數
ptr
是NULL指針,則函數什么事都不做。 - free前后指向的地址不發(fā)生任何變化,改變的只是指針和對應的內存的管理關系。
int num = 20;
int* p = #
printf("p=%p\n", p);
free(p);
if(p == NULL) {printf("p is NULL");
} else {printf("p=%p\n", p);
}
p = NULL;// 一般再添加一句代碼令指針為NULL。
calloc
函數也用來動態(tài)內存分配
void* calloc (size_t num, size_t size);
- 函數的功能是為 num 個大小為 size 的元素開辟一塊空間,并且把空間的每個字節(jié)初始化為0。
- 與函數
malloc
的區(qū)別只在于calloc
會在返回地址之前把申請的空間的每個字節(jié)初始化為全0。
int n = 26;
// 開辟 n 塊內存,每塊內存占 char 類型的大小
char *p = calloc(n, sizeof(char));
for(int i = 0; i < n; i++) {p[i] = (char)(i + 65);
}
printf("p=%s\n", p);
free(p); // 釋放指針
realloc
函數的出現讓動態(tài)內存管理更加靈活。
void * realloc(void * ptr, size_t size);
- ptr 是要調整的內存地址
- size 調整之后新大小
- 返回值為調整之后的內存起始位置。
- 這個函數調整原內存空間大小的基礎上,還會將原來內存中的數據移動到 新的空間。
realloc
在調整內存空間的是存在兩種情況:- 情況1:原有空間之后有足夠大的空間。這種情況下,要擴展內存就在原有內存之后直接追加空間,原來空間的數據不發(fā)生變化。
- 情況2 :原有空間之后沒有足夠多的空間。這種情況下,在堆空間上另找一個合適大小的連續(xù)空間來使用。這樣函數返回的是一個新的內存地址。
#include<stdio.h>
#include<stdlib.h>
#define SIZE_A 50
#define SIZE_B 100int main() {int *ptr = malloc(SIZE_A);if(ptr == NULL) {printf("動態(tài)分配內存失敗\n");exit(0);}printf("ptr=%p\n", ptr);int *p = realloc(ptr, SIZE_B);if(p == NULL) {printf("重新分配內存失敗\n");exit(0);}printf(" p =%p\n", p);ptr = p;free(ptr);free(p);return 0;
}
在上述源代碼中,當 SIZE_B
為 100,在重新分配內存的時候,原有空間之后沒有足夠的空間,符合情況2,會返回一個新的地址,修改 SIZE_B
為4,在重新分配內存的時候,原有空間有足夠的空間,符合情況1,會返回原指針的地址。
常見動態(tài)內存錯誤
列舉幾個常見的錯誤
- 對NULL指針的解引用操作
*p = 20; //如果p的值是NULL,就會有問題
- 對動態(tài)開辟空間的越界訪問
int *p = (int *)malloc(5*sizeof(int));
if(p != NULL) {for(int i=0; i<=5; i++) {*(p+i) = i;//當i是5的時候越界訪問}
}
- 對非動態(tài)開辟內存使用free釋放
int a = 10;
int *p = &a;
free(p);// p 的地址來自變量 a, 但 a 的內存并非動態(tài)開辟, 這種用法是錯誤的
- 使用free釋放一塊動態(tài)開辟內存的一部分
int *p = (int *)malloc(100);
p++;
free(p);// p不再指向動態(tài)內存的起始位置
- 對同一塊動態(tài)內存多次釋放
int *p = (int *)malloc(100);
free(p);
free(p);//重復釋放
- 動態(tài)開辟內存忘記釋放(內存泄漏)
#include<stdio.h>
#include<stdlib.h>int main() {int *p = (int *)malloc(100);if(NULL != p) {*p = 20;// free(p); // 在使用完指針后,不要記得將其 free}return 0;
}
標準函數庫
數學庫
在
math.h
中提供了關于數學相關的函數
三角函數(Trigonometric functions)
數學中的自然常數
π
的值作為常量使用M_PI
表示
printf("sin(30°)=%.1f\n", sin(M_PI/6));
printf("cos(60°)=%.1f\n", cos(M_PI/3));
printf("tan(45°)=%.1f\n", tan(M_PI/4));printf("asin(0.5)=%.0f°\n", asin(0.5) * (180/M_PI));
printf("acos(0.5)=%.0f°\n", acos(0.5) * (180/M_PI));
printf("atan(1)=%.0f°\n", atan(1) * (180/M_PI));
指數函數(Exponential functions)
exp
用來求以自然常數 e 的常數的指數結果。exp2
用來求以 2 的常數的指數結果。
printf("e^(0)=%.3f\n", exp(0));
printf("e^(1)=%.3f\n", exp(1));
printf("e^(2)=%.3f\n", exp(2));printf("exp2(2)=%f\n", exp2(2));
printf("exp2(3 )=%f\n", exp2(3));
對數函數(Logarithmic functions)
數學中的自然常數
e
的值在 C 語言中使用M_E
表示
// 求以 e 為底數的對數
printf("ln(e)=%f\n", log(M_E));
printf("ln(1)=%f\n", log(1));// 求以 10 為底數的對數
printf("log10(10)=%f\n", log10(10));
printf("log10(1)=%f\n", log10(1));// 求以 2 為底數的對數
printf("log2(2)=%f\n", log2(2));
printf("log2(1)=%f\n", log2(1));
冪函數(Power functions)
printf("7^2 = %f\n", pow(7.0, 2.0));
平方根(square root)
printf(" 4 的平方根是 %f\n", sqrt(4.0));
立方根(cube root)
printf(" 8 的立方根是 %f\n", cbrt(8.0));
求斜邊(hypotenuse)
已知直角三角形的兩個直角邊長度,求斜邊長度
printf("直角邊為 3.0 和 4.0, 斜邊為 %.1f\n", hypot(3.0, 4.0));
取整函數
// 向上取整
printf( "ceil(2.3)=%.1f\n", ceil(2.3));
printf( "ceil(2.6)=%.1f\n", ceil(2.6));
// 向下取整
printf( "floor(2.3)=%.1f\n", floor(2.3));
printf( "floor(2.6)=%.1f\n", floor(2.6));
// 四舍五入
printf( "round(2.3)=%.1f\n", round(2.3));
printf( "round(2.6)=%.1f\n", round(2.6));
// 取余
printf( "fmod(5.0, 3.0) = %.1f\n", fmod(5.0,3.0));
printf( "5 mod 3 = %.0f\n", 5%3);
絕對值和最值
printf( "|3.14| = %.2f\n", fabs(3.14));
printf( "|-3.14| = %.2f\n", fabs(-3.14));
printf( "fmax(3.14, 2.72) = %.2f\n", fmax(3.14, 2.72));
printf( "fmin(3.14, 2.72) = %.2f\n", fmin(3.14, 2.72));
通用工具庫
在
stdlib.h
中,提供了大量的工具庫,可以在開發(fā)過程中去使用。
double atof(const char *str)
把參數 str 所指向的字符串轉換為一個浮點數(類型為 double 型)。
double r = atof("3.14"); // 結果是 3.14
r = atof("3.14ok"); // 結果是 3.14
r = atof("3.14 ok"); // 結果是 3.14
int atoi(const char *str)
把參數 str 所指向的字符串轉換為一個整數(類型為 int 型)。
int r = atoi("2024"); // 結果是 2024
r = atoi("2024ok"); // 結果是 2024
r = atoi("2024 ok"); // 結果是 2024
long int atol(const char *str)
把參數 str 所指向的字符串轉換為一個長整數(類型為 long int 型)
long int r = atol("2024"); // 結果是 2024
r = atol("2024ok"); // 結果是 2024
r = atol("2024 ok"); // 結果是 2024
double strtod(const char *str, char *endptr))
把參數 str 所指向的字符串轉換為一個浮點數(類型為 double 型)
str
:要轉換為雙精度浮點數的字符串。endptr
:對類型為char*
的對象的引用,其值由函數設置為str
中數值后的下一個字符。這個當第二個參數為空時和
double atof(const char *str)
是相同,但是當而第二形參引用后,第二個指針會指向存儲字符串位置的地址。
char str[] = "23.67km";
char *pstr;
double ret = strtod(str, &pstr);
printf("ret=%f, pstr=%s", ret, pstr);
ret
的值是 23.67
, pstr
的值是 km
。
long int strtol(const char *str, char *endptr, int base)
把參數 str 所指向的字符串轉換為一個有符號長整數(類型為 long int 型)。
str
:要轉換為長整數的字符串。endptr
:對類型為char*
的對象的引用,其值由函數設置為str
中數值后的下一個字符。base
:基數,必須介于 2 和 36(包含)之間,或者是特殊值 0。這個基數就表示這個多少數字就多少進制
char str[] = "2024year";
char *pstr;
long int ret = strtol(str, &pstr, 8);
printf("ret=%d, pstr=%s\n", ret, pstr);// ret 的值是 1044, pstr 的值是 year
ret = strtol(str, &pstr, 10);
printf("ret=%d, pstr=%s\n", ret, pstr);// ret 的值是 2024, pstr 的值是 year
unsigned long int strtoul(const char *str, char **endptr, int base)
把參數 str 所指向的字符串轉換為一個無符號長整數(類型為 unsigned long int 型)。
char str[] = "2024year";
char *pstr;
unsigned long int ret = strtoul(str, &pstr, 8);
printf("ret=%d, pstr=%s\n", ret, pstr);
ret = strtol(str, &pstr, 10);
printf("ret=%d, pstr=%s\n", ret, pstr);
void *calloc(size_t nitems, size_t size)
分配所需的內存空間,并返回一個指向它的指針。
malloc
和calloc
之間的不同點是,malloc
不會設置內存為零。
// 參考前文【內存管理】章節(jié)的代碼
void free(void *ptr)
釋放指針所指內存空間的數據,該函數不返回任何值
// 參考前文【內存管理】章節(jié)的代碼
void *malloc(size_t size)
分配所需的內存空間,并返回一個指向它的指針。
// 參考前文【內存管理】章節(jié)的代碼
void *realloc(void *ptr, size_t size)
嘗試重新調整之前調用
malloc
或calloc
所分配的ptr
所指向的內存塊的大小。該函數返回一個指針 ,指向重新分配大小的內存。如果請求失敗,則返回 NULL。
// 參考前文內存管理章節(jié)的代碼
void abort(void)
使一個異常程序終止。
for(int i =0; i < 100; i++) {if(i == 30) {abort();終止程序}printf("i=%d\n", i);
}
int atexit(void (*func)(void))
當程序正常終止時,調用指定的函數 func。
void func() {printf("The program exited!");
}
int main() {atexit(func);return 0;
}
void exit(int status)
使程序正常終止。
for(int i =0; i < 100; i++) {if(i == 10) {exit(0);}printf("i=%d\n", i);
}
char *getenv(const char *name)
搜索 name 所指向的環(huán)境字符串,并返回相關的值給字符串。
printf("JAVA_HOME : %s\n", getenv("JAVA_HOME"));
int system(const char *string)
由 string 指定的命令傳給要被命令處理器執(zhí)行的主機環(huán)境。
system("dir");
void *bsearch(const void *key, const void *base, size_t nitems,size_t size, int (*compar)(const void *, const void *))
執(zhí)行二分查找
- key:指向要查找的元素的指針,類型轉換為 void* 。
- base:指向進行查找的數組的第一個對象的指針,類型轉換為 void* 。
- nitems:base 所指向的數組中元素的個數。
- size:數組中每個元素的大小,以字節(jié)為單位。
- compar:用來比較兩個元素的函數。
#include<stdio.h>
#include<stdlib.h>
#define SIZE 5int compar(const void * a, const void *b) {return *(int *)a - *(int *)b;
}int main() {int ns[SIZE] = {5, 8, 3, 6, 4};int key = 6;int *result = (int *)bsearch(&key, ns, SIZE, sizeof(int), compar);if(result != NULL) {printf("*result=%d, result=%p\n", *result, result);} else {printf("can`t find %d\n", key);}for(int i = 0; i < SIZE; i++) {printf("ns[%d]=%d, &ns[%d]=%p\n", i, ns[i], i, &ns[i]);}return 0;
}
void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*))
數組排序
- base:指向要排序的數組的第一個元素的指針。
- nitems:由 base 指向的數組中元素的個數。
- size:數組中每個元素的大小,以字節(jié)為單位。
- compar:用來比較兩個元素的函數。
#include<stdio.h>
#include<stdlib.h>
#define SIZE 5int compar(const void * a, const void *b) {return *(int *)a - *(int *)b;
}int main() {int ns[SIZE] = {5, 8, 3, 6, 4};qsort(ns, SIZE, sizeof(int), compar);for(int i = 0; i < SIZE; i++) {printf("ns[%d]=%d, &ns[%d]=%p\n", i, ns[i], i, &ns[i]);}return 0;
}
int abs(int x)
返回 x 的絕對值。
int a = abs(-4);
printf("a=%d\n", a);
div_t div(int numer, int denom)
分子除以分母。
div_t output = div(27, 4);
printf("(27/4) quotient = %d\n", output.quot);
printf("(27/4) remainder = %d\n", output.rem);
int rand(void)
和void srand(unsigned int seed)
返回一個范圍在 0 到 RAND_MAX 之間的偽隨機數。
// 初始化隨機數發(fā)生器 以CPU的時間作為隨機種子所以是偽隨機數
srand((unsigned)time(NULL));
// 輸出 0 到 49 之間的 5 個隨機數
for (int i = 0; i < 5; i++) {printf("%d\n", rand() % 50);
}