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

C语言---函数概念深入学习基础(3)

函数

1.函数是一段可以重复执行的代码。

它可以接受不同的参数,
完成对应的操作。

下面的例子就是一个函数

int plus(int n) {return n;
}

上面的代码声明了一个函数plus()

2.函数声明的语法有以下几点,需要注意。
  • 返回值类型。
    函数声明时,
    首先需要给出返回值的类型,
    上例是int
    表示函数plus()返回一个整数。

  • 参数。
    函数名后面的圆括号里面,
    需要声明参数的类型和参数名,
    plus(int n)表示这个函数有一个整数参数n

  • 函数体。
    函数体要写在大括号里面,
    后面(即大括号外面)不需要加分号。
    大括号的起始位置,
    可以跟函数名在同一行,
    也可以另起一行。

  • return语句。
    return语句给出函数的返回值,
    程序运行到这一行,
    就会跳出函数体,
    结束函数的调用。
    如果函数没有返回值,
    可以省略return语句,
    或者写成return;

3.调用函数时,

只要在函数名后面加上圆括号就可以了,
实际的参数放在圆括号里面,
就像下面这样。

int a = plus(13);
// a 等于 14
4.函数调用时,

参数个数必须与定义里面的参数个数一致(一一对应),
参数过多或过少都会报错。

int plus(int n) {return n + 1;
}plus(2, 2); // 报错
plus();  // 报错

上面示例中,函数plus()只能接受一个参数,传入两个参数或不传参数,都会报错。

5.函数必须声明后使用,

否则会报错。
也就是说,
一定要在使用plus()之前,声明这个函数。
如果像下面这样写,编译时会报错。

int a = plus(13);int plus(int n) {return n + 1;
}

上面示例中,在调用plus_one()之后,才声明这个函数,编译就会报错。

6.C 语言标准规定,

函数只能声明在源码文件的顶层,
不能声明在其他函数内部。

7.没有返回值的函数,

使用void关键字表示返回值的类型。
没有参数的函数,
声明时要用void关键字表示参数类型。

void myFunc(void) {// ...
}

上面的myFunc()函数,
既没有返回值,
调用时也不需要参数。

8.函数可以调用自身,

这就叫做递归(recursion)
下面是斐波那契数列的例子。

unsigned long Fibonacci(unsigned n) {if (n > 2)return Fibonacci(n - 1) + Fibonacci(n - 2);elsereturn 1;
}

上面示例中,
函数Fibonacci()调用了自身,
这样做可以简化算法。

9.main()

C 语言规定,
main()是程序的入口函数,
即所有的程序一定要包含一个main()函数。
程序总是从这个函数开始执行,
如果没有该函数,
程序就无法启动。
其他函数都是通过它引入程序的。

main()的写法与其他函数一样,
要给出返回值的类型和参数的类型,
就像下面这样。

int main(void) {printf("Hello World\n");return 0;
}

上面示例中,
最后的return 0;表示函数结束运行,返回0

11.C 语言约定,

返回值0表示函数运行成功,
如果返回其他非零整数,
就表示运行失败,
代码出了问题。
系统根据main()的返回值,
作为整个程序的返回值,
确定程序是否运行成功。

正常情况下,
如果main()里面省略return 0这一行,
编译器会自动加上,
main()的默认返回值为0。
所以,写成下面这样,
效果完全一样。

int main(void) {printf("Hello World\n");
}

由于 C 语言只会对main()函数默认添加返回值,
对其他函数不会这样做,
建议总是保留return语句

12.参数的传值引用

如果函数的参数是一个变量,
那么调用时,
传入的是这个变量的值的拷贝,
而不是变量本身。

void increment(int a) {a++;
}int i = 10;
increment(i);printf("%d\n", i); // 10

上面示例中,
调用increment(i)以后,
变量i本身不会发生变化,
还是等于10
因为传入函数的是i的拷贝,
而不是i本身,
拷贝的变化,
影响不到原始变量。
这就叫做“传值引用(单向值传递)”。
还有一种方法是是双向传递(也叫地址传递)下面会讲解

所以,
如果参数变量发生变化,
最好把它作为返回值传出来。

int increment(int a) {a++;return a;
}int i = 10;
i = increment(i);printf("%d\n", i); // 11

再看下面的例子,Swap()函数用来交换两个变量的值,由于传值引用,下面的写法不会生效。

void Swap(int x, int y) {int temp;temp = x;x = y;y = temp;
}int a = 1;
int b = 2;
Swap(a, b); // 无效

上面的写法不会产生交换变量值的效果,
因为传入的变量是原始变量ab的拷贝,
不管函数内部怎么操作,
都影响不了原始变量。

13.如果想要传入变量本身,

有一个办法,
就是传入变量的地址(地址传递是双向的)。

void Swap(int* x, int* y) {int temp;temp = *x;*x = *y;*y = temp;
}int a = 1;
int b = 2;
Swap(&a, &b);

上面示例中,
通过传入变量xy的地址,
函数内部就可以直接操作该地址,
从而实现交换两个变量的值。

虽然跟传参无关,
这里特别注意下,
函数不要返回内部变量的指针。

int* f(void) {int i;// ...return &i;
}

上面示例中,
函数返回内部变量i的指针,
这种写法是错的。
因为当函数结束运行时,
内部变量就消失了,
这时指向内部变量i的内存地址就是无效的,
再去使用这个地址是非常危险的。

14.函数指针

函数本身就是一段内存里面的代码,
C 语言允许通过指针获取函数。

void print(int a) {printf("%d\n", a);
}void (*print_ptr)(int) = &print;

上面示例中,
变量print_ptr是一个函数指针,
它指向函数print()的地址。
函数print()的地址可以用&print获得。
注意,
(*print_ptr)一定要写在圆括号里面,
否则函数参数(int)的优先级高于*
整个式子就会变成void* print_ptr(int)

有了函数指针,
通过它也可以调用函数。

(*print_ptr)(10);
// 等同于
print(10);

比较特殊的是,
C 语言还规定,
函数名本身就是指向函数代码的指针,
通过函数名就能获取函数地址。
也就是说,
print&print是一回事。

if (print == &print) // true

因此,上面代码的print_ptr等同于print

void (*print_ptr)(int) = &print;
// 或
void (*print_ptr)(int) = print;if (print_ptr == print) // true

所以,对于任意函数,
都有五种调用函数的写法。

// 写法一
print(10)// 写法二
(*print)(10)// 写法三
(&print)(10)// 写法四
(*print_ptr)(10)// 写法五
print_ptr(10)

为了简洁易读,
一般情况下,
函数名前面都不加*&

15.这种特性的一个应用是,

如果一个函数的参数或返回值,
也是一个函数,
那么函数原型可以写成下面这样。

int compute(int (*myfunc)(int), int, int);

上面示例可以清晰地表明,
函数compute()的第一个参数也是一个函数。

16.函数原型

前面说过,
函数必须先声明,后使用。
由于程序总是先运行main()函数,
导致所有其他函数都必须在main()函数之前声明。

void func1(void) {
}void func2(void) {
}int main(void) {func1();func2();return 0;
}

上面代码中,
main()函数必须在最后声明,
否则编译时会产生警告,
找不到func1()func2()的声明。

但是,
main()是整个程序的入口,
也是主要逻辑,
放在最前面比较好。
另一方面,
对于函数较多的程序,
保证每个函数的顺序正确,
会变得很麻烦。

C 语言提供的解决方法是,
只要在程序开头处给出函数原型,
函数就可以先使用、后声明。
所谓函数原型,
就是提前告诉编译器,
每个函数的返回类型和参数类型。
其他信息都不需要,
也不用包括函数体,
具体的函数实现可以后面再补上。

int twice(int);int main(int num) {return twice(num);
}int twice(int num) {return 2 * num;
}

上面示例中,
函数twice()的实现是放在main()后面,
但是代码头部先给出了函数原型,
所以可以正确编译。
只要提前给出函数原型,
函数具体的实现放在哪里,
就不重要了。

17.函数原型包括参数名也可以,

虽然这样对于编译器是多余的,
但是阅读代码的时候,
可能有助于理解函数的意图。

int twice(int);// 等同于
int twice(int num);

上面示例中,
twice函数的参数名num
无论是否出现在原型里面,
都是可以的。

注意,
函数原型必须以分号结尾。

一般来说,
每个源码文件的头部,
都会给出当前脚本使用的所有函数的原型。

18.函数说明符

C 语言提供了一些函数说明符,
让函数用法更加明确。

(1)extern 说明符

对于多文件的项目,
源码文件会用到其他文件声明的函数。
这时,当前文件里面,
需要给出外部函数的原型,
并用extern说明该函数的定义来自其他文件。

extern int foo(int arg1, char arg2);int main(void) {int a = foo(2, 3);// ...return 0;
}

上面示例中,
函数foo()定义在其他文件,
extern告诉编译器当前文件不包含该函数的定义。

不过,
由于函数原型默认就是extern
所以这里不加extern
效果是一样的。

(2)static 说明符

默认情况下,
每次调用函数时,
函数的内部变量都会重新初始化,
不会保留上一次运行的值。
static说明符可以改变这种行为。

static用于函数内部声明变量时,
表示该变量只需要初始化一次,
不需要在每次调用时都进行初始化。
也就是说,
它的值在两次调用之间保持不变。

#include <stdio.h>void counter(void) {static int count = 1;  // 只初始化一次printf("%d\n", count);count++;
}int main(void) {counter();  // 1counter();  // 2counter();  // 3counter();  // 4
}

上面示例中,
函数counter()的内部变量count
使用static说明符修饰,
表明这个变量只初始化一次,
以后每次调用时都会使用上一次的值,
造成递增的效果。

注意,
static修饰的变量初始化时,
只能赋值为常量,
不能赋值为变量。

int i = 3;
static int j = i; // 错误

上面示例中,
j属于静态变量,
初始化时不能赋值为另一个变量i

另外,
在块作用域中,
static声明的变量有默认值0

static int foo;
// 等同于
static int foo = 0;

static可以用来修饰函数本身。

static int Twice(int num) {int result = num * 2;return(result);
}

上面示例中,
static关键字表示该函数只能在当前文件里使用,
如果没有这个关键字,
其他文件也可以使用这个函数(通过声明函数原型)。

static也可以用在参数里面,
修饰参数数组。

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

上面示例中,static对程序行为不会有任何影响,
只是用来告诉编译器
该数组长度至少为3,
某些情况下可以加快程序运行速度。
另外,
需要注意的是,
对于多维数组的参数,
static仅可用于第一维的说明。

(3)const 说明符

函数参数里面的const说明符,
表示函数内部不得修改该参数变量。

void f(int* p) {// ...
}

上面示例中,
函数f()的参数是一个指针p
函数内部可能会改掉它所指向的值*p
从而影响到函数外部。

为了避免这种情况,
可以在声明函数时,
在指针参数前面加上const说明符,
告诉编译器,
函数内部不能修改该参数所指向的值。

void f(const int* p) {*p = 0; // 该行报错
}

上面示例中,声明函数时,
const指定不能修改指针p指向的值,
所以*p = 0就会报错。

但是上面这种写法,
只限制修改p所指向的值,
p本身的地址是可以修改的。

void f(const int* p) {int x = 13;p = &x; // 允许修改
}

上面示例中,
p本身是可以修改,
const只限定*p不能修改。

如果想限制修改p
可以把const放在p前面。

void f(int* const p) {int x = 13;p = &x; // 该行报错
}

如果想同时限制修改p*p
需要使用两个const

void f(const int* const p) {// ...
}

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • spring 事物使用场景说明
  • SpringBoot项目部署+属性配置方式+多环境开发
  • 59 - I. 滑动窗口的最大值
  • Windows自动化程序开发指南
  • 获取STM32 MCU的唯一ID
  • Unity动画系统详解
  • C、C++、JAVA 的区别与联系
  • 如何在本地部署大语言模型
  • Python爱心射线(完整代码)
  • 一文弄懂FLink状态及checkpoint源码
  • Liunx常用指令
  • TypeScript系列:初篇 - 类型系统
  • 基于vue框架的城市体育运动交流平台15s43(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。
  • 代码随想录刷题day30丨452. 用最少数量的箭引爆气球, 435. 无重叠区间,763.划分字母区间
  • 欧拉下搭建第三方软件仓库—docker
  • Google 是如何开发 Web 框架的
  • Django 博客开发教程 8 - 博客文章详情页
  • IndexedDB
  • JAVA之继承和多态
  • Laravel Mix运行时关于es2015报错解决方案
  • Vue全家桶实现一个Web App
  • 翻译 | 老司机带你秒懂内存管理 - 第一部(共三部)
  • 机器学习中为什么要做归一化normalization
  • 基于组件的设计工作流与界面抽象
  • 马上搞懂 GeoJSON
  • 每天10道Java面试题,跟我走,offer有!
  • 使用agvtool更改app version/build
  • LIGO、Virgo第三轮探测告捷,同时探测到一对黑洞合并产生的引力波事件 ...
  • 浅谈sql中的in与not in,exists与not exists的区别
  • 直播平台建设千万不要忘记流媒体服务器的存在 ...
  • ​七周四次课(5月9日)iptables filter表案例、iptables nat表应用
  • #Datawhale X 李宏毅苹果书 AI夏令营#3.13.2局部极小值与鞍点批量和动量
  • (13)DroneCAN 适配器节点(一)
  • (aiohttp-asyncio-FFmpeg-Docker-SRS)实现异步摄像头转码服务器
  • (DenseNet)Densely Connected Convolutional Networks--Gao Huang
  • (Matlab)基于蝙蝠算法实现电力系统经济调度
  • (windows2012共享文件夹和防火墙设置
  • (zz)子曾经曰过:先有司,赦小过,举贤才
  • (一)Java算法:二分查找
  • (一)十分简易快速 自己训练样本 opencv级联haar分类器 车牌识别
  • (一)项目实践-利用Appdesigner制作目标跟踪仿真软件
  • (转载)CentOS查看系统信息|CentOS查看命令
  • ..回顾17,展望18
  • .mysql secret在哪_MYSQL基本操作(上)
  • .mysql secret在哪_MySQL如何使用索引
  • .NET C#版本和.NET版本以及VS版本的对应关系
  • .NET Compact Framework 多线程环境下的UI异步刷新
  • .Net Core 微服务之Consul(三)-KV存储分布式锁
  • .NET Framework 3.5中序列化成JSON数据及JSON数据的反序列化,以及jQuery的调用JSON
  • .net通过类组装数据转换为json并且传递给对方接口
  • /etc/shadow字段详解
  • [ 云计算 | AWS ] 对比分析:Amazon SNS 与 SQS 消息服务的异同与选择
  • [100天算法】-每个元音包含偶数次的最长子字符串(day 53)
  • [2016.7 Day.4] T1 游戏 [正解:二分图 偏解:奇葩贪心+模拟?(不知如何称呼不过居然比std还快)]
  • [ActionScript][AS3]小小笔记