C++ Primer 学习笔记——第七章

发布时间 2023-07-08 10:56:37作者: 木木亚伦

第七章 类

前言

基本数据类型有时候并不能解决某些特定问题,而通过自定义的类就可以通过理解问题概念,使得程序更加容易编写、调试和修改。

类的基本思想是数据抽象(data abstraction)和封装(encapsulation)。 数据抽象是一种依赖于接口(interface)和实现
(implementation)分离的编程(以及设计)技术。类的接口包括用户所能执行的操作;类的实现则包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数。

可以说,封装实现了类的接口和实现的分离。封装后的类隐藏类它的实现细节。

介绍:

  • 定义抽象数据类型
  • 访问控制与封装
  • 类其他特性及作用域
  • 构造函数
  • 类静态成员

7.1 定义抽象数据类型

设计Sales_data类

其类接口包括:

  • 一个isbn成员函数,用于返回对象的ISBN编号
  • 一个combine成员函数,用于将一个Sales_data对象加到另一个对象上
  • 一个add函数,执行两个Sales_data对象的加法
  • 一个read函数,将数据从istream中读入到Sales_data对象中
  • 一个print函数,将Sales_data对象的值输出到ostream

开发

优秀的类设计者除了充分了解并实现用户的需求,还应该密切关注哪些有可能使用该类的程序员的需求。

一个设计良好的类,既要有易于使用的接口,也必须具备高效的实现过程。

注意

定义在类内部的函数是隐式的inline函数

定义改进的Sales_data类

定义成员函数

所有类成员必须在类内部声明,但是可以自由选择在类内或类外定义。

this使用

当我们调用成员函数时,实际上是某个对象调用它。例如:

/* 某个成员函数 */
std::string isbn() const { return bookNo; }

当我们调用isbn成员函数时,返回的bookNo数据成员,那么其隐式的返回的应该是total.bookNo(假设该函数的对象为total)。

成员函数通过一个名为this的额外隐式参数来访问调用它的那个对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化this。例如:

/* 如果调用 */
total.isbn();
/* 编译器将会把total的地址传递给isbn的隐式形参this,等价于下面这个伪代码 */
Sales_data::isbn(&total);

注意

在成员函数内部,我们可以直接使用调用该函数的对象的成员,而无须通过成员访问运算符,因为this所指的正是这个对象。任何对类成员的直接访问都被看作this的隐式引用。

因为this的目的总是指向“这个”对象,所以this是一个常量指针。

引入const成员函数

在成员函数中,存在这种写法:

std::string isbn() const {return bookNo};

其中,const关键字作用于修改隐式this指针的类型。

在默认情况下,this的类型是指向类类型非常量版本的常量指针。对使用这种const方式的函数成为常量成员函数(const member function)。

我们可以将上述写法想象成:

std::string Sales_data::isbn(const Sales_data * const this){
    return this->bookNo;
}

类作用域和成员函数

类本身就是一个作用域,类的成员函数的定义嵌套在类的作用域之内

如果在类的外部定义成员函数,其定义必须与它的声明匹配,同时外部定义的成员的名字必须包含它所属的类名。

定义一个返回this对象的函数

定义类相关的非成员函数

类作者常常需要定义一些辅助函数,例如:add、read、print等等。这些辅助函数从概念上讲属于是类的接口的一部分,但是实际上其并不属于类。虽然实际上不属于类,但是概念上属于,所以一般将这些非成员函数写在同一个头文件中。

构造函数

构造函数(constructor)用于初始化类对象的数据成员,无论何时只要类被创建,就会执行构造函数。

构造函数名字与类名相同,与其他函数不同的是,构造函数没有返回类型;同时类可以包含多个构造函数,但是与重载函数不同的是,构造函数之间必须在参数列表或者参数类型上有所区别;同时构造函数不能被声明为const。

当我们没有显式声明并定义构造函数,那么类将会通过一个默认构造函数(default constructor)来控制默认初始化过程。编译器创建的构造函数同时又被称为合成的默认构造函数(synthesized default constructor)。但是合成的默认构造函数仅适合非常简单的类,对于一个普通的类,必须定义一个它自己的默认构造函数,因为:

  1. 编译器只有在类不包含任何构造函数的情况下才会替我们生成一个默认的构造函数。
  2. 含有内置类型或者复合类型成员的类在类的内部初始化时采用默认初始化很有可能其初始化值为未定义的。
  3. 有些时候编译器并不能为某些类合成默认的构造函数。

这里,使用一个示例进行分析:

SalesData()=default;        /* 默认构造函数,希望这个函数的作用等同于合成默认构造函数 */
SalesData(const std::string &str):_bookNo(str){}
SalesData(const std::string &str,unsigned number,double price):_bookNo(str),_units_sold(number),_revenue(price*number){}
SalesData(std::istream &);

=default,在C++标准中,使用SalesData()=default;方式要求编译器生成构造函数。该定义既可以在声明处,也可以在类外部。与其他函数一致,如果在类内部,则默认为内联方式,如果在类外部,默认不使用内联方式。

构造函数初始值列表

首先我们可以看看后两个定义的构造函数,在冒号和大括号之间存在的部分成为构造函数初始值列表(constructor initialize list)。其负责为新创建的对象的一个或者几个数据成员赋初值。