此份笔记建议在完整阅读郑莉老师、董渊老师、何江舟老师所编写的《C++语言程序设计(第4版)》后食用,风味更佳!
最后,由于本人水平有限,笔记中仍存在错误但还没有被检查出来的地方,欢迎大家批评与指正。
第4章 类与对象
4.1 面向对象程序设计的特点
4.1.1 抽象
1.什么是抽象
面向对象方法中的抽象,是指对具体问题(对象)进行概括,抽出一类对象的公共性质井加以描述的过程。
2.对一个问题的抽象包括
(1)数据抽象
描述某类对象的属性或状态,也就是此类对象区别于彼类对象的特征
(2)行为抽象
后者描述的是某类对象的共同行为或功能特征。
4.1.2 封装
1.什么是封装
封装就是将抽象得到的数据和行为(或功能)相结合,形成一个有机的整体,也就是将数据与操作数据的函数代码进行有机的结合,形成“类",其中的数据和函数都是类的成员。
2.封装的作用
通过封装使一部分成员充当类与外部的接口,而将其他成员隐蔽起来,这样就达到了对成员访问权限的合理控制,使不同类之间的相互影响减少到最低限度,进而增强数据的安全性和简化程序编写工作。
4.1.3 继承
1.抛出问题
如何把特殊与一般的概念间的关系描述清楚,使得特殊概念之间既能共享一般的属性和行为,又能具有特殊的属性和行为呢?
2.继承
只有继承,才可以在一般概念基础上,派生出特殊概念,使得一般概念中的属性和行为可以被特殊概念共享,摆脱重复分析、重复开发的困境。
C++语言中提供了类的继承机制,允许程序员在保持原有类特性的基础上,进行更具体、更详细的说明。通过类的这种层次结构,可以很好地反映出特殊概念与一般概念的关系。
4.1.4 多态
1.什么是多态
从广义上说,多态性是指一段程序能够处理多种类型对象的能力。
2.多态的分类
- 特殊多态性
- 强制多态
- 重载多态
- 一般多态性
- 类型参数化多态
- 包含多态
4.2 类与对象
类是面向对象程序设计方法的核心,利用类可以实现对数据的封装和隐蔽。
在面向过程的结构化程序设计中,程序的模块是由函数构成的,函数将逻辑上相关的语句与数据封装,用于完成特定的功能。
在面向对象程序设计中,程序模块是由类构成的。类是对逻辑上相关的函数与数据的封装,它是对问题的抽象描述。
4.2.1 类的定义
1.定义类的语法
class 类名称
{
public:
//公有成员(外部接口)
private:
//私有成员
protected:
//保护型成员
};
- 其中 public、protected、private分别表示对成员的不同访问权限控制
- 在类中可以只声明函数的原型,函数的实现(即函数体)可以在类外定义
- 在书写时通常习惯将公有类型放在最前面,这样便于阅读,因为它们是外部访问时所要了解的。
4.2.2 类成员的访问控制
1.类的成员包括
- 数据成员:描述问题的属性
- 函数成员:描述问题的行为
2.访问控制的属性有
(1)公有类型
定义了类的外部接口,任何外部函数都可以访问公有类型数据和函数。
(2)私有类型
私有成员只能被本类的成员函数访问,来自类外部的任何访问都是非法的。
一般情况下,一个类的数据成员都应该声明为私有成员。这样,内部数据结构就不会对该类以外的其余部分造成影响,程序模块之间的相互作用就被降低到最小。
(3)保护类型
保护类型成员的性质和私有成员的性质相似,其差别在千继承过程中对产生的新类影响不同。
3.实例
class Clock
{
public:
void setTime(int newH,int newM,int newS);
private:
int hour,minute,second;
public:
void showTime();
};
- 在类的定义中,具有不同访问属性的成员,可以按任意顺序出现。修饰访问属性的关键字也可以多次出现。但是一个成员只能具有一种访问属性。
- 如果紧跟在类名称的后面声明私有成员,则关键字private可以省略。
class Clock
{
int hour,minute,second; //默认是私有成员
public:
void showTime();
void setTime(int newH,int newM,int newS);
};
4.2.3 对象
C++中,类的对象就是该类的某一特定实体
1.声明一个对象
类名 对象名;
- 对象所占据的内存空间只是用于存放数据成员,函数成员不在每一个对象中存储副本,每个函数的代码在内存中只占据一份空间。
2.从对象访问数据成员
对象名.数据成员名
3.从对象名访问函数成员
对象名.函数成员名(参数表)
4.2.4 类的成员函数
1.语法
返回值类型 类名::函数成员名(参数表)
{
函数体
}
- 函数的原型声明要写在类体中,原型说明了函数的参数表和返回值类型。
- 而函数的具体实现是写在类定义之外的。
2.实例
//类中声明
class Clock
{
public:
void showTime();
...
};
//类外定义
void Clock::showTime()
{
cout << hour << ":" << minute << ":" << second << endl;
}
3.带默认形参值的成员函数
class Clock{
public:
void setTime(int newH=0, int newM=0, int newS=0);
...
};
//函数setTime()类外定义
void Clock::setTime(int newH/*=0*/,int newM/*=0*/,int newS/*=0*/){
...
}
//像这样在函数定义处,在形参表中以注释来说明参数的默认值,是一种好习惯
- 类成员函数的默认值,一定要写在类定义中,而不能写在类定义之外的函数实现中。
下面所讲的不只针对类的成员函数,对C++的函数都适用
- 如果一个函数在定义之前又有原型声明,默认形参值需要在原型声明中给出,定义中不能再出现默认形参值。
4.内联成员函数
如果有的函数成员需要被频繁调用 ,而且代码比较简单,这个函数也可以定义为内联函数。
(1)隐式声明:将函数体直接放在类体内。
class Clock{
public:
void setTime (int newH, int newM, int newS);
void showTime () {
cout << hour<<": "<<minute<<":"<< second<< endl;}
private:
int hour, minute, second;
};
(2)显示声明:在函数体实现时,在函数返回值类型前加上inline,类定义中不加入showTime的函数体。
class Clock{
public:
void setTime (int newH, int newM, int newS);
void showTime ();
private:
int hour, minute, second;
};
inline void Clock::setTime(){
cout << hour << ":" << minute << ":" << second << endl;
}
4.3 构造函数与析构函数
4.3.1 构造函数
1.作用
在对象被创建时利用特定的值构造对象,将对象初始化为一个特定的状态。
2.特性
- 构造函数也是类的一个成员函数,具有一般成员函数的特征
- 构造函数的函数名与类名相同,而且没有返回值;
- 构造函数通常被声明为公有函数
- 构造函数在对象被创建的时候将被自动调用。
3.默认构造函数
调用时无需提供参数的构造函数成为默认构造函数。
- 如果类中没有写构造函数,编译器会自动生成一个隐含的默认构造函数,该构造函数的参数列表和函数体皆为空。
- 如果类中声明了构造函数(无论是否有参数),编译器便不会再为之生成隐含的构造函数。
4.作为类的成员函数,构造函数可以直接访问类的所有数据成员,可以是内联函数,可以带有参数表,可以带默认的形参值,也可以重载。
5.语法
class Clock{
public:
Clock(){} //默认构造函数,系统自动生成
Clock(int newH, int newM, int newS):hour(newH),minute(newM),second(newS); //构造函数
Clock(){ //构造函数,可重载
hour=0;
mimute=0;
second=0;
}
...
};
4.3.2 复制构造函数
1.什么是复制构造函数
- 复制构造函数是一种特殊的构造函数,具有一般构造函数的所有特性;
- 其形参是本类的对象的引用;
- 其作用是使用一个已经存在的对象(由复制构造函数的参数指定),去初始化同类的一个新对象。
2.其他
- 如果程序员没有定义类的复制构造函数,系统就会在必要时自动生成一个隐含的复制构造函数。
- 这个隐含的复制构造函数的功能是,把初始值对象的每个数据成员的值都复制到新建立的对象中。
3.语法
class 类名
{
public:
类名(形参表); //构造函数
类名(类名 &对象名); //复制构造函数
...
};
类名::类名(类名 &对象名) //复制构造函数的实现
{
函数体
}
4.复制构造函数被调用的三种情况
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;
}
(1)当用类的一个对象去初始化该类的另一个对象时。
int mian()
{
Point a(1,2);
Point b(a);
Point c=a;
cout << b.getX() << endl;
return 0;
}
(2)如果函数的形参是类的对象,调用函数时,进行形参和实参结合时。
void f(Point p)
{
cout << p.getX() << endl;
}
int main()
{
Point(1,2);
f(a);
return 0;
}
- 只有把对象用值传递时,才会调用复制构造函数,如果传递引用,则不会调用复制构造函数。由于这一原因,传递比较大的对象时,传递引用会比传值的效率高很多。
(3)如果函数的返回值是类的对象,函数执行完成返回调用者时。
Point g()
{
Point a(1,2);
return a;
}
int main()
{
Point b;
b = g();
return 0;
}
5.需要注意(浅拷贝与深拷贝)
- 深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。
- 在对含有指针成员的对象进行拷贝时,必须要自己定义拷贝构造函数,使拷贝后的对象指针成员有自己的内存空间,即进行深拷贝,这样就避免了内存泄漏发生。
- 总结:浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。**
当类的数据成员中有指针类型时,默认的复制构造函数实现的只能是浅复制。浅复制会带来数据安全方面的隐患,要实现正确的复制,也就是深复制,必须编写复制构造函数。
4.3.3 析构函数
1.析构函数的作用
析构函数与构造函数的作用几乎正好相反,它用来完成对象被删除前的一些清理工作,也就是专门做扫尾工作的。例如:释放之前动态申请的内存。
2.关于析构函数
- 析构函数是在对象的生存期即将结束的时刻被自动调用的。
- 与构造函数一样,析构函数通常也是类的一个公有函数成员,它的名称是由类名前面加“~”构成,没有返回值。
- 和构造函数不同的是析构函数不接收任何参数,但可以是虚函数。
- 如果不进行显式说明,系统也会生成一个函数体为空的隐含析构函数。
3.实例
class Clock{
public:
Clock();
void setTime(int newH, int newM, int newS);
void shoeTime();
~Clock(){} //析构函数
private:
int hour, minute, second;
};
4.4 类的组合
4.4.1 组合
1.什么是组合
类的组合描述的就是一个类内嵌其他类的对象作为成员的情况,它们之间的关系是 一种包含与被包含的关系。
2.组合类构造函数
类名::类名(参数表):内嵌对象1(参数表),内嵌对象2(参数表),内嵌对象3(参数表),···
{
类的初始化
}
数据成员也可采用如上初始化方式
类名::类名(参数表):数据成员1(参数表),数据成员2(参数表),数据成员3(参数表),···
{
类的初始化
}
3.实例
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;
}
class Line{
public:
Line(Point xp1,Point xp2);
Line(& l);
double getLen(){
return len;
}
private:
Point p1,p2;
double len;
};
//组合类Line的构造函数,需要初始化内嵌对象
Line::Line(Point xp1,Point xp2):p1(xp1),p2(xp2){ //此处调用了Point类的复制构造函数
...
}
//组合类Line的复制构造函数
Line::Line(Line &l):p1(l.p1),p2(l.p2){
...
}
4.组合类构造函数的调用顺序
(1)调用内嵌对象的构造函数,调用顺序按照内嵌对象在组合类的定义中出现的次序;
- 内嵌对象在构造函数的初始化列表中出现的顺序与内嵌对象构造函数的调用顺序无关。
(2)执行组合类构造函数的函数体。
5.组合类析构函数的调用顺序
析构函数的调用执行顺序与构造函数刚好相反。
(1)先执行组合类析构函数的函数体;
(2)内嵌对象的析构函数被一一执行,执行顺序与它们在组合类的定义中出现的次序相反。
4.4.2 前向引用声明
1.什么是前向应用声明
前向引用声明,是在引用未定义的类之前,将该类的名字告诉编译器,使编译器知道那是一个类名。
2.实例
class A{
void fun(B b1); //编译时这里会报错,提示未知符号“B”
};
class B{
void fun(A a1);
};
正确的姿势(使用前向引用声明)
class B;
class A{
void fun(B b1); //这里仅仅是声明,没有分配空间
};
class B{
void fun(A a1);
};
3.问题来了
使用前向引用声明虽然可以解决一些问题,但它并不是万能的。需要注意的是,尽管使用了前向引用声明,但是在提供一个完整的类定义之前,不能定义该类的对象,也不能在内联成员函数中使用该类的对象。
class Fred;
class Barney{
Fred x; //编译报错,类Fred的定义不完善
public:
void method(){
x.showS(); //编译报错,Fred的对象在定义前被使用
}
};
class Fred{
Barney y;
};
正确的姿势
class Fred;
calss Barney{
public:
void method(){
x->showS(); //编译报错,Fred的对象在定义前被使用
z.showS(); //编译报错,Fred的对象在定义前被使用
}
private:
Fred *x; //正确,经前向引用声明,可以声明Fred类的对象引用或指针
Fred &z;
};
class Fred{
Barney y;
};
4.5 UML图形标识
4.6结构体和联合体
4.6.1 结构体
1.什么是结构体(C++中)
结构体是一种特殊形态的类,它和类一样,可以有自己的数据成员和函数成员,可以有自己的构造函数和析构函数,可以控制访问权限,可以继承,支持包含多态等,二者定义的语法形式也几乎一样。
2.问题来了——结构体与类的区别
结构体和类的唯一区别在于,结构体和类具有不同的默认访问控制属性:
- 在类中,对于未指定访问控制属性的成员,其访问控制属性为私有类型;
- 在结构体中,对于未指定任何访问控制属性的成员,其访问控制属性为公有类型。
3.语法
struct 结构体名称
{
公有成员
protected:
保护型成员
private:
私有成员
};
- 在结构体的定义中,如果把公有成员放在最前面,则最前面的"public" 可以省去。
- 与类不同,对于结构体,人们习惯于将数据成员设置为公共的。
4.思考:为什么C++还要引入结构体?
- 为了保持和C程序的兼容性。
- C 语言只有结构体,而没有类,C 语言的结构体中只允许定义数据成员,不允许定义函数成员,而且C语言没有访问控制属性的概念,结构体的全部成员是公有的。C语言的结构体是为面向过程的程序服务的,并不能满足面向对象程序设计的要求。
- C ++ 在 struct 之外引入了另外的关键字 class ,并且把它作为定义抽象数据类型的首选关键字。
- 但为了保持和 C 程序的兼容,C++ 保留了 struct 关键字,并规定结构体的默认访间控制权限为公有类型。
5.结构体的使用实例
struct Student{
string name;
int age;
string StuID;
char sex;
};
int main()
{
Student student1{"Jason",18,"110600329",'M'};
}
4.6.2 联合体
1.什么是联合体
联合体的全部数据成员共享同一组内存单元
2.语法
union 联合体名称
{
公有成员
protected:
保护型成员
private:
私有成员
};
3.实例
union Mark{
char grade;
bool pass;
int percent;
};
联合体Mark类型变量的存储结构:
- 由于联合体的成员共用相同的内存单元,联合体变量中的成员同时至多只有一个是有意义的。
4.联合体的一些限制
- 联合体的各个对象成员,不能有自定义的构造函数、自定义的析构函数和重载的复制赋值运算符,不仅联合体的对象成员不能有这些函数,这些对象成员的对象成员也不能有,以此类推。
- 联合体不能继承,因而也不支持包含多态。
- 一般只用联合体来存储一些公有数据,而不为它定义函数成员。
5.无名联合体
- 联合体也可以不声明名称。
- 无名联合体没有标记名,只是声明一个成员项的集合,这些成员项具有相同的内存地址,可以由成员项的名字直接访问。
- 无名联合体通常用作类或结构体的内嵌成员。
实例:
union{
char grade;
bool pass;
int percent;
};
int main()
{
cin >> a;
if(a==1)
{
grade='A';
}
else if(a==2)
{
pass = true;
}
else if(a==3)
{
percent=50;
}
}