C++Primer第七章:类

第七章:类

类型的基本思想是数据抽象和封装,数据抽象依赖于接口和实现分离。类的接口包括用户所能执行的操作,而类的实现包括类的数据成员,负责接口实现的函数体以及定义类所需的各种私有函数。封装实现了类的接口和实现的分离。

一.定义抽象数据类型

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

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

成员函数是如何访问到类的成员的呢,每个成员函数都有一个额外的隐式参数this,使用对象调用成员函数时对象的地址初始化了this,在函数体内部人恶化对类成员的直接访问都被看作对this的隐式引用。实际上,任何自定义名为this的参数或变量的行为都是非法的。同时this也是一个常量指针,不允许修改this中的地址。

**const成员函数:**上面说到this指针是一个指向普通对象的常量指针,所以this指针不能指向常量对象,有时候我们不需要在函数中修改成员,所以我们可以把this指针定义为指向常量对象的常量指针,定义方法是在参数列表后加一个const关键字,这样的函数我们成为常量成员函数。同时注意,常量对象,以及常量对象的引用或指针都只能调用常量成员函数。

类本身就是一个作用域。编译器分两步处理类:首先编译成员的声明,然后编译成员函数体。因此成员函数体可以随意使用类中的其他成员而无须在意这些成员出现的次序。

在类的外部定义成员函数时要保持与声明的一致(包括形参后面的const),同时要使用作用域说明符声明其在类的作用域中。

如果非成员函数是类接口的组成部分,则其声明应该与类放在同一个头文件中。

IO类属于不能被拷贝的类型,因此我们只能通过引用来传递它们,而且我们常常会改变它们的状态(读入读出),所以不能定义为常量引用。

一般来说,执行输出任务的函数应该尽量减少对格式的控制,例如是否换行由用户决定。

类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。构造函数的任务是初始化类对象的数据成员。

构造函数名字与类型相同,没有返回类型。

构造函数不能被声明为const的。当我们创建一个const对象时,我们可以在构造函数中修改其数据成员,知道构造函数完成初始化过程对象才真正取得了常量的属性。

类通过一个特殊的构造函数来控制默认初始化过程,即默认构造函数。默认构造函数无须任何实参。当我们没有显式的定义任何构造函数,则编译器会为我们隐式的定义一个合成的默认构造函数。其使用类内有的初始值进行初始化或者默认初始化成员。

只要我们声明了任何一个构造函数,编译器就不会提供默认构造函数了。

如果类中还有其他的类类型的成员,且该成员没有默认构造函数,则不能生成默认构造函数。

只要不接受任何实参就是默认构造函数。

如果一个类的成员有内置类型或者复合类型的成员,只有当这些成员全部被赋予了类内初始值才适合使用合成的默认初始构造函数。

在C++11中,如果我们需要默认的行为,那么可以通过再参数列表后面写上 = default来要求编译器生成合成的默认构造函数或其他默认行为。= default既可以出现在内部的声明中,也可以出现在外部的定义中(内联不内联的区别)。

构造函数初始值列表:我们可以利用构造函数初始值列表来初始化数据成员,格式为在形参列表和函数体中间,数据成员用其后的花括号中的值初始化,数据成员间用逗号隔开。

除了定义类的对象如何初始化外,类还需要控制拷贝,赋值和销毁对象时的行为。如果我们不主动定义这些操作,则编译器将替我们合成这些操作,合成的版本一般对每个成员执行拷贝,赋值和销毁操作。管理动态内存的类一般不能依赖于合成版本。

二.访问控制与封装

在前面我们为类定义了一些接口,但是还没有封装,即用户可以直接访问类对象的内部并且修改数据成员。在C++中我们使用访问说明符加强类型的封装性。

定义在public说明符之后的成员可在整个程序中被访问,public成员定义类的接口。定义在private说明符之后的成员可以被类的成员函数访问,private封装了类的实现细节。

一个类可以包含多个访问说明符,同一个访问说明符也能够重复使用,有效范围到下一个访问说明符或者类的结尾。

可以使用struct或者class来定义类,两者的区别在于默认的访问权限不同,struct在第一个访问说明符之前的成员是public的,而class是private的。

类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元。友元通过friend关键字在类内部进行声明。

一般来说,友元可以声明在类内部任意位置,最好定义在类的开头或结束位置。

友元的声明只是关于访问权限的声明而不是函数的真正声明,所以我们还需要对函数进行额外的声明(当然有些编译器不需要)。

三.类的其他特性

类中可以定义类型别名,类型别名也具有访问限制(public or private)。

定义类型的成员必须先定义后使用。因此类型成员通常出现在类开始的地方。

前面说过定义在类内部的成员函数是隐式的内联函数,定义在外部的成员函数可以用inline关键字来显式的定义为内联函数。内联函数的定义都应与类定义放在头一个头文件中。

成员函数也可以重载。

有时候我们希望在const成员函数中改变某个数据成员的,可以通过声明可变数据成员来达到,可变数据成员通过mutable关键字声明,可变数据成员永远不会是const,即使它是const对象的成员。

当我们提供一个类内初始值时,必须以符号=或花括号表示。

一个const成员函数如果以引用的形式返回*this,那么它的返回类型也应该是常量引用,使用这个引用再去调用其他的非const成员函数就会出错。

通过区分成员函数是否是const的,我们可以对其进行重载。因为const成员函数代表this指针指向的是常量,即该const是底层const,我们可以通过底层const区分函数。

即使两个类的成员列表完全一致,它们也是不同的类型。

使用类类型可以直接使用类的名字,也可以用class或struct加上类的名字使用。

我们可以先声明类后定义类。对于已经声明而没有定义的类,我们可以定义指向这种类的指针或引用,也可以用它做返回类型或参数。但是不能把它作为数据成员的类型,因为不清楚它的空间大小,所以一个类的数据成员类型不能是自身(还未完成定义),但是可以是自身的引用或指针(已经声明)。

前面讲过友元函数。除了友元函数,类还可以把其他的类定义成友元,也可以把其他类(之前已经定义过)的成员函数定义成友元。此外,友元函数能定义在函数的内部,这样是隐式内联的。

如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。

友元关系不存在传递性。

如果要令某个类的成员函数作为另一个类的友元,需要遵守一些声明与定义逻辑顺序。如果A中要声明B的函数为友元函数。则B首先要定义,定义中要声明该函数但不能定义该函数。然后A定义中声明该友元函数。然后再定义。(存疑

如果一个类想把一组重置函数声明成它的友元,需要对这组函数中的每一个分别声明。当然也可以只声明其中一些。

友元声明只是影响访问权限,其并不会检查声明的友元是否真的在当前作用域中,即就算当前作用域中没有对应名字的友元,类也可以凭空声明一个,但是在该友元真正的被声明前,类的其他函数不能使用该友元(即使在类中的友元声明直接定义了也不行)。

class B {
  friend class A; // 即使没有A页可以声明
  friend void test(); // 即使没有test也可以声明
  // void h() { test();} // 错误不能使用未真正声明的函数
};

四.类的作用域

在类外部定义函数成员,一旦遇到了类名,定义的剩余部分就处在类的作用域之内了(但是返回类型还是处于类的作用域之外)。

编译器处理完类中的全部声明后才会处理成员函数的定义,所以成员函数体内可以使用类中定义的任何名字。但声明中使用的名字必须确保在使用前可见。

如果类成员使用了外层作用域的某个名字,则该名字代表一种类型,则类不能在之后重新定义该名字(所以类型定义要放在类的开始)。

五.构造函数再探

构造函数中对数据成员的初始化和赋值是有区别的,例如对于常量和引用以及没有默认构造函数的类类型必须初始化(例如初始值列表)而不能赋值。而且初始化更加有效率,赋值是默认初始化后再赋值。所以一般我们尽量为数据成员提供初始化。

构造函数初始值列表只说明用于初始化成员的值,与成员初始化的具体执行顺序无关,成员初始化的顺序取决于它们在类定义中出现的顺序!,有时候当构造函数初始值列表成员顺序与成员声明时的顺序不一致时会引发不可知的错误。所以最好令构造函数初始值的顺序与成员声明的顺序保持一致,且尽量避免使用某些成员初始化其他成员。

如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数(此时就不能再定义默认构造函数了,否则会有二义性)。

我们可以定义委托构造函数使某个构造函数使用它所属类的其他构造函数执行它自己的初始化过程。委托构造函数也是构造函数,也有初始值列表和函数体。其特殊之处在于初始值列表是另一个构造函数及参数,在执行委托构造函数时,先执行另一个构造函数(包括函数体),然后再执行委托构造函数的函数体。

如果我们已经定义了其他构造函数(此时不会生成默认构造函数了),那么最好还是要定义一个默认构造函数。

**能通过一个实参调用的构造函数定义一条从构造函数的参数类型向类类型隐式转换的规则,**即在需要类类型的时候,我们可以使用该构造函数的参数类型,编译器会隐式转换出一个相关的临时变量供使用。

编译器只会自动地执行一步(隐式)转换,不能多步转换。

我们可以阻止上面说的隐式转换,只需要在只有一个实参的构造函数加上explicit声明。加了explicit只能用于直接初始化,编译器不会再自动转换的过程中使用该构造函数。explicit声明只能加在类内,不能加在类外的定义中。

当然加了explicit不能隐式转换了,不过我们还是可以通过static_cast等进行显示转换。

聚合类使得用户可以直接访问其成员,并具有特殊的初始化语法形式。聚合类的要求如下:

  • 所有成员都是public
  • 没有定义任何构造函数
  • 没有类内初始值
  • 没有基类和virtual函数

聚合类用struct定义比较方便一点。聚合类的初始化语法是用赋值=和列表初始值,其中列表初始值中的值的顺序要和类中成员声明的顺序一致,如果数量少于成员数,剩下的成员将被值初始化

除了算术类型,引用和指针外,某些类也是字面值类型,称为字面值常量类。

数据成员都是字面值类型的聚合类是字面值常量类。如果一个类不是聚合类,但它符合下列要求,则其也是一个字面值常量类:

  • 数据类型都是字面值类型
  • 类必须至少有一个constexpr构造函数
  • 如果数据成员有类内初始值,则该初始值必须是常量表达式。如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数
  • 类必须使用析构函数的默认定义。

字面值常量类的构造函数可以是constexpr函数,这种构造函数可以=default,否则一般来说它的函数体应该是空的。

六.类的静态成员

我们可以通过在类成员的声明前加上关键字static使得其余类本身直接相关,而不是与类的各个对象保持关联。静态成员可以是private也可以是public的。

同样,静态成员函数也不与任何对象绑定在一起,它们不包含this指针,所以不能是const的,也不能在函数体中显式或隐式的使用this指针,即不能调用类的非静态成员

我们可以使用作用域运算符直接访问静态成员,也可以使用类的对象访问。

我们既可以在类的内部也可以在类的外部定义静态成员函数。不过只能在内部声明或定义加static关键字,外部不能加。

静态成员不是在创建对象时初始化的,所以不由构造函数初始化。一般来说,我们不能再类的内部初始化静态成员,必须在类的外部定义和初始化每个静态成员。当然,静态成员的初始化也可以使用类的数据成员。

要先确保对象只定义一次,最好是把**静态数据成员(函数不用)**的定义与其他非内联函数的定义放在头一个文件中。

前面说过,类的静态成员不能再类内定义。但是如果静态成员是字面值常量类型的constexpr或const,我们可以在类内用const整数类型的类内初始值定义它。如果刚刚所说的那种静态成员的应用场景仅限于编译器可以直接替换它的值的情况则不用再在外部定义,否则还是要在外部定义,只不过外部定义的时候不需要提供值了。所以即使一个常量静态数据成员在类内部被初始化了,也应该在类的外部定义一下这个成员。

静态数据成员可以是不完全类型(声明了还没定义,普通数据成员不可以),我们还可以使用静态成员作为成员函数的默认实参(普通成员不可以,因为普通成员和成员函数同属于一个对象,所以无法真正提供一个对象以便获取值)。

练习

#include <iostream>
#include <string>

using namespace std;


struct Sales_data {
  string bookNo; // 编号
  unsigned units_sold = 0; // 销量
  double revenue = 0.0; // 销售收入
};

int main() {
  Sales_data total;
  double price;
  if (cin >> total.bookNo >> total.units_sold >> price) {
    total.revenue = total.units_sold * price;
    Sales_data trans;
    while (cin >> trans.bookNo >> trans.units_sold >> price) {
      trans.revenue = trans.units_sold * price;
      if (total.bookNo == trans.bookNo) {
        total.units_sold += trans.units_sold;
        total.revenue += trans.revenue;
      } else {
        cout << total.bookNo << ' ' << total.units_sold << ' ' << total.revenue << endl;
        total = trans;
      }
    }
  } else {
    cerr << "No data !" << endl;
  }
}

7.2

struct Sales_data {
  string isbn() const { return bookNo; }
  Sales_data& combine(const Sales_data&);


  string bookNo; // 编号
  unsigned units_sold = 0; // 销量
  double revenue = 0.0; // 总盈利
};

Sales_data& Sales_data::combine(const Sales_data &rhs) {
  units_sold += rhs.units_sold;
  revenue += rhs.revenue;
  return *this;
}

7.3

#include <iostream>
#include <string>

using namespace std;


struct Sales_data {
  string isbn() const { return bookNo; }
  Sales_data& combine(const Sales_data&);


  string bookNo; // 编号
  unsigned units_sold = 0; // 销量
  double revenue = 0.0; // 总盈利
};

Sales_data& Sales_data::combine(const Sales_data &rhs) {
  units_sold += rhs.units_sold;
  revenue += rhs.revenue;
  return *this;
}

int main() {
  Sales_data total;
  double price;
  if (cin >> total.bookNo >> total.units_sold >> price) {
    total.revenue = total.units_sold * price;
    Sales_data trans;
    while (cin >> trans.bookNo >> trans.units_sold >> price) {
      trans.revenue = trans.units_sold * price;
      if (total.isbn() == trans.isbn()) {
        total.combine(trans);
      } else {
        cout << total.bookNo << ' ' << total.units_sold << ' ' << total.revenue << endl;
        total = trans;
      }
    }
  } else {
    cerr << "No data !" << endl;
  }
}

7.4

struct Person {
  string name;
  string address;
};

7.5

struct Person {
  string get_name() const { return name; }
  string get_address() const { return address; }

  string name;
  string address;
};

是const的,即常量成员函数,因为这些函数不需要也不应该修改类的成员。

7.6

struct Sales_data {
  string isbn() const { return bookNo; }
  Sales_data& combine(const Sales_data&);


  string bookNo; // 编号
  unsigned units_sold = 0; // 销量
  double revenue = 0.0; // 总盈利
};

Sales_data& Sales_data::combine(const Sales_data &rhs) {
  units_sold += rhs.units_sold;
  revenue += rhs.revenue;
  return *this;
}

Sales_data add(const Sales_data &lhs, const Sales_data &rhs) {
  Sales_data sum = lhs;
  sum.combine(rhs);
  return sum;
}

istream& read(istream &in, Sales_data &item) {
  double price = 0;
  in >> item.bookNo >> item.units_sold >> price;
  item.revenue = item.units_sold * price;
  return in;
}

ostream& print(ostream &out, Sales_data &item) {
  out << item.isbn() << item.units_sold << " "
       << item.revenue << " ";
  return out;
}

7.7

#include <iostream>
#include <string>

using namespace std;


struct Sales_data {
  string isbn() const { return bookNo; }
  Sales_data& combine(const Sales_data&);


  string bookNo; // 编号
  unsigned units_sold = 0; // 销量
  double revenue = 0.0; // 总盈利
};

Sales_data& Sales_data::combine(const Sales_data &rhs) {
  units_sold += rhs.units_sold;
  revenue += rhs.revenue;
  return *this;
}

Sales_data add(const Sales_data &lhs, const Sales_data &rhs) {
  Sales_data sum = lhs;
  sum.combine(rhs);
  return sum;
}

istream& read(istream &in, Sales_data &item) {
  double price = 0;
  in >> item.bookNo >> item.units_sold >> price;
  item.revenue = item.units_sold * price;
  return in;
}

ostream& print(ostream &out, Sales_data &item) {
  out << item.isbn() << item.units_sold << " "
       << item.revenue << " ";
  return out;
}

int main() {
  Sales_data total;
  if (read(cin, total)) {
    Sales_data trans;
    while (read(cin, trans)) {
      if (total.isbn() == trans.isbn()) {
        total.combine(trans);
      } else {
        print(cout, total);
        cout << endl;
        total = trans;
      }
    }
  } else {
    cerr << "No data !" << endl;
    return -1;
  }
  return 0;
}

7.8

因为read函数中要修改Sales_data对象,而print函数中不用。

7.9

struct Person {
  string get_name() const { return name; }
  string get_address() const { return address; }

  string name;
  string address;
};

istream& read(istream &in, Person &one) {
  in >> one.name >> one.address;
  return in;
}

ostream& print(ostream &out, Person &one) {
  out << one.get_name() << " " << one.get_address();
  return out;
}

7.11

#include <iostream>
#include <string>

using namespace std;


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

  string isbn() const { return bookNo; }
  Sales_data& combine(const Sales_data&);


  string bookNo; // 编号
  unsigned units_sold = 0; // 销量
  double revenue = 0.0; // 总盈利
};

Sales_data::Sales_data(istream &in) {
  read(in, *this);
}

Sales_data& Sales_data::combine(const Sales_data &rhs) {
  units_sold += rhs.units_sold;
  revenue += rhs.revenue;
  return *this;
}

Sales_data add(const Sales_data &lhs, const Sales_data &rhs) {
  Sales_data sum = lhs;
  sum.combine(rhs);
  return sum;
}

istream& read(istream &in, Sales_data &item) {
  double price = 0;
  in >> item.bookNo >> item.units_sold >> price;
  item.revenue = item.units_sold * price;
  return in;
}

ostream& print(ostream &out, Sales_data &item) {
  out << item.isbn() << item.units_sold << " "
       << item.revenue << " ";
  return out;
}

int main() {
  Sales_data data1;
  Sales_data data2("123");
  Sales_data data3("123", 5, 5.5);
  Sales_data data4(cin);

  return 0;
}

7.12

#include <iostream>
#include <string>

using namespace std;


struct Sales_data {
  Sales_data() = default;
  Sales_data(const string &s) : bookNo(s) { }
  Sales_data(const string &s, unsigned n, double p) :
             bookNo(s), units_sold(n), revenue(n * p) { }
  Sales_data(istream& in) { read(in, *this); }

  string isbn() const { return bookNo; }
  Sales_data& combine(const Sales_data&);


  string bookNo; // 编号
  unsigned units_sold = 0; // 销量
  double revenue = 0.0; // 总盈利
};

Sales_data& Sales_data::combine(const Sales_data &rhs) {
  units_sold += rhs.units_sold;
  revenue += rhs.revenue;
  return *this;
}

Sales_data add(const Sales_data &lhs, const Sales_data &rhs) {
  Sales_data sum = lhs;
  sum.combine(rhs);
  return sum;
}

istream& read(istream &in, Sales_data &item) {
  double price = 0;
  in >> item.bookNo >> item.units_sold >> price;
  item.revenue = item.units_sold * price;
  return in;
}

ostream& print(ostream &out, Sales_data &item) {
  out << item.isbn() << item.units_sold << " "
       << item.revenue << " ";
  return out;
}

int main() {
  Sales_data data1;
  Sales_data data2("123");
  Sales_data data3("123", 5, 5.5);
  Sales_data data4(cin);

  return 0;
}

7.13

#include <iostream>
#include <string>

using namespace std;


struct Sales_data {
  Sales_data() = default;
  Sales_data(const string &s) : bookNo(s) { }
  Sales_data(const string &s, unsigned n, double p) :
             bookNo(s), units_sold(n), revenue(n * p) { }
  Sales_data(istream& in) { read(in, *this); }

  string isbn() const { return bookNo; }
  Sales_data& combine(const Sales_data&);


  string bookNo; // 编号
  unsigned units_sold = 0; // 销量
  double revenue = 0.0; // 总盈利
};

Sales_data& Sales_data::combine(const Sales_data &rhs) {
  units_sold += rhs.units_sold;
  revenue += rhs.revenue;
  return *this;
}

Sales_data add(const Sales_data &lhs, const Sales_data &rhs) {
  Sales_data sum = lhs;
  sum.combine(rhs);
  return sum;
}

istream& read(istream &in, Sales_data &item) {
  double price = 0;
  in >> item.bookNo >> item.units_sold >> price;
  item.revenue = item.units_sold * price;
  return in;
}

ostream& print(ostream &out, Sales_data &item) {
  out << item.isbn() << item.units_sold << " "
       << item.revenue << " ";
  return out;
}

int main() {
  Sales_data total(cin);
  if (cin) {
    Sales_data trans(cin);
    while (cin) {
      if (total.isbn() == trans.isbn()) {
        total.combine(trans);
      } else {
        print(cout, total);
        cout << endl;
        total = trans;
      }
    }
  } else {
    cerr << "No data !" << endl;
    return -1;
  }
  return 0;
}

7.14

#include <iostream>
#include <string>

using namespace std;


struct Sales_data {
  Sales_data() : bookNo(""), units_sold(0), revenue(0.0) { }
  Sales_data(const string &s) : bookNo(s) { }
  Sales_data(const string &s, unsigned n, double p) :
             bookNo(s), units_sold(n), revenue(n * p) { }
  Sales_data(istream& in) { read(in, *this); }

  string isbn() const { return bookNo; }
  Sales_data& combine(const Sales_data&);


  string bookNo; // 编号
  unsigned units_sold = 0; // 销量
  double revenue = 0.0; // 总盈利
};

Sales_data& Sales_data::combine(const Sales_data &rhs) {
  units_sold += rhs.units_sold;
  revenue += rhs.revenue;
  return *this;
}

Sales_data add(const Sales_data &lhs, const Sales_data &rhs) {
  Sales_data sum = lhs;
  sum.combine(rhs);
  return sum;
}

istream& read(istream &in, Sales_data &item) {
  double price = 0;
  in >> item.bookNo >> item.units_sold >> price;
  item.revenue = item.units_sold * price;
  return in;
}

ostream& print(ostream &out, Sales_data &item) {
  out << item.isbn() << item.units_sold << " "
       << item.revenue << " ";
  return out;
}

int main() {
  Sales_data data1;
  Sales_data data2("123");
  Sales_data data3("123", 5, 5.5);
  Sales_data data4(cin);

  return 0;
}

7.15

#include <iostream>
#include <string>

using namespace std;

struct Person {
  Person() = default;
  Person(const string &arg_name, const string &arg_address) :
         name(arg_name), address(arg_address) { }
  string get_name() const { return name; }
  string get_address() const { return address; }

  string name;
  string address;
};

istream& read(istream &in, Person &one) {
  in >> one.name >> one.address;
  return in;
}

ostream& print(ostream &out, Person &one) {
  out << one.get_name() << " " << one.get_address();
  return out;
}

7.16

没有。整个程序都能访问的(例如接口)应该放在public后,类的数据成员和作为实现的函数应该放在private后。

7.17

有,struct的默认权限是public,class的默认权限是private

7.18

封装是指保护类的成员不被随意访问的能力。通过把类的实现细节设置为 private,我们就能完成类的封装。封装实现了类的接口和实现的分离。

如书中所述,封装有两个重要的优点:

确保用户代码不会无意间破坏封装对象的状态;
被封装的类的具体实现细节可以随时改变,而无须调整用户级别的代码。
一旦把数据成员定义成 private 的,类的作者就可以比较自由地修改数据了。当实现部分发生改变时,只需要检查类的代码本身以确认这次改变有什么影响;换句话说,只要类的接口不变,用户代码就无须改变。如果数据是 public 的,则所有使用了原来数据成员的代码都可能失效,这时我们必须定位并重写所有依赖于老版本实现的代码,之后才能重新使用该程序。

把数据成员的访问权限设成 private 还有另外一个好处,这么做能防止由于用户的原因造成数据被破坏。如果我们发现有程序缺陷破坏了对象的状态,则可以在有限的范围内定位缺陷:因为只有实现部分的代码可能产生这样的错误。因此,将错误的搜索限制在有限范围内将能极大地简化更改问题及修正程序等工作。

7.19

struct Person {
 public:
  // 构造函数和成员函数设为public
  Person() = default;
  Person(const string &arg_name, const string &arg_address) :
         name(arg_name), address(arg_address) { }
  string get_name() const { return name; }
  string get_address() const { return address; }
 private:
  // 数据成员设为私有的
  string name;
  string address;
};

7.20

当非成员函数确实需要访问类的私有成员时,我们可以把它声明成该类的友元。此时,友元可以“工作在类的内部”,像类的成员一样访问类的所有数据和函数。但是一旦使用不慎(比如随意设定友元),就有可能破坏类的封装性。

7.21

#include <iostream>
#include <string>

using namespace std;


struct Sales_data {
 public:
  Sales_data() = default;
  Sales_data(const string &s) : bookNo(s) { }
  Sales_data(const string &s, unsigned n, double p) :
             bookNo(s), units_sold(n), revenue(n * p) { }
  Sales_data(istream& in) { read(in, *this); }

  string isbn() const { return bookNo; }
  Sales_data& combine(const Sales_data&);

 private:
  string bookNo; // 编号
  unsigned units_sold = 0; // 销量
  double revenue = 0.0; // 总盈利

 friend istream& read(istream &in, Sales_data &item);
 friend ostream& print(ostream &out, Sales_data &item);
};

Sales_data& Sales_data::combine(const Sales_data &rhs) {
  units_sold += rhs.units_sold;
  revenue += rhs.revenue;
  return *this;
}

Sales_data add(const Sales_data &lhs, const Sales_data &rhs) {
  Sales_data sum = lhs;
  sum.combine(rhs);
  return sum;
}

istream& read(istream &in, Sales_data &item) {
  double price = 0;
  in >> item.bookNo >> item.units_sold >> price;
  item.revenue = item.units_sold * price;
  return in;
}

ostream& print(ostream &out, Sales_data &item) {
  out << item.isbn() << item.units_sold << " "
       << item.revenue << " ";
  return out;
}

int main() {
  Sales_data total(cin);
  if (cin) {
    Sales_data trans(cin);
    while (cin) {
      if (total.isbn() == trans.isbn()) {
        total.combine(trans);
      } else {
        print(cout, total);
        cout << endl;
        total = trans;
      }
    }
  } else {
    cerr << "No data !" << endl;
    return -1;
  }
  return 0;
}

7.22

#include <iostream>
#include <string>

using namespace std;

struct Person {
 public:
  // 构造函数和成员函数设为public
  Person() = default;
  Person(const string &arg_name, const string &arg_address) :
         name(arg_name), address(arg_address) { }
  string get_name() const { return name; }
  string get_address() const { return address; }
 private:
  // 数据成员设为私有的
  string name;
  string address;

 friend istream& read(istream &in, Person &one);
 friend ostream& print(ostream &out, Person &one);
};

istream& read(istream &in, Person &one) {
  in >> one.name >> one.address;
  return in;
}

ostream& print(ostream &out, Person &one) {
  out << one.get_name() << " " << one.get_address();
  return out;
}

7.23

#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];
  }

  char get(pos rol, pos clo) const {
    return contents[rol * width + clo];
  }
    
  Screen& move(pos r, pos c);

 private:
  pos cursor = 0;
  pos height = 0, width = 0;
  std::string contents; 
};

inline 
Screen& Screen::move(pos r, pos c) {
  cursor = r * width + c;
  return *this;
}

7.24

  Screen() = default;
  Screen(pos ht, pos wd, pos len) : height(ht), width(wd),
                                    contents(ht * wd, ' ') { }
  Screen(pos ht, pos wd, char c) : height(ht), width(wd),
                                   contents(ht * wd, c) { }

7.25

Screen 的 4 个数据成员都是内置类型或string(string 类定义了拷贝和赋值运算符),因此直接使用类对象执行拷贝和赋值操作是可以的。

7.26

struct Sales_data {
 public:
  Sales_data() = default;
  Sales_data(const string &s) : bookNo(s) { }
  Sales_data(const string &s, unsigned n, double p) :
             bookNo(s), units_sold(n), revenue(n * p) { }
  Sales_data(istream& in) { read(in, *this); }

  string isbn() const { return bookNo; }
  Sales_data& combine(const Sales_data&);

 private:
  double avg_price() const;

  string bookNo; // 编号
  unsigned units_sold = 0; // 销量
  double revenue = 0.0; // 总盈利

 friend istream& read(istream &in, Sales_data &item);
 friend ostream& print(ostream &out, Sales_data &item);
};

Sales_data& Sales_data::combine(const Sales_data &rhs) {
  units_sold += rhs.units_sold;
  revenue += rhs.revenue;
  return *this;
}

Sales_data add(const Sales_data &lhs, const Sales_data &rhs) {
  Sales_data sum = lhs;
  sum.combine(rhs);
  return sum;
}

istream& read(istream &in, Sales_data &item) {
  double price = 0;
  in >> item.bookNo >> item.units_sold >> price;
  item.revenue = item.units_sold * price;
  return in;
}

ostream& print(ostream &out, Sales_data &item) {
  out << item.isbn() << item.units_sold << " "
       << item.revenue << " ";
  return out;
}

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

7.27

#include <string>
#include <iostream>

class Screen {
 public:
  typedef std::string::size_type pos;

  Screen() = default;
  Screen(pos ht, pos wd) : height(ht), width(wd),
                                    contents(ht * wd, ' ') { }
  Screen(pos ht, pos wd, char c) : height(ht), width(wd),
                                   contents(ht * wd, c) { }
  
  char get() const {
    return contents[cursor];
  }

  char get(pos rol, pos clo) const {
    return contents[rol * width + clo];
  }

  Screen& display(std::ostream &os) {
    do_display(os);
    return *this;
  }

  const Screen& display(std::ostream &os) const {
    do_display(os);
    return *this;
  }

  Screen& set(char);
  Screen& set(pos, pos, char);
  Screen& move(pos r, pos c);

 private:
  void do_display(std::ostream &os) const { os << contents;}

  pos cursor = 0;
  pos height = 0, width = 0;
  std::string contents; 
};

inline 
Screen& Screen::move(pos r, pos c) {
  cursor = r * width + c;
  return *this;
}

inline
Screen& Screen::set(char c) {
  contents[cursor] = c;
  return *this;
}

inline
Screen& Screen::set(pos r, pos l, char ch) {
  contents[r * width + l] = ch;
  return *this;
}

int main() {
  Screen my_screen(5, 5, 'X');
  my_screen.move(4, 0).set('#').display(std::cout);
  std::cout << "\n";
  my_screen.display(std::cout);
  std::cout << "\n";

  return 0;
}

输出:

XXXXXXXXXXXXXXXXXXXX#XXXX
XXXXXXXXXXXXXXXXXXXX#XXXX

7.28

如果我们把 mov、set 和 display 函数的返回类型改成 Screen,则上述函数各自只返回一个临时副本,不会改变 myScreen 的值。即输出的第二行应该没有#了。

7.29

推测正确

7.30

通过 this 指针访问成员的优点是,可以非常明确地指出访问的是对象的成员,并且可以在成员函数中使用与数据成员同名的形参;缺点是显得多余,代码不够简洁。

7.31

class Y;

class X {
  Y* ptr;
};

class Y {
  X val;
};

int main() {
  return 0;
}

7.32

#include <string>
#include <iostream>
#include <vector>

class Screen;

class Window_mgr {
  public:
   typedef std::vector<Screen>::size_type ScreenIndex;

   Window_mgr();
   void clear(ScreenIndex);

  private:
   std::vector<Screen> screens;
};

class Screen {
  friend void Window_mgr::clear(ScreenIndex);
 public:
  typedef std::string::size_type pos;

  Screen() = default;
  Screen(pos ht, pos wd) : height(ht), width(wd),
                                    contents(ht * wd, ' ') { }
  Screen(pos ht, pos wd, char c) : height(ht), width(wd),
                                   contents(ht * wd, c) { }
  
  char get() const {
    return contents[cursor];
  }

  char get(pos rol, pos clo) const {
    return contents[rol * width + clo];
  }

  Screen& display(std::ostream &os) {
    do_display(os);
    return *this;
  }

  const Screen& display(std::ostream &os) const {
    do_display(os);
    return *this;
  }

  Screen& set(char);
  Screen& set(pos, pos, char);
  Screen& move(pos r, pos c);

 private:
  void do_display(std::ostream &os) const { os << contents;}

  pos cursor = 0;
  pos height = 0, width = 0;
  std::string contents; 
};

inline 
Screen& Screen::move(pos r, pos c) {
  cursor = r * width + c;
  return *this;
}

inline
Screen& Screen::set(char c) {
  contents[cursor] = c;
  return *this;
}

inline
Screen& Screen::set(pos r, pos l, char ch) {
  contents[r * width + l] = ch;
  return *this;
}

Window_mgr::Window_mgr() : screens{Screen(24, 80)} { }

void Window_mgr::clear(ScreenIndex i) {
  Screen &s = screens[i];
  s.contents = std::string(s.height * s.width, ' ');
}

7.33

返回类型处于Screen类作用域之外,所以要单独指明为Screen::pos。

7.34

这样会导致编译错误,因为对 pos 的使用出现在它的声明之前,此时编译器并不知道 pos 到底是什么含义。

7.35

typedef string Type;            // 声明类型别名 Type 表示 string
Type initVal();                 // 声明函数 initVal,返回类型是 Type
class Exercise {                // 定义一个新类 Exercise
public:
    typedef double Type;        // 在内层作用域重新声明类型别名 Type 表示 double
    Type setVal(Type);          // 声明函数 setVal,参数和返回值的类型都是 Type
    Type initVal();             // 在内层作用域重新声明函数 initVal,返回类型是 Type

private:
    int val;                    // 声明私有数据成员 val
};
// 定义函数 setVal,此时的 Type 显然是外层作用域的
Type Exercise::setVal(Type parm) {
    val = parm + initVal();     // 此处使用的是类内的 initVal 函数
    return val;
}

这段代码会报错,因为setVal的返回类型和实际返回值(或者说声明)类型不匹配。

7.36

类中先声明rem,后声明base。所以先初始化rem。而rem的初始化用到了base,base还未初始化,所以初始值是错误的。改为int base, rem。

7.37

Sales_data first_item(cin); 使用了接受 std::istream & 参数的构造函数,该对象的成员值依赖于用户的输入。

Sales_data next; 使用了 Sales_data 的默认构造函数,其中 string 类型的成员 bookNo 默认初始化为空字符串,其他几个成员使用类内初始值初始化为 0.

Sales_data last("9-999-99999-9"); 使用了接受 const string & 参数的构造函数,其中 bookNo 使用实参初始化为 “9-999-99999-9”,其他几个成员使用类内初始值初始化为 0.

7.38

Sales_data(std::istream &is = std::cin) { read(is, *this); }

7.39

不合法,相当于有两个默认构造函数了,有二义性。

7.40

class Date {
 public:
  Data() = default;
  Date(int arg_year, int arg_month, int arg_day) :
  	   year(arg_year), month(arg_month) { }
  	   
 private:
  int year = 0, month = 0, day = 0;
}

7.41

#include <iostream>
#include <string>

using namespace std;


struct Sales_data {
 public:
  Sales_data(const string &s, unsigned n, double p) :
             bookNo(s), units_sold(n), revenue(n * p) { 
              cout << "创建了Sales_data对象" << endl;
             }

  Sales_data() : Sales_data("", 0, 0) { }
  Sales_data(const string &s) : Sales_data(s, 0, 0) { }
  Sales_data(istream& in) : Sales_data() { read(in, *this); }

  string isbn() const { return bookNo; }
  Sales_data& combine(const Sales_data&);

 private:
  double avg_price() const;

  string bookNo; // 编号
  unsigned units_sold = 0; // 销量
  double revenue = 0.0; // 总盈利

 friend istream& read(istream &in, Sales_data &item);
 friend ostream& print(ostream &out, Sales_data &item);
};

Sales_data& Sales_data::combine(const Sales_data &rhs) {
  units_sold += rhs.units_sold;
  revenue += rhs.revenue;
  return *this;
}

Sales_data add(const Sales_data &lhs, const Sales_data &rhs) {
  Sales_data sum = lhs;
  sum.combine(rhs);
  return sum;
}

istream& read(istream &in, Sales_data &item) {
  double price = 0;
  in >> item.bookNo >> item.units_sold >> price;
  item.revenue = item.units_sold * price;
  return in;
}

ostream& print(ostream &out, Sales_data &item) {
  out << item.isbn() << item.units_sold << " "
       << item.revenue << " ";
  return out;
}

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

int main() {
  Sales_data test1;
  Sales_data test2("test2");
  Sales_data test3("test3", 1, 2);
  Sales_data test4(cin);

  return 0;
}

7.42

class Date {
 public:
  Data() : Date(0, 0, 0) { };
  Date(int arg_year, int arg_month, int arg_day) :
  	   year(arg_year), month(arg_month) {
           cout << "创建了Date对象" << endl;
       }
  	   
 private:
  int year = 0, month = 0, day = 0;
}

7.43

class NoDefault {
 public:
  NoDefault(int) { }
};

class C {
 public:
  C(int i = 0) : test(i) { }
 private:
  NoDefault test;
};

7.44

不合法,Nodefault没有默认构造函数。

7.45

合法,C有默认构造函数(带默认实参)。

7.46

(a) 错误,编译器可以提供合成的默认构造函数
(b) 错误,不需要任何实参,但是可以带默认实参
(c) 错误
(d) 错误,合成的默认构造函数只对类类型的执行他们的默认构造函数,而内置类型和复合类型只有在全局作用域中才会被初始化

7.47

接受一个 string 参数的 Sales_data 构造函数应该是 explicit 的,否则,编译器就有可能自动把一个 string 对象转换成 Sales_data 对象,这种做法显得有些随意,某些时候会与程序员的初衷相违背。

使用 explicit 的优点是避免因隐式类类型转换而带来意想不到的错误,缺点是当用户的确需要这样的类类型转换时,不得不使用略显繁琐的方式来实现。

7.48

创建string对象

创建item1对象

字符串字面值常量先转换成string对象,再创建Sales_data对象。

加了explicit行为一样。

7.49

(a)是正确的,编译器首先用给定的 string 对象 s 自动创建一个 Sales_data 对象,然后这个新生成的临时对象传给 combine 的形参(类型是 Sales_data),函数正确执行并返回结果。

(b)无法编译通过,因为 combine 函数的参数是一个非常量引用,而 s 是一个 string 对象,编译器用 s 自动创建一个 Sales_data 临时对象,但是这个新生成的临时对象无法传递给 combine 所需的非常量引用。如果我们把函数声明修改为 Sales_data &combine(const Sales_data &); 就可以了。

(c)无法编译通过,因为我们把 combine 声明成了常量成员函数,所以该函数无法修改数据成员的值。

7.50

我的Person类属实是没有。

7.51

string 接受的单参数是 const char * 类型,如果我们得到了一个常量字符指针(字符数组),则把它看作 string 对象是自然而然的过程,编译器自动把参数类型转换成类类型也非常符合逻辑,因此我们无须指定为 explicit 的。

与 string 相反,vector 接受的单参数是 int 类型,这个参数的原意是指定 vector 的容量。如果我们在本来需要 vector 的地方提供一个 int 值并且希望这个 int 值自动转换成 vector,则这个过程显得比较牵强,因此把 vector 的单参数构造函数定义成 explicit 的更加合理。

7.52

Sales_data如下:

class Sales_data {
    string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

因为有类内初始值,所以不是聚合类,所以题目所示初始化方式会报错。

7.53

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_hw(bool b) { hw = b; }
  void set_io(bool b) { io = b; }
  void set_other(bool b) { other = b; }
 private:
  bool hw;
  bool io;
  bool other;
};

7.54

这些以 set_ 开头的成员不能声明成 constexpr,这些函数的作用是设置数据成员的值,而 constexpr 函数只能包含 return 语句,不允许执行其他任务。

7.55

string不是字面值类型,所以Data不是字面值常量类。

7.56

静态成员是指声明语句之前带有关键字 static 的类成员,静态成员不是任意单独对象的组成部分,而是由该类的全体对象所共享。

静态成员的优点包括:作用域位于类的范围之内,避免与其他类的成员或者全局作用域的名字冲突;可以是私有成员;通过阅读程序可以非常容易地看出静态成员与特定类关联,使得程序的含义清晰明了。

静态成员与普通成员的区别主要体现在:普通成员与类的对象关联,是某个具体对象的组成部分;而静态成员不从属于任何具体的对象,它由该类的所有对象共享。另外,还有一个细微的区别,静态成员可以作为默认实参,而普通数据成员不能作为默认实参。

7.57

#include <string>

class Account {
 public:
  void calculate() { amount_ += amount_ * interest_rate_; }
  static double get_rate() { return interest_rate_; }
  static void set_rate(double rate) { interest_rate_ = rate; }
 private:
  std::string owner_;
  double amount_;
  static double interest_rate_;
  static double InitRate();
};

double Account::interest_rate_ = InitRate();

double Account::InitRate() {
  return 0.2;
}

int main() {
  return 0;
}

7.58

在类的内部,rate 和 vec 的初始化是错误的,因为除了静态常量成员之外,其他静态成员不能在类的内部初始化。另外,example.C 文件的两条语句也是错误的,因为在这里我们必须给出静态成员的初始值(类外初始化静态成员)。

// example.h
class Example {
public:
  static double rate; // = 6.5;
  // static member should be initialize ouside class
  static const int vecSize = 20;
  static vector<double> vec; //(vecSize);
  // 1. cannot use parentheses as in-class initializer
  // 2. static member should be initialize ouside class
};

// example.C
#include "example.h"
double Example::rate = 6.5;
// should initialize static data member
vector<double> Example::vec(vecSize);
// should initialize static data member

只能包含 return 语句,不允许执行其他任务。

7.55

string不是字面值类型,所以Data不是字面值常量类。

7.56

静态成员是指声明语句之前带有关键字 static 的类成员,静态成员不是任意单独对象的组成部分,而是由该类的全体对象所共享。

静态成员的优点包括:作用域位于类的范围之内,避免与其他类的成员或者全局作用域的名字冲突;可以是私有成员;通过阅读程序可以非常容易地看出静态成员与特定类关联,使得程序的含义清晰明了。

静态成员与普通成员的区别主要体现在:普通成员与类的对象关联,是某个具体对象的组成部分;而静态成员不从属于任何具体的对象,它由该类的所有对象共享。另外,还有一个细微的区别,静态成员可以作为默认实参,而普通数据成员不能作为默认实参。

7.57

#include <string>

class Account {
 public:
  void calculate() { amount_ += amount_ * interest_rate_; }
  static double get_rate() { return interest_rate_; }
  static void set_rate(double rate) { interest_rate_ = rate; }
 private:
  std::string owner_;
  double amount_;
  static double interest_rate_;
  static double InitRate();
};

double Account::interest_rate_ = InitRate();

double Account::InitRate() {
  return 0.2;
}

int main() {
  return 0;
}

7.58

在类的内部,rate 和 vec 的初始化是错误的,因为除了静态常量成员之外,其他静态成员不能在类的内部初始化。另外,example.C 文件的两条语句也是错误的,因为在这里我们必须给出静态成员的初始值(类外初始化静态成员)。

// example.h
class Example {
public:
  static double rate; // = 6.5;
  // static member should be initialize ouside class
  static const int vecSize = 20;
  static vector<double> vec; //(vecSize);
  // 1. cannot use parentheses as in-class initializer
  // 2. static member should be initialize ouside class
};

// example.C
#include "example.h"
double Example::rate = 6.5;
// should initialize static data member
vector<double> Example::vec(vecSize);
// should initialize static data member
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值