C語言知識點整合(已完結)

一直以來都是零零散散的在記錄學習C語言的知識點,爲了自己之後可以更好查找現將所學習的所有知識點進行一個整合,方便自己,也適合學習C語言的同學,如有紕漏,歡迎指出,有則改之無則加勉。

文章目錄

第壹部分:C語言的基礎知識

一:什麼是C語言程序設計

1. 頭文件的介紹

  1. 頭文件:
    一般而言,每個C++/C程序通常由頭文件和定義文件組成。頭文件作爲一種包含功能函數、數據接口聲明的載體文件,主要用於保存程序的聲明,而定義文件用於保存程序的實現。
  2. #include <stdio.h>#include "stdio.h"有什麼區別?
    #include < >會在系統目錄中查找頭文件,而#include “ ”先在當前項目目錄之中查找,然後再去系統目錄中查找,這樣的做法也是防止頭文件被重複包含
  3. 所以一般來說,如果使用的包含標準庫頭文件,就使用 <>,包含着自己的頭文件的話就使用 “ ” 。

2. 馮諾依曼體系

馮諾依曼體系

二:數據類型

1. 每種數據類型大小

不同的系統所對應的數據類型大小是不盡相同的,不同的系統(32位,64位)所對應的數據類型的大小圖。
C語言中各類型數據的長度
對於這些數據類型的大小我們儘可能自己記住,避免後期編程的時候出現問題。
在這裏插入圖片描述
小提示unsigned無符號類型只能夠表示正數。

short signed int= signed short int 這兩者是相等的

2. 主要數據的範圍

    類型       無符號(unsigned)           有符號(signedchar          0~+255                    -128~+127
    
    short        0~65535                  -32768~32768
    
    int        0~4294967295            -2147483648~+2147483648(21)
    

例如:char的取值爲0-255 則如果是它的數組,所能夠容納的值就只有256個

char的值直接決定了

3. 浮點數

float 和double

  1. 其兩者內存空間是有限的
  2. 浮點數在內存中的存儲很大可能是不精確的,因此在寫代碼的時候不能夠直接拿兩個浮點數進行==的比較,應該做差,看其差值是否低於我們所制定的一個誤差範圍!
  3. 浮點數的十進制5.0 寫成二進制的話則爲101.0 ,相當於1.01x2^2; 根據IEEE的規定,其中s=0,M=1.01,E=2
    在這裏插入圖片描述
  4. int和folat內存差異
  5. float的指數取值範圍是-127~+128,取值範圍是-3.4E+38,3.4E+38.它的指數爲位有8位,尾數爲位有32位。

4. 不同類型之間的轉換

4.1 兩種不同的類型轉換

當操作數的類型不同,而且不屬於基本數據類型時,經常需要將操作數轉化爲所需要的類型,這個過程即爲強制類型轉換。強制類型轉換具有兩種形式:顯式強制轉換和隱式強制類型轉換。
4.1.1顯式強制類型轉換:通過一定語句和相關的前綴,將所給出的類型強制轉換成爲我們所需要的類型。

TYPE b = (TYPE) a;

其中,TYPE爲類型描述符,如int,float等。經強制類型轉換運算符運算後,返回一個具有TYPE類型的數值,這種強制類型轉換操作並不改變操作數本身,運算後操作數本身未改變。
舉例1

int n=0xab65char a=char)n;

上述強制類型轉換的結果是將整型值0xab65的高端一個字節刪掉,將低端一個字節的內容作爲char型數值賦值給變量a,而經過類型轉換後n的值並未改變。
4.1.2隱式強制類型轉換:當算術運算符兩側的操作數類型不匹配的時候,會觸發“隱式的轉換”,先轉換成相同的類型,之後再進行計算。
C語言在以下四種情況下會進行隱式轉換:
1、算術運算式中,低類型能夠轉換爲高類型。
2、賦值表達式中,右邊表達式的值自動隱式轉換爲左邊變量的類型,並賦值給他。
3、函數調用中參數傳遞時,系統隱式地將實參轉換爲形參的類型後,賦給形參。
4、函數有返回值時,系統將隱式地將返回表達式類型轉換爲返回值類型,賦值給調用函數。

4.2 自動類型轉換

在C語言中,自動類型轉換遵循以下規則:
1、若參與運算量的類型不同,則先轉換成同一類型,然後進行運算。
2、轉換按數據長度增加的方向進行,以保證精度不降低。如int型和long型運算時,先把int量轉成long型後再進行運算。
a、若兩種類型的字節數不同,轉換成字節數高的類型
b、若兩種類型的字節數相同,且一種有符號,一種無符號,則轉換成無符號類型
3、所有的浮點運算都是以雙精度進行的,即使僅含float單精度量運算的表達式,也要先轉換成double型,再作運算。
4、char型和short型(在visual c++等環境下)參與運算時,必須先轉換成int型。
5、在賦值運算中,賦值號兩邊量的數據類型不同時,賦值號右邊量的類型將轉換爲左邊量的類型。如果右邊量的數據類型長度比左邊長時,將丟失一部分數據,這樣會降低精度,丟失的部分直接捨去。
一些需要注意的東西:
類型轉換的規則
類型轉換階層圖
在強制類型轉換的時候需要注意的是,如(double)a,則可能會丟失掉原有的精度。
小技巧:想要實現運算之後的四捨五入計算,只需要給一個數加上0.5就可以直接完成。
注意!
例題1 .其中一個需要注意的問題就是對於intunsigned int轉變的一個過程,也是寫了一個相關的代碼爲大家解釋一下:

int a = -20;
unsigned int b = 10;
printf("%d\n", a + b);

此過程涉及到了兩次的轉變,a先從int類型轉變爲unsigne int類型,之後相加之後再轉變爲 int類型,所以大家不妨自己計算一下,看看和自己所想的答案是否一致!
其中unsigned int不能表示小於0的,所以是恆大於0,則是永遠成立的

unsigned int i;
	for (i = 9; i >= 0; i--) {
		printf("%u\n", i);// %u無符號十進制表示
	}
	因此此程序是一個無限循環的程序

例題2 .打印一個無符號的整型

char a=-128;
printf("%u",a);

轉換步驟
例題3. 無符號0減去有符號-1

unsigned int i;
i-1 => i+(-1) 

轉換步驟

5. 整形截斷和整型提升

5.1 整型截斷

把四個字節的變量賦值給一個字節的變量,會存在截斷,則對應的截取它低位上的值:

四字節變量
int 0x11111111 11111111 11111111 11111111;

截斷後

一字節變量
char 0x1111 1111;

5.2 整形的提升和意義

C的整型算術運算總是以缺省整型類型的精度來進行的,爲了獲得這個精度,表達式中的字符和短整型操作數在使用之前被轉換爲普通整型,這種轉換稱爲整型提升。
整型提升的意義
表達式的整型運算要在CPU的相應運算器件內執行,CPU內整型運算器(ALU)的操作數的字節長度一般就是int的字節長度,同時也是CPU的通用寄存器的長度。
因此,即使兩個char類型的相加,在CPU執行時實際上也要先轉換爲CPU內整型操作數的標準長度。
通用CPU(general-purpose CPU)是難以直接實現兩個8比特字節的直接相加運算(雖然機器指令中可能有這種字節相加指令)。所以,表達式中各種長度可能小於int長度的整型值,都必須先轉換爲int或unsigned int,然後才能送入CPU去執行運算。
如:

char a,b,c;
...
a = b + c;
b和c的值被提升爲普通整型,然後再執行加法運算。
加法運算完成之後,結果將被截斷,然後再存儲於a中

5.3 如何進行整型提升

整形提升是按照變量的數據類型的符號位來提升的:

/負數的整形提升
char c1 = -1;
變量c1的二進制位(補碼)中只有8個比特位:
1111111
因爲 char 爲有符號的 char
所以整形提升的時候,高位補充符號位,即爲1
提升之後的結果是:
11111111111111111111111111111111

/正數的整形提升
char c2 = 1;
變量c2的二進制位(補碼)中只有8個比特位:
00000001
因爲 char 爲有符號的 char
所以整形提升的時候,高位補充符號位,即爲0
提升之後的結果是:
00000000000000000000000000000001

/無符號整形提升,高位直接補0

例1

int main()
{
char a = 0xb6;
short b = 0xb600;
int c = 0xb6000000;
if(a==0xb6)
printf("a");
if(b==0xb600)
printf("b");
if(c==0xb6000000)
printf("c");
return 0;
}

例子中的a,b要進行整形提升,c不需要整形提升,而 a,b整形提升之後,變成了負數,所以表達式
a= =0xb6 , b= =0xb600 的結果是假,因爲c不發生整形提升,所以表達式 c= =0xb6000000的結果是真.則程序輸出的結果爲:

c

例2

int main()
{
char c = 1;
printf("%u\n", sizeof(c));
printf("%u\n", sizeof(+c));
printf("%u\n", sizeof(-c));
return 0;
}

例2中的,c只要參與表達式運算,就會發生整形提升,表達式 +c ,就會發生提升,所以 sizeof(+c) 是4個字節.
表達式 -c 也會發生整形提升,所以 sizeof(-c) 是4個字節,但是 sizeof( c ) ,就是1個字節.

!!!截斷提升相結合的例子!!!

#include <stdio.h>
int main()
{
	先進行截斷
	原碼 0x10000000 00000000 00000000 10000000
	反碼 0x11111111 11111111 11111111 01111111
	補碼 0x11111111 11111111 11111111 10000000
	a截斷後內存中所存儲的則是 1000 0000char a=-128;
    輸出的是十進制,則意味着要進行提升(需要根據類型而定)
    0x11111111 11111111 11111111 100000000
    0x11111111 11111111 11111111 011111111
    0x10000000 00000000 00000000 100000000
	printf("%d\n",a);
	return 0;
}

三:常量和變量

1. 常量

C語言之中的常量和變量的定義的形式是有所差異的。
C語言之中的常量可以分爲以下幾種:

  1. 字面常量
  2. const修飾的常變量
  3. #define定義的標識符常量
  4. 枚舉常量:有窮的數目,比如我們的性別
    一個程序就讓大家直接明白這些到底都是什麼意思!
#include <stdio.h>
enum Sex
{
MALE,//枚舉常量
FEMALE,
SECRET
};
int main()
{
3.14;//字面常量
1000;//字面常量
const float pai = 3.14f; //const 修飾的常量
#define MAX 100 //#define的標識符常量
return 0;

小技巧: 常量之中e後面要求必須是整數。

2. 定義變量的方法

int age = 150;
float weight = 45.5f;
char ch = 'w';

對於後半部分所定義的值式可以進行改變,是完全由編程者來進行定義的一種數據。

3. 變量的分類

3.1 局部變量

局部變量又稱之爲內部變量,是由某對象或者某個函數所創建的變量,只能被內部引用,不能夠被其他函數或對象引用。

3.2 全局變量

被整個程序所使用的變量稱之爲全局變量。

#include <stdio.h>
int global = 2019;//全局變量
int main()
{
int local = 2018;//局部變量
//下面定義的global會不會有問題?
int global = 2020;//局部變量
printf("global = %d\n", global);
return 0;
}

小技巧: 在同一個源文件中,外部變量和局部變量同名時, 則在局部變量的作用範圍內,外部變量不起作用!

4. 變量在函數裏的使用

#include <stdio.h>
int main()
{
int num1 = 0;
int num2 = 0;
int sum = 0;
printf("輸入兩個操作數:>");
scanf("%d %d", &a, &b);
sum = num1 + num2;
printf("sum = %d\n", sum);
return 0;
}
//這裏介紹一下輸入,輸出語句 有一個瞭解
//scanf
//printf

5. 變量的作用域和生命週期

5.1 作用域

作用域(scope),程序設計概念,通常來說,一段程序代碼中所用到的名字並不總是有效/可用的,而限定
這個名字的可用性的代碼範圍就是這個名字的作用域。

  1. 局部變量的作用域是變量所在的局部範圍。
  2. 全局變量的作用域是整個工程.

5.2 生命週期:

變量的生命週期指的是變量的創建到變量的銷燬之間的一個時間段

  1. 局部變量的生命週期是:進入作用域生命週期開始,出作用域生命週期結束。
  2. 全局變量的生命週期是:整個程序的生命週期。

6. 創建變量的初始化和賦值

初始化和賦值是不一樣的:

  1. 在創建變量的同時設定值,是初始化
  2. 變量創建完成之後,再去進行設定值的話,這是賦值

四:轉義字符 註釋

1. 轉義字符:

顧名思義是轉變意思,我們要在屏幕上打印一個目錄: c:\code\test.c 我們該如何寫代碼?

#include <stdio.h>
int main()
{
printf("c:\code\test.c\n");
return 0;
}

實際上程序的運行結果是這樣的:
轉義字符運行結果
由於它存在着’\t’ 和’\n’ 所以打印的時候自動進行轉義則不能夠打印出來。

2. 都有那些轉義字符

轉義字符 釋義
\? 在書中寫連續多個問號時使用,防止他們被解析成爲三個字母詞
\’ 用於表示字符串常量 ‘
\’’ 用於表示一個字符串內部的雙引號
\\ 用於表示一個反斜槓,防止它被解釋爲一個轉義序列符
\a 警告字符,蜂鳴
\b 退格符
\f 進紙符
\n 換行
\r 回車
\t 水平製表符
\v 垂直製表符
\ddd ddd表示1~3個八進制的數字。 如: \130
\xddd ddd表示3個十六進制數字。 如: \x030
#include <stdio.h>
int main()
{
//問題1:在屏幕上打印一個單引號',怎麼做?
//問題2:在屏幕上打印一個字符串,字符串的內容是一個雙引號“,怎麼做?
printf("%c\n", '\'');
printf("%s\n", "\"");
return 0;
}

重點例題:不妨自己試試看

//程序輸出什麼?
#include <stdio.h>
int main()
{
printf("%d\n", strlen("abcdef"));
// \32被解析成一個轉義字符
printf("%d\n", strlen("c:\test\32\test.c"));
return 0;

3. 註釋

  1. 代碼中有不需要的代碼可以直接刪除,也可以註釋掉
  2. 代碼中有些代碼比較難懂,可以加一下注釋文字
#include <stdio.h>
int Add(int x, int y)
{
return x+y;
}
/*C語言風格註釋
int Sub(int x, int y)
{
return x-y;
}
*/
int main()
{
//C++註釋風格
//int a = 10;
//調用Add函數,完成加法
printf("%d\n", Add(1, 2));
return 0;
}

註釋有兩種風格:

  1. C語言風格的註釋 /xxxxxx/
    缺陷:不能嵌套註釋
  2. C++風格的註釋 //xxxxxxxx
    可以註釋一行也可以註釋多行

第一部分完

第貳部分:操作符 關鍵字 表達式 語句

一:操作符

1. 算數操作符

 +-*/%  求餘
  1. 除了%操作符之外,其他的幾個操作符是可以作用於整數和浮點數的。
  2. 對於 / 操作符,如果兩個操作符都爲正數的話,執行整數除法,而只要有浮點數存在就執行浮點數除法。
  3. % 操作符的兩個操作符規定必須爲整數,返回的則是整除之後所得到的餘數。

注意:整數/0 所得到的結果顯示爲“運行時出錯”

  1. “運行時出錯”
  2. 運行時進程異常終止
  3. 運行時CPU再執行/0 指令的時候會導致操作系統觸發一個異常,然後給當前進程發送一個信號,導致進行異常終止。

2. 移位操作符

<< 左移操作符
>> 右移操作符
  1. 左移操作符移位規則:
    左邊拋棄,右邊補0
int num=10;

num<<1  (左移一位)

左移

  1. 右移操作符移位規則:
    首先右移運算分兩種:
    1. 邏輯移位 左邊用0填充,右邊丟棄
    2. 算術移位 左邊用原該值的符號位填充,右邊丟棄

      例題
      邏輯右移:左邊補0;
      邏輯右移
      算術右移:左邊用原該值的符號位進行填充,由於是負數,所以符號位爲1,即左邊補1
      算術右移
      小技巧:如在後期遇見次方的運算,那麼儘量使用移位運算!

3. 位操作符

位操作符有:

&  按位與 兩者都爲真結果才爲真
|  按位或 兩者只要存在一個真,結果則爲真
^  按位異或 兩者相同則爲假,兩者不同則爲真
注:他們的操作數必須是整數

小技巧:

  1. 把某個數字的第N位設爲1 num | (1 << N)
  2. 把某個數字的第N爲設爲0 num &~(1 << N)

簡單練習題

#include <stdio.h>
int main()
{
int num1 = 1;
int num2 = 2;
num1 & num2;
num1 | num2;
num1 ^ num2;
return 0;
}

3.1涉及到的短路求值

  1. 對於邏輯與運算來說,如果左側的表達式的值已經確定是假的了,則此時整個表達式的值就已經確定了,右側的表達式則再不需要進行求值。
  2. 對於邏輯或運算來說,如果左側表達式的值已經是真的了,此時整個表達式的值也就確定了,右側的表達式也不需要求值。

高難度練習題
要求:不能夠創建臨時變量(第三個變量),實現兩個數的交換。

#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
a = a^b;
b = a^b;
a = a^b;
printf("a = %d b = %d\n", a, b);
return 0;
}

4. 賦值操作符

賦值操作符是一個很棒的操作符,他可以讓你得到一個你之前不滿意的值。也就是你可以給自己重新賦值。

int weight = 120;//體重
weight = 89;//不滿意就賦值
double salary = 10000.0;
salary = 20000.0;//使用賦值操作符賦值。
賦值操作符可以連續使用,比如:
int a = 10;
int x = 0;
int y = 20;
a = x = y+1;//連續賦值
這樣的代碼感覺怎麼樣?
那同樣的語義,你看看:
x = y+1;
a = x;
這樣的寫法是不是更加清晰爽朗而且易於調試。

4.1 複合賦值符

+=
-=
*=
/=
%=
>>=
<<=
&=
|=
^=
這些運算符都可以寫成複合的效果

例子

int x = 10;
x = x+10;
x += 10;//複合賦值
//其他運算符一樣的道理。這樣寫更加簡潔

5. 單目操作符

單目操作符 含義
邏輯反操作
- 負值
+ 正值
& 取地址
sizeof 操作數的類型長度(以字節爲單位)
~ 對一個數的二進制按位取反
- - 前置、後置- -
++ 前置、後置++
* 間接訪問操作符(解引用操作符)
(類型) 強制類型轉換

小技巧:前置++的運行速度比後置++的運行速度快。

#include <stdio.h>
int main()
{
int a = -10;
int *p = NULL;
printf("%d\n", !2);
printf("%d\n", !0);
a = -a;
p = &a;
printf("%d\n", sizeof(a));
printf("%d\n", sizeof(int));
return 0;
}

6. 關係操作符

>
>=
<
<=
!= 用於測試“不相等”
== 用於測試“相等“

這些關係運算符比較簡單,沒什麼可講的,但是我們要注意一些運算符使用時候的陷阱。
注意: = 和== 的意義是不同的,用時需慎重。

7. 邏輯操作符

&&    邏輯與
||    邏輯或

區分邏輯與和按位與 區分邏輯或和按位或

1&2----->0
1&&2---->1
1|2----->3
1||2---->1

8. 條件操作符

exp1 ? exp2 : exp3
if (a > 5)
b = 3;
else
b = -3;
轉換成條件表達式,是什麼樣?
2.使用條件表達式實現找兩個數中較大值。

二:關鍵字

1. 常見關鍵字

auto break case char const continue default do
double else enum extern float for goto if int
long register return short signed sizeof
static struct switch typedef union unsigned void 
volatile while

2. 關鍵字typedef

typedef 顧名思義是類型定義,這裏應該理解爲類型重命名。

//將unsigned int 重命名爲uint_32, 所以uint_32也是一個類型名
typedef unsigned int uint_32;
int main()
{
//觀察num1和num2,這兩個變量的類型是一樣的
unsigned int num1 = 0;
uint_32 num2 = 0;
return 0;
}

3. 關鍵字static

在C語言中:static是用來修飾變量和函數的

3.1 修飾局部變量

代碼1
#include <stdio.h>
void test()
{
	int i = 0;
	i++;
	printf("%d ", i);
}

int main()
{
	for(i=0; i<10; i++)
	{
		test();
	}
	return 0;
}
//代碼2
#include <stdio.h>
void test()
{
	static int i = 0;
	i++;
	printf("%d ", i);
}
int main()
{
	for(i=0; i<10; i++)
	{
		test();
	}
	return 0;
} 

對比代碼1和代碼2的效果理解static修飾局部變量的意義。
結論:static修飾局部變量改變了變量的生命週期,讓靜態局部變量出了作用域依然存在,到程序結束,生命週期才結束。

3.2 修飾全局變量

代碼1
//add.c
int g_val = 2018;
//test.c
int main()
{
	printf("%d\n", g_val);
	return 0;
}
代碼2
//add.c
static int g_val = 2018;
//test.c
int main()
{
	printf("%d\n", g_val);
	return 0;
}

代碼1正常,代碼2在編譯的時候會出現連接性錯誤。
結論:一個全局變量被static修飾,使得這個全局變量只能在本源文件內使用,不能在其他源文件內使用。

3.3 修飾函數

代碼1
//add.c
int Add(int x, int y)
{
	return c+y;
}
//test.c
int main()
{
	printf("%d\n", Add(2, 3));
	return 0;
}
代碼2
//add.c
static int Add(int x, int y)
{
	return c+y;
}
//test.c
int main()
{
	printf("%d\n", Add(2, 3));
	return 0;
}

代碼1正常,代碼2在編譯的時候會出現連接性錯誤。
結論:一個函數被static修飾,使得這個函數只能在本源文件內使用,不能在其他源文件內使用。

4. define定義常量和宏

define定義標識符常量
#define MAX 1000
define定義宏
#define ADD(x, y) ((x)+(y))
#include <stdio.h>
int main()
{
	int sum = ADD(2, 3);
	printf("sum = %d\n", sum);
	sum = 10*ADD(2, 3);
	printf("sum = %d\n", sum);
	return 0;

使用宏的話:整體而言,宏的本質相當於文本的替換

  1. 定義一個常量
  2. 藉助宏來重定義一個類型的別名
  3. 宏還能夠影響到編譯器的行爲
  4. 宏還能夠定義一個代碼片段(類似於函數的效果)

5. 宏替換

5.1 宏替換

#define DEBUG_PRINT printf("file:%s line:%d", __FILE__, __LINE__)
#define For for(;;)
//#define MAX 10000
#define MAX(a,b) a>b

//1. 系統已經定義好的
	printf("file:%s line:%d", __FILE__, __LINE__);
	DEBUG_PRINT;
//3. 最容易考的  :實現一個MAX宏函數
	int x1 = 1, x2 = 2;
	printf("%d\n",MAX(x1,x2));
	printf("%d\n", MAX(x1|x2, x1&x2));
	//如果想要按照我們的邏輯運算來進行,需要將定義的ab括號括起來
	printf("%d\n", MAX(x1,x2)*5);

5.2 宏和函數的對比

宏替換

5.3 宏續接

#define PRINT(FORMAT,VALUE) \
		printf("the value of"#VALUE"is "FORMAT"\n",VALUE);
宏只會替換一行之中的後面部分,如果要寫成多行,必須使用續行符 “\”
其中#則會把後面的字符換成了字符串
  #define ADD_TO_SUM(num,value) \
		sum##num += (value*3.14+10.1456);
其中##是把兩邊的符號合併成一個符號 
double sum1 = 1, sum2 = 2,sum3 = 3;
	ADD_TO_SUM(1, 10);
	ADD_TO_SUM(2, 10);
	ADD_TO_SUM(3, 10); // 則成爲sum3+=(10*3.14+10.1456)

小技巧:後置++ 返回的是++之前的值!

三:表達式

1. 逗號表達式

exp1, exp2, exp3, …expN

逗號表達式,就是用逗號隔開的多個表達式。 逗號表達式,從左向右依次執行。整個表達式的結果是最後一個表達式的結果。

代碼1
int a = 1;
int b = 2;
int c = (a>b, a=b+10, a, b=a+1);//逗號表達式
c是多少?


代碼2
if (a =b + 1, c=a / 2, d > 0)


代碼3
a = get_val();
count_val(a);
while (a > 0)
{
//業務處理
	a = get_val();
	count_val(a);
}
如果使用逗號表達式,改寫:
while (a = get_val(), count_val(a), a>0)
{
//業務處理
}

2.下標引用

[ ] 下標引用操作符
操作數:一個數組名 + 一個索引值。

int arr[10];//創建數組
arr[9] = 10;//實用下標引用操作符。
[ ]的兩個操作數是arr和9
在數組元素之中 [ ] 之中可以進行運算 如[10-10] 

3. 函數調用

( ) 函數調用操作符
接受一個或者多個操作數:第一個操作數是函數名,剩餘的操作數就是傳遞給函數的參數。

#include <stdio.h>
void test1()
{
	printf("hehe\n");
}
void test2(const char *str)
{
	printf("%s\n", str);
}
int main()
{
	test1(); //實用()作爲函數調用操作符。
	test2("hello bit.");//實用()作爲函數調用操作符。
	return 0;
}

4. 結構成員

訪問一個結構的成員
. (結構體.成員名 )
-> (結構體指針->成員名)

#include <stdio.h>
struct Stu
{
	char name[10];
	int age;
	char sex[5];
	double score;
}

四:分支和循環語句

1. 語句

C語言中由一個分號 ; 隔開的就是一條語句。 比如:

printf("hehe");
1+2;

2. 分支語句(選擇結構)

2.1 if語句

顧名思義,就是在兩者或者多者之間選擇一個或者說選擇多個!
2.1.1 那if語句的語法結構

if(表達式)
語句;
如果表達式結果爲真,則執行下面的語句,否則直接跳出;

if(表達式)
語句1;
else
語句2;
如果表達式爲真則執行語句1,否則執行語句2//多分支
if(表達式1)
語句1;
else if(表達式2)
語句2;
else
語句3;
如果表達式1爲真則執行語句1,如果表達式2爲真,則執行語句2,否則執行語句3

2.1.2 在C語言之中如何表示真假

          0表示假, 非0表示真

2.1.3 多條語句代碼塊的使用
如果條件成立,要執行的語句較多,那麼應該合理的去使用代碼塊進行編寫

#include <stdio.h>
int main()
{
	if(表達式)
	{
		語句列表1}
	else
	{
		語句列表2}
	return 0;
} 
  1. 代碼演示
#include <stdio.h>
int main()
{
	int coding = 0;
	printf("你會去敲代碼嗎?(選擇1 or 0):>");
	scanf("%d", &coding);
	if(coding == 1)
	{
		prinf("堅持,你會有好offer\n");
	}
	else
	{
		printf("放棄,回家賣紅薯\n");
	}
	return 0;
} 
  1. 多分支演示
#include <stdio.h>
int main()
{
	int coding = 0;
	printf("你會去敲代碼嗎?(選擇1 or 0),如果家裏很多房,選擇 -1:>");
	scanf("%d", &coding);
	if(coding == 1)
	{
		prinf("堅持,你會有好offer\n");
	}
	else if(coding == 0)
	{
		printf("放棄,回家賣紅薯\n");
	}
	else if(coding == -1)
	{
		printf("呵呵\n");
	}
	return 0;
}
  1. if書寫形式的對比
//代碼1
if (condition)
return x;
return y;

//代碼2
if(condition)
{
	return x;
}
else
{
	return y;
}

//代碼3
int num = 1;
if(num = 5)
{
	printf("hehe\n");
}

//代碼4
int num = 1;
if(5 == num)
{
	printf("hehe\n");
}
可以清晰的看出來,代碼2和代碼4 是更好的,邏輯也是更加清晰不容易出錯的。

4.練習題

1.判斷一個數是否爲奇數
2.輸出1-100之間的奇數

2.2 switch語句

2.2.1 switch語句也是一種分支語句
常常用於多分支的情況,比如:

輸入1,輸出星期一
輸入2,輸出星期二
輸入3,輸出星期三
輸入4,輸出星期四
輸入5,輸出星期五
輸入6,輸出星期六
輸入7,輸出星期七

如果我們使用if ... else if ...else if...的話形式特別的複雜, 因此我們就需要使用不一樣的語法形式去進行描述,也就是我們說道的switch語句。

switch(整型表達式)
{
	語句項;
}

2.2.2 switch之中的語句項
switch之中的語句項又是什麼呢?

是一些case語句:
如下:
case 整形常量表達式:語句;

2.2.3 break語句
在switch語句之中,我們是無法直接實現分支的,所以正確的switch語句是需要搭配break使用才能夠真正的實現分支,break語句的實際效果是把語句列表劃分爲不同的部分,正確的代碼如下:

#include <stdio.h>
int main()
{
	int day = 0;
	switch(day)
{
case 1printf("星期一\n");
	break;
case 2:printf("星期二\n");
	break;
case 3:printf("星期三\n");
	break;
case 4:printf("星期四\n");
	break;
case 5:printf("星期五\n");
	break;
case 6:printf("星期六\n");
	break;
case 7:printf("星期天\n");
	break;
}
	return 0;
}

如果我們的需求改變,要求輸入1-5的時候輸出“werkday”,而輸入6-7的時候輸出的是“weedend”,代碼則變成了這樣:

#include <stdio.h>
//switch代碼演示
int main()
{
int day = 0;
switch(day)
{
case 1case 2:
case 3:
case 4:
case 5:
printf("weekday\n");
break;
case 6:
case 7:
printf("weekend\n");
break;
}
return 0;
}

2.2.4 default子句
如果表達的值域所有的case標籤的值都不匹配怎麼辦? 其實也沒什麼 ----所有的語句都被跳過而已。 程序並
不會終止,也不會報錯,因爲這種情況在C中並不認爲適合錯誤。 但是,如果你並不想忽略不匹配所有標籤
的表達式的值時該怎麼辦呢? 你可以在語句列表中增加一條default子句,把下面的標籤 default: 寫在任何
一個case標籤可以出現的位置。當 switch表達式的值並不匹配所有case標籤的值時,這個default子句後面的
語句就會執行。所以,每個switch語句中只能出現一條default子句。 但是它可以出現在語句列表的任何位
置,而且語句流會像貫穿一個case標籤一樣貫穿default子句。
2.2.5 編輯好習慣

  1. 在最後一個 case 語句的後面加上一條 break語句。 (之所以這麼寫是可以避免出現在以前的最後一個 case語句後面忘了添加 break語句)。
  2. 在每個 switch 語句中都放一條default子句是個好習慣,甚至可以在後邊再加一個 break 。

練習題

#include <stdio.h>
int main()
{
	int n = 1;
	int m = 2;
	switch (n)
	{
		case 1:
		m++;
		case 2:
		n++;
		case 3:
		switch (n)
		{//switch允許嵌套使用
			case 1:
			n++;
			case 2:
			m++;
			n++;
			break;
		}
		case 4:
		m++;
		break;
		default:
		break;
	}
	printf("m = %d, n = %d\n", m, n);
	return 0;
}

3. 循環語句

C語言中如何實現循環呢?

3. 1 while語句

當條件滿足的情況下,if語句後的語句執行,否則不執行。但是這個語句只會執行一次。
但是我們發現生活中很多的實際的例子是:同一件事情我們需要完成很多次。
那我們怎麼做呢? C語言中給我們引入了:while語句,可以實現循環.

//while 語法結構
while(表達式)
循環語句;

比如我們要在屏幕上打印1-10 的數字:

#include <stdio.h>
int main()
{
	int i = 1;
	while(i<=10)
	{
		printf("%d ", i);
		i = i+1;
	}
	return 0;
}

通過這個代碼我們可以更好的瞭解while語句的基本語法

//while循環的實例
#include <stdio.h>
int main()
{
	printf("努力學習\n");
	int line = 0;
	while(line<=20000)
	{
		line++;
		printf("我要繼續努力敲代碼\n");
	}
	if(line>20000)
	printf("贏取白富美\n");
	return 0;
}
3.1.1while語句中的break
//break 代碼實例
#include <stdio.h>
int main()
{
	int i = 1;
	while(i<=10)
	{
		if(i == 5)
		{	
			break;
		}
		printf("%d ", i);
		i = i+1;
	}
	return 0;
}

這個代碼的輸出結果是什麼?試試看你能否選對

A :1 2 3 4
B :1 2 3 4 5
C :1 2 3 4 5 6 7 8 9 10
D :1 2 3 4 6 7 8 9 10
正確的答案是 A

總結:其實在循環中只要遇到break,就停止後期的所有的循環,直接終止循環。
所以:while中的break是用於永久終止循環的。

3.1.2while語句中的continue
//continue 代碼實例1
#include <stdio.h>
int main()
{
	int i = 1;
	while(i<=10)
	{
		if(i == 5)
		continue;
		printf("%d ", i);
		i = i+1;
	}
return 0;
}

輸出選擇
這裏的輸出又是什麼呢?

//continue 代碼實例2
#include <stdio.h>
int main()
{
	int i = 1;
	while(i<=10)
	{
		i = i+1;
		if(i == 5)
		continue;
		printf("%d ", i);
	}
	return 0;
}

這裏代碼輸出的結果是什麼?

A :1 2 3 4
B :1 2 3 4 5
C :1 2 3 4 5 6 7 8 9 10
D :1 2 3 4 6 7 8 9 10
E :2 3 4 6 7 8 9 10

總結:continue在while循環中的作用就是:continue是用於終止本次循環的,也就是本次循環中continue後邊的代碼不會再執行,而是直接跳轉到while語句的判斷部分,進行下一次循環的入口判斷。
再補充幾個代碼,這都是什麼意思呢?

//代碼1
#include <stdio.h>
int main()
{
int ch = 0;
while ((ch = getchar()) != EOF)
putchar(ch);
return 0;
}
//代碼2
#include <stdio.h>
int main()
{
while ((ch = getchar()) != EOF)
{
if (ch <0|| ch >9)
continue;
putchar(ch);
}
return 0;

break和continue知識點總結
break:跳出當前的循環語句,結束整個循環。
continue:結束這一次循環,直接進入下一次的循環。

3.2 for語句

3.2.1 for語法
for(表達式1;表達式2;表達式3)
循環語句;

表達式1 表達式1爲初始化部分,用於初始化循環變量的。 表達式2 表達式2爲條件判斷部分,用於判斷循環時候終止。 表達式3 表達式3爲調整部分,用於循環條件的調整。

實際問題解決:使用for循環,在屏幕上打印1-10數字
#include <stdio.h>
int main()
{
	int i = 0;
//for(i=1/*初始化*/; i<=10/*判斷部分*/; i++/*調整部分*/)
	for(i=1; i<=10; i++)
	{
		printf("%d ", i);
	}
	return 0;
3.2.1 for循環和while循環的對比
int i = 0;
//實現相同的功能,使用while
i=1;//初始化部分
while(i<=10)//判斷部分
{
printf("hehe\n");
i = i+1;//調整部分
}
//實現相同的功能,使用while
for(i=1; i<=10; i++)
{
printf("hehe\n");

可以發現在while循環中依然存在循環的三個必須條件,但是由於風格的問題使得三個部分很可能偏離較遠,這樣查找修改就不夠集中和方便。所以,for循環的風格更勝一籌。 for循環使用的頻率也最高。

3.2.3 for循環之中的break和continue;

我們發現在for循環中也可以出現break和continue,他們的意義和在while循環中是一樣的。 但是還是有些差異:

  1. While中的continue接下來所執行的是判定循環的條件
  2. for中的continue接下來所執行的是表達式3,然後再是表達式2;
//代碼1
#include <stdio.h>
int main()
{
int i = 0;
for(i=1; i<=10; i++)
{
if(i == 5)
break;
printf("%d ",i);
}
return 0;
}
//代碼2
#include <stdio.h>
int main()
{
int i = 0;
for(i=1; i<=10; i++)
{
if(i == 5)
continue;
printf("%d ",i);
}
return 0;
}
3.2.4 for語句的循環控制變量

一些建議:

1. 不可在for 循環體內修改循環變量,防止 for 循環失去控制。
2. 建議for語句的循環控制變量的取值採用“半開半閉區間”寫法。

int i = 0;
//半開半閉的寫法
for(i=0; i<10; i++)
{}
//兩邊都是閉區間
for(i=0; i<=9; i++)
{}

一些for循環的變種

#include <stdio.h>
int main()
{
//變種1
for(;;)
{
printf("hehe\n");
}
//變種2
int x, y;
for (x = 0, y = 0; x<2, y<5; ++x, y++)
{
printf("hehe\n");
}
return 0;
}
3.2.5 練習題和參看代碼
  1. 編寫代碼,演示多個字符從兩端移動,向中間匯聚。
  2. 編寫代碼實現,模擬用戶登錄情景,並且只能登錄三次。(只允許輸入三次密碼,如果密碼正確則提示
    登錄成,如果三次均輸入錯誤,則退出程序
練習題參考代碼:
//代碼1
#include <stdio.h>
int main()
{
char arr1[] = "welcome to bit...";
char arr2[] = "#################";
int left = 0;
int right = strlen(arr1)-1;
printf("%s\n", arr2);
//while循環實現
while(left<=right)
{
Sleep(1000);
arr2[left] = arr1[left];
arr2[right] = arr1[right];
left++;
right--;
printf("%s\n", arr2);
}
//for循環實現
for (left=0, right=strlen(src)-1;
left <= right;
left++, right--)
{
Sleep(1000);
arr2[left] = arr1[left];
arr2[right] = arr1[right];
printf( "%s\n", target);
}
retutn 0;
}

//代碼2
int main()
{
char psw[10] = "" ;
int i = 0;
int j = 0;
for (i = 0; i < 3 ; ++i)
{
printf( "please input:");
scanf("%s", psw);
if (strcmp(psw, "password" ) == 0)
break;
}
if (i == 3)
printf("exit\n");
else
printf( "log in\n");
}
do
循環語句;
while(表達式

重點面試題:

//請問循環要循環多少次?
#include <stdio.h>
int main()
{
int i = 0;
int k = 0;
for(i =0,k=0; k=0; i++,k++)
k++;
return 0;
}

3.3 do … while語句

3.3.1 do語句的語法
do
循環語句;
while(表達式);
3.3.2 do語句的特點

循環至少執行一次,使用的場景有限,所以不是經常使用。

#include <stdio.h>
int main()
{
int i = 10;
do
{
printf("%d\n", i);
}while(i<10);
return 0;
}

3.3.4 重點算法實現
  1. 在這裏要給大家講的一個知識點就是折半查找算法:
    比如我買了一雙鞋,你好奇問我多少錢,我說不超過300元。你還是好奇,你想知道到底多少,我就讓你猜,你會怎麼猜?
    答案:你每次猜中間數。
    代碼實現:
實現在主函數內:
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,10};
int left = 0;
int right = sizeof(arr)/sizeof(arr[0])-1;
int key = 7;
int mid = 0;
while(left<=right)
{
mid = (left+right)/2;
if(arr[mid]>key)
{
right = mid-1;
}
else if(arr[mid] < key)
{
left = mid+1;
}
else
break;
}
if(left <= right)
printf("找到了,下標是%d\n", mid);
else
printf("找不到\n");
}
  1. 如何實現一個二分查找函數:
int bin_search(int arr[], int left, int right, int key)
{
	int mid = 0;
	while(left<=right)
	{
		mid = (left+right)>>1;
		if(arr[mid]>key)
		{
			right = mid-1;
		}
		else if(arr[mid] < key)
		{
			left = mid+1;
		}
		else
			return mid;//找到了,返回下標
		}
	return -1;//找不到
  1. 猜數字遊戲的實現(小時候是不是經常玩?)
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void menu()
{
printf("**********************************\n");
printf("*********** 1.play **********\n");
printf("*********** 0.exit **********\n");
printf("**********************************\n");
}
//TDD-測試驅動開發。
//RAND_MAX--rand函數能返回隨機數的最大值。
void game()
{
int random_num = rand()%100+1;
int input = 0;
while(1)
{
printf("請輸入猜的數字>:");
scanf("%d", &input);
if(input > random_num)
{
printf("猜大了\n");
}
else if(input < random_num)
{
printf("猜小了\n");
}
else
{
printf("恭喜你,猜對了\n");
break;
}
}
}
int main()
{
int input = 0;
srand((unsigned)time(NULL));
do
{
menu();
printf("請選擇>:");
scanf("%d", &input);
switch(input)
{
case 1:
game();
break;
case 0:
break;
default:
printf("選擇錯誤,請重新輸入!\n");
break;
}
}while(input);
return 0;
}
3.3.5 練習題
  1. 計算 n的階乘。
  2. 計算 1!+2!+3!+……+10!
  3. 在一個有序數組中查找具體的某個數字n。 編寫int binsearch(int x, int v[], int n); 功能:在v[0]<=v[1]
    <=v[2]<= ….<=v[n-1]的數組中查找x。

第叄部分:函數 數組

一:函數

1. 函數是什麼?

根據維基百科中對於C言語函數的定義爲:子程序

1.1 子程序是什麼?

  1. 在計算機科學中,子程序是一個大型程序之中的某部分代碼,由一個或多個語句塊組成,它是用來負責完成某項特定的任務,而且相較於其他代碼,它具備相對的獨立性。
  2. 一般會有輸入參數並有返回值,提供對過程的封裝和細節的隱藏。

2. 庫函數和自定義函數

C語言之中將函數分爲兩類:庫函數,自定義函數

2.1 庫函數

2.1.1 爲什麼會有庫函數?

  1. 我們知道在我們學習C語言編程的時候,總是在一個代碼編寫完成之後迫不及待地想要知道結構,想把這個結構打印到屏幕上看看,這個時候我們就會頻繁的使用一個功能,打印(printf)。
  2. 在編程的過程中我們會頻繁的做一些字符串的拷貝工作(strcpy)。
  3. 在編程中計算的話,需要計算n的k次方,則是(pow)。
    上面這些基本的基礎功能,我們在開發的過程中每個程序員都可能用到,因此爲了可移植性和提高程序的效率,所以C語言的基礎庫中提供了一系列的庫函數,方便程序猿進行軟件開發。
    所以如果我們想要學習庫函數,可以到這個網址進行學習,作爲程序猿這個網址也是我們必須瞭解的 www.cplusplus.com
    2.1.2 C語言常用的庫函數
  4. IO函數
  5. 字符串操作函數
  6. 字符操作函數
  7. 內存操作函數
  8. 時間/日期函數
  9. 數學函數
  10. 其他庫函數
    其中對於幾個庫函數 我們稍稍介紹一下:
    strcpy:
char * strcpy ( char * destination, const char * source )

printf:

int printf ( const char * format, ... );

memset

void * memset ( void * ptr, int value, size_t num );

**注意:**如果想要使用庫函數的話必須知道的一個祕密就是:必須包含庫函數所對應的頭文件 #include< >

2.2 自定義函數

如果庫函數能幹所有的事情,那也就沒有程序猿什麼事了。
所以更加重要的一個就是自定義函數:自定義函數和庫函數一樣,擁有函數名,返回值類型和函數參數,但是不一樣的事這些都是我們自己來進行設計的,這也就考驗我們如何將思想轉化爲代碼,給予了程序猿一個更大的發揮空間。
2.2.1 函數的組成

ret_type fun_name(para1, * )
{
statement;//語句項
}
ret_type 返回類型
fun_name 函數名
para1 函數參數

**舉例1:**寫一個函數可以找出兩個整數中的最大值

#include <stdio.h>
//get_max函數的設計
int get_max(int x, int y)
{
	return (x>y)?(x):(y);
}
int main()
{
	int num1 = 10;
	int num2 = 20;
	int max = get_max(num1, num2);
	printf("max = %d\n", max);
	return 0;
}

**舉例2:**寫一個函數可以交換兩個整形變量的內容

#include <stdio.h>

void Swap1(int x, int y)
{
	int tmp = 0;
	tmp = x;
	x = y;
	y = tmp;
}

void Swap2(int *px, int *py)
{
	int tmp = 0;
	tmp = *px;
	*px = *py;
	*py = tmp;
}

int main()
{
	int num1 = 1;
	int num2 = 2;
	Swap1(num1, num2);
	printf("Swap1::num1 = %d num2 = %d\n", num1, num2);
	Swap2(&num1, &num2);
	printf("Swap2::num1 = %d num2 = %d\n", num1, num2);
	return 0;
}

3. 函數的參數和調用

函數的參數分爲實參和形參。

3.1 實際參數(實參)

真是傳給函數的參數,叫實參。實參可以是:常量、變量、表達式、函數等。無論實參是何種類型的量,在進行函數調用時,它們都必須有確定的值,以便把這些值傳送給形參。
上面在main函數中傳給Swap1的num1,num2和傳給Swap2函數的&num1,&num2是實際參數。

3.2 形式參數(形參)

形式參數是指函數名後括號中的變量,因爲形式參數只有在函數被調用的過程中才實例化(分配內存單元),所以叫形式參數。形式參數當函數調用完成之後就自動銷燬了。因此形式參數只在函數中有效。
上面Swap1和Swap2函數中的參數 x,y,px,py 都是形式參數。

3.3 形參和實參的比較

實參和形參的比較
實參和形參的比較
形參和實參使用的是不同的空間
形參和實參使用的是不同的空間
通過函數的調用,函數之中擁有了和實參一模一樣的內容,所以我們可以簡單的認爲:實參實例化之後其實就相當於實參的一份臨時拷貝。

3.4 函數調用的過程

函數的調用過程:出現函數調用的指令,則進入到函數體內部進行執行,遇到return語句的話,函數就結束,回到所調用位置進行繼續執行。
函數在傳參的過程中擁有兩種傳參方式:可以從左到右,也可以從右到左,具體如何和我們使用的編譯器有關!

void fun(int x, int y) {
	printf("%d,%d", x, y);
}


	int i = 1;
	fun(i++, i++);

4. 函數的嵌套調用和鏈式訪問

4.1 函數的調用:

4.1.1 傳值調用
函數的形參和實參分別佔有不同的內存塊,對形參的修改不會影響實參。
4.1.2 傳址調用

  1. 傳址調用是把函數外部創建的變量的內存地址傳遞給函數參數的一種調用函數的方式。
  2. 這種傳參方式可以讓函數和函數外邊的變量建立起真正的聯繫,也就是函數內部可以直接操作函數外部的變量。

4.2 函數的嵌套

函數和函數之間是可以進行有機的組合。
4.2.1 何爲嵌套?
顧名思義,也就是多層調用,在主函數之中調用第一個函數,在第一個函數之中調用第二個函數,以此連續;或者是在第一個函數中連續調用第二個和第三個函數…
4.2.2 調用的例子

#include <stdio.h>
void new_line()
{
	printf("hehe\n");
}

void three_line()
{
	int i = 0;
	for(i=0; i<3; i++)
	{
		new_line();
	}
}

int main()
{
t	hree_line();
	return 0;
}

4.3 鏈式訪問

把一個函數的返回值作爲另外一個函數的參數來進行調用!

#include <stdio.h>
#include <string.h>
int main()
{
	char arr[20] = "hello";
	int ret = strlen(strcat(arr,"world"));//這裏介紹一下strlen函數
	printf("%d\n", ret);
	return 0;
}

4.4 練習題

1. 寫一個函數可以判斷一個數是不是素數。
2. 寫一個函數判斷一年是不是閏年。
3. 寫一個函數,實現一個整形有序數組的二分查找。
4. 寫一個函數,每調用一次這個函數,就會將num的值增加1.

5. 函數的聲明和定義

5.1 函數的聲明

  1. 告訴編譯器有一個函數叫什麼,所使用的參數是什麼,返回類型是什麼,但是具體是不是存在,無關緊要。
  2. 函數的聲明一般出現在函數的使用之前,要滿足先聲明後使用。
  3. 相關函數的聲明一般要放在頭文件中。

test.h的內容 放置函數的聲明

//函數的聲明
int Add(int x, int y);

5.2 函數的定義

  1. 函數的定義是指函數的具體實現,交代函數的功能實現的。

test.c的內容 放置函數的實現

#include "test.h"
函數Add的實現
int Add(int x, int y)
{
	return x+y;
}

6. 函數的遞歸(小難點)

6.1 什麼是遞歸?

程序調用自身的編程技巧稱之爲遞歸,遞歸作爲一種算法在程序設計語言之中被廣泛地應用,一個過程或函數在其定義或者說明中有直接或間接調用自身地一種方法,它通常把一個大型複雜的問題層層轉化爲一個與原問題相似地規模較小地問題來進行求解,遞歸策略只需要少量的程序就可以描述出解題過程所需要的多次重複計算,大大地減少了程序地代碼量。
遞歸思想:大事化小,小事化了!

6.2 遞歸的兩個必要條件

  1. 存在限制條件,當滿足這個限制條件的時候,遞歸便不再繼續!
  2. 每次遞歸調用之後越來越接近這個限制條件,這是它的收斂性。
  3. 寫遞歸程序的話,往往就是對正在進行的問題進行不斷地細化和拆分。

代碼例子:
4. 接受一個整型值(無符號),按照順序打印它的每一位。 例如: 輸入:1234,輸出 1 2 3 4

#include <stdio.h>
void print(int n)
{
	if(n>9)
	{
		print(n/10);
	}
	printf("%d ", n%10);
}

int main()
{
	int num = 1234;
	print(num);
	return 0;
}
  1. 編寫函數不允許創建臨時變量,求字符串的長度
#incude <stdio.h>
int Strlen(const char*str)
{
	if(*str == '\0')
		return 0;
	else return 1+Strlen(str+1);
}
int main()
{
	char *p = "abcdef";
	int len = Strlen(p);
	printf("%d\n", len);
	return 0;
}

6.3 遞歸和迭代

首先我們看兩個代碼

  1. 求n的階乘。(不考慮溢出)
int factorial(int n)
{
	if(n <= 1)
		return 1;
	else
		return n* factorial(n-1);
}
  1. 求第n個斐波那契數。(不考慮溢出)
int fib(int n)
{
	if (n <= 2)
		return 1;
	else
		return fib(n - 1) + fib(n - 2);
}

如果大家將這兩個代碼運行的話,就回發現,在使用fib這個函數的時候如果我們要計算第50個斐波那契數字的話,是非常的耗時間的;而如果使用factorial函數來求100000的階乘的話,當然是不考慮結果的正確性的話,那麼程序就會崩潰,這是爲什麼呢?
我們用一個計數變量,來計算遞歸到底執行了多少次就知道了

int count = 0;//全局變量
int fib(int n)
{
	if(n == 3)
		count++;
	if (n <= 2)
		return 1;
	else
		return fib(n - 1) + fib(n - 2);
}

通過程序我們可以看的出來,count是一個很大很大的值,這也就意味着在進行函數遞歸的時候,進行了很多次;系統分配給程序的棧空間是有限的,但是如果出現了死循環,或者說是死遞歸的話,有可能導致一直在不斷的開闢棧空間,最終導致棧空間耗盡,我們將這樣的現象稱爲棧溢出。

6.1 如何解決遞歸溢出?
  1. 將遞歸改寫成非遞歸
  2. 使用static對象替代nonstatic局部對象,在遞歸函數的設計中,可以使用static對象替代nonstatic局部對象(即所謂的棧對象),這樣不僅可以減少每次遞歸調用和返回時所產生和釋放的nonstatic對象的開銷,而且static對象還可以保存遞歸調用的中間狀態,並且可爲各個調用層所訪問。

修改之後的代碼:

//求n的階乘
int factorial(int n)
{
	int result = 1;
	while (n > 1)
	{
		result *= n ;
		n -= 1;
	}
	return result;
}

//求第n個斐波那契數
int fib(int n)
{
	int result;
	int pre_result;
	int next_older_result;
	result = pre_result = 1;
	while (n > 2)
	{
		n -= 1;
		next_older_result = pre_result;
		pre_result = result;
		result = pre_result + next_older_result;
	}
	return result;
}
6.2 小提示
  1. 許多問題是以遞歸的形式來進行解釋的,這只是因爲它比非遞歸的形式更爲的清晰。
  2. 但是這些問題的迭代實現往往會比遞歸實現的效率更高,即使是代碼的可讀性稍微差了一些。
  3. 當一個我問題相當的複雜,難以用迭代實現的話,此使遞歸實現的簡潔性便可以不償它所帶來的運行時的開銷了。
  4. 如果所需要做的題嚴格要求不讓創建臨時變量或者是不能使用循環的話,那麼遞歸無疑是最適合的方式!

二:數組

數組是以組相同類型元素所構成的集合。

1. 一維數組

1.1 一維數組的創建和初始化

1.1.1數組的創建
數組創建的方式:

type_t arr_name [const_n];
	type_t 是指數組的元素類型
	const_n 是一個常量表達式,用來指定數組的大小

實例

//代碼1
int arr1[10];

//代碼2
int count = 10;
int arr2[count];//數組時候可以正常創建?

//代碼3
char arr3[10];
float arr4[1];
double arr5[20];

小技巧: 在數組創建的時候,[ ]中要給一個常量纔可以,不能夠使用變量
1.1.2 數組的初始化
數組的初始化時指,在創建數組的同時給數組的內容一些合理的初始值。

int arr1[10] = {1,2,3};
int arr2[] = {1,2,3,4};
int arr3[5] = {12345}char arr4[3] = {'a',98, 'c'};
char arr5[] = {'a','b','c'};
char arr6[] = "abcdef";
  1. 數組初始化中沒有完全給出數值的元素,那麼全部都是以0的形式進行存儲,如int arr1[10]={1,2,3,0,0,0,0,0,0,0};
  2. 數組在創建的時候如果想不指定數組的確定的大小就得初始化。數組的元素個數根據初始化的內容來確定。 但是對於下面的代碼要區分,內存中如何分配.
char arr1[] = "abc";
char arr2[3] = {'a','b','c'}
  1. 對於數組來說,不論是幾位數組,在進行定義的時候,永遠只有第一個[ ] 的數字可以進行省略,其他那幾個省略 會出現錯誤。
  2. 數組初始化

1.2 一維數組的使用

對於數組的使用我們之前介紹了一個操作符: [] ,下標引用操作符,它其實就數組訪問的操作符。

#include <stdio.h>
int main()
{
	int arr[10] = {0};//數組的不完全初始化
//計算數組的元素個數
	int sz = sizeof(arr)/sizeof(arr[0]);
//對數組內容賦值,數組是使用下標來訪問的,下標從0開始。所以:
	int i = 0;//做下標
	for(i=0; i<10; i++)//這裏寫10,好不好?
	{
		arr[i] = i;
	}
//輸出數組的內容
	for(i=0; i<10; ++i)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

總結:

  1. 數組是使用下標來訪問的,下標是從0開始。
  2. 數組的大小可以通過計算得到。
  3. Sizeof 計算的是數組的總大小,是多少就是多少。
  4. 計算字符數組長度的話,計算\0
  5. sizeof 是一個運算符,具有一個重要的特性。
  6. Sizeof(arr[100]) 程序編譯過程中就算出了結果 4 。
int arr[10];
int sz = sizeof(arr)/sizeof(arr[0]);

1.3 一維數組在內存中的存儲

#include <stdio.h>
int main()
{
	int arr[10] = {0};
	int i = 0;
	for(i=0; i<sizeof(arr)/sizeof(arr[0]); ++i)
	{
		printf("&arr[%d] = %p\n", i, &arr[i]);
	}
	return 0;
}

輸出的結果如下:
輸出結構
仔細觀察輸出的結果,我們知道,隨着數組下標的增長,元素的地址,也在有規律的遞增。 由此可以得出結論:數組在內存中是連續存放的
地址的存放

2. 二維數組

2.1 二維數組的創建和初始化

2.1.1 二維數組的創建

//數組創建
int arr[3][4];
char arr[3][5];
double arr[2][4];

2.1.2 二維數組的初始化

//數組初始化
int arr[3][4] = {1,2,3,4};
int arr[3][4] = {{1,2},{4,5}};
int arr[][4] = {{2,3},{4,5}};

2.2 二維數組的使用

二維數組的使用和一維數組一樣,都是通過數組下標的方式來進行:

#include <stdio.h>
int main()
{
	int arr[3][4] = {0};
	int i = 0;
	for(i=0; i<3; i++)
	{
		int j = 0;
		for(j=0; j<4; j++)
		{
			arr[i][j] = i*4+j;
		}
	}
	for(i=0; i<3; i++)
	{
		int j = 0;
		for(j=0; j<4; j++)
		{
			printf("%d ", arr[i][j]);
		}
	}
	return 0;
}

2.3 二維數組在內存中的存儲

#include <stdio.h>
int main()
{
	int arr[3][4];
	int i = 0;
	for(i=0; i<3; i++)
	{
		int j = 0;
		for(j=0; j<4; j++)
		{
			printf("&arr[%d][%d] = %p\n", i, j,&arr[i][j]);
		}
	}
	return 0;
}

輸出的結果是這樣的:
輸出結果
通過結果的分析,我們可以得到,其實二維數組在內存之中也是連續存儲的!
內存中的存儲

3. 數組作爲函數參數

往往我們在寫代碼的時候,會將數組作爲參數傳個函數,比如:我要實現一個冒泡排序(這裏要講算法思想)函數將一個整形數組排序。
那我們將會這樣使用該函數:

#include <stdio.h>
int main()
{
	int arr[] = {3,1,7,5,8,9,0,2,4,6};
	bubble_sort(arr);
	for(i=0; i<sizeof(arr)/sizeof(arr[0]); i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}
void bubble_sort(int arr[])
{
	int sz = sizeof(arr)/sizeof(arr[0]);//這樣對嗎?
...
} 

如果錯了,該怎麼設計?

bubble_sort(arr, sz); //使用的時候,傳數組元素個數
void bubble_sort(int arr[], int sz)//參數接收數組元素個數
{
	//...
}

數組作爲函數參數的時候,是不會把整個數組傳遞過去的,實際上只是把數組的首元素的地址傳遞過去了,所以即使在函數參數部分寫成數組的形式:int arr[ ]表示,依然會隱式轉爲一個指針:int * arr

小技巧: 數組傳參,如果函數內部需要知道數組元素個數,應該在函數外部算出元素個數,以參數的形式傳遞給函數。

4. 柔性數組的使用

柔性數組:結構體的最後可以定義一個大小是未知的這樣一個數組。
柔性數組
如果使用sizeof(結構體)的話,其中大小的計算不包含柔性數組。
柔性數組

第肆部分:結構體 枚舉 聯合

一:結構體的聲明

1. 結構體

結構是一些值得集合,這些值被我們稱爲成員變量,結構得每個成員可以是不同類型得變量。

2. 結構體聲明

struct tag
{
	member-list;
}variable-list;

例如我們想要去描述一個學生得具體信息:

typedef struct Stu
{
	char name[20];//名字
	int age;//年齡
	char sex[5];//性別
	char id[20];//學號
}Stu;//分號不能丟

3. 結構體成員的類型

結構體的成員可以是標量、數組、指針,甚至是其他結構體。

  1. 成員是其他結構體的
struct Student
{
	char name[128];
	int age;
	char tel[20];
	struct School p;
};

struct School
{
	int age;
	int size;
}p;

  1. 結構體成員是鏈表
struct Node
{
	int date;
	struct Node* next;
};鏈表

二:結構體變量的定義和初始化

1. 兩種不同的結構體定義

  1. 結構體類型定義
typedef struct Student
{
	char name[128];
	int age;
	char tel[20];
}S; //S是結構體類型

  1. 結構體變量定義
struct Student
{
	char name[128];
	int age;
	char tel[20];
}S; //S是struct Student類型的結構體變量

  1. 無類型結構體變量
struct 
{
	char name[128];
	int  age;
	char tel[20];
}S; //S是無類型類型的結構體變量

2. 結構體聲明和定義

struct Point
{
	int x;
	int y;
}p1; //聲明類型的同時定義變量p1
struct Point p2; //定義結構體變量p2

3. 結構體的初始化

  1. 初始化:定義變量的同時賦初值
struct Point p3 = {x, y};
struct Stu //類型聲明
{
	char name[15];//名字
	int age; //年齡
};
struct Stu s = {"zhangsan", 20};//初始化

  1. 結構體嵌套初始化
struct Node
{
int data;
	struct Point p;
	struct Node* next;
}n1 = {10, {4,5}, NULL}; //結構體嵌套初始化

struct Node n2 = {20, {5, 6}, NULL};//結構體嵌套初始化

實例

// 其中的結構體在之前的定義中
int main()
{
	struct Student s1;
	//初始化
	s1.age = 12;
	strcpy(s1.name,"xiaoming");
	strcpy(s1.tel,"1319688575");

	struct Student s2 = { "xiaowang", 15, "15158451", {2,3} };
	struct Student copy;
	memcpy(&copy, &s1, sizeof(struct Student));

	system("pause");
	return 0;
}

結構體

三:結構體成員的訪問和傳參

1. 結構體成員的訪問

結構體變量訪問成員 結構變量的成員是通過點操作符(.)訪問的。點操作符接受兩個操作數。
結構體 luckily
我們可以看到 s 有成員 name 和 age ; 那我們如何訪問s的成員?

struct S s;
strcpy(s.name, "zhangsan");//使用.訪問name成員
s.age = 20;//使用.訪問age成員

結構體指針訪問指向變量的成員 有時候我們得到的不是一個結構體變量,而是指向一個結構體的指針,那該如何訪問成員。 如下:

struct Stu
{
	char name[20];
	int age;
};

void print(struct Stu* ps)
{
	printf("name = %s age = %d\n", (*ps).name, (*ps).age);
	//使用結構體指針訪問指向對象的成員
	printf("name = %s age = %d\n", ps->name, ps->age);
}
int main()
{
	struct Stu s = {"zhangsan", 20};
	print(&s);//結構體地址傳參
	return 0;
}

2. 結構體傳參

直接用代碼來進行展示:

struct S
{
	int data[1000];
	int num;
};

struct S s = {{1,2,3,4}, 1000};
//結構體傳參
void print1(struct S s)
{
	printf("%d\n", s.num);
}
//結構體地址傳參
void print2(struct S* ps)
{
	printf("%d\n", ps->num);
}
int main()
{
	print1(s); //傳結構體
	print2(&s); //傳地址
	return 0;
} 

上面的printf1printf2函數那個更好一些?
答案:首選printf2函數,原因是函數在進行傳參的時候,參數是需要壓棧的,如果傳遞一個結構體對象的時候,結構體過大,參數壓棧的系統開銷比較大,所以可能會導致性能下降。

小技巧: 結構體傳參的時候,所傳的一定要是結構體的地址,或者說是儘量去傳指針!

&s (struct Student s)
  1. 結構體數組名能夠作爲實參傳給函數;
  2. 結構體變量的地址能夠作爲實參傳給函數;
  3. 結構體中可以含有指向本結構體的指針成員;
  4. 同類的結構體變量,可以進行整體賦值。

四:結構體的內存對齊

1. 對其規則的計算

  1. 第一個成員在與結構體變量偏移量爲0的地址處;
  2. 其他成員變量要對其到某個數字(對齊數)的整數倍的地址處;
    對齊數= 編譯器默認的一個對齊數與該成員大小的較小值 (vs中默認爲8). 如果想要改變默認對齊數:#pragma pack(1)則表示沒有對齊數 括號裏的數字是多少對齊數就是多少。
  3. 結構體總大小爲最大對齊數(每個成員變量都有一個對齊數)的整數倍如果嵌套了結構體的情況,嵌套的結構體對齊到自己的最大對齊數的整數倍處,結構體的整體大小就是所有最大對齊數(含潛逃結構體的對齊數)整數倍。

2. 對齊數的例子

例子1
例子2
例子3
例子4

3. 爲什麼要有內存對齊

實例圖 讓佔用空間小的成員儘量集中在一起, 以空間來換取時間!

五:枚舉

枚舉:一個定義常量的集合;
默認裏面所列舉出來的第一個值是零,如果第一個所舉出來的值給5,則表示從5往後走;
枚舉

1. enumint是否應該劃上等號呢?

在C語言之中,enumint是等價的,搞個枚舉的目的只是爲了代碼的可讀性能夠更好一點;而enum所表示出來的含義,則就是我們所說到的枚舉出來的概念,整個概念是不應該和整數進行劃等號的;而枚舉在內存之中的存儲和int一樣的,都是以字節序+原碼反碼補碼的形式來進行存儲。

六 聯合(共用體)

一種特殊的自動類型,裏面的這些成員,共用一塊空間,誰打就用誰的空間,當最大成員大小不是最大對齊數的整數倍的時候,就要對齊到最大對齊數的整數倍。(最爲特殊的例子就是對於ip的使用)
聯合
例子:大小端

 int  main (){
 	union Un u;
 	u.i=1;
 	if(u.c==1)
 	{
 		printf("小端機");
	}
	else
	{
		printf("大端機");
	}
	return 0;

在這裏插入圖片描述
shili
其中這兩者是相等的。

七:位段

位段的內存佈局是和平臺強相關的,並沒有統一的標準。位段更多的在網絡通信的時候會進行應用,

struct A {
	int _a:2;// _a這個變量只佔2個bit位
	int _b:5;
	int _c:10;
	int _d:30;
  1. 位段的成員必須是int ,unsigned int,或者signed int
  2. 位段的成員後邊有一個冒號和一個數字,代表整個成員佔據幾個bit位。
    在這裏插入圖片描述

第伍部分:字符串函數,動態內存管理,文件

一:字符串函數

1. 字符串

"hello world.\n"
  1. 字符串的結束標誌是一個 \0 的轉義字符。在計算字符串長度strlen()的時候 \0 是結束標誌,不算作字符串內容。一個字符數組如果沒有\0 就不是字符串,就不能夠使用strlen。
  2. Strlen 字符串是以’\0’來作爲結束標誌的 strlen函數返回的是斜槓0前面的出現的字符個數,不包括\0 (sizeof是一個運算符)。
  3. 字符串

2. 字符串函數的實現

爲後面進行字符串函數的實現做準備介紹!
在這裏插入圖片描述
而對於這種斷言的使用,應該更多的注意。
在這裏插入圖片描述

  1. strlen計算長度函數

strlen的使用

	char *p1 = "hello world";
	printf("%d\n", strlen(p1));
	char p2[] = "hello world"; //char p2[] = "hello\\0world";  則長度爲12 大小爲13
	//p2[5] = '0'; 結果還爲11
	//p2[5] = '\0';結果爲5 
	printf("%d\n", strlen(p2));//結果也爲11 但此數組的大小是12
	printf("%d\n", sizeof(p2));

strlen的代碼實現
在這裏插入圖片描述

  1. strcpy拷貝函數
    拷貝函數的實現
char* my_strcpy(char*dst, const char* src)
{
	assert(dst != NULL);
	assert(src != NULL);
	char* ret = dst;
	while (*src != '\0')
	{
		*dst = *src;
		++src;
		++dst;
	}
	*dst = '\0';
	return ret;
}

  1. strcat追接/拼接函數
#include <stdio.h>
#include <assert.h>
#include <string.h>
// strcat  zhuijia pinjie 
char* my_strcat(char* dst, const char* src){
	assert(dst&&dst);
	char *ret = dst;
	while (*dst != '\0')
	{
		++dst;
	}
	while (*dst++ = *src++);//運算符
	//while (*src != '\0'){
	//	*dst = *src;
	//	++dst;
	//	++src;
	//}  
	//*dst = *src;
	return ret;
}


int main(){
	char *p1 = "hello";
	char p2[11] = "world";

	//strcpy(p2,p1);
	strcat(p2, p1);
	return 0;
}

串接函數必須保證所需要串接的函數內存足夠大。
4. strcmp比較函數
Strcmp 所比較字符串每個字符的ASCII碼
在這裏插入圖片描述
代碼的實現

int my_strcmp(const char* str1, const char* str2){
	assert(str1&&str2);
	unsigned char* s1 = (unsigned char*)str1;
	unsigned char* s2 = (unsigned char*)str2;

	while (*s1&&*s2)
	{
		if (*s1 > *s2)
		{
			return 1;
		}
		else if (*s1 < *s2)
		{
			return -1;
		}
		else {
			return 0;
		}
	}

	if (*s1 == '\0'&&*s2 == '\0')
	{
		return 0;
	}
	else if(*s1=='\0')
	{
		return -1;
	}
	else
	{
		return 1;
	}
	return 0;
}


int main(){
	char *p1 = "hello";
	char *p2 = "world";
	printf("%d\n", my_strcmp(p1, p2));// 大於1 小於-1 等於0
	return 0;
}

在這裏插入圖片描述

  1. 12. strncpy strncat strncmp 進行具體數字的操作
#include <stdio.h>
#include <assert.h>
#include <string.h>
// strncpy  kaobeiyidingliang
// strncat  pinjieyidingliang
// strncmp  bijiaoyidingliang

int main(){
	char *p1 = "hellohello";
	char p2[100] = "world";
	strncpy(p2, p1, 5);
	printf("%s\n", p2);

	strncat(p2, p1, 5);
	strncmp(p2, p1, 5);
	return 0;
}

  1. strstr比較一個字符串中是否存在另一個字符串函數
    使用函數
int main(){
	char *p1 = "abcde";
	char *p2 = "deabc";
	char p3[11];
	strcpy(p3, p1);
	strcat(p3, p1);
	if (strstr(p3, p2) != NULL)
	{
		printf("是旋轉字符串\n");
	}
	else
	{
		printf("不是旋轉字符串\n");
	}
	return 0;
}  

實現函數

const char* my_strstr(const char* src, const char* sub){
	assert(src&&sub);

	const char* srci = src;
	const char* subi = sub;
	while (*srci!='\0')
	{
		while (*srci==*subi&&*subi!='\0')
		{
			++srci;
			++subi;
		}
		if (*subi=='\0')
		{
			return src;
		}
		else
		{
			subi = sub;
			++src;
			srci = src;
		}
	}
	return NULL;
}

int main(){
	char *p1 = "abcde";
	char *p2 = "deabc";
	char p3[11];
	strcpy(p3, p1);
	strcat(p3, p1);
	if (my_strstr(p3, p2) != NULL)
	{
		printf("是旋轉字符串\n");
	}
	else
	{
		printf("不是旋轉字符串\n");
	}
	system("pause");
	return 0;
}

  1. strtok分割函數
    在這裏插入圖片描述
    分割函數
  2. strerror函數:返回錯誤碼所對應的信息
#include <errno.h>
#include <string.h>
#include <stdlib.h>
// strerror 

int main(){
	FILE *pFile;
	pFile = fopen("unexist.ent", "r");
	if (pFile == NULL)//errno  輸出錯誤的數字
		printf("Error opening file unexist.ent:%s\n", strerror(errno));
	system("pause");
	return 0;
}

  1. memcpy函數:拷貝內存
    在這裏插入圖片描述
#include <stdio.h>
#include <assert.h>
#include <string.h>
#include <stdlib.h>
// memcpy 所有的拷貝  

void *my_memcpy(void*dst, const void *src, size_t num)
{
	assert(dst&&src);
	char * str_dst = (char*)dst;
	char * str_src = (char*)src;
	for (size_t i = 0; i < num; i++)
	{
		str_dst[i] = str_src[i];
	}
	return dst;
}


struct Student
{
	char _name[10];
	int _age;
}s1;


int main(){
	int a1[10] = { 1, 2, 3, 4, 5 };
	int a2[10];
	memcpy(a2, a1,10*sizeof(int));


	struct Student s2;
	struct Student s1 = { "peter", 18 };
	strcpy(s1._name, s1._name);
	s2._age = s1._age;
	memcpy(&s2, &s1, sizeof(struct Student));
	return 0;
}

  1. memmove函數,移動解決內存重疊的問題
    在這裏插入圖片描述
void *my_memmove(void*dst, const void *src, size_t num)
{
	assert(src&&dst);
	char * str_dst = (char*)dst;
	char * str_src = (char*)src;
	if (str_src < str_dst&&str_dst<str_src+num)//後重疊,或者不重疊,從後往前拷
	{
		for (int i = num - 1; i >= 0; --i)
		{
			str_dst[i] = str_src[i];
		}
	}
	else// 前重疊 不重疊 從前往後拷
	{
		for (size_t i = 0; i < num; ++i)
		{
			str_dst[i] = str_src[i];
		}
	}
	return dst;
}



int main()
{
	int a[10] = { 1, 2, 3, 4, 5};
	//my_memcpy(a + 3, a, 5 * sizeof(int));
	my_memmove(a + 3, a, 5 * sizeof(int));
	for (size_t i = 0; i < 10; ++i)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
	int b[10] = { 1, 2, 3, 4, 5 };
	int c[10];
	my_memmove(c, b, 10 * sizeof(int));
	for (size_t i = 0; i < 10; ++i)
	{
		printf("%d ", c[i]);
	}
	printf("\n");
	system("pause");
	return 0;
}

memmove函數
12.

二:內部存儲結構

記住:二進制思想最早居然是伏羲提出來的!第一次知道的我也表示🐂plus

0. 內存和外存的區別

  1. 內存的訪問速度快,外存的訪問速度慢
  2. 內存存儲空間比較小,外存存儲空間比較大
  3. 內存成本比較高,外存成本更低
  4. 內存掉電之後數據丟失,外存掉電之後數據仍然還在

1. 內部存放形式

因爲CPU只能夠進行加法運算,所以對於整數來說,在內存之中是以補碼的形式存放的。
而在內存中的地址都是四個字節,不會因爲他是char或者int而改變。
符號位的表示

2. 原碼反碼補碼

正數:原碼是其本身 反碼 補碼和原碼是完全相同的
  7  原碼 0000 0111
     反碼 0000 0111
     補碼 0000 0111
 
負數:原碼是其本身 反碼符號位不變,其餘位全部取反,補碼則是在反碼的基礎上加1
 -7   原碼 1000 0111
      反碼 1111 1000
      補碼 1111 1001
零:零分爲正零+0 和負零 -0
  +0  原碼 0000 0000
      反碼 0000 0000
      補碼 0000 0000
  -0  原碼 1000 0000
      反碼 1111 1111
      補碼 0000 0000
補碼往回轉的話,依然是取反+1 切記!!!

3. 大小端

3.1 在計算機系統中是以字節爲單位的,每個地址單元都對應着一個字節,一個字節爲8bit,但在C語言中還有其他類型的存在,因此要對這些多字節安排問題就導致了大端存儲和小端存儲。
大小端節序
3.2 大小端區別
大端是和我們的認知是相符合的,則是從低到高開始進行排列。
小端的話則和我們的認知相反,它是從小往大的排列。
大小端

int 19 
在內存中的存儲以大端序排列則爲 00 01 00 11;
在內存中的存儲以小端序排列則爲 11 00 01 00

一個代碼檢測自己的電腦到底是大端序還是小端序

#include <stdio.h>

int main(){
    int x=1;
    char c=(char)x;
    if(c==1)
    {
    	printf("小端機\n");
    }
    else if(c==0)
    {
    	printf("大端機\n");
    }
    return 0;
}

4. 什麼是ASCII碼

ASCII 是我們所看見的字母和數字在內存之中所在的十進制地址值

三:動態內存管理

1. 內存區域劃分

內存區域劃分

1.1 代碼所存在的區域

相關代碼所產生的變量所存儲的內存區域

void f2(int a,int b) {
	
	printf("f2:%p\n", &a);
	printf("f2:%p\n", &b);
}

void f1() {
	int a = 10;
	int b = 20;
	printf("f1:%p\n", &a);
	printf("f1:%p\n", &b);
	f2(a, b);
}

//全局變量 被放置到
int a = 0;
int b = 0;

int main() {
	
	//棧
	int a = 10;
	int b = 20;
	printf("main:%p\n", &a);
	printf("main:%p\n", &b);
	f1();

	//訪問等於0的a和b
	printf("main:%p\n", :: a);
	printf("main:%p\n",::b);


	//堆
	void* p1 = malloc(4);  //p1 放在棧  *p1 放在堆 
	void* p2 = malloc(4);
	printf("p1:%p\n", &a);
	printf("p2:%p\n", &b);


	// 
	char* p4 = "hello"; //p4在棧 *p4在堆 hello 在常量區


	system("pause");
	return 0;


在這裏插入圖片描述
局部變量存放在棧幀之中;堆向上伸展之中會存在之前釋放的空間,所以是需要注意的。
棧的地址爲何在堆的上面呢?windows是這樣的(而基於Linux則是棧在上面,堆在下面)

例題展示:
在這裏插入圖片描述
例題展示
返回到printf時, 第二個棧幀已經銷燬了,則無法尋找到H的首地址。

在這裏插入圖片描述
在這裏插入圖片描述
對於需要使用大塊內存,並且又需要頻繁申請釋放的話則使用,內存池!

1.2 局部變量和全局變量

訪問全局變量的話需要使用c++ (::a)
全局變量

2. 動態內存管理

動態內存管理

2.1 malloc

Int* a=int(*)malloc(sizeof(int)*100); 申請100個空間不想用的話就還他自由 free(a);

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

2.2 calloc

calloc void* calloc(size_t num, size_t size);把num個大小爲size的元素開闢一個空間,值全部爲0

2.3 recallo

void* realloc(void* ptr,size_t size); 把一個曾經已經申請到的內存進行擴容
Realloc 擴容 重新開個大的,將原來的內容拷貝過去,之後將原來的空間釋放(後面如果有足夠地空間,直接擴容,如果沒有就找足夠地空間)

在這裏插入圖片描述

擴容
在這裏插入圖片描述

2.4 穩定性測試,內存泄漏

在這裏插入圖片描述

  1. 例題1
    在這裏插入圖片描述
    失敗之後返回的是一個NULL指針的訪問!
  2. 例題2
    在這裏插入圖片描述
    一塊內存不能釋放兩次 第一次釋放之後已經給了別人,再次釋放可能釋放的是其他有用的數據 !!!!
    在這裏插入圖片描述
  3. 例題3
    在這裏插入圖片描述
    越界不一定報錯,只有到10的位置纔會檢查,而11則不會
    在這裏插入圖片描述
    不能重複釋放,也不能只釋放一半,必須全部釋放;更不能不斷地重新申請,不釋放結果就會失敗
  4. 例題4
    在這裏插入圖片描述
    程序結束之後申請的內存會自動泄露!
    內存泄漏是指針丟了,內存是不會丟的
    長期的內存泄漏 會導致運行越來越慢 最終掛掉!
  5. 例題5
    在這裏插入圖片描述
    在這裏插入圖片描述
    所出現的燙燙燙和屯屯屯
    在這裏插入圖片描述

四:文件操作

1. 文件

文件
在這裏插入圖片描述

    //2進制
fwrite(&a, sizeof(int), 1, fin);

    //文本
fputs("10000", fin);

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

2. 文件的讀寫

文件的使用方式
在這裏插入圖片描述
文件的隨機讀寫代碼

FILE* fout = fopen("learn3_4_1.c", "r");
    
    // 讀
    char ch = fgetc(fout);
    while (ch != EOF)
    {
        printf("%c", ch);
        ch = fgetc(fout);
    }

    //清空寫
    FILE* fin = fopen("learn3_4_1.c", "w");
    fputc('h', fin);
    fputc('e', fin);
    fputc('l', fin);
    fputc('l', fin);
    fputc('o', fin);

    //追加寫
    FILE* fain = fopen("learn3_4_1.c", "a");
    fputs("#include <stdio.h>", fain);
    fputs("int main{", fain);
    fputs("return 0;", fain);
    fputs("}", fain);

    //定位
    FILE* fdin = fopen("file.txt", "r+");
    char ch = fgetc(fdin);
    while (ch != EOF)
    {
        if (ch == '/')
        {
            char next = fgetc(fdin);
            if (next == '/')
            {
                fputc('*',fdin);
            }
        }
        ch = fgetc(fdin);
    }
fclose(fdin);
}

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
fseek
在這裏插入圖片描述

int main() {
    FILE* fout = fopen("test.txt", "r");
    fseek(fout, 5, SEEK_SET);//設置的文件的開始
    //fseek(fout, 0, SEEK_END);設置的文件的末尾
    //fseek(fout, 5, SEEK_CUR);//跳過幾個位置
    //fseek(fout, 5, SEEK_CUR);//跳過幾個位置




    //1.讀前五個後跳過五個
    //char out_ch = fget(fout);
    //for(int i=0;i<5;i++){
    //}
    // 之後再跳5個

    //2. 算文件的大小
    //fseek(fout, 0, SEEK_END);設置的文件的末尾
    //printf("文件的大小:%d", ftell(fout));// 計算文件大小

    //3.判斷文件結束 如果沒讀到結尾會返回非零
    char ch = fget(fout);
    while (ch != EOF) {
        printf("%c", ch);
        ch = fgetc(fout);
    }
    if (feof(fout) != 0) {
        printf("read end of file\n");
    }
    /*if (ferror(fout) != 0) {
        printf("read error");
    }*/

    //4. 讀視頻
    fseek(fout, 0, SEEK_END); //設置的文件的末尾
        long long n = ftell(fout);
        printf("%d\n",n);
        fseek(fout, 0, SEEK_SET); //設置的文件的kaishi
        while (n--) {
            printf("%c", ch);
            ch = fgetc(fout);
        }

    fclose(fout);
    system("pause");
    return 0;
}

三:文件編譯鏈接

1. 文件編譯鏈接

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
編譯階段進行的內容: const ,constexpr, template, type_tarits ,sizeof, #define.

2. 宏

在這裏插入圖片描述
在這裏插入圖片描述

3. 自動連接/續接

   // 兩個字符串緊靠在一起,就連在一起   自動鏈接
char* p = "hello""bit\n";
	printf("hello", "bit\n");//只打印hello
	printf("%s", p);

4. 條件編譯

在這裏插入圖片描述

#ifdef __DEBUG__//有這樣的就編譯,沒有就不編譯
		printf("%d\n", arr[i]);
#endif//

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

第陸部分:指針

一:指針和指針類型和數組名

背下來的知識點

1. 指針

  1. 指針是變量,是對內存的編號也就是說是用來存放地址的變量,他的字節大小隻和軟件的系統有關係(32位系統只能夠跑32位的系統——4字節,64位系統的話指針大小是8個字節),他的值直接指向存在存儲器中另一個地方的值。
  2. 代碼中創建的變量,纔是合法內存,除此之外,都是非法的內存,如果非法訪問內存,則屬於未定義行爲。
#include <stdio.h>
int main()
{
	int a = 10;//在內存中開闢一塊空間
	int *p = &a;//這裏我們對變量a,取出它的地址,可以使用&操作符。
//將a的地址存放在p變量中,p就是一個之指針變量。
	return 0;
}

2. 指針類型

char *pc = NULL;
int *pi = NULL;
short *ps = NULL;
long *pl = NULL;
float *pf = NULL;
double *pd = NULL;

這裏可以看到,指針的定義方式是: type + * 。 其實: char* 類型的指針是爲了存放 char 類型變量的地址。 short* 類型的指針是爲了存放 short 類型變量的地址。 int* 類型的指針是爲了存放 int 類型變量的地址。
在這裏插入圖片描述

2.1 指針類型的意義

指針的類型決定了指針向前或者是向後走一步有多大(距離)。

int main()
{
	int n = 10;
	char *pc = (char*)&n;
	int *pi = &n;
	printf("%p\n", &n);
	printf("%p\n", pc);
	printf("%p\n", pc+1);
	printf("%p\n", pi);
	printf("%p\n", pi+1);
	return 0;
}

2.2 指針的解引用

指針的類型決定了,對指針解引用的時候有多大的權限(能操作幾個字節)。 比如: char* 的指針解引用就只能訪問一個字節,而 int* 的指針的解引用就能訪問四個字節。

#include <stdio.h>
int main()
{
	int n = 0x11223344;
	char *pc = (char *)&n;
	char *pi = &n;
	*pc = 0x55;//重點在調試的過程中觀察內存的變化。
	*pi = 0; //重點在調試的過程中觀察內存的變化。
	return 0;
}

3. 字符指針

代碼 char* pstr = “hello bit.”; 特別容易讓同學以爲是把字符串 hello bit 放到字符指針 pstr 裏了,但是/本質是把字符串 hello bit. 首字符的地址放到了pstr中。
字符指針
如下例題:

#include <stdio.h>
int main()
{
	char str1[] = "hello bit.";
	char str2[] = "hello bit.";
	char *str3 = "hello bit.";
	char *str4 = "hello bit.";
	if(str1 ==str2)
		printf("str1 and str2 are same\n");
	else
		printf("str1 and str2 are not same\n");
	if(str3 ==str4)
		printf("str3 and str4 are same\n");
	else
		printf("str3 and str4 are not same\n");
return 0;
}

例題內存圖
其中:棧越往下地址越小,常量區的數據不能夠修改。
str1 str2 這兩者都是數組,在棧中會開數組長度的一個大小,將數組內容拷貝進去,而他們之中所存放的是首字母的地址,兩者在棧之中地址是不同的,後進先出的原則,後面的地址會小一些。
str3 str4 這兩者是指針 則只會開4個字節,存放的是首字母h的地址,兩者是相同的。

4. 指針和數組名

#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
printf("%p\n", arr);//%p打印地址
printf("%p\n", &arr[0]);
return 0;
}

由這組代碼的運行結果我們可以看出數組名和數組首元素的地址是一樣的,也就是說數組名錶示的正是數組首元素的地址。則代碼可以這樣寫:

int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr;//p存放的是數組首元素的地址

二:指針運算和數組

1. 指針加減整數

#define N_VALUES 5
float values[N_VALUES];
float *vp;
//指針+-整數;指針的關係運算
for (vp = &values[0]; vp < &values[N_VALUES];)
{
*vp++ = 0;
}

在這裏插入圖片描述

2.指針減指針

int my_strlen(char *s)
{
char *p = s;
while(*p != '\0' )
p++;
return p-s;
}

指針相減

3.指針的關係運算

for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--)
{
*vp = 0;

允許指向數組元素的指針與指向數組最後一個元素後面的那個內存位置的指針比較,但是不允許與指向第一個元素之前的那個內存位置的指針進行比較。

在這裏插入圖片描述

4. 指針和數組

4.1 指針訪問數組

既然可以把數組名當成地址存放到一個指針中,那麼我們就可以使用指針來訪問一個數組。

#include <stdio.h>
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr; //指針存放數組首元素的地址
int sz = sizeof(arr)/sizeof(arr[0]);
for(i=0; i<sz; i++)
{
printf("&arr[%d] = %p <====> p+%d = %p\n", i, &arr[i], i, p+i);
}
return 0;
}

打印地址圖
通過打印結果我們可以發現,p+i其實所計算的就是數組arr下標爲i的地址,因此我們就可以直接通過指針來訪問數組。

int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
int *p = arr; //指針存放數組首元素的地址
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i<sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}

4.2 數組名和&數組名

對於int arr[10];之中的,arr&arr分別是啥?

#include <stdio.h>
int main()
{
int arr[10] = {0};
printf("%p\n", arr);
printf("%p\n", &arr);
return 0;
}

運行結果
通過運行結果我們可以看的出來,兩者所打印出來的地址是完全相同的,但是兩者所表示的意義卻是完全不同的。
在這裏插入圖片描述
實際上: &arr 表示的是數組的地址,而不是數組首元素的地址。(細細體會一下)
數組的地址+1,跳過整個數組的大小,所以 &arr+1 相對於 &arr 的差值是40.
兩者大小的比較
大小比較
數組名和&數組名

三:二級指針和指針數組

1. 二級指針

指針變量也是變量,是變量就有地址,那指針變量的地址存放在哪裏? 這就是 二級指針。
二級指針
*ppa 通過對ppa中的地址進行解引用,這樣找到的是 pa*ppa 其實訪問的就是 pa

int b = 20;
*ppa = &b;//等價於 pa = &b

**ppa 先通過 *ppa 找到 pa ,然後對 pa 進行解引用操作: *pa ,那找到的是 a

**ppa = 30;
//等價於*pa = 30;
//等價於a = 30;

二級指針

二級指針
char** p

在這裏插入圖片描述

2. 指針數組和數組指針

int *p1[10];
int (*p2)[10];

判斷是數組指針,還是指針數組:

  1. (看優先級來判斷) 一般來說方括號[] 的優先級高一些。
  2. ()[ ] 是從左到右看!! 先確定的寫在最後面(也就是按優先級順序開始讀,但是是從後往前開始書寫)!!
  3. 第一個優先級是用來確認它到底是一個什麼 ! int (*p[10])[5] 數組【5】 指針 數組【10】

2.1 指針數組

指針數組是指針還是數組?
答案:是數組。是存放指針的數組 int* arr[5];//是什麼?arr是一個數組,有五個元素,每個元素是一個整形指針。

2.2 數組指針

數組指針是指針?還是數組?
答案是:指針。
如果一個數組指針,想要這樣轉化應該轉化爲數組指針。
轉化

相關一維指針學習列題:
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
其中只有相近的類型纔可以進行強轉,sizeof 算的是它的類型的大小。
相關二維指針學習練習:
在這裏插入圖片描述
在這裏插入圖片描述

3. 指針傳參

在寫代碼的時候難免要將數組或者是指針傳給函數,因此函數應該如何設計呢?
一維數組的傳參:
在這裏插入圖片描述
二維數組的傳參:
在這裏插入圖片描述

#include <stdio.h>
#include <stdlib.h>
//二維數組傳參

//數組在形參位置退化成爲指針
void test1(int(*p)[5], int n,int m)//  建議使用
{
	for (size_t i = 0; i < n; i++)
	{
		for (size_t j = 0; j < m; j++)
		{
			p[i][j]//*(*(p+i)+j)
		}
	}
}
//兩者等價
void test2(int p[][5], int n,int m)//
{
}

int main(){
	int arr[3][5] = { 0 };
	test1(arr, 3,5);
	printf("%d ", sizeof(arr));//60
	printf("%d ", sizeof(arr+1));//4 數組指針 int (*)[5]

	printf("%d ", arr);// 兩者差20  二維數組的首元素是第一行
	printf("%d ", arr+1);//
	//arr[2][3]  等價於*(*(arr+2)+3)
	//test2(arr, 10);


	printf("%d\n", sizeof(arr[2]));//20類型  int* 代表第三行的數組名 
	printf("%d\n", sizeof(arr[2]+1));//4 



	printf("%p\n", arr[2]);//  兩者相差4 加了一個元素
	printf("%p\n", arr[2] + 1);// 

	printf("%p\n", &arr[2]);//  兩者相差20 加了一個數組
	printf("%p\n", &arr[2] + 1);// 
 	system("pause");
	return 0;
}


4. 函數指針

數組做函數參數時,當做普通指針來用

#include <stdio.h>
void test()
{
printf("hehe\n");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}

4.1 函數指針的調用和定義

在這裏插入圖片描述

//函數指針

void test(){
	printf("hehe\n");
}

int main(){
	printf("%p\n", test);//函數指針
	printf("%p\n", &test);//

	int a[10];
	int(*pa)[10] = &a;

	//函數指針變量
	void(*pf)() = test;
	//調用
	test();
	(*pf)();
	pf();
}

//函數指針的定義和調用
typedef void(*P_FUNC)(int);

void f1(int a){
	printf("wolaile:%d\n",a);
}

void (*signal(int ,void(*)(int)))(int);
P_FUNC signal(int, P_FUNC);

int main(){
	void(*pf1)(int);
	P_FUNC pf2=f1;
	pf1(1);
	pf2(2);
	(*pf1)(3);
	(*pf2)(4);
	system("pause");
	return 0;
}

在這裏插入圖片描述

對於指針的一系列應用來說,更加的注重於我們的理解,因此我們應該更多的進行練習。

*(a+i) = a[i] 
*(a+i)+j = a[i] +j
*((a+i)+j) = a[i][j]
*((a+i)+j)+k =  a[i][j]+k
*( *((a+i)+j)+k ) = a[i][j][k]
*( *((a+i)+j)+k )+l =  a[i][j][k]+l

相關練習題:

  1. 例題1
    在這裏插入圖片描述
    內存圖
    在這裏插入圖片描述
  2. 例題2
    在這裏插入圖片描述
  3. 例題3
    在這裏插入圖片描述
    在這裏插入圖片描述
  4. 例題4
    在這裏插入圖片描述
  5. 例題5
    在這裏插入圖片描述
    內存圖
    在這裏插入圖片描述
  6. 例題6
    在這裏插入圖片描述
  7. 例題8
    在這裏插入圖片描述
    在這裏插入圖片描述
  8. 例題
    在這裏插入圖片描述
    在這裏插入圖片描述
    在這裏插入圖片描述
  9. const char* name=“chen”
    name被定義爲指向常量的指針,所以它所指的內容不能改變,但指針本身的內容可以修改,而name[3]=‘q’,修改了name所指的內容,是錯誤的;name==lin,name=new char[5],name=new char('q')以不同的方法修改了常指針,都是正確的。
    (收藏起來之後慢慢看)
    寫在完結處的話:總計42426個字,其中不包括對於一些圖片和代碼的引用,更加基礎和寬泛的將我們所涉及到的衆多的包含C語言基礎和C語言昇華部分的內容做到了一次全面的整合,是幫助自己也是幫助別人,未來的路還長加油。
    (隨後會將所學習到的數據結構,C++等大多數內容也整理出來給大家進行分享)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章