类(Class)是
面向对象程序设计(OOP,Object-Oriented Programming)实现信息封装的基础。类是一种用户定义的引用
数据类型,也称类类型。每个类包含数据说明和一组操作数据或传递消息的函数。类的实例称为对象。
基本信息
类是
面向对象语言的程序设计中的概念,是
面向对象编程的基础。
类的实质是一种引用数据类型,类似于 byte、short、
int(
char)、
long、
float、double 等基本数据类型,不同的是它是一种复杂的数据类型。因为它的本质是
数据类型,而不是数据,所以不存在于内存中,不能被
直接操作,只有被实例化为对象时,才会变得可操作。
类是对现实生活中一类具有共同特征的事物的抽象。如果一个程序里提供的数据类型与应用中的概念有直接的对应,这个程序就会更容易理解,也更容易修改。一组经过很好选择的用户定义的类会使程序更简洁。此外,它还能使各种形式的代码分析更容易进行。特别地,它还会使
编译器有可能检查对象的非法使用。
类的内部封装了属性和方法,用于操作自身的成员。类是对某种对象的定义,具有行为(
behavior),它描述一个对象能够做什么以及做的方法(method),它们是可以对这个对象进行操作的程序和过程。它包含有关对象
行为方式的信息,包括它的名称、属性、方法和事件。
类的构成包括成员属性和成员方法(数据成员和成员函数)。数据成员对应类的属性,类的数据成员也是一种数据类型,并不需要分配内存。成员函数则用于操作类的各项属性,是一个类具有的特有的操作,比如“学生”可以“上课”,而“水果”则不能。类和外界发生交互的操作称为接口。
特性
类的三大特性
类与结构体的区别
在 C++、
C# 语言中,
class 和
struct 都可以定义一个类,它们的区别如下:
我们可以简单的理解,class 是一个可以动的机器,有行为,有多态,有继承;而 struct 就是个零件箱,组合了不同结构的零件。其实,class 和 struct 最本质的区别就在于 class 是
引用类型,
内存分配于托管堆;而 struct 是
值类型,内存分配于线程的
堆栈上。由此差异,导致了上述所有的不同点。所以只有深刻的理解内存分配的相关内容,才能更好的驾驭。
当然,使用
class 基本可以替代
struct 的任何场合,class 后来居上。虽然在某些方面 struct 有性能方面的优势,但是在
面向对象编程里,基本是 class 横行的天下。
那么,有人不免会提出,既然 class 几乎可以完全替代 struct 来实现所有的功能,那么 struct 还有存在的必要吗……答案是,至少在以下情况下,鉴于性能上的考虑,我们应该考虑使用 struct 来代替 class:
用法
定义一个类
成员函数的实现
成员函数可以在类内实现,也可以在类外实现。内部实现的成员函数被默认为加上了
inline;外部实现的成员函数必须加上
域操作符,即 “ 类名 :: 成员函数 ”。
构造函数与析构函数
构造函数和
析构函数是特殊的
成员函数,和普通成员函数不同的地方在于:
构造函数用于创建类的对象,任何创建对象的行为,都会导致
构造函数被调用。
析构函数和构造函数的功能相反,析构函数用于销毁对象,当类的对象超出
作用域被销毁时,析构函数被调用。
即使显式地定义构造函数和析构函数,也还是会有默认的构造函数和析构函数,函数内部无任何语句,不执行任何操作。
默认构造函数是无参数的。需要注意的是,一旦显式定义任意形参的构造函数,默认构造函数都不会生成,即只有没有定义构造函数的类才存在默认构造函数。
一般情况下,默认的构造函数和析构函数可以满足功能需要,然而当需要重载构造函数,或是需要
动态分配资源的时候,就不得不定义自己的构造函数甚至析构函数了。
拷贝构造函数是特殊的
构造函数,在复制对象时被调用,定义的格式为 “ 类名(类名 & 参数名)”。拷贝构造函数也存在默认的,但很多情况下都需要重载。
类的实例化
就像声明某种类型的变量一样,声明一个类类型的对象,就是类的实例化,会涉及到必要的内存分配。
不同语言中类的实例化形式是不同的。
对象可以访问类的成员,但并不是所有成员都可以被访问,能否访问取决于声明该成员时所用的关键字(public / protected / private)。具体规则如下:
派生与继承
派生和继承是类的重要特性,继承是由抽象到具体的体现。通过继承,子类可以使用父类的成员。
但要注意的是,派生和继承在带来方便的同时,也会使类与类之间的关系变得复杂,尤其是涉及到
私有继承和保护继承时,类中成员的关系可能会变得难以理解。所以在涉及
类时,尽量避免过多层次的继承,私有继承和保护继承的使用也要慎重。
继承来的成员和自身声明的成员并无本质区别,也有公有成员、私有成员、保护成员之分。继承时,父类中成员类型(公有成员 / 私有成员 / 保护成员)和继承方式(公有继承 /
私有继承 / 保护继承)不同,情况不同。可以归纳为:
三种类型的继承,父类的成员均被子类继承(之前的百科关于这点的描述是错误的),只是由类实例化的对象对其继承的成员的
访问权限会有所变化。三种不同方式的继承,描述的是子类实例化对象对其成员的访问权限,并非是描述子类时,子类对继承自父类的成员的访问权限。
操作符重载
操作符重载必须在类中进行,重载
操作符可以使操作符对在类中的语义发生变化。除了. ,.* ,:: ,? : 、
sizeof、
typeid这几个
运算符不能重载之外,大部分运算符都能被重载。但要注意,重载操作符并不能改变操作符的
优先级和
结合律,而且从认知规律上讲,重载的操作符功能必须与原意相近,否则很难被人理解。
操作符重载是函数,在使用该操作符时被调用。操作符重载函数的声明形式:返回值 operator操作符(
参数列表);
友元
友元可以是函数,被称为
友元函数;也可以是类,被称为
友元类。
通常,类中的
私有成员只能被自身使用,无法被它的对象访问。因此,另一个类即便可以使用该类的对象,也无法访问该类的私有成员,通过定义友元的方法可以做到这一点。
友元就是在一个类中“再次声明”另一个
类的成员函数或是另一个类,被“再次声明”的成员函数或类可以访问该类的私有成员。这种“再次声明”并不是普通的声明,格式为:friend 函数 / 类名;
显然,友元会破坏类的封装性,使本该隐藏的成员暴露出来,因此应当谨慎使用。
组合
继承可以描述 “ 交通工具 ” 和 “ 公交车 ” 的关系,却无法描述 “ 公交车 ” 和 “ 车轮 ” 的关系。
大多数 “ 车轮 ” 具有的特性是 “ 公交车 ” 所不具有的。比如说 “ 车轮 ” 具有 “ 重量 ”,而 “ 公交车 ” 的 “ 重量 ” 则是另一个含义。而通过私有成员、保护成员机制控制这些成员的
继承性,会使继承变得复杂而难以理解。而且继承来的数据成员只有一个,而一辆 “ 公交车 ” 却有四个 “ 车轮 ”,四个 “ 车轮 ” 的 “ 重量 ”。
引入组合的概念,“ 公交车 ” 完全可以由 “ 车轮 ”、“
方向盘 ”、“ 车身 ” 等类组合而来。方法就是将类当成其他的
数据类型一样,在另一个类中定义该类类型的数据成员。
抽象类
并不是所有种类的事物都可以被实例化,换而言之,有的种类只是一种
抽象概念,现实中并没有实际存在的对应物。比如:假设所有的动物都会叫,我们可以定义一个类——“ 动物 ”,定义类中的一个成员函数——“ 叫 ”,我们知道猫的叫声,也知道狗的叫声,然而 “ 动物 ” 是如何 “ 叫 ” 的——我们根本无法实现它。所以,我们引入了
抽象类的概念,抽象类是无法被实例化的,无法声明抽象类的对象。
通常,用
abstract 修饰的类是抽象类;C++ 中包含
纯虚函数的类是抽象类;
Java 中含有
抽象方法的类是抽象类;继承了纯虚函数而没有实现它的类也是抽象类。
静态成员
用static修饰的成员是
静态成员,可以是成员函数或
数据成员。静态成员是所有对象共有的,只分配一次内存,产生新的对象时不会产生副本。
静态数据成员的初始化必须在类外进行,使用静态成员时必须使用类名和
域操作符。
示例
类的复用