《C和指针》读书笔记(第七章 函数)

在前面的例子中,我们已经用过了最常见的函数:主函数,其实在C语言还可以灵活地根据需要定义自己的函数,而在C++中,函数类型会更加地丰富。在面向对象的语言中,经常会有方法这个概念,看着比较像,其实作用也是基本一样的,都是对一段代码的抽象。只不过函数是直接传值的,而方法是直接处理对象上的数据,其依赖类或者对象,不能独立存在。

按照惯例,先来看看本章的知识框架(思维导图)。

思维导图

在这里插入图片描述

7.1函数定义

在书中并没有对函数比较通俗的定义,其实要说通俗的定义应该就是对某一段代码的抽象,也就是说我们若是想要实现某个功能,就将这些代码写到一起,然后通过语法和其他符号,就构建了一个函数,然后以后若是想反复使用这段代码,直接调用即可。函数定义的语法如下。

类型
函数名(形式参数)
代码块

另外有一个概念需要清楚:如果函数无需向调用程序返回一个值,它就会被省略。这类函数在绝大多数其他语言中被称为过程。这个概念在我学习《操作系统》过程中有提及。

7.2函数声明

一般函数都会有函数声明和函数实现。函数原型和函数声明几乎没有什么区别。因为C语言一般都是从main函数开始执行的,而在调用某函数的时候,我们告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。否则程序就无法顺利运行,因而函数声明应运而生。比方说下面的程序。

#include <stdio.h> 

size_t strlength(char *string);
int main()
{
    
    
	int res = 0;
	char a[] = "qwer";
	res = strlength(a);
	if (res)
		printf("The string length is %d\n",res);
	else
		printf("The string length is zero!\n");
	system("pause");
	return 0;
}

size_t strlength(char *string)
{
    
    
	int length = 0;
	while (*string++ != '\0')
		length++;
	return length;
}

可以看到在程序的一开始就对strlength函数进行了声明。

7.3函数的参数

函数的参数按照通俗的分类,会分为形参和实参,顾名思义,形参就是“形式”上的参数,实参就是实际调用的时候传入的参数。在C语言中,一般传入的都是实际值的一份拷贝,注意是拷贝

而参数若是数组,则一般会传入数组第一个元素的地址,这个行为被称为“传址调用” 。举个例子。

#include <stdio.h> 

void strexchange(char a[]);
int main()
{
    
    
	int res = 0;
	char a[] = "wwww";
	strexchange(a);
	for(int i = 0; i < 4; i++)
		printf("The string elements is %c\n",a[i]);
	system("pause");
	return 0;
}

void strexchange(char a[])
{
    
    
	a[0] = 'q';
}

打印输出
在这里插入图片描述
可以看到,在strexchange函数中,就可以完成对字符数组第一个元素的修改。所以肯定是“传址调用”。

7.4ADT和黑盒

书中有这样的描述:

C可以用于设计和实现抽象数据类型(ADT,abctract data type),因为它可以显示函数和数据定义的作用域,这个技巧也可以被称为黑盒(black box)设计。

其实也比较好理解,也就是说,有时候我们在开发中,并不需要知道函数的具体实现过程,只是单纯想调用它来实现相应的功能。

通过static关键字可以限制在其他文件中直接对其进行访问。所以在其他文件中,只需要关注其功能即可,而不需要关注其体具体的实现过程。

书上有了一个例子,后经过了些许补充:

创建addrlist.h文件,编写如下程序:

#pragma once
#define NAME_LENGTH 20                       //姓名最大长度
#define ADDR_LENGTH 100                      //地址最大长度
#define PHONE_LENGTH 11                      //电话号码最大长度
#define MAX_ADDRESSES 1000                   //地址个数限制
void data_init();
char const *lookup_address(char const *name);
char const *lookup_phone(char const *name);

创建addrlist.c文件,编写如下程序:

#include "addrlist.h"
#include <stdio.h>
#include <string.h>
static char name[MAX_ADDRESSES][NAME_LENGTH];
static char address[MAX_ADDRESSES][ADDR_LENGTH];
static char phone[MAX_ADDRESSES][PHONE_LENGTH];


static int find_entry(char const *name_to_find)
{
    
    
	int entry;
	for (entry = 0; entry < MAX_ADDRESSES; entry++)
		if (strcmp(name_to_find, name[entry]) == 0)
			return entry;
	return -1;

}
//给定一个名字,找到对应的地址,如果找不到,则返回空指针
char const *lookup_address(char const *name)
{
    
    
	int entry;
	entry = find_entry(name);
	if (entry == -1)
		return NULL;
	else
		return address[entry];
}
char const *lookup_phone(char const *name)
{
    
    
	int entry;
	entry = find_entry(name);
	if (entry == -1)
		return NULL;
	else
		return phone[entry];
}
void data_init()
{
    
    
	char name_1[NAME_LENGTH] = "zhangsan";
	for (int i = 0; i < NAME_LENGTH; i++)
	{
    
    
		name[0][i] = name_1[i];
	}
	char address_1[ADDR_LENGTH] = "shanghai/zhangjiang";
	for (int i = 0; i < ADDR_LENGTH; i++)
	{
    
    
		address[0][i] = address_1[i];
	}
}

main.c中编写如下的代码:

#include "addrlist.h"
#include <stdio.h>
#include<stdlib.h>

int main()
{
    
    
	static char find_addr[MAX_ADDRESSES] = "zhangsan";
	char const *addr_res = NULL;
	//数据初始化
	data_init();

	addr_res = lookup_address(find_addr);

	
	if (addr_res == NULL)
		printf("^-^");
	else
	{
    
    
		for (int i = 0; i < ADDR_LENGTH; i++)
		{
    
    
			if (addr_res[i] != 0)
				printf("%c", addr_res[i]);
			else
				break;
		}
	}
}

运行,打印输出:
在这里插入图片描述
可以看到,当我们输入张三的时候,直接查到了张三的住址:上海张江。而此时在main.c文件中,我们并不知道具体的查询过程。所以这样就起到了封装的效果。

7.5 递归

递归是一种非常重要的编程思想,直观地说,就是函数自己调用自己。

关于递归,书上有这样一段描述:

一旦你理解了递归,阅读递归函数最容易的方法不是纠缠它的执行过程,而是相信递归函数会顺利完成它的任务,如果你的步骤正确无误,你的限制条件设置正确,并且每次调用之后更接近限制条件,递归函数总能正确地完成任务。

最最经典的例子当属斐波那契数列了,程序如下:

int fibonacci(int const n)
{
    
    
	int sum = 0;
	if (n == 0)  return 0;
	if (n == 1 || n == 2)  return 1;
	return fibonacci(n - 1) + fibonacci(n-2);
}

当然,我们也可以写一个非递归版本的,只是稍微复杂一些:

int fibonacci(int const n)
{
    
    
	int f1 = 1, f2 = 1, f3 = 0;
	if (n == 0)  return 0;
	if (n == 1 || n == 2)  return 1;

	for (int i = 3; i <= n; i++)
	{
    
    
		f3 = f1 + f2;
		f1 = f2;
		f2 = f3;
	}
	return f3;
}

开始可能会觉得非递归版本好理解一些,但是习惯了之后,会发现递归版本的更加方便。而且在有的时候,使用递归要比循环容易得多,比方说下面这个例子:

  1. 各位相加

给定一个非负整数 num,反复将各个位上的数字相加,直到结果为一位数。返回这个结果。

如果我们用非递归的方法,可能比较难解决,其中一种解决思路如下:

int addDigits(int num){
    
    
    int add = 0;
    do
    {
    
       
        add = 0;  
        while(num > 0)
        {
    
    
            add += num % 10;
            num /= 10;
        }
        num = add;
    }while(add >= 10);

    return add;
}

也就是当求和的结果大于10的时候继续执行相同的操作,直到小于10,返回计算的结果。但如果我们采用递归,就会变得更加简单:

int addDigits(int num){
    
    
    int add = 0;
    while(num > 0)
    {
    
    
        add += num % 10;
        num /= 10;
    }
    num = add;
    return add < 10 ? add : addDigits(add);
}

所谓的重复的操作,我们就可以直接递归调用,然后就可以输出想要的结果。

注:要把握递归的深度,且确保递归是可终止的,否则可能会出现堆栈溢出的情况。

7.6 可变参数列表

在实际的项目开发中,我们经常会遇到传递的参数个数未知的情况,这个时候就需要用到可变参数列表。书中有这样一个例子:

//可变参数列表头文件
#include<stdarg.h>

float average(int n_values, ...)
{
    
    
	va_list var_arg;
	int count;
	float sum = 0;
	//准备访问可变参数
	va_start(var_arg, n_values);
	//添加取自可变参数的值
	for (count = 0; count < n_values; count++)
	{
    
    
		sum += va_arg(var_arg, int);
	}
	//完成处理可变参数
	va_end(var_arg);
	return sum / n_values;
}

也就是一个求平均值的函数,但我们事先并不知道 究竟有多少个数需要求平均值。调用的时候可以这样:

	printf("%f\n", average(10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10));

就可以直接打印出算得的结果,打印输出:
在这里插入图片描述
注意:可变参数必须从头到尾按照顺序逐个访问。如果在访问了几个可变参数后想半途中止,这是可以的。

从这点上来看,可变参数列表和链表的访问很类似。

总结

C语言也可以用来设计和实现抽象数据类型。

递归在熟练以后用起来很方便,但并非在所有情况下都会那么高效,同时要注意由此可能引发的堆栈溢出问题。

---------------------------------------------------------------------------END---------------------------------------------------------------------------

猜你喜欢

转载自blog.csdn.net/weixin_43719763/article/details/128194759