类与对象

面向对象编程的基本特征

抽象

  • 概括同一类对象的共同属性和行为,形成一个类。
    • 首先关注问题的本质和描述,然后是实现过程或细节。
    • 数据抽象:描述某一类对象的属性或状态(区分对象的物理量)。
    • 代码抽象:描述某一类对象所具有的共同行为特征或功能。
    • 抽象的实现:类。
  • 抽象示例—时钟
    • 数据抽象: int hour,int minute,int second
    • 代码抽象: setTime(),showTime()
1
2
3
4
5
6
7
class  Clock {
public:
void setTime(int newH, int newM, int newS);
void showTime();
private:
int hour, minute, second;
};

封装

  • 将抽象的数据和代码封装在一起,形成一个类。
    • 目的:增强安全性,简化编程。用户不需要了解具体的实现细节,只需通过具有特定访问权限的外部接口使用类成员。
    • 封装的实现:类声明中的 {}
  • 示例:
1
2
3
4
5
class  Clock {
public: void setTime(int newH, int newM, int newS);
void showTime();
private: int hour, minute, second;
};

继承

  • 在现有类的基础上扩展,形成新类。

多态

  • 多态:同名,不同功能实现。
  • 目的:实现统一的行为识别,减少程序中的标识符数量。

类与对象的定义

类定义的语法形式

1
2
3
4
5
6
7
8
9
class class_name
{
public:
public members (external interface)
private:
private members
protected:
protected members
}

类内初始值

  • 可以为数据成员提供类内初始值
  • 创建对象时,使用类内初始值初始化数据成员
  • 没有初始值的成员将被默认初始化

类成员访问控制

  • 公有类型成员
    • 在 public 关键字后声明,它们是类与外部之间的接口,任何外部函数都可以访问公有类型的数据和函数
  • 私有类型成员
    • 在 private 关键字后声明,只有该类中的函数可以访问,外部函数无法访问。
    • 如果私有成员在类名后立即声明,可以省略关键字 private
  • 保护类型成员
    • 类似于私有,区别在于在继承和派生过程中对派生类的影响不同

类成员函数

  • 在类中声明函数原型
  • 可以在类外提供函数体实现,并在函数名前加上类名
  • 也可以直接在类中提供函数体,形成内联成员函数
  • 允许声明重载函数和具有默认参数值的函数

类与对象的程序示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
using namespace std;
//类定义
class Clock{
public:
void setTime(int newH = 0,int newM = 0,int newS = 0);
void showTime();
private:
int hour,minute,second;
}
//成员函数实现
void Clock::setTime(int newH = 0,int newM = 0,int newS = 0);
{
hour = newH;
minute = newM;
second = newS;
}
void Clock::showTime()
{
cout << hour << ":" << minute << ":" << second << endl;
}
//对象使用
int main()
{
Clock myClock;
myClock.setTime(8,30,30);
myClock.showTime();
return 0;
}

构造函数与析构函数

构造函数

构造函数的目的

在创建对象时使用特定值构造对象,初始化对象为特定初始状态。

当希望构造一个 Clock 类对象并将初始时间设置为 0:0:0 时,可以通过构造函数进行设置。


构造函数的形式

  • 函数名与类名相同
  • 不能定义返回类型,且不能有返回语句
  • 可以有形式参数或没有形式参数
  • 可以是内联函数
  • 可以重载
  • 可以有默认参数值

构造函数调用的时机

在创建对象时自动调用


构造函数示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
using namespace std;
//类定义
class Clock{
public:
Clock(int newH,int newM,int newS);//构造函数
void setTime(int newH = 0,int newM = 0,int newS = 0);
void showTime();
private:
int hour,minute,second;
};//这里有一个分号
//构造函数实现
Clock::Clock(int newH,int newM,int newS): hour(newH),minute(newM),second(newS){}
//成员函数实现
void Clock::setTime(int newH = 0,int newM = 0,int newS = 0);
{
hour = newH;
minute = newM;
second = newS;
}
void Clock::showTime()
{
cout << hour << ":" << minute << ":" << second << endl;
}
//对象使用
int main()
{
Clock c(0,0,0);//自动调用构造函数
c.showTime();
return 0;
}

:: 符号是作用域解析运算符,表示函数定义需要使用 class_name:: 来限定成员函数。

非常重要的一点是,类定义结束后有一个分号!!!(浪费了很多时间)


默认构造函数

  • 可以不带实际参数调用的构造函数
    • 参数列表为空的构造函数
    • 所有参数都有默认值的构造函数
  • 以下两个都是 默认构造函数。如果它们同时出现在一个类中,将会发生编译错误:
1
2
3
Clock();
Clock(int newH=0,int newM=0,int newS=0);
// 两个函数都不需要初始值,因此会发生调用冲突

隐含生成的构造函数

如果程序中未定义构造函数,编译器将在需要时自动生成一个默认构造函数

  • 参数列表为空,不为数据成员设计初始值
  • 如果类内定义了成员的初始值,则使用类内定义的初始值
  • 如果没有定义类内的初始值,则以默认方式初始化
  • 基本类型的数据默认初始化的值是不确定的

“=default”

如果类内已定义构造函数,默认情况下编译器不再隐含生成默认构造函数。 如果你坚持希望隐含生成默认构造函数,用“=default”

1
Clock() = default;//指示编译器提供默认构造函数

委托构造函数

委托构造函数(delegating constructor)使用类其他构造函数执行初始化过程

例如

1
2
3
Clock(int newH,int newM,int newS):hour(newH),minute(newM),second(newS){}
Clock():Clock(0,0,0){}
//有的编译器并不支持委托构造函数

复制构造函数

复制构造函数是一种特殊的构造函数,其形参为本类的对象引用。作用是用一个已存在的对象去初始化同类型的新对象。

用法:

1
2
3
4
5
6
7
8
9
class 类名
{
public:
类名(形参);//构造函数
类名(const 类名&对象名);//复制构造函数
// ...
};
类名::类(const 类名&对象名)//复制构造函数的实现
{函数体}

隐含的复制构造函数

  • 如果没有为类声明拷贝初始化构造函数,则编译器自己生成一个复制构造函数。
  • 这个构造函数执行的功能是:用作为初始值对象的每个数据成员的值,初始化将要建立的对象的对应数据成员。

“=delete”

如若不希望被复制构造

1
Point(const Point&p) = delete;

复制构造函数被调用的三种情况

  • 定义一个对象时,以另一个对象作为初始值,发生复制构造
  • 如果函数的形参是类的对象,调用函数时,将使用实参对象初始化形参对象,发生复制构造
  • 如果函数的返回值是类的对象,函数执行完成返回主函数时,将使用return语句中的对象初始化一个临时无名对象,传递给主函数,此时发生复制构造

析构函数

作用:完成对象被删除前的一些清理工作

  • 在对象生存期结束的时刻系统自动调用它,然后再释放此对象所属的空间
  • 如果程序中未声明析构函数,编译器会自动生成一个默认的析构函数,函数体为空

类的组合

简单来说,就是类的数据成员是其他类的对象,下面用计算两点之间线段的距离的程序来说明。


线段类和点类实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <iostream>
#include <cmath>
using namespace std;

//Point类
class Point{
public:
Point(float xx,float yy); //构造函数
Point(); //默认构造函数
Point(Point &p); //复制构造函数
float getX(){return x;}
float getY(){return y;}
private:
float x,y;
};
Point::Point(float xx,float yy):x(xx),y(yy){} //构造函数的定义
Point::Point():x(0),y(0){} //默认构造函数的定义
Point::Point(Point &p) //复制构造函数的定义
{
x=p.x;
y=p.y;
cout << "Calling the copy constructor of Point" << endl;
}
//Line类,类的组合,计算两点之间的距离
class Line{
public:
Line(Point o1,Point o2); //参数为Point对象的构造函数
Line(Line &l); //复制构造函数
float getLen(); //外部接口
private:
Point p1,p2;
float len=0;
};
Line::Line(Point o1,Point o2):p1(o1),p2(o2) //构造函数是每一个成员变量都要初始化的
{
float x=p1.getX()-p2.getX();
//此处并不能够直接写 p1.x - p2.x 因为x属于私有变量
// 通过两个public成员函数可以访问,相当于提供了外部接口
float y=p1.getY()-p2.getY();
len= sqrt(x*x+y*y);
cout << "Calling the constructor of Line" << endl;
}
Line::Line(Line &l): p1(l.p1),p2(l.p2) //复制构造函数是每一个成员变量都需要进行复制的 且这个'.'只能够在类的作用域内使用。
{
len=l.len;
cout << "Calling the copy constructor of Line" << endl;
}
float Line::getLen() {return len;}
int main()
{
Point myp1(1,1),myp2(4,5);
Line line(myp1,myp2); //参数传递的时候也需要复制构造哦
Line line2(line);
cout << "The length of line is " << line.getLen() << endl;
cout << "The length of line2 is " << line2.getLen() << endl;
return 0;
}


前向引用声明

遇到两个类相互引用的情况,也称为循环依赖,简单理解就是你不能使用一个在前面完全没有出现过的标识符。

1
2
3
4
5
6
7
8
9
class B;            //前向引用声明
class A{ //A类的定义
public: //外部接口
void f(B b); //以B类对象b为形参的成员函数
};
class B{ //B类定义
public: //外部接口
void g(A a); //以A类对象a为形参的成员函数
};