2-面向过程的编程风格

Posted itzyjr

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2-面向过程的编程风格相关的知识,希望对你有一定的参考价值。

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);

第一次调用便已计算出第二次、第三次调用所需要计算的值。这里花费了一些不必要的工夫!

  1. vector对象在函数内声明为局部(local),并不能解决上述问题,因为局部对象会在每次调用函数时建立并在函数结束的同时被弃置。
  2. 交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;
}	

仔细考虑一下:

  1. 返回类

    以上是关于2-面向过程的编程风格的主要内容,如果未能解决你的问题,请参考以下文章

    设计模式之美——面试对象与面向过程对比

    原来我们一直写的是违反面向对象编程风格的代码

    原来我们一直写的是违反面向对象编程风格的代码

    C语言面向对象

    VSCode自定义代码片段——JS中的面向对象编程

    VSCode自定义代码片段9——JS中的面向对象编程