近期准备求职面试。
将C++中面向对象部分的基础知识复习整理一下。
类
在C++中,用类来定义自己的抽象数据类型(abstract data type)
类的定义和声明
简单的来说,类就是定义了一个新的类型和新的作用域。
类定义:概要
类成员
每个类可以没有成员也可以定义多个成员,成员可以是数据、函数或类型别名。
一个类可以包含若干公有的(public)**、私有的(private)和受保护的(protected)部分**。
访问标号
- public:**使用public访问标号**定义的成员可被该类型的所有代码访问,类的用户、该类的派生类也都可以访问。
- private:**使用private访问标号定义的成员可被类中其他的成员**访问,不能被类的用户、该类的派生类访问。
- protected:**使用protected访问标号定义的成员,可以认为它是public和private的混合,像private一样,使用类的用户不能直接访问,像public一样,protected成员可以被以该类为基类的派生类**访问。
构造函数
创建一个类类型时,编译器会自动使用一个构造函数来初始化对象。
构造函数:定义如何初始化的类成员函数。(C++ Primer 4th,2.3.3-2,P43)
另:C++支持两种初始化方式:(C++ Primer 4th,2.3.3-1,P42)
- 复制初始化(copy-initialization):使用语法为直接使用等号(=).
- 直接初始化(direct-initialization):使用语法为把初始化式放在括号中().
1 | int ival=1024;//复制初始化(copy-initialization) |
成员函数
在类内部,声明成员函数是必须的,而定义成员函数则是可选的。
在类内部定义的函数默认为inline(内联函数).(C++ Primer 4th,12.1.1-3,P369)
内联函数:在调用节点处内联展开,避免函数调用开销。因为调用函数比直接执行表达式要慢得多。(C++ Primer 4th,7.6,P221)
调用函数要做很多工作:
- 调用前先保存寄存器,并在返回时恢复;
- 复制实参;
- 程序还必须转向一个新位置执行。
在类外部定义的成员函数必须指明他们是在类的作用域中的。
可以使用**::作用域操作符**来指定:
1 | class testClass{ |
成员函数有一个附加的隐含实参,将函数绑定到函数的对象——this指针
当我们定义:
1 | testClass obj; |
调用obj对象的printHelloWorld函数,如果在printHelloWorld函数内对testClass类成员的引用就是对obj对象的成员的引用。
const成员函数
将关键字const加在形参表之后,就可以将成员函数声明为常量。
const成员不能改变其所操作的对象的数据成员。
const关键字必须同时出现在声明和定义中,若只出现一次则会出现一个编译错误。
1 | //我们可以在testClass类中的public访问标号下添加如下成员函数声明 |
const的作用如同函数名描述的那样:)
数据抽象和封装
类背后蕴含的思想是数据抽象和封装。
数据抽象是一种依赖于接口和实现分离的编程(设计)技术。
通过数据抽象,使用一个类型的程序猿仅需要了解类型的接口,他们可以抽象地考虑该类型做什么,而不是具体地考虑类型如何工作。
封装是一种将低层次的元素组合起来形成新的、高层次实体的技术。
函数是封装的一种形式:函数所执行的细节行为被封装在函数本身这个更大的实体中。被封装的元素隐藏了它们的实现细节——可以调用一个函数,但不能访问它所执行的语句。
标准库类型vector同时具备数据抽象和封装的特性。数组在概念上类似于vector,但既不是抽象的也不是封装的。可以通过访问存放数组的内存来直接操纵数组。
访问标号实施抽象和封装
在C++中,使用访问标号来定义类的抽象接口和实施封装。
注意:一个类可以没有访问标号也可以拥有多个访问标号。
在访问标号这里我们已经简单的介绍了三种访问标号之间的区别了。
一个访问标号出现的次数通常是没有限制的。每个访问标号指定了随后的成员定义的访问级别。这个指定范围持续有效,直至遇到下一个访问标号或看到类定义体的右花括号为止。
可以在任意的访问标号出现之前定义类成员。
在类的花括号之后、第一个访问标号之前定义的成员的访问级别依赖于类是如何定义的。
- 使用class定义:在第一个访问标号之前的定义的成员是私有的(private)。
- 使用struct定义:在第一个访问标号之前定义的成员是公有的(public)
数据抽象和封装的好处
数据抽象和封装提供了两个重要的有点:
- 避免类内部出现无意的,可能破坏对象状态的用户级错误。
- 随时间推移可以根据需求改变或缺陷(bug)报告来完善类实现,而无需改变用户级代码。
改变头文件中的类定义可有效地改变包含该头文件的妹子资源文件的额程序文本,所以,当类发生改变时,使用该类的代码必须重新编译。
关于类定义的更多内容
同一类型的多个数据成员
类的数据成员的声明类似于普通变量的声明。
如果一个类具有多个同一类型的数据成员,则这些成员可以在一个成员声明中指定,声明的顺序从左至右。(构造函数中需要用到成员的定义顺序)
用typedef来简化类
如代码
1 | class testClass{ |
成员函数可被重载
成员函数可以被重载。但重载操作符有特殊规则,是个例外。
成员函数只能重载奔雷的其他成员函数。
类的成员函数与普通的成员函数以及在其他类中声明的函数不相关,也不能重载它们。
重载的成员函数和普通函数应用相同的规则:两个重载成员的形参数量和类型不能完全相同。调用非成员重载函数所用到的函数匹配过程也应用于重载成员函数的调用。
1 | class testClass{ |
假设我们定义了一个该类对象classTemp,则我们对get函数调用的方式会决定使用get的哪一个版本:
1 | testClass classTemp; |
显式指定inline成员函数
在类内部定义成员函数,将自动作为inline处理。
也就是说当他们被调用时,编译器将试图在同一行内扩展该函数,也可以显式地将成员函数声明为inline.
1 | class testClass{ |
可以在类定义体内部之定义一个成员为inline,作为其声明的一部分。也可以在类定义体外部定义上指定inline.在声明和定义出指定inline都是合法的。在类外部定义inline的一个好处是使类比较容易阅读。
类声明与类定义
一旦遇到右花括号,类的定义就结束了。并且,一旦定义了类,那么我们就知道了所有的类成员,以及存储该类所需的存储空间。
在一个给定的源文件中,一个类只能被定义一次。
将类定义放在头文件中,可以保证在每个实用类的文件中以同样的方式定义类。
使用**头文件保护符(header guard)**来避免多重包含(保证类定义只包含一次)
1 |
|
也可以声明一个类而不定义它
1 | class testClass; |
这个声明成为前向声明,在声名之后,定义之前,类testClass是一个**不完全类型(incompete type)**,即已知testClass是一个类型,但不知道包含那些成员。
不完全类型只能以有限的方式使用
- 不能定义该类型的对象
- 用于定义指向该类类型的引用或指针
- 用于声明该类型作为形参类型或返回类型的函数
在创建类的对象之前,必须完整的定义该类。
必须定义类,而不只是声明类,这样编译器就会给类的对象预定相应的存储空间。
同样,在使用引用或指针访问类的成员之前,必须已经定义类。
类的前向声明一般用来编写相互依赖的类。
为类的成员使用类声明
前面写道,只有当类定义已经在前面出现过数据成员才能被指定为该类类型。如果该类类型是不完全类型,那么数据成员只能是指向该类类型的指针或引用。
因为只有当类定义体完成之后才能定义类,因此类不能具有自身类型的数据成员。然而,只要类名一出现就可以认为该类已经声明。因此,类的数据成员可以是指向自身类型的指针或引用。
1 | class testClass{ |
类对象
定义一个类,也就是定义了一个类型。一旦定义了类,就可以定义该类型的对象。定义对象时,将为其分配内存空间,但是(一般而言),定义类型时不进行存储分配(在类右括号之后分号之前)。
1 | class testClass{ |
有两种方式可以定义类类型对象:
- 将类的名字直接用作类型名。(C++引入)
- 指定关键字class或struct,后面跟着类的名字。(由c语言继承而来)
1 | testClass item1; |
为什么类要以分号结束?
类的定义以分号结束。分号是必须的,因为在类定义之后可以接一个对象定义列表。定义必须以分号结束。
但是将对象定义成类定义的一部分是个坏主意,会使发生的操作难以理解。
隐含的this指针
类的成员函数有一个附加的隐含形参,即指向该类对象的一个指针。这个隐含指针命名为this,与调用成员函数的对象绑定在一起。