C++类的基础概念:封装、继承、多态
首先,简单的认知一下类:C++不同于C的一种自定义类型的方式。
类(Class) 是 C++ 和 C 的最重要区别,C++ 的早期命名就是 C with Class。
类的存在给C++ 带来了面向对象 (opens new window),封装、继承、多态为类的三大特性。
在学习Class的具体使用方式前,我们必须先对面向对象的核心概念有大致的认知。大家可以通过:
面向对象的核心概念 (opens new window):https://blog.addai.cn/pages/9ac8f7 (opens new window)
做一下概要认知,后面再通过代码理解。
# 类的封装
定义: 将抽象出的 数据、行为 进行有机结合;隐藏细节,指对外提供特定功能的接口 的动作
封装可以分级向外提供访问权限:public、protected、private。
要完成封装,需要做两件事:
从实体猫中抽象 (opens new window) 出我们需要的猫的行为和属性。
将抽象的结果,转换为代码:类 (opens new window)
# 抽象
定义: 从众多的事物中抽取出共同的、本质性的特征,而舍弃其非本质的特征的过程。 衡量特征是否为本质特征,要看编程的目的。
举例:我们想看关注一群猫吃的动作
这时我们的抽象:
当我们只关注猫的吃这个动作时,猫的其他属性和动作对我们而言就没有意义了。
# 类
定义:类是抽象化后的成果。
一般形式:
class 类名称
{
public:
公开的行为定义[函数定义]
protected:
保护的行为定义[函数定义]
private:
私有的行为定义[函数定义]
public:
公开的属性定义[变量定义]
protected:
保护的属性定义[变量定义]
private:
私有的属性定义[变量定义]
}; // 一定注意这里有一个`;`,实际编码中常见的错误;会导致一些无法预知的编译错误。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
关键字解读:
class:类定义关键字
public、protected、private :限定函数、属性的使用范围的关键字, 范围 public > protected > private
封装时范围说明:
public:类内部可以访问、子类可访问、类外部可访问
protected:类内部可以访问、子类可访问
private:类内部可以访问
如果 类 声明时未使用范围的关键字进行标注 函数、属性; 默认为 private
public、protected、private在继承上的使用,呆呆 会在下面 见继承 (opens new window)中讲解
例:上面的猫的抽象得到类
class Cat // 声明一个类
{
public:
void eat()
{
cout << "猫:" << num << " 在吃鱼 " << endl;
}
public:
int num;
};
2
3
4
5
6
7
8
9
10
# 对象
定义:类的实例化结果就是对象。
例:
#include <iostream>
#include <cstring>
using namespace std;
class Cat
{
public:
void eat()
{
cout << "猫:" << num << " 在吃鱼 " << endl;
}
public:
int num;
};
int main(int argc, char* argv[])
{
Cat cat; // 声明一个对象
cat.num = 1;
cat.eat();
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
结果:
猫:1 在吃鱼
# this指针
this指针是类的成员函数的一个隐藏参数,处于形参链表的第一位;它指向当前类对象。
例:
class Cat
{
public:
void eat()
{
cout << "猫:" << this->num << " 在吃鱼 " << endl;
}
public:
int num;
};
2
3
4
5
6
7
8
9
10
# 构造函数 & 析构函数
构造和析构是类中两个重要的概念;
构造函数 :用于初始化类对象,在对象初始化(new 或 直接对象创建)时调用。 析构函数 :用于对象资源释放,在对象释放(delete 或 出作用域时对象直接释放)时调用。
一般形式:
class ClassName
{
public:
ClassName(参数列表) // 函数名 必须同类名
: 初始化列表 // 在初始化列表阶段,对象内存还没建立完成;this是不存在的
{
函数体 // this 可使用
}
~ ClassName() // 函数名 必须同类名; 同时 在前面加上 `~`
{
函数体 // this 可使用
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
例:
#include <iostream>
#include <cstring>
using namespace std;
class Cat
{
public:
Cat(int num)
: m_num(num)
{
cout << "猫: " << this->m_num << " 出生了" << endl;
}
~Cat()
{
cout << "猫: " << this->m_num << " 嗝屁了" << endl;
}
void eat()
{
cout << "猫: " << this->m_num << " 在吃鱼 " << endl;
}
public:
int m_num;
};
int main(int argc, char* argv[])
{
{ // 添加作用域,让 cat析构
Cat cat(1);
cat.eat();
}
Cat *pCat = new Cat(2);
pCat->eat();
delete pCat;
return 0;
}
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
结果:
猫: 1 出生了
猫: 1 在吃鱼
猫: 1 嗝屁了
猫: 2 出生了
猫: 2 在吃鱼
猫: 2 嗝屁了
# 类的继承
定义: 描述父子类的关系,子类继承于父类;子类就是父类的一种特例,子类拥有父类的所有信息
继承的方式一般有:public、protected、private
一般形式:
class 子类 : public\protected\private 父类
{
类实现;
};
2
3
4
关键词解释:
public、protected、private :限定函数、属性的使用范围的关键字, 范围 public > protected > private
继承时时范围说明:
访问方式 public protected private 子类访问父类 public、protected public 无权限 子类对象访问父类 public 无权限 无权限
例:
#include <iostream>
#include <cstring>
using namespace std;
class Pet
{
public:
Pet(int num)
: m_num(num)
{
cout << "宠物: " << this->getNum() << " 出生了" << endl;
}
~Pet()
{
cout << "宠物: " << this->getNum() << " 嗝屁了" << endl;
}
void eat()
{
cout << "宠物: " << this->m_num << " 在吃食物 " << endl;
}
protected:
int getNum()
{
return m_num;
}
private:
int m_num;
};
class Cat : public Pet
{
public:
Cat(int num)
: Pet(num)
{
cout << "猫: " << this->getNum() << " 出生了" << endl;
}
~Cat()
{
cout << "猫: " << getNum() << " 嗝屁了" << endl;
}
};
int main(int argc, char* argv[])
{
{ // 添加作用域,让 cat析构
Cat cat(1);
cat.eat();
}
return 0;
}
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
结果:
宠物: 1 出生了
猫: 1 出生了
宠物: 1 在吃食物
猫: 1 嗝屁了
宠物: 1 嗝屁了
# 类的多态
定义: 为不同数据类型的实体提供统一接口,并表现出不同的行为。多态是针对行为(函数)的知识
一般认为多态有 :重载(overload)、隐藏(hide)、覆盖(override) 三种情况
呆呆这里还没有讲到内存结构,大家可能还不能很好的理解;这一节建议大家:一要看代码,二要自己写demo体会 后面将内存结构时,相信可以让大家豁然开朗
# 重载(overload)
定义:
条件1: 同一个类中
条件2: 相同函数名
条件3: 参数不同(参数类型,或参数个数)
结果: 函数调用由传入参数决定
例:
class Cat
{
public:
Cat(int num)
: m_num(num)
{
cout << "猫: " << this->m_num << " 出生了" << endl;
}
~Cat()
{
cout << "猫: " << this->m_num << " 嗝屁了" << endl;
}
void eat(int weight)
{
cout << "(int)猫: " << this->m_num << " 吃了 " << weight << " kg 鱼 " << endl;
}
void eat(double weight)
{
cout << "(double)猫: " << this->m_num << " 吃了 " << weight << " kg 鱼 " << endl;
}
public:
int m_num;
};
int main(int argc, char* argv[])
{
Cat* pCat = new Cat(2);
pCat->eat(1);
pCat->eat(1.3);
delete pCat;
return 0;
}
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
这里呆呆两次调用了eat方法,只是传入参数的类型不同;对应的结果也不相同。
结果:
猫: 2 出生了
(int)猫: 2 吃了 1 kg 鱼
(double)猫: 2 吃了 1.3 kg 鱼
猫: 2 嗝屁了
这里可能会有疑问:如果是函数名、参数相同,返回值不同会是什么情况?
呆呆这里需要提醒大家:在一个类中,C++类中不能声明两个 函数名和参数都相同的函数。
# 隐藏 (hide)
定义: 隐藏是子类对父类的一种覆盖隐藏的行为,对象是 同名的标识符(函数和属性); 如果子类中存在和父类相同的标识符,且不构成覆盖(override),则是隐藏。
# 属性隐藏
条件1: 两个类呈 父子关系; class A 继承 class B
条件1: A、B 中存在同名属性: attitude
使用: 使用Class A 创建一个对象,赋值给A类型的变量; 使用attitude
结果: 使用的是 A 中的 属性
例:
#include <iostream>
#include <cstring>
using namespace std;
class Pet
{
public:
Pet(int num)
: m_num(num) {}
~Pet() { }
public:
int m_num;
};
class Cat : public Pet
{
public:
Cat(int num)
: Pet(num) // 父类的m_num 赋值为num
, m_num(100) // 子类的m_num 赋值为 100
{ }
~Cat() { }
public:
int m_num;
};
int main(int argc, char* argv[])
{
Cat* pCat = new Cat(2); // 创建 Cat对象赋值给 Cat变量
cout << pCat->m_num << endl;// 输出Cat变量的m_num
delete pCat;
Pet* pPet = new Cat(2); // 创建 Cat对象赋值给 Pet变量
cout << pPet->m_num << endl;// 输出Pet变量的m_num
delete pPet;
return 0;
}
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
结果:
100
2
这里可以发现,父子类的属性都是存在内存中的;我们可以分别使用父子类的变量,去访问他们的属性
# 行为隐藏(函数隐藏)
条件1: 两个类呈 父子关系; class A 继承 class B
条件2: A、B 中存在同名函数: fun
条件3: A、B中的fun不呈覆盖逻辑
使用: 使用Class A 创建一个对象,赋值给A类型的变量; 使用fun
结果: 使用的是 A 中的 函数
例:
#include <iostream>
#include <cstring>
using namespace std;
class Pet
{
public:
void eat() // 定义eat函数
{
cout << "Pet eat!!!" << endl;
}
};
class Cat : public Pet
{
public:
void eat(int num) // 子类声明了同名eat函数,就会隐藏父类eat
{
cout << "Cat eat!!!" << endl;
}
};
int main(int argc, char* argv[])
{
Cat* pCat = new Cat();
// pCat->eat(); // eat() 函数被隐藏,无法被调用
pCat->eat(1);
delete pCat;
Pet* pPet = new Cat();
pPet->eat();
delete pPet;
return 0;
}
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
结果:
Cat eat!!!
Pet eat!!!
# 覆盖 (override)
定义:
条件1: 两个类呈 父子关系; class A 继承 class B
条件2: A、B中存在函数 fun(fun_a、fun_b):函数名、函数参数完全相同,返回值相同 或 为父子关系(fun_a 的返回值 是 fun_b的返回值的子类)
条件3: Class B 的 函数 有virtual修饰符
使用: 使用Class A 创建一个对象,赋值给B类型的变量; 调用 fun
结果: 调用结果为 Class A 定义的 fun_a
这里用virtual标识的函数,又称 虚函数
例:
#include <iostream>
#include <cstring>
using namespace std;
class Pet
{
public:
Pet(int num)
: m_num(num) {}
// 需要注意的是:类成员中存在一个虚函数,那析构函数一定要是虚函数
// 这是一个编程习惯,后续有机会;再讲解
virtual ~Pet() { }
// 父类的 virtual 是必须的
virtual void eat()
{
cout << "宠物: " << this->m_num << " 在吃食物 " << endl;
}
protected:
int m_num;
};
class Cat : public Pet
{
public:
Cat(int num)
: Pet(num) { }
~Cat() { }
// 子类的 virtual 和 override 关键字可以省略;
// virtual: 添加上增加可读性
// override: 添加上可以帮助编译器做编译器检查
virtual void eat() override
{
cout << "猫: " << this->m_num << " 在吃食物 " << endl;
}
};
int main(int argc, char* argv[])
{
// 这里使用 Cat 类 创建一个 Pet类型 的 变量 pCat
Pet* pCat = new Cat(2);
pCat->eat();
delete pCat;
return 0;
}
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
虽然 pCat类型是Pet,但是它的内存中存储的是 Cat类的对象;这时由于覆盖的特性,调用的是Cat类的函数
结果:
猫: 2 在吃食物
# 综述
重载(overload):描述的是一个类的同名函数使用规则
隐藏(hide):描述的是父子类的同名函数,同名属性的使用规则
覆盖(override):描述的是父子类的虚函数使用规则
呆呆在这里提醒:在正式的编程生产时,函数的隐藏(hide)特性是不被提倡的;
即:子类不要有和父类相同名称的函数;
解决方案:
业务意义相同使用父类
业务意义不相同,重命名;子类单独实现
这会干扰编程设计,一个对象在不同的变量状态下,可能行为结果不同
# 扩展认知
多态还可以分为:
变量多态:基础类型变量可以被赋值基础类型对象,也可以被赋值派生类型对象。
函数多态:相同的函数调用(函数名和实参数表),传递给一个对象变量a,可以有不同的行为。行为由变量a的类型决定。
也可以分为:
动态多态:在运行期决定的多态,主要为通过虚继承的方式,实现父类,不同子类的实现不同;即override。
静态多态:在编译期决定的多态
静态多态分为:非参数化多态和参数化多态
非参数化多态:函数重载,运算符重载;即overload
参数化多态:把类型做出参数的多态,泛型编程