IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> Cpp Primer | 第7章 : 类 (笔记+练习) -> 正文阅读

[C++知识库]Cpp Primer | 第7章 : 类 (笔记+练习)

7.1 定义抽象数据类型

  • 类的基本思想:数据抽象 和 封装。
  • 数据抽象是一种依赖于接口和实现分离的编程技术。

类成员

  • 必须在类的内部声明,不能在其他地方增加成员。
  • 成员可以是数据,函数,类型别名。

类的成员函数

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

伪代码:说明隐式的this指针是如何使用的

下面的代码是非法的:因为我们不能显示地定义自己的this指针, 在Sales_data成员函数中,this的类型是Sales_data *const

因为isbn是一个常量成员,此处的this是一个指向常量的指针

std::string Sales_data::isbn(const Sales_data *const this)
{
	return this->isbn;
}
  • 成员函数的声明必须在类的内部
  • 定义既可以在类的内部也可以在类的外部(定义在类内部的函数是隐式的)
  • 使用点运算符.来访问成员函数
  • 引入this
    • total.isbn()当isbn返回bookNo时,实际上它隐式的返回total.bookNo。成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象。
    • 当调用一个成员函数时,用请求该函数的对象地址初始化this 伪代码:Sales_data::isbn(&total)
    • 可以在成员函数体内使用this,尽管没有必要,但是能把isbn定义成:std::string isbn() const {return this->bookNo}
  • const作用:修改隐式this指针的类型。紧跟在参数列表后面的const表示this是一个指向常量的指针 —— 使用const的成员函数被称为 常量成员函数
    • const成员函数:this是指向const类类型的const指针(既不能改变this所指向的值,也不能改变this保存的地址)。
    • 普通的非const成员函数:this是指向类类型的const指针(可以改变this所指向的值,不能改变this保存的地址)。

非成员函数

  • 和类相关的非成员函数,定义和声明都应该在类的外部。

  • 一般来说,如果非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个头文件中。

类的构造函数

  • 类通过一个或几个特殊的成员函数来控制其对象的 初始化过程,这些函数叫做 构造函数
  • 构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。
  • 构造函数名字和类名相同,没有返回类型。
  • 构造函数放在类的public部分
  • = default要求编译器合成默认的构造函数。(C++11)
  • 初始化列表:冒号和花括号之间的代码:Sales_item(): units_sold(0), revenue(0.0) { }
  • IO属于不能被拷贝的类型,因此只能通过引用来传递他们

7.2 访问控制与封装

  • 访问说明符 —— 加强类的封装性
    • public:定义在public后面的成员在整个程序内可以被访问;public成员定义类的接口
    • private:定义在private后面的成员可以被类的成员函数访问,但不能被使用该类的代码访问;private隐藏了类的实现细节。
  • 使用classstruct:都可以被用于定义一个类,唯一的区别在于默认访问权限。
    • 使用class:在第一个访问说明符之前的成员是private的。
    • 使用struct:在第一个访问说明符之前的成员是public的。

友元

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

  • 允许特定的 非成员函数访问一个类的私有成员。
  • 友元的声明以关键字friend开始, friend Sales_data add(const Sales_data&, const Sales_data&);表示非成员函数add可以访问类的非公有成员。
  • 通常将友元声明成组地放在 类定义的开始或者结尾

封装的益处

  • 确保用户的代码不会无意间破坏封装对象的状态。
  • 被封装的类的具体实现细节可以随时改变,而无需调整用户级别的代码。

7.3 类的其他特性

  • 成员函数作为内联函数 inline
    • 在类的内部,常有一些规模较小的函数适合于被声明成内联函数。
    • 定义在类内部的函数是自动内联的。
    • 在类外部定义的成员函数,也可以在声明时显式地加上 inline
  • 可变成员函数(mutable data member)
    • 在变量的声明中加入mutable关键字:mutable data access_ctr;
    • 可变数据成员永远不会是const,即使他是const对象的成员
  • 每个类确定了唯一的类型。对于两个类来说,即使它们的成员完全一样,这两个类也是不同的类型。

7.4 类的作用域

  • 每个类都会定义自己的作用域。在类的作用域之外,普通的数据和函数成员只能由引用、对象、指针使用成员访问运算符来访问。
  • 函数的 返回类型通常在函数名之前,因此当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外,这时,返回类型必须指明他是那个类的成员。
  • 如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字。
  • 类中的类型名定义都要放在一开始。

7.5 构造函数再探

构造函数的初始化列表

  • 如果成员是 const或者是引用的话,必须将其初始化。只能初始化,不能赋值(注意初始化和赋值的区别)
    • 初始化:直接初始化数据成员
    • 赋值:先初始化再赋值
  • **最好令函数初始值的顺序与成员声明的顺序保持一致。**构造函数初始值只说明用于初始化成员的
  • 如果一个构造函数为所有参数都提供了默认参数,那么它实际上也定义了默认的构造函数。

委托构造函数

  • 委托构造函数将自己的职责委托给了其他构造函数
  • Sales_data(): Sales_data(" ", 0, 0) { }

只有当一个类没有定义 任何构造函数的时候,编译器才会生成一个默认构造函数。

隐式的类类型转换(转换构造函数)

  • 如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制。这种构造函数又叫转换构造函数(converting constructor)。

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

  • 只允许 一步类类型转换

  • 抑制构造函数定义的隐式转换

    • 将构造函数声明为 explicit加以阻止
    • explicit关键字只允许出现在类内的构造函数声明处(只对一个实参的构造函数有效)
    • explicit构造函数只能用于直接初始化,不能将explicit构造函数用于拷贝形式的初始化过程。
Sales_data item1(null_book);   //正确,直接初始化
Sales_data item2 = null_book;  
//错误:不能将 explicit 构造函数用于拷贝形式的初始化过程
  • 尽管编译器不会将explicit的构造函数用于隐式转换过程,但是我们可以使用这样的构造函数 显式地强制进行转换。

聚合类(aggregate class)

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

  • 满足以下所有条件:

    • 所有成员都是 public
    • 没有定义任何构造函数
    • 没有类内初始值
    • 没有基类,也没有virtual函数
  • 可以使用一个花括号括起来的成员初始化列表,并用它初始化聚合类的数据成员。初始值的顺序必须与声明的顺序一致。

字面值常量类

  • constexpr函数的参数和返回值必须是字面值类型
  • 字面值类型:除了算术类型、引用和指针外,某些类也是字面值类型
  • 数据成员都是字面值类型的聚合类是字面值常量类。
  • 如果不是聚合类,必须满足下面所有条件
    • 数据成员都必须是字面值类型。
    • 类必须至少含有一个constexpr构造函数。
    • 如果一个数据成员含有类内部初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数。
    • 类必须使用析构函数的默认定义,该成员负责销毁类的对象。

7.6 类的静态成员

  • static数据成员存在于类类型的每个对象中。
  • static数据成员独立于该类的任意对象而存在。
  • 每个static数据成员是与类关联的对象,并不与该类的对象相关联。
  • 声明:
    • 在声明之前加上关键字static
  • 使用:
    • 使用 作用域运算符 :: 直接访问静态成员 r = Account::rate()
    • 可以使用类的对象或引用访问:r = ac1.rate()使用指针访问:r = ac2->rate()
  • 定义:
    • 在类的外部定义静态成员时,不能重复static关键字,该关键字只出现在类内部的声明语句
    • 在类外部定义时不用加static
  • 初始化:
    • 通常不在类的内部初始化,而是在定义时进行初始化,如:double Account::interestRate = initRate();
    • 如果一定要在类内定义,则要求必须是字面值常量类型的constexpr

练习

练习7.1

使用2.6.1节定义的Sales_data类为1.6节的交易处理程序编写一个新版本。

解:

#include <iostream>
#include <string>
using std::cin; using std::cout; using std::endl; using std::string;

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

int main()
{
    Sales_data total;
    if (cin >> total.bookNo >> total.units_sold >> total.revenue)
    {
        Sales_data trans;
        while (cin >> trans.bookNo >> trans.units_sold >> trans.revenue) 
        {
            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;
            }
        }
        cout << total.bookNo << " " << total.units_sold << " " << total.revenue << endl;
    }
    else
    {
        std::cerr << "No data?!" << std::endl;
        return -1;
    }
    return 0;
}

练习7.2

曾在2.6.2节的练习中编写了一个Sales_data类,请向这个类添加combine函数和isbn成员。

解:

#include <string>

struct Sales_data {
    std::string isbn() const { return bookNo; };
    Sales_data& combine(const Sales_data&);
    
    std::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

修改7.1.1节的交易处理程序,令其使用这些成员。

解:

#include <iostream>
using std::cin; using std::cout; using std::endl;

int main()
{
    Sales_data total;
    if (cin >> total.bookNo >> total.units_sold >> total.revenue)
    {
        Sales_data trans;
        while (cin >> trans.bookNo >> trans.units_sold >> trans.revenue) {
            if (total.isbn() == trans.isbn())
                total.combine(trans);
            else {
                cout << total.bookNo << " " << total.units_sold << " " << total.revenue << endl;
                total = trans;
            }
        }
        cout << total.bookNo << " " << total.units_sold << " " << total.revenue << endl;
    }
    else
    {
        std::cerr << "No data?!" << std::endl;
        return -1;
    }
    return 0;
}

练习7.4

编写一个名为Person的类,使其表示人员的姓名和地址。使用string对象存放这些元素,接下来的练习将不断充实这个类的其他特征。

解:

#include <string>

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

练习7.5

在你的Person类中提供一些操作使其能够返回姓名和地址。 这些函数是否应该是const的呢?解释原因。

解:

#include <string>

class Person 
{
    std::string name;
    std::string address;
public:
    auto get_name() const -> std::string const& { return name; }
    auto get_addr() const -> std::string const& { return address; }
};

应该是const的。因为常量的Person对象也需要使用这些函数操作。

练习7.6

对于函数addreadprint,定义你自己的版本。

解:

#include <string>
#include <iostream>

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

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

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

// nonmember functions
std::istream &read(std::istream &is, Sales_data &item)
{
    double price = 0;
    is >> item.bookNo >> item.units_sold >> price;
    item.revenue = price * item.units_sold;
    return is;
}

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

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

练习7.7

使用这些新函数重写7.1.2节练习中的程序。

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

练习7.8

为什么read函数将其Sales_data参数定义成普通的引用,而print函数将其参数定义成常量引用?

解:

因为read函数会改变对象的内容,而print函数不会。

练习7.9

对于7.1.2节练习中代码,添加读取和打印Person对象的操作。

#include <string>
#include <iostream>

struct Person 
{
    std::string const& getName()    const { return name; }
    std::string const& getAddress() const { return address; }
    
    std::string name;
    std::string address;
};

std::istream &read(std::istream &is, Person &person)
{
    return is >> person.name >> person.address;
}

std::ostream &print(std::ostream &os, const Person &person)
{
    return os << person.name << " " << person.address;
}

练习7.10

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

if (read(read(cin, data1), data2)) //等价read(std::cin, data1);read(std::cin, data2);

解:

read函数的返回值是istream对象, if语句中条件部分的作用是从输入流中读取数据给两个data对象。

练习7.11

在你的Sales_data类中添加构造函数, 然后编写一段程序令其用到每个构造函数。

解:

头文件

#include <string>
#include <iostream>

struct Sales_data {
    Sales_data() = default;
    Sales_data(const std::string &s):bookNo(s) { }
    Sales_data(const std::string &s, unsigned n, double p):bookNo(s), units_sold(n), revenue(n*p){ }
    Sales_data(std::istream &is);
    
    std::string isbn() const { return bookNo; };
    Sales_data& combine(const Sales_data&);
    
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

// nonmember functions
std::istream &read(std::istream &is, Sales_data &item)
{
    double price = 0;
    is >> item.bookNo >> item.units_sold >> price;
    item.revenue = price * item.units_sold;
    return is;
}

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

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

// member functions.
Sales_data::Sales_data(std::istream &is)
{
    read(is, *this);
}

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

主函数

int main()
{
    Sales_data item1;
    print(std::cout, item1) << std::endl;
    
    Sales_data item2("0-201-78345-X");
    print(std::cout, item2) << std::endl;
    
    Sales_data item3("0-201-78345-X", 3, 20.00);
    print(std::cout, item3) << std::endl;
    
    Sales_data item4(std::cin);
    print(std::cout, item4) << std::endl;
    
    return 0;
}

练习7.12

把只接受一个istream作为参数的构造函数移到类的内部。

解:

#include <string>
#include <iostream>

struct Sales_data;
std::istream &read(std::istream&, Sales_data&);

struct Sales_data {
    Sales_data() = default;
    Sales_data(const std::string &s):bookNo(s) { }
    Sales_data(const std::string &s, unsigned n, double p):bookNo(s), units_sold(n), revenue(n*p){ }
    Sales_data(std::istream &is) { read(is, *this); }
    
    std::string isbn() const { return bookNo; };
    Sales_data& combine(const Sales_data&);
    
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

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

// nonmember functions
std::istream &read(std::istream &is, Sales_data &item)
{
    double price = 0;
    is >> item.bookNo >> item.units_sold >> price;
    item.revenue = price * item.units_sold;
    return is;
}

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

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

练习7.13

使用istream构造函数重写第229页的程序。

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

练习7.14

编写一个构造函数,令其用我们提供的类内初始值显式地初始化成员。

Sales_data() : units_sold(0) , revenue(0) { }

练习7.15

为你的Person类添加正确的构造函数。

#include <string>
#include <iostream>

struct Person;
std::istream &read(std::istream&, Person&);

struct Person
{
	Person() = default;
	Person(const std::string& sname, const std::string& saddr) :name(sname), address(saddr) {}
	Person(std::istream &is) { read(is, *this); }

	std::string getName() const { return name; }
	std::string getAddress() const { return address; }

	std::string name;
	std::string address;
};

std::istream &read(std::istream &is, Person &person)
{
	is >> person.name >> person.address;
	return is;
}

std::ostream &print(std::ostream &os, const Person &person)
{
	os << person.name << " " << person.address;
	return os;
}

练习7.16

在类的定义中对于访问说明符出现的位置和次数有限定吗? 如果有,是什么?什么样的成员应该定义在public说明符之后? 什么样的成员应该定义在private说明符之后?

解:

在类的定义中对于访问说明符出现的位置和次数没有限定

每个访问说明符指定了接下来的成员的访问级别,其有效范围直到出现下一个访问说明符或者达到类的结尾处为止。

如果某个成员能够在整个程序内都被访问,那么它应该定义为public; 如果某个成员只能在类内部访问,那么它应该定义为private

练习7.17

使用classstruct时有区别吗?如果有,是什么?

解:

classstruct的唯一区别是默认的访问级别不同。

练习7.18

封装是何含义?它有什么用处?

将类内部分成员设置为外部不可见,而提供部分接口给外面,这样的行为叫做封装

用处:

  • 1.确保用户的代码不会无意间破坏封装对象的状态。
  • 2.被封装的类的具体实现细节可以随时改变,而无需调整用户级别的代码。

练习7.19

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

构造函数、getName()getAddress()函数将设为publicnameaddress 将设为private。 函数是暴露给外部的接口,因此要设为public; 而数据则应该隐藏让外部不可见。

练习7.21

修改你的Sales_data类使其隐藏实现的细节。 你之前编写的关于Sales_data操作的程序应该继续使用,借助类的新定义重新编译该程序,确保其正常工作。

#include <string>
#include <iostream>

class Sales_data {
    friend std::istream &read(std::istream &is, Sales_data &item);
    friend std::ostream &print(std::ostream &os, const Sales_data &item);
    friend Sales_data add(const Sales_data &lhs, const Sales_data &rhs);

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

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

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

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

// friend functions
std::istream &read(std::istream &is, Sales_data &item)
{
    double price = 0;
    is >> item.bookNo >> item.units_sold >> price;
    item.revenue = price * item.units_sold;
    return is;
}

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

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

练习7.22

修改你的Person类使其隐藏实现的细节。

#include <string>
#include <iostream>

class Person {
    friend std::istream &read(std::istream &is, Person &person);
    friend std::ostream &print(std::ostream &os, const Person &person);

public:
    Person() = default;
    Person(const std::string sname, const std::string saddr):name(sname), address(saddr){ }
    Person(std::istream &is){ read(is, *this); }

    std::string getName() const { return name; }
    std::string getAddress() const { return address; }
private:
    std::string name;
    std::string address;
};

std::istream &read(std::istream &is, Person &person)
{
    is >> person.name >> person.address;
    return is;
}

std::ostream &print(std::ostream &os, const Person &person)
{
    os << person.name << " " << person.address;
    return os;
}

练习7.23

编写你自己的Screen类型。

#include <string>

class Screen {
    public:
        using pos = std::string::size_type;

        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 r, pos c) const { return contents[r*width+c]; }

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

练习7.24

给你的Screen类添加三个构造函数:一个默认构造函数;另一个构造函数接受宽和高的值,然后将contents初始化成给定数量的空白;第三个构造函数接受宽和高的值以及一个字符,该字符作为初始化后屏幕的内容。

#include <string>

class Screen {
    public:
        using pos = std::string::size_type;

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

        char get() const { return contents[cursor]; }
        char get(pos r, pos c) const { return contents[r*width+c]; }

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

练习7.27

给你自己的Screen类添加movesetdisplay函数,通过执行下面的代码检验你的类是否正确。

Screen myScreen(5, 5, 'X');
myScreen.move(4, 0).set('#').display(cout);
cout << "\n";
myScreen.display(cout);
cout << "\n";

解:

增加代码:

#include <string>
#include <iostream>

class Screen {
public:
    ... ...

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

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

private:
    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;
}

测试代码:

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

练习7.28

如果movesetdisplay函数的返回类型不是Screen& 而是Screen,则在上一个练习中将会发生什么?

解:

如果返回类型是Screen,那么move返回的是*this的一个副本,因此set函数只能改变临时副本而不能改变myScreen的值。

练习7.29

修改你的Screen类,令movesetdisplay函数返回Screen并检查程序的运行结果,在上一个练习中你的推测正确吗?

解:

推测正确。

#with '&'
XXXXXXXXXXXXXXXXXXXX#XXXX
XXXXXXXXXXXXXXXXXXXX#XXXX
                    ^
# without '&'
XXXXXXXXXXXXXXXXXXXX#XXXX
XXXXXXXXXXXXXXXXXXXXXXXXX
                    ^

练习7.31

定义一对类XY,其中X包含一个指向Y的指针,而Y包含一个类型为X的对象。

class Y;

class X {
	Y* y = nullptr;
};

class Y {
	X x;
};

练习7.32

定义你自己的ScreenWindow_mgr,其中clearWindow_mgr的成员,是Screen的友元。

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

class Screen;

class Window_mgr {
public:
	using ScreenIndex = std::vector<Screen>::size_type;
	//按照编号将指定的Screen置为空白
	void clear(ScreenIndex); 
private:
	std::vector<Screen> screen;
};

class Screen {
	friend void Window_mgr::clear(ScreenIndex);

public:
	using pos = std::string::size_type;

	//构造函数
	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){ }
	
	inline Screen& move(pos r, pos c);
	inline Screen& set(char c);
	inline Screen& set(pos r, pos c, char ch);

	const Screen& display(std::ostream& os) const { do_display(os); return *this; }
	Screen& display(std::ostream& os) { do_display(os); return *this; }
private:
	pos cursor = 0;
	pos height = 0, width = 0;
	std::string contents;

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

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

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

练习7.34

如果我们把第256页Screen类的postypedef放在类的最后一行会发生什么情况?

解:

在 dummy_fcn(pos height) 函数中会出现 未定义的标识符pos。

类型名的定义通常出现在类的开始处,这样就能确保所有使用该类型的成员都出现在类名的定义之后。

练习7.38

有些情况下我们希望提供cin作为接受istream&参数的构造函数的默认实参,请声明这样的构造函数。

解:

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

练习7.41

使用委托构造函数重新编写你的Sales_data类,给每个构造函数体添加一条语句,令其一旦执行就打印一条信息。用各种可能的方式分别创建Sales_data对象,认真研究每次输出的信息直到你确实理解了委托构造函数的执行顺序。

头文件

#ifndef CP5_ex7_41_h
#define CP5_ex7_41_h

#include <string>
#include <iostream>

class Sales_data {
    friend std::istream &read(std::istream &is, Sales_data &item);
    friend std::ostream &print(std::ostream &os, const Sales_data &item);
    friend Sales_data add(const Sales_data &lhs, const Sales_data &rhs);

public:
    Sales_data(const std::string &s, unsigned n, double p):bookNo(s), units_sold(n), revenue(n*p)
    { std::cout << "Sales_data(const std::string&, unsigned, double)" << std::endl; }
    
    Sales_data() : Sales_data("", 0, 0.0f)
    { std::cout << "Sales_data()" << std::endl; }
    
    Sales_data(const std::string &s) : Sales_data(s, 0, 0.0f)
    { std::cout << "Sales_data(const std::string&)" << std::endl; }
    
    Sales_data(std::istream &is);

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

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

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

// declarations for nonmember parts of the Sales_data interface.
std::istream &read(std::istream &is, Sales_data &item);
std::ostream &print(std::ostream &os, const Sales_data &item);
Sales_data add(const Sales_data &lhs, const Sales_data &rhs);

#endif

源文件

#include "ex_7_41.h"

// constructor
Sales_data::Sales_data(std::istream &is) : Sales_data()
{
    std::cout << "Sales_data(istream &is)" << std::endl;
    read(is, *this);
}

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

// friend functions
std::istream &read(std::istream &is, Sales_data &item)
{
    double price = 0;
    is >> item.bookNo >> item.units_sold >> price;
    item.revenue = price * item.units_sold;
    return is;
}

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

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

主函数

#include "ex_7_41.h"
using std::cout; using std::endl;

int main()
{
    cout << "1. default way: " << endl;
    cout << "----------------" << endl;
    Sales_data s1;   
    
    cout << "\n2. use std::string as parameter: " << endl;
    cout << "----------------" << endl;
    Sales_data s2("CPP-Primer-5th");
    
    cout << "\n3. complete parameters: " << endl;
    cout << "----------------" << endl;
    Sales_data s3("CPP-Primer-5th", 3, 25.8);
    
    cout << "\n4. use istream as parameter: " << endl;
    cout << "----------------" << endl;
    Sales_data s4(std::cin);
    
    return 0;
}

输出

1. default way:
----------------
Sales_data(const string& s, unsigned n, double p)
Sales_data()

2. use std::string as parameter:
----------------
Sales_data(const string& s, unsigned n, double p)
Sales_data(const string& s)

3. complete parameters:
----------------
Sales_data(const string& s, unsigned n, double p)

4. use istream as parameter:
----------------
Sales_data(const string& s, unsigned n, double p)
Sales_data()
Sales_data(istream& is)

总结:使用委托构造函数,调用顺序是:

  • 1.实际的构造函数的函数体。
  • 2.委托构造函数的函数体。

练习7.43

假定有一个名为NoDefault的类,它有一个接受int的构造函数,但是没有默认构造函数。定义类CC有一个 NoDefault类型的成员,定义C的默认构造函数。

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

class C {
public:
    C() : def(0) { } 
private:
    NoDefault def;
};

练习7.47

说明接受一个string参数的Sales_data构造函数是否应该是explicit的,并解释这样做的优缺点。

解:

是否需要从stringSales_data的转换依赖于我们对用户使用该转换的看法。在此例中,这种转换可能是对的。null_book中的string可能表示了一个不存在的ISBN编号。

优点:

可以抑制构造函数定义的隐式转换

缺点:

为了转换要显式地使用构造函数

练习7.48

假定Sales_data的构造函数不是explicit的,则下述定义将执行什么样的操作?

解:

string null_isbn("9-999-9999-9");
Sales_data item1(null_isbn);
Sales_data item2("9-999-99999-9");

这些定义和是不是explicit的无关。

练习7.49

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

解:

(a) Sales_data &combine(Sales_data); // ok
(b) Sales_data &combine(Sales_data&); // error C2664: 无法将参数 1 从“std::string”转换为“Sales_data &”	因为隐式转换只有一次
(c) Sales_data &combine(const Sales_data&) const; // 该成员函数是const 的,意味着不能改变对象。而 combine函数的本意就是要改变对象

练习7.50

确定在你的Person类中是否有一些构造函数应该是explicit 的。

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

练习7.51

vector将其单参数的构造函数定义成explicit的,而string则不是,你觉得原因何在?

假如我们有一个这样的函数:

int getSize(const std::vector<int>&);

如果vector没有将单参数构造函数定义成explicit的,我们就可以这样调用:

getSize(34);

很明显这样调用会让人困惑,函数实际上会初始化一个拥有34个元素的vector的临时量,然后返回34。但是这样没有任何意义。而string则不同,string的单参数构造函数的参数是const char *,因此凡是在需要用到string的地方都可以用const char *来代替(字面值就是const char *)。如:

void print(std::string);
print("hello world");

练习7.52

使用2.6.1节的 Sales_data 类,解释下面的初始化过程。如果存在问题,尝试修改它。

Sales_data item = {"987-0590353403", 25, 15.99};

解:

Sales_data 类不是聚合类,应该修改成如下:

struct Sales_data {
    std::string bookNo;
    unsigned units_sold;
    double revenue;
};

练习7.53

定义你自己的Debug

解:

class Debug {
public:
    constexpr Debug(bool b = true) : hw(b), io(b), other(b) { }
    constexpr Debug(bool h, bool i, bool o) : hw(r), io(i), other(0) { }

    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;        // runtime error
    bool io;        // I/O error
    bool other;     // the others
};

练习7.54

Debug中以 set_ 开头的成员应该被声明成constexpr 吗?如果不,为什么?

解:

不能。constexpr函数必须包含一个返回语句

练习7.55

7.5.5节的Data类是字面值常量类吗?请解释原因。

解:

不是。因为std::string不是字面值类型。

练习7.57

编写你自己的Account类。

解:

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 constexpr double todayRate = 42.42;
    static double initRate() { return todayRate; }
};

double Account::interestRate = initRate();

练习7.58

下面的静态数据成员的声明和定义有错误吗?请解释原因。

//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.h
class Example {
public:
    static constexpr double rate = 6.5;
    static const int vecSize = 20;
    static vector<double> vec;
};

// example.C
#include "example.h"
constexpr double Example::rate;
vector<double> Example::vec(Example::vecSize);
  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2021-09-07 10:38:51  更:2021-09-07 10:40:15 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/27 11:57:57-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码