函数
这一章,我们将向大家介绍 C++ 语言中,函数的概念、定义与调用。
在数学中,也有 函数
这个概念,但它与程序设计中的概念完全不同。
但在程序设计中,所谓“函数”,指的是一个事先定义好的代码模块,一经定义,就可以在程序的多个位置重复使用——比如我们之前使用的 sort
函数等,就是 C++ 各个库中已经定义好的函数。
函数的定义
我们已经知道,在 C++ 语言中,任何变量在使用之前首先都要进行声明。同样地,任何函数在使用之前也要先完成定义。
简单地说,你要让编译器知道,你定义的这个函数里面究竟有什么代码需要执行,然后编译器才能在你调用函数的时候,知道接下来该做什么。
对于如下的函数定义:
void fun(int n) {
cout<<"Hello";
}
其中,fun为函数名,int n
为形式参数表。形式参数表 说明了函数输入的每个参数的类型和名称,其声明方式与变量的声明类似。内部的两重循环为函数体语句,在这部分语句中,可以用形式参数表中的变量(如变量n
)进行各种操作。
我们一般将void fun(int n) {}
称为函数的头部。
如果有多个形式参数(简称为 形参),则用逗号分隔即可,每个形式参数的类型都要明确写出,不能省略:
void fun(int a, int b, int c) {}
形参的取值决定于调用这个函数的代码。例如,我们可以在主函数调用刚刚定义的输出三角形的output
函数:
output(3);
output(4);
output(5);
每次调用的代码output(3)
中的 33 就是 实际参数(简称 实参)。当我们指定实际参数为 33 之后,output
函数的函数体语句中就可以认为形参变量n
的初始值为 33,也就可以输出一个大小为 33 的直角三角形了。
函数的形参一定要和实参一一对应,参数个数和对应位置的参数类型完全相同。
数组参数
当我们需要使用数组作为参数时,可以这么写:
void fun(int a[],int n){
for(int i=0;i<n;i++){
cout<<a[i]<<" ";
}
}
无参数函数
当一个函数没有任何形参时,可以按照如下的方式定义:
void fun(){
int x;
cin>>x;
cout<<x;
}
int main(){
fun();
return 0;
}
无论是函数的定义还是调用,函数名后的一对小括号都是必不可少的。如果不加这对小括号,会导致编译错误哦。
函数内修改参数
在函数内修改形参的值,是不一定会对实参的值产生影响的。比如下面这段代码:
void fun(int x){
x++;
}
int main(){
int a=5;
add(a); //a==5
return 0;
}
执行add(a)
之后,a
的值仍然为 55 而不是 66。对于int
、string
、double
、char
等类型的形参,都是如此。
函数的返回值
在前面的课程中,我们已经向大家介绍了函数的定义和调用,在每次定义函数的开头,都会写上一个 void,void表示没有返回值。
一个函数除了可以执行一系列指定的逻辑外,还可以有一个返回值。在函数定义的结构中:
[返回值类型] 函数名(参数列表){
函数体
}
除了void
以外,还可以在返回值类型处填写int
、double
等类型,表示有返回值。
注意:返回值类型是由函数类型决定的,返回值返回给函数调用去了
return 语句
如果一个函数的返回值类型不为void
但没有return
语句,则会返回一个不固定的值,所以千万不要漏掉return
语句哦。
如果返回值类型为void
,那么可以不写return
语句,也可以在函数体内部写return
语句。例如:
void fun(int n){
for(int i=2;i<=n;i++){
if(n%i==0){
cout<<i<<endl;
return;
}
}
}
这个函数能够输出n
的最小的大于 11 的因子。这种情况下的return
后面不要接任何值,当执行到return
语句后,会跳出所在的min_factor
函数。当定义的函数返回值类型不为void
时,执行到中间的return
语句同样会结束所在的函数,忽略函数体内之后的所有语句。
main 函数
现在,学习了函数与返回值的相关知识,让我们再回过头来看看 C++ 程序的主体结构:
int main(){
return 0;
}
实际上,这个main
就是一个函数——称为 C++ 程序的 主函数。一个 C++ 程序由一个主函数和若干个子函数组成——主函数是程序执行的开始点和结束点。在主函数中,return 0;
语句将结束主函数,在子函数中,return
相关语句会结束子函数。在主函数中,我们可以调用定义好的各种子函数,而子函数又可以再调用其他的函数。
根据我们已经学过的知识,我们不难发现,main
函数一般没有参数(但在某些情况下也可以有参数),返回值为整数类型,一般情况下在函数的最后一句,我们会将0
作为main
函数的返回值,并结束main
函数的执行。这个返回值最后会传递给操作系统,而返回值为0
代表程序没有错误,正常结束——如果程序出错,最后就会以其他返回值结束(具体返回值是多少取决于错误类型)。
基本函数
在数学函数库中,还有 四舍五入函数 round、上取整函数 ceil 和下取整函数 floor,它们都接受一个(双精度)浮点数作为传入的参数,需使用头文件#include<cmath>
round(2.6)=3;
还有次方计算函数 pow
,开根号函数 sqrt
,绝对值计算函数 fabs
(对于浮点数求绝对值的函数),abs
对整数求绝对值的函数。
pow(2,2)=4;
max 函数和 min 函数,要求两个参数类型必须一致。
max(1,2)=2;
sizeof
sizeof
的括号中可以是变量类型,也可以是某个变量,最终的结果都是该数据类型在计算机内存中所占的字节数。
sizeof(int)=4;
memset
memset
函数可以帮我们对数组进行初始化,但是请记得,memset
函数的初始化并不是将数组中的每个元素进行赋值,而是对字节进行赋值,如果不理解这个概念,没关系。
memset
函数需要头文件#include<cstring>
变量
在前面的课程里,我们已经学习过了各种各样的变量 —— 在自定义函数、主函数内部的变量;循环头部的控制变量;全局变量;自定义函数的形式参数,等等。
我们通常将函数内定义的变量称为局部变量。变量可以根据定义的位置,分为如下三类:
- 局部变量
- 全局变量
- 函数形参
当我们声明一个变量后,这个变量能够被使用的范围,我们称它为变量的作用域。
变量的规范使用
外层循环里的变量a
的作用域为红色框,内层循环里的变量a
的作用域为蓝色框。在蓝色框内,同时有外层和内层两个变量a
,应该以谁为准呢?
当出现内外层代码块定义了名字相同的变量时,应该以最内层的代码块中定义的变量为准。
因此,程序不会编译错误,而是会输出5
。
但是,当在同一个代码块的同一层内定义了名字相同的变量后,就会提示“定义了相同的变量”哦。
建议还是不要写出上面这种代码。为了避免出错,除了循环的控制变量以外,尽量不要起名字相同的变量。
结构体
我们在之前的课程中学习了数组,我们可以声明一个 int a[10]
的数组,在其中包含 10 个整数型的元素。通过数组,我们可以管理和使用这些类型一致的数组元素。
C++ 语言为我们提供了一种被称为 结构体(structure) 的方式,通过定义一个结构体,我们可以将一系列类型相同或不同的元素放在一起。例如,如果我们希望定义一种存放个人基本信息的结构,我们就可以通过定义结构体的关键字struct完成这一过程:
struct Person{
string name; // 姓名
int age; // 年龄
char gender; // 性别
float height; // 身高
};
在这样一个被定义的结构体类型中,我们包含了姓名(字符数组)、年龄(整数型)、性别(字符型)、身高(浮点型)。
当我们定义了person结构体类型后,如果我们需要保存tom的信息,我们就不再需要分别声明四个不同类型变量,而只需要使用结构体类型直接声明结构体变量:
Person tom;
结构体类型生成的结构体变量中的元素可以通过成员运算符 .
进行访问。被访问的结构体变量的元素可以被视为任何一个普通变量,我们可以通过 cin 读入值到结构体变量的元素、也可以通过 cout 输出它的值。例如,对于上面已有的结构体变量tom我们可以进行如下的赋值:
Person tom;
tom.name = "Tom";
tom.age = 54;
tom.gender = 'm';
tom.height = 170.18f;
我们可以看到,无论是结构体中哪一个成员元素变量,都需要在结构体变量名后加上成员运算符后才可以被访问。结构体变量名加上成员运算符 . 再加上成员元素名作为一个整体,在被使用时与一个普通的变量完全一致。
结构体 在这样一个被定义的结构体类型中,我们包含了姓名(字符数组)、年龄(整数型)、性别(字符型)、身高(浮点型)。 当我们定义了person结构体类型后,如果我们需要保存tom的信息,我们就不再需要分别声明四个不同类型变量,而只需要使用结构体类型直接声明结构体变量: Person tom;
我们可以看到,无论是结构体中哪一个成员元素变量,都需要在结构体变量名后加上成员运算符后才可以被访问。结构体变量名加上成员运算符 . 再加上成员元素名作为一个整体,在被使用时与一个普通的变量完全一致。
结构体初始化
当然,我们也可以通过类似于数组初始化的方式直接在声明结构体的时候,就让其中的成员元素获得初值。
Person tom={ "Tom",54,'m',170.18f};
当用于初始化的信息特别长时,我们也可以分多行来写:
Person tom={
"Tom",
54,
'm',
170.18f
};
结构体初始化 当然,我们也可以通过类似于数组初始化的方式直接在声明结构体的时候,就让其中的成员元素获得初值。 Person tom = { "Tom" , 54 , 'm' , 170.18f }; 当用于初始化的信息特别长时,我们也可以分多行来写: Person tom = { "Tom" , 54 , 'm' , 170.18f };
结构体数组
结构体其实就是自定义类型,那么它既然是一种类型,那么它就跟int
,double
,char
这些类型是一样的。
struct student{
string name;
int id;
};
所以我们可以定义整型变量 int a
,我们当然也可以定义结构体类型的变量 student a
。
我们可以定义整型数组 int a[10]
,我们当然也可以定义结构体类型的数组 student
a[10]
。
整型数组由整数型变量构成,而结构体数组则由结构体变量构成。
所以两者之间在概念和定义上,并没有任何区别。
结构体的构造函数
在语法上,构造函数具有这样的性质:
- 函数名与结构体名完全相同
- 不能定义返回值类型,也不能有return语句
- 可以有形参,也可以没有形参,可以带有默认参数
- 可以重载
在使用的时候,我们不需要手动调用构造函数。当我们创建结构体的时候,构造函数会自动被调用。
你可能已经发现了一个问题——之前我们在定义 Student
结构体的时候并没有定义构造函数,那么这个时候,Student
结构体生成的对象又该如何进行初始化呢?事实上,如果我们定义一个结构体的时候,不声明任何构造函数,那么编译器在编译的时候,就会为我们自动生成一个默认构造函数,它具有这样的特点:
- 参数列表为空,不为数据成员赋初值
- 如果结构体内定义了成员的初始值,则使用结构体内定义的初始值
- 如果没有定义结构体内的初始值,则以默认方式初始化
- 基本类型的数据默认初始化的值是不确定的(类似于我们在主函数中声明一个变量却不赋初始值的情况)
简而言之,这样一个构造函数,它的特点就是“什么都不做”,单纯只是创建一个结构体而已——相当于这样的一个形式:
student(){
}
需要注意的是,如果我们在结构体中已经定义了一个构造函数(可以是任意形式)的话,那么编译器就不会再为我们定义默认构造函数了——这个时候,如果我们需要使用到默认构造函数的话,不要忘记自己再定义它。
如上所示,初始化列表的写法是,在构造函数的括号后面加一个冒号,然后按照成员变量(参数)
的格式,依次对每一个变量进行初始化,彼此之间用逗号隔开。
这里一定要注意以下两点:
-
函数的参数列表 绝对不能省略,像
Student():name(n), score(s)
这样的写法是不允许的。 -
如果在初始化完成员变量之后,还有别的事情要做,那可以把代码写在大括号里。但是,就算之后什么都不做,也必须写大括号——大括号不能省略!!!