「地表最强」C语言(六)函数

环境:CLion2021.3;64位macOS Big Sur


地表最强C语言系列传送门:
「地表最强」C语言(一)基本数据类型
「地表最强」C语言(二)变量和常量
「地表最强」C语言(三)字符串+转义字符+注释
「地表最强」C语言(四)分支语句
「地表最强」C语言(五)循环语句
「地表最强」C语言(六)函数
「地表最强」C语言(七)数组
「地表最强」C语言(八)操作符
「地表最强」C语言(九)关键字
「地表最强」C语言(十)#define定义常量和宏
「地表最强」C语言(十一)指针
「地表最强」C语言(十二)结构体、枚举和联合体
「地表最强」C语言(十三)动态内存管理,含柔性数组
「地表最强」C语言(十四)文件
「地表最强」C语言(十五)程序的环境和预处理
「地表最强」C语言(十六)一些自定义函数
「地表最强」C语言(十七)阅读程序

六、函数

6.1 几个概念

函数的返回值:希望得到的数据的类型。在自定义函数中,若不写返回值,默认为int。
形式参数:函数定义的时候括号内的参数为形式参数。形参只有在函数被调用时才会为其分配空间,函数调用结束时自动销毁,即形参的生命周期在函数内部,这一点与局部变量相同。
实际参数:程序运行时实际传递给函数的参数为实际参数。
函数内复合语句中定义的变量只在复合语句内有效(复合语句指一对大括号)

6.2 库函数

C语言提供的函数,可以查询C语言API了解,这里不赘述。
提供两个C语言的API地址:
1.https://cplusplus.com/reference/cstdio/
2.https://en.cppreference.com/w/

6.3 自定义函数

注意:
1.函数不能嵌套定义,但是可以嵌套调用;
2.传值调用和传址调用的区别:
传值调用:将实参的值传递给形参,二者没有直接的联系,函数无法改变实参的值。
传址调用:将实参的地址传递给形参,二者有直接的联系,函数可以改变实参的值。
举个例子,定义一个函数交换a,b的值:

void swap1(int x,int y)//仅仅传递参数的值,为传值调用
{
	int tmp = 0;
	tmp = x;
	x = y;
	y = tmp;
}
void swap2(int *pa, int *pb)//传递了参数的地址,为传址调用
{
	int tmp = 0;
	tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}

主函数:

	int a = 10;
	int b = 20;
	printf("before:a=%d,b=%d\n",a,b);
	swap1(a, b);//传值调用,形参与实参没有联系,形参改变不会影响实参
	printf("传值调用后:a=%d,b=%d\n",a,b);
	swap2(&a,&b);//传址调用,形参与实参有联系,形参改变可以影响实参
	printf("传址调用后:a=%d,b=%d\n",a,b);

运行结果:
在这里插入图片描述

分析:swap1()实际上是为形参x和y新开辟了两块空间,然后将a,b的值分别赋给x和y,在函数体内操作的是x和y,函数外的实参a和b并没有改变,所以打印结果和调用前一样。
如下图,若蓝色表示函数swap()的内部,可以看到,a、b只是把值传给了x和y而已,但在其内部操作的一直是x,y、tmp这三个变量。
在这里插入图片描述
而swap2()将a和b的地址作为参数,其内部虽然也为pa和pb开辟了空间,但是他们存储的是a和b的地址而非简单的存储a和b的值,这样就建立了形参和实参之间的联系,实现了在函数内改变实参的值。在这里插入图片描述

6.4 函数的嵌套调用和链式访问

1.简单说明一下函数的嵌套调用:其实我们一直在使用函数的嵌套调用,你写程序总得写int main()这个函数吧,在这个函数体内你会写printf()吧,这其实就是函数的调用,和套娃一样,比较简单,就不举例了。
2.链式访问:其实就是一个函数的返回值作为另一个函数的参数。

//printf的返回值是打印了多少个字符
	printf("%d", printf("%d", printf("%s","sd1 2")));
//		1			2			3

运行结果:sd1 251
分析:3处的printf()打印了"sd1 2"共5个字符(1和2中间有个空格),因此返回值为5;
2处的printf()接受了3处的返回值5,打印在了屏幕上,此时打印了一个字符,因此返回1;
1处的printf()接受了2处的返回值1,将1打印在了屏幕上。

6.5 函数的声明

函数的声明其实就是告诉编译器有这个函数,包括函数名,返回值类型,参数个数和类型。
编译器编译代码的时候是从上向下扫描的,如果你想要调用的函数定义在了主函数后边,或者你压根还没写这个函数,只是知道一定得用这个函数,那么当他扫描到主函数内的调用这个函数的地方,会找不到这个函数,因此在主函数中函数调用之前就要声明有这个函数,即先声明后使用。
实际上函数声明一般放在.h头文件中,需要的时候用include引用即可;而函数的定义实现放在.c文件中

	int a = 10;
	int b = 20;
	int Add(int, int);//函数声明
	printf("%d", Add(a, b));

6.6 递归

递归的两个必要条件:
1.存在限制条件,不满足限制条件时,递归便不再继续。
2.每次递归调用后越来越接近这个限制条件。
写递归的时候注意两点:
1.不能死递归,要有跳出条件,每次递归接近跳出条件。
2.递归层次不能太深。(函数调用时会在栈其为开辟一片空间来存储函数的临时变量,叫栈帧空间,若层次太深,栈空间可能不够用而导致栈溢出stack overflow)

6.6.1 一些简单的练习

1.不允许创建变量模拟实现strlen()

int myStrlen(char *a)
{
	if ('\0' != *a)
		return 1 + myStrlen(a + 1);
	else
		return 0;
}

2.求n的阶乘

int jieCheng(int n)
{
	if(1 < n)
		return n * jieCheng(n - 1);
	else
 		return 1;
}

3.求第n个斐波那契数

//递归实现,效率低
int fib(int n)
{
	if(2 > n)
		return 1;
	else
		return fib(n-1) + fib(n-2); //效率很低,因为有大量重复计算的值
}
//非递归
int fib(int n)
{
	int a = 1;
	int b = 1;
	int sum= 1;
	while(2 < n)
	{
		sum = a + b;
		a = b;
		b = sum;
		--n;
	}
	return sum;
}

4.倒序字符串

void reverseStr(char *a)
{
	int len = strlen(a);
	char tmp = *a;
	*a = *(a + len - 1);
	*(a + len - 1) = '\0';
	if(strlen(a + 1) >= 2)
	{
		reverseStr(a + 1);
	}
	*(a + len - 1) = tmp;
}

5.汉诺塔,专门写了一篇汉诺塔的详细说明:最强汉诺塔解析!

6.7 一些库函数

6.7.1 字符串函数

函数如果他的参数符合下列条件就返回真
iscntrl任何控制字符
isspace 空白字符:空格‘ ’,换页‘\f’,换行'\n',回车‘\r’,制表符'\t'或者垂直制表符'\v'
isdigit十进制数字 0~9
isxdigit十六进制数字,包括所有十进制数字,小写字母a~f,大写字母A~F
islower小写字母a~z
isupper大写字母A~Z
isalpha字母a~z或A~Z
isalnum字母或者数字,a~z,A~Z,0~9
ispunct标点符号,任何不属于数字或者字母的图形字符(可打印)
isgraph任何图形字符
isprint任何可打印字符,包括图形字符和空白字符
1.字符串长度 size_t strlen ( const char * str ) (1)算的是'\0'之前的长度 (2)传递的参数需要有'\0',否则结果未知 ==(3)返回值是size_t,即无符号数== 关于(3):
	if (strlen("abc") - strlen("abcdef") > 0)
		printf(">");//打印此行:3 - 6 = -3,但是-3被当作无符号数处理,此时-3的补码被当作原码处理,是一个很大的数字(4294967293)。
		//无符号数 +- 无符号数 = 无符号数	>= 0
	else
		printf("<=");

2.长度不受限制的字符串函数strcpy、strcat、strcmp
2.1 拷贝字符串 char * strcpy ( char * destination, const char * source )
(1)源字符串需要包含\0,会将’\0’一同拷贝过去
(2)目标字符串的空间要足够大
(3)目标空间必须可变

	char arr[20] = "##########";
	//arr = "hello";//err		arr是一个地址常量,也就是一个编号,因此不能放入arr中,而应该存放到一个变量空间中。
	char* p = "hello";//字符串传递的是首地址
	char arr2[] = { 'a','b','c' };
	strcpy(arr, arr2);//err,数组arr2中没有'\0',找不到结束标志,会一直拷贝直到遇到\0
	strcpy(arr, "hello");//遇到'\0',会将'\0'一同拷贝过去,然后中止拷贝
	char arr[5] = "####";
	char* p = "hello world";
	strcpy(arr, p);//会全部拷贝过去,但是程序会崩溃,因为越界了
	printf("%s", arr);//hello world		但是程序崩溃
	char* str = "xxxxxxx";
	char* p = "hello world";
	strcpy(str, p);//目标空间是常量字符串,不可变,程序崩溃
	printf("%s", str);//无法打印
	//注意str和数组名的区别,数组名虽然也是地址,但是它所指向的空间是可变的;而str指向的值常量字符串,不可变。

在这里插入图片描述

一个小测试,证明字符串代表的就是首元素地址:

printf("%c","123456"[3]);//4	说明"" 表示的其实就是首元素的地址

2.2 追加字符串 char * strcat ( char * destination, const char * source )
(1)目标空间要足够
(2)需要\0来判断结束,且自己的\0会被带过去
(3)会覆盖目标空间的\0
(4)目标空间要可修改

	char arr1[20] = "hello \0##########";
	char arr2[] = "world";
	strcat(arr1, arr2);//字符串追加(连接)
	printf("%s", arr1);//hello world

模拟实现strcat:
见:「地表最强」C语言(十六)一些自定义函数和宏16.1.3
注意,此函数不能自己追加自己,因为本身的\0会被不断覆盖,陷入死循环

2.3 字符串比较 int strcmp ( const char * str1, const char * str2 )
(1)比较对应位置的ASCLL码值
(2)遇到\0停止比较
(3)字符串不能用< > =比较,因为字符串代表首元素地址,而地址是随机分配的,比较没有意义

	char* p = "obc";
	char* q = "abcdef";
	printf("%d\n", strcmp(p, q));//	1
	printf("%d\n", strcmp("abc", "abcdef"));//	-1

模拟实现strcmp :
见:「地表最强」C语言(十六)一些自定义函数和宏16.1.4

3.长度受限制的字符串函数介绍:strncpy、strncat、strncmp
3.1 char * strncpy ( char * destination, const char * source, size_t num )

	char arr1[20] = "abcdefghi";
	char arr2[] = "qwer";
	strncpy(arr1, arr2, 6);//不够的用\0补
	printf("%s", arr1);//qwer

3.2 char * strncat ( char * destination, const char * source, size_t num )

	char arr1[20] = "hello ";
	char arr2[] = "world";
	strncat(arr1, arr2, 2);//遇到\0就结束追加,结束时也会加上\0
	printf("%s", arr1);//hello wo

3.3 int strncmp ( const char * str1, const char * str2, size_t num )

	char arr1[20] = "abcde";
	char arr2[] = "abcdefgh";
	printf("%d", strncmp(arr1, arr2, 3));//0	比较给定个数

4.字符串查找strstr、strtok
4.1 char * strstr (char * str1, const char * str2 )
找子串,若有返回第一次出现的地址;找不到返回空指针

	char arr1[] = "abbbcdefghijk";
	char arr2[] = "bbc";
	char* ret = strstr(arr1, arr2);//在arr1中找arr2
	if (ret == NULL)
		printf("没找到\n");
	else
		printf("找到了,%s\n", ret);//找到了,bbcdefghijk

模拟实现strstr:
见:「地表最强」C语言(十六)一些自定义函数和宏16.1.5

4.2 char * strtok ( char * str, const char * delimiters )
切割字符串,会改变被切割字符串的内容
strtok第一个参数不是NULL(一般用于第一次调用):从传入的位置开始找,找到标记后将标记改为\0,并记录这个位置
strtok第一个参数是NULL(一般用于除第一次调用以外):从上一个记录的位置开始找,找到标记后将标记改为\0,并记录这个位置
返回被分割出的token的首地址。
若delimiters中没有分隔符,则返回当前str的指针。

	char* strtok(char* str,const char* sep)
	char arr[] = "fudan@daxue.com";
	char* p = "@.";
	char tmp[20] = { 0 };//防止被切割字符串被改变
	strcpy(tmp, arr);
	char* ret = NULL;
	for (ret = strtok(tmp, p); ret != NULL; ret = strtok(NULL, p))
		printf("%s\n", ret);

5.错误信息报告 char * strerror ( int errnum )
在调用库函数失败的时候,都会设置错误码,此函数将错误码翻译成错误信息保存在了字符串中,然后返回这个字符串的首地址。
c语言有一个全局变量来保存错误码:int errno;

	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)//返回NULL表示打开文件失败
	{
		printf("%s\n", strerror(errno));//失败,错误码保存在errno中,调用函数翻译打印失败的原因
		//perror("fopen");//不需要传递errno,实际上它不仅将错误码转换了,而且还自动将其打印
		return 1;
	}
	fclose(pf);
	pf = NULL;

6.字符分类函数

1.iscntrl()任何控制字符    2.isspace()空白字符     3.isdigit()十进制数字0-9      4.isxdigit()十六进制数字
5.islower()小写字母       6.isupper()大写字母     7.isalpha()英文字母           8.isalnum()字母或数组
9.ispunct()标点符号,任何不属于数字或字母的图形字符(可打印)                      10.isgraph()任何图形字符
11.isprint()任何可打印字符,包括图形字符和空白字符
   char ch = 'i';
   printf("%d\n",isdigit(ch));//非数字字符返回0;否则返回非0
   printf("%d\n",isalpha(ch));//非英文字母字符返回0;否则返回非0

7.字符转换函数
(1)tolower()转换为小写字母,返回ASCLL码值
(2)toupper()转换为大写字母,返回ASCLL码值

	char arr[10] = {0};
    scanf("%s", arr);
    int i = 0;
    while ('\0' != arr[i])
    {
        if (isupper(arr[i]));
        {
            arr[i] = tolower(arr[i]);
        }
        printf("%c ", arr[i]);
        i++;
    }

6.7.2 内存函数

  1. void* memcpy( void* dest, const void* src, size_t count )
    其中count为需要拷贝的字节数。
    对比strcpy():srecpy()只能拷贝字符,而内存拷贝memcpy()可以拷贝任意类型
    int arr1[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    //01 00 00 00 02 00 00 00
    int arr2[10] = {0};
    memcpy(arr2,arr1,20);

在这里插入图片描述

模拟实现memcpy(),见「地表最强」C语言(十六)一些自定义函数和宏16.1.1

  1. void* memmove( void* dest, const void* src, std::size_t count )
//将12345拷贝到34567
    int arr1[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
	memmove(arr1 + 2, arr1, 20);

在这里插入图片描述

	//若使用memcpy
    int arr1[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
	memcpy(arr1 + 2, arr1, 20);

在这里插入图片描述
虽然结果一样,但是两者的应用场景应该区分开来:memcpy()函数是为了不重叠的拷贝而设计的,虽然有的编译器上这个函数也支持重叠的拷贝,但并不是所有的都支持;而memmove()就是为了重叠拷贝而设计的。

模拟实现memmove(),见「地表最强」C语言(十六)一些自定义函数和宏16.1.7

  1. int memcmp( const void* lhs, const void* rhs, std::size_t count )
    float arr1[] = {1.0,2.0,3.0,4.0};
    float arr2[] = {1.0,3.0,3.0};
    printf("%d\n",memcmp(arr1,arr2,8));//相等返回0,小于返回负数,大于返回正数
  1. void* memset( void* dest, int ch, size_t count )
    以字节为单位设置内存
    int arr[10] = {0};
    memset(arr,1,20);//将arr的前20个字节设置为1

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值