2025.11.04 C语言程序设计上机实习三

2025.11.04 C语言程序设计上机实习三

函数的基本定义与调用

程序的完整源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include <stdio.h>
#include <math.h>

// 定义圆周率常量,通过反余弦函数计算获得精确值
const double pi = acos(-1);

/***
计算圆的面积
r 圆的半径
return 计算得到的圆的面积
***/
double CircleArea(double r);

/***
计算矩形的面积
l 矩形的长度
w 矩形的宽度
return 计算得到的矩形的面积
***/
double RectangleArea(double l, double w);

/***
打印圆和矩形的面积
circle 圆的面积
rect 矩形的面积
return 无返回值
***/
void PrintAreas(double circle, double rect);

int main(void)
{
// 初始化变量:圆的半径为5.0,矩形的长为4.0、宽为3.0
double radius=5.0, length=4.0, width=3.0;
// 声明变量用于存储计算得到的面积
double circle, rect;

// 调用CircleArea函数计算圆的面积
// 参数传递过程:将radius作为实参传递给形参r,函数内部使用r计算面积后返回结果赋值给circle
circle = CircleArea(radius);

// 调用RectangleArea函数计算矩形的面积
// 参数传递过程:将length和width作为实参分别传递给形参l和w,函数内部使用l和w计算面积后返回结果赋值给rect
rect = RectangleArea(length, width);

// 调用PrintAreas函数打印面积结果
// 参数传递过程:将circle和rect作为实参分别传递给形参circle和rect,函数内部使用这两个参数进行格式化输出
PrintAreas(circle, rect);

return 0;
}

/***
计算圆的面积
r 圆的半径
计算得到的圆的面积,计算公式:π * r * r
***/
double CircleArea(double r)
{
double result; // 声明变量存储计算结果
result = pi * r * r; // 使用圆面积公式计算
return result; // 返回计算得到的面积
}

/***
计算矩形的面积
l 矩形的长度
w 矩形的宽度
return 计算得到的矩形的面积,计算公式:l * w
*/
double RectangleArea(double l, double w)
{
double result; // 声明变量存储计算结果
result = l * w; // 使用矩形面积公式计算
return result; // 返回计算得到的面积
}

/***
打印圆和矩形的面积
circle 圆的面积,以10位小数精度输出
rect 矩形的面积,以2位小数精度输出
return 无返回值
***/
void PrintAreas(double circle, double rect)
{
// 格式化输出两个面积值,圆面积保留10位小数,矩形面积保留2位小数
printf("Circle Area: %.10f, Rectangle Area: %.2f\n", circle, rect);
}

程序的实际运行输出

1
2
3
4
5
6
s13665@ubuntu:~/codes/20251104$ gcc 1.c -lm
1.c:4:19: warning: initializer element is not a constant expression
const double pi = acos(-1);
^
s13665@ubuntu:~/codes/20251104$ ./a.out
Circle Area: 78.5398163397, Rectangle Area: 12.00

程序流程图

image-20251104204632468

参数传递与变量作用域

程序的完整源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <stdio.h>

// 函数声明
int ProcessData(int); // 声明值传递函数(处理数据并返回结果)
void UpdateValues(int *, int *); // 声明地址传递函数(通过指针修改外部变量)
void UpdateValuesFail(int, int); // 声明值传递函数(尝试修改但无法影响外部变量)

int main() {
int a = 10, b = 20;
int result = 0;

result = ProcessData(a); // 将a的值(10)传入,接收返回值(15)
ProcessData(b); // 将b的值(20)传入,返回值(25)未使用
printf("First: a=%d, b=%d, result=%d\n", a, b, result); // a和b仍为初始值

UpdateValues(&a, &b/* 传入a和b的地址,允许函数修改原变量 */);
printf("Second: a=%d, b=%d\n", a, b); // a和b已被修改

UpdateValuesFail(a, b/* 传入当前a和b的值,函数内修改不影响外部 */);
printf("Third: a=%d, b=%d\n", a, b); // a和b保持修改后的值(未受影响)

return 0;
}

int ProcessData(int num) // num是传入参数的副本(如a的副本为10)
{
int result;
result = num + 5; // 仅修改函数内,不影响原变量(如a仍为10)
return result; // 返回处理后的结果
}

void UpdateValues(int *t1, int *t2) // t1指向a的地址,t2指向b的地址
{
*t1 += 5; // 通过指针解引用(*t1)直接修改a的实际值(a = a + 5)
*t2 += 10; // 通过指针解引用(*t2)直接修改b的实际值(b = b + 10)
}

void UpdateValuesFail(int t1, int t2) // t1是a的副本,t2是b的副本
{
t1 += 5; // 仅修改函数内t1,原变量a不受影响
t2 += 10; // 仅修改函数内t2,原变量b不受影响
}
  1. 值传递与地址传递的区别
  • 值传递(如函数==ProcessData==和==UpdateValuesFail==):
    • 函数接收的是实参的副本(即复制一份变量的值传入函数)。
    • 函数内部对参数的修改仅作用于函数内的副本,不会影响外部原变量的值。
  • 地址传递(如==UpdateValues==):
    • 函数接收的是实参的内存地址(通过指针传递)。
    • 函数内部通过指针解引用可以直接访问并修改原变量的内存空间,因此会影响外部原变量。
  1. 指针操作的实际效果
    指针变量存储的是内存地址。
    通过&变量名可以获取变量的地址(如&a表示变量 a 的内存地址)。
    通过*指针名(解引用操作)可以访问指针指向的内存空间:
    在UpdateValues中,*t1 += 5产生了如下效果:找到 t1 所指向的内存(即 a 的地址),将该地址中的值加 5,最终直接修改了原变量 a。
    指针的核心作用是间接访问内存,实现函数对外部变量的修改,突破值传递 “仅操作副本” 的限制。

程序的实际运行输出

1
2
3
4
s13665@ubuntu:~/codes/20251104$ ./a.out
First: a=10, b=20, result=15
Second: a=15, b=30
Third: a=15, b=30

用Debug工具调试和分析代码的过程

设置断点

与上方所示代码相对应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
s13665@ubuntu:~/codes/20251104$ gdb a.out
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from a.out...done.
(gdb) list
1 #include <stdio.h>
2
3 // 函数声明
4 int ProcessData(int); // 声明值传递函数(处理数据并返回结果)
5 void UpdateValues(int *, int *); // 声明地址传递函数(通过指针修改外部变量)
6 void UpdateValuesFail(int, int); // 声明值传递函数(尝试修改但无法影响外部变量)
7
8 int main() {
9 int a = 10, b = 20;
10 int result = 0;
(gdb)
11
12 result = ProcessData(a); // 将a的值(10)传入,接收返回值(15)
13 ProcessData(b); // 将b的值(20)传入,返回值(25)未使用
14 printf("First: a=%d, b=%d, result=%d\n", a, b, result); // a和b仍为初始值
15
16 UpdateValues(&a, &b/* 传入a和b的地址,允许函数修改原变量 */);
17 printf("Second: a=%d, b=%d\n", a, b); // a和b已被修改
18
19 UpdateValuesFail(a, b/* 传入当前a和b的值,函数内修改不影响外部 */);
20 printf("Third: a=%d, b=%d\n", a, b); // a和b保持修改后的值(未受影响)
(gdb) break 12
Breakpoint 1 at 0x4005c2: file 2_comment.c, line 12.
(gdb) break 13
Breakpoint 2 at 0x4005cf: file 2_comment.c, line 13.
(gdb) break 16
Breakpoint 3 at 0x4005f3: file 2_comment.c, line 16.
(gdb) break 19
Breakpoint 4 at 0x40061d: file 2_comment.c, line 19.
(gdb) break ProcessData
Breakpoint 5 at 0x400665: file 2_comment.c, line 28.
(gdb) break UpdateValues
Breakpoint 6 at 0x40067f: file 2_comment.c, line 34.
(gdb) break UpdateValuesFail
Breakpoint 7 at 0x4006aa: file 2_comment.c, line 40.

开始调试

执行run开始调试

打印a与a的地址

1
2
3
4
5
6
7
8
9
(gdb) run
Starting program: /home/s13665/codes/20251104/a.out

Breakpoint 1, main () at 2_comment.c:12
12 result = ProcessData(a); // 将a的值(10)传入,接收返回值(15)
(gdb) print a
$1 = 10
(gdb) print &a
$2 = (int *) 0x7fffffffe4dc

执行 step 进入 ProcessData 函数,打印形参及形参的地址

1
2
3
4
5
6
7
8
(gdb) step

Breakpoint 5, ProcessData (num=10) at 2_comment.c:28
28 result = num + 5; // 仅修改函数内,不影响原变量(如a仍为10)
(gdb) print num
$3 = 10
(gdb) print &num
$4 = (int *) 0x7fffffffe4ac

继续执行 continue,打印b与b的地址

1
2
3
4
5
6
7
8
9
(gdb) continue
Continuing.

Breakpoint 2, main () at 2_comment.c:13
13 ProcessData(b); // 将b的值(20)传入,返回值(25)未使用
(gdb) print b
$5 = 20
(gdb) print &b
$6 = (int *) 0x7fffffffe4e0

执行 step 进入 ProcessData 函数,打印形参及形参的地址

1
2
3
4
5
6
7
8
(gdb) step

Breakpoint 5, ProcessData (num=20) at 2_comment.c:28
28 result = num + 5; // 仅修改函数内,不影响原变量(如a仍为10)
(gdb) print num
$7 = 20
(gdb) print &num
$8 = (int *) 0x7fffffffe4ac

继续执行 continue,打印实参 a与a的地址 b与b的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(gdb) continue
Continuing.
First: a=10, b=20, result=15

Breakpoint 3, main () at 2_comment.c:16
16 UpdateValues(&a, &b/* 传入a和b的地址,允许函数修改原变量 */);
(gdb) print a
$9 = 10
(gdb) print &a
$10 = (int *) 0x7fffffffe4dc
(gdb) print b
$11 = 20
(gdb) print &b
$12 = (int *) 0x7fffffffe4e0

执行 step 进入 UpdateValues 函数,打印形参及形参的地址

1
2
3
4
5
6
7
8
9
10
11
12
(gdb) step

Breakpoint 6, UpdateValues (t1=0x7fffffffe4dc, t2=0x7fffffffe4e0) at 2_comment.c:34
34 *t1 += 5; // 通过指针解引用(*t1)直接修改a的实际值(a = a + 5)
(gdb) print t1
$13 = (int *) 0x7fffffffe4dc
(gdb) print &t1
$14 = (int **) 0x7fffffffe4b8
(gdb) print t2
$15 = (int *) 0x7fffffffe4e0
(gdb) print &t2
$16 = (int **) 0x7fffffffe4b0

继续执行 continue,打印实参 a与a的地址 b与b的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(gdb) continue
Continuing.
Second: a=15, b=30

Breakpoint 4, main () at 2_comment.c:19
19 UpdateValuesFail(a, b/* 传入当前a和b的值,函数内修改不影响外部 */);
(gdb) print a
$17 = 15
(gdb) print &a
$18 = (int *) 0x7fffffffe4dc
(gdb) print b
$19 = 30
(gdb) print &b
$20 = (int *) 0x7fffffffe4e0

执行 step 进入 UpdateValuesFail 函数,打印形参及形参的地址

1
2
3
4
5
6
7
8
9
10
11
12
(gdb) step

Breakpoint 7, UpdateValuesFail (t1=15, t2=30) at 2_comment.c:40
40 t1 += 5; // 仅修改函数内t1,原变量a不受影响
(gdb) print t1
$21 = 15
(gdb) print &t1
$22 = (int *) 0x7fffffffe4bc
(gdb) print t2
$23 = 30
(gdb) print &t2
$24 = (int *) 0x7fffffffe4b8

为什么要打印这些值或地址?

  1. 验证值传递的特性

    • 对于 ProcessDataUpdateValuesFail,形参(numt1t2)是实参的副本,打印形参和实参的地址可证明两者存储在不同内存位置(地址不同),以此来证明函数内修改形参并不会影响实参。
    • 打印可验证形参初始值与实参一致(副本正确),但修改后形参与实参值不同,以此来说明“变量作用域”。
  2. 验证地址传递的特性

    • 在函数 UpdateValues 中,实参是 &a&b(地址),形参 t1t2 是指针,打印形参 t1t2可证明它们确实指向实参 abt1 == &at2 == &b)。
    • 结合后续对 *t1*t2 指针指向的内存的修改,可验证通过指针解引用能直接操作实参内存,以此来解释为何实参值会被函数修改。
  3. 理解函数调用时的参数传递机制

    • 值传递:实参值的副本给形参
    • 地址传递:实参地址的副本给形参指针

程序的部分内存图及其解释

image-20251104204642247

UpdateValues函数中t1、t2均为指针变量,存储实参a、b的地址。

UpdateValuesFail函数中t1、t2均为整型变量,先存储实参a、b原来的值,而后分别+5与+10,所得结果仍存储在形参t1、t2,并没有对实参a、b进行改变。

多函数协作与模块化设计

程序的完整源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>

//函数声明
double CalculateAverage(double s1, double s2, double s3);
char GetGrade(double avg);
void PrintReport(int id, double avg, char grade);

int main() {

int id = 1001;
double score1 = 85.5, score2 = 92.0, score3 = 78.5;

// 计算平均分
double avg = CalculateAverage(score1, score2, score3);

// 获取等级
char grade = GetGrade(avg);

// 输出成绩报告
PrintReport(id, avg, grade);

return 0;
}

// 计算三门课平均分
double CalculateAverage(double s1, double s2, double s3) {
return (s1 + s2 + s3) / 3.0;
}

// 根据平均分返回等级,使用?: 运算符
char GetGrade(double avg) {
return (avg >= 90.0) ? 'A' :
(avg >= 80.0) ? 'B' :
(avg >= 70.0) ? 'C' : 'D';
}

// 输出成绩报告
void PrintReport(int id, double avg, char grade) {
printf("ID:%d, Average:%.2f, Grade:%c\n", id, avg, grade);
}

程序的实际运行输出

1
2
s13665@ubuntu:~/codes/20251104$ ./a.out
ID:1001, Average:85.33, Grade:B