一:函數的本質
? ? ? ? 1:C語言為什么會有函數
? ? ? ? ? ? ? ? (1)整個程序分為多個源文件,一個文件分為多個函數,一個函數分成多個語句,這就是整個程序的組織形式。這樣的組織好處在于:分化問題、、便于程序編寫、便于分工
? ? ? ? ? ? ? ? (2)函數的出現是人(程序員和架構師)的需要,而不是機器(編譯器、cpu)的需要
? ? ? ? ? ? ? ? (3)函數的目的就是實現模塊話編程
? ? ? ? 2:函數書寫的一般原則
? ? ? ? ? ? ? ? (1)遵循一定的格式。函數的返回類型、函數名、參數列表等
? ? ? ? ? ? ? ? (2)一個函數只做一件事,不能太長也不能太短,原則是一個函數只做一件事
????????????????(3)傳參不宜過多,在ARM體系下,傳參不宜超過四個。如果傳參確實需要多則考慮結構體打包
? ? ? ? ? ? ? ? (4)盡量少碰全局變量,函數最好用傳參返回值來和外部交換數據,不需要用全局變量
? ? ? ? 3:函數是動詞,變量是名詞? ? ? ??
? ? ? ? ? ? ? ? (1)函數將來被編譯成可執行代碼段,變量(主要指全局變量)經過編譯后變成數據或者運行時變成數據。一個程序的運行需要數據和代碼兩方面的結合才能完成
? ? ? ? ? ? ? ? (2)代碼和數據需要彼此配合,代碼是為了加工數據,數據必須借助代碼來起作用。
? ? ? ? 4:函數的實質是:數據處理器
? ? ? ? ? ? ? ? (1)程序的主體是數據,程序運行的主要目標是生成目標數據,程序員寫代碼也是為了目標數據。如何得到目標數據?必須兩個因素:原材料和加工算法。原材料就是程序輸入的數據,加工算法就是程序
? ? ? ? ? ? ? ? (2)程序的編寫和運行就是為了把原數據加工成目標數據,所以程序的實質就是一個數據處理器
? ? ? ? ? ? ? ? (3)函數就是程序的一個縮影,函數的參數列表就是為了給函數輸入原材料數據,函數的返回值和輸出參數就是為了向外部輸出目標數據,函數的函數體內部的那些代碼就是加工算法。
? ? ? ? ? ? ? ? (4)函數在靜止時沒有執行(待在硬盤里),此時只會占用一些存儲空間但是并不會占用資源(cpu+內存);函數的每一次運行時都需要耗費資源(cpu+內存),運行時可以對數據加工生成目標數據;函數運行完成后會釋放占用的資源
? ? ? ? ? ? ? ? (5)整個程序的運行其實就是很多個函數相繼運行的連續過程
二:函數的基本使用
? ? ? ? 1:函數三要素:定義、聲明調用
? ? ? ? ? ? ? ? (1)函數的定義就是函數體、函數的聲明就是函數的原型、函數的調用就是使用函數
????????????????????????
#include <stdio.h>int add(int a,int b); //函數聲明int main(void)
{int a = 3;int b = 5;int c = 0;c =add(a,b); //函數調用printf("c = %d\n",c); //結果8 printf("3+5 = %d\n",add(3+5)); //8 //add函數的返回值作為printf函數的參數return 0;}//函數定義
int add(int a,int b) //函數名、參數列表、返回值
{return a + b; //函數體
}
? ? ? ? ? ? ? ? ?(2)函數定義是函數的根本,函數定義中的函數名表示了這個函數在內存中的首地址,所以可以使用函數名來調用執行這個函數(實質是指針解引用訪問);函數定義中的函數體是函數的執行關鍵,函數將來執行主要就是執行函數體。
? ? ? ? ? ? ? ? (3)函數聲明的主要是告訴編譯器函數的原型
? ? ? ? ? ? ? ? (4)函數調用就是執行一個函數?
? ? ? ? 2:函數原型和作用
? ? ? ? ? ? ? ? (1)函數原型就是函數的聲明,就是函數的函數名、返回值類型、參數列表
? ? ? ? ? ? ? ? (2)函數原型的主要作用就是給編譯器提供原型,讓編譯器在編譯程序時幫助程序員進行參數的靜態類型檢查
? ? ? ? ? ? ? ? (3)編譯器在編譯文件時是以單個源文件為單位的(所以一定要在哪里調用在哪里聲明),而且編譯器工作時就已經經過預處理了,最重要的是編譯器編譯文件時是按照文件中語句的先后順序來執行的
? ? ? ? ? ? ? ? (4)編譯器從源文件的第一行開始編譯,遇到函數聲明時就會收到編譯器的函數聲明表中,然后繼續向后。當遇到一個函數調用時,就在本文件函數聲明表中去查這個函數,看有沒有原型相對應的一個函數(這個相對應的函數有且只能有一個)。如果沒有或者只有部分匹配只會報錯和報警告,如果發現多個則會報錯和報警告(函數重復了,C語言中不允許2個函數原型完全一樣這個過程其實是在編譯器遇到函數定義時完成的,所以函數可以重復聲明但是不能重復定義)
? ? ? ? 3:函數傳參
? ? ? ? ? ? ? ? (1)函數傳參的個數和類型要匹配
? ? ? ? ? ? ? ? (2)如果傳參個數太多可以用結構體打包
三:遞歸函數
? ? ? ? 1:什么是遞歸函數
? ? ? ? ? ? ? ? (1)遞歸函數就是函數值調用了自己本身的函數的函數
? ? ? ? ? ? ? ? (2)遞歸函數和循環的區別。遞歸不等于循環
? ? ? ? ? ? ? ? (3)遞歸函數解決問題的典型就是:求階乘、求裴波那切數列
????????????????
//求階乘
#include <stdio.h>int jiecheng(int n); //函數聲明int main(void)
{int a = 5;int c = 0;c = jiecheng(a); //函數表達式展開首先>1 != 1 ,所以進入else 中//retuen (n * jiecheng(n-1))//就等于 (5 * (4 * (3 * (2 * (1))))) 結果為120printf(" c = %d\n",c);return 0;}int jiecheng(int n)
{if(n<1) //判斷是否小于1 ,小于1 輸出n!<1 并返回 -1{printf("n !<1");return -1;}else if(n == 1){return 1; //n等于1返回 1 }else{return (n * jiecheng(n-1)); //n<1則返回 n * 函數本身(傳參-1),直接調用 //本身這個函數,直到不滿足<1時不在繼續調用執行本身函數返回所乘的值}}
? ? ? ? 2:函數的遞歸調用原理
? ? ? ? ? ? ? ? (1)實際上遞歸函數是在棧內存上遞歸執行的,每次遞歸執行一次就需要耗費一些棧內存
? ? ? ? ? ? ? ? (2)棧內存大小是限制遞歸深度的重要因素
? ? ? ? 3:使用遞歸函數的調用原則:收斂性、棧溢出
? ? ? ? ? ? ? ? (1)收斂性就是函數必須要有一個終止遞歸的條件。當每次這個函數被執行時,我們判斷一個條件決定是否結束遞歸,這個條件最終必須被滿足。如果沒有遞歸終止條件或者這個條件永遠不能被滿足,則這個遞歸沒有收斂性,這個遞歸最終要
? ? ? ? ? ? ? ? (2)因為遞歸是占用棧內存的,每次遞歸都會消耗一些棧內存。因此必須在棧內存耗盡之前遞歸收斂(終止)否則棧就會溢出
四:函數庫
? ? ? ? 1:什么是函數庫?
? ? ? ? ? ? ? ? (1)函數庫就是一些事先寫好的函數的集合
? ? ? ? ? ? ? ? (2)函數是模塊化的,因此可以被復用。程序員寫好了一個函數,可以被反復使用。也可以a寫好了一個函數共享出來,當b有需求是就不用重新寫這個函數直接用a寫好的就可以了
? ?? ? 2:函數庫的由來
? ? ? ? ? ? ? ? (1)最開始是沒有函數庫的,每個人寫程序都要自己重頭開始自己寫。時間久了程序員就積累了一些有用的函數(經常要用到的函數 )
? ? ? ? ? ? ? ? (2)早起的程序員經常參加行業聚會,在聚會上大家交換各自的函數庫
? ? ? ? ? ? ? ??(3)后來的行業大神就提出把大家各自的函數收攏在一起,然后經過校驗和整理,形成一份標準的函數庫,就是現在公開的函數庫,
? ? ? ? 3:函數庫的提供形式:動態鏈接庫與靜態鏈接庫
? ? ? ? ? ? ? ? (1)早起的函數共享都是以源代碼的形式進行的。這種共享的方式是最徹底的(后來這種源碼共享的方向就變成了現在的開源社區)。但是也有缺點,缺點就是無法以商業化的方式發布函數庫
? ? ? ? ? ? ? ? (2)商業公司需要將自己有用的函數共享給別人(付費的形式),但是不能給客戶源代碼,這時候的解決方案就是以庫a(主要是以靜態庫和動態庫)的形式來提供
? ? ? ? ? ? ? ? (3)比較早出現的是靜態鏈接庫。靜態庫就是商業公司將自己的函數源代碼只編譯不鏈接形成.O的目標文件,然后用ar工具將.O文件歸檔成.a的歸檔文件(.a的歸檔文件又叫靜態鏈接文件)。商業公司通過發布.a庫文件和.h頭文件來提供靜態庫來給客戶使用:客戶拿到.a和.h文件之后,通過.h頭文件得知庫中庫函數的原型,然后在自己的.c文件中調用這些庫文件 ,在鏈接的時候鏈接器會去.a文件中拿出被調用的那個函數的編譯后的.o二進制代碼段鏈接進去形成最終的可執行程序
? ? ? ? ? ? ? ? (4)動態鏈接庫比靜態鏈接庫要出現的晚一些,效率要高一些。現在基本上都是用動態庫的。靜態庫在用戶鏈接自己的可執行程序時就已經把調用的的庫中的函數的代碼段鏈接進最終的可執行程序中了,這樣的好處是可以執行,壞處是太占地方了。尤其是有多個應用程序都使用了這個庫函數的時候,實際在最后生成的可執行程序中都各自有一份這個函數的代碼段。當這些應用程序同時在內存中運行時,實際上在內存中有多個這個庫函數的代碼段,這完全重復了,而動態鏈接庫本身不將庫函數的代碼段鏈接到可執行程序,只是做個標記,然后當應用程序在執行時,運行時環境發現調用了一個動態庫的庫函數時,會去加載這個動態庫去內存中,然后不管以后多少次調用執行這個動態庫中的函數都會跳轉到第一次加載的地方去執行,不會重復去加載。
//使用靜態鏈接動態鏈接編譯鏈接遞歸函數實現階乘程序來查看動態鏈接和靜態鏈接的區別
#include <stdio.h>int mem(int n)
{if(n<1){return -1;}if(n== 1){return 1; }else{return n * mem(n-1);}}int main(void)
{int a = mem(9);printf("a = %d\n",a);return 0;}
? ? ? ? ?使用linux環境下用gcc編譯工具鏈
? ? ? ? 用gcc默認編譯目標文件4.6.8.func.c (默認為動態鏈接)? ? ? ? 輸出 a = 362880查看可執行文件./a.out文件大小為16728
? ? ? ? 使用強制靜態鏈接方式編譯鏈接目標文件4.6.8.func.c? ? ? ?輸出a = 362880 查看可執行文件大小為871856
?????????
? ? ? ? 4:函數庫中庫函數的使用
? ? ? ? ? ? ? ? (1)linux的gcc中編譯鏈接程序默認是使用動態鏈接,要想使用靜態鏈接需要顯示使用-static來強制靜態鏈接
? ? ? ? ? ? ? ? (2)庫函數的使用需要注意四點:第一,包含相應的頭文件;第二,調用庫函數時要注意函數原型;第三,有些庫函數鏈接時需要額外用-lxxx來指定鏈接;第四,如果是動態庫,要注意-L指定動態庫的地址
五:字符串函數
? ? ? ? 1:什么是字符串
? ? ? ? ? ? ? ? (1)字符串就是在多個字符在內存中連續分布組成的字符結構。字符串的特點是指定了開頭(字符串的指針)和結尾(結尾為固定字符'\0'),而沒有指定長度(長度由開頭地址和結尾地址相減得到)? ??
? ? ?? ?2:常用字符串處理函數
? ? ? ? ? ? ? ? (1)c庫中字符串處理函數包含在string.h頭文件中,這個文件在ubuntu系統中在/use/include中? ? ? ?
? ? ? ? ? ? ? ? (2)常見字符串處理函數及其用法
????????????????????????
六: 數學庫函數
? ? ? ? 1:math.h
? ? ? ? ? ? ? ? (1)真正的數學運算函數定義在:/usr/include/i386-linux-gnu/bits/mathcalls.h
? ? ? ? ? ? ? ? (2)使用數學庫函數的時候,只需要包含math.h即可
? ? ? ? 2:計算開平方
? ? ? ? ? ? ? ? (1)庫函數:double sqrt(double x);
????????????????????????
//使用庫函數中math.h頭文件中聲明的sqrt函數來計算開平方的值#include <stdio.h>
#include <math.h> //數學庫函數int main(void)
{double a = 16.0; //定義一個double類型的變量double b = sqrt(a); //計算a變量存的值的平方printf("b = %lf\n",b); return 0;}
但是如果在linux中進行編譯鏈接可能會出現報錯
分析:這個鏈接錯誤的意思是;sqrt函數有聲明(聲明就在math.h中)有引用(在math.c)但是
沒有定義,鏈接器找不到函數體。sqrt本來是庫函數,在編譯器庫中是有.a和.so鏈接庫的(函
數體在鏈接庫中的)
?????????
? ? ? ? ? ? ? ? ? ? ? ? 這里顯示main函數中的sqrt未定義引用,其實是定義聲明了的只是找不到函數原型
? ? ? ? ? ? ? ? ? ? ? ? 但是當我們把頭文件包含的math.h函數屏蔽時的報錯
解決辦法是在編譯鏈接文件時后面加-lm也就是:? ? ? ?gcc 4.6.10.math.c -lm
下面圖可以看到添加了-lm之后不報錯,運行可執行文件也輸出了a的值的開平方也就是16的開平方4
? ? ? ? ? ??
鏈接器的工作特點:因為庫函數有很多,鏈接器去庫函數目錄搜索的時間比較久。為了提升速
度想了一個折中的方案:鏈接器只是默認的尋找幾個最常用的庫,如果是一些不常用的庫中的函
數被調用,需要程序員在鏈接時明確給出要擴展查找的庫的名字。鏈接時可以用-1xxx來指示鏈
接器去到libxxx.so中去查找這個函數
? ? ? ? 3:鏈接時加 -lm
? ? ? ? ? ? ? ? (1)-lm就是告訴鏈接器到libm中去查找用到的函數
? ? ? ? ? ? ? ? (2)高版本的gcc會出現不用加lm也可以編譯鏈接成功
七:自己制作靜態連接庫并使用
? ? ? ?1: 第一步,自己制作靜態鏈接庫? ? ? ? (在ubuntu系統中)
? ? ? ? 在ubuntu中創建文件夾(名為static_lib)-> 進入創建的文件夾static_lib -> 創建一個名為static_lib.c的.c源文件和一個名為static_lib.h的文件 -> 在static_lib.c源文件中寫入代碼 -> 在static_lib.h頭文件中聲明函數 -> 使用gcc static_lib.c -o static_lib.o -c命令行命令只編譯不鏈接生成static_lib.o文件 -> 使用ar -rc libstatic_lib.a static_lib.o命令行命令使用ar工具打包成.a歸檔文件
????????????????
#include <stdio.h>void func1(void)
{printf("func1 in static_lib.c.\n");}int func1(int a,int b)
{printf("func2 in static_lib.c.\n");return a + b;}
? ? ? ? ? ? ? ? (1)首先使用gcc -c 只編譯不鏈接,生成.o文件;然后使用ar工具進行打包成.a歸檔文件
? ? ? ? ? ? ? ? (2)庫名不能隨便亂起,一般是lib+庫名稱,后綴名是.a表示是一個歸檔文件
? ? ? ? ? ? ? ? (3)制作出來靜態庫后,發布需要發布.a文件和.h文件
? ? ? ? 2:第二步,使用靜態鏈接庫
????????????????在static_lib文件夾中創建一個名為static_lib的子文件夾? ->? 將剛剛生成的歸檔文件libstatic.a和static.h文件復制到ststic_lib子文件夾中->創建一個名為test.c的源文件,在里面寫入代碼調用靜態庫內的函數->使用gcc test.c -o test -lstatic_lib -L.命令編譯鏈接生成可執行文件->運行可執行文件
cp /home/tu/tu_min_kang/4.6/static_lib/libstatic_lib.a ?/home/tu/tu_min_kang/4.6/static_lib/teselib
cp /home/tu/tu_min_kang/4.6/static_lib/static_lib.h /home/tu/tu_min_kang/4.6/static_lib/teselib
#include "static_lib.h"
#include <stdio.h>int main(void)
{func1();int a = func2(3,5);printf("a = %d\n",a);return 0;}
?
? ? ? ? ? ? ? ? (1)把.a文件和.h放在需要引用的文件夾下,然后在.c文件中包含庫文件使用庫函數
? ? ? ? ? ? ? ? 第一次編譯方法 :gcc? tese.c -o test
? ? ? ? ? ? ? ? 報錯:/usr/bin/ld: /tmp/ccniBjn5.o: in function `main':
??????????????????????????test.c:(.text+0xd): undefined reference to `func1'
??????????????????????????/usr/bin/ld: test.c:(.text+0x1c): undefined reference to `func2'
? ? ? ? ? ? ? ? ? ? ? ? ? collect2: error: ld returned 1 exit status
? ? ? ? ? ? ? ? 第二次編譯方法:gcc test.c -o test -lstatic_lib
? ? ? ? ? ? ? ? 報錯:/usr/bin/ld: cannot find -lstatic_lib collect2: error: ld returned 1 exit status
? ? ? ? ? ? ? ? 第三次 編譯方法:gcc test.c -o test -lstatic_lib -L.
? ? ? ? ? ? ? ? 無報錯
? ? ? ? ? ? ? ? 運行結果:
????????????????
? ? ? ??
八:自己制作動態鏈接庫并使用
? ? ? ? 動態鏈接庫的后綴是.so(對于windows系統的dll),靜態庫的擴展名是.a
? ? ? ? 1:創建動態鏈接庫并使用流程
? ? ? ? ? ? ? ? 編寫.c源文件和.h源文件 ->? 使用gcc? dynamic_lib.c? -o? dynamic_lib.o -c -fPIC 命令將.c源文件只編譯不鏈接生成.o文件(-fPIC是告訴編譯器在編譯時將程序變成位置無關碼,這意味著生成的目標文件是與其載入的內存位置無關的,可以被載入到進程的任何位置,只要所有的內存訪問都是正確的。位置無關碼對于共享庫(如.so
文件)是非常重要的,因為它們需要能夠被多個進程同時使用,而不會相互干擾。) -> 再使用gcc -o libdynamic_lib.so? dynamic_lib.o -shared生成動態鏈接庫(-shared告訴編譯器我們要將它設用置為共享庫的方式來進行鏈接)->編寫一個.c文件來調用生成的動態鏈接庫 -> 使用gcc test.c -o test -ldynamic_lib -L 命令來編譯鏈接文件 ->?將動態鏈接庫復制一份到/usr/lib目錄下 ->./test執行程序
????????????????
#include <stdio.h>void func1(void)
{printf("func1 in static_lib.c.\n");}int func1(int a,int b)
{printf("func2 in static_lib.c.\n");return a + b;}
void func1(void);
int func2(int a,int b);
?
?
#include <stdio.h>
#include "dynamic_lib.h" int main(void)
{func1();int a = func2(3,5);printf("a = %d\n",a);return 0;}
?
?
?
? ? ? ? ?第一次編譯:gcc test.c -o test
? ? ? ? 報錯:/usr/bin/ld: /tmp/ccDhA63w.o: in function `main':
????????????????test.c:(.text+0xd): undefined reference to `func1'
????????????????/usr/bin/ld: test.c:(.text+0x1c): undefined reference to `func2'
????????????????collect2: error: ld returned 1 exit status
? ? ? ? ?第二次編譯:gcc test.c -o test -ldynamic_lib
? ? ? ? 報錯:/usr/bin/ld: cannot find -ldynamic_lib
???????????????????collect2: error: ld returned 1 exit status
? ? ? ? 第三次編譯:gcc test.c -o test -ldynamic_lib -L.
? ? ? ? 無報錯
? ? ? ? 但是運行test可執行文件報錯 原因是:動態庫在運行時需要被加載(運行程序時環境在執行test時發現他動態鏈接了 libdynamic_lib.so,于是會去固定目錄嘗試加載 libdynamic_lib.so,失敗則會打印錯誤)
????????
? ? ? ? 解決方法1:將動態鏈接庫復制一份到/usr/lib目錄下?
使用ldd指令查看? ? ? ? :ldd test
?
? ? ? ? 解決方法2:使用環境變量LD_LIBRARY_PATH 操作系統在加載固定目錄/uer/lib之前會去?環境變量LD_LIBRARY_PATH指定的目錄去尋找,如果找到就不要去/usr/lib目錄下找,沒找到才會去/usr/lib
? ? ? ?export
LD_LIBRARY_PATH=$LD_LIBARY_PATH:/home/tu/tu_min_kang/4.6/4.6.12.dynamic_lib/testlib
?
????????
????????????????
????????????????