2-面向过程的编程风格
Posted itzyjr
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2-面向过程的编程风格相关的知识,希望对你有一定的参考价值。
目录
- 2.1如何编写函数(How to Write a Function)
- 2.2调用函数(Invoking a Function)
- Pass by Reference语义
- 作用域及范围
- 动态内存管理
- 2.3提供默认参数值
- 2.4使用局部静态对象(Using Local Static Objects)
- 2.5声明inline函数(Declaring a Function inline)
- 2.6提供重载函数(Providing Overloaded Functions)
- 2.7定义并使用模板函数(Defining and Using Template Functions)
- 2.8函数指针带来更大的弹性(Pointers to Functions Add Flexibility)
- 函数指针
- 枚举类型
- 2.9设定头文件(Setting Up a Header File)
- extern关键字与const object
2.1如何编写函数(How to Write a Function)
1.返回类型 2.函数名 3.参数列表 4.函数体
函数原型(function prototype):int fibon_elem(int pos);
要终止整个程序,标准库exit()
函数可派上用场,它接受一个表示状态值的参数。
#include<cstdlib> // for exit()
int fibon_elem(int pos) {
if (pos <= 0)
exit(-1);
int elem = 1;
int n_1 = 1, n_2 = 1;// 持有前两个元素的值
for (int ix = 3; ix <= pos; ++ix) {
elem = n_2 + n_1;
n_2 = n_1;
n_1 = elem;
}
return elem;// 1,1,2,3,5,8,...
}
重新修正函数原型为:bool fibon_elem(int pos, int &elem);
如果用户要求计算第5000位置上的元素值,那将是个很大的数,计算结果是:
element # 5000 is -1846256875
int是个有符号(signed)类型,fibon_elem()的运算溢出(overflow)了它所能表示的最大正值。将elem类型改为unsigned int,结果为2448710421,正确。但Fibonacci数列是没有止尽的,所以可以设定一个上限。
完整例子如下:
#include<iostream>
using namespace std;
bool fibon_elem(int, int&);
bool print_sequence(int);
int main() {
int pos;
cout << "Please enter a position:";
cin >> pos;
int elem;
if (fibon_elem(pos, elem)) {
cout << "element # " << pos << " is " << elem << endl;
print_sequence(pos);
} else
cout << "Sorry,Could not calculate element # " << pos << endl;
}
bool fibon_elem(int pos, int& elem) {
if (pos <= 0 || pos > 1024) {
elem = 0;
return false;
}
elem = 1;
int n_2 = 1, n_1 = 1;
for (int ix = 3; ix <= pos; ++ix) {
elem = n_2 + n_1;
n_2 = n_1;
n_1 = elem;
}
return true;
}
bool print_sequence(int pos) {
if (pos <= 0 || pos > 1024) {
cerr << "invalid position:" << pos << endl;
return false;
}
cout << "Fibonacci Sequence for " << pos << "position:\\n\\t";
switch (pos) {
default:
case 2:
cout << "1 ";// 注意,此处没有break;
case 1:
cout << "1 ";
break;
}
int elem = 1;
int n_2 = 1, n_1 = 1;
for (int ix = 3; ix <= pos; ++ix) {
elem = n_2 + n_1;
n_2 = n_1;
n_1 = elem;
// 一行打印10个元素
cout << elem << (!(ix%10)? "\\n\\t" : " ");
}
cout << endl;
return true;
}
Please enter a position:|12
element # 12 is 144
Fibonacci Sequence for 12 position:
1 1 2 3 5 8 13 21 34 55
89 144
2.2调用函数(Invoking a Function)
我们可以审视两种参数传递方式:传址(by reference)及传值(by value)。
- 传值
int main() {
int a = 3;
int b = 4;
swap(a, b);
cout << "Now:a=" << a << ",b=" << b;
}
void swap(int val1, int val2) {
int tmp = val1;
val1 = val2;
val2 = tmp;
}
Now:a=3,b=4
为什么a、b变量的值没有交换?
val1和val2是两个形参,传递给它们的值被复制了一份,原对象(a、b)与副本(val1、val2)之间没有任何关联,唯一相同点就是它们的值相等。
当我们调用一个函数时,会在内存中建立起一块特殊区域,称为
程序堆栈(program stack)
。这块特殊区域提供了每个[函数参数]的储存空间。它也提供了函数所定义的每个对象的内存空间——我们将这些对象称为local object(局部对象)。一旦函数完成,这块内存就会被释放掉,或者说是从程序堆栈中被pop出来。
- 传址
void swap(int& val1, int& val2) {
int tmp = val1;
val1 = val2;
val2 = tmp;
}
此swap()替换上例的,就可正确完成a、b变量的交换。
如果交换一个vector内的元素如下是有问题一段代码:
void bubble_sort(vector<int> vec) {
//...
if (vec[ix] > vec[jx])
swap(vec[ix], vec[jx]);
}
首先要检查的便是函数参数的传递应该采用传址方式而非传值方式,修正如下:
void bubble_sort(vector<int> &vec) { //...}
Pass by Reference语义
reference扮演着外界与对象之间一个间接手柄的角色。
int ival = 1024;// 对象,类型为int
int *pi = &ival;// 指针(pointer),【指向】一个int对象
int &rval = ival;// 引用(reference),【代表】一个int对象
当我们这么写:
int jval = 4096;
rval = jval;
便是将jval赋值给rval所代表的对象(也就是ival)。我们无法令rval转而代表jval,因为C++不允许我们改变reference所代表的对象,它们必须【从一而终】。
当我们写:
pi = &ival;
其实是将ival(此为rval所代表的对象)的地址赋值给pi。我们并未令pi指向rval。
注意,重点是,面对reference的所有操作都和面对“reference所代表的对象”所进行的操作一般无二。当我们以reference作为函数参数,亦是如此。
当swap()函数将val2赋值给val1:
void swap(int &val1, int &val2) {
// 【实际参数】的值会因此而改变
int tmp = val1;
val1 = val2;
val2 = temp;
}
当我们以by reference方式将对象作为函数参数传入时,对象本身并不会复制出另一份——复制的是对象的地址。函数中对该对象进行的任何操作,都相当于是对传入的对象进行间接操作。
将参数声明为reference的理由之一:希望得以直接对所传入的对象进行修改。(极其重要)
将参数声明为reference的理由之二:降低复制大型对象的额外负担。(效率问题)
void display(const vector<int> &vec) {
for (int ix = 0; ix < vec.size(); ++ix)
cout << vec[ix] << ' ' << endl;
}
**以上代码如果参数是const vector< int> vec,则是按值传递,vector内的所有元素都会被复制。**如果传入的是vector的地址,速度会更快。为什么前面加上const
关键字(reference to const vector):少了const
并不会造成错误,但加上const
可以让阅读程序的人了解,我们以传址的方式来传递vector,为的是避免复制操作;而不是为了避免在函数之中对它进行修改。
函数参数里添加
const
修饰指针(引用),并不说明此函数就不能修改指针指向(引用代表)的内容,只是说明函数不能通过被const
修饰的指针(引用)改变指向(代表)的内容。
我们都知道const
关键字定义的变量是不可以被改变的,所以当我们声明const引用时即不会进行复制操作,当误操作时又不能编译通过,两全其美。
如果我们愿意,也可以将vector以pointer形式传递。这和以reference传递的效果相同:传递的是对象地址,而不是整个对象的副本。
如下是以pointer形式传递参数:
void display(const vector<int> *vec) {// 以pointer形式传递参数
if (!vec) {// 当提领pointer(即下面代码dereference pointer:*vec操作)时,一定要先确定其值并非0
cout << "display(): the vector pointer is 0 << endl;
return;
}
for (int ix = 0; ix < vec->size(); ++ix) {
cout << (*vec)[ix] << ' ';
cout << endl;
}
int main() {
int ia[8] = {8, 32, 3, 13, 1, 21, 5, 2};
vector<int> vec(ia, ia+8);
cout << "vector before sort:";
display(&vec);// 传址
//...
}
下面看一下vector
的定义与上例用到的一个构造函数的定义:
// vector的定义(模板类)
class template
std::vector
template < class T, class Alloc = allocator<T> > class vector; // 通用模板(generic template)
// vector的其中一个构造函数
template <class InputIterator>
vector (InputIterator first, InputIterator last, const allocator_type& alloc = allocator_type());
pointer参数和reference参数之间更重要的差异是:
pointer可能(也可能不)指向某个实际对象。当我们提领pointer时,一定要先确定其值并非0。
reference必定会代表某个对象,所以不需要做此检查。
一般来说,除非你希望在函数内更改参数值,否则建议在传递内置类型时,不要使用传址方式。传址机制主要用于传递class object。
作用域及范围
函数是暂时位于程序堆栈(内存内的一块特殊区域)之上。局部对象就放在这块区域中。当函数执行完毕,这块区域的内容便会被弃置。于是局部对象不复存在。
下面看一个例子:
vector<int> fibon_seq(int size) {
if (size <= 0 || size > 1024) {
cerr << "Warning:" << size << " not supported -- resetting to 8" << endl;
size = 8;
}
vector<int> elems(size);
for (int ix = 0; ix < size; ++ix) {
if (ix == 0 || ix == 1)
elems[ix] = 1;
else
elems[ix] = elems[ix-1] + elems[ix-2]
}
return elems;
}
上例中,elems是以传值方式返回,不会产生任何问题;因为返回的乃是对象的副本,它在函数之外依然存在。
大多数C++编译器对于“以传值方式返回的class object”,都会通过优化程序,加上额外的reference参数。
上例中,如果以pointer或reference形式将elems返回,都不正确!因为elems在fibon_seq()执行完毕时已不复存在。
每次fibon_seq()执行,都会为elems分配内存,每当fibon_seq()结束便会加以释放。
内置类型的对象,如果定义在文件作用域(file scope)之内,必定被初始化为0。但如果它们被定义于局部作用域(local scope)之内,那么除非程序员指定其初值,否则不会被初始化。
动态内存管理
不论local scope或file scope,都是由系统自动管理。第三种储存期形式称为dynamic extent(动态范围)。其内存系由程序的空闲空间(free store)分配而来,有时也称为heap memory(堆内存)。这种内存必须由程序员自行管理,其分配系通过new
表达式来完成,而其释放则通过delete
表达式完成。
new
表达式形式如下:
new Type;
new Type(initial_value);
此处Type可以是任意内置类型,也可以是程序知道的class类型。
int *pi;
pi = new int;
以上便是先由heap分配出一个类型为int的对象,再将其【地址】赋值给pi。
pi = new int(1024);
不同于上例的是,这个int对象会被初始化为1024。
从heap中分配数组,可以这么写:
int *pia = new int[24];
从heap分配一个数组,拥有24个整数。pia会被初始化为数组第一个元素的地址。
数组中的元素都未初始化,C++没有提供任何语法让我们得以从heap分配数组的同时为其元素设定初值。
从heap分配而来的对象,被称为具有dynamic extent,因为它们是在运行时通过new表达式分配来的,因此可以持续存活,直到以delete表达式加以释放为止。
// 释放pi所指的对象
delete pi;// 注:即使pi为null,表达式也是合理的,编译器会替我们检查
//释放pia所指的数组中的所有对象
delete [] pia;
2.3提供默认参数值
void bubble_sort(vector<int> &vec, ofstream &ofil) {
for (int ix = 0; ix < vec.size(); ++ix) {
for (int jx = ix + 1; jx < vec.size(); ++jx)
if (vec[ix] > vec[jx]) {
ofil << "about to call swap! ix: " << ix << " jx: " << jx << "\\t"
<< "swapping: " << vec[ix] << " with " << vec[jx] << endl;
swap(vec[ix], vec[jx], ofil);
}
}
}
上例中,每次调用bubble_sort()都必须传入一个ofstream对象,而且用户无法关闭我们所产生的信息。
void bubble_sort(vector<int> &vec, ofstream *ofil = 0) {
for (int ix = 0; ix < vec.size(); ++ix) {
for (int jx = ix + 1; jx < vec.size(); ++jx)
if (vec[ix] > vec[jx]) {
if (ofil != 0)
ofil << "about to call swap! ix: " << ix << " jx: " << jx << "\\t"
<< "swapping: " << vec[ix] << " with " << vec[jx] << endl;
swap(vec[ix], vec[jx], ofil);
}
}
}
上例中,第二个参数声明为ofstream对象的一个pointer而非reference。我们必须做这样的改变,才可以为它设定默认值0,表示并未指向任何ofstream对象。reference不同于pointer,无法被设置为0。因此,reference一定得代表某个对象。
bubble_sort(myVec);// 不带第2个参数,不产生任何调试信息
bubble_sort(myVec, myOfstream);// 带第2个参数,产生调试信息
int main() {
int ia[8] = {8, 32, 3, 13, 1, 21, 5, 2};
vector<int> vec(ia, ia + 8);
bubble_sort(vec);// 如同调用bubble_sort(vec, 0);一样,不产生任何调试信息
ofstream ofil("data.txt");
bubble_sort(vec, &ofil);// 调试信息输出在data.txt文件中
display(vec, ofil);
}
为了能够在main()之中同时支持标准屏幕打印和文件里面打印两种使用方式,让cout成为默认的ostream参数是解决之道:
void display(const vector<int> &vec, ostream &os = cout) {
for (int ix = 0; ix < vec.size(); ++ix)
os << vec[ix] << ' ';
os << endl;
}
带默认值的参数都要放到参数列表的最后,如下是错误的用法,因为没有为vec提供默认值:
void display(ostream &os = cout, const vector<int> &vec); // 错误!!!
头文件可为函数带来更高的可见性(visibility),我们决定将默认值放在函数声明处而非定义处:
// NumericSeq.h
void display(const vector<int>&, ostream& = cout);
// MyProgram.cpp
#include "NumericSeq.h"
void display(const vector<int> &vec, ostream &os) {
for (int ix = 0; ix < vec.size(); ++ix)
os << vec[ix] << ' ';
os << endl;
}
2.4使用局部静态对象(Using Local Static Objects)
请看以下对fibon_seq()进行的三次调用:
fibon_seq(24);
fibon_seq(8);
fibon_seq(18);
第一次调用便已计算出第二次、第三次调用所需要计算的值。这里花费了一些不必要的工夫!
- vector对象在函数内声明为局部(local),并不能解决上述问题,因为局部对象会在每次调用函数时建立并在函数结束的同时被弃置。
- 交vector对象定义于文件作用域(file scope),这样过于冒险,通过file scope对象会打乱不同函数的独立性,使它们难以理解。
本例的另一种解法是使用局部静态对象(local static object)。例如:
const vector<int>* fibon_seq(int size) {
static vector<int> elems;
// 函数的工作逻辑...
return &elems;
}
和局部非静态对象不同的是,局部静态对象所处的内存空间,即使在不同的函数调用过程中,依然持续存在。
elems的内容不再像以前一样地在fibon_seq()每次被调用时就被破坏又被重新建立。
这也就是现在我们可以安全地将elems的地址返回的原因。
每当调用fibona_seq()时,我们只需计算那些尚未被放入elems的元素即可。以下是一种可能的实现方式:
const vector<int>* fibon_seq(int size) {
const int max_size = 1024;
static vector<int> elems;// local static object
if (size <= 0 || size > max_size) {
cerr << "fibon_seq(): opps: invalid size: " << size << " -- can't fulfill request.\\n";
return 0;
}
// 如果size <= elems.size(),就不必重新计算了
for (int ix = elems.size(); ix < size; ++ix) {
if (ix == 0 || ix == 1)
elems.push_back(1);
else
elems.push_back(elems[ix-1] + elems[ix-2]);
}
return &elems;
}
仔细考虑一下:
- 返回类
以上是关于2-面向过程的编程风格的主要内容,如果未能解决你的问题,请参考以下文章