《C++ Primer Plus》(第6版)中文版—学习笔记—函数—C++的编程模块

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

书中程序清单和练习将在https://github.com/linlll/CppPrimePlus发布

复习函数的基本知识

要使用C++函数,必须完成如下工作:

  1. 提供函数定义
  2. 提供函数原型
  3. 调用函数

值得一提的是,C++中对于返回值的类型有一定的限制:不能是数组,但可以是其他任何类型:整数、浮点数、指针,甚至可以是结构和对象!(有趣的是,虽然C++函数不能直接返回数组,但可以将数组作为结构或对象组成部分来返回)。

在C++中,不指定参数列表时应使用省略号,例如

void say_bye(...)	// C++ abdication of responsibility

函数原型确保了以下几点

  1. 编译器正确处理函数返回值
  2. 编译器检查使用的参数数目是否正确
  3. 编译器检查使用的参数类型是否正确。如果不正确,则转换为正确的类型(如果可能的话)

也就是说,如果你的程序有函数原型,其实不管是对你还是对编译器来说,都是有好处的,因为在强制转换的时候,可能会出现精度错误或者溢出的情况,给出原型,就可以先一步检查。
在函数原型中,可以省略函数的标识符,例如以下代码是可以的
但请注意,函数定义必须提供标识符,也就是下述代码第一种

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 *arrint 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正好相反。
如果愿意,其实还可以将*pp都设置为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-风格字符串

回忆一下,字符串的表示方法有以下三种

  1. char数组
  2. 用引号括起的字符串常量(也称字符串字面值)
  3. 被设置为字符串的地址的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;

品,你细品!!

猜你喜欢

转载自blog.csdn.net/weixin_49643423/article/details/107635604