【基本语法】函数详解

一、函数概述

1.函数的本质

  1. 组成
    整个程序由多个源文件组成,每个文件由多个函数组成,每个函数由多条语句组成;这种组织形式是为了适应模块化编程即可移植性,而并非机器需要。(因为对于CPU来说所有代码都是二进制机器码,代码的组织形式并不重要)
  2. 函数代码书写的原则
    (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. 普通变量作为参数
    (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. 数组作为参数
    (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. 结构体作为参数
    (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;
}
  1. 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. 概述
    (1)函数指针本质就是指针变量,占内存4byte大小,变量名为函数名,指针指向函数首行代码的地址;
    (2)函数指针、与其他指针本质无区别,只是指向的数据类型不同而已;
    (3)函数的实质就是再内存中连续分布的一段代码,因此得到首行代码的地址就是函数的地址;
  2. 函数指针的定义
    (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函数指针类型的变量pfuncpfunc = 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. 静态链接库
    (1)静态链接库的函数库源码,是将编译后形成众多的.o二进制文件归档成.a文件,然后将.a库文件与.h头文件发布
    (2)用户根据.h头文件得到每个函数的原型,以便在自己的.c文件中传参调用,然后链接时链接器会直接到.a函数库中,使用静态链接库在链接时需要加-static来指定静态链接,将被调用函数的.o二进制代码链接进可执行文件;
    (3)由于使用静态库时,会将函数库中的函数链接进文件中,这就导致了当多个程序都含有共同的一个函数时,就会每个程序都链接这个函数,从而占用大量的内存降低效率;

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

  2. 自己制作动态链接库
    (1)过程同静态库一样,区别在于在编译库文件是需要加上-fPICfPIC是指定库函数为位置无关码,因为任何位置都有可能调用库函数,所以需要指定库函数为位置无关;
    (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. 数学库函数
    (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命令来查看文件中用到的那些库函数;

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值