这篇博客是《C++ Primer Plus》的第一篇学习笔记,用来作为读书之后的产出,也方便日后查看,偏理论一些,需要有一定的代码实践作为基础。
面向对象编程(OOP)是一种特殊的、设计程序的概念性方法。下面是面向对象的三个基本特征:
- 封装;
- 多态;
- 继承;
10.1 过程性编程和面向对象编程
采用OOP方法时,首先从用户的角度考虑对象——描述对象所需的数据以及描述用户与数据交互所需的操作。完成对接口的描述后,需要确定如何实现接口和数据存储。最后,使用新的设计方案创建出程序。
10.2 抽象和类
10.2.1 类型是什么
制定基本类型完成了三项操作:
- 决定数据对象需要的内存数量;
- 决定如何解释内存中的位(long和float在内存中占用的位数相同,但将它们转化为数值的方法不同);
- 决定可使用数据对象执行的操作或方法。
对于内置类型来说,有关操作的信息被内置到编译器中。但在C++种定义用户自定义的类型时,必须自己提供这些信息。付出这些劳动换来了根据实际需要定制新数据类型的强大功能和灵活性。
10.2.2 C++中的类
类是一种将抽象转换位用户定义类型的C++工具,它将数据表示和操纵数据的方法组成一个整洁的包。一般来说,类规范由两个部分组成。
-
类声明:以数据成员的方式描述数据部分,以成员函数(方法)的方式描述共有接口。
-
类方法定义:描述如何实现类成员函数。
本书遵循以中常见但不通用的约定——将类名首字母大写。
1.访问控制
(1)使用类对象的程序都可以直接访问共有部分,但只能通过公有成员函数、友元函数来访问对象的私有成员。
(2)封装即将实现细节放在一起并将它们与抽象分开。数据隐藏(将数据放在类的私有部分中)是一种封装,将类函数定义和类声明放在不同的文件中也是一种封装。
2.控制类成员的访问:公有还是私有
(1)无论类成员是数据成员还是成员函数,都可以在类的公有或私有部分声明它。但由于数据隐藏是OOP主要的目标之一,因此通常将数据项放在私有部分,组成类的接口放在公有部分中。通常,程序员使用私有成员函数来处理不属于公有接口的实现细节。
(2)C++对结构进行了扩展,使之具有与类相同的特性。它们之间唯一的区别是,结构的默认访问类型是public,而类为private(因此可以在类声明中省略private)C++程序员通常使用类来实现类描述,而把结构限制为只表示纯粹的数据对象。
10.2.3 实现类成员函数
成员函数定义与常规函数定义非常相似,它们有函数头和函数体,也可与i有返回类型和参数。但是成员函数定义还有两个特殊的特征:
- 定义成员函数时,使用作用域解析运算符(::)来表示函数所属的类;
- 类方法可以访问类的private组件。
第一个特征意味着我们可以将两个不同类的成员函数使用相同的名称,即确定了方法定义对应的类的身份。另外,Stock::update() 是函数的限定名,而简单的update()是全名的缩写,它只能在类作用域中使用。
1.成员函数说明
(1)私有成员函数的声明主要价值在于:
- 通过函数调用,而不是每次重新输入计算代码,可以确保执行的计算完全相同并省去许多输入代码的工作。
- 如果必须修订计算代码,则只需在一个地方进行修改。
(2)内联方法:
- 定义位于类声明中的函数都将自动成为内联函数;
- 如果愿意,也可以在类声明之外定义成员函数,并使其成为内联函数。为此,只需在类实现部分中(而非声明)定义函数时使用inline限定符。
- 注意,内联函数的特殊规则要求在每个使用它们的文件中都对其进行定义。因此确保内两定义对多文件程序中的所有文件都可用的、最简便的方法是:将内联定义放在定义类的头文件中(有些开发系统包含只能链接程序,允许将内联定义放在一个独立的实现文件)。
- 根据改写规则,在类声明中定义方法等同于用原型替换方法定义,然后再类声明的后面将定义改写为内联函数。
3.使用哪个对象
所创建的每个新对象都有自己的存储空间,用于存储其内部变量和类成员;但同一个类的所有对象共享同一组类方法,及每种方法只有一个副本。
10.3 类的构造函数和析构函数
(1)构造函数:
声明和定义构造函数:
构造函数名称与类名称相同,且没有返回值,原型位于类声明的公有部分。
Stock::Stock(const string & co, long n, double m_company)
{
company = m_company;
...
}
//使用成员初始化列表:
Stock::Stock(const string & co, long n, double m_company):company(m_company),coo(co){...};
默认构造函数(在设计类时,通常应提供对所有类成员做饮食初始化的默认构造函数):
Stock(const string & co = "Error", int n = 0, double pr = 0); //定义默认构造函数
Stock(){ } //系统默认构造函数
构造函数使用:
Stock food; //使用默认构造函数
Stock food("World Cabbage", 250, 1.25); //隐式调用构造函数
//注意不是Stock food();这是函数声明
Stock food = Stock(); //显式调用构造函数
Stock food = Stock("World Cabbage", 250, 1.25); //显式调用构造函数
//注意不是 food = Stock("World Cabbage", 250, 1.25);这是对象的赋值操作,会在赋值之前创建临时对象
Stock* food = new Stock("World Cabbage", 250, 1.25); //动态分配内存
奇怪的是,当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数。如果定义了非默认构造函数则进行如下声明将出错:
Stock stock1;
(2)析构函数
~Stock(){ }
什么时候调用析构函数由编译器决定(静态存储类对象、自动存储类对象、new创建的类对象、临时对象结束时调用),通常不应在代码中显式地调用析构函数。
(3)提示:
-
成员名和参数名:不熟悉构造函数的您可能试图将类成员名称用作构造函数的参数名,这是错误的。构造函数的参数表示的不是类成员,而是付给类成员的值。因此,参数名不能与类成员相同,否则最终的代码将是这样的:shares
= shares;为避免这种混乱,常见的做法是在数据成员名中使用m_前缀或者_后缀。 -
如果既可以通过初始化,也可以通过赋值来设置对象的值,则应采用初始化方式。通常这种方式的效率更高。
(4)const成员函数(只要类方法不修改类对象,就应将其声明为const)
//保证被调用函数不被修改:
void show() const //声明
void Stock::show() const() //定义
10.4 This指针
- 每个成员函数(包括构造函数和析构函数)都有一个this指针。this指针指向调用对象。如果方法需要引用整个调用对象,则可以使用表达式*this。在函数的括号后面使用const限定符将this限定为const,这样将不能使用this来修改对象的值。
- 具体应用如下:
const Stock & Stock::topval(const Stock & s) const
{
if (s.total_val > toal_val)
return s;
else
return *this;
}
top = stock1.topval(stock2) //隐式访问stock1,显示访问stock2
//返回值为const&意味着返回的是调用对象本身,而不是其副本
//在函数的括号后面使用const意味着将不能使用this来修改对象的值
10.5 对象数组
主要是运用间接成员运算符(->)
具体应用如下:
Stock stocks[4];
const Stock * top = &stocks[0];
for (int i = 0; i < 4; i++){
top = &top->topval(stocks[i]);
}
top->show();
10.6 类作用域
有时候,是符号常量的作用域为类很有用。您可能以为这样做可行 :
class Bakery
{
private:
const int Months = 12;
double costs[Months];
...
但这是行不通的,声明类只是描述了对象的形式,并没有创建对象。因此,在创建对象之前,将没有用于存储值的空间。有两种方式可以实现这个目标,并且效果相同。
// 1.作用域内枚举
class Bakery
{
private:
enum {Months = 12};
double costs[Months];
...
// 2.使用关键字static:
class Bakery
{
private:
static const int Months = 12;
double consts[Months];
...