【C++系列小结】面向过程的编程风格
前言
编程语言有面向过程和面向对象之分,因此编程风格也有所谓的面向过程的编程和面向对象的编程,并且语言的性质不会限制编程的风格.
这里主要说一以下向过程的编程.
“面向过程”(Procedure Oriented)是一种以过程为中心的编程思想。
C语言是面向过程的编程语言,可是依旧能够写出面向对象的程序,相同C++也当然能够写出面向过程的程序咯。
假设我们把全部的程序代码都写在一个main函数里面。那么这个程序显然会显得非常不和谐吧。
理想一点的做法是我们把一些看起来和main函数逻辑上关联能够独立分开的、在高层次的抽象上有通用的操作的代码分离出来,形成调用函数。而这即是面向过程的编程。
面向过程的编程,即将函数独立出来的优点有三:
1、以一连串函数调用操作代替反复编写同样的程序代码,能够使程序更易读。
2、能够在不同的程序中使用这些高层次抽象封装的程序,比方之前的SICP中的抽象编程中的样例比比皆是。
3、能够依据功能和过程将项目分块,更easy将工作协作给小组team完毕。
一、怎样编写函数(How to write a function?)
以一个样例驱动解说:
这样一个关于fibonacci数列的程序,用户询问概数列的第几个元素是什么。程序回答出该问题。
显然计算fibonacci数列指定元素大小的这部分能够抽象出函数。
怎样编写这个函数呢?
首先须要了解函数定义包含的内容:
四部分:
1、返回类型:即函数运行完如有须要返回给调用者的数据,其数据的数据类型
2、函数名:即是函数的名字。建议名字跟函数功能相关
3、參数列表:即是函数和调用者之间假设须要数据连接。接受调用者传递的数据。则是=须要有參数列表
4、函数体:即是函数的主体运行部分
详细各部分的含义就不再多说了吧。。。不懂的请自行google,毕竟不可能把全部的东西都说了。
是使用函数的时候须要注意的是函数必须先被声明。然后才干被调用。这个道理非常明确,就是你必须要想别人说你的名字,然后别人须要你的帮助的时候才干找到你(依据你的名字)。
函数的声明让编译器得以检查兴许出现的函数的调用方式是否正确(參数的个数、类型是否匹配etc…)。函数声明仅仅须要声明函数原型就可以。不须要函数体部分。但必须要函数返回类型,函数名,參数类表(空是void)。
详细的怎样fibonacci数列程序的语句就不再写了,这个看了fibonacci数列原理就非常easy写出来。
在编写函数体的时候注意在函数的最后写上return语句,返回给调用者期待的数据。
同一时候我们须要说的另一个关于程序健壮性的问题。即是我们编写的函数尽管是逻辑上对的。可是面对用户千奇百怪的操作,他非常有可能会挂掉。
这就须要我们去处理这些看起来不合理的操作,毕竟我们的程序是要为用户服务的。我们必须站在大多数的用户的角度考虑我们的程序将要遇到什么问题,然后解决它。
首先可能遇到的问题是用户不合理的请求,比方上面样例中,用户询问第-10或者2.5位置的元素。那么显然程序不可能给出答案,所以,我们就须要提示用户请求不对,并要求其输入正整数位置的元素。
其次用户假设询问第10000位置的元素的大小的时候,我们的程序也会挂掉,由于这个数字太大,我们所採用的主要的数据类型都无法表示出来,那么就须要为我们的程序“设限”,就是要先向用户声明。我们的程序能够做什么。能够做到什么程度。这样算是一个负责任的声明吧。
在用户超出程序的功能范围的时候提示用户就可以。
上面说过要在程序的最后写return语句,事实上这种说法不太准确。更加严谨恰当的说法是:在每一个函数的每一个可能的退出点写return语句。函数的退出点就是运行完某一条语句后。该函数将运行完成的位置。
(假设返回数据类型是void,能够省略不写return语句,可是个人建议写空return,这样使函数更加完整,能够非常清楚知道函数的退出点,有始有终吧)
二、调用函数(Invoking a Function)
本节须要了解传值(by value)和传址(byreference)的參数传递方式。
关于这个非常多人应该都了解。假设不了解,我们通过一个程序来看一下:
#include <iostream>
void swap (int a, int b);
int main()
{
int a = 1;
int b = 2;
swap(a ,b);
printf("After swap:%d,%d\n", a ,b);
return 0;
}
void swap(int a, int b)
{
int temp = a;
a = b ;
b = temp;
printf("In function of swapa & b: %d,%d\n", a ,b);
return;
}
程序的执行结果例如以下:
显然,在main函数中的a,b在运行完swap函数之后并没交换,可是在swap内部确实交换的。
我们理所当然的觉得,我们把a,b传递给swap函数,他执行结束交换语句后,a和b的值也会改变,可是程序的执行结果却并不是如此,因此我们须要问自己这种问题:我们传递的真的a和b么?
这就涉及到程序的执行时结构。
当我们在调用一个函数时候,会在内存中建一个特殊的区域。成为“程序堆栈”。这块空间提供了每一个參数的存储空间。
堆栈是两种不同的内存结构。堆和栈。他们有着不同的使命和特点。
详情參:http://www.cppblog.com/oosky/archive/2006/01/21/2958.html,堆栈的差别。
堆栈区域也提供了函数定义的每一个对象的内存空间——我们将这些对象称之为“局部对象”,一旦函数完毕。这些内存区域便会被释放掉,即是这些区域中的数据也不会存在。
因此。我们所谓的将a,b传入swap。并不是真的将我们在main函数中定义的a,b传入。而仅仅是传入其值的大小,在swap函数内部建立了相应的数据拷贝而已。简单地说就是我们把a,b的大小告诉了swap函数,然后swap函数在其内部申请了两个同样类型的空间A,B,来存着这样两个大小的数据。然后在swap运行的数据操作都是操作的A,B相应的空间的数据,所以。这swap中,数据被交换了,可是在main中并没有被交换。
这就是传值的实质。
那么怎样避免传值。真正的改变我们的a和b呢?
要改变a和b,我们要知道怎样去定位和a和b,就是怎样在swap中找到我们在mian函数中定义的a和b,仅仅有找到才干交换他们。
怎样找到呢?显然假设你要找一个人。我们知道他的地址的话,就一定能够找到他(非钻牛角也没办法;))。程序也一样,要找到a,b我们仅仅须要知道a。b的地址就能够了,因此我们能够将他们的地址传递给swap函数。这就可真正的交换啦。
例如以下:
#include <iostream>
void swap_value (int a, int b);
void swap_refer(int *a, int *b);
int main()
{
int a = 1;
int b = 2;
int *pa = &a;
int *pb = &b;
swap_value(a ,b);
printf("After by valueswap: %d,%d\n", a ,b);
swap_refer(pa, pb);
printf("After by referenceswap: %d,%d\n", a ,b);
return 0;
}
void swap_value(int a, int b)
{
int temp = a;
a = b ;
b = temp;
printf("In function ofswap_value a & b: %d,%d\n", a ,b);
return;
}
void swap_refer(int *a, int *b)
{
int temp = *a;
*a = *b;
*b = temp;
printf("In function ofswap_refer a & b: %d,%d\n", *a ,*b);
}
执行结果例如以下:
显然此时的a,b被真正的交换了。
上面的样例我们參数是指针类型数据,传递的是指针。即是数据的地址。
在C++中我们也能够传递參数为引用的类型。在此不再演示样例。
我们要说的是Reference的语义:
他扮演外界与对象之间一个间接的手柄的角色。在类型名称和reference之间插入一个&符号。便是声明了一个reference。(具体的引用定义。使用方法神马的不再具体赘述)
而引用和指针数据实在是很相似的东西。引用能够说是数据的还有一个名字,引用里面没有存储数据,仅仅是一个对源对象的指向,使用对象的引用和使用该对象有一样的效果。
而指针參数和reference參数更重要的差异是,pointer可能是指向某个实际的对象,当我们在提领pointer的时候,必需要确定该pointer的值非0,即是确实指向了某个对象,否则可能会引起指针异常。可是reference则不必。
因此在函数内操作数据的时候。假设只须要在函数内部改变參数的值,那么使用传值就够咯。
在此还须要注意的一个问题就是作用域的问题。
事实上更准确的说法是对象的生存期和作用和范围。
这个一般的原则应该都知道,就是全局对象和局部对象的生存期和作用范围(如其名)。
内置类型的对象,假设定义在file scope内,必然会初始化为0,假设定义在local scope之内。则除非显示指定初值。否则不会被初始化。
除了file scope和local scope。还有第三种生存期的形式dynamicextent。即动态范围。
其内存是上面所说的堆,heap memory(堆内存)。这样的内存必须有程序猿来操作。
之所以被称之为dynamic,仅仅由于在程序中能够动态的添加,new出新的内存。直至我们显示的delete才会释放。
假设我们一直new,而没有delete,就会导致内存泄露和溢出。
。这是程序崩溃的主要原因之中的一个。
三、提供默认的參数值(Providing Default Parameter Values)
在使用“參数传递”作为程序间的沟通方式的同一时候,我们非常easy就会面临參数默认值的问题。由于存在着不须要输入所有參数值的情况。
默认參数值的声明方法是:
void swap_refer(int &f, int *a, int *b = 3)
此时的第三个參数默认是3。
假设要把默认值置为0,则必需要是pointer类型,由于reference不同于pointer不能被设置为0。因此reference必须代表某个对象。
关于默认值的提供,有两个非常不直观的规则。第一个规则,默认值的解析操作有最右边開始,假设我们提供了默认值,那么这一參数右側的全部參数都必须默认值。第二个规则是默认值仅仅能指定一次,能够在函数的声明处,能够在函数定义处。可是不能再两个地方同一时候指定。(推荐声明的时候)。
四、使用局部静态对象和声明inline函数
在一些须要反复计算的函数中,由于在函数的内部定义的变量集合都是局部变量。每次调用结束都会被释放,因此每次调用也都会反复计算。
可是假设为了节省函数间通信的问题而降对象定义与file scope内,则是一种冒险。由于通常file scop的对象会打乱函数间的独立性,使程序变得难以理解。
一个比較合理的解决方法便是使用局部静态对象。
和局部非静态对象不同的是,局部静态对象所处的内存空间。即使在不同的调用过程中,依旧持续存在,即是计算过的过程,不会在调用结束被释放结束。
关于inline函数。仅仅须要在函数前面加inlinekeyword就可以。
将函数声明为inline不过对编译器提出一种要求。编译器是否运行这项请求,需视编译器而定。
一般而言,适合声明为inline函数的函数是:体积小,常被调用,所从事的计算并不复杂。
inline函数的定义常在头文件里。因为编译器必须在他被调用的时候加以展开,所以这个时候其定义必须是有效的。
五、重载函数(Override Function)
重载函数在其它的高级语言中常常见到,这个概念对大多数人来说都不默陌生。所谓的重载的函数就,事实上就是一个名字的函数,可是有多个实现的方式。
既然函数名字一样,编译器怎样知道调用的是那一个函数呢?不要忘记了之前所说的函数声明。声明包含返回类型。函数名,參数列表。借此得以让编译器检查函数调用的正确与否。因此,显然编译器通过识别參数的类型,个数类推断。
须要注意的是,编译器无法依据返回类型来推断和区分两个有同样名称和參数的函数。
为什么呢?由于我们调用的时候没有使用返回类型呀,我们仅仅是使用这个名字和參数来调用的哦。
六、定义和使用模板函数(Define and Using Template Functions)
所谓的模板函数,事实上仅仅是一个更高级的抽象罢了。这个机制在Lisp这样的先祖语言中早就有了。
为什么要模板函数呢?
正如上面所说的,为了更高级的抽象,比方我们有一系列的重载函数,不过參数的数据类型不一样,可是函数内部操作都有一定的模式,即是在更高级的抽象层次上。忽略数据类型的话,他们能够视作一个函数,因此,为了简化抽象,降低反复编码,便产生了所谓的“template function”。
Template function以keywordtemplate開始。其后紧跟尖括号<>包围起来的一个或多个标示符,这些标示符能够表示我们希望推迟确定的数据类型。每次用户使用这一个模板的时候。必须提供相应的类型信息。这些标示符就是起着占位符的作用。
当然。模板函数也能够被再次重载。
具体的模板函数不在此具体解释。毕竟这是一个非常重要也非常复杂的内容。
七、函数指针
所谓的函数指针,其形式相当复杂。他所指明的是函数的返回类型及函数类表。
即是有一系列的同样类型的函数。仅仅是名字不一样。这看起来就像是一系列的整型变量一样,我们能够把整型变量放在一个数组中,当然也能够把这一系列的函数放在一个数组中,每一个元素就是该函数的地址,函数名字就是函数的首地址,因此便产生了函数指针。
八、设定头文件
函数有一个一次定义多次声明的规则,即是在整个程序项目中。同一个函数的定义仅仅能出现一次,能够多次声明,可是有时为了避免多次声明,能够将这种函数声明放在头文件里。
然后在每一个使用该函数的程序代码中包括该头文件就可以。
同一时候有一个例外。就是inline函数。为了可以扩展inline函数的内容。在每一个调用点上,编译器都必须取得其定义,因此将其定义在头文件里是最恰当的。
同一时候我们须要注意函数的声明和定义的差别。
//以下的会被觉得是定义而非声明
Const int seq = 4;
Const vector<int > *(*seq_arry[seq]) (int)
仅仅要在上述定义前面加速extern就可以变成声明。
可是为什么步子啊上面的int seq前面加上extern呢?
这是由于const object和inline函数一样。也是一例外,constobject 的定义仅仅要一出文件之外就不可见。
关于引用头文件的双引號和尖括号之间的差别:
假设头文件和包括此文件的代码在同一个磁盘文件夹。使用引號,否则使用尖括号。
更专业的回答是:
假设此文件被觉得是标准或者专属的头文件,便以尖括号。编译器在搜索改文件的时候会在默认的文件夹寻找。假设是引號。则觉得是用户自己定义的,编译器便会在包括此文件的文件夹開始找。