290 likes | 436 Views
第 8 章 类和复杂对象. 8.1 对象指针和对象引用. 8.4 对象数组和指向 对象数组的指针. 8.2 常对象. 8.5 堆对象. 8.6 对象的生存期. 8.3 子对象. 8.1 对象指针和对象引用. 8.1.1 对象指针 对象指针是对象的内存 ( 首 ) 地址,对象指针常常用来做函数的参数和函数的返回值。 【 例 8.1】 分析程序,注意对象指针作函数参数。. #include<iostream.h> class M { public: M(int i,int j){ x=i;y=j; }
E N D
第8章 类和复杂对象 8.1 对象指针和对象引用 8.4 对象数组和指向 对象数组的指针 8.2 常对象 8.5 堆对象 8.6 对象的生存期 8.3 子对象
8.1 对象指针和对象引用 • 8.1.1 对象指针 • 对象指针是对象的内存(首)地址,对象指针常常用来做函数的参数和函数的返回值。 • 【例8.1】分析程序,注意对象指针作函数参数。
#include<iostream.h> class M { public: M(int i,int j){ x=i;y=j; } M( ){ x=y=0; } void Setxy( int i,int j) { x=i;y=j; } void Copy(M *m); void Print( ){ cout<<x<<','<<y<<endl; } private: int x,y; }; void M::Copy( M *m){x=m->x;y=m->y; } void fun(M m1,M *m2){ m1.Setxy(20,45);m2->Setxy(56,78); } void main( ) { M p(18,25),q,*pq; q.Copy(&p); pq=&q; p.Print( ); pq->Print( ); fun(p,&q); p.Print( ); pq->Print( ); } 注意,用对象指针作函数形参,可实现双向传递数的作用。
8.1.2 对象引用 【例8.2】将例8.1参数传递的指针型改为引用型 #include<iostream.h> class M { public: M(int i,int j){ x=i;y=j; } M( ){ x=y=0; } void Setxy( int i,int j) { x=i;y=j; } void Copy(M &m); void Print( ){ cout<<x<<','<<y<<endl; } private: int x,y; }; void M::Copy( M &m){x=m.x;y=m.y; } void fun(M m1,M &m2){ m1.Setxy(20,45);m2.Setxy(56,78); } void main( ) { M p(18,25),q,&pq=q; q.Copy(p); p.Print( ); pq.Print( ); fun(p,q); p.Print( ); pq.Print( ); }
【例8.3】对象引用作函数返回值 #include<iostream.h> class N { public: N(int i,int j){ x=i;y=j; } void Print( ){cout<<x<<','<<y<<endl; } int Getxy( ){ return x+y; } private: int x,y; }; N &fun( ){ static N a(23,45); return a; } void main( ) { N p(18,25); p.Print( ); cout<<fun( ).Getxy( )<<endl; fun( )=p; cout<<fun( ).Getxy( )<<endl; } 注意,函数调用(函数返回值)可以接受赋值! 返回值为引用类型的,返回的必须是静态或全局的的,否则,返回值可能是不确定的。
8.1.3 this指针 当成员函数被调用时,系统自动生成一个指向所在对象的指针,称为this指针,一般省略不写。 【例8.4】使用this指针的例子。 #include<iostream.h> class A { public:A(int i,int j){ a=i;b=j; } A(){a=b=0;} void Copy(A &); int Returna(){ returna;} //此处a实际是this->a int Returnb(){ return b;} private:int a,b;}; void A::Copy(A &aa){if(this!=&aa)*this=aa;} void main() { A a1,a2(13,24); a1.Copy(a2); cout<<a1.Returna()+a2.Returna()<<','<<a1.Returnb()+a2.Returnb()<<endl; } 注意,上述程序与书本上的区别。
8.2 常对象 常对象像常量一样,在程序中被使用,但不能被改变,常对象只能对用常成员函数,注意区分常对象与常成员。 【例8.5】使用常对象的例子。 #include<iostream.h> class F { public:F(int i,int j){ f1=i;f2=j; } void Print(){ cout<<f1<<','<<f2<<endl;} void Print()const{cout<<f1<<','<<f2<<endl; } private:int f1,f2; }; void main() { F a1(3,5); const F a2=F(6,7); F const a3(8,2); a1.Print(); a1=a2; a1.Print(); a3.Print(); } 注意,①常对象使用不同的定义方法 ②为什么要定义两个Print函数?
【例8.6】常对象指针(分两种:常指针、指向常对象的指针)例子【例8.6】常对象指针(分两种:常指针、指向常对象的指针)例子 #include<iostream.h> class M { public:M(int i){m=i;} int Returnm( )const {return m;} private:int m; }; int fun(const M *m1,const M *m2) //星号不在const前面,表明指针指向的对象是常量,对象值不可变 { int mul=m1->Returnm( )/m2->Returnm( ); return mul; } void main( ) { M m1(77), m2(9), *const pm1=&m1; //星号在const前面,表明指针是常量,指针值不可变 cout<<pm1->Returnm( )<<endl; *pm1=m2; //指针值不变,实际上是将m2赋给了m1 cout<<pm1->Returnm( )<<endl<<m1.Returnm( )<<endl; int k=fun(&m1,&m2);//对象地址对应常对象指针,保证m1、m2的值不变 cout<<k<<endl; } 注意,区分指向常对象的指针和指向对象的常指针
【例8.7】常对象引用例子 #include<iostream.h> class M { public:M(int i){ m=i;} int Returnm( ) const {return m; }//常成员函数 private:int m; }; int fun(const M &m1,const M &m2) { int mul=m1.Returnm( )*m2.Returnm( ); return mul; } void main( ) {M m1(9),m2(11); M const &rm=m1; //常对象引用 cout<<rm.Returnm( )<<endl; m2=rm; //常对象引用调用 int k=fun(m1,m2); cout<<k<<endl; }
8.3 子对象 • 一个类的成员是另一个类的对象,这种成员对象称为子对象。 • 含有子对象的类的构造函数的参数表后面用冒号分隔成员初始化列表,成员初始化列表应包含:子对象初始化、常数据成员初始化、引用数据成员初始化,其他数据成员的初始化可放在这里,也可放在函数体中。带有成员初始化列表的构造函数格式为: • 构造函数名(参数表):成员初始化列表 {函数体}
【例8.8】子对象使用例子。 #include<iostream.h> class A { public:A( ){a=0;cout<<"隐含构造函数调用。"<<a<<endl;} A(int i){a=i;cout<<"构造函数调用。"<<a<<endl; } ~A( ){cout<<"析构函数调用。"<<a<<endl;} int Returna( ){return a;} private: int a; }; class B {public: B( ):b1(b),b2(0) { b=0; cout<<"隐含构造函数调用。"<<b<<endl;} B(int,int,int,int); ~B( ){cout<<"析构函数调用。"<<b<<endl;} void Print( ){cout<<a1.Returna( )<<','<<a2.Returna( )<<endl; cout<<b<<','<<b1<<','<<b2<<endl; } private: A a1,a2;int b;int &b1;const int b2; };
B::B(int i,int j,int k,int l):a2(i),a1(j),b2(k),b1(b){ b=l;cout<<"构造函数调用。"<<b<<endl; } void main ( ) { B x,y(1,2,3,4); y.Print( ); cout<<endl; } 注意,① 类B的两个构造函数的区别 ②类B定义中对类A的引用。 ③程序在什么时候创建了几个对象?何时撤销?
输出结果: 隐含构造函数调用。0 //1 隐含构造函数调用。0 //2 隐含构造函数调用。0 //3 构造函数调用。2 //4 构造函数调用。1 //5 构造函数调用。4 //6 2,1 //7 4,4,3 //8 //9 析构函数调用。4 //10 析构函数调用。1 //11 析构函数调用。2 //12 析构函数调用。0 //13 析构函数调用。0 //14 析构函数调用。0 //15
8.4 对象数组和指向对象数组的指针 • 8.4.1 对象数组 对象数组的元素是同一类的对象。 如定义类A: class A{public:A(int i,int j){a1=i;a2=j;}private:int a1,a2;}; 则可定义类A的对象数组: A a[5]={A(3,1),A(4,2),A(5,3),A(6,4),A(7,5)}; 上述定义的数组a有5个元素:a[0]、a[1]、a[2]、a[3]、a[4] 在定义数组时,5次调用构造函数初始化作为数组元素的对象。 当然也可以先定义数组,再用给数组元素赋值的办法对数组元素(对象)初始化: A a[5]; a[0]=A(3,1);a[1]=A(4,2); a[2]=A(5,3); a[3]=A(6,4); a[4]=A(7,5);
【例8.9】使用对象数组的例子。 #include<iostream.h> class M { public:M(int i,int j){m=i;n=j;cout<<"构造函数调用。\n";} M( ){m=n=0;cout<<"隐含构造函数调用。\n";} ~M( ){cout<<"析构函数调用。\n";} int Getm( ){return m;} int Getn( ){return n;} private:int m,n;}; M mm1[2];//定义全局对象(外部对象) void main( ) { M mm2[4]={M(2,3),M(5,6),M(7,8),M(2,5)};//定义局部对象(内部对象) mm1[0]=mm2[0];mm1[1]=M(5,9); cout<<"mm1[0]=("<<mm1[0].Getm( )<<','<<mm1[0].Getn( )<<")\n"; cout<<"mm1[1]=("<<mm1[1].Getm( )<<','<<mm1[1].Getn( )<<")\n"; for(int i=0;i<4;i++) cout<<"mm2["<<i<<"]=("<<mm2[i].Getm( )<<','<<mm2[i].Getn( )<<")\n"; }
8.4.2 对象指针数组 • 数组元素是指向同类对象的指针。 • 对象指针数组的定义方式为: • 类名 *数组名[数组元素个数]; • 注意,此类数组的每一个元素均为对象指针,指针的初始化应是对象的首地址。使用指针指向的对象的成员,应注意,首先是数组元素,其次是对象指针,即: • 数组名[下标]->数据成员名 • 数组名[下标]->成员函数名(参数表)
【例8.10】使用对象指针数组的例子。 #include<iostream.h> class M { public:M(int i,int j){m=i;n=j;cout<<"构造函数调用。\n";} M( ){m=n=0;cout<<"隐含构造函数调用。\n";} ~M( ){cout<<"析构函数调用。\n"; } void Print(){cout<<'('<<m<<','<<n<<")\n";} private:int m,n;}; void main( ) { M m1(2,3),m2(5,6),m3(7,8),m4(2,5),m5(5,9); M *pm[5]={&m1,&m2,&m3,&m4,&m5}; for(int i=0;i<5;i++) pm[i]->Print(); } 请注意:课本上红色表示的项的位置有错误! 请考虑:程序中红色表示的项,等价写法是什么? (*(pm+i))->Print();
8.4.3 指向对象数组的指针 • 注意区分指针指向的是数组元素还是数组本身。 • 指针指向对象数组元素与指向一般对象没有本质区别,见例8.11(略) • 若指针指向的是对象数组,则因为一维数组与元素指针相当,故指向数组的指针相当于二级指针。 • 指向数组的指针的定义方法为: • 类名 (*指针名)[数组元素个数]; • 注意,定义方法与前面定义对象指针数组的区别。 • 此类指针常常与二维数组相对应,使用二维数组名对此类指针初始化。
【例8.12】使用对象数组指针的例子。 #include<iostream.h> class A { public:A(int i,int j){a=i;b=j;} A( ){a=b=0;} void Print(){cout<<'('<<a<<','<<b<<")\n";} private:int a,b;}; void main( ) { A aa[3][3]; int m(15),n(10); for(int i=0;i<3;i++) for(int j=0;j<3;j++) aa[i][j]=A(m+=2,n+=5); A (*pAaa)[3](aa); for(i=0;i<3;i++) { for(int j=0;j<3;j++) (*(*(pAaa+i)+j)).Print( ); cout<<endl; } } 请考虑:程序中红色表示的项,是否有等价写法?(3种) pAaa[i][j].Print(); (*(pAaa[i]+j)).Print(); (*(pAaa+i))[j].Print();
内存分配方式 内存分配方式有三种: 1、从静态存储区域分配。 内存在程序编译的时候就已经分配好,这块内存在程序的整个运行 期间都存在。例如全局变量,static变量。 2、在栈上创建。 在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 3、从堆上分配,亦称动态内存分配。 程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。
8.5 堆对象 • 8.5.1 运算符new和delete 1. 使用new开辟内存空间,为指针变量赋值 例如,定义指针变量:int *p; 则可为指针赋值: p=new int;或p=new int(8); 前者在内存中开辟存储一个int型变量空间,并将该空间的首地址赋给指针p;后者在前者的基础上,同时用8初始化那个int 型变量。 使用new还可同时开辟连续多个同类变量的空间,供数组使用,如:int *pa; pa=new int[5]; 2. 使用delete释放指针指向变量或数组占用的空间 格式为:delete 指针名;或delete[]指针名; 前者释放单个变量占有的空间,后者释放数组占有的空间。
【例8.13】使用new、delete的例子。 #include<iostream.h> #include<stdlib.h> void main( ) { int *p,*pa; p=new int(10);pa=new int[10]; if(!pa){cout<<"错误!\n";exit(1);}//当pa==0时,终止程序 //exit(1)为关闭文件,终止程序的函数,该函数定义在stdlib.h中 for(int i=0;i<10;i++)pa[i]=i+10; for(i=0;i<10;i++)cout<<pa[i]+*p<<" "; cout<<endl; delete p;delete[]pa; }
8.5.2 堆对象、堆对象数组的创建和释放 • 使用new、delete可以创建、释放堆对象和堆对象数组。 【例8.14】创建、释放堆对象的例子。 #include<iostream.h> class A { public:A(int i,int j){a1=i;a2=j;cout<<"构造函数调用。\n";} ~A( ){cout<<"析构函数调用。\n";} void Print( ){cout<<a1<<','<<a2<<endl;} private:int a1,a2;}; void main( ) {A *pa1,*pa2; pa1=new A(12,9);pa2=new A(14,6); pa1->Print( ); pa2->Print( ); delete pa1; delete pa2; }
【例8.15】使用堆对象数组指针的例子。 #include<iostream.h> #include<string.h> class B { public:B( ){cout<<"隐含构造函数\n";} B(char *s,double n){strcpy(name,s);b=n;cout<<"构造函数\n";} ~B(){cout<<"析构函数。"<<name<<endl;} void Getb(char *s,double &n){strcpy(s,name);n=b;} private:char name[80];double b;}; void main( ) {B *pb;double n;char s[80]; pb=new B[5]; pb[0]=B("wang",4.6); pb[1]=B("zhang",2.9); pb[2]=B("Li",8.2); *(pb+3)=B("Lu",3.5); *(pb+4)=B("Ma",7.1); for(int i=0;i<5;i++){pb[i].Getb(s,n);cout<<s<<','<<n<<endl;} delete[]pb; }
【例8.16】分析程序,思考问题。 #include<iostream.h> #include<string.h> class String { public:String( ){Length=0;Buffer=0;} String(const char *str); void Setc(int index,char newchar); char Getc(int index)const; int GetLength( )const{return Length;} void Print( )const { if(Buffer==0)cout<<"空\n";else cout<<Buffer<<endl;} void Append(const char *Tail); ~String( ){ delete[]Buffer; } private:int Length; char *Buffer; }; String::String(const char *str) { Length=strlen(str);Buffer=new char[Length+1];strcpy(Buffer,str);} void String::Setc(int index,char newchar) { if(index>0&&index<=Length)Buffer[index-1]=newchar; }
char String::Getc(int index) const{ if(index>0&&index<=Length)return Buffer[index-1];else return 0;} void String::Append(const char *Tail) { char *tmp; Length+=strlen(Tail); tmp=new char[Length+1]; strcpy(tmp,Buffer);strcat(tmp,Tail); delete[]Buffer; Buffer=tmp; } void main( ) { String s0,s1("a string."); s0.Print( );s1.Print( );cout<<s1.GetLength( )<<endl; s1.Setc(5,'d'); s1.Print( ); cout<<s1.Getc(6)<<endl; String s2("this is "); s2.Append("a string."); s2.Print( ); cout<<s2.GetLength( )<<endl; } 问题:函数Setc(i,ch)、Getc(i)、Append(char *)作用是什么?
8.6 对象的生存期 • 对象像变量一样,定义(创建)时分配内存,称为诞生;生存期结束时,释放占有的内存,称为死亡。 • 按照生存期的长短不同,分3种:局部对象、全局对象、静态对象。 • 定义在某函数体内或程序块内的对象称为局部对象,它在其定义的范围内起作用,进入该范围时诞生,离开该范围时死亡,再次进入该范围时,再次新生。其生命期最短。 • 不在函数体(或程序块)中定义的对象称为全局对象。它在整个文件中有效,而且其他文件也可引用,但需用extern进行声明。生命期就是程序的执行期。 • 定义时用增加关键字static的对象,称为静态对象。静态对象的作用域要看其定义的地方,分局部和全局(又称内部和外部);生命期同全局对象为程序的执行期。
【例8.17】分析程序,注意对象的生存期与作用域。【例8.17】分析程序,注意对象的生存期与作用域。 #include<iostream.h> #include<string.h> class A { public: A(char *str); ~A( ); private: char string[80]; }; A::A(char *str) { strcpy(string,str);cout<<"构造函数被"<<string<<"调用。\n"; } A::~A( ){ cout<<"析构函数被"<<string<<"调用。\n"; } void fun( ) { A A1("函数中的对象"); static A A2("内部静态对象"); cout<<"在函数fun( )中。"; } A A3("全局对象"); static A A4("外部静态对象"); void main( ) { A A5("主函数中的局部对象"); cout<<"调用子函数fun前,在主函数中。\n"; fun( ); cout<<"调用子函数fun后,回到主函数中。\n";}
小结与作业 • 本章所讲知识实际上是第7章与前6章的混合 • 对象指针与对象数组可以统一理解 • 对象引用作参数时,可以双向传递,此时引用比指针更方便 • 常对象与前6章讲过的常量类似,须注意使用方法 • 堆对象将会经常使用 • 对象的生存期与变量的的生存期一样 • 作业:第8章所有习题