返回 1
第三章 面向对象的程序设计本章导读掌握类与对象的概念,类与对象的定义方法及二者间的区别。
掌握类的成员函数的定义方法,保存方法及调用方法 。
掌握类中成员的访问机制和方法 。
了解对象的作用域和生存期 。
理解并掌握构造函数,析构函数,拷贝构造函数,默认构造函数和缺省参数的构造函数的含义,定义方法以及在对象的构造和撤消中的作用 。
理解并掌握当一个类的对象作为另一个类的数据成员时,利用初始化表调用构造函数的方法,构造函数的执行顺序 。
返回 2
本章导读理解继承的概念和意义,理解单一继承、多重继承。理解并掌握派生类构造函数的编写要求,以及派生类对象的构造过程和机理。
掌握虚函数和多态性的概念,掌握虚函数的定义方法、
调用方法及其在实现多态性方面所起到的作用。了解纯虚函数与抽象基类的概念。
了解类的静态成员 ( 静态数据成员和静态成员函数 ) 的概念,定义方法及其作用 。
了解友元函数与友元类的概念,定义方法及其作用 。
了解运算符重载及在程序中实现运算符重载的方法 。
了解模板的概念,在程序中如何定义类模板和函数模板。
返回 3
3.1 类与对象的定义类和对象是面向对象程序设计 ( OOP) 的两个最基本概念 。 所谓 对象 就是客观事物在计算机中的抽象描述;
类 是对具有相似属性和行为的一组对象的统一描述 。
3.1.1 类的定义
C++的类是在结构体的基础上扩充而来的 。 类是把各种不同类型的数据 ( 称为数据成员 ) 和对数据的操作
( 成员函数 ) 组织在一起而形成的用户自定义的数据类型 。
C++中,类定义包括类说明和类实现两大部分 。 说明部分提供了对该类所有数据成员和成员函数的描述,而实现部分提供了所有成员函数的实现代码 。
返回 4
3.1 类与对象的定义类定义的一般形式为:
class 类名
{private:
数据成员或成员函数
protected:
数据成员或成员函数
public:
数据成员或成员函数
};
<各成员函数的实现代码 >
返回 5
3.1 类与对象的定义说明:
1,class是定义类的关键字,类名由用户自己定名,必须是 C++的有效标识符,但一般首字母大写 。
2,大括号的部分是类的成员 ( 数据成员和函数成员 ),
它们分成三部分,分别由 private,public,proctected三个关键字后跟冒号来指定 。 这三部分可以任何顺序出现,
且在一个类的定义中,这三部分并非必须同时出现 。
( 1) 如果数据成员或成员函数在类的 private部分,那么在类之外是不能存取的,只有类中的成员函数才能存取
private的数据成员和成员函数 。
( 2) 在一个类的 public部分说明的数据成员或成员函数可被程序中的任何函数或语句存取,public成员多为成员函数,用来提供一个与外界的接口,外界只有通过这个接口才可以实现对 private成员的存取 。
返回 6
3.1 类与对象的定义
( 3)在类的 protected部分说明的数据成员和成员函数是不能在类之外存取的,只有类的成员函数及其子类(派生类)可以存取 protected的成员。
( 4) 当定义类时,当未指明成员是哪部分时,默认是属于 private成员,但一般不要采用默认形式 。
如:下例中定义描述图书的类定义
class Record
{ private,//private成员
char bookname[20]; //数据成员 bookname,
// 用于表示图书的名称
int number; //数据成员 number,表示图书编号返回 7
3.1 类与对象的定义
public,//public成员
void regist(char *a,int b); //成员函数 regist,用于给
//各数据成员赋值
void show(); //成员函数 show,显示各数据成员的值
};
3,要特别注意,在类的定义中,类的说明部分的右边大括号后面必须有一,;,,
4,根据类的定义,可看出:类是实现封装的工具,所谓封装就是将类的成员按使用或存取的方式分类,有条件地限制对类成员的使用,而封装是通过 public和
private与成员函数实现的 。 private的成员构成类的内部状态,public的成员则构成与外界通信的接口,通过 public的成员函数来使用 private的数据成员,从而在
C++中实现了封装 。
返回 8
3.1 类与对象的定义
3.1.2成员函数的定义类中的成员函数可以在以下两处定义:
( 1) 将成员函数的定义直接写在类中,
如:对于前面定义的图书类 Record来说,其成员函数
regist和 show的定义可直接写在类的定义体中 。
class Record
{ private:
char bookname[20];
int number;
返回 9
3.1 类与对象的定义
public:
void regist(char *a,int b) //成员函数 regist()的定义
{ strcpy(bookname,a); //给数据成员 bookname赋值
number=b; //给数据成员 number赋值
}
void show() //成员函数 show()的定义
{ cout<<”名称:,<<bookname<<endl;
cout<<”号码:,<<number<<endl;
}
};
返回 10
3.1 类与对象的定义在类中直接定义成员函数的情况一般适合于成员函数规模较小的情况,也就是说它们一般为内联函数,即使没有明确用 inline关键字 。
( 2) 在类的定义体中只写出成员函数的原型说明,而成员函数的定义写在类的定义之后,这种情况比较适合于成员函数体较大的情况,但这时要求在定义成员函数时,在函数的名称之前加上其所属性类名及作用域运算符,,:” 。
定义成员函数的一般类型为:
返回值类型 类名::成员函数名 ( 参数说明 )
{ 类体 }
返回 11
3.1 类与对象的定义此处的,:符号叫作用域运算符,用它来指明哪个函数属于哪个类或哪个数据属于哪个类,所以使用类中成员的全名是:类名,:成员名 。
而如果没有类名,则为全局数据或全局函数 ( 非成员函数 ),也就是说类名是其成员名的一部分 。
如 class Record
{ private:
char bookname[20];
int number;
public:
void regist(char *a,int b); //成员函数 regist的原型
void show(); //成员函数 show的原型
}; //定义图书类 Record
返回 12
3.1 类与对象的定义
void Record::regist(char *a,int b) //regist()是类 Record的
//成员函数
{ strcpy(bookname,a);
number=b; }
void Record::show() // show()是类 Record的成员函数
{ cout<<”名称:,<<bookname<<endl;
cout<<”号码:,<<number<<endl; }
此外,目前开发程序的通常将类的定义写在一个头文件 (,h文件 ) 中,成员函数的定义写在一个程序文件
(,cpp文件 ) 中,这样,就相当于把类的定义 ( 头文件 )
看成是类的外部接口,类的成员函数的定义看成类的内返回 13
3.1 类与对象的定义部实现 。 如:对上例可改成将类的定义体写在 myapp.h文件中,而成员函数的定义体写在另外一个文件 myapp.cpp中:
//myapp.h文件
class Record
{ private:
char bookname[20];
int number;
public:
void regist(char *a,int b);
void show();
};
返回 14
3.1 类与对象的定义
//myapp.cpp文件
#include,iostream.h”
#include,myapp.h” //一定不要忘记嵌入该头文件
void record::regist(char *a,int b)
{ strcpy(bookname,a);
number=b;
}
void record::show()
{ cout<<”名称:,<<bookname<<endl;
cout<<”号码:,<<number<<endl;
}
返回 15
3.1 类与对象的定义
3.1.3 对象的定义对象是类的实例,定义对象的一般格式为:
类名 变量名表;
或 类名 对象名;
如:上例中已定义了类 Record,则:
Record book1,book2; //此处的 book1,book2就是 Record
//类型,也就是类的两个对象类是抽象的概念,而对象是具体的,类只是一种数据类型,而对象是属于该类 ( 数据类型 ) 的一个变量,占用了各自的存储单元,每个对象各自具有了该类的一套数据成员 ( 静态成员除外 ),而所有成员函数是所有对象共有的 。 每个对象的函数成员都通过指针指向同一个代码空间 。
返回 16
3.1 类与对象的定义
3.1.4 访问对象的成员访问对象的成员包括读写对象的数据成员和调用它的成员函数,其访问格式是:
对象名,成员名如上例中,对象的主函数如下:
void main()
{ Record book1,book2; //定义对象 book1和 book2
//调用成员函数 regist,给 book1的两个数据成员
//bookname和 number赋值
book1.regist(“C++编程教程,,1001);
//调用成员函数 regist,给 book2的两个数据成员赋值
book2.regist(“C++语言参考,,1002);
返回 17
3.1 类与对象的定义
//调用成员函数 show,显示 book1对象的数据成员
//bookname和 number的值
book1.show();
//调用成员函数 show,显示 book2对象的数据成员
//bookname和 number的值
book2.show();
}
如改为下面的代码,则错误:
void main()
{ Record book1,book2;
//由于 bookname和 number是类 Record的私有成员,在类外
//不能直接使用返回 18
3.1 类与对象的定义
strcpy(book1.bookname,“C++编程教程,);
book1.number=1001;
strcpy(book2.bookname,“C++语言参考,);
book2.number=1002;
book1.show();
book2.show();
}
注意:
1,对于类的私有成员,只能通过其成员函数来访问,不能在类外对私有成员访问 。
返回 19
3.1 类与对象的定义
2,调用成员函数时要在函数名之前加上对象名和 "."即可,
即先指明对象,再指明成员 。 也可以采用指向对象的指针来访问,但要在函数名前加上指针变量名和,->” 。
3,任何对对象私有数据的访问都必须通过向对象发送消息来实现,而且所发送的消息还必须是该对象能够识别和接受的 。 在 C++中,消息发送正是通过公有成员函数的调用来实现的 。 由于类接口隐藏了对象的内部细节,用户只能通过类接口访问对象,因此,在类设计中必须提供足够的公有接口以捕获对象的全部行为,这正是类设计中的一个最基本的要求 。
4,上例中,在对象调用 book1.regist(“C++编程教程,,
1001);时,成员函数 regist除了接受两个实参外,还接返回 20
3.1 类与对象的定义受了一个对象 book1的地址,这个地址被一个隐含的形参
this指针所获取,它等同于执行 this=&book1,所以所有对数据成员的访问都隐含地被加上前缀,this->,因此,
在成员函数体 regist中,执行
strcpy(bookname,a);number=b;就等价于
strcpy(this->bookname,a); this->number=b;
这样,上例中的成员函数 regist也可这样定义:
void record::regist(char *a,int b)
{ strcpy(this->bookname,a);
this->number=b;
}
通过以上手段就确保了不同对象调用成员函数时访问的是不同对象的数据,而它们之间没有干扰 。
返回 21
3.1 类与对象的定义
3.1.5 对象赋值语句对于同一个类生成的两个对象,可以进行赋值,
其功能是将一个对象的数据成员赋值到另一个对象中去,赋值语句的左右两边各是一个对象名:
【 例 3-1】 对于类 example的两个对象 obj1和 obj2,
让 obj2的成员数据的值等于 obj1的成员数据的值 ( 假定 obj1的成员数据 num已经存有数据 215) 。
返回 22
3.1 类与对象的定义
# include <windows.h>
# include <iostream.h>
//定义类
class example
{ private,// 数据成员
int num;
public,// 函数成员说明
void set ( int i )
{ num=i ; }
void disp ( )
{ cout << "\n num = " << num ; }
};
返回 23
3.1 类与对象的定义
// 主程序
void main ()
{ example obj1,obj2 ;
obj1.set(215);
obj1.disp ();
obj2=obj1; // 对象赋值语句
obj2.disp () ;
cout<<endl<<endl; }
3.1.6 对象的作用域与生存期对象是类的实例,它实质就是某种数据类型的变量,
在不同的位臵以不同的方式定义对象时,其作用域和生存期是不同的 。
返回 24
3.1 类与对象的定义如,class Desk //定义 Desk类
{ public:
int weight;
int high;
int width;
int length; };
class Stool //定义 Stool类
{ public:
int weight;
int high;
int width;
int length;};
返回 25
3.1 类与对象的定义
desk da; //定义全局对象
Stool sa;
void fn()
{ static Stool ss; // 静态局部对象
desk da; // 定义局部对象
//
}
1,局部对象 ( 不包括局部静态对象 )
其作用域是定义它的函数体,生存期从函数调用开始到函数调用结束,下一次再重新调用函数时,再重新构造对象 。
构造局部对象的次序 ( 即分配存储单元 ) 是按它们在函数体中声明的顺序 。
返回 26
3.1 类与对象的定义
2,静态对象 ( 局部静态和全局静态 )
其作用域是定义它的函数体或程序文件,其生存期是整个程序 。 构造静态对象的次序是按它们在程序中出现的次序先后,并在整个程序运行开始时 ( 即在主函数运行前 )
只构造一次 。
3,全局对象全局对象的作用域是整个程序,生存期是整个程序的运行时间 。 它也是在程序运行前 ( 即在主函数运行前 ) 只构造一次 。
4.类中成员的构造次序是以类中声明成员的次序进行。
构造函数和析构函数是类的两种特殊的成员函数 。
返回 27
3.2 构造函数与析构函数
3.2.1 构造函数构造函数 ( constructor) 是与类名同名的特殊的成员函数,当定义该类的对象时,构造函数将被自动调用以实现对该对象的初始化 。 构造函数不能有返回值,因而不能指定包括 void在内的任何返回值类型 。 构造函数的定义体可与其它成员函数成员一样,放在类内或类外都可 。 构造函数的定义格式为:
类名 ( 形参说明 )
{ 函数体 }
构造函数既可定义成有参函数,也可义成无参函数,要根据问题的需要来定 。 全局变量和静态变量在定义时,将自动赋初值为 0;局部变量在定义时,其初始值不固定的 。
而当对象被定义时,由于对象的意义表达了现实世界的实返回 28
3.2 构造函数与析构函数体,所以一旦定义对象,就必须有一个有意义的初始值,
在 C++中,在定义对象的同时,给该对象初始化的方法就是利用构造函数 。 如:
【 例 3-2】 类 person包括 4个数据成员,用来记录人员信息。
生成对象 obj,并使用构造函数为 obj赋予初始值。
# include <windows.h>
# include <iostream.h>
class Person //定义类
{ private,//类 Person的数据成员
char name [10] ; //姓名
int age ; //年龄
int salary ; //薪金
char tel[8]; //电话返回 29
3.2 构造函数与析构函数
public,//构造函数 Person
Person ( char *xname,int xage,int xsalary,char *xtel ) ;
void disp () ; };
//函数 Person的定义
Person,,Person ( char *xname,int xage,int xsalary,char
*xtel )
{ strcpy (name,xname) ; //给各数据成员提供初值
age = xage ;
salary = xsalary ;
strcpy (tel,xtel) ;
}
返回 30
3.2 构造函数与析构函数
//函数 disp的定义
void Person::disp()
{ cout<<endl;
cout << " 姓名," << name << endl ;
cout << " 年龄," << age << endl ;
cout << " 工资," << salary << endl ;
cout << " 电话," << tel << endl<<endl ;}
// 主函数
void main( )
{ //生成对象 obj并初始化
Person obj ("张立三 ",25,850,"45672314");
//显示 obj
obj.disp ( ) ; }
返回 31
3.2 构造函数与析构函数程序的执行结果是:
姓名:张立三年龄,25
工资,850
电话,45672314
在主函数中的 Person obj ("张立三 ",25,850,"45672314");中完成了以下几个功能:
1,定义并生成了对象 obj。
2.在生成对象 obj的同时,自动调用相应类的构造函数 Person
3.将初始值 "张立三 ",25,850,"45672314"传递给构造函数
Person相应的形参 xname,xage,xsalary,xtel。
4,执行构造函数体,将相应的值赋给相应的数据成员 。
返回 32
3.2 构造函数与析构函数
3.2.2 构造函数的重载如果一个类中出现了两个以上的同名的成员函数时,
称为类的成员函数的重载。
【 例 3-3】 类 rec定义两个重载函数,其中一个是无参函数,
另一个是有参函数 。 它们都是构造函数 。
# include <windows.h>
# include <iostream.h>
//定义类
class Rec
{ private:
char bookname[30];
int number;
返回 33
3.2 构造函数与析构函数
public:
Rec(); //第 1个构造函数说明
Rec (char *a,int b); //第 2个构造函数说明
void show();
};
Rec,,Rec () //第 1个构造函数定义
{ strcpy(bookname,'\0');
number=0; }
Rec,,Rec (char *a,int b ) //第 2个构造函数定义
{ strcpy(bookname,a);
number=b; }
返回 34
3.2 构造函数与析构函数
void Rec,,show ( ) //show的函数定义
{ cout<<"bookname is,"<<bookname<<endl;
cout<<"booknumber is:"<<number<<endl;
}
void main() //主程序
{ Rec mybook(,Visual C++6.0”,10020); //自动调用构造
//函数 Rec(char *a,int b)
mybook.show();
Rec yourbook; //自动调用构造函数 Rec()
yourbook.show();
}
返回 35
3.2 构造函数与析构函数程序的执行结果是:
bookname is,Visual C++6.0
booknumber is:10020
bookname is,no name
booknumber is:0
可见,当出现构造函数重载时,其匹配方式同普通函数重载时的匹配方式 。
返回 36
3.2 构造函数与析构函数
3.2.3 默认构造函数与缺省构造函数
C++规定,每个类必须有一个构造函数 。 如果在类中没有显式定义构造函数时,则 C++编译系统在编译时为该类提供一个默认的构造函数,该默认构造函数是个无参函数,它仅负责创建对象,而不做任何初始化工作 。
只要一个类定义了一个构造函数 ( 不一定是无参构造函数 ),C++编译系统就不再提供默认的构造函数 。
与变量定义相似,在用默认构造函数创建对象时,如果创建的是全局对象或静态对象,则对象的默认值为 0,
否则对象的初始值是不定的 。
当构造函数有缺省参数时,称为具有缺省参数的构造函数,在使用时要防止二义性 。
返回 37
3.2 构造函数与析构函数如:
class Myclass //定义类 Myclass
{ private:
int member;
public:
Myclass();
Myclass(int i);
}
Myclass:Myclass() //构造函数 Myclass
{ member=10; }
返回 38
3.2 构造函数与析构函数
Myclass:Myclass(int i=10) //构造函数 Myclass(int i),该函数
// 的形参 i为缺省参数
{ member=i; }
void main()
{ Myclass x(20);
Myclass y; //产生二义性,无法确定自动调用哪个构造
//函数完成对象的构造
}
返回 39
3.2 构造函数与析构函数
3.2.4 析构函数当一个对象被定义时,系统自动调用构造函数为该对象分配相应的资源,当对象使用完毕后,这些系统资源需要在对象消失前被释放 。
析构函数是类的一个特殊成员函数,其函数名称是在类名的前面加上 ~,它没有返回值,没有参数,不能随意调用,也没有重载,只是在类对象生命期结束时,系统自动调用 。 析构函数的定义方式为:
~类名 ( )
{ 函数体 }
注,( 1) 一个类中只能拥有一个析构函数 。
返回 40
3.2 构造函数与析构函数
( 2) 如果程序员在定义类时,没有为类提供析构函数,
则系统会自动创建一个默认的析构函数,其形式为:
~类名 ( ) { }
( 3) 对于一个简单的类来说,大多可以直接使用系统提供的默认析构函数 。 但是,如果在类的对象中分配有动态内存 ( 如:用 new申请分配的内容 ) 时,就必须为该类提供适当的析构函数,完成清理工作 。
( 4) 对象被析构的顺序与对象建立时的顺序正好相反 。
即最后构造的对象先被析构 。
返回 41
3.2 构造函数与析构函数
【 例 3-4】 类 Teacher的构造函数为 name申请存储空间,在析构函数中释放该空间 。
# include <windows.h>
# include <iostream.h>
//定义类
class Teacher
{ private:
char * name;
int age;
public:
//说明构造函数 Teacher
返回 42
3.2 构造函数与析构函数
Teacher(char *i,int a )
{ name=new char[strlen(i)+1] ;
//用 new为 name成员分配堆内存
strcpy (name,i);
age = a;
cout << "\n 执行构造函数 Teacher "<< endl; };
//说明析构函数 ~Teacher
~ Teacher ( )
{ delete name ;
cout << " 执行析构函数 ~Teacher "<< endl<<endl; } ;
void show(); };
返回 43
3.2 构造函数与析构函数
void Teacher,,show ()
{ cout << " 姓名,"<<name<<" "<<"年龄,"<<age<< endl; }
void main() //主程序
{ Teacher obj ("张立三 ",25);
obj.show();
}
程序的执行结果是:
执行构造函数 Teacher
姓名:张立三 年龄,25
执行析构函数 ~Teacher
返回 44
3.2 构造函数与析构函数
3.2.5 拷贝构造函数拷贝构造函数是 C++中引入的一种新的构造函数 。 定义一个拷贝构造函数的方式是:
类名 ( const 类名 &形式参数 )
{ 函数体 }
由此可看出:
( 1) 拷贝构造函数的名称与类的名称相同,且它只有一个参数,该参数就是对该类对象的引用 。
( 2) 拷贝构造函数的功能是用于实现对象值的拷贝,通过将一个同类对象的值拷贝给一个新对象,来完成对新对象的初始化,即用一个对象去构造另外一个对象 。
返回 45
3.2 构造函数与析构函数
【 例 3-5】 Example是一个人员信息类 。 用普通构造函数生成 obj1,用拷贝构造函数生成 obj2。
# include <windows.h>
# include <iostream.h>
class Example
{ private:
char *name;
int num;
public:
example(int i,char *str ) // 构造函数定义
{ name=str;
num=i; }
返回 46
3.2 构造函数与析构函数
example(const Example &x) // 拷贝构造函数定义
{ num=x.num; }
void list() // 定义显示函数 list
{ cout<<"\数据成员 num的值 ="<<num<<endl<<endl; }
};
void main ()
{ example obj1(215,,张立三,);
//调用函数 Example(int i,char *str )构造 obj1
example obj2(obj1); //使用拷贝构造函数构造 obj2
obj2.list(); //显示 obj2的值
//其它程序部分
}
返回 47
3.2 构造函数与析构函数程序的执行结果是:
数据成员 num的值 =215
数据成员 num的值 =215
说明:
( 1) 上例中在 main函数中的语句 Example obj2(obj1);
在执行时,系统会自动调用类 Example的拷贝构造函数完成对 obj2对象的构造 。
( 2) 如果程序员没有为所设计的类提供显式的拷贝构造函数,则系统会自动提供一个默认的拷贝构造函数,其功能是:把作为参数的对象的数据成员逐个拷贝到目标变量中,这称为成员级复制 ( 或浅拷贝 ) 。
返回 48
3.2 构造函数与析构函数
3.2.6 一个类的对象作为另一个类的数据成员一个类中的数据成员除了可以是 int,char,float等这些基本的数据类型外,还可以是某一个类的一个对象 。 用子对象创建新类 。
在 C++中,当把一个类的对象作为新类的数据员时,
则新类的定义格式可表示为:
class X
{ 类名 1 成员名 1;
类名 2 成员名 2;
类名 n 成员名 n;
//其它成员
};
返回 49
3.2 构造函数与析构函数
( 3) 如果一个类 A的对象作为另一个类 B的数据成员,则在类 B的对象创建过程中,调用其构造函数的过程中,数据成员 ( 类 A的对象 ) 会自动调用类 A的构造函数 。
但应 注意,如果类 A的构造函数为有参函数时,则在程序中必须在类 B的构造函数的括号后面加一,,,和被调用的类 A的构造函数,且调用类 A的构造函数时的实参值必须来自类 B的形参表中的形参 。 这种方法称为初始化表的方式调用构造函数 。 如:以上面定义的类 X为例,在对类
X的对象进行初始化时,必须首先初始化其中的子对象,
即必须首先调用这些子对象的构造函数 。 因此,类 X的构造函数的定义格式应为:
X:,X( 参数表 0),成员 1( 参数表 1),成员 2( 参数表 2),…,成员 n(参数表 n)
{ …… }
返回 50
3.2 构造函数与析构函数其中,参数表 1提供初始化成员 1所需的参数,参数表 2提供初始化成员 2所需的参数,依此类推 。 并且这几个参数表的中的参数均来自参数表 0,另外,初始化 X的非对象成员所需的参数,也由参数表 0提供 。
在构造新类的对象过程中,系统首先调用其子对象的构造函数,初始化子对象;然后才执行类 X自己的构造函数,
初始化类中的非对象成员 。 对于同一类中的不同子对象,
系统按照它们在类中的说明顺序调用相应的构造函数进行初始化,而不是按照初始化表的顺序 。
返回 51
3.2 构造函数与析构函数
【 例 3-6】 以下定义了三个 Student,Teacher和 Tourpair,
其中 Student类的对象和 Teacher类的对象作为了 Tourpair
的数据成员,观察对象的构造过程和构造函数被执行的顺序 。
#include <iostream.h>
class Student
{ public:
Student()
{ cout<<”construct student.\n”;
semeshours=100;
gpa=3.5; }
返回 52
3.2 构造函数与析构函数
protected:
int semeshours;
float gpa;
};
class Teacher
{ public:
Teacher()
{ cout<<”construct Teacher.\n”;
}
};
返回 53
3.2 构造函数与析构函数
class Tourpair
{public:
Tourpair()
{cout<<”construct tourpair.\n”;
nomeeting=0; }
protected:
Student student;
Teacher teacher;
int nomeeting;
};
返回 54
3.2 构造函数与析构函数
void main()
{Tourpair tp;
cout<<”back in main.\n”; }
其执行结果是:
construct student.
construct teacher.
construct tourpair.
back in main.
由此可见:主函数 main()运行开始时,遇到要创建
Tourpair类的对象,于是调用其构造函数 Tourpair( ),
该构造启动时,首先分配对象空间 ( 包含一个 Student对返回 55
3.2 构造函数与析构函数象,一个 Teacher对象和一个 int型数据 ),然后根据其在类中声明的对象成员的次序依次调用其构造函数 。 即先调用 Student()构造函数,后调用 Teacher()构造函数,最后才执行它自己的构造函数的函数体 。
由于上例中 Tourpair类的数据成员 student和 teacher的构造函数都是无参函数,所以系统在构造 student和 teacher对象时会自动调用各自的构造函数 Student()和 Teacher(),而不需要用初始化表的方式去调用 。
【 例 3-7】 试分析以下程序的执行结果,
#include <iostream.h>
#include <string.h>
返回 56
3.2 构造函数与析构函数
class Student
{ public:
Student(char *pName="No name")
{ cout<<"构造新同学,"<<pName<<endl;
strncpy(name,pName,sizeof(name));
name[sizeof(name)-1]='\0';
}
Student(Student &s)
{ cout<<"构造 copy of "<<s.name<<endl;
strcpy(name," copy of ");
strcat(name,s.name);
}
返回 57
3.2 构造函数与析构函数
~Student()
{ cout<<"析构 "<<name<<endl; }
protected:
char name[40]; };
class Tutor
{ public:
Tutor(Student &s):student(s)//此为初始化表,调用
//Student的拷贝构造函数
{ cout<<"构造指导教师 \n"; }
protected:
Student student;
};
返回 58
3.2 构造函数与析构函数
void main()
{ Student st1; //此处调用 Student的构造函数 Student(char
*pName="No name")
Student st2("zhang"); //同上
Tutor tutor(st2); //此处调用 Tutor的构造函数 Tutor(Student &s)
//在构造 tutor对象的过程中,用初始化表调用
//Student类的拷贝构造函数 Student(Student &s)
}
执行结果如下:
构造新同学,No name
构造新同学,zhang
构造 copy of zhang
返回 59
3.2 构造函数与析构函数构造指导教师析构 copy of zhang
析构 zhang
析构 No name
3.2.7 利用初始化表对常量数据成员或引用成员提供初值如前所述,构造函数可对对象的数据成员进行初始化,
但若数据成员为常量成员或引用成员时,就有所不同,如:
class Sillyclass
{ public,
Sillyclass() // 此构造函数对成员 ten和 refi的初始化错误 。
{ ten=10;
refi=i; }
返回 60
3.2 构造函数与析构函数
protected:
const int ten; //常量数据成员 ten
int &refi; //引用 refi
};
说明:
1,造成以上错误的原因是在 Sillyclass类的构造函数进入之后 ( 开始执行其函数体时 ),对象结构已经建立,数据成员 ten和 refi已存在,而其数据成员 ten为 const,而 refi为引用,所以在构造函数体内不能再对其指派新的值 。
2,解决以上问题的方法是利用初始化表:在构造函数的括号后面加一,,,和初始化表,初始化表的格式是:
数据成员名 ( 值 ),如果有多个时,需要用逗号隔开 。
返回 61
3.2 构造函数与析构函数
【 例 3-8】 类 employee中包括私有数据成员 x,和 2个公有函数成员 example,show。 程序中使用初始化表是 x(215)。
# include <windows.h>
# include <iostream.h>
// 定义类 employee
class employee
{private:
const int x;
public:
employee ();
void show();
};
返回 62
3.2 构造函数与析构函数
// employee的类外定义
employee,,employee (),x (215) // 初始化表
{ }
// show()的定义 。
void employee,,show()
{ cout << "\n x的值是,"<< x << endl; }
// 主函数
void main()
{ //生成对象并为 x赋予初始值
employee obj;
//调用 show显示 x的值
obj.show();
}
返回 63
3.2 构造函数与析构函数
3.2.8 类作用域类作用域 又可称为类域,它是指在类定义中用一对大括号所括起来的范围 。 由于在程序文件中可包含类,而类中又包含函数,因此,类域显然是一个小于文件域,
而大于函数域的概念 。
由于在一个类中既可定义变量 ( 数据成员 ),又可定义函数 ( 成员函数 ),所以,类域在许多方面与文件域相似 。 但是,在类域中定义的变量不能使用 auto、
register和 extern等修饰符,而且在类域中定义的函数也不能使用 extern修饰符 。 同时,在类域中定义的静态成员和成员函数还具有外部的连接属性 。
返回 64
3.2 构造函数与析构函数
【 例 3-9】 类域及其成员引用举例,设以下程序代码被存放到了一个程序文件中。
#include <iostream.h>
class Myclass
{ private:
int x;
int y;
public:
Myclass(int a,int b) { x=a;y=b;}
void print();
void myfunc();
};
返回 65
3.2 构造函数与析构函数
void Myclass::print()
{ cout<<"x="<<x<<","<<"y="<<y<<endl; }
void Myclass::myfunc()
{ int x=9,y=10;
cout<<"In myfunc,x="<<x<<","<<"y="<<y<<endl;
//输出局部变量
//输出类的数据成员
cout<<"Myclass::x="<<Myclass::x<<","<<"
Myclass::y="<<Myclass::y<<endl;
}
返回 66
3.2 构造函数与析构函数
void main()
{ Myclass test(100,200),*ptest=&test;
test.print(); //通过对象名访问公有成员
ptest->myfunc(); }
程序的运行结果为:
x=100,y=200
In myfunc,x=9,y=10
Myclass::x=100,Myclass::y=200
说明:
( 1) 类成员函数的原型在类的定义体中声明,具有类作用域,但其实现部分在类的定义体外 。 由于不同类的成员函数可以具有相同的名字,因此,需要用作用域运算符,,:,来指明该成员函数所属的类 。
返回 67
3.2 构造函数与析构函数
( 2) 类中的成员拥有类作用域,因此在成员函数中可以直接引用类的数据成员 。 但是,如果在成员函数中定义了同名的局部变量时,则必须用作用域运算符,,:,来指定,以免混乱 。 如:上例中的 myfunc()函数中定义了与类的数据成员同名的局部变量 x,y,所以在 myfunc()函数中要访问类中的数据成员 x和 y的值时,必须加上作用域运算符 。
( 3) 类中的成员拥有类的作用域,如果要从类外访问类的成员时,则必须通过对象名或指向对象的指针 。 当通过对象名时,应使用圆点成员选择符,,”;当通过指针时,
应使用箭头成员选择符,->”。 如上例中的 test.print();
与 ptest->myfunc();
返回 68
3.3 继承和派生
3.3.1 继承的概念一个类的数据成员和成员函数,有些是类本身自己定义的,有一些是可继承的或通过模板生成的 。
所谓 继承 ( inheritance) 就是利用已有的数据类型定义出新的数据类型 。 利用类的,继承,,就可以将原来的程序代码重复使用,从而减少了程序代码的冗余度,符合软件重用的目标 。 所以说,继承是面向对象程序设计的一个重要机制 。 另外,在 C++中扩充派生类成员的方法是非常灵活的 。 派生类不仅可以继承原来类的成员,还可以通过以下方式产生新的成员:
返回 69
3.3 继承和派生
( 1) 增加新的数据成员;
( 2) 增加新的成员函数;
( 3) 重新定义已有成员函数;
( 4) 改变现有成员的属性 。
在继承关系中,称被继承的类为基类 ( base class)
( 或父类 ),而把通过继承关系定义出来的新类称为派生类 ( derived class) ( 子类 ) 。
由此可见,派生类既可以对基类的性质进行扩展,又可以进行限制,从而得到更加灵活,更加适用的可重用模块,大大缩短程序的开发时间 。
返回 70
3.3 继承和派生
3.3.2 单继承
1,定义派生类在基类的基础上定义其派生类的定义形式为:
class 派生类名:访问方式 基类名
{ 派生类中的新成员 }
其中:
( 1) 派生类名由用户自己命名;
( 2) 访问方式即继承方式,可以为 public 或 private,默认为 private方式 。 访问方式为 public方式时,这种继承称为公有继承,而访问方式为 private方式时,称为私有继承;
( 3) 基类名必须是程序中一个已有的类 。
返回 71
3.3 继承和派生
( 4) 在冒号,,,后的部分告诉系统,这个派生类是从哪个基类派生的,以及在派生时的继承方式 。
( 5) 大括号内的部分是派生类中新定义的成员 。
2,基类与派生类之间的关系
( 1) 派生类不仅拥有属于自己的数据成员与成员函数,
还保持了从基类继承来的数据成员与成员函数;同时派生类可对一些继承来的函数重新定义,以适应新的要求 。
( 2) C++关于类的继承方式的规定,如下表 3.1所示:
① 按 private方式继承 ( 即私有继承 ) 时,基类中的公有成员和保护成员在派生类中皆变为私有成员 。
② 按 public方式继承 ( 即公有继承 ) 时,基类中的公有成员和保护成员在派生类中不变 。
返回 72
3.3 继承和派生
③ 无论哪种继承方式,基类的私有成员均不能继承 。 这与私有成员的定义是一致的,符合数据封装的思想 。
④ 在公有继承方式下,基类的公有成员和保护成员被继承为派生类成员时,基访问属性不变 。
注意,私有成员与不可访问成员是两个不同的概念 。 某个类的私有成员只能被该类的成员函数所访问,而类的不可访问成员甚至不能被该类自身的成员函数所访问 。
类的不可访问成员总是从某个基类派生来的,它要么是基类的私有成员,要么是基类的不可访问成员 。
基类 公有派生类 私有派生类
public成员 public成员 private成员
protected成员 protected成员 private成员
private成员 无法继承 无法继承返回 73
3.3 继承和派生
( 3) 在 C++中,可以根据需要定义多层的继承关系,也可以从一个基类派生出多个类,形成类的层次结构,在类的层次结构中,处于高层的类,表示最一般的特征,
而处于底层的类,表示更具体的特征,在多层继承关系中,基类与派生类的关系是相对的,例如:由类 A派生出类 B,再由类 B派生出类 C,这里类 B相对于类 A是派生类,
而相对于类 C是基类,并称类 C是类 A的间接派生类,称类 A是类 C 的间接基类;而称具有直接派生关系的两个类分别为直接派生类和直接基类 。
【 例 3-9】 类 Build_1是一个关于楼房数据的类 。 它的数据成员有 posi_x,posi_y和 area,分别是楼房位臵的经,纬度和建筑面积 。 它的函数成员只有 set1,用于设臵数据成员 posi_x,posi_y和 area的值 。 让 Build_1作为基类,再增加数据成员 high,函数成员 set2和 disp来定义派生类
Build_2。
返回 74
3.3 继承和派生
#include <iostream.h>
class Build_1 //定义基类
{ protected:
int posi_x; // 有三个保护型的数据成员
int posi_y;
int area;
public:
void set1(int x,int y,int a)
{ posi_x=x; posi_y=y; area=a; }
};
//定义派生类 Build_2
class Build_2,public Build_1
返回 75
3.3 继承和派生
{ int height;
public:
void set2(int h)
{ height=h;}
void disp()
{ cout<<"\n 经度,"<<posi_x<<endl;
cout<<" 纬度,"<<posi_y<<endl;
cout<<" 高度,"<<height<<endl;
cout<<" 面积,"<<area<<endl<<endl; }
};
void main()
{ //用 Build_2生成对象 obj
返回 76
3.3 继承和派生
Build_2 obj;
obj.set1(100,200,300);
obj.set2(400);
obj.disp();
}
程序执行的结果是:
经度,100
纬度,200
高度,400
面积,300
由此可见:派生类 Build_2中已继承了基类 Build_1中的数据成员 posi_x,posi_y,area和基类中的成员函数 set1,
并同时增加了新的成员 height和成员函数 set2,disp。
返回 77
3.3 继承和派生
3,派生类的数据成员和成员函数,构造过程与构造函数
( 1) 派生类的数据成员和成员函数的来源有两个,一个来源是从基类继承来的数据成员和成员函数,对于继承来的数据成员,即使没有用也不能取消,只能不理会它们,但允许对一些继承来的成员函数重新定义,即在原有基类的成员函数的基础上,再增加一些操作,以完成派生类所要求的操作 。 另一个来源就是由派生类自己定义的数据成员和成员函数,这些成员的定义方法同一般类成员的定义方法基本一样 。
( 2) 通过派生类的对象调用一个被重新定义过的基类的成员函数,所调用的是派生类的成员函数,此时,若想调用基类的成员函数,必须在成员函数名前加基类名作用域分隔符,,:,。
返回 78
3.3 继承和派生
( 3) 在创建派生类的对象时,由于派生类的对象包含了基类的数据成员,因此派生类的构造函数除初始化其自身定义的数据成员外,还必须对基类中的数据成员进行初始化,也就是说,派生类的构造函数要负责调用基类的构造函数 。 所以派生类的构造函数的定义格式如下:
派生类名::派生类构造函数名 ( 参数表 ),基类构造函数名 ( 参数表 )
{ }
( 4) 虽然派生类可以直接访问基类的保护数据成员,甚至在构造时初始化它们,但是一般不这么做,而是通过基类的接口 ( 成员函数 ) 去访问它们,初始化也是通过基类的构造函数 。 这样,避免了类与类之间的相互干扰 。
返回 79
3.3 继承和派生
( 5) 基类的对象只能调用基类的成员函数,不能调用派生类的成员函数 。
( 6) 在定义派生类的对象时,系统首先执行基类的构造函数,然后执行派生类的构造函数 。 而系统执行析构函数的顺序恰恰相反,即先执行派生类的析构函数,再执行基类的析构函数 。
( 7) 若在基类中没有定义任何构造函数,这时在派生类的构造函数的定义中可以省略对基类构造函数的调用,
此时系统将去调用基类的默认构造函数 。
如,【 例 3-10】 本例中分别定义一个描述圆的类 Ccircle
和描述一个圆柱体的类 Ccylinder。
返回 80
3.3 继承和派生
#include "iostream.h"
class Ccircle //定义圆类
{protected:
double radius;
public:
Ccircle(double radiusval)
{ radius=radiusval; }
void setradius(double radiusval)
{ radius=radiusval; }
double getradius() const
{ return radius ; }
返回 81
3.3 继承和派生
double area() const
{ return 3.14*radius*radius; }
};
class Ccylinder,public Ccircle //定义圆柱体类
{protected:
double height;
public:
Ccylinder(double radiusval,double heightval);
void setheight(double heightval)
{ height=heightval; }
double getheight() const
{ return height; }
返回 82
3.3 继承和派生
double area() const //重新定义 area()函数
{ //此处调用的是基类的成员函数 area(),必须 加::
return 2*Ccircle::area()+2*3.14*radius*height; }
};
Ccylinder::Ccylinder(double radiusval,double heightval):
Ccircle(radiusval)
//调用 Ccircle类的构造函数对 radius初始化
//派生类 Ccylinder的构造函数不但初始化自身定义的成员
height,而且通过调用基类的构造函数 Ccircle( ) 初始化从基类继承来的数据成员 radius
{ height=heightval; }
返回 83
3.3 继承和派生
void main()
{Ccircle circle(10);
Ccylinder cylinder(2,5);
cout<<"圆柱体表面积,"<<cylinder.area()<<endl;
cout<<"圆柱体底面积,"<<cylinder.Ccircle::area()<<endl;
cout<<"圆的面积是,"<<circle.area();
}
程序的执行结果为:
圆柱体表面积,87.92
圆柱体底面积,12.56
圆的面积是,314
返回 84
3.3 继承和派生程序说明:
( 1) 求圆的面积与圆柱体的表面积的方法是不同的 。 因此,在派生类 Ccylinder中重新定义了基类成员函数
area()。 在主函数中,通过基类和派生类的不同对象,
分别调用了这两个 area()函数 。
( 2) 由于继承关系,在类 Ccylinder中存在两个同名的函数 arear()。 其中一个是从基类 Ccircle中继承过来的,
另一个是在派生类 Ccylinder中新定义的 。 这样,当通过派生类对象调用 area()函数时,C++编译器将沿继承关系搜索,使用离调用对象最近的那个版本的函数 。
( 3) 如果确实想通过 Ccylinder的对象访问从基类
Ccircle继承过来的 area()函数,则必须使用作用域运算符,,:”显式指明 。
返回 85
3.3 继承和派生通过以上分析可知,C++中处理同名函数有以下 3种基本方法:
( 1) 根据函数的参数的特征进行区分 。 即编译器根据函数的类型或个数进行区分 。 如:
max(int,int) max(float,float)
( 2) 根据类对象进行区分 。 如:在上例中的 main函数中,
cylinder.area() circle.area()
其中,cyclinder是 Ccylinder的一个对象,circle是 Ccircle
的一个对象 。
( 3) 使用作用域运算符,,:”进行区分,如:
Ccircle::area()
以上三种区分方法都是在程序编译过程中完成的,称为静态联编,除此之外,C++还提供称为动态联编 。
返回 86
3.3 继承和派生
3.3.3 多重继承在单一继承关系中,每个派生类最多只有一个直接基类,但它可以有多个间接基类 。 在 C++中不仅支持单一继承,而且也支持多重继承,所谓 多重继承,是指派生类从多个基类中派生而来,使派生类继承多个基类的特征,
在多重继承关系中,派生类有多个直接基类 。 定义多重继承类的方式如下:
class 派生类名:访问方式 基类名,访问方式 基类名
{ };
其中:访问方式为 public或 private,功能同单一继承 。
多重继承下派生类的构造函数必须同时负责所有基类构返回 87
3.3 继承和派生造函数的调用,对于派生类构造函数的参数个数必须同时满足多个基类初始化的需要 。 所以,在多重继承下,
派生类的构造函数的定义格式如下:
派生类构造函 数名 ( 参 数表 ),基 类名 1(参 数表
1),
{ }
在多重继承下,系统首先执行各基类的构造函数,然后再执行派生类的构造函数,处于同一层次的各基类构造函数的执行顺序与声明派生类时所指定的各基类顺序一致,而与派生类的构造函数定义中所调用基类构造函数的顺序无关 。
返回 88
3.3 继承和派生
【 例 3-11】 测试多重继承关系下,基类和派生类的构造函数的执行顺序 。
#include,iostream.h”
class B1
{protected:
int b1;
public:
B1(int val1)
{ b1=val1;
cout<<”base1 is called,<<endl; }
};
返回 89
3.3 继承和派生
class B2
{ protected:
int b2;
public:
B2(int val2)
{ b2=val2;
cout<<”base2 is called”<<endl; }
};
class D,public B1,public B2
{protected:
int d;
返回 90
3.3 继承和派生
public:
D(int val1,int val2,int val3);
};
D::D(int val1,int val2,int val3):B1(val1),B2(val2)
//如改为 D::D(int val1,int val2,int val3),B2(val2),B1(val1)
效果一样
{ d=val3;
cout<<”erived class is called,;}
void main()
{ D dobj(1,2,3); }
返回 91
3.3 继承和派生该程序的执行结果是:
基类 B1的构造函数被调用基类 B2的构造函数被调用派生类 D的构造函数被调用
3.3.4 虚基类多重继承下,一个派生类可从多个基类派生出来,又由于一个基类可派生出多个派生类,因此可能会产生一个类是通过多条路径从一个给定的类中派生出来的,如图 3.1所示。 B B
D1 D2
D3
图 3.1 多重继承的二义性返回 92
3.3 继承和派生从上图可以看出:派生类 D3中将继承两份类 B的成员,
一份由类 D1派生得到,另一份由 D2派生而来,这时通过派生类 D3的对象访问类 D1和 D2的成员不会有问题,但访问类 B的成员就会出现模棱两可的现象,编译程序不知道到底要访问哪一份的成员,C++为此提供了虚基类,以解决这种二义性 。
虚基类是这样的一个基类:它虽然被一个派生类间接地多次继承,但派生类却只继承一份该基类的成员,这样,避免了在派生类中访问这些成员时产生二义性 。
将一个基类声明为虚基类必须在各派生类定义时,在基类的名称前面加上关键字 virtual,格式如下:
class 派生类名,virtual public 基类名
{ //声明派生类成员 };
返回 93
3.3 继承和派生使用虚基类时,要特别注意派生类的构造函数,对于普通基类,派生类的构造函数负责调用其直接基类的构造函数以初始化其直接基类的数据成员,而对于虚基类的任何派生类,其构造函数不仅负责调用直接基类的构造函数,
还需调用虚基类的构造函数,如图 3.1所示的结构中,若基类 B被声明为虚基类,则派生类 D3负责调用三个基类
( 直接基类 D1,D2和虚基类 B) 的构造函数,而派生类
D1和 D2不会调用虚基类 B的构造函数,只由最终端的派生类 D3负责调用虚基类的构造函数 。 如:
class B //定义类 B
{ protected:
int b;
public:
B(int bval=0)
返回 94
3.3 继承和派生
{ b=bval; }
};
class D1,virtual public B
{protected:
int d1;
public:
D1(int bval,int dval);
};
D1::D1(int bval,int dval):B(bval)
{ d1=dval; }
class D2:virtual public B
{ protected:
int d2;
返回 95
3.3 继承和派生
public:
D2(int bval,int dval ); }
D2::D2(int bval,int dval):B(bval)
{d2=dval; }
class D3:public D1,public D2
{ protected:
int d3;
public:
D3(int bval,int dval1,int dval2,int dval3);
};
D3::D3(int bval,int dval1,int dval2,int dval3):D1(bval,
dval1),D2(bval,dval2),B(bval)
{ d3=dval3; }
返回 96
3.4 虚函数与多态性
3.4.1 多态性多态性 就是指同样的消息被类的不同的对象接收时导致的完全不同的行为的一种现象 。 这里所说的消息即对类成员函数的调用 。
C++支持两种不同类型的多态:一种是编译时的多态,
另一种是运行时的多态 。 在编译时的多态是通过静态联编实现的;而在运行时的多态则是通过动态联编实现的 。
很明显,函数的重载实现了一种多态性;这里要讲的多态性是建立在虚函数的概念和方法基础之上,通过虚函数来实现的,而虚函数又必须存在于继承的环境下 。
利用多态性,用户能够发送一般形式的消息,而将所有的实现细节留给了消息的对象,所以说多态性与数据封装和继承共同构成面向对象程序设计的三大机制 。
返回 97
3.4 虚函数与多态性
3.4.2 子类型
C++中的动态联编是通过虚函数实现的,而要理解虚函数必须首先讨论一个与之相关的概念,即子类型 。
如果一个特定的类型 S,当且仅当它提供了类型 T的行为时,则称类型 S是类型 T的子类型 。 子类型体现了类型间的一般与特殊的关系 。
在 C++中,子类型的概念是通过公有继承 ( 或公有派生 )
来实现的 。
根据继承方式的概念,我们知道,按公有继承的方式产生的派生类中,必然包含了原来基类中的全部成员 。 因此,
一个公有派生类的对象可以提供其基类对象的全部行为
( 基类的全部接口 ),也就是说,在程序中可以把一个公有派生类对象当作其基类对象来处理 。
返回 98
3.4 虚函数与多态性
【 例 3-13】 子类型的概念及实现示例 。
#include <iostream.h>
class A //定义类 A
{private:
int a;
public:
A(int i=0){a=i;}
void print();
};
void A::print ()
{ cout<<"In class A,print() is called."<<endl; }
返回 99
3.4 虚函数与多态性
class B:public A //定义类 B,类 B是类 A的公有派生类
{
private:
int b;
public:
B(int j=-1){b=j;}
};
void commfun(A &aref)
{
aref.print();
}
返回 100
3.4 虚函数与多态性
void main()
{A a;
commfun(a);//以基类 A的对象 a作为实参调用函数 commfun
B b;
commfun(b);//以派生类 B的对象 b调用函数 commfun
}
程序的运行结果为:
In class A,print() is called.
In class A,print() is called.
说明:
返回 101
3.4 虚函数与多态性
( 1 ) 在本例中,类 B 是类 A的公有派生类,函数
commfun()的形参是一个基类 A对象的引用,所以在 main
函数中,把基类 A的对象 a作为实参调用函数 commfun()时,
产生的结果是不言而喻的 。 但在 main函数中,当把类 B的对象 b作为实参调用函数 commfun()时,函数 commfun()仍能正常工作,且打印结果与对象 a作为实参时的结果相同,
这说明,在程序中可以把一个公有派生类对象当作其基类对象来处理 。
( 2) 将类型 B的对象 b传递给函数 commfun()处理是在程序运行时发生的 。 但在程序编译时,编译器只能对源程序代码进行静态检查 。
( 3) 子类型的重要性在于可以减轻程序员编写程序代码的负担 。
返回 102
3.4 虚函数与多态性
3.4.3 用基类指针指向公有派生类对象既然一个公有派生类对象可以当作基类对象使用,那么,
指向基类的指针自然也可以指向其公有派生类对象 。 因此,基类指针,派生类指针,基类对象和派生类对象四者间有以下四种组合的情况:
( 1) 直接用基类指针指向基类对象 。
( 2) 直接用派生类指针指向派生类对象 。
( 3) 用基类指针引用其派生类对象 。
( 4) 用派生类指针引用基类对象 。
由于 ( 1),( 2) 两种情况,指针类型和对象类型统一,
因此完全行得通 。
返回 103
3.4 虚函数与多态性对于第 ( 3) 种情况,由于可以把一个公有派生类对象当作基类对象处理,所以可以用基类指针指向其派生类对象 。 但必须注意的是,由于基类指针本身的类型并没有改变,因此基类指针仅能访问派生类中的基类部分 。 在程序中,当把派生类对象的指针赋给基类指针时,编译器能自动完成隐式类型转换 。
对于第 ( 4) 种情况,将派生类指针直接指向基类对象是危险的,因为编译器不允许这么做,也不提供隐式类型转换 。 当然,程序员如果采用强制类型转换,也可以把基类指针转换为派生类指针,但这时要正确地使用该指针 。
返回 104
3.4 虚函数与多态性
【 例 3-14】 基类指针,派生类指针,基类对象和派生类对象四者间组合的使用情况示例 。
#include <iostream.h>
class A //定义类 A
{private:
int a;
public:
A(int i=1){a=i;}
void print();
int geta();
};
void A::print ()
{ cout<<"a="<<a<<endl; }
返回 105
3.4 虚函数与多态性
int A::geta()
{ return a; }
class B:public A //定义类 B,类 B是类 A的公有派生类
{private:
int b;
public:
B(int j=-1){b=j;}
void print();
};
void B::print ()
{ cout<<"b="<<b<<endl; }
返回 106
3.4 虚函数与多态性
void main()
{A aa(10),*pa;
B bb(20),*pb;
pa=&aa; //基类指针可以指向基类对象
pa->print();
pb=&bb; //派生指针可以指向派生类对象
pb->print();
pa=&bb; //基类指针可以指向派生类对象
cout<<pa->geta()<<endl; //如改为 pa->getb();则错误,
//因为基类指针仅能看到派生类中的基类部分
pa->print();
bb.print();
返回 107
3.4 虚函数与多态性
pb=(B *)pa; //经过强制类型转换,派生类指针也可以
//指向基类对象
//上面语句如改为 pb=pa;则错误,因为派生类指针不可
//以直接指向基类对象 }
程序的运行结果:
a=10
b=20
1
a=1
b=20
返回 108
3.4 虚函数与多态性程序分析:在上例的 main函数中,虽然基类指针 pa指向派生对象 bb( 即,pa=&bb),但语句 pa->print()与语句
bb.print()的输出结果并不相同,从结果来看,前者的输出结果是,a=1”,而后者的输出结果为,b=20” 。 这是由于虽然一个基类指针可以指向其派生类对象,但指针本身的属性并没有改变,因此,系统认为它所指向的仍然是一个基类对象,于是就只能调用其基类的成员函数
print()。 进一步分析发现,在派生类 B中虽然继承了基类
A的成员函数 print(),但为了适应派生类自己的需要,在派生类中已经改变了这个函数的实现,即在派生类中又定义了一个同名的 print()函数,而这种改变在静态联编的条件上编译器并不知道,以致于造成以上结果的不统一 。
所以,必须通知编译器这种可能的改变,即需要进行动态联编 。 其方法就是在基类中将可能发生改变的成员函数声明为虚函数 。
返回 109
3.4 虚函数与多态性
3.4.4虚函数
C++通过虚函数实现了多态性,而虚函数存在于继承环境中,在继承关系下,派生类作为基类的子类,在任何要求基类对象的地方使用派生类对象是有意义的 。
声明虚函数的方法是在基类中的成员函数原型前加上关键字 virtual。 其格式如下:
class 类名
{
virtual 类型 函数名 ( 参数表 ) ;
};
当一个类的成员函数说明为虚函数后,就可以在该类的
( 直接或间接 ) 派生类中定义与其基类虚函数原型相返回 110
3.4 虚函数与多态性同的函数 。 这时,当用基类指针指向这些派生类对象时,
系统会自动用派生类中的同名函数来代替基类中的虚函数 。 也就是说,当用基类指针指向不同派生类对象时,
系统会在程序运行中根据所指向对象的不同,自动选择适当的函数,从而实现了运行时的多态性 。
虚函数可以在一个或多个派生类中被重新定义,因此,
属于函数重载的情况,但这种重载与一般的函数重载是不同的,要求在派生类中重新定义时,必须与基类中的函数原型完全相同,包括函数名,返回类型,参数个数和参数类型的顺序 。 这时无论在派生类的相应成员函数前是否加上关键字 virtual,都将视其为虚函数,如果函数原型不同,只是函数名相同,C++将视其为一般的函数重载,而不是虚函数 。 只有类的成员函数才能声明为虚函数,全局函数及静态成员函数不能声明为虚函数 。
返回 111
3.4 虚函数与多态性
【 例 3-15】 虚函数的定义与应用举例 。
#include,iostream.h”
class Base
{ public:
virtual void show() { cout<<”base class\n”; } };
class Der1,public Base
{ public:
void show() { cout<<”derived class 1 \n”; } };
class Der2,public Base
{ public:
void show() { cout<<”derived class 2”; } };
返回 112
3.4 虚函数与多态性
void main()
{ Base bobj;
Base *p;
Der1 dobj1;
Der2 dobj2;
p=&bobj;
p->show();
p=&dobj1;
p->show();
p=&dobj2;
p->show();
}
返回 113
3.4 虚函数与多态性程序的运行结果,base class
derived class 1
derived class 2
由上例可以看出:
( 1) 通过虚函数实现了运行时的多态性 。
( 2) 基类用虚函数提供了一个派生类对象都具有的共同界面,派生类又各自对虚函数定义自己的具体实现,这样,使得程序既简洁又具有扩充性,并能帮助程序员控制更大的复杂性 。 若派生类中没有重新定义基类的虚函数,则该派生类直接继承其基类的虚函数 。
( 3)当一个函数在基类被声明为虚函数后,不管经历多少层派生,都将保持其虚拟性。
返回 114
3.4 虚函数与多态性
3.4.5 静态联编与动态联编在向对象的程序设计中,联编 的含义是指把一个消息和一个方法联系在一起,也就是把一个函数名与其实现代码联系在一起 。 根据实现联编的阶段的不同,可分为静态联编和动态联编两种 。
静态联编 是在编译阶段进行的 。 而 动态联编 是在程序运行过程中,根据程序运行的需要进行的联编 。
实现静态联编的前提是:在编译阶段就必须能够确定函数名与代码间的对应关系 。 因此,当通过对象名调用成员函数时,只可能是调用对象自身的成员,所以,这种情况可采用静态联编实现 。 但当通过基类指针调用成员函数时,
由于基类指针可以指向该基类的不同派生类对象,因此存在需要动态联编的可能性,但具体是否使用动态联编,还要看所调用的是否是虚函数 。
返回 115
3.4 虚函数与多态性
3.4.6纯虚函数与抽象类纯虚函数 是在基类中只声明虚函数而不给出具体的函数定义体,将它的具体定义放在各派生类中,称此虚函数为纯虚函数 。 通过该基类的指针或引用就可以调用所有派生类的虚函数,基类只是用于继承,仅作为一个接口,具体功能在派生类中实现 。
纯虚函数的声明如下,( 注:要放在基类的定义体中 )
virtual 函数原型 =0;
其中:函数原型的格式同前面所学格式一样,要包括函数返回值的类型,函数名,圆括号,形参及其类型等 。
声明了纯虚函数的类,称为 抽象类 。
使用纯虚函数时应 注意:
( 1) 抽象类中可以有多个纯虚函数 。
返回 116
3.4 虚函数与多态性
( 2) 不能声明抽象类的对象,但可以声明指向抽象类的指针变量和引用变量 。
( 3) 抽象类也可以定义其他非纯虚函数 。
( 4) 如果派生类中没有重新定义基类中的纯虚函数,则在派生类中必须再将该虚函数声明为纯虚函数 。
( 5) 从抽象类可以派生出具体或抽象类,但不能从具体类派生出抽象类 。
( 6) 在一个复杂的类继承结构中,越上层的类抽象程度越高,有时甚至无法给出某些成员函数的实现,显然,抽象类是一种特殊的类,它一般处于类继承结构的较外层 。
( 7) 引入抽象类的目的,主要是为了能将相关类组织在一个类继承结构中,并通过抽象类来为这些相关类提供统一的操作接口 。
返回 117
3.4 虚函数与多态性
【 例 3-16】 设计一个抽象类 shape,它表示具有形状的东西,体现了抽象的概念,在它下面可以派生出多种具体形状,比如三角形、矩形。
#include<iostream.h>
class Shape
{ protected:
double x,y;
public:
void set(double i,double j)
{ x=i; y=j; }
virtual void area()=0; //声明纯虚函数
};
返回 118
3.4 虚函数与多态性
class Triangle,public Shape
{ public:
void area()
{ cout<< "三角形面积," <<0.5*x*y<<endl; ; }
};
class Rectangle,public Shape
{ public:
void area()
{ cout<<"矩形面积," <<x*y<<endl; ; }
};
返回 119
3.4 虚函数与多态性
void main()
{ Shape *p;
Triangle t;
Rectangle r;
p=&t;
p->set(5.1,10);
p->area();
p=&r;
p->set(5.1,10);
p->area(); }
结果:三角形面积,25.5
矩形面积,51
返回 120
3.5 静态成员
C++还有一种数据成员,称作,静态,成员,静态成员是所有对象公有的 。 静态成员有静态数据成员和静态函数成员之分 。
3.5.1 静态数据成员说明静态数据成员的语句格式是:
static 类型说明符 成员名;
【 例 3-17】 报名登记处登记每一位来访者的姓名,同时使用静态数据成员 account自动产生一个流水号数,记入
number中 。
返回 121
3.5 静态成员
# include <windows.h>
# include <iostream.h>
//定义类 married
class married
{private:
int number; //编号
char *name; //姓名
public:
static int glob; // 定义静态数据成员 glob
void set_mes (char *a); // set_mes函数说明
} ;
返回 122
3.5 静态成员
// set_mes函数定义
void married,,set_mes (char *a)
{name = new char[strlen(a) + 1] ;
strcpy (name,a) ; //用参数 a的值修改私有变量 。
number=++glob; //glob加班后赋给 number
cout << " 编号,"<<number<<endl;
}
int married,,glob= 0 ; //静态变量赋初始值 0
// 主函数
void main ()
{ // 生成对象数组 person
married person[100];
返回 123
3.5 静态成员
int i ; // 局部变量 i
char str[8] ; // 局部变量 str
cout<<endl;
for ( i=0; i<100; i++) // 循环 100次
{ //读入姓名,存于 str
cout << " 输入姓名,"; cin >> str ;
person[i].set_mes ( str ) ; //保存并显示 }
cout<<endl;
}
说明:
(1) 不管一个类的对象有多少个,其静态数据成员也只有一个,由这些对象所共享,可被任何一个对象所访问 。
返回 124
3.5 静态成员
(2) 在一个类的对象空间内,不包含静态成员的空间,所以静态成员所占空间不会随着对象的产生而分配,或随着对象的消失而回收 。
(3) 静态数据成员的存储空间的分配是在程序一开始运行时就被分配 。 并不是在程序运行过程中在某一函数内分配空间和初始化 。
(4) 静态数据成员的赋值语句,既不属于任何类,也不属于包括主函数在内的任何函数,静态变量赋初值语句应当写在程序的全局区域中,并且必须指明其数据类型与所属的类名,并用如下格式:
类型 类名::变量名 =值;
如:上例中的,int visited::glob=0;
返回 125
3.5 静态成员
(5) 对于在类的 public部分说明的静态数据成员,可以不使用成员函数而直接访问,即使未定义类的对象,同样也可以直接访问,但在使用时也必须用类名指明所属的类,如在上例中的 glob数据成员,可以在 main函数体中直接访问,cout<<visited::glob;而 private和 protected部分的静态成员只能通过类的成员函数访问 。
3.5.2 静态成员函数静态成员函数的定义:
static 类型 函数名 ( 形参 )
{函数体 }
与静态数据成员一样,静态成员函数与类相联系,不与类的对象相联系,所以访问静态成员函数时,不需要对象 。 如:
返回 126
3.5 静态成员
#include,iostream.h”
class objcount
{ private:
static int count;
public:
objcount() { count++; }
static int get() { return count; }
};
int objcount::count=0;
void main()
{cout<<objcount::get();
objcount a,b,c,d,e,f;
返回 127
3.5 静态成员
cout<<objcount::get();
count<<a.get();
}
一个静态成员函数不与任何对象相联系,所以它不能对非静态成员进行默认访问 。 如:
#include,iostream.h”
class student
{ public:
static char *sname()
{ cout << noofstudent<<endl;
return name; }
返回 128
3.5 静态成员
protected:
char name[40];
static int noofstudent;
};
int student::noofstudent=0;
void fn()
{ student s;
cout<<s.sname()<<endl; //error
//……
}
返回 129
3.6 友元函数与友元类
3.6.1 友元函数
C++提供一种允许外部类和函数存取类的私有成员和保护成员的辅助方法,即将它们声明为一个给定类的友元
(或友元函数),使其具有类成员函数的访问权限。但友元本身不是类的成员,它不属于任何类。
对于使用友元函数,有以下几点 说明:
(1) 为了在类的定义中对友元加以声明,只需在友元的名称前加上关键字 friend即可 。
(2) 友元函数是能访问类的所有成员的普通函数,一个函数可以是多个类的友元函数,只需在各个类中分别声明 。
返回 130
3.6 友元函数与友元类
(3) 将一个函数声明为某个类的友元函数的方法是在该类定义里提供一个以关键字 friend开头的函数原型,友元函数的定义,可以在类的内部或外部,友元函数虽然是在类内进行声明,但它不是该类的成员函数,不属于任何类,在类外定义友元函数时,与普通函数的定义一样,
不应在函数名前用类名加以限制,因此,友元函数不象成员函数那样在调用时使用对象名,友元函数要对类的成员进行访问,必须在参数表中显式指明要访问的对象。
(4) 一个类的友员函数与该类的类内成员函数一样,享有对该类一切成员的访问权 。
(5) 友元函数的调用与一般函数的调用方式和原理一致 。
(6) C++不允许将构造函数、析构函数和虚函数声明为友元函数。
返回 131
3.6 友元函数与友元类
【 例 3-18】 友元函数的定义和使用方法 。
#include,iostream.h”
class X1()
{ private:
int x;
public:
X1(int i) { x=i; }
int getx();
friend void sum(X1 &a,X2 &b); //声明友元函数
};
int X1::getx()
{ return x;}
返回 132
3.6 友元函数与友元类
void sum(X1 &a,X1 &b) //定义函数
{cout<<"用友元函数求各和,"<<a.x+b.x<<endl;
//可以访问类的私有成员 }
void sum1(X1 &a,X1 &b) //定义普通函数
{cout<< "用普通函数调用类公共接口函数求和,"
<<a.getx()+b.getx()<<endl;
}
void main()
{ X1 m(1);
X1 n(2);
sum(m,n);
sum1(m,n);
}
返回 133
3.6 友元函数与友元类程序的执行结果为:
用友元函数求各和,3
用普通函数调用类公共接口函数求和,3
返回 134
3.6 友元函数与友元类
3.6.2 友元类友元类是在多个类之间建立一种访问机制,当程序中定义了两个或两个以上的类时,如果一个类将自己说明为另一个类的友员类,其成员就可以被该类使用 。
定义友元类的语句格式为:
friend class 类名;
其中,friend 和 class 是关键字,类名必须是程序中的一个已定义过的类 。
当希望一个类可以存取另一个类的私有成员时,可以将该类声明为另一类的友元,友元类的所有成员函数都可视为该类的友元函数,能存取该类的私有成员和保护成员 。
返回 135
3.6 友元函数与友元类如:将类 B声明为类 A的友元类的方法是在 A类的定义中加上说明:
friend class B;
注意,友元关系不具有对称性 。 此外,友元关系不具有传递性,如果类 B是类 A的友元类,类 C是类 B的友元类,
这并不隐含类 C是类 A的友元类 。
【 例 3-8】 有两个类 Cla_1和 Cla_2。在类 Cla_1的定义中说明了友员类 Cla_2。主函数中用它们生成了两个对象,然后对这两个对象的私有成员进行访问。
返回 136
3.6 友元函数与友元类
#include <iostream.h>
#include <windows.h>
#include <stdio.h>
//定义类 Cla_1
class Cla_1
{ private:
friend class Cla_2; //说明友员类 Cla_2
char *name;
int age;
public:
Cla_1( char *str,int i );
};
返回 137
3.6 友元函数与友元类
//Cla_1的构造函数定义
Cla_1::Cla_1(char *str,int i)
{ name=str; //为类 Cla_1的成员赋值
age=i; }
//定义类 Cla_2
class Cla_2 //类 Cla_2中并没有说明 Cla_1友员类
{ public:
void show(Cla_1 x);
} ;
返回 138
3.6 友元函数与友元类
// Cla_2的 show函数定义
void Cla_2::show(Cla_1 x)
{ cout<<"\n\n 姓名,"<<x.name<<endl;
cout<<"\n 年龄,"<<x.age<<endl<<endl; }
// 主函数
void main()
{ //生成对象 obj1,定义友员类 Cla_2
Cla_1 obj1("李小丽 ",30);
Cla_2 obj2; //生成对象 obj2
obj2.show(obj1); //调用 obj2.show,去访问 obj1的成员
}
返回 139
3.7 堆对象和对象数组
3.7.1 堆对象我们知道,用 new和 delete可以动态地分配堆内存或释放堆内存。在此可利用 new建立对象(会自动调用构造函数),利用 delete可删除对象(会自动调用析构函数)。
class Tdate
{public:
Tdate(int m,int d,int y);
protected:
int month;
int day;
int year;
};
返回 140
3.7 堆对象和对象数组
Tdate::Tdate()
{if (m>0 && m<13)
month=m;
if (d>1 && d<32)
day=d;
if (y>0 && y<300)
year=y; }
void main()
{ Tdate *pd;
pd=new Tdate(1,1,1998);
//……
delete(pd);}
返回 141
3.7 堆对象和对象数组说明:
( 1) 堆对象的生存期是整个程序的生命期,所以只有程序运行结束时,堆对象才被删除 。 这一点与一般的局部对象的生命期不同,局部对象的生存期开始于函数体的执行,
而终止于函数体执行结束 。
( 2) 堆对象用 delete来释放 。
返回 142
3.7 堆对象和对象数组
3.7.2 对象数组一个数组的类型除了可以为基本的数据类型外,还可以为类类型,则这时,该数组中的每个元素都是该类中的一个对象,则这种数组就是对象数组 。
对象数组的定义方式:
类名 数组名 [数组大小 ];
【 例 3-9】 类 person是说明人员的信息结构 。 用 person生成一个对象数组 emp[5],通过 assignment将人员信息填入,
然后将它们显示出来 。
# include <windows.h>
# include <iostream.h>
返回 143
3.7 堆对象和对象数组
class person
{private:
char *name; //name指向信息串
int age; //年龄
public:
person(); // 构造函数说明
~person(); // 析构函数说明
void assignment(char *a,int b); // assignment函数说明
void show (); // show函数说明
};
// 构造函数定义
person,,person()
返回 144
3.7 堆对象和对象数组
{ name=new char('\0');// 为 name申请存储空间,臵为空
age=-1; // 为 age 赋初始值
}
// 析构函数定义
person,,~person()
{ delete [ ] name; // 回收 name空间 }
// assignment函数定义
void person,,assignment(char *a,int b)
{ name = new char[strlen(a) + 1];
strcpy(name,a); // 用参数 a的值修改 name
age=b;
};
返回 145
3.7 堆对象和对象数组
// show函数定义
void person,,show ()
{ cout << "\n 姓名," << name << " 年龄," << age ; };
void main ()
{ // 生成对象数组 emp[5]
person emp[5];
// 给对象数组赋值
emp[0].assignment("张立三 ",25);
emp[1].assignment("王冠之 ",28);
emp[2].assignment("王大成 ",35);
emp[3].assignment("英乐乐 ",21);
emp[4].assignment(,胡忠厚,,26);
返回 146
3.7 堆对象和对象数组
// 显示 emp
int i;
for (i=0; i<5; i++)
{ emp[i].show(); }
cout <<endl<<endl;
}
3.8 运算符的重载运算符的重载与函数重载的方法一致,是一种特殊的函数重载 。
使用运算符重载的一般格式为:
类型名 operator@( 形参表 )
其中,operator是关键字,@是运算符 。
返回 147
3.8 运算符的重载
【 例 】 运算符重载举例 。
class Cint
{public:
friend Cint operator+(int a,int b);//,+” ( 加号 ) 被重
//载为友元函数
Cint operator-(); //,-” ( 负号 ) 被重载为成员函数,
}
Cint Cint::operator+(int a,int b)
{ Cint t;
t=a+b;
return t;}
返回 148
3.8 运算符的重载
Cint Cint::operator-()
{ Cint t;
t=-t;
return t;
}
说明:
(1) 在 C++ 中几乎所有的运算符 ( 除,,”,,,*”,
,::,,,?,,外 ) 都可以被重载 。
(2) 运算符的重载既不会改变原运算符的优先级和结合性,
也不会改变使用运算符的语法和参数个数 。
(3) 由于重载后的运算符函数经常需要访问类的私有成员,
因此运算符函数 operator@()通常被声明为类的成员函数或友元函数。其等价的函数调用形式,如表 3.2所示。
返回 149
3.8 运算符的重载表 3.2 运算符表达式及其等价的函数调用形式
(4) 当重载为类的成员函数时,运算符重载函数的形参个数要比运算符操作数个数少一个;若重载为友元函数,
则参数个数与操作数个数相同 。
(5) 当重载为友元函数时,,=”,,(),,,[]”,
,->” 等运算符不能重载。
表达式 友元函数调用 成员函数调用
a+b operator+(a,b) a.operator+(b)
a++ operator++(a,0) a.operator++(0)
-a operator-(a) a.operator-( )
返回 150
3.9 模板与使用模板就是使程序能够对不同类型的数据进行相同方式的处理 。 C++中的模板分为类模板和函数模板 。
3.9.1 类模板说明类模板的一般格式为:
template <类型参数表 > class 类模板名
{ private:
私有成员定义
protected:
保护成员定义
public:
公有成员定义 };
其中,(1)类型形式参数表可以包含基本数据类型,也可返回 151
3.9 模板与使用以包含类类型,如果是类类型,则须加前缀 class。 当参数有多个时,需用逗号隔开 。
(2)类模板中的成员函数的定义,可以放在类模板的定义体中 ( 此时与类中的成员函数的定义方法一致 ),也可以放在类模板的外部定义成员函数,此时成员函数的定义格式如下:
template <类型形式参数表 >
函数值的返回类型 类模板名 <类型名表 >::成员函数
( 形参 )
{ 函数体 }
其中:类模板名即是类模板中定义的名称;类型名表即是类模板定义中的类型形式参数表中的参数名 。
返回 152
3.9 模板与使用
(3)利用类模板定义的只是对类的描述,它本身还不是一个实实在在的类 。 因此是类模板 。
(4)要定义类模板的对象 ( 即实例 ),需要用下列格式的语句:
类模板名 <类型实际参数表 > 对象名;
【 例 3-22】 定义类模板 ABC,内含成员函数 set和 get。用
ABC生成对象 abc1和 abc2。它们的数组元素数不同,显示的结果也不同。
# include <iostream.h>
// 定义类模板 ABC
template <class T,int I> class ABC
{ private:
T array [I] ; // 定义数组 array
返回 153
3.9 模板与使用
public:
void set (int x) // 定义成员函数 set
{ int i;
for (i=0; i<I; i++) //循环 I次
array[i]=x+i; //数组元素赋值
}
void get () //定义成员函数 get
{ cout <<"\n 数组元素总数为,"<< I<<endl;
cout <<" array["<<I-1<<"]="<<array[I-1]<<endl; }
};
void main()
{ //由模板 ABC生成对象 abc1
返回 154
3.9 模板与使用
ABC <int,50> abc1;
abc1.set(0); //调用对象 abc1.set
abc1.get(); //调用对象 abc1.get
//由模板 ABC生成对象 abc2
ABC <int,100> abc2;
abc2.set(10); //调用对象 abc2.set
abc2.get(); //调用对象 abc2.get
}
以下是对上例中的成员函数定义体放于类模板外部定义的示例。
【 例 3-23】 定义模板 ABC,内含成员函数 set和 get。用
ABC生成对象 abc1和 abc2。它们的数组元素数不同,显示的结果也不同。
返回 155
3.9 模板与使用
#include <iostream.h>
// 定义类模板 ABC
template <class T,int I> class ABC
{ private:
T array [I] ; // 定义数组 array
public:
void set (int x); // 定义成员函数 set
void get () ; //定义成员函数 get
};
template <class T,int I>
void ABC<T,I>,:set (int x) // 定义成员函数 set
{ int i;
返回 156
3.9 模板与使用
for (i=0; i<I; i++) //循环 I次
{ array[i]=x+i; } //数组元素赋值
}
template <class T,int I>
void ABC<T,I>::get()
{ cout <<"\n 数组元素总数为,"<< I<<endl;
cout <<" array["<<I-1<<"]="<<array[I-1]<<endl; }
void main()
{ //由模板 ABC生成对象 abc1
ABC <int,50> abc1;
abc1.set(0); //调用对象 abc1.set
abc1.get(); //调用对象 abc1.get
返回 157
3.9 模板与使用
//由模板 ABC生成对象 abc2
ABC <int,100> abc2;
abc2.set(10); //调用对象 abc2.set
abc2.get(); //调用对象 abc2.get
}
类模板的使用方法可以总结为:
(1) 给出类模板的定义体 。
(2)在适当的位臵创建一个类模板的实例,即一个实实在在的类定义,同时创建该模板类的对象 。
(3)有了对象名,以后的使用就和普通类的对象是一致的 。
返回 158
3.9 模板与使用
3.9.2 函数模板函数模板是函数的一种抽象形式 。 由于 C++中的数据类型很多,针对某种类型设计的函数显然不能用于其它类型,因此对于同一个功能的要求,不得不编制多个相似的程序,比如求两个量中最小值的函数,相似的函数可能有:
(1)求两个整型数据的最小值,int min(int x,int y)
(2)求两个单精度数据的最小值,float min(float x,float y)
(3)求两个双精度数据的最小值,double min(double x,
double y)
为了解决以上的麻烦,C++引入了函数模板的概念 。 函数模板就是用来解决一个模板生成多个函数的问题 。
返回 159
3.9 模板与使用定义函数模板的格式为:
template <类型形式参数表 >
函数返回值类型名 函数模板名 ( 函数形参及类型 )
{函数体 }
如:两个数最小值的函数模板定义如下:
template <class T> T min( T x,T y)
{ return x<y?x:y; }
有了以上函数模板,则下列语句都是正确的 。
a=min(20,10);
b=min(-5.43,50.23);
c=min(?A?,?a?);
返回 160
文件及其操作一、文件的概念和分类文件是指存储在存储介质上的数据的集合。 C++将文件看作是由一个一个字符(字节)的数据顺序组成的。
按照文件中数据的存放形式可以将文件分为,ASCII文件和二进制文件。
二、文件的读写与文件指针
,文件指针,是指表示读写文件的文件位置指示器 。 一个文件指针总是和一个文件相关联,当文件每一次打开时,文件指针都指向文件的开始,随着对文件进行操作,文件指针不断地在文件中移动,并一直指向最新处理的字符 ( 字节 ) 位置 。
对文件的读写操作方式有两种方式:顺序文件操作和随机文件操作 。
返回 161
与文件处理相关的类及其继承关系结构图
ios
istream ostream
ifstream iostream ofstream
fstream
返回 162
文件操作包括打开文件,读写文件和关闭文件 3
个步骤 。
文件的打开和关闭是通过使用 fstream类的成员函数 open和 close来实现的。 fstream类的头文件是 fstream.h
返回 163
1,打开文件打开文件应使用成员函数 open(),该成员函数的函数原型为:
void open(const unsigned char *filename,int mode,int
access=filebuf::openprot);
其中,filename是一个字符型指针,指定了要打开的文件名; mode指定文件的打开方式,见下表。 access指定了文件的系统属性,其取值为:
0 0 一般文件
1 1 只读文件
2 2 隐藏文件
3 3 系统文件返回 164
在 ios类中定义的文件打开方式文件打开方式 含 义
ios::in 以输入 ( 读 ) 方式打开文件
ios::out 以输出 ( 写 ) 方式打开文件
ios::app 打开一个文件使新的内容始终添加在文件的末尾
ios::ate 打开一个文件使新的内容添加在文件尾,但下一次添加时,写在当前位置处
ios::trunc 若文件存在,则清除文件所有内容;若文件不存在,
则创建新文件
ios::binary 以二进制方式打开文件,缺省时以文本方式打开文件
ios::nocreat 打开一个已有文件,若该文件不存在,则打开失败
ios::noreplace 若打开的文件已经存在,则打开失败返回 165
几点说明:
( 1) 在实际使用过程中,可根据需要将以上打开文件的方式用,|”组合起来 。 如:
ios::in|ios::out表示以读 /写方式打开文件
ios::in|ios::binary表示以二进制读方式打开文件
ios::out|ios::binary表示以二进制写方式打开文件
ios::in|ios::out|ios::binary表示以二进制读 /写方式打开文件
( 2) 如果未指明以二进制方式打开文件,则默认是以文本方式打开文件 。
返回 166
2,关闭文件在文件操作结束时应及时调用成员函数 close()来关闭文件 。 如:要关闭的文件对象名为 myfile,
则可使用如下语句关闭文件:
myfile.close();
返回 167
3,文件的读写在打开文件后就可以对文件进行读写操作了。从一个文件中读出数据,可以使用 iostream类的
get,getline,read成员函数以及运算符,>>”;
而向一个文件写入数据,可以使用其 put,write
函数以及插入符,<<”。
返回 168
iostream的文件操作常用的函数函数原型 说明
get(char &ch) 从文件中读取一个字符
getline(char *pch,int
count,chardelim=?\n?)
从文件中读取多个字符,读取个数由参数 count决定,参数 delim是读取字符时指定的结束符
read(char *pch,int count) 从文件中读取多个字符,读取个数由参数 count决定
put(char ch) 向文件写入一个字符
write (const char *pch,int
count)
向文件写入多个字符,字符个数由
count决定返回 169
从文件的第一个字符(字节)开始顺序地处理到文件的最后一个字符(字节),这种操作方式只能从文件的开始处依次顺序读写文件内容,
而不能任意读写文件内容。
【 例 3-24】 向文本文件中分别写入一个整数和一个字符串,然后再以读方式打开该文件,从中读取相应的信息并显示到屏幕上 。
顺序文件的操作返回 170
随机文件操作随机文件操作,即在文件中通过 C++相关的函数移动文件指针,并指向所要处理的字符(字节)。
( 1)在 istream类中提供了 3个操作读指针的成员函数:
istream&istream::seekg (long pos);
istream&istream::seekg(long off,dir);
streampos istream::tellg();
其中,pos为文件指针的绝对位置; off为文件指针的相对偏移量; dir为文件指针的参照位置,其值可能为:
ios::cur文件指针的当前位置
ios::beg 文件开头
ios::end 文件尾
tellg()函数没有参数,它返回一个 long型值,用来表示从文件开始处到当前指针位置之间的字节数 。
返回 171
( 2) 在 ostream类中同样提供了 3个操作写指针的成员函数:
ostream&istream::seekp (long pos);
ostream&istream::seekp(long off,dir);
streampos istream::tellp();
这 3个成员函数的含义与前面 3个操作读指针成员函数的含义相同,只不过它们是用来操作写指针的。
【 例 3-25】 随机文件的读写操作。
第三章 面向对象的程序设计本章导读掌握类与对象的概念,类与对象的定义方法及二者间的区别。
掌握类的成员函数的定义方法,保存方法及调用方法 。
掌握类中成员的访问机制和方法 。
了解对象的作用域和生存期 。
理解并掌握构造函数,析构函数,拷贝构造函数,默认构造函数和缺省参数的构造函数的含义,定义方法以及在对象的构造和撤消中的作用 。
理解并掌握当一个类的对象作为另一个类的数据成员时,利用初始化表调用构造函数的方法,构造函数的执行顺序 。
返回 2
本章导读理解继承的概念和意义,理解单一继承、多重继承。理解并掌握派生类构造函数的编写要求,以及派生类对象的构造过程和机理。
掌握虚函数和多态性的概念,掌握虚函数的定义方法、
调用方法及其在实现多态性方面所起到的作用。了解纯虚函数与抽象基类的概念。
了解类的静态成员 ( 静态数据成员和静态成员函数 ) 的概念,定义方法及其作用 。
了解友元函数与友元类的概念,定义方法及其作用 。
了解运算符重载及在程序中实现运算符重载的方法 。
了解模板的概念,在程序中如何定义类模板和函数模板。
返回 3
3.1 类与对象的定义类和对象是面向对象程序设计 ( OOP) 的两个最基本概念 。 所谓 对象 就是客观事物在计算机中的抽象描述;
类 是对具有相似属性和行为的一组对象的统一描述 。
3.1.1 类的定义
C++的类是在结构体的基础上扩充而来的 。 类是把各种不同类型的数据 ( 称为数据成员 ) 和对数据的操作
( 成员函数 ) 组织在一起而形成的用户自定义的数据类型 。
C++中,类定义包括类说明和类实现两大部分 。 说明部分提供了对该类所有数据成员和成员函数的描述,而实现部分提供了所有成员函数的实现代码 。
返回 4
3.1 类与对象的定义类定义的一般形式为:
class 类名
{private:
数据成员或成员函数
protected:
数据成员或成员函数
public:
数据成员或成员函数
};
<各成员函数的实现代码 >
返回 5
3.1 类与对象的定义说明:
1,class是定义类的关键字,类名由用户自己定名,必须是 C++的有效标识符,但一般首字母大写 。
2,大括号的部分是类的成员 ( 数据成员和函数成员 ),
它们分成三部分,分别由 private,public,proctected三个关键字后跟冒号来指定 。 这三部分可以任何顺序出现,
且在一个类的定义中,这三部分并非必须同时出现 。
( 1) 如果数据成员或成员函数在类的 private部分,那么在类之外是不能存取的,只有类中的成员函数才能存取
private的数据成员和成员函数 。
( 2) 在一个类的 public部分说明的数据成员或成员函数可被程序中的任何函数或语句存取,public成员多为成员函数,用来提供一个与外界的接口,外界只有通过这个接口才可以实现对 private成员的存取 。
返回 6
3.1 类与对象的定义
( 3)在类的 protected部分说明的数据成员和成员函数是不能在类之外存取的,只有类的成员函数及其子类(派生类)可以存取 protected的成员。
( 4) 当定义类时,当未指明成员是哪部分时,默认是属于 private成员,但一般不要采用默认形式 。
如:下例中定义描述图书的类定义
class Record
{ private,//private成员
char bookname[20]; //数据成员 bookname,
// 用于表示图书的名称
int number; //数据成员 number,表示图书编号返回 7
3.1 类与对象的定义
public,//public成员
void regist(char *a,int b); //成员函数 regist,用于给
//各数据成员赋值
void show(); //成员函数 show,显示各数据成员的值
};
3,要特别注意,在类的定义中,类的说明部分的右边大括号后面必须有一,;,,
4,根据类的定义,可看出:类是实现封装的工具,所谓封装就是将类的成员按使用或存取的方式分类,有条件地限制对类成员的使用,而封装是通过 public和
private与成员函数实现的 。 private的成员构成类的内部状态,public的成员则构成与外界通信的接口,通过 public的成员函数来使用 private的数据成员,从而在
C++中实现了封装 。
返回 8
3.1 类与对象的定义
3.1.2成员函数的定义类中的成员函数可以在以下两处定义:
( 1) 将成员函数的定义直接写在类中,
如:对于前面定义的图书类 Record来说,其成员函数
regist和 show的定义可直接写在类的定义体中 。
class Record
{ private:
char bookname[20];
int number;
返回 9
3.1 类与对象的定义
public:
void regist(char *a,int b) //成员函数 regist()的定义
{ strcpy(bookname,a); //给数据成员 bookname赋值
number=b; //给数据成员 number赋值
}
void show() //成员函数 show()的定义
{ cout<<”名称:,<<bookname<<endl;
cout<<”号码:,<<number<<endl;
}
};
返回 10
3.1 类与对象的定义在类中直接定义成员函数的情况一般适合于成员函数规模较小的情况,也就是说它们一般为内联函数,即使没有明确用 inline关键字 。
( 2) 在类的定义体中只写出成员函数的原型说明,而成员函数的定义写在类的定义之后,这种情况比较适合于成员函数体较大的情况,但这时要求在定义成员函数时,在函数的名称之前加上其所属性类名及作用域运算符,,:” 。
定义成员函数的一般类型为:
返回值类型 类名::成员函数名 ( 参数说明 )
{ 类体 }
返回 11
3.1 类与对象的定义此处的,:符号叫作用域运算符,用它来指明哪个函数属于哪个类或哪个数据属于哪个类,所以使用类中成员的全名是:类名,:成员名 。
而如果没有类名,则为全局数据或全局函数 ( 非成员函数 ),也就是说类名是其成员名的一部分 。
如 class Record
{ private:
char bookname[20];
int number;
public:
void regist(char *a,int b); //成员函数 regist的原型
void show(); //成员函数 show的原型
}; //定义图书类 Record
返回 12
3.1 类与对象的定义
void Record::regist(char *a,int b) //regist()是类 Record的
//成员函数
{ strcpy(bookname,a);
number=b; }
void Record::show() // show()是类 Record的成员函数
{ cout<<”名称:,<<bookname<<endl;
cout<<”号码:,<<number<<endl; }
此外,目前开发程序的通常将类的定义写在一个头文件 (,h文件 ) 中,成员函数的定义写在一个程序文件
(,cpp文件 ) 中,这样,就相当于把类的定义 ( 头文件 )
看成是类的外部接口,类的成员函数的定义看成类的内返回 13
3.1 类与对象的定义部实现 。 如:对上例可改成将类的定义体写在 myapp.h文件中,而成员函数的定义体写在另外一个文件 myapp.cpp中:
//myapp.h文件
class Record
{ private:
char bookname[20];
int number;
public:
void regist(char *a,int b);
void show();
};
返回 14
3.1 类与对象的定义
//myapp.cpp文件
#include,iostream.h”
#include,myapp.h” //一定不要忘记嵌入该头文件
void record::regist(char *a,int b)
{ strcpy(bookname,a);
number=b;
}
void record::show()
{ cout<<”名称:,<<bookname<<endl;
cout<<”号码:,<<number<<endl;
}
返回 15
3.1 类与对象的定义
3.1.3 对象的定义对象是类的实例,定义对象的一般格式为:
类名 变量名表;
或 类名 对象名;
如:上例中已定义了类 Record,则:
Record book1,book2; //此处的 book1,book2就是 Record
//类型,也就是类的两个对象类是抽象的概念,而对象是具体的,类只是一种数据类型,而对象是属于该类 ( 数据类型 ) 的一个变量,占用了各自的存储单元,每个对象各自具有了该类的一套数据成员 ( 静态成员除外 ),而所有成员函数是所有对象共有的 。 每个对象的函数成员都通过指针指向同一个代码空间 。
返回 16
3.1 类与对象的定义
3.1.4 访问对象的成员访问对象的成员包括读写对象的数据成员和调用它的成员函数,其访问格式是:
对象名,成员名如上例中,对象的主函数如下:
void main()
{ Record book1,book2; //定义对象 book1和 book2
//调用成员函数 regist,给 book1的两个数据成员
//bookname和 number赋值
book1.regist(“C++编程教程,,1001);
//调用成员函数 regist,给 book2的两个数据成员赋值
book2.regist(“C++语言参考,,1002);
返回 17
3.1 类与对象的定义
//调用成员函数 show,显示 book1对象的数据成员
//bookname和 number的值
book1.show();
//调用成员函数 show,显示 book2对象的数据成员
//bookname和 number的值
book2.show();
}
如改为下面的代码,则错误:
void main()
{ Record book1,book2;
//由于 bookname和 number是类 Record的私有成员,在类外
//不能直接使用返回 18
3.1 类与对象的定义
strcpy(book1.bookname,“C++编程教程,);
book1.number=1001;
strcpy(book2.bookname,“C++语言参考,);
book2.number=1002;
book1.show();
book2.show();
}
注意:
1,对于类的私有成员,只能通过其成员函数来访问,不能在类外对私有成员访问 。
返回 19
3.1 类与对象的定义
2,调用成员函数时要在函数名之前加上对象名和 "."即可,
即先指明对象,再指明成员 。 也可以采用指向对象的指针来访问,但要在函数名前加上指针变量名和,->” 。
3,任何对对象私有数据的访问都必须通过向对象发送消息来实现,而且所发送的消息还必须是该对象能够识别和接受的 。 在 C++中,消息发送正是通过公有成员函数的调用来实现的 。 由于类接口隐藏了对象的内部细节,用户只能通过类接口访问对象,因此,在类设计中必须提供足够的公有接口以捕获对象的全部行为,这正是类设计中的一个最基本的要求 。
4,上例中,在对象调用 book1.regist(“C++编程教程,,
1001);时,成员函数 regist除了接受两个实参外,还接返回 20
3.1 类与对象的定义受了一个对象 book1的地址,这个地址被一个隐含的形参
this指针所获取,它等同于执行 this=&book1,所以所有对数据成员的访问都隐含地被加上前缀,this->,因此,
在成员函数体 regist中,执行
strcpy(bookname,a);number=b;就等价于
strcpy(this->bookname,a); this->number=b;
这样,上例中的成员函数 regist也可这样定义:
void record::regist(char *a,int b)
{ strcpy(this->bookname,a);
this->number=b;
}
通过以上手段就确保了不同对象调用成员函数时访问的是不同对象的数据,而它们之间没有干扰 。
返回 21
3.1 类与对象的定义
3.1.5 对象赋值语句对于同一个类生成的两个对象,可以进行赋值,
其功能是将一个对象的数据成员赋值到另一个对象中去,赋值语句的左右两边各是一个对象名:
【 例 3-1】 对于类 example的两个对象 obj1和 obj2,
让 obj2的成员数据的值等于 obj1的成员数据的值 ( 假定 obj1的成员数据 num已经存有数据 215) 。
返回 22
3.1 类与对象的定义
# include <windows.h>
# include <iostream.h>
//定义类
class example
{ private,// 数据成员
int num;
public,// 函数成员说明
void set ( int i )
{ num=i ; }
void disp ( )
{ cout << "\n num = " << num ; }
};
返回 23
3.1 类与对象的定义
// 主程序
void main ()
{ example obj1,obj2 ;
obj1.set(215);
obj1.disp ();
obj2=obj1; // 对象赋值语句
obj2.disp () ;
cout<<endl<<endl; }
3.1.6 对象的作用域与生存期对象是类的实例,它实质就是某种数据类型的变量,
在不同的位臵以不同的方式定义对象时,其作用域和生存期是不同的 。
返回 24
3.1 类与对象的定义如,class Desk //定义 Desk类
{ public:
int weight;
int high;
int width;
int length; };
class Stool //定义 Stool类
{ public:
int weight;
int high;
int width;
int length;};
返回 25
3.1 类与对象的定义
desk da; //定义全局对象
Stool sa;
void fn()
{ static Stool ss; // 静态局部对象
desk da; // 定义局部对象
//
}
1,局部对象 ( 不包括局部静态对象 )
其作用域是定义它的函数体,生存期从函数调用开始到函数调用结束,下一次再重新调用函数时,再重新构造对象 。
构造局部对象的次序 ( 即分配存储单元 ) 是按它们在函数体中声明的顺序 。
返回 26
3.1 类与对象的定义
2,静态对象 ( 局部静态和全局静态 )
其作用域是定义它的函数体或程序文件,其生存期是整个程序 。 构造静态对象的次序是按它们在程序中出现的次序先后,并在整个程序运行开始时 ( 即在主函数运行前 )
只构造一次 。
3,全局对象全局对象的作用域是整个程序,生存期是整个程序的运行时间 。 它也是在程序运行前 ( 即在主函数运行前 ) 只构造一次 。
4.类中成员的构造次序是以类中声明成员的次序进行。
构造函数和析构函数是类的两种特殊的成员函数 。
返回 27
3.2 构造函数与析构函数
3.2.1 构造函数构造函数 ( constructor) 是与类名同名的特殊的成员函数,当定义该类的对象时,构造函数将被自动调用以实现对该对象的初始化 。 构造函数不能有返回值,因而不能指定包括 void在内的任何返回值类型 。 构造函数的定义体可与其它成员函数成员一样,放在类内或类外都可 。 构造函数的定义格式为:
类名 ( 形参说明 )
{ 函数体 }
构造函数既可定义成有参函数,也可义成无参函数,要根据问题的需要来定 。 全局变量和静态变量在定义时,将自动赋初值为 0;局部变量在定义时,其初始值不固定的 。
而当对象被定义时,由于对象的意义表达了现实世界的实返回 28
3.2 构造函数与析构函数体,所以一旦定义对象,就必须有一个有意义的初始值,
在 C++中,在定义对象的同时,给该对象初始化的方法就是利用构造函数 。 如:
【 例 3-2】 类 person包括 4个数据成员,用来记录人员信息。
生成对象 obj,并使用构造函数为 obj赋予初始值。
# include <windows.h>
# include <iostream.h>
class Person //定义类
{ private,//类 Person的数据成员
char name [10] ; //姓名
int age ; //年龄
int salary ; //薪金
char tel[8]; //电话返回 29
3.2 构造函数与析构函数
public,//构造函数 Person
Person ( char *xname,int xage,int xsalary,char *xtel ) ;
void disp () ; };
//函数 Person的定义
Person,,Person ( char *xname,int xage,int xsalary,char
*xtel )
{ strcpy (name,xname) ; //给各数据成员提供初值
age = xage ;
salary = xsalary ;
strcpy (tel,xtel) ;
}
返回 30
3.2 构造函数与析构函数
//函数 disp的定义
void Person::disp()
{ cout<<endl;
cout << " 姓名," << name << endl ;
cout << " 年龄," << age << endl ;
cout << " 工资," << salary << endl ;
cout << " 电话," << tel << endl<<endl ;}
// 主函数
void main( )
{ //生成对象 obj并初始化
Person obj ("张立三 ",25,850,"45672314");
//显示 obj
obj.disp ( ) ; }
返回 31
3.2 构造函数与析构函数程序的执行结果是:
姓名:张立三年龄,25
工资,850
电话,45672314
在主函数中的 Person obj ("张立三 ",25,850,"45672314");中完成了以下几个功能:
1,定义并生成了对象 obj。
2.在生成对象 obj的同时,自动调用相应类的构造函数 Person
3.将初始值 "张立三 ",25,850,"45672314"传递给构造函数
Person相应的形参 xname,xage,xsalary,xtel。
4,执行构造函数体,将相应的值赋给相应的数据成员 。
返回 32
3.2 构造函数与析构函数
3.2.2 构造函数的重载如果一个类中出现了两个以上的同名的成员函数时,
称为类的成员函数的重载。
【 例 3-3】 类 rec定义两个重载函数,其中一个是无参函数,
另一个是有参函数 。 它们都是构造函数 。
# include <windows.h>
# include <iostream.h>
//定义类
class Rec
{ private:
char bookname[30];
int number;
返回 33
3.2 构造函数与析构函数
public:
Rec(); //第 1个构造函数说明
Rec (char *a,int b); //第 2个构造函数说明
void show();
};
Rec,,Rec () //第 1个构造函数定义
{ strcpy(bookname,'\0');
number=0; }
Rec,,Rec (char *a,int b ) //第 2个构造函数定义
{ strcpy(bookname,a);
number=b; }
返回 34
3.2 构造函数与析构函数
void Rec,,show ( ) //show的函数定义
{ cout<<"bookname is,"<<bookname<<endl;
cout<<"booknumber is:"<<number<<endl;
}
void main() //主程序
{ Rec mybook(,Visual C++6.0”,10020); //自动调用构造
//函数 Rec(char *a,int b)
mybook.show();
Rec yourbook; //自动调用构造函数 Rec()
yourbook.show();
}
返回 35
3.2 构造函数与析构函数程序的执行结果是:
bookname is,Visual C++6.0
booknumber is:10020
bookname is,no name
booknumber is:0
可见,当出现构造函数重载时,其匹配方式同普通函数重载时的匹配方式 。
返回 36
3.2 构造函数与析构函数
3.2.3 默认构造函数与缺省构造函数
C++规定,每个类必须有一个构造函数 。 如果在类中没有显式定义构造函数时,则 C++编译系统在编译时为该类提供一个默认的构造函数,该默认构造函数是个无参函数,它仅负责创建对象,而不做任何初始化工作 。
只要一个类定义了一个构造函数 ( 不一定是无参构造函数 ),C++编译系统就不再提供默认的构造函数 。
与变量定义相似,在用默认构造函数创建对象时,如果创建的是全局对象或静态对象,则对象的默认值为 0,
否则对象的初始值是不定的 。
当构造函数有缺省参数时,称为具有缺省参数的构造函数,在使用时要防止二义性 。
返回 37
3.2 构造函数与析构函数如:
class Myclass //定义类 Myclass
{ private:
int member;
public:
Myclass();
Myclass(int i);
}
Myclass:Myclass() //构造函数 Myclass
{ member=10; }
返回 38
3.2 构造函数与析构函数
Myclass:Myclass(int i=10) //构造函数 Myclass(int i),该函数
// 的形参 i为缺省参数
{ member=i; }
void main()
{ Myclass x(20);
Myclass y; //产生二义性,无法确定自动调用哪个构造
//函数完成对象的构造
}
返回 39
3.2 构造函数与析构函数
3.2.4 析构函数当一个对象被定义时,系统自动调用构造函数为该对象分配相应的资源,当对象使用完毕后,这些系统资源需要在对象消失前被释放 。
析构函数是类的一个特殊成员函数,其函数名称是在类名的前面加上 ~,它没有返回值,没有参数,不能随意调用,也没有重载,只是在类对象生命期结束时,系统自动调用 。 析构函数的定义方式为:
~类名 ( )
{ 函数体 }
注,( 1) 一个类中只能拥有一个析构函数 。
返回 40
3.2 构造函数与析构函数
( 2) 如果程序员在定义类时,没有为类提供析构函数,
则系统会自动创建一个默认的析构函数,其形式为:
~类名 ( ) { }
( 3) 对于一个简单的类来说,大多可以直接使用系统提供的默认析构函数 。 但是,如果在类的对象中分配有动态内存 ( 如:用 new申请分配的内容 ) 时,就必须为该类提供适当的析构函数,完成清理工作 。
( 4) 对象被析构的顺序与对象建立时的顺序正好相反 。
即最后构造的对象先被析构 。
返回 41
3.2 构造函数与析构函数
【 例 3-4】 类 Teacher的构造函数为 name申请存储空间,在析构函数中释放该空间 。
# include <windows.h>
# include <iostream.h>
//定义类
class Teacher
{ private:
char * name;
int age;
public:
//说明构造函数 Teacher
返回 42
3.2 构造函数与析构函数
Teacher(char *i,int a )
{ name=new char[strlen(i)+1] ;
//用 new为 name成员分配堆内存
strcpy (name,i);
age = a;
cout << "\n 执行构造函数 Teacher "<< endl; };
//说明析构函数 ~Teacher
~ Teacher ( )
{ delete name ;
cout << " 执行析构函数 ~Teacher "<< endl<<endl; } ;
void show(); };
返回 43
3.2 构造函数与析构函数
void Teacher,,show ()
{ cout << " 姓名,"<<name<<" "<<"年龄,"<<age<< endl; }
void main() //主程序
{ Teacher obj ("张立三 ",25);
obj.show();
}
程序的执行结果是:
执行构造函数 Teacher
姓名:张立三 年龄,25
执行析构函数 ~Teacher
返回 44
3.2 构造函数与析构函数
3.2.5 拷贝构造函数拷贝构造函数是 C++中引入的一种新的构造函数 。 定义一个拷贝构造函数的方式是:
类名 ( const 类名 &形式参数 )
{ 函数体 }
由此可看出:
( 1) 拷贝构造函数的名称与类的名称相同,且它只有一个参数,该参数就是对该类对象的引用 。
( 2) 拷贝构造函数的功能是用于实现对象值的拷贝,通过将一个同类对象的值拷贝给一个新对象,来完成对新对象的初始化,即用一个对象去构造另外一个对象 。
返回 45
3.2 构造函数与析构函数
【 例 3-5】 Example是一个人员信息类 。 用普通构造函数生成 obj1,用拷贝构造函数生成 obj2。
# include <windows.h>
# include <iostream.h>
class Example
{ private:
char *name;
int num;
public:
example(int i,char *str ) // 构造函数定义
{ name=str;
num=i; }
返回 46
3.2 构造函数与析构函数
example(const Example &x) // 拷贝构造函数定义
{ num=x.num; }
void list() // 定义显示函数 list
{ cout<<"\数据成员 num的值 ="<<num<<endl<<endl; }
};
void main ()
{ example obj1(215,,张立三,);
//调用函数 Example(int i,char *str )构造 obj1
example obj2(obj1); //使用拷贝构造函数构造 obj2
obj2.list(); //显示 obj2的值
//其它程序部分
}
返回 47
3.2 构造函数与析构函数程序的执行结果是:
数据成员 num的值 =215
数据成员 num的值 =215
说明:
( 1) 上例中在 main函数中的语句 Example obj2(obj1);
在执行时,系统会自动调用类 Example的拷贝构造函数完成对 obj2对象的构造 。
( 2) 如果程序员没有为所设计的类提供显式的拷贝构造函数,则系统会自动提供一个默认的拷贝构造函数,其功能是:把作为参数的对象的数据成员逐个拷贝到目标变量中,这称为成员级复制 ( 或浅拷贝 ) 。
返回 48
3.2 构造函数与析构函数
3.2.6 一个类的对象作为另一个类的数据成员一个类中的数据成员除了可以是 int,char,float等这些基本的数据类型外,还可以是某一个类的一个对象 。 用子对象创建新类 。
在 C++中,当把一个类的对象作为新类的数据员时,
则新类的定义格式可表示为:
class X
{ 类名 1 成员名 1;
类名 2 成员名 2;
类名 n 成员名 n;
//其它成员
};
返回 49
3.2 构造函数与析构函数
( 3) 如果一个类 A的对象作为另一个类 B的数据成员,则在类 B的对象创建过程中,调用其构造函数的过程中,数据成员 ( 类 A的对象 ) 会自动调用类 A的构造函数 。
但应 注意,如果类 A的构造函数为有参函数时,则在程序中必须在类 B的构造函数的括号后面加一,,,和被调用的类 A的构造函数,且调用类 A的构造函数时的实参值必须来自类 B的形参表中的形参 。 这种方法称为初始化表的方式调用构造函数 。 如:以上面定义的类 X为例,在对类
X的对象进行初始化时,必须首先初始化其中的子对象,
即必须首先调用这些子对象的构造函数 。 因此,类 X的构造函数的定义格式应为:
X:,X( 参数表 0),成员 1( 参数表 1),成员 2( 参数表 2),…,成员 n(参数表 n)
{ …… }
返回 50
3.2 构造函数与析构函数其中,参数表 1提供初始化成员 1所需的参数,参数表 2提供初始化成员 2所需的参数,依此类推 。 并且这几个参数表的中的参数均来自参数表 0,另外,初始化 X的非对象成员所需的参数,也由参数表 0提供 。
在构造新类的对象过程中,系统首先调用其子对象的构造函数,初始化子对象;然后才执行类 X自己的构造函数,
初始化类中的非对象成员 。 对于同一类中的不同子对象,
系统按照它们在类中的说明顺序调用相应的构造函数进行初始化,而不是按照初始化表的顺序 。
返回 51
3.2 构造函数与析构函数
【 例 3-6】 以下定义了三个 Student,Teacher和 Tourpair,
其中 Student类的对象和 Teacher类的对象作为了 Tourpair
的数据成员,观察对象的构造过程和构造函数被执行的顺序 。
#include <iostream.h>
class Student
{ public:
Student()
{ cout<<”construct student.\n”;
semeshours=100;
gpa=3.5; }
返回 52
3.2 构造函数与析构函数
protected:
int semeshours;
float gpa;
};
class Teacher
{ public:
Teacher()
{ cout<<”construct Teacher.\n”;
}
};
返回 53
3.2 构造函数与析构函数
class Tourpair
{public:
Tourpair()
{cout<<”construct tourpair.\n”;
nomeeting=0; }
protected:
Student student;
Teacher teacher;
int nomeeting;
};
返回 54
3.2 构造函数与析构函数
void main()
{Tourpair tp;
cout<<”back in main.\n”; }
其执行结果是:
construct student.
construct teacher.
construct tourpair.
back in main.
由此可见:主函数 main()运行开始时,遇到要创建
Tourpair类的对象,于是调用其构造函数 Tourpair( ),
该构造启动时,首先分配对象空间 ( 包含一个 Student对返回 55
3.2 构造函数与析构函数象,一个 Teacher对象和一个 int型数据 ),然后根据其在类中声明的对象成员的次序依次调用其构造函数 。 即先调用 Student()构造函数,后调用 Teacher()构造函数,最后才执行它自己的构造函数的函数体 。
由于上例中 Tourpair类的数据成员 student和 teacher的构造函数都是无参函数,所以系统在构造 student和 teacher对象时会自动调用各自的构造函数 Student()和 Teacher(),而不需要用初始化表的方式去调用 。
【 例 3-7】 试分析以下程序的执行结果,
#include <iostream.h>
#include <string.h>
返回 56
3.2 构造函数与析构函数
class Student
{ public:
Student(char *pName="No name")
{ cout<<"构造新同学,"<<pName<<endl;
strncpy(name,pName,sizeof(name));
name[sizeof(name)-1]='\0';
}
Student(Student &s)
{ cout<<"构造 copy of "<<s.name<<endl;
strcpy(name," copy of ");
strcat(name,s.name);
}
返回 57
3.2 构造函数与析构函数
~Student()
{ cout<<"析构 "<<name<<endl; }
protected:
char name[40]; };
class Tutor
{ public:
Tutor(Student &s):student(s)//此为初始化表,调用
//Student的拷贝构造函数
{ cout<<"构造指导教师 \n"; }
protected:
Student student;
};
返回 58
3.2 构造函数与析构函数
void main()
{ Student st1; //此处调用 Student的构造函数 Student(char
*pName="No name")
Student st2("zhang"); //同上
Tutor tutor(st2); //此处调用 Tutor的构造函数 Tutor(Student &s)
//在构造 tutor对象的过程中,用初始化表调用
//Student类的拷贝构造函数 Student(Student &s)
}
执行结果如下:
构造新同学,No name
构造新同学,zhang
构造 copy of zhang
返回 59
3.2 构造函数与析构函数构造指导教师析构 copy of zhang
析构 zhang
析构 No name
3.2.7 利用初始化表对常量数据成员或引用成员提供初值如前所述,构造函数可对对象的数据成员进行初始化,
但若数据成员为常量成员或引用成员时,就有所不同,如:
class Sillyclass
{ public,
Sillyclass() // 此构造函数对成员 ten和 refi的初始化错误 。
{ ten=10;
refi=i; }
返回 60
3.2 构造函数与析构函数
protected:
const int ten; //常量数据成员 ten
int &refi; //引用 refi
};
说明:
1,造成以上错误的原因是在 Sillyclass类的构造函数进入之后 ( 开始执行其函数体时 ),对象结构已经建立,数据成员 ten和 refi已存在,而其数据成员 ten为 const,而 refi为引用,所以在构造函数体内不能再对其指派新的值 。
2,解决以上问题的方法是利用初始化表:在构造函数的括号后面加一,,,和初始化表,初始化表的格式是:
数据成员名 ( 值 ),如果有多个时,需要用逗号隔开 。
返回 61
3.2 构造函数与析构函数
【 例 3-8】 类 employee中包括私有数据成员 x,和 2个公有函数成员 example,show。 程序中使用初始化表是 x(215)。
# include <windows.h>
# include <iostream.h>
// 定义类 employee
class employee
{private:
const int x;
public:
employee ();
void show();
};
返回 62
3.2 构造函数与析构函数
// employee的类外定义
employee,,employee (),x (215) // 初始化表
{ }
// show()的定义 。
void employee,,show()
{ cout << "\n x的值是,"<< x << endl; }
// 主函数
void main()
{ //生成对象并为 x赋予初始值
employee obj;
//调用 show显示 x的值
obj.show();
}
返回 63
3.2 构造函数与析构函数
3.2.8 类作用域类作用域 又可称为类域,它是指在类定义中用一对大括号所括起来的范围 。 由于在程序文件中可包含类,而类中又包含函数,因此,类域显然是一个小于文件域,
而大于函数域的概念 。
由于在一个类中既可定义变量 ( 数据成员 ),又可定义函数 ( 成员函数 ),所以,类域在许多方面与文件域相似 。 但是,在类域中定义的变量不能使用 auto、
register和 extern等修饰符,而且在类域中定义的函数也不能使用 extern修饰符 。 同时,在类域中定义的静态成员和成员函数还具有外部的连接属性 。
返回 64
3.2 构造函数与析构函数
【 例 3-9】 类域及其成员引用举例,设以下程序代码被存放到了一个程序文件中。
#include <iostream.h>
class Myclass
{ private:
int x;
int y;
public:
Myclass(int a,int b) { x=a;y=b;}
void print();
void myfunc();
};
返回 65
3.2 构造函数与析构函数
void Myclass::print()
{ cout<<"x="<<x<<","<<"y="<<y<<endl; }
void Myclass::myfunc()
{ int x=9,y=10;
cout<<"In myfunc,x="<<x<<","<<"y="<<y<<endl;
//输出局部变量
//输出类的数据成员
cout<<"Myclass::x="<<Myclass::x<<","<<"
Myclass::y="<<Myclass::y<<endl;
}
返回 66
3.2 构造函数与析构函数
void main()
{ Myclass test(100,200),*ptest=&test;
test.print(); //通过对象名访问公有成员
ptest->myfunc(); }
程序的运行结果为:
x=100,y=200
In myfunc,x=9,y=10
Myclass::x=100,Myclass::y=200
说明:
( 1) 类成员函数的原型在类的定义体中声明,具有类作用域,但其实现部分在类的定义体外 。 由于不同类的成员函数可以具有相同的名字,因此,需要用作用域运算符,,:,来指明该成员函数所属的类 。
返回 67
3.2 构造函数与析构函数
( 2) 类中的成员拥有类作用域,因此在成员函数中可以直接引用类的数据成员 。 但是,如果在成员函数中定义了同名的局部变量时,则必须用作用域运算符,,:,来指定,以免混乱 。 如:上例中的 myfunc()函数中定义了与类的数据成员同名的局部变量 x,y,所以在 myfunc()函数中要访问类中的数据成员 x和 y的值时,必须加上作用域运算符 。
( 3) 类中的成员拥有类的作用域,如果要从类外访问类的成员时,则必须通过对象名或指向对象的指针 。 当通过对象名时,应使用圆点成员选择符,,”;当通过指针时,
应使用箭头成员选择符,->”。 如上例中的 test.print();
与 ptest->myfunc();
返回 68
3.3 继承和派生
3.3.1 继承的概念一个类的数据成员和成员函数,有些是类本身自己定义的,有一些是可继承的或通过模板生成的 。
所谓 继承 ( inheritance) 就是利用已有的数据类型定义出新的数据类型 。 利用类的,继承,,就可以将原来的程序代码重复使用,从而减少了程序代码的冗余度,符合软件重用的目标 。 所以说,继承是面向对象程序设计的一个重要机制 。 另外,在 C++中扩充派生类成员的方法是非常灵活的 。 派生类不仅可以继承原来类的成员,还可以通过以下方式产生新的成员:
返回 69
3.3 继承和派生
( 1) 增加新的数据成员;
( 2) 增加新的成员函数;
( 3) 重新定义已有成员函数;
( 4) 改变现有成员的属性 。
在继承关系中,称被继承的类为基类 ( base class)
( 或父类 ),而把通过继承关系定义出来的新类称为派生类 ( derived class) ( 子类 ) 。
由此可见,派生类既可以对基类的性质进行扩展,又可以进行限制,从而得到更加灵活,更加适用的可重用模块,大大缩短程序的开发时间 。
返回 70
3.3 继承和派生
3.3.2 单继承
1,定义派生类在基类的基础上定义其派生类的定义形式为:
class 派生类名:访问方式 基类名
{ 派生类中的新成员 }
其中:
( 1) 派生类名由用户自己命名;
( 2) 访问方式即继承方式,可以为 public 或 private,默认为 private方式 。 访问方式为 public方式时,这种继承称为公有继承,而访问方式为 private方式时,称为私有继承;
( 3) 基类名必须是程序中一个已有的类 。
返回 71
3.3 继承和派生
( 4) 在冒号,,,后的部分告诉系统,这个派生类是从哪个基类派生的,以及在派生时的继承方式 。
( 5) 大括号内的部分是派生类中新定义的成员 。
2,基类与派生类之间的关系
( 1) 派生类不仅拥有属于自己的数据成员与成员函数,
还保持了从基类继承来的数据成员与成员函数;同时派生类可对一些继承来的函数重新定义,以适应新的要求 。
( 2) C++关于类的继承方式的规定,如下表 3.1所示:
① 按 private方式继承 ( 即私有继承 ) 时,基类中的公有成员和保护成员在派生类中皆变为私有成员 。
② 按 public方式继承 ( 即公有继承 ) 时,基类中的公有成员和保护成员在派生类中不变 。
返回 72
3.3 继承和派生
③ 无论哪种继承方式,基类的私有成员均不能继承 。 这与私有成员的定义是一致的,符合数据封装的思想 。
④ 在公有继承方式下,基类的公有成员和保护成员被继承为派生类成员时,基访问属性不变 。
注意,私有成员与不可访问成员是两个不同的概念 。 某个类的私有成员只能被该类的成员函数所访问,而类的不可访问成员甚至不能被该类自身的成员函数所访问 。
类的不可访问成员总是从某个基类派生来的,它要么是基类的私有成员,要么是基类的不可访问成员 。
基类 公有派生类 私有派生类
public成员 public成员 private成员
protected成员 protected成员 private成员
private成员 无法继承 无法继承返回 73
3.3 继承和派生
( 3) 在 C++中,可以根据需要定义多层的继承关系,也可以从一个基类派生出多个类,形成类的层次结构,在类的层次结构中,处于高层的类,表示最一般的特征,
而处于底层的类,表示更具体的特征,在多层继承关系中,基类与派生类的关系是相对的,例如:由类 A派生出类 B,再由类 B派生出类 C,这里类 B相对于类 A是派生类,
而相对于类 C是基类,并称类 C是类 A的间接派生类,称类 A是类 C 的间接基类;而称具有直接派生关系的两个类分别为直接派生类和直接基类 。
【 例 3-9】 类 Build_1是一个关于楼房数据的类 。 它的数据成员有 posi_x,posi_y和 area,分别是楼房位臵的经,纬度和建筑面积 。 它的函数成员只有 set1,用于设臵数据成员 posi_x,posi_y和 area的值 。 让 Build_1作为基类,再增加数据成员 high,函数成员 set2和 disp来定义派生类
Build_2。
返回 74
3.3 继承和派生
#include <iostream.h>
class Build_1 //定义基类
{ protected:
int posi_x; // 有三个保护型的数据成员
int posi_y;
int area;
public:
void set1(int x,int y,int a)
{ posi_x=x; posi_y=y; area=a; }
};
//定义派生类 Build_2
class Build_2,public Build_1
返回 75
3.3 继承和派生
{ int height;
public:
void set2(int h)
{ height=h;}
void disp()
{ cout<<"\n 经度,"<<posi_x<<endl;
cout<<" 纬度,"<<posi_y<<endl;
cout<<" 高度,"<<height<<endl;
cout<<" 面积,"<<area<<endl<<endl; }
};
void main()
{ //用 Build_2生成对象 obj
返回 76
3.3 继承和派生
Build_2 obj;
obj.set1(100,200,300);
obj.set2(400);
obj.disp();
}
程序执行的结果是:
经度,100
纬度,200
高度,400
面积,300
由此可见:派生类 Build_2中已继承了基类 Build_1中的数据成员 posi_x,posi_y,area和基类中的成员函数 set1,
并同时增加了新的成员 height和成员函数 set2,disp。
返回 77
3.3 继承和派生
3,派生类的数据成员和成员函数,构造过程与构造函数
( 1) 派生类的数据成员和成员函数的来源有两个,一个来源是从基类继承来的数据成员和成员函数,对于继承来的数据成员,即使没有用也不能取消,只能不理会它们,但允许对一些继承来的成员函数重新定义,即在原有基类的成员函数的基础上,再增加一些操作,以完成派生类所要求的操作 。 另一个来源就是由派生类自己定义的数据成员和成员函数,这些成员的定义方法同一般类成员的定义方法基本一样 。
( 2) 通过派生类的对象调用一个被重新定义过的基类的成员函数,所调用的是派生类的成员函数,此时,若想调用基类的成员函数,必须在成员函数名前加基类名作用域分隔符,,:,。
返回 78
3.3 继承和派生
( 3) 在创建派生类的对象时,由于派生类的对象包含了基类的数据成员,因此派生类的构造函数除初始化其自身定义的数据成员外,还必须对基类中的数据成员进行初始化,也就是说,派生类的构造函数要负责调用基类的构造函数 。 所以派生类的构造函数的定义格式如下:
派生类名::派生类构造函数名 ( 参数表 ),基类构造函数名 ( 参数表 )
{ }
( 4) 虽然派生类可以直接访问基类的保护数据成员,甚至在构造时初始化它们,但是一般不这么做,而是通过基类的接口 ( 成员函数 ) 去访问它们,初始化也是通过基类的构造函数 。 这样,避免了类与类之间的相互干扰 。
返回 79
3.3 继承和派生
( 5) 基类的对象只能调用基类的成员函数,不能调用派生类的成员函数 。
( 6) 在定义派生类的对象时,系统首先执行基类的构造函数,然后执行派生类的构造函数 。 而系统执行析构函数的顺序恰恰相反,即先执行派生类的析构函数,再执行基类的析构函数 。
( 7) 若在基类中没有定义任何构造函数,这时在派生类的构造函数的定义中可以省略对基类构造函数的调用,
此时系统将去调用基类的默认构造函数 。
如,【 例 3-10】 本例中分别定义一个描述圆的类 Ccircle
和描述一个圆柱体的类 Ccylinder。
返回 80
3.3 继承和派生
#include "iostream.h"
class Ccircle //定义圆类
{protected:
double radius;
public:
Ccircle(double radiusval)
{ radius=radiusval; }
void setradius(double radiusval)
{ radius=radiusval; }
double getradius() const
{ return radius ; }
返回 81
3.3 继承和派生
double area() const
{ return 3.14*radius*radius; }
};
class Ccylinder,public Ccircle //定义圆柱体类
{protected:
double height;
public:
Ccylinder(double radiusval,double heightval);
void setheight(double heightval)
{ height=heightval; }
double getheight() const
{ return height; }
返回 82
3.3 继承和派生
double area() const //重新定义 area()函数
{ //此处调用的是基类的成员函数 area(),必须 加::
return 2*Ccircle::area()+2*3.14*radius*height; }
};
Ccylinder::Ccylinder(double radiusval,double heightval):
Ccircle(radiusval)
//调用 Ccircle类的构造函数对 radius初始化
//派生类 Ccylinder的构造函数不但初始化自身定义的成员
height,而且通过调用基类的构造函数 Ccircle( ) 初始化从基类继承来的数据成员 radius
{ height=heightval; }
返回 83
3.3 继承和派生
void main()
{Ccircle circle(10);
Ccylinder cylinder(2,5);
cout<<"圆柱体表面积,"<<cylinder.area()<<endl;
cout<<"圆柱体底面积,"<<cylinder.Ccircle::area()<<endl;
cout<<"圆的面积是,"<<circle.area();
}
程序的执行结果为:
圆柱体表面积,87.92
圆柱体底面积,12.56
圆的面积是,314
返回 84
3.3 继承和派生程序说明:
( 1) 求圆的面积与圆柱体的表面积的方法是不同的 。 因此,在派生类 Ccylinder中重新定义了基类成员函数
area()。 在主函数中,通过基类和派生类的不同对象,
分别调用了这两个 area()函数 。
( 2) 由于继承关系,在类 Ccylinder中存在两个同名的函数 arear()。 其中一个是从基类 Ccircle中继承过来的,
另一个是在派生类 Ccylinder中新定义的 。 这样,当通过派生类对象调用 area()函数时,C++编译器将沿继承关系搜索,使用离调用对象最近的那个版本的函数 。
( 3) 如果确实想通过 Ccylinder的对象访问从基类
Ccircle继承过来的 area()函数,则必须使用作用域运算符,,:”显式指明 。
返回 85
3.3 继承和派生通过以上分析可知,C++中处理同名函数有以下 3种基本方法:
( 1) 根据函数的参数的特征进行区分 。 即编译器根据函数的类型或个数进行区分 。 如:
max(int,int) max(float,float)
( 2) 根据类对象进行区分 。 如:在上例中的 main函数中,
cylinder.area() circle.area()
其中,cyclinder是 Ccylinder的一个对象,circle是 Ccircle
的一个对象 。
( 3) 使用作用域运算符,,:”进行区分,如:
Ccircle::area()
以上三种区分方法都是在程序编译过程中完成的,称为静态联编,除此之外,C++还提供称为动态联编 。
返回 86
3.3 继承和派生
3.3.3 多重继承在单一继承关系中,每个派生类最多只有一个直接基类,但它可以有多个间接基类 。 在 C++中不仅支持单一继承,而且也支持多重继承,所谓 多重继承,是指派生类从多个基类中派生而来,使派生类继承多个基类的特征,
在多重继承关系中,派生类有多个直接基类 。 定义多重继承类的方式如下:
class 派生类名:访问方式 基类名,访问方式 基类名
{ };
其中:访问方式为 public或 private,功能同单一继承 。
多重继承下派生类的构造函数必须同时负责所有基类构返回 87
3.3 继承和派生造函数的调用,对于派生类构造函数的参数个数必须同时满足多个基类初始化的需要 。 所以,在多重继承下,
派生类的构造函数的定义格式如下:
派生类构造函 数名 ( 参 数表 ),基 类名 1(参 数表
1),
{ }
在多重继承下,系统首先执行各基类的构造函数,然后再执行派生类的构造函数,处于同一层次的各基类构造函数的执行顺序与声明派生类时所指定的各基类顺序一致,而与派生类的构造函数定义中所调用基类构造函数的顺序无关 。
返回 88
3.3 继承和派生
【 例 3-11】 测试多重继承关系下,基类和派生类的构造函数的执行顺序 。
#include,iostream.h”
class B1
{protected:
int b1;
public:
B1(int val1)
{ b1=val1;
cout<<”base1 is called,<<endl; }
};
返回 89
3.3 继承和派生
class B2
{ protected:
int b2;
public:
B2(int val2)
{ b2=val2;
cout<<”base2 is called”<<endl; }
};
class D,public B1,public B2
{protected:
int d;
返回 90
3.3 继承和派生
public:
D(int val1,int val2,int val3);
};
D::D(int val1,int val2,int val3):B1(val1),B2(val2)
//如改为 D::D(int val1,int val2,int val3),B2(val2),B1(val1)
效果一样
{ d=val3;
cout<<”erived class is called,;}
void main()
{ D dobj(1,2,3); }
返回 91
3.3 继承和派生该程序的执行结果是:
基类 B1的构造函数被调用基类 B2的构造函数被调用派生类 D的构造函数被调用
3.3.4 虚基类多重继承下,一个派生类可从多个基类派生出来,又由于一个基类可派生出多个派生类,因此可能会产生一个类是通过多条路径从一个给定的类中派生出来的,如图 3.1所示。 B B
D1 D2
D3
图 3.1 多重继承的二义性返回 92
3.3 继承和派生从上图可以看出:派生类 D3中将继承两份类 B的成员,
一份由类 D1派生得到,另一份由 D2派生而来,这时通过派生类 D3的对象访问类 D1和 D2的成员不会有问题,但访问类 B的成员就会出现模棱两可的现象,编译程序不知道到底要访问哪一份的成员,C++为此提供了虚基类,以解决这种二义性 。
虚基类是这样的一个基类:它虽然被一个派生类间接地多次继承,但派生类却只继承一份该基类的成员,这样,避免了在派生类中访问这些成员时产生二义性 。
将一个基类声明为虚基类必须在各派生类定义时,在基类的名称前面加上关键字 virtual,格式如下:
class 派生类名,virtual public 基类名
{ //声明派生类成员 };
返回 93
3.3 继承和派生使用虚基类时,要特别注意派生类的构造函数,对于普通基类,派生类的构造函数负责调用其直接基类的构造函数以初始化其直接基类的数据成员,而对于虚基类的任何派生类,其构造函数不仅负责调用直接基类的构造函数,
还需调用虚基类的构造函数,如图 3.1所示的结构中,若基类 B被声明为虚基类,则派生类 D3负责调用三个基类
( 直接基类 D1,D2和虚基类 B) 的构造函数,而派生类
D1和 D2不会调用虚基类 B的构造函数,只由最终端的派生类 D3负责调用虚基类的构造函数 。 如:
class B //定义类 B
{ protected:
int b;
public:
B(int bval=0)
返回 94
3.3 继承和派生
{ b=bval; }
};
class D1,virtual public B
{protected:
int d1;
public:
D1(int bval,int dval);
};
D1::D1(int bval,int dval):B(bval)
{ d1=dval; }
class D2:virtual public B
{ protected:
int d2;
返回 95
3.3 继承和派生
public:
D2(int bval,int dval ); }
D2::D2(int bval,int dval):B(bval)
{d2=dval; }
class D3:public D1,public D2
{ protected:
int d3;
public:
D3(int bval,int dval1,int dval2,int dval3);
};
D3::D3(int bval,int dval1,int dval2,int dval3):D1(bval,
dval1),D2(bval,dval2),B(bval)
{ d3=dval3; }
返回 96
3.4 虚函数与多态性
3.4.1 多态性多态性 就是指同样的消息被类的不同的对象接收时导致的完全不同的行为的一种现象 。 这里所说的消息即对类成员函数的调用 。
C++支持两种不同类型的多态:一种是编译时的多态,
另一种是运行时的多态 。 在编译时的多态是通过静态联编实现的;而在运行时的多态则是通过动态联编实现的 。
很明显,函数的重载实现了一种多态性;这里要讲的多态性是建立在虚函数的概念和方法基础之上,通过虚函数来实现的,而虚函数又必须存在于继承的环境下 。
利用多态性,用户能够发送一般形式的消息,而将所有的实现细节留给了消息的对象,所以说多态性与数据封装和继承共同构成面向对象程序设计的三大机制 。
返回 97
3.4 虚函数与多态性
3.4.2 子类型
C++中的动态联编是通过虚函数实现的,而要理解虚函数必须首先讨论一个与之相关的概念,即子类型 。
如果一个特定的类型 S,当且仅当它提供了类型 T的行为时,则称类型 S是类型 T的子类型 。 子类型体现了类型间的一般与特殊的关系 。
在 C++中,子类型的概念是通过公有继承 ( 或公有派生 )
来实现的 。
根据继承方式的概念,我们知道,按公有继承的方式产生的派生类中,必然包含了原来基类中的全部成员 。 因此,
一个公有派生类的对象可以提供其基类对象的全部行为
( 基类的全部接口 ),也就是说,在程序中可以把一个公有派生类对象当作其基类对象来处理 。
返回 98
3.4 虚函数与多态性
【 例 3-13】 子类型的概念及实现示例 。
#include <iostream.h>
class A //定义类 A
{private:
int a;
public:
A(int i=0){a=i;}
void print();
};
void A::print ()
{ cout<<"In class A,print() is called."<<endl; }
返回 99
3.4 虚函数与多态性
class B:public A //定义类 B,类 B是类 A的公有派生类
{
private:
int b;
public:
B(int j=-1){b=j;}
};
void commfun(A &aref)
{
aref.print();
}
返回 100
3.4 虚函数与多态性
void main()
{A a;
commfun(a);//以基类 A的对象 a作为实参调用函数 commfun
B b;
commfun(b);//以派生类 B的对象 b调用函数 commfun
}
程序的运行结果为:
In class A,print() is called.
In class A,print() is called.
说明:
返回 101
3.4 虚函数与多态性
( 1 ) 在本例中,类 B 是类 A的公有派生类,函数
commfun()的形参是一个基类 A对象的引用,所以在 main
函数中,把基类 A的对象 a作为实参调用函数 commfun()时,
产生的结果是不言而喻的 。 但在 main函数中,当把类 B的对象 b作为实参调用函数 commfun()时,函数 commfun()仍能正常工作,且打印结果与对象 a作为实参时的结果相同,
这说明,在程序中可以把一个公有派生类对象当作其基类对象来处理 。
( 2) 将类型 B的对象 b传递给函数 commfun()处理是在程序运行时发生的 。 但在程序编译时,编译器只能对源程序代码进行静态检查 。
( 3) 子类型的重要性在于可以减轻程序员编写程序代码的负担 。
返回 102
3.4 虚函数与多态性
3.4.3 用基类指针指向公有派生类对象既然一个公有派生类对象可以当作基类对象使用,那么,
指向基类的指针自然也可以指向其公有派生类对象 。 因此,基类指针,派生类指针,基类对象和派生类对象四者间有以下四种组合的情况:
( 1) 直接用基类指针指向基类对象 。
( 2) 直接用派生类指针指向派生类对象 。
( 3) 用基类指针引用其派生类对象 。
( 4) 用派生类指针引用基类对象 。
由于 ( 1),( 2) 两种情况,指针类型和对象类型统一,
因此完全行得通 。
返回 103
3.4 虚函数与多态性对于第 ( 3) 种情况,由于可以把一个公有派生类对象当作基类对象处理,所以可以用基类指针指向其派生类对象 。 但必须注意的是,由于基类指针本身的类型并没有改变,因此基类指针仅能访问派生类中的基类部分 。 在程序中,当把派生类对象的指针赋给基类指针时,编译器能自动完成隐式类型转换 。
对于第 ( 4) 种情况,将派生类指针直接指向基类对象是危险的,因为编译器不允许这么做,也不提供隐式类型转换 。 当然,程序员如果采用强制类型转换,也可以把基类指针转换为派生类指针,但这时要正确地使用该指针 。
返回 104
3.4 虚函数与多态性
【 例 3-14】 基类指针,派生类指针,基类对象和派生类对象四者间组合的使用情况示例 。
#include <iostream.h>
class A //定义类 A
{private:
int a;
public:
A(int i=1){a=i;}
void print();
int geta();
};
void A::print ()
{ cout<<"a="<<a<<endl; }
返回 105
3.4 虚函数与多态性
int A::geta()
{ return a; }
class B:public A //定义类 B,类 B是类 A的公有派生类
{private:
int b;
public:
B(int j=-1){b=j;}
void print();
};
void B::print ()
{ cout<<"b="<<b<<endl; }
返回 106
3.4 虚函数与多态性
void main()
{A aa(10),*pa;
B bb(20),*pb;
pa=&aa; //基类指针可以指向基类对象
pa->print();
pb=&bb; //派生指针可以指向派生类对象
pb->print();
pa=&bb; //基类指针可以指向派生类对象
cout<<pa->geta()<<endl; //如改为 pa->getb();则错误,
//因为基类指针仅能看到派生类中的基类部分
pa->print();
bb.print();
返回 107
3.4 虚函数与多态性
pb=(B *)pa; //经过强制类型转换,派生类指针也可以
//指向基类对象
//上面语句如改为 pb=pa;则错误,因为派生类指针不可
//以直接指向基类对象 }
程序的运行结果:
a=10
b=20
1
a=1
b=20
返回 108
3.4 虚函数与多态性程序分析:在上例的 main函数中,虽然基类指针 pa指向派生对象 bb( 即,pa=&bb),但语句 pa->print()与语句
bb.print()的输出结果并不相同,从结果来看,前者的输出结果是,a=1”,而后者的输出结果为,b=20” 。 这是由于虽然一个基类指针可以指向其派生类对象,但指针本身的属性并没有改变,因此,系统认为它所指向的仍然是一个基类对象,于是就只能调用其基类的成员函数
print()。 进一步分析发现,在派生类 B中虽然继承了基类
A的成员函数 print(),但为了适应派生类自己的需要,在派生类中已经改变了这个函数的实现,即在派生类中又定义了一个同名的 print()函数,而这种改变在静态联编的条件上编译器并不知道,以致于造成以上结果的不统一 。
所以,必须通知编译器这种可能的改变,即需要进行动态联编 。 其方法就是在基类中将可能发生改变的成员函数声明为虚函数 。
返回 109
3.4 虚函数与多态性
3.4.4虚函数
C++通过虚函数实现了多态性,而虚函数存在于继承环境中,在继承关系下,派生类作为基类的子类,在任何要求基类对象的地方使用派生类对象是有意义的 。
声明虚函数的方法是在基类中的成员函数原型前加上关键字 virtual。 其格式如下:
class 类名
{
virtual 类型 函数名 ( 参数表 ) ;
};
当一个类的成员函数说明为虚函数后,就可以在该类的
( 直接或间接 ) 派生类中定义与其基类虚函数原型相返回 110
3.4 虚函数与多态性同的函数 。 这时,当用基类指针指向这些派生类对象时,
系统会自动用派生类中的同名函数来代替基类中的虚函数 。 也就是说,当用基类指针指向不同派生类对象时,
系统会在程序运行中根据所指向对象的不同,自动选择适当的函数,从而实现了运行时的多态性 。
虚函数可以在一个或多个派生类中被重新定义,因此,
属于函数重载的情况,但这种重载与一般的函数重载是不同的,要求在派生类中重新定义时,必须与基类中的函数原型完全相同,包括函数名,返回类型,参数个数和参数类型的顺序 。 这时无论在派生类的相应成员函数前是否加上关键字 virtual,都将视其为虚函数,如果函数原型不同,只是函数名相同,C++将视其为一般的函数重载,而不是虚函数 。 只有类的成员函数才能声明为虚函数,全局函数及静态成员函数不能声明为虚函数 。
返回 111
3.4 虚函数与多态性
【 例 3-15】 虚函数的定义与应用举例 。
#include,iostream.h”
class Base
{ public:
virtual void show() { cout<<”base class\n”; } };
class Der1,public Base
{ public:
void show() { cout<<”derived class 1 \n”; } };
class Der2,public Base
{ public:
void show() { cout<<”derived class 2”; } };
返回 112
3.4 虚函数与多态性
void main()
{ Base bobj;
Base *p;
Der1 dobj1;
Der2 dobj2;
p=&bobj;
p->show();
p=&dobj1;
p->show();
p=&dobj2;
p->show();
}
返回 113
3.4 虚函数与多态性程序的运行结果,base class
derived class 1
derived class 2
由上例可以看出:
( 1) 通过虚函数实现了运行时的多态性 。
( 2) 基类用虚函数提供了一个派生类对象都具有的共同界面,派生类又各自对虚函数定义自己的具体实现,这样,使得程序既简洁又具有扩充性,并能帮助程序员控制更大的复杂性 。 若派生类中没有重新定义基类的虚函数,则该派生类直接继承其基类的虚函数 。
( 3)当一个函数在基类被声明为虚函数后,不管经历多少层派生,都将保持其虚拟性。
返回 114
3.4 虚函数与多态性
3.4.5 静态联编与动态联编在向对象的程序设计中,联编 的含义是指把一个消息和一个方法联系在一起,也就是把一个函数名与其实现代码联系在一起 。 根据实现联编的阶段的不同,可分为静态联编和动态联编两种 。
静态联编 是在编译阶段进行的 。 而 动态联编 是在程序运行过程中,根据程序运行的需要进行的联编 。
实现静态联编的前提是:在编译阶段就必须能够确定函数名与代码间的对应关系 。 因此,当通过对象名调用成员函数时,只可能是调用对象自身的成员,所以,这种情况可采用静态联编实现 。 但当通过基类指针调用成员函数时,
由于基类指针可以指向该基类的不同派生类对象,因此存在需要动态联编的可能性,但具体是否使用动态联编,还要看所调用的是否是虚函数 。
返回 115
3.4 虚函数与多态性
3.4.6纯虚函数与抽象类纯虚函数 是在基类中只声明虚函数而不给出具体的函数定义体,将它的具体定义放在各派生类中,称此虚函数为纯虚函数 。 通过该基类的指针或引用就可以调用所有派生类的虚函数,基类只是用于继承,仅作为一个接口,具体功能在派生类中实现 。
纯虚函数的声明如下,( 注:要放在基类的定义体中 )
virtual 函数原型 =0;
其中:函数原型的格式同前面所学格式一样,要包括函数返回值的类型,函数名,圆括号,形参及其类型等 。
声明了纯虚函数的类,称为 抽象类 。
使用纯虚函数时应 注意:
( 1) 抽象类中可以有多个纯虚函数 。
返回 116
3.4 虚函数与多态性
( 2) 不能声明抽象类的对象,但可以声明指向抽象类的指针变量和引用变量 。
( 3) 抽象类也可以定义其他非纯虚函数 。
( 4) 如果派生类中没有重新定义基类中的纯虚函数,则在派生类中必须再将该虚函数声明为纯虚函数 。
( 5) 从抽象类可以派生出具体或抽象类,但不能从具体类派生出抽象类 。
( 6) 在一个复杂的类继承结构中,越上层的类抽象程度越高,有时甚至无法给出某些成员函数的实现,显然,抽象类是一种特殊的类,它一般处于类继承结构的较外层 。
( 7) 引入抽象类的目的,主要是为了能将相关类组织在一个类继承结构中,并通过抽象类来为这些相关类提供统一的操作接口 。
返回 117
3.4 虚函数与多态性
【 例 3-16】 设计一个抽象类 shape,它表示具有形状的东西,体现了抽象的概念,在它下面可以派生出多种具体形状,比如三角形、矩形。
#include<iostream.h>
class Shape
{ protected:
double x,y;
public:
void set(double i,double j)
{ x=i; y=j; }
virtual void area()=0; //声明纯虚函数
};
返回 118
3.4 虚函数与多态性
class Triangle,public Shape
{ public:
void area()
{ cout<< "三角形面积," <<0.5*x*y<<endl; ; }
};
class Rectangle,public Shape
{ public:
void area()
{ cout<<"矩形面积," <<x*y<<endl; ; }
};
返回 119
3.4 虚函数与多态性
void main()
{ Shape *p;
Triangle t;
Rectangle r;
p=&t;
p->set(5.1,10);
p->area();
p=&r;
p->set(5.1,10);
p->area(); }
结果:三角形面积,25.5
矩形面积,51
返回 120
3.5 静态成员
C++还有一种数据成员,称作,静态,成员,静态成员是所有对象公有的 。 静态成员有静态数据成员和静态函数成员之分 。
3.5.1 静态数据成员说明静态数据成员的语句格式是:
static 类型说明符 成员名;
【 例 3-17】 报名登记处登记每一位来访者的姓名,同时使用静态数据成员 account自动产生一个流水号数,记入
number中 。
返回 121
3.5 静态成员
# include <windows.h>
# include <iostream.h>
//定义类 married
class married
{private:
int number; //编号
char *name; //姓名
public:
static int glob; // 定义静态数据成员 glob
void set_mes (char *a); // set_mes函数说明
} ;
返回 122
3.5 静态成员
// set_mes函数定义
void married,,set_mes (char *a)
{name = new char[strlen(a) + 1] ;
strcpy (name,a) ; //用参数 a的值修改私有变量 。
number=++glob; //glob加班后赋给 number
cout << " 编号,"<<number<<endl;
}
int married,,glob= 0 ; //静态变量赋初始值 0
// 主函数
void main ()
{ // 生成对象数组 person
married person[100];
返回 123
3.5 静态成员
int i ; // 局部变量 i
char str[8] ; // 局部变量 str
cout<<endl;
for ( i=0; i<100; i++) // 循环 100次
{ //读入姓名,存于 str
cout << " 输入姓名,"; cin >> str ;
person[i].set_mes ( str ) ; //保存并显示 }
cout<<endl;
}
说明:
(1) 不管一个类的对象有多少个,其静态数据成员也只有一个,由这些对象所共享,可被任何一个对象所访问 。
返回 124
3.5 静态成员
(2) 在一个类的对象空间内,不包含静态成员的空间,所以静态成员所占空间不会随着对象的产生而分配,或随着对象的消失而回收 。
(3) 静态数据成员的存储空间的分配是在程序一开始运行时就被分配 。 并不是在程序运行过程中在某一函数内分配空间和初始化 。
(4) 静态数据成员的赋值语句,既不属于任何类,也不属于包括主函数在内的任何函数,静态变量赋初值语句应当写在程序的全局区域中,并且必须指明其数据类型与所属的类名,并用如下格式:
类型 类名::变量名 =值;
如:上例中的,int visited::glob=0;
返回 125
3.5 静态成员
(5) 对于在类的 public部分说明的静态数据成员,可以不使用成员函数而直接访问,即使未定义类的对象,同样也可以直接访问,但在使用时也必须用类名指明所属的类,如在上例中的 glob数据成员,可以在 main函数体中直接访问,cout<<visited::glob;而 private和 protected部分的静态成员只能通过类的成员函数访问 。
3.5.2 静态成员函数静态成员函数的定义:
static 类型 函数名 ( 形参 )
{函数体 }
与静态数据成员一样,静态成员函数与类相联系,不与类的对象相联系,所以访问静态成员函数时,不需要对象 。 如:
返回 126
3.5 静态成员
#include,iostream.h”
class objcount
{ private:
static int count;
public:
objcount() { count++; }
static int get() { return count; }
};
int objcount::count=0;
void main()
{cout<<objcount::get();
objcount a,b,c,d,e,f;
返回 127
3.5 静态成员
cout<<objcount::get();
count<<a.get();
}
一个静态成员函数不与任何对象相联系,所以它不能对非静态成员进行默认访问 。 如:
#include,iostream.h”
class student
{ public:
static char *sname()
{ cout << noofstudent<<endl;
return name; }
返回 128
3.5 静态成员
protected:
char name[40];
static int noofstudent;
};
int student::noofstudent=0;
void fn()
{ student s;
cout<<s.sname()<<endl; //error
//……
}
返回 129
3.6 友元函数与友元类
3.6.1 友元函数
C++提供一种允许外部类和函数存取类的私有成员和保护成员的辅助方法,即将它们声明为一个给定类的友元
(或友元函数),使其具有类成员函数的访问权限。但友元本身不是类的成员,它不属于任何类。
对于使用友元函数,有以下几点 说明:
(1) 为了在类的定义中对友元加以声明,只需在友元的名称前加上关键字 friend即可 。
(2) 友元函数是能访问类的所有成员的普通函数,一个函数可以是多个类的友元函数,只需在各个类中分别声明 。
返回 130
3.6 友元函数与友元类
(3) 将一个函数声明为某个类的友元函数的方法是在该类定义里提供一个以关键字 friend开头的函数原型,友元函数的定义,可以在类的内部或外部,友元函数虽然是在类内进行声明,但它不是该类的成员函数,不属于任何类,在类外定义友元函数时,与普通函数的定义一样,
不应在函数名前用类名加以限制,因此,友元函数不象成员函数那样在调用时使用对象名,友元函数要对类的成员进行访问,必须在参数表中显式指明要访问的对象。
(4) 一个类的友员函数与该类的类内成员函数一样,享有对该类一切成员的访问权 。
(5) 友元函数的调用与一般函数的调用方式和原理一致 。
(6) C++不允许将构造函数、析构函数和虚函数声明为友元函数。
返回 131
3.6 友元函数与友元类
【 例 3-18】 友元函数的定义和使用方法 。
#include,iostream.h”
class X1()
{ private:
int x;
public:
X1(int i) { x=i; }
int getx();
friend void sum(X1 &a,X2 &b); //声明友元函数
};
int X1::getx()
{ return x;}
返回 132
3.6 友元函数与友元类
void sum(X1 &a,X1 &b) //定义函数
{cout<<"用友元函数求各和,"<<a.x+b.x<<endl;
//可以访问类的私有成员 }
void sum1(X1 &a,X1 &b) //定义普通函数
{cout<< "用普通函数调用类公共接口函数求和,"
<<a.getx()+b.getx()<<endl;
}
void main()
{ X1 m(1);
X1 n(2);
sum(m,n);
sum1(m,n);
}
返回 133
3.6 友元函数与友元类程序的执行结果为:
用友元函数求各和,3
用普通函数调用类公共接口函数求和,3
返回 134
3.6 友元函数与友元类
3.6.2 友元类友元类是在多个类之间建立一种访问机制,当程序中定义了两个或两个以上的类时,如果一个类将自己说明为另一个类的友员类,其成员就可以被该类使用 。
定义友元类的语句格式为:
friend class 类名;
其中,friend 和 class 是关键字,类名必须是程序中的一个已定义过的类 。
当希望一个类可以存取另一个类的私有成员时,可以将该类声明为另一类的友元,友元类的所有成员函数都可视为该类的友元函数,能存取该类的私有成员和保护成员 。
返回 135
3.6 友元函数与友元类如:将类 B声明为类 A的友元类的方法是在 A类的定义中加上说明:
friend class B;
注意,友元关系不具有对称性 。 此外,友元关系不具有传递性,如果类 B是类 A的友元类,类 C是类 B的友元类,
这并不隐含类 C是类 A的友元类 。
【 例 3-8】 有两个类 Cla_1和 Cla_2。在类 Cla_1的定义中说明了友员类 Cla_2。主函数中用它们生成了两个对象,然后对这两个对象的私有成员进行访问。
返回 136
3.6 友元函数与友元类
#include <iostream.h>
#include <windows.h>
#include <stdio.h>
//定义类 Cla_1
class Cla_1
{ private:
friend class Cla_2; //说明友员类 Cla_2
char *name;
int age;
public:
Cla_1( char *str,int i );
};
返回 137
3.6 友元函数与友元类
//Cla_1的构造函数定义
Cla_1::Cla_1(char *str,int i)
{ name=str; //为类 Cla_1的成员赋值
age=i; }
//定义类 Cla_2
class Cla_2 //类 Cla_2中并没有说明 Cla_1友员类
{ public:
void show(Cla_1 x);
} ;
返回 138
3.6 友元函数与友元类
// Cla_2的 show函数定义
void Cla_2::show(Cla_1 x)
{ cout<<"\n\n 姓名,"<<x.name<<endl;
cout<<"\n 年龄,"<<x.age<<endl<<endl; }
// 主函数
void main()
{ //生成对象 obj1,定义友员类 Cla_2
Cla_1 obj1("李小丽 ",30);
Cla_2 obj2; //生成对象 obj2
obj2.show(obj1); //调用 obj2.show,去访问 obj1的成员
}
返回 139
3.7 堆对象和对象数组
3.7.1 堆对象我们知道,用 new和 delete可以动态地分配堆内存或释放堆内存。在此可利用 new建立对象(会自动调用构造函数),利用 delete可删除对象(会自动调用析构函数)。
class Tdate
{public:
Tdate(int m,int d,int y);
protected:
int month;
int day;
int year;
};
返回 140
3.7 堆对象和对象数组
Tdate::Tdate()
{if (m>0 && m<13)
month=m;
if (d>1 && d<32)
day=d;
if (y>0 && y<300)
year=y; }
void main()
{ Tdate *pd;
pd=new Tdate(1,1,1998);
//……
delete(pd);}
返回 141
3.7 堆对象和对象数组说明:
( 1) 堆对象的生存期是整个程序的生命期,所以只有程序运行结束时,堆对象才被删除 。 这一点与一般的局部对象的生命期不同,局部对象的生存期开始于函数体的执行,
而终止于函数体执行结束 。
( 2) 堆对象用 delete来释放 。
返回 142
3.7 堆对象和对象数组
3.7.2 对象数组一个数组的类型除了可以为基本的数据类型外,还可以为类类型,则这时,该数组中的每个元素都是该类中的一个对象,则这种数组就是对象数组 。
对象数组的定义方式:
类名 数组名 [数组大小 ];
【 例 3-9】 类 person是说明人员的信息结构 。 用 person生成一个对象数组 emp[5],通过 assignment将人员信息填入,
然后将它们显示出来 。
# include <windows.h>
# include <iostream.h>
返回 143
3.7 堆对象和对象数组
class person
{private:
char *name; //name指向信息串
int age; //年龄
public:
person(); // 构造函数说明
~person(); // 析构函数说明
void assignment(char *a,int b); // assignment函数说明
void show (); // show函数说明
};
// 构造函数定义
person,,person()
返回 144
3.7 堆对象和对象数组
{ name=new char('\0');// 为 name申请存储空间,臵为空
age=-1; // 为 age 赋初始值
}
// 析构函数定义
person,,~person()
{ delete [ ] name; // 回收 name空间 }
// assignment函数定义
void person,,assignment(char *a,int b)
{ name = new char[strlen(a) + 1];
strcpy(name,a); // 用参数 a的值修改 name
age=b;
};
返回 145
3.7 堆对象和对象数组
// show函数定义
void person,,show ()
{ cout << "\n 姓名," << name << " 年龄," << age ; };
void main ()
{ // 生成对象数组 emp[5]
person emp[5];
// 给对象数组赋值
emp[0].assignment("张立三 ",25);
emp[1].assignment("王冠之 ",28);
emp[2].assignment("王大成 ",35);
emp[3].assignment("英乐乐 ",21);
emp[4].assignment(,胡忠厚,,26);
返回 146
3.7 堆对象和对象数组
// 显示 emp
int i;
for (i=0; i<5; i++)
{ emp[i].show(); }
cout <<endl<<endl;
}
3.8 运算符的重载运算符的重载与函数重载的方法一致,是一种特殊的函数重载 。
使用运算符重载的一般格式为:
类型名 operator@( 形参表 )
其中,operator是关键字,@是运算符 。
返回 147
3.8 运算符的重载
【 例 】 运算符重载举例 。
class Cint
{public:
friend Cint operator+(int a,int b);//,+” ( 加号 ) 被重
//载为友元函数
Cint operator-(); //,-” ( 负号 ) 被重载为成员函数,
}
Cint Cint::operator+(int a,int b)
{ Cint t;
t=a+b;
return t;}
返回 148
3.8 运算符的重载
Cint Cint::operator-()
{ Cint t;
t=-t;
return t;
}
说明:
(1) 在 C++ 中几乎所有的运算符 ( 除,,”,,,*”,
,::,,,?,,外 ) 都可以被重载 。
(2) 运算符的重载既不会改变原运算符的优先级和结合性,
也不会改变使用运算符的语法和参数个数 。
(3) 由于重载后的运算符函数经常需要访问类的私有成员,
因此运算符函数 operator@()通常被声明为类的成员函数或友元函数。其等价的函数调用形式,如表 3.2所示。
返回 149
3.8 运算符的重载表 3.2 运算符表达式及其等价的函数调用形式
(4) 当重载为类的成员函数时,运算符重载函数的形参个数要比运算符操作数个数少一个;若重载为友元函数,
则参数个数与操作数个数相同 。
(5) 当重载为友元函数时,,=”,,(),,,[]”,
,->” 等运算符不能重载。
表达式 友元函数调用 成员函数调用
a+b operator+(a,b) a.operator+(b)
a++ operator++(a,0) a.operator++(0)
-a operator-(a) a.operator-( )
返回 150
3.9 模板与使用模板就是使程序能够对不同类型的数据进行相同方式的处理 。 C++中的模板分为类模板和函数模板 。
3.9.1 类模板说明类模板的一般格式为:
template <类型参数表 > class 类模板名
{ private:
私有成员定义
protected:
保护成员定义
public:
公有成员定义 };
其中,(1)类型形式参数表可以包含基本数据类型,也可返回 151
3.9 模板与使用以包含类类型,如果是类类型,则须加前缀 class。 当参数有多个时,需用逗号隔开 。
(2)类模板中的成员函数的定义,可以放在类模板的定义体中 ( 此时与类中的成员函数的定义方法一致 ),也可以放在类模板的外部定义成员函数,此时成员函数的定义格式如下:
template <类型形式参数表 >
函数值的返回类型 类模板名 <类型名表 >::成员函数
( 形参 )
{ 函数体 }
其中:类模板名即是类模板中定义的名称;类型名表即是类模板定义中的类型形式参数表中的参数名 。
返回 152
3.9 模板与使用
(3)利用类模板定义的只是对类的描述,它本身还不是一个实实在在的类 。 因此是类模板 。
(4)要定义类模板的对象 ( 即实例 ),需要用下列格式的语句:
类模板名 <类型实际参数表 > 对象名;
【 例 3-22】 定义类模板 ABC,内含成员函数 set和 get。用
ABC生成对象 abc1和 abc2。它们的数组元素数不同,显示的结果也不同。
# include <iostream.h>
// 定义类模板 ABC
template <class T,int I> class ABC
{ private:
T array [I] ; // 定义数组 array
返回 153
3.9 模板与使用
public:
void set (int x) // 定义成员函数 set
{ int i;
for (i=0; i<I; i++) //循环 I次
array[i]=x+i; //数组元素赋值
}
void get () //定义成员函数 get
{ cout <<"\n 数组元素总数为,"<< I<<endl;
cout <<" array["<<I-1<<"]="<<array[I-1]<<endl; }
};
void main()
{ //由模板 ABC生成对象 abc1
返回 154
3.9 模板与使用
ABC <int,50> abc1;
abc1.set(0); //调用对象 abc1.set
abc1.get(); //调用对象 abc1.get
//由模板 ABC生成对象 abc2
ABC <int,100> abc2;
abc2.set(10); //调用对象 abc2.set
abc2.get(); //调用对象 abc2.get
}
以下是对上例中的成员函数定义体放于类模板外部定义的示例。
【 例 3-23】 定义模板 ABC,内含成员函数 set和 get。用
ABC生成对象 abc1和 abc2。它们的数组元素数不同,显示的结果也不同。
返回 155
3.9 模板与使用
#include <iostream.h>
// 定义类模板 ABC
template <class T,int I> class ABC
{ private:
T array [I] ; // 定义数组 array
public:
void set (int x); // 定义成员函数 set
void get () ; //定义成员函数 get
};
template <class T,int I>
void ABC<T,I>,:set (int x) // 定义成员函数 set
{ int i;
返回 156
3.9 模板与使用
for (i=0; i<I; i++) //循环 I次
{ array[i]=x+i; } //数组元素赋值
}
template <class T,int I>
void ABC<T,I>::get()
{ cout <<"\n 数组元素总数为,"<< I<<endl;
cout <<" array["<<I-1<<"]="<<array[I-1]<<endl; }
void main()
{ //由模板 ABC生成对象 abc1
ABC <int,50> abc1;
abc1.set(0); //调用对象 abc1.set
abc1.get(); //调用对象 abc1.get
返回 157
3.9 模板与使用
//由模板 ABC生成对象 abc2
ABC <int,100> abc2;
abc2.set(10); //调用对象 abc2.set
abc2.get(); //调用对象 abc2.get
}
类模板的使用方法可以总结为:
(1) 给出类模板的定义体 。
(2)在适当的位臵创建一个类模板的实例,即一个实实在在的类定义,同时创建该模板类的对象 。
(3)有了对象名,以后的使用就和普通类的对象是一致的 。
返回 158
3.9 模板与使用
3.9.2 函数模板函数模板是函数的一种抽象形式 。 由于 C++中的数据类型很多,针对某种类型设计的函数显然不能用于其它类型,因此对于同一个功能的要求,不得不编制多个相似的程序,比如求两个量中最小值的函数,相似的函数可能有:
(1)求两个整型数据的最小值,int min(int x,int y)
(2)求两个单精度数据的最小值,float min(float x,float y)
(3)求两个双精度数据的最小值,double min(double x,
double y)
为了解决以上的麻烦,C++引入了函数模板的概念 。 函数模板就是用来解决一个模板生成多个函数的问题 。
返回 159
3.9 模板与使用定义函数模板的格式为:
template <类型形式参数表 >
函数返回值类型名 函数模板名 ( 函数形参及类型 )
{函数体 }
如:两个数最小值的函数模板定义如下:
template <class T> T min( T x,T y)
{ return x<y?x:y; }
有了以上函数模板,则下列语句都是正确的 。
a=min(20,10);
b=min(-5.43,50.23);
c=min(?A?,?a?);
返回 160
文件及其操作一、文件的概念和分类文件是指存储在存储介质上的数据的集合。 C++将文件看作是由一个一个字符(字节)的数据顺序组成的。
按照文件中数据的存放形式可以将文件分为,ASCII文件和二进制文件。
二、文件的读写与文件指针
,文件指针,是指表示读写文件的文件位置指示器 。 一个文件指针总是和一个文件相关联,当文件每一次打开时,文件指针都指向文件的开始,随着对文件进行操作,文件指针不断地在文件中移动,并一直指向最新处理的字符 ( 字节 ) 位置 。
对文件的读写操作方式有两种方式:顺序文件操作和随机文件操作 。
返回 161
与文件处理相关的类及其继承关系结构图
ios
istream ostream
ifstream iostream ofstream
fstream
返回 162
文件操作包括打开文件,读写文件和关闭文件 3
个步骤 。
文件的打开和关闭是通过使用 fstream类的成员函数 open和 close来实现的。 fstream类的头文件是 fstream.h
返回 163
1,打开文件打开文件应使用成员函数 open(),该成员函数的函数原型为:
void open(const unsigned char *filename,int mode,int
access=filebuf::openprot);
其中,filename是一个字符型指针,指定了要打开的文件名; mode指定文件的打开方式,见下表。 access指定了文件的系统属性,其取值为:
0 0 一般文件
1 1 只读文件
2 2 隐藏文件
3 3 系统文件返回 164
在 ios类中定义的文件打开方式文件打开方式 含 义
ios::in 以输入 ( 读 ) 方式打开文件
ios::out 以输出 ( 写 ) 方式打开文件
ios::app 打开一个文件使新的内容始终添加在文件的末尾
ios::ate 打开一个文件使新的内容添加在文件尾,但下一次添加时,写在当前位置处
ios::trunc 若文件存在,则清除文件所有内容;若文件不存在,
则创建新文件
ios::binary 以二进制方式打开文件,缺省时以文本方式打开文件
ios::nocreat 打开一个已有文件,若该文件不存在,则打开失败
ios::noreplace 若打开的文件已经存在,则打开失败返回 165
几点说明:
( 1) 在实际使用过程中,可根据需要将以上打开文件的方式用,|”组合起来 。 如:
ios::in|ios::out表示以读 /写方式打开文件
ios::in|ios::binary表示以二进制读方式打开文件
ios::out|ios::binary表示以二进制写方式打开文件
ios::in|ios::out|ios::binary表示以二进制读 /写方式打开文件
( 2) 如果未指明以二进制方式打开文件,则默认是以文本方式打开文件 。
返回 166
2,关闭文件在文件操作结束时应及时调用成员函数 close()来关闭文件 。 如:要关闭的文件对象名为 myfile,
则可使用如下语句关闭文件:
myfile.close();
返回 167
3,文件的读写在打开文件后就可以对文件进行读写操作了。从一个文件中读出数据,可以使用 iostream类的
get,getline,read成员函数以及运算符,>>”;
而向一个文件写入数据,可以使用其 put,write
函数以及插入符,<<”。
返回 168
iostream的文件操作常用的函数函数原型 说明
get(char &ch) 从文件中读取一个字符
getline(char *pch,int
count,chardelim=?\n?)
从文件中读取多个字符,读取个数由参数 count决定,参数 delim是读取字符时指定的结束符
read(char *pch,int count) 从文件中读取多个字符,读取个数由参数 count决定
put(char ch) 向文件写入一个字符
write (const char *pch,int
count)
向文件写入多个字符,字符个数由
count决定返回 169
从文件的第一个字符(字节)开始顺序地处理到文件的最后一个字符(字节),这种操作方式只能从文件的开始处依次顺序读写文件内容,
而不能任意读写文件内容。
【 例 3-24】 向文本文件中分别写入一个整数和一个字符串,然后再以读方式打开该文件,从中读取相应的信息并显示到屏幕上 。
顺序文件的操作返回 170
随机文件操作随机文件操作,即在文件中通过 C++相关的函数移动文件指针,并指向所要处理的字符(字节)。
( 1)在 istream类中提供了 3个操作读指针的成员函数:
istream&istream::seekg (long pos);
istream&istream::seekg(long off,dir);
streampos istream::tellg();
其中,pos为文件指针的绝对位置; off为文件指针的相对偏移量; dir为文件指针的参照位置,其值可能为:
ios::cur文件指针的当前位置
ios::beg 文件开头
ios::end 文件尾
tellg()函数没有参数,它返回一个 long型值,用来表示从文件开始处到当前指针位置之间的字节数 。
返回 171
( 2) 在 ostream类中同样提供了 3个操作写指针的成员函数:
ostream&istream::seekp (long pos);
ostream&istream::seekp(long off,dir);
streampos istream::tellp();
这 3个成员函数的含义与前面 3个操作读指针成员函数的含义相同,只不过它们是用来操作写指针的。
【 例 3-25】 随机文件的读写操作。