Skip to content

LV005-函数简介

一、函数简介

1. 什么是函数

函数是一个完成特定功能的代码模块,其程序代码独立,通常要求有返回值,也可以是空值。

函数的本质其实就是一段可以重复使用的代码,这段代码是提前编写好了的,我们使用的时候直接调取就可以。函数的实现人员将函数设置为一个“黑盒子”,隐藏实现细节,并对外部提供尽可能简单的接口,因此对于函数调用方来说,只需要关心如何提供参数,根据函数的功能可以得到什么样的结果,而函内部是如何工作的,这并不重要。

2. 函数分类

我们平时见到的函数,分为库函数和自定义函数。

C 语言自带的函数称为库函数( Library Function )。库( Library )是编程中的一个基本概念, C 语言自带的库称为标准库( Standard Library ,例如我们常用的 stdio.h ),其他公司或个人开发的库称为第三方库( Third-Party Library )。

自定义函数就是我们自己编写的,方便自己使用的函数。

【注意】 C 语言中的函数在数目上没有上限,但是在 C 语言程序中,有且仅有一个名为 main 的函数。

二、主函数 main

我们接触到的第一个函数,其实就是 main ,这个函数被称为主函数,程序的执行总是从 main 开始,是所有程序运行的入口。新的 C99 标准中, main 函数有两种形式,一种带参数,一种不带参数。

1. 无参数

c
/* 无参数形式 */
int main(void)  
{
    语句块;
    
    return 0;
}

2. 带参数

c
/* 带参数形式 */
int main(int argc, char *argv[]) 
{
    语句块;
    
    return 0;
}

其实我们也有可能会看到这种的 void main() ,但是似乎它并没有在哪种标准中被提及。有些编译器认为这样是正确的,有一些可能会认为这是错误的,所以为了程序可移植性,最好还是使用标准中的形式比较好。

三、自定义函数

1. 一般形式

函数定义的一般形式如下:

c
<数据类型> <函数名称>( <形式参数> ) 
{
	语句块;
	return [(<表达式>)];
}
数据类型整个函数的返回值类型,可以是C语言中的任意数据类型,例如 int、float、char 等。可以包括存储类型说明符、数据类型说明符以及时间域说明符。如果没有返回值应该写为 void 型。
函数名称函数名称是一个标识符,需要符合标识符的命名规则。
形式参数形式参数列表,简称形参。形参可以是任意类型的变量,各参数之间用逗号(",")分隔开。在进行函数调用的时候将会赋予这些形式参数实际的值。例如,
dataType functionName( dataType1 param1, dataType2 param2 , ... )
return函数的返回值,后边表达式的值要求必须和函数名前边的数据类型保持一致。如果前边数据类型为void,那么可以写为 return; 或者全部省略,表示没有返回值
{ }花括号 { } 中的内容为函数体,函数体内有若干条语句以实现特定的功能。

【注意】

(1)在函数体中,表达式里边使用的所有变量必须事先已有说明,否则不可以使用呢。

(2)形参可以缺省说明的变量名称,但类型不能缺省。例如,

c
double Power(double x, int n) 
{
    /* 省略 */
}
/* 也可以写为下边的形式 */
double Power(double, int)
{
    /* 省略 */
}

(3)函数不可以嵌套定义,就是说,不能在一个函数中定义另外一个函数,必须在所有函数之外定义另外一个函数。 main() 也是一个函数定义,也不能在 main() 函数内部定义新函数。例如,

c
/* 下边的做法是错误的 */
void func1()
{
    printf("Hello");
    
    void func2()
    {
        printf("World!");
    }
}

(4) return 语句是提前结束函数的唯一办法。

(5)可以在不同的函数中使用相同的变量名,它们表示不同的数据,分配不同的内存,互不干扰,也不会发生混淆。

(6)函数返回值若为整型,那么在函数定义时可以省略数据类型。但是这样做的话,很有可能会报警告,所以一般还是写上更加规范点。

2. 函数声明

函数定义是对函数功能的确立,包括函数名、函数返回值类型、形参列表、函数体,是一个完整、独立的函数。

函数声明是为了把函数名、返回值类型以及形参类型、个数和顺序通知编译系统,以便在调用该函数时,编译系统进行对照检查,包括函数名是否正确、传递参数的类型、个数是否与形参一致。如若出现不对应的情况,编译会有语法错误。

在 C 语言中,函数声明称为函数原型( function prototype )。用函数原型是 ANSIC 的一个重要特点。它的作用主要是利用它在程序的编译阶段对调用函数的合法性进行全面检查。函数原型的一般形式如下:

c
<数据类型> <函数名称>( <形式参数> );

例如,

c
double Power(double x, int n);
/* 也可以省略参数名 */
double Power(double, int);

推荐一个网站,它提供了所有 C 语言标准函数的原型,并给出了详细的介绍和使用示例:

C libraryhttp://www.cplusplus.com/reference/clibrary/

【注意】

(1)如果函数调用前,没有对函数作声明,且同一源文件的前面出现了该函数的定义,那么编译器就会记住它的参数数量和类型以及函数的返回值类型,即把它作为函数的声明,并将函数返回值的类型默认为 int 型。 (2)如果在同一源文件的前面没有该函数的定义,则需要提供该函数的函数原型。用户自定义的函数原型通常可以一起写在头文件中,通过头文件引用的方式来声明。 (3)编译器实际上并不检查参数名,参数名可以任意改变。所以上边的参数名可以省略。

(4)如果函数的声明中带有 static 关键字,那么就表示告诉编译器这个函数只能在当前文件中被调用;如果是函数声明中带有 extern 关键字,表示告诉编译器这个函数是在别的源文件中被定义的。

3. 怎么调用?

3.1 调用格式

我们定义了函数,接下来就是使用了啊,一般使用形式如下:

c
<函数名称>( <实际参数> );
函数名称就是定义函数的时候的函数名。
实际参数可以简称为实参,在使用函数时,调用函数传递给被调用函数的数据。需要确切的数据。如果有形参的话,实参与形参的个数应该相等,类型应该一致。
(1)如果函数在定义的时候,没有形参,那么调用的时候也不需要传入实际参数,调用形式如下:
c
<函数名称>();

(2)函数调用可以作为一个运算量出现在表达式中,也可以单独形成一个语句。对于无返回值的函数来讲,只能形成一个函数调用语句。

(3)函数调用支持嵌套调用。

如果一个函数 fun1() 在定义或调用过程中出现了对另外一个函数 fun2() 的调用,那么我们就称 fun1() 为主调函数或主函数,称 fun2() 为被调函数

当主调函数遇到被调函数时,主调函数会暂停, CPU 转而执行被调函数的代码;被调函数执行完毕后再返回主调函数,主调函数根据刚才的状态继续往下执行。

3.2 使用实例

c
#include <stdio.h>

void fun1();
int fun2(int, int);

int main(int argc, const char *argv[]) 
{
	int x = 3;
	int y = 5;
	int z = 0;

	fun1();

	z = fun2(x, y);
	printf("z = %d\n", z);

    return 0;
}

void fun1()
{
	printf("This is fun1!\n");
}

int fun2(int a, int b)
{
	int c = 0;
	c = a + b;
	printf("This is fun2! c = %d\n", c);
	return c;
}

然后在终端运行以下命令:

shell
gcc test.c -Wall # 编译程序
./a.out          # 执行可执行程序

接着我们会在终端中看到以下输出信息:

shell
This is fun1!
This is fun2! c = 8
z = 8

4. 函数地址

函数是一块代码,那自然要在内存中存储,那它的地址怎么获取呢?函数的名称就代表了函数的地址,我们可以通过函数名打印函数存放的地址空间首地址。

c
#include <stdio.h>

void fun1();

int main(int argc, const char *argv[]) 
{
	fun1();
	printf("fun1 = %p\n", fun1);

    return 0;
}

void fun1()
{
	printf("This is fun1!\n");
}

然后在终端运行以下命令:

shell
gcc test.c -Wall # 编译程序
./a.out          # 执行可执行程序

接着我们会在终端中看到以下输出信息:

shell
This is fun1!
fun1 = 0x55c7dd5076c2

5. 形参与实参

形参在函数定义中出现的参数可以看做是一个占位符,它没有数据,只能等到函数被调用时接收传递进来的数据,所以称为形式参数,简称形参。
实参函数被调用时给出的参数是实实在在的数据,会被函数内部的代码使用,所以称为实际参数,简称实参。

两者的功能是传递数据,发生函数调用时,实参的值会传递给形参。

【两者的区别与联系】

(1)形参变量只有在函数被调用时才会分配内存,函数调用结束后,立刻释放所分配的内存空间,所以形参只有在函数内部有效,不能在函数外部使用。调用结束回到主调函数(调用该函数的函数)后则不能使用形参中的变量。

(2)实参可以是常量、变量、表达式、函数等,无论实参是何种类型的数据,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参,所以应该提前用赋值、输入等办法使实参获得确定的值。

(3)实参和形参在数量上、类型上、顺序上必须严格一致,否则会发生类型不匹配的错误。当然,如果能够进行自动类型转换,或者进行了强制类型转换,那么实参类型也可以不同于形参类型。

(4)函数调用中发生的数据传递是单向的,只能把实参的值传递给形参,而不能把形参的值反向地传递给实参。一旦完成数据的传递,实参和形参就再也没有任何关系了。所以,在函数调用过程中,形参的值发生改变并不会影响实参。

(5) 形参和实参可以同名,但它们之间是相互独立的,互不影响,因为实参在函数外部有效,而形参在函数内部有效。

四、变量作用域

前边的时候就提到了作用域的概念,但是由于没有学习函数,无法进行验证,接下来就来更加深入了解一下变量的作用域问题。

1. 局部变量

1.1 作用域说明

定义在函数内部的变量称为局部变量( Local Variable ),它的作用域仅限于函数内部, 离开该函数后就是无效的,再使用就会报错。

Tips:在 main 函数中定义的变量也是局部变量,只能在 main 函数中使用。 main 函数中也不能使用其它函数中定义的变量。 main 函数也是一个函数,具有与其他函数平等的地位。

形参变量、在函数体内定义的变量都是局部变量。实参给形参传值的过程其实就是给局部变量赋值的过程。在语句块中也可定义变量,它的作用域只限于当前语句块,也相当于局部变量。例如在 while 循环, for 循环中定义的变量。

【注意】

(1)局部变量定义后,若未初始化,则值随机,并且直接编译可能会有警告提示。

(2)语句块中定义局部变量,不仅仅可以在 while 循环, for 循环中定义, C 语言也允许出现单独的 {} ,这也是一个局部的作用域。

1.2 示例1

c
#include <stdio.h>

int fun1(int a, int b);

int main(int argc, const char *argv[]) 
{
	int x = 3, y = 2;
	int i = 0, sum = 0;

	sum = fun1(x, y);
	/* 此句调用fun1函数中的局部变量,将报错 */
	printf("a = %d, &a = %p\n b = %d, &b = %p", a, &a, b, &b);

	printf("sum = %d\n", sum);
	for(i = 0; i < 2; i++)
	{
		int b = 8;
		printf("b = %d\n", b);
	}

    return 0;
}

int fun1(int a, int b)
{
	printf("\n----- start fun1!-----\n");
	/* 此句调用main函数中的局部变量,将报错 */
	printf("x = %d, &x = %p\n y = %d, &y = %p", x, &x, y, &y);
	printf("a = %d, &a = %p\n", a, &a);
	printf("b = %d, &b = %p\n", b, &b);
	printf("-----end fun1!-----\n");
	return (a + b);
}

然后在终端运行以下命令:

shell
gcc test.c -Wall # 编译程序
./a.out          # 执行可执行程序

若保存两条会报错的语句,执行完编译命令后,会有如下报错信息:

c
test.c: In function ‘main’:
test.c:12:46: error: ‘a’ undeclared (first use in this function)
  printf("a = %d, &a = %p\n b = %d, &b = %p", a, &a, b, &b);
                                              ^
test.c:12:46: note: each undeclared identifier is reported only once for each function it appears in
test.c:12:53: error: ‘b’ undeclared (first use in this function)
  printf("a = %d, &a = %p\n b = %d, &b = %p", a, &a, b, &b);
                                                     ^
test.c: In function ‘fun1’:
test.c:28:46: error: ‘x’ undeclared (first use in this function)
  printf("x = %d, &x = %p\n y = %d, &y = %p", x, &x, y, &y);
                                              ^
test.c:28:53: error: ‘y’ undeclared (first use in this function)
  printf("x = %d, &x = %p\n y = %d, &y = %p", x, &x, y, &y);

若注释掉会报错的两句,重新执行上边两条接着我们会在终端中看到以下输出信息:

shell
----- start fun1!-----
a = 3, &a = 0x7ffe61287fcc
b = 2, &b = 0x7ffe61287fc8
-----end fun1!-----
sum = 5
b = 8
b = 8

1.3 示例2

c
#include <stdio.h>
int main(int argc, const char *argv[]) 
{
    int n = 22; 
    /* 由{ }包围的代码块 */
    {
        int n = 40;
        printf("block n: %d\n", n);
    }
    printf("main n: %d\n", n);
   
    return 0;
}

然后在终端运行以下命令:

shell
gcc test.c -Wall # 编译程序
./a.out          # 执行可执行程序

接着我们会在终端中看到以下输出信息:

shell
block n: 40
main n: 22

2. 全局变量

2.1 作用域说明

在所有函数外部定义的变量称为全局变量( Global Variable ),它的作用域默认是整个程序,也就是所有的源文件,包括 .c 和 .h 文件。

全局变量和局部变量可以同名,但是在局部范围内全局变量会被“屏蔽”,不再起作用。也就是,变量的使用遵循就近原则,如果在当前作用域中存在同名变量,就不会向更大的作用域中去寻找变量。

【注意】

(1)全局变量定义后若未初始化,则自动赋值空值。

(2) C 语言规定,只能从小的作用域向大的作用域中去寻找变量。

(3)在一个函数内部修改全局变量的值会影响其它函数,全局变量的值在函数内部被修改后并不会自动恢复,它会一直保留该值,直到下次被修改。

2.1 示例

c
#include <stdio.h>

int global;
void fun1();
void fun2();

int main(int argc, const char *argv[]) 
{
	printf("global = %d, &global = %p\n", global, &global);

	int global = 20;

	printf("global = %d, &global = %p\n", global, &global);

	fun1();
	fun2();
    return 0;
}

void fun1()
{
	printf("\n----- start fun1!-----\n");
	global = 11;
	printf("global = %d, &global = %p\n", global, &global);
	printf("-----end fun1!-----\n");
	return;
}

void fun2()
{
	printf("\n----- start fun2!-----\n");
	printf("global = %d, &global = %p\n", global, &global);
	int global = 22;
	printf("global = %d, &global = %p\n", global, &global);
	printf("-----end fun2!-----\n");
	return;
}

然后在终端运行以下命令:

shell
gcc test.c -Wall # 编译程序
./a.out          # 执行可执行程序

接着我们会在终端中看到以下输出信息:

shell
global = 0, &global = 0x56016f5a5014
global = 20, &global = 0x7ffd04c87cb4

----- start fun1!-----
global = 11, &global = 0x56016f5a5014
-----end fun1!-----

----- start fun2!-----
global = 11, &global = 0x56016f5a5014
global = 22, &global = 0x7ffd04c87c84
-----end fun2!-----

2. static 关键字

2.1 静态局部变量

前边也有提到静态局部变量,加上该关键字的局部变量拥有更长的生存周期,但是当函数结束的时候此变量存储空间虽然不会释放,原来的值也会保留,但是便无法再使用。

c
#include <stdio.h>

void fun1();

int main(int argc, const char *argv[]) 
{
	printf("\n----- First fun1!-----\n");
	fun1();
	printf("\n----- Second fun1!-----\n");
	fun1();
	/* 以下两句访问fun1中的静态局部变量,将会报错*/
	// printf("a = %d\n", a);
	// printf("&a = %p\n", &a);

    return 0;
}

void fun1()
{
	printf("----- start fun1!-----\n");
	static int a = 11;
	int b = 3;
	printf("a = %d, &a = %p\n", a, &a);
	printf("b = %d, &b = %p\n", b, &b);

	a++;
	b++;
	printf("a = %d, &a = %p\n", a, &a);
	printf("b = %d, &b = %p\n", b, &b);
	printf("-----end fun1!-----\n");
	return;
}

然后在终端运行以下命令:

shell
gcc test.c -Wall # 编译程序
./a.out          # 执行可执行程序

若保存两条会报错的语句,执行完编译命令后,会有如下报错信息:

c
test.c: In function ‘main’:
test.c:12:21: error: ‘a’ undeclared (first use in this function)
  printf("a = %d\n", a);
                     ^
test.c:12:21: note: each undeclared identifier is reported only once for each function it appears in

若注释掉会报错的两句,重新执行上边两条接着我们会在终端中看到以下输出信息:

shell
----- First fun1!-----
----- start fun1!-----
a = 11, &a = 0x55d500b8f010
b = 3, &b = 0x7fff751bb6d4
a = 12, &a = 0x55d500b8f010
b = 4, &b = 0x7fff751bb6d4
-----end fun1!-----

----- Second fun1!-----
----- start fun1!-----
a = 12, &a = 0x55d500b8f010
b = 3, &b = 0x7fff751bb6d4
a = 13, &a = 0x55d500b8f010
b = 4, &b = 0x7fff751bb6d4
-----end fun1!-----

2.2 静态的全局变量

那在全局变量前边加上该关键字呢?这样就会把这个全局变量的作用域限制在当前文件中,这里先用一下后边会学到的多文件编程,具体的可以看下一节。

2.2.1 main.c
c
#include <stdio.h>
extern int a;
extern int b;
extern void fun1(void);
int main(int argc, char *argv[])
{
    fun1();
    printf("main:a = %d,\n", a);
    printf("main:b = %d,\n", b);
    return 0;
}
2.2.2 fun1.c
c
#include <stdio.h>
int a = 10;
static int b = 20;
void fun1(void)
{
    printf("fun1:a = %d, b = %d\n", a, b);
}
2.2.3 编译测试

在终端执行以下命令:

shell
gcc *.c -Wall    # 编译程序

会在终端看到以下提示信息:

shell
/tmp/ccRSRi58.o:在函数‘main’中:
main.c:(.text+0x2f):对‘b’未定义的引用
collect2: error: ld returned 1 exit status