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