C语言——函数

函数的主要思想是:函数其实是从上到下逐步求解的过程,把一个大的问题拆成多个小的子问题或者说把一个大的功能拆成小的功能模块,通过实现小的功能最终实现大的功能的过程。

函数的语法

类型标识符 函数名(形式参数)
{
函数体语句;
}

其中类型标识符是函数要带出结果的类型也就是返回值的数据类型,注意数组类型不能作为返回值类型。如果函数不需要带出结果时就可以将返回结果的类型标识符设置为void。如果返回结果的类型和函数的类型标识符不一致,其最终结果的类型以类型标识符为准,也就是最终结果的类型都会转换为类型标识符的类型,所以在返回结果时要注意返回结果的类型和类型标识符是不是一致,否则可能会导致数据类型有高精度转为低精度而导致返回结果的精度丢失。还需要注意的是如果不写类型标识符默认是int类型。

函数名,函数名的命名规则和命名标识符的命名规则是一致的,名字必须以字母数字下划线构成、数字不能开头,不能和关键字或库文件重名,在进行函数名的命名时建议起与函数功能有关的名字这样做在后期再查看代码时看到函数名就能大致知道该函数的作用。

形式参数,形式参数表示的是函数体会用到的数据,它的作用是用来接收实际参数的,那么实际参数是怎么传给形式参数的呢?这里暂且只介绍值传递,一般情况下我们会在main函数中定义实际参数然后在调用函数的时候把实际参数传给形式参数,此时需要注意的是main函数定义的实际参数和函数中的形式参数是两个不同的变量,它们存放的内存空间也不同,所以再传参是实际上是把实际参数的数值传给了形式参数,这一个过程就叫做值传递,对于值传递形式参数的改变是不会影响到实际参数的。如果函数不需要接受参数时可以把形式参数设置为void,实际参数和形式参数是有一定的对应关系的:1、实参和形参的类型必须匹配;2、传进函数的参数必须和定义的形式参数个数相同;3、传参的顺序要一 一对应;
形参的写法:
数据类型 形参变量名1,数据类型 形参变量名2 … …
在定义形参变量是要注意:形参变量必须明确指定类型,而不能写成:
int max(int x, y),这里形参y并没有指定类型。

函数体代码,函数体代码就是函数要实现功能的那一部分代码,在编写函数体代码实现功能时要尽量保证函数功能的单一性。

**函数定义的位置,**函数定义的位置有两种:1、定义在main函数之前;2、定义在main函数之后;如果函数定义在main函数之后,需要在使用(函数调用)前作函数声明,函数声明就是函数头+分号。

下面用判断一个数是否为素数的例子来说明函数的定义到调用最后实现功能过程;

#include <stdio.h>


int isPrimeNum(int num)
{
	int i = 2;
	int flag = 1;

	for (i = 2; i < num; ++i)
	{
		if(num % i == 0)
		{
			flag = 0;
			break;
		}
	}

	return flag;
}

int main(void)
{
	int num = 0;

	scanf("%d", &num);

	if(isPrimeNum(num))
	{
		printf("%d is primenumber\n", num);
	}
	else
	{
		printf("%d is not primenumber\n", num);
	}

	return 0;
}

这个函数实现了在键盘上输入一个数据并判断该数据是否为素数,其实这个功能可以直接在main函数中实现转换成函数的方式实现可以让代码看起来更加整洁,在后期维护的时候也更加方便。

函数的调用关系

函数的调用关系中只有调用者和被调用者的关系,对于main函数它是整个程序的入口所以它只能是调用者别的函数不能去调用main函数,但是对于其他的函数调用者和被调用者的关系是相对的,一个函数可以去调用别的函数也可以被别的函数去调用。例如:

int isLeapYear(int year)

getMonthDays(int year, int month)
{
	isLeapYear(int year);
}

main()
{
	getMonthDays(year, month);
}

这里main函数去调用getMonthDays函数而getMonthDays函数去调用isLeapYear函数,从这里就可以看出函数调用和被调用的关系是相对的。
还需要注意的是:函数是不支持嵌套定义的,但是支持嵌套调用。

函数名代表的是函数的入口地址,在调用的时候可以通过函数名来查找相关函数,那么CPU在执行别的函数的时候再回到main函数CPU是怎么知道从哪一个位置继续往下执行的呢?
其实是这样的当要执行别的函数跳出main函数前做了一个保护现场的动作,当执行完别的函数的时候再回到main函数时恢复现场CPU就能继续执行下去了。那CPU是怎么保护现场的呢?其实是通过栈(是一块内存空间)这种数据结构来保护现场的,在调用别的函数时会把main函数的数据进行压栈再恢复现场时只需要把数据出栈就可以了,上述过程对于其他的函数也适用,如果存在多个函数嵌套调用时,在调用下一个函数前都会把当前函数的数据进行压栈,直到被调用的最后一个函数执行完之后,会把数据出栈利用栈先进后出的特点先进栈的数据会在后面出栈,这特点就能够让程序正确运行,能让现场得到正确的恢复。
栈也是有大小的默认情况下是8M大小但是可以修改,如果一致往栈上放数据直到栈满程序会报段错误。
C语言程序把内存划分了5个区域:
1、栈,主要用来存放自动变量或函数调用的数据 ;
2、堆的特点是:空间大,堆上的空间需要手动申请手动释放 ;
3、字符串常量区,这片内存是只读的;
4、静态区(全局区)用来存放全局变量和静态变量 ;
5、代码区这片区域也是只读的 ;

递归

递归是一种特殊的函数嵌套调用和循环,解决递归问题的思路:要求解决问题n就必须依赖于问题n-1的解决;
递归代码实现思路:
递推关系怎么从问题n到问题n-1再从为题n - 1然后怎么由问题n - 1返回给问题n实现回归的过程,递归对于我来说还是会有一些难理解但是通过话递归展开图能让我清楚的了解递归的过程,下面以一个例子来说吧:
1、递归求1~n的和;

#include <stdio.h>

int sumR(int n)
{
	if(n == 1)
	{
		return 1;
	}
	else
	{
		return sumR(n - 1) + n;
	}
}

int main(void)
{
	int n = 0, sum = 0;
	scanf("%d", &n);

	sum = sumR(n);

	printf("sum = %d\n", sum);

	return 0;
}

以求1~5的和为例,5传进sumR函数5不等于1会执行else里的语句sumR(5 - 1) + 5,此时调用了sumR函数但参数为4再一次进行判断4不等于1执行else里的语句sumR(4 - 1) + 4,这里再一次调用了sumR函数但参数为3再一次进行判断3不等于1执行else里的语句sumR(3 - 1) + 3,这里再一次调用了sumR函数但参数为2再一次进行判断2不等于1执行else里的语句sumR(2 - 1) + 2,这里再一次调用了sumR函数参数为1,1不等于1执行往上一级返回1,这里的返回是往上一级返回而不是作为最终结果返回,1返回给sumR(1)然后1 + 2作为返回值返回给sumR(2),1 + 2 + 3作为返回值返回给sumR(3),1 + 2 + 3 + 4作为返回值返回给sumR(4),最后返回最终结果1 + 2 + 3 + 4 + 5;

在进行函数传参时数组也可以作为函数参数,数组作为参数总共有两种情况:
1、数组元素作为函数参数 ;
2、数组本身作为函数参数 ;

在一维整型数组本身作为函数的参数时,可能会出现的问题:

int printArray(int a[])
{
	int len = sizeof(a) / sizeof(a[0]);
	int i = 0;
	
	for(i = 0; i < len; ++i)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}

int main(void)
{
	int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	printfArray(a)
	return 0;
}

上述程序的输出结果是1 2,原因是printArray(int a[])的形参int a[]在编译时编译器会把它识别为
int *a这是一个指针,指针在64位系统中的大小是占8个字节,所以在计算len时8/4=2,最终的打印结果是1 2,通过上述情况可以知道在把数组作为实参传给函数是同时也要把数组的长度传给函数用作函数中对数组元素的调用。对于一维数组:
int a[10];
数组名 代表类型 —int[10] 这种数组类型 ;
数组名 代表的值 —首元素的地址(数组所占内存空间的首地址) ;
下面以数组作为参数传给函数的方式实现冒泡排序来看程序执行的效果吧:

在这里插入图片描述
程序输出结果:
在这里插入图片描述
其实从输出结果上看跟在main函数上实现的冒泡排序没有什么区别,只是通过函数的方式去实现且以数组作为参数,但是数组是用作处理大批数据的在我们需要传大量的数据的时候相比与传多个值我只传一个数组就能解决问题这样效率会高得多,不过也要具体问题具体分析,今天先到这了明天还会继续更新的!
一维字符型数组作为参数,因为一维字符型数组是用来存放字符串的而字符串是以**‘\0’**结尾的在操作字符串时通过结束标志就能够判断是否到了字符串结尾,所以在进行函数传参时只需要把数组名传进去就可以了,而不像整型数组需要传数组长度才能够判断数组末尾。下面一个小的例子来说明一维说明一维字符型数组作为参数的效果吧;

#include <stdio.h>

void myGets(char s[])
{
	int i = 0;
	scanf("%c", &s[i]);

	while (s[i] != '\n')//从键盘上输入数据是以\n结尾的
	{
		++i;
		scanf("%c", &s[i]);
	}
	s[i] = '\0';
}

int main(void)
{
	char s[100];

	myGets(s);

	printf("%s", s);
	printf("\n");

	return 0;
}

上诉代码实现了gets()函数的简单功能,以一维字符型数组作为参数就能通过数组名找到该数组所在的空间,scanf()函数就能把扫描到的合法字符放进数组里从而实现了gets()函数的功能。
一维字符型数组作为函数参数我把它写成char s[]的样子其实编译器在识别的时候会把它识别成char (*s)指针类型。

二维字符型数组和二维整型数组作为函数参数,其实二维数组本质上由一维数组来模拟而成的,二维字符型数组的每一行都可以用来存放一个字符串,所以在函数传参时我们要把数组的行数传进去这样在访问时才能够访问到想要访问的字符串;二维整型数组作为函数参数也是一样的在函数传参时行数可以省略而列数不能省略且要把数组的行数传进去,原因是一个二维数组的正确写法是这样的:int a[3][4]其实可以这样理解 int [4] a[3] ,int [4]为数组类型如果把4去掉那么数组的类型就是错误的,所以函数的列数不能省略。下面以一个二维字符型数组的插入排序算法来看一下二维数组作为函数参数时的情况吧。

#include <stdio.h>
#include <string.h>

void insertSort(char s[][20], int row)
{
	int i = 0, j = 0;
	char temp[20];
	for(i = 1; i < row; ++i)
	{
		strcpy(temp, s[i]);
		j = i;
		while(j > 0 && strcmp(temp, s[j - 1]) < 0)
		{
			strcpy(s[j], s[j - 1]);
			--j;
		}
		strcpy(s[j], temp);
	}
}

int main(void)
{
	char s[5][20] = { "hello", "help", "abcd", "abcdf", "ldwdw" };
	int row = sizeof(s) / sizeof(s[0]);
	int i = 0;
	int ret = 0;
	char tar[20];

	insertSort(s, row);

	for(i = 0; i < row; ++i)
	{
		printf("%s\n", s[i]);
	}

	return 0;
}

通过上述程序二维字符型作为函数的参数可以知道在进行二维数组传参时除了把数组传进去还需要把数组的行数传进去,这样才能正确的访问到相应的字符串。插入排序的算法思想主要分为以下几个步骤:
1、从数组中拿数据,因为要不断地拿所以可以通过循环来控制拿取的元素;
2、把拿出来的字符串跟从当前位置前面的字符串开始进行依次比较,判断两个字符串的大小可以通过strcmp函数;
3、如果当前的字符串小就让另一个字符串往后拖动一个位置;
4、当拿出来的字符串大于前面的字符串或者到了数组首元素的位置就停止比较并把拿出来的字符串放进拖出来的空位;

标识符的作用域和可见性

所谓的作用域就是变量作用的范围, 而可见性就是程序运行到某一个位置 哪些变量名可以被使用或者说被看见。
根据作用域的不同可以把变量分为全局变量和局部变量,局部变量就是在局部作用域定义的变量,全局变量你就是在全局作用域 定义的变量。局部作用域就是在{}括起来的范围,而全局变量就是在整个文件范围内。
局部变量的特点:
局部变量的空间 一般都在栈上,如果不初始化,局部变量中的值是随机值(垃圾值);
局部变量的生命周期从程序运行到定义处开始存在到程序运行到它作用范围结束时销毁;
全局变量的生命周期,从程序开始就存在直到程序运行结束时它的作用范围才会被销毁;
注意全局变量是不能用变量来初始化;

例如:

#include <stdio.h>

int a = 100;
int b = a;

int main(void)
{
	int a1 = 10;

	{
		int a = 20;

		printf("a = %d\n", a);
	}

	printf("a = %d\n", a);

	return 0;
}

在上述代码中定义了两个全局变量a和b同时用a初始化b,也就是用变量去初始化全局变量编译就能看到以下结果;
在这里插入图片描述
对于全局变量a它的生命周期就是整个程序文件,对于局部变量它的生命周期是在main函数的范围内;

标识符的可见性的规则:
1、必须先定义后使用
2、同一作用域中,不能有同名标识符
3、在不同的作用域,同名标识符,相互之间没有影响
4、如果是不同的作用域,但是作用域之间存在嵌套关系,则内层的作用域的同名标识符,会屏蔽外层的作用域的同名标识符。(就近原则)
下面通过例子来说明:

对于一,标识符必须先定义后使用;

#include <stdio.h>

int main(void)
{
	{
		printf("a = %d\n", a);
	}

	printf("a = %d\n", a);

	return 0;
}

上述代码没有定义变量a就直接打印a,编译会报一下错误:
在这里插入图片描述
编译器提示说a没有定义,所以我们在使用标识符前必须先进行定义然后在使用;

对于二,同一作用域中,不能有同名标识符;

#include <stdio.h>

int main(void)
{
		int a = 20;
		int a = 30;
		
		printf("a = %d\n", a);
		
	return 0;
}

在这里插入图片描述
在上述程序中我在main函数定义了两个a变量,编译此时编译会报重复定义a变量的错误。

对于三,在不同的作用域,同名标识符,相互之间没有影响 ;

#include <stdio.h>

int main(void)
{
	int a = 10;

	{
		int a = 30;

		printf("a = %d\n", a);
	}

	printf("a = %d\n", a);

	return 0;
}

我在main函数中的不同作用域定义了两个同名的变量,编译没有报错然后运行,可以看到程序可以输出。
在这里插入图片描述

对于四,如果是不同的作用域,但是作用域之间存在嵌套关系,则内层的作用域的同名标识符,会屏蔽外层的作用域的同名标识符。(就近原则)

#include <stdio.h>

int a = 100;

int main(void)
{
		int a = 30;

		printf("a = %d\n", a);

	return 0;
}

上述程序我定义一个全局变量a在main函数中的定义了一个全局变量a编译运行可以看到程序输出a = 30;原因是两个变量分别位于全局和局部但是作用域之间存在嵌套关系,则内层的作用域的同名标识符,会屏蔽外层的作用域的同名标识符。
在这里插入图片描述

存储类别关键字

auto

auto表示它是一个自动变量 (局部变量),局部变量是存放在栈区的所以对于局部变量的空间是自动申请且在出了作用域以后就会被自动自动释放 。其实每个局部变量的数据类型前面都会有一个auto只不过可以省略不写比如说

int main(void)
{
	auto int a = 0;

	return 0;
}

register

用register关键字修饰变量表示把该变量放在寄存器中而不是内存中,不过这条指令对于编译器来说是建议性的执不执行关键还是看编译器,register 修饰的变量 不能做 & (取地址)的操作。

extern

extern修饰的全局变量或者函数是告诉编译器这个变量或函数是定义了的但是不在本文件中得去别的文件找。

static

static关键字可以用来修饰全局变量和静态变量,如果修饰的是局部变量表示将局部变量放在静态区,延长局部变量的生命周期把局部变量的生命周期从局部提升到整个程序结束才会被销毁,修饰全局变量时就是把全局变量的作用域限制在本文件不能在通过extern来声明调用了,修饰函数时限定作用域为本文件,别的文件不能通过extern来声明使用 。使用static关键字时需要注意的是:
1、static修饰的变量不能用变量初始化 ;
2、变量只会被初始化一次 ;
3、反复使用该变量时,static修饰的变量的值具有继承性;

定义语法:
[存储类别] 类型 变量名;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值