面向过程的编程风格
传值和传址
这部分内容在C语言中就已经有强调,即函数的传参的细节。目前的参数传递分成两种方式,传址(by reference)和传值(by value)。为了将两者的用法进行说明,需要先解释调用函数时,程序运行的袭击。
当我们调用一个函数时,会在内存中建立起一块特殊区域,成为“程序堆栈(program stack)"。这块特殊区域提供了每个函数参数的储存空间。他也提供了函数所定义的每个对象的内存空间——我们将这些对象称为local object(局部对象)。一旦函数完成,这块内存就会被释放掉,或者说是从程序堆栈中被pop出来。
传值(by value)和传址(by reference)分别的使用方法如下
int swap(int &a, int b)
前者为传址,后者为传值。传值情形下,变量的值会被复制一份,成为参数的局部性定义(local definition),此时改变函数内的值b
,并不会影响main
函数中的变量b
的值。而第二种方法传址&a
,则可以将main
函数中的变量和子函数中的变量关联。
而传址这种用法,和引用的用法是一致的,即
int &rval = ival;
此时变量rval
是一个整型变量,相当于整型变量ival
的别名,(似乎rval的地址就是ival?不太确定),如果改变两个变量任意一个的值,另外一个的值也会跟着变化。我们考虑前面中的函数参数a
,如果按照如下方式调用
swap(c,d);
相当于执行了&a=c
,此时如果改变子函数中变量c
的值,那么主程序中,c
的值也会发生变化。书中的解释是:“当我们以by reference方式将对象作为参数传入时,对象本身并不会复制出另一份——复制的是对象的地址。函数中对该对象进行的任何操作,都相当于对传入的对象进行间接操作。”
除去为了让程序内的计算结果对主程序造成影响外,使用传址的方式也可以提高程序的运行速度,尤其是当传值所复制的变量数据量比较大时。这在子函数调用数组时比较常见(咦,之前不都是直接用指针传地址嘛?还能直接引用的嘛?)
void display(const vector<int> &vec)
声明const
作用的是数组变量vec
,表示数组中不会对变量的值进行修改。在函数中,变量vec
可以像正常的vector数组一样使用。而之前比较常用的指针传参方式如下:
void display(const vector<int> *vec)
不过由于vector本书属于class template,此时如果我们希望调用class中的函数,需要使用箭头,即
size = vec->size();
另外,传址后,子函数就有可能改变所传参数的值,在一些不喜欢参数值变化的场景,传值要比传址更加保险。
作用域及范围
首先子函数中的变量在程序执行结束之后就会销毁,如果返回值返回的是局部对象(local object)的地址,在程序运行时会发生错误。例如
vector<int> fibon_seq(int size)
{
....
vector<int> elems(size);
...
return elems;
}
引用一段原话,“不论以pointer或reference形式将elems返回,都不正确,因为elems在fibon_seq()执行完毕时已不复存在。如果将elems以传值方式返回,便不会产生任何问题;因为返回乃是对象的副本,他在函数之外依然存在。”class变量elems
本身是指针,必须将变量中的取值取出,进行return才能正确执行。
程序中的对象分配的内存,有存活时间储存器(storage duration)或范围(extent)的说法,而对象在程序内的存活区域成为作用域(scope)。函数中的对象为局部性范围(local extent),具有局部作用域(local scope)。而函数以外的声明,具有file scope,即直到文尾都可见,具有静态范围(static extent,这是我翻译的,书上只给了英文)。
同时局部范围和静态范围的变量在内存不同,静态范围是程序执行前就分配好的,前者则是程序执行到调用函数时创建内存空间。这导致静态范围的变量在声明时,会被自动初始化为0,而局部范围的变量则不一定,需要根据编译区别。
动态内存管理
即出去local extent和static extent以外,还有第三种内存分配方式。主要使用new
和delete
两个命令。直接给出使用方法
int *pi;
pi = new int;
pi = new int(1024);
int *pia = new int[1024];
delete pi;
delete [] pia;
对单个变量的声明比较好理解,而动态内存赋初值和数组的定义需要仔细区别一下,前者为圆括号,后者为方括号。而删除单个变量和删除一个数组时也有区别,后者要加方括号。另外,delete变量时,不需要检查指针是否指向空地址,系统会自动进行检查。而如果用户忘记进行delete,就会造成memory leak
内存泄漏。
提供默认参数值
直接给出实例,还是比较好理解的
viod bubble_sort(vector<int> &vec, ofstream *ofil=0)
{
....
(*ofil) << "about to call swap"
....
}
void display(const vector<int> &vec, ostream &os = cout)
{
...
os<< vec[ix]<<' ';
...
}
调用函数时,如果右边已经给出默认值的变量未给定值,则使用默认值,前者代表默认文件流为NULL指针,后者为默认输出到终端。
另外还有两个细节需要强调。其一,默认值的参数不限个数,但是必须全部写在参数声明的右侧。其二,当使用头文件时,头文件中会进行额外的函数声明,这种默认参数不管声明在头文件还是实体中,都是允许的。但是只允许在一处进行默认参数设置,书中建议声明在头文件中。
使用局部静态对象
前面已经提到过,函数中的变量所使用的内存空间都是在程旭运行过程中分配的,在程序执行结束之后,变量就会被销毁,但是有时需要将函数中的变量值保留下来。例如书中给出的斐波那契数列的场景,希望之前计算的数列的前几个值能够保留。为了实现这一功能,直接将数组保存在main函数中也是一个选择,但是这种方式会导致程序高度耦合,给后续维护造成困难。这部分介绍另外一种方法,直接把代码给出来
const vector<int>* fibon_seq(int size)
{
static vector<int> elems;
...
return &elems;
}
使用关键字static
,就可以定义局部静态变量。此时的数组elems
在函数执行结束之后,就不会被销毁。那么此时return
该数组的地址就是有意义的。同时,我们还注意到此时的数组并没有给定长度,vector是支持这种用法的,通过elems.push_back(elems[ix-1]+elems[ix-2])
命令,就可以在当前数组后方补充新的元素,数组的长度是可以动态变化的(那不是和matlab差不多嘛,很方便了)。
inline函数
其实我觉得inline函数非常像宏,不过太菜了也指不出其中的区别,文中对inline函数的执行行为如此描述:“将函数声明为inline,表示要求编译器在每个函数调用点上,将内容展开。面对一个inline函数,编译器可将该函数的调用操作改为以一份函数代码副本代替。这将使我们获得性能改善,其结果等于是把三个函数写入fibon_elem()内,但仍然维持三个独立的运算单元”。具体用法如下:
inline bool fibon_elem(int pos, int &elem)
其中inline
是专门给函数的关键字。另外,要强调一下inline只是一种对编译器提出的请求,编译器会视情况执行请求,即这种加速策略有时无效。
inline函数适合体积小,调用频繁的函数。而定义常常直接塞在头文件中,这是因为编译器必须在调用的时候就将inline函数展开,所以此时的定义必须有效。
提供重载函数
名字起得很高端,其实就是希望同一个函数能够根据不同的输入值有不同的行为,这和前面的默认参数值是类似的。首先给出一个用法,
void display_message(char ch);
void display_message(const string &);
void display_message(const string &, int);
void display_message(const string &, int, int);
同一个函数名,不同的输入值,就可以有不同的实体。强调两点,首先是不同的函数实现所对应的函数声明,全部要在头文件写出。另外是必须是参数列表不同,返回值不同并不能形成重载。
定义并使用模板函数
前面已经大量使用了class template即vector,实际上函数也有类似的需求,函数行为完全一致,只是改变了参数列表的数据类型。此时引入function template(函数模板)就比较方便。给一个实例如下:
template <typename elemType>
void display_name(const string &msg,
const vector<elemType> &vec)
{
···
elemType t = vec[ix];
···
}
在函数中elemType属于占位符,被当做一般的数据类型使用。而定义好函数后,调用函数和一般的函数的调用方式相同,具体的数据类型编译器会自行进行识别:
vector<int> ivec;
display_message(msg,ivec);
另外就是重载和模板函数是可以组合使用的。
函数指针带来更大的弹性
***带来弹性系列,上一章也有,之前是讨论指针,这部分讨论函数指针(pointer to function)。即需要在程序运行过程中动态的调用不同的函数类型时使用,例如如下一组函数
const vector<int> *fibon_seq(int size);
const vector<int> *lucas_seq(int size);
const vector<int> *pell_seq(int size);
const vector<int> *triang_seq(int size);
const vector<int> *square_seq(int size);
我们定义函数指针形式如下:
const vector<int>* (*seq_ptr)(int)
注意这里面的括号并不能去掉,当前代表返回类型是一个vector的指针,而第二个星号代表是这种函数的指针。如果去掉的话,只能代表返回值是指针的指针。那么可以写成如出如下函数:
bool seq_elem(int pos, int &elem,
const vector<int> * (*seq_ptr)(int))
{
const vector<int> *pseq = seq_ptr(pos);
if (!pseq)
{elem=0; return false;}
elem = (*pseq)[pos-1];
return ture;
}
就是说,我们在这个函数的参数中给定需要调用的函数的指针,然后在程序中调用这个指针对应的函数,函数名就用指针名即可,和普通的函数调用并无不同。而函数指针是可以通过函数名赋值的,比如说:
seq_ptr = pell_seq;
同时vector本身不支持初始化,但是他的指针数组支持初始化,而函数指针数组也支持初始化
const vector<int> * (*seq_array[])(int) = {
fibon_seq, lucas_seq, pell_seq,
triang_seq, square_seq, pent_seq
};
此时对数组指针的赋值行为就成了
seq_ptr = seq_array[0];
更进一步,直接使用编号的形式调用不同的函数并不直观,一个解决办法就是引入枚举型(出现了!从来没用过的类型!居然这么用!)
enum ns_type{
ns_fibon, ns_lucas, ns_pell,
ns_triang , ns_square, ns_pent
};
其中标识符ns_type
可有可无,而后面的初始化的枚举项必须和前面初始化的数组中的函数顺序一致。此时枚举型的枚举项在被当做索引时就可以当做普通的数据使用。
seq_ptr = seq_array[ns_pell];
//等价于
seq_ptr = seq_array[2];
设定头文件
头文件的基础作用不解释了太基础了。但是强调两个问题
其一,不同的.c文件中会多次调用同一个头文件,因此就存在重复定义的问题。这也是函数实体不能写在头文件的原因。但是inline函数则是例外,“在每个调用点上,编译器都得取得其定义”。
file scope内定义的对象,可能被多个文件访问,也需要写在头文件中。声明方式如下:
const int seq_cnt = 6;
extern const vector<int> * (*seq_array[seq_cnt])(int)
变量的定义同样存在多次定义出现错误的问题,解决办法就是加入关键字extern
。而第一行的int
类型,则是因为const
关键字,和inline类似,“定义只要一出文件之外便不可见,意味着我们可以在多个程序代码文件中加以定义,不会导致任何的错误”。
其二,头文件有的为双引号,有的为尖括号
#include <stdio.h>
#include "NumSeq.h"
区别在于,如果头文件和包含此文件的程序代码位于同一个磁盘目录下,就使用双引号(一般就是用户自己定义的头文件)。如果在不同的磁盘目录下,我们便使用尖括号(即一些C语言内置的库)。这里直接引用更加专业的回答:“如果此文件被认定为标准的或项目专属的文件,我们便以尖括号将文件名括住;编译器检索此文件时,会先在某些默认的磁盘目录中寻找。如果文件名由成对的双引号括住,此文件便被认为是一个用户提供的头文件;搜索此文件时,会由要包含此文件的文件所在的磁盘目录开始找起。”
菜鸡如我又基本啥都不会,全是新知识。加油!奥利给!