要点提炼:
1、函数的基本知识
2、函数原型
3、按值传递函数参数
4、设计处理数组的函数
5、使用const指针参数
6、设计处理文本字符串的函数
7、设计处理结构的函数
8、设计处理string对象的函数
9、调用自身的函数(递归)
10、指向函数的指针
正文:
C++自带了一个包含数据的大型库(标准ANSI库加上多个C++类)。
要提高编程效率,可更深入地学习STL标准库和BOOST C++提供的功能。
1、函数的基本知识
【函数如何工作的】
要使用C++函数,必须完成以下工作:
提供函数定义;
提供函数原型;
调用函数。
定义函数:函数分两类,有/无返回值的函数
C++对于返回值的类型有一定的限制:不能是数组,但可以是其他任何类型。【但可以将数组作为结构或对象组成部分返回】
函数是如何返回值的原理:
通常,函数通过将返回值复制到指定的CPU寄存器或内存单元中来将其返回。随后,调用程序将查看该内存单元。返回函数和调用函数必须就该内存单元中存储的数据的类型达成一致。函数原型将返回值类型告知调用程序,而函数定义命令被调用函数应返回什么类型的数据。
函数在执行返回语句后结束。
2、函数原型
函数原型经常隐藏在include文件中。
C++提供函数原型的原因:
原型描述了函数到编译器的接口,即它将函数返回值的类型(有的话)以及参数的类型和数量告知编译器。
若调用程序没有提供匹配参数,原型将让编译器能够捕获这种错误。计算完函数返回值后,将把返回值复制放置在指定的位置——可能是CPU寄存器或内存单元中。然后调用函数将从这个位置获取返回值。由于原型指出了函数的返回类型,因此编译器知道应检索、读取多少个字节以及如何解释它们。若没有这些信息,编译器将只能进行猜测,而编译器不会这样做的。
【若不提供原型主要两个问题:一、编译器必须在文件中查询函数如何定义的,效率不高;二、更严重的问题是,函数甚至可能不在文件中,C++运行单独编译部分文件,然后再将它们组合起来,即这种情况下编译器无权访问函数代码。函数位于库中,也是这种情况。】
函数原型不要求提供变量名,有类型列表就足够了。如 double abc(int);
通常,在原型的参数列表中,可以包含变量名,也可以不包含。原型中的变量名相当于占位符,因此不必与函数定义中的变量名相同。
在C++中,不指定参数列表时应使用省略号:
void say_bye(…);
与C兼容,通常仅当与接受可变参数的C函数(如printf())交互时才需要这么做。
参数或返回值不匹配时,C++中,通常,原型自动将被传递的参数强制转换为期望的类型。
【但注意:后续介绍的函数重载可能导致二义性,因此不允许某些自动强制类型转换。】
自动类型转换并不能避免所有可能的错误。如当较大的类型被自动转换成较小的类型时,有些编译器将发出警告,指出这样可能会丢失数据。
仅当有意义时,原型化才会导致类型转换。如原型不会将整数转换为结构或指针。
在编译阶段进行的原型化被称为静态类型检查(static type checking)。
因此静态类型检查可捕获许多在运行阶段非常难以捕获的错误。
3、按值传递函数参数
C++通常按值传递函数,这意味着将数值参数传递给函数,而后者将其赋给一个新的变量。
因此被调用函数的参数使用的是被传递参数的副本,而不是原来的数据,也就不会影响原来的数据。
用于接收传递值的变量被称为形参。
传递给函数的值被称为实参。
C++标准简化为使用参数(argument)来表示实参,使用参量(parameter)来表示形参,因此参数传递将参数赋给参量。
局部变量(被限制在函数中使用):函数中声明的变量(包括参数),是该函数私有的。在函数被调用时,计算机降为这些变量分配内存;在函数结束时,计算机将释放这些变量使用的内存。
局部变量也称为自动变量,因为它们是在程序执过程中自动被分配和释放的。
【局部变量存储在栈中,使用函数栈内存】
错误的函数声明:
void fufu(float a, b);
正确的函数声明:
void fufu(float a, float b);
可省略变量名,但提供变量名将使原型更容易理解,尤其两个参数类型相同时。
4、设计处理数组的函数
例如:
int sum_arr(int arr[], int n); // arr = array name, n = size
但注意:实际上arr并不是数组,而是一个指针。
大多数情况下,C++和C一样将数组名视为一个指针,指向其第一个元素的地址。
arr == &arr[0]
因此上面正确的函数原型应该是:
int sum_arr(int * arr, int n); // arr = array name, n = size
arr的类型必须是指向int指针,即int *。
前后两个函数头都是正确的。
警告:因为在C++中,当且仅当用于函数头或函数原型中,int *aar和int arr[] 的含义才是相同的。
都指出aar都是一个int指针。
因此注意在其他的上下文中,int *aar和int arr[] 的含义并不相同。例如,不能在函数体中使用int aar[]来声明指针,它只是一个数组声明而已。
两个恒等式:
aar[i] == *(arr + i) // values in two notations
&aar[i] == aar + i // address in two notations
记住,任何指针(包括数组名)加1,实际上是加上了一个与指针指向的类型的长度(以字节为单位)相等的值。对于遍历数组而言,使用指针加法和数组下标是等效的。
注意:传递常规变量时,函数将使用该变量的拷贝【副本】,但传递数组时,函数将使用原来的数组。【因为传递的是数组名,即其第一个元素的地址】。
但实际上,这种区别并不违反C++按值传递的方法,sum_arr()函数仍传递了一个值,这个值被赋给一个新变量,但这个值是一个地址,而不是数组的内容。【即数组参数也是按值传递,只是该值是一个地址】。
好处:参数高效率【节省时间和空间】:将数组地址作为参数可以节省复制整个数组所需的时间和内容。
可能坏处:使用原始数据增加了破坏数据的风险。但C++中可以使用 const 限定符解决这种问题。
指针本身没有指出数组的长度,只有数组名才能通过sizeof来计算数组长度(总元素字节)。
注意:sizeof用于形参aar时,其值只是一个int指针变量的长度如4字节,而不是数组总长度,因为aar此时只是指向一个int元素的指针而已。
接受数组名参数的函数访问的是原始数组,而不是其副本。
const限定符防止函数无意中修改数组的内容。
void show_arr(const double arr[], int n);
aar为只读数据。
C++中将声明const double arr[](编译器)解释为const double * aar。因此该声明实际上是说,arr指向的是一个常量值,不能修改该值内容。
自下而上的程序设计(bottom-up programming):设计从组建到整体进行。【OOP】
传统的过程性编程倾向于自上而下的程序设计(top-down programming)。
这两种方式都有用,最终的产品都是模块化程序。
使用数组区间处理的函数:
// return the sum of an integer array
int sum_arr(const int * begin, const int * end) {
const int * pt;
int total = 0;
for (pt = begin; pt != end; pt++) {
total = total + *pt;
}
return total;
}
指定元素区间(range):传递两个指针,数组开头和数组尾部。
数组尾部指针指的是“超尾”概念【STL标准】来指定区间,即对于数组而言,标识数组结尾的参数将是指向最后一个元素后面的指针。【该尾部指针不是数组的元素】
根据指针加减法规则:在sum_arr()中,表达式end-begin是一个整数值,等于数组的元素数目。
5、使用const指针参数
const修饰指针有两种:
5.1、让指针指向一个常量对象,防止使用该指针来修改所指向的值。
5.2、将指针本身声明为常量,防止改变指针指向的位置。
声明一个指向常量的指针pt:【也称为常量指针即指向常量的指针】
int age = 24;
const int * pt = &age;
说明:pt指向一个const int(这里为24)【可以将非const值赋给const值,但默认是不能反过来,即C++禁止将const的地址赋给非const指针,若非要这么做,可以使用强制类型转换来突破这种限制(使用const_cast运算符)】,因此不能使用pt来修改这个值。换句话说,*pt的值为const,不能修改。
可以通过age变量来修改age的值,但不能使用pt指针来修改它。
四个重要概念:
(1)非const地址可以赋给非const指针;
(2)const地址可以赋给const指针;
(3)当且仅当非const地址上的数据类型本身并不是指针时,才可以将非const地址赋给const指针,否则禁止这么操作【即若非const数据类型是指针时不能将其地址赋给const双指针变量】;
(4)C++禁止将const的地址赋给非const指针,若非要这么做,可以使用强制类型转换来突破这种限制(使用const_cast运算符)。
尽可能使用const:
将指针参数声明为指向常量数据的指针【const typeName * pointerName】有两条理由:
(1)可以避免由于无意间修改数据而导致的编译错误;
(2)使用const使得函数能够处理const和非const实参,否则将只能接受非const实参数据。
若条件允许,则应将指针形参声明为指向const的指针。
const typeName * pointerName:【常量指针,是一个指针即指向const的指针(即指向常量地址的指针),const修饰(typeName *)类型指针名】
指向const地址的指针只能防止修改指针指向的值,而不能防止修改指针变量的值,即可以将新的数据地址赋给该指针变量。
常量指针,即指向常量的指针,指针指向的内容不能改变,但是地址可以改变。
typeName * const pointerName:【指针常量(const指针),指针是一个常量(不可修改指针指向的值),const修饰指针变量名】
const指针可以修改指针指向的值,但不能修改指针变量的值。
指针常量,即指针本身是个常量,是指针指向的位置不能改变,但是指向的对象本身是可以改变的。
const typeName * const pointerName:【指向const的const指针】
不能修改指针指向的值,也不能修改指针变量的值【即不能修改指针指向的变量地址】。
通常,将指针作为函数来传递时,可以使用指向const的指针来保护数据。但注意,只能只有一层间接关系时才能使用这种技术(只能用于指向基本数据类型的指针)。即若实参是指针或指向指针的指针,则不能使用const。
函数和二维数组参数:
int data[3][4] = {{, 2, 3, 4}, {2, 3, 4. 5}, {3, 4, 5, 6}};
int total = sum(data, 3);
data是一个数组名,有3个元素,每个元素本身是一个数组,有4个int值组成,data的类型是指向由4个int组成的数组的指针。
因此sum原型:
int sum(int (*arr)[4], int size);
其中的括号是必不可少的。因为下面的声明将声明一个由4个指向int的指针组成的数组,而不是由一个指向由4个int组成的数组的指针。另外,函数参数不能是数组:
int *arr[4] // invalid
还有另一种格式与上述原型的含义完全相同,但可读性更强:
int sum(int arr[] [4], int size);
arr[] 类似于 *arr,都指出,arr是指针而不是数组。另外,指针类型指出,它指向由4个int组成的数组。因此列数不需要传递。
访问该二维数组中元素的方式:
arr[r][c] == ((arr + r) + c) // same thing
不能使用const修饰参数名,因为它是指向指针的指针。
6、设计处理文本字符串的函数
C-风格字符串由一系列字符组成,以空值字符结尾。
字符串参数传递的是第一个字符的地址,其类型是char指针(准确的说是char*),可以使用const来禁止对字符串参数进行修改。
字符串常量(即C-风格字符串【结尾隐式包括空字符\0】)与常规char数组之间的一个重要区别是,字符串有内置的结束空字符(包含字符,但不以空值字符结尾的char数组只是数组,而不是字符串)。
处理字符串中字符的标准方式:
while (*str) { // quit when *str is '\0'
statement
str++;
}
空值字符串的字符编码为0即false。
返回C-风格字符串的函数
注意:函数无法返回一个字符串,也无法返回一个数组,但可以返回一个字符串的地址,这样效率更高。
// builds string made of n c characters
char * buildstr(char c, int n) {
char * pstr = new char[n + 1];
pstr[n] = '\0'; // teminate string
while (n-- > 0){
pstr[n] = c; // fill rest of string
}
return pstr;
}
创建包含n个字符的字符串,需要存储n+1个字符的空间,以便能够存储空值字符。
警告:变量pstr的作用域为buildstr函数内,因此该函数结束时,pstr(而不是字符串)使用的内存将被释放。即存储pstr指针变量的内存将被释放,而不是它指向的字符串内容的内存被释放【字符串内容的内存是通过new来创建的未命名的内存空间,只是将该空间的开始地址赋给了该指针变量】。
char * ps = buildstr(ch, times);
但由于函数返回了pstr的值,因此调用函数还是可以正确通过新指针ps来访问新建的字符串。
注意,此处描述的返回的pstr的值为什么是正确的呢,原因就是返回的pstr的值,实际上是它指向的字符串内容的内存地址,该地址并没有被释放,只是释放了pstr变量存储本身变量的内存,因此调用函数使用完后必须记得释放字符串内存。
【可以使用构造函数和析构函数来处理这些细节】
7、设计处理结构的函数
可以按值传递结构,就像普通变量那样,函数将使用原始结构的副本。
函数也可以返回结构。
获取结构的地址,使用地址运算符&。 C++中该运算符还可以用于引用变量。
按值传递结构有一个缺点:
如果结构非常大,则复制结构将增加内存要求,降低系统运行的速度。
因此推荐传递结构的地址,然后使用指针来方法结构内容。
另外,也可以使用第三种选择——按引用传递。【后续介绍】
传递和返回结构:
(>>):此为抽取运算符
类运算符是通过函数实现的。
传递结构的地址而不是整个结构以节省时间和空间。使用指向结构的指针。
访问结构或对象成员:数据对象指针使用箭头间接成员运算符(->),数据对象使用句点成员运算符(.)。
可以将结构对象赋值给另一个结构对象,但不能将数组对象赋值给另一个数组对象。
8、设计处理string对象的函数
string list[SIZE];
getline(cin, list[i]);
void display(const string sa[], int n) {
string s = sa[0];
}
const string sa[] 可以写成: const string * sa
在C++中,类对象时基于结构的。可按值将类对象传递给函数,函数将使用原始对象的副本。
另外,也可以传递指向对象的指针,使函数能够修改原始对象。
std::array<double, 4> exp;
void show(std::array<double, 4> da); // da an object
void fill(std::array<double, 4> * pa); // pa a pointer to an object
const std::array<std::string, 4> = {“aa”, “ss”, “dd”, “ff”};
可以使用数组表示法访问array对象的元素值。如(*pa)[i],必须要括号,运算符优先级
9、调用自身的函数(递归)
函数自己调用自己(但C++中不允许main()函数调用自己),被称为递归。
包含一个递归调用的递归,通用格式:
void recurs(argumentlist) {
statements1
if (test) {
recurs(argumentlist)
}
statements2
}
test最终将为false,调用链将断开。
程序说明:若recurs()进行了n次递归调用,则第一个statements1部分将按函数调用的顺序执行n次,然后statement2部分将以与函数调用相反的顺序执行n次。进入n层递归后,程序将沿进入的路径返回。
注意:每个递归调用都创建自己的一套变量。【函数栈】
包含多个递归调用的递归:
在需要将一项工作不断分为两项较小的、类似的工作时,递归非常有用。
递归方法有时被称为分而治之策略【divide-and-conquer strategy】。
【绘制刻度尺例子核心分割代码】:
void subdivide(char ar[], int low, int high, int level) {
if (level <=0){
return;
}
int mid = (high + low) / 2;
ar[mid] = '|';
subdivide(ar, low, mid, level -1);
subdivide(ar, mid, high, level -1);
}
调用次数将呈几何级数增长。即调用一次导致两个调用,然后导致4个调用,以此类推。6层可以填充64个元素【2^6 = 64】。
若要求的递归层次很多,则这种递归方式将是一种糟糕的选择。
10、指向函数的指针:函数指针,首先它是一个指针,指向一个函数。
函数也有地址。
函数的地址是存储其机器语言代码的内存的开始地址。通常,这些地址对程序有用,对用户没啥用。
可以编写将另一个函数的地址作为参数的函数。这样第一个函数将能够找到第二个函数,并允许它。
与直接调用另一个函数相比,这种方法很笨拙,但它允许在不同的时间传递不同的函数的地址,这意味着可以不同的时间使用不同的函数。
函数指针需要完成的工作:
(1)获取函数的地址;
(2)声明一个函数指针;
(3)使用函数指针来调用函数。
直接使用函数名,即可获取函数的地址。
声明指向函数的指针时,必须指定指针指向的函数类型。即声明应指定函数的返回类型以及函数的特征标(参数列表)。也就是说,声明应像函数原型那样指出有关函数的信息。
如:
double pam(int); // prototype
其指针类型声明:
double (*pf) (int); // pf points to a funstion that takes one int argument and that returns type double
重要概念:与pam()声明类似,这是将pam替换成了 (*pf)。由于pam是函数,因此 (*pf) 也是函数。而如果 (*pf) 是函数,则 pf 就是函数指针。
通常,要声明指向特定类型的函数的指针,可以先编写这种函数的原型,然后用 (*pf) 替换函数名。这样 pf 就是这类函数的指针。
由于运算符优先级问题,必须使用括号括起 *pf 。否则如下:
double *pf (int); // pf() a funstion that returns a pointer-to-double
这样没括号的声明,*pf(int)意味着pf()是一个返回double类型指针的函数,而 (*pf)(int)意味着pf是一个指向函数的指针。
赋值:【直接赋值函数名】
pf = pam; // pf now points to the pam() function
使用指针来调用函数:
使用指针来调用被指向的函数。由于 (*pf) 也是函数,则只需将它看做函数名即可:
double y = (*pf)(5); // call pam() using the pointer pf
实际上,C++也运行像使用函数名那样使用pf:
double y = pf(5); // also call pam() using the pointer pf
第一种格式有很好的提示——代码正使用函数指针。
【虽然第二种格式在逻辑上存在冲突,但C++至少允许了】
深入探讨函数指针:
例:【它们的特征标和返回类型相同】
const double * f1(const double ar[], int n);
const double * f2(const double [], int n);
const double * f3(const double *, int n);
返回值为常量指针,即返回的是一个指针,指向double类型的常量。即不能修改指向的常量值。
声明一个指针,指向这三个函数之一。则只需将目标函数原型中的函数名替换为 (*pf):
const double * (*pf3)(const double *, int n);
也可在声明的同时进行初始化:
const double * (*pf3)(const double *, int n) = f1;
也可以使用C++自动类型推断功能简化为:
auto pf2 = f2; // C++ automatic type deduction
声明一个指向三个函数指针的数组:【并初始化】
const double * (pa[3])(const double , int) = {f1, f2, f3};
pa是一个包含三个元素的数组,即pa[3]。运算符[]的优先级高于。因此pa[3]表明pa是一个包含三个指针的数组。每个指针指向的是:特征标为const double *, int,且返回类型为const double *的函数。
因此,pa是一个包含三个指针的数组,其中每个指针都指向这样的函数,即将const double * 和 int 作为参数,并返回一个const double * 。
警告:这里不能使用auto自动推断类型。自动推断类型只能用于单值初始化,而不能用于初始化列表。但声明同类型的数组就可以:【因为pa已经声明好了】
auto pb = pa;
pa 是指向(函数指针数组中第一个元素)函数指针的指针。
使用:[* 解除引用运算符优先级低]
const double * px = pa[0](av, 3);
const double * py = (*pa[1])(av, 3);
double x = *pa[0](av, 3);
double y = *(*pa[1])(av, 3);
创建指向整个数组的指针。
数组名pa是指向函数指针的指针,因此指向数组的指针将是这样的指针,即它指向数组指针的指针。
第一种简单声明:
auto pc = &pa;
完整显示的声明:
const double * (*(*pd)[3])(const double *, int) = &pa;
将pa数组名替换为 (*pd) 。即使用 * 号指出它是个指针,而不是数组。运算符优先级必须使用括号。
*pd[3] // an array of 3 pointers
(*pd)[3] // a pointer to an array of 3 elements
即pd是一个指针,指向一个包含三个元素的数组。
使用pd方式:pd是一个指向数组的指针,则 *pd 就是数组,而 (*pd)[i]是数组中的元素即函数指针。
因此:
const double * px = (pd)[i](av, 3);
const double * py =(( *pd)[i])(av, 3); // 指针调用函数语法
double x = *(*pd)[i](av, 3);
double y = ((*pd)[i])(av, 3);
警告:注意,pa(数组名表示地址)与&pd的区别:即大多数情况下,pa表示数组第一个元素的地址即&pa[0],因此它是单个指针的地址。而&pa是整个数组(即三个元素【指针块】)的地址。
从数值上说,pa和&pa的值相同,但它们的类型不同。
如区别一在于:
pa + 1表示下一个元素的地址。
&pa + 1为数组pa后面的一个12字节内存块的地址(假设地址为4字节)。
另一个区别:
得到第一个元素的值,只需要对pa解除引用一次,但需要对&pa解除两次引用。
**&pa == *pa == pa[0]
指向函数指针数组的指针并不少见。实际上,类的虚方法实现通常都采用了这种技术。好消息是,这些细节由编译器处理。
使用typedef进行简化:
除auto外,C++还提供了其他简化声明的工具。关键字typedef创建类型别名。
typedef typeName aliasName; // makes alias name for typeName such as double
因此将别名当做标识符进行声明,并在开头使用关键字typedef。因此可声明如下的函数指针类型的别名:
typedef const double * (*p_fun)(const double *, int); // p_fun now a type name
p_fun p1= f1; // p1 points to the f1() fuction
然后别名简化代码:
指针数组:
p_fun pa[3] = {f1, f2, f3}; // pa an array of 3 function pointers
数组指针:
p_fun (*pd)[3] = &pa; // pd points to an array of 3 function pointers
本章结尾:
几个重要概念简单记忆:
常量指针:指向常量的指针。
指针常量:指针本身是个常量。
函数指针:指向函数的指针。
指针函数:它是一个函数,即返回指针的函数。
数组指针,即指向数组的指针。
指针数组,即指针作为元素的数组。
函数定义是实现函数功能的代码。
函数原型描述了函数的接口:传递给函数的值的数目和类型以及函数的返回类型。
按值传递参数:函数定义中的形参时新的变量,使用的是原始数据的副本,通过拷贝,保护了原始数据的完整性。
C++将数组名参数视为数组第一个元素的地址。从技术上讲,这仍然是按值传递的,因为指针是原始地址的拷贝,但函数将使用指针来访问原始地址上的数据,即可访问原始数组的内容。
const修饰符:不用对基本类型的函数参数使用const限定符。因为是按值传递,使用的是原始数据的副本。
C++提供3中C-风格字符串的表示方法:字符数组(char[])、字符串常量(“xxx”)、字符串指针(char )。它们的类型都是 char (char指针)。因此被作为 char* 类型参数传递给函数。