第7章 函数——C++的编程模块

定义函数


  • 可以将函数分成两类:没有返回值的函数和有返回值的函数
  • 对于有返回值的函数,必须有返回语句
  • C++对于返回值的类型有一定限制:不能是数组,但可以是其他任何类型——整数、浮点数、指针、结构体和对象。
  • 函数的返回值过程:
    • 函数通过将返回值复制到指定的CPU寄存器或内存单元中来将值返回。
    • 调用程序将查看该内存单元。
  • 函数在执行返回语句后结束。如果函数包含多条返回语句,则函数在执行遇到的第一条返回语句后结束

函数原型和函数调用


1
2
3
4
#include<iostream>

void cheers(int n);
double cube(double x);
  • 原型描述了函数到编译器的接口,也就是说,它将返回值的类型以及参数的类型和数量告诉了编译器。
  • 原型的功能:
    • 编译器正确处理函数的返回值
    • 编译器检查使用的参数数目是否正确
    • 编译器检查使用的参数类型是否正确,如果不正确,则转换成正确的类型(如果可能的话)。
  • 仅当有意义时,原型化才会导致类型转换。
  • 在编译阶段进行的原型话被称为静态类型检查(static type checking)

函数如何使用指针来处理数组

  • C++和C一样,也将数组名视为指针。C++将数组名解释为其第一个元素的地址。
  • 规则的例外:
    • 数组声明使用数组名来标记存储位置
    • 对数组名使用 sizeof 将得到整个数组的长度(以字节为单位)
    • 将地址运算符 & 用于数组名时将返回整个数组的地址。
  • 为防止函数无意中修改数组的内容,可在声明形参时使用关键字 const
    • 这并不意味着原始数组必须是常量,而只是意味着不能在函数中使用数组指针变量来修改这些数据。(如果尝试修改,则编译器会报错)

指针和 const

  • 可以用两种不同的方式将 const 关键字用于指针。
    • 让指针指向一个常量对象,这样可以防止使用该指针来修改所指向的值
    • 将指针本身声明为常量,这样可以防止改变指针指向的位置
1
2
int age = 39;
const int *pt = &age;
  • pt 指向一个 const int ,因此不能使用 pt 来修改这个值。
  • 可以直接通过 age 变量来修改 age 的值,但不能使用 pt 指针来修改它
1
2
*pt = 20; // INVALID bevause pt points to a const int
age = 20; // VALID because age is not declared to be const
  • 不能将 const 变量的地址赋给常规指针

1
2
3
4
5
const fload g_earth = 9.80;
cosnt float * pe = &g_earth; // VALID

const float g_moon = 1.63;
float * pm = &g_moon; // INVALID
  • 第一种情况:既不能用 g_earth 来修改值 9.80,也不能使用 pe 来修改。C++禁止第二种情况,因为不能用 pm 来修改 g_moon

  • 如果涉及一级间接关系,则将非 const 指针赋值给 const 指针是可以的
1
2
3
int age = 39; // age++ is a valid operation
int* pd = &age; // *pd = 41 is a valid operation
const int* pt = pd; // *pt = 42 is a invalid operation
1
2
3
4
5
6
const int **pp2;
int *p1;
const int n = 14;
pp2 = &p1; // not allowed but suppose it were
*pp2 = &n; // valid, both const, but sets p1 to point at n;
*p1 = 10; // valid, but changes const n
  • 仅当只有一层间接关系(如指针指向基本数据类型时),才可以将非 const 地址或指针赋值给 const 指针。

  • 禁止将常量数组的地址赋给非常量指针意味着不能将数组名作为参数传递给使用非常量形参的函数。
1
2
3
4
const int cArr = {1, 2, 3, 4};
int sum(int arr[], int n)
...
int j = sum(cArr, 4); // not allowed
  • 禁止将指针参数声明为指向常量数据的指针有两条理由:
    • 可以避免由于无意间修改数据而导致编程错误
    • 使用 const 使得函数能够处理 const 和非 const 实参,否则只能接受非 const 数据。如果条件允许,则应将指针形参声明为指向 const 的指针。

函数和结构

  • 定义结构体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct travel_time {
int hours;
int mins;
};

travel_time sum(travel_time t1, travel_time t2) {
const int Mins_per_hr = 60;
travel_time total;

total.mins = (t1.mins + t2.mins) % Mins_per_hr;
total.hours = t1.hours + t2.hours + (t1.mins + t1.mins) % Mins_per_hr;

return total;
}

其他定义结构体的例子

1
2
3
4
5
6
7
8
9
struct polar {
double distance;
double angle;
};

struct rect {
double x;
double y;
};

  • while 循环同时读取两个变量
1
2
3
while (cin >> rplace.x >> rplace.y) {
cout << rplace.x << ' ' << rplace.y;
}
  • cin 是 istream 类的一个对象。抽取运算符 >> 被设计成使得 cin >> rplace.x 也是一个 istream 对象。

传递结构的地址 (结构体指针)


  • 假设要传递结构的地址而不是整个结构以节省时间和空间,则需要修改函数:

    • 调用函数时,将结构的地址 &pplace 而不是结构本省 pplace 传递给函数

    • 将形参声明为指向 polar 的指针,即 polar* 类型。由于函数不应该就该结构,因此使用了 const 修饰符

    • 形参是指针而不是结构,因此应使用间接成员运算符 -> ,而不是 .

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      void show_polar (const polar* pda) {
      using namespace std;
      const double Rad2deg = 57.29577951;

      cout << "distance = " << pad->distance;
      cout << ", angle = " << pad->angle * Rad2deg;
      cout << " degrees\n";
      }

      int main() {
      ...
      show_polar(&pplace);
      }
    • 函数使用的是指针,是的函数能对原始结构进行操作

函数和 string 对象


  • 函数读取 string 对象数组
1
2
3
4
5
6
7
int main() {
const int SIZE = 5;
string list[SIZE];
for (int i = 0; i < SIZE; i++) {
getline(cin, list[i]);
}
}

函数和 array 对象


  • array 对象的定义
1
2
std::arrray<double, 4> expenses;
fill(&expenses, 0);

递归


函数指针

  • 与数据项相似,函数也有地址。函数的地址是存储其机器语言代码的内存的开始地址。
  • 可以编写将另一个函数的地址作为参数的函数。这样第一个函数就能找到第二个函数,并运行它。

声明函数的指针

1
2
double pam(int); // 函数原型
double (*pf)(int); // 函数指针 pf, 函数输入为一个 int 数字,输出为一个 double

函数的传递 - 函数的地址与函数的返回值

1
2
process(think); // passes address of think() to process()
thought(think()); // passes return value of think() to thought

函数指针的声明和赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
double pam(int)
double (*pf)(int);
pf = pam;

int ted(int)
int ned(double)
pf = ted; // invalid
pf = ned; // invalid

const double * f1(const double ar[], int n);
const double * (*p1)(const double *, int);
p1 = f1;

auto p1 = f1;
  • 使用指针来调用函数
1
2
3
4
5
6
7
8
9
double best(int)

void estimate(int lines, double (*pf)(int)) {
cout << (*pf)(lines) << " hour(s)\n";
}

int main() {
esitimate(code, best);
]
1
2
3
4
5
double pam(int);
double (*pf)(int);
pf = pam;
double x = pam(4); // call pam() using the function name
double y = (*pf)(5); // call pam() using the pointer pf
  • 函数指针的数组
1
const double * (*pa[3]) (const double *, int) = {f1, f2, f3};
  • 这里不能使用 auto ,自动类型推断只能用于单值初始化,而不能用于列表初始化

深入探讨函数指针

1
2
3
4
5
6
7
8
9
const double * f1(const double ar[], int n);
const double * f2(const double [], int);
const double * f3(const double *, int);

const double * (*p1)(const double *, int) = f1;
auto p2 = f2; // c++11 automatic type deduction

cout << (*p1)(av,3) << ": " << *(*p1)(av, 3) << endl;
cout << p2(av,3) << ": " << *p2(av, 3) << endl;
  • 这里 p1, p2 都调用了指向的函数。在每条 cout 语句中,前半部分显示存储函数返回值的地址。后半部分为存储在这些地址里的实际返回值。

  • 指向函数数组的指针初始化

1
2
3
4
const double * (*pa[3]) (const double *, int) = {f1, f2, f3};

auto pc = &pa; // c++ 11 automatic type deduction
const double *(*(*pd)[3])(const double *, int) = &pa;

用 typedef 进行简化


1
2
3
4
5
6
7
typedef double real; // make real another name for double

typedef const double *(*p_fun)(const double *, int); // p_fun now a type name
p_fun p1 = f1; // p1 points to the f1() function

p_fun pa[3] = {f1, f2, f3};
p_fun (*pd)[3] = &pa;