Linux 中,函數在內存的代碼段(code 區),地址比較靠前。web
C 語言中,函數有三個要素:入參、返回值、函數名,缺一不可。函數使用前必須先聲明,或者在使用以前定義。數組
函數聲明格式以下:socket
int test(int a, char *p);
函數定義格式以下:svg
int test(int a, char *p) { // 乾點啥 return 666; }
char c = 'a'; int result; result = fun(666, &c);
函數定義時,爲了用參數進行操做,爲參數預留的佔位符就是形參。
函數調用時,調用方傳到函數中的真實參數就是實參。函數
函數調用時,傳遞的是參數的值(實際上就是複製一分內存),而非參數的地址。值傳遞時,形參的全部改動,都不會影響實參。值傳遞和引用傳遞的區別:spa
值傳遞示例:指針
#include <stdio.h> int swap(int a, int b) { int tmp; tmp = a; a = b; b = tmp; } int main() { int a = 1, b = 666; printf("before swap, a is: %d, b is: %d\n", a, b); swap(a, b); printf("after swap, a is: %d, b is: %d\n", a, b); return 0; }
輸出:code
before swap, a is: 1, b is: 666 after swap, a is: 1, b is: 666
若是要想在調用的函數中修改參數,就必須傳參數的地址過去,相似上面的函數能夠改成引用傳遞:orm
#include <stdio.h> int swap(int *a, int *b) { int tmp; tmp = *a; *a = *b; *b = tmp; } int main() { int a = 1, b = 666; printf("before swap, a is: %d, b is: %d\n", a, b); swap(&a, &b); // 這裏須要傳地址 printf("after swap, a is: %d, b is: %d\n", a, b); return 0; }
引用傳遞能夠改變原參數,輸出:xml
before swap, a is: 1, b is: 666 after swap, a is: 666, b is: 1
由於值傳遞時,須要爲實參多開闢一分內存,因此在函數參數佔用空間較大時(例如數組、結構體),一般使用引用傳遞。
對於下面的結構體,一般用引用傳遞,而不是值傳遞:
#include <stdio.h> struct People { int age; char * name; }; void fun2(struct People p) { printf("people's name is:%s, age is: %d\n", p.name, p.age); } void fun(struct People *p) { printf("people's name is:%s, age is: %d\n", p->name, p->age); } int main() { struct People p1 = {22, "jack"}; fun(&p1); // 推薦 fun2(p1); }
C 語言中,用數組作函數的參數時要注意,由於數組名自己就是個表示地址的標籤,因此實參是數組時,實際上就是引用傳遞:
int arr[10]; int fun(int *p) {}
引用傳遞時,若是隻是想節省內存空間,而不想讓調用的函數修改該空間;或者會傳遞常量指針給函數。這兩種狀況下,都須要明確把函數聲明中的指針用 const 描述。
編譯經過,運行時段錯誤示例:
#include <stdio.h> void fun(char * p) { p[0] = 'x'; // 由於傳過來的是字符串常量,這裏的修改會報 段錯誤 segmentation fault } int main() { fun("hello"); return 0; }
只讀參數限定示例:
#include <stdio.h> void fun(const char * p) { p[0] = 'x'; // 由於參數限定爲 const,函數內不可修改,不然編譯會報錯 } int main() { fun("hello"); return 0; }
printf 將格式化字符串打印到標準輸出流,而 sprintf 則將格式化字符串輸出到變量中,這幾個函數及定義能夠經過 man 3 sprintf
查看:
int printf(const char *format, ...); int fprintf(FILE *stream, const char *format, ...); int sprintf(char *str, const char *format, ...); int snprintf(char *str, size_t size, const char *format, ...);
#include <stdio.h> int main(void) { int a = 666; char * str; printf("a is: %d\n", a); sprintf(str, "a is: %d\n", a); printf("str is: %s", str); }
輸出:
a is: 666 str is: a is: 666
任何內存空間,在操做以前都須要知道兩個要素:首地址、結束標誌(或字節個數)。
字符空間是以 \0 (0x0000 0000)結束的連續內存空間。\0 這個字符不會出如今字符空間,可是可能出如今非字符空間。字符空間有兩種限定方式:
const char *p
:常量,不可修改,例如字符串常量。一般用雙引號初始化 "..."
。char *p
:變量,容許修改,例如字符數組。一般用字符數組初始化 char buf[5]
。void fun(char *p) { int i = 0; while(p[i] != '\0') // 這裏也能夠直接用 while(p[i]) { //乾點啥 i++; } }
strlen 函數用於統計字符空間中字符的個數,函數語義以下:
int strlen(const char * str);
能夠本身實現一個 strlen:
int mystrlen (const char *p) { // 錯誤處理 if (p == NULL) return 0; // 內存處理 int i = 0; while(p[i]) { i++; } return i; }
strcpy 用於拷貝字符,函數語義以下:
void strcpy(char * dest, const char *src);
可見 strcpy 函數的源字符串限定爲 const char *
類型,不可修改。
字符空間固定以 \0
結束,相反,非字符空間沒有結束標誌,因此在操做的時候,須要另一個參數:字節數。非字符空間也有兩種定義方式:
unsigned char * p
:非字符空間,能夠讀寫。const unsigned char * p
:非字符空間,只讀。非字符空間的函數須要兩個參數:空間首地址,空間大小,例如:
void fun(unsigned char *p, int size) { int i; for (i = 0; i < size; i++) { // 針對當前字節 p[i] 進行讀寫操做,而後 i 自增 } }
定義非字符空間處理函數時,老是想作的儘量通用,通常就是逐個字節處理。可是調用處理函數的地方可能須要傳入各類類型的指針(int、long、struct 等)。C++ 中有模板類,而 C 語言針對這種狀況,容許函數聲明中用 void *
通配各類參數。通配符非字符空間也有兩種定義方式:
void * p
:非字符空間,能夠讀寫。const void * p
:非字符空間,只讀。通配符接受的參數,在使用前須要強轉爲具體類型(一般就是無符號字符):
void fun(void *p, int size) { unsigned char * ps = (unsigned char *)p; // 轉爲字節指針 //printf("%s\n", ps); // 這是個反例,非字符不可當字符串讀取,可能出問題 }
memcpy 函數用於操做非字符空間,能夠在 Linux 終端經過 man 3 memcpy
查看語義。
void *memcpy(void *dest, const void *src, size_t n);
這是兩個 socket 通訊的函數,在 <sys/socket.h>
頭文件中聲明,函數語義爲:
ssize_t recv(int socket, void *buffer, size_t length, int flags); ssize_t send(int socket, const void *buffer, size_t length, int flags);
根據子函數是否具備修改實參的能力,能夠分爲:
字符空間和數據空間的引用類型:
char *
:字符空間,以 \0
結束void *
或 unsigned char *
(推薦用 void *
):數據空間,操做時需同時指定字節數引用傳遞時,若是要限制子函數對實參的修改能力,能夠加 const 限定:
const char *
:字符空間const void *
:數據空間函數是個代碼集合,可是有三個要素:入參,返回值,函數名。
函數經過入參和返回值實現承上啓下的效果。
函數的執行結果,有兩種方式傳給調用者:
返回值不是必須的,能夠經過指針類型的入參返回數據給調用者。例如:
int fun1(); //函數返回 int 值 void fun2(int *); //函數接收並直接操做 int 指針,實現跟上面返回值同樣的效果
上面兩個函數,調用方式以下:
int a = 0; a = fun1(); fun2(&a);
函數能夠直接返回 int、char、double 等類型。由於是值傳遞,調用者和子函數各自都有一份返回值的內存空間,因此數據較大(例如 struct 結構體)時,不適合直接返回。
直接返回變量在內存空間中的地址。
注意:函數返回值是指針時,須要確保其指向地址的合法性!!
若是返回值在棧中(局部變量),則必定有問題!能夠在全局變量區、數據區、堆區。
int * fun1(); // 函數返回 int 指針 void fun2(int **p); // 函數接收 int 指針的指針
完整實例:
#include <stdio.h> int * fun1() { int a = 666; //return &a; // 這裏有警告,由於返回了局部變量,這塊內存空間在子函數執行完後會被回收掉 return 666; } void fun2(int **p) { int a = 888; **p = a; // 直接改值,也能夠改指針地址 } int main () { int *a; a = fun1(); printf("a is: %x, a's value is: %d\n", a, *a); fun2(&a); printf("a is: %x, a's value is: %d\n", a, *a); return 0; }
輸出:
a is: 59298a3c, a's value is: 666 a is: 59298a3c, a's value is: 888
注意:函數返回值是指針時,須要確保其指向地址的合法性!!
若是返回值在棧中(局部變量),則必定有問題!能夠在全局變量區、數據區、堆區。
C 函數中,沒法直接返回數組。若是須要返回連續空間,須要返回指針。例如上面的
int *fun();
就是返回 int 類型的連續空間。
函數返回指針時,須要注意地址指向的合法性。
返回字符串指針時,須要指向常量區等全局有效的地址。若是當作字符數組,由於是局部變量,會出問題。示例:
#include <stdio.h> char * fun3() { //char str[] = "hello"; // 這裏建立的字符數組,在子函數執行結束後釋放內存,因此返回值的地址非法!! //return str; return "hello"; // 這裏建立的字符串常量,存放在內存的常量區,程序執行過程當中不會釋放 } int main () { char * p = fun3(); printf("p is: %s\n", p); return 0; }
輸出:
p is: hello
要保證子函數執行結束後,子函數中開闢的內存空間不被回收,能夠在子函數中建立下面三種類型的數據:
返回基本類型的數據時,由於是值傳遞,直接用便可:
int fun() { int a = 666; return a; }
若是返回的是基本類型的指針,就須要確保指針的合法性。下面兩個例子是反例,由於局部變量的內存空間在函數執行完畢後被釋放,因此指針非法,編譯時部分編譯器會給出警告:
#include <stdio.h> int * fun() { int * a; // 局部變量在程序執行結束後釋放 int b = 666; a = &b; return a; } char * fun2() { char *str = {"hello"}; // 局部變量在程序執行結束後釋放 return str; } char * fun3() { static char *str = {"hello"}; // 靜態數據區的數據,在程序執行過程當中一直有效 return str; } int main() { int *a = fun(); char * s = fun2(); printf("%d\n", *a); printf("%s\n", s); return 0; }
前面說了,局部變量在子函數執行完畢後,內存會被釋放。返回這個野指針就會出問題。
爲了不這種狀況,能夠用 static 修飾局部變量,使其存儲在靜態區。靜態區的數據跟數據區同樣,在程序執行時不會釋放:
#include <stdio.h> #include <string.h> #include <stdlib.h> char * fun() { char * s = (char *)malloc(100); strcpy(s, "hello"); return s; // 只讀區的數據在程序執行時不會釋放 } char * fun2() { return "hello"; // 只讀區的數據在程序執行時不會釋放 } char * fun3() { static char str[] = "hello"; // 靜態區的數據跟只讀區同樣,在程序執行時不會釋放 return str; } int main () { char * p = fun(); printf("p is: %s\n", p); free(p); //釋放堆空間 char * p2 = fun2(); printf("p is: %s\n", p2); char * p3 = fun3(); printf("p is: %s\n", p3); return 0; }
輸出:
p is: hello p is: hello p is: hello
C 語言中,數組名就是一個標籤,指向一段內存。函數名跟數組名相似,也是一個指向一段內存的標籤,有對應的地址:
#include <stdio.h> int main() { int a[3]; printf("array a locate at: %p\n", a); printf("function main locate at: %p\n", main); return 0; }
輸出:
array a locate at: 0x7ffec8099430 function main locate at: 0x40052d
數組的地址能夠賦值給指針,函數的地址一樣也能夠傳給指針。這裏以 printf 爲例,庫函數的具體定義,能夠經過 man 3 printf
查看。
注意,在建立指向函數的指針時,須要保證參數的一致,不然編譯會報錯:
#include <stdio.h> void fun(int a) { printf("printed in fun(), a is:%d", a); } int main() { printf("fun's address is: %p\n", fun); int (*p1)(const char *, ...) = printf; p1("print by p: hello\n"); int (*myshow)(const char *, ...); myshow = (int (*)(const char*, ...))printf; myshow("print by myshow:666\n"); int (*p2)(int); // 建立指向函數的指針 p2 = (int (*)(int))fun; // 將函數的地址轉爲指針 p2(666); // 用指針執行函數 int (*p[1])(int); p[0] = (int (*)(int))fun; p[0](888); return 0; }
#include <stdio.h> void fun1(int a) { printf("printed in fun1(), a is:%d\n", a); } void fun2(int a) { printf("printed in fun2(), a is:%d\n", a); } int main() { int (*p[2])(int); // 建立包含兩個元素的數組 p,每一個元素是都指向函數的指針 p[0] = (int (*)(int))fun1; p[1] = (int (*)(int))fun2; p[0](888); p[1](666); return 0; }
輸出:
printed in fun1(), a is:888 printed in fun2(), a is:666