2-函数详解

mac2024-10-27  13

一、函数概述

1.函数的本质

组成 整个程序由多个源文件组成,每个文件由多个函数组成,每个函数由多条语句组成;这种组织形式是为了适应模块化编程即可移植性,而并非机器需要。(因为对于CPU来说所有代码都是二进制机器码,代码的组织形式并不重要)函数代码书写的原则 (1)语法:type func1(type params1, type params2,...) {return n;}; (2)函数体:一个函数只做一件事,若代码较少可用inline修饰; (3)参数列表:传入的参数一般少于4个,若需多个参数,建议将参数打包成结构体,然后传入结构体指针; (4)返回值:一般只一个返回值,若需多个返回值,可用输出型函数(即指针方式)返回多个值;而尽量少用全局变量来返回多个值。 int i, j, k; //定义全局变量 void func1(void) //通过全局变量传参数 { i = 1, j = 2, k = 3; } void func2(int *a, int *b, int *c) //输出型函数 { *a = 1;*b = 2;*c = 3; } int main(void) { int a, b, c; func(&a, &b, &c); printf("%d, %d, %d\n", a, b, c); printf("%d, %d, %d\n", i, j, k); }

** 3. 程序运行的实质就是将程序分成可执行部分和数据部分,通过可执行部分对数据进行加工计算得到目标数据;**

2.函数的参数

普通变量作为参数 (1)普通变量作为传递的函数参数时,只是将实参的值复制,然后赋值给形参,本身不改变实参; (2)若要通过子函数来改变实参的值,只能通过传递实参指针,然后通过访问实参的地址来改变实参的值,也可以通过此方法返回多个结果; (3)实参和形参并不是同一变量,二者在函数调用传递参数时都在内存(栈)中占有空间且地址不同; void func(int a, int b) { printf("&a = %d\n&b = %d\n", &a, &b); } int main() { int a = 10; int b = 20; func(a, b); //形参的地址 printf("&a = %d\n&b = %d\n", &a, &b); //实参的地址,与形参不同 } 数组作为参数 (1)数组作为传递的函数参数时,实际上传递的是指针,没有数组长度这个信息;即将数组首元素的地址传给了形参,因此形参的形式是int a[]或者int a[50]或者int *都无所谓; (2)通常若要将整个数组作为参数时,需要同时传递数组首地址和长度。 void func(int a[], int len) //参数时数组首地址和数组长度 { int i; for (i = 0; i < len; i++) { a[i] = i; //给数组元素赋值 } } int main() { int a[10] = {0}; int i; func(a, 10); for( i= 0; i < 10; i ++) { printf("%d\n", a[i]); //输出 0~10 } return 0; } 结构体作为参数 (1)结构体本质也是普通变量,只是其中包含多种基本数据类型而已; (2)由于结构体一般都很大,将结构体作为参数效率太低,因此一般将结构体指针作为参数,只需传递4个byte,然后通过指针访问结构体变量的实参。 struct A{ //定义结构体 int a; char c; }; void func1(struct A abc) //结构体作为参数 { printf("a1.a = %d\n", abc.a); printf("a1.c = %c\n", abc.c); } void func2(struct A* abc) //结构体指针作为参数 { printf("a1.a = %d\n", abc->a); printf("a1.c = %c\n", abc->c); } int main() { struct A a1 = { //定义结构体变量 a1.a = 1, a1.c = 'c' }; struct A* ps = &a1; //定义结构体指针变量 func1(a1); func2(ps); return 0; } const修饰的指针参数 (1)在形参中使用const关键字,意味着子函数无法修改const修饰的参数; (2)其目的为了向主函数声明,被调用的子函数不会对传入的指针变量指向地址的值进行修改,可以放心大胆的将某个地址传进来; void func(const int* a) //const修饰形参 { *a = 5; //此处会报错,因为const修饰未只读变量,无法修改 printf("%d\n", *a); } int main() { int a = 10; func(&a); //调用子函数 return 0; }

3.函数指针

概述 (1)函数指针本质就是指针变量,占内存4byte大小,变量名为函数名,指针指向函数首行代码的地址; (2)函数指针、与其他指针本质无区别,只是指向的数据类型不同而已; (3)函数的实质就是再内存中连续分布的一段代码,因此得到首行代码的地址就是函数的地址;函数指针的定义 (1)在将函数名赋值给函数指针时,函数的参数列表与返回值必须与函数指针的相同,否则会报警告; (2)函数名func 代表函数首行代码的地址,而在做右值时(即给函数指针赋值时),func与&func代表的意义与数值是相同的。 (3)当用户自己定义数据类型时(如结构体、函数指针等),定义的数据类型可能书写会比较麻烦,可以通过typedef关键字来重新命名数据类型。 /*******例1********/ void (*p1) (void); //定义函数指针 void func1 (void); //定义函数 p1 = func1; //将函数名赋值给函数指针 p1(); //运行func1; /*******例2********/ typedef int (*p1) (int); //定义函数指针类型,类型为p1类型 int func1 (int a); //定义函数 p1 pfunc; //p1类型的函数指针变量pfunc pfunc = func1; //将函数名赋值给函数指针 p1(a); //运行func1;

注意: typedef修饰后定义的是类型, p1 pfunc;是定义p1函数指针类型的变量pfunc,pfunc = func1; 是给pfunc变量赋值; 未用typedef修饰定义的是变量, p1 = func1;是给p1变量赋值

4.递归函数

概述 (1)递归函数就是在函数内部又调用了本身的函数。 (2)递归有区别于循环,递归是一层层深入调用,然后逐层返回结果,而循环是循环调用后直接返回结果; (3)函数调用时,局部变量、实参和返回值都会保存在栈中,每次调用都会占用空间,因此使用递归函数时注意栈内存的消耗,递归调用必须有一个终止递归的条件,否则会陷入死循环递归,最终也会栈溢出;

int func(int n) { printf("%d\n", n); //递归调用4次,打印结果为4 3 2 1 if (n > 1) { func(n-1); //递归调用 } printf("n = %d\n", n); //返回4次的值,结果为1,2,3,4; } int main() { func(4); }

二、函数库

1.静态与动态链接库

静态链接库 (1)静态链接库的函数库源码,是将编译后形成众多的.o二进制文件归档成.a文件,然后将.a库文件与.h头文件发布 (2)用户根据.h头文件得到每个函数的原型,以便在自己的.c文件中传参调用,然后链接时链接器会直接到.a函数库中,使用静态链接库在链接时需要加-static来指定静态链接,将被调用函数的.o二进制代码链接进可执行文件; (3)由于使用静态库时,会将函数库中的函数链接进文件中,这就导致了当多个程序都含有共同的一个函数时,就会每个程序都链接这个函数,从而占用大量的内存降低效率;

自己制作静态链接库 (1)编写函数库func.c文件,然后只编译不链接,生成.o文件:gcc -c func.c -o func.o,并在func.h中声明func.c中的函数; (2)通过ar命令将.o文件归档成.a文件:ar -rc func.o -o libfunc.a (3)在使用时调用库函数及头文件 (4)编译时,通过-lfunc链接libfunc.c函数库,通过-L.指定在当前目录下查找函数库:gcc -c test.c test -lfunc -L. (5)可以通过nm libfunc.a来确定libfunc.a中的.o文件,每个.o文件有多少个func函数。

#inlcude "func.h" //调用库函数的头文件 int main(void) { func1(); //调用库函数中的函数 func2(4, 5); return 0; }

动态链接库 (1)当使用动态链接库时,要注意-L指定动态库的地址,动态库文件不会被链接进可执行程序中,而是只是做一个标记。 (2)当程序执行当中若需要调用库函数时,会到动态库中加载这个函数到内存中,当其他程序也需要该库函数时,就直接跳转到第一次加载的地方去执行,无需重复加载; 注意: (1)使用函数库,需要包含相应的头文件; (2)有些库函数链接时需要额外用-lxxx来指定链接;

自己制作动态链接库 (1)过程同静态库一样,区别在于在编译库文件是需要加上-fPIC,fPIC是指定库函数为位置无关码,因为任何位置都有可能调用库函数,所以需要指定库函数为位置无关; (2)使用gcc编译成.so文件,加-shared后缀成共享类型:

gcc -c func.c -o func.o -fPIC; gcc -o libfunc.so func.o -shared;

(3)调用库函数后,编译主函数的方式与调用静态库方式相同,在同目录下静态库与动态库同名时,系统默认先链接动态库;

gcc -o test test.c -lfunc -L.

(4)由于调用动态库时,只是做了调用动态库函数的标记,因此在运行时系统会先LD_LIBRARY_PATH这个环境变量指定的目录去找动态库函数,若未找到再到默认/usr/lib目录下调用库函数,而自己制作的动态库在当前目录下,因此需要将libfunc.so导入到环境变量中:

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/book/test1

(5)将目录导入环境变量后就可以执行test文件。注意:编译时加 -lfunc -L是链接时为了标记出调用出动态库函数;而运行时无法运行需要修改变量,是由于系统运行时到指定和默认的目录下寻找动态库; (5)ldd命令可以得到文件中调用了多少库函数及是否调用成功;

/******调用成功*******/ book@www.100ask.org:~/test1$ ldd test linux-vdso.so.1 => (0x00007fff5bbed000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5614a6e000) /lib64/ld-linux-x86-64.so.2 (0x00007f5614e38000) /**********调用失败*********/ linux-vdso.so.1 => (0x00007ffd7a9f9000) libfunc.so => not found libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0ccdbf5000) /lib64/ld-linux-x86-64.so.2 (0x00007f0ccdfbf000) 数学库函数 (1)math.h:/usr/include/x86_64-linux-gnu/bits$,需要数学函数(如开平方、三角函数等)的时候,需要包含数库库; (2)注意区分编译错误与链接错误 math.c:9:13: warning: incompatible implicit declaration of built-in function ‘sqrt’ [enabled by default] //编译错误:math.c:9:13:逐行编译发现错误 math.c:(.text+0x1b): undefined reference to `sqrt' collect2: error: ld returned 1 exit status //链接错误:ld:连接器

(3)上述链接错误:sqrt函数有声明(mathcalls.h)、有引用(math.c),但找不到没有函数体,即无法链接到函数库;原因是C语言默认链接常用的库,若要链接不常用的库,需要链接时用-lxxx来指示链接器去到libxxx.so函数库中去查找这个函数。通过ldd a.out即ldd命令来查看文件中用到的那些库函数;

最新回复(0)