7、类与对象
7.1面向对象的程序设计
4个特点:抽象、封装、继承、多态性(有时只选3个)
什么是面向对象的程序设计
1、对象
客观世界中任何一个事物都可以看成一个对象。
一个班级作为一个对象时有两个要素:
静态特征:学生人数、所在教室——属性
动态特征:开会、体育比赛——行为(功能)
任何一个对象都具有两个要素,即属性和行为,对象是由一组属性和一组行为构成的。
在C++中,每个对象都是由数据和函数组成的,数据体现了“属性”,函数是用来对数据进行操作的,就是“行为”,也叫“方法”。
2、封装与信息隐蔽
可以对一个对象进行封装处理,把它的一部分属性和功能对外界屏蔽,也就是说从外界是看不到的、甚至是不可知的。
C++类对象中的函数名就是对象的对外接口,外界可以通过函数名调用这些函数来事项某些功能。
3、抽象
抽象的作用是表示同一类食物的本质。
类是对象的抽象,而对象则是类的特例,即类的具体表象形式。
4、继承与重用
父类/基类:“马”
子类/派生类:“白马”
采用继承的方法可以很方便地利用一个已有的类建立一个新的类,大大节省了编程工作量。这就是常说的“软件重用”的思想。
5、多态性
由继承而产生的不同的派生类,其对象对统一消息会作出不同的响应。
7.2 类与对象的概念与设计
类和对象的关系
类是对象的抽象,而对象是类的具体实例。
类是抽象的,不占用内存,而对象是具体的,占用存储空间。
声明类类型
不声明则默认为private
class 类名{
private:
私有的数据和成员函数;
public:
公用的数据和成员函数;
};
还有一种成员访问限定符protected,它不能被类外访问,但能被派生类的成员函数访问。
定义对象的方法
1、先声明类类型,再定义对象
(1)class 类名 对象名
(2)类名 对象名
2、在声明类的同时定义对象
class Student{
}stu1,stu2;
3、不出现类名,直接定义对象(不提倡)
class{
}stu1,stu2;
成员函数
“::”是作用域限定符/作用域运算符
类名::类成员函数
class Student{
public:
void display();
};
void Student::display(){}
内置成员函数
在类体中定义的成员函数中不包括循环等控制结构,C++系统自动地对它们作为内置函数来处理。程序调用这些函数时把函数代码嵌入程序的调用点。减少时间开销。
PS:函数只有在类体中定义时,才会自动作内置函数。若函数在类体外定义,但又想将其指定为内置函数,应用inline作显式声明,且声明和定义应在同一个文件。
对象成员的引用
1、通过成员运算符
形式:对象名.成员名
2、通过指向对象的指针
Time t,*p;
p = &t;
cout<<p->hour;
3、通过对象的引用
Time t1;
Time &t2 = t1;
cout<<t2.hour;
7.3 构造函数和析构函数
对象的初始化
如果一个类中所有的成员都是公用的,则可以在定义对象时对数据成员进行初始化
Time t1={10,22,36};
但如果数据成员是私有的,或者类中有private或protected的数据成员,就不能这样初始化。
构造函数:一种特殊的成员函数,不需要用户来调用它,而是在建立对象时自动执行。
构造函数必须与类名同名,构造函数是在声明类的时候定义的,程序用户只需在定义对象的同时指定数据成员的初值。
PS:
1)构造函数没有返回值因此没有类型
2)可以用一个类对象初始化另一个类对象
3)构造函数中可以包含其他(非赋值)语句,如cout,但不建议
4)如果用户没有定义构造函数,系统会自动生成,但是该函数体为空,没有参数,不执行初始化操作
1、不带参数的构造函数
class Time{
private:
int hour;
int minute;
public:
Time(){
hour = 0;
minute = 0;
}
};
int main(){
Time t1;
//此时t1的hour,minute为0
}
2、带参数的构造函数
class Time{
private:
int hour;
int minute;
public:
Time(int h,int m){
hour = h;
minute = m;
}
};
int main(){
Time t1(10,23);
//此时t1的hour为10,minute为23
}
3、用参数初始化表对数据成员初始化
不在函数体内对数据成员初始化,而是在函数首部实现。
在原来函数首部的末尾加一个冒号,然后列出参数的初始化表。后面的花括号是空的,即函数体为空。减少函数体的长度,使结构函数精炼简单。
类名::构造函数名(参数表):成员初始化表{}
class Time{
private:
int hour;
int minute;
public:
Time(int h,int m):hour(h),minute(m){}
};
int main(){
Time t1(10,23);
//此时t1的hour为10,minute为23
}
构造函数的重载
在一个类中可以定义多个构造函数,以便为对象提供不同的初始化方法,这些构造函数名相同,但参数个数/参数类型不同。
PS:建立对象时不必给出实参的构造函数称为默认构造函数,无参构造函数属于默认构造函数。一个类只能由一个默认构造函数,若用户未定义,系统提供一个,但函数体为空。
使用默认参数的构造函数
class Time{
private:
int hour;
int minute;
public:
Time(int h=10,int m=23){
hour = h;
minute = m;
}
};
如果构造函数的全部参数都指定了默认值,则在定义对象时,可以给或不给或给几个实参。
由于不需要给出实参也可以调用构造函数,因此全部参数都指定了默认值的构造函数也属于默认构造函数。
一个类中定义了全部都是默认参数的构造函数后,不能再定义重载构造函数。
析构函数
“~”位取反运算符,
形式:~类名
当对象的生命期结束时,会自动执行构造函数。析构函数的作用并不是删除对象,而是在撤销对象占用的内存之前完成一些清理工作不能被重载。
一个类可以有多个构造函数,但只能有一个析构函数,还可以用来执行“用户希望在最后一次使用对象之后所执行的任何操作"。
class Student{
public:
~Student(){
cout<<"调用析构函数!"<<endl;
}
};
特殊情况:
常数据成员,只能通过构造函数的参数初始化列表对常数据成员进行初始化,其他任何函数都不能对常数据成员赋值。
如果将成员函数声明为常成员函数,则只能引用本类中的数据成员,而不能修改它们。
调用构造函数和析构函数的顺序
一般情况下,调用析构函数的次序正好与调用构造函数的次序相反:最先被调用的构造函数,其对应的(同一对象中的)析构函数最后被调用。
先构造的后析构,后构造的先析构
7.4复制构造函数
形式:类名 对象2(对象1)
复制构造函数也是构造函数,但它只有一个参数,这个参数是本类的对象。
class Time{
private:
int hour;
int minute;
public:
一般约定加const声明,使参数值不能改变,以免在调用此函数时不慎使实参对象被修改
Time(const Time &t){
hour = t.hour;
minute = t.minute;
}
};
int main(){
Time t2(t1);//建立一个新对象t2
如果用户未定义复制构造函数,系统自动提供一个默认的复制构造函数,
但作用只是简单地复制类中每个数据成员
Time t3 = t1;//此方法效果一样
}
需要复制构造函数的情况:
1)程序中需要建立一个对象,并用另一个同类对象对其初始化。
2)当函数的参数为类的对象时。
3)函数的返回值是类的对象。
7.5 共有数据的保护
既要使数据能在一定范围内共享,又要保证它不被任意修改,此时把有关的数据定义为常量。
常对象
形式:
类名 const 对象名(实参)
const 类名 对象名(实参)
定义常对象时,必须同时初始化,之后不能再改变。
const Time t1(10,23);
常对象只能调用它的常成员函数,常成员函数是常对象唯一的对外接口。
常成员函数可以访问常对象中的数据成员,但不允许修改常对象中的数据成员的值。
PS:若有需要,一定要修改常对象中的某个数据成员,则将该数据成员声明为mutable
mutable int count;
常对象成员
1、常数据成员
形式:const 类型名 变量名
只能通过构造函数的参数初始化表对常数据成员进行初始化
class Time{
private:
const int hour;
public:
Time(int h){
hour = h;
}//非法
Time(int h):hour(h){}//合法
2、常成员函数
形式:类型名 函数名(参数表)const
只能引用本类中的数据成员,但不能修改它们。不能调用另一个非const的成员函数
指向对象的常指针
形式:*类名 const 指针变量名
指针变量始终保持为初值,不能改变,即指向不变
Time t1(10,23),t2;
Time *const ptr;
ptr = &t1;
ptr = &t2;//非法,ptr已指向t1,不能改变
但可以改变t1的值
指向常对象的指针变量
形式:const 类型名 * 指针变量名
1)如果一个变量已被声明为常变量,只能用指向常变量的指针变量指向他。
2)指向常变量的指针变量除了可以指向常变量,还可以指向未被声明为const的变量,此时不能通过此指针变量改变该变量的值。
3)如果函数的形参是指向普通变量的指针变量,实参只能用指向普通变量的指针。
当指针指向常对象时,同理,只需将“常变量”替换为“常对象”。
当希望在调用时对象的值不被修改,就把形参定义为只指向常对象的指针变量,同时用对象的地址作实参(对象是否const都行)
PS:若定义了一个指向常对象的指针变量,是不能通过它改变所指向的对象的值,但是指针变量本身的值是可以改变的。
对象的常引用
如果不希望在函数中修改实参的值,可以把函数的形参声明为const
void fun(const Time &t){}
const型数据小结
形式 | 含义 |
---|---|
Time const t; | t是常对象,其值任何情况下不能改变 |
void Time::fun()const; | 常成员函数,可以引用,但不能修改本类中的数据成员 |
Time *const p | 常指针变量,p的指向不能改变 |
const Time *p | 指向常对象的指针变量,p指向的类对象不能通过p改变 |
const Time &t1 = t | t1是t的引用,二者指向同一存储空间,t的值不能改变 |
7.6 静态成员
静态数据成员
如果希望各对象中的数据成员的值一样,将其定义为静态数据成员,这样它就为各对象所共有,所有对象都可引用 ,在内存中只占一份空间。
静态数据成员可以初始化,但只能在类体外。
class Box{
public:
static int height;
int width;
};
int Box::height = 10;
静态数据成员既可以通过对象名引用,也可以通过类名引用。
但在类外可以直接访问静态数据成员的前提是,其为public。
Box.height;
box1.height;
静态成员函数
静态成员函数没有this指针(根本区别),因此不能访问本类中的非静态成员,但可直接引用本类中的静态成员。
C++中静态成员函数主要用来访问静态数据成员。
class Box{
private:
static int height;
int width;
public:
static float fun1();
};
float Box::fun1(){
return height/2;
}
7.7 友元
提前说明,友元的关系是单向、不传递的。
a认为b是朋友,b不一定认为a是朋友;b是a的朋友,c是b的朋友,c不一定是a的朋友。
提前引用
涉及一个额外知识点,提前引用:
在正式声明一个类之前先声明一个类名,表示此类将稍后声明,只包含类名,不包括类体。
PS:类的提前声明的作用范围是有限的,只有在正式声明以后才能用它定义类对象。
友元函数
在本类外的地方定义了一个函数(可以是别的类的函数或者不属于任何类),在类体中用friend对其进行声明,此函数就成为本类的友元函数。
友元函数可以访问这个类的私有成员。
1、将普通函数声明为友元函数
2、友元成员函数(把另一类的成员函数声明为friend)
友元类
将B类声明为A的友元类,此时友元类B中的所有函数都是A类的友元函数,可以访问A类中的所有成员。
形式:friend 类名;
7.8 类模板
功能是相同的,仅仅是数据类型不同
声明模板前形式:template<class 虚拟类型参数>
定义对象形式:类模板名<实际类型名>对象名(参数表);
类外定义成员函数形式:
template<class 虚拟类型参数>
函数类型 类模板名<虚拟类型参数>::成员函数名(形参){}
template<class T>
class Compare{
private:
T a,b;
public:
Compare(T a,T b){
...
}
print(){
...
}
};
int main(){
Compare<int> com1(3,4);
Compare<float> com1(3.5,4.7);
7.9 运算符的重载
运算符重载的特点与使用
运算符重载的规则
1)不允许用户自己定义新的运算符。
2)不能重载的运算符有5个:"." , “*” , “::” , “sizeof” , “?:”
3)重载运算符不能改变运算符运算对象的个数、不能改变优先级、不能改变结合性
4)不能有默认的参数
5)重载的运算符必须和用户定义的自定义类型的对象一起使用,其参数至少有一个是类对象
6)用于类对象的运算符一般必须重载,但有两个例外,运算符"=","&"
“<<",">>",和类型转换运算符只能定义为友元函数重载
一般将单目运算符和复合运算符重载为成员函数
一般将双目运算符重载为友元函数
几种常用的运算符重载的写法
class Complex{
private:
double real;//实部
double imag;//虚部
public:
Complex(){
real = 0;
imag = 0;
}
Complex(double r,double i){
real = r;
imag = i;
}
friend Complex operator +(Complex &a,Complex &b);
friend istream &operator >>(istream &in,Complex &a);
friend ostream &operator <<(ostream &out,Complex &a);
void display(){
cout<<real<<"+"<<imag<<'i'<<endl;
}
};
Complex operator +(Complex &a,Complex &b){
Complex c;
c.real = a.real + b.real;
c.imag = a.imag + b.imag;
return c;
}
istream &operator >>(istream &in,Complex &a){
in>>a.real>>a.imag;
return in;
}
ostream &operator <<(ostream &out,Complex &a){
out<<a.real<<"+"<<a.imag<<'i'<<endl;
return out;
}