7.函数

参考资料:
C语言中文网:http://c.biancheng.net/
《C语言编程魔法书基于C11标准》
视频教程:C语音深入剖析班(国嵌 唐老师主讲)

自定义函数

函数是一段可以重复使用的代码,用来独立地完成某个功能,它可以接收用户传递的数据,也可以不接收。接收用户数据的函数在定义时要指明参数,不接收用户数据的不需要指明,根据这一点可以将函数分为有参函数和无参函数。

将代码段封装成函数的过程叫做函数定义。

无参函数的定义

如果函数不接收用户传递的数据,那么定义时可以不带参数。如下所示:

dataType  functionName(){
    //body
}
  • dataType 是返回值类型,它可以是C语言中的任意数据类型,例如 int、float、char 等。
  • functionName 是函数名,它是标识符的一种,命名规则和标识符相同。函数名后面的括号( )不能少。
  • body 是函数体,它是函数需要执行的代码,是函数的主体部分。即使只有一个语句,函数体也要由{ }包围。
  • 如果有返回值,在函数体中使用 return 语句返回。return 出来的数据的类型要和 dataType 一样。

例如,定义一个函数,计算从1加到100的结果:

#include 
#include 

int add()
{
    int sum = 0;
    for(int i = 1;i <= 100;i++)
    {
        sum += i;
    }
    return sum;
}
void main()
{
    sum = add();
    printf("%d\n",sum);
    system("pause");
}

累加结果保存在变量sum中,最后通过return语句返回。sum 是 int 型,返回值也是 int 类型,它们一一对应。return是C语言中的一个关键字,只能用在函数中,用来返回处理结果。

将上面的代码补充完整:

#include 
int sum()
{    
    int i, sum=0;
    for(i=1; i<=100; i++)
    {        
        sum+=i;    
    }    
    return sum;
}

int main()
{    
    int a = sum();
    printf("The sum is %d\n", a);
    return 0;
}

运行结果:

The sum is 5050

函数不能嵌套定义,main 也是一个函数定义,所以要将 sum 放在 main 外面。函数必须先定义后使用,所以 sum 要放在 main 前面。

注意:main 是函数定义,不是函数调用。当可执行文件加载到内存后,系统从 main 函数开始执行,也就是说,系统会调用我们定义的 main 函数。

无返回值函数

有的函数不需要返回值,或者返回值类型不确定(很少见),那么可以用 void表示,例如:

void hello()
{    
	printf ("Hello,world \n");    //没有返回值就不需要 return 语句
}

void是C语言中的一个关键字,表示“空类型”或“无类型”,绝大部分情况下也就意味着没有 return 语句。

返回值与定义类型不匹配

返回值与定义类型不匹配的意思就是说,例如:定义了一个void类型的函数,但return 1return 1.0等的情况,也就是定义与返回不匹配

在C语言中

#include 
#include 

void main()
{
	int a = 0;
	printf("a = %d\n", a);
	system("pause");
	return 1;
}

运行结果

a = 0

在C语言的编译器中没有问题,这个是因为C语言的编译器没有那么多限制,比较宽泛,但这个是一个不好的编写,因为这样编写以后可能会出现BUG,而且这种BUG很难发现,所以不要这样写。

如果是在C++中,那么就会直接编译不通过,编译器会报错。

7.函数_第1张图片

不写函数类型

若函数开头不写类型,那么函数默认为int类型。

#include 
#include 

main()
{
	int a = 0;
	printf("a = %d\n", a);
	system("pause");
	return 0;
}

运行结果

a = 0

这个能编译通过是因为用的是C编译器,如果用C++编译器是没办法通过的。

有参函数的定义

如果函数需要接收用户传递的数据,那么定义时就要带上参数。如下所示:

dataType  functionName( dataType1 param1, dataType2 param2 ... )
{
    //body
}

dataType1 param1, dataType2 param2 ...是参数列表。函数可以只有一个参数,也可以有多个,多个参数之间由,分隔。参数本质上也是变量,定义时要指明类型和名称。与无参函数的定义相比,有参函数的定义仅仅是多了一个参数列表。

数据通过参数传递到函数内部进行处理,处理完成以后再通过返回值告知函数外部。

更改上面的例子,计算从 m 加到 n 的结果:

int sum(int m, int n)
{    
	int i, sum=0;    
	for(i=m; i<=n; i++)
	{        
		sum+=i;    
	}    
	return sum;
}

参数列表中给出的参数可以在函数体中使用,使用方式和普通变量一样。

调用 sum() 函数时,需要给它传递两份数据,一份传递给 m,一份传递给 n。你可以直接传递整数,例如:

int result = sum(1, 100);  //1传递给m,100传递给n

也可以传递变量:

int begin = 4;
int end = 86;
int result = sum(begin, end);  //begin传递给m,end传递给n

也可以整数和变量一起传递:

int num = 33;
int result = sum(num, 80);  //num传递给m,80传递给n

函数定义时给出的参数称为形式参数,简称形参;函数调用时给出的参数(也就是传递的数据)称为实际参数,简称实参。函数调用时,将实参的值传递给形参,相当于一次赋值操作。

原则上讲,实参的类型和数目要与形参保持一致。如果能够进行自动类型转换,或者进行了强制类型转换,那么实参类型也可以不同于形参类型,例如将 int 类型的实参传递给 float 类型的形参就会发生自动类型转换。

将上面的代码补充完整:

#include 
int sum(int m, int n)
{    
    int i, sum=0;    
    for(i=m; i<=n; i++)
    {        
        sum+=i;    
    }    
    return sum;
}
int main()
{    
    int begin = 5, end = 86;    
    int result = sum(begin, end);    
    printf("The sum from %d to %d is %d\n", begin, end, result);    	
    return 0;
}

运行结果:

The sum from 5 to 86 is 3731

定义 sum() 时,参数 m、n 的值都是未知的;调用 sum() 时,将 begin、end 的值分别传递给 m、n,这和给变量赋值的过程是一样的,它等价于:

m = begin;
n = end;

函数不能嵌套定义

强调一点,C语言不允许函数嵌套定义;也就是说,不能在一个函数中定义另外一个函数,必须在所有函数之外定义另外一个函数。main() 也是一个函数定义,也不能在 main() 函数内部定义新函数。

下面的例子是错误的:

#include 
void func1()
{    
    printf("http://www.baidu.com");    
    void func2()
    {        
        printf("百度一下你就知道");    
    }
}int main()
{    
    func1();    
    return 0;
}

有些初学者认为,在 func1() 内部定义 func2(),那么调用 func1() 时也就调用了 func2(),这是错误的。

正确的写法应该是这样的:

#include 
void func2()
{    
    printf("百度一下你就知道");
}
void func1()
{    
    printf("http://www.baidu.com");    
    func2();
}
int main()
{    
    func1();    
    return 0;
}

func1()、func2()、main() 三个函数是平行的,谁也不能位于谁的内部,要想达到「调用 func1() 时也调用 func2()」的目的,必须将 func2() 定义在 func1() 外面,并在 func1() 内部调用 func2()。

函数的参数和返回值

如果把函数比喻成一台机器,那么参数就是原材料,返回值就是最终产品;从一定程度上讲,函数的作用就是根据不同的参数产生不同的返回值。

参数

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

函数被调用时给出的参数包含了实实在在的数据,会被函数内部的代码使用,所以称为实际参数,简称实参

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

形参和实参有以下几个特点:

  1. 形参变量只有在函数被调用时才会分配内存,调用结束后,立刻释放内存,所以形参变量只有在函数内部有效,不能在函数外部使用。

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

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

  4. 函数调用中发生的数据传递是单向的,只能把实参的值传递给形参,而不能把形参的值反向地传递给实参;换句话说,一旦完成数据的传递,实参和形参就再也没有瓜葛了,所以,在函数调用过程中,形参的值发生改变并不会影响实参。请看下面的例子:

#include 

//计算从1加到n的值
int sum(int n)
{    
    int i;    
    for(i=n-1; i>=1; i--)
    {        
        n+=i;    
    }    
    printf("The inner n = %d\n",n);    
    return n;
}
int main()
{    
    int m, total;    
    printf("Input a number: ");    
    scanf("%d", &m);    
    total = sum(m);    
    printf("The outer m = %d \n", m);    
    printf("1+2+3+...+%d+%d = %d\n", m-1, m, total);    
    return 0;
}

运行结果:

Input a number: 100↙

The inner n = 5050

The outer m = 100

1+2+3+…+99+100 = 5050

通过 scanf 输入 m 的值,作为实参,在调用 sum() 时传送给形参 n。

从运行情况看,输入 m 值为100,即实参 m 的值为100,把这个值传给函数 sum 时,形参 n 的初始值也为100,在函数执行过程中,形参 n 的值变为 5050。函数运行结束后,输出实参 m 的值仍为100,可见实参的值不随形参的变化而变化。

可变参数

C语言中可以定义参数可变的函数,就像printf()函数那样,可以接收很多个值,参数可变函数的实现依赖于stdarg.h头文件,va_list变量与va_start,va_endva_arg配合使用能够访问参数值。

这种函数必须至少有一个强制参数。可选参数的类型可以变化。可选参数的数量由强制参数的值决定,或由用来定义可选参数列表的特殊值决定。

对于每一个强制参数来说,函数头部都会显示一个适当的参数,像普通函数声明一样。参数列表的格式是强制性参数在前,后面跟着一个逗号和省略号(…),这个省略号代表可选参数。

可变参数函数要获取可选参数时,必须通过一个类型为 va_list 的对象,它包含了参数信息。这种类型的对象也称为参数指针,它包含了栈中至少一个参数的位置。可以使用这个参数指针从一个可选参数移动到下一个可选参数,由此,函数就可以获取所有的可选参数。

当编写支持参数数量可变的函数时,必须用 va_list 类型定义参数指针,以获取可选参数。在下面的讨论中,va_list 对象被命名为 argptr。可以用 4 个宏来处理该参数指针,这些宏都定义在头文件 stdarg.h 中:

void va_start(va_list argptr, lastparam);

va_star 使用第一个可选参数的位置来初始化 argptr 参数指针。该宏的第二个参数必须是该函数最后一个有名称参数的名称(也就是...前面的那个参数)。必须先调用该宏,才可以开始使用可选参数。

type va_arg(va_list argptr, type);

展开宏 va_arg 会得到当前 argptr 所引用的可选参数,也会将 argptr 移动到列表中的下一个参数。宏 va_arg 的第二个参数是刚刚被读入的参数的类型。

void va_end(va_list argptr);

当不再需要使用参数指针时,必须调用宏 va_end。如果想使用宏 va_start 或者宏 va_copy 来重新初始化一个之前用过的参数指针,也必须先调用宏va_end

void va_copy(va_list dest, va_list src);

va_copy 使用当前的src 值来初始化参数指针 dest。然后就可以使用 dest 中的备份获取可选参数列表,从src 所引用的位置开始。

例:

#include 
#include 

float average(int m,...)    //...是占位符
{
    va_list args;   //声明可变参数指针
    int i = 0;
    float sum = 0;

    va_start(args,m);   //使可变参数指针指向可变参数的首元素

    for(i = 0;i < m;i++)
    {
        sum += va_arg(args,int);    //获取可变参数中的值,并指明可变参数的类型
    }

    va_end(args);   //释放可变参数内存空间

    return sum / m;

}

int main(int argc, const char * argv[])
{
    printf("%f\n",average(5,1,2,3,4,5));
    printf("%f\n",average(4,1,2,3,4));

    return 0;
}

运行结果:

3.000000
2.500000

可变参数的限制

  • 可变参数必须从头到尾按照顺序逐个访问
  • 参数列表中至少要存在一个确定的命名参数
  • 可变参数宏无法判断实际存在的参数的数量
  • 可变参数宏无法判断参数的实际类型
  • 无法直接访问可变参数列表中的参数值
  • va_arg中如果指定了错误的类型,那么结果是不可预测的。

返回值

函数的返回值是指函数被调用之后,执行函数体中的代码所得到的结果,这个结果通过 return 语句返回。

return语句的一般形式为:

return 表达式;

或者:

return (表达式);

有没有( )都是正确的,为了简明,一般也不写( )。例如:

return max;
return a+b;
return (100+200);
  1. 没有返回值的函数为空类型,用void表示。例如:
void func()
{    
	printf("http://c.biancheng.net\n");
}

一旦函数的返回值类型被定义为 void,就不能再接收它的值了。例如,下面的语句是错误的:

int a = func();

为了使程序有良好的可读性并减少出错, 凡不要求返回值的函数都应定义为 void 类型。

  1. return 语句可以有多个,可以出现在函数体的任意位置,但是每次调用函数只能有一个 return 语句被执行,所以只有一个返回值(少数的编程语言支持多个返回值,例如Go语言)。例如:
//返回两个整数中较大的一个
int max(int a, int b)
{    
	if(a > b)
    {        
        return a;    
    }
    else
    {        
        return b;    
    }
}

如果a>b成立,就执行return areturn b不会执行;如果不成立,就执行return breturn a不会执行。

  1. 函数一旦遇到 return 语句就立即返回,后面的所有语句都不会被执行到了。从这个角度看,return 语句还有强制结束函数执行的作用。例如:
//返回两个整数中较大的一个
int max(int a, int b)
{    
    return (a>b) ? a : b;    
    printf("Function is performed\n");
}

第 4 行代码就是多余的,永远没有执行的机会。

下面我们定义了一个判断素数的函数,这个例子更加实用:

#include 

int prime(int n)
{    
    int is_prime = 1, i;    //n一旦小于0就不符合条件,就没必要执行后面的代码了,所以提前结束函数    
    if(n < 0){ return -1; }    
    for(i=2; i<n; i++)
    {        
        if(n % i == 0)
        {            
            is_prime = 0;            
            break;        
        }    
    }    
    return is_prime;}
int main()
{    
    int num, is_prime;    
    scanf("%d", &num);    
    is_prime = prime(num);    
    if(is_prime < 0)
    {        
        printf("%d is a illegal number.\n", num);
    }
    else if(is_prime > 0)
    {        
        printf("%d is a prime number.\n", num);
    }
    else
    {        
        printf("%d is not a prime number.\n", num);    
    }    
    return 0;
}

prime() 是一个用来求素数的函数。素数是自然数,它的值大于等于零,一旦传递给 prime() 的值小于零就没有意义了,就无法判断是否是素数了,所以一旦检测到参数 n 的值小于 0,就使用 return 语句提前结束函数。

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

return 后面可以跟一份数据,表示将这份数据返回到函数外面;return 后面也可以不跟任何数据,表示什么也不返回,仅仅用来结束函数。

更改上面的代码,使得 return 后面不跟任何数据:

#include 
void prime(int n)
{    
    int is_prime = 1, i;    
    if(n < 0)
    {        
        printf("%d is a illegal number.\n", n);        
        return;  //return后面不带任何数据    
    }    
    for(i=2; i<n; i++)
    {        
        if(n % i == 0)
        {            
            is_prime = 0;            
            break;        
        }    
    }    
    if(is_prime > 0)
    {        
        printf("%d is a prime number.\n", n);    
    }
    else
    {        
        printf("%d is not a prime number.\n", n);    
    }
}
int main()
{    
    int num;    
    scanf("%d", &num);    
    prime(num);    
    return 0;
}

prime() 的返回值是 void,return 后面不能带任何数据,直接写分号即可。

函数参数的副本机制

实际上,函数的参数是一种副本机制,例如 int a = 1; a把值传给了函数中的参数b,那么b就开辟了一个临时的副本空间用来存放该值,所以在函数中对参数的修改(副本的修改)不会影响到原有的值。

#include 
#include 

int add(int sum,int num)
{
	for (int i = 0; i <= num; i++)
	{
		sum += i;
	}
	return sum;
}

int main()
{
	int num = 100,sum = 60;
	printf("sum = %d\n", sum);
	printf("add() = %d\n", add(sum,num));
	printf("sum = %d\n", sum);
	system("pause");
	return 0;
}

运行结果

sum = 60
add() = 5110
sum = 60

可以发现main函数中的sum并没有发生改变,所以也验证了函数参数的副本机制。

#include 
#include 

void address(int add)
{

	printf("%p\n", &add);
	
	printf("%s\n", "避免被回收内存");
}

int main()
{
	address(10);
	system("pause");
	return 0;
}

运行结果

0136FC28

7.函数_第2张图片

这次传递的值为常量,常量是存在寄存器中的,所以常量是没有地址的,但当传入函数后,那么这个函数就被内存创建了一个副本,所以才能打印出地址,但这个地址不是常量的地址,是函数中副本机制产生的地址。

参数与返回值的类型转换

函数的参数其实是一次赋值过程,所以函数的参数是会进行类型转换的,并且返回值也会进行数据类型的转换。

#include 
#include 

int print(int a, int b)
{
	printf("a = %d,b = %d\n", a, b);
	printf("a = %f,b = %f\n", a, b);	//如果没有进行数据类型转换,那么这里会打印正确信息
	return 3.14159;
}

void main()
{
	float a = 1.123, b = 2.456;
	float c;
	int d;
	c = print(a, b);
	d = print(a, b);
	printf("c = %f,d = %d", c, d);
}

运行结果

a = 1,b = 2
a = 0.000000,b = 0.000000
a = 1,b = 2
a = 0.000000,b = 0.000000
c = 3.000000,d = 3

从结果可以看出,参数其实也是一个赋值的过程,并且进行了数据类型的转换,如果没有进行数据类型的转换用浮点型输出数据的时候应该是正确结果,而不是0.000000,返回值也进行了数值的转换,返回值会根据定义的函数的类型进行数据类型的转换,如果返回值没有进行数据类型的转换的话那么c打印的应该是3.14159,而不是3.000000,所以从上面的结果可以得出,返回值和函数的参数都会进行数据类型的转换。

return也有副本机制

#include 
#include 

int getnum()
{
	int num;
	printf("%p\n", &num);
	num = 10;
	return num;
}

void main()
{
	printf("%d\n", getnum());

	system("pause");
}

Debug的过程

7.函数_第3张图片

上图是定位到num的内存地址

7.函数_第4张图片

上图为num修改成了10

7.函数_第5张图片

上图可看到,地址中的数据会在函数调用完后全部被回收了的,既然都被回收后,那么return的值是哪里来的,那就是在return时会先把这份数据给备份成为一份副本,然后把副本返回回来。但这个值存在内存还是存在寄存器,这个就要看编译器了。这个地址由编译器来进行维护,我们不必关心,也很难关心。

总结:凡是由副本机制的,都会通过赋值,赋值时又会自动完成类型转换,所以函数的参数,返回值都会进行类型转换,如果不想因为类型转换而造成BUG,那么就要传入类型匹配的参数,与返回类型匹配的值。

函数调用发现程序运行的秘密

求值顺序

所谓函数调用(Function Call),就是使用已经定义好的函数。函数调用的一般形式为:

函数名(实参列表);

实参可以是常数、变量、表达式等,多个实参用逗号,分隔。

在C语言中,函数调用的方式有多种,例如:

//函数作为表达式中的一项出现在表达式中
z = max(x, y);
m = n + max(x, y);
//函数作为一个单独的语句
printf("%d", a);
scanf("%d", &b);
//函数作为调用另一个函数时的实参
printf( "%d", max(x, y) );
total( max(x, y), min(m, n) );

在函数调用中还应该注意的一个问题是求值顺序。所谓求值顺序是指对实参列表中各个参数是自左向右使用呢,还是自右向左使用。对此,不同编译器的规则可能不同。请看下面一段代码:

#include 

int f()
{
    return 1;
}
int g()
{
    return 2;
}

int main()
{    
    int i=8; 
    int y = f() * g();	//到底是f这个函数先调用还是g这个函数先调用,不同编译器有不同处理顺序
    printf("%d %d %d %d\n",++i,++i,--i,--i);    
    return 0;
}

在 VC6.0 和 C-Free 5.0 下的运行结果为:

8 7 6 7

在 Xcode 下的运行结果为:

9 10 9 8

可见 VC 6.0 和 C-Free 5.0 是按照从右至左的顺序求值,而 Xcode 相反,按照从左向右的顺序求值。

嵌套调用

函数不能嵌套定义,但可以嵌套调用,也就是在一个函数的定义或调用过程中出现对另外一个函数的调用。

【示例】计算sum = 1! + 2! + 3! + … + (n-1)! + n!

分析:可以编写两个函数,一个用来计算阶乘,一个用来计算累加的和。

#include 
//求阶乘
long factorial(int n)
{    
    int i;    
    long result=1;    
    for(i=1; i<=n; i++)
    {        
        result *= i;    
    }    
    return result;
}

// 求累加的和
long sum(long n)
{    
    int i;    
    long result = 0;    
    for(i=1; i<=n; i++)
    {        
        //嵌套调用        
        result += factorial(i);    
    }    
    return result;
}
int main()
{    
    printf("1!+2!+...+9!+10! = %ld\n", sum(10));    
    return 0;
}

运行结果:

1!+2!+…+9!+10! = 4037913

sum() 的定义中出现了对 factorial() 的调用,printf() 的调用过程中出现了对 sum() 的调用,而 printf() 又被 main() 调用,它们整体调用关系为:

factorial()  -->  sum()  -->  printf()  -->  main()

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

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

一个C语言程序的执行过程可以认为是多个函数之间的相互调用过程,它们形成了一个或简单或复杂的调用链条。这个链条的起点是 main(),终点也是 main()。当 main() 调用完了所有的函数,它会返回一个值(例如return 0;)来结束自己的生命,从而结束整个程序。

函数是一个可以重复使用的代码块,CPU 会一条一条地挨着执行其中的代码,当遇到函数调用时,CPU 首先要记录下当前代码块中下一条代码的地址(假设地址为 0X1000),然后跳转到另外一个代码块,执行完毕后再回来继续执行 0X1000 处的代码。整个过程相当于 CPU 开了一个小差,暂时放下手中的工作去做点别的事情,做完了再继续刚才的工作。

**从上面的分析可以推断出,在所有函数之外进行加减乘除运算、使用 if…else 语句、调用一个函数等都是没有意义的,这些代码位于整个函数调用链条之外,永远都不会被执行到。**C语言也禁止出现这种情况,会报语法错误,请看下面的代码:

#include 

int a = 10, b = 20, c;
//错误:不能出现加减乘除运算
c = a + b;

//错误:不能出现对其他函数的调用
printf("www.baidu.com");
int main()
{    
    return 0;
}

函数的声明以及函数原型

C语言代码由上到下依次执行,原则上函数定义要出现在函数调用之前,否则就会报错。但在实际开发中,经常会在函数定义之前使用它们,这个时候就需要提前声明。

所谓声明(Declaration),就是告诉编译器我要使用这个函数,你现在没有找到它的定义不要紧,请不要报错,稍后我会把定义补上。

函数声明的格式非常简单,相当于去掉函数定义中的函数体,并在最后加上分号;,如下所示:

dataType  functionName( dataType1 param1, dataType2 param2 ... );

也可以不写形参,只写数据类型:

dataType  functionName( dataType1, dataType2 ... );

函数声明给出了函数名、返回值类型、参数列表(重点是参数类型)等与该函数有关的信息,称为函数原型(Function Prototype)。函数原型的作用是告诉编译器与该函数有关的信息,让编译器知道函数的存在,以及存在的形式,即使函数暂时没有定义,编译器也知道如何使用它。

有了函数声明,函数定义就可以出现在任何地方了,甚至是其他文件、静态链接库、动态链接库等。

【实例1】定义一个函数 sum(),计算从 m 加到 n 的和,并将 sum() 的定义放到 main() 后面。

#include 
//函数声明
int sum(int m, int n);  //也可以写作int sum(int, int);
int main(){
    int begin = 5, end = 86;
    int result = sum(begin, end);
    printf("The sum from %d to %d is %d\n", begin, end, result);
    return 0;
}
//函数定义
int sum(int m, int n){
    int i, sum=0;
    for(i=m; i<=n; i++){
        sum+=i;
    }
    return sum;
}

我们在 main() 函数中调用了 sum() 函数,编译器在它前面虽然没有发现函数定义,但是发现了函数声明,这样编译器就知道函数怎么使用了,至于函数体到底是什么,暂时可以不用操心,后续再把函数体补上就行。

【实例2】定义两个函数,计算1! + 2! + 3! + ... + (n-1)! + n!的和。

#include 
// 函数声明部分
long factorial(int n);  //也可以写作 long factorial(int);
long sum(long n);  //也可以写作 long sum(long);
int main(){
    printf("1!+2!+...+9!+10! = %ld\n", sum(10));
    return 0;
}
//函数定义部分
//求阶乘
long factorial(int n){
    int i;
    long result=1;
    for(i=1; i<=n; i++){
        result *= i;
    }
    return result;
}
// 求累加的和
long sum(long n){
    int i;
    long result = 0;
    for(i=1; i<=n; i++){
        result += factorial(i);
    }
    return result;
}

运行结果:
1!+2!+…+9!+10! = 4037913

初学者编写的代码都比较简单,顶多几百行,完全可以放在一个源文件中。对于单个源文件的程序,通常是将函数定义放到 main() 的后面,将函数声明放到 main() 的前面,这样就使得代码结构清晰明了,主次分明。

使用者往往只关心函数的功能和函数的调用形式,很少关心函数的实现细节,将函数定义放在最后,就是尽量屏蔽不重要的信息,凸显关键的信息。将函数声明放到 main() 的前面,在定义函数时也不用关注它们的调用顺序了,哪个函数先定义,哪个函数后定义,都无所谓了。

然而在实际开发中,往往都是几千行、上万行、百万行的代码,将这些代码都放在一个源文件中简直是灾难,不但检索麻烦,而且打开文件也很慢,所以必须将这些代码分散到多个文件中。对于多个文件的程序,通常是将函数定义放到源文件(.c文件)中,将函数的声明放到头文件(.h文件)中,使用函数时引入对应的头文件就可以,编译器会在链接阶段找到函数体。

前面我们在使用 printf()、puts()、scanf() 等函数时引入了 stdio.h 头文件,很多初学者认为 stdio.h 中包含了函数定义(也就是函数体),只要有了头文件就能运行,其实不然,头文件中包含的都是函数声明,而不是函数定义,函数定义都放在了其它的源文件中,这些源文件已经提前编译好了,并以动态链接库或者静态链接库的形式存在,只有头文件没有系统库的话,在链接阶段就会报错,程序根本不能运行。

函数的递归调用

一个函数在它的函数体内调用它自身称为递归调用,这种函数称为递归函数。执行递归函数将反复调用其自身,每调用一次就进入新的一层,当最内层的函数执行完毕后,再一层一层地由里到外退出。

下面我们通过一个求阶乘的例子,看看递归函数到底是如何运作的。阶乘 n! 的计算公式如下:

在这里插入图片描述

根据公式编程:

long factorial(int n){
    long result;
    if(n==0 || n==1){
        result = 1;
    }else{
        result = factorial(n-1) * n;  // 递归调用
    }
    return result;
}

这是一个典型的递归函数。调用 factorial() 后即进入函数体,只有当 n0 或 n1 时函数才会执行结束,否则就一直调用它自身。

由于每次调用的实参为 n-1,即把 n-1 的值赋给形参 n,所以每次递归实参的值都减 1,直到最后 n-1 的值为 1 时再作递归调用,形参 n 的值也为1,递归就终止了,会逐层退出。

例如求 5!,即调用factorial(5)。当进入 factorial() 函数体后,由于 n=5,不等于0或1,所以执行result = factorial(n-1) * n;,即result = factorial(5-1) * 5;,接下来也就是调用factorial(4)。这是第一次递归。

进行四次递归调用后,实参的值为 1,也就是调用factorial(1)。这时递归就结束了,开始逐层返回。factorial(1) 的值为 1,factorial(2) 的值为 1 * 2 = 2,factorial(3) 的值为 2 * 3 = 6,factorial(4) 的值为 6 * 4= 24,最后返回值 factorial(5) 为 24 * 5= 120。

注意:为了防止递归调用无终止地进行,必须在函数内有终止递归调用的手段。常用的办法是加条件判断,满足某种条件后就不再作递归调用,然后逐层返回。

递归调用不但难于理解,而且开销很大,如非必要,不推荐使用递归。很多递归调用可以用迭代(循环)来代替。

【示例】用迭代法求 n!。

long factorial(int n){
    int i;
    long result=1;
    if(n==0 || n==1){
        return 1;
    }
    for(i=1; i<=n; i++){
        result *= i;
    }
    return result;
}

内联函数

内联函数:把函数变为内联函数将建议编译器尽可能高速地调用该函数,至于建议的效果则由实现来定义。因此,使函数变为内联函数可能会简化函数的调用机制,但也可能不起作用。内联函数是通过编译器来实现的,而宏则是在预编译的时候替换。也就是说,是否进行函数的内联由编译器决定

创建内联函数方法:在函数声明中使用函数说明符inline

内联函数的具体效果

例如:

#include 

inline int add(int x)	//内联函数的定义
{
    return  x + x;
}

int main()
{
    int c = add(1);
    printf("%d\n",c);
    return 0;
}

使用了内联函数后的效果,也就相当于把函数中的内容替换到了原来函数调用中,这样就减少了函数调用时所浪费的开销。

#include 

int main()
{
    int c = x + x;
    printf("%d\n",c);
    return 0;
}

内联函数的特点:

  • 具有内部连接的任一函数都可以作为一个内联函数,也就是没有用extern标识为外部函数的都可以作为内联函数,因为如果标识了外部函数会出现函数名相同的冲突。

    例子:

    # a.c
    #include 
    void Func()
    {
    	printf("hello");
    }
    # b.c
    #include 
    extern inline void Func()
    {
    	printf("hello");
    }
    int main()
    {
    	Func();
    	return 0;
    }
    

    这时编译会报错

具有外部连接的函数则有以下限制

  • 如果一个函数用inline函数说明符进行声明,那么它也应该定义在同一翻译单元中(同一翻译单元也就是同一个文件中)

    例子:

    #include 
    inilne void Func();	//用inline进行内联函数声明
    inline void Func()	//对内联函数进行定义,上面进行了内联函数的声明,所以定义也要在相同文件中进行定义
    {
    	printf("hello");
    }
    int main()
    {
    	Func();
    	return 0;
    }
    
  • 如果一个函数在所有文件作用域中声明,但在某一翻译单元中(某一文件)包含了inline该函数说明符,而没有extern,那么在该翻译单元中(该文件)的定义是一个内联定义。也就是说,如果在另外一个文件中已经声明了全局的声明,但在另外一个文件却又出现了inline的函数声明,而没有extern声明,那么在该文件中,该函数就是一个内联定义

    例子:

    #a.c
    int Func(int n);	//函数声明
    int Func(int n)
    {
        return n * 3;
    }
    #b.c
    inline int Func(int n)	//b.c中的内联函数
    {
        return n * 2;
    }
    
  • 内联定义不提供对该函数的外部定义,并且也禁止用它在另一个翻译单元中(另外一个文件)的外部定义。也就是说如果该函数作了内联定义,那么就禁止调用它的外部定义

    例子:

    #a.c
    inline int Func(int n)
    {
        return n * 2;
    }
    #b.c
    extern int Func(int n);
    

    这时编译就会报错

  • 内联定义提供了对一个外部定义的替代品,编译器可以用来实现在同一翻译单元(同一文件)中对该函数的任一调用(选择内联定义的调用或外部定义的调用)。

    例子:

    #main.c
    #include 
    
    //Func是一个具有内联定义的函数
    inline int Func(int n)
    {
        return n * 2;
    }
    
    extern inline int Func2(int n)
    {
        //对于具有外部连接的一个函数的内联定义,不应该包含可修改的静态存储周期对象。
        //这里编译器可能会报出警告
        static int s;
    
        s += n;
    
        return s + n;
    }
    
    static inline int Func3(int n)
    {
        //对于具有内部连接的一个内联函数,可以包含可修改的静态存储对象
        static int s;
    
        s += n;
    
        return s + n;
    }
    
    //MyTest函数定义在hello.c源文件中
    extern void MyTest(void);   //外部函数声明
    
    int main(int argc, const char * argv[])
    {
        int result = Func(3);
        printf("result = %d\n",result);
    
        MyTest();
    
        //调用完MyTest()函数之后,Func2函数中静态对象s的值变为了20
        result = Func2(10);
        printf("result in main is: %d\n",result);
    
        //在调用Func3函数之前,它所包含的静态对象s的值为0
        result = Func3(1);
        printf("result 1 = %d\n",result);
    
        //在调用了一次Func3函数之后,它所包含的静态对象s的值为1
        result = Func3(2);
        printf("result 2 = %d\n",result);
    
        return 0;
    }
    #hello.c
    #include 
    
    //这里将Func定义具有外部连接的一个函数
    int Func(int n)
    {
        return n * 3;
    }
    
    inline int Func2(int n)
    {
        static int s;
    
        s += n;
    
        return s + n;
    }
    
    void MyTest(void)
    {
        printf("value is: %d\n",Func(2));
    
        int result = Func2(20);
    
        printf("result in MyTest is: %d\n",result);
    }
    

    Debug运行结果:

    result = 9
    value is: 6
    result in MyTest is: 40
    result in main is: 40
    result 1 = 2
    result 2 = 5
    

    Release运行结果:

    result = 6
    value is: 6
    result in MyTest is: 40
    result in main is: 20
    result 1 = 2
    result 2 = 5
    

    结果不同就是因为Debug模式没有使用到内联函数,而Release模式用到了内联函数,所以导致结果不同,所以说选择内联定义的调用或外部定义的调用由编译器决定

  • 对函数的调⽤是使⽤的是内联定义还是外部定义则是未指定的,这个的意思就说,不同的编译器会选择使用不同的函数,例如GCC,debug使用的是外部函数,release使用的是内联函数

什么时候使用内联函数

  1. 函数代码体积不大,如果代码体积太大就不要使用了,这样会造成编译出来的程序会比较大
  2. 频繁调用的函数,这个是第二个条件,就是函数代码不多,并且频繁调用的函数使用内联函数

内联函数与宏

内联函数和宏,都可以减少函数调用的开销,但内联函数比宏多了语法检测和函数特征,这个是因为宏只是字符串替换

例:

#define Func(x) ((x) + (x)) 
inline int Func(x)
{
    return x + x;
}
虽然上面两句的效果是一样的,但有的问题就是
例如
#define Func(x) ((x) + (x)) sakdjaj
后面多添加一些没用的内容是没问题的,因为宏只是字符的替换,然而内联函数就可以检测到语法问题

副作用与顺序点

副作用:对⼀个易变对象的访问、对⼀个对象的修改、对⼀个⽂件的修改,或调⽤⼀个函数,所有这些操作都具有副作⽤。副作⽤就是对执⾏环境中的状态做了改变。例如a++,int a = 1;等都会有副作用,因为对程序执行状态发生了改变。

那么多个副作用之间的发生顺序就是顺序点。

C语言规定发生顺序的顺序点是:C 标准规定代码执行过程中的某些时刻是顺序点,当到达一个顺序点时,在此之前的副作用必须全部作用完毕,在此之后的副作用必须一个都没发。至于两个顺序点之间的多个副作用哪个先发生哪个后发生则没有规定,编译器可以任意选择各副作用的作用顺序。
解释一下就是,在目标的代码里,对同一元素的多次访问(内存的访问)必然通过几段独立代码完成。现代计算机的计算都在寄存器里做,顺序点的作用就是确保在某个时刻这些改变必须反应到随后对同一存储位置的访问中。也就是说在一个顺序点的时候最好只发生一次副作用,否则可能处理的结果与你想要的不一样

常见顺序点的位置:

  1. 每个完整表达式结束时,即分号哪里。完整表达式包括变量初始化表达式(int a = 0;),表达式语句(int a = b + c;),return 语句的表达式 (return 0;),以及条件、循环和 switch 语句的控制表达式(for 头部有三个控制表达式)。
  2. 运算符 &&||、三目运算符?: 和逗号运算符的每一个运算对象计算之后。
  3. 函数调用中对所有实际参数和函数名表达式(需要调用的函数也可能通过表达式描述)的求值完成之后(进入函数体之前)。
  4. 在一个完整的声明末尾是顺序点,所谓完整的声明是指这个声明不是另外一个声明的一部分。比如声明int a[10], b[20];,在a[10]末尾是顺序点,在b[20]末尾也是。
  5. printfscanf这种带转换说明的输入/ 输出库函数,在处理完每一个转换说明相关的输入/ 输出操作时是一个顺序点。
  6. 库函数bsearchqsort在查找和排序过程中的每一步比较或移动操作之间是一个顺序点。

例如:在一个顺序点中发生了多次副作用,所以导致结果不一样

#include 
int main2()
{
    int k = 2;
    k = k++ + k++;
    printf("%d\n",k);
    return 0;
}

运算结果:

VS:6

gcc:5

这个就是两个编译器在处理顺序点的问题上不同的处理方法导致的

例如:&&每个运算对象之后就是一个顺序点

#include 

int main()
{
	int a = 1;
	if (a-- && a)
	{
		printf("a = %d\n", a);	//a = 0
	}
	return 0;
}

运算结果为空,也就是不打印a的值,但此时a的值为0,因为 &&运算符的每个处理点都是顺序点,那么a--就要执行进行处理

例子:函数调用前对参数的计算

#include 
int f(int i, int j)
{
    printf("%d, %d\n", i, j);
}

int main(int argc, char* argv[])
{
    int k = 1;
    f(k, k++);
    printf("%d\n", k);
    return 0;
}

结果:

2, 1
2

VS与gcc计算结果一样,这个是因为在进入函数前要计算好每个参数的值,那么每个参数的计算都是一个顺序点

main函数

应用启动时调用的函数命名为main函数,实现不需要对此函数做原型声明。它赢被定义为返回类型为int,并且不带任何形参的函数,如int main(void){ ... },或者带有两个形参的函数,如int main(int argc,char * argv[]){ ... }。这两个形参所对应的实参是在执⾏该程序时传⼊的。argc⼀般存放执⾏当前程序时输⼊的命令字符串个数;argv则存放了指向各个输⼊字符串的指针。假设我们现在对C源⽂件编译构建后,⽣成了⼀个名为test的可执⾏⽂件。那么我们在控制台中输⼊ testarg1arg2,再按回车,那么此时,main函数的第⼀个参数argc的值为 3,因为test其实就属于要传⼊到argv数组的第1个参数,然后后⾯跟着2个 命令⾏参数arg1arg2;所以argv对应的实参内容为: {“test”,“arg1”,“arg2”},即由应⽤程序名与其命令⾏参数字符串所构成 的数组。

带参main函数有两个形参的形式,那么他们应该遵循

  1. argc的值应该是一个非负数。
  2. argv[argc]应该是一个空指针。
  3. 如果argc的值大于零,那么数组成员argv[0]argv[grgc-1]应该包含指向字符串的指针,这些字符串在程序启动前由主机环境给出。如果argc的值大于1,那么argv[1]argv[-1]所指向的字符串才表示程序形参,即当前应用名后面的命令行参数。
  4. 形参argcargv以及argv数组所指向的字符串可以在程序中修改,并且在程序启动和终止之间保留其最后被修改的值。

main函数的返回值相当于调⽤库函数exit所传⼊的实参值。如果main函 数中缺省return语句,那么默认为return 0

例子:

#include 
//const char *argv[]类型实际上是const char **argv
int main(int argc,const char *argv[])
{
    if(argc == 0)
    {
        return -1;
    }

    //argv[0]指向的字符串为当前程序名
    //argv[0]的类型实际上是const char *argv,因为这个指针指向的是传入的字符串,既然是字符串,那么也就是要用指针指向字符串的地址的。
    printf("当前程序名为:%s\n",argv[0]);

    //以下依次输出程序名后面的参数名
    for(int i = 1;i < argc;i++)
    {
        printf("参数%d :%s\n",i,argv[i]);
    }

    //我们可以对argc、argv参数进行任意修改
    argc = 10;

    argv[0] = "hello";
    argv[1] = "world";
    argv = NULL;

    //缺省return语句,这里默认为return 0;
}

运行结果:

输入:main_test.exe test1 test2

输出:

当前程序名为:main_test.exe
参数1 :test1
参数2 :test2

注意:程序默认的入口函数就是main函数,因此main函数必须要有外部连接,它不能是static的,也不能带有inline等函数说明符。

函数与函数调用作为sizeof操作数

C语言标准规定,sizeof操作符不应该应用于

  1. 具有函数类型
  2. 一个不完整类型的表达式
  3. 访问一个位域成员的表达式。

_Alignof操作符也是不应该用于一个函数类型或不完整类型,注意,当一个函数名作为sizeof_Alignof的操作数时,它不会被隐式转换为指向该函数类型的指针类型,这个与它单独用于其计算表达式有所不同。所以,如果我们定义了一个函数:void Foo (void),那么sizeof(Foo)的结果是未定义的,而sizeof(&Foo)是合法的,其结果相当于sizeof(void (*) (void)),也就是一个指针对象大小。

如果sizeof的操作数是一个函数调用表达式,那么它的结果相当于sizeof(函数返回类型),同时,作为sizeof操作数的函数调用将不会发生。由于函数返回类型不能是一个可变修改类型,因此这里不会涉及程序在运行时对可变修改类型对象所占存储空间大小的计算。

#include 

static int Func1(void) {
    puts("Func1");
    return 0;
}

static void Func2(void) {
    puts("Func2");
}

int main(int argc,const char *argv[]) {
    //这里由于Func1返回的时int类型,所以sizeof的结果相当于sizeof(int)
    size_t size = sizeof(Func1());
    printf("Func1() size is: %zu\n",size);

    //由于Func2的返回类型时void,它属于不完整类型,
    //因此理论上这里的sizeof结果在标准里是未定义的,
    //在GCC与Clang的实现上,结果为1
    size = sizeof(Func2());
    printf("Func2() size is: %zu\n",size);

    //这里将Func1,一个函数标识作为sizeof操作数,其行为是未定义的
    size = sizeof(Func1);
    printf("Function size is: %zu\n",size);

    //这里对&Func1,即一个函数指针类型作为sizeof操作数,
    //该值与sizeof(void(*)(void))相同
    size = sizeof(&Func1);
    printf("Function pointer size is: %zu\n",size);
    return 0;
}

运行结果

Func1() size is: 4
Func2() size is: 1
Function size is: 1
Function pointer size is: 8

你可能感兴趣的:(C语言学习笔记,c语言)