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