前言

🎡前面几篇文章介绍了C++面向对象的基础,本篇将介绍面向对象最重要的部分。

🎃它是所有采用面向对象思想的编程语言的核心:封装、基础和多态

🎉面向对象思想需要通过实践学习,仅仅通过文字描述很难理解它的本质。

封装

🎡封装指将数据成员私有化,通过成员函数的形式访问数据成员。

🎃封装的特点是降低各模块间的耦合度、提高可维护性,避免错误的发生

下面通过一个例子说明为什么需要封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>

using namespace std;

class Student
{
public:
int age;
};

int main()
{
Student student;

student.age = -1;
cout << student.age << endl;

return 0;
}

输出结果:

1
-1

🎡首先,直接对数据成员进行访问,不利于模块独立化,也不利于后期的维护。

🎃其次,对age数据成员赋值-1显然不符合逻辑。

采用封装思想,可以改为下面的代码:

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
#include <iostream>

using namespace std;

class Student
{
public:
bool setAge(int age); //成员函数:设置age
int getAge(); //成员函数:获取age

private:
int age; //age访问权限为private,仅限类内部访问
};

bool Student::setAge(int age)
{
if (age < 0)
{
cout << "年龄范围错误,请输入大于0的数字" << endl; //不符合要求不设置age
return false; //失败返回false
}
else
{
this->age = age; //符合要求设置age
return true; //成功返回true
}
}

int Student::getAge()
{
return age; //返回age
}

int main()
{
Student student;

student.setAge(-1); //不符合要求
student.setAge(18); //成功设置age
cout << student.getAge() << endl; //输出age

return 0;
}

输出结果:

1
2
年龄范围错误,请输入大于0的数字
18

🎃通过封装,我们给外界预留接口,使用接口的人不需要知道具体的实现细节

类的友元

C++提供了友元机制,可用来破坏数据封装和隐藏

封装是一个优越的机制,为了不破坏封装和隐藏原则,不推荐使用友元机制。

友元函数

🎡函数或其它类的成员函数可以声明为类的友元,这样的函数叫做友元函数

🎈友元函数可以访问类内部的私有成员。

❗友元函数虽然在类中声明,但他不是类的成员函数。

函数声明为类的友元:

1
friend 返回类型 函数名(参数列表);

其它类的成员函数声明为类的友元函数:

1
friend 返回类型 其它类名::函数名(参数列表);

友元类

我们可以把一个类声明为另一个类的友元类。

如果类B对类A频繁的数据访问,并且由于类A的private的限制,类B只能通过public的成员函数进行访问。

这时候,不妨让类B成为类A的友元类。这样,类B的成员函数就全部成为类A的友元函数。

类B就可以直接访问类A的private成员了。

友元类定义格式:

1
2
3
4
class A
{
friend class B; //声明类B为类A的友元类
}

继承

🎃继承是指在原有类的基础上进行拓展或重写部分成员,以提高代码利用率。

🐾原有类即基类或父类,继承原有类的叫做派生类或子类

🎡派生类拥有基类的所有成员,并可以增加新成员

我们可以新增基类中存在的成员,达到覆盖重写的目的,隐藏基类的成员。

注意:派生类不能继承基类的构造函数、析构函数和赋值运算符重载

派生类定义方法:

1
2
3
4
class 派生类名:继承方式1 基类1, 继承方式2 基类2
{
派生类成员;
};

😃C++中允许一个派生类同时继承多个基类

下面,通过例子说明继承:

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
#include <iostream>

using namespace std;

class Animal //定义类Animal
{
public:
void run(); //Animal会跑
};

class Dog:public Animal //定义Dog类继承Animal
{

};

void Animal::run()
{
cout << "跑" << endl;
}

int main()
{
Dog dog; //创建Dog对象

dog.run(); //dog继承Animal也会跑

return 0;
}

输出结果:

1

新增成员

🎡我们可以在派生类中新增成员,达到拓展的目的。

通过例子说明:

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
#include <iostream>

using namespace std;

class Animal
{
public:
void run();
};

class Dog:public Animal
{
public:
void shout(); //新增成员函数
};

void Animal::run()
{
cout << "跑" << endl;
}

void Dog::shout()
{
cout << "汪汪汪" << endl;
}

int main()
{
Dog dog;

dog.run();
dog.shout(); //dog除了继承run函数,自己也有了shout()函数

return 0;
}

输出结果:

1
2

汪汪汪

隐藏成员

🎡我们可以在派生类中重写基类的成员达到覆盖重写的目的,隐藏基类成员。

下面通过例子说明:

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
#include <iostream>

using namespace std;

class Animal
{
public:
void run();
};

class Dog:public Animal
{
public:
void run(); //同名成员,会隐藏基类的成员
};

void Animal::run()
{
cout << "跑" << endl;
}

void Dog::run()
{
cout << "跑的更快了" << endl;
}

int main()
{
Dog dog;

dog.run(); //达到了覆盖重写的目的

return 0;
}

输出的结果:

1
跑到更快了

继承方式

🎡除了public、protected和private成员外,在派生过程中,还会出现一种不可访问成员。

🎃不可访问成员被隐藏,在类内和类外均不能访问。

公有继承

使用public继承方式:

1
2
3
4
class 派生类 : public 基类
{
派生类成员;
};

公有继承对基类成员访问权限:

基类成员 派生类成员
public成员 public成员
protected成员 protected成员
private成员 不可访问成员
不可访问成员 不可访问成员

🎃如果我们想某个成员只能在类内访问,并且可以被子类继承。可以给定它为protected

🎡peotected可以理解为可以被继承的private

私有继承

使用private继承方式:

1
2
3
4
class 派生类 : private 基类
{
派生类成员;
};

公有继承对基类成员访问权限:

基类成员 派生类成员
public成员 private成员
protected成员 private成员
private成员 不可访问成员
不可访问成员 不可访问成员

保护继承

使用protected继承方式:

1
2
3
4
class 派生类 : protected 基类
{
派生类成员;
};

公有继承对基类成员访问权限:

基类成员 派生类成员
public成员 protected成员
protected成员 protected成员
private成员 不可访问成员
不可访问成员 不可访问成员

public继承使用最频繁,无论何种继承方式,private和不可访问成员在派生类中均为不可访问成员

公有继承中,public成员和protected成员保留原有访问属性

私有继承中,public成员和protected成员访问权限变为private

保护继承中,public成员和protected成员访问权限变为protected

派生类的构造函数

派生类的构造函数有两个功能:

  1. 调用基类构造函数完成基类数据成员初始化。
  2. 初始化派生类新增的数据成员

派生类构造函数定义:

1
2
3
4
派生类名::派生类构造函数(参数列表):基类构造函数(参数列表)
{
派生类构造函数语句;
}

🎡如果基类定义了带参数的构造函数,必须为派生类定义构造函数。

🎃如果基类没有定义带参构造函数,派生类构造函数可以省略。

😄C++11中提供了继承基类构造函数的方法,使用using语句继承。有兴趣可自行查阅资料。

派生类的析构函数

编译器先调用派生类的析构函数,然后调用子类的析构函数

派生类的析构函数可以省略,系统提供默认的析构函数。

如果在子类中创建了内存空间,则需要定义析构函数用于回收空间。

多重继承

当世界从滥用继承从吃到苦头时,组合成了大家关注的问题。

”能够组合就尽量不要用继承“,这是<<Java编程思想>>的名言。

继承,是一种高度的耦合,派生类和基类被紧紧的绑在一起,灵活性大大降低。

而且,滥用继承,也会使继承树变得又大又复杂,很难理解和维护

因此,本文不再讲解多重继承,有兴趣可以自行查阅资料。

多态

虚函数

🎡在介绍多态前,我们先来了解一下虚函数

虚函数(Virtual Function)即用Virtual修饰的成员函数

虚函数用于多态,所在类一般是基类。派生的子类重写虚函数(子类可省略Virtual)。

纯虚函数

🎃纯虚函数除了使用virtual修饰,还需要在函数声明后加 = 0

🎡纯虚函数的作用:基类可以仅仅给出纯虚函数的声明,而不给出具体定义。

🎈由继承它的派生类完成对纯虚函数的定义。

纯虚函数声明的格式:

1
virtual 返回类型 函数名(参数列表) = 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
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>

using namespace std;

class Animal //定义基类Animal
{
public:
void run(); //定义成员函数run()
};

class Dog : public Animal //定义派生类Dog公有继承Animal类
{
public:
void run(); //重写成员函数run()
void shout(); //新增成员函数shout()
};

void Animal::run()
{
cout << "动物在跑" << endl;
}

void Dog::run()
{
cout << "狗在跑" << endl;
}

void Dog::shout()
{
cout << "汪汪汪" << endl;
}

int main()
{
Animal* animal; //指向基类的指针
Dog dog; //派生类对象

animal = &dog; //多态:基类指针或引用指向了派生类对象
animal->run(); //只能访问继承自基类的成员

return 0;
}

输出的结果:

1
动物在跑

🎃可以看出,尽管我们在派生类中重写了run函数,将基类的run函数隐藏

🎈但是,这里调用run函数时,还是调用了基类的run函数

😋我们实际上想调用的是重写后的run函数,否则,多态就显得毫无意义了。

🎃这时,我们就用到了虚函数。我们重写虚函数达到覆盖基类函数的目的。

下面通过例子说明:

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
#include <iostream>

using namespace std;

class Animal //定义基类Animal
{
public:
virtual void run(); //定义虚成员函数run()
};

class Dog : public Animal //定义派生类Dog公有继承Animal类
{
public:
void run(); //重写虚成员函数run(),可省略virtual
void shout(); //新增成员函数shout()
};

void Animal::run()
{
cout << "动物在跑" << endl;
}

void Dog::run()
{
cout << "狗在跑" << endl;
}

void Dog::shout()
{
cout << "汪汪汪" << endl;
}

int main()
{
Animal* animal; //指向基类的指针
Dog dog; //派生类对象

animal = &dog; //多态:基类指针或引用指向了派生类对象
animal->run(); //只能访问继承自基类的成员

return 0;
}

输出的结果:

1
狗在跑

😋通过重写虚函数,我们覆盖而不是隐藏了基类的函数。

多态的优势

🎡基类中声明虚函数,派生类中重写覆盖虚函数,通过基类指针或引用访问派生类对象叫做多态。

通过多态,提供了公共接口,降低模块间的耦合度,增强后期可维护性和拓展性

虚析构函数

🎡因为基类的指针和引用只能访问继承自基类的成员,所以编译器默认情况下调用基类的析构函数回收内存。

🎃如果我们使用派生类对象申请内存空间,则同样需要调用派生类的析构函数

😋这就用到了:虚析构函数

在基类的析构函数前加virtual关键字,就可以声明虚析构函数。

这样,在对象销毁时,编译器即调用基类的析构函数也调用派生类的析构函数

总结

🎡本节介绍了C++面向对象思想最重要的部分:封装、继承和多态。

🎃此外,还补充了友元机制的相关知识。

🚩创作不易,本人保证所发文章均为精心筹备。

💌如需转载,请保留作者信息和博客地址。

📡如果感觉博客对你略有帮助,欢迎转发给你的朋友,让他们加入到技术风暴中来吧!