建设专业网站网络,厦门免费推广平台,英文网站模版,wordpress订单插件在学习这本书进阶内容之前#xff0c;我们可以跟着它的第一章部分再巩固和复习。本书由Sartaj Sahni撰写#xff0c;由王立柱和刘志红翻译。全书通俗易懂#xff0c;内容丰富#xff0c;是巩固C内容的不二选择。希望本文对各位有所帮助。
目录
1.函数与参数
1.1.传值参数… 在学习这本书进阶内容之前我们可以跟着它的第一章部分再巩固和复习。本书由Sartaj Sahni撰写由王立柱和刘志红翻译。全书通俗易懂内容丰富是巩固C内容的不二选择。希望本文对各位有所帮助。
目录
1.函数与参数
1.1.传值参数
1.2.模板函数
1.3.引用参数
1.4.常量引用参数
1.5.返回值
1.6.重载函数
1.7.练习
2.异常
2.1.抛出异常
2.2.处理异常
2.3.练习
3.动态内存空间分配
3.1.操作符new
3.2.一维数组
3.3.异常处理
3.4.操作符delete
3.5.二维数组
4.自有数据类型
4.1.类 currency
4.2.一种不同的描述方法
4.3.操作符重载
4.4.友元和保护性类成员
4.5.增加#ifndef、#define和#endif语句
5.异常类illegalParameterValue
6.递归函数
6.1.递归的数学函数
6.2.归纳
6.3.C递归函数
7.标准模板库
7.1.accumulate
7.2.copy和next_permutation 1.函数与参数
1.1.传值参数
对于普通的传值参数我们已经司空见惯了我们一般只要对相应的函数体传入形参在执行的main函数主体中传入实参就可以调用相应的内容。在运行时函数体在执行前把实参复制给形参复制的过程是由形参类型的复制构造函数来完成的。如果实参和形参的类型不一致那么就必须进行类型转换把实参转化为形参的类型前提也很明确那就是该类型转换是允许的。在函数结束系统会调用形参类型的析构函数来释放形式参数。当函数运行结束以后那么形参的只就不会复制到实参当中去。因此也就是我们所说的单向值传递。当然在我们所学习的类与对象板块中我们也知道了类内的方法就很类似于我们的函数而且我们还能自己进行编写构造函数和析构函数。
1.2.模板函数 float abc(float a, float b, float c)
{return a b * c;
} 这种形式过于拘泥被数据类型限制了所以我们可以使用模板对相应的形参进行替换当我们调用同一个方法的时候我们就无须再多虑它的数据类型了。 templateclass T
T abc(T a, T b, T c)
{return a b * c;
} 1.3.引用参数
在使用上面的模板函数时会增加不少时间开销这实际上就是因为要重新开辟空间复制形参的原因。而且传入的数据越多它的负担也就越大也就导致了相应的时间、空间双重损失。这时候我们就要是使用引用来减少这种时间和空间上的损失。 templateclass T
T abc(T a, T b, T c)
{return a b * c;
} 引用的好处就是将原来的形参复制形式转变为了实参直接调用当然这个函数返回时也就不会调用析构函数。
1.4.常量引用参数
C还提供另外一种参数传递模式——常量引用。这种模式指明的引用参数不能被函数修改。这个名字实际上就已经暗示了这一点了之前我们遇到的常变量、常量指针都是使用了const是相应的数值转变为一个不可更改的左值。 templateclass T
T abc(const T a, const T b, const T c)
{return a b * c;
} 用关键字const来指明函数不可修改的引用参数在软件工程上具有重要的意义。函数头会告诉用户该函数是不能修改实参的。
改成更通用的版本就是 templateclass Ta, class Tb, class Tc
T abc(const Ta a, const Tb b, const Tc c)
{return a b * c;
} 1.5.返回值
一个函数可以返回一个值、一个引用或者一个常量引用。在这种情况下返回的值会被复制到调用环境中去也就是我们获得了我们调用函数之后想要得到的那个结果。
1.6.重载函数
一个函数的签名是由这个函数的形参类型以及形参个数所确定的。C可以定义两个或多个同名函数但是两个同名函数不能有相同的签名。定义多个同名函数的机制就是我们所说的函数重载。 int abc(int a, int b, int c)
{return a b * c;
}
float abc(float a, float b, float c)
{return a b * c;
}
int abc(int a, int b)
{return a b;
} 1.7.练习
练习1交换函数为什么失败这是因为这是单向值传递形参复制了实参但是无法对实参进行修改函数运行结束该形参也被回收了。
练习2编写一个模板函数count返回值是数组a[0:n-1]中value出现的次数。
#includeiostream
using namespace std;
#define N 20
templateclass Ta, class Tb, class Tcint count(Ta arr, Tb n, Tc value)
{int count 0;for(int i 0;i n; i){if(arr[i] value){count;}}return count;
}
void menu()
{cout 使用int 类型————1 endl;cout 使用char类型————2 endl;
}
int main()
{int n, arr[N];char arr1[N];cout 输入个数为;cin n;int value;char value1;int number;menu();cout 输入类型;while(1){int val;cin val;if(val 1){for(int i 0;i n; i){cin arr[i];}cout 要查询的值;cin value;number count(arr, n, value);break;}else if(val 2){for(int i 0;i n; i){cin arr1[i];}cout 要查询的值;cin value1;number count(arr1, n, value1);break;}else {cout 输入错误请重新输入;}}cout 该值个数为 number endl;return 0;
}
练习3编写一个模板函数fill给数组a[start:end-1]赋值value
#includeiostream
using namespace std;
#define N 20
template class Ta, class Tb, class Tc
void fill(Ta arr, Tb start, Tb n, Tc value)
{for(int i start;i n; i){arr[i] value;}
}
int main()
{int arr[N];int n;cout 输入个数;cin n;for(int i 0;i n; i){arr[i] i;}int start;while(1){cout 输入起始位置;cin start;if(start 0 start n){break;}}int value;cout 复制的内容为;cin value;fill(arr, start, n, value);cout 复制后的代码 endl;for(int i 0;i n; i){cout arr[i] ;}cout endl;return 0;
}
此处可以将主函数中的int arr[N]改为char arr[N]之类的类型从而实现类型的差异或者说也可以像上面的那道题一样准备多个选项进行实现内容。
练习4编写一个模板函数inner_product返回值是a[i]*b[i]的总和。
#includeiostream
using namespace std;
#define N 20
template class T
T inner_product(T a, T b)
{return a * b;
}
void test()
{int a[N],b[N];int n;int Count 0;cout 请输入数据个数;cin n;for(int i 0;i n; i){cout 输入a数组的值;cin a[i];}for(int i 0;i n; i){cout 输入b数组的值;cin b[i];}for(int i 0;i n; i){Count inner_product(a[i], b[i]); }cout Sum的数据 Count endl;
}
int main()
{test();return 0;
}
练习5编写一个模板函数iota使a[i]valuei0≤in。
#includeiostream
using namespace std;
#define N 20
templateclass Ta, class Tb
Ta iota(Ta i, Tb value)
{return i value;
}
void test()
{int a[N];int n, value;cout 请输入个数;cin n;cout 请输入value值;cin value;for(int i 0;i n; i){a[i] iota(i, value);}for(int i 0;i n; i){cout a[i] ;}cout endl;
}
int main()
{test();return 0;
}
练习6编写一个模板函数is_sorted当且仅当a[0:n-1]有序时返回值是true。
#includeiostream
using namespace std;
#define N 20
templateclass Ta, class Tb
bool is_sorted(Ta a, Tb n)
{for(int i 1;i n; i){if(a[i] a[i-1])return false;}return true;
}
void test()
{int a[N];int n;cout 输入个数;cin n;for(int i 0;i n; i){cout 输入数据;cin a[i];}bool j is_sorted(a, n);if(j true) cout 从小到大升序排列 endl;else cout 非升序排列 endl;
}
int main()
{test();return 0;
}
练习7编写一个模板函数mismatch返回值是使不等式a[i]不等于b[i]成立的最小的索引ii在0到n之间。
#includeiostream
using namespace std;
#define N 20
templateclass Ta, class Tb
int mismatch(Ta a, Ta b, Tb n)
{for(int i 0;i n; i){if(a[i] ! b[i])return i;}return -1;
}
int main()
{int a[N], b[N];int n;cout 输入个数;cin n;for(int i 0;i n; i){cout 输入a数组;cin a[i];}for(int i 0;i n; i){cout 输入b数组;cin b[i];}int num mismatch(a, b, n);if(num -1) cout ab数组完全相等 endl;else cout 第一个不相同的下标是 num endl;return 0;
}
2.异常
2.1.抛出异常
异常是表示程序出现错误的信息。比如说b/c当c0时这就是一个错误。对于这个错误C检查不出来但是硬件会检查出来并抛出一个异常。同样的当我们学习Python的时候已经接触过它的异常并做了一些预防措施和代码。
我们可以编写这样的C程序它可以对一些异常的情况进行检查而且当查出一个异常的时候就抛出这个异常。就如下面的代码一样程序函数abc可以定义仅当三个参数都大于0时才进行只要有一个或多个为0的值就可以抛出异常而这个程序抛出的异常类型是char*。 int abc(int a, int b, int c)
{if(a 0 || b 0 || c 0){throw All parameters should be 0;}return a b * c;
} 程序可能抛出的异常有很多比如0除数、非法参数值、非法输入值、数组下标越界等。如果对每一种类型异常都定义一个异常类那么异常的处理就有很大灵活性。
2.2.处理异常
一段代码抛出的异常由包含这段代码的try块来处理。紧跟在try之后的是catch块。每一个catch块都有一个参数参数的类型决定了这个catch块要捕捉的异常类型。 //块
catch(char * e){}
//捕捉的异常类型是char*而块
catch(bad_alloc e){}
//捕捉的异常类型是bad_alloc
catch(exception e){}
//捕捉的异常类型是基类型exception以及所有从exception派生的类型例如bad_alloc和bad_typeid
catch(...){} catch块一般包含异常改正之后所恢复的代码。如果不能恢复那么catch块的代码输出错误的信息。 int main()
{try {cout abc(2,0,5) endl};catch(char* e){cout The parameters to abc were 2, 0, and 5 endl;cout An exception has been throw endl;cout e endl;return 1;}return 0;
}
//输出结果
The parameters to abc were 2, 0, and 5
An exception has been throw
All parameters should be 0 abc函数抛出一个类型为char*的异常。这个异常使函数abc还没有计算表达式的值就停止了。块try也立即停止了其中的cout语句没有执行完。因为抛出的异常与catch块的参数e是同一种类型所以异常被这个catch块捕捉e的赋值是抛出的异常然后进入catch块。
2.3.练习
练习1修改上面的程序使抛出的异常类型是整型。如果a、b、c都小于0那么抛出的异常值是1如果a、b、c都等于0那么抛出的异常值为2否则没有异常。编写一个主函数应用修改后的代码若有异常抛出则捕捉异常根据异常值输出信息。
#includeiostream
using namespace std;
int panduan(int a, int b, int c)
{if(a 0 b 0 c 0)return 2;else if(a 0 b 0 c 0)return 1;else return 0;
}
int main()
{int a, b, c;cout 依次输入a、b、c;cin a b c;int flag panduan(a, b, c);catch(flag)return 0;
} 3.动态内存空间分配
3.1.操作符new
C操作符new用来进行动态存储分配或者运行时存储分配它的值是一个指针指向所分配的空间.
#includeiostream
using namespace std;
int main()
{int * y new int;*y 10;//或者可以用如下方式直接进行操作int * x new int(10);//或者第三种方式int * z;z new intreturn 0;
}
3.2.一维数组
许多函数中都要用到一维和二维数组这些数组的大小在编译的时侯可能还是未知的它们随着函数的调用的变化而变化因此对这些数组只能进行动态存储分配。
为了在运行时创建一个一维浮点数组必须把x声明为一个浮点型指针然后为数组分配充足的空间 #includeiostream
using namespace std;
int main()
{int n;cin n;float * x new float[n];return 0;
} 使用操作符new为n个浮点数分配了存储空间并返回第一个浮点数空间的指针。对每个数组的元素的访问都可以使用x[0]x[1]x[2]......x[n-1]的形式。
3.3.异常处理 #includeiostream
using namespace std;
int main()
{float * x;try{x new float [n]};catch(bad_alloc e){cerr Out of Memory endl;exit(1);}return 0;
} 如果计算机没有充足空间对float数组进行分配时就会捕捉到异常主动弹出异常问题抛出一个bad_alloc的异常并终止程序利用try-catch结构进行相应的操作
3.4.操作符delete
动态内存的存储空间不再需要时相应的需要进行空间释放释放的空间可以重新用来动态分配C操作符delete用来释放操作符new所分配的空间。 #includeiostream
using namespace std;
int main()
{int * x new int(10);delete x;return 0;
} 3.5.二维数组
虽然C采用多种机制来说明二维数组但这些机制大多数要求在编译阶段就知道而二维数组的大小。具体来说使用这些机制很难编写出相应的函数它的形参是一个第二维大小未知的二维数组。这是因为当形参是二维数组时必须指定第二维的大小。例如a[][10]是一个合法的定义但是a[][]不合法。
而克服这一问题的最佳方法就是采用动态内存分配的形式。 #includeiostream
using namespace std;
int main()
{int n;cin n;char(*c)[5];try{c new char[n][5];}catch(bad_alloc){cerr Out of Memory endl;exxit(1);}return 0;
} 在运行时行数n要么是用户自行输入的要么就是通过计算确定的如果数组1的列数在编译阶段是未知的话那么就不能只调用一次new就能创建二维数组即使数组的行数是已知的。要构建这样的二维数组可以把它看做是由若干行所构成的结构每一行都是一个能用new来创建的一维数组。指向每一行的指针保存在另外一个一位数组之中。
一个3*4的数组 x[0]、x[1]、x[2]分别指向第0行、第1行、第2行的首元素如归x是一个字符数组那么x[0:2]是指向字符的指针而x本身就是一个指向指针的指针x的声明语法char **x;
#includeiostream
using namespace std;
template class T
bool makeArray(T ** x, int numberOfRows, int numberOfColumns)
{//创建一个二维数组try{//创建行指针x new T * [numberOfRows];//为每一行分配空间for(int i 0;i numberOfRows; i){x[i] new int [numberOfColumns];}return true;}catch (bad_alloc) {return false};
}
int main()
{int numberOfRows, numberOfColumns;cout 输入行;cin numberOfRows;cout 输入列;cin numberOfColumns;char ** x;makeArray(x, numberOfRows, numberOfColumns);//释放空间void deleteArray()return 0;
}
创建一个类型为T的二维数组。这个数组的行数是numberOfRows列数是numberOfColumns。程序首先为指针x[0]......x[numberOfRows-1]申请空间然后为数组的每一行申请空间。在程序中操作符new被调用了numberOfRows1次。如果new的某一次调用发生了异常程序控制将转移到catch块并返回false。而每一次new的调用都没有任何问题的话那么数组就创建成功。函数返回true。对于创建的数组x每个元素都可以使用标准的下标法x[i][j]来引用。0≤inumberOfRows, 0≤jnumberOfColumns。
我们分两步来释放这个二维数组空间首先释放放在for循环中为每一行所分配的空间然后释放为行指针所分配的空间。x被指为NULL防止用户继续访问已经释放的空间。 template class T
void deleteArray(T ** x, int numberOfRows)
{//删除二维数组xfor(int i 0;i numberOfRows; i){delete [] x[i];}delete [] x;x NULL;
} 4.自有数据类型
4.1.类 currency
C语言支持诸如int、float、和char这样的数据类型。而书本上的许多应用的数据类型是C不支持的需要自己定义。定义自有数据类型最灵活的方式就是使用C的类class结构。
假设你想处理货币类型currency的对象也称实例这种对象有三个成员符号-、美元和美分。对于这些对象我们想要执行的操作如下 给成员赋值 确定成员值即符号、美元数和美分数 两个对象相加 增加成员值 输出
currency类声明 class currency
{
public://构造函数currency(signType theSign plus,unsigned long theDollar 0,unsigned int theCents 0);//析构函数~currency(){};void setValue(signType, unsigned long, unsigned int);void setValue(double);signType getSign() const {return sign;}unsigned long getDollars() const {return dollars;}unsigned int getCents() const {return cents;}currency add(const currency) const;currency increment(const currency);void output() const;
private:signType sign;//符号unsigned long dollars;//美元unsigned int cents;//美分
}; 类的成员声明有两个部分公有public和私有private其实还有一个保护protect。公有部分所声明的是用来操作类对象或实例的成员函数又称方法。对类的用户是可见的是用户与类对象进行交互的唯一手段。私有部分所声明的是用户不可见的数据成员如简单变量、数组及其他可赋值的结构和成员函数。通过公有和私有以及保护部分我们可以让用户只看到他们所看到的部分同时把其余的部分隐藏起来这部分通常是与实现细节有关内容。
尽管C语法可以在公有部分声明数据成员但是优秀的软件设计者不会这样做。
上面代码中公有部分的第一个成员函数与类名相同这种名称与类名相同的成员函数称为构造函数construction。构造函数指明了创建一个类对象的方法而且没有返回值。~加上类名的这种成员函数被称为析构函数destructor每当一个类对象超出作用域的时候析构函数就会自动调用来删除这个对象。
在创建一个class类对象如果没有构造函数系统会自动调用空的构造函数同理如果没有析构函数那么也会自动调用回收的系统析构函数。
创建currency类对象的方式有如下两种 currency f, g(plus, 3, 45), h(minus, 10);
currency *m new currency(plus, 8, 12); 调用成员函数的方式有 g.setValue(minus, 33, 0);
h.setValue(20.52); 其中的g和h是currency的类对象也是函数的赋值对象。而所定义的类对象的关键字const是指这些函数值不会改变调用对象的值。我们把这种成员函数叫做常量函数constant function。
当然这里复制构造函数copy constructor没有在代码中实现。C将使用缺省复制构造函数仅仅复制数据成员。对于这个currency类而言缺省复制构造函数已经足够了但是对于很大一部分类我们最好还是在堆上空间进行开辟空间也就是自己去定义一个复制构造函数来进行复制数据缺省复制构造函数已经无法满足程序需要了。
currency的构造函数 currency::currency(signType theSign, unsigned long theDollars, unsigned int theCents)
{//创建一个currency类对象setValue(theSign, theDollars, theCents);
} 成员函数如果不在类声明体内部实现而在外部实现就必须要使用作用域说明符scope resolution operator::以指明该函数是currency类的成员函数。因此currency::currency表示currency类的构造函数而currency::output这表示currency类的output成员函数。
给私有数据成员赋值 void currency::setValue(signType theSign, unsigned long theDollars, unsigned int theCents)
{//给调用对象的数据成员赋值if(theCent 99)throw illegalParameterValue(Cents should be 100);sign theSign;dollars theDollars;cents theCents;
}
void currency::setValue(double theAmount)
{//给调用对象的数据成员赋值if(theAmount 0){sign minus;theAmount -theAmount;}else sign plus;dollars (unsigned long) theAmount;//提取整数cents (unsigned int) ((theAmount 0.001 - dollars) * 100);//提取两位小数
} 这是两个成员函数setValue的代码。第一个成员函数验证参数值的合法性只有当参数合法了才能拿来给调用函数的私有数据成员赋值。如果参数不合法就抛出一个类型为illeaglParametervalue的异常。第二个函数参数不验证参数值的合法性仅用小数点后面头两位数字。
但是我们知道用计算机来表示一些小数是不够精确的比如说5.29用计算机表示是5.2899那么加上0.001在通过强制转换把它转换为整型那么再通过*100的操作就可以拿到相应的数据啦。
把两个currency对象的值相加 currency currency::add(const currency x) const
{//把x和*this相加long a1, a2, a3;currency result;//把调用对象转化为符号整数a1 dollars * 100 cents;if(sign minus) a1 -a1;//把x转化为符号整数a2 x.dollars * 100 x.cents;if(x.sign minus) a2 -a2;a3 a1 a2;//转换为currency对象的表达式if(a3 0){result.sign minus;a3 -a3;}else result.sign plus;result.dollars a3 / 100;result.cents a3 - result.dollars * 100;return result;
} 上述程序是方法add的代码它首先把要相加的两个对象转化为整数如$2.32转换为232。引用调用对象的数据成员与引用对象x的数据成员在语法上是有区别的。x.dollars指的是x的数据成员而前面没有对象名称的dollars指的是调用对象的数据成员。当方法add终止时局部变量a1、a2、a3和ans被析构函数删除它们的空间也均被释放。
函数increment和output currency currency::increment(const currency x)
{//增加x*this add(x);return *this;
}
void currency::output() const
{//输出调用对象的值if(sign minus) cout -;cout $ dollars .;if(cents 10) cout 0;cout cents;
} 在C中保留关键字this指向调用对象*this就是调用对象。以调用语句g.increment(h)为例方法increment第一行语句调用公有函数add它把x即h与调用对象g相加然后把相加的结果作为返回值赋值给 *this而 *this就是g。我们使用了返回引用这样就省略返回值的复制过程。
类currency的数据成员已经设为私有private类的用户不能直接访问这些成员。因此用户通过下列的语句可以直接改变私有数据成员的值 h.cents 20;
h.dollars 100;
h.sign plus; 如果数据成员在处理之前是有效的而且经过成员函数处理之后仍然是有效的那么我们就能保证它们在经过用户程序处理之后依然是有效的因为用户程序是通过成员函数来处理数据成员的。构造函数和成员函数setValue的代码在使用数据之前都要验证它的有效性。而其余的函数特性是如果数据在处理之前有效那么在处理之后仍然是有效的。
类currency的应用
#includeiostream
#includecurrency.h
using namespace std;
int main()
{currency g, h(plus, 3, 50), i, j;//使用两种形式的setValue来赋值g.setValue(minus, 2, 25);i.setValue(-6.45);//调用成员函数add和outputj h.add(g);h.output();cout ;g.output();cout ;j.output();cout endl;//连续调用两次成员函数addj i.add(g).add(h);//省略了输出语句//测试异常cout Attempting to initalize with cents 152 endl;try{i.setValue(plus, 3, 152);}catch(illegalParameterValue e){cout Caught thrown exception endl;e.outputMessage();}return 0;
}
这段代码假定类声明和类实现都在文件currency.h之中但是这种分置对后续章节要引入的大量模板函数和模板类是行不通的。
4.2.一种不同的描述方法
假设已经有很多应用程序采用我们上面的currency类的形式现在我们想要修改对currency类对象的数据描述是应用最多的两个成员函数add和increment运行更快进而提高应用程序的执行速度。因为用户仅仅通过公有部分所提供的的接口与currency类进行交互所以对私有部分的修改不会影响应用程序的正确性。因此私有部分修改而应用程序不用改。
类currency的新声明 class currency
{
public://构造函数currency(signType theSign plus,unsigned long theDollars 0,unsigned int theCents 0);//析构函数~currency(){}void setValue(signType, unsigned long, unsigned int);void setValue(double);signType getSign() const{if(amount 0) return minus;else return plus;}unsigned long getDollars() const{if(amount 0) return (-amount) / 100;else return amount / 100;}unsigned int getCents() const{if(amount 0) return -amount - getDollars() * 100;else retrun amount - getDollars() * 100;}currency add(const currency) const;currency increment(const currency x){amount x.amount;return *this;}void output() const;
private:long amount;
}; 构造函数和成员函数setValue的新代码 currency::currency(sigeType theSign, unsigned long theDollars, unsigned int theCents)
{//创建一个currency类对象setValue(theSign, theDollars, theCents);
}
void currency::setValue(signType theSign, unsigned long theDollars, unsigened int theCents)
{//给调用对象赋值if(theCents 99)//美分值太大throw illegalParamenterValue(Cents should be 100);amount theDollars * 100 theCents;if(theSign minus) amount -amount;
}
void currency::setValue(double theAmount)
{//给调用对象赋值if(theAmount 0)amount (long) ((theAmount - 0.001) * 100);elseamount (long) ((theAmount 0.001) * 100);//取两个十位数
} 成员函数add和output的新代码
currency currency::add(const currency x) const
{//把x和*this相加currency y;y.amount amount x.amount;return y;
}
void currency::output() const
{//输出调用对象的值long theAmount amount;if(theAmount 0){cout -;theAmount -theAmount;}long dollars theAmount / 100;cout $ dollars .;int cents theAmount - dollars * 100;if(cents 10) cout 0;cout cents endl;
}
4.3.操作符重载
类currency有若干成员函数和C标准操作符类似。例如add实施的是操作increment实施的是操作。使用这些标准的C操作符比定义新的诸如add和increment的成员函数要自然多得多。为了使用这些操作符和我们进行操作符重载operator overloading它可以扩大C操作符的应用范围使其操作新的数据类型或类。
包含操作符重载的类声明
class currency
{
public://构造函数currency(signType theSign plus,unsigned long theDollars 0,unsigned int theCents 0);//析构函数~currency(){};void setValue(signType, unsigned long, unsigned int);void setValue(double);signType getSign() const{if(amount 0) return (-amount) / 100;else return amount / 100;} unsigned int getCents() const{if(amount 0) return -amount - getDollars() * 100;else return amount - getDollars() * 100;}currency operator(const currency) const;currency operator(const currency x){amount x.amount;return *this;}void output(ostream) const;
private:long amount;
};
上述代码的类声明分别用操作符和替代了add和increment。成员函数output用一个输入流的名字作为参数。
、output和的代码
currency currency::operator(const currency x) const
{//把参数对象x和调用函数*this相加currency result;result.amount amount x.amount;return result;
}
void currency::output(ostream out) const
{//把货币值插入流outlong theAmount amount;if(theAmount 0){out -;theAmount -theAmount;}long dollars theAmount / 100;out $ dollars * 100;int cents theAmount - dollars * 100;if(cents 10) out 0;out cents;
}
//重载
ostream operator(ostream out, const currency x)
{x.output(out);return out;
}
上述程序给定add和output的新代码以及重载的C流插入操作符的代码。
注意我们重载流插入操作符但没有把它声明为类的成员函数而是把重载和声明为类的成员函数。同样我们也可以重载流提取操作符而没有把它声明为类的成员函数。还要注意使用成员函数output有助于对流插入操作符的重载。因为非成员函数不能访问currency对象的私有成员重载的不是成员函数而重载的是所以重载的代码不能直接引用要插入到输出流的对象x的私有成员。
使用重载操作符
#includeiostream
#includecurrencyOverload.h
using namespace std;
int main()
{currency g, h(plus, 3, 50), i, j;//使用两种形式的setValue来赋值g.setValue(minus, 2, 25);i.setValue(-6.45);//调用成员函数add和outputj h g;cout h g j endl;//连续两次调用成员函数addj i g h;cout i g and then add h endl;//调用成员函数increment和addcout Incement i by g and then add h endl;j (i h) h;cout Result is j endl;cout Incemented object is i endl;//测试异常cout Attempting to iniitialize with cents 152 endl;try{i.setValue(plus, 3, 152);}catch(illegalParameterValue e){cout Caught throw exception endl;e.outputMessage();}return 0;
}
4.4.友元和保护性类成员
对一个类的私有成员仅有类的成员函数才能直接访问。我们必须给予别的类和函数直接访问该类私有成员的权利。这就需要这些类和函数声明为该类的友元friend。
重载友元操作符
class currency
{friend ostream operator(ostream, const currency);public:
};
//重载
ostream operator(ostream out, const currency x)
{//把货币值插入流outlong theAmount x.amount;if(theAmount 0){out -;theAmount -theAmount;}long dollars theAmount / 100;out $ dollars .;int cents theAmount - dollars * 100;if(cents 10) out 0;out cents;return out;
}
当我们把ostream operator声明为currency类的友元它就可以直接访问currency类的所有成员私有和公有这时也就不用另外定义成员函数output了。为了建立友元我们在currency类的描述中引入了friend语句。
一个类A从另外一个类B派生A是派生类derived classB是基类base class。派生类需要访问基类的部分或所有成员为此C提供了第三方类成员——保护性类成员protected。保护性成员类似于私有成员区别于派生类函数可以访问基类的保护性成员。
用户应用程序可以访问的类成员应该是公开的。数据成员永远不要出现在公有部分但是他们可以定义为保护性成员或者私有成员。优秀的软件工程师设计原则要求数据成员是私有的。通过成员函数派生类可以间接访问基类的私有数据成员同时修改基类的实现代码时不用修改它的派生类。
4.5.增加#ifndef、#define和#endif语句
在上面的文件currency.h或者currencyOverload.h包含了currency类的声明和实现细节。在文件头加上以下的语句 #ifndef Currency_
#define CUrrency_ 在文件尾添加上 #endif 包含在这组语句之内的代码只能编译一次。
5.异常类illegalParameterValue
定义一个异常类
class illegalParameterValue
{
public:illegalParameterValue():message(Illegal parameter value){}illegalParameterValue(char* theMessage){message theMessage;}void outputMessage(){cout message endl;}
private:string message;
};
抛出illegalParameterValue类型的异常 int abc(int a, int b, int c)
{if(a 0 || b 0 || c 0)throw illegalParameterValue(All parameters should be 0);return a b * c;
} 捕捉illegalParameterValue类型的异常 int main()
{try {cout abc(2, 0, 4) endl;}catch(illegalParameterValue e){cout The parameters to abc were 2, 0, and 4 endl;cout illegalParameterValue exeception throw endl;e.outputMessage();return 1;}return 0;
} 6.递归函数
递归函数recursive function或方法自己调用自己。在直接调用直接递归direct recursion中递归函数f的代码包含了调用f的语句而在间接递归中递归函数f调用了函数gg有调用了函数h如此下去直到有调用了f。在深入探讨C递归函数之前我们来看看两个相关的数学概念——数学函数的递归定义和归纳证明。
6.1.递归的数学函数
数学中经常有这样的函数它自己定义自己。
在一个基础部分base component它包含n的一个或多个值对这些值f(n)是直接定的在递归调用部分recursive component右侧f有一个参数小于n因此重复应用递归部分可以把右侧f的表达式转换为基础部分。
比如说递归定义的斐波那契数列
F0 0, F1 1, Fn Fn-1 Fn-2 (n1)
Fibonacci-斐波那契数列问题
#includeiostream
using namespace std;
int m;//定义要求的第m项斐波那契数列的项
int fib(int i)
{if(i 0) return 0;if(i 1) return 1;return (fib(i - 1) fib(i - 2));//递归公式
}
int main()
{cout 请输出fib的项数;cin m;cout endl;cout 第 m 项的fibonacci fib(m) endl;return 0;
}
6.2.归纳
现在我们把注意力转移到与递归函数有关的第二个概念——归纳证明。
一般证明的方法是首先检验对n的一个或者多个基础值一般n0就可以公式成立。然后假设当n从0到m时公式成立其中m是任意一个大于或等于最大基础值的整数。最后根据这个假设证明当n等于m1时公式成立。这种证明方法有三个部分——归纳基础induction、归纳假设induction hypothesis和归纳步骤induction step。
在归纳假设中假设n≤m时公式均成立其中m是任意大于或等于0的整数假设nm时公式成立亦可。在归纳步骤阶段要证明当nm1时公式成立。
乍一看归纳证明好像是一个循环证明——因为我们给出的是一个假设为正确的结论其实不然。就像递归定义并不是循环定义一样。每一个正确的归纳证明都有一个归纳基础部分它与递归定义的基础部分相似。归纳步骤使用的是在归纳基础部分已经检验的正确结果。反复应用归纳步骤把证明部分转化为基础部分所具有的形式。
6.3.C递归函数
使用C可以编写递归函数。正确的递归函数必须包含基础部分。每一次递归调用其参数值都比上一次的参数值要小从而重复调用递归函数使参数值达到基础部分的值。
计算n!的递归函数
#includeiostream
using namespace std;
int n;
int factorial(int i)
{//计算n!if(i 1) return 1;else return i * factorial(i - 1);
}
int main()
{scanf(%d, n);printf(%d!的递归值%d, n, factorial(n));return 0;
}
阶乘程序是一个典型的C递归函数它利用相应的数学公式来计算阶乘n!。基础部分是n≤1.考虑到factorial(2)的计算过程。将factorial(2)挂起来然后调用factorial(1)。程序状态即局部变量和传值形参的值、与引用形参绑定的值、代码执行位置等被保留在递归栈中。当factorial(1)的计算结束时程序状态恢复。
累加数组元素a[0:n-1]
#includeiostream
#define N 1000
using namespace std;
templateclass T
T sum(T a[], int n)
{//返回值为数组元素a[0:n-1]的和T theSum 0;for(int i 0;i n; i){theSum a[i];}return theSum;
}
int main()
{int n, q[N];scanf(%d, n);for(int i 0;i n; i){scanf(%d, q[i]);}int total sum(q, n);printf(%d, total);return 0;
}
模板函数sum对数组元素a[0]至a[n-1]求和当n0时函数返回值是0。
当然累加数组元素也能使用递归代码 templateclass T
T rSum(T a[], int n)
{//返回值为数组元素a[0:n-1]的和if(n 0){return rSum(a, n-1) a[n-1];}return 0;
} 排列
我们常常要从n个不同元素的所有排列中确定一个最佳的排列。例如a、b和c的排列就有abc、acb、bac、bca、cab和cba。n个元素的排列个数是n!。
为了输出n个元素的所有排列编写非递归的C函数比较困难但是编写递归函数就没有那么困难了。设E{e1, ..., en}是n个元素的集合求E的元素的所有排列。令Ei表示从E中去除第i个元素ei以后的集合令perm(X)的表示集合X所有元素所组成的所有排列令ei.perm(X)表示在perm(X)中的每个排列加上前缀.ei之后的排列表。
当n1时是递归的基础部分。这时的集合E只有一个元素e因此只有一个排列perm(E)(e)。当n1时perm(E)是一个表。
使用递归函数生成排列 templateclass T
void permytations(T list[], int k, int m)
{//生成list[k:m]的所有排列if(k m){//list[k:m]仅有一个排列输出它copy(list, listm1,ostream_iteratorT(cout, ));cout endl;}else//list[k:m]有多于一个的排列递归的生成这些排列for(int i k;i m; i){swap(list[k], list[i]);permytations(list, k1, m);swap(list[k], list[i]);}
} 7.标准模板库
C标准模板库STL是一个容器、适配器、迭代器、函数对象也称仿函数和算法的集合。有效的使用STL应用程序的设计会简单许多。本书首先使用基本的C语言结构解决一个问题以说明求解问题的方法。然后利用STL说明如何使用更简单的方法解决同样的问题。
7.1.accumulate
STL有一个算法accumulate是对顺序表元素顺序累计求和。
它的语法accumulate(start, end, initialValue)
其中的start指向首元素end指向尾元素的下一个位置因此要累计求和的元素范围是[start, end]调用的语句也就是accumulate(a, an, theSum);
其中一个a是一维数组。返回值initialValuea[i]的和
利用STl的算法accumulate templateclass T
T sum(T a[], int n)
{//返回数组a[0:n-1]的累计和T thsSum 0;return accumulate(a, a n, theSum);
} STL的算法accumulate利用操作符从start开始到end结束相继访问要累计求和的顺序表元素。因此对于任意一个序列如果他的元素可以通过重复应用操作符来访问。一维数组和STl的vector容器都是这种顺序表的实例。
STL算法accumulate还有一个更加通用的形式accumulate(start , end, initialValue, operator)
其中operator是一个函数它规定了在累计过程中的操作。
计算数组元素a[0:n-1]的乘法 templateclass T
T product(T a[], int n)
{//返回数组a[0:n-1]的累计和T theProduct 1;return accumulate(a, an, theProduct, multipliesT());
} 7.2.copy和next_permutation
算法copy是把一个顺序表的元素从一个位置复制到另一个位置上去。语法copy(start, end, to);其中to给出了第一个元素要复制到的位置。因此元素从位置start,start1, ...,end-1依次复制到位置to, to1,...,toend-start。
算法next_permutation其语法为next_permutation
对范围[start, end)内的元素按字典顺序产生下一个更大的排列。当且仅当这个排列存在时返回true。
使用STL算法next_permutation求排列 templateclass T
void permutation(T list[], int k, int m)
{//生成list[k:m]的所有排列//假设k≤m//将排序逐个输出do{copy(list, listm1,ostream_iteratorT(cout, ));cout endl;}while(next_permutation(list, listm1));
} next_permutation算法具有更一般的形式它带有第三个参数compare。而compare函数用来判定一个排列是否比另一个排序小。