C语言进阶教程(函数参数的秘密)

简介: C语言进阶教程(函数参数的秘密)

前言

本篇文章带大家学习一下函数的参数,函数的参数有很多同学认为就是很简单的东西,但是里面却包含了非常多的知识。

一、函数参数会占用内存吗

当你调用函数时,需要将函数参数的值传递给函数的形式参数。这个过程中,内存会被分配来存储传递的参数值。具体地说,对于基本数据类型的参数(如整数、浮点数等),其值会直接传递给函数,并在函数内部使用。而对于复杂数据类型的参数(如结构体、数组等),通常使用指针或引用传递,将参数的地址传递给函数,在函数内部通过指针或引用访问参数的值。

内存的分配和使用是由编译器和操作系统管理的。在函数调用过程中,栈(stack)是通常用来存储函数参数的内存区域。当函数调用结束后,栈上的参数内存会被释放,可以用于其他函数调用或程序的运行。

需要注意的是,函数参数的内存占用是临时的,仅在函数调用期间有效。一旦函数调用结束,函数参数的内存将被释放,不再占用内存。

总结起来,函数参数在调用函数时会占用内存,但是这个内存占用是临时的,并在函数调用结束后被释放。

函数调用栈:

二、函数参数的求值顺序

下面给出一个程序:

#include <stdio.h>
int main(int argc, char** argv)
{
    int k = 1;
    printf("k = %d k = %d\n", k++, k++);
    return 0;
}

大家肯定都会认为结果就是1和2,其实结果并不是1和2。

结果和我们认为的是相反的,是2和1,那么这是为什么呢?

在C语言中,函数参数的求值顺序是未定义的。这意味着编译器可以根据需要按任意顺序对函数参数进行求值。

在你提供的示例代码中,printf函数的参数表达式中包含了两次k++操作。由于未定义的求值顺序,编译器可以按任意顺序对这两个k++进行求值。

在这种情况下,编译器选择先求值第一个k++,并将其值(1)传递给printf函数的参数。然后,编译器再求值第二个k++,将其值(2)传递给printf函数的参数。因此,结果显示为1和2。

需要注意的是,因为编译器的选择是未定义的,所以在实际编程中,应避免在函数参数中使用具有副作用的表达式,特别是多次使用同一个变量并改变其值。这样的代码会导致行为不确定,不同的编译器可能得到不同的结果。

总结起来,结果为1和2是由于函数参数求值顺序是未定义的。在实际编程中,应避免依赖于未定义的行为,并编写清晰、可预测的代码。

三、什么是函数调用栈

函数调用栈(Function Call Stack),也称为调用栈、执行栈或运行栈,是计算机程序在执行函数调用及返回时所使用的一种数据结构。它主要用于跟踪函数的调用关系、保存函数的局部变量和控制函数的执行流程。

函数调用栈是基于栈(Stack)数据结构实现的。栈是一种特殊的数据结构,遵循"后进先出"(Last-In-First-Out, LIFO)的原则。在函数调用栈中,每个函数调用都会创建一个新的栈帧(Stack Frame),即一个独立的区域。

当一个函数被调用时,它的参数、局部变量和返回地址等信息被存储在函数调用栈的栈帧中。栈帧的结构通常包括以下几个重要的部分:

1.返回地址(Return Address):指向调用函数的指令地址,用于在函数执行完后返回到正确的位置继续执行。

2.局部变量(Local Variables):存储函数内部定义的局部变量的空间。

3.参数(Arguments):存储传递给函数的参数值。

4.上一个函数的栈帧指针(Previous Frame Pointer):指向上一个函数的栈帧,用于函数返回时恢复上一级函数的上下文。

当函数调用结束时,它的栈帧会被弹出(出栈),同时控制权会返回到上一级函数的栈帧中继续执行。这个过程称为函数返回。

函数调用栈的管理和维护是由处理器、操作系统和编译器共同协作完成的。每个线程都有自己的函数调用栈,栈的大小是有限的,当递归函数层次太深或者占用的栈空间超过了限制,就可能引发栈溢出错误。

函数调用栈在程序执行期间发挥着重要的作用,它使得函数调用和返回能够正确执行,并提供了存储局部变量和管理函数执行流程的机制。

总结

本篇文章就讲解到这里,大家不仅仅是看文章更多的是需要去动手练习,这才是提高的方法。


相关文章
|
1月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
35 3
|
13天前
|
C语言
c语言调用的函数的声明
被调用的函数的声明: 一个函数调用另一个函数需具备的条件: 首先被调用的函数必须是已经存在的函数,即头文件中存在或已经定义过; 如果使用库函数,一般应该在本文件开头用#include命令将调用有关库函数时在所需要用到的信息“包含”到本文件中。.h文件是头文件所用的后缀。 如果使用用户自己定义的函数,而且该函数与使用它的函数在同一个文件中,一般还应该在主调函数中对被调用的函数做声明。 如果被调用的函数定义出现在主调函数之前可以不必声明。 如果已在所有函数定义之前,在函数的外部已做了函数声明,则在各个主调函数中不必多所调用的函数在做声明
28 6
|
1月前
|
存储 缓存 C语言
【c语言】简单的算术操作符、输入输出函数
本文介绍了C语言中的算术操作符、赋值操作符、单目操作符以及输入输出函数 `printf` 和 `scanf` 的基本用法。算术操作符包括加、减、乘、除和求余,其中除法和求余运算有特殊规则。赋值操作符用于给变量赋值,并支持复合赋值。单目操作符包括自增自减、正负号和强制类型转换。输入输出函数 `printf` 和 `scanf` 用于格式化输入和输出,支持多种占位符和格式控制。通过示例代码详细解释了这些操作符和函数的使用方法。
37 10
|
26天前
|
存储 算法 程序员
C语言:库函数
C语言的库函数是预定义的函数,用于执行常见的编程任务,如输入输出、字符串处理、数学运算等。使用库函数可以简化编程工作,提高开发效率。C标准库提供了丰富的函数,满足各种需求。
|
1月前
|
机器学习/深度学习 C语言
【c语言】一篇文章搞懂函数递归
本文详细介绍了函数递归的概念、思想及其限制条件,并通过求阶乘、打印整数每一位和求斐波那契数等实例,展示了递归的应用。递归的核心在于将大问题分解为小问题,但需注意递归可能导致效率低下和栈溢出的问题。文章最后总结了递归的优缺点,提醒读者在实际编程中合理使用递归。
61 7
|
1月前
|
存储 编译器 程序员
【c语言】函数
本文介绍了C语言中函数的基本概念,包括库函数和自定义函数的定义、使用及示例。库函数如`printf`和`scanf`,通过包含相应的头文件即可使用。自定义函数需指定返回类型、函数名、形式参数等。文中还探讨了函数的调用、形参与实参的区别、return语句的用法、函数嵌套调用、链式访问以及static关键字对变量和函数的影响,强调了static如何改变变量的生命周期和作用域,以及函数的可见性。
30 4
|
1月前
|
存储 编译器 C语言
C语言函数的定义与函数的声明的区别
C语言中,函数的定义包含函数的实现,即具体执行的代码块;而函数的声明仅描述函数的名称、返回类型和参数列表,用于告知编译器函数的存在,但不包含实现细节。声明通常放在头文件中,定义则在源文件中。
|
29天前
|
存储 C语言
【c语言】字符串函数和内存函数
本文介绍了C语言中常用的字符串函数和内存函数,包括`strlen`、`strcpy`、`strcat`、`strcmp`、`strstr`、`strncpy`、`strncat`、`strncmp`、`strtok`、`memcpy`、`memmove`和`memset`等函数的使用方法及模拟实现。文章详细讲解了每个函数的功能、参数、返回值,并提供了具体的代码示例,帮助读者更好地理解和掌握这些函数的应用。
25 0
|
29天前
|
C语言
【c语言】qsort函数及泛型冒泡排序的模拟实现
本文介绍了C语言中的`qsort`函数及其背后的回调函数概念。`qsort`函数用于对任意类型的数据进行排序,其核心在于通过函数指针调用用户自定义的比较函数。文章还详细讲解了如何实现一个泛型冒泡排序,包括比较函数、交换函数和排序函数的编写,并展示了完整的代码示例。最后,通过实际运行验证了排序的正确性,展示了泛型编程的优势。
20 0
|
1月前
|
算法 C语言
factorial函数c语言
C语言中实现阶乘函数提供了直接循环和递归两种思路,各有优劣。循环实现更适用于大规模数值,避免了栈溢出风险;而递归实现则在代码简洁度上占优,但需警惕深度递归带来的潜在问题。在实际开发中,根据具体需求与环境选择合适的实现方式至关重要。
28 0