490 likes | 632 Views
面向对象程序设计. 1. www.themegallery.com. 引入: 在 C++ 语言中,不仅定义对象和定义变量类同,更进一步说,对象的作用域也和传统程序中变量的作用域类同。对象按作用域不同可划分为以下 3 种。 (1) 全局对象: 程序运行时,此类对象被创建,且以全 0 的形式表示对象;程序结束时,此类对象被撤销; (2) 局部对象: 程序运行到某函数(程序块)时,此类对象被创建,并以随机值表示对象;程序退出该函数(程序块)时,此类对象被撤销;
E N D
面向对象程序设计 1 www.themegallery.com
引入: 在C++语言中,不仅定义对象和定义变量类同,更进一步说,对象的作用域也和传统程序中变量的作用域类同。对象按作用域不同可划分为以下3种。 (1) 全局对象:程序运行时,此类对象被创建,且以全0的形式表示对象;程序结束时,此类对象被撤销; (2) 局部对象:程序运行到某函数(程序块)时,此类对象被创建,并以随机值表示对象;程序退出该函数(程序块)时,此类对象被撤销; (3) 堆对象(动态对象):执行运算符new时,此类对象被创建,执行运算符delete时,此类对象被撤销。
//日期类 class Date{int year, month, day; public:void set(int y, int m, int d); //设置日期值bool isLeapYear(int y) const;//判断闰年friend ostream& operator<<(ostream& out, const Date& d); …… }; Date d; //全局对象 int main(){ Date e; //局部对象 cout<<d<<e; }
问题: • 在建立一个对象时,常常需要作某些初始化的工作,例如对数据成员赋初值。(对象的生) • 在对象不复存在以前,常常要做一些释放工作,例如,关闭文件;或者,释放分配的堆空间。 (对象的灭)
第4章 对象生灭 教学内容 构造函数设计 ( Constructor Design ) 构造函数重载 ( Constructor Overload ) 类成员初始化 ( Class Member Initializations ) 构造顺序 ( Constructing Order ) 拷贝构造函数 ( Copy Constructors ) 析构函数 ( Destructors ) 对象转型与赋值 ( Conversion & Assignment )
§ 构造函数设计 • 构造函数主要是两个功能:为对象开辟空间,为对象中的数据成员赋初值。 • 构造函数的名字一定和类的名字相同。在该类的对象创建时,自动被调用。 • 构造函数不应返回值,也不应是void类型函数。 • 如果没有定义构造函数,编译系统会自动生成一个默认的构造函数。该构造函数不带参数,函数体是空的,不作任何操作。 • 构造函数可以带参数或没带参数、可以设置默认参数,可以重载。
class Date{ int year, month, day; public: Date(int y=2000, int m=1, int d=1); // 在声明时设置默认参数 …… }; Date::Date(int y, int m, int d){ year=y,month=m,day=d; } //------------------------------------------------------------------------------ int main(){ Date d(2003,12,6); Date e(2002); // 默认两个参数 Date f(2002,12); // 默认一个参数 Date g; // 默认三个参数 cout<<d<<e<<f<<g; }
§ 构造函数的重载 在一个类中可以定义多个构造函数,以便对类对象提供不同的初始化的方法,供用户选用。这些构造函数具有相同的名字,而参数的个数或参数的类型不相同。这称为构造函数的重载。
class Date{ int year, month, day; public: Date(int y=2000, int m=1, int d=1); // 设置默认参数 Date(const string& s); // 构造函数重载 …… };//------------------------------------------------------------------- Date::Date(const string& s){ year = atoi(s.substr(0,4).c_str()); month = atoi(s.substr(5,2).c_str()); day = atoi(s.substr(8,2).c_str()); }//------------------------------------ Date::Date(int y, int m, int d){ year=y,month=m,day=d; } //---------------------------------------------------------------------- int main(){ Date d("2006-12-26"); Date e(2000, 12, 25); Date f(2001, 10); Date g(2002); Date h; //无参对象定义 Date("2006-12-26"); //一次性对象 }
注:如果手工定义了构造函数,则系统不再提供默认的构造函数注:如果手工定义了构造函数,则系统不再提供默认的构造函数 class Date{ int year, month, day; public: Date(int y, int m, int d); Date(const string& s); …… }; int main(){ Date d("2006-12-26"); Date e(2000,12,25); Date f; //error }
§类成员初始化 ( Class Member Initializations ) 数据成员的空间分配是在构造函数被调用和其过程被执行时完成,如果在类中有对象成员时,那么刹那间便是调用对象所在类的构造函数,以创建对象空间的时机。
class StudentID{ int value; public: StudentID( ){ //无参构造函数 static int nextStudentID = 0; value = ++nextStudentID; cout<<"Student Id: "<<value<<"\n"; } };//----------------------------------- class Student{ string name; StudentID id; //对象成员 public: Student(string n = "noName"){ cout <<"Student :" + n + "\n"; name = n; } };//----------------------------------- int main(){ Student s("Randy"); } 运行结果:Student Id: 1Student: Randy 说明先成员构造,后自身构造.成员构造不见显式调用,而是默认调用StudentID类的无参构造函数.
Q:若要把参数传递给对象成员的有参构造函数,那么类该如何操作?Q:若要把参数传递给对象成员的有参构造函数,那么类该如何操作?
class StudentID{ int value; public: StudentID(int id=0){ value = id; cout<<"Student Id "<<value<<"\n"; } };//----------------------------------- int main(){ Student s("Randy", 58); } 运行结果:Student Id: 0Student: Randy Student Id: 58 class Student{ string name; StudentID id; public: Student(string n = "noName", int ssID=0){ cout <<"Student " + n + "\n"; name = n; StudentID id(ssID); //id是局部对象 }};//-----------------------------------
class StudentID{ int value; public: StudentID(int id=0){ value = id; cout<<"Student Id "<<value<<"\n"; } };//----------------------------------- int main(){ Student s("Randy", 98); Student t("Jenny"); } 其运行结果为:StudentId: 98Student: RandyStudentId: 0Student: Jenny class Student{ string name; StudentID id; public: Student(string n = "noName", int ssID=0):name(n) , id(ssID) { cout <<"Student " + n + "\n"; } };//-----------------------------------
C++定义了一个新的方式,在类的构造函数定义中,使其带有成员初始化列表的形式,从而对类中的成员进行初始化。C++定义了一个新的方式,在类的构造函数定义中,使其带有成员初始化列表的形式,从而对类中的成员进行初始化。 • 构造函数(参数列表):成员初始化列表 {…………} 成员1(参数表 1),成员2(参数表 2), ...,成员n(参数表 n) 此外,常量成员和常量引用成员在程序运行期间不能改变,须通过上面成员初始化方式可以实现
#include <iostream.h> class Date{ private: const int year; //常量成员 const int month; const int day; public: ……. const int &r; //常量引用 }; Date::Date(int y, int m, int d):year(y),month(m),day(d),r(year) { }
§ 构造顺序 在一个大程序中,各种作用域的对象很多,有些对象包含在别的对象里面,有些对象早在主函数开始运行之前就已经建立。创建对象的唯一途径是调用构造函数。构造函数是一段程序,所以构造对象的先后顺序不同,直接影响程序执行的先后顺序,导致不同的运行结果。 C++给构造对象的顺序作了专门的规定。 1.局部和静态对象 根据运行中定义对象的顺序来决定对象创建的顺序。
class A{ public: A(){ cout<<"A->"; } }; //---------------------------- class B{ public: B(){ cout<<"B->"; } }; //---------------------------- class C{ public: C(){ cout<<"C->"; } }; //---------------------------- void func(){ cout<<"\nfunc: "; A a; //局部对象 static B b; //静态对象 C c; //局部对象 }//-------------------------------- int main(){ cout<<"main: "; for(int i=1; i<=2; ++i){ for(int j=1; j<=2; ++j) if(i==2) C c; else A a; B b; } func(); func(); } 运行结果: main: A->A->B->C->C->B-> func: A->B->C-> func: A->C->
包含局部对象的函数或程序块每被调用(执行)一次,局部对象就被构造一次 。 • 静态对象只被构造一次静态对象和静态变量一样,文件作用域的静态对象在主函数开始运行前全部构造完毕。块作用域中的静态对象,则在首次进入到定义该静态对象的函数时,进行构造。
2.全局对象 和全局变量一样,所有全局对象在主函数开始运行之前,全部已被构造。2.全局对象 和全局变量一样,所有全局对象在主函数开始运行之前,全部已被构造。 这会给调试带来问题。当要开始调试时,所有全局对象的构造函数都已被执行,如果它们中的一个有致命错误,那么你可能永远也得不到控制权。这种情况下,该程序在它开始执行之前就死机了。 有两种方法可以解决这个问题:一是将全局对象先作为局部对象来调试;二是在所有怀疑有错的构造函数的开头,增加输出语句,这样在程序开始调试时,你可以看到来自这些对 象的输出信息。
单个文件的程序中,全局对象按定义的顺序进行构造。 • 多个文件组成的,这些文件被分别编译、连接。 • 因为编译器不能控制文件的连接顺序,所以它不能决定不同文件中全局对象之间的构造顺序。 例如,下面的代码是个多文件程序结构,创建了两个全局对象:
//===================================== // student.h //===================================== #include<iostream> using namespace std; //------------------------------------- class Student{ const int id; public: Student(int d):id(d){ cout<<"student\n"; } void print(){ cout<<id<<"\n"; } };//----------------------------------- class Tutor{ Student s; public: Tutor(Student& st) :s(st) { cout<<"tutor\n"; s.print(); } };//-----------------------------------
//======================== // f09092.cpp //======================== #include"student.h" Student ra(18); //全局对象 运行结果: tutor 0 student //======================== // f0909.cpp //======================== #include"student.h" extern Student ra; //----------------------------------------- Tutor je(ra); //全局对象 int main(){} 为了避免编译器实现中的不确定问题,应尽量不要设置全局对象.
class A{ public: A(int x){ cout<<"A:"<<x<<"->"; } };//----------------------------------- class B{ public: B(int x){ cout<<"B:"<<x<<"->"; } };//----------------------------------- class C{ A a; B b; public: C(int x,int y):b(x),a(y) { cout<<"C\n"; } };//----------------------------------- int main(){ C c(15, 9); } 3.成员对象 以其在类中声明的顺序构造 运行结果: A:9->B:15->C
例: #include <iostream> class A { public: A(int j):age(j),num(age+1) { cout<<“age:”<<age<“,num:”<<num<<endl;} protected: int num; int age; }; void main() { A sa(15);} 运行结果: age:15,num:2
§拷贝构造函数 当构造函数的参数为自身类的对象引用时,这个构造函数称为拷贝构造函数。 拷贝构造函数的功能是用一个已有对象初始化一个正在建立的同类对象, 例如:Student s1("Jenny");Student s2=sl; //用s1的值去初始化s2 或 Student s2(sl); 调用拷贝构造函数
对象作为函数参数传递时,也要涉及对象的拷贝,例如:对象作为函数参数传递时,也要涉及对象的拷贝,例如: • void fn(Student fs) • { • //… • } • void main() • { • Student ms; • fn(ms); • }
拷贝构造函数具有以下特点: 1、是一种特殊的构造函数,用于将一个已知对象的数据成员的值拷贝给正在创建的另一个对象。 2、只有一个参数,且必需是对自身类对象的常量引用:P316 3、定义拷贝构造函数的格式: class 类名{ public : 类名(形参);//构造函数 类名(const类名 &对象名);//拷贝构造函数 ... };
一、默认的拷贝构造函数 • 如果不定义拷贝构造函数,系统自动为类提供一个默认的拷贝构造函数(逐一拷贝数据成员)。 • //例: point.cpp • 二、自定义拷贝构造函数
例:拷贝构造函数的使用 class point { public: point(float x1=0, float y1=0); //带默认参数构造函数 point( const point &p); //拷贝构造函数 void show( ); //打印显示点 private: float x; //点的横坐标 float y; //点的纵坐标 }; //类定义结束
point::point(float x1, float y1) {x=x1; y=y1;} point::point( const point &p) //拷贝构造函数 { x=p.x; y=p.y; } void point::show( ) //显示点 { cout <<x<<","<< y <<endl; }
void main( ) { point p1; point p2(2, 2); //调用参数构造函数 point p3(p2); //调用拷贝构造函数 p1.show( ); p2.show( ); p3.show( ); } 运行结果:0,0 2,2 2,2
注意: • 一般情况下,不必定义拷贝构造函数。但是,如果构造函数中存在动态分配,则必须定义拷贝构造函数
//f0912.cpp • class Person{ • public: • Person(char* pN= “ noName”) {//构造函数 • cout <<"Constructing " <<pN <<endl; • pName=new char[strlen(pN)+1];//分配堆内存 • if(pName!=0) strcpy(pName,pN); • } • ~Person() { • cout <<"Destructing " <<pName <<endl; • delete[] pName; • } • protected: • char* pName; • }; new运算符按要求分配堆内存,如果成功,则返回指向该内存起始地址的指针;如果不成功时,new运算符返回空指针NULL。
p1 pName 堆 p1 pName 堆 p2 pName void main() { Person p1("Randy"); Person p2=p1; // 即Person p2(p1);调用默认拷贝构造函数 } 执行结果出错 默认拷贝构造函数仅仅拷贝对象本体
//定义拷贝构造函数来解决上述问题 class Person{ public: Person(char* pN = “ noName”) { cout <<"Constructing " <<pN <<endl; pName=new char[strlen(pN)+1]; if(pName!=0) strcpy(pName,pN); } Person(Person& p) { cout<<"Copying "<<p.pName <<" into its own block\n"; pName=new char[strlen(p.pName)+1]; if(pName!=0)strcpy(pName,p.pName); }
p1 pName 堆 p1 pName 堆 p2 pName 堆
在默认拷贝构造函数中,拷贝的策略是逐个成员依次拷贝。但是,一个类可能会拥有资源,当其构造函数分配了一个资源(例如堆内存)的时候,会发生什么呢?如果拷贝构造函数简单地制作了一个该资源的拷贝,而不对它本身分配,就得面临一个麻烦的局面:两个对象都拥有同一个资源。当对象析构时,该资源将经历两次资源释放。在默认拷贝构造函数中,拷贝的策略是逐个成员依次拷贝。但是,一个类可能会拥有资源,当其构造函数分配了一个资源(例如堆内存)的时候,会发生什么呢?如果拷贝构造函数简单地制作了一个该资源的拷贝,而不对它本身分配,就得面临一个麻烦的局面:两个对象都拥有同一个资源。当对象析构时,该资源将经历两次资源释放。 • 发生以下三种情况拷贝构造函数被调用。 --用类的对象去初始化该类的另一个对象时。 --对象作为函数的实参传递给函数的形参时。 --函数返回值是类的对象,函数调用返回时。
§ 析构函数 • 一个类可能在构造函数里分配资源,这些资源需要在对象不复存在以前被释放。例如,如果构造函数打开了一个文件,文件就需要被关闭。或者,如果构造函数从堆中分配了内存,这块内存在对象消失之前必须被释放。 • C++提供了析构函数(destructor)自动完成这些清理工作,不必调用其他成员函数。 析构函数也是一个特殊的成员函数,它没有返回类型,没有参数,也没有重载。只是在类对象生命期结束的时候,由系统自动调用。一个类可以有多个构造函数,但只能有一个析构函数。如果用户没有定义析构函数,C++编译系统会自动生成一个析构函数,
当对象的生命期结束时,会自动执行析构函数。具体地说如果出现以下几种情况,程序就会执行析构函数:当对象的生命期结束时,会自动执行析构函数。具体地说如果出现以下几种情况,程序就会执行析构函数: • ①如果在一个函数中定义了一个对象(它是自动局部对象),当这个函数被调用结束时,对象应该释放,在对象释放前自动执行析构函数。 • ② 如果定义了一个全局对象,则在程序的流程离开其作用域时(如main函数结束或调用exit函数) 时,调用该全局对象的析构函数。 • ③如果用new运算符动态地建立了一个对象,当用delete运算符释放该对象时,先调用该对象的析构函数。它撤销对象占用的内存之前完成一些清理工作,使这部分内存可以被程序分配给新对象使用。
在一般情况下,调用析构函数的次序正好与调用构造函数的次序相反: 最先被调用的构造函数,其对应的(同一对象中的)析构函数最后被调用,而最后被调用的构造函数,其对应的析构函数最先被调用。如图
void func(){ cout<<"\nfunc: "; A a; cout<<"ok->"; static B b; C c; }//---------------------------- int main(){ cout<<"main: "; for(int i=1; i<=2; ++i) { for(int j=1; j<=2; ++j) if(i==2) C c; else A a; B b; } func(); func(); }//================= class A{ public: A(){ cout<<"A->"; } ~A() { cout<<"<-~A"; } };//------------------------------- class B{ public: B(){ cout<<"B->"; } ~B() { cout<<"<-~B"; } };//------------------------------ class C{ public: C(){ cout<<"C->"; } ~C() { cout<<"<-~C"; } };//------------------------------ 运行结果: main: A-><-~A A-><-~A B-><-~B C-><-~C C-><-~C B-><-~B func: A->ok->B->C-> <-~C<-~A func: A->ok->C-> <-~C<-~A<-~B 析构函数在对象的生命期结束时被自动调用 静态对象在程序运行结束时被自动调用
转型与赋值 ( Conversion & Assignment ) • 对象转型:类类型,其自动转换的功能必须编程实现,定义含一个参数的构造函数。 class Student{ string name; public: Student(const string& s):name(s){} };//----------------------------------- void fn( Student& s ){ cout<<"ok\n"; } //------------------------------------- int main(){ string t="jenny"; fn( t ); // 参数为string,却能匹配Student类型 }//===================== 用构造函数实现从一种类型转成另一种类型。
因为:"Jenny" -> string -> Student 经历了两步. • 对象转型的规则: • 只会尝试含有一个参数的构造函数 • 如果有二义性,则会放弃尝试 • 推导是一次性的,不允许多步推导 //f0915.cpp class Student{ string name; public: Student(const string& s):name(s){} };//----------------------------------- void fn( Student& s ){ cout<<"ok\n"; } //------------------------------------- int main(){ fn("jenny"); //error }//=====================
class Student{ string name; public: Student(const string& n="noName"):name(n){} };//-------------------------------------------- class Teacher{ string name; public: Teacher(const string& n="noName"):name(n){} };//-------------------------------------------- void addCourse(const Student& s); void addCourse(const Teacher& t); //----------------------------------------------- int main(){ addCourse(“Prof.DingleBerry”); // error,二义性 }//================================ addCourse(teacher("Prof.DingleBerry"));
对象赋值即对象拷贝:两个已经存在的对象之间的复制对象赋值即对象拷贝:两个已经存在的对象之间的复制 • Person d, g; • d = g; // 对象赋值 • 对象赋值便是使用类中的赋值操作符. • 如果类中没有定义赋值操作符,则系统悄悄地定义一个默认的赋值操作符 • Person& operator=(const Person& p){ • memcpy(*this, *p, sizeof(p)); • }
当对象本体与对象实体不同时,则对象赋值操作符与拷贝构造函数一样,必须自定义:当对象本体与对象实体不同时,则对象赋值操作符与拷贝构造函数一样,必须自定义: • //f0917.cpp • class Person{ • char* pName; • public: • Person(char* pN="noName"); • Person(const Person& s); • Person&operator=(const Person& s){ • if(this==&s) return s; • delete[] pName; • pName = new char[strlen(s.pName)+1]; • if(pName) strcpy(pName,s.pName); • return *this; • } • ~Person(){ • delete[] pName; • } • }; 定义赋值操作符: 1排除客体对象与本对象同一的情况 2释放本对象的资源 3申请客体对象相同大小的资源空间 4拷贝客体对象的资源到本对象 注:赋值操作的返回必须是引用返回