类(C++) - Part 1

正好下学期有 OOP 的课,看一下 C 艹 的 OOP 部分

  • struct
  • class
  • Reference:
    • C++ Primer, Fifth Edition
    • C++ 类 & 对象 | 菜鸟教程

类的概念

类的基本思想是 数据抽象(data abstraction)封装(encapsulation) 。数据抽象是一种依赖于 接口(interface)实现(implementation) 分离的编程(以及设计)技术。 —— C++ Primer, Fifth Edition

在 C++ 中,一个类可以通过两个关键字实现:

  • struct
  • class

struct

继承自 C 的语法,但略有区别,以如下代码为例

Sales_data structure
1
2
3
4
5
struct Sales_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};

在 C 中,无论是函数参数还是结构体、共用体,都不支持参数的默认值,而在 C++ 中,这些则都得到了支持。
类体 中定义了类的 成员(member) ,分为 数据成员(data member) 以及 成员函数(member function)

声明一个类

Sales_data structure
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Sales_data {
// 成员函数
std::string isbn() const { return bookNo; }
Sales_data& combine(const Sales_data&);
double ave_price() const;
// 数据成员
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
// 非成员接口函数
Sales_data add(const Sales_data&, const Sales_data&);
std::ostream &print(std::ostream&, const Sales_data&);
std::istream &read(std::istream&, const Sales_data&);

这里有一些微妙的东西,在 C 中,struct 是不支持内嵌函数的,而 C++ 中,struct 可以被当作一个精简版的 class 使用,支持了一部分的类特性。

this

这里的函数声明有一些不一样的地方,可以看到,在我们的成员函数定义中,语句块与函数声明间夹着一个 const,这牵涉到一个重要的概念 this,this 在 代码中不被显式的定义,其由编译器负责实现。成员函数可以通过 this 来访问调用它的对象,事实上,成员函数对实例内成员的访问都可以理解为通过 this 指针实现,且任何自定义名为 this 的参数或变量的行为都是非法的。这边给出一个使用 this 的例子:

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

isbn 函数返回 Sales_data 对象的 bookNo ,可以看到这边的 bookNo 通过 -> 间接访问,因此 this 是一个指针。上面这段代码与下面这段是等价的,通常我们不会使用 this

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

const 成员函数

回归正题,这里的 const 的作用是修改隐式 this 指针的类型。

默认情况下,this 的类型是指向类类型非常量版本的常量的指针,对于如上例子也就是 Salesdata *const,这意味着可以经由 _this 修改调用对象的成员。这意味着,我们无法直接将 this 绑定到一个常量对象上,所以我们需要 const Salesdata * const。而 C++ 语言允许把 const 关键字放在成员函数的参数列表之后,表示 _this 是一个常量指针。

像这样由 const 修饰的成员函数被称为 常量成员函数(const member function)

定义成员函数

不同于非成员接口函数的定义和声明均在类外,成员函数的声明必须在类内完成,但成员函数体可以定义在类外。如下代码所示:

avg_price
1
2
3
4
5
6
double Sales_data::avg_price() const {
if (units_sold)
return revenue / units_sold;
else
return 0;
}

对于现代的类设计,我们通常会实现链式调用,因而我们可以通过让成员函数返回调用对象本身来实现,如下代码所示:

combine
1
2
3
4
5
Sales_data& Sales_data::combine(const Sales_data &rhs){
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}

构造函数

类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这类函数被称为 构造函数(constructor) 。构造哈函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。

不同于其他成员函数,构造函数不能被声明成 const ,当我们创建类的一个 const 对象时,知道构造函数完成初始化过程,对象才能真正取得其“常量”属性。

构造函数分为两种:

  • 默认构造函数(default constructor):在我们未传递任何参数时的默认初始化
    • 当我们的类没有显式地定义构造函数时,编译器会隐式地定义一个默认构造函数,又称为 合成的默认构造函数(synthesized default constructor)
  • 自定义构造函数

给出一个例子:

Sales_data constructor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Sales_data{
// 新增的构造函数
Sales_data() = default;
Sales_data(const std::string &s): bookNo(s) {}
Sales_data(const std::string &s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p*n) {}
Sales_data(std::istream &);

std::string isbn() const { return bookNo; }
Sales_data& combine(const Sales_data&);
double ave_price() const;
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};

=default 的含义

1
Sales_data() = default;

这是一个默认构造函数,因为其不含任何实参,在 C++11 新标准中,= default 可以要求编译器来生成构造函数 (合成的默认构造函数)

构造函数列表

1
2
Sales_data(const std::string &s): bookNo(s) {}
Sales_data(const std::string &s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p*n) {}

在这两个构造函数中,我们注意到在参数列表后跟着一个 : 以及逗号表达式。这一部分称为 构造函数初始值列表(constructor initialize list) ,它负责为新创建的对象的一个或几个数据成员赋初值。

值得注意的是,这两个构造函数的函数体是空的,这是因为这些构造函数的唯一目的就是为数据成员赋初值。
此外,没有出现在构造函数初始值列表中的成员,将通过相应的类内初始值(如果存在的话)进行初始化,或者执行默认初始化。

在类的外部定义构造函数

1
2
3
Sales_data::Sales_data(std::istream &is){
read(is, *this);
}

构造函数没有返回值,无需标注返回类型。

拷贝、赋值和析构

除了定义类的对象如何初始化之外,类还需要控制拷贝、赋值和销毁对象时发生的行为。
如果我们不主动定义这些操作,则编译器将会合成它们。

这些 复制 操作实际上是 浅拷贝 ,当对象中使用动态内存时,可能会导致异常。

访问控制与封装(class)

在 C++ 中,通过 访问说明符(access specifiers) 加强类的封装性:

  • 定义在 public 说明符之后的成员在整个程序内可被访问, public 成员定义类的接口。
  • 定义在 private 说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问, private 部分封装了(即隐藏了)类的实现细节。

以此为例:

class Sales_data
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Sales_data {
public:
Sales_data() = default;
Sales_data(const std::string &s): bookNo(s) {}
Sales_data(const std::string &s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p*n) {}
Sales_data(std::istream &);
std::string isbn() const { return bookNo; }
Sales_data& combine(const Sales_data&);
private:
double ave_price() const;
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};

一个类可以包含 0 个或多个访问说明符,而且对于某个访问说明符能出现多少次也没有严格限定。每个访问说明符其有效范围知道出现下一个访问说明符或者到达类的结尾处为止。

class vs struct

class 与 struct 定义类的唯一区别就是默认的访问权限:

  • class - 默认访问权限为 private
  • struct - 默认访问权限为 public