新书推荐:9.3 函数调用

本节必须掌握的知识点:

 示例三十五值调用

示例三十六引用调用

 参数传递与const修饰符    

 多维数组的传参

  随机数函数

在很多程序设计语言中,调用函数的两种途径是值调用和引用调用。当函数以值调用的方式传递时,主程序就会生成参数值的一个副本,并把它传递给被调用的函数。对这个副本进行的修改并不会影响主程序中的原始变量值。当参数按照引用调用的方式传递时,主程序实际上会允许被调用函数去修改原始变量值。

如果被调用函数不需要修改主程序的原始变量值,就应该使用值调用的方式。这可以防止意外的负面影响,这些意外的负面影响会大大妨碍我们开发正确可靠的软件系统。只有在需要修改原始变量值时,才使用引用调用。

在C语言中,所有调用都是值调用。但可以使用地址运算符和间接运算符来模拟引用调用。数组是按照自动引用的方式来传递的。接下来我们将分别分析值调用和引用调用的实现过程和区别。

9.3.1 示例三十五值调用

在上面的代码解析中涉及了几个词,调用、传参、返回值。这几个词很好的诠释了函数执行的整个过程。接下来我们将详细介绍这几个步骤。

函数的值调用

在C语言中,使用函数就是函数调用,在调用函数时,必须知道函数的返回值类型、函数名、参数个数及参数类型。

接下来让我们一起分析函数调用过程的实现。示例代码三十五输入两个数,并判断这两个数的大小。

示例代码三十五

在VS中新建项目9-3-1.c:

/*

   输入两个数,并判断这两个数的大小

*/

#include <stdio.h>

#include <stdlib.h>

int cmp(int x, int y)

{

    if (x < y)

        return x;

    else

        return y;

}

int main(void) {

    int x, y;

    printf("请输入两个整数比较哪个数小\n");

    printf("第一个数:");

    scanf_s("%d", &x);

    printf("第二个数:");

    scanf_s("%d", &y);

    printf("比较小的数是:%d\n", cmp(x, y));

    system("pause");

    return 0;

}

 ●输出结果:

请输入两个整数比较哪个数小

第一个数:1

第二个数:2

比较小的数是:1

请按任意键继续. . .

代码分析:

不管运行哪个C语言程序,都会从入口函数(main函数)开始执行。main函数首先定义了int类型的两个局部变量x和y。然后调用printf函数和scanf_s函数提示用于键盘输入两个整数,分别存入变量x和变量y中。

最后通过调用printf函数输出较小的值,printf函数的第二个参数为cmp函数调用。

printf("比较小的数是:%d\n",cmp(x,y));执行此行代码时,编译器先去寻找cmp(x,y);

我们点击VS菜单栏“调试”->“窗口”->“调用堆栈”,在调用堆栈窗口观察函数是怎样调用的。如图9-11所示:

图9-11 调用堆栈

首先在int x,y;处下断点,按F10单步执行,分别输入两个整数1和2。执行到printf(“比较小的数是:%d\n”,cmp(x,y));时,分别把x 、y的值1、2传递给cmp函数。按F11执行cmp函数。如图9-12所示,调用堆栈窗口中出现了cmp(int x,int y),说明再我们按F11的那一刻,程序记调用了cmp()函数,并且传递了参数,实现了这两步就可以动手切换到反汇编窗口一探究竟。

图9-12 执行cmp函数

   接着按F10会执行cmp函数内的程序块,如果x<y则返回x,否则返回y。我们输入的是x = 1、y = 2;所以执行else语句,返回x的值1。

以上程序的具体调用关系和返回值图解,如图9-13所示:

                                                    图9-13 函数调用关系

汇编解析

●汇编代码

;C标准库头文件和导入库

include vcIO.inc

compare proto x:sdword,y:sdword;cmp为汇编指令关键字,改用compare

.data

x sdword  ?

y sdword  ?

.const    

szMsg1 db "请输入两个整数比较哪个数小",0dh,0ah,0

szMsg2 db "第一个数:",0

szMsg3 db "%d",0

szMsg4 db "第二个数:",0

szMsg5 db "比较小的数是:%d",0dh,0ah,0

.code     

main proc

       invoke printf,offset szMsg1

       invoke printf,offset szMsg2

       lea ebx,x

       push ebx

       lea ecx,offset szMsg3

       push ecx

       call scanf

       invoke printf,offset szMsg4

       lea ebx,y

       push ebx

       lea ecx,offset szMsg3

       push ecx

       call scanf

       ;调用cmp函数比较大小

       push y    ;先将变量y的值入栈

       push x    ;再将变量x的值入栈

       call compare

       invoke printf,offset szMsg5,eax  

       ;     

       invoke _getch

       ret                       

main endp

;--------------------------

;函数定义:比较两个整数的大小

;入口参数:堆栈传参x,y

;出口参数:寄存器传参eax

compare proc x:sdword,y:sdword

       mov eax,x

       .if eax < y

              mov eax,x

       .else

              mov eax,y

       .endif

       ret

compare endp

;-------------------------

end main       ;程序入口地址

编译时出现错误,错误提示信息如下:

Assembling: 9-3-1.asm

9-3-1.asm(49) : error A2005: symbol redefinition : x

9-3-1.asm(49) : error A2111: conflicting parameter definition

NMAKE : fatal error U1077: “d:\masm32\bin\ml.EXE”: 返回代码“0x1”

Stop.

提示符号x重定义。上述汇编代码中将变量x和y定义在.data数据段,为全局变量,函数compare中将x和y符号定义为形参,所以出现符号重定义错误。是否将函数compare的形参x和y改成a和b就可以了呢?

D:\code\asm_to_c\MyProjectOne\chapter9\9-3>nmake /a

Microsoft (R) 程序维护实用工具 14.16.27045.0 版

版权所有 (C) Microsoft Corporation。  保留所有权利。

        ml -c -coff 9-3-1.asm

Microsoft (R) Macro Assembler Version 6.14.8444

Copyright (C) Microsoft Corp 1981-1997.  All rights reserved.

 Assembling: 9-3-1.asm

        Link /subsystem:Console 9-3-1.obj

Microsoft (R) Incremental Linker Version 14.16.27045.0

Copyright (C) Microsoft Corporation.  All rights reserved.

编译成功。

●输出结果:

请输入两个整数比较哪个数小

第一个数:1

第二个数:2

比较小的数是:1

 

结论

上述汇编代码可以明确:汇编语言汇编器对符号的解析与C语言编译器对符号的解析有着明显的不同。

在汇编语言中,汇编器不允许在源代码中使用相同的符号,如变量名、函数名、代码段的地址标号。这些标号都用来表示地址,地址必定是唯一的,不能有冲突。

而在C语言中,变量名是可以出现重定义的,但是函数名和代码段的地址标号不允许出现重定义。之所以变量名可以重复,是因为C语言编译器编译一个变量时,会屏蔽作用域之外相同的变量名符号,C语言将变量的作用域分为代码块、函数和文件三个作用范围,在同一个作用域内不允许出现重复的变量名。我们将在9.4节详细讲解作用域。

●cmp函数反汇编代码:

int cmp(int x, int y)

{

建立堆栈框架

00291080  push        ebp 

00291081  mov         ebp,esp 

    if (x < y)

00291083  mov         eax,dword ptr [x]  ;参数x存入eax

00291086  cmp         eax,dword ptr [y]  ;比较x和y的大小

00291089  jge         cmp+12h (0291092h) ;如果x>=y,跳转到地址0291092h

        return x;

0029108B  mov         eax,dword ptr [x]   ;如果x<y,返回较小值x

        return x;

0029108E  jmp         cmp+15h (0291095h

00291090  jmp         cmp+15h (0291095h) 

    else

        return y;

00291092  mov         eax,dword ptr [y]  ;返回较小值y

}

00291095  pop         ebp  ;释放堆栈框架

00291096  ret  ;跳转到返回地址

●参数传递顺序:

     仔细观察示例代码三十五及图9-13时,注意参数传递的顺序,示例代码三十五调用函数cmp的第一个实参是x,第二个实参是y,分别对应cmp函数定义的x形参和y形参。实际上函数的调用过程是一个值传参的调用过程,cmp(x,y);所表示的意思是,把x、y的值传递给函数,并不是把x、y这两个变量传进去。为什么这样说?

       查看以下cmp函数调用的反汇编代码就知道了。在printf("比较小的数是:%d\n", cmp(x, y));语句下断点,切换到反汇编窗口:

    printf("比较小的数是:%d\n", cmp(x, y));

002910F9  mov         edx,dword ptr [y]  ;将变量y的值存入edx寄存器

    printf("比较小的数是:%d\n", cmp(x, y));

002910FC  push        edx              ;edx压入堆栈

002910FD  mov         eax,dword ptr [x]  ;将变量x的值存入eax寄存器

00291100  push        eax               ;eax压入堆栈

00291101  call        cmp (0291080h)     ;调用函数cmp

00291106  add         esp,8 

       从上面的反汇编代码中,我们可以清晰的看到,通过堆栈传递给cmp函数的参数是变量x和y的值,而不是变量x和变量y本身。

这里可能会有一个疑问,是不是在函数定义时,形参名必须与传递的实参名保持一致呢?恰恰相反,虽然形参名和变量名相同并不会影响程序的执行,但是为了防止出现混淆,增强代码的可读性,我们应该尽量将形参名和变量名定义为两个不同的名称。

实验七十:函数形参符号的命名

在VS中新建项目9-3-2.c

/*

   函数形参符号的命名

*/

#include <stdio.h>

#include <stdlib.h>

int cmp(int a, int b)

{

    if (a < b)

        return a;

    else

        return b;

}

int main(void) {

    int x, y;

    printf("请输入两个整数比较哪个数小\n");

    printf("第一个数:");

    scanf_s("%d", &x);

    printf("第二个数:");

    scanf_s("%d", &y);

    printf("比较小的数是:%d\n", cmp(x, y));

    system("pause");

    return 0;

}

●输出结果:

请输入两个整数比较哪个数小

第一个数:1

第二个数:2

比较小的数是:1

请按任意键继续. . .     

       ●代码解析:

在进入函数cmp(x,y)时,调用int cmp(int a,int b);时传递的实参是x、y的值,对应形参int a和int b,即int a = x;int b = y;。

printf("比较小的数是:%d\n", cmp(x, y));处下断点,查看main函数反汇编代码:

    printf("比较小的数是:%d\n", cmp(x, y));

00FD10F9  mov         edx,dword ptr [y] 

00FD10FC  push        edx 

00FD10FD  mov         eax,dword ptr [x] 

00FD1100  push        eax 

00FD1101  call        cmp (0FD1080h) 

00FD1106  add         esp,8 

同时在监视1窗口输入&x和&y,如图9-14所示:

图9-14 查看局部变量x和y的值

在int cmp(int a, int b)处下断点,查看cmp函数反汇编代码:

00FD1080  push        ebp 

00FD1081  mov         ebp,esp 

    if (a < b)

00FD1083  mov         eax,dword ptr [a] 

00FD1086  cmp         eax,dword ptr [b] 

00FD1089  jge         cmp+12h (0FD1092h) 

        return a;

00FD108B  mov         eax,dword ptr [a] 

00FD108E  jmp         cmp+15h (0FD1095h) 

00FD1090  jmp         cmp+15h (0FD1095h) 

    else

        return b;

00FD1092  mov         eax,dword ptr [b] 

}

00FD1095  pop         ebp 

00FD1096  ret 

同时在监视1窗口输入&a和&b,如图9-15所示:

图9-15 查看形参a和b的值

      

在内存窗口输入形参a的地址,如图9-16所示:

图9-16 查看堆栈内存空间

       如图9-16所示,在堆栈中从高地址到低地址分配内存空间,先分配main函数的局部变量x和y的空间,并通过scanf_s接收键盘输入1和2,分别存入局部变量x和y中,即图中右侧堆栈内存储的值为x=1,y=2。

       当调用函数cmp时,通过两个push语句,将x和y的值压入堆栈,传递给形参a和b,即图中左侧的堆栈空间a=1,b=2。

结论:形参a的值=局部变量x的值,形参b的值=局部变量y的值,分别位于栈内不同的地址空间内。调用cmp函数传递的是值,我们称之为值传参。

实验七十一:不使用函数的返回值

在VS中新建项目9-3-3.c

/*

   不使用函数的返回值

*/

#include <stdio.h>

#include <stdlib.h>

int add(int a, int b)

{//函数定义的大括号不可以省略

    return a + b;

}

int main(void) {

    add(1, 2);  //并没有输出add函数的返回值,所以可以忽略返回值

    system("pause");

    return 0;

}

输出结果:

请按任意键继续. . .

代码分析:

上述示例代码与之前的函数调用有所不同,调用add函数时,并未取函数的返回值。

 

总结

学习C语言,一定要多敲代码,搞不明白的多调试,调试几遍有可能解开心里的疑惑。

函数说白了就是写一块代码,这一块代码可以实现你想要的功能,把这一块代码封装起来,可以实现调用,没有错误且成功实现想要的功能就算成功的封装了函数。

本书看到这里,且前面的知识点都搞明白了,就可以自己尝试写各种代码了。

9.3.2 示例三十六引用调用

函数的引用调用

虽然在C语言中,所有函数调用都是值调用,但是如果参数的值传递的值是一个地址得副本,我们就将其称为引用调用。

在C语言中,变量名前添加“&”地址符,表示变量的地址。数组名表示数组的起始地址,函数名表示函数的起始地址,二者都不需要再添加地址符。如果要取该地址的存储的变量值,需要使用解运算符“*”。

举例

       定义一个int类型的变量a:int a = 1;

       定义一个int*类型的变量ptr:int* ptr;      //该指针类型的地址用于存储int类型的值。

       ptr = &a;       //ptr=变量a的地址

       a = *ptr;         //a=ptr地址处存储的值。

接下来我们举例说明。

示例代码三十六

       在VS中新建项目9-3-4.c:

       /*

   交换两个变量的值

*/

#include <stdio.h>

#include <stdlib.h>

void tow_swap(int* a, int* b)

{

    int temp = *a;

    *a = *b;

    *b = temp;

    //return;//没有返回值的时候可以省略

}

int main(void) {

    int a = 1;

    int b = 2;

    printf("交换前:a = %d,b = %d\n",a,b);

    tow_swap(&a, &b);  //交换变量a和b的值

    printf("交换后:a = %d,b = %d\n", a, b);

    system("pause");

    return 0;

}

●输出结果:

交换前:a = 1,b = 2

交换后:a = 2,b = 1

请按任意键继续. . .

代码分析:

上述代码定义了一个函数two_swap,交换两个变量a和b的值。变量a和变量b存在于main函数的堆栈内,根据示例三十五的经验,如果调用函数two_swap传递的参数为变量a和变量b的副本,即值调用,那么在函数two_swap中只会修改变量a和变量b在函数two_swap栈内的副本,并不会影响main函数中变量a和变量b的值。这与我们的意图相悖。所以我们改用引用调用,即将变量a和变量b的地址作为参数传递给函数two_swap,在函数two_swap内,我们可以得到变量a和变量b地址的副本,直接修改变量a和变量b地址处的值就可以了。

引用调用传参时,实参为变量a和变量b的地址,需要在变量名前添加取地址符‘&’,应该变量a和变量b的数据类型为int类型,所以&a和&b的数据类型为int*类型,表示在该地址处存储int类型的整数值。

在函数two_swap的定义中,将形参a和形参b的数据类型定义为int*类型,与实参类型相匹配。在函数two_swap函数内,定义一个int类型的临时变量temp,作为交换变量a和变量b值的中介。

【注意】此时,函数two_swap的形参a和形参b都表示一个地址,形参a等于实参&a,形参b等于实参&b,如果要取形参a地址处的值,需要使用解运算符‘*’,*a表示取a地址处的值,*b表示取b地址处的值。

由于函数two_swap的返回值类型为void,所以最后的return语句可有可无。

汇编解析

●汇编代码

;C标准库头文件和导入库

include vcIO.inc

tow_swap proto ptra:ptr sdword ,ptrb:ptr sdword

;定义全局变量

;.data

;a sdword  1

;b sdword  2

.const 

szMsg1 db "交换前:a = %d,b = %d",0dh,0ah,0

szMsg2 db "交换后:a = %d,b = %d",0dh,0ah,0

.code  

main proc

    ;定义局部变量

    local a:sdword ;定义局部变量a

    local b:sdword ;定义局部变量b

    mov sdword ptr a,1

    mov sdword ptr b,2 

    invoke printf,offset szMsg1,a,b

    invoke tow_swap,addr a,addr b

    invoke printf,offset szMsg2,a,b

    ;  

    invoke _getch  

    ret            

main endp

;--------------------------

;函数定义:交换两个变量的值

;入口参数:堆栈传递参数为-变量a和变量b的地址

;出口参数:无

tow_swap proc uses eax ebx ecx edx,;uses操作符保护寄存器入栈

    ptra:ptr sdword,;形参ptra为sdword ptr指针类型

    ptrb:ptr sdword

    mov ebx,[ptra]  ;取ptra地址处的值(变量a的地址)送入ebx

    mov eax,[ebx]   ;将变量a地址处的值送入eax

    mov ecx,[ptrb]  ;取ptrb地址处的值(变量b的地址)送入ecx

    mov edx,[ecx]   ;将变量b地址处的值送入edx 

    mov [ebx],edx   ;将变量b地址处的值存入变量a的地址处

    mov [ecx],eax   ;将变量a地址处的值存入变量b的地址处

    ret

tow_swap endp

;-------------------------

end main    ;程序入口地址

●输出结果:

交换前:a = 1,b = 2

交换后:a = 2,b = 1

上述的汇编代码非常有意义:

1.汇编代码中将变量a和变量b分别定义为全局变量和局部变量,都是可以的。

2.tow_swap函数的声明和定义均采用高级汇编的写法,与C语言非常接近。变量a和b的数据类型为sdword类型,传递的实参为变量a和b的地址(addr a和addr b)。tow_swap函数的声明和定义中形参的数据类型为ptr sdword指针类型,其实就是sdword型变量的地址的意思,等同于C语言中的int*指针类型,与堆栈传递的实参一致。

3.在tow_swap函数内,先取出堆栈中变量a和变量b的地址,然后再从该地址处取值进行交换,其实就相当于C语言的解运算符’*’。

接下来我们使用DtDebug调试器跟踪一下程序的运行过程,以此为证。

第一步:打开DtDebug调试器,拖入9-3-2.exe,按下Ctrl+F9,进入main函数程序入口地址,如图9-17所示。

图9-17 9-3-2.exe的main函数

第二步:按F8单步执行,执行到调用tow_swap函数时,按F7单步步入,进入tow_swap函数,如图9-18所示:

图9-18 tow_swap函数

第三步:按F7单步执行tow_swap函数,注意观察堆栈内的值,如图9-19所示。

图9-19 9-3-2.exe运行时栈

●反汇编代码

调用tow_swap函数的反汇编代码:

    tow_swap(&a, &b);  //交换变量a和b的值

00F01073  lea         edx,[b]  ;取变量b的地址

    tow_swap(&a, &b);  //交换变量a和b的值

00F01076  push        edx     ;将变量b的地址入栈

00F01077  lea         eax,[a]  ;取变量a的地址

00F0107A  push        eax     ;将变量a的地址入栈

00F0107B  call        tow_swap (0F01100h)  ;调用tow_swap函数

00F01080  add         esp,8    ;恢复堆栈平衡

tow_swap函数的反汇编代码:

void tow_swap(int* a, int* b)

{

00F01100  push        ebp 

00F01101  mov         ebp,esp 

00F01103  push        ecx 

    int temp = *a;

00F01104  mov         eax,dword ptr [a]  ;取形参a地址处存储的值(变量a的地址)

00F01107  mov         ecx,dword ptr [eax];将变量a地址处存储的值送入ecx

00F01109  mov         dword ptr [temp],ecx ;将ecx存入临时变量temp地址处

    *a = *b;

00F0110C  mov         edx,dword ptr [a]   ;取形参a地址处存储的值(变量a的地址)

00F0110F  mov         eax,dword ptr [b]   ;取形参b地址处存储的值(变量b的地址)

00F01112  mov         ecx,dword ptr [eax] ;ecx=变量b地址处存储的值2

00F01114  mov         dword ptr [edx],ecx ;将2存入变量a的地址处

    *b = temp;

00F01116  mov         edx,dword ptr [b]   ;取形参b地址处存储的值(变量b的地址)

00F01119  mov         eax,dword ptr [temp]  ;将临时变量temp的值1送入eax

00F0111C  mov         dword ptr [edx],eax  ;将1存入变量b的地址处

    //return;//没有返回值的时候可以省略

}

00F0111E  mov         esp,ebp 

00F01120  pop         ebp 

00F01121  ret 

C语言的调用约定

所谓调用约定,就是调用方与被调用方做一个约定:

1.约定堆栈传参的顺序,在汇编语言中使用invoke tow_swap,addr a,addr b语句调用函数时,或者在C语言中tow_swap(&a, &b);调用函数时,实参从右往左的顺序入栈还是从左往右的顺序入栈;当然在call指令语句中绝对不会存在这样的问题,因为在call之前的push语句已经逐个把参数压入堆栈了(参考反汇编语句)。

2.由谁负责堆栈平衡也需要双方约定一下,调用方与被调用方既不能同时负责堆栈平衡,也不能同时不负责堆栈平衡。必须约定一下由谁来负责堆栈平衡。假设函数都是自定义的,那么也不可能会存在这个问题,程序员自己决定就好了,喜欢让谁负责堆栈平衡就负责堆栈平衡。但是,如果函数是第三方提供的(例如标准库函数),那么就必须要约定一下了。

3.还有一点需要注意,如果编译器第一次编译一个外部的函数名符号时,需要空下函数的地址。在链接器链接时,需要在第三方库或者模块中查找函数名。这就涉及到函数名的命名规范,必须按照同一的规范命名函数名。在汇编语言中,汇编器编译后的函数名为tow_swap@8,@8表示有2个参数,占用8个字节的堆栈空间。在C语言中,编译后的函数名为_tow_swap@8,比汇编函数名多了一个前缀‘_’。

上述反汇编代码与汇编代码实现的过程基本类似,请读者仔细阅读代码注释。需要特别说明的是,在汇编代码中堆栈传参的顺序是从右往左,由tow_swap函数负责堆栈平衡,调试器中tow_swap函数的最后一条语句是“ret 8”。

而在C语言的反汇编代码中,tow_swap函数的最后一条语句是“ret”,并没有堆栈平衡。由main函数负责堆栈平衡。

00F0107B  call        tow_swap (0F01100h)  ;调用tow_swap函数

00F01080  add         esp,8    ;恢复堆栈平衡

这是因为,C语言遵循cdecl调用约定,而汇编语言中,遵循stdcall调用约定,传参的顺序都是从右往左,但是堆栈平衡的方式各不相同。C语言由主调函数负责堆栈平衡,汇编语言由被调用函数负责堆栈平衡。

接下来我们再举一个常用的引用调用的例子。

实验七十二:打印输出数组元素

在VS中新建项目9-3-5.c

/*

   打印输出数组元素

*/

#include <stdio.h>

#include <stdlib.h>

void print(int a[], int n)

{

    for (int i = 0;i < n;i++)

    {

        printf("a[%d] = %d ",i,a[i]);

    }

    //return;//没有返回值的时候可以省略

}

int main(void) {

    int a[] ={1,2,3,4,5,6};

    print(a, 6);

    putchar('\n');

    system("pause");

    return 0;

}

●输出结果:

       a[0] = 1 a[1] = 2 a[2] = 3 a[3] = 4 a[4] = 5 a[5] = 6

请按任意键继续. . .

●代码分析:

实验七十二定义了一个print函数,输出数组元素的值。print函数的实参为数组名(数组的起始地址)和数组元素的个数。print函数的形参为int a[]和int n。int n表示int类型的整数n,表示数组元素的个数,很好理解。int a[]表示int类型的数组a,这里的a为数组名。函数内使用一个for循环语句,循环变量用于表示数组下标0~5。

9.3.3 参数传递与const修饰符

       在上一小节中,我们知道,函数调用分为值调用和引用调用两种方式。使用值调用参数传递的是值的副本,在被调用函数内无法影响调用函数原有的值,更为安全可靠。如果采用引用调试,被调用函数可以修改调用函数原有的值,这样就可能会造成安全隐患。为此,C语言编译器增加了一个const修饰符,在变量名或形参前添加const修饰符表示在该变量或形参的作用域范围内不可以修改变量或形参的值。

实验七十三:const修饰符

没有添加const限定词

在VS中新建项目9-3-6.c

/*

   打印输出数组元素

*/

#include <stdio.h>

#include <stdlib.h>

void print(int a[], int n)

{

    a[0] = 9;//修改数组元素的值

    for (int i = 0; i < n; i++)

    {

        printf("a[%d] = %d ", i, a[i]);

    }

    //return;//没有返回值的时候可以省略

}

int main(void) {

    int a[] = { 1,2,3,4,5,6 };

    int b = 0;

    b = 1;//修改变量b的值

    print(a, 6);

    putchar('\n');

    system("pause");

    return 0;

}

●输出结果:

a[0] = 9 a[1] = 2 a[2] = 3 a[3] = 4 a[4] = 5 a[5] = 6

请按任意键继续. . .

变量添加const限定词

在VS中新建项目9-3-7.c

       /*

   打印输出数组元素-变量添加const限定词

*/

#include <stdio.h>

#include <stdlib.h>

void print(int a[], int n)

{

    a[0] = 9;//修改数组元素的值

    for (int i = 0; i < n; i++)

    {

        printf("a[%d] = %d ", i, a[i]);

    }

    //return;//没有返回值的时候可以省略

}

int main(void) {

    int a[] = { 1,2,3,4,5,6 };

    //int b = 0;

    const int b = 0;//变量b作为一个常量,不允许修改

    b = 1;//修改变量b的值

    print(a, 6);

    putchar('\n');

    system("pause");

    return 0;

}

编译时报错:9-3-7.c(26): error C2166: 左值指定 const 对象-不允许修改变量的值。

形参添加const限定词一

在VS中新建项目9-3-8.c

/*

   打印输出数组元素-形参添加const限定词一

*/

#include <stdio.h>

#include <stdlib.h>

void print(const int a[], int n)//数组a的数组元素不可以被修改

{

    a[0] = 9;//修改数组元素的值

    for (int i = 0; i < n; i++)

    {

        printf("a[%d] = %d ", i, a[i]);

    }

    //return;//没有返回值的时候可以省略

}

int main(void) {

    int a[] = { 1,2,3,4,5,6 };

    int b = 0;

    //const int b = 0;//变量b作为一个常量,不允许修改

    b = 1;//修改变量b的值

    print(a, 6);

    putchar('\n');

    system("pause");

    return 0;

}

编译时报错:9-3-8.c(14): error C2166: 左值指定 const 对象,数组元素的值不可以修改。

形参添加const限定词二

在VS中新建项目9-3-9.c

       /*

   打印输出数组元素-形参添加const限定词二

*/

#include <stdio.h>

#include <stdlib.h>

void print(int const a[], int n)//数组a元素的值不可以被修改

{

    a[0] = 9;//修改数组元素的值

    for (int i = 0; i < n; i++)

    {

        printf("a[%d] = %d ", i, a[i]);

    }

    //return;//没有返回值的时候可以省略

}

int main(void) {

    int a[] = { 1,2,3,4,5,6 };

    int b = 0;

    //const int b = 0;//变量b作为一个常量,不允许修改

    b = 1;//修改变量b的值

    print(a, 6);

    putchar('\n');

    system("pause");

    return 0;

}

编译时报错:9-3-9.c(14): error C2166: 左值指定 const 对象。

 

结论

       1.在没有添加const修饰词限定的时候,定义的变量或形参的值是可以被修改的。

       2.变量添加const修饰词后,变量的值不可以被修改。

       3.地址类型的形参前添加const修饰词后,该形参地址处存储的值不可以被修改。

       4.形参传递数组地址时,int const a[]修饰效果和const int a[]修饰的效果完全一致,不可以修改数组元素的值。

9.3.4 多维数组的传参

接收多维数组的函数,可以省略相当于开头下标的n维的元素个数。但是,(n-1)维之下的元素个数必须是常量。

举例

void func1(int v[], int n) ;             //元素类型为int类型、元素个数随意(n)。

void func1(int v[][3], int n) ;        //元素类型为int[3] 类型、元素个数随意(n)。

void func1(int v[][2][3], int n) ;   //元素类型为int[2][3] 类型、元素个数随意(n)。

所接收的数组的元素类型必须是固定的,但元素的个数是自由的。

实验七十四:多维数组传参

在VS中新建项目9-3-10.c 

/*

   为n行3列的二维数组的所有构成元素赋上同样的值

*/

#include <stdio.h>

#include <stdlib.h>

/*---将v赋值给元素类型为int[3]、元素个数为n的数组m的所有构成元素---*/

void fill(int m[][3], int n, int v)

{

    int i, j;

    for (i = 0; i < n; i++)

    {

        for (j = 0; j < 3; j++)

        {

            m[i][j] = v;

        }

    }

}

/*---显示元素类型为int[3]、元素个数为n的数组m的所有构成元素---*/

void mat_print(const int m[][3], int n)

{

    int i, j;

    for (i = 0; i < n; i++)

    {

        for (j = 0; j < 3; j++)

        {

            printf("%4d ", m[i][j]);

        }

        putchar('\n');

    }

}

int main(void) {

    int no;

    int x[2][3] = { 0 };

    int y[4][3] = { 0 };

    printf("赋给所有构成元素的值:");

    scanf_s("%d", &no);

    fill(x, 2, no); //将no赋值给x数组的所有构成元素

    fill(y, 4, no); //将no赋值给y数组的所有构成元素

    printf("----x----\n");     mat_print(x, 2);

    printf("----y----\n");     mat_print(y, 4);

    system("pause");

    return 0;

} 

●输出结果:

赋给所有构成元素的值:2

----x----

   2    2    2

   2    2    2

----y----

   2    2    2

   2    2    2

   2    2    2

   2    2    2

请按任意键继续. . .

       ●代码分析:

fill函数原型:void fill(int m[][3], int n, int v);

       形参int m[][3] 表示一个包含3个数组元素的一维数组组成的二维数组,二维数组元素的个数为n,最后一个形参int v表示填充二维数组的值。

mat_print函数原型:void mat_print(const int m[][3], int n);

       形参const int m[][3]表示表示一个包含3个数组元素的一维数组组成的二维数组,且数组元素的值不可以被修改。第二个形参int n表示数组元素的个数。

9.3.5 随机数函数

       C语言的工具库中有两个非常重要的函数srand和rand。这两个函数常被用来模拟现实世界中的一些棘手问题,我们通过计算机程序模拟实验来帮助我们进行决策。为了保证模拟的正确性,我们需要随机取值,这些随机值之间都具有相同的概率。

       ■rand函数

rand函数能够生成在0和RAND+MAX(在<stdlib.h>头文件中定义的符号常量)之间的整数。

ANSI标准指出,RAND_MAX的值必须不小于32767,即双字节16位整数的最大值。如果rand确实能够随机产生一个整数,那么每次调用rand函数时,在0和RAND_MAX之间的每个数字被选中的几率都是相同的。

rand函数之间产生的值始终在如下范围内:

0≤rand()≤RAND_MAX

我们需要注意:如果两次或者多次执行rand函数的结果都是一样的,会按照相同的顺序产生相同的随机数。实际上,rand函数产生的是伪随机数。重复调用rand函数都会产生一系列相同顺序的数字。

我们可以利用rand函数的这种特性用来调试程序,证明对程序的修改能够正常运行。

如果每次执行,产生一组不同顺序的随机数,我们称为随机化。需要使用另外一个标准库函数srand函数。

srand函数

srand函数需要一个unsigned无符号整型参数(unsigned int 缩写),每次执行程序时使用函数rand去生成一组不同顺序的随机数。

unsigned 类型至少占用2个字节的内存空间,如果是双字节,范围是0~65535的正数,如果是4字节,范围是0~4 294 967 295的正数。

如果我希望无需每次输入一个种子就能够实现随机化,可以使用如下语句:

srand(time(NULL));

这会使计算机自动读取操作系统的时钟来作为种子值。函数time将返回计算机的当前时间,并将这个值转换为无符号整数,作为随机数生成器的种子。函数time使用NULL作为参数。time的函数原型在头文件<time.h>中。

接下来我们举例说明。

实验七十五:使用随机数初始化一个数组

在VS中新建项目9-3-11.c 

/*

   使用随机数初始化一个数组

*/

#include <stdio.h>

#include <stdlib.h>

/*---使用随机数填充数组---*/

void fill(int m[][3], int n)

{

    int i, j;

    for (i = 0; i < n; i++)

    {

        for (j = 0; j < 3; j++)

        {

            int v = 1 + rand() % 100;//生成1~100之间的随之整数值

            m[i][j] = v;

        }

    }

}

/*---显示元素类型为int[3]、元素个数为n的数组m的所有构成元素---*/

void mat_print(const int m[][3], int n)

{

    int i, j;

    for (i = 0; i < n; i++)

    {

        for (j = 0; j < 3; j++)

        {

            printf("%4d ", m[i][j]);

        }

        putchar('\n');

    }

}

int main(void) {

    int x[2][3] = { 0 };

    int y[4][3] = { 0 };

    printf("生成随机整数值填充数组:\n");

   

    fill(x, 2); //填充数组x

    fill(y, 4); //填充数组y

    printf("--------x--------\n");     mat_print(x, 2);

    printf("--------y--------\n");     mat_print(y, 4);

    system("pause");

    return 0;

}

       ●输出结果:

生成随机整数值填充数组:

--------x--------

  42   68   35

   1   70   25

--------y--------

  79   59   63

  65    6   46

  82   28   62

  92   96   43

请按任意键继续. . .

       ●代码分析:

       程序非常简单,只需要对9-3-10.c代码稍微改一下,由用户输入一个值填充数组,变为使用随机函数填充一个1~100之间的随机值,然后调用mat_print函数打印数组。

rand() % 100;的取值范围0~100-1;

1 + rand() % 100;的取值范围1~100;

如果多次运行这个程序,我们会发现,每次生成的随机数和顺序都是一样的,因此我们将其称为伪随机数。如果要生成真正的随机数,还需要调用另一个随机函数srand。

实验七十六:使用随机数种子生成器

在VS中新建项目9-3-12.c 

/*

   使用随机数种子生成器初始化一个数组

*/

#include <stdio.h>

#include <stdlib.h>

#include <time.h>

/*---使用随机数填充数组---*/

void fill(int m[][3], int n)

{

    int i, j;

    for (i = 0; i < n; i++)

    {

        for (j = 0; j < 3; j++)

        {

            int v = 1 + rand() % 100;//生成1~100之间的随之整数值

            m[i][j] = v;

        }

    }

}

/*---显示元素类型为int[3]、元素个数为n的数组m的所有构成元素---*/

void mat_print(const int m[][3], int n)

{

    int i, j;

    for (i = 0; i < n; i++)

    {

        for (j = 0; j < 3; j++)

        {

            printf("%4d ", m[i][j]);

        }

        putchar('\n');

    }

}

int main(void) {

    int x[2][3] = { 0 };

    int y[4][3] = { 0 };

    srand((unsigned int)time(NULL));//随机数种子生成器

    printf("生成随机整数值填充数组:\n");

    fill(x, 2); //填充数组x

    fill(y, 4); //填充数组y

    printf("--------x--------\n");     mat_print(x, 2);

    printf("--------y--------\n");     mat_print(y, 4);

    system("pause");

    return 0;

}

●输出结果:

生成随机整数值填充数组:

--------x--------

  74   58   84

  24   63    8

--------y--------

  42   78   39

  92    1   96

  19   87   60

  75   83   29

请按任意键继续. . .

生成随机整数值填充数组:

--------x--------

  32   21   59

  80   17   54

--------y--------

  98   23   35

  31   39   89

  35    9    2

  48   87    3

请按任意键继续. . .

       ●代码分析:

       多次运行程序,得到的结果每次都是不一样的。说明添加种子生成器之后,我们取到了真正的随机数。

srand((unsigned int)time(NULL));//随机数种子生成器

       随机数的种子值为time函数获取的当前系统时间。Time函数是C语言标准库函数,需要引用time.h头文件。

谨慎

       1.time函数的返回值类型为time_z类型,而srand函数的参数类型为insigned int类型,需要做强制类型转换,否则会提示警告信息:

9-3-12.c(45): warning C4244: “函数”: 从“time_t”转换到“unsigned int”,可能丢失数据。

       VS中查看函数定义:选中time函数,鼠标右键速揽定义:

        static __inline time_t __CRTDECL time(

            _Out_opt_ time_t* const _Time

            )

        {

            return _time64(_Time);

        }

       2.如果我们将随机数种子值改为1,运行程序后输出的结果竟然和9-3-11.c完全一致。说明,如果不调用随机数种子生成器,将随机数种子值默认为1。

       3.切忌,不要把随机数种子生成器放在循环结构内,否则无法正常生成随机数。因为计算机执行循环语句的速度非常快,还没来得及更新随机数种子值,循环语句很可能就已经结束了。

练习

1、创建一个函数,返回元素个数为n的int型数组v中的最小值。

2、创建一个函数,返回元素个数为n的int型数组v中的最小值。

3、创建一个函数,对元素个数为n的int型数组v进行倒序排列。

4、创建一个函数,将4行3列矩阵a和3行4列矩阵b的乘积,存储在3行3列矩阵c中。

5、设计一个计算机辅助教学系统,系统可以监视学生在一段时间的表现。

要求随机生成10个两位数的加法运算、减法运算、乘法运算和除法运算题,并检查学生计算结果的正确性。统计出每个学生输入的正确答案和错误答案的个数。当学生输入10个答案之后,程序应该计算出这个学生正确答案的百分比。如果百分比低于75%,那么程序应该显示“您的学习尚未达到最低目标要求,请联系老师,以得到帮助!”,然后结束运行。

本文摘自编程达人系列教材《汇编的角度——C语言》。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值