2-面向过程的编程风格

本文探讨了C++中的函数编写、调用和参数传递,包括传值与传址的区别,以及传址带来的内存管理问题。详细讲解了动态内存分配和释放,以及局部静态对象的使用。另外,文章还阐述了函数模板、重载函数、内联函数和默认参数值的概念,展示了如何提高代码的灵活性和效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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 = tmp;
}

当我们以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)之内,那么除非程序员指定其初值,否则不会被初始化。

以下代码能完成swap的功能吗?

void swap(int* val1, int* val2) {
	int* tmp = val1;
	val1 = val2;
	val2 = tmp;
}

答:swap(&a, &b);不能完成变量a、b的交换!原理同上。
用指针完成swap功能的正确写法如下:(通过提领操作,局部变量也可以通过地址去操作全局变量的值)

void swap(int* val1, int* val2) {
	int tmp = *val1;
	*val1 = *val2;
	*val2 = tmp;
}

引用和指针相似,仅仅是一个地址值,但它被编译器赋予了一些特殊的属性。
函数返回值为引用类型
之前我们的函数返回值为值类型,如果返回值为引用类型呢?

#include <iostream>
using namespace std;
double vals[] = { 10.1, 12.6, 33.1, 24.1, 50.0 };
double& setValues(int i) {
	double& ref = vals[i];
	return ref;// 当函数返回一个引用时,则返回一个指向返回值的隐式指针。
}
int main() {
	setValues(1) = 20.23; // 改变第 2 个元素
	setValues(3) = 70.8;  // 改变第 4 个元素
	for (int i = 0; i < 5; i++) {
		cout << "vals[" << i << "] = ";
		cout << vals[i] << endl;
	}
	return 0;
}
vals[0] = 10.1
vals[1] = 20.23
vals[2] = 33.1
vals[3] = 70.8
vals[4] = 50

通过使用引用来替代指针,会使 C++ 程序更容易阅读和维护。C++ 函数可以返回一个引用,方式与返回一个指针类似。
返回值vs返回引用:

const string manip1(const string& s){  
    string ret = s;  
    return ret;  
}

此时,返回的不是ret本身,而是ret的副本。这个副本调用【复制构造函数】,与ret一模一样。无法区别它和ret,带来性能的损失。
记住,C++函数返回引用,一定不能返回指向局部变量的引用。否则,函数运行完毕,本地变量销毁,那这个引用是谁的别名呢?程序只有崩溃了。C++函数如果返回引用,就要返回在这个函数之前就存在的变量的引用才行!

const string& manip2(const string& s){  
    string ret = s;  
    return ret;// never do this!
}

例子2和例子1的区别是很微妙的,只有返回值类型不一样:string&和string。如果申明了string&,那么函数就直接返回了ret而不是ret的副本。当函数结束的时候,ret变量被销毁(自动调用它的~string()析构函数),如果主函数用到了这次函数的调用,程序立即崩溃(exception:std::bad_alloc)!

修正:
➀manip2()函数里去掉中间赋值过程,直接return s; 程序正常运行。
➁manip2()函数里中间赋值过程改为const string& ret = s; 程序正常运行。

在重载+运算符号的时候,可以返回引用:

my_complex& my_complex::operator +(const my_complex& rhs) {    
    real = rhs.real + real;// 复数的实部
    imag = rhs.imag + imag;// 复数的虚部
    return *this;    
} 

由于返回的引用在此次调用函数之前就已经存在(*this),那么,返回的引用是合适的。

最后,如果引用的内存是用new分配的,则需要使用delete手动销毁。

动态内存管理

不论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分配数组的同时为其元素设定初值。

对于数组,可以直接用指针+下标去访问元素,可以这样给分配的数组赋值:pia[6]=123; …
它与int arr[24]; 操作相同,就是生命周期不同,一个是静态内存分配、一个是动态内存分配。

从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. 返回类型const关键字为什么不省略?
    因为返回的对象不应该能被修改,即为const。
  2. 返回类型为什么是vector< int>* 而不是vector< int>&?
    因为reference一定得代表某个对象,局部对象会在函数结束时被销毁。作为指针返回还可以是0,即空指针;引用不可以。
  3. 函数中static vector< int> elems;中的static关键字为什么不能省略?
    因为省略后,局部对象会在函数结束时被销毁。
  4. 此时,调用fibon_seq(24);再调用fibon_seq(8);是什么效果?
    首先elems.size()==24,接着24>8,for循环不执行,直接返回elems地址。
2.5声明inline函数(Declaring a Function inline)

我们将上例中各个小工作分解为独立函数,以求更简化:

bool is_size_ok(int size) {
	const int max_size = 1024;
	if (size <= 0 || size > max_size) {
		cerr << "fibon_seq(): opps: invalid size: " << size << " -- can't fulfill request.\n";
		return false;
	}
	return true;
}
————————————————————————————————————————
const vector<int>* fibon_seq(int size) {
	const int max_size = 1024;
	static vector<int> elems;
	if (!is_size_ok(size))
		return 0;
	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;
}
————————————————————————————————————————
bool fibon_elem(int pos, int &elem) {// 注:pos代表第几个元素,从1开始
	const vector<int> *pseq = fibon_seq(pos);
	if (!pseq) {
		elem = 0;
		return false;
	}
	elem = (*pseq)[pos - 1];
	return true;
}

先前的做法中,fibon_elem()只需调用一个函数使可完成所有运算,如今必须使用三个函数。这成了它的缺点。如果其执行性能不理想,只好再将这三个函数重新组合为一个。C++inline关键词的作用由此诞生。

将函数声明为inline表示要求编译器在每个函数调用点上,将函数内容展开。面对一个inline函数,编译器可将该函数的调用操作改为以一份函数代码副本代替。这将使我们获得性能改善,其结果等于是把三个函数写入fibon_elem()内,但依然维持三个独立的运算单元。

只要在函数前面加上关键字inline,便可将该函数声明为内联函数(Function inline):

inline bool fibon_elem(int pos, int &elem) {
	/* 函数定义与先前版本相同 */
}

将函数声明为inline只是对编译器提出的一种要求。编译器是否执行这项请示,需视编译器而定。inline仅仅是一种请示而没有强制性。

一般而言,最合适声明为inline的函数:➊体积小 ➋常被调用 ➌所从事的计算并不复杂
如本例的is_size_ok()、fibon_elem()就满足要求,很适合声明为inline。
inline函数的特征是在调用的地方插入相应函数的代码,所以编译之后的目标文件里是没有inline函数体的,因为在要调用的地方它都已经用相应的语句替换掉了(当然这只限于内联成功的情况)。

2.6提供重载函数(Providing Overloaded Functions)

参数列表(parameter list)不相同(可能是参数类型不同,可能是参数个数不同)的两个或多个函数,可以拥有相同的函数名称。

void display_message(char ch);
void display_message(const string&);
void display_message(const string&, int);
void display_message(const string&, int, int);

既然名称相同,编译器如何知道应该调用哪一个函数呢?它会将调用者提供的实际参数拿来和每个重载函数的参数比对,找出其中最适合的。
参数列表(parameter list)而不是返回类型必须不同,所以下面是错误的写法:

ostream& display_message(char ch);
bool display_message(char ch);

为什么返回类型不足以将函数重载呢?因为返回类型无法保证提供给我们一个足以区分不同重载函数的语境。
例如,编译器无法判断以下的函数调用操作究竟想要调用哪个函数:

display_message('\t');// 到底是哪一个呢?
2.7定义并使用模板函数(Defining and Using Template Functions)

以下函数分别处理元素类型为int、double和string的三种vector:

void display_message(const string&, const vector<int>&);
void display_message(const string&, const vector<double>&);
void display_message(const string&, const vector<string>&);
// 加入类型为ostream的第三个参数,并将其默认参数设为cout,是更具有弹性的一种做法:
void display_message(const string&, const vector<string>&, ostream& = cout);

以上函数就是第二个参数类型不一样,其他一样,没有理由不为此搞一个能省下工夫的机制,得以将单一函数的内容与希望显示的各种vector类型绑定(bind)起来。所谓function template(函数模板)便提供了这样的机制。
function template以关键字template开场,其后紧接着以<>包围起来一个或多个标识符。这些标识符用以表示我们希望推迟决定的数据类型。关键字typename表示:elemType在display_message()函数中是一个暂时放置类型的占位符。

template <typename elemType>// 可以像int类型一样声明elemType类型的变量了
void display_message(const string &msg, const vector<elemType> &vec) {
	cout << msg;
	for (int i = 0; i < vec.size(); i++) {
		elemType t = vec[i];
		cout << t + ' ';
	}
}

以下是使用function template:

vector<int> ivec;
string msg;
// ...
display_message(msg, ivec);

编译器会将elemType绑定(bind)为int类型,然后产生一份display_message()函数实例,于是其第二个参数的类型即变成vector< int>。函数体内的局部对象的类型同样也变成了int。

2.8函数指针带来更大的弹性(Pointers to Functions Add Flexibility)

假设我们必须提供一个——“类似于fibon_seq(),可以通过vector返回另五种数列” 的函数。
做法之一是定义以下一整组函数:

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> *pent_seq(int size);

下面是fibon_elem()的定义,难道我们必须针对所有数据提供六个不同的函数吗?

bool fibon_elem(int pos, int &elem) {
	const vector<int> *pseq = fibon_seq(pos);
	if (!pseq) {
		elem = 0;
		return false;
	}
	elem = (*pseq)[pos - 1];
	return true;
}
函数指针

函数指针(pointer to function),其形式相当复杂。它必须指明其所指函数的返回类型及参数列表——本例的参数列表内容为int,返回类型为const vector<int>*。此外,函数指针的定义必须将*号放在某个位置,表示这份定义所表现的是一个指针。当然,最后还必须给予一个名称。

const vector<int>* (*seq_ptr)(int);
  1. 返回类型const vector< int>*;
  2. 参数列表类型为int;
  3. (*seq_ptr)中*号表示这是一个指针——函数指针,指针名为seq_ptr。
    现在重写fibon_elem()函数,让它更为通用:
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 true;
}

我们可以给予函数指针初值。如果初值为0,表示并未指向任何函数:

const vector< int>* (*seq_ptr)(int) = 0;

==函数有地址吗?有,就是函数名!==所以,可以将函数名作为参数传递到函数里。

// 将pell_seq()的地址赋值给seq_ptr
seq_ptr = pell_seq;

关于两种指针数组的对比:
1.普通指针数组:int *arr[] = {ptr1, ptr2, …}; (数组指针格式:int (*arr)[]={1,2,3},{4,5,6})
2.函数指针数组:int (*farr[])(int) = {funcName1, funcName2, …};

我们改进一下程序,先创建一个持有6个函数指针的数组

const vector<int>* (*seq_array[])(int) = {
	fibon_seq, lucas_seq, pell_seq,
	triang_seq, square_seq, pent_seq
}
int seq_index = 0;
while (next_seq == true) {
	seq_ptr = seq_array[++seq_index];
	...
}
枚举类型

我们得记住每个数列在数组中的索引,然后借此加以指定,那显得有点笨拙。
更直接的方式是通过一组辅助记忆的常量来进行索引操作,如:

enum ns_type {
	ns_fibon, ns_lucas, ns_pell,
	ns_triang, ns_square, ns_pent
};

关键字enum之后是一个可有可无的标识符。如上面的ns_type可省略。借此定义出所谓的枚举类型(enumerated type)。大括号里头是以逗号分隔的列表,其中的每个项称为枚举项(enumerator)

默认情况下,第一个枚举值为0,接下来的每个枚举项都比前一个多1。
上面,ns_fibon=0, ns_lucas=1, ns_pell=2, ns_triang=3, ns_square=4, ns_pent=5

用检举项代替数字索引,可使代码更清晰:

seq_ptr = seq_array[ns_pell];
2.9设定头文件(Setting Up a Header File)

在调用seq_elem()之前,必须先声明它,以使程序知道它的存在。
如果它被5个程序文件调用,就必须进行5次声明。
为了不用分别在5个文件中声明seq_elem(),我们把函数声明放在头文件中,并在每个程序代码文件内包含(include)这些函数声明

使用这种编写习惯,我们只需为函数维护一份声明即可。如果其参数列表或返回类型需要改变,也只需要更改这份声明即可——函数用户会包含更新后的函数声明。

头文件的扩展名,习惯上是*.h。标准库例外,它们没有扩展名。

// NumSeq.h
bool seq_elem(int pos, int &elem);
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> *pent_seq(int size);
...

函数的定义只能有一份,不过倒是可以有许多份声明。我们不把函数的定义放入头文件,因为同一个程序的多个代码文件可能都会包含这个头文件

“只定义一份”的规则有个例外:inline函数的定义。为了能够扩展inline函数的内容,在每个调用点上,编译器都得取得其定义。这意味着如果一个inline函数会在多个源文件中被用到,我们必须将inline函数的定义放在头文件中,而不是把它放在各个不同的程序代码文件中。
把函数声明放到头文件中,把函数定义放在程序代码文件中。

extern关键字与const object
// NumSeq.h
const int seq_cnt = 6;
const vector<int>* (*seq_array[seq_cnt])(int);

第2行代码并不正确(并非编译运行有错误),因为它被解读为seq_array的定义而非声明。seq_array是一个存储函数指针的数组对象。
就像函数一样,一个对象只能在程序中被定义一次。
对象的定义,就像函数定义一样,必须放在程序代码文件中(而非头文件中)。
加上extern关键字,它便成为一个声明!这样这个对象就不会被解读为定义

// NumSeq.h
const int seq_cnt = 6;
extern const vector<int>* (*seq_array[seq_cnt])(int);

为什么seq_cnt不需要加上关键字extern呢?

const object就和inline函数一样,是“一次定义”规则下的例外。
const object的定义只要一出文件之外便不可见。这意味着我们可以在多个程序代码文件中加以定义,不会导致任何错误。
seq_cnt就是一个const object,作用域为NumSeq.h文件内。
seq_array并不是一个const object,它是一个指向const object的指针,它本身并不是const object。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

itzyjr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值