面向对象方法中的抽象,是指对具体问题(对象)进行概括,抽出一类对象的公共性质并加以描述的过程。
对一个问题的抽象包括两个方面:数据抽象和行为抽象(或称为:功能抽象和代码抽象)
封装:
就是将抽象得到的数据和行为(或功能)相结合,形成的一个有机的整体,也就是将数据用户操作数据的函数代码进行有机的结合,形成“类”,其中的数据和函数都是类的成员。
c++语言中提供了类的继承机制,允许程序员在保持原有类特性的基础上,进行更具体、更详细的说明。
多态:是指一段程序能够处理多种类型对象的能力。在c++语言中,这种多态性可以通过强制多态、重载多态、类型参数化多态、包含多态4种形式来实现。
在面向对象程序设计中,程序模块是由类构成的。类是逻辑上相关的函数与数据的封装,它是对问题的抽象描述。
类的定义形式(语法):
class 类名称
{
public:
外部接口
protected:
保护型成员
private:
私有成员
};
访问控制属性有一下三种:公有类型(public),私有类型(private),和保护类型(protected)。
公有类型成员定义了类的外部接口。
私有成员只能被本类的成员函数访问,来自类外部的任何访问都是非法的。
保护类型成员的性质和私有成员类似,其差别在于继承过程中对产生的新类的影响不同。
在c++语言中,类的对象就是该类的某一特定实体(也称实例)。
声明一个对象和声明一个一般变量相同,采用如下的方式:
类名 对象名;
例如
Clock myclock;
就声明了一个时钟类型的对象myclock。
访问数据成员的一般形式:
对象名.数据成员
调用函数成员的一般形式:
对象名.函数成员名(参数表)
例如,访问类clock的对象myclock的函数成员ShowTime()的方式如下:
myclock.ShowTime()
在类的外部只能访问类的公有成员;在类的成员函数中,可以访问到类的全部成员。
类的成员函数
成员函数描述的是类的行为,成员函数是程序算法的实现部分,是对封装的类的数据进行操作的方法。
1.成员函数的实现
函数的原型声明要写在类体中,原型说明了函数的参数表和返回值类型。而函数的具体实现是写在类定义之外的。与普通函数不同的是,实现成员函数时要指明类的名称。
具体形式为:
返回值类型 类名::函数成员名(参数表)
{
函数体
}
例如:
void Clock::setTime(int newH,int newM,int newS)
{
hour = newH;
minute = newM;
second = newS;
}
void Clock::showTime()
{
cout<<hour<<":"<<minute<<":"<<second<<endl;
}
2.成员函数调用中的目的对象
调用一个成员函数与调用普通函数的不用在于,需要使用“.”操作符指出调用所针对的对象,这一对象在本次调用中成员目的对象。
注意: 在类的成员函数中,既可以访问目的对象的私有成员,又可以当前类的其他对象的私有成员。
3.带默认形参值的成员函数
类的成员函数也可以有默认的形参值,其调用规则与普通函数相同。类成员函数的默认值一定要写在类的定义中,而不能写在类定义之外的函数实现中。有事喊这个默认值可以带来很大的方便。
4.内联成员函数
函数的调用要消耗内存资源和运行时间来传递参数和返回值,要记录调用时的状态,以便保证调用完成后能够正确的返回并继续执行。
如果函数成员需要被频繁的调用,而且代码比较简单,这个函数也可以定义为内联函数(inline function)。
内联成员函数在编译时被插入到每一个调用它的地方。这样做可以减少开销,提高执行效率,但是却增加了编译后代码的长度。
---牺牲空间换时间!!!
所以在使用内联成员函数时应该权衡利弊,只有对相当简单的成员函数才可以声明为内联函数。
内联函数的声明有两种方式:隐式声明和显式声明。
将函数体直接放在类体内,这用方法成为隐式声明。
比如:将时钟类的showTime()函数声明为内联函数可以写作:
class Clock
{
public:
void setTime(int newH,int newM,int newS)
void showTime()
{
cout<<hour<<":"<<minute<<":"<<second<<endl;
}
private:
int hour,minute,second;
};
为了保重定义的简洁,可以采用关键字inline的显式声明的方式。记载函数体实现时,在函数返回值类型前加上inline,类定义中不加入showTime的函数体。
inline void Clock::showTime()
{
cout<<hour<<":"<<minute<<":"<<second<<endl;
}
两种方式的效果是相同的。
程序实例:
#include<iostream>
using namespace std;
class Clock
{
public:
void showTime();
void setTime(int newH=0,int newM=0,int newS=0);
private:
int hour,minute,second;
};
inline void Clock::showTime()//显式声明为内联函数
{
cout<<hour<<":"<<minute<<":"<<second<<endl;
}
void Clock::setTime(int newH,int newM,int newS)
{
hour = newH;
minute = newM;
second = newS;
}
int main()
{
Clock myClock;// the definition of class Clock's object myClock
cout<<"First time set and out put:"<<endl;
myClock.setTime();//set the time as the default time
myClock.showTime();// just show time;
cout<<"Second time set and output:"<<endl;
myClock.setTime(16,50,55);//set time as:16:50:55
myClock.showTime();//show the second time
return 0;
}
构造函数和析构函数
每个对象区别其他对象的地方主要有两个:
外在区别是对象的名称,而内在的区别是对象自身的属性值,即数据成员的值。
在定义对象的时候进行的数据成员的设置,称为对象的初始化。
在对象结束时还经常需要进行一些清理工作。c++程序中的初始化和清理工作,分贝由两个特殊的成员函数来完成:他们是构造函数和析构函数。
构造函数
构造函数的作用就是在对象被创建时利用特定的值构造对对象,将对象初始化为一个特定的状态。
构造函数也是一个成员函数,除了具有一般成员函数的特征外,还有一些特殊的性质:构造函数名与类名相同,而且没有返回值;
构造函数通常被声明为公有函数。只要类中有构造函数,编译器就会在建立新对象的地方自动插入对构造函数调用的代码。因此我们通常说构造函数在对象被创建的时候被自动调用。
调用时无须提供参数的构造函数成为默认构造函数。
如果类中没有写构造函数,编译器会自动生成一个遗憾的默认构造函数,该构造函数的参数列表和函数体皆为空。如果类中声明了构造函数(无论是否有参数),编译器便不会再为之生成隐含的构造函数。
在前面的例子中没有定义与类Clock通名的成员函数--构造函数,这时编译系统就会在编译时自动生成一个默认形式的构造函数:
class Clock
{
public:
Clock(){}//编译系统生成的饮用含的默认构造函数
.......
};
Example:
#include<iostream>
using namespace std;
class Clock
{
private:
int hour,minute,second;
public:
Clock(int newH=0,int newM=0,int newS=0);//Clock is the construction of the Clock
void showTime();
};
Clock::Clock(int newH,int newM,int newS)
{
hour = newH;
minute = newM;
second = newS;
cout<<"construction!"<<endl;//to show that the function was ever used
}
void Clock::showTime()
{
cout<<hour<<":"<<minute<<":"<<second<<endl;
}
int main()
{
Clock myClock,yourClock(12,00,00);//一个没有默认值,一个初始化了
cout<<"the default time:"<<endl;
myClock.showTime();
yourClock.showTime();
return 0;
}
复制构造函数:
生成一个对象的副本有两种途径:一是建立一个新对象,然后将一个已有的对象的数据成员值取出来,一一赋给新的对象。这样做可行但是很繁琐。
复制构造函数是一种特殊的构造函数,具有一般构造函数的所有特性,其形参是本来的对象的引用。其作用是使用一个已经存在的对象(由复制构造函数的参数指定),去初始化同类的一个新对象。
如果程序员没有定义复制构造函数,系统会在必要时自动生成 一个隐含的复制构造函数。这个隐含的复制构造函数的功能是:
把初始值对象的每个数据成员的值都复制到新建立的对象中。
下面是声明和实现复制构造函数的一般方法:
class 类名
{
public:
类名(形参表);//构造函数
类名(类名&对象名);//复制构造函数
.......
};
类名::类名(类名&对象名);//复制构造函数的实现
{
函数体
}
实例:通过水平和垂直两个方向的坐标值x和y来确定屏幕上的一个点。(Point)点类的定义如下:
class Point
{
public:
Point(int xx=0,int yy =0 )//构造函数
{
x =xx;
y =yy;
}
Point(Point &p);//复制构造函数
int getx()
{
return x;
}
int gety()
{
return y;
}//getx和gety在类体里面实现实际上就成为了内联函数
private:
int x,y;
};
Point::Point(Point &p)
{
x=p.x;
y=p.y;
cout<<"Calling the copy constructor"<<endl;
}
普通构造函数是在对象创建时被调用,而复制构造函数是在以下三种情况下被调用。
(1)当用类的一个对象去初始化该类的另一个对象时。
例如:
int main()
{
Point a(1,2);
Point b(a);//用对象a去初始化对象b,复制构造函数被调用
Point c=a;//用对象a初始化对象c,复制构造函数被调用
cout<<b.getx()<<endl;
cout<<b.gety()<<endl;
return 0;
}
细节:以上对b和c的初始化都能够调用复制构造函数,两种写法只是形式上有所不同罢了,执行的操作完全相同,前者是直接初始化,后者是复制初始化
(2)如果函数的形参是类的对象,调用函数时,进行形参和实参结合时。
void f(Point p)
{
cout<<p.getx()<<endl;
}
int main()
{
Point a(1,2);
f(a);//函数的形参为类的对象,当调用函数时,复制构造函数被调用
return 0;
}
提示:只有把对象用值传递时,才会调用复制构造函数,如果传递引用,则不会调用复制构造函数。由于这一原因,传递比较大的对象时,传递引用会比传值的效率高很多。
(3)如果函数的返回值是类的对象,函数执完成返回调用者
Point g()
{
Point a(1,2);
return a;//函数返回值是类对象,返回函数值时,调用复制构造函数
}
int main()
{
Point b;
b=g();
return 0;
}
Example:
#include<iostream>
using namespace std;
class Point
{
public:
Point(int xx=0,int yy=0)
{
x = xx;
y = yy;
}//构造函数
Point(Point &p);//复制构造函数
int getx()
{
return x;
}
int gety()
{
return y;
}
private:
int x,y;
};
Point::Point(Point &p)
{
x=p.x;
y=p.y;
cout<<"Calling the copy constructor!"<<endl;
}
void fun1(Point p)//形参为Point类对象的函数
{
cout<<p.getx()<<endl;
}
Point fun2()
{
Point a(1,2);
return a;//a是Point 的一个对象
}
int main()
{
Point a(4,5);//第一个对象a
Point b=a;//情况一:用a初始化b,第一次调用复制构造函数
cout<<b.gex()<<endl;
fun1(b);//情况二:对象b作为fun1的实参,第二次调用复制构造函数
b=fun2();//情况三,函数的返回值是类对象,函数返回时,调用复制构造函数
cout<<b.getx()<<endl;
return 0;
}
这个例题中的复制构造函数与隐含的复制构造函数功能一样,都是直接将原对象的数据成员值一一赋给新对象中对应的数据成员。也许在这里没有必要写这个复制构造函数,但是如果想选择性的复制的话,就得自己写一个“特殊的”复制构造函数了。
默认的复制构造函数实现的仅仅是浅复制,浅复制会带来安全方面的隐患,要实现正确的复制,也就是深复制,必须写复制构造函数。
析构函数
对象会消失吗?当然会!自然界万物都是有生有灭,程序中的对象也是一样。已经知道对象定义时诞生,至于何时消亡,就牵涉到对象的生成期问题。
析构函数与构造函数的作用恰好相反,它是用来完成对象被删除前的一些清理工作,也就是专门做扫尾工作的。析构函数是在对象的生存期即将结束的时刻被自动调用的。它的调用完成后,对象也就消失了,相应的内存空间也被释放。
与构造函数相同,析构函数通常也是类的一个公有的函数成员,它的名称是由类名前面加上“~”构成,没有返回值。和构造函数不同的是析构函数不接受任何参数,但可以是虚参数。如果不进行显式说明,系统也会生成一个函数体为空的隐含析构函数。
提示:
函数体为空的函数未必不做任何事情。
class Clock
{
public:
Clock();//构造函数
void setTime(int newH,int newM,int newS);
void showTime();
~Clock(){}
private:
int hour,minute,second;
};
double pow(double x,double n)
是math.h里一函数,求x的n次幂
x必须>0,
立方根就是1/3次幂了
但这里要用3.0或1.0/3,否则成了整除结果为1
实例1:游泳池改造预算,Circle类
一圆形游泳池,现在需要在其周围建一圆形过道,并在其周围上栅栏,
栅栏的价格为35元/米,过道的造价为20元/平方米。过道宽度为3米,
游泳池半径由键盘输入。要求编程计算输出过道和 栅栏的造价
#include<iostream>
using namespace std;
const float PI=3.141593;//给出π的值
const float FENCE_PRICE=35;//栅栏的单价
const float CONCRETE_PRICE=20;//给出水泥的单价
class Circle //声明定义Circle及其数据和方法
{
public: //外部接口
Circle(float r);//构造函数
float circumference();//计算圆的周长
float area();//计算圆的面积
private:
float radius;//私有成员:圆的半径
};
//类的实现
//构造函数初始化数据成员radius
Circle::Circle(float r)
{
radius=r;
}
//计算圆的周长
float Circle::circumference()
{
return 2*PI*radius;
}
float Circle::area()
{
return PI*radius*radius;
}
//主函数实现
int main()
{
float radius;
cout<<"Enter the radius of the pool:";//提示用户输入半径
cin>>radius;
Circle pool(radius);//游泳池边界对象
Circle poolRim(radius+3);//栅栏对象
//计算栅栏造价并输出
float fenceCost=pool.circumference()*FENCE_PRICE;
cout<<"Fence Cost is ¥"<<fenceCost<<endl;
//计算过道造价并输出
float concreteCost=(poolRim.area()-pool.area())*CONCRETE_PRICE;
cout<<"Concrete Cost is ¥"<<concreteCost<<endl;
return 0;
}
类的组合:
类的组合描述的就是一个类内嵌套其他类的对象作为成员的情况,它们之间的关系是一种包含与被包含的关系。
当创建类的对象时,如果这个类具有内嵌对象成员,那各个内嵌对象将首先被自动创建。
因为部件对象是复杂对象的一部分,因此在创建对象时既要对本类的基本类型数据成员进行初始化,又要对内嵌对象成员进行初始化。
这时理解对象的构造函数被调用的顺序很重要。
组合类构造函数定义的一般形式为:
类名::类名(形参表):内嵌对象1(形参表),内嵌对象2(形参表),.....
{
类的初始化
}
其中“内嵌对象1(形参表),内嵌对象2(形参表),.....”又称作初始化列表,其作用是对内嵌的对象进行初始化。
对基本类型的数据成员也可以这样初始化,例如Circle类的构造函数也可以这样写:
Circle::Circle(float r):radius(r){}
在创建一个组合类的的对象时,不仅它自身的构造函数的函数体将被执行,而且将调用内嵌对象的构造函数。这时造函数的调用顺序如下:
(1)
调用内嵌对象的构造函数,调用顺序按照内嵌对象在组合类的定义中出现的次序
注意:内嵌对象在构造函数的初始化列表中出现的顺序与内嵌对象构造函数的调用顺序无关。
(2)
执行本类构造函数的函数体。
有些数据成员的初始化必须在构造函数的初始化列表中进行。
这些数据成员包括两类:
一类是那些没有默认构造函数的内嵌对象--因为这类对象初始化时必须提供参数,
另一类是引用类型的数据成员--因为引用型变量必须在初始化时绑定引用的对象。如果一个类包括这两类成员,那么编译器不能够为这个类提供隐含的默认构造函数,这时必须编写显式的构造函数,并且在每个构造函数的初始化列表中至少为这两类数据成员进行初始化。
析构函数的调用执行顺序与构造函数刚好相反。
析构函数的函数体被执行完毕后,内嵌对象的析构函数被一一执行,这些内嵌对象的析构函数调用顺序与它们在组合类的定义中出现的次序刚好相反。
提示:由于要调用内嵌对象的析构函数,所有有时隐含的析构函数并非什么都不错。
如果要为组合类编写复制构造函数,则需要为内嵌成员对象的复制构造函数传递参数。例如:
假如C类中包含B类的对象b作为成员,C类的复制构造函数形式如下:
c::c(c &c1):b(c1,&b){....}
一个实例:
#include<iostream>
using namespace std;
#include<cmath>
class Point
{
public:
Point(int xx=0,int yy=0)
{
x =xx;
y =yy;
}
Point(Point &p);
int getx()
{
return x;
}
int gety()
{
return y;
}
private:
int x,y;
};
Point::Point(Point &p)//复制构造函数的实现
{
x=p.x;
y=p.y;
cout<<"Calling the copy constructor of Point:"<<endl;
}
//类的组合
class Line
{
public:
Line(Point xp1,Point xp2);
Line(Line &l);
double getLen()
{
return len;
}
private:
Point p1,p2;//Point 类的对象p1,p2
double len;
};
//组合类的构造函数
Line::Line(Point xp1,Point xp2):p1(xp1),p2(xp2)
{
cout<<"Calling constructor of Line"<<endl;
double x=static_cast<double>(p1.getx()-p2.getx());
double y=static_cast<double>(p1.gety()-p2.gety());
len=sqrt(x*x+y*y);
}
//组合类的复制构造函数
Line::Line(Line &l):p1(l.p1),p2(l.p2)
{
cout<<"Calling the copy constructor of Line!"<<endl;
len=l.len;
}
//主函数
int main()
{
Point myp1(1,1),myp2(4,5);//建立Point类的对象
Line line(myp1,myp2);//建立Line类的对象
Line line2(line);//利用复制构造函数建立一个新对象
cout<<"The length of the line is";
cout<<line.getLen()<<endl;
cout<<"the length of the line2 is:";
cout<<line2.getLen()<<endl;
return 0;
}
前向引用声明
C++的类应该先定义在在使用。但是在处理复杂问题时很可能遇到两个类相互引用的情况,这种情况也成为循环依赖。
Class A //A类的定义
{
Public:
void f(B b);//以B类对象b为形参的成员函数
};
class B
{
public:
void g(A a);//以A类对象a为形参的成员函数
};