590 likes | 730 Views
第三章 关于类和对象的进一步讨论. 3.1 构造函数. 构造函数和析构函数是在类体中说明的两种 特殊的成员函数 。 构造函数是在创建对象时,使用给定的值来将对象初始化。 析构函数的功能正好相反,是在系统释放对象前,对对象做一些善后工作. 类与对象. 基本数据类型与变量. 一般与特殊. 新课导入. 每个对象区别于其他对象的两个方面 外:对象名称 内:对象自身的属性值,即数据成员的值 在声明对象时进行数据成员的设置(给数据成员赋初值),称为 对象的初始化。(构造函数) 在特定对象使用结束时,需要进行清理工作( 析构函数 ).
E N D
3.1 构造函数 构造函数和析构函数是在类体中说明的两种特殊的成员函数。 • 构造函数是在创建对象时,使用给定的值来将对象初始化。 • 析构函数的功能正好相反,是在系统释放对象前,对对象做一些善后工作
类与对象 基本数据类型与变量 一般与特殊 新课导入 • 每个对象区别于其他对象的两个方面 • 外:对象名称 • 内:对象自身的属性值,即数据成员的值 • 在声明对象时进行数据成员的设置(给数据成员赋初值),称为对象的初始化。(构造函数) • 在特定对象使用结束时,需要进行清理工作(析构函数)
3.1.1 为什么需要构造函数初始化对象? • 同一类,N个对象。为每个对象分配空间存放数据成员的值,不单独为每个对象分配函数成员空间。 • 在声明类时,类的数据成员不能直接赋值,进行初始化。例:教材P69 • 但是,声明对象时必须对其数据成员赋值(初始化),否则值是不可预知的,因为被分配的内存中保留了前状。
构造函数 • 构造函数的作用:是在对象被创建时使用特定的值构造对象,或者说将对象初始化为一个特定的状态。 构造函数也是类的一个成员函数,但有特殊性质: 1、构造函数在对象创建时,由系统自动调用 2、构造函数的函数名与类名相同,且没有返回值 3、构造函数通常被声明为public 4、若没有显式声明构造函数,编译器自动生成默认形式的构造函数(无参数,无功能)
3.1.2 构造函数的作用 • 构造函数是类的一种特殊的成员函数,构造函数的主要作用是完成初始化对象的数据成员以及其它的初始化工作。在建立对象时自动执行,不需要用户显式调用。 • 系统约定构造函数名必须与类名相同。 • 构造函数没有返回值。所以,不能指定函数返回值的类型,也不能指定为void类型。 • 一个类可以定义若干个构造函数。当定义多个构造函数时,同时必须满足函数重载的原则。即:构造函数可以带参数、可以重载,
3.1.2 构造函数的作用 • 构造函数的写法:P70例3.1 • 可以与普通成员函数一样,在类内声明构造函数,在类外定义。 • 在构造函数的函数体内(定义时),对对象的数据成员赋初值。符合通过public型的函数成员访问private型的数据成员的规则。 • 构造函数的使用: • P71 main()函数体。 • 注意对象t2:自动调用构造函数。
构造函数举例 定义了构造函数,所以编译系统就不会再为其生成默认构造函数 class Clock { public: Clock(int NewH,int NewM,int NewS);//构造函数 void SetTime(int NewH,int NewM,int NewS); void ShowTime(); private: int Hour,Minute,Second; };
构造函数的实现: Clock::Clock(int NewH, int NewM, int NewS) { Hour= NewH; Minute= NewM; Second= NewS; } 建立对象时构造函数的作用: int main() { Clock c(0,0,0); //隐含调用构造函数,将初始值作为实参。 c.ShowTime(); } Clock c2;语法错误,没有给出必要的实参 9
有关构造函数使用说明(p71) • 第一条:何时?在对象的生命周期开始时,系统自动调用构造函数;在生命结束时,自动调用析构函数。 • 对象(类的对象或变量)从产生到结束的这段时间就是它的生存期。在对象生存期内,对象将保持它的状态(数据成员的值),直到被更新为止。 • 第四条:一般不提倡在构造函数中加入与初始化无关的内容。 • 第五条:总存在构造函数
小结及导入 • 构造函数是类的成员函数,可以直接访问类的所有数据成员,可以是内联函数,可以带有参数表,可以带默认的形参值,也可以重载。 • 对一个类,可以建立多个对象,但注意对象所占据的内存控件只是用于存放数据成员,函数成员不在每一个对象中存储副本。
3.1.3 带参数的构造函数 • 前:例3.1,构造函数无形参,在函数体中对各数据成员赋初值。该类的每个对象都得到同一组初值。 • 现:对不同的对象赋予不同的初值。 • 书写形式: • 例3.2 • 3.1.4 用参数初始化表对数据成员初始化。方便&简练 • 结论: • 带参数的构造函数中的形参,其对应的实参在定义对象时给定。 • 用这种方法可以对不同对象进行不同的初始化。
class A{ float x,y; public: A(float a,float b){ x=a; y=b;}//构造函数,初始化对象 float Sum(void) { return x+y; } void Set(float a,float b) { x=a; y=b;} Print(void) { cout<<"x="<<x<<'\t'<<"y="<<y<<endl;} }; void main(void) { A a1(2.0, 3.0);//定义时调用构造函数初始化 A a2(1.0,2.0); a2.Set(10.0, 20.0); //利用成员函数重新为对象赋值 a1.Print(); a2.Print(); }
3.1.5 构造函数的重载 • P73:第一章中的函数重载知识也适用于构造函数 • 例3.3 • 编译系统根据函数调用的形式去确定对应的函数。 • 默认构造函数:不必给出实参的构造函数。如:用户定义的无参构造函数,系统自动给出的构造函数,全部参数都指定了默认值的构造函数。 • 其他说明:p75
重载的构造函数 重载构造函数,同名但形参类型或个数不同 class Clock { public: Clock(int NewH,int NewM,int NewS); Clock() {Hour=0;Minute=0;Second=0;} void SetTime(int NewH,int NewM,int NewS); void ShowTime(); private: int Hour,Minute,Second; }; Int main( ) { Clock c1(0,0,0); //系统自动调用带有参数的构造函数 Clock c2; //系统自动调用无参数的构造 }
3.1.6 使用默认参数的构造函数 • 构造函数中参数的值既可以通过实参传递,也可以指定为某些默认值。 • 例3.4 • 优点:P77 • 说明:P77,4项
3.2 析构函数 析构函数的作用与构造函数正好相反,是在对象的生命期结束时,释放系统为对象所分配的空间,即要撤消一个对象。 析构函数也是类的成员函数,定义析构函数的格式为: ClassName::~ClassName( ) { ...... // 函数体; }
析构函数 • 完成对象被删除前的一些清理工作。 • 在对象的生存期结束的时刻系统自动调用它,然后再释放此对象所属的空间。 • 如果程序中未声明析构函数,编译器将自动产生一个默认的析构函数。
构造函数和析构函数举例 #include<iostream> using namespace std; class Point { public: Point(int xx,int yy); ~Point(); //...其他函数原型 private: int X,int Y; };
Point::Point(int xx,int yy) { X=xx; Y=yy; } Point::~Point() { } //...其他函数的实现略 例:3.5,P79 20
析构 函数的特点如下: 1、析构函数是成员函数,函数体可写在类体内,也可写在类体外。 2、析构函数是一个特殊的成员函数,函数名必须与类名相同,并在其前面加上字符“~”,以便和构造函数名相区别。 3、析构函数不能带有任何参数,不能有返回值,不指定函数类型。
4、一个类中,只能定义一个析构函数,析构函数不允许重载。4、一个类中,只能定义一个析构函数,析构函数不允许重载。 5、析构函数是在撤消对象时由系统自动调用的。 在程序的执行过程中,当遇到某一对象的生存期结束时,系统自动调用析构函数,然后再收回为对象分配的存储空间。
class A{ float x,y; public: A(float a,float b) { x=a;y=b;cout<<"调用非缺省的构造函数\n";} A() { x=0; y=0; cout<<"调用缺省的构造函数\n" ;} ~A() { cout<<"调用析构函数\n";} void Print(void) { cout<<x<<'\t'<<y<<endl; } }; void main(void) { A a1; A a2(3.0,30.0); cout<<"退出主函数\n"; } 调用缺省的构造函数 调用非缺省的构造函数 退出主函数 调用析构函数 调用析构函数
补充材料:对象的生存期 • 对象(类的对象或变量)从产生到结束的这段时间就是它的生存期。在对象生存期内,对象将保持它的状态(数据成员的值),直到被更新为止。 • 分类:静态生存期、动态生存期
静态生存期 • 静态生存期与程序的运行期相同。 • 两种情况下对象具有静态生存期: • 在文件作用域中声明的对象具有这种生存期。(全局) • 在函数内部声明静态生存期对象,要冠以关键字static 。 • static • 在内存中是以固定地址存放的,在整个程序运行期间都有效。 • static int I; • 注意与作用域和可见性的区别(例:全局寿命,局部可见)
例 #include<iostream> using namespace std; int i=5; //文件作用域 int main() { cout<<"i="<<i<<endl; return 0; } i具有静态生存期,在文件作用域,全局寿命
动态生存期 • 块作用域中声明的,没有用static修饰的对象是动态生存期的对象(习惯称局部生存期对象)。 • 开始于程序执行到声明点时,结束于命名该标识符的作用域结束处。
例 #include<iostream> using namespace std; void fun(); void main() { fun(); fun(); } void fun() { static int a=1; int i=5; a++; i++; cout<<"i="<<i<<",a="<<a<<endl; } 运行结果: i=6, a=2 i=6, a=3 i是动态生存期 a是静态生存期
例5-2 变量的生存期与可见性 #include<iostream> using namespace std; int i=1; // i 为全局变量,具有静态生存期。 void main(void) { static int a; // 静态局部变量,有全局寿命,局部可见。 int b=-10; // b, c为局部变量,具有动态生存期。 int c=0; void other(void); //声明函数 cout<<"---MAIN---\n"; cout<<" i: "<<i<<" a: "<<a<<" b: "<<b<<" c: "<<c<<endl; c=c+8; other(); cout<<"---MAIN---\n"; cout<<" i: "<<i<<" a: "<<a<<" b: "<<b<<" c: "<<c<<endl; i=i+10; other(); }
void other(void) { static inta=2; static intb; // a,b为静态局部变量,具有全局寿命,局部可见。 //只第一次进入函数时被初始化。 int c=10; // C为局部变量,具有动态生存期, //每次进入函数时都初始化。 a=a+2; i=i+32; c=c+5; cout<<"---OTHER---\n"; cout<<" i: "<<i<<" a: "<<a<<" b: "<<b<<" c: "<<c<<endl; b=a; } 17
运行结果: ---MAIN--- i: 1a: 0 b: -10 c: 0 ---OTHER--- i: 33a: 4 b: 0 c: 15 ---MAIN--- i: 33a: 0 b: -10 c: 8 ---OTHER--- i: 75a: 6 b: 4 c: 15 18
例5-3具有静态、动态生存期对象的时钟程序 #include<iostream> using namespace std; class Clock //时钟类声明 {public: //外部接口 Clock(); void SetTime(int NewH, int NewM, int NewS); //三个形参均具有函数原型作用域 void ShowTime(); ~Clock(){} private: //私有数据成员 int Hour,Minute,Second; };
//时钟类成员函数实现 Clock::Clock() //构造函数 { Hour=0; Minute=0; Second=0; } void Clock::SetTime(int NewH, int NewM, int NewS) { Hour=NewH; Minute=NewM; Second=NewS; } void Clock::ShowTime() { cout<<Hour<<":"<<Minute<<":"<<Second<<endl; } 20
ClockglobClock;//声明对象globClock, //具有静态生存期,文件作用域 void main() //主函数 { cout<<"First time output:"<<endl; //引用具有文件作用域的对象: globClock.ShowTime();//对象的成员函数具有类作用域 globClock.SetTime(8,30,30); Clock myClock(globClock); //声明具有块作用域的对象myClock cout<<"Second time output:"<<endl; myClock.ShowTime();//引用具有块作用域的对象 } 21
程序的运行结果为: First time output: 0:0:0 Second time output: 8:30:30 22
3.3 调用构造函数和析构函数的顺序 • 先构造的后析构,后构造的先析构 • 对象在生命周期开始时,调用构造函数;生命周期结束时,调用析构函数。 • 因此,很多时候,当函数多次被调用时,在函数体内建立的对象,都要调用构造函数。
3.4 对象数组 • 数组不仅可以由简单变量组成,也可以由对象组成。 • 如建立Student类,全班50个学生就是这个类的50个对象。每个学生包括: • 属性:姓名、年龄、性别、成绩 • 行为:选课、交费、考试
数组的初始化 • 数组的初始化就是在声明数组时给部分或全部元素赋初值。 • 对于基本类型的数组,初始化过程就是给数组元素赋值; • 对于对象数组,每个元素都是某类的一个对象,初始化就是调用该对象的构造函数。若有50个元素,则需调用50次构造函数。
对象数组 • 数组元素可是基本数据类型,也可是自定义类型。 • 对象数组的元素是对象,具有数据成员和函数成员 • 声明: 类名 数组名[元素个数]; Point A[2]; • 访问方法: 与基本类型数组一样,在使用对象数组时,只能引用单个数组元素。通过下标访问: 数组名[下标].成员名
对象数组初始化 • 数组中每一个元素对象被创建时,系统都会调用类构造函数初始化该对象。 • Location A[2]={Location(1,2), Location(3,4)} • Location A[2]={Location(1,2)}; • 如果没有为数组元素指定显式初始值,数组元素调用默认构造函数初始化。 • 各元素对象的初值要求为相同的值时,可以声明具有默认形参值的构造函数。 • 各元素对象的初值要求为不同的值时,需要声明带形参的构造函数。
数组元素所属类的构造函数 • 当数组中每一个对象被删除时,系统都要调用一次析构函数。 • 例1:教材P83例3.6
例2 对象数组应用举例 //Point.h #if !defined(_POINT_H) #define _POINT_H classPoint { public: Point(); Point(int xx,int yy); ~Point(); void Move(int x,int y); int GetX() {return X;} int GetY() {return Y;} private: int X,Y; }; #endif
//Point.cpp #include<iostream> using namespace std; #include "Point.h" Point::Point() { X=Y=0; cout<<"Default Constructor called."<<endl; } Point::Point(int xx,int yy) { X=xx; Y=yy; cout<< "Constructor called."<<endl; } Point ::~Point() { cout<<"Destructor called."<<endl; } void Point ::Move(int x,int y) { X=x; Y=y; } 43
//app.cpp #include<iostream> #include "Point.h" using namespace std; int main() { cout<<"Entering main..."<<endl; PointA[2]; for(int i=0;i<2;i++) A[i].Move(i+10,i+20); cout<<"Exiting main..."<<endl; return 0; } 44
运行结果: Entering main... Default Constructor called. Default Constructor called. Exiting main... Destructor called. Destructor called. 45
3.5 对象指针:对象指针的一般概念 • 和基本类型的变量一样,每个对象在初始化之后都会在内存中占有空间,存放数据成员。因此,既可以通过对象名,也可以通过对象地址来访问一个对象。对象空间的起始地址就是对象的指针 • 声明形式 类名 *对象指针名; Point A(5,10); Piont *ptr; ptr=&A; • 通过指针访问对象成员 对象指针名->成员名 ptr->getx() 相当于 (*ptr).getx();
对象指针应用举例 例1: int main() { Point A(5,10); //对象名A Point *ptr; //类指针 ptr=&A; //和基本数据类型用法类似 int x; x=ptr->GetX(); //访问形式 cout<<x<<endl; return 0; } 例2:P84
3.5 对象指针 • 指向对象的指针 • 指向对象数据成员的指针 • 指向对象函数成员的指针
指向类的非静态成员的指针 • 类的成员自身也是一些变量、函数或对象。因此,可以使指针直接指向对象的成员,将它们的地址放到指针变量中,通过指针访问对象的成员。 • 通过指向成员的指针只能访问公有成员
指向对象数据成员的指针 • int *p; • p1=&t1.hour; //将对象t1的数据成员hour的地址赋给p1,p1指向t1.hour; • cout<<*p1<<endl;