c++基础:7-类

43 阅读6分钟

成员函数

  • 成员函数的声明必须在类内部,成员函数的定义既可以在类内部也可以在类外部;

  • 每个成员函数都绑定了隐式形参this,当调用成员函数时,用调用该函数的对象地址来初始化this;

    // 伪代码说明
    Sales_data item;
    item.isbn(); // 调用
    Sales_data::isbn(&item); // 初始化this
    
  • 在成员函数内部,可以对类成员进行直接访问(无须通过成员访问运算符),其实质是对this的隐式引用;

    return bookNo; // 隐式使用了this指向的成员
    return this->bookNo;
    
  • 默认情况下,this是一个指向类类型的常量指针;

    // 伪代码
    std::string Sales_data::isbn(Sales_data *const this);
    
  • 常量成员函数: 紧跟在参数列表后面的const表示this是一个指向常量对象的常量指针, 这样使用const的成员函数称为常量成员函数;

    因为this是隐式形参,不会出现在参数列表中,所以const放在参数列表之后:

    std::string Sales_data::isbn() const; // this是一个指向常量对象的常量指针
    // 伪代码
    std::string Sales_data::isbn(const Sales_data *const);
    
  • 成员函数体中可以随意使用其他成员而无须在意成员出现的顺序,因为编译器首先编译成员的声明,其次才是成员函数体。(也就是先声明后定义,编译顺序也是如此)。

#include <istream>
#include <ostream>
#include <string>

struct Sales_data {
  // 成员函数
  std::string isbn() const { return bookNo; } // 类内声明、类内定义
  Sales_data &combine(const Sales_data &);
  double avg_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 &, Sales_data &);

// 类外定义成员函数
double Sales_data::avg_price() const {
  if (units_sold) {
    return revenue / units_sold;
  }
  return 0;
}

Sales_data &Sales_data::combine(const Sales_data &rhs) {
  units_sold += rhs.units_sold;
  revenue += rhs.revenue;
  return *this; // 返回调用此函数的对象
}

// 定义非成员函数
std::istream &read(std::istream &is, Sales_data &item) {
  double price = 0;
  is >> item.bookNo >> item.units_sold >> price;
  item.revenue = price * item.units_sold;
  return is;
}

std::ostream &print(std::ostream &os, const Sales_data &item) {
  os << item.isbn() << " " << item.units_sold << " " << item.revenue << " "
     << item.avg_price() << std::endl;
  return os;
}

构造函数

  • 构造函数的名字和类名相同;
  • 构造函数没有返回类型;
  • 构造函数不能被声明成const的;
  • 构造函数可以有多个,类似于函数重载;
  • 类没有任何构造函数时,编译器会自动生成默认构造函数;
  • 当类具有自定义构造函数时,编译器不再生成默认构造函数;
  • 当类具有自定义构造函数时,默认构造函数需要通过=default自行指定;
    • =default在类内部,默认构造函数是内联的;
    • =default在类外部,默认构造函数不是内联的;
  • 构造函数初始值列表:为新创建对象的数据成员赋初值;
  • 如果类成员是const的、引用、或者未提供默认构造函数的类类型,则必须通过构造函数初始值列表为这些成员提供初值;
  • 类成员的初始化顺序与出现顺序一致,构造函数初始值列表中的初始值位置不会影响实际的初始化顺序;
  • 委托构造函数:使用类的其他构造函数执行自己的初始化;
  • 显式构造函数:见隐式类类型转换。
#include <istream>
#include <string>

struct Sales_data {
  // 数据成员
  std::string bookNo;
  unsigned units_sold = 0;
  double revenue = 0.0;

  // 构造函数
  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(n * p) {} // 构造函数-初始值列表
  Sales_data(std::istream &); // 构造函数-类内声明,类外定义
  Sales_data(std::string s) : Sales_data(s, 0, 0) {} // 委托构造函数

  // 成员函数
  std::string isbn() const { return bookNo; }
  Sales_data &combine(const Sales_data &);
  double avg_price() const;
};

// 声明非成员接口函数
Sales_data add(const Sales_data &, const Sales_data &);
std::ostream &print(std::ostream &, const Sales_data &);
std::istream &read(std::istream &, Sales_data &);

// 类外定义构造函数
Sales_data::Sales_data(std::istream &is) { read(is, *this); }

// 类外定义成员函数
double Sales_data::avg_price() const {
  if (units_sold) {
    return revenue / units_sold;
  }
  return 0;
}

Sales_data &Sales_data::combine(const Sales_data &rhs) {
  units_sold += rhs.units_sold;
  revenue += rhs.revenue;
  return *this; // 返回调用此函数的对象
}

// 定义非成员函数
std::istream &read(std::istream &is, Sales_data &item) {
  double price = 0;
  is >> item.bookNo >> item.units_sold >> price;
  item.revenue = price * item.units_sold;
  return is;
}

std::ostream &print(std::ostream &os, const Sales_data &item) {
  os << item.isbn() << " " << item.units_sold << " " << item.revenue << " "
     << item.avg_price() << std::endl;
  return os;
}

访问控制

  • 类关键字唯一区别: class: 默认访问权限是privatestruct:默认访问权限是public

  • 类可以让其他类或函数成为友元friend,以访问它的非公有成员;

    • 友元只能在类内部声明;
    • 友元不是类成员,不受所在区域访问控制级别的约束;
    • 友元仅仅指定了访问权限,不属于函数声明;
    • 如果要调用友元函数,则必须在友元声明之外再专门对函数进行声明;
    • 即使友元在类内部进行了定义,也必须在类外部提供相应的声明以使函数可见;
    • 友元函数是隐式内联的;
  • 类型成员:类可以定义某种类型在类中的别名,该别名必须先定义后使用;

  • 可变数据成员:mutable关键字用于声明一个可变数据成员,即使在const对象内部该成员也是可变的;

  • 成员函数可以内联、可以重载;

#include <cstddef>
#include <ostream>
#include <string>
class Screen {
public:
  // 类型成员—先声明后使用
  typedef std::string::size_type pos;

  // 构造函数
  Screen() = default; // 默认构造
  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);            // 返回*this,类外显式内联
  Screen &set(char);                     // 返回*this,类外显式内联
  Screen &set(pos, pos, char);           // 返回*this,类外显式内联

  Screen &display(std::ostream &os) {
    do_display(os);
    return *this;
  } // 非常量版本
  const Screen &display(std::ostream &os) const {
    do_display(os);
    return *this;
  } // 常量版本

  void incr_access() const; // const对象内也能修改access_ctr;

private:
  pos cursor = 0;
  pos height = 0, width = 0;
  std::string contents;
  mutable std::size_t access_ctr; // 可变数据成员, 记录成员函数被调用的次数

  // 显示screen的内容
  void do_display(std::ostream &os) const { os << contents; }
};

// 成员函数-类外显式内联
inline Screen &Screen::move(pos r, pos c) {
  pos row = r * width;
  cursor = row + c;
  return *this;
}

// 成员函数-重载
char Screen::get(pos r, pos c) const {
  pos row = r * width;
  return contents[row + c];
}

// 修改可变数据成员
void Screen::incr_access() const { ++access_ctr; }

// 返回*this,类外显式内联
inline Screen &Screen::set(char c) {
  contents[cursor] = c;
  return *this;
}
inline Screen &Screen::set(pos row, pos col, char ch) {
  contents[row * width + col] = ch;
  return *this;
}

隐式类类型转换

  • 转换构造函数: 如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制;

  • 针对转换构造函数,编译器只会自动执行一次类型转换;

    #include <string>
    class Sales_data {
     public:
      // 数据成员
      std::string bookNo;
    
      // 构造函数
      Sales_data() = default;                         // 默认构造函数-内联
      Sales_data(const std::string &s) : bookNo(s) {} // 构造函数-初始值列表:转换构造函数
    
      // 成员函数
      Sales_data &combine(const Sales_data &);
    };
    
    Sales_data item("No202200001");
    string bookNo = "No202200002";
    item.combine(bookNo); // ok, 编译器使用string对象(bookNo)隐式创建了一个Sales_data对象
    item.combine("No202200002"); // not ok, 字符串字面值隐式转换为string类型,然后停止自动类型转换(编译器只会自动执行一次类型转换,不会再将string("No202200002")转换为Sales_data)
    item.combine(string("No202200002")); // ok, 将string("No202200002")隐式转换为Sales_data
    item.combine(Sales_data("No202200002")); // ok, 无隐式转换
    
  • 显式构造函数:通过explicit关键字抑制构造函数定义的隐式转换;

  • 关键字explicit只对一个实参的构造函数有效(即只对转换构造函数有效);

  • explicit构造函数不能用于拷贝初始化;

    #include <string>
    class Sales_data {
     public:
      // 数据成员
      std::string bookNo;
    
      // 构造函数
      Sales_data() = default; // 默认构造函数-内联
      explicit Sales_data(const std::string &s)
          : bookNo(s) {} // 构造函数-初始值列表:显式构造函数
    
      // 成员函数
      Sales_data &combine(const Sales_data &);
    };
    
    string bookNo = "No202200001";
    Sales_data item(bookNo); // ok,直接初始化
    Sales_data item2 = bookNo; // not ok, `explicit`构造函数不能用于拷贝初始化
    
    string bookNo2 = "No202200002";
    item.combine(bookNo2); // not ok
    item.combine("No202200002"); // not ok
    item.combine(string("No202200002")); // not ok
    item.combine(Sales_data("No202200002")); // ok, 字符串字面值可以初始化常量引用
    item.combine(Sales_data(string("No202200002"))); // ok
    

静态成员

  • 静态成员只属于类本身,与类的任何对象均无关,即:
  • 静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据;
  • 静态成员函数也不与任何对象绑定,不包含this指针;
  • 仍然可以使用类的对象、引用、或者指针来访问静态成员;
  • static关键字只出现在类内部的声明语句中;
  • 除了constexpr类型的静态成员外,静态成员不能在类内初始化;
  • 即使一个常量静态成员在类内部初始化了,通常也应该在类外部定义一下该成员;
#include <string>
class Account {
public:
  Account() = default;
  Account(std::string owner = "", double amount = 0.0){};
  void calculate() { amount += amount * interest_rate; }
  static double rate() { return interest_rate; } // 公有静态成员函数
  static void rate(double);                      // 公有静态成员函数

private:
  std::string owner;
  double amount;
  static constexpr int period = 30; // 静态常量表达式成员,可以类内初始化
  static double interest_rate; // 私有静态数据成员
  static double init_rate();   // 私有静态成员函数
};

// 即使一个常量静态成员在类内部初始化了,通常也应该在类外部定义一下该成员
constexpr int Account::period;
// 静态成员不能在类内部初始化
double Account::interest_rate = 0.05;
// 定义静态成员函数
double Account::init_rate() { return 0.05; }
void Account::rate(double new_rate) { interest_rate = new_rate; }
Account ac1;
Account *ac2 = &ac1;

double r;
r = Account::rate(); // 使用类本身访问静态成员
r = ac1.rate(); // 使用类对象访问静态成员
r = ac2->rate(); // 使用对象指针访问静态成员
  • 静态成员可以是不完全类型;
  • 静态成员可以作为默认实参;
class Bar {
  public:
  //...
  Bar& run(char=c); // 静态成员可以作为默认实参
  private:
   static Bar mem1; //ok, 静态成员可以是不完全类型
   Bar *mem2; //ok, 指针成员可以是不完全类型
   Bar mem3; //not ok, 数据成员必须是完全类型
  static const char c;
}

参考书籍:《C++ Primer》第5版