第7章 函数—C++的编程模块
书中程序清单和练习将在https://github.com/linlll/CppPrimePlus发布
复习函数的基本知识
要使用C++函数,必须完成如下工作:
- 提供函数定义
- 提供函数原型
- 调用函数
值得一提的是,C++中对于返回值的类型有一定的限制:不能是数组,但可以是其他任何类型:整数、浮点数、指针,甚至可以是结构和对象!(有趣的是,虽然C++函数不能直接返回数组,但可以将数组作为结构或对象组成部分来返回)。
在C++中,不指定参数列表时应使用省略号,例如
void say_bye(...) // C++ abdication of responsibility
函数原型确保了以下几点
- 编译器正确处理函数返回值
- 编译器检查使用的参数数目是否正确
- 编译器检查使用的参数类型是否正确。如果不正确,则转换为正确的类型(如果可能的话)
也就是说,如果你的程序有函数原型,其实不管是对你还是对编译器来说,都是有好处的,因为在强制转换的时候,可能会出现精度错误或者溢出的情况,给出原型,就可以先一步检查。
在函数原型中,可以省略函数的标识符,例如以下代码是可以的
但请注意,函数定义必须提供标识符,也就是下述代码第一种
const double fun1(const double ar[])
const double fun2(const double *ar)
const double fun1(const double [])
const double fun2(const double *)
函数参数和按值传递
C++通常按值传递参数,这意味着将数值参数传递给函数,而后者将其赋给一个新的变量。C++标准使用参数来表示实参,使用参量表示形参。
有一点需要了解的,对于局部变量,包括形参,也就是在子函数中创建的一切变量,在返回的时候都会被销毁,只有返回的值才是实参,也就是说这些变量对于该函数而言是私有的。例如,在这个子函数中,创建的i
变量将会在返回true
值的时候被程序销毁。
#include <iostream>
bool print(int n);
int main()
{
int n = 10;
bool res = print(n);
}
bool print(int n)
{
int i = 0;
for (; i < n ; i++)
{
std::cout << i << endl;
}
return true;
}
函数和数组
数组
前面有提过,函数不能够把数组作为返回值返回,但是函数是可以接受数组作为参数传入的,我们知道,大多数情况下,C++和C语言一样,也将数组名视为指针。例如int sum_arr(int arr[], int n)
,其中int arr[]
就可以接受数组,我们调用函数如int sum = sum_arr(cookies, ArSize)
其中cookies
为一个整数数组,在传入的时候,其实传入的为数组首位的地址。
这意味着int *arr
和int arr[]
对于处理数组而言是一样的,对于数组而言,两者都可以使用,但是我们需要传入的是个指针的时候,int arr[]
就不能够使用了。
将数组地址传入的好处是什么?这将不用复制整个数组,节约了时间,
指针和const
对于指针,永远是那么的微妙。
首先看以下代码
int age = 39;
const int *pt = &age
该声明指出,pt
指向一个const int
(这里指39),因此不能使用pt
来修改这个值。换句话说,*pt
的值为const
,不能被修改。也就是说,以下代码是不可以被使用的。
*pt += 1;
cin >> *pt;
const
变量的地址只能赋给const
的指针,看一下代码,其实第二种被禁止很好理解,a
变量是个不可修改的变量,但是*b
是可以修改的,这样const
就显得没用了,所以这种情况肯定是要被禁止的。
// VALID
const int a = 10;
const int * b = &a;
// INVALID
const int a = 10;
int * b = &a;
之前提过,非const
指针赋给const
指针是可以的,但是涉及的是一级间接关系,然而进入二级间接关系时,将const
和非const
混合的指针赋值方式将不再安全,请看
const int **pp2;
int *p1;
const int n = 13;
pp2 = &p1;
*pp2 = &n;
*p1 = 10;
这串代码确实是可以改变n
的值的。所以仅当只有一层间接关系(如指针指向基本数据类型)时,才可以将非const
地址或指针赋值给const
指针。
将指针参数声明为指向常量数据的指针有两条理由:
- 这样可以避免由于无意间修改数据而导致的编程错误
- 使用
const
使得函数能够处理const
和非const
实参,否则将只能接受非const
数据
如果条件允许,则应将指针形参声明为指向const
的指针
请看以下代码
int gorp = 16;
int chips = 12;
const int *p_snake = &gorp
*p_snake = 20 //invalid
p_snake = &chips //valid
int gorp = 16;
int chips = 12;
int * const p_snake = &gorp
*p_snake = 20 //valid
p_snake = &chips //invalid
这两段代码指出: const int *p_snake = &gorp
指出*p_snake
才是const
类型,但是p_snake
却不是,也就说,*p_snake
不可以修改,但是p_snake
可以修改;而int * const p_snake = &gorp
正好相反。
如果愿意,其实还可以将*p
和p
都设置为const
类型。如下
int q = 10;
const int * const p = &q;
函数和二维数组
声明:
...
int data[3][4] = {
{
1,2,3,4},{
9,8,7,6},{
2,4,6,8}};
int total = sum(data, 3);
...
int sum(int (*ar2)[4], int size);
int sum(int ar2[][4], int size);
以上有两个声明用于二维数组的参数传递,(*ar2)[4]
参数本身是一个数组,有4个int
值组成。第二种格式和第一种格式含义完全相同,但是可读性更强。
这两种声明方式规定了列数一定,但是对行数没有限制。
注意理解以下代码
ar2 //pointer to first row of an array of 4 int
ar2 + r //pointer to row r (an array of 4 int)
*(ar2 + r) //row r (an array of 4 int, hence the name of an array),
// thus a pointer to the first int in the row, i.e., ar2[r]
*(ar2 + r) + c // pointer int number c in row r, i.e., ar2[r] + c
*(*(ar2 + r) + c) // value of int number c in row r, i.e., ar2[r][c]
这就说明了二维数组就相当于二级指针,而一维数组相当于一级指针。
函数和C-风格字符串
回忆一下,字符串的表示方法有以下三种
char
数组- 用引号括起的字符串常量(也称字符串字面值)
- 被设置为字符串的地址的
char
指针
上述三种的类型其实都是char
指针(准确地说是char *
)。C-风格字符串于常规char
数组之间的一个重要区别是,字符串有内置的结束字符。
以下是一个简单的函数,函数返回了一串字符串中某个字符的数量
unsigned int c_in_str(const char *str, char ch)
{
unsigned int count = 0;
while (*str)
{
if (*str == ch)
count++;
str++;
}
return count;
}
注意const char *str
参数,这个参数作为字符串传入的入口,也可以使用数组表示法进行代替:const char str[]
,两者都是可以的。
返回C-风格字符串的函数
是的,子函数不可以返回数组,但是学习到现在我们可以注意到,数组的传入是利用指针传入的,并非类似形参复制过去,那就可以类比于返回,我们就可以使用指针返回字符串,如下
char *buildstr(char c, int n)
{
char *pstr = new char[n + 1];
pstr[n] = '\0';
while (n-- > 0)
{
pstr[n] = c;
}
return pstr;
}
该函数返回了一串由字符c
组成的一串字符串,并返回,可以注意到,在子函数中,使用到了new
关键字,申请了空间,也就是说在返回的时候,函数是不能够销毁new
的空间上的数值的,这就保证了字符串的完整性。还有一点,字符串作为指针返回,也大大提高了效率。
函数和结构
结构体作为函数返回的类型时,用法与基本类型一样。有一点需要注意的时,当要传入较大的结构体的时候,由于整个复制会花费较大的时间,所以这个时候往往会利用指针传入,比如树结构,一般都是使用二级指针进行运算等操作,这样也更加效率。
函数和string对象
用法于C-风格字符串几乎一致,但是和数组比起来,string对象和结构更相似,可以将string
对象赋值给另一个string
对象。
函数和array对象
模板类array
并非只能存储基本数据类型,它还可储存对象。
递归
递归的思想有助于将问题缩小话,一个可以使用递归的问题,往往可以将问题化解为一个子问题,而父问题由子问题构成,层层递进,但是递归总要由溢出检测,也就是最小子问题,这将终止递归,让函数停止工作,即问题已经解决。对于数学问题,往往可以由一个递归的数学式子来表示,就比如说阶乘。
递归还可以带来一个好处,就是可以优化算法的时间,对于普通的递归,比如阶乘算法,使用递归和线性相乘其实并没有区别,但是对于特定的算法,比如二叉树的遍历算法就可以使用递归进行时间上的优化。
书中对递归的描述篇幅比较少,推荐使用一些算法题库进行编程练习,例如LeetCode,ACM等,在此不再赘述。
函数指针
重头戏
与数据项相似,函数也有地址。函数的地址时存储其机器语言代码的内存的开始地址。
函数指针的出现解决了,这样一类问题:假如要设计一个estimate()
函数,但是我们希望不同的程序员用不同的方式进行使用该函数,就好比LeetCode中测试函数一样,不同的程序员写的算法不同,那么LeetCode时如何进行测试案例的呢?这我不从而知,但是这就恰恰说明这个问题。那么如何解决呢,使用函数指针,将其指针作为参数传入!
完成函数指针,必须能够完成下面的工作:
- 获取函数的地址
- 声明一个函数指针
- 使用函数指针来调用函数
下面我们来看是如何操作的吧
获取函数的地址
获取函数的地址其实很简单,只需要使用函数名即可。但是必须清楚下面两串的区别
process(think);
process(think());
首先,process(think);
中的think
是一个地址,而process(think());
是先执行think()
,process
将使用think()
返回的值作为参数传入,然后进行计算。
声明一个函数指针
声明一个函数指针要注意以下几点
- 要声明的是一个指针
- 函数有传入参数时
- 函数有返回值
请看下述代码
double fun(int); // prototype
double (*p)(int);
首先函数原型不需要标识符,其次上述代码完成了函数指针的声明,显而易见。
使用函数指针来调用函数
请看下述代码
double pam(int);
double (*pf)(int);
pf = pam;
double x = pam(4);
double y = (*pf)(5);
double y = pf(5);
为什么pf
和*pf
都可以使用呢,这是由于历史遗留下来的问题,C++进行了折衷,认为两种都可以使用,我们称之为函数表示法和指针表示法。
请看以下代码
const *f1(const double ar[], int n);
const *f2(const double [], int);
const *f3(const double *, int);
对于函数原型而言,这三者都是完全正确的。那么对于这三个的函数指针应该如何表示呢,其实很简单,只需要记住,1.要声明的是一个指针;2.函数有传入参数时;3.函数有返回值。如下
const *f1(const double *, int); // function prototype
const double * (*p1)(const double *, int) = f1;
auto p2 = f1;
(*p1)(av, 3);
p2(av, 3);
const double * (*pa[3])(const double *, int) = {
f1, f2, f3};
double *px = pa[0](av, 3);
请注意*pa[3]
,其中pa
是一个指针,而后面方括号表明了这是一个数组,而*
表明,pa
是一个包含三个指针的数组。这种情况的时候,auto
就不管用了,自动类型推断只能用于单值初始化,而不能用于初始化列表。
const double * (*(*pb)[3])(const double *, int) = &pa;
auto pb = & pa;
// 函数表示法
(*pd)[i](av, 3);
*(*pd)[i](av, 3);
// 指针表示法
(*(*pd)[i])(av, 3);
*(*(*pd)[i])(av, 3);
这句话就不太好理解了,这是声明了一个可以指向整个数组的指针,注意它不是一个列表,而是单个指针,所以可以使用auto
进行自动类型判断。pd
是一个指向数组的指针,那么*pd
就是一个数组了。
typedef double real;
typedef const double *(*p_fun)(const double *, int);
p_fun p1 = f1;
p_fun pa[3] = {
f1, f2, f3};
p_fun (*pd)[3] = &pa;
品,你细品!!