C 语言函数

embedded/2024/11/14 6:05:23/

目录

1.简介

2.main()  

3.参数的传值引用

4.函数指针

5.函数原型

6.exit()

7.函数说明符

7.1 extern 说明符

7.2 static 说明符

7.3 const 说明符

8.可变参数


1.简介

函数是一段可以重复执行的代码。它可以接受不同的参数,完成对应的操作。下面的例子就是一个函数。

int plus_one(int n) {return n + 1;
}

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

函数声明的语法有以下几点,需要注意。

(1)返回值类型。函数声明时,首先需要给出返回值的类型,上例是int,表示函数plus_one()返回一个整数。

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

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

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

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

int a = plus_one(13);
// a 等于 14

函数调用时,参数个数必须与定义里面的参数个数一致,参数过多或过少都会报错。

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

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

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

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

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

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

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

C 语言标准规定,函数只能声明在源码文件的顶层,不能声明在其他函数内部。

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

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

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

函数可以调用自身,这就叫做递归(recursion)。下面是斐波那契数列的例子。

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

上面示例中,函数Fibonacci()调用了自身,大大简化了算法

2.main()  

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

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

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

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

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

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

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

由于 C 语言只会对main()函数默认添加返回值,对其他函数不会这样做,所以建议总是保留return语句,以便形成统一的代码风格。

3.参数的传值引用

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

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的拷贝,不管函数内部怎么操作,都影响不了原始变量。

如果想要传入变量本身,只有一个办法,就是传入变量的地址。

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的内存地址就是无效的,再去使用这个地址是非常危险的。

4.函数指针

函数本身就是一段内存里面的代码,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)

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

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

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

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

5.函数原型

前面说过,函数必须先声明,后使用。由于程序总是先运行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()后面,但是代码头部先给出了函数原型,所以可以正确编译。只要提前给出函数原型,函数具体的实现放在哪里,就不重要了。

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

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

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

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

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

6.exit()

exit()函数用来终止整个程序的运行。一旦执行到该函数,程序就会立即结束。该函数的原型定义在头文件stdlib.h里面。

exit()可以向程序外部返回一个值,它的参数就是程序的返回值。一般来说,使用两个常量作为它的参数:EXIT_SUCCESS(相当于 0)表示程序运行成功,EXIT_FAILURE(相当于 1)表示程序异常中止。这两个常数也是定义在stdlib.h里面。

// 程序运行成功
// 等同于 exit(0);
exit(EXIT_SUCCESS);// 程序异常中止
// 等同于 exit(1);
exit(EXIT_FAILURE);

main()函数里面,exit()等价于使用return语句。其他函数使用exit(),就是终止整个程序的运行,没有其他作用。

C 语言还提供了一个atexit()函数,用来登记exit()执行时额外执行的函数,用来做一些退出程序时的收尾工作。该函数的原型也是定义在头文件stdlib.h

int atexit(void (*func)(void));

atexit()的参数是一个函数指针。注意,它的参数函数(下例的print)不能接受参数,也不能有返回值。

void print(void) {printf("something wrong!\n");
}atexit(print);
exit(EXIT_FAILURE);

上面示例中,exit()执行时会先自动调用atexit()注册的print()函数,然后再终止程序。

7.函数说明符

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

7.1 extern 说明符

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

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

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

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

7.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仅可用于第一维的说明。

7.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) {// ...
}

8.可变参数

有些函数的参数数量是不确定的,声明函数的时候,可以使用省略号...表示可变数量的参数。

int printf(const char* format, ...);

上面示例是printf()函数的原型,除了第一个参数,其他参数的数量是可变的,与格式字符串里面的占位符数量有关。这时,就可以用...表示可变数量的参数。

注意,...符号必须放在参数序列的结尾,否则会报错。

头文件stdarg.h定义了一些宏,可以操作可变参数。

(1)va_list:一个数据类型,用来定义一个可变参数对象。它必须在操作可变参数时,首先使用。

(2)va_start:一个函数,用来初始化可变参数对象。它接受两个参数,第一个参数是可变参数对象,第二个参数是原始函数里面,可变参数之前的那个参数,用来为可变参数定位。

(3)va_arg:一个函数,用来取出当前那个可变参数,每次调用后,内部指针就会指向下一个可变参数。它接受两个参数,第一个是可变参数对象,第二个是当前可变参数的类型。

(4)va_end:一个函数,用来清理可变参数对象。

下面是一个例子。

double average(int i, ...) {double total = 0;va_list ap;va_start(ap, i);for (int j = 1; j <= i; ++j) {total += va_arg(ap, double);}va_end(ap);return total / i;
}

上面示例中,va_list ap定义ap为可变参数对象,va_start(ap, i)将参数i后面的参数统一放入apva_arg(ap, double)用来从ap依次取出一个参数,并且指定该参数为 double 类型,va_end(ap)用来清理可变参数对象。


http://www.ppmy.cn/embedded/137427.html

相关文章

VLC-QT----Linux编译并运行示例

linux:ubuntu 16.04 qt:5.13.2 总体安装步骤 下载安装,编译 下载源码仓库,下载cmake,新建一个build文件夹,cd进去,执行代码 cmake .. -DCMAKE_BUILD_TYPEDebug 遇到报错,没有qt5Coreconfig,运行 sudo apt-get install qtdeclarative5-dev进行安装 遇到报错 Could not fi…

MySQL与Oracle对比及区别

一、比较 1、MySQL的特点 性能卓越&#xff0c;服务稳定&#xff0c;很少出现异常宕机&#xff1b; 开放源代码无版本制约&#xff0c;自主性及使用成本低&#xff1b; 历史悠久&#xff0c;社区和用户非常活跃&#xff0c;遇到问题及时寻求帮助&#xff1b; 软件体积小&#…

巧妙注入的奥秘:在 Spring 中优雅地使用 List 和 Map

文章目录 一、注入 List&#xff1a;同类项&#xff0c;一次拿下二、注入 Map&#xff1a;当键值对遇上多态三、进阶&#xff1a;使用 Qualifier 灵活注入四、总结总结推荐阅读文章 在 Spring 框架里&#xff0c;我们经常需要将不同的组件组织到集合中&#xff0c;比如在注入多…

边缘计算在工业互联网中的应用

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 边缘计算在工业互联网中的应用 边缘计算在工业互联网中的应用 边缘计算在工业互联网中的应用 引言 边缘计算概述 定义与原理 发展…

部署zabbix遇到问题: cannot find a valid baseurl for repo:centos-sclo-rh/x86 64 怎么解决 ?

安装 Zabbix 前端包&#xff0c;提示cannot find a valid baseurl for repo&#xff1a;centos-sclo-rh/x86 64 安装zabbix前端包 # yum install zabbix-web-mysql-scl zabbix-apache-conf-scl 解决办法&#xff1a; 原因是&#xff1a;CentOS7的SCL源在2024年6月30日停止维护…

odoo-040 odoo17前端的js方法调用后端py方法action报错

文章目录 问题描述梳理写法xml写法前端方法后端action的写法 错误解释 问题描述 在前端的kanban视图上添加了几个自定义按钮&#xff0c;按钮点击可以跳转到对应的tree视图&#xff0c;在写按钮调用方法的时候报错如下&#xff1a; 前端调用后端action报错&#xff1a; actio…

快递物流查询API接口如何用PHP调用

在现代商业中&#xff0c;供应链的协同运作至关重要。 快递物流查询API接口可以实现供应商、电商平台、物流企业和消费者之间的信息无缝对接&#xff0c;各方能够及时获取快递物流信息&#xff0c;从而更好地协调生产、销售和配送等环节&#xff0c;提高整个供应链的效率和效益…

手机版产品目录如何制作?

随着互联网的快速发展&#xff0c;手机已经成为人们日常生活中不可或缺的一部分。许多企业都开始重视手机端的产品展示&#xff0c;而手机版产品目录的制作就显得尤为重要。现在由我来教大家手机版产品目录制作的步骤、技巧&#xff0c;轻松打造一款高颜值、易操作的移动端产品…