C語言基礎知識總結


1、C關鍵字

C語言中的關鍵字有32個:

  • 數據類型關鍵字(12個)
    char, short, int, long, float, double, unsigned, signed, struct, union, enum, void
  • 控制語句關鍵字(12個)
    if, else, switch, case, default, for, do, while, break, continue, goto, return
  • 存儲類關鍵字(5個)
    auto, extern, register, static, const
  • 其它關鍵字(3個)
    sizeof, typedef, volatile

 

2、數據類型

數據類型的作用:編譯器預算對象(變量)分配的內存空間大小

 

3、常量

常量特點:

  • 在程序運行過程中,值不能被改變的量
  • 常量一般出現在表達式或者賦值語句中
整型常量 100,200,-1000,0
實型常量 3.14,0.125,-3.123
字符常量 'a','b','\r','\n'
字符串常量 "a","abc","2333"

 

4、變量

(1)變量

  • 在程序運行過程中,其值可以被改變
  • 變量在使用前必須先定義,定義變量前必須有相對應的數據類型

標識符命名規則

  • 標識符不能夠是關鍵字
  • 標識符只能由字母、數字、下划線組成
  • 第一個字符必須是字母或者下划線
  • 標識符中的字母區分大小寫

變量的特點:

  • 變量在編譯的時候為其分配相應的內存空間大小
  • 可以通過變量的名字和變量的地址訪問相應內存

(2)聲明和定義區別

  • 聲明變量不需要建立存儲空間,如:extern int a;
  • 定義變量需要建立存儲空間,如:int a;
  • int b;既是聲明同時也是定義
  • 對於extern int b;只是聲明不是定義

一般情況下,把建立存儲空間的聲明稱之為“定義”,而把不需要建立存儲空間的聲明稱之為“聲明”。

 

5、整型

(1)整型int類型變量定義和輸出

打印格式 含義
%d 輸出有符號的10進制int類型
%o 輸出8進制的int類型
%x 輸出16進制的int類型,字母以小寫輸出
%X 輸出16進制的int類型,字母以大寫輸出
%u 輸出10進制的無符號數
#include <stdio.h>

int main(void)
{
  int a = 123;  //定義變量a,以10進制方式賦值為123
  int b = 0456;  //定義變量b,以8進制方式賦值為456
  int c = 0xabc;  //定義變量c,以16進制方式賦值為0xabc

  printf("a = %d\r\n", a);  //10進制格式輸出
  printf("b = %o\r\n", b);  //8進制格式輸出
  printf("c = %x\r\n", c);  //16進制格式輸出

  return 0;  
}

(2)整型變量的輸入

#include <stdio.h>

int main(void)
{
  int a;

  printf("Please input the value of a:");
  scanf("%d", &a);

  printf("a = %d\r\n", a);

  return 0;
}

(3)整型short、int、long、long long

整型類型 占用空間
short(短整型) 2字節
int(整型) 4字節
long(長整型) Windows為4字節,Linux(32bit)為4字節,Linux(64bit)為8字節
long long(長整型) 8字節

注意:

  • 整型數據在內存中所占用的字節數與所選擇的操作系統有關,C語言標准中沒有明確規定整型數據的長度,但是long類型整數的長度不能短於int類型,short類型的長度不能長於int類型;
  • 當一個小的數據類型賦值給大的數據類型時,不會出錯,編譯器會自動轉化,但當一個大的數據類型賦值給一個小的數據類型時,有可能會丟失高位。
整型常量 所需類型
10 代表int類型
10l,10L 代表long類型
10ll,10LL 代表long long類型
10u,10U 代表unsigned int類型
10ul,10UL 代表unsigned long類型
10ull,10ULL 代表unsigned long long類型

各整型類型的格式化輸出:

打印格式 含義
%hd 輸出short類型
%d 輸出int類型
%ld 輸出long類型
%lld 輸出long long類型
%hu 輸出unsigned short類型
%u 輸出unsigned int類型
%lu 輸出unsigned long類型
%llu 輸出unsigned long long類型

整型格式化輸出例子:

#include <stdio.h>

int main(void)
{
  short a = 10;
  int b = 10;
  long c = 10L;
  long long d = 10LL;

  printf("sizeof(a) = %u\r\n", sizeof(a));
  printf("sizeof(b) = %u\r\n", sizeof(b));
  printf("sizeof(c) = %u\r\n", sizeof(c));
  printf("sizeof(d) = %u\r\n", sizeof(d));

  unsigned short a2 = 20u;
  unsigned int b2 = 20u;
  unsigned long c2 = 20ul;
  unsigned long long d2 = 20ull;

  printf("unsigned short a = %hu\r\n", a2);
  printf("unsigned int b = %u\r\n", b2);
  printf("unsigned long = %lu\r\n", c2);
  printf("unsigned long long = %llu\r\n", d2);

  return 0;
}

(4)有符號數和無符號數的區別

  • 有符號數的最高位為符號位,0代表正數,1代表負數
#include <stdio.h>

int main(void)
{
  signed int a = -1089474374;  //定義有符號整型變量a
  printf("a = %X\r\n", a);  //結果16進制輸出為a = BF0FF0BA

  //B    F    0    F     F     0    B    A
  //1011 1111 0000 1111  1111  0000 1011 1010

  return 0;
}
  • 無符號數最高位不是符號位,就是數的一部分,無符號數不可能是負數
#include <stdio.h>

int main(void)
{
  unsigned int a = 3236958022;  //定義無符號整型變量a
  printf("a = %X\r\n");  //16進制輸出結果為C0F00F46

  //C    0    F    0     0     F     4     6
  //1100 0000 1111 0000  0000  1111  0100  0110

  return 0;
}

在編寫程序處理一個不可能出現負值的數值時,一般使用無符號數,這樣處理能增大數的表達最大值。

  • 有符號數和無符號數整型的取值范圍
數值類型 占用空間 取值范圍
short 2字節 -32768~32767(-215~215-1)
int 4字節  -2147483648~2147483647(-231~231-1) 
long 4字節  -2147483648~2147483647(-231~231-1) 
unsigned short 2字節 0~65535(0~216-1) 
unsigned int 4字節  0~4294967295(0~232-1)
unsigned long  4字節  0~4294967295(0~232-1) 

 

6、sizeof關鍵字

  • sizeof不是函數,是關鍵字,不需要包含任何頭文件,它的功能是計算一個數據類型的大小,單位為字節
  • sizeof的返回值為size_t類型
  • size_t類型在32位操作系統下是unsigned int類型,為無符號整數
#include <stdio.h>

int main(void)
{
  int a;
  int b = sizeof(a);  //獲取變量a所占用的內存大小,單位為字節

  printf("b = %d\r\n", b);

  size_t c = sizeof(a);
  printf("c = %u\r\n", c);  //使用無符號整數的格式輸出
}

 

7、字符型

(1)字符型變量的定義和輸出

字符型變量用於存儲一個單一的字符,在C語言中,使用char數據類型進行表示,每個字符變量會占用1個字節,在給字符變量賦值時,需要使用一對英文半角格式的單引號(' ')把字符括起來。

字符變量實際上並不是把該字符本身存儲到變量的內存單元中,而是將該字符對應的ASCII編碼存儲到變量的內存單元中,char類型的本質就是一個1字節大小的整型。

#include <stdio.h>

int main(int argc, char *argv[])
{
  char c = 'a';

  printf("sizeof(c) = %u\r\n", sizeof(c));

  printf("c = %c\r\n", c);
  printf("c = %d\r\n", c);

  return 0;
}

(2)字符型變量的輸入

#include <stdio.h>

int main(int argc, char *argv[])
{
  char c;

  printf("Please input the char:");
  
  scanf("%c", &c);
  printf("c =  %c\r\n", c);

  return 0;
}

(3)ASCII碼表

ASCII碼大致由以下兩部分組成:

  • ASCII非打印控制字符:ASCII表上的數字0~31分配給了控制字符,用於控制像打印機等一些外圍設備;
  • ASCII打印字符:數字32~126分配給了能在鍵盤上找到的字符,當查看或者打印文檔時就會出現,數字127代表Del命令。

(4)轉義字符

轉義字符 含義 ASCII碼值(十進制)
\a 警報 007
\b 退格(BS),將當前位置移到前一列 008
\f 換頁(FF),將當前位置移到下頁開頭 012
\n 換行(LF),將當前位置移到下一行開頭 010
\r 回車(CR),將當前位置移到本行開頭 013
\t 水平制表(HT),跳到下一個TAB位置 009
\v 垂直制表(VT) 011
\\ 代表一個反斜線'\'字符 092
\' 代表一個單引號'''字符 039
\" 代表一個雙引號'"'字符 034
\? 代表一個問號字符 063
\0 數字0 000
\ddd 8進制轉義字符,d范圍0~7 3位8進制
\xhh 16進制轉義字符,h范圍0~9,a~f,A~F 3位16進制

簡單示例如下:

#include <stdio.h>

int main(int argc, char *argv[])
{
  printf("abc");
  printf("\befg\n");  //\b為退格鍵,\n為換行鍵

  printf("%d\n", '\123');//'\123'為8進制轉義字符,0123對應十進制數83
  printf("%d\n", '\x23');//'\x23'為16進制轉義字符,0x23對應十進制數53

  return 0;
}

 

8、實型(浮點型)

實型變量也可以稱為浮點型變量,浮點型變量是用來存儲小數數值的,在C語言中,浮點型變量分為兩種,分別為單精度浮點數(float)、雙精度浮點數(double),但是,double類型變量表示的浮點數比float類型變量更精確。

由於浮點型變量是由有限的存儲單元組成的,因此,只能提供有限的有效數字,在有效位以外的數字將會被舍棄,所以可能會產生一定的誤差。

#include <stdio.h>

int main(int argc, char *argv[])
{
  //傳統方法賦值
  float a = 3.14f;
  double b = 3.14;

  printf("a = %f\r\n", a);
  printf("b = %lf\r\n", b);

  //科學法賦值
  a = 3.2e3f;  //3.2*1000=3200
  printf("a = %f\r\n", a);

  a = 100e-3f;  //100*0.001=0.1
  printf("a = %f\r\n", a);

  return 0;
}

 

9、進制

進制就是進位制,是人們規定的一種進位方法,對於任何一種進制-X進制,就表示某一位置上的數運算時是逢X進一位,十進制就是逢十進一,十六進制就是逢十六進一,二進制是逢二進一,以此類推。

十進制 二進制 八進制 十六進制
0 0 0 0
1 1 1 1
2 10 2 2
3 11 3 3
4 100 4 4
5 101 5 5
6 110 6 6
7 111 7 7
8 1000 10 8
9 1001 11 9
10 1010 12 A
11 1011 13 B
12 1100 14 C
13 1101 15 D
14 1110 16 E
15 1111 17 F
16 10000 20 10

(1)二進制

二進制是計算機技術中廣泛采用的一種數制,它是使用0和1兩個數碼來表示的數,基數為2,進位規則是逢二進一,借位規則是借一當二,目前計算機系統使用的基本是二進制系統,數據在計算機中主要是以補碼的形式存儲的。

術語 含義
bit(比特) 一個二進制代表一位,一個位只能表示0或1兩種狀態
Byte(字節) 一個字節有8bit,計算機中存儲的最小單位是字節
WORD(雙字節) 兩個字節,16bit
DWORD 兩個WORD,四個字節,32bit
1b 1bit
1B 1個字節,8bit
1k,1K 1024
1M 1024K
1G 1024M
1T 1024G
1Kb(千位) 1024bit
1KB(千字節) 1024字節
1Mb(兆位) 1024Kb=1024*1024bit
1MB(兆字節) 1024KB=1024*1024字節

十進制轉化為二進制的方法如下:

用十進制數除以2,分別取余數和商數,當商數為0的時候,將余數倒着數就是轉化后的結果。

十進制的小數轉換成二進制的方法如下:

小數部分和2相乘,取整數,不足1取0,每次相乘都是小數部分,順序看取整后的數就是轉換后的結果。

(2)八進制

八進制,Octal,縮寫為OCT或0,一種以8為基數的計數法,采用0~7八個數字,逢八進一,在編程語言中常以數字0開始表明該數字是八進制數,八進制的數和二進制數可以按位對應(八進制一位對應二進制三位),因此常用在計算機語言中。

十進制轉化為八進制的方法:

用十進制數除以8,分別取余數和商數,當商數為0的時候,將余數倒着數就是轉化后的結果。

(3)十六進制

十六進制,英文名稱為Hexadecimal,它由0~9,A~F組成,字母不區分大小,與十進制的對應關系為,0~9對應0~9,A~F對應10~15。

十六進制數和二進制數可以按位相對應(十六進制一位對應二進制四位),因此常應用在計算機語言中。

十進制轉化為十六進制數的方法:

用十進制數除以16,分別取余數和商數,當商數為0的時候,將余數倒着數就是轉化后的結果。

(4)C語言表示進制數

在C語言中,表示各種進制數的方法如下:

進制 表示方法
十進制 以正常數字1~9開頭,如123
八進制 以數字0開頭,如0123
十六進制 以0x開頭,如0x123
二進制 C語言不支持直接寫二進制數

簡單示例如下:

#include <stdio.h>

int main(int argc, char *argv[])
{
  int a = 123;  //十進制方式賦值
  int b = 0123;  //八進制方式賦值
  int c = 0x123;  //十六進制方式賦值

  printf("a = %d\r\n");
  printf("b = %o\r\n");
  printf("c = %x\r\n");

  return 0;
}

 

10、計算機內存數值存儲方式

(1)原碼

一個數的原碼(原始的二進制碼)具有以下特點:

  • 最高位作為符號位,0表示為正數,1表示為負數
  • 其它數值部分就是數值本身絕對值的二進制數
  • 負數的原碼是在其絕對值的基礎商,最高位變為1

原碼表示簡單示例如下:

十進制數 原碼
+15 0000 1111
-15 1000 1111
+0 0000 0000
-0 1000 0000

原碼表示方法簡單易懂,與帶符號數本身轉換方便,只需要將符號還原即可,但當兩個正數相減或不同符號數相加時,必須比較兩個數哪個絕對值大,才能確定結果是正還是負,所以原碼並不便於加減運算。

(2)反碼

反碼的含義:

  • 對於正數,反碼與原碼相同
  • 對於負數,符號位不變,其它部分取反(1變0,0變1)
十進制數 反碼
+15 0000 1111
-15 1111 0000
+0 0000 0000
-0 1111 1111

反碼運算也不方便,通常用來作為求補碼的中間過渡。

(3)補碼

在計算機系統中,數值一律使用補碼來存儲,補碼的特點如下:

  • 對於正數,原碼、反碼、補碼相同;
  • 對於負數,其補碼為它的反碼加1;
  • 補碼符號位不變,其它位求反,最后整個數加1,得到原碼。

計算機使用補碼來存儲的主要原因為:

  • 統一了零的編碼;
  • 將符號位和其它位統一處理;
  • 將減法運算轉變為加法運算;
  • 兩個使用補碼表示的數相加時,如果最高位(符號位)有進位,則進位被舍棄。

(4)數值溢出

當超過一個數據類型能夠存放的最大范圍時,數值就會溢出。

有符號位最高位溢出的區別:符號位溢出會導致數的正負發生改變,但是最高位的溢出會導致最高位丟失。

數據類型 占用空間 取值范圍
char 1字節 -128~127(-27~27-1)
unsigned char 1字節 0~255(0~28-1)
#include <stdio.h>

int main(int argc, char *argv[])
{
  char a;

  //符號位溢出會導致數的正負發生改變
  a = 0x7f + 2;  //127+2
  printf("a = %d\r\n", a);
  //0111 111
  //+2后為1000 0001,這是負數補碼,其原碼為1111 1111,結果是-127

  //最高位的溢出會導致最高位丟失
  unsigned char a2;
  a2 = 0xff + 1;  //255+1
  printf("a2 = %u\r\n", a2);
  //1111 1111
  //+1后為10000 0000,char類型只有8位,最高位溢出,結果為0

  a2 = 0xff + 2;  //255+2
  //1111 1111
  //+2后為10000 0001,char類型只有8位,最高位溢出,結果為1

  return 0;
}

 

11、字符串格式化輸出和輸入

(1)字符串常量

  • 字符串是內存中一段連續的char空間,以'\0'(數字0)結尾;
  • 字符串常量是由雙引號括起來的字符序列,如"Hello World","$12.5"等都是合法的字符串常量。

字符串常量與字符常量的區別:

每個字符串常量的結尾,編譯器會自動的添加一個結束標志位'\0',也就是"a"包含兩個字符'a'和'\0','\0'是字符串結束符。

(2)printf函數和putchar函數

printf函數用來輸出字符串,而putchar用來輸出一個字符。

printf格式字符:

打印格式 對應數據類型 含義
%d int 有符號的十進制整數
%hd short int 短整數
%hu unsigned short 無符號短整數
%o unsigned int 無符號八進制整數
%u unsigned int 無符號十進制整數
%x,%X unsigned int 無符號十六進制整數
%f float 單精度浮點數
%lf double 雙精度浮點數
%e,%E double 科學計數法表示的數
%c char 字符類型
%s char * 字符串類型
%p void * 以十六進制形輸出指針
%% % 輸出一個百分號

printf附加格式:

字符 含義
l字母(l) 附加在d,u,x,o前面,表示長整數
- 左對齊
m(代表一個整數) 數據最小寬度
0(數字0) 將輸出的前面補0直到占滿列寬為止
m.n(代表一個整數) m指定域寬,n指定精度,符點數小數位數

(3)scanf函數與getchar函數

  • getchar函數用來從標准輸入設備讀取一個char;
  • scanf通過%轉義的方式可以得到用戶通過標准輸入設備輸入的數據。

 

12、運算符和表達式

 

13、程序流程結構

 

14、數組和字符串

(1)概述

在程序設計中,為了方便處理數據,把具有相同類型的若干變量按有序形式組織起來的數據類型稱為數組,數組就是在內存中連續的相同類型的變量空間,所有成員在內存中的地址是連續的。

數組屬於構造數據類型:

  • 一個數組可以分解為多個數組元素,這些數組元素可以是基本的數據類型或者構造類型
int a[10];
struct student stu[10];
  • 按數組元素類型的不同,數組可以分為:數值數組、字符數組、指針數組、結構數組等
int a[10];
char str[10];
char *p[10];

通常情況下,數組元素下標的個數也稱為維數,根據維數的不同,可將數組分為一維數組、二維數組、三維數組等,將二維及以上的數組稱為多維數組。

(2)一維數組

(2.1)一維數組的定義和使用

  • 數組名符合標識符的書寫規定(數字、英文字母、下划線)
  • 數組名不能與其它變量名相同,同一作用域內是唯一的
  • 方括號[]中常量表達式表示數組元素的個數
  • 定義數組時[]內最好是常量,使用數組時[]內可以是常量,也可以是變量

簡單示例如下:

#include <stdio.h>

int main(int argc, char *argv[])
{
  int a[10];//定義數組a,有10個成員,每個成員都是int類型,a[0]~a[9]

  int i;
  for (i = 0; i < 10; i++)
    a[i] = i;  //數組元素賦值操作

  for (i = 0; i < 10; i++)
    printf("%d ", a[i]);
  printf("\r\n");

  return 0;
}

(2.2)一維數組的初始化

在定義數組的同時進行賦值,稱為初始化,如果全局數組不初始化的話,編譯器將其初始化為0,局部數組如果不初始化的話,變量將會是隨機值。

int a[10] = {1,2,3,4,5,6,7,8,9,10};//定義數組同時初始化所有成員變量
int b[10] = {1,2,3};//定義數組同時初始化前3個成員,剩余成員變量為0
int c[10] = {0};//定義數組同時初始化所有成員變量為0
int d[] = {1,2,3,4,5};//[]中不定義元素個數,定義時必須初始化,5個成員變量

(2.3)數組名

數組名是一個地址的常量,代表數組中首元素的地址。

#include <stdio.h>

int main(int argc, char *argv[])
{
  int a[10] = {0};

  printf("a = %p\r\n", a);//數組首地址
  printf("&a[0] = %p\r\n", &a[0]);//數組首元素地址

  return 0;
}

(3)二維數組

(3.1)二維數組的定義和使用

二維數組定義的一般形式是:

類型說明符 數組名[常量表達式1][常量表達式2]

其中,常量表達式1表示第一維下標的長度,常量表達式2表示第二維下標的長度。

定義一個二維數組:

int a[3][4];
  • 命令規則同一維數組
  • 定義了一個三行四列的數組,數組名為a,其元素類型為int,數組的元素個數為3*4個,如下:

二維數組a是按行進行存放的,先存放a[0]行,再存放a[1]行、a[2]行,並且每行有4個元素,也是依次存放的。

  • 二維數組在概念上二維的,其下標在兩個方向上變化,對其訪問一般需要兩個下標
  • 在內存中並不存在二維數組,二維數組實際的硬件存儲是連續編址,內存中只有一維數組,存儲第一行后,依次存儲第二行,與一維數組存儲方式是一樣的

簡單實例:

#include <stdio.h>

int main(int argc, char *argv[])
{
  int a[3][4];
  int val = 0;
  int i, j;

  for (i = 0; i < 3; i++)
  {
    for (j = 0; j < 4; j++)
      a[i][j] = val++;
  }
return 0; }

(3.2)二維數組的初始化

//分段賦值
int a[3][4] = {
    {1,2,3,4},
    {5,6,7,8},
    {9,10,11,12}
};

//連續賦值
int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};

//部分元素賦初值,未初始化的成員變量為0
int a[3][4] = {1,2,3,4};

//[]中不定義元素個數,定義時必須初始化
int a[][4] = {1,2,3,4,5,6,7,8};

(3.3)數組名

數組名是一個地址的常量,代表數組中首元素的地址。

#include <stdio.h>

int main(int argc, char *argv[])
{
    //定義一個二維數組,數組名為a
    //本質上還是一維數組,該一維數組有3個元素
    //每個元素又是一個一維數組int[4]
    unsigned int a[3][4] = {
        {1,2,3,4},
        {5,6,7,8},
        {9,10,11,12}
    };

    printf("addr: a = %p\r\n", a); //數組名為首元素地址
    printf("addr: a[0] = %p\r\n", a[0]); //第0個一維數組首地址
    printf("addr: a[1] = %p\r\n", a[1]); //第1個一維數組首地址
    printf("addr: a[2] = %p\r\n", a[2]); //第2個一維數組首地址
    printf("addr: a[0][0] = %p\r\n", &a[0][0]); //二維數組首個元素地址

    //int類型為4個字節
    printf("size: sizeof(a) = %lu\r\n", sizeof(a)); //二維數組所占空間,結果48個字節(3 * 4 * 4)
    printf("size: sizeof(a[0]) = %lu\r\n", sizeof(a[0])); //第0個一維數組所占空間,結果為16個字節(4 * 4)
    printf("size: sizeof(a[0][0]) = %lu\r\n", sizeof(a[0][0])); //二維數組第一個元素所占空間,結果為4個字節

    //求二維數組行數
    printf("n = %lu\r\n", sizeof(a) / sizeof(a[0]));

    //求二維數組列數
    printf("m = %lu\r\n", sizeof(a[0]) / sizeof(a[0][0]));

    //求二維數組行列總數
    printf("n*m = %lu\r\n", sizeof(a) / sizeof(a[0][0]));

    return 0;
}

(4)字符數組與字符串

(4.1)字符數組與字符串的區別

  • C語言中沒有字符串這種數據類型,可以使用char數組來替代
  • 字符串一定是一個char類型的數組,但char類型的數組未必是字符串
  • 數字0(和字符'\0'等價)結尾的char數組就是一個字符串,但如果char類型數組沒有以數字0結尾,那么就不是一個字符串,只是普通數組,所以字符串是一種特殊的char類型數組。
#include <stdio.h>

int main(int argc, char *argv[])
{
  char str1[] = {'c', ' ', 'p', 'r', 'o', 'g'}; //定義字符數組並初始化
  printf("str1 = %s\r\n", str1);  //亂碼,沒有'\0'結束符

  //以'\0'結束符結尾的字符數組是字符串
  char str2[] = {'c', ' ', 'p', 'r', 'o', 'g', '\0'};
  printf("str2 = %s\r\n", str2);  //輸出正常

  //字符串處理以'\0'作為結束符,后面的"hello"將不會輸出
  char str3[] = {'c', ' ', 'p', 'r', 'o', 'g', '\0', 'h', 'e', 'l', 'l', 'o', '\0'};
  printf("str3 = %s\r\n", str3);

  return 0;
}

(4.2)字符串的初始化

#include <stdio.h>

//C語言沒有字符串類型,通過字符數組模擬
//C語言字符串,以字符'\0'結束
int main(int argc, char *argv[])
{
  //不指定長度,沒有'\0'結束符,長度就是元素個數
  char buf1[] = {'a', 'b', 'c'};
  printf("buf[1] = %s\r\n", buf1);  //亂碼

  //指定長度,后面沒有賦初值的元素,自動補0
  char buf2[10] = {'a', 'b', 'c'};
  printf("buf[2] = %s\r\n", buf2);

  //字符數組所有元素賦值為0
  char buf3[10] = {0};
  printf("buf[3] = %s\r\n", buf3);

  //數組越界
  //char buf4[2] = {'a', 'b', 'c'};

  char buf5[10] = {'a', 'b', 'c', '\0', 'd'};
  printf("buf[5] = %s\r\n", buf5);

  char buf6[10] = {'a', 'b', 'c', 0, 'd'};
  printf("buf[6] = %s\r\n", buf5);

  //使用字符串初始化,編譯器自動在后面補0
  char buf7[10] = "abc";
  printf("buf[7] = %s\r\n", buf7);

  return 0;
}

 

15、函數

 

16、指針

(1)概述

(1.1)內存

內存的基本含義如下:

  • 存儲器:計算機組成中,用來存儲程序和數據,輔助CPU進行運算處理的重要部分
  • 內存:內部存儲器,暫存程序和數據,掉電會丟失,例如:SRAM、SDRAM、DDR等
  • 外存:外部存儲器,長時間保存程序和數據,掉電不會丟失,例如:FLASH、ROM、NAND、EMMC等

內存是溝通CPU和硬盤的橋梁:

  • 暫存CPU中的運算數據和執行程序
  • 暫存與硬盤等外部存儲器交換的數據

(1.2)物理存儲器和存儲地址空間

物理存儲器:實際存在的具體存儲器芯片。

  • 主板上的內存條
  • 顯卡上的RAM芯片
  • 各種適配卡上的RAM和ROM芯片

存儲地址空間:對存儲器編碼的范圍,軟件上常說的內存是指這一層含義。

  • 編碼:對每個物理存儲單元(一個字節)分配一個號碼
  • 尋址:可以根據分配的號碼找到相應的存儲單元,完成數據的讀寫

(1.3)內存地址

內存地址的含義:

  •  將內存抽象成一個很大的一維字符數組
  • 編碼就是對內存的每一個字節分配一個32位或64位的編號(與處理器的位數有關)
  • 這個內存編號就是內存地址

內存中的每個數據都會分配相應的內存地址:

  • char類型:占一個字節就會分配一個地址
  • int類型:占四個字節就會分配四個地址
  • float類型、struct類型、數組、函數等也會分配相應的地址。

(1.4)指針和指針變量

 指針和指針變量的含義:

  • 內存區的每個字節都有一個編號,這就是地址
  • 如果在程序中定義了一個變量,在對程序進行編譯或運行時,系統就會給這個變量分配內存單元,並且確定它的內存地址(編號)
  • 指針的實質就是內存地址,指針就是地址,地址就是指針
  • 指針是內存單元的編號,指針變量就是存放地址的變量
  • 通常我們敘述時會把指針變量簡稱為指針,實際指針和指針變量的含義並不一樣

(2)指針的基礎知識

(2.1)指針變量定定義和使用

  •  指針也是一種數據類型,指針變量也是一種變量
  • 指針變量指向誰,就把誰的地址賦值給指針變量
  • *操作符操作的是指針變量所指向的內存空間

簡單實例如下:

#include <stdio.h>

int main(int argc, char *argv[])
{
    int a = 100;
    char b = 'A';

    printf("Addr: &a = %p, &b = %p\r\n", &a, &b);//打印變量a,b的地址

    //int *代表一種數據類型,int*指針類型,p是變量名
    //定義一個指針類型的變量,可以指向一個int類型變量
    int *p;
    p = &a; //將變量a的地址賦值給指針變量p,p也是一個變量,值是內存地址編號
    printf("Addr: p = %p\r\n", p);  //輸出指針變量p的值
    printf("Value: *p = %d\r\n", *p);   //輸出指針變量p所指向的值

    char *p2;
    p2 = &b; //將變量b的地址賦值給指針變量p2
    printf("Addr: p2 = %p\r\n", p2);
    printf("Value: *p2 = %c\r\n", *p2);

    return 0;
}

注意:&可以取得一個變量在內存中的地址,但是不能夠獲取寄存器變量,因為寄存器變量不在內存里,而是在CPU內部。

(2.2)通過指針間接修改變量的值

簡單示例如下:

#include <stdio.h>

int main(int argc, char *argv[])
{
    int a = 100;
    int b = 200;
    int *p;

    printf("Init_Value: a = %d, b = %d\r\n", a, b);

    p = &a;
    *p = 300;
    printf("a = %d, *p = %d\r\n", a, *p);

    p = &b;
    *p = 400;
    printf("b = %d, *p = %d\r\n", b, *p);

    return 0;
}

(2.3)指針大小

  • 使用sizeof()能夠測量指針的大小
  • sizeof()測的是指針變量指向存儲地址的大小
  • 在32位平台,所有的指針(地址)都是32位(4字節)
  • 在64位平台,所有的指針(地址)都是64位(4字節)

簡單實例:

#include <stdio.h>

int main(int argc, char *argv[])
{
    int *p1 = NULL;
    int **p2 = NULL;
    char *p3 = NULL;
    char **p4 = NULL;

    printf("sizeof(p1) = %lu\r\n", sizeof(p1));
    printf("sizeof(p2) = %lu\r\n", sizeof(p2));
    printf("sizeof(p3) = %lu\r\n", sizeof(p3));
    printf("sizeof(p4) = %lu\r\n", sizeof(p4));

    return 0;
}

(2.4)野指針和空指針

指針變量也是變量,變量是可以任意賦值的,只要不越界即可,32位的為4字節,64位的為8字節,但是,任意數值賦值給指針變量沒有意義,因為這樣的指針就成為了野指針,該指針指向的區域是未知的,操作系統不允許操作該指針指向的內存區域,所以,野指針不會直接引發錯誤,操作野指針指向的內存區域才會出現錯誤。

簡單示例:

#include <stdio.h>

int main(int argc, char *argv[])
{
    int *p = NULL;
    int a = 100;

    p = a; //直接把a的值賦給p,指針變量p成為野指針
    p = 0x123456; //指針變量p成為野指針
    printf("[1]\r\n");

    *p = 1000;  //操作野指針指向的未知區域,內存出現問題
    printf("[2]\r\n");

    return 0;
}

野指針和有效指針變量保存的都是數值,為了標志此指針變量沒有指向任何變量,在C語言中,可以把NULL賦值給此指針,這樣就可以標志位空指針,NULL是一個值為0的宏常量。

#define NULL ((void *)0)

int *p = NULL;

(2.5)萬能指針void *

void *指針可以指向任意變量的內存空間:

#include <stdio.h>

int main(int argc, char *argv[])
{
    void *p = NULL;
    int a = 100;
    
    printf("Value: a = %d\r\n", a);
    p = (void *)&a; //獲取變量a的地址並強制轉換void *指針,賦值給p

    //使用指針變量指向的內存時,轉換為int *類型
    *((int *)p) = 1;
    printf("Value: a = %d\r\n", a);

    return 0;
}

(2.6)const修飾的指針變量

簡單實例:

#include <stdio.h>

int main(int argc, char *argv[])
{
    int a = 100;
    int b = 200;

    //指向常量的指針
    //修飾*,指針指向的內存區域不能修改,指針的指向可以改變
    const int *p1 = &a; //等價於int const *p1 = &a;
    //*p1 = 1000; //錯誤,指針指向的內存區域不能修改
    p1 = &b;    //正確,指針指向能夠修改

    //指針常量
    //修飾p2,指針指向不能改變,指針指向的內存區域可以修改
    int *const p2 = &a;
    //p2 = &b;  //錯誤,指針指向不能改變
    *p2 = 2000; //正確,指針指向的內存區域可以修改

    return 0;
}

在編寫程序的時候,指針作為函數參數時,如果不想修改指針指向內存區域的值,需要使用const修飾指針數據類型。

(3)指針和數組

(3.1)數組名

數組名字是數組的首元素地址,但它是一個地址常量:

#include <stdio.h>

int main(int argc, char *argv[])
{
    int a[] = {1, 2, 3, 4, 5};

    printf("a = %p\r\n", a);
    printf("&a[0] = %p\r\n", &a[0]);

    //a = 0x1234;   //錯誤,數組名是常量地址,不能修改

    return 0;
}

(3.2)指針操作數組元素

#include <stdio.h>

int main(int argc, char *argv[])
{
    int a[] = {1, 2, 3, 4, 5};
    int i;
    int n = sizeof(a) / sizeof(a[0]);

    printf("Addr: a = %p\r\n", a);
    for (i = 0; i < n; i++)
        printf("%d ", *(a + i));
    printf("\r\n");

    int *p = a; //定義指針變量p,用於保存數組a的首地址
    for (i = 0; i < n; i++)
        p[i] = p[i] + 1;
    
    printf("Addr: p = %p\r\n", p);
    for (i = 0; i < n; i++)
        printf("%d ", *(p + i));
    printf("\r\n");

    return 0;
}

(3.3)指針加減運算

(3.3.1)加法運算

  • 指針計算並不是簡單的整數相加
  • 如果是一個int *類型指針,+1的結果是增加一個int的大小
  • 如果是一個char *類型指針,+1的結果是增加一個char的大小

簡單實例如下:

#include <stdio.h>

int main(int argc, char *argv[])
{
    int a = 100;
    int *p = &a;

    printf("Addr: p = %p\r\n", p);
    p += 2; //移動了兩個int
    printf("Addr: p = %p\r\n", p);

    char b = 'A';
    char *p2 = &b;

    printf("Addr: p2 = %p\r\n", p2);
    p2 += 2; //移動了兩個char
    printf("Addr: p2 = %p\r\n", p2);

    return 0;
}

通過改變指針指向操作數組的元素:

#include <stdio.h>

int main(int argc, char *argv[])
{
    int a[] = {1, 2, 3, 4, 5};
    int i;
    int n = sizeof(a) / sizeof(a[0]);
    int *p = a;

    for (i = 0; i < n; i++)
    {
        printf("%d ", *p);
        p++;
    }
    printf("\r\n");

    return 0;
}

(3.3.2)減法運算

簡單實例1:

#include <stdio.h>

int main(int argc, char *argv[])
{
    int a[] = {1, 2, 3, 4, 5};
    int i;
    int n = sizeof(a) / sizeof(a[0]);
    int *p = a + n - 1;

    for (i = 0; i < n; i++)
    {
        printf("%d ", *p);
        p--;
    }
    printf("\r\n");

    return 0;
}

簡單實例2:

#include <stdio.h>

int main(int argc, char *argv[])
{
    int a[] = {1, 2, 3, 4, 5};
    int *p1 = &a[1]; //第1個元素地址
    int *p2 = &a[2]; //第2個元素地址

    printf("Addr: p1 = %p, p2 = %p\r\n", p1, p2);

    int n1 = p2 - p1;   //n1 = 1
    int n2 = (unsigned long long)p2 - (unsigned long long)p1; //n2 = 4
    printf("n1 = %d, n2 = %d\r\n", n1, n2);

    return 0;
}

(3.4)指針數組

指針數組,它是數組,數組的每個元素都是指針類型的。

簡單實例如下:

#include <stdio.h>

int main(int argc, char *argv[])
{
    int *p[3];
    int a = 1, b = 2, c = 3;
    int i;

    p[0] = &a;
    p[1] = &b;
    p[2] = &c;

    for (i = 0; i < 3; i++)
    {
        printf("Addr: p[%d] = %p,", i, p[i]);
        printf("Value: *p[%d] = %d\r\n", i, *p[i]);
    }

    return 0;
}

(4)多級指針

  • C語言中允許有多級指針的存在,在實際的程序中一級指針最常用,其次是二級指針
  • 二級指針就是指向一個一級指針變量地址的指針
  • 三級指針依次類推

簡單實例:

#include <stdio.h>

int main(int argc, char *argv[])
{
    int a = 100;
    int *p1;    //一級指針
    int **p2;   //二級指針
    int ***p3;  //三級指針

    //*p1就是變量a的值
    p1 = &a;
    printf("Addr: p1 = %p, Value = %d\r\n", p1, *p1);

    //*p2就是指針變量p1的值
    //**p2就是變量a的值
    p2 = &p1;
    printf("Addr: p2 = %p, *p2 = %p, Value = %d\r\n", p2, *p2, **p2);

    //*p3就是指針變量p2的值
    //**p3就是指針變量p1的值
    //***p3就是指針變量a的值
    p3 = &p2;
    printf("Addr: p3 = %p, *p3 = %p, **p3 = %p, Value = %d\r\n", p3, *p3, **p3, ***p3);

    return 0;
}

(5)指針和函數

(5.1)函數形參改變實參的值

簡單實例如下:

#include <stdio.h>

void swap1(int x, int y)
{
    int temp;

    temp = x;
    x = y;
    y = temp;

    printf("Addr: &x = %p, &y = %p\r\n", &x, &y);
    printf("Value: x = %d, y = %d\r\n", x, y);
}

void swap2(int *x, int *y)
{
    int temp;

    temp = *x;
    *x = *y;
    *y = temp;

    printf("Addr: x = %p, y = %p\r\n", x, y);
    printf("Value: *x = %d, *y = %d\r\n", *x, *y);
}

int main(int argc, char *argv[])
{
    int a = 1;
    int b = 2;

    printf("Addr: &a= %p, &b = %p\r\n", &a, &b);

    swap1(a, b); //值傳遞
    printf("[1]: a = %d, b = %d\r\n", a, b);

    a = 3;
    b = 4;
    swap2(&a, &b); //地址傳遞
    printf("[2]: a = %d, b = %d\r\n", a, b);

    return 0;
}

(5.2)數組名做函數參數

使用數組名做函數參數,函數的形參會退化為指針:

#include <stdio.h>

void print_arry(int *arry, int n)
{
    int i;

    for (i = 0; i < n; i++)
        printf("%d ", *(arry + i));
    printf("\r\n");
}

int main(int argc, char *argv[])
{
    int a[] = {1, 2, 3, 4, 5};
    int n = sizeof(a) / sizeof(a[0]);

    //數組名作為函數參數
    print_arry(a, n);

    return 0;
}

(5.3)指針作為函數的返回值

#include <stdio.h>

static int g_a = 100;

int *get_global_a_addr(void)
{
    return &g_a;
}

int main(int argc, char *argv[])
{
    int *p = NULL;
    
    p = get_global_a_addr();
    printf("Addr: p = %p\r\n", p);
    printf("Value: *p = %d\r\n", *p);

    *p = 1000;
    printf("Value: *p = %d, g_a = %d\r\n", *p, g_a);

    return 0;
}

(6)指針和字符串

(6.1)字符指針

#include <stdio.h>


int main(int argc, char *argv[])
{
    char string[] = "hello world";
    char *p = string;
    char *p2;

    printf("[1]: string = %s, p = %s\r\n", string, p);
    *p = 'H';
    p = p + 6;
    *p = 'W';
    p = string;
    printf("[2]: string = %s, p = %s\r\n", string, p);

    p2 = "Test String";
    printf("p2 = %s\r\n", p2);

    return 0;
}

(6.2)字符指針作為函數參數

#include <stdio.h>

void mystrcat(char *dest, const char *src)
{
    unsigned int len1 = 0, len2 = 0, i;

    while (dest[len1])
        len1++;
    
    while (src[len2])
        len2++;
    
    for (i = 0; i < len2; i++)
        dest[len1 + i] = src[i];
}

int main(int argc, char *argv[])
{
    char string1[100] = "Hello World";
    char string2[] = ",Test String.";

    mystrcat(string1, string2);
    printf("string1 = %s\r\n", string1);

    return 0;
}

(6.3)const修飾的指針變量

#include <stdio.h>

int main(int argc, char *argv[])
{
    //const修飾一個變量時為只讀
    const int a = 100;
    //a = 100   //錯誤,變量不能被修改

    //指針變量,指針指向的內存,兩個不同概念
    char string[] = "hello world";
    char buf[] = "Hello World";

    //從左往右看,跳過數據類型,看修飾哪個字符
    //如果是*,說明指針指向的內存不能被改變
    //如果是指針變量,說明指針的指向不能改變
    const char *p = string; //等價於char const *p = string;
    //p[0] = 'H';   //錯誤,指針指向的內存不能被改變
    printf("[1]: Addr->p = %p, Value->p = %s\r\n", p, p);
    
    p = buf;    //正確,指針的指向能夠改變
    printf("[2]: Addr->p = %p, Value->p = %s\r\n", p, p);

    char *const p2 = string; //指針的指向不能改變,指針指向的內存能改變
    p2[0] = 'H'; //正確
    p2[6] = 'W'; //正確
    //p2 = buf  //錯誤,指針的指向不能改變
    printf("[3] = Addr->p2 = %p, Value->p2 = %s\r\n", p2, p2);

    //p3為只讀,指針的指向和指針指向的內存都不能改變
    const char * const p3 = buf;
    //p3[0] = 'h'; //錯誤
    //p3 = string; //錯誤

    return 0;
}

(6.4)指針數組作為main函數的形參

int main(int argc, char *argv[])
  • main函數是由操作系統調用的,第一個參數標明argv數組的成員數量,第二個參數為指針數組,argv數組的每個成員都是char *類型
  • argv是命令行參數的字符串數組
  • argc是命令行參數的數量,程序名本身算一個參數

指針數組的簡單示例:

#include <stdio.h>

//argc為傳參數的個數(包含可執行程序)
//argv為指針數組,指向輸入的字符串參數
int main(int argc, char *argv[])
{
    //指針數組,它是數組,並且每個元素都是指針變量
    char *str[] = {"123", "456", "789"};
    int n = sizeof(str) / sizeof(str[0]);
    int i;

    for (i = 0; i < n; i++)
        printf("Addr: str[%d] = %p, Value: str[%d] = %s\r\n", i, str[i], i, str[i]);

    for (i = 0; i < argc; i++)
        printf("Addr: argv[%d] = %p, Value: argv[%d] = %s\r\n", i, argv[i], i, argv[i]);

    return 0;
}

(6.5)常用字符串應用模型

(6.5.1)strstr中的while和do-while模型

利用字符串操作的標准庫函數strstr找出一個字符串中子串出現的個數。

(a)while模型

#include <stdio.h>
#include <string.h>

unsigned int find_substr(const char *str, const char *substr)
{
    const char *temp = str;
    unsigned int n = 0;

    while ((temp = strstr(temp, substr)) != NULL)
    {
        n++;
        temp = temp + strlen(substr); //重新設置起點位置
        if (*temp == '\0') //判斷是否到結束位置
            break;
    }

    return n;
}

int main(int argc, char *argv[])
{
    char *str = "123abc456abc789abcqweabcrrr";
    char *substr = "abc";
    unsigned int n = 0;

    n = find_substr(str, substr);
    printf("n = %d\r\n", n);

    return 0;
}

(b)do-while模型

#include <stdio.h>
#include <string.h>

unsigned int find_substr(const char *str, const char *substr)
{
    const char *temp = str;
    unsigned int n = 0;

    do
    {
        temp = strstr(temp, substr);
        if (temp != NULL)
        {
            n++;
            temp = temp + strlen(substr); //重新設置查找起點
        }
        else
            break;
        
    } while (*temp != '\0');

    return n;
}

int main(int argc, char *argv[])
{
    char *str = "123abc456abc789abcqweabcrrr";
    char *substr = "abc";
    unsigned int n = 0;

    n = find_substr(str, substr);
    printf("n = %d\r\n", n);

    return 0;
}

(6.5.2)兩頭堵模型

求非空字符串元素的個數。

#include <stdio.h>
#include <string.h>

int test_fun(const char *str, int *n)
{
    if (!str || !n)
        return -1;
    
    int begin = 0;
    int end = strlen(str) - 1;

    //從左邊開始
    //如果當前字符為空,而且沒有結束
    while (str[begin] == ' ' && str[begin] != '\0')
        begin++;    //位置向右移動一位
    
    //從右往左移動
    while (str[end] == ' ' && end > 0)
        end--;
    if (end == 0)
        return -1;
    
    //非空元素個數
    *n = end - begin + 1;

    return 0;
}

int main(int argc, char *argv[])
{
    const char *str = "    123456     ";
    int n = 0;

    test_fun(str, &n);
    printf("n = %d\r\n", n);

    return 0;
}

(6.5.3)字符串反轉模型(逆置)

簡單實例如下:

#include <stdio.h>
#include <string.h>

int str_inverse(char *string, int len)
{
    if (!string)
        return -1;
    
    char *p1 = string;
    char *p2 = string + len - 1;
    char temp;
    int n, i;

    if ((len % 2) == 0)
        n = len / 2;
    else
        n = (len - 1) / 2;
    
    for (i = 0; i < n; i++)
    {
        temp = *p1;
        *p1 = *p2;
        *p2 = temp;
        p1++;
        p2--;
    }

    return 0;
}

int main(int argc, char *argv[])
{
    char string[] = "123456789";
    int len = strlen(string);

    printf("[1]: string = %s\r\n", string);
    str_inverse(string, len);
    printf("[2]: string = %s\r\n", string);

    return 0;
}

(6.6)常見字符串處理函數

(6.6.1)strcpy函數

#include <string.h>

char *strcpy(char *dest, const char *src);
功能:
    將src所指向的字符串復制到dest所指向的空間中,'\0'也會拷貝過去
參數:
    dest: 目的字符串首地址
    src: 源字符串首地址
返回值:
    成功:返回dest字符串首地址
    失敗:NULL
注意:
    如果參數dest所指向的內存空間不夠大,可能造成緩沖溢出錯誤情況

簡單實例代碼如下:

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char str1[25] = {0};
    char str2[20] = "Hello World";
    char *temp = NULL;

    printf("[1]: str1 = %s, str2 = %s\r\n", str1, str2);
    temp = strcpy(str1, str2);
    if (!temp)
        return -1;

    printf("[2]: str1 = %s, temp = %s\r\n", str1, temp);

    return 0;
}

(6.6.2)strncpy函數

#include <string.h>

char *strncpy(char *dest, const char *src, size_t n);
功能:
    將src所指向的字符串的前n個字符復制到dest所指向的空間中,
    是否拷貝'\0'取決於拷貝的字符長度是否包含
參數:
    dest: 目的字符串首地址
    src: 源字符串首地址
    n: 指定需要拷貝字符串個數
返回值:
    成功:返回dest字符串首地址
    失敗:NULL

簡單實例代碼如下:

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char str1[25] = {0};
    char str2[20] = "Hello World";
    char *temp = NULL;

    printf("[1]: str1 = %s, str2 = %s\r\n", str1, str2);
    temp = strncpy(str1, str2, sizeof(str1));
    if (!temp)
        return -1;

    printf("[2]: str1 = %s, temp = %s\r\n", str1, temp);

    return 0;
}

(6.6.3)strcat函數

#include <string.h>

char *strcat(char *dest, const char *src);
功能:
    將src所指向的字符串連接到dest字符串的尾部,'\0'也會追加
參數:
    dest: 目的字符串首地址
    src: 源字符串首地址
返回值:
    成功:返回dest字符串的首地址
    失敗:NULL

簡單示例如下:

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char str1[25] = "123456";
    char str2[20] = "Hello World";
    char *temp = NULL;

    printf("[1]: str1 = %s, str2 = %s\r\n", str1, str2);
    temp = strcat(str1, str2);
    if (!temp)
        return -1;

    printf("[2]: str1 = %s, temp = %s\r\n", str1, temp);

    return 0;
}

(6.6.4)strncat函數

#include <string.h>

char *strncat(char *dest, const char *src, size_t n);
功能:
    將src所指向的字符串的前n個字符連接到dest字符串的尾部,
    '\0'是否追加取決於長度中是否包含
參數:
    dest: 目的字符串首地址
    src: 源字符串首地址
    n: 指定需要追加的字符個數
返回值:
    成功:返回dest字符串的首地址
    失敗:NULL

簡單實例如下:

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char str1[25] = "123456";
    char str2[20] = "Hello World";
    char *temp = NULL;

    printf("[1]: str1 = %s, str2 = %s\r\n", str1, str2);
    temp = strncat(str1, str2, 5);
    if (!temp)
        return -1;

    printf("[2]: str1 = %s, temp = %s\r\n", str1, temp);

    return 0;
}

(6.6.5)strcmp函數

#include <string.h>

int strcmp(const char *s1, const char *s2);
功能:
    比較s1字符串和s2字符串的大小,比較的是字符ASCII碼的大小
參數:
    s1: 字符串1首地址
    s2:字符串2首地址
返回值:
    相等:0
    大於:> 0
    小於:< 0

簡答示例代碼如下:

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char str1[25] = "Hello World";
    char str2[25] = "Hello world";
    int n = 0;

    n = strcmp(str1, str2);
    if (n == 0)
        printf("str1 == str2\r\n");
    else if (n > 0)
        printf("str1 > str2\r\n");
    else
        printf("str1 < str2\r\n");

    return 0;
}

(6.6.6)strncmp函數

#include <string.h>

int strncmp(const char *s1, const char *s2, size_t n);
功能:
    比較s1字符串和s2字符串的前n個字符大小,比較的是字符ASCII碼的大小
參數:
    s1: 字符串1首地址
    s2:字符串2首地址
    n: 指定比較的字符個數
返回值:
    相等:0
    大於:> 0
    小於:< 0

簡單示例代碼如下:

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char str1[25] = "Hello World";
    char str2[25] = "Hello world";
    int n = 0;

    n = strncmp(str1, str2, 6);
    if (n == 0)
        printf("str1 == str2\r\n");
    else if (n > 0)
        printf("str1 > str2\r\n");
    else
        printf("str1 < str2\r\n");

    return 0;
}

(6.6.7)sprintf函數

#include <stdio.h>

int sprintf(char *str, const char *format, ...);
功能:
    根據參數format字符串來轉換並格式化數據,然后將結果輸出到str指定
    的空間中,直到出現字符串結束符'\0'為止
參數:
    str: 結果輸出緩沖區首地址
    format: 字符串格式,用法和printf一樣
返回值:
    成功:實際格式化的字符個數
    失敗:-1

簡單示例代碼如下:

#include <stdio.h>

int main(int argc, char *argv[])
{
    char str[100] = {0};
    int a = 100;
    char buf[] = "Hello World";
    int ret = 0;

    printf("a = %d, buf = %s\r\n", a, buf);
    ret = sprintf(str, "a:%d,buf:%s", a, buf);
    printf("ret = %d, str = %s\r\n", ret, str);

    return 0;
}

(6.6.8)sscanf函數

#include <stdio.h>

int sscanf(const char *str, const char *format, ...);
功能:
    從str指定的字符串讀取數據,並根據參數format字符來轉換並格式化
    數據
參數:
    str: 指定的字符串首地址
    format: 字符串格式,用戶和scanf一樣
返回值:
    成功:參數數目,成功轉換的值的個數
    失敗:-1

簡單示例代碼如下:

#include <stdio.h>

int main(int argc, char *argv[])
{
    char str[] = "a = 10, b = 20";
    int a, b;
    int ret = 0;

    ret = sscanf(str, "a = %d, b = %d", &a, &b);
    printf("ret = %d, a = %d, b = %d\r\n", ret, a, b);

    return 0;
}

(6.6.9)strchr函數

#include <string.h>

char *strchr(const char *str, int c);
功能:
    在字符串str中查找字符c出現的位置
參數:
    str: 要查找的字符串首地址
    c: 要查找的字符
返回值:
    成功:返回第一次出現字符c的地址
    失敗:NULL

簡單示例代碼如下:

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char str[] = "Hello World";
    char *p;

    p = strchr(str, 'W');
    printf("str = %s, p = %s\r\n", str, p);

    return 0;
}

(6.6.10)strstr函數

#include <string.h>

char *strstr(const char *haystack, const char *needle);
功能:
    在字符串haystack中查找字符串needle出現的位置
參數:
    haystack: 源字符串首地址
    needle: 匹配字符串首地址
返回值:
    成功:返回第一次出現needle地址
    失敗:NULL

簡單示例代碼如下:

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char str[] = "123abc456abc789";
    char substr[] = "abc";
    char *p;

    p = strstr(str, substr);
    printf("str = %s, p = %s\r\n", str, p);

    return 0;
}

(6.6.11)strtok函數

#include <string.h>

char *strtok(char *str, const char *delim);
功能:
    將字符串分割成一個個片段,當strtok()在參數str中發現參數delim中
    包含的分割字符時,則會將該字符改成'\0'字符,當連續出現多個時只
    替換第一個為'\0'
參數:
    str: 指向要分割的字符串
    delim: 分割字符串中包含的所有字符
返回值:
    成功:分割后字符串首地址
    失敗:NULL
注意:
    在第一次調用時,strtok()必須給予參數str字符串
    在往后的調用則將參數str設置成NULL,每次調用成功返回指向被分割片段
    的指針

簡單示例代碼如下:

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char str[] = "123s*abcgggd*5454ssg$yui";
    char *p = strtok(str, "*");

    while (p != NULL)
    {
        printf("p = %s\r\n", p);
        p = strtok(NULL, "*");
    }
}

(6.6.12)atoi函數

#include <stdlib.h>

int atoi(const char *nptr);
功能:
    字符串轉int類型,掃描nptr字符串,跳過前面的空格字符,直到遇到數字或正負號才開始
    轉換,而遇到非數字或字符串結束符'\0'才結束轉換,並將結果返回
參數:
    nptr: 要轉換的字符串
返回值:
    成功轉換后返回整數

類似的函數有:
    atof(): 將一個小數形式的字符串轉換為一個浮點數
    atol(): 將一個字符串轉換為long類型

簡單示例代碼如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    char str[] = "  -10";
    int num = 0;

    num = atoi(str);
    printf("num = %d\r\n", num);

    return 0;
}

(6.7)指針小結

定義 說明
int i; 定義整型變量
int *p; 定義一個指向int類型的指針變量
int a[10]; 定義一個有10個元素的數組,每個元素類型為int
int *p[10]; 定義一個有10個元素的數組,每個元素類型為int *
int func(); 定義一個函數,函數返回值為int類型
int *func(); 定義一個函數,函數返回值為int *類型
int **p; 定義一個指向int的指針的指針,二級指針

 

17、內存管理

(1)作用域

C語言變量的作用域分為:

  • 代碼塊作用域(代碼塊是{}之間的一段代碼)
  • 函數作用域
  • 文件作用域

(1.1)局部變量

局部變量也叫auto自動變量(auto可寫可不寫),一般情況下代碼塊{}內部定義的變量都是自動變量,它有如下特點:

  • 在一個函數內定義,只在函數范圍內有效
  • 在復合語句中定義,只在復合語句中有效
  • 隨着函數調用的結束或復合語句的結束,局部變量的聲明周期也結束
  • 如果沒有賦初值,變量的值是隨機的

簡單代碼示例:

#include <stdio.h>

void test(void)
{
  //auto寫不寫是一樣的
  //auto只能出現在{}內部
  auto int a = 10; //定義局部變量a並賦初值10
}

int main(int argc, char *argv[])
{
  //a = 100; //錯誤,main作用域內沒有變量a

  if (1)
  {
    //在復合語句中定義,變量在復合語句中有效
    int a = 100;
    printf("a = %d\r\n", a);
  }

  //a = 10; //錯誤,離開復合語句后,變量a已經不存在

  return 0;
}

(1.2)靜態(static)局部變量

  • static局部變量的作用域也是在定義的函數內有效
  • static局部變量的生命周期和程序運行周期一樣,同時static局部變量的值只初始化一次,但可以賦值多次
  • static局部變量若未賦初值,則由系統自動賦值,數值型變量自動賦值為0,字符型變量賦空字符
#include <stdio.h>

void fun1(void)
{
  int a = 0;
  
  a++;
  printf("fun1: a = %d\r\n", a);
}

void fun2(void)
{
  //靜態局部變量,沒有賦初值,系統初始化為0,只初始化一次
  static int a;

  a++;
  printf("fun2: a = %d\r\n", a);
}

int main(int argc, char *argv[])
{
  fun1();
  fun1();
  fun2();
  fun2();

  return 0;
}

(1.3)全局變量

  • 在函數外定義,可被文件及其它文件中的函數所共用,若其它文件中的函數調用此變量,需要使用extern聲明
  • 全局變量的生命周期和程序運行周期一樣
  • 不同文件的全局變量不可重名

(1.4)靜態(static)全局變量

  • 在函數外定義,作用范圍被限制在所定義的文件中
  • 不同文件靜態全局變量可以重名,但作用域不沖突
  • static全局變量的生命周期和程序運行周期一樣,同時static全局變量的值只初始化一次

(1.5)extern全局變量聲明

extern int a;聲明一個變量,這個全局變量在別的文件中已經定義,這里只是聲明,不是定義。

(1.6)全局函數和靜態函數

在C語言中函數默認是全局的,使用關鍵字static可以將函數聲明為靜態,函數定義為static就意味着這個函數只能在定義在這個函數的文件中使用,在其它文件中不能被調用,即使在其他文件中聲明這個函數都沒用。

對於不同文件中的static函數名字可以相同。

注意:

  • 允許在不同的函數中使用相同的變量名,它們代表不同的對象,分配不同的內存單元,互不干擾
  • 同一個源文件中,允許全局變量和局部變量同名,在局部變量的作用域內,全局變量不起作用
  • 所有的函數默認都是全局的,意味着所有的函數都不能重名,但如果是static函數,那么作用域是文件內的,所有不同的文件static函數名是可以相同的

(1.7)小結

類型 作用域 生命周期
auto變量 一對{}內 當前函數
static局部變量 一對{}內 整個程序運行期
extern變量 整個程序 整個程序運行期
static全局變量 當前文件 整個程序運行期
extern函數 整個程序 整個程序運行期
static函數 當前文件 整個程序運行期
register變量 一對{}內 當前程序
全局變量 整個程序 整個程序運行期

(2)內存布局

(2.1)內存分區

C源代碼經過預處理、編譯、匯編、鏈接4步后將生成一個可執行程序,在Windows系統下,程序是一個普通的可執行文件,以下簡單列出一個二進制可執行文件的基本情況:

在上面可以得知,在沒有運行程序前,也就是程序沒有加載到內存前,可執行程序內部已經分好3段信息,分別為代碼區(text)、數據區(data)、和未初始化數據區(bss)這3個部分,有的人直接把data和bss合起來叫做靜態區和全局區。

  • 代碼區(text段)

用來存放CPU執行的機器指令,通常代碼區是可以共享的,也就是另外的執行程序可以調用它,使其可共享的目的是對於頻繁被執行的程序,只需要在內存中有一份代碼即可,代碼區通常是只讀的,原因是防止程序意外地修改了它的指令,另外代碼區還規划了局部變量的相關信息。

  • 全局初始化數據區/靜態數據區(data段)

該區包含了在程序中明確被初始化的全局變量,已經初始化的靜態變量,包括靜態全局變量和靜態局部變量,還有常量數據,例如字符串常量。

  • 未初始化數據區(bss段)

該區存入的是全局未初始化變量和未初始化的靜態變量,未初始化數據區的數據在程序開始執行之前會被內核初始化為0或者NULL。

程序在加載到內存前,代碼區和全局區(data段和bss段)的大小就是固定的,程序在運行期間不能改變,當運行可執行程序后,系統把程序加載到內存,除前面分析到的text段、data段、bss段之外,額外增加棧區、堆區。

  • 代碼區(text段)

加載的是可執行文件代碼段,所有的可執行代碼都加載到代碼區,這塊內存在程序運行期間不能夠被修改。

  • 未初始化數據區(bss段)

加載的是可執行文件bss段,位置可以分開也可以緊靠data段,存儲全局未初始化和靜態未初始化數據,數據的生存周期為整個程序運行過程。

  • 全局初始化數據區/靜態數據區(data段)

加載的是可執行文件的data段,存儲全局初始化和靜態初始化數據,字符常量,數據的生存周期為整個程序運行過程。

  • 棧區(stack)

棧是一種先進后出的內存結構,由編譯器自動分配釋放,存放函數的參數值、返回值、局部變量等,在程序運行過程中實時加載和釋放,因此,局部變量的生存周期為申請到釋放該段棧空間。

  • 堆區(heap)

堆是一個大容器,它的容量要遠遠大於棧,但是沒有棧那樣先進后廚順序,用於動態內存分配,堆區在內存中位於bss段和棧區之間,一般是由程序員分配和釋放,若程序員不釋放,程序結束時由操作系統回收。

(2.2)存儲類型小結

類型 作用域 生命周期 存儲位置
auto變量 一對{}內 當前函數 棧區
static局部變量 一對{}內 整個程序運行期 初始化在data段,未初始化在bss段
extern變量 整個程序 整個程序運行期 初始化在data段,未初始化在bss段
static全局變量 當前文件 整個程序運行期 初始化在data段,未初始化在bss段
extern函數 整個程序 整個程序運行期 代碼區
static函數 當前文件 整個程序運行期 代碼區
register變量 一對{}內 當前函數 運行時存儲在CPU寄存器
字符串常量 當前文件 整個程序運行期 data段

簡單實例代碼如下:

#include <stdio.h>
#include <stdlib.h>

int g_a;
static int g_b;
int g_c = 100;
static int g_d = 1000;

int main(int argc, char *argv[])
{
  int a;
  int b = 100;
  static int c;
  static int d = 100;
  char *i = "Hello World";
  char *k = (char *)malloc(100);

  printf("&a = %p\r\n", &a);  //局部未初始化變量(棧區)
  printf("&b = %p\r\n", &b);  //局部初始化變量(棧區)

  printf("&c = %p\r\n", &c);  //靜態局部未初始化變量(bss段)
  printf("&d = %p\r\n", &d);  //靜態局部初始化變量(data段)

  printf("&g_a = %p\r\n", &g_a);  //全局未初始化變量(bss段)
  printf("&g_b = %p\r\n", &g_b);  //靜態全局未初始化變量(bss段)

  printf("&g_c = %p\r\n", &g_c);  //全局初始化變量(data段)
  printf("&g_d = %p\r\n", &g_d);  //靜態全局初始化變量(data段)

  printf("i = %p\r\n", i);    //只讀數據(常量區)
  printf("k = %p\r\n", k);    //動態分配內存(堆區)

  free(k);
  return 0;
}

(2.3)內存操作函數

(2.3.1)memset函數

#include <string.h>

void *memset(void *s, int c, size_t n);
功能:
    將內存區域s的前n個字節以參數c進行填入
參數:
    s: 要操作的內存區域首地址
    c: 填充的字符,c的參數int類型,但必須是unsigned char類型的范圍0~255
    n: 指定要填充的字節個數
返回值:
    內存區域s的首地址

簡單實例代碼如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    char buf[10];

    memset(buf, 'A', sizeof(buf));

    for (int i = 0; i < sizeof(buf); i++)
        printf("buf[%d] = %c\r\n", i, buf[i]);

    return 0;
}

(2.3.2)memcpy函數

#include <string.h>

void *memcpy(void *dest, const void *src, size_t n);
功能:
    拷貝內存區域src的前n個字節到內存區域dest中
參數:
    dest: 目的內存首地址
    src: 源內存首地址
    n: 需要拷貝的字節數
返回值:
    內存區域dest首地址

簡單實例代碼如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    char buf[50] = "Hello World";
    char str[50] = {0};

    printf("[1]: str = %s\r\n", str);
    memcpy(str, buf, strlen(buf));
    printf("[2]: str = %s\r\n", str);

    return 0;
}

(2.3.3)memmove函數

#include <string.h>

void *memmove(void *dest, const void *src, size_t n);
功能:
    拷貝內存區域src的前n個字節到內存區域dest中
參數:
    dest: 目的內存首地址
    src: 源內存首地址
    n: 需要拷貝的字節數
返回值:
    內存區域dest首地址

注意:memmove()函數的用法和memcpy()函數的用法一樣,區別在於,dest和src所指向的內存空間重疊時,memmove()函數仍然能處理,不過執行效率比memcpy()函數低些。

簡單實例代碼如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    char buf[50] = "Hello World";
    char str[50] = {0};

    printf("[1]: str = %s\r\n", str);
    memmove(str, buf, strlen(buf));
    printf("[2]: str = %s\r\n", str);

    return 0;
}

(2.3.4)memcmp函數

#include <string.h>

int memcmp(const void *s1, const void *s2, size_t n);
功能:
    比較s1和s2所指向內存區域的前n個字節
參數:
    s1: 內存首地址s1
    s2: 內存首地址s2
    n: 需要比較的前n個字節
返回值:
    相等:= 0
    大於:> 0
    小於:< 0

簡單實例代碼如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    char str1[50] = "Hello World";
    char str2[50] = "Hello world";
    int ret;

    ret = memcmp(str1, str2, 25);
    if (ret == 0)
        printf("str1 == str2\r\n");
    else if (ret > 0)
        printf("str1 > str2\r\n");
    else
        printf("str1 < str2\r\n");

    return 0;
}

(2.4)堆區內存分配和釋放

(2.4.1)malloc函數

#include <stdlib.h>

void *malloc(size_t size);
功能:
    在內存的動態存儲區(堆區)中分配一塊長度為size字節的連續區域,用來存放
    類型說明符指定的類型,分配的內存空間內容不確定,一般使用memset函數初始化
參數:
    size: 需要分配的內存大小(字節單位)
返回值:
    成功:分配的內存空間起始地址
    失敗:NULL

簡單實例代碼如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    char *buf = NULL;

    buf = malloc(50);   //在堆區申請50個字節內存區域
    if (!buf)
    {
        printf("malloc buf failed\r\n");
        return -1;
    }

    memset(buf, 0, 50);
    printf("[1]: buf = %s\r\n", buf);
    memcpy(buf, "Hello World", strlen("Hello World"));
    printf("[2]: buf = %s\r\n", buf);

    free(buf);  //釋放已經申請成功的內存區域

    return 0;
}

(2.4.2)free函數

#include <stdlib.h>

void free(void *ptr);
功能:
    釋放ptr所指向的一塊內存空間,ptr是一個任意類型的指針變量,指向被釋放
    區域的首地址,對同一內存空間多次釋放會出錯
參數:
    ptr: 需要釋放空間的首地址,被釋放區應是malloc函數分配的內存區域
返回值:
    無

(3)內存分區代碼分析

(3.1)返回棧區地址

簡單實例代碼如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int *fun(void)
{
    int a = 10;
    return &a;  //函數調用完畢,a變量釋放
}

int main(int argc, char *argv[])
{
    int *p = NULL;

    p = fun();
    //*p = 100;   //錯誤,操作野指針指向的內存

    return 0;
}

(3.2)返回data區地址

簡單實例代碼如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int *fun(void)
{
    static int a = 10;
    return &a;  //函數調用完畢,靜態變量a不釋放
}

int main(int argc, char *argv[])
{
    int *p = NULL;

    p = fun();
    *p = 100;
    printf("*p = %d\r\n", *p);

    return 0;
}

(3.3)值傳遞1

簡單實例代碼如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void fun(int *temp)
{
    temp = (int *)malloc(sizeof(int));

    *temp = 100;
}

int main(int argc, char *argv[])
{
    int *p = NULL;

    fun(p); //值傳遞,形參修改不會影響實參
    //printf("*p = %d\r\n", *p);  //錯誤,操作空指針指向的內存

    return 0;
}

(3.4)值傳遞2

簡單實例代碼如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void fun(int *temp)
{
    *temp = 100;
}

int main(int argc, char *argv[])
{
    int *p = NULL;

    p = (int *)malloc(sizeof(int));
    fun(p); //值傳遞
    printf("*p = %d\r\n", *p);  //正確,*p的值為100

    return 0;
}

(3.5)返回堆區地址

簡單實例代碼如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int *fun(void)
{
    int *temp = NULL;

    temp = (int *)malloc(sizeof(int));
    *temp = 100;
    return temp;  //返回堆區地址,函數調用完畢,內存不釋放
}

int main(int argc, char *argv[])
{
    int *p = NULL;

    p = fun();
    printf("*p = %d\r\n", *p);

    //堆區空間,使用完畢后需要手動釋放
    if (p != NULL)
    {
        free(p);
        p = NULL;
    }

    return 0;
}

18、復合類型(自定義類型)

(1)結構體

(2)共用體(聯合體)

(3)枚舉

(4)typedef自定義類型

19、文件操作

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM