一、函数概述
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命令来查看文件中用到的那些库函数;