c++的类的封装/继承/多态的简单介绍
本篇文章仅仅从很表层来介绍一个C++语言中的类,包括什么是类,类的封装性/继承性和多态性。高手直接跳过吧,看了浪费时间,新手或者想温习一下的可以浏览看看。
1. 什么是类?
到底什么是类(class)??类就是一种类型,是用户自己定义的一个类型,和内置类型如int/float/double类似, 用一个类可以去定义一个变量,即课本中所谓的类的实例化,会得到一个object。类这个类型比较特别,它即包括了数据(数据成员),又包含了若干个操作这些数据的方法(即成员函数)。为什么需要类呢?类提供了一种对事物的抽象,增强了事物的聚合性,类让我们可以把一个事物当作一个整体去看,方便描述,方便建模。 例如:我们可以定义一个学生的类:包括了姓名/性别/年龄/学号ID等信息
1 class Student 2 { 3 public: 4 int GetID(); 5 string GetName(); 6 string GetSex(); 7 int GetAge(); 8 9 void SetID(int ID_); 10 void SetName(string Name_); 11 void SetSex(string Sex_); 12 void SetAge(int Age_); 13 14 private: 15 string m_Name; 16 string m_Sex; 17 int m_ID; 18 int m_Age 19 };
如上所示,我们定义了一个Student的类, 定义它之后,编译器就不光知道了Student是一个类型,而且知道了类型的一些细节,例如当使用该类型去定义一个变量(object)时,需要分配多少内存等,例如:
1 // 输入Student类型在内存中占多少字节 2 cout << "Student类型的大小为:" << sizeof(Student) << endl; 3 4 //实例化一个叫小明的学生,并命名为SB(有点矛盾) 5 Student xiaoming; 6 xiaoming.SetName("SB"); 7 cout << xiaoming.GetName() << endl;
接下来,详细说说如何定义一个类:
定义一个类,需要做的是:1. 声明类拥有的数据成员, 2. 声明类拥有的成员函数:
1 class Dog 2 { 3 public: 4 int Age; 5 int GetAge(); 6 };
成员函数在哪里定义呢?即可以在类的内部直接定义,也可以在类的外部进行定义(此时,需要指明所属的类)。当定义在类的内部时,默认声明为inline函数。 当类外部定义成员函数时,可以有类内声明为inline函数,也可以在定义时候声明了inline函数,但是个人更喜欢在类外定义的时候声明为inline函数,这样可以根据自己定义一个函数的实际情况,决定是否声明为inline函数,而不需要提前考虑好。
1 // 类的内部定义 2 class Dog 3 { 4 public: 5 int Age; 6 int GetAge() //当定义在类的内部时,默认声明为inline函数 7 { 8 return Age; 9 } 10 }; 11 12 // 类外部定义 13 class Dog 14 { 15 public: 16 int Age; 17 int GetAge(); 18 }; 19 20 Dog::GetAge() 21 { 22 return Age; 23 } 24 25 // 类外部定义,显示声明为inline函数 26 class Dog 27 { 28 public: 29 int Age; 30 inline int GetAge(); 31 };
另外:
1. 类成员函数通过隐式this指针访问类内部的数据成员的; 如果把this指针修饰为const,在成员函数后面加const ,这就是const 成员函数: GetAge() const
2. 编译器在解析一个类,总是先把类内部的数据成员解析完毕,再去处理成员函数,因此,定义一个类时,不必关心数据成员与成员函数的先后位置,数据成员圣成员函数问题可见的。
3. 一个类域也是一个作用域(用{ }括起来的部分),正因为如此, 1. 在类的外部定义成员函数时,指定类名就进入了类的作用域中了,就可以找到数据成员了; 2. 对类内的静态数据成员与静态成员函数,可以通过类名作用域访问(对非静态的不可以,因为非静态的数据成员属于具体的一个对象而不属于类,虽然非静态的成员函数实际上在多个对象之间是共享的,但是也只能通过对象名对象的指针访问,因为它们有隐式的this指针)
2. 类的封装性
封装性就是说可以把一部分东西封装起来,不让别人看到。在C++中,类这种类型通过它的访问控制符来体现了它的封装性,包括:
public: 公有的,在类的外部放着,谁都可以看到;
protected: 保护性的,只让它的子类可以看到;
private: 私有的,即把它们封装在了类的内部,在类的外面是看不到的,只有类内部的人可以看到;
例如:定义了一个House的类,House外面的只能看到pulic下的内容,而在House里面,可以看到所有内容;
1 class House 2 { 3 public: 4 int Windows; 5 void OpenWindow(); 6 int doors; 7 void OpenDoors(); 8 9 protected: 10 int *** // 这个不好举例子 11 12 private: 13 int Desk; 14 int light; 15 void OpenLight(); 16 }
使类具有封装性的目的是:我们可以定义一些类,只对类的使用者留一下公有的接口(即pulic下的内容),而类内部的相关操作对类的使用者来说是透明的,用户不操心。我可以随便改类内部的代码,只有公有的接口不变,类的用户的代码是不需要调整的。保证数据安全,方便用户使用,大家都省心啊。
3. 类的继承
如果我们想在一个类的基础上继续创建一个新类,这就用到了类的继承性。继承可以使用三个访问标志符控制:public、protectd和private。 无论哪个继承,对直接子类没有任何影响,只对子类的用户有影响。基类中的private成员无论使用哪个访问标志符,子类的用户是看不到的,基类的public与protected成员是否能让子类的用户看到由三种访问标志符控制 。
public继承: 基类内的数据成员与成员函数封装特性不变在子类中不变
protected继承: 基类内的数据成员与成员函数的public部分在子类中变为protected
private继承: 基类内的数据成员与成员函数的public和protected部分变为private
例如:
1 class Base 2 { 3 public: 4 ***** 5 protected: 6 ***** 7 }; 8 9 // 公有继承 10 class Derived1 : public Base 11 { 12 public: 13 ..... 14 private: 15 .... 16 }; 17 18 // 私有继承 19 class Derived2 : private Base 20 { 21 public: 22 ;;;;;; 23 };
a. 基类中虚函数和纯虚函数
思考这么一个问题: 当基类定义了某个函数,而在子类中又定义了一个同名的函数(返回类型与参数列表可以不同),这时会发生什么?
答: 子类内的该函数会隐藏基类中同名的函数。即使他们的参数列表与返回类型不同,也不会发生重载,因为重载必须在相同的作用域内发生,如果作用域不同,编译器查找时,总会先找到最近作用域内的同名函数。
很多时候,我们想在基类与子类中定义相同的接口(即同名函数),它们实现各自的功能,这就可以把这样的函数声明为虚函数,即virtual. 当通过类的指针与引用调用虚函数时,会发生动态绑定,即多态。如下面例子所示:
1 // 程序 2 #include<iostream> 3 using namespace std; 4 5 class Base 6 { 7 public: 8 virtual void Say() {cout << "I am Base!" << endl;} 9 }; 10 11 class Derived : public Base 12 { 13 public: 14 virtual void Say() {cout << "I am Derived!" << endl;} 15 }; 16 17 int main() 18 { 19 Base* pA = new Base; 20 Derived* pB = new Derived; 21 pA->Say(); 22 pB->Say(); 23 } 24 25 // 输出 26 yin@debian-yinheyi:~/c$ ./a.out 27 I am Base! 28 I am Derived!
当我们不想实例化一个类时,我们可以定义一个抽象基类,它只负责提供接口。包含纯虚函数的类为抽象类。纯虚函数必须在子类中进行声明与定义。而虚函数可以不在子类中声明与定义,这时候它会像普通成员函数一样继承基类中的虚函数的实现。
1 class Base 2 { 3 public: 4 virtual void Say() = 0; //纯虚函数 5 };
总结来说:
1. 当我们要继承一个类的接口与实现时,我们在基类中定义普普通通的成员即可。
2. 当我们想要继承一个类的接口与默认的实现时,我们在基类中定义为虚函数。
3. 当我们只要继承一个类的接口时,我们在基类中定义为纯虚函数。
其它说明:
1. 在子类中,当我们重新声明和定义虚函数时,可以加上virtual关键字(virtual只能用在类内,不可以把virtual 用在类外),也可以不加, 在c++11标准中,引入了override关键字来显示表示覆盖基类中虚函数的定义,override关键字有利于给编译器更多信息,用于查错。例如当我们在子类中定义了一个与基类中虚函数名字相同,但是参数列表不同的函数,我们本意是定义子类特有的虚函数版本,来覆盖基类中的版本。然而这时候,基类与子类中的函数是独立的,只是基类中的版本隐藏了而已。如果使用了override,编译器发现没有覆盖,就会报错。 如果我们不想让基类中的某个虚函数被覆盖掉,可以使用final关键字。(另外覆盖,即override只会发生在虚函数身上)
2. 如果我们定义 了一个类,并且不想该类被继承,可以在定义这个类时,在类名后面加上final关键字。
1 class Base 2 { 3 public: 4 virtual void Say() {cout << "I am Base!" << endl;} 5 virtual void Dad() final { cout << " I am your dad!" << endl;} //对该虚函数使用final关键字 6 }; 7 8 class Derived final : public Base // 对类Derived 使用了final 关键字 9 { 10 public: 11 void Say() override { cout << " I am Derived!" << endl;} //使用了override关键字 12 };
3. 虽然一个纯虚函数不需要定义,但是其实我们是可以定义一个纯虚函数的,不过调用它的唯一途径是”调用时明确指出它的class的名称“。
1 // 程序 2 class Base 3 { 4 public: 5 virtual void Hello() = 0; 6 }; 7 void Base::Hello() {cout << "hello " << endl;} 8 9 class Derived final : public Base 10 { 11 public: 12 void Hello() override {cout << "a,很疼的" << endl;} 13 }; 14 15 int main() 16 { 17 Derived* pB = new Derived; 18 pB->Hello(); 19 pB->Base::Hello(); 20 } 21 22 // 输出 23 yin@debian-yinheyi:~/c$ ./a.out 24 a,很疼的 25 hello
4. 基类与子类中的虚函数的返回类型和参数列表必须完全一致,如果不一致的话,编译器认为他们是完全不相关的函数。他们之间不会发生覆盖(override),子类中的同名函数只会隐藏子类中的同名函数。
b. 继承中的作用域
关于类的作用域,我们要明白以下几点:
1. 类本身是一个作用域,使用{ }括起来的。
2. 在类的继承过程中,子类的作用域是嵌套在基类的作用域之内的(这就明白了为什么有时候子类中的成员函数会隐藏掉基类中函数,就时候如果我们想要使用被隐藏的基类函数,可以通过显示指明类名(这时可以理解为作用域名)来访问。
3. 在一个类的作用域中,编译器在解析类时,它总会先解析类中声明的所以数据成员与成员函数,再去解析成员函数的定义。正因为这样的原因,无论数据成员定义在成员函数的后面还是前面,还是成员函数的顺序前后之类的, 一个成员函数总是可以找到该类的数据成员或调用其它成员函数。
基于作用域的一个例子:
1 // 程序 2 class Base 3 { 4 public: 5 virtual void Hello() { cout << "Hello, I am Base!" << endl; } 6 void Hi() { cout << "Hi, Hi, Base!" << endl;} 7 }; 8 9 class Derived : public Base 10 { 11 public: 12 void Hello() override { cout << "Hello, I am Derived!" << endl;} 13 void Hi() { cout << "Hi, Hi, Derived!" << endl;} 14 }; 15 16 int main() 17 { 18 Derived Derive; 19 Derive.Hello(); 20 Derive.Hi(); 21 Derive.Base::Hello(); //显示调用基类的虚函数 22 Derive.Base::Hi(); // 显示调用基类普通函数 23 24 Base *pBase = &Derive; 25 pBase->Hello(); //指针调用,进行动态绑定,即多态 26 pBase->Base::Hello(); //显示调用基类的虚函数 27 pBase->Base::Hi(); // 显示调用基类普通函数 28 29 return 0; 30 } 31 32 //程序输出: 33 Hello, I am Derived! 34 Hi, Hi, Derived! 35 Hello, I am Base! 36 Hi, Hi, Base! 37 Hello, I am Derived! 38 Hello, I am Base! 39 Hi, Hi, Base!
额外小知识点:
1. 待后续遇到补充!