Bootstrap

C语言函数:编程世界的魔法钥匙(1)

目录

1.C语言中的函数是什么?

2.函数的分类:

2.1 标准库函数

2.1.1 库函数的诞生:

2.1.2 库函数的作用:

2.1.3 如何学习使用库函数

2.2 自定义函数

2.2.1 函数的组成:

2.2.2 自定义函数的优点 

 2.2.3 例题

3.函数的参数

3.1 实际参数(实参):

3.2 形式参数(形参):

4. 函数的调用

4.1 传值调用

4.1.1传值调用的概念:

4.1.2 传值调用适用场景

4.1.3 传值调用的特点

4.2 传址调用

4.2.1 传址调用的概念:

4.2.2 传址调用的适用场景

4.2.3 传址调用的优缺点

4.3 传值调用和传址调用的对比

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

5.1 嵌套调用

5.2 链式访问

链式访问的前提条件:

6. 函数的声明和定义

6.1 函数声明

6.2 函数定义:

结语:


前言:

本文全篇近一万字,由于大多数是小编手敲的,因此可能会有些错误的地方,各位大佬可以在评论区指正,万分感谢!!

恭喜你发现了一篇超级实用的长文。如果你正在寻找具体的模块,可以先查看目录,找到自己需要的内容。在这里,你将会发现我们为你准备的各种有趣、有用的信息。快来一起探索吧!

引言:

你是否曾在 C 语言的编程之路上迷茫徘徊,渴望找到那把能开启高效编程之门的钥匙?答案或许就藏在那些看似平凡,实则蕴含着无限能量的 C 语言函数之中。准备好了吗?让我们一起踏上探索之旅。


1.C语言中的函数是什么?

在 C 语言中,函数是一段具有特定功能的、可重复使用代码块,它接受输入参数(可以没有),进行一系列操作,并可能返回一个结果(也可以没有),用于将复杂的程序分解为较小的、可管理的模块,提高代码的可读性、可维护性和可复用性。

维基百科中对函数的定义:子程序

  • 在计算机中,子程序,是一个大型程序中的某部分代码,由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其它代码,具备相对的独立性
  • 一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏,这些代码通常被集成为软件库。

2.函数的分类:

1.标准库函数:库函数是指预先编写好的程序代码,可以供其他程序使用的函数。它们是在编程过程中所需要的常用功能的封装和集成,提供了一种快速解决问题的方式。

2.自定义函数:自定义函数是一段可重复使用的代码块,用于执行特定的任务。它可以接受输入参数,并返回一个结果。

2.1 标准库函数

2.1.1 库函数的诞生:

在软件开发过程中,我们经常会遇到相似的问题和需求。为了解决这些问题,我们可以重复编写相同的代码,但这样会导致代码冗余,增加了代码的复杂性和维护成本。

为了避免这种情况,一些常见的操作和功能被封装成了库函数库函数是一组已经开发和测试过的函数,可以直接在程序中调用。这样,当我们遇到相同的问题时,只需要调用库函数即可,而不必重新编写代码。

2.1.2 库函数的作用:

1. 提高开发效率:开发人员不必每次都重新编写常见的、基础的功能代码,直接调用库函数可以节省大量的时间和精力,从而更专注于解决特定问题和实现核心业务逻辑。
2. 代码复用:库函数是经过精心编写、测试和优化的代码段,可以在多个项目中重复使用,保证了代码的一致性和可靠性。
3. 标准化和规范化:库函数提供了一套标准的接口和功能实现,使得不同开发者编写的程序在处理相同任务时具有相似的方式,增强了程序的可移植性和可维护性。
4. 专业性和优化:库函数通常由专业的开发团队编写,能够充分利用特定的算法和技术进行优化,以达到更好的性能和效率。
5. 知识共享:库函数的存在促进了编程知识和技术的共享,使得开发者能够利用前人的经验和成果。
6. 简化编程难度:对于一些复杂或底层的操作,如文件操作、网络通信等,库函数将复杂的细节封装起来,降低了编程的难度和门槛。

库函数的出现使得编程变得更加高效、简洁和可靠,减少了重复劳动,提高了代码的可维护性和可读性。

2.1.3 如何学习使用库函数

C语言常用的库函数分类:

• IO函数(输入/输出):头文件<stdio.h>

• 字符串函数:头文件<string.h>

• 内存操作函数:头文件<string.h>

• 时间/日期函数:头文件<time.h>

• 数字函数:头文件<math.h>

• 其他库函数

库函数必须知道的秘密:使用库函数,必须要包含 #include 对应的头文件 

那么我们应该怎么学习库函数呢?

cplusplus.com/reference/

打开链接后可以看到 C  Library,它介绍了C语言所对应的一切头文件

点开其中某个头文件,可以看到相关库函数的功能介绍

通过该链接,你可以很容易的找到你想了解的库函数的使用方法。当然了,该网站都是英文介绍,如果觉得阅读起来比较麻烦,可以使用翻译将其转换为中文。不过呢,小编还是比较推荐英文浏览,虽然开始可能有点困难,但是若想为以后做准备,我们需要学会阅读英文文档。因为在计算机这个领域里,基本上都是英文文档。

接下来小编会用一些实际的编程案例,演练如何调用库函数来解决具体问题。

1.strcpy

函数原型:char * strcpy(char * destination, const char * source);

函数原型包含以下几个部分:

  1. 返回类型:strcpy函数返回一个char指针。

  2. 函数名:函数名为strcpy。

  3. 参数列表:strcpy函数有两个参数,即目标字符串的指针destination和源字符串的指针source。目标字符串应该具有足够的空间来容纳源字符串的内容。

  4. 参数修饰符:在参数列表中,const关键字修饰了源字符串的指针source。这表示在函数的执行过程中,不会修改源字符串的内容。

注意:函数原型中出现的具体变量名(如destination和source)只是为了说明参数的作用,并不是实际使用时的变量名。在实际使用时,应该用具体的变量名来代替。

详细介绍:

strcpy函数用于将一个字符串复制到另一个字符串中。

具体而言,strcpy函数接收两个参数:目标字符串的指针和源字符串的指针。目标字符串应该具有足够的空间来容纳源字符串的内容。

strcpy函数会将源字符串的内容复制到目标字符串中,包括字符串的所有字符和结束符'\0'。复制后,目标字符串将与源字符串完全相同。

下面是一个示例,演示了如何使用strcpy函数:

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

int main() {
    char arr1[20] = {0};
    char arr2[] = "Hello China";

    strcpy(arr1, arr2);

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

    return 0;
}
 

运行结果:

hello China

上述示例中,源字符串为"Hello China",目标字符串arr1被声明为一个大小为20的字符数组。通过strcpy函数,源字符串的内容被复制到目标字符串中,最终目标字符串也变为"Hello China"。

2.memset

函数原型:void *memset(void *s, int c, size_t n);

其中,s为要操作的目标内存块的指针,c为要设置的字符值,n为要设置的字节数。

该函数的返回值为指向s的指针。

memset函数用于将给定的字符值设置到指定的内存块中。具体而言,该函数将连续的字节设置为指定的字符值。

memset函数的代码演示(1)

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

int main() 
{
    char arr[20] =  "hello sister";
    memset(arr,'x',5);
    printf("%s\n", arr);
    return 0;
}

运行结果(1)

xxxxx sister

 代码演示(2)

  char arr[20] =  "hello sister";
    memset(arr+6,'x',3);
    printf("%s\n", arr);

运行结果(2)

hello xxxter

库函数众多,如繁星点点,难以尽诉。故在这里戛然而止,愿各位伙伴能将库函数运用自如,成为函数界的翘楚!


2.2 自定义函数

在编程的世界里,有一个强大的工具能够让我们的代码更加灵活和高效,那就是自定义函数。今天,让我们一同揭开它神秘的面纱。

自定义函数和库函数一样,有函数名,返回值类型和函数参数。

但是和库函数不一样的是这些都是我们自己来设计。因此,这给我们一个很大的发挥空间。

2.2.1 函数的组成:

ret_type fun_name(paral,*)
{
   statement;//语句项
}
  ret_type  返回类型
  fun_name  函数名
  paral     函数参数
  {}内的叫函数体

2.2.2 自定义函数的优点 

1.模块化:自定义函数可以将复杂的问题划分为小的模块,提高代码的可读性和理解性。将代码分成多个函数,每个函数负责一个具体的任务,使得代码更加清晰、组织有序

2.代码重用:自定义函数可以在程序中多次调用,减少代码的重复编写,提高代码的可维护性和开发效率。当需要执行相同或类似的操作时,可以直接调用函数,避免重复编写相同的代码。

3.抽象化:自定义函数可以隐藏具体实现细节,使调用者只关心函数的输入和输出,提高代码的封装性和抽象性。通过函数名和参数列表来表示函数的功能,简化了代码的使用和理解

4.简化调试:自定义函数可以独立测试和调试,有助于快速定位修复问题。通过单独调试函数,可以更容易地检查函数的输入和输出,定位问题所在,减少调试时间

5.可扩展性:自定义函数可以随着需求的变化进行修改和扩展,而不会对其他部分产生影响。通过修改函数的实现,可以适应新的需求和改进功能,而不需要修改调用该函数的其他代码。

总之,自定义函数可以提高代码的可读性、可维护性和可重用性。它们使程序更易于理解、调试和扩展,提高开发效率和代码质量。通过将代码分解为小的模块,自定义函数能够更好地组织和管理代码,使得程序开发更加高效和可靠。

 2.2.3 例题

写一个函数可以找出两个整数中的最大值。

代码演示:

//get_max函数的设计
#include <stdio.h>
//函数的定义
int get_max(int x, int y)//子函数
{
	return(x > y ? x : y);//函数体
}

int main()//主函数
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
    //求最大值,函数的调用
	int m = get_max(a, b);
	printf("%d\n", m);
	return 0;
}

 运行结果:比较20和30

20 30
30

 例题2:写出一个函数可以交换整型变量的内容(令a = 10 ,b = 20)

#include <stdio.h>
void Swap(int x, int y)
{
	int z = 0;
	z = x;
	x = y;
	y = z;
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d",&a,&b);
	//交换
	printf("交换前:a=%d b=%d\n", a, b);
	Swap(a, b);
	printf("交换后:a=%d b=%d\n", a, b);
	return 0;
}

解释:比如说要将醋和酱油交换,我们需要一个空瓶子,然后将酱油倒入空瓶子,醋倒入酱油瓶,酱油再从空瓶子到醋瓶里,这样就实现了醋和酱油的交换。

 运行结果:

交换前:a = 10 b = 20
交换后:a = 10 b = 20

 为什么这个结果是错误的呢?我们来通过调试看一下。

 从图中我们可以看到

开始:&a:0x000000a014dff634{10};&b: 0x000000a014dff654{20}

结束:&x:0x000000a014dff610{20};&y: 0x000000a014dff618{10}

  为什么开始和结束的值不同呢?

这里我们就要引入新的概念了, a和b叫做实际参数x和y叫形式参数   

这道题的问题是实参a和b传给x和y的时候,形参是实参的临时拷贝,所以修改形参不会对实参有影响。

想完成这道题,就需要使用指针,这里先给个参考代码,关于指针会在以后的篇章有详细介绍。  

#include <stdio.h>
void Swap(int*  px, int* py)
{
	int z = *px;//z=a
	*px  =  *py;//a=b
	*py = z;    //b=a
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d",&a,&b);
	//交换
	printf("交换前:a=%d b=%d\n", a, b);
	Swap(&a, &b);//这里要取地址
	printf("交换后:a=%d b=%d\n", a, b);
	return 0;
}

 运行结果:

交换前:a = 10 b = 20
交换后:a = 20 b = 10

了解函数的小伙伴们,是不是已经迫不及待想要深入探索函数的内心世界了呢?那就让我们开启一段精彩的旅程,一起揭开函数内在结构的神秘面纱吧!

3.函数的参数

在C语言中,函数参数起着至关重要的作用。函数参数可以分为两类:实际参数(实参)和形式参数(形参)

3.1 实际参数(实参):

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

1.位置参数的例子:

#include <stdio.h>

int add(int a, int b)
 {
    return a + b;
 }

int main()
 {
    int result = add(8, 7);
    printf("%d\n", result);  
    return 0;
 }
 

输出:15

在这个例子中,函数add接收两个位置参数a和b,调用add(8, 7)时,8和7是实际参数,被传递给函数进行计算。

3.2 形式参数(形参):

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

换句话说,在 C 语言里,咱们把函数想象成一个加工机器。形式参数呢,就像是这个机器上预留的接口或者位置。
 
比如说有个计算两个数之和的函数  int sum(int a, int b)  ,这里的  a  和  b  就是形式参数。
 
当我们使用这个函数的时候,比如  sum(8, 7)  ,那  3 8就会放到  a  这个位置, 7  就会放到  b  这个位置。
 
形式参数只是给函数提前准备好的“空位置”,等着在调用函数的时候,把具体的数字或者其他数据填进去,然后函数根据这些填进去的数据进行相应的计算或者操作。

代码展示:

#include <stdio.h>
int add(int x, int y)
{
	int z = 0;
	z = x + y;
	return z;
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d%d", &a, &b);
	int c = add(a, b);
	printf("%d\n", c);
	return 0;
}

4. 函数的调用

函数的调用是指在程序中使用函数来执行特定的操作或计算。函数调用的一般形式是函数名后跟一对括号,括号中是函数的实际参数,如果函数不需要参数则括号可以为空。

4.1 传值调用

4.1.1传值调用的概念:

传值调用(call-by-value)是一种函数参数传递的方式,即在函数调用时,实参的值被复制并传递给函数的形参。在函数内部对形参的修改不会影响到实参的值

通俗来说:传值调用就好比您要给一个朋友送东西,但您不是直接把东西给朋友本人,而是做了一个一模一样的复制品送过去。复制品的修改也不会影响到东西本身。是不是一下就理解了!

4.1.2 传值调用适用场景

  • 1. 当您只需要在函数内部使用参数的值进行计算或操作,而不希望对原始数据进行修改时。例如,计算一个数的平方、判断一个数是否为奇数等操作,只需要读取参数的值,不需要改变原始数据。
  • 2. 当参数是基本数据类型(如整数、浮点数、字符等),并且不需要在函数内部修改其值时。因为对于基本数据类型,传值调用的效率相对较高。
  • 3. 在多线程编程中,如果多个线程同时调用同一个函数,并且不希望函数内部的操作影响到其他线程中的原始数据,传值调用可以确保每个线程都有自己独立的数据副本,避免数据竞争和不一致性。
  • 4. 当函数的功能是对输入数据进行某种一次性的处理,并且处理结果不需要反馈给原始数据所在的上下文时,传值调用可以使函数的逻辑更加清晰和独立。
  • 5. 如果函数的目的是为了获取一些基于输入值计算得到的新值,而不是修改原始输入值,传值调用也是一个合适的选择。例如,根据输入的年龄计算对应的退休年份。

4.1.3 传值调用的特点

简单和直观,适用于处理简单数据类型。

传值调用能保证原始数据的安全性,不会因为函数内部的操作而意外改变,但有时候可能不太灵活,如果您希望函数能修改原始的值,那就得用其他方式,比如传址调用啦。

4.2 传址调用

4.2.1 传址调用的概念:

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

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

通俗来说:函数的传址调用呢,就像是给朋友家门钥匙,朋友拿着钥匙能直接进您家门对里面的东西进行改动。

4.2.2 传址调用的适用场景

  • 1. 当您需要在函数内部修改外部变量的值时,比如要对一个数组进行排序,或者修改某个结构体中的成员变量。
  • 2. 当需要节省内存空间,避免复制大型数据结构(如大型数组或复杂结构体)时,通过传址调用可以直接操作原始数据,而不用复制一份。
  • 3. 实现多个函数之间共享和修改同一份数据时,传址调用可以让数据的修改在各个函数中生效。

4.2.3 传址调用的优缺点

优点: 
1. 高效性:对于大型数据结构,避免了复制数据的大量内存和时间开销。
2. 数据共享和修改:能够在函数内部修改外部的数据,实现多个部分对同一数据的共享和同步修改。
3. 更灵活的功能:可以实现一些通过传值调用难以完成的复杂操作。
 
缺点:
1. 数据安全性降低:因为函数可以直接修改原始数据,可能会导致意外的数据修改,从而引发错误,数据的安全性相对较低。
2. 代码可读性和可维护性降低:传址调用可能使代码的逻辑变得复杂,增加了理解和维护代码的难度。
3. 错误排查困难:如果出现了不期望的数据修改,追踪和定位问题的源头可能会比较困难。

以上对函数调用的讲解可能略显冗长,抱歉让大家受累了。接下来,我将尽量用简洁的代码,为大家揭示两种调用方式的精妙差异。

4.3 传值调用和传址调用的对比

传值调用:代码展示

void Swap1(int x, int y)//接收
{
	int z = 0;
	z = x;
	x = y;
	y = z;
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d%d", &a, &b);
    printf("交换前:a = %d b = %d\n", a, b);
    swap1(a, b);//传值调用
	printf("交换后:a = %d b = %d\n", a, b);
    return 0;
}   

在该代码里,调用swap1中,把a b 本身传过去了,用x y 接收。

传址调用:代码展示

void Swap2(int*  px, int* py)//接收
{
	int z = *px;
	*px  =  *py;
	*py = z;
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d",&a,&b);
	//交换
	printf("交换前:a=%d b=%d\n", a, b);
	Swap2(&a, &b);//传址调用
	printf("交换后:a=%d b=%d\n", a, b);
	return 0;
}

在该代码中,调用swap2中,将&a &b传过去了,用x y 接收

区别:

在传值调用的形式,a 和 b传给 x 和 y的时候,x 和  y会是 a 和 b 的临时拷贝 ,因此x y 的变动不会影响a b的值

在传址调用的形式,a 和 b的地址传给x 和 y,x和y需要拿指针来接收,也就是形参的指针存的是实参的地址。这样swap2函数和main函数之间就会建立联系。

传值调用相当于分身给子函数,而传址调用相当于在main函数和子函数之间建立一个传送门。

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

函数和函数之间可以根据实际的需求进行组合的,也就是互相调用的。

5.1 嵌套调用

#include <stdio.h>
// 定义一个内部函数
void inner(int num)
{
    printf("Inner : %d\n", num);
}
// 外部函数
void outer()
{
    int num = 520;
    printf("Outer :xuyuan \n");
    inner(num);
    printf("Outer :liangni\n");
}
int main() 
{
    outer();
    return 0;
}

在上诉例子中,inner 是在outer 内部定义和调用的,这就是一个简单的函数嵌套的例子。

函数可以嵌套调用,但是不能嵌套定义。

示例:

#include <stdio.h>
void outer() 
{
    // 以下是错误的嵌套定义方式,C 语言不允许这样做
    void inner(int num)
    {
        printf("This is an inner function. Number: %d\n", num);
    }
    // 后续无法使用 inner,因为它的定义是错误的
}

int main() {
    outer();
    return 0;
}

在这个示例中,在 outer  内部尝试定义 inner  是不符合 C 语言语法规则的,会导致编译错误。

5.2 链式访问

把一个函数的返回值作为另外一个函数的参数。

代码演示:

#include <stdio.h>
int main()
{
   int len = strlen("abcdef";
   printf("%d\n",len);
  
   printf("%d\n",strlen("abcdef"));//链式访问
   return 0;
}
输出:6
输出:6

经典链式例题:打印结果是什么?

#include <stdio.h>
int main()
{
    printf("%d",printf("%d",printf("%d",43)));//链式访问
    return 0;
}

 结果:

为什么最终打印出来的是4321呢?

解释:首先,printf("%d",43)打印出来的就是43,然后printf("%d",printf("%d",43))这个printf打印的是printf("%d",43)的返回值。 最后 printf("%d",printf("%d",printf("%d",43)))这里面的printf打印的是printf("%d",printf("%d",43))的返回值。

补充:printf的返回值是字符打印的个数。

printf("%d",printf("%d",43)) == printf("%d",2),在屏幕上打印2;

printf("%d",printf("%d",printf("%d",43))) ==  printf("%d",1),在屏幕上打印1。

所以最终在屏幕上看到的是 4321

链式访问的前提条件:

函数要有返回值

6. 函数的声明和定义

在C语言中,函数的声明和定义是两个不同但是相关的概念。

需要注意的是,函数的声明和定义必须保持一致,包括函数的返回类型、函数名、参数列表等。否则会导致编译错误。

6.1 函数声明

1.告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。
2.函数的声明一般出现在函数的使用之前。要满足先声明后使用。
3.函数的声明一般要放在头文件中的。

 函数声明的一般形式如下:

返回类型 函数名(参数列表);
 

 函数声明的具体例子:

int add(int a, int b);
 

将函数的定义写在main函数下方

  由于呢程序在执行的时候是一步一步往下走的,该函数在扫描到add的时发现之前没有看到过add函数,因此将add函数放在main函数后面就会报警告。

如果就想要把add函数放在后面,我们可以在main函数前面写一个函数声明。

注意:这个函数声明后面需要写冒号,x y是可以省略的。  

6.2 函数定义:

函数的定义是指函数的具体实现,交代函数的功能实现

int add(int a, int b)
 {
    return a + b;
 }
 

代码展示:

#include <stdio.h>
 //函数的声明

int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	int sum = add(a, b);
	printf("%d\n", sum);
	return 0;
}
int add(int x, int y)
 //函数的定义
{
	return x + y;
}

结语:

至此,函数基础知识的介绍就暂告一段落。相信通过这些内容,您对函数已经有了较为清晰的认识和理解。
接下来,我们将进入更具挑战性和趣味性的领域——函数递归。在函数递归中,函数将调用自身来解决问题,这是一种独特而强大的编程技巧,能帮助我们以巧妙的方式处理许多复杂的任务。让我们一起期待下一次的探索之旅!

;