0%

Cpp基础(7)类和对象

面向对象程序设计的基本特点

抽象:对同一类对象的共同属性和行为进行概括,形成类。

  • 首先注意问题的本质及描述,其次是实现过程或细节。
  • 数据抽象:描述某类对象的属性或状态(对象相互区别的物理量)。
  • 代码抽象:描述某类对象的共有的行为特征或具有的功能。
  • 抽象的实现:类。
1
2
3
4
5
6
7
8
class Clock
{
public:
void setTime(int newH, int newM. int newS);
void showTiem();
private:
int hour, minute, second;
}

封装:将抽象出的数据,代码封装在一起,形成类。

  • 目的:增强安全性和简化编程,使用者不必了解具体的实现细节,而只需要通过外部接口,以特定的访问权限,来使用类的成员。
  • 实现封装:类声明中的{ }

QQ图片20200204174646

继承:在已有类的基础上,进行扩展形成新的类。

多态:同一名称,不同的功能实现方式。达到行为标识统一,减少程序中标识符的个数。

类和对象的定义

对象是现实中的对象在程序中的模拟;类是同一类对象的抽象,对象是类的实例。定义类的对象,才可以通过对象使用类中定义的功能。

设计类就是设计类型,需要关注哪些问题

  • 此类型的“合法值”是什么?
  • 此类型应该有什么样的函数和操作符?
  • 新类型的对象该如何被创建和销毁?
  • 如何进行对象的初始化和赋值?
  • 对象作为函数的参数如何以值传递?
  • 谁将使用此类型的对象成员?

类定义的语法形式

1
2
3
4
5
6
7
8
9
class 类名称
{
public:
公有成员(外部接口)
private:
私有成员
protected:
保护型成员
}

在定义类时也可以为数据成员设置类内初始值,用于初始化数据成员。

1
2
3
4
5
6
7
8
class Clock
{
public:
void setTime(int newH, int newM. int newS);
void showTiem();
private:
int hour = 0, minute = 0, second = 0;
}

类成员的访问控制

公有类型成员:在关键字public后面声明,它们是类与外部的接口,任何外部函数都可以访问公有类型数据和函数。

私有类型成员:在关键字private后面声明,只允许本类中的函数访问,而类外部的任何函数都不能访问。如果紧跟在类名称的后面声明私有成员,则关键字private可以省略。如果某个成员前面没有上述关键字,则缺省地被认为是私有成员。

保护类型成员:与private类似,其差别表现在继承与派生时对派生类的影响不同。

类中成员之间直接使用成员名互相访问。

从类外访问成员使用“ 对象名.成员”,来访问公有成员。

类的成员函数

在类中声明函数原型:

  • 可以直接在类中给出函数体,形成内联成员函数;
1
2
3
4
5
6
7
8
9
10
//定义一个矩形的类
class Rectangle
{
private:
int w;
int h;
public:
int getArea() { return w*h; }
int getPerimeter() { return 2*(w+h); }
};
  • 也可以在类外给出函数体实现,并在函数名前用类名加以限定;
1
2
3
4
5
6
7
8
9
10
11
12
13
//定义一个矩形的类
class Rectangle
{
private:
int w;
int h;
public:
int getArea();
int getPerimeter();
};

int Rectangle::getArea() { return w*h; }
int Rectangle::getPerimeter() { return 2*(w+h); }
  • 允许声明重载函数和带默认参数值的函数。

例子:设计一个圆的类,该类的成员变量为圆心的x轴坐标,y轴坐标,半径长度;该类的成员变量对外都是不可见的;该类的成员函数为:设置圆心坐标,设置圆心半径,计算圆的面积,计算圆的周长。

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
#include<iostream>
using namespace std;

class Circle
{
private:
double x;
double y;
double r;
public:
void setM(double _x, double _y) { x = _x; y = _y; }
void setR(double _r) { r = _r; }
double getArea() { return 3.14 * r * r; }
double getPerimeter() { return 2 * 3.14 *r; }
};

int main()
{
Circle myCircle;
myCircle.setM(1.7, 3.2);
myCircle.setR(4.2);
double myArea = myCircle.getArea();
double myPeri = myCircle.getPerimeter();
cout << "Area = " << myArea << ", Perimeter = " << myPeri << endl;

myCircle.setR(8.4);
myArea = myCircle.getArea();
myPeri = myCircle.getPerimeter();
cout << "Area = " << myArea << ", Perimeter = " << myPeri << endl;
}

QQ图片20200204174231

构造函数

当我们定义对象时,如何对对象进行初始化?在定义基本类型的变量时,是可以直接给定初始值的,但是在定义对象时却不是这么简单,因为一个类是我们自己定义的,对类的对象按照什么规则进行初始化,编译器是不会自动知道的,必须由程序员写程序来规定。为此C++中提供了一种特殊的机制:构造函数,在构造函数中我们可以描述如何对类的对象进行初始化。

基础知识

构造函数的作用

  • 在对象被创建时使用特点的值构造对象,将对象初始化为一个特定的初始状态

例如:希望在构造一个Clock类对象时,将初始时间设为0:0:0,就可以通过构造函数来设置

构造函数的形式

  • 函数名与类名相同;
  • 不能定义返回值类型,也不能在函数体有return语句;
  • 可以有形式参数,也可以没有形式参数;
  • 可以是内联函数;
  • 可以是重载;
  • 可以带默认参数值。

构造函数的调用时机

  • 在对象创建时被自动调用,但如果没有定义构造函数就进行初始化,那么编译器就会报错。
1
Clockk myClock(0,0,0);

默认构造函数

调用时可以不需要实参的构造函数:

  • 参数表为空的构造函数
  • 全部参数都有默认值的构造函数

下面两个都是默认构造函数,如在类中同时出现,将产生编译错误,不是合法的函数重载形式

1
2
Clock();
Clock(int newH=0, int newM=0, int newS=0)

隐含生成的构造函数

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

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

如果定义的类的成员不是基本类型的成员,而是其他类的对象,这个就是类组合的情况,其默认的初始化方式由它所属的类决定。

=default

如果程序中已定义构造函数,默认情况下编译器就不会再隐含生成默认构造函数。如果此时依然希望编译器隐含生成的默认构造函数,可以使用=default

1
2
3
4
5
6
7
class Clock{
public:
Clock() = default;
Clock(int newH, int newM, int newS);
private:
int hour, minute, second;
}

例子1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include<iostream>
using namespace std;
class Clock {
public:
Clock(int newH, int newM, int newS);
void setTime(int newH, int newM, int newS);
void showTime();
private:
int hour, minute, second;
};

Clock::Clock(int newH, int newM, int newS) :hour(newH), minute(newM), second(newS) {

} //初始化列表

int main() {
Clock c(0, 0, 0);
c.showTime();
return 0;
}

例子2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Clock {
public:
Clock(int newH, int newM, int newS); //构造函数
Clock(); //默认构造函数,如果类要重复使用,一般要提供一个默认构造函数
void setTime(int newH, int newM, int newS);
void showTime();
private:
int hour, minute, second;
};

Clock::Clock():hour(0),mintue(0),second(0){} //默认构造函数
Clock::Clock(int newH, int newM, int newS) :hour(newH), minute(newM), second(newS) {}

int main(){
Clock c1(8,10,0); //调用有参数的构造函数
Clock c2; //调用无参数的默认构造函数
}

委托构造函数

当我们在一个类中重载多个构造函数的时候,往往发现这些构造函数它们只是形参表不同,初始化列表不同,而其他都是一样的,初始化算法都是相同的,函数体都是相同的。那么在这种情况下,如果我们写多个函数体来重载,往往就显得重复了,为了避免这种重复,C++11新标准提供了一种新的机制:委托构造函数,也就是让一个构造函数可以去委托另一个构造函数去帮它完成初始化功能。

回顾Clock类的两个构造函数,第一个构造函数是有三个参数的,第二个构造函数是默认构造函数,没有参数。实际上,这两个构造函数进行初始化的方式是完全一样的,只不过第一个构造函数是用参数表里的参数进行初始化,第二个构造函数用默认状态全部用0进行初始化。那么我们其实没有必要写两个类似的重复的代码。

1
2
Clock::Clock(int newH, int newM, int newS) :hour(newH), minute(newM), second(newS) {} 
Clock::Clock():hour(0),mintue(0),second(0){} //默认构造函数

委托构造函数使用类的其他构造函数执行初始化过程,我们用委托构造函数的方法重写上面的代码,这里第二个构造函数调用了另外一个有参数的构造函数,将默认的三个初始化参数传给有参数表的Clock构造函数,这样就不用把同样的初始化方法再写一遍了。

1
2
Clock::Clock(int newH, int newM, int newS) :hour(newH), minute(newM), second(newS) {}
Clock::Clock():Clock(0,0,0){}

用委托构造函数不仅可以减少重复的工作,其最大的好处是可以保持代码实现的一致性,如果想要修改构造函数的初始化算法时,就只需在一处修改,其他的委托这个构造函数来进行初始化的构造函数的算法也就同步修改了。

复制构造函数

当我们在定义一个基本类型的变量时,经常会用一个已经存在的已经有值的变量去初始化这个变量;我们在定义对象时可会有这样的需求,即用一个存在的对象去初始化一个新的对象,这时要如何实现这种初始化呢?C++中提供了一种特殊的构造函数,叫复制构造函数

在复制构造函数中我们可以规定如何用一个已经存在的对象去初始化一个新对象,可以用这个已经存在的对象的引用作为构造函数的参数。如果在定义类的时候没有定义复制构造函数,编译器也生成一个默认的复制构造函数,它会实现类的两个对象的数据成员之间一一对应复制,这些功能在很多时候已经能满足需求,那么我们就不需要再写复制构造函数了。

复制构造函数定义

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

由于复制构造函数的目的不会是将原有的那个形参对象给修改了,所以最好是在形参引用前加上const关键字

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

类名::类(const 类名 &对象名) //复制构造函数的实现
{ 函数体 }

复制构造函数的调用

除了在定义新对象时,用已有的对象作为参数去初始化它这种情况以外,共有三种情况是典型的要调用复制构造函数的情况:

  • 定义一个对象时,以本类另一个对象作为初始值,发生复制构造;
  • 如果函数的**形参是类的对象**,调用函数时,将使用实参对象初始化形参对象,发生复制构造;
  • 如果函数的返回值是类的对象,函数执行完成返回主调函数时,将使用return语句中的对象初始化一个临时无名对象,传递给主调函数,此时发生复制构造。这种情况也可以通过移动构造避免不必要的复制。

隐含的复制构造函数

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

如果类的成员中有指针的时候,很多情况下,默认的复制构造函数其浅层的复制功能就不够用了,这是我们就需定义深层的复制构造。

=delete

如果我们不希望对象被复制构造,那么可以采用下面的方法:

  • C++98做法:将复制构造函数声明为private,并且不提供函数的实现。
  • C++11做法:用=delete指示编译器不生成默认复制构造函数。
1
2
3
4
5
6
7
class Point{
public:
Point(int xx=0, int yy=0) {x=xx; y=yy} //构造函数,内联
Point(const Point &p) = delete; //指示编译器不生成默认复制构造函数
private:
int x, y;
}

析构函数

当一个对象在存续期间会占用系统资源,当这个对象的生存期结束时,需要进行善后工作将其删除清理掉,C++中提供了这样一种机制:析构函数。当对象被构造时,构造函数会自动调用;当对象要消亡时,其析构函数也会自动调用。

  • 完成对象被删除前的一些清理工作;
  • 在对象的生存期结束的时刻系统自动调用它,然后再释放此对象所属的空间;
  • 如果程序中未声明析构函数,编译器将自动产生一个默认的析构函数,其函数体为空;
  • 析构函数的原型:~类名( );
  • 析构函数没有参数,没有返回类型
1
2
3
4
5
6
7
8
9
10
11
12
class Point{
public:
Point(int xx, int yy) //构造函数
~Point(); //析构函数
private:
int x, y;
}
Point::Point(int xx, int yy)
{
x=xx; y=yy;
}
Point::~Point(){}

类的组合

在制造业多年来都一直使用部件组装的生产方式,与一切手工从头做起相比,部件组装的生产效率肯定是要高,产品的标准化 它的可靠性也都更好。在程序中我们也可以借用这种部件组装的思想,用已经存在的这些类去组装新的类,C++语言支持类的组合。我们在定义一个新类的时候,可以让它的类成员是已有类的对象,也就是说一些类的对象可以作为另外一个类的部件,这就是类的组合。

类组合的基本概念

  • 类中的成员是另外其他类的对象;
  • 可以在已有抽象的基础上实现更复杂的抽象。

成员对象:一个类的成员变量是另一个类的对象

包含成员对象的类叫封闭类(Enclosing)

类组合的构造函数设计

那么组合类的构造函数如何设计呢?每个类的构造函数都是负责自己本类成员初始化的,如果用另外类的对象作为新定义类的成员,那么这个组合类是没有权利去访问部件对象内部的私有成员。因为一个类的私有成员只有这个类内部的函数可以访问,类外任何地方是不可以访问的,而且部件类的设计者、开发者,跟组合类的设计者 开发者可能不是一个人,甚至不是一个团队。因此在写组合类的构造函数时要考虑,由组合类的构造函数负责将部件对象初始化所需要的初始化参数传递给它,然后编译器会自动去调用部件类的构造函数,来初始化这些部件对象。其语法形式如下:

  • 原则:不仅要负责对本类中的基本类型成员数据初始化,也要对对象成员初始化。
  • 声明形式:初始化列表
1
2
3
4
5
类名::类名(对象成员所需的形参,本类成员形参):
对象1(参数), 对象2(参数),......
{
//函数体其他语句
}

构造组合类对象时的初始化次序

  • 首先对构造函数初始化列表中列出的成员(包括基本类型成员和对象成员)进行初始化,初始化次序是成员在类体中定义的次序。
    • 成员对象构造函数调用顺序:按对象成员的定义顺序,先声明者先构造
    • 初始化列表中未出现的成员对象,调用默认构造函数(即无形餐的)初始化
  • 处理完初始化化列表之后,再执行构造函数的函数体

也可以这样理解,构造函数和析构函数的调用顺序

  • 当封闭类对象生成时:
    • S1:执行所有成员对象的构造函数
    • S2:执行封闭类的构造函数
  • 成员对象的构造函数调用顺序
    • 和成员对象在类中的说明顺序一致
    • 与在成员初始化列表中出现的顺序无关
  • 当封闭类的对象消亡时
    • S1:先执行封闭类的析构函数
    • S2:再执行成员对象的析构函数
  • 析构函数顺序和构造函数的调用顺序相反(先构造的后析构,后构造的先析构)

需要注意的是,我们在写类的构造函数时,最好再写一个无参数的默认构造函数。当这个类的对象被用作其他类的部件成员时,可能组合类中没有写构造函数只使用默认构造函数,这个时候我们上面的操作就显得很必要了。

例子:构造一个Point类,再用Point类构造组合类Line类,通过构造函数和复制构造函数中的“调试信息”(cout<<……)可以更好地理解构造函数和复制构造函数的调用过程。

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;
class Point { //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 { //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);
Line line(myp1, myp2);
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;
}

前向引用声明

类应该先声明,后使用,如果需要在某个类的声明之前引用该类,则应进行前向引用声明。前向引用声明只为程序引入一个标识符,但具体声明在其他地方。前向引用声明某个类之后,可在之后的其他类的成员函数中将该类作为参数类型使用。

1
2
3
4
5
6
7
8
9
class B; //前向引用声明
class A{
public:
void f(B b);
};
class B{
public:
void g(A a);
}

需要注意的是:

  • 使用前向引用声明虽然可以解决一些问题,但它并不是万能的。
  • 在提供一个完整的类声明之前,不能声明该类的对象,也不能在内联成员函数中使用该类的对象。
  • 当使用前向引用声明时,只能使用被声明的符号,而不能涉及类的任何细节。
1
2
3
4
5
6
7
class Fred;  //前向引用声明
class Barney{
Fred x; //错误:类Fred的声明尚不完整,不能声明该类的对象
};
class Fred{
Barney y;
}

示例

  1. 声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,有两个公有成员函数run、stop。其中,rank为枚举类型CPU_Rank,声明为enum CPU_Rank {P1=1,P2,P3,P4,P5,P6,P7},frequency为单位是MHz的整型数,voltage为浮点型的电压值。类似地声明一个RAM类。
  2. 声明一个简单的Computer类,有数据成员芯片(cpu)、内存(ram),有两个公有成员函数run、stop。cpu为CPU类的一个对象,ram为RAM类的一个对象。
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#include <iostream>
using namespace std;

enum CPU_Rank { P1 = 1, P2, P3, P4, P5, P6, P7 };
class CPU
{
private:
CPU_Rank rank;
int frequency;
float voltage;
public:
CPU(CPU_Rank r, int f, float v) //构造函数
{
rank = r;
frequency = f;
voltage = v;
cout << "构造了一个CPU!" << endl;
}

CPU(CPU &c) //复制构造函数
{
rank = c.rank;
frequency = c.frequency;
voltage = c.voltage;
cout << "复制构造了一个CPU!" << endl;
}

~CPU() { cout << "析构了一个CPU!" << endl; } //析构函数

CPU_Rank GetRank() const { return rank; } //外部接口
int GetFrequency() const { return frequency; }
float GetVoltage() const { return voltage; }

void SetRank(CPU_Rank r) { rank = r; }
void SetFrequency(int f) { frequency = f; }
void SetVoltage(float v) { voltage = v; }

void Run() { cout << "CPU开始运行!" << endl; }
void Stop() { cout << "CPU停止运行!" << endl; }
};

enum RAM_TYPE { DDR2 = 2, DDR3, DDR4 };
class RAM
{
private:
enum RAM_TYPE type;
unsigned int frequency; //MHz
unsigned int size; //GB
public:
RAM(RAM_TYPE t, unsigned int f, unsigned int s) //构造函数
{
type = t;
frequency = f;
size = s;
cout << "构造了一个RAM!" << endl;
}

RAM(RAM &c) //复制构造函数
{
type = c.type;
frequency = c.frequency;
size = c.size;
cout << "复制构造了一个RAM!" << endl;
}

~RAM() { cout << "析构了一个RAM!" << endl; } //析构函数

RAM_TYPE GetType() const { return type; }
unsigned int GetFrequency() const { return frequency; }
unsigned int GetSize() const { return size; }

void SetType(RAM_TYPE t) { type = t; }
void SetFrequency(unsigned int f) { frequency = f; }
void SetSize(unsigned int s) { size = s; }

void Run() { cout << "RAM开始运行!" << endl; }
void Stop() { cout << "RAM停止运行!" << endl; }
};

//COMPUTER类
class COMPUTER
{
private:
CPU my_cpu;
RAM my_ram;
unsigned int storage_size; //GB
unsigned int bandwidth; //MB

public:
COMPUTER(CPU c, RAM r,unsigned int s, unsigned b); //构造函数

~COMPUTER() { cout << "析构了一个COMPUTER!" << endl; } //析构函数

void Run()
{
my_cpu.Run();
my_ram.Run();
cout << "COMPUTER开始运行!" << endl;
}

void Stop()
{
my_cpu.Stop();
my_ram.Stop();
cout << "COMPUTER停止运行!" << endl;
}
};

//COMPUTER类的构造函数,内嵌对象采用初始化列表初始化
//一共会调用两次复制构造函数,形实结合调用依次,初始化列表调用依次
//当COMPUTER构造函数结束以后,形实结合那个形参的生命周期就结束,于是执行析构函数
COMPUTER::COMPUTER(CPU c, RAM r, unsigned int s, unsigned int b) :my_cpu(c), my_ram(r)
{
storage_size = s;
bandwidth = b;
cout << "构造了一个COMPUTER!" << endl;
}

int main()
{
CPU a(P6, 300, 2.8);
a.Run();
a.Stop();
cout << "***********************\n";

RAM b(DDR3, 1600, 8);
b.Run();
b.Stop();
cout << "***********************\n";

COMPUTER my_computer(a, b, 128, 10);
cout << "***********************\n";

my_computer.Run();
my_computer.Stop();
cout << "***********************\n";

//return之前会执行析构函数,先析构my_computer,和它的两个内嵌成员,然后析构CPU a和RAM b
return 0;
}

PS:结构体,联合体,枚举类的内容在上一篇文章Cpp基础(6)中。