C 語言的函數 - 內存分析

函數基本概念

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;
}

sprintf 示例

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 示例

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 示例

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 自增
	}
}

void * 形參化指針參數

定義非字符空間處理函數時,老是想作的儘量通用,通常就是逐個字節處理。可是調用處理函數的地方可能須要傳入各類類型的指針(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 函數

memcpy 函數用於操做非字符空間,能夠在 Linux 終端經過 man 3 memcpy 查看語義。

void *memcpy(void *dest, const void *src, size_t n);

recv 和 send 函數

這是兩個 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 *:數據空間

函數返回值

函數是個代碼集合,可是有三個要素:入參,返回值,函數名。

函數經過入參和返回值實現承上啓下的效果。

函數的執行結果,有兩種方式傳給調用者:

  • 返回值:函數執行完後,經過 return 將返回值傳給調用者。函數返回值是值傳遞,調用者需建立新變量接收這個值。
  • 入參的指針:入參是指針,函數執行結果放到這個指針所指向的內存

返回值不是必須的,能夠經過指針類型的入參返回數據給調用者。例如:

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

函數返回值的用法

要保證子函數執行結束後,子函數中開闢的內存空間不被回收,能夠在子函數中建立下面三種類型的數據:

  • 只讀數據區:也就是直接返回雙引號括起來的常量字符串,注意不要賦值給局部變量,不然仍是會被回收
  • 靜態數據區:static 修飾的靜態數據在程序的生命週期內一直存在
  • 堆區:經過 malloc 在堆中開闢的內存空間,只有在 free 後纔會釋放

返回基本類型

返回基本類型的數據時,由於是值傳遞,直接用便可:

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
相關文章
相關標籤/搜索