C语言学习-函数

Posted 庸人冲

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言学习-函数相关的知识,希望对你有一定的参考价值。


在这里插入图片描述

函数介绍

函数是什么?

函数是一个大型程序中的某部分代码,由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性

一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。

为什么使用函数?

在编写代码的过程中,经常会重复需求某些相同的功能,如果每一次功能的实现都需要编写一段相关的代码,那么对于程序员来说工作量是十分巨大的,同时也增加了整个工程的复杂程序、降低了代码的可读性。而如果使用函数来实现功能,可以编写合适的代码封装为函数,哪里需要就哪里调用,省去重复编写相同代码的工作,降低了工程的复杂程度,提高了代码的可读性,同时也将整个工程模块化,利于后期的维护和修改。

int Add(int x, int y) // 形参 
{
	return x + y; // 返回值 int类型
}
int main()

{
	int a = 10;
	int b = 20;
	int sum = Add(a,b); // 实参 
	printf("%d\\n", sum); 
	return 0;
}

C语言函数的分类

C语言中函数分为 库函数自定义函数

库函数

C语言提供给程序员使用的一系列具备基础通用功能的函数,C语言本身并没有库函数,但是随着C语言的发展,程序员在编写程序时会经常使用到一些通用的功能,这些功能每个程序员都可能有需求(例如 输出、输入、字符串操作、公式计算等等…),但是每个程序员对功能的实现不同,因此C语言将这些具备通用的功能整合为库函数提供给开发人员使用,提高了系统可移植性和程序开发的效率。

C语言常用的库函数

  • IO函数(输入输出函数)
  • 字符串操作函数
  • 字符操作函数
  • 内存操作函数
  • 时间/日期函数
  • 数学函数
  • 其他库函数

头文件

C语言的库函数在使用时需要先引用相应的头文件。

// 预处理指令 <头文件名>
#include <stdio.h>

在这里插入图片描述

利用文档学习

对于库函数的学习可以在线查找文档,例如:

cplusplus.com - The C++ Resources Network

https://zh.cppreference.com/w/c

举例

  1. strcpy
char * strcpy ( char * destination, const char * source );

将源地址指向的字符串复制到目的地指向的数组中,复制包括字符串结束标志\\0,并在此位置结束。

strcpy 函数返回类型是 char * ,指针,返回值是目的地地址。

参数

  • 源地址
  • 目的地地址
#include <stdio.h>
#include <string.h> // strcpy()
int main()
{
	char str1[] = "abc";
	char str2[] ="#######";
	// strcpy(目标地址,源地址) 源的长度要小于目标的长度,不然会报BUG
	strcpy(str2, str1); // 数组本质上存放的是地址
	printf("%s\\n", str2);
	return 0;
}

通过调试可以看到,strcpy 会将 str1 的所有字符全部拷贝至str2,并在\\0结束,\\0之后的内容不会被打印。

在这里插入图片描述

在这里插入图片描述

  1. memset
void * memset ( void * ptr, int value, size_t num );

从内存块空间开始位置往后设置n个字节的字符,返回值是ptr。

参数

  • ptr

    被设置的内存块的地址。

  • value

    被填充进内存块里的值,以整型数值传递,但被解释为无符号字符型。

  • num

    被设置的字节个数,size_t == unsigned int。

int main()
{
	char arr[] = "hello world";
    //memset(ptr,value,num)
    // The value is passed as an `int` 
	memset(arr, 5, 5); 
	printf("%s", arr);  
	return 0; 
}

在这里插入图片描述

自定义函数

程序员根据不同的任务需求,自己编写的函数。自定义函数和库函数一样,有函数名、返回值类型和函数参数。

函数的组成

函数通常由函数名、参数、函数体、返回值类型组成

// 定义函数
// 返回值类型  函数名  (参数)
ret_type fun_name (para1, *) // 形参
{
   statement; // 函数体, 交代函数的实现
}
fun_name(para1, *); //函数调用 ,实参

举例

  1. 函数实现 找出2个数的较大值
// 定义函数
int Max(int x, int y)
{
	return x > y ? x : y;
}
int main()
{
    
	int a = 10;
	int b = 20;
	int max = Max(a, b); // 函数调用
	printf("%d\\n", max);
    
	return 0;
}
  1. 写一个函数可以交换两个变量的内容
// 交换内容不需要返回值。
void Swap(int* pa, int* pb)// 利用指针变量接收地址
{                          // 解引用操作
    int tmp = *pa;         // *pa代表的就是main函数里的a
    *pa = *pb;             // *pb 同理
    *pb = tmp;             // 通过解引用操作在函数内部曹操纵外部变量。
}
int main()
{
    int a = 10;
    int b = 20;
    Swap(&a, &b);          // 将a、b的地址传递给函数
    printf("a = %d\\nb = %d\\n", a, b);
    return 0;
}

在这里插入图片描述

函数的使用

函数的使用一般分成3个步骤,函数声明函数调用函数定义

下面的代码是一个完整的函数使用。

int Add(int x,int y);  // 函数声明  x和y可以省略

int main()
{
    int a = 10;
    int b = 20;
    int sum = 0;
    sum = Add(a,b);    // 函数调用
    return 0;
}


int Add(int x, int y)  // 函数定义
{
    return x + y;
}

函数声明

上面例子中的第一行的代码就是一个函数的声明。

int Add(int x,int y);
//返回值类型  函数名(参数1,参数2...);

告诉编译器有个Add函数,他有两个整型参数,函数的返回值是整型。

  1. 函数声明是告知编译器有一个函数,它们函数名是什么,它的参数是什么,以及它的返回类型是什么,至于函数存不存在,并不重要。
  2. 在函数声明中并没有实际创建变量。
  3. 函数声明一般出现在函数调用之前。要满足先声明后使用。
  4. 函数声明一般放在头文件中。

函数调用

上面第8行代码就是一个函数调用。

Add(a,b);
//函数名 (实参1,实参2...);
  1. 当程序执行到函数调用语句是,会调用函数的定义并执行函数定义中的内容。
  2. 函数调用本质上是一个表达式语句,圆括号是运算符,括号左边的函数名是运算对象。在C11标准中,这种表达式是一种后缀表达式。
  3. 函数调用由函数名和实参组成(实参后面会介绍)
  4. 函数调用又有传值调用和传址调用两种用法(后面会介绍)。

传值调用

函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参,如例1。

传址调用

传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式,如例2。

这种传参方式可以让函数和函数外边的变量建立起正真的联系,也就是函数内部可以直接操作函数外部的变量

两种调用的使用时机

传值调用:当调用函数不考虑改变外部变量时。

int get_max(int x, int y)
{
	int z = x > y ? x : y;
	return z;
}
int main()
{// 这里只是求两数的最大值,并将结果返回给接收变量max,并没有改变原来外部变量a和b的值
	int a = 10;
	int b = 20;
	int max = get_max(a,b); 
	printf("max=%d\\n", max); // max=20
	max = get_max(100, 200);
	printf("max=%d\\n", max); // max=200
	return 0;
}

**传址调用:**当调用函数考虑通过操纵函数内部变量来改变外部变量时。

// 写一个函数可以交换两个整形变量的内容
void Swap(int* x, int* y) 
{
	int tmp = 0;
	tmp = *x;
	*x = *y;
	*y = tmp;
}
int main()
{// 这里需要改变交换a和b的值,而如果是传值操作,x,y与a,b不在同一内存空间,只能是实参a,b将值赋给形参下x,y 不能逆向操作,因此使用传址操作将a,b的地址传给指针变量x,y,同过解引用操作来远程控制a,b值得互换
	int a = 10;
	int b = 20;
	Swap(&a, &b); 
	printf("a=%d,b=%d", a, b);
	return 0;
}

函数定义

上面13~16行就是一个函数定义

int Add(int x, int y)  // 函数定义
{
    return x + y;
}
  1. 当函数被调用,会执行函数定义里的语句。
  2. 函数定义通常是由,返回值类型,函数名,形参,函数体组成。
  3. 函数定义里的形参与函数体里的变量都是局部变量,不会与函数外的参数冲突。
    在这里插入图片描述

注意:函数定义的每个形参都必须指定类型。

int add(int x,y)      // 错误
int add(int x, int y) // 正确

函数声明的使用场景

我们经常在代码中省略函数声明,它似乎显得有些多此一举,其实函数声明的正真使用场景是当有一些功能需要被模块化时,此时的函数定义应该单独写在一个.c源文件中,而要使用这个函数就需要创建一个.h的头文件并在其中声明这个函数。

例如,将一个实现加法的函数,单独写在add.c文件中。
在这里插入图片描述

并创建一个对应的add.h头文件,在其中声明这个变量。
在这里插入图片描述

其他源文件想使用,需要用#include,引用这个头文件。自定义的头文件名用"" 引发括起来,这点和库函数的引头文件方式有所差异。
在这里插入图片描述

函数的参数

实参

实参是写在函数调用内的参数,实参的值传递给函数定义中的形参,实参可以是:常量变量表达式函数等。无论实参是何种类型的变量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。

形参

形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中猜实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁。因此形式参数只在函数中有效。

ret_type fun_name (形参1, ...) 
{
   statement; 
}
fun_name(实参1..); 

当实参传递值给形参时(实例化),形参其实是实参的一份临时拷贝,对形参的修改是不会改变实参的。

在这里插入图片描述
在这里插入图片描述

练习

函数实现判断素数

#include <stdio.h>
#include <math.h>
int is_Prime(int x)
{  
      int j = 0;
      if (x < 2)
      {
          return 0;
      }
      else {
          for (j = 2; j <= sqrt(x); j++)
          {
              if (x % j == 0)
              {
                  return 0;
              }
          }
      }
          return 1;         
}
int main()
{
    int count = 0;
    for (int i = 101; i <= 199; i += 2)
    {
        if (is_Prime(i))
        {
            printf("%d ", i);
            count++;
        }
    }
    printf("\\n%d\\n", count);
    return 0;
}
// 计算范围内素数的个数
#include <stdio.h>
#include <math.h>

int count_prime(int x, int y)
{
	int i = 0;
	int j = 0;
	int count = 0;
	if (2 == x)                               // 判断最小值是否为2,如果是count++(2为素数);
	{
		count++;
	}
	x = x % 2 == 0 ? x + 1 : x;               // 判断x是不是偶数,是偶数+1,跳过所有偶数 
	y = y & 2 == 0 ? y - 1 : y;               // 判断y是不是偶数,是偶数-1,跳过所有偶数

	for (i = x; i <= y; i += 2)               // 在指定范围内,循环遍历所有奇数
	{
		for (j = 3; j <= sqrt(i); j += 2)    // 试除 3~sqrt(i) 之间的所有奇数  
		{
			if (i % j == 0)                  // 判断为真,则是合数,跳出内存循环,判断下一个数
			{
				break;
			}
		}
		if (j > sqrt(i))                     // 只有当j > sqrt(i) 时,说明没有能整除i的数,则是素数。
		{
			count++;
		}
	}

	return count;                           // 返回范围内的素数的数量
    
}
int main()
{
	int min = 0;
	int max = 0;
	printf("请输入最小值和最大值中间用\\",\\"分隔>:");
	scanf("%d,%d", &min, &max);
	int count = count_prime(min, max);
	printf("\\n%d\\n", count);
	return 0;
}

函数实现 判断闰年

// 判断是否为闰年
int is_leap_year(int y)
{
    if (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
    {
        return 1;
    }
    return 0;
}
int main()
{
    int year = 0;
    scanf("%d", &year);
    if (is_leap_year(year))
    {
        printf("%d是闰年", year);
    }
    else
    {
        printf("%d是平年", year);
    }
    return 0;
}
// 统计范围内的闰年
int is_leap_year(int y)
{
    if (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
    {
        return 1;
    }
    return 0;
}

int main()
{
    int i = 0;
    int count = 0;
    for (i = 1000; i <= 2000; i += 2)
    {
        if (is_leap_year(i))
        {
            printf("%d ", i);
            count++;
        }
    }

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

函数实现 整型有序数组二分查找(重要)

// 写函数 先考虑函数如何使用,在考虑函数如何实现
// 函数里的形参arr本质上是一个指针,存放的是arr[0]的地址
// []相当于解引用操作
int binary_search(int arr[],int k,int sz)
{
	// 2.数组传参不能使用下面的方法计算元素个数
	// sizeof计算指针大小为4byte(x86)/8byte(x64),而arr[0]是int类型也是4byte
	// 因此sz得到的结果为 1, 这明显是错误的
	// 可以将计算公式放在函数外完成
	//int sz = sizeof(arr) / sizeof(arr[0]);
	int left = 0;
	int right = sz -1;
    
	while (left <= right)
	{
		int mid = (left + right) / 2; 
		if (k > arr[mid])
			left = mid + 1; 
		else if (k < arr[mid])
			right = mid - 1; 
		else
			return mid;
	}
		return -1; // 当找不时返回-1,返回负数是因为下标不会小于0,所有不会和下标冲突。
}
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int k = 7;
	// 3. 在函数外实现计算数组元素个数
	int sz = sizeof(arr) / sizeof(arr[0]);
	printf("请输入1-10的值\\n");
	scanf("%d", &k);
	
	// 1. 实参arr实际上传给形参的是数组"首元素"的地址
	int idx = binary_search(arr,k,sz);
	if (idx == -1)
		printf("该数字不存在于数组中");
	else
      	printf("数字%d的下标是:%d\\n", k, idx);
	return 0;
}

写一个函数,每调用一次这个函数,num+1

void slef_inc(int* p_num)
{
    // *p_num++; // * 和 后置++ 都是操作符 ,后置++优先级高于*,但是后置++是先使用后自增,因此得到的结果为0;
    // 正确写法
    (*p_num)++; //  这两种写法都是可以,()的运算级高于++
    // ++*p_num;  // 前置自增是先自增再使用
}
int main()
{
    int num = 0;
    slef_inc(&num);
    printf("%d\\n", num); // 0
    slef_inc(&num);
    printf("%d\\n", num); // 0
    slef_inc

以上是关于C语言学习-函数的主要内容,如果未能解决你的问题,请参考以下文章

如何优化C ++代码的以下片段 - 卷中的零交叉

inline内联函数

VBS 环境下如何调用EXCEL内置函数

我的C语言学习进阶之旅解决 Visual Studio 2019 报错:错误 C4996 ‘fscanf‘: This function or variable may be unsafe.(代码片段

我的C语言学习进阶之旅解决 Visual Studio 2019 报错:错误 C4996 ‘fscanf‘: This function or variable may be unsafe.(代码片段

Qt编程遇到的问题,我在qt中直接使用C语言的程序片段,有问题 ,求解