类与对象
一 基础知识
1.类的定义
类是面对对象程序设计实现信息封装的基础。类是一种用户定义类型,也称类类型,每个类包含数据说明(成员数据)和一组操作数据或传递消息的函数(成员函数)。
类是对具有相同属性和行为的一组对象的抽象与统一描述。是用户自定义的数据类型
2.(1)类的说明语句一般形式为:
class <类名>
{
public:
共有段数据成员和成员函数;
Protected:
保护段数据成员和成员函数;
Private:
私有段数据成员和成员函数;
};
关键字private用于声明私有成员,私有成员只能在类中可见,不能在类外或派生类中使用。如果私有成员放在第一段,则可以忽略关键字private。
Protected声明保护成员。保护成员在类和它的派生类中可见。
Public 声明共有成员。共有成员是类的接口,在类中和类外可见
(2)注意事项、
类的成员可以是其他类的对象,但不能以类自身的对象作为本类的成员,而类自身的指针和引用可以作为类的成员。
类定义必须以分号“;”结束。
类与结构体的区别:
没有明确指定类成员的访问权限时,C++结构体的成员是公有的,而类的成员是私有的。
(3)数据成员和成员函数
class Date
{public:
void SetDate(int y,int m,int d);
int IsLeapYear();
void PrintDate();
private:
int year,month,day;
};
在这个类的定义中
定义了三个私有数据成员:year,month,day
成员函数在类外的定义使用作用域区分符进行说明,即以下的形式:
返回类型 类名::函数名(参数表)
则将以上的三个成员函数可以输入为:
void Date::SetDate(int y,int m,int d)
{year=y;
month=m;
day=y;
}
int Date::IsLeapYear()
{return(year%4==0&&year%100!=0)||(year%400==0);}
Void Date::PrintDate()
{cout<<year<<”.”<<month<<”.”<<day;}
成员函数有两个作用:
一是操作数据成员,包括访问和修改数据成员;
二是用于协同不同的对象操作,称为传递消息
当然成员函数也可以在函数类内实现
class Coord{
public:
void setCoord(int a,int b)
{ x=a; y=b;}
int getx()
{ return x;}
int gety()
{ retrun y;}
private:
int x,y;
};
在类内定义的函数被默认成内联函数,
内联函数用inline修饰,它的作用是减少频繁调用小子程序的运行的时间开销
重载函数
函数名相同,但参数不相同(类型不同,或者个数不同)的一组函数。
它的作用是用来处理不同数据类型的相似功能的任务
例如以下coding
#include<iostream>
using namespace std ;
int abs ( int a ) ;
double abs ( double f ) ;
int main ()
{ cout<< abs ( -5 ) << endl;
cout << abs ( -7.8 )<< endl ;
}
int abs ( int a )
{ return a < 0 ? -a : a; }
double abs ( double f )
{ return f < 0 ? -f : f ; }
这里定义的abs函数,用来处理了两种数据类型的(int型和double型)绝对值,这样就可以利用重载函数;
3.对象
对象是类的实例或实体。
类与对象的关系,如同C++基本数据类型和该类型的变量之间的关系。
定义的基本格式为:
类名 对象名1,对象名2,对象名3,,,,,
比如以下定义的点类:
class Point
{
public:
voidInitPoint(float PointA_x, float PointA_y);
voidMove(float New_x, float New_y);
floatGetPointx();
floatGetPointy();
private:
float P1_x,P1_y;
};
int main()
{
Point p1,p2;
}
在主函数中就定义了两个Point类的对象p1和p2
那么,对象该如何调用类中的成员函数?
有两种方法
法一:使用运算符“.” 来进行调用
例如:
#include<iostream>
using namespace std;
classTclass
{public:
Int x,y;
voidprint()
{cout<<x<<”,”<<y;}
};
Intmain()
{ Tclasstest;
test.x=100; //访问共有数据成员
test.y=200;
test.print(); //共有段成员函数
}
法二:
用指针访问对象成员
#include<iostream>
usingnamespace std;
classTclass
{public:
Int x,y;
Voidprint()
{cout<<x<<”,”<<y<<endl;
};
intadd(Tclass *ptf)
{rutern(ptf->x+ptf->y);
}
Intmain()
{Tclasstest,*pt=&test; //说明一个对象test和对象指针pt
pt->x=100; //用对象指针访问数据成员
pt->y=200;
pt->print(); //用对象指针调用成员函数
test.x=150;
test.y=450;
test.print();
cout<<”x+y=”<<add(&test)<<endl; //把对象地址传给指针参数
}
4.this指针
在调用成员函数的时候,我们会用到指针的方法,在C++中,同一类的各个对象都有自己的数据成员的存储空间,但系统不会为每个类的对象建立建立成员函数副本,类的成员函数可以被各个对象调用。例如,说明一个Tclass类的对象test,函数 调用:
test.print()
在对象test上操作。同样,若说明一个指向Tclass的指针:
Tclass *p;
则函数调用为:p->print()
在*P上操作。
但从成员函数:
voidTclass::print();
的参数上看,并不知道它正在哪个对象上操作。其实C++为成员函数提供一个称为this的隐含指针参数,所以,我们常常称成员函数拥有this指针。
当一个对象类调用类的成员函数时,对象的地址被传递给this指针,即this指针指向了该对象。This是一个隐含指针,不能被显示说明,但可以在成员函数中显示使用。
Tclass的成员函数print可以这样书写:
voidTclass::print()
{cout<<this->x<<”,”<<this->y<<endl;}
this指针的显示使用主要在运算符重载,自引用等场合。
5.构造函数与析构函数
(1)构造函数的特点:
与类名相同;
构造函数可以有任意类型的参数,但不能又返回类型;
构造函数在建立类对象时自动调用;
当用户没有自定义构造函数时,系统将自动定义一个缺省的构造函数(即一个空函数)
构造函数的作用是:
为对象分配空间;
对数据成员进行赋值;
请求其他资源;
构造函数的原型是:
类名::类名(参数表);
例如以下的程序,是为Date类建立一个构造函数
#include <iostream.h>
class Date {
public:
Date(); // 无参构造函数
Date(int y,int m,int d);
void showDate();
private:
int year, month, day;
};
Date::Date() // 构造函数的实现
{ year=0; month=0; day=0; }
Date::Date(int y,int m,int d)
{ year=y; month=m; day=d; }
inline void Date::showDate()
{ cout<<year<<"."<<month<<"."<<day<<endl;}
int main()
{
Date a_date,b_date(2014,3,25); //a_date是无参类型,b_date是带参的类型
a_date.showDate();
b_date.showDate();
return 0;
}
则这个程序的输出为
0.0.0
2014.3.25
利用构造函数创建对象,有以下两种方式:
①利用构造函数直接创建对象,其一般形式为
类名 对象名(实参表)
这里的“类名”与构造函数名相同
“实参表”是为构造函数提供的实际参数
比如 Date b_date(2014,3,25);
②利用构造函数创建对象时,通过指针和new来实现,
语法为:
类名 *指针变量=new 类名(实参表)
例如以下的程序:
intmain()
{Date*date 1;
date=newDate(1998,4,28);
cout<<”Date1 output 1:”<<endl;
date1->showDate();
deletedate 1;
return 0;
}
(2)构造函数初始化成员有两种方法
a.使用构造函数的函数体来进行初始化
b.使用构造函数的初始化列表进行初始化
a.coding
classDate
{intd,m,y;
public:
Date(intdd,int mm,int yy)
{d=dd;
m=mm;
y=yy;
}
Date(intdd,int mm)
{d=dd;
m=mm;
}
}
b.coding
funname(参数列表):初始化列表
{函数体,可以是空函数体}
初始化列表的形式:
成员名1(形参名1),成员名(形参名2)……
ClassDate
{intd,m,y;
Public:
Date(intdd,int mm,int yy):d(dd),m(mm),y(yy)
{}
Date(intdd,int mm):d(dd),m(mm)
{}
};
必须使用参数初始化列表对数据成员进行初始化的几种情况
数据成员为常量
数据成员为引用类型
数据成员为没有无参构造函数的类的对象
以下这个程序是数据成员为常量,数据成员为引用类型
#include<iostream>
usingnamespace std;
class A
{public:
A(inti):x(i),rx(x),pi(3.14)
{};
voiddisplay()
{cout<<”x=”<<x<<”rx=”<<rx<<”pi=”<<pi<<endl;}
Private:
Intx,℞
constfloat pi;
};
intmain()
{A aa(10);
aa.display();
return 0;
}
类成员初始化的顺序:也就是定义顺序
按照数据成员在类中的声明顺序进行初始化,与初始化成员列表中出现的顺序无关;
(3)构造函数的重载
class Box
{public:Box();
Box(int,int,int);
Intvolume();
Private:
intheight,width,length;
};
Box::Box()
{height=10;
width=10;
length=10;
}
Box::Box(inth,int w,int l):height(h),width(w),length(l)
{}
Int Box::volume()
{returnwidth*length*height;}
intamin()
{Boxbox();
Cout<<”Thevolume is”<<box1.volume();
Boxbox2(12,30,25);
cout<<”Thevolume is”<<box2.volume();//如果出现这样的情况 Box box3(30,25)//这样的话 第三个数为10,缺哪个那个增添为10
return 0;
}
(2)析构函数
析构函数是与构造函数相对应的,析构函数是在类名之前冠以一个波浪号“~”析构函数没有参数,也没有返回类型。析构函数在类对象作用域结束时自动调用。
基本形式是
类名::~类名()
例如以下这个程序
#include<iostream>
usingnamespace std;
class test
{ private:
int num;
float f1;
public:
test();
test(int n,float f);
~test();
};
test::test()
{num,=0;
f1=0.0;
}
test::test(int n,float f)
{num=n;
f1=f;
}
test::~test()
{cout<<”destructor isactive”<<endl;
}
int main()
{text x;
Cout<<”Exiting main”<<end;
}
执行结果为:
Exiting main
destructor is active
则析构函数的调用发生在上面这个程序的最后一条输出语句之后,当对象的生存期结束时,程序调用析构函数,并回收占用内存。
#include <cstring>
#include <iostream>
using namespace std;
class Student{
public:
Student(intn, char * a_name, char s) {
num=n;name=new char[strlen(a_name)+1];
strcpy(name,a_name); sex=s;
cout<<"Constructorcalled."<<endl;
}
~Student(){
cout<<"Destructorcalled."<<endl;
delete[]name; //delete【】是释放连续空间的功能
}
voiddisplay()
{cout<<name<<endl;cout<<num<<endl;cout<<sex<<endl; }
private:
intnum; char *name; char sex;
};
6.复制构造函数
复制构造函数要求有一个类类型的引用参数
类名::类名(const 类名&引用名,…);
为保证所引用对象不被修改,通常把引用参数说明为const参数例如:
class A
{public:
A(int);
A(consta&,int=1);
};
A a(1); //创建对象a,调用A(int)
A b(a,0); //创建对象b,调用 A(const A&,int=1)
A c=b; //创建对象c,调用A(constA&,int=1)
第一条语句创建对象a,调用一般的构造函数A(int)。第二条和第三条都调用了复制构造函数,复制对象a创建吧,复制对象b创建C,
复制构造函数的特点:
复制构造函数名与类名相同,并且也没有返回值类型。
复制构造函数可写在类中,也可以写在类外。
复制构造函数要求有一个类类型的引用参数。
如果没有显式定义复制构造函数,系统自动生成一个默认形式的复制构造函数。
复制构造函数的调用,以下几种情况由系统自动调用:
声明语句中用类的一个已知对象初始化该类的另一个对象时。
当对象作为一个函数实参传递给函数的形参时,需要将实参对象去初始化形参对象时,需要调用复制构造函数。
当对象是函数的返回值时,由于需要生成一个临时对象作为函数返回结果,系统需要将临时对象的值初始化另一个对象,需要调用复制构造函数。
7.关于深复制和浅复制
(1)浅复制
在用一个对象初始化另一个对象时,只复制了数据成员,而没有复制资源,使两个对象同时指向了同一资源的复制方式称为浅复制。
即:对于复杂类型的数据成员只复制了存储地址而没有复制存储内容
默认复制构造函数所进行的是简单数据复制,即浅复制
#include<iostream>
#include<string.h>
using namespacestd;
class Person
{
public:
Person(char* name1,int a,double s);
voiddisplay(){cout<<name<<"\t"<<age<<"\t"<<salary<<endl;}
~Person(){delete name;} //析构函数的声明
Person::Person(char*name1,int a,double s)
{
name=new char[strlen(name1)+1];
strcpy(name,name1);
age=a;
salary=s;
}
int main()
{
Person *P1=newPerson("WangWei ",8,3880); //调用构造函数创建对象P1
P1->display();
Person P2(*P1); //调用复制构造函数,用P1的数据初始化对象P2
delete P1;
P2.display();
return 0;
}
而输出的结果是
WangWei,8,3880
8,3880
出现这种情况的原因是delete P1时 把“WangWei”的资源库删了,所以不会输出
那么就要考虑到深复制
(2)深复制
通过一个对象初始化另一个对象时,不仅复制了数据成员,也复制了资源的复制方式称为深复制
深复制构造函数的特点
定义:类名::类名(【const】类名&对象名);
成员变量的处理:对复杂类型的成员变量,使用new操作符进行空间的申请,然后进行相关的复制操作
代码:
Person::Person(const Person&Po)
{name=new char[strlen(Po.name)+1];
strcpy(name,Po.name);
age=Po.age;
salary=Po.salsry;
cout<<”ff”<<endl;
}
这样的话,程序的结果就变成了
WangWei 8 3880
ff
WangWei 8 3880
8.常成员(只读不写)
(1)常数据成员是指数据成员在实例化被初始化后,其值不能改变
在类的成员函数说明后面加const关键字,则该成员函数成为常量成员函数
如果在一个类中说明了常数据成员,那么构造函数就能通过初始化列表对该数据成员进行初始化,而任何其他函数都不能对该成员赋值
例如:
const int M;
void PrintStudent() const;
(2)常对象
再说明对象时用const修饰,则被说明的对象为常对象,常对象的说明形式如下:
类名 const 对象名(参数表)
或者
const 类名 对象名(参数表);
在定义常对象时必须进行初始化,而且不能被更新
(1)C++中不允许直接或间接更改常对象的数据成员
(2)C++规定常对象只能调用它的常成员函数,静态成员函数构造函数(具有公访权限)
#include<iostream>
using namespace std;
class T_class
{public:
int a,b;
T_class(int i,int j)
{a=i;
b=j;
}
};
int mian()
{const T_class t1(1,2);
T_class t2(3,4);
//t1.a=5;
//t2.b=6;
t2.b=8;
t2.a=7;
cout<<”t1.a=”<<t1.a<<’\t’<<”t1.b=”<<t1.b<<endl;
cout<<”t2.a=”<<t2.a<<’\t’<<”t2.b=”<<t2.b<<endl;
}
结果为 t1.a=1 t1.b=2
T2.a=7 t2.b=8
常成员函数
格式:
类型说明符 函数名(参数表) const
const 是函数类型的一个组成部分,在实现函数的部分,也要带const
常成员函数不能更新对象的数据,也不能调用非const修饰的成员函数(静态构造除外)
9.静态成员
静态成员不属于某一个单独的对象,而是为类的所有对象所共有
静态成员函数的作用不是为了对象之间的沟通,而是为了能处理静态数据成员: 保证在不依赖于某个对象的情况下,访问静态数据成员数据
静态数据成员在定义或说明时前面加关键字static,如:
class A
{
intn;
static int s;
};
10.类的包含
在一个类中,使用另一个类的内容。但是遇到private时,没有访问权限。
包含的那个类是以对象的形式出现
包含类的初始化语法形式
构造函数(参数表):对象成员1(参数表),对象成员2(参数表)….
初始化顺序
先出示化包含的对象的成员,在初始化本身的成员
二,这一章的经典例题和心得总结
这一章到目前为止,就完整的写了Student这个系统
现在结合这个系统来总计我的心得
1.首先再写一个系统之前要知道自己要实现什么功能
需要哪些数据,也就是要考虑数据类,这是最基础的
比如,写Student类就要考虑 一个学生有哪些基本的信息
首先是姓名,用string类型的字符串即可解决
然后是学号,成绩,平均分,排名
好了,这就是数据类的全部成员了
那么先来定义这个类
class Student
{
string name;
int numb;
int score[3];
float average;
int order;
public:
Student(int id,string na,intx,int y,int z):name(na),numb(id)
{
score[0]=x;
score[1]=y;
score[2]=z;
average=(score[0]+score[1]+score[2])/3;
order=1;
}
Student()
{
score[0]=score[1]=score[2]=0;
average=0;
order=1;
}
void display();
int getNo(){return numb;}
float getAverage(){returnaverage;}
void setAverage(floatavg){average=avg;}
void setOreder(int x){order=x;}
string getName(){return name;}
void setName(stringname){this->name=name;}
};
先抛开函数不看,那么就是定义了所分析的基本的数据成员,并且用列表的形式进行了初始化,这个过程中不要忘记了构造一个缺省的构造函数,因为有自定义的构造函数时,系统便不会给定义无参类型
下一步,就是要考虑要得到的对于数据成员进行的基本操作,也就是
得到数据 int getNo(){return numb;}
更改数据 void setName(string name){this->name=name;}
还有就是 要展示
void Student::display()
{
cout<<numb<<"\t"<<name<<"\t"<<score[0]<<"\t"<<score[1]<<"\t"<<score[2]<<"\t"
<<average<<"\t"<<order<<endl;
}
到此数据类的基本成员就已全部定义完毕
这时写一个主函数来一步步的调试
int main()
{
Students(201718,"ff",89,89,78);
//s.display();
//cout<<s.getNo();
//cout<<s.getAverage();
//cout<<s.getName();
//s.setName("分左右");
// s.display();
//s.setAverage(12.2);
//s.display();
//s.setOreder(5);
//s.display();}
调试成功后即可
2.第二部就是要考虑操作类了
那么首先 对于学生有什么操作
应该有 输入学生的信息,查询学生的信息,更改学生的信息,还有把学生的信息进行一个排名打印
那么这是可以构造一个类来实现这些功能
class StudentOperation
{
Student list[60];
int n;
public:
StudentOperation():n(0){};
void add();
void sortlist();
void deleteh();
void display(int flag);
int search(int no);
void query();
void change();
};
由上一步的数据类得到 student类,可以构造一个对象数组
那么先来考虑 如何增添学生的信息
void StudentOperation::add()
{
int no,i,x,y,z;
string name;
system("cls");
cout<<"请按照学生学号,姓名,数学,英语,c++成绩的顺序输入学生信息,按-1结束"<<endl;
while((cin>>no)&&no!=-1)
{
cin>>name>>x>>y>>z;
Student s(no,name,x,y,z);//对于对象数组的初始化
list[n++]=s;
sortlist();//这一步的功能是及时的对学生的信息进行一个排名
}
}
那么接着就是排名了,排名的方式有两种,一种是按照学号的方式,另一种是按照成绩的方式
bool cmp1(Student stu1,Student stu2)
{
if(stu1.getAverage()-stu2.getAverage()>=1e-9) return 1;
else return 0;
}
bool cmp2(Student stu1,Student stu2)
{
returnstu1.getNo()>stu2.getNo();
}
void StudentOperation::sortlist()
{
sort(list,list+n,cmp1);
int i;
for(i=0;i<n;i++)
list[i].setOreder(i+1);
}
以下的是展示的功能
void StudentOperation::display(int flag)
{
cout<<"学号"<<"\t"<<"姓名"<<"\t"<<"数学"<<"\t"<<"英语"<<"\t"<<"c++"<<"\t"<<"平均分"
<<"\t"<<"排名"<<endl;
if(flag)
{
sort(list,list+n,cmp2);
int i;
for(i=0;i<n;i++)
list[i].display();
}
else
{
sort(list,list+n,cmp1);
int i;
for(i=0;i<n;i++)
list[i].display();
}
}
查询和更改的功能
int StudentOperation::search(int no)
{
int i;
for(i=0;i<n;i++)
if(list[i].getNo()==no) returni;
return -1;
}
void StudentOperation::query()
{
int no,i;
cout<<"请输入要查询学生的学号,按-1结束"<<endl;
while((cin>>no)&&no!=-1)
{
i=search(no);
if(i!=-1)
{
cout<<"学号"<<"\t"<<"姓名"<<"\t"<<"数学"<<"\t"<<"英语"<<"\t"<<"c++"
<<"\t"<<"平均分"<<"\t"<<"排名"<<endl;
list[i].display();
cout<<"请输入要查询的学生学号,按-1结束"<<endl;
}
else cout<<"输入学号有误,请重输!"<<endl;
}
}
void StudentOperation::change()
{
int no,i;
string name;
cout<<"请输入要更改信息的学生的学号,按-1结束"<<endl;
while(cin>>no&&no!=-1)
{
i=search(no);
cout<<"学号"<<"\t"<<"姓名"<<"\t"<<"数学"<<"\t"<<"英语"<<"\t"<<"c++"<<"平均分"
<<"\t"<<"排名"<<endl;
list[i].display();
cout<<"请输入要更改的名字"<<endl;
cin>>name;
list[i].setName(name);
list[i].display();
cout<<"请输入要更改信息的学生学号,按-1结束"<<endl;
}
cout<<"输入学生的学号有误,请重输!"<<endl;
}
最后加上主函数即可,那么完成一个系统的大致过程也即是如此,采用“滚雪球”的方法,一步步的考虑,即可完成