「C++极简教程」第三章 C++函数和编译预处理 - Extremeer 极振科技传媒工作室

本系列为C++的入门学习者服务,旨在于为此前无C++基础的学习者简单介绍关于C++语言基础的部分知识,如果已入门C++,则不需要阅读本系列。

3.1 C++函数的定义

  • 任何c++程序由至少一个函数组成,其中main函数是必不可少的。
  • 函数除了是程序的基本组成部分之外,更重要的是可以完成特定的功能。
  • 为了完成特定的功能,函数需要数据的输入(参数),并给出数据的结果(返回值)。
  • 通过编写和调用函数可以简化程序逻辑,提高编码效率,也是团队合作必备的基础。

1. 系统函数与自定义函数

  • 系统函数:不需要我们编写,由系统提供的函数,也称为库函数。如:sqrt()
  • 自定义函数:自己编写的函数。

使用系统函数可以提高编程效率,但并不能覆盖所有的需求。因此我们需要学习自己编写函数,也就是自定义函数。编写自定义函数是结构化程序设计的主要步骤。

2. 函数参数和返回值

例:

sqrt()开平方根函数

使用该函数的时候,需要一个输入的值,并得到对应的输出。如:y=sqrt(x)

对于sqrt函数,需要一个类型为你double的参数(输入),并提供一个类型为double的返回值(输出)。

3. 自定义函数的步骤

① 分析函数的参数(个数,类型)

② 分析函数的返回值(类型)

③ 实现从参数得到返回值的过程

例:编写一个自定义函数max。

函数功能:求两个整数当中较大的数。

函数逻辑:使用判断语句即可。

参数分析:需要两个参数,都是整数类型。

返回值分析:结果返回一个整数。

4. 自定义函数的语法格式

1
2
3
4
5
返回值类型 函数名 ( 函数参数列表 ) //函数头
{
//函数体
return 返回值;
}
  • 函数参数:数量为任意个,如果为0可以省略。
  • 返回值:数量只能为0个或1个,用关键词return实现,且代码运行时遇return自动结束该函数。如果没有返回值,函数名前的返回值类型用void表示。

例1:有参有返函数:编写一个自定义函数max求两数的较大数。

1
2
3
4
5
int max(int a, int b) //每一个参数独立,即不能写成int a, b
{
int c = a > b ? a : b;
return c; //通过该语句返回结果
}

例2:无参无返函数:打印函数print。

1
2
3
4
5
6
void print (void) //参数的void可省略
{
cout << "*********" << endl;
cout << "*example*" << endl;
cout << "*********" << endl;
} //本函数仅仅实现打印的功能,无数据处理

例3:有参有返函数:求最大公约数的函数gcd。

  • 函数功能:求两个整数的最大公约数。
  • 函数逻辑:使用递推法(欧几里得算法)或穷举法皆可。
  • 参数分析:需要两个参数,都是整数类型。
  • 返回值分析:结果返回一个整数。

穷举法:

1
2
3
4
5
6
7
int gcd(int a, int b) //与上例相同
{
int c = a < b ? a : b; //二者中较小的数
while (a % c != 0 || b % c != 0) //尝试
--c;
return c; //得到结果
}

递推法(欧几里得算法或辗转相除法):

1
2
3
4
5
6
7
8
9
10
11
int gcd(int a, int b) //注意分析返回和参数类型
{
int c = a % b; //c是a除以b的余数
while(c != 0) //欧几里得算法递推求解
{
a = b;
b = c;
c = a % b;
}
return b; //返回最后的结果
}

5. 函数定义小结

  1. 分析清楚函数所需的参数和能提供的返回值至关重要。
  2. 在函数实现过程中需要的辅助变量,不能作为参数来使用,只能定义在函数的内部。
  3. 遇到return语句之后函数自动结束,因此一般将return语句放在函数的最后。

3.2 函数的调用和声明

1. 函数的调用过程

  1. 主调函数将参数(实际参数)传递给自定义函数,并暂停主调函数的运行。
  2. 自定义函数根据得到的参数(形式参数)值进行计算,并得出返回值结果。
  3. 自定义函数将返回值回传给主调函数,结束自定义函数并继续主调函数的运行。

2. 函数的三种调用情况

  • 系统函数:

    • 包含所在的头文件
    1
    2
    3
    4
    5
    #include <cmath>
    int main()
    {
    sqrt(3);
    }
  • 自定义函数:

    • 先定义,再调用,可以直接调用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    int max(int a, int b)
    {
    int c = a > b ? a : b;
    return c;
    }
    int main()
    {
    max(3, 4);
    }
    • 先调用,再定义,则需补充声明
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    int max(int a, int b);//声明函数(见下一小节)
    int main()
    {
    max(3,4);
    }
    int max(int a, int b)
    {
    int c = a > b ? a : b;
    return c;
    }

3. 函数的声明

从结构化程序设计的角度,通常是先调用后定义。

而先调用后定义要先声明函数避免系统由上到下执行语句时函数名未定义。

函数声明是将函数头添加分号而构成的语句,参数名可以省略。

例:

  • int max(int a, int b);
  • void print(void);
  • int gcd(int, int); //省略参数名

4. 函数的参数传递

(1) 两种参数

参数种类 参数定义 格式
实际参数(实参) 调用函数实际需要参与运算的参数 常量、变量、表达式
形式参数(形参) 声明函数的时候用来占位的参数 变量

注:实参形参个数和排列顺序一一对应,并且对应的参数应类型匹配赋值兼容)。

(2) 传递过程

先计算实参表达式的值,再将该值传递(拷贝)给对应的形参变量。

注:实参形参单向传递拷贝的解释)

形参具备自己的存储空间,和实参的关系是单向传递关系。对形参的赋值不会影响对应的实参

例1:单向传递的示例

1
2
3
4
5
6
7
8
9
10
11
12
void f(int a) //a为形参
{
++a;
cout << a << ' '; //输出行1
}
int main()
{
int x = 100; //x为实参
f(x);
cout << x << endl; //输出行2
}
// 输出结果为101 100 即x的值不变

例2:反例——数据交换的失败

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void myswap(int x, int y)
{
cout << "myswap:" << x << ' ' << y << endl;
int t = x;
x = y;
y = t;
cout << "myswap:" << x << ' ' << y << endl;
}
int main()
{
int a, b;
cin >> a >> b;
cout << "main: " << a << ' ' << b << endl;
myswap(a, b);//传值
cout << "main: " << a << ' ' << b << endl;
}
// 运行中形参可以成功交换,但实参不会交换

可以将形参添加引用如:void myswap(int &x, int &y)可以成功交换。

5. 函数返回值

1
return 表达式;

作用:返回函数的结果。

操作本质:(拷贝)将return后面表达式的值传给指定类型的临时变量输出,不可寻址。

如:临时变量不能做左值,max(a, b) = 2是非法的。

返回值的类型:

  • 如果该函数返回类型不为void,那么函数的调用可以组成函数调用表达式参与运算。

    • 例:cout << max(a, b);
  • 如果该函数返回类型为void,那么函数的调用只能加上分号成为函数语句。

    • 例:print();
  • 如果return后面表达式的类型和函数定义的返回类型不一致,以函数定义类型为准进行转换。

    • 例:返回值类型转换

      1
      2
      3
      4
      5
      int f()
      {
      return 2.5;
      }
      // 返回的临时变量将为2

3.3 变量作用域和生存期

  • 变量除了具备类型、名字和值之外,还有空间和时间上的特性,也被称为变量的作用域和生存期。
  • 根据作用域和生存期的不同,可对变量进行不同形式的分类。

1. C++的内存分布

区域 作用 备注
代码区 存放程序代码指令
堆区 存储动态数据
栈区 存放局部变量 分配栈区时不处理内存,即变量取随机值
全局数据区 存放全局和静态变量 分配该区时内存清零,变量自动初始化为零

2. 变量的作用域

根据作用域不同,将变量分为全局变量和局部变量。

(1) 全局变量

  • 如果一个变量定义的位置不在任何函数或复合语句内,则该变量成为全局变量可以被所有函数或层次访问。
  • 全局变量的作用域:由定义处开始,到所在文件尾部结束,因此也称为文件作用域。
  • 一般情况下,将全局文件定义在文件的开头部分,则所有函数中都可以对全局变量进行读写。

例:全局变量的使用

1
2
3
4
5
6
7
8
9
10
11
int n=100; //n为全局变量
void func()
{
n *= 2;
}
int main()
{
n *= 2; cout<<n<<endl; //输出200
func(); cout<<n<<endl; //输出400
return 0;
}

注:

  • 全局变量的使用应尽量谨慎,过多地使用因为每一个函数都可以更改全局变量,容易导致逻辑和流程的混乱。
  • 全局变量的存储地点在内存的全局数据区,如不赋初始值则自动初始化为0

(2) 局部变量

  • 如果一个变量定义在某个特定的函数或复合语句内,则该变量为局部变量,只能在该函数或层次内被访问。
  • 局部变量的作用域:从变量定义开始,到所在的函数或复合语句的尾部结束,也称为块域
  • 局部变量的存储地点在内存的栈区,如果不初始化就为随机值。

(3) 作用域的覆盖

作用域 是否可以重名
不同函数的局部变量 可以
全局变量和局部变量 可以
不同层次的局部变量 可以
同一函数的局部变量 不可以
同一层次的局部变量 不可以
  • 变量重名在作用域覆盖的区域内遵循局部优先的规则。
  • 如果局部变量覆盖了全局变量,可以使用域作用符::调用同名全局变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int n=100; //全局变量n
int main()
{
int i=200, j=300;
cout << n << '\t' << i << '\t' << j << endl;
{
int i=500, j=600, n; //局部变量n,层次内的变量i, j
n=i+j;
cout<< n << '\t' << i << '\t' << j << endl;
cout<< ::n <<endl; //输出全局变量n
}
n=i+j; //修改全局变量
cout << n << '\t' << i << '\t' << j << endl;
return 0;
}

(4) 局部声明作用域

作用范围仅限声明语句内,因此可省略其参数名。

1
2
int max(int a, int b);
int max(int, int);

3. 变量的生存期

  • 根据生存期不同,将变量分为动态变量静态变量
  • 不同生存期的变量,用存储类型的不同来体现。

(1) 动态变量

当程序运行至变量所在的函数的时候,系统对其进行空间分配;当所在的函数运行结束时,系统收回其存储空间。之前使用的局部变量一般就属于动态变量。

(2) 静态变量

静态变量:当程序开始运行的时候,变量即被创建,并一直生存至程序的结束。之前的全局变量即为静态变量。另可改造普通局部变量的属性,使其成为静态局部变量。

(3) 存储类型

变量定义的完整方式:存储类型 数据类型 变量名 = 初始值

C++中支持的4种不同的存储类型:

① auto
  • 普通的局部变量都属于auto类型,也称为自动变量,属于默认类别,可以省略。
  • 最新版本的C++中对auto的定义已经有所变化。

例:int cauto int c是等价的。(在C++98环境中)

② register
  • register修饰的变量称为寄存器变量,使用方式与auto变量几乎一致,但存储在CPU的寄存器中,不可寻址,不推荐使用。
  • registerC++17及之后被弃用。
③ static

static修饰的变量具备静态属性,根据所修饰变量的作用域不同可以分为静态局部变量静态全局变量

A. 静态局部变量

如果在局部变量上使用static修饰,则成为静态局部变量,其作用域不变依旧为局部,当其所在的块第一次被执行时,系统在全局数据区开辟其空间,直到整个程序结束才释放,即:静态局部变量具有局部作用域,但却具有全局生命期。

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int st()
{
static int t = 100; //仅执行一次,下一次接着上一次的值运算
t ++;
return t;
}
int at()
{
int t = 100; //自动变量,每运行一次销毁一次,下一次又重新生成
t ++;
return t;
}
int main()
{
int i;
for(i=0; i<5; i++)
cout << at() << '\t'; //输出5个101
cout << endl;
for(i=0; i<5; i++)
cout << st() << '\t'; //输出101~105的序列
cout << endl;
return 0;
}

B. 静态全局变量

  • 全局变量本已经具备静态属性:存放在全局数据区内,生存期限为整个程序,并默认初始化为0等。
  • static修饰全局变量,其作用是将该变量的作用域限定在文件内,不能为其他文件调用。
④ extern

敬请期待

3.4 函数的嵌套和递归

在C++中所有的函数地位是平等的,也就是说不允许嵌套定义,即将函数A定义在函数B当中。

嵌套调用是允许的,也就是在函数A中调用在函数B

main函数的特权:是程序运行的起点和终点。

1. 函数的递归

  • 如果在定义函数A中的过程中调用函数A,则称为直接递归调用。
  • 如果在定义函数A中调用函数B,在定义函数B中调用函数A,则称为间接递归。

2. 递归函数注意事项

  • 递归函数的执行分为递推回归两个过程。这两个过程由递归终止条件控制,即逐 层递推,直至递归终止条件,然后逐层回归。
  • 编写递归函数的大致模式:首先判断递归终止条件,如果不终止则展开递归条件。
  • 递归函数容易编写且可读性高,但运行效率极低,应避免随意使用。

3. 递归函数算法示例

(1) 阶乘求解

1
2
3
4
5
6
7
int fac(int n)
{
if(n == 0 || n == 1) //递归终止条件
return 1;
else
return n * fac(n - 1); //递归进行条件
}

运行分析:以fac(5)为例:

n=5,返回的结果为5*fac(4)。在fac(4)得出结果前,函数无法结束,程序转入执行fac(4)

n=4,返回的结果为4*fac(3)。在fac(3)得出结果前,函数无法结束,程序转入执行fac(3)

……

n=1,此时得出结果为1,至此分解部分结束,递归终止。

⑥ 在确认fac(1)的值之后,fac(2)的结果也可以确定为2*1=2

⑦ 在确认fac(2)的值之后,fac(3)的结果也可以 确定为3*2=6

……

⑨ 在确认fac(4)的值之后,fac(5)的结果最终确定为5*24=120,此时综合部分结束,运行完毕。

评论