Skip to content

面向对象编程基础

本课程入选教育部产学合作协同育人项目 课程主页:http://cpp.njuer.org 课程老师:陈明 http://cv.mchen.org

ppt和代码下载地址
git clone https://gitee.com/cpp-njuer-org/book

第7章

面向对象编程思想

C++之父Bjarne Stroustrup(摄影 陈明@南京大学 2016) cppFather

发展历史

historty - 打好基础很重要 - 兴趣是最好的老师

怎么把大象装进冰箱

大象

怎么把大象装进冰箱

第一步冰箱门打开
第二步大象装进去
第三步冰箱门关上

怎么把大象装进冰箱:面向过程的思想

分三步
第一步冰箱门打开
第二步大象装进去
第三步冰箱门关上

伪代码示例:
冰箱门打开();
大象装进去();
冰箱门关上();

虚拟实验室:阿里云SHELL界面

https://developer.aliyun.com/adc/scenario/3b3aac5d2991467cafefb3a3a5a1f603

虚拟实验室:阿里云IDE界面

https://developer.aliyun.com/adc/scenario/ea37002044c0427988bfaa4746ba2908

把大象装进冰箱 :面向对象的思想

分析问题问题中包含大象类和冰箱类
- 大象类包括大象的数据和操作进入)。
- 冰箱类包括冰箱数据和操作方法(开门关门装进去)接下来
- 用大象类和冰箱类分别生成具体的大象对象和冰箱对象
- 冰箱对象调用 门打开 方法
- 冰箱对象调用装进去方法参数是 大象对象
    - 或者 大象对象调用进入方法参数是冰箱”。
- 冰箱对象调用门关上方法

伪代码示例: 
- 用大象类和冰箱类分别生成具体的大象对象和冰箱对象
- 冰箱.门打开();
- 冰箱. 装进去大象);或者 大象.进入(冰箱);
- 冰箱.门关上();

面向过程和面向对象

面向过程    
- 分析出问题要解决的步骤
- 关注该怎么做
    - 第一步
    - 第二步
    - 
- 特点数据和对数据的操作相互独立数据的表示是公开的

面向对象
- 分析出问题万物皆可对象 找出类与对象
- 关注 类与对象
    - 构造类
    - 生成对象
    - 对象调用方法发消息
- 特点数据和对数据的操作构成了一个整体只能通过提供的函数来操作数据

面向对象三大特征:封装

- 封装是把数据和数据的操作作为一个整体来定义内部具体表示被隐藏起来
    - 对数据的访问只能通过封装体对外提供的操作来进行
- 程序由若干对象组成每个对象由一些数据以及对这些数据所能实施的操作构成
- 把大象装进冰箱:
    - 问题包含大象对象和冰箱对象
    - 大象对象由一些数据和对数据的操作构成
    - 大象类封装了大象的全部数据与操作
    - 冰箱类封装了冰箱的全部数据和操作
    - 只能通过大象类的方法来操作大象对象
    - 只能通过冰箱类的方法来操作冰箱对象

面向对象三大特征:继承

- 继承是指定义一个类时可以利用已有类的一些特征描述
    - 子类除了包含父类的特征可拥有新的特征也可对父类的特征重新定义
    - 子类与父类往往存在特殊与一般的关系
        - 单继承中一个类最多一个父类多继承中一个类可以有多个直接父类

- 把非洲大象装进海尔冰箱:
    - 非洲大象是特殊的大象
    - 海尔冰箱是特殊的冰箱
    - 非洲大象类继承了大象类的全部数据与操作
    - 非洲大象不需要重写大象类的功能
    - 可以给非洲大象类增加新功能也可重新定义大象功能

面向对象三大特征:多态

- 多态性是指一个元素存在多种解释体现为
    - 一名多用重载
    - 类属性一个程序实体能对多种类型数据进行操作或描述的特性)。
- 把非洲大象装进海尔冰箱:
    - 非洲大象属于大象
    - 大象的引用\指针可以引用\指向大象对象也可以引用\指向非洲大象对象
    - 发给大象的消息也能发给非洲大象但它们会有不同处理
    - 可以复用代码 大象.进入冰箱)。

总结:面向对象编程思想

- 把程序构造成由若干对象组成
    - 每个对象由一些数据以及对这些数据所能实施的操作构成
- 对数据的操作是通过向包含数据的对象发送消息调用对象的操作来实现
- 对象的特征数据与操作由相应的类来描述
- 一个类所描述的对象特征可以从其它的类获得继承)。

面向对象三大特征封装继承多态

定义抽象数据类型

定义抽象数据类型

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

设计Sales_data类

Sales_data的接口包含
- isbn
- combine
- add
- read 读入
- print 输出

使用改进的Sales_data类

//avg_price.cpp //编译命令g++ avg_price.cpp Sales_data.cpp
#include <iostream>
using std::cerr; using std::cin; using std::cout; using std::endl;
#include "Sales_data.h"
int main()
{   Sales_data total;         // variable to hold the running sum
    if (read(cin, total))  {  // read the first transaction
        Sales_data trans;     // variable to hold data for the next transaction
        while(read(cin, trans)) {      // read the remaining transactions
            if (total.isbn() == trans.isbn())   // check the isbns
                total.combine(trans);  // update the running total
            else {
                print(cout, total) << endl;  // print the results
                total = trans;               // process the next book
            }
        }
        print(cout, total) << endl;          // print the last transaction
    } else {cerr << "No data?!" << endl;}        // notify the user
    return 0;
}

练习

//使用定义的Sales_data类为交易处理程序编写一个新版本。
#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;
}

定义改进的Sales_data类

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

    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};
Sales_data add(const Sales_data&, const Sales_data&);
std::ostream &print(std::ostream&, const Sales_data&);
std::istream &read(std::istream&, Sales_data&);

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

类成员 (Member)

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

定义成员函数

  • 成员函数的声明必须在类的内部。
  • 成员函数的定义既可以在类的内部也可以在外部。
        std::string isbn() const {return bookNo;}
    

引入this

total.isbn();
//成员函数通过名为this的额外隐式参数访问调用它的那个对象。
Sales_data::isbn(&total)

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

引入const 成员函数

//const 的作用是修改this指针的类型,
    //Sales_data* const到const Sales_data* const
//默认情况下,不能把this绑定到常量对象
//不能在常量对象上调用普通的成员函数
//const紧跟参数列表后面,表示this是一个指向常量的指针,
    //像这样使用const的成员函数称为常量成员函数
    //可以读取对象的数据成员,不能写入新值
    std::string isbn() const {return bookNo;}
//常量对象,常量对象的引用或指针都只能调用常量成员函数

类作用域和成员函数

类本身就是一个作用域
成员函数体可以随意使用类中的其它成员无需在意成员出现的次序

在类的外部定义成员函数

//返回类型,参数列表,函数名都得与类内部声明一致
//类外部定义的成员的名字,必须包含所属的类名
double Sales_data::avg_price() const {
    if (units_sold)
        return revenue/units_sold;
    else
        return 0;
}

定义一个返回this对象的函数

// add the value of the given Sales_data into this object
Sales_data& Sales_data::combine(const Sales_data &rhs)
{
    units_sold += rhs.units_sold; // add the members of rhs into 
    revenue += rhs.revenue;       // the members of ``this'' object
    return *this; // return the object on which the function was called
}

练习

//编写了一个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;
}

练习

//修改交易处理程序,令其使用成员
#include <iostream>
#include "Sales_data.h"
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;
}

练习

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

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

练习

//在你的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对象也需要使用这些函数操作。

定义类相关的非成员函数

  • 如果非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个文件
  • 和类相关的非成员函数,定义和声明都应该在类的外部

定义read print函数

// transactions contain ISBN, number of copies sold, and sales price
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;
}

定义add函数

Sales_data add(const Sales_data &lhs, const Sales_data &rhs)
{
    Sales_data sum = lhs;  // copy data members from lhs into sum
    sum.combine(rhs);      // add data members from rhs into sum
    return sum;
}

练习

//对于函数add、read和print,定义你自己的版本。
#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;
}

练习

//使用这些新函数重写交易处理程序。
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;
}

练习

//为什么read函数将其Sales_data参数定义成普通的引用,而print函数将其参数定义成常量引用?
//因为read函数会改变对象的内容,而print函数不会。

练习

//添加读取和打印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;
}

练习

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

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

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

构造函数

  • 类通过一个或者几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数
  • 构造函数没有返回值
  • 参数列表 函数体可能为空
  • 构造函数是特殊的成员函数。
  • 类可以包含多个构造函数
  • 构造函数不能声明成const

默认构造函数

  • 类通过一个特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数。
  • 无需任何实参。
  • 若没有显式定义构造函数,编译器会隐式定义一个默认构造函数。
  • 合成的默认构造函数(synthesized default constructor)
    • 编译器创建的构造函数
    • 若存在类内初始值,用它初始化成员
    • 否则,默认初始化成员

某些类不能依赖于合成的默认构造函数

  • 编译器发现类不包含任何构造函数,则生成一个默认的构造函数
  • 合成的默认构造函数可能执行错误的操作
  • 编译器有时不能位某些类合成默认的构造函数。
    • 比如类成员没有默认构造函数时

定义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(p*n){}
    Sales_data(std::istream&);

    std::string isbn() const {return bookNo;}
    Sales_data& combine(const Sales_data&);
    double avg_price() const;
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

=default 的含义

  • =default要求编译器合成默认的构造函数。(C++11)
  • =default在类内部,则内联;在类外部,则默认不是内联的。

构造函数初始值列表

  • 负责为新创建的对象的一个或几个数据成员赋初值。
  • 冒号和花括号之间的代码: Sales_item(): units_sold(0), revenue(0.0) { }
  • 当某个数据成员被构造函数初始值列表忽略时,它将以与合成默认构造函数相同的方式隐式初始化。
    Sales_data(const std::string &s):bookNo(s){}
    //等价于
    Sales_data(const std::string &s):
        bookNo(s),units_sold(0),revenue(0){}
    

在类的外部定义构造函数

Sales_data::Sales_data(std::istream &is) 
{
    // read will read a transaction from is into this object
    read(is, *this);
}
// transactions contain ISBN, number of copies sold, and sales price
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;
}

练习

//在你的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;
}

练习

//把只接受一个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;
}

练习

//使用istream构造函数重写程序。
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;
}

练习

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

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

练习

//为你的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;
}

拷贝、赋值和析构

  • 拷贝:初始化变量以及以值的方式传递或返回一个对象等
  • 赋值:使用赋值运算符,发生对象的赋值操作
  • 析构:当对象不在存在时,执行销毁的操作。
    • 一个局部对象在创建它的块结束时被销毁
    • vector对象或数组销毁时,存储在其中的对象也会被销毁

某些类不能依赖于合成的版本

尽管编译器能合成拷贝赋值销毁操作 - 对于某些类来说合成的版本无法正常工作 - 类分配类对象以外的资源时,合成版本常常失效 - 动态内存 - 需要动态内存的类可使用vector或string对象管理存储空间, - 避免分配和释放内存带来的复杂性

访问控制与封装

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

访问控制与封装

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() const
        {return unis_sold?revenue/units_sold:0}
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

使用class或struct关键字

都可以被用于定义一个类,唯一的却别在于访问权限。 - 使用 class:在第一个访问说明符之前的成员是 priavte的。 - 使用 struct:在第一个访问说明符之前的成员是 public的。

练习

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

在类的定义中对于访问说明符出现的位置和次数没有限定
每个访问说明符指定了接下来的成员的访问级别
其有效范围直到出现下一个访问说明符或者达到类的结尾处为止
如果某个成员能够在整个程序内都被访问那么它应该定义为public; 
如果某个成员只能在类内部访问那么它应该定义为private

练习

使用class和struct时有区别吗如果有是什么

class和struct的唯一区别是默认的访问级别不同

练习

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

将类内部分成员设置为外部不可见而提供部分接口给外面这样的行为叫做封装
用处
- 1.确保用户的代码不会无意间破坏封装对象的状态
- 2.被封装的类的具体实现细节可以随时改变而无需调整用户级别的代码

练习

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

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

友元

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

封装的益处

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

练习

友元在什么时候有用请分别举出使用友元的利弊

当其他类或者函数想要访问当前类的私有变量时这个时候应该用友元
与当前类有关的接口函数能直接访问类的私有变量
牺牲了封装性与可维护性

练习

//修改你的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;
}

练习

//修改你的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;
}

类的其它特性

定义一个类型成员

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

Screen 类的成员函数

class Screen{
public:
    typedef std::string::size_type pos;
    Screen()=default;
    Screen(pos ht,pos wd,char c):height(ht),width(wd),contents(ht *wd,c){}
    char get() const {return contents[cursor];}
    inline char get(os ht,pos wd) const;
    Screen &move(pos r,pos c);
private:
    pos cursor=0;
    pos height=0,width=0;
    std::string contents;
};

令成员作为内联函数

inline                   // we can specify inline on the definition
Screen &Screen::move(pos r, pos c)
{
    pos row = r * width; // compute the row location
    cursor = row + c;    // move cursor to the column within that row
    return *this;        // return this object as an lvalue
}

char Screen::get(pos r, pos c) const // declared as inline in the class
{
    pos row = r * width;      // compute row location
    return contents[row + c]; // return character at the given column
}
  • 成员函数作为内联函数 inline
  • 在类的内部,常有一些规模较小的函数适合于被声明成内联函数。
  • 定义在类内部的函数是自动内联的。
  • 在类外部定义的成员函数,也可以在声明时显式地加上 inline

重载成员函数

  • 成员函数也可被重载,函数之间在参数数量类型有所区别
    Screen myscreen;
    char ch = myscreen.get();
    ch = myscreen.get(0,0);
    

可变数据成员

  • 希望能修改类的某个数据成员,即使在一个const成员函数内。通过在变量声明中加mutable关键字做到这一点。
  • 永远不会是const,即使它是const对象的成员。
    class Screen{
    public:
        void som_member() const;
    private:
        mutable size_t access_ctr;
    };
    void Screen::some_member() const{
        ++access_ctr;
    }
    

类数据成员的初始值

class Window_mgr{
private:
    std::vector<Screen> screens{Screen(24,80,' ')};
};
- 提供类内初始值时,必须以符号=或者花括号表示。

练习

//编写你自己的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;
};

练习

//给你的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;
};

练习

Screen能安全地依赖于拷贝和赋值操作的默认版本吗
如果能为什么如果不能为什么


Screen的成员只有内置类型和string因此能安全地依赖于拷贝和赋值操作的默认版本
管理动态内存的类则不能依赖于拷贝和赋值操作的默认版本
而且也应该尽量使用string和vector来避免动态管理内存的复杂性

练习

将Sales_data::avg_price定义成内联函数

在头文件中加入
inline double Sales_data::avg_price() const
{
    return units_sold ? revenue/units_sold : 0;
}

返回*this 的成员函数

class Screen{
public:
    Screen & set(char);
    Screen & set(pos,pos,char);
};
inline Screen &Screen::set(char c){
    contents[cursor]=c;
    return *this;
}
inline Screen &Screen::set(pos r,pos col,char ch){
    contents[r*width+col]=ch;
    return *this;
}
//move set 返回Screen& 可嵌入一组动作序列
myScreen.move(4,0).set('#');

从const成员函数返回*this

//display 是一个const成员
//返回类型是 const Sales_data& 不能嵌入一组动作序列
Screen myScreen;
myScreen.display(cout).set('*');
//返回常量引用,不能调用set,会引发错误
//一个const成员函数如果以引用形式返回*this,那么返回类型是常量引用

基于const的重载

class Screen{
public:
    Screen &display(std::ostream &os)
        {do_display(os);return *this;}
    const Screen &display(std::ostream &os) const
        {do_display(os);return *this;}
private:
    void do_display(std::ostream &os)const {os<<contents;}
};

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

练习

给你自己的Screen类添加moveset 和display函数通过执行下面的代码检验
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;
}

练习

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

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

练习

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

推测正确
#with '&'
XXXXXXXXXXXXXXXXXXXX#XXXX
XXXXXXXXXXXXXXXXXXXX#XXXX
                    ^
# without '&'
XXXXXXXXXXXXXXXXXXXX#XXXX
XXXXXXXXXXXXXXXXXXXXXXXXX
                    ^

练习

通过this指针使用成员的做法虽然合法但是有点多余
讨论显示使用指针访问成员的优缺点

优点
程序的意图更明确
函数的参数可以与成员同名
  void setAddr(const std::string &addr) { this->addr = addr; }

缺点
有时候显得有点多余
std::string getAddr() const { return this->addr; }

类类型

  • 每个类定义了唯一的类型。
    • 对于两个类,即使成员完全一样,也是两个不同的类型。
  • 类名作为类型名使用,或类名跟在关键字class struct 后面:
    Sales_data item1;       //默认初始化
    class Sale_data item1;  //一条等价的声明
    

类的声明

//仅声明类而暂时不定义它
class Screen; 
- 这种声明称作前向声明 - 在声明后定义前是一个不完全类型。 - 可以定义指向这种类型的指针或引用 - 可以声明(不能定义)不完全类型作为参数或返回类型的函数。

类允许包含指向自身类型的引用或指针

class Link_screen{
    Screen window;
    Link_screen *next;
    Link_screen *prev;
}

练习

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

class Y;
class X{
    Y* y = nullptr;    
};

class Y{
    X x;
};

类之间的友元

  • 如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。
    class Screen{
        friend class Window_mgr;
    };
    class Window_mgr{
    public:
        using ScreenIndex=std::vector<Screen>::size_type;
        void clear(ScreenIndex);
    private:
        std::vector<Screen> screens{Screen(24,80,' ')};
    };
    void Window_mgr::clear(ScreenIndex i){
        Screen &s = screens[i];
        s.contents=string(s.height*s.width,' ');
    
    }
    

令成员函数作为友元

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

//首先定义Window_mgr类,其中声明clear函数但不定义。
//在clear使用Screen成员之前必须声明Screen.
//接下来定义Screen 包括对clear的友元声明
//最后定义clear,此时它才可以使用Screen的成员

函数重载和友元

  • 如果一个类想把一组重载函数声明成它的友元,它需要对这组函数每个分别声明。

友元声明与作用域

  • 友元声明的作用是影响访问权限,并非普通意义上的声明
  • 类和非成员函数的声明不是必须在它们的友元声明之前
    • 当一个名字第一次出现在一个友元声明时,隐式假定该名子在当且作用域可见,友元本身不一定真的声明在当前作用域中。
      struct X{
          friend void f(){/*友元函数可以定义在类内部*/}
          X(){f();}       //错误 f还没有被声明
          void g();
          void h();
      };
      void X::g(){return f();}    //错误 f还没有被声明
      void f();
      void X::h(){return f();}    //正确 现在f的声明在作用域中了
      

练习

//定义自己的Screen和Window_mgr,其中clear是Window_mgr的成员,是Screen的友元。
#include <vector>
#include <iostream>
#include <string>

class Screen;

class Window_mgr
{
public:
    using ScreenIndex = std::vector<Screen>::size_type;
    inline void clear(ScreenIndex);

private:
    std::vector<Screen> screens;
};
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) {}
    char get() const { return contents[cursor]; }
    char get(pos r, pos c) const { return contents[r*width + 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:
    void do_display(std::ostream &os) const { os << contents; }
private:
    pos cursor = 0;
    pos width = 0, height = 0;
    std::string contents;
};
inline void Window_mgr::clear(ScreenIndex i)
{
    Screen& s = screens[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;
}

类的作用域

  • 每个类都会定义它自己的作用域。在类的作用域之外,普通的数据和函数成员只能由引用、对象、指针使用成员访问运算符来访问。
    Screen::pos ht=24,wd=80;
    Screen scr(ht,wd,' ');
    Screen *p = &scr;
    char c=scr.get();
    c = p->get();
    

作用域和定义在类外部的成员

  • 在类的外部,成员名字被隐藏起来了
  • 一旦遇到类名,定义的剩余部分就在类的作用域之内。剩余部分包括参数列表函数体。
    void Window_mgr::clear(ScreenIndex i){
        Screen &s= screens[i];
        s.contents=string(s.height*s.width,' ');
    }
    
  • 函数的返回类型通常在函数名前面,因此当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外。
    class Window_mgr{
    public:
        ScreenIndex addScreen(const Screen&);
    };
    Window_mgr::ScreenIndex
    Window_mgr::addScreen(const Screen &s){
        screen.push_back(s);
        return screens.size()-1;
    }
    

练习

如果我们给Screen添加一个如下所示的size成员将发生什么情况
如果出现了问题请尝试修改它

pos Screen::size() const
{
    return height * width;
}

编译错误
改为
Screen::pos Screen::size() const
{
    return height * width;
}

名字查找与类的作用域

  • 名字查找
    • 在名字所在块寻找其声明语句,只考虑名字使用之前出现的声明
    • 没找到继续查找外层作用域
    • 最终没找到则报错
  • 类的定义
    • 编译成员的声明
    • 直到类全部可见后才编译函数体

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

  • 如果某个成员的声明使用了类中尚未出现的名字,则编译器将会在定义该类的作用域中继续查找。
    typedef double Money;
    string bal;
    class Account{
    public:
        Money balance(){return bal;}
    private:
        Money bal;
        //...
    };
    

类型名要特殊处理

  • 在类中,如果成员使用了外层作用域中的某个名字,该名字代表一种类型,则类不能在之后重新定义该名字。
    typedef double Money;
    string bal;
    class Account{
    public:
        Money balance(){return bal;}
    private:
        typedef double Money; //错误 不能重新定义Money
        Money bal;
        //...
    };
    
  • 类型名的定义通常出现在类开始处,确保所有使用该类型的成员都出现在类名的定义后。

成员定义中的普通块作用于名字查找

  • 首先在成员函数内查找该名字的声明
  • 在类中继续查找
  • 在成员函数定于之前的作用域继续查找
    int height;
    class Screen{
    public:
        typedef std::string::size_type pos;
        void dummy_fcn(pos height){
            cursor = width*height; //使用形参height
        }
        void dummy_fcn2(pos height){
            cursor = width*this->height; //使用成员height
            cursor = width*Screen::height; //使用成员height
        }
        void dummy_fcn3(pos ht){
            cursor = width*height;} //使用成员height
    private:
        pos cursor=0;
        pos height=0,width=0;
    };
    

类作用域之后,在外围的作用域中查找

  • 尽管外层的对象被隐藏掉了,但我们仍可以用作用域运算符访问它
    int height;
    class Screen{
    public:
        void dummy_fcn(pos height){
            cursor = width*::height; //使用全局height
        }
    private:
        pos cursor=0;
        pos height=0,width=0;
    };
    

在文件中名字的出现处对其进行解析

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

练习

如果我们把Screen类的pos的typedef放在类的最后一行会发生什么情况

 dummy_fcn(pos height) 函数中会出现 未定义的标识符pos
类型名的定义通常出现在类的开始处这样就能确保所有使用该类型的成员都出现在类名的定义之后

练习

解释下面代码的含义说明其中的Type和initVal分别使用了哪个定义如果代码存在错误尝试修改它
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;
}

在类中如果成员使用了外层作用域中的某个名字而该名字代表一种类型
则类不能在之后重新定义该名字因此重复定义Type是错误的行为
Type Exercise::setVal(Type parm) 改为 
Exercise::Type Exercise::setVal(Type parm) 

构造函数再探

//(注意初始化和赋值的区别)
string foo="hello"; //定义并初始化
string bar;         //默认初始化 ,空string对象
bar="hello";        //为bar赋值

Sales_data::Sales_data(const string &s,
        usigned cnt,double price){
        bookNo=s;
        units_sold=cnt;
        revenue=cnt*price;
}

构造函数的初始值有时必不可少

  • const或者引用类型的数据,只能初始化,不能赋值。
    class ConstRef{
    public:
        ConstRef(int ii);
    private:
        int i;
        cosnt int ci;
        int &ri;
    };
    ConstRef::ConstRef(int ii){
        //赋值//错误 ci ri 必须初始化
        i=ii;
        ci=ii;//错误 不能给const赋值
        ri=i; //错误 ri没被初始化
    }
    ConstRef::ConstRef(int ii):i(ii),ci(ii),ri(i){}//正确
    

成员初始化的顺序

  • 最好让构造函数初始值的顺序和成员声明的顺序保持一致。
  • 尽量避免使用某些成员初始化其它成员
    class X{
        int i;
        int j;
    pulic:  
        //未定义的 i在j之前被初始化
        X(int val):j(val),i(j){}
    }
    

默认实参和构造函数

  • 如果一个构造函数为所有参数都提供了默认参数,那么它实际上也定义了默认的构造函数。
    class Sales_data{
    public:
        Sales_data(std::string s=""):bookNo(s){}
        Sales_data(std::string s,unsigned cnt,double rev):
            bookNo(s),units_sold(cnt),revenue(rev*cnt){}
        Sales_data(std::istream& is){read(is,*this);}
    };
    

练习

下面的初始值是错误的请找出问题所在并尝试修改它
struct X {
    X (int i, int j): base(i), rem(base % j) {}
    int rem, base;
};


应该改为
struct X {
    X (int i, int j): base(i), rem(base % j) {}
    int base, rem;
};

练习

使用本节提供的Sales_data类确定初始化下面的变量时分别使用了
哪个构造函数然后罗列出每个对象所有的数据成员的值

// 使用 Sales_data(std::istream &is) ; 各成员值从输入流中读取
Sales_data first_item(cin);
int main() {
    // 使用默认构造函数  bookNo = "", cnt = 0, revenue = 0.0
    Sales_data next;

    // 使用 Sales_data(std::string s = "");   bookNo = "9-999-99999-9", cnt = 0, revenue = 0.0
    Sales_data last("9-999-99999-9"); 
}

练习

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

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

练习

如果接受string的构造函数和接受istream&的构造函数都使用默认实参
这种行为合法吗如果不为什么


不合法当你调用Sales_data()构造函数时无法区分是哪个重载

练习

从下面的抽象概念中选择一个或者你自己指定一个),思考这样的类需要哪些数据成员
提供一组合理的构造函数并阐明这样做的原因
(a) Book
(b) Data
(c) Employee
(d) Vehicle
(e) Object
(f) Tree
(a) Book.
class Book 
{
public:
    Book(unsigned isbn, std::string const& name, 
        std::string const& author, std::string const& pubdate)
        :isbn_(isbn), name_(name), author_(author), pubdate_(pubdate){ }
    explicit Book(std::istream &in) { 
        in >> isbn_ >> name_ >> author_ >> pubdate_;
    }
private:
    unsigned isbn_;
    std::string name_;
    std::string author_;
    std::string pubdate_;
};

委托构造函数 (delegating constructor, C++11

  • 委托构造函数将自己的职责委托给了其他构造函数。
    class Sales_data{
    public:
        Sales_data(std::string s,unsigned cnt,double price):
            bookNo(s),units_sold(cnt),revenue(cnt*price ){}
        Sales_data():Sales_data("",0,0){}
        Sales_data(std::string s):Sales_data(s,0,0){}
        Sales_data(std::istream &is):Sales_data(){read(is,*this)}
    
    };
    

练习

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

使用委托构造函数调用顺序是
- 1.实际的构造函数的函数体
- 2.委托构造函数的函数体

练习

对于你在练习中编写的类确定哪些构造函数可以使用委托
如果可以的话编写委托构造函数如果不可以从抽象概念列表中
重新选择一个你认为可以使用委托构造函数的为挑选出的这个概念编写类定义

class Book 
{
public:
    Book(unsigned isbn, std::string const& name, 
        std::string const& author, std::string const& pubdate)
        :isbn_(isbn), name_(name), author_(author), pubdate_(pubdate){ }
    Book(unsigned isbn) : Book(isbn, "", "", "") {}
    explicit Book(std::istream &in) 
    { 
        in >> isbn_ >> name_ >> author_ >> pubdate_;
    }
private:
    unsigned isbn_;
    std::string name_;
    std::string author_;
    std::string pubdate_;
};

默认构造函数的作用

  • 当对象被默认初始化或值初始化时自动执行默认构造函数。
  • 默认初始化
    • 在块作用域不使用初始值定义一个非静态变量或数组
    • 类本身含有类类型的成员且使用合成的默认构造函数
    • 类类型的成员没有在构造函数初始值列表中显式初始化
  • 值初始化
    • 数组初始化,初值值少于数组大小
    • 不使用初始值定义一个局部静态变量
    • 形如T()的表达式显式请求值初始化

默认构造函数的作用

//缺少默认构造函数
class NoDefault{
public:
    NoDefault(const std::string&);
};
struct A{
    NoDefault my_mem;
}
A a;        //错误 不能为A合成构造函数
struct B{
    B(){} //错误 b_member 没有初始值
    NoDefault b_member;
}
//如果定义了其它构造函数,最好提供一个默认构造函数

使用默认构造函数

Sales_data obj(); //定义了一个函数
Sales_data obj2;//定义了一个默认初始化对象 

练习

假定有一个名为NoDefault的类它有一个接受int的构造函数
但是没有默认构造函数定义类CC有一个 NoDefault类型的成员定义C的默认构造函数
class NoDefault {
public:
    NoDefault(int i) { }
};

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

练习

下面这条声明合法吗如果不为什么
vector<NoDefault> vec(10);//vec初始化有10个元素

不合法因为NoDefault没有默认构造函数

练习

如果在上一个练习中定义的vector的元素类型是C则声明合法吗为什么

合法因为C有默认构造函数

练习

下面哪些论断是不正确的为什么
- (a) 一个类必须至少提供一个构造函数
- (b) 默认构造函数是参数列表为空的构造函数
- (c) 如果对于类来说不存在有意义的默认值则类不应该提供默认构造函数
- (d) 如果类没有定义默认构造函数则编译器将为其生成一个
并把每个数据成员初始化成相应类型的默认值

- (a) 不正确如果我们的类没有显式地定义构造函数
那么编译器就会为我们隐式地定义一个默认构造函数并称之为合成的默认构造函数
- (b) 不完全正确为每个参数都提供了默认值的构造函数也是默认构造函数
- (c) 不正确哪怕没有意义的值也需要初始化
- (d) 不正确只有当一个类没有定义任何构造函数的时候
编译器才会生成一个默认构造函数

隐式的类型转换

  • 如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制
    • 这种构造函数又叫转换构造函数(converting constructor)。
  • 编译器只会自动地执行仅一步类型转换。
    string null_book="999-99";
    item.combine(null_book);//string -> Sales_data 
    
    item.combine("999-99");//错误 "999-99"->string->Sales_data 不是一步
    item.combine(string("999-99");//正确 string->Sales_data 
    item.combine(Sales_data("999-99"));//正确
    

类类型转换不是总有效

  • string可能表示不存在的isbn号
    item.combine(cin);//cin->Sales_data 
    

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

- 将构造函数声明为explicit加以阻止
- explicit构造函数只能用于直接初始化不能用于拷贝形式的初始化
- explicit只对一个实参的构造函数有效
  - 多个实参的构造函数不能执行隐式转换无需指定explicit 
- 只能在类内声明构造函数时使用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 Sales_data::Sales_data(istream& is);

explicit 构造函数只能用于直接初始化

Sales_data item1(null_book);//正确 直接初始化
//错误 不能将explicit 构造函数用于拷贝形式的初始化过程
Sales_data item2=null_book;
//当用explicit 关键字声明构造函数时,它将只能以直接初始化的形式使用
//编译器将不会在自动转换过程中使用该构造函数

为转换显示地使用构造函数

//尽管编译器不会将explicit 构造函数用于隐式转换过程
//我们可以使用构造函数显式强制进行转换
//正确 实参是显式构造的Sales_data 对象
item.combine(Sales_data(null_book));
//正确 static_cast可以使用explicit 的构造函数
item.combine(static_cast<Sales_data>(cin));

标准库中含有显式构造函数的类

  • 接受一个单参数const char*的string构造函数不是explicit的
  • 接受一个容量参数的vector构造函数是explicit的

练习

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

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

优点可以抑制构造函数定义的隐式转换
缺点为了转换要显式地使用构造函数

练习

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

string null_isbn("9-999-9999-9");
Sales_data item1(null_isbn);
Sales_data item2("9-999-99999-9");
这些定义和是不是explicit的无关

练习

对于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函数的本意就是要改变对象

练习

确定在你的Person类中是否有一些构造函数应该是explicit 
explicit Person(std::istream &is){ read(is, *this); }

练习

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

假如我们有一个这样的函数
int getSize(const std::vector<int>&);
如果vector没有将单参数构造函数定义成explicit的我们就可以这样调用
getSize(34);
很明显这样调用会让人困惑函数实际上会初始化一个拥有34个元素的vector的临时量
但是这样没有任何意义而string则不同string的单参数构造函数的参数是const char*
因此凡是在需要用到string的地方都可以用 const char *来代替)。
void print(std::string);
print("hello world");

聚合类 (aggregate class)

  • 满足以下所有条件:
  • 所有成员都是public的。
  • 没有定义任何构造函数。
  • 没有类内初始值。
  • 没有基类,也没有virtual函数。
  • 可以使用一个花括号括起来的成员初始值列表,初始值的顺序必须和声明的顺序一致。
//聚合类
struct Data{
    int ival;
    string s;
};
//val1.val=0;val1.s=string("Anna");
Data val1={0,"Anna"};
//错误 顺序不一致
Data val2={"Anna",1024};

显式初始化类的对象的成员存在三个明显缺点
- 要求类的所有成员都是public
- 正确初始化对象成员的工作交给了类的用户过程冗长乏味易出错
- 添加删除一个成员所有初始化语句都要更新

练习

使用Sales_data 解释下面的初始化过程如果存在问题尝试修改它
Sales_data item = {"987-0590353403", 25, 15.99};

Sales_data 类不是聚合类应该修改成如下
struct Sales_data {
    std::string bookNo;
    unsigned units_sold;
    double revenue;
};

字面值常量类

- constexpr函数的参数和返回值必须是字面值除了算术类型引用和指针外
    某些类也是字面值类型
- 数据成员都是字面值类型的聚合类是字面值常量类
    如果不是聚合类则必须满足下面所有条件,也是字面值常量类
  - 数据成员都必须是字面值类型
  - 类必须至少含有一个constexpr构造函数
  - 如果一个数据成员含有类内部初始值则内置类型成员的初始值必须是一条常量表达式
    或者如果成员属于某种类类型则初始值必须使用成员自己的constexpr构造函数
  - 类必须使用析构函数的默认定义该成员负责销毁类的对象
//constexpr 函数体一般来说应该是空的
class Debug{
public:
    constexpr Debug(bool b=true):hw(b),io(b),other(b){}
    constexpr Debug(bool h,bool i,bool o):
                        hw(h),io(i),other(o){}
    constexpr bool any(){return hw||io||other;}
    void set_io(bool b){io=b;}
    void set_hw(bool b){hw=b;}
    void set_other(bool b){hw=b}
private:
    bool hw;
    bool io;
    bool other;
}
constexpr Debug io_sub(false,true,false);
if(io_sub.any()){cerr<<"print appropriate error message"<<endl;}
constexpr Debug prod(false);
if(prod.any()){cerr<<"print an error message"<<endl;}

练习

定义你自己的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
};

练习

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

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

练习

Data类是字面值常量类吗请解释原因

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

类的静态成员

  • 有时候类需要它的一些成员与类本身直接相关,而不是与类的各个对象保持关联
    • 如银行账户类中的基准利率,与类关联,而非与类的对象关联
    • 没必要每个对象都存储利率。利率更新,所有对象都使用新值。

声明静态成员

  • 成员声明之前加上关键词static,使得其与类关联在一起
  • 类的静态数据成员存在于任何对象之外,对象中不包括任何与静态数据成员有关的数据
  • 静态成员函数也不与任何对象绑定,它们不包含this指针
    • 静态成员函数不能声明成const 也不能在函数体内使用this
    • 不能显式使用this 也不能调用非静态成员隐式使用this
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();
}

使用类的静态成员

// 使用作用域运算符::直接访问静态成员
double r;
r=Account::rate();//使用作用域运算符访问静态成员

Account ac1;
Account *ac2=&ac1;
//也可以使用对象与指针访问
//调用静态成员函数rate
r=ac1.rate();
r=ac2->rate();

使用类的静态成员

//成员函数不用通过作用域运算符 直接使用静态成员
class Account{
public:
    void calculate(){amount+=amount* interestRate;}
private:
    static double interestRate;
};

定义静态成员

- 在类外部定义静态成员时不能重复static关键字
- 该关键字只出现在类内部的声明语句
void Account::rate(double newRate){
    interestRate=newRate;
}

- 不能在类内部初始化静态成员
- 必须在类外部定义和初始化每个静态成员
- 一个静态成员只能定义一次
- 最好把静态数据成员的定义和其它非内联函数定义放在同一个文件
double Account::interestRate=initRate();

静态成员的类内初始化

- 通常情况下类的静态成员不应该在类内部初始化
- 可为静态成员提供const整数类型的类内初值
    - 静态成员必须是字面值常量类型的constexpr,初始值必须是常量表达式
class Account{
public:
    static double rate(){return interesRate;}
    static void rate(double);
private:
    static constexpr int period=30;//常量表达式
    double daily_tbl[period];
};
//一个不带初始值的静态成员的定义
constexpr int Account::period; //初始值在类内部提供
//即使一个常量静态数据成员在类内部被初始化了,也应该在类外部定义一下该成员

静态成员能用于某些场景,而普通成员不能

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

class Screen{
public: 
    //可以使用静态成员作为默认实参
    //bkground表示一个在类中稍后定义的静态成员
    Screen& clear(char=bkground);
private:
    static const char bkground;
}

练习

什么是类的静态成员它有何优点静态成员与普通成员有何区别

与类本身相关而不是与类的各个对象相关的成员是静态成员
静态成员能用于某些场景而普通成员不能

练习

编写你自己的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();

练习

下面的静态数据成员的声明和定义有错误吗请解释原因
//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);

实践课

  • 从课程主页 cpp.njuer.org 打开实验课 类 界面
  • 使用g++编译代码
  • 编辑一个 readme.md 文档,键入本次实验心得.
  • 使用git进行版本控制 可使用之前的gitee代码仓库
      - 云服务器elastic compute service,简称ecs
      - aliyun linux 2是阿里云推出的 linux 发行版
      - vim是从vi发展出来的一个文本编辑器.
      - g++ 是c++编译器
    
习题1
实现时间类Time,提供以下操作并调用执行
构造函数Time(int h,int m,int s)
调整时间void set(int h,int m,int s)
显示时间void display()
比较时间bool equal (Time & other_time)

习题2
定义一个类A使得程序只能创建一个类A的对象
当试图创建类A的第二个对象时返回第一个对象的指针


习题3
实现字符串类String提供以下操作并调用执行
判断子串是否在当前字符串里,bool is_substring(const String &sub_str)
查找子串并替换,返回替换次数,int replace(const char* find,const char* replace)
编辑c++代码和markdown文档,使用git进行版本控制
yum install -y git gcc-c++
使用git工具进行版本控制
git clone你之前的网络git仓库test(或其它名字)
cd test 进入文件夹test
(clone的仓库,可移动旧文件到目录weekN:  mkdir -p weekN ; mv 文件名 weekN;)

vim test1.cpp
g++ ./test1.cpp 编译
./a.out 执行程序

vim test2.cpp
g++ ./test2.cpp 编译
./a.out 执行程序

vim test3.cpp
g++ ./test3.cpp 编译
./a.out 执行程序
git add . 加入当前文件夹下所有文件到暂存区
git config --global user.email "you@example.com"
git config --global user.name "Your Name"
vim readme.md 键入新内容(实验感想),按ESC 再按:wq退出
git add .
git commit –m "weekN" 表示提交到本地,备注weekN

git push 到你的git仓库

git log --oneline --graph 可看git记录
键入命令并截图或复制文字,并提交到群作业.
cat test* readme.md

提交

  • 截图或复制文字,提交到群作业.
  • 填写阿里云平台(本实验)的网页实验报告栏,发布保存.本次报告不需要分享提交
  • 填写问卷调查 https://rnk6jc.aliwork.com/o/cppinfo

关于使用tmux

sudo yum install -y tmux
cd ~ && wget https://cpp.njuer.org/tmux && mv tmux .tmux.conf
tmux 进入会话 .
前缀按键prefix= ctrl+a, 
prefix+c创建新面板,
prefix+"分屏,
prefix+k选上面,prefix+j选下面,
prefix+1选择第一,prefix+n选择第n,
prefix+d脱离会话
tmux attach-session -t 0 回到会话0

vim 共分为三种模式

图片1

- 命令模式
  - 刚启动 vim,便进入了命令模式.其它模式下按ESC,可切换回命令模式
    - i 切换到输入模式,以输入字符.
    - x 删除当前光标所在处的字符.
    - : 切换到底线命令模式,可输入命令.
- 输入模式
  - 命令模式下按下i就进入了输入模式.
    - ESC,退出输入模式,切换到命令模式
- 底线命令模式
  - 命令模式下按下:英文冒号就进入了底线命令模式.
    - wq 保存退出

vim 常用按键说明

除了 i, Esc, :wq 之外,其实 vim 还有非常多的按键可以使用.命令模式下
- 光标移动
  - j下 k上 h左 l右
  - w前进一个词 b后退一个词
  - Ctrl+d 向下半屏  ctrl+u 向上半屏
  - G 移动到最后一行 gg 第一行 ngg 第n行
- 复制粘贴
  - dd 删一行 ndd 删n行
  - yy 复制一行 nyy复制n行
  - p将复制的数据粘贴在下一行 P粘贴到上一行
  - u恢复到前一个动作 ctrl+r重做上一个动作
- 搜索替换
  - /word 向下找word     word 向上找
  - n重复搜索 N反向搜索
  - :1,$s/word1/word2/g从第一行到最后一行寻找 word1 字符串,并将该字符串
    取代为 word2

vim 常用按键说明

底线命令模式下
- :set nu   显示行号
- :set nonu 取消行号
- :set paste    粘贴代码不乱序
把caps lock按键映射为ctrl,能提高编辑效率.

Markdown 文档语法

# 一级标题
## 二级标题
*斜体* **粗体**
- 列表项
  - 子列表项
> 引用
[超链接](http://asdf.com)
![图片名](http://asdf.com/a.jpg)

|表格标题1|表格标题2|
 |-|-|
|内容1|内容2|

谢谢