回忆曾经学过的函数

简介: 函数的参数形参指的是:在函数原型或定义及 catch 语句的参数列表中被声明的对象或指针、宏定义中的参数、模板定义中的类型参数等。实参指的是:函数调用语句中以逗号分隔的参数列表中的表达式、宏调用语句中以逗号分隔的列表中一个或多个预处理标识符的序列、throw 语句的操作数、表达式的操作数、模板实例化时的实际类型参数等

目录

函数的参数

形参指的是:在函数原型或定义及 catch 语句的参数列表中被声明的对象或指针、宏定义中的参数、模板定义中的类型参数等。

实参指的是:函数调用语句中以逗号分隔的参数列表中的表达式、宏调用语句中以逗号分隔的列表中一个或多个预处理标识符的序列、throw 语句的操作数、表达式的操作数、模板实例化时的实际类型参数等

void fun(int a)//a就是形参

fun(x)//x就是实参

函数调用中参数传递的本质就是用实参来初始化形参而不是替换形参(形参就是实参的临时拷贝)

注:应当在函数原型中写出形参名称,虽然编译器会忽略它们。这样做的目的是使函数具有'’自说明''和“自编档”能力。不要在函数体内定义与形参同名的局部变量,因为形参也被看做本地变量

函数堆栈(堆栈就是内存)

我们知道函数的调用是靠堆栈来完成的,函数堆栈实际上使用的是程序的堆栈段内存空间,虽然程序的堆栈段是系统为程序分配的一种静态数据区,但是函数堆栈却是在调用到它的时候才动态分配的。也就是说,你不能在编译时就为函数分配好一块静态空间作为堆栈。一是因为无法在编译时确定一个函数运行时所需的堆栈大小;二是因为函数在调用完后如果还为它保留堆栈空间就会浪费内存,一个软件系统中成千上万的函数就会耗尽内存了!

堆栈是自动管理的,也就是说局部变量的创建和销毀、堆栈的释放都是函数自动完成的,不需要程序员的干涉。局部变量在程序执行流到达它的定义的时候创建,在退出其所在程序块的地方销毁,堆栈在函数退出的时候清退(还给程序堆栈段)。显然,由于函数堆栈在预先分配好的内存空间上创建,不需要运行时的搜索,因此比动态内存分配速度快而且安全。这是堆栈与堆和自由存储空间的区别所在(即是否显式分配和释放)。

函数堆栈主要有三个用途:在进入函数前保存环境变量和返回地址,在进入函数时保存实参的拷贝,在函数体内保存局部变量。

不论是函数的原型还是定义,都要明确写出每个参数的类型和名宇,不要贪图省事只写参数的类型而省略参数名字。如果函数没有参数,那么使用 void 而不要空着,这是因为标谁C把空的参数列表解释为可以接受任何类型和个数的参数;而标准 C++则把空的参数列表解释为不可以接受任何参数。在移植 C++/C 程序时尤其要注意这方面的不同

注:getchar的返回类型不是char类型,而是int类型,其原型如下

int getchar(void);

在正常情况下,getchar()的确返回单个字符(一般的可见字符的 ASCII 码值都是大于0的)。但如果getchar()碰到文件结束或发生读错误,它必须返回一个标志 EOF。为了区别于正常的字符,只好将EOF 定义为负数(通常为-1)。因此函数 getchar()就成了 int 类型。

函数返回值

有时候函数原本不需要返回值,但为了增加灵活性如支持链式表达,可以附加返回值。

例如字符串拷贝的数strcpy 的原型:

char *strcpy(char *strDest, const char *strSrc);

strcpy 函数将strSre 拷贝至输出参数 strDest 中,同时两数的返回值叉是strDest。 这样

做并非多此一举,可以获得如下灵活性:

char str[20];

int length = strlen( strcpy(str, "Hello World") );//这样在主函数就可以计算拷贝字符串的长度

但是注意不要把返回指针的函数用做左值,例如:

double* func(double * p)

return p;

double d= 100:

*func(&d) = 200:

虽然编译器可以接受,但是最后这个语句非常难以理解,应避免类似写法。

return 语句不可返回指向 “堆栈内存”的“指针” 或者“引用”,因为该内存单元在两数体结束时被自动释放

存储类型

标准C语言为变量、常量和函数等定义了4种存储类型,即:extern、auto、static、register,它们分别用一个关键字(存储类型说明符)来说明。一个程序元素的存储类型与它的作用域、生存期限及连接类型具有某种微妙的关系,但是一个具有作用域和连接类型的标识符不一定就具有存储类型。

这4种存储类型可分为两种生存期限:永久的(即在整个程序执行期间都存在)和临时的(即暂时保存在堆栈和寄存器中)。

extern 和 static 用来标识永久生存期限的变量和函数而auto 和register 则用来标识临时生存期限的变量(注意,只有变量才能具有临时生存期限。一个变量或函数只能具有一种存储类型,当然也只能有一种生存期限。

默认情况下,全局变量和全局函数的存储类型是extern的,能够被定义在它们之后的同一个编译单元内的函数所调用。如果变量和函数被显式地加上extern 声明,那么其他编译单元中的函数也能调用它们。

显式地声明为 static 的全局变量和全局函数具有static 存储类型,只能被同一个编译单元内的函数调用。

局部变量默认具有auto 存储类型,除非用 static 或 register 来定义。但不管如何,它们的作用域都是程序块作用域,连接类型都是内连接,在进入函数的时候创建,在函数退出的时候销毁。register 和auto 只能用于声明局部变量和局部常量。

全局常量的默认存储类型为 static 的,除非在定义了它的编译单元之外的其他编译单元中显式地用extern 声明,否则不能被访问

局部符号常量(注意,不是函数内出现的字面常量)的默认存储类型为auto,除非显示定义为static或register

函数的形参是局部变量,因此与一般的局部变量的存储类型相同,但是最好不要声明为static的

作用域

有些人可能会说,局部变量的作用域因该是函数作用域啊!局部变量不是创建在函数堆栈上吗?局部变量是创建在函数堆栈上不假,但是由于在嵌套的程序块中内层程序块定义的变量不能在其外层程序块中访问,所以局部变量不具有函数作用域,而是具有程序块作用域(由{}决定)

当局部变量与某一 个全局变量同名时,在函数内部将遮蔽该全局变量。此时在函数内部我们可以通过一元作用域解析运算符(::)来引用全局变量,例如:::g_iCount++。

连接类型:

连接类型分为外连接、内连接及无连接 3种。连接类型表明了一个标识符的可见性,因此容易和作用域概念相混淆。

如果一个标识符能够在其他编译单元中或者在定义它的编译单元中的其他范围内被调用,那么它就是外连接的。外连接的标识符需要分配运行时的存储空间(extern)

如果一个标识符能在定义它的编译单元中的其他范围内被调用,但是不能在其他编译单元中被调用,那么它就是内连接的

*一个仅能够在声明它的范围内被调用的名字是无连接的

C8AC90C8-9C72-4835-9FD4-DF6B7D790BCB.png

递归函数

一般情况下,我们都是使用层次分明的、没有环路的函数调用链来编写程序。函数除了能够嵌套调用外,还可以调用自己,这样的函数就是递归函数。递归函数的概念来源于数学领域函数的递归定义。常见的例子如 乘幂、n!、等差等比数列、斐波那契数列等:

15F61F4D-5B26-4FEC-885D-5369CD60F71D.jpeg

实际上,递归函数是通过解决基本问题进而解决复杂问题的。像上面这些数学问题中的n=0(或n=1)的情况就是基本问题,或者叫做基本条件、终止条件;而n>0(或n>1)的情况就叫做递归步,或叫做条件递归。递归步必须与原始问题相似,但规模要小于原始问题,这样才能使递归函数最终收敛于基本条件。也就是说,递归必须是有条件的递归,每次递归都要能简化原始问题并最终能够收敛于基本条件。如果出现无条件递归或递归不能简化原始问题的状况,就可能导致无限递归,最终导致程序崩溃(比死循环还要严重)。

只要问题能够用递归来定义,就能够用递归函数来实现。递归函数为什么能够进行下去?主要有3个原因:

(1):函数在进入下一轮递归的时候并没有退出,因此当前堆栈的内容并没有销毁,从而每次递归进入和退出时能够保证有序地入栈和出栈操作,不会混乱;

(2):函数内的局部变量都是动态创建的,即每次调用进入函数时才创建(压栈),而当函数返回时才销毁(出栈),因此能够保证每一次递归返回时局部变量有序地销毁;

(3):函数堆栈是自动增长的,理论上只要内存足够,它就会按需增长,直到达到最大堆栈限制为止 (堆栈溢出)

注:由于递归使用了函数的反复调用并占用了大量堆栈空间,所以其运行时的开销非常大;而迭代只是发生在一个函数内部,反复使用局部变量进行计算,因此运行时系统开销比递归要小得多。但是递归函数能够直观地反映使用递归方法定义的问题,因此编写出来的代码易于理解和阅读。我们要在程序的性能与清晰性之问做一个选择。

使用断言

断言(assert)的语义如下:如果表达式的值为0(假),则输出错误消息并终止程序的执行(一般还会出现提示对话框,说明在什么地方引发了 assert);如果表达式为真,则不进行任何操作。因此断言失败就表明程序存在一个 bug。

C++/C 的宏 assert(expression)就是这样的断言,当表达式为假时,调用库函数abort()终止程序。

程序一般分为 Debug 版本和 Release 版本,Debug 版本用于内部调试,Release版本发行给用户使用。由于 assert(expression) 的宏体全部被条件编译伪指令#ifdef_DEBUG 和#endif 所包含,因此assert()只在Debug 版本里有效。

assert 不是一个仓促拼凑起来的宏。为了不在程序的 Debug 版本和 Release 版本中中造成差别,

assert 不应该带来任何副作用。所以assert 不是函数,而是宏。程序员可以把 assert 看成一个在任何系统状态下都可以安全使用的无害测试手段。所以不要把程序中的assert 语句删除掉。

如果程序在assert处终止了,并不是说含有该assert的函数有错误,而是调用函数出了差错,assert可以帮组我们追踪到错误发生原因。

1.一般教科书鼓励程序员们进行防错性程序设计,但要知道这种编程风格可能会隐瞒错误。当进行防错性设计时,如果“不可能发生”的事情的确发生了,则要使用断言进行报警。

2.要区分断言(assert)和跟踪语句(tracer)的不同。后者是指一些用于报告程序执行过程中 当前状态的输出语句,但它们并不一定就是bug

3. 程序通过了断言的检查,并不保证就万无一失了。例如memopy()中的断言,如果给它传入两个未初始化的野指针(地址不为0 。但是没有指向合法的内存块),那么assert()就失去了作用

使用const提高函数的健壮性

Const->constant:恒定不变

用const修饰函数的参数:

如果参数用于输出,不论它是什么数据类型,也不论它采用“指针传递”还是“引用传递”,都不能加 const 修饰,否则该参数将失去输出功能。const 只能修饰输入参数。

如果输入参数采用“指针传递”,那么加 const 修饰可以防止意外地改动该指针指向的内存单元,起到保护的作用。

用const修饰函数的返回值:

如果给“指针传递”的函数返回值加 const 修饰符,那么函数返回值是一种契约性常量,不能被直接修改,并且该返回值只能被赋值给加 const 修饰的同类型指针(除非强制转型)。例如函数:

const char * GetString(void);

则如下语句将出现编译错误:

char *str = GetString();

正确的用法是

const char *str = GetString();

如果函数返回值采用“值传递”的方式,在一般情况下由于函数会把返回值拷贝到外部临时的存储单元中,所以加 const 修饰没有什么意义。例如函数:

int GetInt(void);

一般作为右值来调用:

Int x=GetInt();

相关文章
|
17天前
|
编译器 程序员 C语言
爱上C语言:什么C语言函数不太会,那你千万不要错过这篇(函数篇)
爱上C语言:什么C语言函数不太会,那你千万不要错过这篇(函数篇)
|
1月前
|
机器学习/深度学习 人工智能 C语言
【c语言基础题】— —第五版,可当作日常练习和期末复习,有奇效哟!
【c语言基础题】— —第五版,可当作日常练习和期末复习,有奇效哟!
42 1
|
1月前
|
算法 Java C语言
【c语言基础题】— —第一版,可当作日常练习和期末复习,有奇效哟!
【c语言基础题】— —第一版,可当作日常练习和期末复习,有奇效哟!
77 0
|
4月前
|
C语言
c语言编程练习题: 7-1 重要的话说三遍
这道超级简单的题目没有任何输入。 你只需要把这句很重要的话 —— “I'm gonna WIN!”——连续输出三遍就可以了。 注意每遍占一行,除了每行的回车不能有任何多余字符。 代码长度限制16 KB时间限制400 ms内存限制64 MB
34 0
|
1月前
|
程序员 C语言 CDN
【c语言基础题】— —第三版,可当作日常练习和期末复习,有奇效哟!
【c语言基础题】— —第三版,可当作日常练习和期末复习,有奇效哟!
78 0
|
1月前
|
C语言 wax
【c语言基础题】— —第四版,可当作日常练习和期末复习,有奇效哟!
【c语言基础题】— —第四版,可当作日常练习和期末复习,有奇效哟!
90 0
|
1月前
|
机器学习/深度学习 C语言
【c语言基础题】— —第二版,可当作日常练习和期末复习,有奇效哟!
【c语言基础题】— —第二版,可当作日常练习和期末复习,有奇效哟!
79 0
|
1月前
|
人工智能 网络协议 BI
【c语言基础题】— —第七版,可当作日常练习和期末复习,有奇效哟!
【c语言基础题】— —第七版,可当作日常练习和期末复习,有奇效哟!
19 0
|
9月前
|
存储
用处巨广的操作符,快来学学叭(C语言版)
用处巨广的操作符,快来学学叭(C语言版)
41 1
|
11月前
|
搜索推荐
这个知识点搞明白了,游戏开发中90%的数学问题就解决了
关键词:三角函数,游戏开发,游戏数学,微信小游戏制作工具,游戏开发教程。 嗨!大家好,我是小蚂蚁。 晓得上面那句话的逻辑谬误了吗?不论是学习数学,还是学习编程,都是锻炼一个人逻辑思维的方式,因为这两者对于思维的要求非常的严谨,稍有差池就会带来一个错误的结果。所以学习它们适合锻炼一个人严谨缜密的思维,你的逻辑思维是因为学习这两者而锻炼出来的,而不是要先要具备一定的逻辑思维,才能够进行学习。
149 0