(C/C++学习笔记) 十七. 面向对象程序设计
十七. 面向对象程序设计
● 面向对象程序设计的基本概念
※ 类实际上是一种复杂的数据类型,它不仅包含不同类型的数据,还包含对这些数据的一些必要的操作. 而对象则是这种复杂的数据类型的一个变量. 类是抽象的,对象是具体的,一个对象是某个类的一个具体实例(instance);如同动物和鱼类/人类......的关系. ※ 在面向对象的方法中把对象发出的服务请求称为消息。 消息有不同的实现方式,如函数调用、程序间的内部通信、各种事件的发生和响应等。也就是说, 消息不能简单地等同于对象的成员函数调用,事实上两者之间是有区别的:消息是表示对象间信息传递的抽象概念,而对象的成员函数调用只是消息在程序设计中的具体表现形式之一. ※ 数据成员: 类中声明的变量; 成员函数: 类中操作变量的函数 ※ 面向对象程序设计的基本特点 1. 抽象: 数据被抽象为类的数据成员, 数据成员的行为被抽象为类的成员函数. 2. 封装: 将数据变量和成员函数进行有机的结合 3. 继承: 以旧类为基础创建新类, 新类包含了旧类的数据成员和成员函数, 并且可以在新类中添加新的数据成员和成员函数 4. 多态: (一个接口, 多种实现)一段程序处理多种类型对象的能力, 多态性能让具有不同内部结构的对象共享同样的外部接口(external interface). C++的多态性包括: ①强制多态—隐式或显式的数据类型转换; ②重载多态—函数重载 & 运算符重载 ※ 强制多态和重载多态属于表面多态性 ③包含多态—虚函数 ④类型参数化多态—函数模板和类模板
C++中的多态(虽然多态不是C++所特有的,但是C++中的多态确实是很特殊的)分为静多态和动多态(也就是静态绑定和动态绑定两种现象)。 静动的区别主要在于这种绑定发生在编译期还是运行期,发生在编译期的是静态绑定,也就是静多态;发生在运行期的则是动态绑定,也就是动多态。 静多态可以通过模板和函数重载来实现。 动多态则是通过继承、虚函数(virtual)、指针来实现。 |
● 数据成员&成员函数
数据成员: 类定义中的数据成员描述了类对象所包含的数据类型,数据成员的类型可以是C++基本数据类型,也可以是构造数据类型。 成员函数: 作为类成员的成员函数描述了对类中的数据成员实施的操作。 类的内部结构 internal structure = 数据成员 data member = 成员变量 member variable = 属性 attribute = 域 field; 类的接口 interface of class = 成员函数 member function = 行为 behavior = 方法 method |
● 类的声明, 类的定义/类的实现, 对象引用的方式有两种
类的声明----主要说明类包括哪些数据成员和成员函数 类的定义/类的实现----主要讲类的成员函数应该放置在何处: 在类体内 || 在类体外(需使用作用域解析运算符scope resolution operator) 对象引用的方式有两种: ① 成员引用的方式; ② 对象指针方式 |
//类的声明 class 类名 { public: 公有数据成员或公有函数成员的定义; protected: 保护数据成员或保护函数成员的定义; private: 私有数据成员或私有函数成员的定义; }; //在类外定义函数体的格式如下: 返回值类型 类名 :: 成员函数名(形参表) { 函数体; } //建立对象的格式如下: 类名 对象名; //其中,对象名可以是简单的标识符,也可以是数组 //存取对象中的数据成员, 调用成员函数, 语法如下: 对象名.属性 对象名.成员函数名(实参1, 实参2,…,) //通过设置成员的存取控制属性, 使对类成员的存取得到控制,从而达到了信息隐藏的目的。C++的存取控制属性有:公有类型(public)、私有类型(private)和保护类型(protected)。 |
#include <iostream> using namespace std; class Person //声明了一个名为Person的类类型(class type), 对象就是类类型的一个变量。 { public: int i; //数据成员 int j; void Show1() //成员函数(在类体内定义) { cout<<"i="<<i<<endl; } void SetValue(int a) //成员函数 { j=a; } void Show2() //成员函数 { cout<<"j="<<j<<endl; } }; void main() { CPerson p; //声明一个对象p p.i=2; //引用数据成员 p.Show1(); p.SetValue(3); //引用成员函数 p.Show2(); //引用成员函数 cout<<"j="<<p.j<<endl; //引用数据成员 } |
#include <iostream> using namespace std; class CPerson //类的声明 { public: int i; //数据成员 int j; void Show1() //成员函数(在类体内定义) { cout<<"i="<<i<<endl; } void SetValue(int a) //成员函数 { j=a; } void Show2() //成员函数 { cout<<"j="<<j<<endl; } }; void main() { Person *p; //声明一个对象指针p p=new Person(); //必须用开辟一个内存空间的方法将对象的指针p初始化, 否则虽然编译不报错, 但结果会出错 //关键字new后面的CPerson()是类的默认构造函数, 因为这个构造函数无参数, 故可将()省略 //将一个对象的指针初始化的形式----类名 *对象指针名 = new 类名(); p->i=2; //引用数据成员 p->Show1(); //引用成员函数 (*p).Show1(); //等同于p->Show1(); p->SetValue(3); //引用成员函数 p->Show2(); //引用成员函数 cout<<"j="<<p->j<<endl; //引用数据成员 delete p; p=NULL; } |
//上面是在类体内定义成员函数, 这里在类体外定义, 需使用到作用域解析运算符: #include <iostream> using namespace std; class CPerson //类的声明 { public: int i; //数据成员 int j; void Show1(); //成员函数 void SetValue(int a); //成员函数 void Show2(); //成员函数 }; void CPerson::Show1() //成员函数(在类体外定义) { cout<<"i="<<i<<endl; } void CPerson::SetValue(int a) //成员函数 { j=a; } void CPerson::Show2() //成员函数 { cout<<"j="<<j<<endl; } void main() { CPerson *p; //声明一个对象指针p p=new CPerson(); p->i=2; //引用数据成员 p->Show1(); //引用成员函数 (*p).Show1(); //等同于p->Show1(); p->SetValue(3); //引用成员函数 p->Show2(); //引用成员函数 cout<<"j="<<p->j<<endl; //引用数据成员 delete p; p=NULL; } |
● 对象指针
类相当于一种包含函数的自定义数据类型,它不占内存,是一个抽象的"虚"体,使用已定义的类建立对象就像用数据类型定义变量一样。对象建立后,对象占据内存,变成了一个"实"体。 对象如同一般变量,占用一块连续的内存区域,因此可以使用一个指向对象的指针来访问对象,即对象指针,它指向存放该对象的地址。可用类来定义对象指针变量,通过对象指针来访问对象的成员。 对象指针遵循一般变量指针的各种规则,其语法定义形式如下: 类名 *对象指针名; 如同通过对象名访问对象的成员一样,使用对象指针也只能访问该类的公有数据成员和函数成员,但与前者使用"."运算符不同,对象指针采用"->"运算符访问公有数对象指针名->数据成员名 对象指针名->数据成员名 或: 对象指针名->成员函数名(参数表) |
例如: Clock C1(8,0,0); Clock *Cp; Cp=&C1; Cp->ShowTime(); |
● 对象指针作形参
在C++中,对象指针可以作为成员函数的形参,一般而言,使用对象指针作为函数的参数要比使用对象作为函数的参数更普遍一些,因为使用对象指针作为函数的参数有如下两点好处: (1) 实现地址传递。 通过在函数调用时将实参对象的地址传递给形参指针对象,使形参指针对象和实参对象指向同一内存地址,这样,对象指针所指向对象的改变也将同样影响着实参对象,从而实现信息的双向传递。 (2) 使用对象指针效率高 使用对象指针传递的仅仅是对应实参对象的地址,并不需要实现对象之间的副本拷贝,这样就会减小时空开销,提高运行效率。 |
//时间加法。时间加法有两种, 一种是时钟加秒数, 另一种是时钟加时、分、秒。采用重载函数实现这两种加法。 #include <iostream> using namespace std; class Clock { private: int H,M,S; public: void SetTime(int h,int m,int s) { H=h,M=m,S=s; } void ShowTime() { cout<<H<<":"<<M<<":"<<S<<endl; } Clock(int h=0,int m=0,int s=0) { H=h,M=m,S=s; } Clock(Clock & p) //函数重载 { H=p.H, M=p.M, S=p.S; } void TimeAdd(Clock *Cp); void TimeAdd(int h,int m,int s); void TimeAdd(int s); //函数原型声明 }; void Clock::TimeAdd(Clock *Cp) //对象指针 { H=(Cp->H+H+(Cp->M+M+(Cp->S+S)/60)/60)%24; M=(Cp->M+M+(Cp->S+S)/60)%60; S=(Cp->S+S)%60; } void Clock::TimeAdd(int h,int m,int s) //函数重载 { H=(h+H+(m+M+(s+S)/60)/60)%24; M=(m+M+(s+S)/60)%60; S=(s+S)%60; } void Clock::TimeAdd(int s) //函数重载 { H=(H+(M+(S+s)/60)/60)%24; M=(M+(S+s)/60)%60; S=(S+s)%60; } int main() { Clock C1; Clock C2(8,20,20); C1.TimeAdd(4000); C1.ShowTime(); C2.TimeAdd(&C1); C2.ShowTime(); return 0; } |
● 对象引用
对象引用就是对某类对象定义一个引用,其实质是通过将被引用对象的地址赋给引用对象,使二者指向同一内存空间,这样引用对象就成为了被引用对象的"别名"。 对象引用的定义方法与基本数据类型变量引用的定义是一样的。 定义一个对象引用,并同时指向一个对象的格式为: 类名 & 对象引用名=被引用对象; 注意: 对象引用与被引用对象必须是同类型的。 除非是作为函数参数与函数返回值,对象引用在定义时必须要初始化。 定义一个对象引用并没有定义一个对象,所以不分配任何内存空间,不调用构造函数。 对象引用的使用格式为: 对象引用名.数据成员名 或: 对象引用名.成员函数名(参数表) |
例如: Clock C1(8,20,20); Clock & Cr=C1; //定义了C1的对象引用Cr。 Cr.ShowTime(); //通过对象引用使用对象的成员 运行结果为: 8:20:20 |
对象引用的优点: 对象引用通常用作函数的参数,它不仅具有对象指针的优点,而且比对象指针更简洁,更方便,更直观。将p5_6.cpp中添加如下函数: //将上例中添加如下函数: void Clock::TimeAdd(Clock & Cr) { H=(Cr.H+H+(Cr.M+M+(Cr.S+S)/60)/60)%24; M=(Cr.M+M+(Cr.S+S)/60)%60; S=(Cr.S+S)%60; } //将C2.TimeAdd(&C1); 替换为:C2.TimeAdd(C1); //运行结果与上例一样。 |
● 对象数组
对象数组是以对象为元素的数组。对象数组的定义、赋值、引用与普通数组一样,只是数组元素与普通数组的数组元素不同。 对象数组定义格式如下: 类名 对象数组名[常量表达式n],...,[ 常量表达式2][常量表达式1]; 其中,类名指出该数组元素所属的类,常量表达式给出某一维元素的个数。 与结构数组不同,对象数组初始化需要使用构造函数完成,以一个大小为n的一维数组为例,对象数组的初始化格式如下: 初值,数据成员2初值,…), 初值,数据成员2初值,…), … 初值,数据成员2初值,…)}; 注意:不带初始化表的对象数组,其初始化靠调用不带参数的构造函数完成。 以一个m维数组为例,对象数组元素的存取格式如下: 对象数组名[下标表达式1][ 下标表达式2]…[下标表达式m].数据成员名 或: 对象数组名[下标表达式1][ 下标表达式2]…[下标表达式m].成员函数名(参数表) |
//计算一个班学生某门功课的总评成绩。 //分析: 首先设计一个类Score,这个类的数据成员为一个学生的学号、姓名、平时成绩、期末考试成绩,成员函数有求总评成绩、显示成绩。然后,定义一个对象数组存储一个班学生的成绩。最后,通过逐一调用数组元素的成员函数求每个学生的总评成绩。 #include<iostream> using namespace std; const int MaxN=100; const double Rate=0.6; //平时成绩比例 class Score { private: long No; //学号 char *Name; //姓名 int Peace; //平时成绩 int Final; //期末考试成绩 int Total; //总评成绩 public: Score(long=0,char* = NULL,int=0,int=0,int=0); //构造函数 void Count(); //计算总评成绩 void ShowScore(); //显示成绩 }; Score::Score(long no,char *name,int peace,int final,int total) //构造函数 { No=no; Name=name; Peace=peace; Final=final; Total=total; } void Score::Count() { Total=Peace*Rate+Final*(1-Rate)+0.5; } void Score::ShowScore() { cout<<No<<"\t"<<Name<<"\t"<<Peace<<"\t"<<Final<<"\t"<<Total<<endl; } int main() { Score ClassScore1[3]; //对象数组 Score ClassScore2[3]={ Score(200607001,"LiuNa",80,79), Score(200607002,"CuiPeng",90,85), Score(200607003,"ZhouJun",70,55)}; for(int i=0;i<3;i++) ClassScore2[i].Count(); for(i=0;i<3;i++) ClassScore2[i].ShowScore(); return 0; } |
● 动态对象
※ 使用类名定义的对象都是静态的,在程序运行过程中,对象所占的空间是不能随时释放的。但有时人们希望在需要用到对象时才建立对象,在不需要用该对象时就撤销它,释放它所占的内存空间以供别的数据使用。这样可提高内存空间的利用率。 动态对象: 动态对象是指编程者随时动态建立并可随时消失的对象。 建立动态对象采用动态申请内存的语句new,删除动态对象使用delete语句。 建立一个动态对象的格式为: 类名 *对象指针=new 类名(初值表); 注意: 对象指针的类型应与类名一致。 动态对象存储在new语句从堆申请的空间中。 建立动态对象时要调用构造函数,当初值表缺省时调用默认的构造函数。 |
例如: Clock *Cp; //建立对象指针 Cp=new Clock; //建立动态对象,调用默认构造函数Clock()。 Cp->ShowTime(); //结果为0:0:0 Cp=new Clock(8,0,0); //建立动态对象,调用构造函数Clock(int,int,int) Cp->ShowTime(); //结果为8:0:0 |
注意: 函数体内的局部对象在函数调用时建立,在函数调用完后消失;全局对象则在程序执行时建立,执行完成后才消失;这些对象在何时建立,何时消失是C++规定好了的,不是编程者能控制的。 |
在堆中建立的动态对象不能自动消失,需要使用delete语句删除对象,格式为: delete 对象指针; 在删除动态对象时,释放堆中的内存空间,在对象消失时,调用析构函数。 例如: delete Cp; //删除Cp指向的动态对象 动态对象的一个重要的使用方面是用动态对象组成动态对象数组,建立一个一维动态对象数组的格式为: 对象指针 = new 类名[数组大小]; 删除一个动态对象数组的格式为: delete [] 对象指针; 在建立动态对象数组时,要调用构造函数,调用的次数与数组的大小相同;删除对象数组时,要调用析构函数,调用次数与数组的大小相同。 |
//将上例改为用动态对象数组实现如下: Score::SetScore(long no,char *name,int peace,int final,int total) { No=no; Name=name; Peace=peace; Final=final; Total=total; } SetScore()函数为动态数组设置初值。 int main() { Score * ClassScore; ClassScore=new Score [3]; ClassScore[0].SetScore(200607001,"Tom",80,79), ClassScore[1].SetScore(200607002,"John",90,85), ClassScore[2].SetScore(200607003,"Wilson",70,55); for(int i=0;i<3;i++) ClassScore[i].Count(); for(i=0;i<3;i++) ClassScore[i].ShowScore(); delete [] ClassScore; return 0; } |
● 对象组合
组合概念体现的是一种包含与被包含的关系,在语义上表现为"is a part of"的关系,即在逻辑上A是B的一部分 。 在C++程序设计中,类的组合用来描述一类复杂的对象,在类的定义中,它的某些属性,往往是另一个类的对象,而不是像整型、浮点型之类的简单数据类型,也就是"一个类内嵌其它类的对象作为成员",将对象嵌入到类中的这样一种描述复杂类的方法,我们称之为"类的组合",一个含有其他类对象的类称为组合类,组合类的对象称为组合对象。 组合类定义的步骤为:先定义成员类,再定义组合类。 |
//计算某次火车的旅途时间。 小时内到达,旅途时间为到达时间减出发时间。 //用空方框表示类,灰框表示对象,组合类可以表示为空框包含灰框。设计TrainTrip类的示意图与成员构成图如图7-4: |
#include <iostream> using namespace std; class Clock //Clock类 { private: int H,M,S; public: void ShowTime() { cout<<H<<":"<<M<<":"<<S<<endl; } void SetTime(int H=0,int M=0,int S=0) { this->H=H, this->M=M, this->S=S; } Clock(int H=0,int M=0,int S=0) { this->H=H, this->M=M, this->S=S; } int GetH() { return H; } int GetM() { return M; } int GetS() { return S; } }; class TrainTrip //组合类 { private: char *TrainNo; //车次 Clock StartTime; //出发时间 Clock EndTime; //到达时间 public: TrainTrip(char * TrainNo, Clock S, Clock E) { this->TrainNo=TrainNo; StartTime=S; EndTime=E; } Clock TripTime() { int tH,tM,tS; //临时存储小时、分、秒数 int carry=0; //借位 Clock tTime; //临时存储时间 (tS=EndTime.GetS()-StartTime.GetS())>0?carry=0:tS+=60, carry=1; (tM=EndTime.GetM()-StartTime.GetM()-carry)>0?carry=0:tM+=60, carry=1; (tH=EndTime.GetH()-StartTime.GetH()-carry)>0?carry=0: tH+=24;_ tTime.SetTime(tH,tM,tS); return tTime; } }; int main() { Clock C1(8,10,10), C2(6,1,2); //定义Clock类的对象 Clock C3; //定义Clock类对象,存储结果 TrainTrip T1("K16",C1,C2); //定义TrainTrip对象, 组合对象及其初始化 C3=T1.TripTime(); C3.ShowTime(); return 0; } |
C++为组合对象提供了初始化机制:在定义组合类的构造函数,可以携带初始化表,其格式如下 建立对象时,调用组合类的构造函数;在调用组合类的构造函数时,先调用各个成员对象的构造函数,成员对象的初值从初始化列表中取得。这样,实际上是通过成员类的构造函数对成员对象进行初始化,初始化值在初始化表中提供。 为组合类定义了带初始化表的构造函数后,在建立组合对象同时为对象提供初值的格式如下: 类名 对象名(实参表); |
//使用初始化列表,将p5_8.cpp修改成p5_8a.cpp其中的构造函数修改如下: TrainTrip(char * TrainNo, int SH,int SM, int SS, int EH, int EM,int ES): EndTime(EH,EM,ES), StartTime(SH,SM,SS) { this->TrainNo=TrainNo; } //定义组合对象的程序修改如下: int main() { Clock C3; //定义Clock类对象,存储结果 TrainTrip T1("K16",8,10,10,6,1,2); //定义TrainTrip对象 C3=T1.TripTime(); C3.ShowTime(); return 0; } |
注意: 初始化列表既不能决定是否调用成员对象的构造函数,也不能决定调用构造函数的顺序,成员对象调用顺序由成员对象定义的顺序决定。初始化列表只是提供调用成员对象构造函数的参数. |
● C++项目至少划分为3个文件
个文件: 类声明文件(.h), 类实现文件(.cpp)和主函数文件(.cpp) |
例如, 在一个test工程下, 有如下文件: ※ 头文件是.h后缀名的文件,源文件是.cpp,里边都是代码。资源文件,像声音,图片,字符串都是资源文件。 |
//头文件 class CPerson //类的声明 { public: int i; //数据成员 int j; void Show1(); //成员函数 void SetValue(int a); //成员函数 void Show2(); //成员函数 }; |
//类的实现 #include "Person.h" #include <iostream> using namespace std; void CPerson::Show1() //成员函数(在类体外定义) { cout<<"i="<<i<<endl; } void CPerson::SetValue(int a) //成员函数 { j=a; } void CPerson::Show2() //成员函数 { cout<<"j="<<j<<endl; } |
//主函数 #include "Person.h" #include <iostream> using namespace std; void main() { CPerson *p; //声明一个对象指针p p=new CPerson(); p->i=2; //引用数据成员 p->Show1(); //引用成员函数 (*p).Show1(); //等同于p->Show1(); p->SetValue(3); //引用成员函数 p->Show2(); //引用成员函数 cout<<"j="<<p->j<<endl; //引用数据成员 delete p; p=NULL; } |
//再如下面这个C++多文件组织结构图, 其中point.h是类的定义; Point.cpp是类的实现; 5_10.cpp是主函数. |
● ::的用法
The scope resolution operator :: is used to identify and disambiguate identifiers used in different scopes. The identifier can be a variable, a function, or an enumeration value. Scope resolution operator(::) is used to define a function outside a class or when we want to use a global variable but also has a local variable with same name. 在上述案例中, 如果在类体外定义类成员, 需要在类成员前加类名::成员名; 这里看::的另外一种用法 |
#include<iostream> using namespace std; int x; //global variable int main() { int x; //local variable x=50; ::x=100; //global variable cout<<"local variable x="<<x<<endl; cout<<"global variable x="<<::x<<endl; return 0; } |
● VC++中文件类型小结
.dsw---- 这种类型的文件在VC中是级别最高的,称为Workspace文件 .dsp---- 在VC中,应用程序是以Project的形式存在的,Project文件的扩展名为.dsp,在Workspace文件中可以包含多个Project,由Workspace文件对它们进行统一的协调和管理,每个工程都对应一个dsp文件 .opt---- 与dsw类型的Workspace文件像配合的一个重要的文件类型是以opt为扩展名的文件,这个文件中包含的是Workspace文件中要用大本地计算机的有关配置信息,所以这个文件不能在不同的计算机上共享。当我们打开一个Workspace文件时,如果系统找不到需要的opt类型文件,就会自动的创建一个与之配合的包含本地计算机信息的opt文件。 .clw---- 以clw为扩展名的文件是用来存放应用程序中用到的类和资源的信息,这些信息是VC中的ClassWizard工具管理和使用类的信息来源 readme.txt---- 这个文件每个应用程序都有一个,这个文件中列出了应用程序中用到的所有文件的信息,打开并查看其中的内容就可以对应用程序的文件结构有一个基本的知识 .h----- 这种文件为头文件,包含的主要是类的定义 .cpp---- 这种文件为实现文件,该种文件包含的主要是类成员函数的实现代码。一般来说,h为扩展名的文件和cpp为扩展名的文件是一一对应配合使用的 .rc---- 在VC中以.rc为扩展名的文件为资源文件,其中包含了应用程序中用的所有的Windows资源,要指出的一点是rc文件可以直接在VC集成环境中以可视化的方法进行编辑和修改 .rc2----- 也是资源文件,但这个文件中的资源不能在VC的集成环境下直接进行编辑和修改,而是由我们自己根据需要手工编辑这个文件 .ico,.bmp,.cur----- 还有一些具体的资源文件不一一列举 .exe,.dll,.fon,.mod,.drv,.ocx------ 都是所谓的动态链接库文件 |
● 构造函数
我们希望程序能像对待普通变量一样, 在分配内存空间的同时将数据成员的初始值写入, 这就需要一个初始化程序, 即构造函数, 它是一种特殊的成员函数 构造函数的函数名与类名相同, 而且没有返回值, 通常被声明为公有(public)函数. 构造函数在对象被创建的时候将被自动调用, 如果类中没有写构造函数, 编译器会自动生成一个隐含的默认构造函数, 如: CPerson() {} //虽然这个构造函数不做任何事(实际上它还是要负责基类的构造和成员对象的构造), 但建立对象时自动调用构造函数是C++程序的例行公事 |
下面通过讲述数据成员赋值的方式来理解构造函数: ①通过普通的成员函数 ②通过不带参数的构造函数 ③通过带参数的构造函数 ※ 构造函数可以为整个对象一次性赋值, 但一般的成员函数一次只能给一个成员函数赋值 |
//①通过普通的成员函数 #include <iostream> using namespace std; class Person //类的声明 { public: int i; //数据成员 void SetValue(int a); //成员函数 void Show(); //成员函数 }; void Person::SetValue(int a) //成员函数 { i=a; } void Person::Show() //成员函数 { cout<<"i="<<i<<endl; } void main() { Person p; p.SetValue(2); p.Show(); } |
//②通过不带参数的构造函数 #include <iostream> using namespace std; class Person //类的声明 { public: Person() //无参数的构造函数 { i=0; } Person(int b) //带参数的构造函数, 可与上面的无参数的构造函数同名 { i=b; } int i; //数据成员 void SetValue(int a); //成员函数 void Show(); //成员函数 }; void Person::SetValue(int a) //成员函数 { i=a; } void Person::Show() //成员函数 { cout<<"i="<<i<<endl; } void main() { Person p; p.Show(); } |
//③通过带参数的构造函数 #include <iostream> using namespace std; class Person //类的声明 { public: Person() { i=0; } Person(int b) { i=b; } int i; //数据成员 void SetValue(int a); //成员函数 void Show(); //成员函数 }; void Person::SetValue(int a) //成员函数 { i=a; } void Person::Show() //成员函数 { cout<<"i="<<i<<endl; } void main() { Person p(4); //4传给了带参数的构造函数Person p.Show(); } |
● private构造函数
△构造函数也可以定义成private函数, 但这会导致此类不能直接被外部实例化。如果需要创建对象就需要提供一个static方法来访问本身的构造函数。简单例子: class A { public: static A* createInstance() {return new A();} private: A(){} }; ※ singleton()单例设计模式会用到private构造函数 |
● 拷贝构造函数(复制构造函数)
拷贝构造函数时一个特殊的构造函数, 它用一个已知的对象初始化一个正在创建的对象, 它的一般格式为: 类名:: 类名(类名& 对象名)//形象是本类对象的引用 { … }; 类的拷贝构造函数一般由用户定义,如果用户没有定义构造函数,系统就会自动生成一个默认函数来进行对象之间的位拷贝(bitwise copy),这个默认拷贝构造函数的功能是把初始值对象的每个数据成员的值依次复制到新建立的对象中。因此,也可以说是完成了同类对象的克隆(clone),这样得到的对象和原对象具有完全相同的数据成员,即完全相同的属性。事实上,拷贝构造函数是由普通构造函数和赋值操作符共同实现的。 ※ 当一个类的对象赋值给该类的另一个对象时, 这是赋值而不是创建对象, 此时不会调用拷贝构造函数 |
#include <iostream> using namespace std; class Person //类的声明 { public: Person() { i=0; } Person(int x) { i=x; } Person(Person & copy) //x是一个对象, &x是初始化另一个对象的引用 { i=copy.i; //拷贝构造函数的函数体 } int i; //数据成员 void SetValue(int a); //成员函数 void Show(); //成员函数 }; void Person::SetValue(int a) //成员函数 { i=a; } void Person::Show() //成员函数 { cout<<"i="<<i<<endl; } void main() { Person p1(8); //8传给了带参数的构造函数Person p1.Show(); Person p2(p1); //对象p1是p的副本 p2.Show(); } |
#include <iostream> using namespace std; class Person //类的声明 { public: Person() { i=0; } Person(int x) { i=x; } int i; //数据成员 void SetValue(int a); //成员函数 void Show(); //成员函数 }; void Person::SetValue(int a) //成员函数 { i=a; } void Person::Show() //成员函数 { cout<<"i="<<i<<endl; } void main() { Person p(8); //8传给了带参数的构造函数Person p.Show(); Person p1(p); p1.Show(); } 另外, 复制构造函数也可以在类体外定义, 即: #include <iostream> using namespace std; class Person //类的声明 { public: Person() { i=0; } Person(int x) { i=x; } Person(Person &x); //声明一个拷贝构造函数, 记住要打分号 int i; //数据成员 void SetValue(int a); //成员函数 void Show(); //成员函数 }; void Person::SetValue(int a) //成员函数 { i=a; } void Person::Show() //成员函数 { cout<<"i="<<i<<endl; } Person::Person(Person &b) //定义拷贝构造函数 { i=b.i; //拷贝构造函数的函数体 } void main() { Person p(8); //8传给了带参数的构造函数Person p.Show(); Person p1(p); p1.Show(); } |
● 析构函数
析构函数: 用来完成对象被删除前的一些清理工作(housekeeping). 和构造函数一样, 它与类同名(还需在类名前加一个~), 是一种特殊的公有的成员函数, 且没有返回值, 不同的是它不接收任何参数, 不能被重载,但可以是虚函数,一个类只有一个析构函数。 例如: ~Person() {}; //在类体内, 此时空的析构函数不做任何事情, 但调用一个析构函数也是C++程序的例行公事 析构函数在对象的生命期即将结束的时刻被自动调用, 它的调用完成之后, 对象就消失了, 相应的内存空间也被释放. 例如: 在一个函数中定义了一个局部对象, 那么当这个函数运行结束返回调用者时, 函数中的对象所占用的内存空间也就被释放了. 如果程序员希望在对象被删除之前的时刻自动(不是人为进行函数调用)完成某些事情, 就可以把某些操作写在析构函数的函数体内. |
#include <iostream> using namespace std; class Person //类的声明 { public: int i; char *p; void SetValue(int a); void Show(); Person(); ~Person(); }; void Person::SetValue(int a) //成员函数 { i=a; } void Person::Show() //成员函数 { cout<<"i="<<i<<endl; cout<<"*p="<<*p<<endl; } Person::Person() //定义构造函数 { i=6; 存放到该内存单元中, 如果是[8]表示分配八个字节 } Person::~Person() {} //定义析构函数,()和{}都不能省略, 其函数体为空 void main() { Person obj1; //自动调用上面无参的构造函数, 不能带空括号() obj1.Show(); Person obj2(obj1); obj2.Show(); //因为析构函数为空, 对象obj1实际上没有被删除, 因次我们还可以利用赋值构造函数(编译器自动生成的) } |
#include <iostream> using namespace std; class Person //类的声明 { public: int i; char *p; void SetValue(int a); void Show(); Person(); ~Person(); }; void Person::SetValue(int a) //成员函数 { i=a; } void Person::Show() //成员函数 { cout<<"i="<<i<<endl; cout<<"*p="<<*p<<endl; } Person::Person() //定义构造函数 { i=6; 存放到该内存单元中, 如果是[8]表示分配八个字节 } Person::~Person() { delete p; p=NULL; //释放指针p指向的在堆区的内存(注意, p在栈区), 然后让系统回收该内存(即这块内存可被再次分配); 并将指针p赋值为空, 防止其变为野指针 //至于另外一个普通变量i, 因为它在栈区, 编译器会将其自动释放, 然后让系统回收该内存 } void main() { Person obj1; //自动调用上面无参的构造函数, 不能带空括号() obj1.Show(); Person obj2(obj1); obj2.Show(); //此时, 对象obj1已被删除, 不能被复制, 故出错 } |
#include <iostream> using namespace std; class Person //类的声明 { public: int i; char *p; void SetValue(int a); void Show(); Person(); //第一个无参构造函数 Person(int x,char *y); //第二个同名的带参构造函数 ~Person(); }; void Person::SetValue(int a) //成员函数 { i=a; } void Person::Show() //成员函数 { cout<<"i="<<i<<endl; cout<<"p="<<p<<endl; } Person::Person() { i=6; p=new char('a'); } Person::Person(int x,char *y) //定义构造函数 { i=x; p=new char[8]; //分配八个char类型的空间 p=strcpy(p, y); //也可以这样定义函数体: p=y; 但此时在下面的析构函数中就不能写delete p; 否则报错, 因为此时没有在在堆分配堆空间 } Person::~Person() //定义析构函数 { delete p; p=NULL; } void main() { Person p(8,"abc"); //调用带参数的构造函数, 不管同名的不带参数的构造函数(这是" 构造函数的重载"问题), 8和字符串"abc"传给了带参数的构造函数Person p.Show(); } |
● 浅拷贝(shallow copy)和深拷贝(deep copy)
//先看如下案例: #include <iostream> using namespace std; class String { private: char *Str; int len; public: void ShowStr() { cout<<"string:"<<Str<<",length:"<<len<<endl; } String() //构造函数 { len=0; Str=NULL; } String(const char *p) //构造函数重载 { len=strlen(p); Str=new char[len+1]; strcpy(Str,p); } ~String() //析构函数 { if (Str!=NULL) { delete []Str; Str=NULL; } } }; int main(void) { char s[]="ABCDE"; String s1(s); String s2("123456"); s1.ShowStr(); s2.ShowStr(); return 0; } |
在默认的拷贝构造函数中,拷贝的策略是直接将原对象的数据成员值依次拷贝给新对象中对应的数据成员,那么我们为何不直接使用系统默认的拷贝构造函数,何必又自己定义一个拷贝构造函数呢?但是,有些情况下使用默认的拷贝构造函数却会出现意想不到的问题。 例如,使用程序p5_3.cpp中定义的String类,执行下列程序系统就会出错: void main() { String s1("123456"); String s2=s1; } 为什么会出错呢?程序中首先创建对象s1, 为对象s1分配相应的内存资源,调用构造函数初始化该对象,然后调用系统缺省的拷贝构造函数将对象s1拷贝给对象s2,这一切看来似乎很正常,但程序的运行却出现异常, 如下图: 原因在于默认的拷贝构造函数实现的只能是浅拷贝,即直接将原对象的数据成员值依次拷贝给新对象中对应的数据成员,并没有为新对象另外分配内存资源。这样,如果原对象的数据成员是指针,两个指针对象实际上指向的是同一块内存空间。 当执行String s2=s1时,默认的浅拷贝构造函数进行的是下列操作: s2.len=s1.len; s2.Str=s1.Str; "s2.Str=s1.Str;"这一语句实际上是将s1.Str的地址赋给了s2.Str,但并没有为s2.Str另外分配一个让它指向的内存,执行String s2=s1;后,首先对象s2析构,释放了最初是s1.Str指向的内存,然后对象s1析构,由于s1.Str 和s2.Str所占用的是同一块内存,而同一块内存不可能释放两次,所以当对象s1析构时,程序出现异常,无法正常执行和结束。 由此可见,在某些情况下,浅拷贝会带来数据安全方面的隐患。 当类的数据成员中有指针类型时,我们就必须定义一个特定的拷贝构造函数,该拷贝构造函数不仅可以实现原对象和新对象之间数据成员的拷贝,而且可以为新的对象分配单独的内存资源,这就是深拷贝构造函数。 |
#include <iostream> using namespace std; class String { private: char *Str; int len; public: void ShowStr() { cout<<"string:"<<Str<<",length:"<<len<<endl; } String() //构造函数 { len=0; Str=NULL; } String(const char *p) //构造函数重载 { len=strlen(p); Str=new char[len+1]; strcpy(Str,p); } String(String & r) //拷贝构造函数 { len=r.len; //对象的引用的len成员的值赋值给len if(len!=0) { Str=new char[len+1]; //strlen返回的都是字符数目, 字符串在结尾处有一个 '\0' 用于标记末尾, 所以这里要分配[len+1]个char类型的空间 strcpy(Str,r.Str); } } ~String() //析构函数 { if (Str!=NULL) { delete []Str; Str=NULL; } } }; int main(void) { String s1("123456"); String s2=s1; s1.ShowStr(); s2.ShowStr(); return 0; } |
注意: ●在重新定义拷贝构造函数后,默认拷贝构造函数与默认构造函数就不存在了, ●如果在此时调用默认构造函数就会出错。 ●在重新定义构造函数后,默认构造函数就不存在了,但默认拷贝构造函数还存在。 ●在对象进行赋值时,拷贝构造函数不被调用。此时进行的是结构式的拷贝。 |
● 综合案例
#include <iostream> using namespace std; #include <stdlib.h> #include <string.h> /////////////////////////////////////// class Person { public: Person(int index,short age,double salary);//带参构造函数 Person(); Person(Person & copy);//复制构造函数 int index; short age; double salary; char *message; int GetIndex(); short GetAge(); double GetSalary(); char* ShowMessage(); }; //带参构造函数的定义 Person::Person(int x,short y,double z) //成员函数的形参名绝对不能和数据成员名相同 { index=x; age=y; salary=z; } //无参构造函数的定义 Person::Person() { message=new char[30]; } //复制构造函数 Person::Person(Person & copy) { index=copy.index; age=copy.age; salary=copy.salary; } /* 析构函数 Person::~Person() { delete message; message=NULL; }*/ //不能用这个析构函数, 因为析构函数会应用与所有对象, 但下面的对象p1和p2都没有message指针, 所以会出错 /////////////////////////////////////// short Person::GetAge() { return age; } int Person::GetIndex() { return index; } double Person::GetSalary() { return salary; } char* Person::ShowMessage() { strcpy(message, "Hello!"); return message; //也可以写成: return strcpy(message, "Hello!"); //还可以在此写: strcpy(message, "Hello!"); cout<<message<<endl; 然后主函数写p3.ShowMessage(); } /////////////////////////////////////// void main() { Person p1(20,30,100); Person p2(p1); cout << "index of p1 is:" << p1.GetIndex() << endl; cout << "age of p1 is:" << p1.GetAge() << endl; cout << "salary of p1 is:" << p1.GetSalary() << endl; cout << "index of p2 is:" << p2.GetIndex() << endl; cout << "age of p2 is:" << p2.GetAge() << endl; cout << "salary of p2 is:" << p2.GetSalary() << endl; Person p3; //使用无参构造函数, 不能带空括号() cout << "message of p3 is:" << p3.ShowMessage() << endl; cout << "index of p3 is:" << p3.GetIndex() << endl; } |
● 再一案例
#include <iostream> using namespace std; #include <stdlib.h> #include <string.h> /////////////////////////////////////// class Person { public: Person(int x,short y,double z, char *w);//带参构造函数 Person(); Person(Person & copy);//复制构造函数 int index; short age; double salary; char *message; int GetIndex(); short GetAge(); double GetSalary(); char* ShowMessage(); ~Person(); }; //带参构造函数的定义 Person::Person(int x,short y,double z, char *w) //成员函数的形参名绝对不能和数据成员名相同 { index=x; age=y; salary=z; message=new char[30]; message=strcpy(message, w); } //无参构造函数的定义 Person::Person() { message=new char[30]; } //复制构造函数; 类似这种对象的完全复制, 我们可以不定义这个复制构造函数, 除非我们只想赋值一个类的若干成员变量 Person::Person(Person & copy) { index=copy.index; age=copy.age; salary=copy.salary; message=copy.message; } //析构函数 Person::~Person() { delete message; message=NULL; } //如果要用这个析构函数, 就必须想主函数中那样把有关赋值对象的代码注释掉, 否则出错 /////////////////////////////////////// short Person::GetAge() { return age; } int Person::GetIndex() { return index; } double Person::GetSalary() { return salary; } char* Person::ShowMessage() { return message; } /////////////////////////////////////// void main() { Person p1(20,30,100, "Hello"); //Person p2(p1); cout << "index of p1 is:" << p1.GetIndex() << endl; //p1.GetIndex()等价于p1.index cout << "age of p1 is:" << p1.GetAge() << endl; cout << "salary of p1 is:" << p1.GetSalary() << endl; cout << "message of p1 is:" << p1.ShowMessage() << endl; /*cout << "index of p2 is:" << p2.GetIndex() << endl; cout << "age of p2 is:" << p2.GetAge() << endl; cout << "salary of p2 is:" << p2.GetSalary() << endl; cout << "message of p2 is:" << p2.ShowMessage() << endl;*/ } |
● 类的作用域
类的作用域简称类域,它是指在类的定义中由一对花括号所括起来的部分。每一个类都具有该类的类域,该类的成员局部于该类所属的类域中。 在类的定义中可知,类域中可以定义变量,也可以定义函数。从这一点上看类域与文件域很相似。但是,类域又不同于文件域,在类域中定义的变量不能使用auto,register和extern等修饰符,只能用static修饰符,而定义的函数也不能用extern修饰符。另外,在类域中的静态成员和成员函数还具有外部的连接属性。 文件域中可以包含类域,显然,类域小于文件域。一般地,类域中可包含成员函数的作用域。 |
● 类成员的三种属性
类成员的三种属性(表示对外可见或不可见):
① public属性的成员: 在外可见, 在内可见, 可被该类的对象访问 ② private属性的成员: 在外不可见, 在内可见(类成员只能在类作用域内被访问), 在派生类不可见, 不可被该类的对象访问, 但可通过该类的public成员函数间接访问 ③ protected属性的成员: 在外不可见, 在内可见, 在派生类可见, 不可被该类的对象访问, 但可通过该类的public成员函数间接访问 ※ 类成员如果没有加任何关键字, 则其默认属性为private |
#include <iostream> using namespace std; class Person { public: int a; Person() { a=12; b=13; c=14; } void SetValue(int x); void Show(); //注意, 一个关键字的作用域是该关键字和下一个关键字之间的区域 private: int b; protected: int c; int d; }; void Person::SetValue(int x) { a=x; } void Person::Show() { cout<<a<<endl; cout<<b<<endl; cout<<c<<endl; } void main() { Person p; p.Show(); //对象p不可直接访问私有成员变量, 但可通过 //p.c=2; //c是私有数据成员, 编译器会警示: cannot access protected member declared in class 'Person' //cout<<"p.c="<<p.c,,endl; } //注意: 虽然SetValue(int x)和Show()都在类体外定义, 但由于::的作用, 它们仍在类作用域内, 所以可以类体内的数据成员可以被SetValue(int x)和Show()访问. |
● 内联成员函数
内联成员函数: 在调用内联成员函数的地方直接插入该函数的代码. |
静态类成员: 静态数据成员 & 静态成员函数 静态数据成员和非静态数据成员的区别: 如果某一个对象修改了静态数据成员, 其它对象的静态数据成员(实际上是同一个静态数据成员, 即多个对象会共享同一个静态数据成员)也将改变. 静态成员函数和非静态成员函数的区别: 一个静态成员函数不与任何对象相联系,故不能对非静态成员(即普通数据成员)进行默认访问. 它们的根本区别在于静态成员函数没有指向当前对象的this指针,而非静态成员函数有一个指向当前对象的this指针. ※ 静态数据成员可以是公有或私有的, 静态成员函数也可以是公有或私有的, 但一般是公有的, 私有的静态成员函数一般没什么用处. |
#include <iostream> using namespace std; class Book { public: static unsigned int size; //定义一个公有静态数据成员 }; unsigned int Book::size=10; //初始化静态数据成员 void main() { Book book1; Book book2; Book book3; Book::size=16; //① 通过类名为静态数据成员赋值; 假设所有图书的大小都是16开 cout<<book1.size<<endl; cout<<book2.size<<endl; cout<<book3.size<<endl; book1.size=32; //② 通过某一个对象名为静态数据成员赋值; 如果仅仅改变book1的size的大小, book2和book3的size会跟着一起变化 cout<<book1.size<<endl; cout<<book2.size<<endl; cout<<book3.size<<endl; } |
● 利用静态数据成员来就算有多少个对象
#include <iostream> using namespace std; class Person //声明了一个名为Person的类类型(class type), 对象就是类类型的一个变量。 { public: static int i; Person() { i++; } void SetValue(int a); void Show(); }; void Person::SetValue(int a) { i=a; } void Person::Show() //成员函数 { cout<<"There is(are) "<<i<<" object(s)."<<endl; } int Person::i=0; //在类体外对静态成员进行赋值 void main() { Person p1; p1.Show(); Person p2; p2.Show(); } |
● 静态数据成员
静态成员函数只能直接访问静态数据成员. |
#include <iostream> using namespace std; class Point { public: Point(int xx=0, int yy=0, int zz=0) { X=xx; Y=yy; Z=zz; count++; } static void Display(Point &p); //公有静态成员函数 private: int X,Y,Z; static int count; //私有静态数据成员 }; void Point::Display(Point &p) //不能加static关键字 { cout<<p.X<<','<<p.Y<<','<<p.Z<<endl; cout<<count<<endl; //静态数据成员只可以直接访问静态数据成员, 不过也可以借助对象名来间接访问 //也就是说上面的"p.X"不能写成"X" //"count"虽然可以写成"p.count", 但没必要 } int Point::count=0; //不能加static关键字 void main() { Point p1(1,2,3), p2(4,5,6); Point::Display(p1); Point::Display(p2); } |
● this指针
※ 一个类的每个对象都有自己的数据成员, 但是其成员函数却是每个对象共享的. 那么当一个对象调用共享的成员函数时, 这个成员函数如何找到这个对象的数据成员而不是其它对象的数据成员呢? 答案是隐藏的this指针. 隐藏的this指针:
实际上,当一个对象调用其成员函数时,编译器先将该对象的地址赋给this指针,然后调用成员函数,这样成员函数对对象的数据成员进行操作时,就隐含使用了this指针。
简而言之: 当一个类的对象调用其非静态成员函数时, 会有一个隐藏的this指针作为该成员函数的第一个参数, 并指向这个对象. |
#include <iostream> using namespace std; class Person { public: int i; void SetValue(int a); void Show(); }; void Person::SetValue(int a) { i=a; } void Person::Show() { cout<<this->i<<endl; //在普通(非静态)成员函数中访问数据成员, 相当于cout<<i<<endl; //死记:因为this在Person类内, 所以它是Person*型的指针, 并且它只能指向Person类的对象. } void main() { Person p; p.SetValue(2); p.Show(); } |
注意1: //例如: void Clock::SetTime (int h, int m, int s) { H=h, M=m, S=s; //H,M和S是假设的数据成员 this->H=h, this->M=m, this->S=s; (* this).H=h, (* this).M=m, (* this).S=s; } // 上面三条语句是等效的 注意2: void Person::Show() { cout<<this->i<<endl; //上面这段代码在编译后成为: void Person::Show(Person *this) //添加Show()成员函数的隐藏的参数, 即this指针, 它指向某一对象, 其类型为Person* { cout<<this->i<<endl; } 注意3: 在对象调用成员函数时, 对象的地址会传到成员函数中, 如p.Show();经过编译器的解释会变为: p.Show(&p) |
● 嵌套类
嵌套类: 在一个类中定义另一个类 |
● 局部类
局部类: 在一个函数中定义类; 在该函数之外, 这个类不能被访问, 因为局部类被封装在了函数的局部作用域中 |
● 友元
友元: 使类的私有成员和保护成员能够被其它类 / 类的成员函数 / 不属于类的普通函数访问 友元包括: ① 友元函数(friend function)(如果这个函数是类的函数则可称为"友元方法(friend method)"); ② 友元类(friend class)
|
//① 友元函数案例: #include <iostream.h> #include <math.h> class point { double x,y; //默认的私有数据成员 public: point(double a=0,double b=0) //定义带缺省值的构造函数 { x=a; y=b; } point(point &p); //赋值构造函数 double getx() { return x; } double gety() { return y; } friend double dist(point &p1,point &p2); //声明友元函数 //因为友元函数dist不是class类的成员函数, 不能直接调用对象(的)成员; 所以当class类的对象作dist()的形参时, 必须使用引用参数, 即这里的&p1和&p2 }; double dist(point &p1,point &p2) //定义友元函数(通常在类的外部定义), 可以访问class类的私有成员和保护成员 { return (sqrt((p1.x-p2.x)*(p1.x-p2.x)+(p1.y-p2.y)*(p1.y-p2.y))); //sqrt表示求平方根 } void main() { point ob1(1,1); point ob2(4,5); cout<<"The distance is:"<<dist(ob1,ob2)<<endl; } |
//② 友元方法案例 #include <iostream.h> #include <string.h> class boy; //如果在类定义前要使用到该类的成员函数, 需要事先声明该类, 否则报错 class girl { char *name; int age; public: girl(char *n,int a) { name=new char[strlen(n)+1]; strcpy(name,n); age=a; } void prt(boy &b); //上面是声明公有成员函数, 在下面的boy类里面被声明为友元方法; //另外, 这里是通过引用传递boy类的一个对象的值, 这个函数的形参是boy类类型的对象b的引用, 形参也可省略对象名, 即(boy &) //△这里的形参, 即对象b必须引用才行, 否则提示:BLOCK_TYPE_IS_VALID(pHead->nBlockUse); 除非把下面的boy和class类的析构函数都删掉才行, 但这样又会造成内存泄露 ~girl() { delete name; } }; class boy { char *name; int age; public: boy(char *n,int a) { name=new char[strlen(n)+1]; strcpy(name,n); age=a; } friend void girl::prt(boy &b); //声明友元方法, 这个方法在上面的girl类中, 它可以访问当前类, 即boy类的私有/保护成员 ~boy() { delete name; } }; void girl:: prt(boy &b) //定义上面已经声明的友元函数 { cout<<"girl\'s name:"<<name<<" age:"<<age<<"\n"; cout<<"boy\'s name:"<<b.name<<" age:"<<b.age<<"\n"; } void main() { girl girl_1("Lucy",15); boy boy_1("Jim",16); girl_1.prt(boy_1); } |
//③ 友元类的案例: //A是B类的友元类时, A类的所有成员函数都是B类的友元函数, 即A的所有成员函数都可以访问B的数据成员 #include <iostream.h> #include <string.h> class boy; //如果在类定义前要使用到该类的成员函数, 需要事先声明该类, 否则报错 class girl { char *name; int age; public: girl(char *n,int a) { name=new char[strlen(n)+1]; strcpy(name,n); age=a; } void prt(boy &b); ~girl() { delete name; } }; class boy { friend class girl; //声明girl类为boy类的友元类, 即girl类内的成员函数可以任意访问boy类中的成员函数 char *name; int age; public: boy(char *n,int a) { name=new char[strlen(n)+1]; strcpy(name,n); age=a; } ~boy() { delete name; } }; void girl:: prt(boy &b) { cout<<"girl\'s name:"<<name<<" age:"<<age<<"\n"; cout<<"boy\'s name:"<<b.name<<" age:"<<b.age<<"\n"; } void main() { girl girl_1("Lucy",15); boy boy_1("Jim",16); girl_1.prt(boy_1); } |
● 命名空间(namespace)
它是一个命名的范围, 程序员在这个特定的范围内创建的所有标识符都是唯一的. 定义命名空间的格式: namespace 名称 { 常量/变量/函数/类等定义 } |
#include<iostream> using namespace std; namespace myname1 //定义命名空间 { int value=10; }; namespace myname2 //定义命名空间 { int value=20; }; int value=30; //全局变量 int main() { cout<<myname1::value<<endl; //引用myname1命名空间中的变量 cout<<myname2::value<<endl; //引用myname2命名空间中的变量 cout<<value<<endl; return 0; } |
使用命名空间的格式: using namespace 命名空间名称; //不要忘记分号 |
#include<iostream> ///////////////////////////// namespace myname //定义命名空间 { int value=10; //定义整型变量 } ///////////////////////////// using namespace std; //使用命名空间std using namespace myname; //使用命名空间MyName ///////////////////////////// int main() { cout<<value<<endl; //输出命名空间中的变量 return 0; } |
一个工程内命名空间的位置: 一般来说: 在.h头文件中声明命名空间的函数, 在.cpp文件中定义命名空间中的函数. 注意: 一定要在函数外定义命名空间, 因为命名空间内才能包含类和函数 |
//.h文件 namespace Output { void Demo(); //声明函数 } ///////////////////////// //.cpp文件 #include<iostream> #include"Detach.h" using namespace std; void Output::Demo() //定义函数 { cout<<"This is a function!\n"; } ///////////////////////// //.cpp文件 #include "Detach.h" int main() { Output::Demo(); //调用函数 return 0; } |
结果为: This is a function! |
嵌套的命名空间: 在其它命名空间中定义另一个命名空间 |
#include<iostream> using namespace std; namespace Output //定义命名空间 { void Show() //定义函数 { cout<<"Output's function!"<<endl; } namespace MyName //定义嵌套命名空间 { void Demo() //定义函数 { cout<<"MyName's function!"<<endl; } } } int main() { Output::Show(); //调用Output命名空间中的函数 Output::MyName::Demo(); //调用MyName命名空间中的函数 return 0; } |
● 综合案例
#include <iostream> using namespace std; class B { public: B():data(0) //默认构造函数 { cout << "Default constructor is called." << endl; } B(int i):data(i) //带参数的构造函数 { cout << "Constructor is called." << data << endl; } B(B &b) // 复制(拷贝)构造函数 { data = b.data; cout << "Copy Constructor is called." << data << endl; } B& operator = (const B &b) //赋值运算符的重载 { this->data = b.data; cout << "The operator \"= \" is called." << data << endl; return *this; } ~B() //析构函数 { cout << "Destructor is called. " << data << endl; } private: int data; }; //函数,参数是一个B类型对象,返回值也是一个B类型的对象 B fun(B b) { return b; } //测试函数 void main() { fun(1); cout << endl; B t1 = fun(2); cout << endl; B t2; t2 = fun(3); } |
● 常对象, 常成员函数(常方法), 常数据成员
常对象, 常成员函数(常方法), 常数据成员 意义: 对于既需要共享有需要防止改变的数据应该定义为常量进行保护 定义: 由const关键字修饰的类对象, 成员函数和数据成员分别称为常对象, 常成员函数(常方法)和常数据成员 常对象: 常对象在定义时必须进行初始化, 而且不能被更新(不管是用一般成员函数(包括构造函数), 还是常成员函数, 都不能更新常对象). 定义格式: 类名 const 对象名 或 const 类名 对象名 例如: 有一个A类, 现在定义一个它的常对象: const A a(1,2); //或 A const a(1,2); //a是常对象,它不能被更新 常成员函数 定义格式: 返回值类型 成员函数名 (参数表) const 意义: ① 供常对象调用(一般对象也可以调用常成员函数), 常对象不能调用一般成员函数. ② 常成员函数只能读取同一类中的数据成员的值,而不能修改它。 常数据成员 ① 常数据成员的定义与一般常量的定义方式相同; ② 常数据成员的定义必须出现在类体中, 必须且只能通过构造函数的成员初始化列表进行初始化; ③ 与常对象一样, 常数据成员不能被更新. |
#include <iostream> using namespace std; class DATE { public: DATE(int y=2015, int m=1, int d=1) { year=y; month=m; day=d; } int GetYear() const {return year;} int GetMonth() const {return month;} int GetDay() const; /*void ShowDate() { cout<<"The date is "<<GetYear()<<':'<<GetMonth<<':'<<GetDay<<endl; } void PrintDate() const { cout<<"The date is "<<DATE::GetYear()<<':'<<DATE::GetMonth<<':'<<DATE::GetDay<<endl; }*/ private: int year, month, day; }; int DATE::GetDay() const //在类体外定义成员函数时, const关键字不能省略 { return day; //常成员函数也不能更新常对象, 例如我们这里不能写return day++; } int main() { DATE d1(2015, 7, 8); //一般对象d1可以更新 const DATE d2(2015, 9, 10); //常对象d2不可以更新 cout<<d1.GetMonth()<<endl; cout<<d2.GetMonth()<<endl; /* d1.ShowDate(); d2.ShowDate(); //错误, 因为常对象p2不能调用常成员函数ShowDate() d2.PrintDate(); //错误, 因为常对象p2不能调用常成员函数ShowDate()*/ system("pause"); } |
#include <iostream> using namespace std; class Point { public: Point(int xx=0, int yy=0) { X=xx; Y=yy; } void Move(int xOff, int yOff) { X+=xOff; Y+=yOff; } void Print() const { cout<<"The point is ("<<X<<','<<Y<<')'<<endl; } private: int X, Y; }; int main() { Point p1(1,2); const Point p2(3,4); //p2是常对象 p1.Move(5,6); //一般对象p1调用一般成员函数Move()进行更新 p1.Print(); //一般对象p1调用常成员函数Print() //p2.Move(7,8); //常对象不能调用一般成员函数 p2.Print(); //常对象调用常成员函数Print() system("pause"); return 0; } |
#include <iostream> using namespace std; class Point { public: Point(int xx=0, int yy=0) { X=xx; Y=yy; } void Print() { cout<<"The point is ("<<X<<':'<<Y<<')'<<endl; } void Print() const //const关键字可用于重载函数的区分 { cout<<"The point is ("<<X<<','<<Y<<')'<<endl; } private: int X, Y; }; int main() { Point p1(1,2); const Point p2(3,4); //p2是常对象 p1.Print(); //一般对象p1调用一般成员函数Print() p2.Print(); //一般对象p2调用常成员函数Print() system("pause"); return 0; } |
#include <iostream> using namespace std; class MyClass { public: MyClass(int i, int j); void Print() const { cout<<a<<','<<b<<endl; } private: const int a; //常数据成员 int b; //一般数据成员 }; MyClass::MyClass(int i, int j):a(i) //常成员函数后面必须通过初始化列表进行初始化 { b=j; } //普通成员函数也可以通过初始化列表初始化, 上面也可以写成MyClass::MyClass(int i, int j):a(i), b(j) {} int main() { MyClass obj1(1,2); obj1.Print(); system("pause"); return 0; } |
(C/C++学习笔记) 十七. 面向对象程序设计的更多相关文章
- python3.4学习笔记(十七) 网络爬虫使用Beautifulsoup4抓取内容
python3.4学习笔记(十七) 网络爬虫使用Beautifulsoup4抓取内容 Beautiful Soup 是用Python写的一个HTML/XML的解析器,它可以很好的处理不规范标记并生成剖 ...
- UML和模式应用学习笔记-1(面向对象分析和设计)
UML和模式应用学习笔记-1(面向对象分析和设计) 而只是对情节的记录:此处的用例场景为:游戏者请求掷骰子.系统展示结果:如果骰子的总点数是7,则游戏者赢得游戏,否则为输 (2)定义领域模型:在领域模 ...
- Lua学习笔记:面向对象
Lua学习笔记:面向对象 https://blog.csdn.net/liutianshx2012/article/details/41921077 Lua 中只存在表(Table)这么唯一一种数据结 ...
- ASP.NET MVC 学习笔记-7.自定义配置信息 ASP.NET MVC 学习笔记-6.异步控制器 ASP.NET MVC 学习笔记-5.Controller与View的数据传递 ASP.NET MVC 学习笔记-4.ASP.NET MVC中Ajax的应用 ASP.NET MVC 学习笔记-3.面向对象设计原则
ASP.NET MVC 学习笔记-7.自定义配置信息 ASP.NET程序中的web.config文件中,在appSettings这个配置节中能够保存一些配置,比如, 1 <appSettin ...
- Java学习笔记之---面向对象
Java学习笔记之---面向对象 (一)封装 (1)封装的优点 良好的封装能够减少耦合. 类内部的结构可以自由修改. 可以对成员变量进行更精确的控制. 隐藏信息,实现细节. (2)实现封装的步骤 1. ...
- 学习笔记之Java程序设计实用教程
Java程序设计实用教程 by 朱战立 & 沈伟 学习笔记之JAVA多线程(http://www.cnblogs.com/pegasus923/p/3995855.html) 国庆休假前学习了 ...
- 我的Java学习笔记-Java面向对象
今天来学习Java的面向对象特性,由于与C#的面向对象类似,不需详细学习 一.Java继承 继承可以使用 extends 和 implements 这两个关键字来实现继承. extends:类的继承是 ...
- C++ Primer笔记14_面向对象程序设计
版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/scottly1/article/details/31371611 OOP概述 面向对象程序设计(ob ...
- 《JavaScript高级程序设计》学习笔记(5)——面向对象编程
欢迎关注本人的微信公众号"前端小填填",专注前端技术的基础和项目开发的学习. 本节内容对应<JavaScript高级程序设计>的第六章内容. 1.面向对象(Object ...
随机推荐
- ExtJS使用入门
extjs是基于 yui 由 jack slocum开发, sencha是他们的公司, sencha是由三个项目合并起来的开源项目: ExtJS, jqTouch, Raphael(拉斐尔, 圣经中的 ...
- POJ 1625 Censored!(AC自动机+高精度+dp)
http://poj.org/problem?id=1625 题意: 给出一些单词,求长度为m的串不包含这些单词的个数. 思路: 这道题和HDU 2243和POJ 2778是一样的,不同的是这道题不取 ...
- selenium-chrome-headless
#coding=utf-8 from selenium import webdriver import time chrome_options = webdriver.ChromeOptions() ...
- UI 自动化测试 Macaca测试框架 安装时遇到的log
https://macacajs.github.io/zh/environment-setup macaca run -d ./macaca-test/desktop-browser-sample.t ...
- django路由转发
一.路由转发 通常,我们会在每个app里,各自创建一个urls.py路由模块,然后从根路由出发,将app所属的url请求,全部转发到相应的urls.py模块中. 例如,下面是Django网站本身的UR ...
- spring boot开发 静态资源加载不出来
spring boot 1.5 版本之前 不拦截静态资源 springboot 2.x版本 拦截静态资源 private static final String[] CLASSPATH_RESOURC ...
- 《剑指offer》第十四题(剪绳子)
// 面试题:剪绳子 // 题目:给你一根长度为n绳子,请把绳子剪成m段(m.n都是整数,n>1并且m≥1). // 每段的绳子的长度记为k[0].k[1].…….k[m].k[0]*k[1]* ...
- URLConnection和HttpClient使用入门
本讲内容:URLConnection和HttpClient使用入门 在 Android中除了使用WebView控件访问网络以外,还有用代码方式访问网络的方法,代码方式有时候会显得更加灵活.本讲会介绍使 ...
- 关于QT的QPainterPath::arcTo 详解
这个函数文档的意思就是画弧,看了文档也不太明白,自己做了demo终于明白了意思 移动到圆心,画180度半圆 void TestArcTo::paintEvent(QPaintEvent *) { QP ...
- StartCoroutine 和 StopCoroutine
我的Unity版本是2017.2.0p4(64-bit) StartCoroutine的两个版本: StartCoroutine(string methodName) StartCoroutine(I ...