撰写了文章 发布于 2020-02-07 19:32:47
《程序编写 编程入门 - C/C++》(六)函数
本系列收藏夹 https://cowlevel.net/collection/3932815
爱发电!https://afdian.net/@kingfeng
函数
我们已经在前文中介绍了函数声明和定义格式,并撰写了我们第一个函数,也就是main函数,在这篇文章中,我将要从功能和实际出发,来详细讲述是如何设计一个函数的。
在数学意义上,我们称 y = f(x) 的格式是函数,在编程意义上,y接受了f(x)的“返回值”,而x则是传入函数的“参数”
返回值
我们说过,表达式在完成一系列功能操作的同时,其本身具有的值的属性,在函数之中,返回值可以类似的看作本身具有的值属性。
大体上返回值有两种及常见的使用方法,结果 和 错误。
对于结果较为简单,容易量化的情况下,例如判断多个数字中的最大值,或者计算一个算式并得出结果。 更为复杂的操作,对于编程者和使用者,更在乎一个功能是否执行成功,也因此会提供一个表明函数运行状况的返回值,用以判断函数是否运行成功,又在何处运行失败。
参数
形式参数
在函数foo(x, y, z)中, x, y, z被称作形式参数,在C语言中,形式参数总是通过传值调用的,C++ 提供了引用方法, 你已经在前面的文章中见到过这些值的撰写方法,我们将在此文中详细展开讲解这些撰写方法的细节与如何决定
其形式参数的意义,即在程序中不为这些参数分配实际上的内存,他们会在堆栈或你的寄存器中,但你仍然可以通过这些名字调用他们
传值调用
对于一个函数 foo(x, y, z) 和其的调用 foo(1, 2, 3),程序执行时会把对应的值压入内存区的堆栈之中,或许你不明白堆栈这个词的含义,你可以先简单的理解为 编译器会把调用函数时传入的参数,记录在一个区域,当进入函数,编译器会从这个区域选择这些参数用作运算。
如例子中所说,我们在调用时存入了 1、2、3三个数字,然后再函数执行的开始,函数新声明了三个变量,并将这三个数字读取出来。
请注意,这个流程的只是概念上的,实际上的参数传递会涉及到相当多的步骤和不同平台不同编译器的决策方式,我所叙述的这种方式是其中最为古老和原始的版本,仅仅是为了便于理解。
而当函数返回时,会把返回值存入指定区域,再由返回点处作为一个变量读取。
再这个流程中, x, y, z三个参数再结束的时候是不会被传递出去的,也就是说在函数中对x y z 进行修改是不会影响到函数外传入的变量原始值,这就是传统的传值调用
他看起来像这样 foo(type a, type b, type c)
指针调用
针对传值调用所导致的,无法修改外部变量的值这种问题,可以通过指针调用来解决。
如前文所说,若我们传入值是无法修改外部变量的,因此如果我们需要修改外部变量,可以通过传入外部变量的“地址”并针对这个地址所指示的内存的值进行修改,即可修改对应变量的值,完成这个目的。
如 scanf 其传入变量需要增加&符号,即为取地址符号,目的就是传入参数的内存地址而非参数本身,也因此,scanf可以修改这个变量。函数看起来就像这样
foo(type* a, type* b, type* c),而调用时需要
foo(&x, &y, &z)来传入指针
引用调用
由于前文中指针调用所需要使用的情况并不少见,因此在 C++ 提供了一种更为方便的方式
我们通过 foo(type &a, type &b, type &c)的格式来传入参数的时候,可以通过修改a, b, c的值来改变外部传入数据的值。
但这种特性,实际上也是一种陷阱,在多人合作代码的时候,函数的编写者与使用者常常不是同一个人,通过这种编写,会形成一个陷阱,即调用时不能明确的通过代码来保证传入的参数是否会被改变。
作用域
这是你第一次见到作用域,想象一个监狱,有囚犯、清洁工、访客、狱警、典狱长,这些人拥有其活动范围,而其只能在这个范围内活动。
对于一段代码,整个代码是由多个文件组成的,而每个文件又包含了许多个函数,这些函数所能影响到的范围即是作用域。
在很多时候,编程者使用一些临时的变量,这些变量会在达成一定条件的时候被编译器认为完成了使命,并销毁回收其内存空间,这被称作“生命周期”
在不同作用域中的变量,是不会覆盖和冲突的,编译器会找到距离调用者最近的一个变量,局部变量会覆盖全局变量,块内变量会覆盖块外变量。
局部变量 / 块级变量
在C语言中,最小的范围被称作代码块,为花括号括起来的一个区域,其内部可以定义变量,这些变量被称作块级变量,当离开代码块的时候,这些变量会被销毁。
前文中提到过的for结构可在初始化语句中声明变量,而这个变量就是我们遇见的一个块级变量,他的声明周期会结束于代码块结束
for(int i=0;;){
// ^-- 局部变量 i
} // <-- 变量i被回收
{
int i=0;
// ^-- 局部变量 i
} // <-- 变量i被回收
int main(){
int a = 0; //<-- 局部变量a
{
int b = 0; //<-- 局部变量 b
a = 1;
b = 1;
} //<-- b被回收
a = 2;
//b = 2; <-- b已被回收,无法访问
}//<-- a被回收
C语言是允许空代码块的,即只有花括号括起来的一系列语句。 当嵌套调用时,下级代码块可以访问上级代码块所定义的局部变量。 函数调用的形式参数是一种特殊的局部变量,其遵循局部变量的规则运行。
局部静态变量
使用 static修饰的时候为静态变量,其访问形式与局部变量相同,只可以在所定义的代码块中访问,但其生命周期会持续到程序结束
int main(){
{
staitc int b = 0;
b = 1;
}
//b = 2; <-- b未被回收,但编译器无法访问
} //< -- b被回收
你或许无法理解这么做的意义,但当静态变量储存于一个函数中时
void count(){
static int count =0;
count ++;
printf("%d", count);
}
int main(){
count();
count();
count();
}
你会发现,count变量在函数结束之后他的值没有改变,这就是这类变量所最常用的环境
全局变量
相对于局部变量/块级变量,全局变量是全局可以访问的,你可以在所有代码块的外"定义",任何变量只能定义一次,在整个程序中,当你需要再次使用到这个变量,你可以对其进行"声明"
int a = 0; //定义(写在所有块以外) 声明并分配内存
extern int a; //声明 只声明,不分配内存
extern int a = 0; //定义 声明并分配内存
文件级变量
除此以外,c语言中有一个特殊的规则,当你使用static修饰一个不在任何代码块中的定义的变量时,他会成为一个“文件级变量”,即只有在文件内可以访问的变量,但十分不建议这么用
命名空间
当程序代码量上升到一定程度的时候,变量数量会飙升,因此c++ 引入了命名空间来方便程序员对各种名称进行管理
namespace kingfeng{
void foo(){
printf("Hello");
}
}
当我们这么做时,实际上声明了一个命名空间,我不再需要为了防止冲突给代码中的每个名字加上前缀,而使用命名空间调用
kingfeng::foo()
来找到命名空间中的变量或函数,同样,我还可以通过using来省略命名空间
using namespace kingfeng;
foo();
这么做会在using的作用域内,使用特定某个命名空间,并省略其前缀。
命名空间时可以是非连续的,你可以在多个文件使用同一个命名空间,并通过命名空间找到所有的函数和变量。
命名空间也可以是嵌套的,你可以在嵌套的命名空间使用::作为间隔进行调用
预编译指令
预编译是一种特殊的指令,其作用是在程序编译过程中对编译器进行编程,要求其进行一些特定的处理,这类指令大多是以"#"作为起始
include
这是你见到的第一个预编译指令,其功能是将include的文件展开,一般是头文件,里面包含有一系列的函数声明,或全局变量的声明。 其具有两种调用格式
include <名称>
include "名称"
其区别在于尖括号是搜寻系统所配置好的环境中寻找,而引号是从源文件所在文件夹(工程文件夹)进行搜索,你可以使用引号来插入自己编写的文件
define
这是一个非常非常庞大的命令体系,define的作用域是文件级的,其最基本的行为是
define text_A text_B
会将其后所有 text_A作为标识符,替换为 text_B 如
#define PI 3.14
你还可以只进行定义,如
define DEBUG
DEBUG会被作为一个标识符并使用其他预编译指令进行识别。
最后你可以define一个宏,格式如下
define 名称(x) (x+x)
#define add(x,y) x+y
//当你这么定义的时候,程序所做的是文字上的替换
add(x,y)*2
/*
* 程序实际上将add(x,y)替换为x+2
* 而这段程序从结果上为 x+y*2
* 这与直觉上不服,从而引发错误
*/
在宏中,你可以使用两种特殊运算
运算 字符串化其后的第一个参数
运算 连接合并两个参数的内容
#define foo(x) #x
foo(HelloWorld) //==> 变为 "HelloWorld"
#define foo(x, y) x##y
foo(a, b) //==> 变为ab
你可以使用#undef 来取消一个标识符的定义
if
在if家族里有一系列指令
指令 | 功能 |
---|---|
#if | 与C语言if类似,对其后的条件进行判断 |
#ifdef | 判断是否define了一个标识符 |
#ifndef | 与ifdef相反,判断一个标识符是否还未被define |
#else | 与C语言类似,若不满足if |
#elif | 与C语言else if 类似,在不满足if后进行判断 |
#endif | 由于预编译指令中没有花括号,因此endif作为结束标志 |
最为常用与头文件中,为了防止同一个头文件被反复载入,通常对头文件进行如下处理
#ifndef __MY_HEAD_H__
#define __MY_HEAD_H__
...
#endif
这么做时,第一次运行该头文件时ifndef条件通过,并定义了 MY_HEAD_H标识符,当后续再次读取该头文件时(可能你和你include的一个其他文件都使用了这个头文件)条件无法通过,头文件的内容就会被跳过。
课题
完成了上一次的课题,我写了如下代码
#include <cstdio>
#include <cstdlib>
#include <ctime>
int main(){
int r1, r2, r3, answer, input;
char symbol;
srand(time(NULL));
bool mark[10]={};
int count_problem=0;
int count_mark=0;
while(count_mark < 6){
r1 = rand() % 10;
r2 = rand() % 4 ;
r3 = rand() % 10;
switch(r2){
case 0:
symbol = '+';
answer = r1 + r3;
break;
case 1:
symbol = '-';
answer = r1 - r3;
break;
case 2:
symbol = '*';
answer = r1 * r3;
break;
case 3:
symbol = '/';
r1 || (r1 = 1);
r3 || (r3 = 1);
answer = r1 / r3;
break;
}
printf("%d%c%d=", r1, symbol, r3);
scanf("%d", &input);
if(input == answer)
mark[count_problem % 10] = true;
getchar();
count_mark = 0;
for(int i=10; i--;){
if(mark[i]) count_mark++;
}
count_problem++;
}
printf("Your Mark Is: %d/10, before %d questions" ,count_mark, count_problem);
return 1;
}
如果你和我的不一样,没有关系,只要结果时正确的就行了。我会用一个相当麻烦的方法拆解整个流程,来展示和练习我们学到的函数与预编译知识。
首先,我设计的第一个函数,用以生成一个“可用的”字符,我使用一个宏定义来确定什么字符是可用的,并且我们把随机生成并打印数字和符号制作成函数。
编写一个函数,生成一个数字,并将其打印
编写一个函数,生成一个四则运算符号,并将其打印
先对你自己的代码下手尝试改改吧!
#define MAX_RANDOM_NUMBER 9
int get_and_print_a_random_number(){
int a = rand() % (MAX_RANDOM_NUMBER + 1);
printf("%d ", a);
return a;
}
int get_and_print_a_random_mark(){
int a = rand() % 4;
char c = 0;
switch(a){
case 0:
c = '+';
break;
case 1:
c = '-';
break;
case 2:
c = '*';
break;
case 3:
c = '/';
break;
}
putchar(c);
return a;
}
这样,我们完成了第一块函数。接下来,我们需要一个计算这个式子结果与用户结果输入是否相匹配的函数
设计一个函数,输入a, b, 符号, 用户结果 并返回这个结果是否正确
bool if_userAnswer_is_right(int number_a, int mark, int number_b, int user_answer){
int answer = 0;
switch(mark){
case 0:
answer = number_a + number_b;
break;
case 1:
answer = number_a - number_b;
break;
case 2:
answer = number_a * number_b;
break;
case 3:
answer = number_a / number_b;
break;
}
if(answer == user_answer){
return true;
}else{
return false;
}
}
我们已经重写完第一阶段的课题,让我们进行第一步尝试。我的主程序现在看起来是这样,修改你的主程序,以适配新撰写的函数。
int main(){
int user_answer =0;
int a, b, m;
a = get_and_print_a_random_number();
m = get_and_print_a_random_mark();
b = get_and_print_a_random_number();
putcher('=');
scanf("%d", &user_answer);
if(if_userAnswer_is_right(a, m, b, user_answer)){
puts("Right");
}else{
puts("Wrong");
}
}
按照这个步骤,编写一系列评分函数
然后我们将分数系统融入进去,如果你希望通过传递的方式传入数组,“数组名”本身是一种指针,你可以不用对其取地址,对直接传入数组名的数组进行修改的时候,其外部原本变量的值也会被修改。你也可以使用全局变量,就像我一样。
定义两个变量(全局或者局部),作为分数的储存和已做题目的储存
写一个判断是否合格的函数,利用我们上一节的方法,返回用户是否合格
写一个记录函数,当用户回答正确/错误的时候,将分数记录
写一个打印函数,在结束程序之前,输出用户一共经过了多少个题目
你的main函数最终看起来像这样。
int main(){
int user_answer =0;
int a, b, m;
while(!mark_can_exit()){
a = get_and_print_a_random_number();
m = get_and_print_a_random_mark();
b = get_and_print_a_random_number();
putchar('=');
scanf("%d", &user_answer);
if(if_userAnswer_is_right(a, m, b, user_answer)){
puts("Right");
mark_answer_right();
}else{
puts("Wrong");
mark_answer_wrong();
}
}
mark_show_message();
}
动手尝试一下,探讨是否有更好的功能划分方法?你划分的功能时什么样的。欢迎在评论区讨论。我的最终代码会在文章末尾展示。
如果你的程序完成并且成功了,你思考并尝试一下如下内容
编写一个函数,测试我们所编写的所有函数,使其功能正确
我们的下一篇文章-测试与调试一 将会讲解调试器的使用,以及一个简单的测试代码是如何组织的,
最终,程序看起来像这样
#include <cstdio>
#include <cstdlib>
#include <ctime>
#define MAX_RANDOM_NUMBER 9
#define MARK_CAN_EXIT_PROBLEM 10
#define MARK_CAN_EXIT_RIGHT 6
int get_and_print_a_random_number(){
int a = rand() % (MAX_RANDOM_NUMBER + 1);
printf("%d", a);
return a;
}
int get_and_print_a_random_mark(){
int a = rand() % 4;
char c = 0;
switch(a){
case 0:
c = '+';
break;
case 1:
c = '-';
break;
case 2:
c = '*';
break;
case 3:
c = '/';
break;
}
putchar(c);
return a;
}
bool if_userAnswer_is_right(int number_a, int mark, int number_b, int user_answer){
int answer = 0;
switch(mark){
case 0:
answer = number_a + number_b;
break;
case 1:
answer = number_a - number_b;
break;
case 2:
answer = number_a * number_b;
break;
case 3:
answer = number_a / number_b;
break;
}
if(answer == user_answer){
return true;
}else{
return false;
}
}
extern int count_problem = 0;
extern bool mark[MARK_CAN_EXIT_PROBLEM] = {};
bool mark_can_exit(){
int count=0;
for(int i = MARK_CAN_EXIT_PROBLEM; i--;)
if(mark[i]) count++;
return (count >= MARK_CAN_EXIT_RIGHT);
}
void mark_answer_right(){
count_problem++;
mark[count_problem % MARK_CAN_EXIT_PROBLEM] = true;
}
void mark_answer_wrong(){
count_problem++;
mark[count_problem % MARK_CAN_EXIT_PROBLEM] = false;
}
void mark_show_message(){
print("You Already Do %d problem", count_problem);
}
int main(){
int user_answer =0;
int a, b, m;
while(!mark_can_exit()){
a = get_and_print_a_random_number();
m = get_and_print_a_random_mark();
b = get_and_print_a_random_number();
putchar('=');
scanf("%d", &user_answer);
if(if_userAnswer_is_right(a, m, b, user_answer)){
puts("Right");
mark_answer_right();
}else{
puts("Wrong");
mark_answer_wrong();
}
}
mark_show_message();
}
目录