当前位置: 首页 > article >正文

《C语言程序设计现代方法》note-6 函数

文章目录

    • 助记提要
    • 9章 函数
      • 9.1 函数定义
        • 返回类型
        • 形式参数
        • 函数体
      • 9.2 函数调用
      • 9.3 函数声明
        • 定义前调用函数的问题
        • 函数声明
      • 9.4 实际参数
        • 实际参数的转换
        • 数组型实参
        • 变长数组做形式参数
        • 数组参数声明时使用static
        • 复合字面量
      • 9.5 return语句
      • 9.6 程序终止
      • 9.7 泛型选择
      • 9.8 递归

助记提要

  1. 函数定义的4个组成;
  2. 函数声明和函数原型;
  3. 实参提升;
  4. 数组型实参的特点;
  5. 星号表示数组长度;
  6. static声明数组参数;
  7. 使用复合字面量避免定义变量;
  8. return和exit;
  9. 泛型选择;

9章 函数

函数是C程序的构建块,是自带声明和语句的小程序。
可以用函数把程序分为很多小块,便于理解和修改程序。
函数可以复用。

9.1 函数定义

函数定义的格式:

返回值类型 函数名(形式参数列表)
函数体
返回类型
  • 返回类型不能是数组类型;
  • void类型返回表示函数没有返回值;
  • C89可以省略返回类型,默认返回int;C99不允许省略返回类型。

返回类型过长的话,可以放在单独的一行:

unsigned long int
func(...)
{
    ...
	return ...;
}
形式参数
  • 形式参数可以有多个,逗号分开;
  • 每个形式参数前面必须说明其类型;
  • 如果没有形式参数,需要在括号里写void
函数体
  • 函数体是一堆花括号包起来的复合语句;
  • 函数体内声明的变量专属于此函数,其他函数无法调用或修改;
  • C89中变量声明必须在语句之前,C99中声明和语句可以混一起;
  • 返回类型是void的函数,函数体可以只有一对花括号;这样的操作可以在开发中预留空间,后续再来编写。
  • void函数必须在函数体中使用return语句指定返回值。

9.2 函数调用

函数调用语法:

函数名(实际参数列表);

注意 调用时只写函数名,不写圆括号和实参,也是合法语句,但是不起任何作用。

void函数调用没有返回值。非void函数调用会产生一个值,可以存储在变量中。
这个返回值在不需要时,允许被丢弃。可以在函数前加强制转换操作(void),让别人知道这个函数的返回值是故意被丢弃的。

// 函数返回打印的字符数
n = printf("Hi!\n");
// 程序用不到打印字符数,丢弃这个返回值
(void) printf("Hi\n");

9.3 函数声明

定义前调用函数的问题

编译器在main函数中遇到未知的函数调用时,不知道这个函数的形参数目和类型、返回值类型。编译器会先假设这个函数返回int类型,即为该函数做一个隐式声明
函数的实参个数和类型仍无法确定,编译器只能进行默认实参提升。
在后面遇到该函数的定义时,如果返回类型不符合,就会产生错误。

函数声明

有时无法在调用之前把函数定义好。提前定义也会使程序顺序不自然,难以阅读。

函数声明可以让编译器在调用前先浏览函数的信息,具体的函数定义可以后面再给。
函数声明的格式类似于函数定义的第一行,但是结尾要写分号:

返回类型 函数名(形式参数);

函数声明必须和函数的定义一致。

函数声明中形式参数只写类型就够了,参数名可以省略不写。
但是最好写上,因为名字可以说明形参的作用,并提醒程序员实参的传递顺序。

C99中在调用函数前必须声明或定义,否则会出错。

老式的函数声明不提供形式参数的信息,为了作区分,将提供返回类型、形参数目和类型的的函数声明称为函数原型

有时会出于防御性目的在函数原型中省略参数名字。因为在很多人编写的大型应用程序中,可能刚好有一个宏的名字和参数名一样,在预处理时参数名会被换掉。

注意 函数声明放在函数体外会更好。如果放在函数体内,那另一个函数要调用这个函数时,还需要再次声明。后续修改过程中想添加或移除函数声明会很麻烦。

注意 返回类型相同的函数可以放在一起声明,甚至函数声明可以和变量声明放在一起。但是这样做会让程序显得乱。

// 函数声明合并
void print_num(void), print_count(int n);
// 函数声明和变量声明混合
double x, y, average(double a, double b);

9.4 实际参数

形式参数出现在函数定义当中,使用假名字表示函数执行时需要的值。
形式参数的改变不会影响实际参数的值,所以可以把形式参数做为函数内的变量使用。

实际参数是出现在函数调用中的表达式。
实际参数是值传递的,函数调用时先计算出实际参数的值,然后将值赋给相应的形式参数。

实际参数的转换

实际参数的类型与形式参数的类型不匹配时,也能进行函数调用。编译器会对实际参数进行转换:

  • 若编译器调用前遇到原型,实参的值会被隐式转换为形参的类型。
  • 若编译器调用前没有遇到原型,编译器执行默认实参提升:把float类型的实参转为double;非浮点型实参执行整值提升(C99为整数提升)。

注意 默认实参提升可能无法产生期望的结果。

int main(void) {
    double x = 3.0;
    printf("%d\n", square(x));
    return 0;
}

int square(int n)
{
    return n * n;
}

上述程序打印结果为1。函数square需要int类型的实参,拿到double类型后会产出无效的结果。

数组型实参

形式参数是一维数组时,一般不说明数组的长度。

// 一维数组形参,不写长度
int f(a[])
{ ... }

即使数组形参写了长度,也会被编译器忽略。

该函数的实际参数可以是类型对应的任意的一维数组。
函数执行时,无法确定数组的元素数目。sizeof(a) / sizeof(a[0])只能计算数组总长度,而不是数组的元素数。
一维数组型的实际参数,如果函数需要知道数组元素数,就必须把元素数做为第二个参数提供出来。这个参数可以告知函数,数组的长度比实际长度小。
注意 元素数不要填写大于实际情况的值。访问超出末尾的元素,会导致未定义的行为。

注意 函数改变数组型形式参数的元素时,相应的实际参数也会被改变。

如果形式参数是多维数组,声明参数时只能省略第一维的长度,后续的维度必须指定。

C语言是按行主序存储数组的。
二维数组的行数可以由总长度除以行长推断,而行长是由这一行的列数决定的。

// 多维数组形参,第二维之后的长度必须写
int f2(a[][10])
{ ... }
变长数组做形式参数

C99允许使用非常量表达式指定数组长度,这样的变长数组也可以做参数。

int f(int n, int a[n])
{ ... }

显式地说明数组a的长度是n。
注意 参数要先写n,后写数组a。

函数声明时,参数名是可以省略的,如果第一个参数定义被省略了,就无法把a的长度和n联系起来。这时用星号来表示数组的长度与前面的参数相关:

// 省略参数名
int f(int, int [*]);

声明时方括号为空也是可以的:

int f(int, int []);

形式参数是多维变长数组时,可以指定任意的列数。

// 二维数组的全部元素相加
int sum(int n, int m, int a[n][m])
{
    int i, j, sum = 0;
    for (i = 0; i < n; i++)
        for (j = 0; j < n; j++)
            sum += a[i][j];
    return sum;
}

这个函数的声明可以是以下几种:

int sum(int n, int m, int a[n][m]);
int sum(int n, int m, int a[*][*]);
int sum(int n, int m, int a[][m]);
int sum(int n, int m, int a[][*]);
数组参数声明时使用static

C99可以在数组参数声明时使用static

int f(int a[static 3], int n)
{ ... }

static 3表示这个数组至少有3个元素。
使用static可以使编译器在函数调用时预先取出最小数目的元素值,而不是在函数内部需要用到元素时才去取。
对于多维数组参数,static仅可用于第一维。

复合字面量

调用函数时,需要传递数组的名字:

int a[] = {1, 2, 3, 4};
s = sum(a, 4);

数组a是一个变量,调用前需要初始化。如果后续不再用到它,可以使用符合字面量避免定义变量a。
复合字面量是使用给定的元素创建的没有名字的数组。

s = sum((int []){1, 2, 3, 4}, 4);

复合字面量的格式:

(数组类型 [数组长度]){数组初始化}
  • 数组长度可以不写。写长度时,初始化列表不足的部分会补0。
  • 初始化时也可以使用指示器。
  • 函数内部创建的复合字面量可以包含任意的表达式,不只是常量。
  • 复合字面量中元素的值可以被改变。如果不允许改变,可以在类型前面加const。如(const int []){2, 3}

9.5 return语句

  • void函数必须使用return语句指定返回值。
  • renturn语句的类型和函数的返回类型不符合时,会把表达式的类型隐式转换为返回类型。
  • void函数中也可以使用return语句,但是后面不能跟表达式。否则会编译错误。

return语句不是必须的,因为执行完最后一条语句之后,函数会自动返回。C89中非void函数不写return,程序在尝试使用函数返回值时,会出现未定义的行为。C99中不写return,编译器会报错。

9.6 程序终止

一些操作系统在程序终止时可以检测到状态码。
main函数的返回值就是状态码,是int类型。程序正常终止,返回0,异常终止返回非0值。

C89中不允许把main函数的返回类型定义为void。
C99中main函数返回值可以不是int类型,但是这么做降低了程序的可移植性。

想要终止程序,除了在main函数中使用return语句外,还可以调用exit函数。
exit函数是<stdlib.h>头中的函数。

// 正常终止程序
exit(0);

为了语义清晰,<stdlib.h>提供了两个宏EXIT_SUCCESSEXIT_FAILURE,通常分别是0和1。

// 正常终止程序
exit(EXIT_SUCCESS);
// 异常终止
exit(EXIT_FAILURE);

return函数只在被main函数调用时才终止程序。exit函数不论被哪个函数调用,都会终止程序。

9.7 泛型选择

有时希望使用同一个名字编写多个函数。这些函数实现的功能相似,只是参数和返回值的类型不相同。

C99使用泛型宏来统一数学函数的各个版本。如正弦函数,标准库针对不同的参数类型定义了多种相应的函数:sin、sinf、sinl、csin、csinf、csinl。使用泛型宏之后,可以直接用sin来调用它们。泛型宏可以根据传入的参数选择对应版本。

C11提供了泛型选择表达式:

_Generic(表达式, 泛型关联列表)

使用泛型表达式:

#define sin(x) _Generic(x, \
                        float: sinf, \
                        double: sin, \
                        long double: sinl, \
                        float _Complex: csinf, \
                        double _Complex: csin, \
                        long double _Complex: csinl))(x)

泛型表达式中的第一个表达式是控制表达式,它并不求值,C语言只会提取它的类型信息。后续的某个泛型的类型名和控制表达式的类型兼容,则将其作为泛型选择的结果表达式。

  • 泛型表达式的控制表达式不允许匹配多个泛型关联的类型名。
  • 可以使用一个default泛型关联,做为控制表达式都不匹配时的默认选项。
  • 泛型表达式在预处理期间被识别和替换。
  • 泛型选择不能选择数组类型,因为数组类型的表达式传过来的是指向其首元素的指针。

9.8 递归

递归是指一个函数调用它本身的情况。
C语言允许递归,但是大部分程序不需要用到递归。

函数1调用函数2,函数2又调用函数1,这种写法相当于间接的递归,也是合法的,但是确保它们可以终止。


http://www.kler.cn/a/404443.html

相关文章:

  • 【AIGC】如何使用高价值提示词Prompt提升ChatGPT响应质量
  • 【代码随想录day36】【C++复健】1049. 最后一块石头的重量 II ; 494. 目标和 ;474.一和零
  • 爬虫开发(1)爬虫开发工具介绍与环境搭建
  • Flink升级程序和版本
  • 视觉顶会论文 | 基于Swin Transformer的轴承故障诊断
  • MySQL —— explain 查看执行计划与 MySQL 优化
  • 原生微信小程序在顶部胶囊左侧水平设置自定义导航兼容各种手机模型
  • 目标检测YOLO实战应用案例100讲-基于深度学习的海上船舶识别(续)
  • Spark 分布式计算中网络传输和序列化的关系(一)
  • Java面试题分享
  • html兼容性问题处理
  • 小白怎样入门网络安全?
  • [Redis#1] 前言 | 再谈服务端高并发分布式结构的演进
  • solr 迁移数据-使用solr-import-export
  • Web 网络安全
  • ESP8266 STA模式TCP客户端 电脑手机网络调试助手
  • 【愚公系列】《微信小程序与云开发从入门到实践》002-如何设计一款小程序
  • 解决CondaError: Run ‘conda init‘ before ‘conda activate‘
  • 【SpringBoot】【log】 自定义logback日志配置
  • 使用可视化工具kafkatool连接docker的kafka集群,查看消息内容和offset
  • 字符串学习篇-java
  • Vue通用组件设计原则
  • 14. 【.NET 8 实战--孢子记账--从单体到微服务】--简易权限--章节总结
  • 十大网络安全事件
  • 打开串口程序卡死,关闭串口程序正常运行
  • MFC 实现动态调整对话框控件与字体大小