480 likes | 710 Views
《 C++ 语言程序设计 》 第二次直播课堂 主讲 首都经贸大学 李宁 副教授. 一、关于函数原形和头文件. 函数的定义和函数的原型 例: double cubic ( double d){ // 定义 return d*d*d; } double cubic ( double d); // 原形. 函数原形提供了生成函数调用代码所必须的接口信息,因此: 在函数的调用处之前,需通过函数原形提供该调用函数的接口信息。. 函数定义也能提供同样的接口信息,因此: 如果函数的定义出现在函数调用处之前,函数原形即可省略。.
E N D
《C++语言程序设计》 第二次直播课堂 主讲 首都经贸大学 李宁 副教授
一、关于函数原形和头文件 • 函数的定义和函数的原型 例:double cubic(double d){ //定义return d*d*d;}double cubic(double d); //原形
函数原形提供了生成函数调用代码所必须的接口信息,因此:函数原形提供了生成函数调用代码所必须的接口信息,因此: • 在函数的调用处之前,需通过函数原形提供该调用函数的接口信息。
函数定义也能提供同样的接口信息,因此: • 如果函数的定义出现在函数调用处之前,函数原形即可省略。
但是,通过函数定义来担负函数原形的任务存在着下列问题:但是,通过函数定义来担负函数原形的任务存在着下列问题: • 函数的定义必须出现在函数调用的同一文件中,限制了函数的使用范围;
函数定义必须以源程序的形式呈现,不便于将之作为产品提供;函数定义必须以源程序的形式呈现,不便于将之作为产品提供; • 不便于实现大系统,不便于分工。
应尽可能做到函数的实现(定义)与函数的使用(调用)的分离:应尽可能做到函数的实现(定义)与函数的使用(调用)的分离: • 首先设计函数原形,作为函数定义和函数调用必须共同遵守的接口标准; • 将定义函数的程序和使用函数的程序放在不同文件中。
头文件以固定的、便于利用的形式保存一组函数的调用接口信息,确保了接口信息描述的正确性和一致性。头文件以固定的、便于利用的形式保存一组函数的调用接口信息,确保了接口信息描述的正确性和一致性。 • 用#include命令将头文件插入到需要的程序文件中
二、关于函数模板 • 什么情况下不可省略模板实参? • 无法通过模板函数的参数隐含获得相关信息的模板参数; • 在任何不可省略参数前面的模板参数。
例1: template<class T1, class T2, class T3>T2 Max(T1 x, T3 y){ return T3(T3(x)>T3(y)? x : y);} //定义 cout<<Max<int,foat>(3,3.5); //调用 提示:未出现在模板函数参数表中的模板参数,其实参不得省略。
例2(教材例5.10): template <typename Type, int Rows,int Cols> void sumAll(Type data[][Cols], Type result[]){ for(int i=0;i<Rows;i++){ result[i]=0; for(int j=0;j<Cols;j++) result[i]+=data[i][j];}} //定义
int d[][3]={ {1,2,3},{2,3,4}, {4,5,6},{6,7,8}};int r[4];sumAll<int,4,3>(d,r);//调用 提示:对于普通类型(非虚拟类型)的模板参数,其实参不得省略。
例3: template<int Rows,int Cols, typename Type>.. .. .. //定义 .. .. .. sumAll<4,3>(d,r); //调用 提示:通过调整模板参数的顺序,有可能使更多的模板实参可以省略。
三、关于参数表中的数组 • 参数表中的数组就是指针 • 因此,定义参数表中的数组时不必给出第一维的大小; • 因此,对于参数表中的数组,也可以定义为指针,对应关系是:A[] ←→ *A; A[][]… ←→ (*A)[]…
因此,调用这样的函数时,只需以数组名作为实参;因此,调用这样的函数时,只需以数组名作为实参; • 因此,修改形参数组就是在修改形参指针所指向的数据,也就是在修改实参数组。但这并不违反“传值”原则,因为对应于形参的实参数组名(指针)并没有被修改;
四、动态空间的使用应避免的问题 • 悬挂访问:通过一个没有初始化的指针或空指针访问不存在的数据; • 存储泄漏:申请的动态空间用完后被丢弃,没有释放;
重复释放:同一动态空间被多次释放; • 申请操作与释放操作不配套例如用 malloc() 申请,用 delete释放;或用 new 申请,用 free() 释放。
五、两种重要的构造函数 • 无参构造函数 • 指: 未定义参数的构造函数,或 : 所有参数均为可选参数的构造函数。
如果类中包含一个类对象数据成员,而类的构造函数又没有对之进行初始化,则系统隐含调用该成员所属类的无参构造函数进行初始化。如果类中包含一个类对象数据成员,而类的构造函数又没有对之进行初始化,则系统隐含调用该成员所属类的无参构造函数进行初始化。
对于派生类,构造函数没有对基类进行初始化,则系统隐含调用基类的无参构造函数进行初始化。对于派生类,构造函数没有对基类进行初始化,则系统隐含调用基类的无参构造函数进行初始化。 • 每个类至少要有一个构造函数。如果没有定义,系统自动生成一个形如 X(){} 的默认无参构造函数。
由于无参构造函数有可能被隐含调用,因而类中通常应该有无参构造函数。如果类中已经定义了若干构造函数,应检查其中是否包含无参构造函数,如没有,应添加一个。由于无参构造函数有可能被隐含调用,因而类中通常应该有无参构造函数。如果类中已经定义了若干构造函数,应检查其中是否包含无参构造函数,如没有,应添加一个。
隐含调用无参构造函数例1 class XX{ int xx; public: XX(int n=0):xx(n){} ... }; class YY{ XX xd; double yy; public: YY(double d):yy(d){} //YY(…):xd( ),yy(d){} ... };
隐含调用无参构造函数例2 class YY{ XX xd; double yy; public: YY(double d):yy(d){} … }; class ZZ:public YY{ long k; public: ZZ(long m):k(m){} //错误! … };
拷贝构造函数 • 指:参数中只包含一个同类对象引用的构造函数。 • 用于创建一个与参数所引用对象相同的对象,这种创建对象的方式称为拷贝(复制,克隆)。
在调用一个具有类对象参数的函数时,系统隐含地以实参对象为参数调用拷贝构造函数,创建形参对象。在调用一个具有类对象参数的函数时,系统隐含地以实参对象为参数调用拷贝构造函数,创建形参对象。 • 对于返回值为类对象的函数,在用return 返回时,系统隐含地以标return 语句中的对象为参数调用拷贝构造函数,创建作为返回值的对象。
每个类至少要有一个拷贝构造函数。如果没有定义,系统自动生成一个默认的拷贝构造函数,它以“浅层复制”的方式构造新对象。每个类至少要有一个拷贝构造函数。如果没有定义,系统自动生成一个默认的拷贝构造函数,它以“浅层复制”的方式构造新对象。 • 对于必须通过“深层复制”才能正确构造相同对象的情况,应当定义专门的拷贝构造函数。
假定class String{char *p;public:String(const char *c){p=c;}…};中没有定义拷贝构造函数,
则执行String s1("This is a string"); String s2(s1);的效果是:
s1 p p s2 “浅层复制"示意
在 String 中增加如下的拷贝构造函数,实现“深层复制” :String::String(String &s){ p=new char[strlen(s.p)+1]; strcpy(p,s.p);}; 这样,执行同样的语句序列的效果是:
s1 p p s2 “深层复制”示意
如果定义了专门的拷贝构造函数,一般也就需要重载专门的赋值操作符。如果定义了专门的拷贝构造函数,一般也就需要重载专门的赋值操作符。 例如,String 中就应重载赋值操作符 = 如下:
String& String::operator = (const String &s){ if(p) delete []p; p=new char[strlen(s.p)+1]; strcpy(p,s.p); return *this;};
六、关于操作符的特性 • 操作数个数 • 优先级和结合性 • 是否要求第一操作数必须是变量或视同变量的数据
是否有副作用(是否修改第一操作数) • 操作结果是否为视同变量的数据(是否就是第一操作数)
操作名称 操作符 第一操作数 副作用 操作结果 前增1 ++ 变量或视同变量 有 视同变量 前减1 -- 赋值 = 复合赋值 += *= … 后增1 ++ 视同常量 后减1 -- 取地址 & 无 间接访问 * 任意 视同变量 其他 + - == … 视同常量
七、关于操作符重载 • 应保持操作符原有的基本语义,重载的操作符应体现为原操作符功能在新的数据类型中的延伸 • 应尽量保持操作符原有的特性
优先级、结合性和操作数个数这三个特性自动得以保持优先级、结合性和操作数个数这三个特性自动得以保持 • +=、*=、++(前增1)等操作符要求第一操作数必须是变量;当作为非成员函数重载这类操作符时,为了达到同样效果,第一参数应说明为引用
=、+=、*=、++等具有副作用的操作符,除后增1、后减1之外,其操作结果视同变量。当重载这类操作符时,为了达到同样效果,返回值应说明为引用,return 语句应返回第一参数(对于非成员函数重载),或返回*this(对于成员函数重载)
例如,例 8.1 的 Fraction 类(分数类)中重载了前增1操作符: Fraction &opertor++(){ nume+=deno; return *this; }
因此执行 Fraction x(2,3); ++++x; 后,x 变为8/3
操作符的重载应当配套 • 如果重载了+、-,应考虑同时重载 ++、-- • 如果重载了==,应应考虑同时重载!= • 如果重载了>,应应考虑同时重载<……
作为成员函数重载还作为非成员函数重载? • 某些操作符只能作为成员函数重载,包括:=、[ ]、( )、->以及所有类型转换操作符。
若第一操作数不是同类对象,而是其他数据类型,则只能作为非成员函数重载(如输入输出流操作符 >> 和 << )。
其他情况既可作为成员函数,也可作为非成员函数重载。其他情况既可作为成员函数,也可作为非成员函数重载。 • 作为非成员函数重载时,可将其声明为友元。
使用引用参数还是非引用参数? • 使用引用参数可减少参数传递过程中的数据复制量 • 如果操作符作为非成员函数重载,且该操作符要修改第一操作数(如+=),则代表第一操作数的第一参数必须是引用参数
使用非引用参数可利用只需一个实参的构造函数实现类型的自动转换使用非引用参数可利用只需一个实参的构造函数实现类型的自动转换 例:例 8.1 中 增加构造函数: Fraction(int n=0,int d=1) :num(n),deno(d){ FracSimp();}
类中已重载+,原形是: Fraction operator+(Fraction&); 因此可以完成下面的运算: Fraction x,y(2,5); x=y+Fraction(2); 但如使用非引用参数,后一句也可改为: x=y+2;