C++ Primer 总结索引 | 第七章:类

1、使用类定义自己的数据类型。通过 定义新的类型来 反映待解决的问题中的 各种概念

2、是第2章关于 类的话题的延续,主要关注 数据抽象的重要性。数据抽象 能帮助我们将对象的具体实现 与对象所能执行的操作 分离开来

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

4、封装 实现了类的接口 和 实现分离。封装后的类 隐藏了它的实现细节,类的用户 只能用接口而 无法访问实现部分

5、类想要 实现数据抽象 和 封装,首先 需要定义一个 抽象数据类型。在抽象数据类型中,由 类的设计者 负责考虑 类的实现过程;使用该类的程序员只需要抽象地思考 类型做了什么,而 无需了解类型的工作细节

6、数据封装是一种把数据和操作数据的函数捆绑在一起的机制,即 定义数据成员 和 函数成员的能力;

数据抽象是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制,即 保护类的成员 不被随意访问的能力。通过将 类的实现细节设为 private,就能完成 类的封装。类 可以将其他类 或者 函数设为友元,这样它们 就能访问类的非共有成员了

1、定义抽象数据类型

1、在第一章中 使用的 Sales_item 类是一个抽象数据类型,通过它的接口 来使用一个 Sales_item 对象。我们 不能访问 Sales_item 对象的数据成员,甚至 根本不知道这个类 有哪些数据成员

头文件Sales_item.h 见第一章

2、与之相反,Sales_data类 不是一个抽象数据类型。允许类的用户 直接访问 它的数据成员,并要求 由用户来编写操作。想要把 Sales_data 变成 抽象数据类型,需要定义一些操作 以供类的用户使用。一旦 Sales_data 定义了它自己的操作,就可以 封装(隐藏)它的数据成员了
C++ Primer 总结索引 | 第七章:类_第1张图片

1.1 设计 Sales_data 类

1、执行 加法和IO的函数不作为 Sales_data 的成员,相反的,我们 将其定义成 普通函数;执行复合赋值运算的函数 是成员函数,Sales_data 类无须专门定义 赋值函数

综上,Sales_data 的接口 应该包含以下操作:
1)一个isbn 成员函数,用于 返回对象的ISBN号
2)一个combine 成员函数,用于 将一个 Sales_data 对象加到 另一个对象上
3)一个名为 add的 函数,执行两个 Sales_data 对象的加法
4)一个 read 函数,将数据 从istream 读入到 Sales_data 对象中
5)一个 print 函数,将 Sales_data 对象的值 输出到 ostream

2、设计类的接口时,应该考虑 如何才能使得 类 易于使用;使用类时,不应该 顾及类的实现机理

3、使用改进的 Sales_data 类:在考虑 如何实现类之前,先看看 如何使用上述 接口函数

编写该程序的另一个版本
C++ Primer 总结索引 | 第七章:类_第2张图片
C++ Primer 总结索引 | 第七章:类_第3张图片

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; // 通知用户
}

使用read函数 代替了cin >>,combine函数 代替了 +=,print函数 代替了cout <<
read函数 返回它的流参数,而 条件部分 负责检查 这个返回值

在while循环内部,如果 total和trans指示的是 同一本书,调用 combine函数 将trans的内容添加到 total 表示的实时汇总信息 输出出来。因为 print 返回的是 它的流参数的引用,所以 可以把print的返回值 作为<<运算符的左侧运算对象。把 trans 赋给 total,从而 为接着处理文件中下一本书的记录 做好准备

1.2 定义改进的 Sales_data 类

1、bookNo,string 类型,表示 ISBN编号;units_sold,unsigned 类型,表示本书的销量;以及 revenue,double类型,表示这本书的总销售收入

2、类 将包含两个成员函数:combine和isbn。此外还将赋予 Sales_data 另一个成员函数 avg_price 用于返回售出书籍的平均价格。因为 avg_price 的目的并非通用,所以 它应该属于类的实现的一部分,而非接口的一部分

3、定义 和 声明 成员函数的方式 与 普通函数差不多。成员函数的声明 必须在类的内部,它的定义 既可以在类的内部 也可以在类的外部。作为 接口组成部分的 非成员函数,例如 add、read和print等,它们的定义和声明 都在类的外部

附:c++变量的定义和声明 区别

转自 C++变量的声明和定义 终于搞明白了
->1、变量的定义:变量的定义用于为变量分配存储空间,还可以为变量指定初始值。在一个程序中,变量有且仅有一个定义

->2、变量的声明:用于向程序表明变量的类型和名字。程序中变量可以声明多次,但只能定义一次

->3、两者联系与区别:
1)定义也是声明,因为当定义变量时我们也向程序表明了它的类型和名字;
2)但声明不是定义,可以通过使用extern关键字声明变量而不定义它。不定义变量的声明包括对象名、对象类型和对象类型前的关键字extern;
例:

extern int i;//声明但不定义
int i;//声明也定义

extern声明不是定义,也不分配存储空间。事实上,它只是说明变量定义在程序的其他地方

注意:如果声明有初始化式,那么它可被当作是定义,此时声明也是定义了,即使声明标记为extern
例如:extern double pi = 3.1416; 声明也定义,此句话虽然使用了extern,但是这条语句还是定义了pi,分配并初始化了存储空间。

注意:只有当extern声明位于函数外部时,才可以含有初始化式

注意:因为初始化的extern声明已经是定义了,而变量在程序中只能定义一次,所以对该变量随后的任何定义都是错误的:

extern double pi = 3.1416//定义了
double pi;//重定义,不合法

注意:在C++语言中,变量必须仅能定义一次,而且在使用变量之前必须定义或声明变量

->4、为什么需要区分声明和定义:
C++程序通常由许多文件组成。为了让多个文件访问相同的变量,C++区分了声明和定义。任何在多个文件中使用的变量都需要既有定义又有声明。在这种情况下,在一个文件中定义了变量,在其他使用改变了的文件中则只能包含变量的声明(不能再包含定义,因为变量只能定义一次)

->5、真正用的时候 这种需要被别的文件用的变量,必须在本文件中定义好,比如在文件1中定义extern int i=0;然后才可以再别的文件中使用,使用方式是:在类体的外部使用extern int i;这样在这个类中就可以使用该外部变量了,再次强调必须先定义好,才能再别的地方通过extern声明使用
例:

#include 
#include "myclass.h"
int test_i;//定义 等同于 extern test_i =0;
MyClass::MyClass(QWidget *parent, Qt::WFlags flags)
 : QMainWindow(parent, flags)
{
 ui.setupUi(this);
 qDebug()<<test_i;
}

MyClass::~MyClass()
{
}


#include "YourClass.h"
//#include "myclass.h"

extern int test_i;//在此类类体外部声明,在此类中即可使用
YourClass::YourClass(void)
{
 test_i++;
 int test_i;
}


YourClass::~YourClass(void)
{
}

4、由此,改进后的Sales_data 类应该如下所示:

struct Sales_data {
	// 加入 关于Sales_data对象的操作
	std::string isbn() const { return bookno; } // const解释在后面
	Sales_data& combine(const Sales_data&); // 声明在类内,定义在类外
	double avg_price() const;
	// 数据成员没有改变
	std::string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0;
};
// Sales_data的非成员接口函数
Sales_data add(const Sales_data&, const Sales_data&);
std::ostream &print(std::ostream&, const Sales_data&);
std::istream &read(std::istream&, Sales_data&);

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

6、定义成员函数:所有成员 都必须在类的内部声明,但是 成员函数体 可以定义在类内 也可以定义在类外。对于 Sales_data 类来说,isbn函数定义在了类内,而 combine和avg_price定义在了类外

7、isbn函数:std::string isbn() const { return bookno; }
块只有一条 return语句,用于返回 Sales_data 对象的bookNo数据成员

1)如何获得 bookNo 成员所依赖的对象?引入this
对isbn成员函数的调用:total.isbn() 使用了点运算符 来访问total对象的isbn成员,然后 调用它
当我们 调用成员函数时,实际上 是在替某个对象调用它。isbn指向Sales_data 的成员(比如 bookNo),则它隐式地指向 调用该函数的对象的成员。当 isbn返回 bookNo 时,实际上 它隐式地返回 total.bookNo

成员函数 通过一个名为 this 的额外的 隐式参数来访问调用它的那个对象。当我们 调用一个 成员函数时,用 请求该函数的对象地址 初始化this
例如:函数调用 total.isbn() 则编译器负责把 total的地址转递给 isbn的隐式形参 this

// 伪代码,用于说明 调用成员函数的实际执行过程
Sales_data::isbn(&total)

调用 Sales_data 的 isbn 成员时传入了 total 的地址
在成员函数内部,可以直接调用 该函数的对象的成员,而无需通过 成员访问运算符 来做到这一点,因为 this所指的正是这个对象。任何对类成员的直接访问 都被看作 this的隐式引用,当 isbn使用 bookNo时,它隐式地使用 this指向的成员,就像 我们书写了 this->bookNo 一样

this形参是隐式定义的,任何自定义名为this的参数 或变量的行为都是非法的。我们可以在成员函数体内部 使用this,尽管没必要,但 还是能把isbn定义成:std::string isbn() const { return this->bookNo; }
this的目的总是指向这个 对象,所以 this是一个常量指针,不允许改变 this中保存的地址

2)参数列表后的 const关键字:引入const 成员函数(只能在成员函数中)
isbn函数的另一个关键之处 是紧随参数列表之后的const关键字,const的作用是 修改隐式this指针的类型
默认情况下,this的类型是 指向类类型非常量版本的 常量指针。例如在 Sales_data 成员函数中,this的类型是 Sales_data *const。尽管 this是隐式的,但它仍然需要 遵循初始化规则,意味着(在默认情况下)不能把 this 绑定到一个常量对象上,不能在一个常量对象上 调用普通的成员函数

应该把this声明成const Sales_data *const。在isbn的函数体内 不会改变this所指的对象,所以把 this 设置为 指向常量的指针 有助于提高函数的灵活性

允许把const关键字 放在成员函数的参数列表之后,紧跟在 参数列表后面的const表示 this是一个指向常量的指针。像这样使用 const的成员函数 被称为 常量成员函数

// 伪代码,说明隐式的this指针是如何 使用的
// 下面的代码是非法的:因为我们不能显式地定义自己的this指针
// 谨记 此处的this是一个指向常量的指针,因为 isbn是一个常量成员
std::string Sales_data::isbn(const Sales_data *const this)
{ return this->isbn; }

this是指向 常量的指针,常量成员函数 不能改变调用它的对象的内容。isbn可以读取调用它的对象 的数据成员,但是 不能写入新值

8、常量对象,以及 常量对象的引用 或 指针 都只能调用常量成员函数(体现灵活性)

9、类作用域 和 成员函数:类本身就是一个作用域。类的成员函数的定义 嵌套在 类的作用域内,isbn中用到的名字 bookNo 其实就是定义在 Sales_data 内的数据成员

10、即使 bookNo 定义在 isbn之后,isbn也还是能使用 bookNo。编译器分两步处理类:首先 编译成员的声明,然后 才轮到成员函数体。成员函数体 可以 随意使用 类中的其他成员 而无须在意 这些成员出现的次序

11、在类的外部定义 成员函数 时,成员函数的定义 必须与它的声明 匹配
返回类型、参数列表 和 函数名 都得与类内部的声明 保持一致。如果 成员被声明成 常量成员函数,那么它的定义 必须在参数列表后 明确指定const属性

同时,类外部定义的成员的名字 必须 包含它所属的类名
与之前声明的double avg_price() const; 对应的类外的函数的定义

double Sales_data::avg_price() const {
	if (units_sold)
		return revenue / units_sold;
	else
		return 0;
}

函数名Sales_data::avg_price 使用作用域运算符说明:定义了一个名为avg_price的函数,而且 该函数被声明在类Sales_data的作用域内。一旦 编译器看到这个函数名,就能理解剩余的代码 是位于类似的作用域内的。当 avg_price使用 revenue和units_sold时,实际上是 隐式地 使用了Sales_data的成员

12、定义一个返回this对象的函数:函数 combine的设计初衷 类似于 复合赋值运算符 +=,调用 该函数的对象 代表是赋值运算符左侧的运算对象,右侧运算对象 则通过显式的实参 被传入函数

Sales_data& Sales_data::combine(const Sales_data &rhs)
{
	units_sold += rhs.units_sold; // 把rhs的成员加到this对象的成员上
	revenue += rhs.revenue;
	return *this; // 返回调用该函数的对象(数据上面都更新过了)
}

当处理程序调用combine函数时 total.combine(trans); // 更新变量totaltotal的地址 被绑定到 隐式的this参数上,而 rhs 绑定到了 trans 上

当执行 units_sold += rhs.units_sold; // 把rhs的成员加到this对象的成员上效果等同于求 total.units_sold 和 trans.units_sold 的和,然后把结果 保存在 total.units_sold 中

对于它的 返回类型 和 返回语句,当 定义的函数类似于 某个内置运算符时,应该 令该函数的行为 尽量模仿 这个运算符。内置的赋值运算符 把他的左侧运算对象当成 左值返回,combine函数 必须返回 引用类型(第六章 3.2 6 引用返回左值)
此时的左侧运算对象 是一个Sales_data的对象,所以 返回类型应该是 Sales_data&

无须使用 隐式的this指针 访问函数调用者的各个具体成员,而是 需要把调用函数的对象 当成一个整体来访问

return *this; // 返回调用 该函数的对象

return语句解引用 this 指针以 获得执行该函数的对象,上面的调用返回的是 total的引用

13、编写一个名为Person的类,使其表示人员的姓名和地址。使用string对象存放这些元素

#ifndef PERSON_H_
#define PERSON_H_

#include 

struct Person
{
    std::string name;
    std::string address;
};

#endif

在你的Person类中提供一些操作 使其能够返回姓名和地址。这些函数是否应该是const的呢
应该是const,这两个成员函数 只需读取 成员对象,无需 改变 成员对象

#ifndef PERSON_H_
#define PERSON_H_

#include 

struct Person
{
    std::string name;
    std::string address;

    std::string get_name() const{return name;}
    std::string get_address() const{return address;}
};

#endif

1.3 定义类相关的 非成员函数

1、比如 add、read 和 print 等,尽管 这些函数定义的操作 从概念上来说属于 类的接口的组成部分,但它们实际上 不属于 类本身

2、定义 非成员函数的方式 与定义其他函数一样,通常把 函数的定义和声明 分离开来。如果 函数在概念上 属于类 但是不定义在类中,则 它一般应与 类声明(而非定义)在 同一个头文件中。这样 用户使用接口的任何部分 都只需要引入一个文件

3、非成员函数是 类接口的组成部分,则这些函数的声明 应该与类 在同一个头文件内

4、定义 read 和 print函数:(注意对流(istream、ostream)的引用,以及 常量引用 和 非常量的引用)

// 输入的交易信息 包括ISBN、售出总数 和 售出价格
istream &read(istream &is, Sales_data &item)
{
	double price = 0;
	is >> item.bookNo >> item.units_sold >> price;
	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;
}

read函数 从给定流中 将数据读到对象里,print函数 将对象的内容 打印到流中

重点:
1)read 和 print 分别接受一个 各自IO类型的引用 作为参数,因为 IO类属于 不能被拷贝的类型,因此 只能 通过引用来传递它们
因为 读入写入操作 会改变流的内容,所以 两个函数接受的都是 普通引用,而非 对常量的引用

2)print函数 不负责换行。执行输出任务的函数 应该尽量减少 对格式的控制,这样可以确保由用户代码决定 是否换行

5、定义 add函数:add函数 接受两个Sales_data对象 作为其参数,返回一个 Sales_data 对象 表示前两个对象的和

Sales_data add(const Sales_data &lhs, const Sales_data &rhs) // 参数都是 const
{
	Sales_data sum = lhs; // 把lhs的数据成员 拷贝给sum
	sum.combine(rhs); // 把rhs的数据成员加到sum当中
	return sum;
}

在拷贝工作完成之后,sum的bookNo、units_sold和revenue 将和 lhs一致

6、定义自己的版本 Sales_data类(注意注释),然后写完整程序 统计书籍的信息
Sales_data.h

#ifndef SALES_DATA_H
#define SALES_DATA_H

#include 
#include 

struct Sales_data {
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
    Sales_data& combine(const Sales_data&); // 声明,注意返回类型,调用的时候 前面加上Sales_data类型加点
    std::string isbn() const { return bookNo; }
    double avg() const; // 常函数,返回平均值
};

Sales_data &Sales_data::combine(const Sales_data &s) { // 定义combine,在类外要指明类(Sales_data::),左值要返回引用(模仿+=)
    units_sold += s.units_sold;
    revenue += s.revenue;
    return *this; // this是一个指针
}

double Sales_data::avg() const { // 只有成员函数才可以加const,让传入的this指针有底层const
    if (units_sold) {
        return revenue / units_sold;
    }
    else {
        return 0;
    }
}

Sales_data add(const Sales_data &s1, const Sales_data &s2) {
    Sales_data tmp = s1;
    tmp.combine(s2);
    return tmp;
}

std::istream &read(std::istream &is, Sales_data& s) {
    double singlePrice;
    is >> s.bookNo >> s.units_sold >> singlePrice;
    s.revenue = s.units_sold * singlePrice;
    return is; // 因为istream无法复制,所以 形参和返回值都需要是引用类型的
}

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

#endif

7.7.cpp

#include 
#include 
#include "Sales_data.h"

int main()
{
    Sales_data f;

    if (read(std::cin, f)) {
       
        Sales_data cur;
        while (read(std::cin, cur)) {
           
            if (cur.isbn() == f.isbn()) {
                f.combine(cur);
            }
            else {
                if (f.units_sold != 0)
                    print(std::cout, f) << std::endl;
                else
                    print(std::cout, f) << std::endl;
                f = cur;
            }
        }
        if (f.units_sold != 0)
            print(std::cout, f) << std::endl;
        else
            print(std::cout, f) << std::endl;
    }
    else {
        std::cerr << "No data" << std::endl;
        return -1;
    }
    return 0;
}

7、在下面这条if语句中,条件部分的作用是什么

if (read(read(cin, data1), data2))

读入data1和data2,并判断返回是否为真

1.4 构造函数

1、每个类都分别定义了 它的对象 被初始化的方式,类 通过一个或几个 特殊的成员函数来 控制其对象的初始化过程,这些函数 叫构造函数。构造函数的任务是 初始化类对象的数据成员,无论何时 只要类的对象 被创建,就会执行构造函数

2、构造函数的名字 和 类名相同,构造函数 没有返回类型,有 一个(可能为空的)参数列表 和 一个(可能为空的)函数体。类可以 包含多个构造函数,和其他 重载函数 差不多,不同构造函数之间 必须在 参数数量或参数类型上 有所区别

3、不同于 其他成员函数,构造函数不能被 声明成const
当创建类的一个const对象时,直到 构造函数完成 初始化过程,对象 才能真正取得其“常量”属性,因此 狗在函数 在const对象的构造过程中 可以向其写值

4、合成的默认构造函数:如果没有 为这些对象提供初始值,因此 知道它们执行了 默认初始化。类 通过一个特殊的构造函数 来控制默认初始化过程,这个函数叫做 默认构造函数。默认构造函数 无需任何实参
默认构造函数 在很多方面 都有特殊性。如果 我们的类 没有显式地定义构造函数,那么编译器 就会为我们 隐式地定义 一个默认构造函数

编译器创建的构造函数 又被称为 合成的默认构造函数。按照如下规则 初始化类的数据成员
1)如果存在 类内的初始值,用它来 初始化成员
2)默认初始化 该成员(类外0类内随意)

Sales_data为units_sold和revenue提供了初始值,所以 合成的默认构造函数 将使用这些值来初始化对应的成员;同时 它把bookNo默认初始化为 一个空字符串

5、某些类 不能依赖于 合成的默认构造函数:合成的默认构造函数 只适合非常简单的类,比如现在的Sales_data,对一个普通的类来说,必须定义 它自己的默认构造函数,有三个原因
1)编译器只有在 发现类不包含任何构造函数的情况下 才会生成默认的构造函数。一旦 定义了其他构造函数,除非再定义一个 默认的构造函数,否则 类将没有默认构造函数。如果一个类 在某种情况下 需要控制对象初始化,那么该类 很可能在所有情况下 都需要控制

2)对于某些类来说,合成的默认构造函数 可能执行 错误的操作。如果 定义在块中的内置类型 或 复合类型(比如 数组和指针)的对象 被默认初始化,则 它们的值将是未定义的。因此 含有内置类型 或 复合类型成员的类 应该在类的内部初始化这些成员,或者 定义一个自己的默认构造函数
否则,用户在创建类的对象时 就可能得到 未定义的值

3)有的时候 编译器不能为某些类合成默认的 构造函数。类中包含一个 其他类型的成员 且这个成员的类型没有默认构造函数,那么 编译器将无法 初始化该成员。必须 自定义默认构造函数,否则 该类没有可用的默认构造函数

6、定义Sales_data的构造函数:对Sales_data类来说,定义四个构造函数
1)一个istream&,从中 读取一条交易信息
2)一个const string&,表示编号;一个unsigned表示数量;一个double表示售出价格
3)一个const string&,表示编号;编译器 赋予其他成员默认值

4)一个空参数列表(默认构造函数)
既然 已经定义了其他 构造函数,那么也 必须定义一个 默认构造函数

对于 4),默认构造函数:Sales_data() = default;
因为 该构造函数 不接受任何实参,所以 它是一个默认构造函数
定义这个函数的目的 仅仅是因为 既需要其他形式的构造函数,也需要 默认的构造函数。希望 这个函数的作用 完全等同于 之前使用的 合成默认构造函数

在C++11,如果需要默认行为,可以 通过在参数列表后面 写上 = default 来要求编译器生成的 构造函数。= default 既可以和声明一起出现在 类的内部,也可以 作为定义出现在 类的外部

和其他函数一样,如果 = default 在类的内部,则默认 构造函数是内联的;如果 它在类的外部,则该成员 默认情况下 不是内联的

对于 2)、3),构造函数初始值列表:

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) {}

花括号定义了空的函数体,对于 冒号 以及 冒号和花括号之间的代码,称为 构造函数初始值列表,它负责 为新创建的对象的 一个或几个数据成员赋初值。构造函数初始值 是成员名字的一个列表,每个名字 后面紧跟括号括起来的(或者在花括号内的)成员初始值。不同成员的初始化 通过逗号分隔开来

只有一个 string类型参数的构造函数 使用这个string对象 初始化bookNo
当 某个数据成员 被构造函数初始化列表忽略时,它将 以与合成默认构造函数相同的方式 隐式初始化
因此 Sales_data(const std::string &s): bookNo(s) { }等价于

Sales_data(const std::string &s) : bookNo(s), units_sold(0), revenue(0) {}

// 因为类内初始值如下:
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;

构造函数 使用类内初始值 是个好选择,能确保 为成员赋予了一个正确的值。如果 编译器不支持 类内初始值,则 所有的构造函数 都应该显式地初始化 每个内置类型的成员
构造函数 不应该 轻易覆盖类内初始值

7、在类的外部定义 构造函数:与 其他几个构造函数不同,以 istream为参数的构造函数 需要执行 一些实际操作。在它的函数体内,调用了 read函数以给 数据成员 赋初值

Sales_data::Sales_data(std::istream &is)
{
	read(is, *this); // read函数的作用 是从is中读取一条交易信息 然后存入 this对象中
}

构造函数 没有返回类型,定义 直接从 指定的函数名字开始。和其他 成员函数 一样,当我们 在类的外部定义 构造函数时,必须 指明该构造函数 是哪个类的成员

Sales_data::Sales_data(std::istream &is)含义是 Sales_data类的成员函数 Sales_data(成员名字与类名相同 为构造函数),没有 构造函数初始值列表,但是 由于执行了 构造函数体,所以 对象的成员 仍然能被初始化

没有 出现在构造函数 初始值列表中的成员 将通过 相应的类内初始值(如果有)初始化,或者 执行默认初始化

read的第二个参数 是一个Sales_data对象的引用。使用 this 来把 对象当成一个整体访问,而非 直接访问对象的某个成员。使用 *this 将“this”对象作为 实参传递给 read函数

8、在上面的 Sales_data类中添加 构造函数,并编写一段程序 令其用到 每个构造函数
Sales_data_11.h

#ifndef SALES_DATA_11_H
#define SALES_DATA_11_H

#include 
#include 

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 u, double r) :bookNo(s), units_sold(u), revenue(r*u) {};
    Sales_data(std::istream &);
    Sales_data& combine(const Sales_data &); 
    std::string isbn() const { return bookNo; }
    double avg() const; 
};

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

double Sales_data::avg() const { 
    if (units_sold) {
        return revenue / units_sold;
    }
    else {
        return 0;
    }
}

Sales_data add(const Sales_data &s1, const Sales_data &s2) {
    Sales_data tmp = s1;
    tmp.combine(s2);
    return tmp;
}

std::istream& read(std::istream &is, Sales_data &s) {
    double singlePrice;
    is >> s.bookNo >> s.units_sold >> singlePrice;
    s.revenue = s.units_sold * singlePrice;
    return is; 
}

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

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

#endif

7.11.cpp

#include 
#include 
#include "Sales_data_11.h"

int main()
{
	Sales_data sales_data1; // 默认构造函数
	print(std::cout, sales_data1) << std::endl;

	Sales_data sales_data2("1-01"); // Sales_data(const std::string& s)
	print(std::cout, sales_data2) << std::endl;

	Sales_data sales_data3("1-01", 1, 100);
	print(std::cout, sales_data3) << std::endl;

	Sales_data sales_data4(std::cin);
	print(std::cout, sales_data4) << std::endl;

	Sales_data sales_data5 = Sales_data(); // 默认构造函数
	print(std::cout, sales_data5) << std::endl;

	return 0;
}

把只接受一个 istream 作为参数的构造函数定义 移到类的内部
移动到内部后 read 读不到了,可以提前声明 或 通过友元解决 read 读取不到的问题

最好不同题目的文件 放在一个工程内,不然会显示 重复定义,对于头文件也一样,不能在 多个头文件中 定义同名的函数,就算其他没被 #include 进去,一样会有冲突
Sales_data_12.h

#ifndef SALES_DATA_12_H  
#define SALES_DATA_12_H  

#include   
#include   

// 为了能在类成员使用read函数,还可以加入声明,除了声明read,还要声明Sales_data
struct Sales_data;

std::istream& read(std::istream& is, Sales_data& item);

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 u, double r) : bookNo(s), units_sold(u), revenue(r* u) {}

    // 因为 read 函数并不是 Sales_data 类的成员函数,而是一个独立的函数,所以在 Sales_data 类的成员函数内部无法直接访问它
    // 声明 read 函数为友元函数,让 read 函数能够访问 Sales_data 对象的私有成员(后面有介绍)
    //friend std::istream& read(std::istream& is, Sales_data& s);

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

    Sales_data& combine(const Sales_data&);
    std::string isbn() const { return bookNo; }
    double avg() const;
};

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

double Sales_data::avg() const {
    if (units_sold) {
        return revenue / units_sold;
    }
    else {
        return 0;
    }
}

Sales_data add(const Sales_data& s1, const Sales_data& s2) {
    Sales_data tmp = s1;
    tmp.combine(s2);
    return tmp;
}
 
std::istream& read(std::istream& is, Sales_data& s) {
    double singlePrice;
    is >> s.bookNo >> s.units_sold >> singlePrice;
    s.revenue = s.units_sold * singlePrice;
    return is;
}
 
std::ostream& print(std::ostream& os, const Sales_data& s) {
    os << s.isbn() << " " << s.units_sold << " " << s.revenue << " " << s.avg();
    return os;
}

#endif

7.13.cpp

#include 
#include 
#include "Sales_data_12.h"

int main()
{
    Sales_data f(std::cin); // 利用构造函数直接读入

    if (!f.isbn().empty()) { // 读到东西了,不是还是空的

        Sales_data cur;
        while (read(std::cin, cur)) {

            if (cur.isbn() == f.isbn()) {
                f.combine(cur);
            }
            else {
                if (f.units_sold != 0)
                    print(std::cout, f) << std::endl;
                else
                    print(std::cout, f) << std::endl;
                f = cur;
            }
        }
        if (f.units_sold != 0)
            print(std::cout, f) << std::endl;
        else
            print(std::cout, f) << std::endl;
    }
    else {
        std::cerr << "No data" << std::endl;
        return -1;
    }
    return 0;
}

1.5 拷贝、赋值和析构

1、除了 定义类的对象 如何初始化外,类还需要 控制拷贝、赋值和销毁对象时发生的行为
对象会被拷贝,如:初始化变量 以及以值的方式 传递 或 返回一个对象等
当 使用了赋值运算符时 会发生对象的赋值操作
当 对象不再存在时 执行销毁操作,比如:一个局部对象 会在创建它的块 结束时被销毁,当 vector对象(或者 数组)销毁时 存储在其中的对象 也会被销毁

2、如果 我们不主动定义 这些操作,则 编译器将替我们合成它们。编译器生成的版本 将对对象的每个成员 执行拷贝、赋值 和 销毁操作。当编译器执行 如下赋值语句时

total = trans; // 处理下一本书的信息

它的行为 与下面的代码相同

// Sales_data的默认赋值操作等价于:
total.bookNo = trans.bookNo;
total.units_sold = trans.units_sold;
total.revenue = trans.revenue;

3、某些类 不能依赖于 合成的版本:当类 需要分配 类对象之外的资源时,合成的版本 常常会失效。管理动态内存的类 通常不能依赖于 上述操作的合成版本

不过,很多 需要动态内存的类 能(而且应该)使用 vector对象或者 string对象管理 必要的存储空间。使用vector或者string的类 能避免分配 和 释放内存带来的复杂性
如果 类包含vector或者string成员,则其 拷贝、赋值和销毁的合成版本 能够正常工作。当对含有 vector成员的对象 执行拷贝或赋值操作时,vector类 会设法拷贝 或赋值 成员中的元素。当这样的对象被销毁时,将销毁vector对象,也就是依次销毁vector中的每一个元素。这一点和 string非常相似

如何自定义操作的知识 之前,类中所有分配的资源 都应该直接 以类的数据成员的形式存储

2、访问控制与封装

1、类定义了接口,并没有任何机制 强制用户使用这些接口。类还没有封装,也就是说 用户可以直达 Sales_data 对象的内部 并且 控制它的具体实现细节。C++中,使用访问说明符 加强类的封装性
1)定义在 public说明符之后的成员 在整个程序内 可被访问,public成员定义 类的接口
2)定义在 private说明符之后的成员 可以被类的成员函数 访问,但是不能被 使用该类的代码访问,private 部分封装了(即隐藏了)类的实现细节

再一次定义 Sales_data类,新形式如下

class Sales_data {
public: // 添加了访问说明符
	Sales_data() = default;
	Sales_data(const std::string &s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p*n) {}
	Sales_data(const std::string &s): bookNo(s) {}
	Sales_data(std::istream&);
	std::string isbn() const { return bookNo; }
	Sales_data &combine(const Sales_data&);
private: // 添加了访问说明符
	double avg_price() { return units_sold ? revenue/units_sold : 0; }
	std::string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0;
};

作为 接口的一部分,构造函数 和 部分成员函数(即 isbn和combine)紧跟在 public说明符之后;而 数据成员和作为实现部分的函数 则跟在 private说明符后面

一个类 可以包含0个或多个 访问说明符,对于 访问说明符 能出现多少次 也没有严格限定。每个访问说明符 指定了 接下来的成员的访问级别,其 有效范围 直到出现下一个访问说明符 或者 到达类的结尾处为止

2、使用 class或struct关键字:可以使用这两个关键字中的任意一个 定义类,唯一区别是 struct和class的默认访问权限 不一样
类可以在 它的第一个访问说明符之前 定义成员,对这种成员的访问权限 依赖于类定义的方式。如果 使用struct关键字,则定义在 第一个访问说明符之前的成员 是public的;相反,如果我们使用class关键字,则 这些成员是private的

使用class和struct定义类 唯一的区别就是 默认的访问权限

3、在Person 类中,将把哪些成员声明成public 的?哪些声明成private 的?解释你这样做的原因

struct Person
{
public:
	Person() : name(""), address(""){}
	Person(const std::string &sname, const std::string &saddress = "") : name(sname), address(saddress){}
	Person(std::istream &is){read(is, *this);}
    std::string get_name() const{return name;}
    std::string get_address() const{return address;}
private:
    std::string name;
    std::string address;
};

接口应该被定义为公共的,数据不应该暴露在类之外

2.1 友元

1、Sales_data的数据成员是private的,read、print和add函数 也就无法正常编译了,尽管这几个函数是 类的接口的一部分,但它们 不是类的成员

2、类可以允许 其他类或者函数 访问它的非公有成员,方法是令其他类 或者 函数 成为它的友元。如果 类想把一个函数作为它的友元,只需要 增加一条以friend关键字 开始的函数声明语句即可

3、友元的声明:友元的声明 仅仅指定了访问权限,不是 一个通用意义上的 函数声明。如果希望 类的用户能调用 某个友元函数,那么 必须在 友元声明之外 再专门对函数进行一次声明

为了使友元 对类用户可见,通常把 友元的声明 与 类本身 放置在同一个头文件中,且放在 类的外部。Sales_data头文件 应该为read、print和add提供 独立的声明(除了类内部的 友元声明外)

class Sales_data {
// 为Sales_data的非成员函数 所做的友元声明,不然成员变量用不了
friend Sales_data add(const Sales_data&, const Sales_data&);
friend std::istream &read(std::istream&, Sales data&);
friend std::ostream &print(std::ostream&, const Sales_data&);
// 其他成员及访问说明符 与之前一致
public: 
	Sales_data() = default;
	Sales_data(const std::string &s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p*n) {}
	Sales_data(const std::string &s): bookNo(s) {}
	Sales_data(std::istream&);
	std::string isbn() const { return bookNo; }
	Sales_data &combine(const Sales_data&);
private: 
	double avg_price() { return units_sold ? revenue/units_sold : 0; }
	std::string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0;
};// 分号
// Sales_data接口的非成员组成部分的声明
Sales_data add(const Sales_data&, const Sales_data&);
std::istream &read(std::istream&, Sales_data&);
std::ostream &print(std::ostream&, const Sales_data&);

3、友元声明只能出现在 类定义的内部,但是 在类内出现的具体位置不限。友元 不是类的成员 也不受它所在区域访问控制级别的约束

一般来说,最好在类定义开始 或者 结束前的位置 集中声明友元

4、分别举出使用友元的利弊
优点:
外部函数可以方便地使用类的成员,而不需要显示地给它们加上类名;
可以方便地访问所有非公有成员;
有时,对类的用户更容易读懂
缺点:
减少封装和可维护性;
代码冗长,类内的声明,类外函数声明

5、修改Sales_data类使其隐藏实现的细节

#ifndef SALES_DATA_21_H  
#define SALES_DATA_21_H  

#include   
#include   

struct Sales_data;

std::istream& read(std::istream& is, Sales_data& item);
Sales_data add(const Sales_data& s1, const Sales_data& s2);
std::ostream& print(std::ostream& os, const Sales_data& s); // 还需要声明

class Sales_data {
public:
    friend Sales_data add(const Sales_data& s1, const Sales_data& s2);
    friend std::istream& read(std::istream& is, Sales_data& s);
    friend std::ostream& print(std::ostream& os, const Sales_data& s); // 友元声明

    Sales_data() = default;
    Sales_data(const std::string& s) : bookNo(s) {}
    Sales_data(const std::string& s, unsigned u, double r) : bookNo(s), units_sold(u), revenue(r* u) {}
    Sales_data(std::istream& is) {
        read(is, *this);
    }

    Sales_data& combine(const Sales_data&);
    std::string isbn() const { return bookNo; }
    double avg() const;

private:
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

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

double Sales_data::avg() const {
    if (units_sold) {
        return revenue / units_sold;
    }
    else {
        return 0;
    }
}

Sales_data add(const Sales_data& s1, const Sales_data& s2) {
    Sales_data tmp = s1;
    tmp.combine(s2);
    return tmp;
}

std::istream& read(std::istream& is, Sales_data& s) {
    double singlePrice;
    is >> s.bookNo >> s.units_sold >> singlePrice;
    s.revenue = s.units_sold * singlePrice;
    return is;
}

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

#endif

2.2 关键概念:封装的益处

1、封装有两个 重要的优点:
1)确保用户代码 不会无意间 破坏封装对象的状态
2)被封装的类的具体实现细节 可以随时改变,而无需 调整用户级别的代码

一旦 把数据成员 定义成private的,类的作者 可以自由地修改数据。只要类的接口不变,用户代码 就无需改变。如果数据是public的,则 所有使用了原来 数据成员的代码 都可能失效,这时 我们必须定位 并重写 所有依赖于老版本实现的代码,之后 才能重新使用 该程序

把数据成员 的访问权限设成private 还有另一个好处,这么做 能防止 由于用户的原因 造成数据被破坏。如果 发现有程序缺陷 破坏了对象的状态,则可以 在有限的范围内定位缺陷:因为 只有实现部分的代码 可能产生这样的错误,将差错限制在 有限范围内

2、尽管 当类的定义 发生改变时 无需更改用户代码,但是使用了 该类的源文件必须重新编译

3、类的其他特性

特性包括:类型成员、类的成员的类内初始值、可变数据成员、内联成员函数、从成员函数返回*this、关于如何定义并使用 类类型

3.1 类成员再探

1、定义 一对互相关联的类,分别是 Screen 和 Window_mgr,来展示新的特性

2、定义 类型成员:Screen表示 显示器中的一个窗口。每个 Screen包含一个 用于保存Screen内容的string成员 和 三个string::size_type类型的成员(光标的位置、屏幕的高、宽)

类 自定义某种类型在类中的别名。由 类定义的类型名字 和 其他成员一样有 访问限制,public / private

class Screen {
public:
	typedef std::string::size_type pos;
private:
	pos cursor = 0;
	pos height = 0, width = 0;
	std::string contents;
};

Screen的用户 不知道Screen使用了一个string对象 来存放它的数据,通过 把pos定义成public成员 可以隐藏Screen实现的细节

既可以使用 typedef,也可以使用 类型别名(using pos = std::string::size_type; )等价的。用来定义类型 的成员(pos)必须先定义 后使用,与 普通成员不同。因此 类型成员通常出现在 类开始的地方

3、Screen类的成员函数:添加了一个构造函数 令用户 能够定义屏幕的尺寸和内容(注意 类内初始值初始化 和 内联)

class Screen {
public:
	typedef std::string::size_type pos;
	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); // 能在之后被设为 内联
private:
	pos cursor = 0;
	pos height = 0, width = 0;
	std::string contents;
}

第二个构造函数(接受三个参数,没有接受 cursor)为 cursor成员 隐式地使用了 类内初始值。如果 不存在 类内初始值,就要像 其他成员一样 显式地初始化 cursor了

附:C++string 初始化

摘自 C++string 初始化的几种方式
方式一 :最简单直接, 直接赋值

string str1 = "test01" ;

方式二 :以length为长度的ch的拷贝(即length个ch)string(size_type length, char ch);

string str2( 5, 'c' );  //  str2 'ccccc'

方式三 :string( const char *str );

string str3( "Now is the time..." );

方式四 :以index为索引开始的子串,长度为length, 或者 以从start到end的元素为初值 string( string &str, size_type index, size_type length );

string str4( str3, 11, 4 );  //time

4、令成员 作为 内联函数:一些 规模较小的函数 适合于被声明为 内联函数。定义在 类内部的成员函数 是自动inline的。因此,Screen的构造函数 和 返回光标所指字符的get函数 默认是 inline函数

也可以在 类的内部 把inline作为声明的一部分 显式地声明 成员函数,类的外部用 inline关键字 修饰函数的定义

inline Screen &Screen::move(pos r, pos c) // 在函数定义处(之前声明没有)指定inline
{
	pos row = r * width; 
	cursor = row + c; // 把光标移动到指定的列
	return *this; // 以左值的形式 返回对象
}
char Screen::get(pos r, pos c) const // 之前已经类的内部 声明成inline,在函数定义处没有也是 inline
{
	pos row = r * width; // 计算行的位置
	return contents[row + c]; // 返回给定列的字符
}

无需在 声明和定义的地方 同时说明inline,但是 这么做是合法的。最好 只在类外部定义的地方 说明Inline,易于理解
inline成员函数 也应该与 相应的类 定义在同一个头文件中

5、重载成员函数:主要函数之间 在参数的数量或类型上 有所区分就行。成员函数的匹配过程 与 非成员函数相似

6、可变数据成员:希望能修改 类的某个数据成员,即使是在 一个const成员函数内。通过 在变量的声明中 加入mutable 关键字 做到这一点

一个 可变数据成员 永远不会是 const,即使它是 const对象的成员。一个 const成员函数 可以改变一个 可变成员的值

class Screen {
public:
	void some_member() const; // const成员函数
private:
	mutable size_t access_ctr; // 即使在一个const对象内 也能被修改
};

void Screen::some_member() const
{
	++access_ctr; // 记录被调用次数,这个值可以修改
}

7、类数据成员的初始值:Screen类之后 继续定义一个 窗口管理类 并用它表示 显示器上的一组Screen。这个类 将包含一个 Screen类型的vector,每个元素表示 一个特定的Screen。默认情况下,希望Window_mgr类开始时 总是拥有一个 默认初始化的Screen。在C++11新标准中,最好的方式 就是把这个默认值声明成 一个类内初始值

class Window_mgr {
private:
	// 这个Window_mgr追踪的Screen
	// 默认情况下,一个Window_mgr包含一个 标准尺寸的空白 Screen
	std::vector<Screen> screens{Screen(24, 80, ' ')};
};

初始化类类型的成员时,要为 构造函数 传递一个 符合成员类型的实参。本例 使用单独的元素值 对vector成员执行了 列表初始化,这个Screen的值 被传递给 vector的构造函数,创建了 一个单元素的vector对象

类内初始值 必须使用 = 的初始化形式(初始化 Screen的数据成员时所用)或者 花括号括起来的直接初始化形式(初始化screens所用的)

8、Screen 能安全地依赖于拷贝和赋值操作的默认版本吗?
能,Screen类中只有内置类型和string类型,可以使用拷贝和赋值操作

3.2 返回 *this 的成员函数

1、添加 负责设置光标所在位置 或 其他任一给定位置的 字符

class Screen {
public:
	Screen &set(char);
	Screen &set(pos, pos, char);
	// 其他成员和之前的版本一致
};
inline Screen &Screen::set(char c)
{
	contents[cursor] = c; // 设置当前光标 所在的位置的新值
	return *this; // 将this对象作为 左值返回
}
inline Screen &Screen::set(pos r, pos col, char c)
{
	contents[r*width+col] = c; // 设置当前光标 所在的位置的新值
	return *this; // 将this对象作为 左值返回
}

和move一样,set成员的返回值 是调用set的对象的引用。返回引用的函数 是左值,意味着 这些函数返回的对象本身 而非对象的副本,可以改变 myScreen本身(但不管是不是引用 都可以把操作 连接在一条表达式中)

// 把光标移动到一个指定的位置,然后设置 该位置的字符值
myScreen.move(4, 0).set('#');

因为返回的是 本身,所以这些操作会在同一个对象上执行,首先移动myScreen内的光标,然后设置myScreen的contents成员。上述语句 等价于

myScreen.move(4, 0);
myScreen.set('#');

如果令move和set返回Screen而非Screen &,行为不同,此例中等价于

// 返回 Screen
Screen temp = myScreen.move(4, 0); // 对返回值进行拷贝
temp.set('#'); // 不会改变myScreen的contents

假如当初 定义的返回类型 不是引用,则move的返回值 将是*this的副本,因此 调用set只能改变 临时副本,而不能 改变myScreen的值

2、从const成员函数 返回*this
继续添加 名为display的操作,负责打印Screen的内容。希望 这个函数能和move以及set 出现在同一序列中,类似于move和set,display函数 也应该返回 执行它的对象的引用

从逻辑上,显示一个Screen并不需要 改变它的内容,因此 令display为一个const成员,this将是一个指向const的指针 而*this是const对象,display的返回类型 应该是const Sales_data&。如果 真的令display返回一个const的引用,则 不能把display嵌入到 一组动作的序列中

Screen myScreen;
// 如果 display返回 常量引用,则 调用set将引发错误
myScreen.display(cout).set('*'); // 错

即使myScreen是个 非常量对象,对 set的调用也无法编译
display的const版本 返回的是 常量引用,显然 无权set一个常量对象

3、基于 const的重载:非常量版本的函数 对常量对象 不可用,只能 在一个常量对象上 调用const成员函数。另外,虽然 可以在非常量对象上 调用常量版本 或 非常量版本,但是 非常量是个更好的匹配

下例中,定义 do_display 的私有成员,负责打印Screen的实际工作,所有 display操作都调用这个函数

class Screen {
public:
	// 根据对象 是否是const 重载了display函数
	Screen &display(std::ostream &os) { do_display(os); return *this; }
	const Screen &display(std::ostream &os) const { do_display(os); return *this; }
private:
	// 该函数负责显示 Screen的内容
	void do_display(std::ostream &os) const { os << contents; }
}

当 一个成员 调用另一个成员时,this指针 在其中隐式地传递。当 display调用do_display时,它的this指针 隐式地传递给 do_display。而当 display的非常量版本 调用do_display时,它的 this指针 将隐式地指向非常量的指针的指针 转换成 指向常量的指针

当do_display完成后,display函数 各自返回解引用this所得的对象。在 非常量版本中,this指向一个 非常量对象,因此 display返回一个 普通的(非常量)引用;而 const成员 则返回一个常量引用

当在 某个对象上 调用display时,该对象是否是 const 决定了应该调用 display的哪个版本

Screen myScreen(5, 3);
const Screen blank(5, 3);
myScreen.set('#').display(cout); // 调用 非常量的版本
blank.display(cout); // 调用 常量版本

4、对于公共代码 使用私有功能函数(do_display)
1)基本的愿望是 避免在多处 使用同样的代码
2)预期 随着类的规模发展,display函数 有可能 变得更复杂
3)很可能 在开发过程中 给do_display函数添加 某些调试信息,而这些信息 将在代码的最终产品版本中 去掉。显然,只在 do_display一处 添加或删除这些信息 要更容易
4)这个额外的函数调用 不会增加 任何开销,因为 在类的内部 定义了do_display,所以它 被隐式地声明成 内联函数

设计良好的C++代码 常常包含 大量类似于 do_display的小函数

5、给自己的Screen 类添加move、set 和display 函数
screen.h

#ifndef _SCREEN_H_
#define _SCREEN_H_

#include 

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

	Screen() = default;
	Screen(pos ht, pos wd) : height(ht), width(wd) {};
	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 r, pos c) const { return contents[r * width + c]; };
	Screen& move(pos, pos);
	Screen& set(char);
	Screen& set(pos, pos, char);

	Screen& display(std::ostream& os) { do_display(os); return *this; }
	const Screen& display(std::ostream& os) const { do_display(os); return *this; } 
	// const函数 和 非const函数可以只有返回值不同

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

	void do_display(std::ostream& os) const { os << 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 c, char ch) {
	contents[r * width + c] = ch;
	return *this;
}

#endif

7.27.cpp

#include "Screen.h"
#include 

int main() {
	Screen myScreen(5, 5, 'X');

	myScreen.move(4, 0).set('#').display(std::cout);
	std::cout << "\n";
	myScreen.display(std::cout);
	std::cout << "\n";

	return 0;
}

如果move、set和display函数的返回类型不是Screen& 而是Screen,则会发生什么

返回类型是Screen&的输出:
XXXXXXXXXXXXXXXXXXXX#XXXX
XXXXXXXXXXXXXXXXXXXX#XXXX
返回类型是Screen的输出:
XXXXXXXXXXXXXXXXXXXX#XXXX
XXXXXXXXXXXXXXXXXXXXXXXXX

因为这样的话move、set和display返回的是Screen的临时副本,后续set和display操作(注意 move还是会改变的)并不会改变myScreen

3.3 类类型

1、每个类 定义了唯一的类型。对于 两个类来说,即使 它们的成员 完全一样,这两个类 也是两个不同的类型

struct First {
	int memi;
	int getMem();
};
struct Second {
	int memi;
	int getMem();
};
First obj1;
Second obj2 = obj1; // 错误:obj1和obj2的类型不同

对于一个类来说,它的成员 和 其他任何类(或者 任何其他作用域)的成员 都不是一回事

2、可以把类名 作为 类型的名字使用,从而直接 指向类类型。也可以 把类名 跟在关键字class或struct后面

Sales_data item1; // 默认初始化Sales_data类型的对象
class Sales_data item1; // 一条等价的声明,从C语言 继承而来

3、类的声明,不完全类型:就像 函数 可以把声明和定义 分离开来一样,我们也能仅仅声明类 而暂时 不定义它

class Screen; // Screen类的声明

这种声明称为 前向声明,它向程序中 引入了名字Screen 并且指明Screen是一种类类型。对于 类型Screen来说,在它声明之后 定义之前 是一个 不完全类型

不完全类型 只能在 非常有限的情景下使用:可以定义指向 这种类型的指针或引用,可以声明(但是不能定义,与指针和引用 不同)以 不完全类型 作为参数或者 返回类型的函数

对于一个类来说,创建它的对象 之前必须被定义过,而 不能仅仅被声明。否则,编译器 就无法了解 这样的对象要多少存储空间。类 也必须首先 被定义,然后 才能用 引用或者指针 访问其成员。直到 类被定义之后 数据成员才能被 声明为这种类类型

3.4 友元再探

1、之前 把三个普通的非成员函数 定义成友元。类 还可以把其他类 定义为 友元,也可以 把其他类(之前已定义过的)的成员函数 定义成 友元。友元函数 能定义在 类的内部,这样的函数 是隐式内联的

2、类之间的友元关系:Window_mgr类的某些成员 可能要访问它管理的 Screen类的内部数据。如:为Window_mgr添加一个名为 clear的成员,它负责把一个指定的Screen的内容 都设为空白
为了完成,clear需要访问Screen的 私有成员;而要令这种访问合法,Screen需要把Window_mgr指定成 它的友元

class Screen {
	// Window_mgr 的成员可以访问Screen类的 私有部分
	friend class Window_mgr;
	// Screen类的剩余部分
};

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

class Window_mgr {
public:
	// 窗口中每个屏幕的编号
	using ScreenIndex = std::vector<Screen>::size_type;
	// 按照编号 将指定的Screen重置为空白
	void clear(ScreenIndex);
private:
	std::vector<Screen> screens{Screen(24, 80, ' ')};
};
void Window_mgr::clear(ScreenIndex i)
{
	// s是一个Screen的引用,指向我们想清空的那个屏幕
	Screen &s = screens[i];
	// 将那个选定的Screen重置为空白
	s.contents = string(s.height * s.width, ' ');
}

如果clear不是Screen的友元,此时clear将 不能访问Screen的height、width和contents成员

友元关系 不存在 传递性。如果Window_mgr有它自己的友元,这些友元 并不能理所当然地 具有访问Screen的特权。每个类 负责控制自己的友元类 或友元函数

3、令成员函数 作为友元:除了令整个Window_mgr 作为友元之外,Screen还可以 只为clear提供 访问权限
当把一个成员函数 声明成 友元时,我们必须明确指出 该成员函数 属于哪个类

class Screen {
	// Window_mgr::clear必须在 Screen类之前被声明
	friend void Window_mgr::clear(ScreenIndex);
	// Screen类的剩余部分
};

要想令某个成员函数 作为友元,必须 仔细组织程序的结构 以满足 声明和定义的 彼此依赖关系,必须按照 如下方式设计程序
1)首先定义Window_mgr 类,其中声明clear函数,但是 不能定义它。在clear使用Screen的成员之前 必须声明 Screen
2)接下来定义Screen,包括 对于clear的友元声明
3)最后定义clear,此时它才可以 使用screen的成员

4、函数重载和友元:尽管 重载函数的名字 相同,但它们仍然是 不同的函数。如果 一个类想把一组重载函数 声明成 它的友元,它需要对这组函数中的 每一个分别说明

// 重载的storeOn函数
extern std::ostream& storeOn(std::ostream &, Screen &);
extern BitMap& storeOn(BitMap &, Screen &);
class Screen {
	// storeOn的ostream版本 能访问Screen对象的私有部分
	friend std::ostream& storeOn(std::ostream &, Screen &);
	// ...
};

Screen类 把接受 ostream&的storeOn函数 声明成它的友元,但是接受BitMap&作为 参数的版本 仍然不能访问Screen

5、友元声明和作用域:类和非成员函数的声明 不是必须在它们的友元声明之前。当 一个名字第一次出现在 一个友元声明中时,我们隐式地假定 该名字在当前作用域中 是可见的。然而 友元本身不一定真的 声明在 当前作用域中

甚至 就算在有友元定义的类 的内部定义 该函数,也必须 在类的外部 提供相应的声明 从而使得函数可见 。仅仅是 用声明友元的类 的成员 调用该友元函数,它也必须被声明过的(友元的声明 不是真正的声明)

struct X {
	friend void f() { /* 友元函数可以定义在类的内部 */ } // 友元本身不在 当前作用域中的例子
	X() { f(); } // 错误,f还没有被声明
	void g();
	void h();
};
void X::g() { return f(); } // 错误:f还没有被声明
void f(); // 声明那个定义在X中的函数 
void X::h() { return f(); } // 正确:现在f的声明 在作用域中了

最重要的是 理解友元的声明的作用是 影响访问权限,它本身 并非普通意义上的声明

6、定义自己的Screen 和 Window_mgr,其中clear是Window_mgr的成员,是Screen的友元(声明友元,友元是类成员)

#ifndef _SCREEN_32_H_
#define _SCREEN_32_H_

#include 
#include 

class Screen;

class Window_mgr
{
public:
	using screenIndex = std::vector<Screen>::size_type;
	void clear(screenIndex i); // 函数声明,在clear之后不会再有其他操作了(后面不会有点了),所以不需要返回Screen / Screen引用
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) {};
	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 r, pos c) const { return contents[r * width + c]; };
	Screen& move(pos, pos);
	Screen& set(char);
	Screen& set(pos, pos, char);

	Screen& display(std::ostream& os) { do_display(os); return *this; }
	const Screen& display(std::ostream& os) const { do_display(os); return *this; } // const函数 和 非const函数可以只有返回值不同

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

	void do_display(std::ostream& os) const { os << 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 c, char ch) {
	contents[r * width + c] = ch;
	return *this;
}

void Window_mgr::clear(screenIndex i) { // 函数定义过程一致
	Screen &s = screens[i];
	s.contents = std::string(s.height * s.width, ' ');
}

#endif

4、类的作用域

1、在类的作用域之外,普通的数据和函数成员 只能由对象、引用 或者 指针 使用成员访问运算符来访问。对于类类型成员 则使用 域预算符 访问。不论哪种情况,跟在 运算符之后的名字 都必须是 对应类的成员

Screen::pos ht = 24, wd = 80; // 使用Screen定义的pos类型,域预算符
Screen scr(ht, wd, ' ');
Screen *p = &scr;
char c = scr.get(); // 访问scr对象的get成员,成员访问运算符
c = p->get(); // 访问p所指对象的get成员,成员访问运算符

2、作用域和 定义在类外部的成员:一个类就是 一个作用域,在类的外部 定义成员函数时 必须同时提供 类名和函数名。在类的外部,成员的名字 被隐藏起来

一旦 遇到了类名,定义的剩余部分 就在类的作用域内了。剩余部分 包括参数列表和函数体。结果就是,我们可以直接 使用类的其他成员 而无需再次授权了

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

编译器 在处理参数列表之前 已经明确了 当前位于Window_mgr类的作用域中,不必 再专门说明 ScreenIndex 是 Window_mgr类中定义的

3、函数的返回值 通常在 函数名之前。当成员函数 定义在类的外部时,返回类型中 使用的名字 都位于类的作用域外。返回类型 必须指明它是哪个类的成员
例:向 Window_mgr类添加 一个新的名为addScreen的函数,它负责 向显示器添加一个新的屏幕,返回类型是 ScreenIndex

class Window_mgr {
public:
	// 向窗口添加一个Screen,返回它的编号
	ScreenIndex addScreen(const Screen&); // 声明
}
// 先处理返回类型 再进入Window_mgr的作用域
Window_mgr::ScreenIndex Window_mgr::addScreen(const Screen &s) // 定义
{
	screens.push_back(s);
	return screens.size() - 1;
}

返回类型 出现在类名之前,事实上 它是位于Window_mgr类的作用域之外的,所以 明确指定 哪个类定义了它

4.1 名字查找 与 类的作用域

1、名字查找(寻找与 所用名字最匹配的声明的过程)的过程:
1)首先 在名字所在的块中 寻找其声明语句,只考虑 在名字的使用 之前 出现的声明
2)如果没有找到,继续 查找外层的作用域
3)最终没有找到匹配的声明,程序报错

对于 定义在 类内部的成员函数来说,解析其中名字的方式 与上述的查找规则 有所区别
1)首先 编译成员的声明
2)直到 类全部可见后 才编译函数体

因为 成员函数体 直到整个类可见后 才会被处理,所以它能 使用类中定义的 任何名字

2、用于 类成员声明的名字查找:这种两阶段的处理方式 只适用于 成员函数中使用的名字。声明中使用的名字,包括 返回类型或者参数列表中 使用的名字,都必须在 使用前确保可见。如果 某个成员的声明 使用了类中 尚未出现的名字,则 编译器将会在 定义该类的作用域中 继续查找

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

当编译器 看到balance函数的声明语句时,它将在Account类的范围内 寻找对Money的声明。编译器 只考虑Account中 在使用Money前出现的声明,因为 没找到匹配的成员,所以 编译器会接着到Account的外层作用域中查找

在这个例子中,编译器会找到 Money的typedef语句,该类型被用作 balance函数 的返回类型 以及 数据成员 bal的类型。而 balance函数体 在整个类可见后 才被处理,因此,该函数的return语句返回名为 bal 的成员,而非 外层作用域的string对象

3、类型名 要特殊处理:一般来说,内层作用域 可以重新定义 外层作用域中的名字,即使 该名字已经在 内层作用域中 使用过
然而在类中,如果成员 使用了外层作用域中 的某个名字,而 该名字代表一种类型,则类 不能在之后 重新定义该名字

typedef double money;
class Account {
public:
	Money balance() { return bal; } // 使用外层作用域的Money
private:
	typedef double Money; // 错误:不能重新定义Money
	Money bal;
	// ...
};

即使Account中定义的Money类型 与外层作用域一致,上述代码 仍然是错误的

类型名的定义 通常出现在 类的开始处,这样就能确保 所有使用该类型的成员 都出现在类名的定义之后,成员就不会 使用外层作用域中的某个名字

4、成员定义中的普通块作用域的 名字查找
成员函数中 使用的名字 按照如下方式解析:
1)首先,在成员函数内 查找该名字的声明。和前面一样,只有在函数使用之前 出现的声明 才被考虑
2)如果 在成员函数内 没有找到,则 在类内继续寻找,这时 类的所有成员都可以考虑
3)如果 类内也没找到 该名字的声明,在 成员函数 定义之前的作用域内 继续查找

不建议 使用其他成员的名字 作为某个成员函数的参数

// 不是一段 很好的代码
int height; // 下下段代码中会在Screen中使用
class Screen {
public:
	typedef std::string::size_type pos; 
	void dummy_fcn(pos height) {
		cursor = width * height; // 是哪个height?是函数的参数 pos height
	}
private:
	pos cursor = 0;
	pos height = 0, width = 0;// 下段代码使用
};

首先 在函数作用域内 查找表达式中 用到的名字。函数的参数 位于函数作用域内,因此 dummy_fcn 函数体内 用到的名字 height指的是 参数声明
height 参数隐藏了 同名的成员。如果想绕开 上面的查找规则,将代码变为

// 不建议的写法:成员函数中的名字 不应该隐藏 同名的成员
void Screen::dummy_fcn(pos height) {
	cursor = width * this->height; // 成员height
	// 另外一种 表达该成员的方式
	cursor = width * Screen::height; // 成员height
}

尽管 类的成员 被隐藏了,但我们 仍然可以 通过加上类的名字 或 显式地使用this指针 来强制访问成员

最好的 确保使用height成员的 方法是 给参数起个 其他名字

// 建议的写法:不要把 成员名字 作为参数 或 其他局部变量使用
void Screen::dummy_fcn(pos ht) {
	cursor = width * height; // 成员height
}

当编译器 查找名字height时,显然在 dummy_fcn 函数内部是 找不到的。编译器接着会 在Screen内查找匹配的声明,即使 height的声明 出现在dummy_fcn使用它之后,编译器 也能正确地 解析函数使用的是 名为height的成员

5、编译器在 函数和类的作用域中 都没找到名字,即类作用域之后,在外围的作用域中查找

外层作用域中的对象 被名为height的成员 隐藏掉了。因此,如果需要的是 外层作用域中的名字,可以显式地通过 作用域运算符 来进行请求

// 不建议的写法:不要隐藏外层作用域中 可能被用到的名字
void Screen::dummy_fcn(pos height) {
	cursor = width * ::height; // 全局的那个height,即 int height;
}

6、在文件中 名字的出现处 对其进行解析:当成员函数 在类的外部时,名字查找的第三步 不仅要考虑 类定义之前的全局作用域中的声明,还需要 考虑在 成员函数定义之前的全局作用域中的 声明

int height; // 定义了一个名字
class Screen {
public:
	typedef std::string::size_type pos;
	void setHeight(pos);
	pos height = 0; // 隐藏了外层作用域中的height
}
Screen::pos verify(Screen::pos); // 不一定在类内声明
void Screen::setHeight(pos var) {
	// var:函数参数
	// height:类的成员
	// verify:全局函数
	height = verify(var);
}

全局函数verify的声明 在Screen类的定义之前 是不可见的,然后,名字查找的第三步 包括了 成员函数出现之前的全局作用域。verify的声明 位于setHeight的定义之前,因此 可以被正常使用

7、用于类成员声明的名字查找

// 不是一段 很好的代码
int height; // 下下段代码中会在Screen中使用
class Screen {
public:
	typedef std::string::size_type pos; 
	void dummy_fcn(pos height) {
		cursor = width * height; // 是哪个height?是函数的参数 pos height
	}
private:
	pos cursor = 0;
	pos height = 0, width = 0;// 下段代码使用
};

把Screen类的pos的typedef 放在类的最后一行会发生什么情况(与 成员函数中 不同)
这样做会导致编译出错,因为对 pos 的使用出现在它的声明之前,此时编译器并不知道 pos 到底是什么含义

8、理解名字查找与类的作用域的关系,包括用于类成员声明的名字查找和成员定义中的名字查找

#include 

using namespace std;

typedef string Type;
Type initVal();
class Exercise {
public:
    typedef double Type;
    Type setVal(Type);
    Type initVal();

private:
    int val;
};

Type Exercise::setVal(Type parm) {
    val = parm + initVal();
    return val;
}

其中,在 Exercise 类的内部,函数 setVal 和 initVal 用到的 Type 都是 Exercise 内部声明的类型别名,对应的实际类型是 double
在 Exercise 类的外部,定义 Exercise::setVal 函数时形参类型 Type 用的是 Exercise 内部定义的别名,对应 double;返回类型 Type 用的是全局作用域的别名,对应 string

因此 在 setVal 函数的定义处发生错误,此处定义的函数形参类型是 double、返回值类型是 string,而类内声明的同名函数形参类型是 double、返回值类型也是 double,二者无法匹配

修改的措施是在定义 setVal 函数时,使用作用与运算符强制指定函数的返回值类型

5、构造函数 再探

5.1 构造函数初始值列表

1、定义变量时 习惯于 立即对其初始化,跟 先定义再赋值 是有区别的

string foo = "Hello World!"; // 定义 并初始化
string bar; // 默认初始化成空string对象
bar = "Hello World"; // 为bar赋一个新值

就对象的数据成员 而言,初始化和赋值 也有类似的区别。没有在构造函数的初始化列表 中显式地初始化成员,则 该成员 将在构造函数体之前 执行默认初始化

// Sales_data构造函数的一种写法,虽然合法 但比较草率:没有使用构造函数初始值
Sales_data::Sales_data(const string &s, unsigned cnt, double price)
{
	bookNo = s;
	units_sold = cnt;
	revenue = cnt * price;
}

这个版本 是对数据成员 执行了赋值操作

2、构造函数的初始值 有时必不可少:有时我们可能忽略 数据成员初始化和赋值之间的差异,但并非 总能这样。如果成员是const 或者 引用的话,必须将其 初始化。当成员属于某种类类型 且该类没有定义默认构造函数时,也必须将这个 成员初始化

class ConstRef {
public:
	ConstRef(int ii);
private:
	int i;
	const int ci;
	int &ri;
};

常量对象 或者 引用 都需要初始化,成员ci和ri都必须被初始化

// 错误:ci和ri必须被初始化
ConstRef::ConstRef(int ii)
{// 赋值
	i = ii; // 正确
	ci = ii; // 错误:不能给const赋值
	ri = i; // 错误:ri没有初始化
}

随着 构造函数体一开始执行,初始化 就完成了。我们 初始化const或者引用类型的数据成员的唯一机会 就是通过 构造函数初始值,因此 该构造函数的正确形式 应该是

// 正确:显式地初始化 引用和const成员
ConstRef::ConstRef(int ii) : i(ii), ci(ii), ri(ii) {}

3、初始化和赋值的区别 事关底层效率问题:前者 直接初始化数据成员,后者 则先初始化再赋值
如果成员是 const、引用,或者 属于某种 未提供默认构造函数的类类型,必须通过 构造函数初始值列表 为这些成员提供初值

所以建议使用 构造函数初始值

4、成员初始化的顺序:在构造函数初始值中 每个成员只能出现一次,构造函数 初始值列表 只能说明 用于初始化成员的值,而不限定 初始化的具体执行顺序

成员的初始化顺序 与它们在类定义中的出现顺序 一致,构造函数初始值列表中 初始值的先后位置关系 不会影响实际的 初始化顺序

如果 一个成员是用 另一个成员 来初始化,那么 这两个成员的初始化顺序 就很关键了

// 错误例子
class X {
	int i;
	int j;
public:
	// 未定义的:i在j之前被初始化
	X(int val):j(val), i(j) {}
};

实际上,i先被初始化,这个初始值的效果 是试图使用 未定义的值j初始化i

下面的初始值是错误的,请找出问题所在并尝试修改它

struct X {
    X (int i, int j): base(i), rem(base % j) {}
    int rem, base;
};

成员的初始化顺序与它们在类定义中的出现顺序一致,所以会先初始化rem再初始化base,初始化rem时会用到base,故程序出错
可以改变定义的顺序:int base, rem;

如果接受string 的构造函数和接受 istream& 的构造函数都使用默认实参,这种行为合法吗?
非法。因为这样的话,重载构造函数Sale_data()将不明确

5、最好令 构造函数初始值的顺序 与成员函数声明的顺序 保持一致,尽量 避免使用 某些成员初始化其他成员

最好 用构造函数的参数 作为成员的初始值,尽量避免 使用同一个对象的 其他成员,这样可以 不必考虑成员的初始化顺序

X(int val) : i(val), j(val) {}

6、默认实参 和 构造函数:Sales_data 默认构造函数的行为 只接受一个string实参的构造函数 差不多。唯一区别是 接受string实参的构造函数 使用这个实参 初始化bookNo,而 默认构造函数(隐式地)使用string的默认构造函数 初始化bookNo

class Sales_data {
public:
	// 定义默认构造函数,令其与接受一个string实参的构造函数 功能相同
	Sales_data(std::string s = ""):bookNo(s) {}
	// 其他构造函数 与之前一致
	// 其他成员 与之前的版本一致
};

该构造函数 实际上为我们的类 提供了默认构造函数
如果 一该构造函数 为所有参数都提供了 默认实参,则它 实际上也定义了 默认构造函数

默认构造函数是参数列表为空的构造函数 是错误的,如果某个构造函数 包含若干形参,但是同时为 这些形参都提供了 默认实参,则该构造函数也具备 默认构造函数的功能

5.2 委托构造函数

1、C++11拓展了 构造函数初始值的功能,使得可以定义 委托构造函数。一个委托构造函数 使用它所属的类的其他构造函数 执行自己的初始化(它把自己的一些(或者全部)职责 委托给了其他构造函数)

和其他构造函数一样,委托构造函数 也有 一个成员初始值列表(只有唯一的入口:类名本身,就是用 已存在的构造函数 代替 原有构造函数的参数列表)
和其他成员初始值一样,类名后面 紧跟圆括号括起来的参数列表,参数列表 必须与类中另外一个构造函数匹配

2、使用 委托构造函数 重写Sales_data类,同时输出执行过程
Sales_data_41.h

#ifndef SALES_DATA_41_H  
#define SALES_DATA_41_H  

#include   
#include   

struct Sales_data;

std::istream& read(std::istream& is, Sales_data& item);
Sales_data add(const Sales_data& s1, const Sales_data& s2);
std::ostream& print(std::ostream& os, const Sales_data& s); // 还需要声明

class Sales_data {
public:
    friend Sales_data add(const Sales_data& s1, const Sales_data& s2);
    friend std::istream& read(std::istream& is, Sales_data& s);
    friend std::ostream& print(std::ostream& os, const Sales_data& s); // 友元声明

    // 使用委托函数重新编写构造函数
    Sales_data(const std::string& s, unsigned u, double r) : bookNo(s), units_sold(u), revenue(r* u) { std::cout << "Sales_data(const std::string& s, unsigned u, double r)" << std::endl; }
    Sales_data() : Sales_data("", 0, 0) { std::cout << "Sales_data(): Sales_data("", 0, 0)" << std::endl; };
    Sales_data(const std::string& s) : Sales_data(s, 0, 0) { std::cout << "Sales_data(const std::string& s) : Sales_data(s, 0, 0)" << std::endl; }
    Sales_data(std::istream& is) : Sales_data(){
        read(is, *this);
        std::cout << "Sales_data(std::istream& is) : Sales_data()" << std::endl;
    }

    Sales_data& combine(const Sales_data&);
    std::string isbn() const { return bookNo; }
    double avg() const;

private:
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

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

double Sales_data::avg() const {
    if (units_sold) {
        return revenue / units_sold;
    }
    else {
        return 0;
    }
}

Sales_data add(const Sales_data& s1, const Sales_data& s2) {
    Sales_data tmp = s1;
    tmp.combine(s2);
    return tmp;
}

std::istream& read(std::istream& is, Sales_data& s) {
    double singlePrice;
    is >> s.bookNo >> s.units_sold >> singlePrice;
    s.revenue = s.units_sold * singlePrice;
    return is;
}

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

#endif

7.41.cpp

#include 
#include "Sales_data_41.h"

int main()
{
	Sales_data data1("123", 10, 10);
	std::cout << std::endl;
	Sales_data data2; // 调用Sales_data() 不加括号
	std::cout << std::endl;
	Sales_data data3("1234");
	std::cout << std::endl;
	Sales_data data4(std::cin); 
	return 0;
}

运行结果:注意调用函数的过程
C++ Primer 总结索引 | 第七章:类_第4张图片
这个 Sales_data类中,除了一个构造函数外 其他的都委托了它们的工作

接受 istream&的构造函数 也是委托构造函数,它委托给了 默认构造函数,默认构造函数 又接着 委托给 三参数构造函数。当 这些受委托的构造函数 执行完后,接着执行 istream& 构造函数体的内容

当一个构造函数 委托给另一个构造函数时,受委托的构造函数的初始值列表 和 函数体 被依次执行。假如 函数体包含有代码的话,将先执行 这些代码(比如上面代码的输出操作),然后 控制权 才会交还给 委托者的函数体

5.3 默认构造函数的作用

1、当对象 被默认初始化 或 值初始化时 自动执行默认构造函数
默认初始化 在以下情况下发生:
1)在块作用域内 不使用 任何初始值定义一个 非静态变量 或者 数组时
2)当一个类本身含有 类类型的成员 且使用 合成的默认构造函数时
3)当类类型的成员 没有在 构造函数初始值列表中 显式地初始化时

值初始化 在以下情况下发生:
1)在数组初始化的过程中 提供的初始值数量 少于 数组的大小时
2)当我们 不使用 初始值定义 一个局部静态变量时
3)通过形如T() 的表达式 显式地请求 值初始化时,其中 T是类型名(如 vector(10) 使用vector的一个构造函数)

2、类 必须包含一个 默认构造函数 以便在上述情况下 使用,类的某些数据成员 缺少 默认构造函数

class NoDefault {
public:
	NoDefault(const std::string&);
	// 还有其他成员,但是 没有其他构造函数了
};
struct A {
	Nodefault my_mem;
};
A a; // 错误:不能为A合成构造函数
struct B {
	B() {} // 错误:b_member没有初始值
	NoDefault b_member;
};

下面这条声明合法吗 vector vec(10);
非法,因为NoDefault 没有默认构造函数,如果有默认构造函数 就是合法的

3、使用默认构造函数:下面的obj的声明可以正常编译通过

Sales_data obj(); // 正确:定义了一个函数 而非对象
if (obj.isbn() == p.isbn()) // 错误:obj是一个函数

obj实际的含义 是一个不接受任何参数的函数 并且 其返回值是 Sales_data 类型的对象

如果 像定义一个 使用默认构造函数 进行初始化的对象,正确的方法是 去掉对象名之后的 空的括号对

// 正确:obj是个默认初始化的对象
Sales_data obj;

5.4 隐式的类类型转换

1、跟C++内置类型之间 定义的几种 自动转换规则 一样,类也有隐式转换规则。如果 构造函数只接受 一个实参,则它实际上定义了 转换为此类类型的 隐式转换机制。我们把这种构造函数 称作 转换构造函数

能通过一个实参调用的构造函数 定义了 一条从构造函数的参数类型 向类类型 隐式转换的规则

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

string null_book = "9999";

// 构造一个临时的Sales_data对象
// 该对象的units_sold和revenue等于0,bookNo等于null_book
item.combine(null_book); 

用一个string实参 调用了 Sales_data 的 combine 成员。编译器用给定的string自动创建了 一个Sales_data对象。新生成的这个(临时)Sales_data 对象被传递给 combine。因为 combine 的参数是 一个常量引用,所以 我们可以 给该参数传递一个临时量(引用 不能绑定 临时量)

3、只允许一步 类类型转换:编译器 只会自动地执行 一步类型转换。
例:下面的代码 隐式地使用了 两种转换规则,所以它是错误的

// 错误:需要用户定义的两种转换
// (1)把“9999” 转换成string
// (2)再把这个临时的string 转换成Sales_data
item.combine("9999");

想完成 上述调用,可以 显式地把字符串 转换成 string 或者 Sales_data对象

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

4、类类型转换 不是总有效:从istream到Sales_data的转换

// 使用 istream 构造函数 创建一个函数传递给combine
item.combine(cin);

这段代码 隐式地把cin转换成Sales_data,这个转换 执行了接受一个 istream 的Sales_data构造函数。该构造函数 通过读取 标准输入创建了一个 临时的 Sales_data 对象,随后 将得到的对象 传递给combine

Sales_data 对象是个临时量,一旦combine完成 我们就不能再访问它了。实际上,我们构建了一个对象,先将它的值加到item中,随后将其丢弃

5、抑制构造函数定义的 隐式转换:在要求 隐式转换的程序的上下文中,可以通过 将构造函数 声明为 explicit加以阻止

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

此时,没有任何构造函数 能用于隐式地创建Sales_data对象,之前的 两种方法 都无法通过 编译

item.combine(null_book); // 错误:string构造函数是explicit的
item.combine(cin); // 错误:istream构造函数是 explicit的

关键字explicit只对一个实参的构造函数 有效。需要 多个实参的构造函数 不能用于执行 隐式转换,所以 无需将这些构造函数指定为explicit的。只能在 类内声明构造函数时 使用explicit关键字,在类外部 定义时 不应重复

// 错误:explicit关键字 只允许出现在类内的 构造函数声明处
explicit Sales_data::Sales_data(istream& is)
{
	read(is, *this);
}

6、explicit构造函数 只能用于 直接初始化:发生隐式转换的一种情况是 当我们执行拷贝形式的初始化时(使用=),只能使用 直接初始化 而不能使用 explicit构造函数

Sales_data item1(null_book); // 正确:直接初始化

// 错误:不能将explicit构造函数 用于拷贝形式的初始化过程
Sales_data item2 = null_book;

7、explicit构造函数总结:当我们用explicit关键字 声明构造函数时,它将只能 以直接初始化的形式使用。而且,编译器 将不会在 自动转换的过程中 使用该构造函数

8、为转换 显式地使用构造函数:尽管 编译器不会将 explicit的构造函数 用于隐式转换过程,但是 我们可以使用这样的构造函数 显式地强制进行转换

// 正确:实参是一个显式构造的Sales_data对象
item.combine(Sales_data(null_book));

// 正确:static_cast 可以使用 explicit的构造函数
item.combine(static_cast<Sales_data>(cin));

使用 static_cast 执行了 显式地 转换(非隐式的)

9、标准库中 含有显式构造函数的类
标准库中的类 含有单参数的构造函数
1)接受一个单参数的const char*的string构造函数 不是explicit的(string s6(“hiya”))
2)接受一个容量参数的vector构造函数 是explicit的(vector v4(n)

10、假定 Sales_data 的构造函数不是 explicit的,则 下述定义有没有影响

string null_isbn("9999");
Sales_data item1(null_isbn);
Sales_data item2("9999"); 

无论是不是 explicit 都无影响
都是调用 Sales_data 的构造函数 创建它的对象,无须 类类型转换

11、对于combine 函数的三种不同声明,当我们调用i.combine(s) 时分别发生什么情况?其中 i 是一个 Sales_data,而 s 是一个string对象

(a)Sales_data &combine(Sales_data);
(b)Sales_data &combine(Sales_data&);
(c)Sales_data &combine(const Sales_data&) const; 

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

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

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

12、vector 将其单参数的构造函数定义成 explicit 的,而string则不是,你觉得原因何在?
string 接受的单参数是 const char * 类型,如果我们得到了一个常量字符指针(字符数组),则把它看作 string 对象是自然而然的过程,编译器自动把参数类型转换成类类型也非常符合逻辑,因此我们无须指定为 explicit 的

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

5.5 聚合类

1、聚合类 使得用户 可以直接访问其 成员,并且具有 特殊的初始化 语法形式
当一个类 满足如下条件时,我们说 它是聚合的
1)所有成员是public
2)没有定义 任何构造函数
3)没有类内初始值
4)没有基类,也没有virtual函数

下面是个聚合类:

struct Data {
	int ival;
	string s;
};

特殊的初始化 语法形式:可以提供 一个花括号括起来的 成员初始值列表,并用它 初始化聚合类的数据成员

// val1.ival = 0; val1.s = string("Anna")
Data val1 = {0, "Anna"};

初始值的顺序 必须与 声明的顺序一致

与初始化数组的规则一样,如果 初始值列表中的元素数量 少于 类中的成员数量,则靠后的成员 被值初始化。初始值列表的元素个数 绝对不能超过 类的成员数量

2、显式地初始化 类的对象的成员 存在三个明显缺点:
1)要求类的所有成员 都是public的
2)将 正确初始化每个对象的每个成员的重任 交给了类的用户
3)添加或删除 一个成员后,所有的初始化语句 都要更新

5.6 字面值常量类

1、constexpr函数的参数和返回值 必须是 字面值类型。除了 算术类型、引用 和 指针外,某些类 也是字面值类型。字面值类型的类 可能含有constexpr函数成员。这样的成员 必须符合 constexpr 函数的所有要求,它们是隐式const的

数据成员 都是字面值类型的聚合类 是字面值常量类。如果 一个类不是聚合类,但它符合下述要求,则 它也是一个字面值常量类:
1)数据成员 都必须是 字面值类型(聚合类要求一致)
2)类必须 至少含有一个 constexpr 构造函数
3)如果 一个数据成员含有 类内初始值,则 内置类型成员的初始值 必须是一条 常量表达式;或者 如果成员属于 某种类类型,则 初始值必须使用 成员自己的constexpr构造函数
4)类 必须使用 析构函数的默认定义,该成员 负责销毁 类的对象

2、constexpr构造函数:尽管 构造函数不能是 const的,但是 字面值常量类的构造函数 可以是constexpr函数。一个字面值常量类 必须至少提供 一个constexpr构造函数

constexpr构造函数 可以声明成=default的形式(或者是 删除函数的形式)
constexpr构造函数 必须又符合构造函数的要求(意味着 不能包含返回语句),又符合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_hw(bool b) { hw = b; }
	void set_io(bool b) { io = b; }
	void set_other(bool b) { other = b; }

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

constexpr构造函数必须初始化 所有数据成员,初始值 或者使用constexpr构造函数,或者 是一条常量表达式

constexpr 构造函数用于生成 constexpr对象 以及constexpr函数的参数 或 返回类型

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;

Debug中以 set_ 开头的成员应该被声明成 constexpr 吗
在C++11中,constexpr函数时隐式的const,将不能更改数据成员

#include 
#include 
#include 

struct Data {
    int ival;
    std::string s;
};

int main()
{
    std::cout << std::boolalpha;
    std::cout << std::is_literal_type<Data>::value << std::endl;
    // output: false
}

Data 类是字面值常量类吗
不是,std::string不是字面值类型

附:字面值类型 与 字面值常量、常量表达式

摘自 字面值类型
1、字面值常量:一个形如42的值被称作字面值常量,这样的值一望而知。每个字面值常量都对应一种数据类型,字面值常量的形式和值决定了它的数据类型,包含:

1)整型和浮点型字面值
2)字符和字符串字面值
3)布尔字面值和指针字面值:bool test = false;nullptr是指针字面值;

2、常量表达式:指值不会改变并且在编译过程就能得到计算结果的表达式。很显然,字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。一个对象是不是常量表达式由它的数据类型和初始值共同决定

const int max_files = 20//常量表达式
const int limit = max_files + 1//limit是常量表达式
const int sz = get_size()//sz不是常量表达式,因为在编译期间不能得到sz的值,只有在运行时才能得到;

3、C++11规定,允许将变量声明为constexpr类型,以便由编译器来验证变量的值是不是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化

constexpr int mf = 20//20是常量表达式;
constexpr int limit = mf + 1//mf+1是常量表达式;
constexpr int sz = size() //只有当size()是一个constexpr函数时,才是一条正确的声明语句

4、字面值类型:常量表达式的值需要在编译时就得到计算,因此对声明constexpr时用到的类型必须有所限制。这些类型统称为字面值类型

算数类型、引用、指针是字面值类型

constexpr int a = 0//算数类型int是字面值类型;

某些类也是字面值类型,这些类叫做字面值常量类假设类Debug是字面值常量类。那么:

constexpr Debug debug(args)//生成一个constexpr对象-debug;

6、类的静态成员

1、类 需要它的成员 与 类本身直接相关,而不是 与类的各个对象 保持关联。比如 从实现效率的角度来看,没必要 每个对象都存储利率信息。一旦 利率浮动,希望所有的对象 都能使用新值

2、声明静态成员:成员的声明之前 加上关键字static使其 与类关联在一起。和其他成员一样,静态成员可以是public的或private的。静态数据成员的类型 可以是常量、引用、指针、类类型等

例如:定义一个类,用它表示 银行的账户记录

class Account {
public:
	void calculate() { amount += amount * interestRate; }
	static double rate() { return interestRate; } // 静态成员
	static void rate(double); // 静态成员
private:
	std::string owner;
	double amount;
	static double interestRate; // 静态成员
	static double initRate();
};

类的静态成员 存在于 任何对象之外,对象中 不包含任何与 静态数据成员有关的数据
每个 Account对象 将包含两个数据成员:owner和amount。只存在一个interestRate对象 并且 它被所有Account对象共享

静态成员函数 也不与 任何对象绑定在一起,它们 不包含this指针。作为结果,静态成员函数 不能声明成 const,也不能在static函数体内部 使用this指针。这一限制 既适用于this的显式使用,也对 调用非静态成员的 隐式使用有效

3、使用类的静态成员:使用作用域运算符 直接访问静态成员

double r;
r = Account::rate();

静态成员 不属于 类的某个对象,仍然可以 使用类的对象、引用 或 指针来访问静态成员

Account ac1;
Account *ac2 = &ac1;

// 调用 静态成员函数 rate的等价形式
r = ac1.rate(); // 通过Account的对象或引用
r = ac2->rate(); // 通过指向Account对象的指针

成员函数 不用通过作用域运算符 就能直接 使用静态成员

class Account {
public:
	void calculate() { amount += amount * interestRate; } // interestRate是静态变量
private:
	static double interestRate;
};

4、定义 静态成员,和其他的成员函数 一样,既可以在 类的内部 也可以在 类的外部 定义静态成员函数。当在类的外部 定义静态成员时,不能重复 static关键字,该关键字 只出现在类内部的声明语句

void Account::rate(double newRate)
{
	interestRate = newRate;
}

和类的所有成员一样,当我们 指向类外部的静态成员时,必须指明 成员所属的类名

5、静态数据成员 不属于 类的任何一个对象,所以 它们并不是在创建类的对象时 被定义的。意味着 它们不是由类的构造函数 初始化的。一般来说,不能在类的内部 初始化静态成员。必须在 类的外部定义和初始化 每个静态成员。和其他对象一样,一个 静态数据成员 只能定义一次

类似于 全局变量,静态数据成员 定义在 任何函数之外。因此 一旦它被定义,就将一直 存在于程序的整个生命周期中
定义静态成员函数的方式 和 在类的外部定义成员函数一样。需要 指定对象的类型名,然后是 类名、作用域运算符 以及 成员自己的名字

// 定义并初始化 一个静态成员函数
double Account::interestRate = initRate();

这条语句 定义了名为 interestRate 的对象,该对象 是类Account的 静态成员。从类名开始,这条定义语句的剩余部分 就都位于 类的作用域之内了,因此 可以直接使用initRate函数。虽然 initRate是私有的,也能用它初始化 interestRate。和 其他成员的定义一样,interestRate的定义 也可以访问类的私有成员

要想确保对象 只定义一次,最好的办法 是把静态数据成员的定义 与 其他非内联函数的定义 放在同一个文件中

6、静态成员的 类内初始化:通常情况下,类的静态成员 不应该在类的内部 初始化。然而,我们可以为 静态成员提供const整数类型的 类内初始值,不过 要求静态成员必须是 字面值常量类型的 constexpr。初始值 必须是 常量表达式,因为 这些成员本身 就是常量表达式,所以 它们能用在 所有适合于 常量表达式的地方
可以用一个 初始化了的 静态数据成员 指定数组成员的维度

class Account {
public:
	static double rate() { return interestRate; }
	static void rate(double);
private:
	static constexpr int period = 30; // period是常量表达式
	double daily_tbl[period];
};

如果 某个静态成员的应用场景 仅限于编译器可以替换它的值的情况,则 一个初始化的const或constexpr static 不需要分别定义。如果将它用于 值不能替换的场景中,则 该成员必须有一条 定义语句

例:如果 period的唯一用途就是 定义daily_tbl的维度,则 不需要在Account外面 专门定义period。如果 我们忽略了这条定义,那么对程序非常微小的改动 也可能造成 编译错误,因为 程序找不到该成员的定义语句。当需要把Account::period传递给 一个接受const int&的函数时,必须定义period(类内的初始化 外面看不到)

如果在 类的内部 提供了一个初始值,则成员的定义 不能再指定 一个初始值(常量不能多次赋值)

// 一个不带初始值的静态成员的定义
constexpr int Account::period; // 初始值在类的定义内 提供

即使 一个常量静态数据成员在 类内部 被初始化了,通常情况下 也应该在类的外部定义一下 该成员

7、静态成员能用于 某些场景,而普通成员不能:静态成员 独立于 任何对象(静态 与 常量别混了),因此 在某些非静态数据成员 可能非法的场合,静态成员 却可以正常使用
静态数据成员 可以是 不完全类型。静态数据成员的类型 可以就是 它所属的类类型。而非 静态数据成员 则受到限制,只能声明成 它所属类的指针 或 引用

class Bar {
public:
	// ...
private:
	static Bar mem1; // 正确:静态成员可以是 不完全类型
	Bar *mem2; // 正确:指针成员可以是 不完全类型
	Bar mem3; // 错误:数据成员 必须是 完全类型
};

静态成员 和 普通成员的 另外一个区别是:可以使用 静态成员 作为 默认实参

class Screen {
public:
	// bkground 表示一个在类中 稍后定义的 静态成员
	Screen& clear(char = bkground);
private:
	static const char bkground;
};

非静态数据成员 不能作为 默认实参,因为它的值本身属于 对象的一部分,这么做的结果 是无法真正提供一个对象 以便从中 获取成员的值,最终将引发错误

8、静态成员总结
什么是类的静态成员?它有何优点?静态成员与普通成员有何区别
类的静态成员与类本身直接相关,而不是与类的各个对象保持关联
每个对象不需要存储公共数据,如果数据被改变,则每个对象都可以使用新值

静态数据成员可以是不完全类型;可以使用静态成员作为默认实参

9、完整的Account类的实现

#ifndef _ACCOUNT_H_
#define _ACCOUNT_H_

#include 

class Account
{
public:
	void calculate() {amount += amount * interestRate;}
	static double rate() { return interestRate; }
	static void rate(double newRate) { interestRate = newRate; }
private:
	std::string owner;
	double amount;
	static double interestRate;
	static double initRate() { return 4.0; }
};
double Account::interestRate = initRate();

#endif

10、静态成员的用法:下面的静态数据成员的声明和定义有错误吗

//example.h
class Example {
public:
    static double rate = 6.5;
    static const int vecSize = 20;
    static vector<double> vec(vecSize);
};
//example.c
#include "example.h"
double Example::rate;
vector<double> Example::vec;

在类的内部,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

附:static修饰全局变量

摘自 C/C++ static关键字详解
1、全局变量的作用域十分的广,只要在一个源文件中定义后,这个程序中的所有源文件、对象以及函数都可以调用,生命周期更是贯穿整个程序。文件中的全局变量想要被另一个文件使用时就需要进行外部声明(以下用extern关键字进行声明)。即 全局变量既可以在源文件中使用,也可以在其他文件中使用(只需要使用extern外部链接以下即可)
C++ Primer 总结索引 | 第七章:类_第5张图片

2、static修饰全局变量和函数时,会改变全局变量和函数的链接属性-------变为只能在内部链接,从而使得全局变量的作用域变小

术语表

不完全类型:已经声明但是 尚未定义的类型。不完全类型 不能用于 定义变量或者类的成员,但是用不完全类型 定义指针 或者 引用是合法的

你可能感兴趣的:(C++,Primer,c++,开发语言)