08. C语言函数

【函数基础】

函数用于将程序代码分类管理,实现不同功能的代码放在不同函数内,一个函数等于一种功能,其它函数可以调用本函数执行。

C语言规定所有的指令数据必须定义在函数内部,比如之前介绍的程序执行流程控制语句,另外修改全局变量的操作也是通过指令进行的,所以全局变量只能在函数内修改。

数据作用域

定义的数据有使用区域限制,分为以下三种:

1.全局数据,在函数外定义的数据,所有函数都能使用。

2.局部数据,在函数内定义的数据,只有本函数可以使用(可通过指针绕过这一限制)。

3.语句内数据,在程序执行流程控制语句内定义的数据,只能在本语句内部使用,本质上属于局部数据的一种,存储方式与局部数据相同,都是在栈空间存储。

不同作用域的数据可以同名,比如全局变量、局部变量、语句内变量三者可以同名,调用同名数据时使用距离最近的数据,但是为了避免混乱一般不会定义同名数据。

在C语言中定义全局数据并引用另一个全局数据赋值时,不能引用全局变量赋值,可以引用全局常量赋值,而在C++中没有这个限制。

#include <stdio.h>

int a = 1;          //全局变量
const int b = 2;    //全局常量

//int c = a;    //错误,禁止编译
int c = b;      //可以编译

int main()
{
    return 0;
}

函数之间的通讯

函数之间可以互相调用执行,从一个函数跳转到另一个函数,调用者一般使用call指令跳转到接任者,接任者执行完毕后使用ret指令返回到调用者。

有些函数执行时需要与调用者进行通信,执行前需要调用者传入数据,执行完毕后需要向调用者发送执行结果,最简单的函数通信方式是使用全局变量进行中转,调用者将通信数据写入全局变量,接任者读取全局变量获得通信数据,接任者执行完毕后将执行结果写入全局变量并返回,调用者读取全局变量接收执行结果,此方式可以实现双向通信,但是无法保密,所有函数都可以读取通信数据。

为了实现保密通信,C语言为函数提供了参数和返回值。

1.参数,用于调用者向接任者发送数据,参数由接任者定义、由调用者赋值,参数在语意上属于定义者的局部数据,只不过可以在函数执行时由调用者设置一个初始值,参数可以有多个,也可以没有。

2.返回值,用于接任者向调用者发送执行结果,返回值由编译器自动定义,接任者使用return关键词为返回值赋值,调用者定义一个变量接收返回值,返回值最多只能有一个,也可以没有。

函数定义方式:

1.设置返回值类型,若没有返回值则设置为void。

2.设置函数名称。

3.编写()符号,并在内部定义参数。

4.编写{}符号,并在内部定义执行语句,若有返回值则在函数末尾使用return关键词为返回值赋值。

#include <stdio.h>

/* 定义函数,int为返回值类型,add为函数名,参数定义时不赋值,执行时由调用者赋值 */
int add(const int a, const int b)
{
    return a+b;    //return语句为返回值赋值,同时终止函数执行
}
int main()
{
    int result = add(1,2);    //调用add函数执行,需要为参数赋值,同时定义变量result接收返回值,若不接收返回值则返回值自动废弃
    printf("%d\n", result);
    
    return 0;
}

参数使用方式:

1.参数的定义和使用与基础类型数据相同。

2.为参数赋值时,若类型不同则编译器会自动进行类型转换,转换方式与使用 = 符号为数据赋值时相同。

3.参数可以定义为常量,常量参数由调用者赋值后禁止在接任者内部修改。

返回值使用方式:

1.使用return关键词为编译器自动管理的返回值赋值,可以直接使用数据赋值,也可以使用运算式、有返回值的另一个函数为本函数的返回值赋值,此时会首先执行一遍运算式或另一个函数。

2.函数执行到return语句后终止执行并返回,若return语句之后还有代码也不会再执行,可以使用return语句当做终止函数语句,在符合指定条件时终止函数执行。

3.若函数无需返回值,可以将返回值类型定义为void,此时函数内无需定义return语句,函数会执行到最后定义的语句。

4.调用函数的语句本身表示函数返回值,可以使用此语句为一个数据赋值,这与运算式本身表示运算结果相同,比如上述代码中的 add(1,2); 即表示执行add函数也表示此函数的返回值,add函数执行完毕后返回main函数,并为变量result赋值。

指针作为参数

参数可以定义为指针,若调用者将指针参数赋值为本函数局部数据的地址,则接任者可以使用调用者的局部数据。

#include <stdio.h>
void f1(int * const arg)    //指针参数本身无需修改时,定义为常量更合适
{
    *arg = 0;
}
int main()
{
    int a = 9;
    f1(&a);
    
    printf("%d\n", a);    //输出0,变量a的值被f1函数修改
    
    return 0;
}

指针作为返回值

指针返回值一般用于返回全局数据的地址,不能返回本函数局部数据的地址,因为函数执行完毕后其使用的栈空间区域将会被回收,此区域存储的局部数据将会被其它数据覆盖,但是若参数本身是个指针,则可以直接返回指针参数,指针参数由调用者赋值,调用者不会将其赋值为接任者定义的局部数据地址。

若返回本函数局部数据的地址会导致如下情况:

1.在GCC编译器中,会自动将指针赋值为0,返回的指针不能使用,并会发出警告。

2.在VC++编译器中,不会进行任何干预,这将会导致调用了错误的数据,当进行函数内联后,函数会合并,会执行成功,但是这种使用方式是错误的,不要被内联函数所迷惑。

#include <stdio.h>
int * f1()
{
    int a = 9;
    return &a;
}
int main()
{
    printf("%d\n", f1());    //不同编译器有不同的执行结果
    
    return 0;
}

main函数

main函数是用户自定义代码的执行入口,用户编写的代码从main函数开始执行,main函数只能有一个,返回值是int类型,一般返回0,作用是告知操作系统此程序正常执行完毕,没有出现异常情况。

#include <stdio.h>
int main()
{
    /*
      自定义代码
     */
    return 0;
}

注:main函数并非程序最早执行的代码,main只是用户编写代码的入口,编译器会自动添加一些代码在main函数之前执行,这些代码用于设置程序执行必要的功能,之后再跳转到main函数执行。

扩展全局数据使用范围

编译器在编译代码时是从前向后进行的,首先定义的代码首先编译,如果一个全局数据在定义之前调用它则会报错,编译器找不到此数据,此时可以使用 extern 关键词将全局数据或函数的使用范围扩展到之前的代码,extern 关键词扩展数据使用范围时只需定义主体部分即可,若是全局数据则定义数据的类型和名称,若是函数则无需定义{}符号以及内部代码。

#include <stdio.h>
extern void f1();         //函数外的extern针对所有函数
int main()
{
    extern int a;         //函数内的extern只针对本函数
    printf("%d\n", a);
    
    f1();
    
    return 0;
}
int a = 9;
void f1()
{
    printf("阿狸\n");
}

函数内联

函数内联是一种优化方式,将一个函数的代码合并到调用者中,此时调用函数时无需执行跳转与返回,执行速度更快,缺点是函数被多次调用时编译后的程序体积会增加很多。

函数内联由编译器自动控制,在GCC编译器中可以为函数添加如下代码人为控制内联:

attribute((noinline)),禁用内联优化。

attribute((always_inline)),强制进行内联优化。

__attribute__((noinline)) void f1()    //本函数禁止内联
{
    //......
}

函数指针

函数指针存储函数的执行入口地址,定义语法如下:返回值类型(*指针名)(参数类型)。

#include <stdio.h>
int add(const int data1, const int data2)
{
    return data1 + data2;
}
int main()
{
    int (*p1)(int,int) = add;    //定义函数指针,p1为指针名,直接使用函数名赋值
    printf("%d\n", p1(1,2));     //通过指针调用函数,将指针名作为函数名使用即可
    
    return 0;
}

【数据的存储方式】

全局数据

1.全局变量,操作系统分配专用的一组内存页存储,可以称其为全局变量区,全局变量区未使用的部分全部设置为0,所以定义全局变量不赋值时默认值为0。

2.全局常量,操作系统分配专用的一组内存页存储,可以称其为全局常量区,这里的内存页会被操作系统设置为只读,修改其中的数据会被CPU禁止,开启编译优化后某些常量也会转换为立即数寻址,增加执行速度。

局部数据

1.局部变量(包括参数),它们不需要在程序执行期间一直存在,所属函数执行完毕后既删除,局部数据会在函数执行期间频繁读写,为了增加局部变量读写速度操作系统为程序分配一段地址连续的内存空间存储,同时函数为了更快的读写数据经常以栈的方式使用这段内存空间,所以此段内存也称为程序的栈空间,栈空间不会进行初始化操作,定义局部变量不赋值时默认值无法预测。操作系统按线程分配栈空间,线程内所有函数共用此栈空间,不同函数使用不同的区域,函数执行完毕后释放此段栈空间的使用权,供其他函数使用,此时函数内定义的局部数据将会被其它数据覆盖。

2.局部常量,开启编译优化后局部常量会尽量转换为立即数寻址,若需参与其它运算则将立即数写入寄存器,若需使用指针调用则将立即数写入栈空间,而长度很大的临时常量(比如临时字符串)会放在全局常量区中存储,这样无需使用多个指令的组合通过立即数存储。

静态局部变量

定义局部变量时添加static关键词表示静态局部变量,静态局部变量存储在全局变量区中,所以它在程序执行期间一直存在,它本质是在函数外定义的全局数据,但是只允许本函数使用,这个限制是由编译器提供的,对程序进行逆向分析并修改时并不存在此限制。

静态局部变量的作用是保存本函数的执行结果,并禁止其它函数使用,本函数再次执行时直接取上次保存的结果使用。

#include <stdio.h>
int f1()
{
    static int a = 0;    //静态局部数据只会定义一次,下次执行函数时不会重复定义,而是直接使用上次的值
    a++;
    
    return a;            //可以使用静态局部变量为返回值赋值
}
int main()
{
    printf("f1函数执行第%d次\n", f1());
    printf("f1函数执行第%d次\n", f1());
    
    return 0;
}

上述代码等同于如下代码,只不过编译器禁止其它函数使用变量a。

#include <stdio.h>
int a = 0;
int f1()
{
    a++;
    return a;
}
int main()
{
    printf("f1函数执行第%d次\n", f1());
    printf("f1函数执行第%d次\n", f1());
    
    return 0;
}

因为静态局部变量等于全局变量,所以在C语言中不能在定义它时引用其它全局变量赋值,也不能使用函数局部数据进行赋值。

void f(int arg)
{
    static int a = arg;    //错误
}

【函数实现原理】

保存现场与还原现场

函数使用某个寄存器时会首先将寄存器原值入栈存储,函数执行完毕后取栈中的数据还原寄存器,这种行为称为保存现场与还原现场,防止返回上一级函数时寄存器原值丢失。

栈空间设置

程序执行时操作系统按线程分配栈空间,线程内所有函数共用此栈空间,不同的函数使用不同区域,防止混乱,栈空间可以使用push/pop、mov分别进行读写,push/pop只能按固定顺序读写,但是连续读写速度快,mov可以在任意位置读写,但是读写速度稍慢,两种读写方式互补,功能复杂的函数经常同时使用这两种指令读写栈空间,为了防止混乱编译器将一个函数使用的栈空间区域再分为两部分,并使用bp、sp寄存器分别存储两部分的地址,push/pop使用sp寄存器确定操作地址,mov使用bp寄存器确定操作地址。

参数、返回值

使用x86-64处理器、Linux操作系统的C语言程序参数存储方式如下:

1.整数参数,使用rdi、rsi、rdx、rcx、r8、r9寄存器按顺序存储,若整数参数超过6个,剩余参数使用栈空间存储,栈参数使用C规范,最后定义的参数最先使用push入栈,最先定义的参数最后入栈,函数执行完毕后由调用者修改sp寄存器释放参数占用的栈空间。

2.浮点数参数,使用 xmm0 - xmm7 寄存器按顺序存储,若超过8个则剩余浮点数使用栈存储。

另外Linux的某些API函数会使用eax传递一个额外的参数,用于说明要操作的浮点数数量,比如printf函数,若要输出一个浮点数,则会设置eax为1。

返回值存储方式如下:

1.整数返回值,使用rax、rdx寄存器存储,长度不超过64位使用rax,超过64位使用rax+rdx。

2.浮点数返回值,使用xmm0、xmm1寄存器存储。

#include <stdio.h>
int add(int data1, int data2)
{
    return data1 + data2;
}
int main()
{
    int a,b;
    scanf("%d%d", &a, &b);       //输入a、b的值
    
    printf("%d\n", add(a,b));    //输出add函数返回值
    
    return 0;
}

GCC -O0 汇编代码:

0000000000401132 <add>:
  401132:	push   rbp                        ;保存现场
  401133:	mov    rbp,rsp                    ;rsp写入rbp,mov使用rbp读写栈空间

  401136:	mov    DWORD PTR [rbp-0x4],edi    ;data1入栈保存
  401139:	mov    DWORD PTR [rbp-0x8],esi    ;data2入栈保存
  40113c:	mov    edx,DWORD PTR [rbp-0x4]    ;data1写入edx
  40113f:	mov    eax,DWORD PTR [rbp-0x8]    ;data2写入eax
  401142:	add    eax,edx                    ;data1 + data2,计算结果保存在eax,直接作为返回值

  401144:	pop    rbp                        ;还原现场
  401145:	ret                               ;返回


0000000000401146 <main>:
  401146:	push   rbp                        ;保存现场
  401147:	mov    rbp,rsp                    ;rsp写入rbp
  40114a:	sub    rsp,0x10                   ;栈顶地址减0x10,将栈空间分为两段使用,rsp原值 至 rsp减0x10 这段区域为mov操作区域,push/pop操作之后的区域,执行入栈指令时不影响mov操作的栈区域

  40114e:	lea    rdx,[rbp-0x8]              ;变量b地址写入rdx传参
  401152:	lea    rax,[rbp-0x4]              ;变量a地址写入rax,之后写入rsi传参
  401156:	mov    rsi,rax
  401159:	mov    edi,0x402004               ;"%d%d"字符串地址作为第一个参数
  40115e:	mov    eax,0x0                    ;0个浮点数
  401163:	call   401040                     ;执行scanf函数

  401168:	mov    edx,DWORD PTR [rbp-0x8]    ;变量b写入edx,之后写入esi传参
  40116b:	mov    eax,DWORD PTR [rbp-0x4]    ;变量a写入eax,之后写入edi传参
  40116e:	mov    esi,edx
  401170:	mov    edi,eax
  401172:	call   401132                     ;执行add函数

  401177:	mov    esi,eax                    ;add返回值写入esi,作为printf的参数
  401179:	mov    edi,0x402009               ;"%d\n"字符串地址
  40117e:	mov    eax,0x0                    ;0个浮点数
  401183:	call   401030                     ;执行printf函数

  401188:	mov    eax,0x0                    ;设置返回值
  40118d:	leave                             ;还原rbp、rsp
  40118e:	ret                               ;返回


Contents of section .rodata:                   ;rodata段存储全局常量
 402000 01000200 25642564 0025640a 00          ;"%d%d" "%d\n" 字符编码

栈空间是从上到下使用的,在main函数中,使用rbp寄存器之前首先将其入栈存储原值,之后将存储栈顶的rsp写入rbp,再将rsp减0x10,mov指令使用bp操作栈空间,操作范围是 rsp原值 到 rsp原值减0x10,push/pop指令使用 rsp原值减0x10 之后的栈空间。

函数末尾使用leave指令还原rsp、rbp的值,leave等同于如下两条指令的组合:

mov rsp, rbp
pop rbp

GCC -O3,禁用函数内联,汇编代码:

0000000000401050 <main>:
  401050:	sub    rsp,0x18                   ;栈顶地址减0x18,栈空间从高地址向低地址使用,为了让lea、mov指令使用rsp+x的方式调用数据需要将栈顶减去所需字节
  401054:	mov    edi,0x402004               ;"%d%d"字符串地址写入edi
  401059:	xor    eax,eax                    ;0个浮点数
  40105b:	lea    rdx,[rsp+0xc]              ;变量b地址写入rdx传参
  401060:	lea    rsi,[rsp+0x8]              ;变量a地址写入rsi传参
  401065:	call   401040                     ;scanf

  40106a:	mov    esi,DWORD PTR [rsp+0xc]    ;变量b写入esi传参
  40106e:	mov    edi,DWORD PTR [rsp+0x8]    ;变量a写入edi传参
  401072:	call   401180                     ;add

  401077:	mov    edi,0x402009               ;"%d\n"字符串地址写入edi
  40107c:	mov    esi,eax                    ;add返回值写入esi
  40107e:	xor    eax,eax                    ;0个浮点数
  401080:	call   401030                     ;printf

  401085:	xor    eax,eax                    ;设置返回值
  401087:	add    rsp,0x18                   ;还原rsp
  40108b:	ret                               ;返回

0000000000401180 <add>:
  401180:	lea    eax,[rdi+rsi*1]            ;data1 + data2,结果保存在eax作为返回值
  401183:	ret                               ;返回

【函数递归调用】

函数可以调用自己执行,称为递归执行,可以直接调用也可以间接调用,比如 A->B,B->C,C->A,这种递归称为间接递归。

函数递归执行与循环语句的作用相同,都是将一段代码循环执行,区别在于循环语句是在本语句内部循环执行,而函数递归是整个函数循环执行,函数递归执行会从函数的起始地址处开始循环,函数起始地址处是保存现场、设置栈空间相关指令,每次递归执行都会消耗一些栈空间,递归次数过多会导致栈顶超界,程序将会被操作系统强制退出,并且递归执行效率也不高。

#include <stdio.h>
void f1(int arg)
{
	/* 递归代码 */
	printf("%d\n", arg);
	
	/* 每次递归后参数+1 */
	arg++;
	
	/* 若arg小于10则递归执行 */
	if(arg < 10)
	{
		f1(arg);    //递归执行时可以使用本次的参数值为下次执行时的参数赋值
	}
}
int main()
{
	f1(0);
	
	return 0;
}

【数组作为参数和返回值】

数组作为参数

C语言不支持数组整体作为参数,数组作为参数时可以通过指针实现,若直接将数组设置为参数则编译器会自动转换为指针,比如printf终端输出函数的第一个参数为字符串指针,但是可以直接使用字符串赋值,编译器会自动转换为指针。

#include <stdio.h>
void output(const char * arg)    //常量指针参数,即可赋值为变量地址也可赋值为常量地址
{
    printf("%s\n", arg);
}
int main()
{
    char name[100] = "阿狸";
    output(name);              //数组参数自动转换为指针,等于 output(&name[0]);
    
    return 0;
}

也可以定义为如下代码形式:

void output(const char arg[])    //编译器自动转换为指针参数,变量数组转换为变量指针,常量数组转换为常量指针
{
    printf("%s\n", arg);
}

上述代码中,函数定义的数组参数并不等同于本函数的局部数组成员,而是局部指针成员,这一点新手很容易误解,修改数组参数时会通过指针修改指向的数据,比如下面的代码:

#include <stdio.h>
void f1(char arg[])
{
    arg[0] = 0;
}
int main()
{
    char name[100] = "阿狸";
    f1(name);
    
    printf("%s\n", name);    //输出换行,字符串的首元素被f1函数修改为0,等同于字符串有效字符为空
    
    return 0;
}

二维数组参数

二维数组作为参数时会转换为双重指针。

#include <stdio.h>
void output(char arg[][100])    //二维数组参数需要指定内部一维数组的长度
{
	printf("%s\n%s\n", arg[0], arg[1]);
}
int main()
{
	char fox[2][100] = {"阿狸", "桃子"};
	output(fox);
	
	return 0;
}

编译器会转换为类似如下的代码:

#include <stdio.h>
void output(char ** arg)    //双重指针参数
{
	printf("%s\n%s\n", arg[0], arg[1]);
}
int main()
{
	char ali[100] = "阿狸";
	char taozi[100] = "桃子";
	char * fox[2] = {&ali[0], &taozi[0]};    //定义指针数组
	output(&fox[0]);                         //指针数组本身转换为指针传参,相当于双重指针
	
	return 0;
}

二维数组传参时,编译器首先将其内部的一维数组转换为指针,此时二维数组变成元素为指针的一维数组,之后继续转换为指针进行传参,最终形成双重指针参数。

双重指针可以像二维数组一样使用,通过两个下标调用数据。

#include <stdio.h>
void output(int ** arg)
{
	printf("%d\n%d\n", arg[0][0], arg[1][0]);    //输出0、5
}
int main()
{
	int a[5] = {0,1,2,3,4};
	int b[5] = {5,6,7,8,9};
	int * p1[2] = {&a[0], &b[0]};
	output(&p1[0]);
	
	return 0;
}

数组作为返回值

C语言不支持数组整体作为返回值,若需返回数组可以使用如下方式:

1.使用全局数组进行通信,无需返回。

2.接任者通过指针参数使用调用者内部定义的数组,无需返回。

3.接任者通过指针参数修改调用者内部定义的数组,实现返回。

方式3示例:

#include <stdio.h>
void f1(int * arg)
{
    int a[5] = {1,2,3,4,5};
    
    /* 返回数组a */
    for(int i = 0; i < 5; i++)
    {
        arg[i] = a[i];
    }
}
int main()
{
    int b[5];
    f1(b);
    
    /* 输出返回的数组 */
    for(int i = 0; i < 5; i++)
    {
        printf("%d\n", b[i]);
    }
    
    return 0;
}

【结构体作为参数、返回值】

结构体在功能上是异型数组,在语意上是用户自定义类型的单个数据,全局声明的结构体可以作为参数和返回值,此时函数会保证结构体的值能够完全传递。

结构体作为参数

结构体作为参数时编译器会将其所有成员作为参数,但并非每个成员都占用一个寄存器,长度小的成员可能会进行合并,之后将合并数据使用一个寄存器传参,参数过多时剩余成员使用栈传参。

#include <stdio.h>
struct k
{
    int a;
    char b;
    float c;
};
void f1(struct k arg)
{
    printf("%d\n%c\n%f\n", arg.a, arg.b, arg.c);
}
int main()
{
    struct k k1 = {9, 'a', 3.14};
    f1(k1);
    
    return 0;
}

编译后的代码功能相当于如下C代码:

#include <stdio.h>
struct k
{
    int a;
    char b;
    float c;
};
void f1(int arg1, char arg2, float arg3)
{
    printf("%d\n%c\n%f\n", arg1, arg2, arg3);
}
int main()
{
    struct k k1 = {9, 'a', 3.14};
    f1(k1.a, k1.b, k1.c);
    
    return 0;
}

使用结构体作为参数时,若结构体长度较大,可以将参数定义为结构体指针,这样只需要传递一个指针参数即可,执行速度更快,但前提是接任者不会修改此结构体,或者修改后不会出错。

#include <stdio.h>
struct zoo
{
    char name[100];
    int age;
};
void output(const struct zoo * const p1)
{
    printf("姓名:%s\n年龄:%d岁\n", p1->name, p1->age);
}
int main()
{
    struct zoo ali = {"阿狸", 8};
    output(&ali);
    
    return 0;
}

结构体作为返回值

在x86-64 Linux程序中,整数返回值使用rax、rdx寄存器保存,浮点数返回值使用xmm0、xmm1寄存器保存。

若结构体成员总长度不超过如上寄存器容量,则编译器将结构体成员直接使用寄存器返回、或合并后使用寄存器返回。

若结构体成员总长度超过如上寄存器容量,则通过指针返回,返回原理与之前介绍的返回数组方式相同。

#include <stdio.h>
struct zoo
{
    char name[100];
    int age;
};
struct zoo f1()
{
    struct zoo ali = {"阿狸", 8};
    return ali;
}
int main()
{
    struct zoo fox = f1();
    printf("姓名:%s\n年龄:%d岁\n", fox.name, fox.age);
    
    return 0;
}

编译器会转换为类似如下的代码:

#include <stdio.h>
struct zoo
{
    char name[100];
    int age;
};
void f1(struct zoo * arg)
{
    struct zoo ali = {"阿狸", 8};
    
    arg->name[0] = ali.name[0];    //"阿狸"UTF8编码,1个中文字符占3个字节,末尾包含一个空字符
    arg->name[1] = ali.name[1];
    arg->name[2] = ali.name[2];
    arg->name[3] = ali.name[3];
    arg->name[4] = ali.name[4];
    arg->name[5] = ali.name[5];
    arg->name[6] = ali.name[6];
    arg->age = ali.age;
};
int main()
{
    struct zoo fox;
    f1(&fox);
    printf("姓名:%s\n年龄:%d岁\n", fox.name, fox.age);
    
    return 0;
}

若需要返回一个长度很大的结构体,使用编译器的返回方式显然不合适,此时可以在调用者内部定义一个结构体,之后使用指针参数将其提供给接任者使用,接任者无需返回此结构体。

#include <stdio.h>
#include <string.h>
struct zoo
{
    char name[100];
    int age;
};
void f1(struct zoo * const arg)
{
    strcpy(arg->name, "阿狸");    //为arg->name赋值
    arg->age = 8;
};
int main()
{
    struct zoo fox;
    f1(&fox);
    printf("姓名:%s\n年龄:%d岁\n", fox.name, fox.age);
    
    return 0;
}

【函数可变参数】

某些情况下定义函数时无法确定参数的数量,而是在执行时确定,C语言支持定义可变参数函数,在函数定义期间不指定所有的参数,而是在执行期间临时确定参数的数量,比如终端输入输出函数就使用了可变参数。

void f(char *s, ...)    //不固定的参数使用 ... 符号代替,表示这是一个可变参数函数

定义可变参数函数时,至少需要有一个可以确定的参数,用于说明其他可变参数的属性信息,比如可变参数的数量、类型。

执行函数时,可变参数的传递方式与普通参数相同,也是优先使用寄存器传参、之后使用栈传参,可变参数没有名称,无法使用数据名调用,当然你可以在函数中内嵌汇编代码直接使用寄存器调用、或者使用指针调用栈中的参数,但是这样做太繁琐,而且不同的编译器、不同的处理器传参汇编代码不同,为此C语言标准函数库提供了stdarg.h文件,其提供了统一的调用可变参数的方式,具体调用原理我们无需深究,这是编译器系统的工作。

stdarg.h文件内定义了一些数据和宏代码用于调用可变参数,常用代码如下:

1.va_list,表示一个数据类型,用于绑定可变参数的类型。

2.va_start(v, l),执行功能初始化工作,v指定一个va_list类型变量,l指定函数第一个有名称的参数。

3.va_arg(v, T),返回一个未使用的可变参数,v指定初始化后的va_list变量,T指定可变参数的类型。

4.va_end(v),使用完毕后执行清理工作,v指定初始化后的va_list变量。

#include <stdio.h>
#include <stdarg.h>
void f1(unsigned int format, ...)
{
	va_list v1;              //定义va_list变量v1,v1由编译器自动管理
	va_start(v1, format);    //功能初始化
	
	/* 循环输出可变参数 */
	for(int i = 0; i < format; i++)
	{
		printf("可变参数%d\n", va_arg(v1, int));    //va_arg返回一个未使用的可变参数,这里约定可变参数都为int类型,所以无需做复杂判断
	}
	
	va_end(v1);    //使用完毕之后执行清理工作
}
int main()
{
	int a, b, c;
	scanf("%d%d%d", &a, &b, &c);    //输入a、b、c的值
	
	f1(3, a, b, c);    //这里约定可变参数的类型都为int,无需在第一个有名称参数中指定可变参数的类型,只需指定可变参数的数量
	
	return 0;
}