570 likes | 901 Views
C++ 基础 —— 类和对象基本概念. 类的概念、对象的概念 构造函数、析构函数、 拷贝构造函数、成员函数及其重载 this 指针 类的静态成员 友元. 1. 类与对象的基本概念. 类是 对象 的 抽象及描述 ,它是具有 统一属性 和 方法 的一类对象的统一描述体,是用来定义某类对象 共有属性 和 方法 的模板。 类是用来 创建对象 实例的模板,它包含了该类对象的 属性 声明和 方法 定义。类是一个型,而对象则是这个型的一个实例。 在面向对象程序中, 类 是 静态 概念,而 对象 则是一个 动态 概念,因为只有在运行时才为对象分配空间,对象才真正存在. 类的定义.
E N D
C++基础——类和对象基本概念 类的概念、对象的概念 构造函数、析构函数、 拷贝构造函数、成员函数及其重载 this指针 类的静态成员 友元 1
类与对象的基本概念 类是对象的抽象及描述,它是具有统一属性和方法的一类对象的统一描述体,是用来定义某类对象共有属性和方法的模板。 类是用来创建对象实例的模板,它包含了该类对象的属性声明和方法定义。类是一个型,而对象则是这个型的一个实例。 在面向对象程序中,类是静态概念,而对象则是一个动态概念,因为只有在运行时才为对象分配空间,对象才真正存在
类的定义 • 假设一个学生有四门课程成绩和平均成绩,有学号、姓名等属性。在标准C中,将学生作为一种复杂数据时,采用结构描述,则其存储结构为: struct Student { int Number; char Name[20]; double Score[5]; };
标准C的结构存在的问题是:针对数据的操作和数据没有明确的联系,它们是相对独立的。标准C的结构存在的问题是:针对数据的操作和数据没有明确的联系,它们是相对独立的。 • 所以,在加工数据时,必须将数据的实例作为参数传递给操作函数,数据是被动的。 • 从应用的角度来看,数据应该是主动的,而操作是作为主体的数据主动发起的,也就是说,操作是被动的。 数据处理方法的描述为: void InitStudent(Student *p,int num, char *name,double *score); //对学生数据类型进行初始化 void Stu_Average(Student *p);//求学生的课程平均分 void Modify_Score(Student *p) ;//修改学生的课程成绩 void Print_Stu(Student *p) ;//输出学生的课程成绩单 ……
类的定义 C++的解决方案是:将操作归入数据结构中。 struct Student{ int Number; char Name[20]; double Score[5]; void InitStudent(Student *p,int num, char *name,double *score); void Stu_Average(Student *p); void Modify_Score(Student *p) ; void Print_Stu(Student *p) ; …… };
经过这样的改造后,数据成为能动的主体,它不仅包含解决问题的数据,同时也包含加工数据的操作,从而数据与操作紧密地结合在一起。经过这样的改造后,数据成为能动的主体,它不仅包含解决问题的数据,同时也包含加工数据的操作,从而数据与操作紧密地结合在一起。 • 这种数据结构真正地成为独立的单元。 • 另外一方面,数据结构内部的变化不会反射到它的外部。这就是说,数据内部结构的改变不会对使用数据结构的程序带来影响。 这就是数据封装的一般概念。
一般地,C++是用类来实现封装机制的: class className { attributes; // data members; operations; // member functions; };//注意这个分号的存在 定义了一个新的数据类型:className className是类的名字,也是该类型的类型名。
类的定义 C++语言提供了三种不同的信息隐藏程度,分别用三种不同的关键字private、protected和public表示外界对它们的访问程度。 可以被外部访问的部分被定义在public段中。这些成员被称为公有成员。 不能被外部访问的部分被定义在private段中,称为私有成员。 保护(protected)成员只能被该类的成员或派生类的成员访问,类外不可访问,但是派生类可以访问其父类的保护段成员。
私有段 保护段 公有段 接口 数据封装是相对的概念,只是对于类外而言; 而对于类内部,所有的成员都是相互可见的。 9
类的定义 较小的函数可在类内部直接给出其实现,自动成为内联函数 较复杂的函数可以在类外给出定义,使得类的结构更加清晰 在类外定义成员函数时,必须加上::作用域运算符,用来说明该函数属于哪个类所有,其一般形式如下: Datatype class_name::func_name(<arg_list>) { func_body; } 10
class Person { private: char Name[20], ID[19]; unsigned Age; char Sex; public: void Register(char *, char *, unsigned,char); void GetName(char *); void GetID(char *); unsigned GetAge(void){return Age;} char Getsex(void){return Sex ? 'M':'F';} …… };
void Person::Register( char *name, char *id, unsigned age, char sex) { strcpy(Name, name); strcpy(ID, id); Age = age; if(sex == 'M' || sex == 'm') Sex = 1; else if(sex == 'F' || sex == 'f') Sex = 0; else{cout<<"The sex should be M or F!"<<endl; exit(0); } } void Person::GetName(char *name){ strcpy(name, Name);} void Person::GetID(char *id){ strcpy(id, ID);}
类只是一种形式化定义。要使用类提供的功能,必须使用类的实例——对象。类只是一种形式化定义。要使用类提供的功能,必须使用类的实例——对象。 • 对象要占据一定的内存空间。 • 类和对象的关系就像整型和变量的关系。 int i; Person d; • 变量i和d分别是数据类型int和Person的对象,占据一定的内存空间; • int或Person是数据类型,是对整数和人的抽象描述,并不分配内存空间。
类与结构 C++的class是C语言的struct的扩展 在标准C中,结构仅有数据成员,没有成员函数 在C++中,结构可以有成员函数,与类的功能类似 结构的成员默认是公有的,而类的默认成员是私有的 一般说来,在使用struct类型时,为了与标准C的struct兼容,仅定义数据成员,不定义成员函数。
构造函数 定义普通变量时,可以进行初始化,例如: int a = 10, b = a; /*这里用常量10初始化变量a,用变量a的值初始化变量b*/ 一个类的对象是否也可以初始化呢? C++为类设计了构造函数(constructor)机制,它可以达到初始化数据成员的目的。 15
类的构造函数是类的一个特殊成员函数,它没有返回类型(void也不行),可以有参数,函数名和类名一样。类的构造函数是类的一个特殊成员函数,它没有返回类型(void也不行),可以有参数,函数名和类名一样。 • 当创建类的一个新对象时,自动调用构造函数,完成初始化工作(需要注意构造函数是否有参数,以及参数的个数、类型)。 • 构造函数的作用为: • 分配一个对象的数据成员的存储空间; • 执行构造函数(体),一般是初始化一个对象的部分或全体数据成员。 16
构造函数的参数可以是缺省的 • 定义类对象时给构造函数提供参数: 1)仅仅只有一个参数: 类名 对象名 = 参数; 2)有一个或多个参数: 类名 对象名(参数列表); • 构造函数的定义例: class Date { private: int year, month, day; public: Date(int mon, int day, int y=2011); }; 17
构造函数有两种方式初始化数据成员: 1)在构造函数体内用赋值语句的方式 Date::Date(int y, int m, int d) { year = y; month = m; day = d; } 2)构造函数的初始化列表的方式 Date::Date(int y,int m,int d):year(y), month(m),day(d) {} 18
缺省的构造函数 用户定义的类类型中,可以没有构造函数。 编译器会自动给该类类型生成一个没有参数的构造函数,该函数不作任何初始化工作。 这种构造函数称为缺省的构造函数。 注意: 一个类如果有显式定义的构造函数,编译器就不会生成缺省构造函数了。 • 重载构造函数 一个类可以提供多个构造函数,用于在不同场合进行类对象的初始化工作。 构造函数的重载,它们的参数表必须互不相同。 19
class Date { int year, month, day; public: Date(int d, int m, int y); Date(int d, int m); Date(int d); Date(const char * dateStr); …}; Date today(8, 11, 2011); Date day1(9, 5); Date day2(8); Date Christmas(“Dec 25, 2008”); Date now; // 错,无缺省构造函数 20
析构函数 • 与构造函数对应的是析构函数。C++通过析构函数来处理对象的善后工作。 • 析构函数没有返回类型,没有参数,函数名是类名前加“~”。 • 析构函数的作用为: (1) 执行析构函数(一般没有具体的工作); (2) 释放对象的存储空间(该功能由系统自动完成)注:析构函数需要负责释放new申请的空间。 • 可以显式地调用析构函数;若没有显式调用,则在一个对象的作用域结束时,系统自动调用析构函数。 • 系统自动调用构造函数和自动调用析构函数的顺序是相反的。 21
例:设计一个数组类,数组的大小在定义时初始化,而且其大小在运行时可以改变。例:设计一个数组类,数组的大小在定义时初始化,而且其大小在运行时可以改变。 class Array { int * p; int size; public: Array(int num) { size=num; p=new int[size]; } ~Array( ) {delete [] p; } ...};
拷贝构造函数 拷贝构造函数定义 构造函数的参数可以是任何类型参数,甚至可以将自己类对象的(常量)引用作为参数,称它为拷贝构造函数。 拷贝构造函数有两个含义: 首先,它是一个构造函数,当创建一个新对象时,系统自动调用它; 其次,它将一个已经定义过的对象(参数代表的对象)的数据成员逐一对应地复制给新对象。 23
拷贝构造函数 拷贝构造函数的用途: • 创建一个新对象,并将一个已存在的对象复制到这个新对象 • 对象本身做参数(将实参对象复制给形参对象) • 函数返回对象(复制返回的对象给一个临时对象) 如果一个类没有显式定义拷贝构造函数,C++编译器可以为该类产生一个缺省的拷贝构造函数。 缺省的拷贝构造函数,也将复制对象的各个数据成员复制给被复制对象的各个数据成员。 这样一来,两个对象的内存映像是一模一样的。 24
void main( ) { A obj1(10,20); //调用A(int,int) A obj2(obj1); //调用A(const A &); A obj3 = obj2; //调用A(const A &); } class A{ int x;int y; public: A(int,int); //一般构造函数 A(constA &objA) // 拷贝构造函数 {x=objA.x;y=objA.y;} }; 25
class student{ private: char *name; float score; public: student(char *na, float sc); ~student(); }; student::student(char *na,float sc){ name=new char[strlen(na)+1]; if(name!=NULL) { strcpy(name, na); score=sc; } } student::~student( ) { cout<<"析构函数运行,学生"<<name<<"占用的内存释放!"<<endl; delete name; } void main(void) { student s1(“Tom", 83.5); student s2(s1); }
使用缺省的拷贝构造函数 27
student::student(const student &w) { name=new char[strlen(w.name)+1] ; if(name!=NULL) { strcpy(name, w.name); score = w.score; } }
类的对象 可以创建不同形式的类对象: • 命名的自动对象: 每次进入该对象的作用域,都调用构造函数; 每次退出该对象的作用域,都调用析构函数。 • 自由对象(动态对象) 使用new创建对象(实际上调用构造函数); 使用delete释放对象(实际上调用析构函数); 当delete释放对象后,该对象就不能再被使用。 如果构造函数有参数,也必须给出实参。
class Table {…}; void f(int a) { Table aa; //aa的生命期到f函数返回 Table bb; //bb的生命期到f函数返回 if (a>0) { Table cc; //cc的生命期到if语句结束 … } Table dd; //dd的生命期到f函数返回 … }; //若调用函数f(10), 则调用构造函数的顺序是:aa、bb、cc、dd 调用析构函数的顺序是:cc、dd、bb、aa
对象的初始化: • 对象的初始化有许多表示法,C++语言允许下述三种表示方法。 • C风格的初始值表的方法 • 赋值表达式的方法 • 表达式表的方法 • Class_Name Object(…); • Class_Name Object = …;
this指针 C++为所有非静态成员函数提供了一个称为this的指针,因此,常常称成员函数拥有this指针。 • this是一个隐含的指针,不能被显式声明 • 它只是一个形参,一个局部变量 • 在任何一个非静态成员函数里都存在 • 它局部于某一对象。 • this指针是一个常指针,可以表示为(但不能显式声明): X * const this; • 可以使用const将this声明为指向常量的常指针。 • this 指针主要用在运算符重载、自引用等场合。
void main( ) { INT mainObj; mainObj.set(1); mainObj.out( ); mainObj.fun(mainObj); mainObj.out( ); } 则程序输出: 1 10 20 30 40 50 60 40 class INT { int num; public: void set(int x) { num=x;} void out( ) {cout<<num<<‘ ’;} void fun( INT obj) { INT funObj; num=10; obj.num=20; funObj.num=30; out( ); obj.out( ); funObj.out( ); set(40);obj.set(50); funObj.set(60); out( ); obj.out( ); funObj.out( ); } }; //类定义结束
类的静态成员 一个类的例子: class Circle { private: int x, y; //圆心的X、Y坐标 float fRadius; //半径 public: void SetXY(int a,int b); void SetRadius(float r); void Move(int newx,int newy); … //其它成员 };
C1 C2 C3 x x x y y y fRadius fRadius fRadius SetXY() SetRadius() Move() Circle c1, c2, c3; 类Circle的三个对象及其存储空间:
静态数据成员:该数据成员为该类的所有对象所共有。静态数据成员:该数据成员为该类的所有对象所共有。 在数据成员的说明前面加上关键字“static”,则该成员成为静态数据成员。 静态数据成员被该类的所有对象共享。无论建立多少个该类的对象,都只有一个静态数据的存储空间。 静态数据成员属于类,而不属于对象。 静态函数成员:成员函数也能被说明为静态的。 静态数据成员一般通过静态成员函数来访问。
假设类ClassA包括一个普通数据成员x和一个静态成员static_x,假设定义了ClassA的三个对象:a,b,c,则其存储空间示意图为:假设类ClassA包括一个普通数据成员x和一个静态成员static_x,假设定义了ClassA的三个对象:a,b,c,则其存储空间示意图为:
静态数据成员的存储空间必须在类外定义,并初始化,具体的语法如下:静态数据成员的存储空间必须在类外定义,并初始化,具体的语法如下: 类型名 类名::静态数据成员 [ = 常量表达式 ]; • 静态数据成员也分为公有和私有的,公有的静态数据成员访问方式为:className::staticMember • 与静态数据成员一样,静态成员函数属于类而不是某个类对象(没有this指针)。 因此,在类外调用一个公有静态成员函数,不需要指明对象或指向对象的指针。其访问方式为: 类名::静态公有成员函数名(参数列表)
例:仓库管理中,可以使用static数据表示总重量。例:仓库管理中,可以使用static数据表示总重量。 #include <iostream> using namespace std; class Goods { private: float fWeight; static float fTotalWeight; public: void AddGoods(float weight) { fWeight = weight; fTotalWeight += weight; } void RemoveGoods( ) { fTotalWeight -= fWeight; } void ShowWeight( ) { cout << fWeight << endl; }
void ShowTotalWeight( ) // static void ShowTotalWeight( ) { cout << fTotalWeight << endl; } };//类定义结束 float Goods::fTotalWeight = 0.0; //static 成员的定义及初始化 void main( ) { Goods goods1, goods2, goods3; goods1.AddGoods(100); goods2.AddGoods(70); goods3.AddGoods(150); goods2.ShowWeight( ); goods2.ShowTotalWeight( ); goods1.ShowTotalWeight( ); //Goods::ShowTotalWeight( ); } 输出结果为?
class Point{ private: int x, y; static int nPtCount; public: Point(int intx = 0, int inty = 0); ~Point( ); //Point(const Point &p); static void PrintCount(const char *msg); }; 统计一个类在程序运行过程中一共实例化出多少个对象。可以在类中加入一个静态变量,并在该类的构造函数中自增该变量,然后在析构函数中减计数。
Point::Point(int intx = 0, int inty = 0) { x = intx; y = inty; nPtCount++; PrintCount("In constructor..."); } Point:: ~Point( ) { nPtCount--; PrintCount("After object destroyed..."); } /* Point::Point(const Point &p) { x = p.x; y = p.y; nPtCount++; PrintCount("In Copy constructor..."); }*/
static void Point::PrintCount(const char *msg) { cout << msg << endl; cout <<"Point count = "<<nPtCount << endl; } int Point::nPtCount = 0; Point fun(Point p) { Point::PrintCount("In fun()..."); return p; } void main( ) { Point pt1, pt2; pt2 = fun(pt1); // pt2 = pt1; pt2.PrintCount("After point2 created..."); }
友元 • 一个对象的私有数据,只能通过成员函数进行访问,这是一堵不透明的墙。 • 这种限制性的用法给两个类必须共享同一函数的情形带来了较大的开销。 • 出于效率(而非技术上必须)考虑,C++提供了一种辅助手段,允许外面的类或函数去访问一个类的私有数据。 • 类X的友元可以是一个函数(或是一个类) 它不是X的成员函数, 但能访问X的私有成员和保护段的成员。
class INTEGER { private: int num; public: void set(int n){ num=n;} }; void Print( INTEGER obj) { cout<<obj.num;} // 错误 void mian( ) { INTEGER INTobj; INTobj.set(100); Print(INTobj); …}
class INTEGER { private: int num; public: void set(int n){ num=n;} friend void Print(INTEGER ); //友元函数 }; void Print( INTEGER obj) { cout<<obj.num;} // ok void mian( ) { INTEGER INTobj; INTobj.set(100); Print(INTobj); …}
友元类 class Printer; class INTEGER { private: int num1; friend Printer; //友元类 … }; //类Printer的成员函数全都是类INTEGER的友元函数; 可以访问类INTEGER的任何成员。 友元函数 友元函数不属于类,友元函数没有this 指针 这是友元函数与成员函数的主要区别。 友元函数的声明可以放在类的任何段里。
友元具有如下特性: 非传递性。即A是B的友元,B是C的友元,但A不一定是C的友元(除非将A声明 为C的友元); 非对称性。即A是B的友元,但B不一定是A的友元(除非将B声明为A的友元)。 友元是C++提供的一种破坏数据封装和数据隐藏的机制。为了确保数据的完整性,及数据封装与隐藏的原则,建议尽量不使用或少使用友元。
与类和对象相关的问题 1 、类类型做参数类型 • 由于类是一个数据类型,可以将对象作为参数传递给函数 • 参数传递遵循传值(或传地址)的方式,这同所有其他的数据类型是相同的。 • 类类型做形参类型,一般有3种方式: • 对象本身做参数(传对象值) • 对象引用做参数(传地址) • 对象指针做参数(传指针值) 49
class OBJ { int num; public: void set_num(int x) {num=x;} void out_num( ) { cout<<num<<‘ ’;} }; void fun(OBJ objx) { objx.out_num( ); objx.set_num(100); objx.out_num( ); } void main( ) { OBJ obj; obj.set_num(10); fun(obj); obj.out_num( ); } 结果为 10 100 10