第04章 C++语言专题(一.03)类

声明:仅为个人学习总结,还请批判性查看,如有不同观点,欢迎交流。

摘要

本文整理了类的部分知识点。首先,通过两份示例代码说明类的多种基础特性,包括类的定义、this 指针、const 成员函数、类型成员、重载函数、内联函数、可变数据成员等;然后,分别对构造函数、访问控制、名字查找、静态成员、非静态成员指针、嵌套类和局部类进行了展开说明。


1、类的基本思想

类(class),用于定义新的数据类型,以及与其关联的一组操作。类具有数据抽象和封装能力。

  • 数据抽象(data abstraction),依赖于“接口和实现分离”思想
    • 类的接口(interface),包括用户所能执行的操作;
    • 类的实现(implementation),包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数;
    • 类通过定义数据成员和函数成员实现数据抽象。
  • 封装(encapsulation),隐藏实现细节
    • 类的用户,只能使用接口,而无法访问实现部分;
    • 类的设计者,负责考虑类的实现细节;
    • 类通过访问控制实现封装,保护类的成员不被随意访问。

2、类的定义

2.1 示例类1

示例类 Sales_data 用于表示一本书的总销售额、售出册数和平均售价。相关接口包括:

  • 一个 isbn 成员函数,用于返回对象的 ISBN 编号;
  • 一个 combine 成员函数,用于将一个 Sales_data 对象加到另一个对象上;
  • 一个 read 函数,将数据从 istream 读入到 Sales_data 对象中;
  • 一个 print 函数,将 Sales_data 对象的值输出到 ostream;
  • 一个 add 函数,执行两个 Sales_data 对象的加法。
#include <iostream>
#include <string>
using namespace std;

class Sales_data
{
public:
  // 默认构造函数,= default(C++11)作用完全等同于合成默认构造函数
  Sales_data() = default;

  // 定义默认构造函数,令其与只接受一个 string 实参的构造函数功能相同
  // 如果一个构造函数为所有参数都提供了默认实参,那么它实际上也定义了默认构造函数
  // Sales_data(const string &s = "") : bookNo(s) {}

  // 构造函数初始值列表(constructor initialize list),为新创建的对象的一个或几个数据成员赋初值
  // 被初始值列表忽略的数据成员,将以“与合成默认构造函数相同的方式”隐式初始化
  Sales_data(const string &s) : bookNo(s) {}
  Sales_data(const string &s, unsigned n, double p) : bookNo(s), units_sold(n), revenue(p * n) {}
  Sales_data(istream &is);

  // 成员函数的声明必须在类的内部,它的定义则既可以在类的内部也可以在类的外部
  string isbn() const { return bookNo; } // 定义在类内部的函数是隐式的 inline 函数
  Sales_data &combine(const Sales_data &rhs);

private:
  double avg_price() const { return units_sold ? revenue / units_sold : 0; }

  // 成员定义的顺序不影响编译,因为会首先编译成员的声明,然后再编译成员函数体(如果有)
  string bookNo;
  // 类内的初始值(in-class initializer,C++ll)
  unsigned units_sold = 0;
  double revenue = 0.0;

  // 为 Sales_data 的“非成员函数”做友元声明
  // 1)友元声明仅仅指定了访问的权限,而不是通常意义上的函数声明;
  //    如果希望类的用户能够调用某个友元函数,需要在友元声明之外再专门对函数进行一次声明(依赖编译器)
  // 2)友元声明只能出现在类定义的内部,具体位置不限,建议放在类定义的开始或结束前;
  // 3)友元不是类的成员,不受它所在区域访问控制级别的约束。
  friend istream &read(istream &is, Sales_data &item);
  friend ostream &print(ostream &os, const Sales_data &item);
  friend Sales_data add(const Sales_data &lhs, const Sales_data &rhs);
};

// Sales_data 的“非成员接口函数”
// 对于类相关的非成员函数(即从概念上来说属于类的接口的组成部分,但实际上并不属于类本身),
// 一般将函数声明与类声明(非定义)放在同一个头文件内。这样,用户使用接口的任何部分都只需要引入一个文件。
istream &read(istream &is, Sales_data &item);
ostream &print(ostream &os, const Sales_data &item);
Sales_data add(const Sales_data &lhs, const Sales_data &rhs);

// 在类的外部定义成员函数,成员函数的定义必须与它的声明匹配
Sales_data::Sales_data(istream &is)
{
  // read 函数的作用是从 is 中读取一条交易信息然后存入 this 对象中
  read(is, *this);
}

// 函数 combine 被声明在类 Sales_data 的作用域内,units_sold、revenue 是 Sales_data 的成员
// 模仿内置的赋值运算符 += ,把它的左侧运算对象当成左值返回
Sales_data &Sales_data::combine(const Sales_data &rhs)
{
  units_sold += rhs.units_sold; // 把 rhs 的成员加到 this 对象的成员上
  revenue += rhs.revenue;
  return *this; // 返回调用该函数的对象
}

// IO 类属于不能被拷贝的类型,所以只能通过引用来传递 is, os
// 读取和写入操作会改变流的内容,所以 is, os 不能使用对常量的引用
istream &read(istream &is, Sales_data &item)
{
  double price = 0;
  // 在类的友元函数中访问类的非公有成员
  is >> item.bookNo >> item.units_sold >> price; // 读入交易信息,包括 ISBN、售出总数和售出价格
  item.revenue = price * item.units_sold;
  return is;
}
ostream &print(ostream &os, const Sales_data &item)
{
  os << item.isbn() << " " << item.units_sold << " "
     << item.revenue << " " << item.avg_price();
  return os;
}
Sales_data add(const Sales_data &lhs, const Sales_data &rhs)
{
  // 默认情况下,拷贝类的对象其实拷贝的是对象的数据成员
  Sales_data sum = lhs; // 把 lhs 的数据成员拷贝给 sum
  sum.combine(rhs);     // 把 rhs 的数据成员加到 sum 当中
  return sum;
}

int main()
{
  Sales_data total; // 保存当前求和结果的变量
  if (read(cin, total))
  {                   // 读入第一笔交易
    Sales_data trans; // 保存下一条交易数据的变量
    while (read(cin, trans))
    {                                   // 读入剩余的交易
      if (total.isbn() == trans.isbn()) // 检查 isbn
        total.combine(trans);           // 更新变量 total 当前的值
      else
      {
        print(cout, total) << endl; // 输出结果
        total = trans;              // 处理下一本书
      }
    }
    print(cout, total) << endl; // 输出最后一条交易
  }
  else
  {                              // 没有输入任何信息
    cerr << "No data?!" << endl; // 通知用户
  }

  return 0;
}

关于声明和定义

  • 声明(declaration),使得名字为程序所知,一个文件如果想使用别处定义的名字,则必须包含对那个名字的声明。
  • 定义(definition),负责创建与名字关联的实体。

变量的声明和定义

  • 变量声明,规定了变量的类型和名字;
  • 变量定义,规定了变量的类型和名字,而且还会申请存储空间,也可能会为变量赋一个初始值;
  • 如果想声明一个变量而非定义它,就在变量名前添加关键字 extern,并且不要显式地初始化变量;
  • 变量能且只能被定义一次,但是可以被声明多次。
extern int i;              // 声明 i 而非定义 i
int j;                     // 声明并定义 j
extern double pi = 3.1416; // 任何包含了显式初始化的声明即成为定义,即使变量已由 extern 关键字标记

2.2 引入 this

当我们调用类的成员函数时,实际上是在替某个对象调用它。例如,使用点运算符 . 访问 total 对象的 isbn 成员,并调用它:

total.isbn();

成员函数 string Sales_data::isbn() const { return bookNo; }
访问 Sales_databookNo 成员,其实是在隐式地访问“调用该函数的对象的成员”。在 total.isbn() 调用中,当 isbn 返回 bookNo 时,实际上它隐式地返回 total.bookNo

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

total.isbn();

则编译器负责把 total 的地址传递给 isbn 的隐式形参 this,可以等价地认为编译器将该调用重写成了如下形式(仅为示意性代码):

Sales_data::isbn(&total);

isbn 的定义则为如下形式(仅为示意性代码):

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

与实际的成员函数定义比较:

string Sales_data::isbn() const { return bookNo; }
  • Sales_data *const this 是隐式形参,传入指向调用对象的常量指针;
  • 在成员函数内部,任何对类成员(如 bookNo)的直访接问都被看作 this 的隐式引用(如 this->bookNo)。

2.3 引入 const 成员函数

在常量成员函数(const member function)isbn 的定义中,

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

紧随参数列表之后的 const 关键字,用于修改隐式 this 指针的类型,将其改为指向常量的常量指针,这样才能把 this 绑定到一个常量对象上。保证在函数内部,不会改变 this 所指向的(调用函数的)对象。

等价的定义形式为(仅为示意性代码):

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

Note: 常量对象,以及常量对象的引用或指针,都只能调用常量成员函数。

2.4 示例类2

#include <iostream>
#include <string>
#include <vector>
using namespace std;

// Screen 表示显示器中的一个窗口
// 在窗口 height x width 空间内存放 contents 字符串,可以读写 cursor 指定位置的字符
class Screen
{
public:
  // 自定义某种类型在类中的别名(和其他成员一样存在访问限制)
  // 类型成员通常出现在类开始的地方,因为必须先定义、后使用
  typedef string::size_type pos;

  // 也可以等价地使用类型别名
  // using pos = string::size_type;

  Screen() = default; // 因为 Screen 有另一个构造函数,所以本函数是必需的

  // cursor 被其类内初始值初始化为 0
  Screen(pos ht, pos wd, char c) : height(ht), width(wd), contents(ht * wd, c) {}

  // 重载成员函数,在参数的数量和/或类型上有所区别
  char get() const { return contents[cursor]; } // 隐式内联,返回光标当前位置的字符
  inline char get(pos ht, pos wd) const;        // 显式内联,返回由行号和列号指定的位置的字符
  Screen &move(pos r, pos c);                   // 可以在之后被设为内联

  Screen &set(char c);
  Screen &set(pos row, pos col, char ch);

  // 根据对象是否是 const 重载 display 函数
  const Screen &display(ostream &os) const {
    do_display(os); // 当一个成员调用另外一个成员时,this 指针在其中隐式地传递
    return *this;
  }
  Screen &display(ostream &os) {
    do_display(os); // this 指针将隐式地从指向非常量的指针转换成指向常量的指针
    return *this;
  }

  void some_member() const; // 用于测试 mutable 可变数据成员 access_ctr

private:
  // 负责显示 Screen 的内容;隐式内联,不会增加调用开销;独立函数,提高代码可复用性
  void do_display(ostream &os) const { os << contents; }

  pos cursor = 0;            // 光标的位置
  pos height = 0, width = 0; // 屏幕的高和宽
  string contents;           // 屏幕的内容

  // 可变数据成员(mutable data member),永远不会是 const,即使它是 const 对象的成员,也能被修改
  mutable size_t access_ctr = 0;

  // Window_mgr 的成员可以访问 Screen 类中包括非公有成员在内的所有成员
  // 友元关系不存在传递性,如果 Window_mgr 有它自己的友元,则这些友元并不具有访问 Screen 的特权
  friend class Window_mgr;

  // 可以只为其他类的成员函数提供访问权限
  // 要想令某个成员函数作为友元,我们必须仔细组织程序的结构以满足声明和定义的彼此依赖关系
  // Window_mgr::clear 必须在 Screen 类之前被声明(仅为示意)
  // friend void Window_mgr::clear(ScreenIndex);
};

char Screen::get(pos r, pos c) const // 已在类的内部声明 inline
{
  pos row = r * width;      // 计算行的位置
  return contents[row + c]; // 返回给定列的字符
}

inline Screen &Screen::move(pos r, pos c) // 可以在函数的定义处指定 inline
{
  pos row = r * width; // 计算行的位置
  cursor = row + c;    // 在行内将光标移动到指定的列
  return *this;        // 以左值的形式返回对象
}

inline Screen &Screen::set(char c)
{
  contents[cursor] = c; // 设置当前光标所在位置的新值
  return *this;         // 将this 对象作为左值返回
}

inline Screen &Screen::set(pos row, pos col, char ch)
{
  contents[row * width + col] = ch; // 设置给定位置的新值
  return *this;                     // 将 this 对象作为左值返回
}

void Screen::some_member() const
{
  // 一个 const 成员函数可以改变一个 mutable 成员的值
  // access_ctr 中保存一个计数值,用于记录成员函数被调用的次数
  ++access_ctr;
  // ...
}

// 窗口管理类,管理显示器上的一组 Screen
class Window_mgr
{
public:
  // 窗口中每个屏幕的编号
  using ScreenIndex = vector<Screen>::size_type;

  // 向窗口添加一个 Screen,返回它的编号
  ScreenIndex addScreen(const Screen &);

  // 按照编号将指定的 Screen 重置为空白
  void clear(ScreenIndex);

private:
  // 默认情况下,一个 Window_mgr 包含一个标准尺寸的空白 Screen
  // 类内初始值必须使用 = 或者 {} 的初始化形式
  vector<Screen> screens{Screen(24, 80, ' ')};
};

// 在类的外部定义成员函数时,必须同时提供类名和函数名
// 一旦遇到了类名 Window_mgr,函数定义的剩余部分(参数列表和函数体)就在类的作用域之内了
// 可以直接使用类的其他成员,如参数列表中的 ScreenIndex,函数体中的 screens
void Window_mgr::clear(ScreenIndex i)
{
  // s 是一个 Screen 的引用,指向我们想清空的那个屏幕
  Screen &s = screens[i];
  // 作为友元,Window_mgr 的成员可以访问 Screen 类中的所有成员
  s.contents = string(s.height * s.width, ' '); // 将那个选定的 Screen 重置为空白
}

// 返回类型 ScreenIndex 出现在类名 Window_mgr 之前,位于类的作域用之外,必须明确指定哪个类定义了它
Window_mgr::ScreenIndex Window_mgr::addScreen(const Screen &s)
{
  screens.push_back(s);
  return screens.size() - 1;
}

int main()
{
  Screen myScreen(2, 2, '-');

  // 函数 move、set 和 display 均返回引用,是左值的,连接在一条表达式中之后,操作将在同一个对象上执行
  // 把光标移动到一个指定的位置,然后设置该位置的字符值,最后显示屏幕内容
  myScreen.move(1, 0).set('#').display(cout); // 调用非常量版本 display

  cout << endl;
  const Screen blank(2, 2, '-');
  blank.display(cout); // 调用常量版本 display

  return 0;
}

3、构造函数

构造函数(constructor),一种特殊的成员函数,用于初始化类对象的数据成员。

3.1 默认构造函数

默认构造函数(default constructor),无参数,实现默认初始化(default initialized)。当对象被默认初始化或值初始化时自动执行默认构造函数。

发生默认初始化的情况包括:

  • 在块作用域内,不使用任何初始值,定义一个非静态变量或者数组;
    • 示例:Sales_data obj; Sales_data objs[10];
  • 一个类本身含有类类型的成员,且使用合成的默认构造函数;
    • 示例:string Sales_data::bookNo;
  • 类类型的成员没有在构造函数初始值列表中显式地初始化。
    • 示例:string Sales_data::bookNo;

发生值初始化的情况包括:

  • 在数组初始化的过程中,如果提供的初始值数量少于数组的大小;
    • 示例:int a[10] = {0, 1, 2};
  • 不使用初始值定义一个局部静态变量;
    • 示例:static size_t ctr;
  • 通过书写形如 T() 的表达式显式地请求值初始化(T 是类型名)。
    • 示例:vector<int> v(10);

3.2 合成的默认构造函数

合成的默认构造函数(synthesized default constructor),在类没有声明任何构造函数时,由编译器隐式定义的默认构造函数,数据成员的初始化规则为:

  • 如果存在类内的初始值,用它来初始化;
  • 内置类型成员、复合类型成员,值是未定义的(不被初始化,uninitialized);
  • 类类型成员,值由每个类各自的默认构造函数决定。

对于某些类来说,并不适用合成的默认构造函数:

  • 已经存在任意构造函数的声明,编译器不再自动生成默认构造函数;
  • 对于内置类型、复合类型数据成员,如果没有初始化,其值是未定义的,使用合成的默认构造函数,可能导致操作执行错误;
  • 对于其他类类型数据成员,如果没有默认构造函数,编译器将无法对其进行初始化,也就无法自动生成默认构造函数;
  • 以下情况,类的默认构造函数被定义为删除的(= delete,C++11):
    • 类的某个成员的析构函数是删除的或不可访问的;
    • 类有一个引用成员,它没有类内初始化器;
    • 类有一个 const 成员,它没有类内初始化器,且其类型未显式定义默认构造函数。

3.3 构造函数初始值列表

在下述示例代码的两种构造函数的定义方式中,

方式1:在构造函数的初始值列表中显式地初始化成员

  • 如果成员是 const 或者是引用,必须将其初始化;
  • 如果成员属于某种未定义默认构造函数的类类型,必须将其初始化;
  • 相比于先初始化再赋值,直接初始化效率更高,建议使用。

方式2:

  • 由于没有通过初始值列表初始化,成员将在构造函数体之前执行默认初始化(如果存在类内初始值);
  • 然后在构造函数体内部,对数据成员执行赋值操作。
class Sales_data
{
public:
  // 方式1:直接初始化
  Sales_data(const string &s, unsigned n, double p) : bookNo(s), units_sold(n), revenue(p * n) {}

  // 方式2:先默认初始化,后赋值
  Sales_data(const string &s, unsigned n, double p) {
    bookNo = s;
    units_sold = n;
    revenue = n * p;
  }

  // ...
}

成员初始化的顺序:

  • 初始值列表只说明用于初始化成员的值,并不限定初始化的执行顺序;
  • 成员的初始化顺序与它们在类定义中的出现顺序一致,第一个成员先被初始化,然后第二个,以此类推。(同编译顺序)

3.4 委托构造函数(C++11)

委托构造函数(delegating constructor),使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它自己的一些(或者全部)职责委托给了其他构造函数。其定义包括:

  • 成员初始值列表:类名(参数列表),参数列表必须与类中另外一个构造函数匹配;
  • 函数体。

执行顺序:

  1. 委托构造函数的“初始值列表”;
  2. 受委托构造函数的“初始值列表”;
  3. 受委托构造函数的“函数体”;
  4. 委托构造函数的“函数体”。
class Sales_data
{
public:
  // 非委托构造函数,使用对应的实参初始化成员
  Sales_data(const string &s, unsigned n, double p) : bookNo(s), units_sold(n), revenue(p * n) {}

  // 其余构造函数,全都委托给另一个构造函数
  Sales_data() : Sales_data("", 0, 0) {}
  Sales_data(const string &s) : Sales_data(s, 0, 0) {}
  Sales_data(istream &is) : Sales_data() { read(is, *this); }

  // ...
}

3.5 隐式的类型转换

3.5.1 通过构造函数定义的隐式转换

如果构造函数只接受一个实参,则它实际上定义了一条“从构造函数的参数类型向类类型隐式转换的规则”,所以这种构造函数也被称作转换构造函数(converting constructor)。

2.1 示例类1Sales_data 类中,接受 string 的构造函数和接受 istream 的构造函数分别定义了从这两种类型向 Sales_data 隐式转换的规则。也就是说,在需要使用
Sales_data 的地方,可以使用 string 或者 istream 作为替代:

int main()
{
  Sales_data item;
  //...

  // 1. 从构造函数的参数类型向类类型的隐式转换
  // 将一个 string 实参传递给 combine 函数, 编译器隐式地把 string 转换成 Sales_data:
  // (1) 自动执行了“接受一个 string 参数”的 Sales_data 构造函数,
  // (2) 该构造函数创建了一个(临时的)Sales_data 对象,
  // (3) 将得到的对象传递给 Sales_data 的 combine 成员。
  // 因为 combine 的参数是一个常量引用,所以可以给该参数传递一个临时量
  string null_book = "9-999-99999-9";
  item.combine(null_book);

  // 2. 编译器只会自动地执行一步类型转换
  // 错误:需要两种转换
  // (1) 把 “9-999-99999-9” 转换 string
  // (2) 再把这个(临时的)string 转换成 Sales_data
  // item.combine("9-999-99999-9");

  // 正确:显式地转换成 string,隐式地转换成 Sales_data
  item.combine(string("9-999-99999-9"));

  // 正确:隐式地转换成 string,显式地转换成 Sales_data
  item.combine(Sales_data("9-999-99999-9"));

  // 3. 类类型转换不是总有效
  // 编译器自动调用“接受一个 istream 参数”的 Sales_data 构造函数,通过读取标准输入创建了一个(临时的)Sales_data对象。
  // 实际上,我们构建了一个对象,先将它的值加到 item 中,随后将其丢弃。这可能并不符合实际应用的逻辑。
  item.combine(cin);

  //...
  return 0;
}

3.5.2 使用 explicit 阻止隐式转换

可以通过将构造函数声明为 explicit 阻止隐式转换。用 explicit 关键字声明的构造函数,只能以直接初始化的形式使用,不会被编译器在自动转换的过程中使用。但可以在显式转换过程中使用。

class Sales_data {
public:
  Sales_data() = default;
  Sales_data(const string &s, unsigned n, double p) : bookNo(s), units_sold(n), revenue(p * n) {}

  // 关键字 explicit 只对一个实参的构造函数有效
  // 需要多个实参的构造函数不能用于执行隐式转换,所以无须为这些构造函数指定 explicit
  // 关键字 explicit 只允许出现在类内的构造函数声明处
  explicit Sales_data(const string &s) : bookNo(s) {}
  explicit Sales_data(istream &is);

  // ...
}

int main() {
  //...
  item.combine(cin);            // 错误:istream 构造函数是 explicit 的
  item.combine(null_book);      // 错误:string 构造函数是 explicit 的
  Sales_data item1 = null_book; // 错误:explicit 构造函数不能用于拷贝形式的初始过化程

  Sales_data item2(null_book);                // 正确:explicit 构造函数只能用于直接初始化
  item.combine(Sales_data(null_book));        // 正确:实参是一个显式构造的 Sales_data 对象
  item.combine(static_cast<Sales_data>(cin)); // 正确:static_cast 可以使用 explicit 声明的构造函数
  //...
}

标准库中相关类的示例:

  // 接受一个单参数的 const char* 的 string 构造函数不是 explicit 的。
  inline string::basic_string(const char *_Ptr);

  // 接受一个容量参数的 vector 构造函数是 explicit 的。
  inline explicit vector<int>::vector(size_t _Count);

3.6 聚合类

聚合类(aggregate class),使得用户可以直接访问其成员,并且具有特殊的初始化语法形式。

聚合类需要满足的条件如下:

  1. 所有成员都是 public 的;
  2. 没有定义任何构造函数;
  3. 没有类内初始值;
  4. 没有基类,也没有 virtual 函数。
struct Data
{
  int ival;
  string s;
};

int main()
{
  // 1. 可以用一个花括号括起来的成员初始值列表,来初始化聚合类的数据成员;
  // 2. 初始值的顺序必须与声明的顺序一致;
  Data val1 = {0, "Anna"}; // val1.ival=0; val1.s=string("Anna");

  // 3. 与初始化数组元素的规则一样,如果初始值列表中的元素个数少于类的成员数量,则靠后的成员被值初始化;
  // 4. 初始值列表的元素个数绝对不能超过类的成员数量。
  Data val2 = {10}; // val1.ival=10; val1.s=string("");
}

显式地初始化类的对象的成员存在三个明显的缺点:

  1. 要求类的所有成员都是 public 的;
  2. 由类的用户负责初始化对象的每个成员,这个过程冗长乏味且容易出错;
  3. 添加或删除一个成员之后,所有的初始化语句都需要更新。

3.7 字面值常量类

3.7.1 constexpr 变量(C++11)

声明为 constexpr 的变量一定是一个常量,而且必须用常量表达式(const expression)初始化:

  constexpr int mf = 20;        // 20 是常量表达式
  constexpr int limit = mf + 1; // mf + 1 是常量表达式
  constexpr int sz = size();    // size 需要是一个 constexpr 函数,函数足够简单,在编译时就可以计算其结果

常量表达式的值需要在编译时就得到计算,其中用到的类型一般比较简单,值也显而易见,称为“字面值类型”(literal type),包括算术类型、枚举类型、引用和指针类型,以及字面值常量类类型。
其中,一个 constexpr 指针的初始值必须是 nullptr 或者 0 或者是存储某个对象的固定的地址,例如定义于所有函数体之外的对象的地址。

3.7.2 constexpr 函数(C++11)

constexpr 函数(constexpr function)是指能用于常量表达式的函数。但 constexpr 函数不一定返回常量表达式,由编译器负责检查函数的结果是否符合要求。

与其他函数比较,constexpr 函数需要:

  1. 返回类型及所有形参的类型都是“字面值类型”;
  2. 函数体中必须有且只有一条 return 语句(也是唯一可执行语句);
  3. 函数体内可以包含其他语句,但这些语句在运行时不能执行任何操作,例如空语句、类型别名以及 using 声明。
constexpr int new_sz() { return 42; }
constexpr int foo = new_sz(); // 正确:能够在编译时验证 new_sz 函数返回的是常量表达式

// constexpr 函数的返回值也可以不是一个常量:
// 当 scale 的实参是常量表达式时,它的返回值也是常量表达式;反之则不然
constexpr size_t scale(size_t cnt) { return new_sz() * cnt; }

// 当把 constexpr 函数用在需要常量表达式的上下文中时,由编译器负责检查函数的结果是否符合要求
int a1[scale(2)];  // 正确:scale(2) 是常量表达式
int i = 2;         // i 不是常量表达式
int a2[scale(i)];  // 错误:scale(i) 不是常量表达式

执行初始化任务时,编译器把对 constexpr 函数的调用替换成其结果值。为了能在编译过程中随时展开,constexpr 函数被隐式地指定为内联函数。

因为编译器需要函数的定义才能展开函数,所以内联函数 和 constexpr 函数可以在程序中多次定义,但一个函数的多个定义必须完全一致。所以通常会将这些函数的定义放在头文件中。

3.7.3 字面值常量类

字面值常量类,包括:

  1. 数据成员都是字面值类型的聚合类;
  2. 符合下述要求的类:
    • 数据成员都必须是字面值类型;
    • 类必须至少含有一个 constexpr 构造函数(尽管构造函数不能是 const 的);
    • 如果一个数据成员含有类内初始值,则
      • 对于内置类型成员,初始值必须是一条常量表达式;
      • 对于类类型成员,初始值必须使用成员自己的 constexpr 构造函数。
    • 类必须使用析构函数的默认定义,负责销毁类的对象。

关于 constexpr 构造函数:

  • 可以声明成 =default 或者 =delete 的形式;
  • 可以声明成“空构造函数体”的形式,因为
    • 既要符合构造函数的要求(意味着它不能包含返回语句)
    • 又要符合 constexpr 函数的要求(意味着它能拥有的唯一可执行语句就是返回语句)
  • 必须初始化所有数据成员,初始值或者使用 constexpr 构造函数,或者是一条常量表达式;
  • 用于生成 constexpr 对象、constexpr 函数的参数或返回值。
class Debug
{
public:
  constexpr Debug(bool b = true) : hw(b), io(b), other(b) {}
  constexpr Debug(bool h, bool i, bool o) : hw(h), io(i), other(o) {}
  constexpr bool any() { return hw || io || other; }
  void set_io(bool b) { io = b; }
  void set_hw(bool b) { hw = b; }
  void set_other(bool b) { hw = b; }

private:
  bool hw;    // 硬件错误,而非 IO 错误
  bool io;    // IO 错误
  bool other; // 其他错误
};

int main()
{
  constexpr Debug io_sub(false, true, false); // 调试 IO
  if (io_sub.any())                           // 等价于 if(true)
    cerr << "print appropriate error messages" << endl;
  constexpr Debug prod(false); // 无调试
  if (prod.any())              // 等价于 if(false)
    cerr << "print an error message" << endl;
}

4、拷贝、赋值和析构

  • 拷贝,在初始化变量、以值的方式传递或返回一个对象等情况下,对象会被拷贝;
  • 赋值,使用了赋值运算符时,会发生对象的赋值操作;
  • 销毁,当对象不再存在时,执行销毁的操作。

如果我们不主动定义这些操作,那么编译器将替我们合成它们。一般来说,编译器生成的版本将对“对象的每个成员”执行拷贝、赋值和销毁操作。

与合成的默认构造函数类似,对于某些类来说,并不适用合成的版本。包括被定义为删除的 =delete 情况,特别是,当类需要分配类对象之外的资源(如动态内存)时,合成的版本常常会失效。

注:关于“拷贝、赋值和析构”相关内容,会在“C++语言专题(一.04)拷贝控制”章节展开说明。

5、访问控制与封装

5.1 访问说明符

在 C++ 语言中,使用访问说明符(access specifiers)加强类的封装性:

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

关于访问说明符:

  • 一个类可以包含 0 个或多个访问说明符,每个访问说明符可以出现任意次;
  • 每个访问说明符指定了接下来的成员的访问级别,其有效范围直到出现下一个访问说明符或者到达类的结尾处为止;
  • 类可以在它的第一个访问说明符之前定义成员,对这种成员的访问权限依赖于类定义的方式。如果使用 class 关键字,则上述成员是 private 的;如果使用 struct 关键字,则这些成员是 public 的。这种默认的访问权限也是使用 classstruct 定义类唯一的区别。

5.2 友元

通过成为类的友元(friend),其他类或者函数可以访问类的非公有成员。

6、名字查找与类的作用域

名字查找(name lookup),是寻找与所用名字最匹配的声明的过程。

每个类都会定义它自己的作用域。对于类的定义,编译器

  1. 首先,编译全部成员(数据、函数、类型)的声明;
  2. 在类的全部成员可见之后,才编译成员函数体。

所以

  • 在成员函数体中,可以使用类中定义的任何名字;
  • 声明中使用的名字,包括返回类型或者参数列表中使用的名字,都必须确保在使用前可见。

6.1 成员声明中的名字查找

声明中使用的名字,必须在使用前可见。如果某个成员的声明使用了类中尚未出现的名字,则编译器将会在定义该类的作用域中继续查找。例如:

typedef double Money;
string bal;
class Account
{
public:
  Money balance() { return bal; }

private:
  Money bal;
  // ...
};
  1. 当编译器看到 balance 函数的声明语句时,它将在 Account 类中,使用 Money 之前的范围内,寻找对 Money 的声明;
  2. 因为没有找到匹配的成员,所以编译器会接着到 Account 的外层作用域中查找。在这个例子中,编译器会找到 Moneytypedef 语句,该类型被用作 balance 函数的返回类型,以及数据成员 bal 的类型。
  3. balance 函数体在整个类可见后才被处理,因此该函数的 return 语句返回名为 bal 的成员,而非外层作用域的 string 对象。

6.2 函数定义中的名字查找

成员函数中使用的名字按照如下方式解析:

  1. 首先,在成员函数中、使用该名字之前的范围内,查找该名字的声明;
    • 可以通过 this->名字类名::名字 来强制访问被隐藏的“类作用域中的名字”;
    • 可以通过 ::名字 来强制访问被隐藏的“全局作用域中的名字”。
  2. 如果在成员函数内没有找到,则在类内继续查找,这时类的所有成员都可以被考虑;
  3. 如果类内也没有找到该名字的声明,则在类外层的作用域中继续查找。
    • 即会考虑“类定义之前”的作用域中的声明;
    • 也会考虑“成员函数定义之前”的作用域中的声明。

在类的作用域之外,

  • 对于数据成员和函数成员,由对象、引用或者指针使用成员访问运算符来访问;
  • 对于类型成员,则使用作用域运算符来访问;
  • 对于静态成员,既可以使用成员访问运算符,也可以使用作用域运算符来访问。

7、类的静态成员

类的静态成员,与类本身直接相关,而不是与类的各个对象建立关联。静态成员存在于任何对象之外。

  • 对象中不包含任何与静态数据成员有关的数据;
  • 静态成员函数也不与任何对象绑定在一起,它们不包含 this 指针。也不能声明成 const 的。

7.1 声明与定义

class Account
{
public:
  // 成员函数 calculate 不用通过作用域运算符就能直接使用静态成员 interestRate
  void calculate() { amount += amount * interestRate; }
  static double rate() { return interestRate; }
  // static 关键字只出现在类内部的声明语句中
  static void rate(double);

private:
  string owner;
  double amount;              // 每个 Account 对象包含两个数据成员:owner 和 amount
  static double interestRate; // 只存在一个 interestRate 对象,它被所有的 Account 对象共享
  static double initRate() { return 0.1; }
};

// 通常情况下,必须在类的外部定义和初始化每个静态数据成员
// 静态数据成员定义在任何函数之外,一旦被定义,就将一直存在于程序的整个生命周期中
// 和其他成员的定义一样,interestRate 的定义也可以访问类的私有成员 initRate
double Account::interestRate = initRate();

// 在类的外部定义静态成员时,不能重复 static 关键字
void Account::rate(double newRate) {
  interestRate = newRate;
}

int main()
{
  double r;
  Account acl;
  Account *ac2 = &acl;
  r = acl.rate();      // 可以通过 Account 的对象或引用来访问静态成员
  r = ac2->rate();     // 可以通过指向 Account 对象的指针来访问静态成员
  r = Account::rate(); // 可以使用作用域运算符直接访问静态成员

  //...
}

7.2 类内初始化

如果静态成员是字面值常量类型的 constexpr,那么可以为静态成员提供 const 整数类型的类内初始值。

初始值必须是常量表达式,因为这些成员本身就是常量表达式,所以它们能用在所有适合于常量表达式的地方。例如,用一个初始化了的静态数据成员指定数组成员的维度:

class Account
{
private:
  // 如果 period 的唯一用途就是定义 daily_tbl 的维度,则不需要在 Account 外面专门定义 period
  static constexpr int period = 30; // period 是常量表达式
  double daily_tbl[period];
};

// 如果某个静态成员的应用场景仅限于编译器可以替换它的值的情况,则一个初始化的 const 或 constexpr static 不需要分别定义;
// 相反,如果我们将它用于值不能替换的场景中,则该成员必须有一条定义语句,
// 例如,当需要把 Account::period 传递给一个接受 const int& 的函数时,必须定义 period。
// 综上,即使一个常量静态数据成员在类内部被初始化了,通常情况下,也应该在类的外部定义一下该成员,此时不需要再指定初始值。
constexpr int Account::period;

7.3 独特应用场景

  • 静态数据成员可以是不完全类型。特别的,静态数据成员的类型可以就是它所属的类类型。
class Bar
{
private:
  static Bar mem; // 正确:静态数据成员可以是不完全类型
  Bar *mem2;      // 正确:普通指针成员可以是不完全类型
  Bar mem3;       // 错误:普通数据成员必须是完全类型
};
  • 可以使用静态成员作为默认实参。
class Screen
{
public:
  // bkground 表示一个在类中稍后定义的静态成员
  Screen &clear(char = bkground);

private:
  static const char bkground;
};

8、类成员指针

成员指针(pointer to member),指向类的非静态成员,而不是指向类的对象。类的静态成员不属于任何对象,指向静态成员的指针与普通指针没有区别。

使用 classname::* 表示当前声明的名字是一个类成员指针。初始化成员指针时,令其指向类的某个成员,但是不指定该成员所属的对象;直到使用成员指针时,才提供成员所属的对象。通过 对象.*成员指针对象指针->*成员指针 来解引用成员指针获取对象的成员。

8.1 数据成员指针

class Screen
{
public:
  // data 是一个静态成员函数,可以返回私有数据成员 contents 的指针
  static const string Screen::*data() { return &Screen::contents; }

private:
  // contents 是私有成员,对于成员指针的使用必须位于 Screen 类的成员或友元内部
  string contents;

  friend void friend_fun(); // 友元函数,可以直接访问类的私有成员,使用成员指针
};

void friend_fun()
{
  // pdata 可以指向一个常量(非常量)Screen 对象的 string 类型成员
  const string Screen::*pdata;

  // 在初始化一个成员指针或为成员指针赋值时,需指定它所指向的成员,此时该指针并没有指向任何数据。
  // 成员指针指定了成员而非该成员所属的对象,只有当解引用成员指针时,我们才提供对象的信息。
  // 例如,令 pdata 指向某个非特定 Screen 对象的 contents 成员
  pdata = &Screen::contents;

  // 在 C++ll 标准中,声明成员指针最简单的方法是使用 auto 或 decltype
  auto pdata2 = &Screen::contents;

  Screen myScreen, *pScreen = &myScreen;

  // 使用成员指针时,首先解引用成员指针以得到所需的成员,然后像成员访问运算符一样,通过对象或指针访问成员
  // 使用 .* 解引用 pdata 以获得 myScreen 对象的 contents 成员
  auto s = myScreen.*pdata;
  // 使用 ->* 解引用 pdata 以获得 pScreen 所指对象的 contents 成员
  s = pScreen->*pdata;

  // ...
}

int main()
{
  Screen myScreen, *pScreen = &myScreen;
  // ...

  // data() 返回一个指向 Screen 类的私有成员 contents 的指针
  // 同样,pdata 指向 Screen 类的成员而非实际数据
  const string Screen::*pdata = Screen::data();

  // 要想使用 pdata,必须把它绑定到 Screen 类型的对象上
  auto s = myScreen.*pdata; // 获得 myScreen 对象的 contents 成员
  s = pScreen->*pdata;      // 获得 pScreen 所指对象的 contents 成员

  // ...

  friend_fun();

  return 0;
}

8.2 成员函数指针

class Screen
{
public:
  typedef string::size_type pos;
  char get_cursor() const { return contents[cursor]; }             // 无参函数
  char get() const { return contents[cursor]; }                    // 无参函数,用于函数重载示例
  char get(pos r, pos c) const { return contents[r * width + c]; } // 重载函数,两个参数

  // 光标移动函数,均无参且返回引用,是相同类型的成员,可以统一放在成员函数表中
  Screen &home() { cursor = 0; return *this; }
  Screen &forward() { cursor++; return *this; }
  Screen &back() { cursor--; return *this; }
  Screen &up() { cursor -= width; return *this; }
  Screen &down() { cursor += width; return *this; }

  // 指定光标要移动的方向
  enum Directions { HOME, FORWARD, BACK, UP, DOWN };

  // move 函数接受一个枚举成员,调用相应的光标移动函数,执行对应的操作
  // Menu[cm] 是指向 Screen 成员函数的指针,针对 this 所指的对象,调用该指针所指的成员函数
  Screen &move(Directions cm) { return (this->*Menu[cm])(); }

private:
  string contents;
  pos cursor;
  pos height, width;

  // Action 是一个成员函数指针,可以用任意一个光标移动函数对其赋值
  using Action = Screen &(Screen::*)();

  // Menu 是静态数据成员,是指向光标移动函数的指针的数组,即成员函数指针表
  // 如果一个类含有几个相同类型的成员,则通过成员函数指针表,可以根据索引选择指定成员
  static Action Menu[];
};

// 数组 Menu 依次保存每个光标移动函数的指针,这些函数指针将按照 Directions 中枚举成员对应的偏移量存储
Screen::Action Screen::Menu[] = {
    &Screen::home,
    &Screen::forward,
    &Screen::back,
    &Screen::up,
    &Screen::down,
};

// 和其他函数指针类似,指向成员函数的指针,也需要指定目标函数的返回类型、形参列表以及 const 或引用限定符
// 通过使用类型别名或 typedef,可以让含有成员指针的代码更容易读写
// 类型别名 GetAction 是指向 Screen 类的常量成员函数的指针,这个成员函数接受两个 pos 形参,返回一个 char
using GetAction = char (Screen::*)(Screen::pos, Screen::pos) const;

// 和其他函数指针类似,可以将指向成员函数的指针作为某个函数的返回类型或形参类型
// 指向成员的指针形参也可以拥有默认实参
// 函数 action 接受一个 Screen 对象的引用,一个指向 Screen 成员函数的指针
Screen &action(Screen &srn, GetAction = &Screen::get)
{
  // ...
  return srn;
}

int main()
{
  Screen myScreen, *pScreen = &myScreen;
  // ...

  // 1. 声明和使用成员函数指针
  // 创建一个指向成员函数的指针,最简单的方法是使用 auto 来推断类型
  // pmf 被初始化为指向 Screen 的 get_cursor 成员
  // pmf 的类型是一个成员函数指针,可以指向 Screen 的某个与 get_cursor 类型一致的成员函数
  // 该函数不接受任何实参,返回一个 char,并且包含 const 限定符
  auto pmf = &Screen::get_cursor;

  // 在成员函数和指向该成员的指针之间不存在自动转换规则(不同于普通函数指针),必须显式地使用取地址运算符
  pmf = &Screen::get;

  // 对于重载函数,必须显式地声明函数类型以明确指出想要使用的是哪个函数
  // 例如,声明一个指针,令其指向含有两个形参的 get
  char (Screen::*pmf2)(Screen::pos, Screen::pos) const;
  pmf2 = &Screen::get;

  // 通过 pScreen 所指的对象调用 pmf 所指的函数
  char c1 = (pScreen->*pmf)();
  // 通过 myScreen 对象将实参 0, 0 传给含有两个形参的 qet 函数
  char c2 = (myScreen.*pmf2)(0, 0);

  // 2. 使用成员指针的类型别名
  // 通过使用类型别名 GetAction,可以简化指向 get 的指针定义
  // using GetAction = char (Screen::*)(Screen::pos, Screen::pos) const;
  GetAction get = &Screen::get; // get 指向 Screen 的 get 成员
  c2 = (pScreen->*get)(0, 0);

  // 3. 将成员函数指针作为参数
  // 函数 action 接受一个 Screen 对象的引用,一个指向 Screen 成员函数的指针,指针拥有默认实参
  // 以下是等价的调用:
  action(myScreen);               // 使用默认实参
  action(myScreen, get);          // 使用函数指针变量 get
  action(myScreen, &Screen::get); // 显式地传入地址

  // 4. 使用成员指针函数表
  // 调用 move 函数时,给它传入一个表示光标移动方向的枚举成员
  myScreen.move(Screen::HOME); // 调用 myScreen.home
  myScreen.move(Screen::DOWN); // 调用 myScreen.down

  return 0;
}

8.3 将成员函数用作可调用对象

要想通过一个指向成员函数的指针进行函数调用,必须首先利用 .* 运算符或 ->* 运算符将该指针绑定到特定的对象上。因此,与普通的函数指针不同,成员指针不是一个可调用对象,这样的指针不支持函数调用运算符。也就不能直接将一个指向成员函数的指针传递给算法。

例如,想在一个 stringvector 中找到第一个空 string,不能使用下面的语句:

vector<string> svec = {"home", "forward", "", "back"};
auto fp = &string::empty;              // fp 指向 string 的 empty 函数
find_if(svec.begin(), svec.end(), fp); // 错误,find_if 算法需要一个可调用对象,fp 并不是

8.3.1 使用 function(C++11)生成一个可调用对象

使用标准库模板 function,可以从指向成员函数的指针获取可调用对象。当一个 function 对象包含有一个指向成员函数的指针时,function 类知道它必须使用正确的指向成员的指针运算符来执行函数调用。

vector<string> svec{"home", "forward", "", "back"};

// 在定义一个 function 对象时,必须指定该对象所能表示的函数类型,即可调用对象的形式
// 告诉 function :empty 是一个接受 string 参数并返回 bool 值的函数
function<bool (const string &)> fcn = &string::empty;

// 我们可以认为在 find_if 当中含有类似于如下形式的代码:
// 1)假设 it 是 find_if 内部的迭代器,则 *it 是给定范围内的一个对象
//    if( fcn(*it) ) // 假设 fcn 是 find_if 内部的一个可调用对象的名字
// 2)function 类将函数调用转换成了如下形式
//    if( ((*it).*p)() ) // 假设 p 是 fcn 内部的一个指向成员函数的指针
find_if(svec.begin(), svec.end(), fcn);

// 如果 vector 保存的是 string 的指针,必须指定 function 接受指针
vector<string *> pvec{new string("home"), new string("forward"), new string(""), new string("back")};
function<bool (const string *)> fp = &string::empty;
// fp 接受一个指向 string 的指针,然后使用 ->* 调用 empty
find_if(pvec.begin(), pvec.end(), fp);

8.3.2 使用 mem_fn(C++11)生成一个可调用对象

还可以使用标准库功能 mem_fn 来生成一个可调用对象。

  • function 一样,mem_fn 也可以从成员指针生成一个可调用对象;
  • function 不同的是,mem_fn 可以根据成员指针的类型,推断可调用对象的类型,而无须用户显式地指定。
vector<string> svec = {"home", "forward", "", "back"};

// 使用 mem_fn(&string::empty) 生成一个可调用对象,该对象接受一个 string 实参,返回一个 boo1 值
find_if(svec.begin(), svec.end(), mem_fn(&string::empty));

// mem_fn 生成的可调用对象可以通过对象调用,也可以通过指针调用
// 可以认为 mem_fn 生成的可调用对象含有一对重载的函数调用运算符:一个接受 string*,另一个接受 string&
auto f = mem_fn(&string::empty); // f 接受一个 string 或者一个 string*
f(*svec.begin());                // 正确:传入一个 string 对象,f 使用 .* 调用 empty
f(&svec[0]);                     // 正确:传入一个 string 的指针,f 使用 ->* 调用 empty

8.3.3 使用 bind(C++11)生成一个可调用对象

还可以使用 bind 从成员函数生成一个可调用对象。

  • function 类似的地方是,使用 bind 时,必须将函数中用于表示执行对象的隐式形参转换成显式的;
  • mem_fn 类似的地方是,bind 生成的可调用对象的第一个实参既可以是 string 的指针,也可以是 string 的引用。
#include <string>
#include <vector>
#include <algorithm>
#include <functional>
using namespace std;
using namespace std::placeholders;

int main()
{
  vector<string> svec = {"home", "forward", "", "back"};

  // 选择范围中的每个 string,并将其 bind 到 empty 的第一个隐式实参上
  auto it = find_if(svec.begin(), svec.end(), bind(&string::empty, _1));

  auto f = bind(&string::empty, _1); // f 接受一个 string 或者一个 string*
  f(*svec.begin());                  // 正确:传入一个 string 对象,f 使用 .* 调用 empty
  f(&svec[0]);                       // 正确:传入一个 string 的指针,f 使用 ->* 调用 empty

  return 0;
}

9、嵌套类

嵌套类(nested class),定义在另一个类的内部的类。

  • 嵌套类是一个独立的类;
  • 外层类的对象和嵌套类的对象是相互独立的,嵌套类的对象只包含嵌套类定义的成员,外层类的对象只包含外层类定义的成员;
  • 嵌套类的名字在外层类作用域中是可见的,在外层类作用域之外不可见;
  • 外层类对嵌套类的成员没有特殊的访问权限,同样,嵌套类对外层类的成员也没有特殊的访问权限;
  • 嵌套类是其外层类定义的一个类型成员(嵌套类型,nested type)。和其他成员类似,该类型的访问权限由外层类决定:
    • 位于外层类 public 部分的嵌套类,实际上定义了一种可以随处访问的类型;
    • 位于外层类 protected 部分的嵌套类,定义的类型只能被外层类及其友元和派生类访问;
    • 位于外层类 private 部分的嵌套类,定义的类型只能被外层类的成员和友元访问。
class TextQuery
{
public:
  using line_no = vector<string>::size_type;

  // 嵌套类必须声明在类的内部,但是可以定义在类的内部或者外部
  class QueryResult; // 声明一个嵌套类,是一个不完全类型,会在后面进行定义

  TextQuery(ifstream &);

  // 嵌套类 QueryResult 的主要作用是,表示 TextQuery 对象上 query 操作的结果
  QueryResult query(const string &) const;

private:
  shared_ptr<vector<string>> file;
  map<string, shared_ptr<set<line_no>>> wm;
};

// 定义嵌套类 QueryResult,QueryResult 是 TextQuery 的成员
// 在外层类之外定义一个嵌套类时,必须以外层类的名字限定嵌套类的名字
class TextQuery::QueryResult
{
  // 位于类的作用域内,不必再对 OueryResult 形参进行限定
  friend ostream &print(ostream &, const QueryResult &);

public:
  // 无须定义 QueryResult::line_no,嵌套类可以直接使用外层类的成员,无须对该成员的名字进行限定
  // 在嵌套类中,同样适用名字查找的一般规则,如果在嵌套类作用域中没有找到匹配的名字,会继续查找外层类的作用域
  QueryResult(string s,
              shared_ptr<set<line_no>> p,
              shared_ptr<vector<string>> f);

private:
  string sought;
  shared_ptr<set<line_no>> lines;
  shared_ptr<vector<string>> file;
  static int static_mem; // 声明一个静态成员
};

// 为 QueryResult 类定义构造函数
// 同样必须指明 QueryResult 是嵌套在 TextQuery 的作用域内之的
TextQuery::QueryResult::QueryResult(string s,
                                    shared_ptr<set<line_no>> p,
                                    shared_ptr<vector<string>> f) : sought(s), lines(p), file(f) {}

// 在 TextQuery 的作用域之外定义 QueryResult 的静态成员
int TextQuery::QueryResult::static_mem = 1024;

// 返回类型不在类的作用域中,必须指明 QueryResult 是一个嵌套类
TextQuery::QueryResult TextQuery::query(const string &sought) const
{
  static shared_ptr<set<line_no>> nodata(new set<line_no>);
  auto loc = wm.find(sought);

  // 外层类 TextQuery 的成员 query 可以像使用任何其他类型成员一样使用嵌套类的名字 QueryResult
  if (loc == wm.end())
    return QueryResult(sought, nodata, file);
  else
    return QueryResult(sought, loc->second, file);
}

10、局部类

局部类(local class),定义在某个函数的内部的类。

  • 局部类定义的类型只在定义它的作用域内可见;
  • 局部类的所有成员(包括函数,除外嵌套类)都必须完整定义在类的内部;
  • 在局部类中不允许声明静态数据成员,因为不能够在类的外部进行定义。

局部类不能使用函数作用域中的普通局部变量,局部类只能访问外层作用域定义的类型名、静态变量以及枚举成员。

int a, val;
void foo(int val)
{
  static int si;
  enum Loc { a = 1024, b };

  // Bar 是 foo 的局部类
  struct Bar
  {
    Loc locVal; // 正确:使用一个局部类型名
    int barVal;
    void fooBar(Loc l = a) // 正确:默认实参是 Loc::a
    {
      barVal = val;   // 错误:val 是 foo 的局部变量
      barVal = ::val; // 正确:使用一个全局对象
      barVal = si;    // 正确:使用一个静态局部对象
      locVal = b;     // 正确:使用一个枚举成员
    }
  };
  //...
}

可以在局部类的内部再嵌套一个类。此时,嵌套类的定义可以出现在局部类之外。不过,嵌套类必须定义在与局部类相同的作用域中。并且,局部类内的嵌套类也是一个局部类,必须遵循局部类的各种规定。嵌套类的所有成员都必须定义在嵌套类内部。

void foo()
{
  class Bar
  {
  public:
    // ...
    class Nested; // 声明 Nested 类
  };
  // 定义 Nested 类
  class Bar::Nested
  {
    // ...
  };
}

参考

  1. [美] Stanley B.Lippman著.C++ Primer 中文版(第5版).电子工业出版社.2013.
  2. [美] Stephen Prata著.C++ Primer Plus(第6版)中文版.人民邮电出版社.2012.

宁静以致远,感谢 Vico 老师。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值