C語言程序設計【基礎篇】


 1.  C語言概述


 

1.1 什么是C語言

  一提到語言這個詞語,自然會想到的是像英語、漢語等這樣的自然語言,因為它是人和人交換信息不可缺少的工具。而今天計算機遍布了我們生活的每一個角落,除了人和人的相互交流之外,我們必須和計算機角落。用什么的什么樣的方式和計算機做最直接的交流呢?人們自然想到的是最古老也最方便的方式——語言,而C語言就是人和計算機交流的一種語言。語言是用來交流溝通的。有一方說,有另一方聽,必須有兩方參與,這是語言最重要的功能:

•說的一方傳遞信息,聽的一方接收信息;

•說的一方下達指令,聽的一方遵循命令做事情。

  語言是人和人交流,C語言是人和機器交流。只是,人可以不聽另外一個人,但是,計算機是無條件服從。語言有獨特的語法規則和定義,雙方必須遵循這些規則和定義才能實現真正的交流。

 

1.2 計算機介紹

1.2.1 計算機結構組成

 

1.2.2 計算機系統組成

 

 

1.2.3 程序和指令

  • l  指令是對計算機進行程序控制的最小單位。
  • l  所有的指令的集合稱為計算機的指令系統。
  • l  程序是為完成一項特定任務而用某種語言編寫的一組指令序列。

 

1.3 語言發展歷程

1.3.1 機器語言

  計算機的大腦或者說心臟就是CPU,它控制着整個計算機的運作。每種CPU,都有自己的指令系統。這個指令系統,就是該CPU的機器語言。機器語言是一組由0和1系列組成的指令碼,這些指令碼,是CPU制作廠商規定出來的,然后發布出來,請程序員遵守。要讓計算機干活,就得用機器語言(二級制數)去命令它。這樣的命令,不是一條兩條,而是上百條。而且不同型號的計算機其機器語言是不相通的,按着一種計算機的機器指令編制的程序,不能在另一種計算機上執行。

1.3.2 匯編語言和編譯器

  機器語言編程是不是很令人煩惱呢,終於出現了匯編語言,就是一些標識符取代0與1。一門人類可以比較輕松認識的編程語言。只是這門語言計算機並不認識,所以人類還不能用這門語言命令計算機做事情。這正如如何才能讓中國人說的話美國人明白呢?——翻譯!所以,有一類專門的程序,既認識機器語言,又認識匯編語言,也就是編譯器,將標識符換成0與1,知道怎么把匯編語言翻譯成機器語言。

1.3.3 高級語言

  匯編語言和機器語言都是面向機器的,機器不同,語言也不同。既然有辦法讓匯編語言翻譯成機器語言,難道就不能把其他更人性化的語言翻譯成機器語言?1954年,Fortran語言出現了,其后相繼出現了其他的類似語言。這批語言,使程序員擺脫了計算機硬件的限制,把主要精力放在了程序設計上,不在關注低層的計算機硬件。這類語言,稱為高級語言。同樣的,高級語言要被計算機執行,也需要一個翻譯程序將其翻譯成機器語言,這就是編譯程序,簡稱編譯器。這類高級語言解決問題的方法是分析出解決問題所需要的步驟,把程序看作是數據被加工的過程。基於這類方法的程序設計語言成為面向過程的語言。C語言就是這種面向過程的程序設計語言。

1.3.4 語言的層次

 

1.3.5 語言進化史

 

 

 

1.4 為什么要學習C語言

1.4.1 C語言特點

1) 優點

  1. 代碼量小
  2. 執行速度快
  3. 功能強大
  4. 編程自由

2) 缺點

  1. 寫代碼實現周期長
  2. 可移植性較差
  3. 過於自由,經驗不足易出錯
  4. 對平台庫依賴較多

1.4.2 學習C語言理由

 

 

1.4.3 C語言應用領域

C語言的應用極其廣泛,從網站后台,到底層操作系統,從多媒體應用到大型網絡游戲,均可使用C語言來開發:

  1.   C語言可以寫網站后台程序
  2.   C語言可以專門針對某個主題寫出功能強大的程序庫
  3.   C語言可以寫出大型游戲的引擎
  4.   C語言可以寫出另一個語言來
  5.   C語言可以寫操作系統和驅動程序,並且只能用C語言編寫
  6.   任何設備只要配置了微處理器,就都支持C語言。從微波爐到手機,都是由C語言技術來推動的

 

1.4.4 C語言的簡潔

C語言僅有32個關鍵字,9種控制語句,34種運算符,卻能完成無數的功能:

 

 

 

 

 

1.4.5 學習C語言的常見困惑

1.5 第一個C語言程序:HelloWorld

1.5.1 編寫C語言代碼:hello.c

#include <stdio.h>
int main(void)
{
       //這是第一個C語言代碼
       printf("hello world\n");
       return 0;
  //C語言的源代碼文件是一個普通的文本文件,但擴展名必須是.c。
}

1.5.2 通過gcc編譯C代碼

1) gcc編譯器介紹

  編輯器(如vi、記事本)是指我用它來寫程序的(編輯代碼),而我們寫的代碼語句,電腦是不懂的,我們需要把它轉成電腦能懂的語句,編譯器就是這樣的轉化工具。就是說,我們用編輯器編寫程序,由編譯器編譯后才可以運行!編譯器是將易於編寫、閱讀和維護的高級計算機語言翻譯為計算機能解讀、運行的低級機器語言的程序。gcc(GNU Compiler Collection,GNU 編譯器套件),是由 GNU 開發的編程語言編譯器。gcc原本作為GNU操作系統的官方編譯器,現已被大多數類Unix操作系統(如Linux、BSD、Mac OS X等)采納為標准的編譯器,gcc同樣適用於微軟的Windows。gcc最初用於編譯C語言,隨着項目的發展gcc已經成為了能夠編譯C、C++、Java、Ada、fortran、Object C、Object C++、Go語言的編譯器大家族。

編譯命令格式:

gcc [-option1] ... <filename>

g++ [-option1] ... <filename>
  • l  命令、選項和源文件之間使用空格分隔
  • l  一行命令中可以有零個、一個或多個選項
  • l  文件名可以包含文件的絕對路徑,也可以使用相對路徑
  • l  如果命令中不包含輸出可執行文件的文件名,可執行文件的文件名會自動生成一個默認名,Linux平台為a.out,Windows平台為a.exe

gcc、g++編譯常用選項說明:

選項

含義

-o file

指定生成的輸出文件名為file

-E

只進行預處理

-S(大寫)

只進行預處理和編譯

-c(小寫)

只進行預處理、編譯和匯編

2) Linux平台下編譯代碼

  • l  gcc warning的意思是警告,是有問題,但不影響編譯
  • l  error是錯誤,如果遇到error,那么就不能編譯了

3) Windows平台下gcc環境配置

windows命令行界面下,默認是沒有gcc編譯器,我們需要配置一下環境。由於我們安裝了Qt,Qt是一個集成開發環境,內部集成gcc編譯器,配置一下環境變量即可使用gcc。

a)進入Qt安裝目錄:

 

b)拷貝gcc所在的路徑(不同安裝目錄有所差異)

C:\Qt\Qt5.5.0\Tools\mingw492_32\bin

c) 設置windows環境變量

計算機(右擊)-> 屬性:

 

如果是win7,雙擊Path后,把拷貝的路徑追加到后面,路徑之間需要英文” ”分隔:

如果是win10,界面做得友好一下,新建添加路徑即可:

d)測試gcc命令:

 

4) Windows平台下編譯代碼

 

 

5) 注意事項

  Linux編譯后的可執行程序只能在Linux運行,Windows編譯后的程序只能在Windows下運行。

  64位的Linux編譯后的程序只能在64位Linux下運行,32位Linux編譯后的程序只能在32位的Linux運行。

  64位的Windows編譯后的程序只能在64位Windows下運行,32位Windows編譯后的程序可以在64位的Windows運行。

1.5.3 代碼分析

1) include頭文件包含

  • l  #include的意思是頭文件包含,#include <stdio.h>代表包含stdio.h這個頭文件
  • l  使用C語言庫函數需要提前包含庫函數對應的頭文件,如這里使用了printf()函數,需要包含stdio.h頭文件
  • l  可以通過man 3 printf查看printf所需的頭文件

 

#include< > 與 #include ""的區別:

  • l  < > 表示系統直接按系統指定的目錄檢索
  • l  "" 表示系統先在 "" 指定的路徑(沒寫路徑代表當前路徑)查找頭文件,如果找不到,再按系統指定的目錄檢索

stdio.h是在操作系統的系統目錄下:

 

2) main函數

  • l  一個完整的C語言程序,是由一個、且只能有一個main()函數(又稱主函數,必須有)和若干個其他函數結合而成(可選)。
  • l  main函數是C語言程序的入口,程序是從main函數開始執行。

 

3) {} 括號,程序體和代碼塊

  • l  {}叫代碼塊,一個代碼塊內部可以有一條或者多條語句
  • l  C語言每句可執行代碼都是";"分號結尾
  • l  所有的#開頭的行,都代表預編譯指令,預編譯指令行結尾是沒有分號的
  • l  所有的可執行語句必須是在代碼塊里面

 

4) 注釋

  • l  //叫行注釋,注釋的內容編譯器是忽略的,注釋主要的作用是在代碼中加一些說明和解釋,這樣有利於代碼的閱讀
  • l  /**/叫塊注釋
  • l  塊注釋是C語言標准的注釋方法
  • l  行注釋是從C++語言借鑒過來的

5) printf函數

  • l  printf是C語言庫函數,功能是向標准輸出設備輸出一個字符串
  • l  printf(“hello world\n”);//\n的意思是回車換行

6) return語句

  • l  return代表函數執行完畢,返回return代表函數的終止
  • l  如果main定義的時候前面是int,那么return后面就需要寫一個整數;如果main定義的時候前面是void,那么return后面什么也不需要寫
  • l  在main函數中return 0代表程序執行成功,return -1代表程序執行失敗
  • l  int main()和void main()在C語言中是一樣的,但C++只接受int main這種定義方式

1.6 system函數

1.6.1 system函數的使用

#include <stdlib.h>
int system(const char *command);
功能:在已經運行的程序中執行另外一個外部程序
參數:外部可執行程序名字
返回值:
成功:不同系統返回值不一樣
失敗:通常是 - 1

示例代碼:

#include <stdio.h>
#include <stdlib.h>
int main() { //system("calc"); //windows平台 system("ls"); //Linux平台, 需要頭文件#include <stdlib.h> return 0; }

1.6.2 system返回值不同系統結果不一樣

C語言所有的庫函數調用,只能保證語法是一致的,但不能保證執行結果是一致的,同樣的庫函數在不同的操作系統下執行結果可能是一樣的,也可能是不一樣的。

在學習Linux發展史時,我們得知Linux的發展離不開POSIX標准,只要符合這個標准的函數,在不同的系統下執行的結果就可以一致。

Unix和linux很多庫函數都是支持POSIX的,但Windows支持的比較差。

如果將Unix代碼移植到Linux一般代價很小,如果把Windows代碼移植到Unix或者Linux就比較麻煩。

1.6.3 Qt圖形界面調用system

 

void Widget::on_pushButton_clicked()
{
    //system("calc"); //需要頭文件:#include <stdlib.h>
    WinExec("calc", SW_NORMAL); //需要頭文件:#include <windows.h>
} void Widget::on_pushButton_2_clicked() { system("notepad"); } void Widget::on_pushButton_3_clicked() { system("mmc"); }

1.6.4 MFC圖形界面調用system

 

 

void CvssystemDlg::OnBnClickedButton1()
{
       // TODO:  在此添加控件通知處理程序代碼

       //WinExec("calc", SW_NORMAL); 需要頭文件:#include <windows.h>
     system("calc"); //需要頭文件:#include <stdlib.h>
 } void CvssystemDlg::OnBnClickedButton2() { // TODO: 在此添加控件通知處理程序代碼 //WinExec("notepad", SW_NORMAL);  system("notepad"); }

1.7 C語言編譯過程

1.7.1 C程序編譯步驟

C代碼編譯成可執行程序經過4步:

  1)預處理:宏定義展開、頭文件展開、條件編譯等,同時將代碼中的注釋刪除,這里並不會檢查語法

  2)編譯:檢查語法,將預處理后文件編譯生成匯編文件

  3)匯編:將匯編文件生成目標文件(二進制文件)

  4)鏈接:C語言寫的程序是需要依賴各種庫的,所以編譯之后還需要把庫鏈接到最終的可執行程序中去

 

1.7.2 gcc編譯過程

1) 分步編譯

預處理:gcc -E   hello.c -o hello.i
編  譯:gcc -S  hello.i -o hello.s 匯 編:gcc -c hello.s -o hello.o 鏈 接:gcc hello.o -o hello_elf

選項

含義

-E

只進行預處理

-S(大寫)

只進行預處理和編譯

-c(小寫)

只進行預處理、編譯和匯編

-o file

指定生成的輸出文件名為 file

 

文件后綴

含義

.c

C 語言文件

.i

預處理后的 C 語言文件

.s

編譯后的匯編文件

.o

編譯后的目標文件

2) 一步編譯

gcc hello.c -o demo(還是經過:預處理、編譯、匯編、鏈接的過程):

 

1.7.3 查找程序所依賴的動態庫

1)Linux平台下,ldd(“l”為字母) 可執行程序:

 

2)Windows平台下,需要相應軟件(Depends.exe):

 

 

1.8 CPU內部結構與寄存器(了解)

1.8.1 64位和32位系統區別

  • l  寄存器是CPU內部最基本的存儲單元
  • l  CPU對外是通過總線(地址、控制、數據)來和外部設備交互的,總線的寬度是8位,同時CPU的寄存器也是8位,那么這個CPU就叫8位CPU
  • l  如果總線是32位,寄存器也是32位的,那么這個CPU就是32位CPU
  • l  有一種CPU內部的寄存器是32位的,但總線是16位,准32位CPU
  • l  所有的64位CPU兼容32位的指令,32位要兼容16位的指令,所以在64位的CPU上是可以識別32位的指令
  • l  在64位的CPU構架上運行了64位的軟件操作系統,那么這個系統是64位
  • l  在64位的CPU構架上,運行了32位的軟件操作系統,那么這個系統就是32位
  • l  64位的軟件不能運行在32位的CPU之上

1.8.2 寄存器名字(了解)

8

16

32

64

A

AX

EAX

RAX

B

BX

EBX

RBX

C

CX

ECX

RCX

D

DX

EDX

RDX

 

1.8.3 寄存器、緩存、內存三者關系

  按與CPU遠近來分,離得最近的是寄存器,然后緩存(CPU緩存),最后內存。

  CPU計算時,先預先把要用的數據從硬盤讀到內存,然后再把即將要用的數據讀到寄存器。於是 CPU<--->寄存器<--->內存,這就是它們之間的信息交換。

  那為什么有緩存呢?因為如果老是操作內存中的同一址地的數據,就會影響速度。於是就在寄存器與內存之間設置一個緩存。

  因為從緩存提取的速度遠高於內存。當然緩存的價格肯定遠遠高於內存,不然的話,機器里就沒有內存的存在。

  由此可以看出,從遠近來看:CPU〈---〉寄存器〈---> 緩存 <---> 內存。

1.9 匯編語言

1.9.1 VS中C語言嵌套匯編代碼(了解)

#include <stdio.h>
int main()
{
      //定義整型變量a, b, c
       int a; int b; int c; __asm { mov a, 3 //3的值放在a對應內存的位置 mov b, 4 //4的值放在a對應內存的位置 mov eax, a //把a內存的值放在eax寄存器 add eax, b //eax和b相加,結果放在eax mov c, eax //eax的值放在c中  } printf("%d\n", c);//把c的值輸出 return 0;//成功完成 }

1.9.2 VS反匯編

#include <stdio.h>
int main(void)
{
       //定義整型變量a, b, c
       int a; int b; int c; a = 3; b = 4; c = a + b; printf("%d\n", c);//把c的值輸出 return 0;//成功完成 }

  1)設置斷點F9

  2)選擇反匯編按鈕 

  3)根據匯編代碼分析程序

1.10 集成開發環境IDE

  集成開發環境(IDE,Integrated Development Environment )是用於提供程序開發環境的應用程序,一般包括代碼編輯器、編譯器、調試器和圖形用戶界面工具。集成了代碼編寫功能、分析功能、編譯功能、調試功能等一體化的開發軟件服務套。所有具備這一特性的軟件或者軟件套(組)都可以叫集成開發環境。

1.10.1 Qt Creator

  Qt Creator是跨平台的 Qt IDE, Qt Creator 是 Qt 被 Nokia 收購后推出的一款新的輕量級集成開發環境(IDE)。此 IDE 能夠跨平台運行,支持的系統包括 Linux(32 位及 64 位)、Mac OS X 以及 Windows。根據官方描述,Qt Creator 的設計目標是使開發人員能夠利用 Qt 這個應用程序框架更加快速及輕易的完成開發任務。

快捷鍵

含義

Ctrl + i

自動格式化代碼

Ctrl + /

注釋/取消注釋

Alt + Enter

自動完成類函數定義

F4

.h 文件和對應.cpp 文件切換

F9

設置斷點

F5

調試運行

Ctrl + r

編譯,但不調試運行

Ctrl + b

編譯,不運行

F10

next調試

F11

step調試

 

1.10.2 Microsoft Visual Studio

  Microsoft Visual Studio(簡稱VS)是美國微軟公司的開發工具包系列產品。VS是一個基本完整的開發工具集,它包括了整個軟件生命周期中所需要的大部分工具,如UML工具、代碼管控工具、集成開發環境(IDE)等等,所寫的目標代碼適用於微軟支持的所有平台。Visual Studio是目前最流行的Windows平台應用程序的集成開發環境。

1) VS常用快捷鍵

快捷鍵

含義

Ctrl + k,Ctrl + f

自動格式化代碼

Ctrl + k,Ctrl + c

注釋代碼

Ctrl + k,Ctrl + u

取消注釋代碼

F9

設置斷點

F5

調試運行

Ctrl + F5

不調試運行

Ctrl + Shift + b

編譯,不運行

F10

next調試

F11

step調試

 

2) VS2013的C4996錯誤

由於微軟在VS2013中不建議再使用C的傳統庫函數scanf,strcpy,sprintf等,所以直接使用這些庫函數會提示C4996錯誤:

 

 

VS建議采用帶_s的函數,如scanf_s、strcpy_s,但這些並不是標准C函數。

 

要想繼續使用此函數,需要在源文件中添加以下指令就可以避免這個錯誤提示:

#define _CRT_SECURE_NO_WARNINGS     //這個宏定義最好要放到.c文件的第一行
#pragma warning(disable:4996)     //或者使用這個

2. 數據類型

2.1 常量與變量

2.1.1 關鍵字

 

 

2.1.2 數據類型

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

2.1.3 常量

常量:

  • l  在程序運行過程中,其值不能被改變的量
  • l  常量一般出現在表達式或賦值語句中

 

整型常量

100,200,-100,0

實型常量

3.14 , 0.125,-3.123

字符型常量

‘a’,‘b’,‘1’,‘\n’

字符串常量

“a”,“ab”,“12356”

 

2.1.4 變量

1) 變量

變量:

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

 

標識符命名規則:

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

 

變量特點:

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

 

 

 

2) 聲明和定義區別

  • l  聲明變量不需要建立存儲空間,如:extern int a;
  • l  定義變量需要建立存儲空間,如:int b;
#include <stdio.h>
int main(void)
{
       //extern 關鍵字只做聲明,不能做任何定義,后面還會學習,這里先了解
       //聲明一個變量a,a在這里沒有建立存儲空間
       extern int a; a = 10; //err, 沒有空間,就不可以賦值 int b = 10; //定義一個變量b,b的類型為int,b賦值為10 return 0; }

從廣義的角度來講聲明中包含着定義,即定義是聲明的一個特例,所以並非所有的聲明都是定義:

  • l  int b 它既是聲明,同時又是定義
  • l  對於 extern b來講它只是聲明不是定義

 

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

2.1.5 使用示例

#include <stdio.h>
#define MAX 10 //聲明了一個常量,名字叫MAX,值是10,常量的值一旦初始化不可改
int main(void)
{
       int a;      //定義了一個變量,其類型為int,名字叫a
       const int b = 10; //定義一個const常量,名為叫b,值為10
       //b = 11; //err,常量的值不能改變
       //MAX = 100;     //err,常量的值不能改變
       a = MAX;//將abc的值設置為MAX的值
       a = 123; printf("%d\n", a); //打印變量a的值 return 0; }

2.2 進制

 

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

 

2.2.1 二進制

二進制是計算技術中廣泛采用的一種數制。二進制數據是用0和1兩個數碼來表示的數。它的基數為2,進位規則是“逢二進一”,借位規則是“借一當二”。

當前的計算機系統使用的基本上是二進制系統,數據在計算機中主要是以補碼的形式存儲的。

 

術語

含義

bit(比特)

一個二進制代表一位,一個位只能表示0或1兩種狀態。數據傳輸是習慣以“位”(bit)為單位。

Byte(字節)

一個字節為8個二進制,稱為8位,計算機中存儲的最小單位是字節。數據存儲是習慣以“字節”(Byte)為單位。

WORD(雙字節)

2個字節,16位

DWORD

兩個WORD,4個字節,32位

1b

1bit,1位

1B

1Byte,1字節,8位

1k,1K

1024

1M(1兆)

1024k, 1024*1024

1G

1024M

1T

1024G

1Kb(千位)

1024bit,1024位

1KB(千字節)

1024Byte,1024字節

1Mb(兆位)

1024Kb = 1024 * 1024bit

1MB(兆字節)

1024KB = 1024 * 1024Byte

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

  

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

 

2.2.2 八進制

八進制,Octal,縮寫OCT或O,一種以8為基數的計數法,采用0,1,2,3,4,5,6,7八個數字,逢八進1。一些編程語言中常常以數字0開始表明該數字是八進制。

八進制的數和二進制數可以按位對應(八進制一位對應二進制三位),因此常應用在計算機語言中。

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

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

 

2.2.3 十六進制

十六進制(英文名稱:Hexadecimal),同我們日常生活中的表示法不一樣,它由0-9,A-F組成,字母不區分大小寫。與10進制的對應關系是:0-9對應0-9,A-F對應10-15。

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

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

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

 

2.2.4 C語言如何表示相應進制數

十進制

以正常數字1-9開頭,如123

八進制

以數字0開頭,如0123

十六進制

以0x開頭,如0x123

二進制

C語言不能直接書寫二進制數

 

#include <stdio.h>
int main(void)
{
       int a = 123;          //十進制方式賦值
       int b = 0123;        //八進制方式賦值, 以數字0開頭
       int c = 0xABC;   //十六進制方式賦值
       //如果在printf中輸出一個十進制數那么用%d,八進制用%o,十六進制是%x
       printf("十進制:%d\n",a ); printf("八進制:%o\n", b); //%o,為字母o,不是數字 printf("十六進制:%x\n", c); return 0; }

2.3 計算機內存數值存儲方式

2.3.1 原碼

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

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

下面數值以1字節的大小描述:

十進制數

原碼

+15

0000 1111

-15

1000 1111

+0

0000 0000

-0

1000 0000

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

2.3.2 反碼

  • l  對於正數,反碼與原碼相同
  • l  對於負數,符號位不變,其它部分取反(1變0,0變1)

 

十進制數

反碼

+15

0000 1111

-15

1111 0000

+0

0000 0000

-0

1111 1111

 

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

 

2.3.3 補碼

在計算機系統中,數值一律用補碼來存儲。

補碼特點:

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

 

十進制數

補碼

+15

0000 1111

-15

1111 0001

+0

0000 0000

-0

0000 0000

 

#include <stdio.h>
int main(void)
{
       int  a = -15; printf("%x\n", a); //結果為 fffffff1 //fffffff1對應的二進制:1111 1111 1111 1111 1111 1111 1111 0001 //符號位不變,其它取反:1000 0000 0000 0000 0000 0000 0000 1110 //上面加1:1000 0000 0000 0000 0000 0000 0000 1111 最高位1代表負數,就是-15 return 0; }

2.3.4 補碼的意義

示例1:用8位二進制數分別表示+0和-0

十進制數

原碼

+0

0000 0000

-0

1000 0000

 

十進制數

反碼

+0

0000 0000

-0

1111 1111

 

不管以原碼方式存儲,還是以反碼方式存儲,0也有兩種表示形式。為什么同樣一個0有兩種不同的表示方法呢?

 

但是如果以補碼方式存儲,補碼統一了零的編碼:

十進制數

補碼

+0

 0000 0000

-0

10000 0000由於只用8位描述,最高位1丟棄,變為0000 0000

 

示例2:計算9-6的結果

以原碼方式相加:

十進制數

原碼

9

0000 1001

-6

1000 0110

 結果為-15,不正確。

以補碼方式相加:

十進制數

補碼

9

0000 1001

-6

1111 1010

 最高位的1溢出,剩余8位二進制表示的是3,正確。

在計算機系統中,數值一律用補碼來存儲,主要原因是:

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

2.4 sizeof關鍵字

  • l  sizeof不是函數,所以不需要包含任何頭文件,它的功能是計算一個數據類型的大小,單位為字節
  • l  sizeof的返回值為size_t
  • l  size_t類型在32位操作系統下是unsigned int,是一個無符號的整數
#include <stdio.h>
int main()
{
       int a; int b = sizeof(a);//sizeof得到指定值占用內存的大小,單位:字節 printf("b = %d\n", b); size_t c = sizeof(a); printf("c = %u\n", c);//用無符號數的方式輸出c的值 return 0; }

2.5整型:int

2.5.1 整型變量的定義和輸出

打印格式

含義

%d

輸出一個有符號的10進制int類型

%o(字母o)

輸出8進制的int類型

%x

輸出16進制的int類型,字母以小寫輸出

%X

輸出16進制的int類型,字母以大寫寫輸出

%u

輸出一個10進制的無符號數

 

#include <stdio.h>
int main()
{
       int a = 123;   //定義變量a,以10進制方式賦值為123
       int b = 0567; //定義變量b,以8進制方式賦值為0567
       int c = 0xabc;      //定義變量c,以16進制方式賦值為0xabc printf("a = %d\n", a); printf("8進制:b = %o\n", b); printf("10進制:b = %d\n", b); printf("16進制:c = %x\n", c); printf("16進制:c = %X\n", c); printf("10進制:c = %d\n", c); unsigned int d = 0xffffffff; //定義無符號int變量d,以16進制方式賦值 printf("有符號方式打印:d = %d\n", d); printf("無符號方式打印:d = %u\n", d); return 0; }

2.5.2 整型變量的輸入

#include <stdio.h>
int main()
{
       int a; printf("請輸入a的值:"); //不要加“\n” scanf("%d", &a); printf("a = %d\n", a); //打印a的值 return 0; }

2.5.3 short、int、long、long long

數據類型

占用空間

short(短整型)

2字節

int(整型)

4字節

long(長整形)

Windows為4字節,Linux為4字節(32位),8字節(64位)

long long(長長整形)

8字節

 

注意:

  • l  需要注意的是,整型數據在內存中占的字節數與所選擇的操作系統有關。雖然 C 語言標准中沒有明確規定整型數據的長度,但 long 類型整數的長度不能短於 int 類型, short 類型整數的長度不能短於 int 類型。
  • l  當一個小的數據類型賦值給一個大的數據類型,不會出錯,因為編譯器會自動轉化。但當一個大的類型賦值給一個小的數據類型,那么就可能丟失高位。

整型常量

所需類型

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類型

%l

輸出long類型

%ll

輸出long long類型

%hu

輸出unsigned short類型

%u

輸出unsigned int類型

%lu

輸出unsigned long類型

%llu

輸出unsigned long long類型

 

#include <stdio.h>
int main()
{
       short a = 10; int b = 10; long c = 10l; //或者10L long long d = 10ll; //或者10LL printf("sizeof(a) = %u\n", sizeof(a)); printf("sizeof(b) = %u\n", sizeof(b)); printf("sizeof(c) = %u\n", sizeof(c)); printf("sizeof(c) = %u\n", sizeof(d)); printf("short a = %hd\n", a); printf("int b = %d\n", b); printf("long c = %ld\n", c); printf("long long d = %lld\n", d); unsigned short a2 = 20u; unsigned int b2 = 20u; unsigned long c2= 20ul; unsigned long long d2 = 20ull; printf("unsigned short a = %hu\n", a2); printf("unsigned int b = %u\n", b2); printf("unsigned long c = %lu\n", c2); printf("unsigned long long d = %llu\n", d2); return 0; }

2.5.4 有符號數和無符號數區別

1) 有符號數

有符號數是最高位為符號位,0代表正數,1代表負數。

#include <stdio.h>
 int main()
{
       signed int a = -1089474374; //定義有符號整型變量a
       printf("%X\n", a); //結果為 BF0FF0BA
       //B       F      0        F       F     0        B           A
       //1011 1111 0000 1111 1111 0000 1011 1010
       return 0; } 

2) 無符號數

無符號數最高位不是符號位,而就是數的一部分,無符號數不可能是負數。

#include <stdio.h>
int main()
{
       unsigned int a = 3236958022; //定義無符號整型變量a
       printf("%X\n", a); //結果為 C0F00F46
       return 0; }

 當我們寫程序要處理一個不可能出現負值的時候,一般用無符號數,這樣可以增大數的表達最大值。

3) 有符號和無符號整型取值范圍

數據類型

占用空間

取值范圍

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)

 

2.6字符型:char

2.6.1 字符變量的定義和輸出

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

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

#include <stdio.h>
int main()
{
       char ch = 'a'; printf("sizeof(ch) = %u\n", sizeof(ch)); printf("ch[%%c] = %c\n", ch); //打印字符 printf("ch[%%d] = %d\n", ch); //打印‘a’ ASCII的值 char A = 'A'; char a = 'a'; printf("a = %d\n", a); //97 printf("A = %d\n", A); //65 printf("A = %c\n", 'a' - 32); //小寫a轉大寫A printf("a = %c\n", 'A' + 32); //大寫A轉小寫a  ch = ' '; printf("空字符:%d\n", ch); //空字符ASCII的值為32 printf("A = %c\n", 'a' - ' '); //小寫a轉大寫A printf("a = %c\n", 'A' + ' '); //大寫A轉小寫a return 0; }

2.6.2 字符變量的輸入

#include <stdio.h>
int main()
{
       char ch; printf("請輸入ch的值:"); //不要加“\n” scanf("%c", &ch); printf("ch = %c\n", ch); //打印ch的字符 return 0; }

2.6.2 ASCII對照表

ASCII

控制字符

ASCII

字符

ASCII

字符

ASCII

字符

0

NUT

32

(space)

64

@

96

1

SOH

33

!

65

A

97

a

2

STX

34

"

66

B

98

b

3

ETX

35

#

67

C

99

c

4

EOT

36

$

68

D

100

d

5

ENQ

37

%

69

E

101

e

6

ACK

38

&

70

F

102

f

7

BEL

39

,

71

G

103

g

8

BS

40

(

72

H

104

h

9

HT

41

)

73

I

105

i

10

LF

42

*

74

J

106

j

11

VT

43

+

75

K

107

k

12

FF

44

,

76

L

108

l

13

CR

45

-

77

M

109

m

14

SO

46

.

78

N

110

n

15

SI

47

/

79

O

111

o

16

DLE

48

0

80

P

112

p

17

DCI

49

1

81

Q

113

q

18

DC2

50

2

82

R

114

r

19

DC3

51

3

83

S

115

s

20

DC4

52

4

84

T

116

t

21

NAK

53

5

85

U

117

u

22

SYN

54

6

86

V

118

v

23

TB

55

7

87

W

119

w

24

CAN

56

8

88

X

120

x

25

EM

57

9

89

Y

121

y

26

SUB

58

:

90

Z

122

z

27

ESC

59

;

91

[

123

{

28

FS

60

92

/

124

|

29

GS

61

=

93

]

125

}

30

RS

62

94

^

126

`

31

US

63

?

95

_

127

DEL

 

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

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

2.6.3 轉義字符

轉義字符

含義

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()
{
       printf("abc"); printf("\refg\n"); //\r切換到句首, \n為換行鍵  printf("abc"); printf("\befg\n");//\b為退格鍵, \n為換行鍵 printf("%d\n", '\123');// '\123'為8進制轉義字符,0123對應10進制數為83 printf("%d\n", '\x23');// '\x23'為16進制轉義字符,0x23對應10進制數為35 return 0; }

2.6.4 數值溢出

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

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

數據類型

占用空間

取值范圍

char

1字節

-128到 127(-27 ~ 27-1)

unsigned char

1字節

0 到 255(0 ~ 28-1)

 

#include <stdio.h>
int main()
{
       char ch; //符號位溢出會導致數的正負發生改變 ch = 0x7f + 2; //127+2 printf("%d\n", ch); // 0111 1111 //+2后 1000 0001,這是負數補碼,其原碼為 1111 1111,結果為-127 //最高位的溢出會導致最高位丟失 unsigned char ch2; ch2 = 0xff+1; //255+1 printf("%u\n", ch2); // 1111 1111 //+1后 10000 0000, char只有8位最高位的溢出,結果為0000 0000,十進制為0  ch2 = 0xff + 2; //255+1 printf("%u\n", ch2); // 1111 1111 //+1后 10000 0001, char只有8位最高位的溢出,結果為0000 0001,十進制為1 return 0; }

2.7實型(浮點型):float、double

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

數據類型

占用空間

有效數字范圍

float

4字節

7位有效數字

double

8字節

15~16位有效數字

 

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

不以f結尾的常量是double類型,以f結尾的常量(如3.14f)是float類型。

#include <stdio.h>
int main()
{
       //傳統方式賦值
       float a = 3.14f; //或3.14F
       double b = 3.14; printf("a = %f\n", a); printf("b = %lf\n", b); //科學法賦值 a = 3.2e3f; //3.2*1000 = 32000,e可以寫E printf("a1 = %f\n", a); a = 100e-3f; //100*0.001 = 0.1 printf("a2 = %f\n", a); a = 3.1415926f; printf("a3 = %f\n", a); //結果為3.141593 return 0; }

2.8類型限定符

限定符

含義

extern

聲明一個變量,extern聲明的變量沒有建立存儲空間。

extern int a;

const

定義一個常量,常量的值不能修改。

const int a = 10;

volatile

防止編譯器優化代碼

register

定義寄存器變量,提高效率。register是建議型的指令,而不是命令型的指令,如果CPU有空閑寄存器,那么register就生效,如果沒有空閑寄存器,那么register無效。

 

2.9字符串格式化輸出和輸入

2.9.1 字符串常量

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

字符串常量與字符常量的不同:

 

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

2.9.2 printf函數和putchar函數

printf是輸出一個字符串,putchar輸出一個char。

printf格式字符:

打印格式

對應數據類型

含義

%d

int

接受整數值並將它表示為有符號的十進制整數

%hd

short int

短整數

%hu

unsigned short

無符號短整數

%o

unsigned int

無符號8進制整數

%u

unsigned int

無符號10進制整數

%x,%X

unsigned int

無符號16進制整數,x對應的是abcdef,X對應的是ABCDEF

%f

float

單精度浮點數

%lf

double

雙精度浮點數

%e,%E

double

科學計數法表示的數,此處"e"的大小寫代表在輸出時用的"e"的大小寫

%c

char

字符型。可以把輸入的數字按照ASCII碼相應轉換為對應的字符

%s

char *

字符串。輸出字符串中的字符直至字符串中的空字符(字符串以'\0‘結尾,這個'\0'即空字符)

%p

void *

以16進制形式輸出指針

%%

%

輸出一個百分號

printf附加格式:

字符

含義

l(字母l)

附加在d,u,x,o前面,表示長整數

-

左對齊

m(代表一個整數)

數據最小寬度

0(數字0)

將輸出的前面補上0直到占滿指定列寬為止不可以搭配使用-

m.n(代表一個整數)

m指域寬,即對應的輸出項在輸出設備上所占的字符數。n指精度,用於說明輸出的實型數的小數位數。對數值型的來說,未指定n時,隱含的精度為n=6位。

#include <stdio.h>
int main()
{
       int a = 100; printf("a = %d\n", a);//格式化輸出一個字符串 printf("%p\n", &a);//輸出變量a在內存中的地址編號 printf("%%d\n"); char c = 'a'; putchar(c);//putchar只有一個參數,就是要輸出的char long a2 = 100; printf("%ld, %lx, %lo\n", a2, a2, a2); long long a3 = 1000; printf("%lld, %llx, %llo\n", a3, a3, a3); int abc = 10; printf("abc = '%6d'\n", abc); printf("abc = '%-6d'\n", abc); printf("abc = '%06d'\n", abc); printf("abc = '%-06d'\n", abc); double d = 12.3; printf("d = \' %-10.3lf \'\n", d); return 0; }

2.9.3 scanf函數與getchar函數

  • l  getchar是從標准輸入設備讀取一個char。
  • l  scanf通過%轉義的方式可以得到用戶通過標准輸入設備輸入的數據。
#include <stdio.h>
int main()
{
       char ch1; char ch2; char ch3; int a; int b; printf("請輸入ch1的字符:"); ch1 = getchar(); printf("ch1 = %c\n", ch1); getchar(); //測試此處getchar()的作用  printf("請輸入ch2的字符:"); ch2 = getchar(); printf("\'ch2 = %ctest\'\n", ch2); getchar(); //測試此處getchar()的作用 printf("請輸入ch3的字符:"); scanf("%c", &ch3);//這里第二個參數一定是變量的地址,而不是變量名  printf("ch3 = %c\n", ch3); printf("請輸入a的值:"); scanf("%d", &a); printf("a = %d\n", a); printf("請輸入b的值:"); scanf("%d", &b); printf("b = %d\n", b); return 0; }

3. 運算符與表達式

3.1 常用運算符分類

運算符類型

作用

算術運算符

用於處理四則運算

賦值運算符

用於將表達式的值賦給變量

比較運算符

用於表達式的比較,並返回一個真值或假值

邏輯運算符

用於根據表達式的值返回真值或假值

位運算符

用於處理數據的位運算

sizeof運算符

用於求字節數長度

 

3.2 算術運算符

運算符

術語

示例

結果

+

正號

+3

3

-

負號

-3

-3

+

10 + 5

15

-

10 - 5

5

*

10 * 5

50

/

10 / 5

2

%

取模(取余)

10 % 3

1

++

前自增

a=2; b=++a;

a=3; b=3;

++

后自增

a=2; b=a++;

a=3; b=2;

--

前自減

a=2; b=--a;

a=1; b=1;

--

后自減

a=2; b=a--;

a=1; b=2;

 

3.3 賦值運算符

運算符

術語

示例

結果

=

賦值

a=2; b=3;

a=2; b=3;

+=

加等於

a=0; a+=2;

a=2;

-=

減等於

a=5; a-=3;

a=2;

*=

乘等於

a=2; a*=2;

a=4;

/=

除等於

a=4; a/=2;

a=2;

%=

模等於

a=3; a%2;

a=1;

 

3.4 比較運算符

C 語言的比較運算中, “真”用數字“1”來表示, “假”用數字“0”來表示。

運算符

術語

示例

結果

==

相等於

4 == 3

0

!=

不等於

4 != 3

1

小於

4 < 3

0

大於

4 > 3

1

<=

小於等於

4 <= 3

0

>=

大於等於

4 >= 1

1

 

3.5 邏輯運算符

運算符

術語

示例

結果

!

!a

如果a為假,則!a為真;

如果a為真,則!a為假。

&&

a && b

如果a和b都為真,則結果為真,否則為假。

||

a || b

如果a和b有一個為真,則結果為真,二者都為假時,結果為假。

 

3.6 運算符優先級

優先級

運算符

名稱或含義

使用形式

結合方向

說明

1

[]

數組下標

數組名[常量表達式]

左到右

--

()

圓括號

(表達式)/函數名(形參表)

--

.

成員選擇(對象)

對象.成員名

--

->

成員選擇(指針)

對象指針->成員名

--

 

2

-

負號運算符

-表達式

右到左

單目運算符

~

按位取反運算符

~表達式

++

自增運算符

++變量名/變量名++

--

自減運算符

--變量名/變量名--

*

取值運算符

*指針變量

&

取地址運算符

&變量名

!

邏輯非運算符

!表達式

(類型)

強制類型轉換

(數據類型)表達式

--

sizeof

長度運算符

sizeof(表達式)

--

 

3

/

表達式/表達式

左到右

雙目運算符

*

表達式*表達式

%

余數(取模)

整型表達式%整型表達式

4

+

表達式+表達式

左到右

雙目運算符

-

表達式-表達式

5

<< 

左移

變量<<表達式

左到右

雙目運算符

>> 

右移

變量>>表達式

 

6

> 

大於

表達式>表達式

左到右

雙目運算符

>=

大於等於

表達式>=表達式

< 

小於

表達式<表達式

<=

小於等於

表達式<=表達式

7

==

等於

表達式==表達式

左到右

雙目運算符

=

不等於

表達式!= 表達式

 

8

&

按位與

表達式&表達式

左到右

雙目運算符

9

^

按位異或

表達式^表達式

左到右

雙目運算符

10

|

按位或

表達式|表達式

左到右

雙目運算符

11

&&

邏輯與

表達式&&表達式

左到右

雙目運算符

12

||

邏輯或

表達式||表達式

左到右

雙目運算符

 

13

?:

條件運算符

表達式1?

表達式2: 表達式3

右到左

三目運算符

 

14

=

賦值運算符

變量=表達式

右到左

--

/=

除后賦值

變量/=表達式

--

*=

乘后賦值

變量*=表達式

--

%=

取模后賦值

變量%=表達式

--

+=

加后賦值

變量+=表達式

--

-=

減后賦值

變量-=表達式

--

<<=

左移后賦值

變量<<=表達式

--

>>=

右移后賦值

變量>>=表達式

--

&=

按位與后賦值

變量&=表達式

--

^=

按位異或后賦值

變量^=表達式

--

|=

按位或后賦值

變量|=表達式

--

 

15

逗號運算符

表達式,表達式,…

左到右

--

 

3.7 類型轉換

數據有不同的類型,不同類型數據之間進行混合運算時必然涉及到類型的轉換問題。

轉換的方法有兩種:

  • l  自動轉換(隱式轉換):遵循一定的規則,由編譯系統自動完成。
  • l  強制類型轉換:把表達式的運算結果強制轉換成所需的數據類型。

類型轉換的原則:占用內存字節數少(值域小)的類型,向占用內存字節數多(值域大)的類型轉換,以保證精度不降低。 

3.7.1 隱式轉換

#include <stdio.h>
int main()
{
       int num = 5; printf("s1=%d\n", num / 2); printf("s2=%lf\n", num / 2.0); return 0; }

3.7.2 強制轉換

強制類型轉換指的是使用強制類型轉換運算符,將一個變量或表達式轉化成所需的類型,其基本語法格式如下所示:

(類型說明符) (表達式)
#include <stdio.h>
int main()
{
       float x = 0; int i = 0; x = 3.6f; i = x; //x為實型, i為整型,直接賦值會有警告 i = (int)x; //使用強制類型轉換  printf("x=%f, i=%d\n", x, i); return 0; }

4. 程序流程結構

4.1 概述

C語言支持最基本的三種程序運行結構:順序結構、選擇結構、循環結構。

  • l  順序結構:程序按順序執行,不發生跳轉。
  • l  選擇結構:依據是否滿足條件,有選擇的執行相應功能。
  • l  循環結構:依據條件是否滿足,循環多次執行某段代碼。

 

4.2 選擇結構

4.2.1 if語句 

 

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

4.2.2 if…else語句

 

#include <stdio.h>
int main()
{
       int a = 1; int b = 2; if (a > b) { printf("%d\n", a); } else { printf("%d\n", b); } return 0; }

4.2.3 if…else if…else語句

 

#include <stdio.h>
int main()
{
       unsigned int a; scanf("%u", &a); if (a < 10) { printf("個位\n"); } else if (a < 100) { printf("十位\n"); } else if (a < 1000) { printf("百位\n"); } else { printf("很大\n"); } return 0; }

4.2.4 三目運算符

#include <stdio.h> 
int main()
{
       int a = 10; int b = 20; int c; if (a > b) { c = a; } else { c = b; } printf("c1 = %d\n", c); a = 1; b = 2; c = ( a > b ? a : b ); printf("c2 = %d\n", c); return 0; }

4.2.5 switch語句

#include <stdio.h>
int main()
{
       char c; c = getchar(); switch (c) //參數只能是整型變量  { case '1': printf("OK\n"); break;//switch遇到break就中斷了 case '2': printf("not OK\n"); break; default://如果上面的條件都不滿足,那么執行default printf("are u ok?\n"); } return 0; }

4.3 循環結構

4.3.1 while語句

 

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

4.3.2 do…while語句 

 

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

4.3.3 for語句

#include <stdio.h>
int main()
{
       int i; int sum = 0; for (i = 0; i <= 100; i++) { sum += i; } printf("sum = %d\n", sum); return 0; }

4.3.4 嵌套循環

循環語句之間可以相互嵌套:

#include <stdio.h>
int main(void)
{
       int num = 0; int i, j, k; for (i = 0; i < 10; i++) { for (j = 0; j < 10; j++) { for (k = 0; k < 10; k++) { printf("hello world\n"); num++; } } } printf("num = %d\n", num); return 0; }

4.4 跳轉語句break、continue、goto

4.3.1 break語句

在switch條件語句和循環語句中都可以使用break語句:

  • l  當它出現在switch條件語句中時,作用是終止某個case並跳出switch結構。
  • l  當它出現在循環語句中,作用是跳出當前內循環語句,執行后面的代碼。
  • l  當它出現在嵌套循環語句中,跳出最近的內循環語句,執行后面的代碼。
#include <stdio.h>
int main()
{
       int i = 0; while (1) { i++; printf("i = %d\n", i); if (i == 10) { break; //跳出while循環  } } int flag = 0; int m = 0; int n = 0; for (m = 0; m < 10; m++) { for (n = 0; n < 10; n++) { if (n == 5) { flag = 1; break; //跳出for (n = 0; n < 10; n++)  } } if (flag == 1) { break; //跳出for (m = 0; m < 10; m++)  } } return 0; }

4.3.2 continue語句

在循環語句中,如果希望立即終止本次循環,並執行下一次循環,此時就需要使用continue語句。

#include<stdio.h>
int main()
{
       int sum = 0;           //定義變量sum
       for (int i = 1; i <= 100; i++) { if (i % 2 == 0) //如果i是一個偶數,執行if語句中的代碼  { continue; //結束本次循環  } sum += i; //實現sum和i的累加  } printf("sum = %d\n", sum); return 0; }

4.3.3 goto語句(無條件跳轉,盡量少用)

#include <stdio.h>
int main()
{
       goto End; //無條件跳轉到End的標識
       printf("aaaaaaaaa\n"); End: printf("bbbbbbbb\n"); return 0; }

5. 數組和字符串

5.1 概述

在程序設計中,為了方便處理數據把具有相同類型的若干變量按有序形式組織起來——稱為數組。

數組就是在內存中連續的相同類型的變量空間。同一個數組所有的成員都是相同的數據類型,同時所有的成員在內存中的地址是連續的。

 

數組屬於構造數據類型:

l  一個數組可以分解為多個數組元素:這些數組元素可以是基本數據類型或構造類型。

       int a[10]; 
struct Stu boy[10];

l  按數組元素類型的不同,數組可分為:數值數組、字符數組、指針數組、結構數組等類別。 

 int a[10];
       char s[10];
char *p[10]; 

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

5.2 一維數組

5.2.1 一維數組的定義和使用

  • l數組名字符合標識符的書寫規定(數字、英文字母、下划線)
  • l數組名不能與其它變量名相同,同一作用域內是唯一的
  • 方括號[]中常量表達式表示數組元素的個數
  • int a[3]表示數組a有3個元素
  • 其下標從0開始計算,因此3個元素分別為a[0],a[1],a[2]
  • l定義數組時[]內最好是常量,使用數組時[]內即可是常量,也可以是變量
#include <stdio.h>
int main(void)
{
       int a[10];//定義了一個數組,名字叫a,有10個成員,每個成員都是int類型
       //a[0]…… a[9],沒有a[10]
       //沒有a這個變量,a是數組的名字,但不是變量名,它是常量
       a[0] = 0; //…… a[9] = 9; int i = 0; for (i = 0; i < 10; i++) { a[i] = i; //給數組賦值  } //遍歷數組,並輸出每個成員的值 for (i = 0; i < 10; i++) { printf("%d ", a[i]); } printf("\n"); return 0; }

 

5.2.2 一維數組的初始化

在定義數組的同時進行賦值,稱為初始化。全局數組若不初始化,編譯器將其初始化為零。局部數組若不初始化,內容為隨機值。    

 int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };//定義一個數組,同時初始化所有成員變量
       int a[10] = { 1, 2, 3 };//初始化前三個成員,后面所有元素都設置為0
       int a[10] = { 0 };//所有的成員都設置為0
        //[]中不定義元素個數,定義時必須初始化
            int a[] = { 1, 2, 3, 4, 5 };//定義了一個數組,有5個成員

5.2.3 數組名

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

#include <stdio.h>
int main()
{
       int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };//定義一個數組,同時初始化所有成員變量
       printf("a = %p\n", a); printf("&a[0] = %p\n", &a[0]); int n = sizeof(a); //數組占用內存的大小,10個int類型,10 * 4 = 40 int n0 = sizeof(a[0]);//數組第0個元素占用內存大小,第0個元素為int,4 int i = 0; for (i = 0; i < sizeof(a) / sizeof(a[0]); i++) { printf("%d ", a[i]); } printf("\n"); return 0; }

5.2.4 強化訓練

1) 一維數組的最值

#include <stdio.h>
int main()
{
       int a[] = {  1, -2, 3,- 4, 5, -6, 7, -8, -9, 10 };//定義一個數組,同時初始化所有成員變量 
       int i = 0; int max = a[0]; for (i = 0; i < sizeof(a) / sizeof(a[0]); i++) { if (a[i] > max) { max = a[i]; } } printf("數組中最大值為:%d\n", max); return 0; }

2) 一維數組的逆置

#include <stdio.h>
int main()
{
       int a[] = {  1, -2, 3,- 4, 5, -6, 7, -8, -9, 10 };//定義一個數組,同時初始化所有成員變量
       int i = 0; int j = sizeof(a) / sizeof(a[0]) -1; int tmp; while (i < j) { tmp = a[i]; a[i] = a[j]; a[j] = tmp; i++; j--; } for (i = 0; i < sizeof(a) / sizeof(a[0]); i++) { printf("%d ", a[i]); } printf("\n"); return 0; }

3) 冒泡法排序

#include <stdio.h>
int main()
{
       int a[] = {  1, -2, 3,- 4, 5, -6, 7, -8, -9, 10 };//定義一個數組,同時初始化所有成員變量
       int i = 0; int j = 0; int n = sizeof(a) / sizeof(a[0]); int tmp; //1、流程 //2、試數 for (i = 0; i < n-1; i++) { for (j = 0; j < n - i -1 ; j++)//內循環的目的是比較相鄰的元素,把大的放到后面  { if (a[j] > a[j + 1]) { tmp = a[j]; a[j] = a[j+1]; a[j+1] = tmp; } } } for (i = 0; i < n; i++) { printf("%d ", a[i]); } printf("\n"); return 0; }

5.3二維數組

5.3.1 二維數組的定義和使用

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

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

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

int a[3][4];

  • l  命名規則同一維數組
  • l  定義了一個三行四列的數組,數組名為a其元素類型為整型,該數組的元素個數為3×4個,即: 二維數組a是按行進行存放的,先存放a[0]行,再存放a[1]行、a[2]行,並且每行有四個元素,也是依次存放的。
  • l  二維數組在概念上是二維的:其下標在兩個方向上變化,對其訪問一般需要兩個下標。
  • l  在內存中並並存在二維數組,二維數組實際的硬件存儲器是連續編址的,也就是說內存中只有一維數組,即放完一行之后順次放入第二行,和一維數組存放方式是一樣的。
#include <stdio.h>
int main()
{
       //定義了一個二維數組,名字叫a
       //由3個一維數組組成,這個一維數組是int [4]
       //這3個一維數組的數組名分別為a[0],a[1],a[2]
       int a[3][4]; a[0][0] = 0; //…… a[2][3] = 12; //給數組每個元素賦值 int i = 0; int j = 0; int num = 0; for (i = 0; i < 3; i++) { for (j = 0; j < 4; j++) { a[i][j] = num++; } } //遍歷數組,並輸出每個成員的值 for (i = 0; i < 3; i++) { for (j = 0; j < 4; j++) { printf("%d, ", a[i][j]); } printf("\n"); } return 0; }

5.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 } }; //連續賦值 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 }; //所有的成員都設置為0 int a[3][4] = {0}; //[]中不定義元素個數,定義時必須初始化 int a[][4] = { 1, 2, 3, 4, 5, 6, 7, 8};

5.3.3 數組名

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

#include <stdio.h>
int main()
{
       //定義了一個二維數組,名字叫a
       //二維數組是本質上還是一維數組,此一維數組有3個元素
//每個元素又是一個一維數組int[4]
       int a[3][4] = { 1, 2, 3, 4 , 5, 6, 7, 8, 9, 10, 11, 12 }; //數組名為數組首元素地址,二維數組的第0個元素為一維數組 //第0個一維數組的數組名為a[0] printf("a = %p\n", a); printf("a[0] = %p\n", a[0]); //測二維數組所占內存空間,有3個一維數組,每個一維數組的空間為4*4 //sizeof(a) = 3 * 4 * 4 = 48 printf("sizeof(a) = %d\n", sizeof(a)); //測第0個元素所占內存空間,a[0]為第0個一維數組int[4]的數組名,4*4=16 printf("sizeof(a[0]) = %d\n", sizeof(a[0]) ); //測第0行0列元素所占內存空間,第0行0列元素為一個int類型,4字節 printf("sizeof(a[0][0]) = %d\n", sizeof(a[0][0])); //求二維數組行數 printf("i = %d\n", sizeof(a) / sizeof(a[0])); // 求二維數組列數 printf("j = %d\n", sizeof(a[0]) / sizeof(a[0][0])); //求二維數組行*列總數 printf("n = %d\n", sizeof(a) / sizeof(a[0][0])); return 0; }

5.3.4 強化訓練

#include <stdio.h>
int main(void)
{
       //二維數組:  五行、三列
       //行代表人:  老大到老五
       //列代表科目:語、數、外
       float a[5][3] = { { 80, 75, 56 }, { 59, 65, 71 }, { 59, 63, 70 }, { 85, 45, 90 }, { 76, 77, 45 } }; int i, j, person_low[3] = { 0 }; float s = 0, lesson_aver[3] = { 0 }; for (i = 0; i < 3; i++) { for (j = 0; j < 5; j++) { s = s + a[j][i]; if (a[j][i] < 60) { person_low[i]++; } } lesson_aver[i] = s / 5; s = 0; } printf("各科的平均成績:\n"); for (i = 0; i < 3; i++) { printf("%.2f\n", lesson_aver[i]); } printf("各科不及格的人數:\n"); for (i = 0; i < 3; i++) { printf("%d\n", person_low[i]); } return 0; }

5.4多維數組(了解)

多維數組的定義與二維數組類似,其語法格式具體如下:

數組類型修飾符 數組名 [n1][n2]…[nn];
int a[3][4][5];

定義了一個三維數組,數組的名字是a,數組的長度為3,每個數組的元素又是一個二維數組,這個二維數組的長度是4,並且這個二維數組中的每個元素又是一個一維數組,這個一維數組的長度是5,元素類型是int。 

#include <stdio.h>
int main(void)
{
       //int a[3][4][5] ;//定義了一個三維數組,有3個二維數組int[4][5]
       int a[3][4][5] = { { { 1, 2, 3, 4, 5 }, { 6, 7, 8, 9, 10 }, { 0 }, { 0 } }, { { 0 }, { 0 }, { 0 }, { 0 } }, { { 0 }, { 0 }, { 0 }, { 0 } } }; int i, j, k; for (i = 0; i < 3; i++) { for (j = 0; j < 4; j++) { for (k = 0; k < 5; k++) { //添加訪問元素代碼 printf("%d, ", a[i][j][k]); } printf("\n"); } } return 0; }

5.5 字符數組與字符串

5.5.1 字符數組與字符串區別

  • l  C語言中沒有字符串這種數據類型,可以通過char的數組來替代;
  • l  字符串一定是一個char的數組,但char的數組未必是字符串;
  • l  數字0(和字符‘\0’等價)結尾的char數組就是一個字符串,但如果char數組沒有以數字0結尾,那么就不是一個字符串,只是普通字符數組,所以字符串是一種特殊的char的數組。
#include <stdio.h>
int main(void)
{
       char c1[] = { 'c', ' ', 'p', 'r', 'o', 'g' }; //普通字符數組
       printf("c1 = %s\n", c1); //亂碼,因為沒有’\0’結束符

       //以‘\0’(‘\0’就是數字0)結尾的字符數組是字符串
       char c2[] = { 'c', ' ', 'p', 'r', 'o', 'g', '\0'}; printf("c2 = %s\n", c2); //字符串處理以‘\0’(數字0)作為結束符,后面的'h', 'l', 'l', 'e', 'o'不會輸出 char c3[] = { 'c', ' ', 'p', 'r', 'o', 'g', '\0', 'h', 'l', 'l', 'e', 'o', '\0'}; printf("c3 = %s\n", c3); return 0; }

5.5.2 字符串的初始化

#include <stdio.h>
// C語言沒有字符串類型,通過字符數組模擬
// C語言字符串,以字符‘\0’, 數字0
int main(void)
{
       //不指定長度, 沒有0結束符,有多少個元素就有多長
       char buf[] = { 'a', 'b', 'c' }; printf("buf = %s\n", buf); //亂碼 //指定長度,后面沒有賦值的元素,自動補0 char buf2[100] = { 'a', 'b', 'c' }; printf("buf2 = %s\n", buf2); //所有元素賦值為0 char buf3[100] = { 0 }; //char buf4[2] = { '1', '2', '3' };//數組越界 char buf5[50] = { '1', 'a', 'b', '0', '7' }; printf("buf5 = %s\n", buf5); char buf6[50] = { '1', 'a', 'b', 0, '7' }; printf("buf6 = %s\n", buf6); char buf7[50] = { '1', 'a', 'b', '\0', '7' }; printf("buf7 = %s\n", buf7); //使用字符串初始化,編譯器自動在后面補0,常用 char buf8[] = "agjdslgjlsdjg"; //'\0'后面最好不要連着數字,有可能幾個數字連起來剛好是一個轉義字符 //'\ddd'八進制字義字符,'\xdd'十六進制轉移字符 // \012相當於\n char str[] = "\012abc"; printf("str == %s\n", str); return 0; }

5.5.3 字符串的輸入輸出

由於字符串采用了'\0'標志,字符串的輸入輸出將變得簡單方便。

#include <stdio.h> 
int main(void)
{
       char str[100]; printf("input string1 : \n"); scanf("%s", str);//scanf(“%s”,str)默認以空格分隔 printf("output:%s\n", str); return 0; }

5.5.4 強化訓練:字符串追加

#include <stdio.h>
int main(void)
{
       char str1[] = "abcdef"; char str2[] = "123456"; char dst[100]; int i = 0; while (str1[i] != 0) { dst[i] = str1[i]; i++; } int j = 0; while (str2[j] != 0) { dst[i + j] = str2[j]; j++; } dst[i + j] = 0; //字符串結束符 printf("dst = %s\n", dst); return 0; }

5.5.5 函數的調用:產生隨機數

當調用函數時,需要關心5要素:

  • l  頭文件:包含指定的頭文件
  • l  函數名字:函數名字必須和頭文件聲明的名字一樣
  • l  功能:需要知道此函數能干嘛后才調用
  • l  參數:參數類型要匹配
  • l  返回值:根據需要接收返回值
#include <time.h>
time_ttime(time_t *t);
功能:獲取當前系統時間
參數:常設置為NULL
返回值:當前系統時間, time_t 相當於long類型,單位為毫秒

#include <stdlib.h>
voidsrand(unsignedintseed);
功能:用來設置rand()產生隨機數時的隨機種子
參數:如果每次seed相等,rand()產生隨機數相等
返回值:無

#include <stdlib.h>
intrand(void);
功能:返回一個隨機數值
參數:無
返回值:隨機數

#include <stdio.h>
#include <time.h>
#include <stdlib.h>
int main()
{
       time_t tm = time(NULL);//得到系統時間
       srand((unsigned int)tm);//隨機種子只需要設置一次即可
       int r = rand();
       printf("r = %d\n", r);
     return 0;
}

5.5.6 字符串處理函數

1) gets()

#include <stdio.h>
char *gets(char *s);
功能:從標准輸入讀入字符,並保存到s指定的內存空間,直到出現換行符或讀到文件結尾為止。
參數:
       s:字符串首地址
返回值:
       成功:讀入的字符串
       失敗:NULL
gets(str)與scanf(“%s”,str)的區別:
 (1)gets(str)允許輸入的字符串含有空格
 (2)scanf(“%s”,str)不允許含有空格

注意:由於scanf()和gets()無法知道字符串s大小,必須遇到換行符或讀到文件結尾為止才接收輸入,因此容易導致字符數組越界(緩沖區溢出)的情況。

       char str[100];
       printf("請輸入str: ");
       gets(str);
       printf("str = %s\n", str);

2) fgets()

#include <stdio.h>

char *fgets(char *s, intsize, FILE *stream);

功能:從stream指定的文件內讀入字符,保存到s所指定的內存空間,直到出現換行字符、讀到文件結尾或是已讀了size - 1個字符為止,最后會自動加上字符 '\0' 作為字符串結束。

參數:
       s:字符串
       size:指定最大讀取字符串的長度(size - 1)
       stream:文件指針,如果讀鍵盤輸入的字符串,固定寫為stdin
返回值:
       成功:成功讀取的字符串
       讀到文件尾或出錯: NULL

fgets()在讀取一個用戶通過鍵盤輸入的字符串的時候,同時把用戶輸入的回車也做為字符串的一部分。通過scanf和gets輸入一個字符串的時候,不包含結尾的“\n”,但通過fgets結尾多了“\n”。fgets()函數是安全的,不存在緩沖區溢出的問題。

       char str[100];
       printf("請輸入str: ");
       fgets(str, sizeof(str), stdin);
       printf("str = \"%s\"\n", str);

3) puts()

#include <stdio.h>
intputs(constchar *s);

功能:標准設備輸出s字符串,在輸出完成后自動輸出一個'\n'。

參數:
       s:字符串首地址
返回值:
       成功:非負數
       失敗:-1
#include <stdio.h>
int main()
{
       printf("hello world"); puts("hello world"); return 0; }

4) fputs()

#include <stdio.h>
int fputs(constchar * str, FILE * stream);

功能:將str所指定的字符串寫入到stream指定的文件中, 字符串結束符 '\0'  不寫入文件。

參數:
       str:字符串
       stream:文件指針,如果把字符串輸出到屏幕,固定寫為stdout
返回值:
       成功:0
       失敗:-1

fputs()是puts()的文件操作版本,但fputs()不會自動輸出一個'\n'。

       printf("hello world");

       puts("hello world");

       fputs("hello world", stdout);

5) strlen()

#include <string.h>
size_tstrlen(constchar *s);

功能:計算指定指定字符串s的長度,不包含字符串結束符‘\0’

參數:
s:字符串首地址

返回值:字符串s的長度,size_t為unsigned int類型
       char str[] = "abcdefg";
       int n = strlen(str);
       printf("n = %d\n", n);

6) strcpy()

#include <string.h>
char *strcpy(char *dest, constchar *src);

功能:把src所指向的字符串復制到dest所指向的空間中,'\0'也會拷貝過去

參數:

       dest:目的字符串首地址
       src:源字符首地址

返回值:
       成功:返回dest字符串的首地址
       失敗:NULL

注意:如果參數dest所指的內存空間不夠大,可能會造成緩沖溢出的錯誤情況。

char dest[20] = "123456789";
       char src[] = "hello world";
       strcpy(dest, src);
       printf("%s\n", dest);

7) strncpy()

#include <string.h>

char *strncpy(char *dest, constchar *src, size_tn);

功能:把src指向字符串的前n個字符復制到dest所指向的空間中,是否拷貝結束符看指定的長度是否包含'\0'。

參數:
       dest:目的字符串首地址
       src:源字符首地址
       n:指定需要拷貝字符串個數

返回值:
       成功:返回dest字符串的首地址
       失敗:NULL

       char dest[20] ;
       char src[] = "hello world";

       strncpy(dest, src, 5);
       printf("%s\n", dest);
       dest[5] = '\0';
       printf("%s\n", dest);

8) strcat()

#include <string.h>

char *strcat(char *dest, constchar *src);

功能:將src字符串連接到dest的尾部,‘\0’也會追加過去

參數:

       dest:目的字符串首地址

       src:源字符首地址

返回值:

       成功:返回dest字符串的首地址

       失敗:NULL

       char str[20] = "123";

       char *src = "hello world";

       printf("%s\n", strcat(str, src));

9) strncat()

#include <string.h>

char *strncat(char *dest, constchar *src, size_tn);

功能:將src字符串前n個字符連接到dest的尾部,‘\0’也會追加過去

參數:

       dest:目的字符串首地址

       src:源字符首地址

       n:指定需要追加字符串個數

返回值:

       成功:返回dest字符串的首地址

       失敗:NULL

       char str[20] = "123";

       char *src = "hello world";

       printf("%s\n", strncat(str, src, 5));

10) strcmp()

#include <string.h>

intstrcmp(constchar *s1, constchar *s2);

功能:比較 s1 和 s2 的大小,比較的是字符ASCII碼大小。

參數:
       s1:字符串1首地址
       s2:字符串2首地址

返回值:
       相等:0
       大於:>0
       小於:<0

    char *str1 = "hello world";
       char *str2 = "hello mike";
       if (strcmp(str1, str2) == 0)
       {
              printf("str1==str2\n");
       }
       else if (strcmp(str1, str2) > 0)
       {
              printf("str1>str2\n");
       }    
       else
       {
              printf("str1<str2\n");
       }

11) strncmp()

#include <string.h>

intstrncmp(constchar *s1, constchar *s2, size_tn);

功能:比較 s1 和 s2 前n個字符的大小,比較的是字符ASCII碼大小。
參數: s1:字符串1首地址 s2:字符串2首地址 n:指定比較字符串的數量 返回值: 相等:0 大於: > 0 小於: < 0 char *str1 = "hello world"; char *str2 = "hello mike"; if (strncmp(str1, str2, 5) == 0) { printf("str1==str2\n"); } else if (strcmp(str1, "hello world") > 0) { printf("str1>str2\n"); } else { printf("str1<str2\n"); }

12) sprintf()

#include <stdio.h>
intsprintf(char *_CRT_SECURE_NO_WARNINGS, constchar *format, ...);

功能:根據參數format字符串來轉換並格式化數據,然后將結果輸出到str指定的空間中,直到出現字符串結束符 '\0'  為止。

參數:
       str:字符串首地址
       format:字符串格式,用法和printf()一樣

返回值:
       成功:實際格式化的字符個數
       失敗: - 1

       char dst[100] = { 0 };
       int a = 10;
       char src[] = "hello world";
       printf("a = %d, src = %s", a, src);
       printf("\n");
       int len = sprintf(dst, "a = %d, src = %s", a, src);
       printf("dst = \" %s\"\n", dst);
       printf("len = %d\n", len);

13) sscanf()

#include <stdio.h>
intsscanf(constchar *str, constchar *format, ...);

功能:從str指定的字符串讀取數據,並根據參數format字符串來轉換並格式化數據。

參數:
       str:指定的字符串首地址
       format:字符串格式,用法和scanf()一樣

返回值:
       成功:參數數目,成功轉換的值的個數
       失敗: - 1

       char src[] = "a=10, b=20";
       int a;
       int b;
       sscanf(src, "a=%d,  b=%d", &a, &b);
       printf("a:%d, b:%d\n", a, b);

14) strchr()

#include <string.h>
char *strchr(const char *s, int c);

功能:在字符串s中查找字母c出現的位置

參數:
       s:字符串首地址
       c:匹配字母(字符)

返回值:
       成功:返回第一次出現的c地址
       失敗:NULL

       char src[] = "ddda123abcd";
       char *p = strchr(src, 'a');
       printf("p = %s\n", p);

15) strstr()

#include <string.h>
char *strstr(constchar *haystack, constchar *needle);

功能:在字符串haystack中查找字符串needle出現的位置

參數:
       haystack:源字符串首地址
       needle:匹配字符串首地址

返回值:
       成功:返回第一次出現的needle地址
       失敗:NULL

       char src[] = "ddddabcd123abcd333abcd";
       char *p = strstr(src, "abcd");
       printf("p = %s\n", p);

16) strtok()

#include <string.h>
char *strtok(char *str, constchar *delim);

功能:來將字符串分割成一個個片段。當strtok()在參數s的字符串中發現參數delim中包含的分割字符時, 則會將該字符改為\0 字符,當連續出現多個時只替換第一個為\0。

參數:
       str:指向欲分割的字符串
       delim:為分割字符串中包含的所有字符

返回值:
       成功:分割后字符串首地址
       失敗:NULL

// 在第一次調用時:strtok()必需給予參數s字符串
// 往后的調用則將參數s設置成NULL,每次調用成功則返回指向被分割出片段的指針

       char a[100] = "adc*fvcv*ebcy*hghbdfg*casdert";
       char *s = strtok(a, "*");//將"*"分割的子串取出
       while (s != NULL)
       {
              printf("%s\n", s);
              s = strtok(NULL, "*");
       }

17) atoi()

#include <stdlib.h>
intatoi(constchar *nptr);

功能:atoi()會掃描nptr字符串,跳過前面的空格字符,直到遇到數字或正負號才開始做轉換,而遇到非數字或字符串結束符('\0')才結束轉換,並將結果返回返回值。

參數:
       nptr:待轉換的字符串

返回值:
    成功轉換后整數
 
類似的函數有:
// atof():把一個小數形式的字符串轉化為一個浮點數。
// atol():將一個字符串轉化為long類型

       char str1[] = "-10";
       int num1 = atoi(str1);
       printf("num1 = %d\n", num1);

       char str2[] = "0.123";
       double num2 = atof(str2);
       printf("num2 = %lf\n", num2); 
 

6. 函數

6.1 概述

6.1.1 函數分類

C 程序是由函數組成的,我們寫的代碼都是由主函數 main()開始執行的。函數是 C 程序的基本模塊,是用於完成特定任務的程序代碼單元。

從函數定義的角度看,函數可分為系統函數和用戶定義函數兩種:

  • l  系統函數,即庫函數:這是由編譯系統提供的,用戶不必自己定義這些函數,可以直接使用它們,如我們常用的打印函數printf()。
  • l  用戶定義函數:用以解決用戶的專門需要。

6.1.2 函數的作用

// 1. 函數的使用可以省去重復代碼的編寫,降低代碼重復率

// 求兩數的最大值
int max(int a, int b)
{
       if (a > b){ return a; } else{ return b; } } int main() { // 操作1 …… // …… int a1 = 10, b1 = 20, c1 = 0; c1 = max(a1, b1); // 調用max() // 操作2 …… // …… int a2 = 11, b2 = 21, c2 = 0; c2 = max(a2, b2); // 調用max() // …… return 0; } // 2. 函數可以讓程序更加模塊化,從而有利於程序的閱讀,修改和完善

假如我們編寫一個實現以下功能的程序:讀入一行數字;對數字進行排序;找到它們的平均值;打印出一個柱狀圖。如果我們把這些操作直接寫在main()里,這樣可能會給用戶感覺代碼會有點凌亂。但,假如我們使用函數,這樣可以讓程序更加清晰、模塊化:

#include <stdio.h>
int main()
{
       float list[50]; // 這里只是舉例,函數還沒有實現 readlist(list, 50); sort(list, 50); average(list, 50); bargraph(list, 50); return 0; } 

這里我們可以這么理解,程序就像公司,公司是由部門組成的,這個部門就類似於C程序的函數。默認情況下,公司就是一個大部門( 只有一個部門的情況下 ),相當於C程序的main()函數。如果公司比較小( 程序比較小 ),因為任務少而簡單,一個部門即可( main()函數 )勝任。但是,如果這個公司很大( 大型應用程序 ),任務多而雜,如果只是一個部門管理( 相當於沒有部門,沒有分工 ),我們可想而知,公司管理、運營起來會有多混亂,不是說這樣不可以運營,只是這樣不完美而已,如果根據公司要求分成一個個部門( 根據功能封裝一個一個函數 ),招聘由行政部門負責,研發由技術部門負責等,這樣就可以分工明確,結構清晰,方便管理,各部門之間還可以相互協調。

6.2 函數的定義

6.2.1 函數定義格式

函數定義的一般形式:
       返回類型 函數名(形式參數列表)
       {
              數據定義部分;
              執行語句部分;
}

6.2.2 函數名字、形參、函數體、返回值

1) 函數名

理論上是可以隨意起名字,最好起的名字見名知意,應該讓用戶看到這個函數名字就知道這個函數的功能。注意,函數名的后面有個圓換號(),代表這個為函數,不是普通的變量名。

2) 形參列表

在定義函數時指定的形參,在未出現函數調用時,它們並不占內存中的存儲單元,因此稱它們是形式參數或虛擬參數,簡稱形參,表示它們並不是實際存在的數據,所以,形參里的變量不能賦值。

void max(int a = 10, int b = 20) // error, 形參不能賦值
{

}

 在定義函數時指定的形參,必須是,類型+變量的形式:

  //1: right, 類型+變量
  void max(int a, int b)
  {
  
  }

  //2: error, 只有類型,沒有變量

  void max(int, int)

  {

  }

  //3: error, 只有變量,沒有類型

  int a, int b;

  void max(a, b)

  {

  }

在定義函數時指定的形參,可有可無,根據函數的需要來設計,如果沒有形參,圓括號內容為空,或寫一個void關鍵字:

// 沒形參, 圓括號內容為空
void max()
{
}

// 沒形參, 圓括號內容為void關鍵字
void max(void)
{
}

3) 函數體

花括號{ }里的內容即為函數體的內容,這里為函數功能實現的過程,這和以前的寫代碼沒太大區別,以前我們把代碼寫在main()函數里,現在只是把這些寫到別的函數里。

4) 返回值

函數的返回值是通過函數中的return語句獲得的,return后面的值也可以是一個表達式。

a)盡量保證return語句中表達式的值和函數返回類型是同一類型。
int max() // 函數的返回值為int類型

{
       int a = 10;
       return a;// 返回值a為int類型,函數返回類型也是int,匹配
}
b)如果函數返回的類型和return語句中表達式的值不一致,則以函數返回類型為准,即函數返回類型決定返回值的類型。對數值型數據,可以自動進行類型轉換。
double max() // 函數的返回值為double類型

{
       int a = 10;
       return a;// 返回值a為int類型,它會轉為double類型再返回
}
注意:如果函數返回的類型和return語句中表達式的值不一致,而它又無法自動進行類型轉換,程序則會報錯。

c)return語句的另一個作用為中斷return所在的執行函數,類似於break中斷循環、switch語句一樣。

int max()

{
       return 1;// 執行到,函數已經被中斷,所以下面的return 2無法被執行到
       return 2;// 沒有執行
}

d)如果函數帶返回值,return后面必須跟着一個值,如果函數沒有返回值,函數名字的前面必須寫一個void關鍵字,這時候,我們寫代碼時也可以通過return中斷函數(也可以不用),只是這時,return后面不帶內容( 分號“;”除外)。

void max()// 最好要有void關鍵字
{
       return; // 中斷函數,這個可有可無
}

6.3 函數的調用

定義函數后,我們需要調用此函數才能執行到這個函數里的代碼段。這和main()函數不一樣,main()為編譯器設定好自動調用的主函數,無需人為調用,我們都是在main()函數里調用別的函數,一個 C 程序里有且只有一個main()函數。

6.3.1函數執行流程

#include <stdio.h>
void print_test()
{
       printf("this is for test\n");
}
int main()
{
       print_test();  // print_test函數的調用
       return 0;
}

1)  進入main()函數

2)  調用print_test()函數:

  1. 它會在main()函數的前尋找有沒有一個名字叫“print_test”的函數定義;
  2. 如果找到,接着檢查函數的參數,這里調用函數時沒有傳參,函數定義也沒有形參,參數類型匹配;
  3. 開始執行print_test()函數,這時候,main()函數里面的執行會阻塞( 停 )在print_test()這一行代碼,等待print_test()函數的執行。

3)  print_test()函數執行完( 這里打印一句話 ),main()才會繼續往下執行,執行到return 0, 程序執行完畢。

6.3.2 函數的形參和實參

  • l  形參出現在函數定義中,在整個函數體內都可以使用,離開該函數則不能使用。
  • l  實參出現在主調函數中,進入被調函數后,實參也不能使用。
  • l  實參變量對形參變量的數據傳遞是“值傳遞”,即單向傳遞,只由實參傳給形參,而不能由形參傳回來給實參。
  • l  在調用函數時,編譯系統臨時給形參分配存儲單元。調用結束后,形參單元被釋放。
  • l  實參單元與形參單元是不同的單元。調用結束后,形參單元被釋放,函數調用結束返回主調函數后則不能再使用該形參變量。實參單元仍保留並維持原值。因此,在執行一個被調用函數時,形參的值如果發生改變,並不會改變主調函數中實參的值。

6.3.3 無參函數調用

如果是調用無參函數,則不能加上“實參”,但括號不能省略。

// 函數的定義
void test()
{

}
int main()
{
       // 函數的調用
       test();     // right, 圓括號()不能省略
       test(250); // error, 函數定義時沒有參數
return 0;
}

6.3.4有參函數調用

a)如果實參表列包含多個實參,則各參數間用逗號隔開。

// 函數的定義
void test(int a, int b)
{

}
int main()
{
       int p = 10, q = 20;
       test(p, q);      // 函數的調用
      return 0;
}

b)實參與形參的個數應相等,類型應匹配(相同或賦值兼容)。實參與形參按順序對應,一對一地傳遞數據。

 

c)實參可以是常量、變量或表達式,無論實參是何種類型的量,在進行函數調用時,它們都必須具有確定的值,以便把這些值傳送給形參。所以,這里的變量是在圓括號( )外面定義好、賦好值的變量。

// 函數的定義
void test(int a, int b)
{

}
int main()
{
       // 函數的調用
       int p = 10, q = 20;
       test(p, q);      // right
       test(11, 30 - 10); // right
       test(int a, int b); // error, 不應該在圓括號里定義變量
       return 0;
}

6.3.5 函數返回值

a)如果函數定義沒有返回值,函數調用時不能寫void關鍵字,調用函數時也不能接收函數的返回值。

// 函數的定義
void test()
{

}
int main()
{
       // 函數的調用
       test(); // right
       void test(); // error, void關鍵字只能出現在定義,不可能出現在調用的地方
       int a = test(); // error, 函數定義根本就沒有返回值
       return 0;
}

b)如果函數定義有返回值,這個返回值我們根據用戶需要可用可不用,但是,假如我們需要使用這個函數返回值,我們需要定義一個匹配類型的變量來接收。

// 函數的定義, 返回值為int類型

int test()

{

}

int main() { // 函數的調用 int a = test(); // right, a為int類型 int b; b = test(); // right, 和上面等級 char *p = test(); // 雖然調用成功沒有意義, p為char *, 函數返回值為int, 類型不匹配 // error, 必須定義一個匹配類型的變量來接收返回值 // int只是類型,沒有定義變量 int = test(); // error, 必須定義一個匹配類型的變量來接收返回值 // int只是類型,沒有定義變量 int test(); return 0; }

6.4 函數的聲明

如果使用用戶自己定義的函數,而該函數與調用它的函數(即主調函數)不在同一文件中,或者函數定義的位置在主調函數之后,則必須在調用此函數之前對被調用的函數作聲明。

 

所謂函數聲明,就是在函數尚在未定義的情況下,事先將該函數的有關信息通知編譯系統,相當於告訴編譯器,函數在后面定義,以便使編譯能正常進行。

注意:一個函數只能被定義一次,但可以聲明多次。

#include <stdio.h>
int max(int x, int y); // 函數的聲明,分號不能省略
// int max(int, int); // 另一種方式
int main()
{
       int a = 10, b = 25, num_max = 0; num_max = max(a, b); // 函數的調用 printf("num_max = %d\n", num_max); return 0; } // 函數的定義 int max(int x, int y) { return x > y ? x : y; }

函數定義和聲明的區別:

1)定義是指對函數功能的確立,包括指定函數名、函數類型、形參及其類型、函數體等,它是一個完整的、獨立的函數單位。

2)聲明的作用則是把函數的名字、函數類型以及形參的個數、類型和順序(注意,不包括函數體)通知編譯系統,以便在對包含函數調用的語句進行編譯時,據此對其進行對照檢查(例如函數名是否正確,實參與形參的類型和個數是否一致)。

6.5 main函數與exit函數

在main函數中調用exit和return結果是一樣的,但在子函數中調用return只是代表子函數終止了,在子函數中調用exit,那么程序終止。

#include <stdio.h>
#include <stdlib.h>
void fun() { printf("fun\n"); //return; exit(0); } int main() { fun(); while (1); return 0; }

6.6 多文件(分文件)編程

6.6.1 分文件編程

  • l  把函數聲明放在頭文件xxx.h中,在主函數中包含相應頭文件
  • l  在頭文件對應的xxx.c中實現xxx.h聲明的函數

 

6.6.2 防止頭文件重復包含

當一個項目比較大時,往往都是分文件,這時候有可能不小心把同一個頭文件 include 多次,或者頭文件嵌套包含。

 

a.h 中包含 b.h :

#include "b.h"

b.h 中包含 a.h:

#include "a.h"

main.c 中使用其中頭文件:

#include "a.h"
int main()
{
      return 0;
}

編譯上面的例子,會出現如下錯誤:

 

為了避免同一個文件被include多次,C/C++中有兩種方式,一種是 #ifndef 方式,一種是 #pragma once 方式。

方法一:

#ifndef __SOMEFILE_H__
#define __SOMEFILE_H__
// 聲明語句
#endif

方法二:

#pragma once
// 聲明語句

7. 指針

7.1 概述

7.1.1 內存

內存含義:

  • l  存儲器:計算機的組成中,用來存儲程序和數據,輔助CPU進行運算處理的重要部分。
  • l  內存:內部存貯器,暫存程序/數據——掉電丟失 SRAM、DRAM、DDR、DDR2、DDR3。
  • l  外存:外部存儲器,長時間保存程序/數據—掉電不丟ROM、ERRROM、FLASH(NAND、NOR)、硬盤、光盤。

 

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

  • l  暫存放CPU中的運算數據
  • l  暫存與硬盤等外部存儲器交換的數據

7.1.2 物理存儲器和存儲地址空間

有關內存的兩個概念:物理存儲器和存儲地址空間。

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

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

 

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

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

7.1.3 內存地址

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

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

  • l  char:占一個字節分配一個地址
  • l  int: 占四個字節分配四個地址
  • l  float、struct、函數、數組等

 

7.1.4 指針和指針變量

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

 

7.2 指針基礎知識

7.2.1 指針變量的定義和使用

  • l  指針也是一種數據類型,指針變量也是一種變量
  • l  指針變量指向誰,就把誰的地址賦值給指針變量
  • l  “*”操作符操作的是指針變量指向的內存空間
#include <stdio.h>
int main()
{
       int a = 0; char b = 100; printf("%p, %p\n", &a, &b); //打印a, b的地址 //int *代表是一種數據類型,int*指針類型,p才是變量名 //定義了一個指針類型的變量,可以指向一個int類型變量的地址 int *p; p = &a;//將a的地址賦值給變量p,p也是一個變量,值是一個內存地址編號  printf("%d\n", *p);//p指向了a的地址,*p就是a的值 char *p1 = &b; printf("%c\n", *p1);//*p1指向了b的地址,*p1就是b的值 return 0; }

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

7.2.2 通過指針間接修改變量的值

int a = 0;

       int b = 11; int *p = &a; *p = 100; printf("a = %d, *p = %d\n", a, *p); p = &b; *p = 22; printf("b = %d, *p = %d\n", b, *p);

7.2.3 指針大小

  • l  使用sizeof()測量指針的大小,得到的總是:4或8
  • l  sizeof()測的是指針變量指向存儲地址的大小
  • l  在32位平台,所有的指針(地址)都是32位(4字節)
  • l  在64位平台,所有的指針(地址)都是64位(8字節)
    int *p1;
       int **p2; char *p3; char **p4;
printf("sizeof(p1) = %d\n", sizeof(p1)); printf("sizeof(p2) = %d\n", sizeof(p2)); printf("sizeof(p3) = %d\n", sizeof(p3)); printf("sizeof(p4) = %d\n", sizeof(p4)); printf("sizeof(double *) = %d\n", sizeof(double *));

7.2.4 野指針和空指針

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

       int a = 100;
       int *p; p = a; //把a的值賦值給指針變量p,p為野指針, ok,不會有問題,但沒有意義 p = 0x12345678; //給指針變量p賦值,p為野指針, ok,不會有問題,但沒有意義 *p = 1000; //操作野指針指向未知區域,內存出問題,err 

但是,野指針和有效指針變量保存的都是數值,為了標志此指針變量沒有指向任何變量(空閑可用),C語言中,可以把NULL賦值給此指針,這樣就標志此指針為空指針,沒有任何指針。

       int *p = NULL;

NULL是一個值為0的宏常量:

#define NULL    ((void *)0)

7.2.5萬能指針void *

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

       void *p = NULL;
       int a = 10; p = (void *)&a; //指向變量時,最好轉換為void * //使用指針變量指向的內存時,轉換為int * *( (int *)p ) = 11; printf("a = %d\n", a);

7.2.6 const修飾的指針變量

       int a = 100;
       int b = 200; //指向常量的指針 //修飾*,指針指向內存區域不能修改,指針指向可以變 const int *p1 = &a; //等價於int const *p1 = &a; //*p1 = 111; //err p1 = &b; //ok //指針常量 //修飾p1,指針指向不能變,指針指向的內存可以修改 int * const p2 = &a; //p2 = &b; //err *p2 = 222; //ok

7.3 指針和數組

7.3.1 數組名

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

       int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
       printf("a = %p\n", a);
       printf("&a[0] = %p\n", &a[0]);
       //a = 10; //err, 數組名只是常量,不能修改

7.3.2 指針操作數組元素

#include <stdio.h>
int  main()
{
       int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; int i = 0; int n = sizeof(a) / sizeof(a[0]); for (i = 0; i < n; i++) { //printf("%d, ", a[i]); printf("%d, ", *(a+i)); } printf("\n"); int *p = a; //定義一個指針變量保存a的地址 for (i = 0; i < n; i++) { p[i] = 2 * i; } for (i = 0; i < n; i++) { printf("%d, ", *(p + i)); } printf("\n"); return 0; }

7.3.3 指針加減運算

1)加法運算

  • l  指針計算不是簡單的整數相加
  • l  如果是一個int *,+1的結果是增加一個int的大小
  • l  如果是一個char *,+1的結果是增加一個char大小
#include <stdio.h>
int main()
{
      int a; int *p = &a; printf("%d\n", p); p += 2;//移動了2個int printf("%d\n", p); char b = 0; char *p1 = &b; printf("%d\n", p1); p1 += 2;//移動了2個char printf("%d\n", p1); return 0; } 

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

#include <stdio.h>
int main()
{
       int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; int i = 0; int n = sizeof(a) / sizeof(a[0]); int *p = a; for (i = 0; i < n; i++) { printf("%d, ", *p); p++; } printf("\n"); return 0; }

2) 減法運算

示例1:

#include <stdio.h>
int main()
{
       int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; int i = 0; int n = sizeof(a) / sizeof(a[0]); int *p = a+n-1; for (i = 0; i < n; i++) { printf("%d, ", *p); p--; } printf("\n"); return 0; }

示例2:

#include <stdio.h>
int main()
{
       int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; int *p2 = &a[2]; //第2個元素地址 int *p1 = &a[1]; //第1個元素地址 printf("p1 = %p, p2 = %p\n", p1, p2); int n1 = p2 - p1; //n1 = 1 int n2 = (int)p2 - (int)p1; //n2 = 4 printf("n1 = %d, n2 = %d\n", n1, n2); return 0; }

7.3.4 指針數組

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

#include <stdio.h>
int main()
{

      //指針數組
       int *p[3]; int a = 1; int b = 2; int c = 3; int i = 0; p[0] = &a; p[1] = &b; p[2] = &c; for (i = 0; i < sizeof(p) / sizeof(p[0]); i++ ) { printf("%d, ", *(p[i])); } printf("\n"); return 0; }

7.4 多級指針

  • l  C語言允許有多級指針存在,在實際的程序中一級指針最常用,其次是二級指針。
  • l  二級指針就是指向一個一級指針變量地址的指針。
  • l  三級指針基本用不着,但考試會考。
       int a = 10;
       int *p = &a; //一級指針
       *p = 100; //*p就是a
       int **q = &p; //*q就是p //**q就是a int ***t = &q; //*t就是q //**t就是p //***t就是a

7.5 指針和函數

7.5.1 函數形參改變實參的值

#include <stdio.h>
void swap1(int x, int y)
{
       int tmp; tmp = x; x = y; y = tmp; printf("x = %d, y = %d\n", x, y); } void swap2(int *x, int *y) { int tmp; tmp = *x; *x = *y; *y = tmp; } int main() { int a = 3; int b = 5; swap1(a, b); //值傳遞 printf("a = %d, b = %d\n", a, b); a = 3; b = 5; swap2(&a, &b); //地址傳遞 printf("a2 = %d, b2 = %d\n", a, b); return 0; }

7.6.2 數組名做函數參數

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

#include <stdio.h>
//void printArrary(int a[10], int n)
//void printArrary(int a[], int n)

void printArrary(int *a, int n)
{
       int i = 0; for (i = 0; i < n; i++) { printf("%d, ", a[i]); } printf("\n"); } int main() { int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; int n = sizeof(a) / sizeof(a[0]); //數組名做函數參數  printArrary(a, n); return 0; }

7.6.3 指針做為函數的返回值

#include <stdio.h>
int a = 10
int *getA()
{
       return &a; } int main() { *( getA() ) = 111; printf("a = %d\n", a); return 0; }

7.7 指針和字符串

7.7.1 字符指針

#include <stdio.h>
int main()
{
       char str[] = "hello world"; char *p = str; *p = 'm'; p++; *p = 'i'; printf("%s\n", str); p = "mike jiang"; printf("%s\n", p); char *q = "test"; printf("%s\n", q); return 0; }

7.7.2 字符指針做函數參數

#include <stdio.h>
void mystrcat(char *dest, const char *src)
{
       int len1 = 0; int len2 = 0; while (dest[len1]) { len1++; } while (src[len2]) { len2++; } int i; for (i = 0; i < len2; i++) { dest[len1 + i] = src[i]; } } int main() { char dst[100] = "hello mike"; char src[] = "123456"; mystrcat(dst, src); printf("dst = %s\n", dst); return 0; }

7.7.3 const修飾的指針變量

#include <stdio.h>
#include <stdlib.h> #include <string.h> int main(void) { //const修飾一個變量為只讀 const int a = 10; //a = 100; //err //指針變量, 指針指向的內存, 2個不同概念 char buf[] = "aklgjdlsgjlkds"; //從左往右看,跳過類型,看修飾哪個字符 //如果是*, 說明指針指向的內存不能改變 //如果是指針變量,說明指針的指向不能改變,指針的值不能修改 const char *p = buf; // 等價於上面 char const *p1 = buf; //p[1] = '2'; //err p = "agdlsjaglkdsajgl"; //ok char * const p2 = buf; p2[1] = '3'; //p2 = "salkjgldsjaglk"; //err //p3為只讀,指向不能變,指向的內存也不能變 const char * const p3 = buf; return 0; }

7.7.4 指針數組做為main函數的形參

int main(int argc, char *argv[]);
  • l  main函數是操作系統調用的,第一個參數標明argv數組的成員數量,argv數組的每個成員都是char *類型
  • l  argv是命令行參數的字符串數組
  • l  argc代表命令行參數的數量,程序名字本身算一個參數
#include <stdio.h>
//argc: 傳參數的個數(包含可執行程序)
//argv:指針數組,指向輸入的參數
int main(int argc, char *argv[])
{
       //指針數組,它是數組,每個元素都是指針
       char *a[] = { "aaaaaaa", "bbbbbbbbbb", "ccccccc" }; int i = 0; printf("argc = %d\n", argc); for (i = 0; i < argc; i++) { printf("%s\n", argv[i]); } return 0; }

7.7.5 項目開發常用字符串應用模型

1) strstr中的while和do-while模型

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

a) while模型

#include <stdio.h>

#include <stdlib.h> #include <string.h> int main(void) { char *p = "11abcd111122abcd333abcd3322abcd3333322qqq"; int n = 0; while ((p = strstr(p, "abcd")) != NULL) { //能進來,肯定有匹配的子串 //重新設置起點位置 p = p + strlen("abcd"); n++; if (*p == 0) //如果到結束符  { break; } } printf("n = %d\n", n); return 0; }

b) do-while模型

#include <stdio.h>

#include <stdlib.h> #include <string.h> int main(void) { char *p = "11abcd111122abcd333abcd3322abcd3333322qqq"; int n = 0; do { p = strstr(p, "abcd"); if (p != NULL) { n++; //累計個數 //重新設置查找的起點 p = p + strlen("abcd"); } else //如果沒有匹配的字符串,跳出循環  { break; } } while (*p != 0); //如果沒有到結尾 printf("n = %d\n", n); return 0; }

2) 兩頭堵模型

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

#include <stdio.h>
#include <stdlib.h> #include <string.h> #include <ctype.h> int fun(char *p, int *n) { if (p == NULL || n == NULL) { return -1; } int begin = 0; int end = strlen(p) - 1; //從左邊開始 //如果當前字符為空,而且沒有結束 while (p[begin] == ' ' && p[begin] != 0) { begin++; //位置從右移動一位  } //從右往左移動 while (p[end] == ' ' && end > 0) { end--; //往左移動  } if (end == 0) { return -2; } //非空元素個數 *n = end - begin + 1; return 0; } int main(void) { char *p = " abcddsgadsgefg "; int ret = 0; int n = 0; ret = fun(p, &n); if (ret != 0) { return ret; } printf("非空字符串元素個數:%d\n", n); return 0; }

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

#include <stdio.h>
#include <stdlib.h> #include <string.h> int inverse(char *p) { if (p == NULL) { return -1; } char *str = p; int begin = 0; int end = strlen(str) - 1; char tmp; while (begin < end) { //交換元素 tmp = str[begin]; str[begin] = str[end]; str[end] = tmp; begin++; //往右移動位置 end--; //往左移動位置  } return 0; } int main(void) { //char *str = "abcdefg"; //文件常量區,內容不允許修改 char str[] = "abcdef"; int ret = inverse(str); if (ret != 0) { return ret; } printf("str ========== %s\n", str); return 0; }

7.8 指針小結

定義

說明

int  i

定義整形變量

int *p

定義一個指向int的指針變量

int a[10]

定義一個有10個元素的數組,每個元素類型為int

int *p[10]

定義一個有10個元素的數組,每個元素類型為int*

int func()

定義一個函數,返回值為int型

int *func()

定義一個函數,返回值為int *型

int **p

定義一個指向int的指針的指針,二級指針

 

8. 內存管理

8.1 作用域

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

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

8.1.1 局部變量

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

  • l  在一個函數內定義,只在函數范圍內有效
  • l  在復合語句中定義,只在復合語句中有效
  • l  隨着函數調用的結束或復合語句的結束局部變量的聲明聲明周期也結束
  • l  如果沒有賦初值,內容為隨機
#include <stdio.h>
void test()
{
       //auto寫不寫是一樣的
      //auto只能出現在{}內部
       auto int b = 10; } int main(void) { //b = 100; //err, 在main作用域中沒有b if (1) { //在復合語句中定義,只在復合語句中有效 int a = 10; printf("a = %d\n", a); } //a = 10; //err離開if()的復合語句,a已經不存在 return 0; }

8.1.2 靜態(static)局部變量

  • l  static局部變量的作用域也是在定義的函數內有效
  • l  static局部變量的生命周期和程序運行周期一樣,同時staitc局部變量的值只初始化一次,但可以賦值多次
  • l  static局部變量若未賦以初值,則由系統自動賦值:數值型變量自動賦初值0,字符型變量賦空字符
#include <stdio.h>
void fun1()
{
       int i = 0; i++; printf("i = %d\n", i); } void fun2() { //靜態局部變量,沒有賦值,系統賦值為0,而且只會初始化一次 static int a; a++; printf("a = %d\n", a); } int main(void) { fun1(); fun1(); fun2(); fun2(); return 0; }

8.1.3 全局變量

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

8.1.4 靜態(static)全局變量

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

8.1.5 extern全局變量聲明

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

8.1.6 全局函數和靜態函數

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

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

 

注意:

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

8.1.7 總結

類型

作用域

生命周期

auto變量

一對{}內

當前函數

static局部變量

一對{}內

整個程序運行期

extern變量

整個程序

整個程序運行期

static全局變量

當前文件

整個程序運行期

extern函數

整個程序

整個程序運行期

static函數

當前文件

整個程序運行期

register變量

一對{}內

當前函數

 

8.2 內存布局

8.2.1 內存分區

C代碼經過預處理、編譯、匯編、鏈接4步后生成一個可執行程序。

在 Linux 下,程序是一個普通的可執行文件,以下列出一個二進制可執行文件的基本情況:

 

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

  • l  代碼區

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

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

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

 

  • l  未初始化數據區(又叫 bss 區)

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

 

程序在加載到內存前,代碼區和全局區(data和bss)的大小就是固定的,程序運行期間不能改變。然后,運行可執行程序,系統把程序加載到內存,除了根據可執行程序的信息分出代碼區(text)、數據區(data)和未初始化數據區(bss)之外,還額外增加了棧區、堆區。

 

  • l  代碼區(text segment)

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

 

  • l  未初始化數據區(BSS)

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

 

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

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

 

  • l  棧區(stack)

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

 

  • l  堆區(heap)

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

8.2.2 存儲類型總結

類型

作用域

生命周期

存儲位置

auto變量

一對{}內

當前函數

棧區

static局部變量

一對{}內

整個程序運行期

初始化在data段,未初始化在BSS段

extern變量

整個程序

整個程序運行期

初始化在data段,未初始化在BSS段

static全局變量

當前文件

整個程序運行期

初始化在data段,未初始化在BSS段

extern函數

整個程序

整個程序運行期

代碼區

static函數

當前文件

整個程序運行期

代碼區

register變量

一對{}內

當前函數

運行時存儲在CPU寄存器

字符串常量

當前文件

整個程序運行期

data段

 

#include <stdio.h>
#include <stdlib.h>
int e; static int f; int g = 10; static int h = 10; int main() { int a; int b = 10; static int c; static int d = 10; char *i = "test"; char *k = NULL; printf("&a\t %p\t //局部未初始化變量\n", &a); printf("&b\t %p\t //局部初始化變量\n", &b); printf("&c\t %p\t //靜態局部未初始化變量\n", &c); printf("&d\t %p\t //靜態局部初始化變量\n", &d); printf("&e\t %p\t //全局未初始化變量\n", &e); printf("&f\t %p\t //全局靜態未初始化變量\n", &f); printf("&g\t %p\t //全局初始化變量\n", &g); printf("&h\t %p\t //全局靜態初始化變量\n", &h); printf("i\t %p\t //只讀數據(文字常量區)\n", i); k = (char *)malloc(10); printf("k\t %p\t //動態分配的內存\n", k); return 0; }

8.2.3 存儲類型總結內存操作函數

1) memset()

#include <string.h>
void *memset(void *s, intc, size_tn);
功能:將s的內存區域的前n個字節以參數c填入
參數:
       s:需要操作內存s的首地址
       c:填充的字符,c雖然參數為int,但必須是unsigned char , 范圍為0~255 n:指定需要設置的大小 返回值:s的首地址 int a[10]; memset(a, 0, sizeof(a)); memset(a, 97, sizeof(a)); int i = 0; for (i = 0; i < 10; i++) { printf("%c\n", a[i]); }

2) memcpy()

#include <string.h>
void *memcpy(void *dest, constvoid *src, size_tn);
功能:拷貝src所指的內存內容的前n個字節到dest所值的內存地址上。
參數:
       dest:目的內存首地址
       src:源內存首地址,注意:dest和src所指的內存空間不可重疊
       n:需要拷貝的字節數
返回值:dest的首地址
       int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int b[10]; memcpy(b, a, sizeof(a)); int i = 0; for (i = 0; i < 10; i++) { printf("%d, ", b[i]); } printf("\n"); //memcpy(&a[3], a, 5 * sizeof(int)); //err, 內存重疊

3) memmove()

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

4) memcmp()

#include <string.h>
intmemcmp(constvoid *s1, constvoid *s2, size_tn); 功能:比較s1和s2所指向內存區域的前n個字節 參數: s1:內存首地址1 s2:內存首地址2 n:需比較的前n個字節 返回值: 相等:=0 大於:>0 小於:<0 int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int b[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int flag = memcmp(a, b, sizeof(a)); printf("flag = %d\n", flag);

8.2.4 堆區內存分配和釋放

1)malloc()

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

#include <stdlib.h> #include <stdio.h> #include <string.h> int main() { int count, *array, n; printf("請輸入要申請數組的個數:\n"); scanf("%d", &n); array = (int *)malloc(n * sizeof (int)); if (array == NULL) { printf("申請空間失敗!\n"); return -1; } //將申請到空間清0 memset(array, 0, sizeof(int)*n); for (count = 0; count < n; count++) /*給數組賦值*/ array[count] = count; for (count = 0; count < n; count++) /*打印數組元素*/ printf("%2d", array[count]); free(array); return 0; }

2)free()

#include <stdlib.h>

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

8.3 內存分區代碼分析(在Linux下測試)

1) 返回棧區地址

#include <stdio.h>
int a = 10;
int *fun() { return &a;//函數調用完畢,a釋放 } int main(int argc, char *argv[]) { int *p = NULL; p = fun(); *p = 100; //操作野指針指向的內存,err return 0; }

2) 返回data區地址

#include <stdio.h>
int *fun()
{
       static int a = 10; return &a; //函數調用完畢,a不釋放 } int main(int argc, char *argv[]) { int *p = NULL; p = fun(); *p = 100; //ok printf("*p = %d\n", *p); return 0; }

3) 值傳遞1

#include <stdio.h>
#include <stdlib.h>
void fun(int *tmp) { tmp = (int *)malloc(sizeof(int)); *tmp = 100; } int main(int argc, char *argv[]) { int *p = NULL; fun(p); //值傳遞,形參修改不會影響實參 printf("*p = %d\n", *p);//err,操作空指針指向的內存 return 0; }

4) 值傳遞2

#include <stdio.h>
#include <stdlib.h>
void fun(int *tmp) { *tmp = 100; } int main(int argc, char *argv[]) { int *p = NULL; p = (int *)malloc(sizeof(int)); fun(p); //值傳遞 printf("*p = %d\n", *p); //ok,*p為100 return 0; }

5) 返回堆區地址

#include <stdio.h>
#include <stdlib.h>
int *fun() { int *tmp = NULL; tmp = (int *)malloc(sizeof(int)); *tmp = 100; return tmp;//返回堆區地址,函數調用完畢,不釋放 } int main(int argc, char *argv[]) { int *p = NULL; p = fun(); printf("*p = %d\n", *p);//ok //堆區空間,使用完畢,手動釋放 if (p != NULL) { free(p); p = NULL; } return 0; }

9. 復合類型(自定義類型)

9.1 結構體

9.1.1 概述

數組:描述一組具有相同類型數據的有序集合,用於處理大量相同類型的數據運算。

有時我們需要將不同類型的數據組合成一個有機的整體,如:一個學生有學號/姓名/性別/年齡/地址等屬性。顯然單獨定義以上變量比較繁瑣,數據不便於管理。

C語言中給出了另一種構造數據類型——結構體。

  

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

定義結構體變量的方式:

  • l  先聲明結構體類型再定義變量名
  • l  在聲明類型的同時定義變量
  • l  直接定義結構體類型變量(無類型名)

 

結構體類型和結構體變量關系:

  • l  結構體類型:指定了一個結構體類型,它相當於一個模型,但其中並無具體數據,系統對之也不分配實際內存單元。
  • l  結構體變量:系統根據結構體類型(內部成員狀況)為之分配空間。
//結構體類型的定義
struct stu
{
      char name[50]; int age; }; //先定義類型,再定義變量(常用) struct stu s1 = { "mike", 18 }; //定義類型同時定義變量 struct stu2 { char name[50]; int age; }s2 = { "lily", 22 };
struct { char name[50]; int age; }s3 = { "yuri", 25 };

9.1.3 結構體成員的使用

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

//結構體類型的定義
struct stu { char name[50]; int age; }; int main() { struct stu s1; //如果是普通變量,通過點運算符操作結構體成員 strcpy(s1.name, "abc"); s1.age = 18; printf("s1.name = %s, s1.age = %d\n", s1.name, s1.age); //如果是指針變量,通過->操作結構體成員 strcpy((&s1)->name, "test"); (&s1)->age = 22; printf("(&s1)->name = %s, (&s1)->age = %d\n", (&s1)->name, (&s1)->age); return 0; }

9.1.4 結構體數組

#include <stdio.h>
//統計學生成績
struct stu
{
       int num; char name[20]; char sex; float score; }; int main() { //定義一個含有5個元素的結構體數組並將其初始化 struct stu boy[5] = { { 101, "Li ping", 'M', 45 }, { 102, "Zhang ping", 'M', 62.5 }, { 103, "He fang", 'F', 92.5 }, { 104, "Cheng ling", 'F', 87 }, { 105, "Wang ming", 'M', 58 }}; int i = 0; int c = 0; float ave, s = 0; for (i = 0; i < 5; i++) { s += boy[i].score; //計算總分 if (boy[i].score < 60) { c += 1; //統計不及格人的分數  } } printf("s=%f\n", s);//打印總分數 ave = s / 5; //計算平均分數 printf("average=%f\ncount=%d\n\n", ave, c); //打印平均分與不及格人數 for (i = 0; i < 5; i++) { printf(" name=%s, score=%f\n", boy[i].name, boy[i].score); // printf(" name=%s, score=%f\n", (boy+i)->name, (boy+i)->score);  } return 0; }

9.1.5 結構體套結構體

#include <stdio.h>
struct person
{
      char name[20]; char sex; }; struct stu { int id; struct person info; }; int main() { struct stu s[2] = { 1, "lily", 'F', 2, "yuri", 'M' }; int i = 0; for (i = 0; i < 2; i++) { printf("id = %d\tinfo.name=%s\tinfo.sex=%c\n", s[i].id, s[i].info.name, s[i].info.sex); } return 0; }

9.1.6 結構體賦值

#include<stdio.h>
#include<string.h>
//結構體類型的定義
struct stu { char name[50]; int age; }; int main() { struct stu s1; //如果是普通變量,通過點運算符操作結構體成員 strcpy(s1.name, "abc"); s1.age = 18; printf("s1.name = %s, s1.age = %d\n", s1.name, s1.age); //相同類型的兩個結構體變量,可以相互賦值 //把s1成員變量的值拷貝給s2成員變量的內存 //s1和s2只是成員變量的值一樣而已,它們還是沒有關系的兩個變量 struct stu s2 = s1; //memcpy(&s2, &s1, sizeof(s1)); printf("s2.name = %s, s2.age = %d\n", s2.name, s2.age); return 0; }

9.1.7 結構體和指針

1)指向普通結構體變量的指針

#include<stdio.h>
//結構體類型的定義
struct stu
{
       char name[50]; int age; }; int main() { struct stu s1 = { "lily", 18 }; //如果是指針變量,通過->操作結構體成員 struct stu *p = &s1; printf("p->name = %s, p->age=%d\n", p->name, p->age); printf("(*p).name = %s, (*p).age=%d\n", (*p).name, (*p).age); return 0; }

2)堆區結構體變量

#include<stdio.h>
#include <string.h> #include <stdlib.h> //結構體類型的定義 struct stu { char name[50]; int age; }; int main() { struct stu *p = NULL; p = (struct stu *)malloc(sizeof(struct stu)); //如果是指針變量,通過->操作結構體成員 strcpy(p->name, "test"); p->age = 22; printf("p->name = %s, p->age=%d\n", p->name, p->age); printf("(*p).name = %s, (*p).age=%d\n", (*p).name, (*p).age); free(p); p = NULL; return 0; }

3)結構體套一級指針

#include<stdio.h>
#include <string.h> #include <stdlib.h> //結構體類型的定義 struct stu { char *name; //一級指針 int age; }; int main() { struct stu *p = NULL; p = (struct stu *)malloc(sizeof(struct stu)); p->name = malloc(strlen("test") + 1); strcpy(p->name, "test"); p->age = 22; printf("p->name = %s, p->age=%d\n", p->name, p->age); printf("(*p).name = %s, (*p).age=%d\n", (*p).name, (*p).age); if (p->name != NULL) { free(p->name); p->name = NULL; } if (p != NULL) { free(p); p = NULL; } return 0; }

9.1.8 結構體做函數參數

1)結構體普通變量做函數參數

#include<stdio.h>
#include <string.h>
//結構體類型的定義
struct stu { char name[50]; int age; }; //函數參數為結構體普通變量 void set_stu(struct stu tmp) { strcpy(tmp.name, "mike"); tmp.age = 18; printf("tmp.name = %s, tmp.age = %d\n", tmp.name, tmp.age); } int main() { struct stu s = { 0 }; set_stu(s); //值傳遞 printf("s.name = %s, s.age = %d\n", s.name, s.age); return 0; }

2)結構體指針變量做函數參數

#include<stdio.h>
#include <string.h>
//結構體類型的定義
struct stu { char name[50]; int age; }; //函數參數為結構體指針變量 void set_stu_pro(struct stu *tmp) { strcpy(tmp->name, "mike"); tmp->age = 18; } int main() { struct stu s = { 0 }; set_stu_pro(&s); //地址傳遞 printf("s.name = %s, s.age = %d\n", s.name, s.age); return 0; }

3)結構體數組名做函數參數

#include<stdio.h>
//結構體類型的定義
struct stu
{
       char name[50]; int age; }; //void set_stu_pro(struct stu tmp[100], int n) //void set_stu_pro(struct stu tmp[], int n) void set_stu_pro(struct stu *tmp, int n) { int i = 0; for (i = 0; i < n; i++) { sprintf(tmp->name, "name%d%d%d", i, i, i); tmp->age = 20 + i; tmp++; } } int main() { struct stu s[3] = { 0 }; int i = 0; int n = sizeof(s) / sizeof(s[0]); set_stu_pro(s, n); //數組名傳遞 for (i = 0; i < n; i++) { printf("%s, %d\n", s[i].name, s[i].age); } return 0; }

4)const修飾結構體指針形參變量

//結構體類型的定義
struct stu
{
       char name[50]; int age; }; void fun1(struct stu * const p) { //p = NULL; //err p->age = 10; //ok } //void fun2(struct stu const* p) void fun2(const struct stu * p) { p = NULL; //ok //p->age = 10; //err } void fun3(const struct stu * const p) { //p = NULL; //err //p->age = 10; //err }

9.2 共用體(聯合體)

  • l  聯合union是一個能在同一個存儲空間存儲不同類型數據的類型;
  • l  聯合體所占的內存長度等於其最長成員的長度,也有叫做共用體;
  • l  同一內存段可以用來存放幾種不同類型的成員,但每一瞬時只有一種起作用;
  • l  共用體變量中起作用的成員是最后一次存放的成員,在存入一個新的成員后原有的成員的值會被覆蓋;
  • l  共用體變量的地址和它的各成員的地址都是同一地址。 
#include <stdio.h>
//共用體也叫聯合體
union Test
{
       unsigned char a; unsigned int b; unsigned short c; }; int main() { //定義共用體變量  union Test tmp; //1、所有成員的首地址是一樣的 printf("%p, %p, %p\n", &(tmp.a), &(tmp.b), &(tmp.c)); //2、共用體大小為最大成員類型的大小 printf("%lu\n", sizeof(union Test)); //3、一個成員賦值,會影響另外的成員 //左邊是高位,右邊是低位 //低位放低地址,高位放高地址 tmp.b = 0x44332211; printf("%x\n", tmp.a); //11 printf("%x\n", tmp.c); //2211  tmp.a = 0x00; printf("short: %x\n", tmp.c); //2200 printf("int: %x\n", tmp.b); //44332200 return 0; }

9.3 枚舉

枚舉:將變量的值一一列舉出來,變量的值只限於列舉出來的值的范圍內。

枚舉類型定義:
enum  枚舉名
{
       枚舉值表
};
  • l  在枚舉值表中應列出所有可用值,也稱為枚舉元素。
  • l  枚舉值是常量,不能在程序中用賦值語句再對它賦值。
  • l  舉元素本身由系統定義了一個表示序號的數值從0開始順序定義為0,1,2 …
#include <stdio.h>
enum weekday
{
       sun = 2, mon, tue, wed, thu, fri, sat } ; enum bool { flase, true }; int main() { enum weekday a, b, c; a = sun; b = mon; c = tue; printf("%d,%d,%d\n", a, b, c); enum bool flag; flag = true; if (flag == 1) { printf("flag為真\n"); } return 0; }

9.4 typedef

typedef為C語言的關鍵字,作用是為一種數據類型(基本類型或自定義數據類型)定義一個新名字,不能創建新類型。

  • l  與#define不同,typedef僅限於數據類型,而不是能是表達式或具體的值
  • l  #define發生在預處理,typedef發生在編譯階段
#include <stdio.h>

typedef int INT; typedef char BYTE; typedef BYTE T_BYTE; typedef unsigned char UBYTE; typedef struct type { UBYTE a; INT b; T_BYTE c; }TYPE, *PTYPE; int main() { TYPE t; t.a = 254; t.b = 10; t.c = 'c'; PTYPE p = &t; printf("%u, %d, %c\n", p->a, p->b, p->c); return 0; }

10. 文件操作

10.1 概述

10.1.1磁盤文件和設備文件

  • l  磁盤文件

指一組相關數據的有序集合,通常存儲在外部介質(如磁盤)上,使用時才調入內存。

 

  • l  設備文件

在操作系統中把每一個與主機相連的輸入、輸出設備看作是一個文件,把它們的輸入、輸出等同於對磁盤文件的讀和寫。

 

10.1.2 磁盤文件的分類

計算機的存儲在物理上是二進制的,所以物理上所有的磁盤文件本質上都是一樣的:以字節為單位進行順序存儲。

 

從用戶或者操作系統使用的角度(邏輯上)把文件分為:

  • l  文本文件:基於字符編碼的文件 
  • l  二進制文件:基於值編碼的文件

10.1.3 文本文件和二進制文件

1)文本文件

  • l  基於字符編碼,常見編碼有ASCII、UNICODE等
  • l  一般可以使用文本編輯器直接打開
  • l  數5678的以ASCII存儲形式(ASCII碼)為:00110101 00110110 00110111 00111000

2)二進制文件

  • l  基於值編碼,自己根據具體應用,指定某個值是什么意思
  • l  把內存中的數據按其在內存中的存儲形式原樣輸出到磁盤上
  • l  數5678的存儲形式(二進制碼)為:00010110 00101110

10.2 文件的打開和關閉

10.2.1 文件指針

在C語言中用一個指針變量指向一個文件,這個指針稱為文件指針。

typedef struct

{

       short           level;    //緩沖區"滿"或者"空"的程度
 unsigned flags; //文件狀態標志 char fd; //文件描述符  unsigned char hold; //如無緩沖區不讀取字符 short bsize; //緩沖區的大小  unsigned char *buffer;//數據緩沖區的位置  unsigned ar; //指針,當前的指向  unsigned istemp; //臨時文件,指示器 short token; //用於有效性的檢查  }FILE;

FILE是系統使用typedef定義出來的有關文件信息的一種結構體類型,結構中含有文件名、文件狀態和文件當前位置等信息。

聲明FILE結構體類型的信息包含在頭文件“stdio.h”中,一般設置一個指向FILE類型變量的指針變量,然后通過它來引用這些FILE類型變量。通過文件指針就可對它所指的文件進行各種操作。

C語言中有三個特殊的文件指針由系統默認打開,用戶無需定義即可直接使用:

  • l  stdin: 標准輸入,默認為當前終端(鍵盤),我們使用的scanf、getchar函數默認從此終端獲得數據。
  • l  stdout:標准輸出,默認為當前終端(屏幕),我們使用的printf、puts函數默認輸出信息到此終端。
  • l  stderr:標准出錯,默認為當前終端(屏幕),我們使用的perror函數默認輸出信息到此終端。

10.2.2 文件的打開

任何文件使用之前必須打開:

#include <stdio.h>

FILE * fopen(constchar * filename, constchar * mode); 功能:打開文件 參數: filename:需要打開的文件名,根據需要加上路徑 mode:打開文件的模式設置 返回值: 成功:文件指針 失敗:NULL 第一個參數的幾種形式: FILE *fp_passwd = NULL; //相對路徑: //打開當前目錄passdw文件:源文件(源程序)所在目錄  FILE *fp_passwd = fopen("passwd.txt", "r"); //打開當前目錄(test)下passwd.txt文件 fp_passwd = fopen(". / test / passwd.txt", "r"); //打開當前目錄上一級目錄(相對當前目錄)passwd.txt文件  fp_passwd = fopen(".. / passwd.txt", "r"); //絕對路徑: //打開C盤test目錄下一個叫passwd.txt文件  fp_passwd = fopen("c://test//passwd.txt","r");

第二個參數的幾種形式(打開文件的方式):

打開模式

含義

r或rb

以只讀方式打開一個文本文件(不創建文件,若文件不存在則報錯)

w或wb

以寫方式打開文件(如果文件存在則清空文件,文件不存在則創建一個文件)

a或ab

以追加方式打開文件,在末尾添加內容,若文件不存在則創建文件

r+或rb+

以可讀、可寫的方式打開文件(不創建新文件)

w+或wb+

以可讀、可寫的方式打開文件(如果文件存在則清空文件,文件不存在則創建一個文件)

a+或ab+

以添加方式打開文件,打開文件並在末尾更改文件,若文件不存在則創建文件

 

注意:

  • l  b是二進制模式的意思,b只是在Windows有效,在Linux用r和rb的結果是一樣的
  • l  Unix和Linux下所有的文本文件行都是\n結尾,而Windows所有的文本文件行都是\r\n結尾
  • l  在Windows平台下,以“文本”方式打開文件,不加b:

n  當讀取文件的時候,系統會將所有的 "\r\n" 轉換成 "\n"

n  當寫入文件的時候,系統會將 "\n" 轉換成 "\r\n" 寫入

n  以"二進制"方式打開文件,則讀\寫都不會進行這樣的轉換

  • l  在Unix/Linux平台下,“文本”與“二進制”模式沒有區別,"\r\n" 作為兩個字符原樣輸入輸出
int main(void)
{
      FILE *fp = NULL; // "\\"這樣的路徑形式,只能在windows使用 // "/"這樣的路徑形式,windows和linux平台下都可用,建議使用這種 // 路徑可以是相對路徑,也可是絕對路徑 fp = fopen("../test", "w"); //fp = fopen("..\\test", "w"); if (fp == NULL) //返回空,說明打開失敗  { //perror()是標准出錯打印函數,能打印調用庫函數出錯原因 perror("open"); return -1; } return 0; }

10.2.3 文件的關閉

任何文件在使用后應該關閉:

  • l  打開的文件會占用內存資源,如果總是打開不關閉,會消耗很多內存
  • l  一個進程同時打開的文件數是有限制的,超過最大同時打開文件數,再次調用fopen打開文件會失敗
  • l  如果沒有明確的調用fclose關閉打開的文件,那么程序在退出的時候,操作系統會統一關閉。
#include <stdio.h>
int fclose(FILE * stream);
功能:關閉先前fopen()打開的文件。此動作讓緩沖區的數據寫入文件中,並釋放系統所提供的文件資源。
參數:
       stream:文件指針
返回值:
       成功:0 失敗:-1 FILE * fp = NULL; fp = fopen("abc.txt", "r"); fclose(fp); 

10.3 文件的順序讀寫

10.3.1 按照字符讀寫文件fgetc、fputc

1)寫文件

#include <stdio.h>
int fputc(int ch, FILE * stream);
功能:將ch轉換為unsigned char后寫入stream指定的文件中
參數:
       ch:需要寫入文件的字符
       stream:文件指針
返回值:
       成功:成功寫入文件的字符
       失敗:返回-1
char buf[] = "this is a test for fputc"; int i = 0; int n = strlen(buf); for (i = 0; i < n; i++) { //往文件fp寫入字符buf[i] int ch = fputc(buf[i], fp); printf("ch = %c\n", ch); }

2)文件結尾

在C語言中,EOF表示文件結束符(end of file)。在while循環中以EOF作為文件結束標志,這種以EOF作為文件結束標志的文件,必須是文本文件。在文本文件中,數據都是以字符的ASCII代碼值的形式存放。我們知道,ASCII代碼值的范圍是0~127,不可能出現-1,因此可以用EOF作為文件結束標志。

#define EOF     (-1)

當把數據以二進制形式存放到文件中時,就會有-1值的出現,因此不能采用EOF作為二進制文件的結束標志。為解決這一個問題,ANSI C提供一個feof函數,用來判斷文件是否結束。feof函數既可用以判斷二進制文件又可用以判斷文本文件。

#include <stdio.h>
int feof(FILE * stream);
功能:檢測是否讀取到了文件結尾。判斷的是最后一次“讀操作的內容”,不是當前位置內容(上一個內容)。
參數:
       stream:文件指針
返回值:
       非0值:已經到文件結尾
       0:沒有到文件結尾

3)讀文件

#include <stdio.h>

int fgetc(FILE * stream);

功能:從stream指定的文件中讀取一個字符

參數:

       stream:文件指針

返回值:

       成功:返回讀取到的字符

       失敗:-1

 

char ch; #if 0 while ((ch = fgetc(fp)) != EOF) { printf("%c", ch); } printf("\n"); #endif while (!feof(fp)) //文件沒有結束,則執行循環 { ch = fgetc(fp); printf("%c", ch); } printf("\n");

4)強化訓練:實現vi、cat命令

 

10.3.2按照行讀寫文件fgets、fputs

1)寫文件

#include <stdio.h>
int fputs(constchar * str, FILE * stream);
功能:將str所指定的字符串寫入到stream指定的文件中,字符串結束符 '\0' 不寫入文件。 參數: str:字符串 stream:文件指針 返回值: 成功:0 失敗:-1 char *buf[] = { "123456\n", "bbbbbbbbbb\n", "ccccccccccc\n" }; int i = 0; int n = 3; for (i = 0; i < n; i++) { int len = fputs(buf[i], fp); printf("len = %d\n", len); }

2)讀文件

#include <stdio.h>
char * fgets(char * str, int size, FILE * stream);
功能:從stream指定的文件內讀入字符,保存到str所指定的內存空間,直到出現換行字符、讀到文件結尾或是已讀了size - 1個字符為止,最后會自動加上字符 '\0' 作為字符串結束。 參數: str:字符串 size:指定最大讀取字符串的長度(size - 1) stream:文件指針 返回值: 成功:成功讀取的字符串 讀到文件尾或出錯: NULL char buf[100] = 0; while (!feof(fp)) //文件沒有結束 { memset(buf, 0, sizeof(buf)); char *p = fgets(buf, sizeof(buf), fp); if (p != NULL) { printf("buf = %s", buf); } }

3)強化訓練:文件版四則運算

有個文件大小不確定,每行內容都是一個四則運算表達式,還沒有算出結果,寫一個程序,自動算出其結果后修改文件。

  

10.3.3按照格式化文件fprintf、fscanf

1)寫文件

#include <stdio.h>
int fprintf(FILE * stream, constchar * format, ...);
功能:根據參數format字符串來轉換並格式化數據,然后將結果輸出到stream指定的文件中,指定出現字符串結束符 '\0' 為止。 參數: stream:已經打開的文件 format:字符串格式,用法和printf()一樣 返回值: 成功:實際寫入文件的字符個數 失敗:-1 fprintf(fp, "%d %d %d\n", 1, 2, 3);

2)讀文件

#include <stdio.h>
int fscanf(FILE * stream, constchar * format, ...);
功能:從stream指定的文件讀取字符串,並根據參數format字符串來轉換並格式化數據。
參數:
       stream:已經打開的文件
       format:字符串格式,用法和scanf()一樣
返回值:
       成功:參數數目,成功轉換的值的個數
       失敗: - 1
int a = 0; int b = 0; int c = 0; fscanf(fp, "%d %d %d\n", &a, &b, &c); printf("a = %d, b = %d, c = %d\n", a, b, c);

3)強化訓練:文件版排序

 

10.3.4按照塊讀寫文件fread、fwrite

1)寫文件

#include <stdio.h>
size_t fwrite(constvoid *ptr, size_t size, size_t nmemb, FILE *stream); 功能:以數據塊的方式給文件寫入內容 參數: ptr:准備寫入文件數據的地址 size: size_t 為 unsigned int類型,此參數指定寫入文件內容的塊數據大小 nmemb:寫入文件的塊數,寫入文件數據總大小為:size * nmemb stream:已經打開的文件指針 返回值: 成功:實際成功寫入文件數據的塊數目,此值和nmemb相等 失敗:0 typedef struct Stu { char name[50]; int id; }Stu; Stu s[3]; int i = 0; for (i = 0; i < 3; i++) { sprintf(s[i].name, "stu%d%d%d", i, i, i); s[i].id = i + 1; } int ret = fwrite(s, sizeof(Stu), 3, fp); printf("ret = %d\n", ret);

2)讀文件

#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); 功能:以數據塊的方式從文件中讀取內容 參數: ptr:存放讀取出來數據的內存空間 size: size_t 為 unsigned int類型,此參數指定讀取文件內容的塊數據大小 nmemb:讀取文件的塊數,讀取文件數據總大小為:size * nmemb stream:已經打開的文件指針 返回值: 成功:實際成功讀取到內容的塊數,如果此值比nmemb小,但大於0,說明讀到文件的結尾。 失敗:0 typedef struct Stu { char name[50]; int id; }Stu; Stu s[3]; int ret = fread(s, sizeof(Stu), 3, fp); printf("ret = %d\n", ret); int i = 0; for (i = 0; i < 3; i++) { printf("s = %s, %d\n", s[i].name, s[i].id); }

3)強化訓練:大文件拷貝

 

10.4 文件的隨機讀寫

#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);
功能:移動文件流(文件光標)的讀寫位置。
參數:
      stream:已經打開的文件指針
offset:根據whence來移動的位移數(偏移量),可以是正數,也可以負數,如果正數,則相對於whence往右移動,如果是負數,則相對於whence往左移動。如果向前移動的字節數超過了文件開頭則出錯返回,如果向后移動的字節數超過了文件末尾,再次寫入時將增大文件尺寸。
       whence:其取值如下:
              SEEK_SET:從文件開頭移動offset個字節
              SEEK_CUR:從當前位置移動offset個字節
              SEEK_END:從文件末尾移動offset個字節
返回值:
       成功:0 失敗:-1 #include <stdio.h> long ftell(FILE *stream); 功能:獲取文件流(文件光標)的讀寫位置。 參數: stream:已經打開的文件指針 返回值: 成功:當前文件流(文件光標)的讀寫位置 失敗:-1 #include <stdio.h> void rewind(FILE *stream); 功能:把文件流(文件光標)的讀寫位置移動到文件開頭。 參數: stream:已經打開的文件指針 返回值: 無返回值 typedef struct Stu { char name[50]; int id; }Stu; //假如已經往文件寫入3個結構體 //fwrite(s, sizeof(Stu), 3, fp); Stu s[3]; Stu tmp; int ret = 0;
//文件光標讀寫位置從開頭往右移動2個結構體的位置 fseek(fp, 2 * sizeof(Stu), SEEK_SET); //讀第3個結構體 ret = fread(&tmp, sizeof(Stu), 1, fp); if (ret == 1) { printf("[tmp]%s, %d\n", tmp.name, tmp.id); }
//把文件光標移動到文件開頭 //fseek(fp, 0, SEEK_SET); rewind(fp); ret = fread(s, sizeof(Stu), 3, fp); printf("ret = %d\n", ret); int i = 0; for (i = 0; i < 3; i++) { printf("s === %s, %d\n", s[i].name, s[i].id); }

10.5 Windows和Linux文本文件區別

  • l  b是二進制模式的意思,b只是在Windows有效,在Linux用r和rb的結果是一樣的
  • l  Unix和Linux下所有的文本文件行都是\n結尾,而Windows所有的文本文件行都是\r\n結尾
  • l  在Windows平台下,以“文本”方式打開文件,不加b:
  • n  當讀取文件的時候,系統會將所有的 "\r\n" 轉換成 "\n"
  • n  當寫入文件的時候,系統會將 "\n" 轉換成 "\r\n" 寫入
  • n  以"二進制"方式打開文件,則讀\寫都不會進行這樣的轉換
  • l  在Unix/Linux平台下,“文本”與“二進制”模式沒有區別,"\r\n" 作為兩個字符原樣輸入輸出

 

判斷文本文件是Linux格式還是Windows格式:

#include<stdio.h>
int main(int argc, char **args)
{
       if (argc < 2) return 0; FILE *p = fopen(args[1], "rb"); if (!p) return 0; char a[1024] = { 0 }; fgets(a, sizeof(a), p); int len = 0; while (a[len]) { if (a[len] == '\n') { if (a[len - 1] == '\r') { printf("windows file\n"); } else { printf("linux file\n"); } } len++; } fclose(p); return 0; }

10.6 獲取文件狀態

#include <sys/types.h>
#include <sys/stat.h> intstat(constchar *path, structstat *buf); 功能:獲取文件狀態信息 參數: path:文件名 buf:保存文件信息的結構體 返回值: 成功:0 失敗-1 struct stat { dev_t st_dev; //文件的設備編號 ino_t st_ino; //節點 mode_t st_mode; //文件的類型和存取的權限 nlink_t st_nlink; //連到該文件的硬連接數目,剛建立的文件值為1 uid_t st_uid; //用戶ID  gid_t st_gid; //組ID dev_t st_rdev; //(設備類型)若此文件為設備文件,則為其設備編號 off_t st_size; //文件字節數(文件大小) unsigned long st_blksize; //塊大小(文件系統的I/O 緩沖區大小) unsigned long st_blocks; //塊數 time_t st_atime; //最后一次訪問時間 time_t st_mtime; //最后一次修改時間 time_t st_ctime; //最后一次改變時間(指屬性) }; #include <sys/types.h> #include <sys/stat.h> #include <stdio.h> int main(int argc, char **args) { if (argc < 2) return 0; struct stat st = { 0 }; stat(args[1], &st); int size = st.st_size;//得到結構體中的成員變量 printf("%d\n", size); return 0; }

10.7 刪除文件、重命名文件名

#include <stdio.h>
intremove(constchar *pathname); 功能:刪除文件 參數: pathname:文件名 返回值: 成功:0 失敗:-1 #include <stdio.h> intrename(constchar *oldpath, constchar *newpath); 功能:把oldpath的文件名改為newpath 參數: oldpath:舊文件名 newpath:新文件名 返回值: 成功:0 失敗: - 1

10.8 文件緩沖區

10.8.1文件緩沖區

ANSI C標准采用“緩沖文件系統”處理數據文件。

所謂緩沖文件系統是指系統自動地在內存區為程序中每一個正在使用的文件開辟一個文件緩沖區從內存向磁盤輸出數據必須先送到內存中的緩沖區,裝滿緩沖區后才一起送到磁盤去。

如果從磁盤向計算機讀入數據,則一次從磁盤文件將一批數據輸入到內存緩沖區(充滿緩沖區),然后再從緩沖區逐個地將數據送到程序數據區(給程序變量) 。

10.8.2磁盤文件的存取

 

  • l  磁盤文件,一般保存在硬盤、U盤等掉電不丟失的磁盤設備中,在需要時調入內存
  • l  在內存中對文件進行編輯處理后,保存到磁盤中
  • l  程序與磁盤之間交互,不是立即完成,系統或程序可根據需要設置緩沖區,以提高存取效率

10.8.3更新緩沖區

#include <stdio.h>
intfflush(FILE *stream); 功能:更新緩沖區,讓緩沖區的數據立馬寫到文件中。 參數: stream:文件指針 返回值: 成功:0 失敗:-1

 


免責聲明!

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



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