C++ Primer记录_第七章

第7章 类

  • 我们使用类定义自己的数据类型,通过定义新的类型来反映待解决问题中的各种概念,可以使得更容易编写和调试程序。
  • 类的基本思想是数据抽象封装
  • 数据抽象是一种依赖于接口实现分离的编程技术。
  • 类要想实现数据抽象的封装,需要首先定义一个抽象数据类型

7.1 定义抽象数据类型

  • Sales_item是一个抽象数据类型,Sale_data类不是一个抽象数据类型,我们需要定义一些操作供用户使用,并封装它的成员函数。

7.1.1 设计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。
  • 使用改进的Sales_data类。
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
    {
        //没有输入任何信息
        err << "No data?!" << endl; //通知用户
    }
}

7.1.2 定义改进的Sales_data类

  • 数据成员包括:
    • bookNo,string类型,表示ISBN编号。
    • units_sold,unsigned类型,表示某本书的销量。
    • revenue,double类型,表示这本书的总销售额。
  • 成员函数包括:
    • combine
    • ISBN
    • avg_price,返回售出书籍的平均价格。
  • 定义在类内部的函数是隐式的inline函数。
struct Sales_data
{
    //新成员:关于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的非成员接口函数
Sales_data add(const Sales_data &, const Sales_data &);
std::ostream &print(std::ostream &, const Sales_data &);
std::istream &read(std::istram &, Sales_data &);
  • 所有成员都必须在类的内部声明,但是成员函数体可以定义在类内也可以定义在类外。
  • std::string isbn() const { return bookNo; }定义在类内,所以内联。
  • total是一个Sales_data的实例,观察total.isbn()的调用,实际上成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象。
//伪代码。用于说明调用成员函数的实际执行过程
Sales_data::isbn(&total)
  • 然后对类成员的直接访问都被看作this的隐式引用。
//我们可以在成员函数体内部使用this尽管没有必要
std::string isbn() const { return this->bookNo; }
  • isbn函数的另外一个关键之处是紧随参数列表之后的const关键词,这里的作用是修改隐式this指针的类型。
  • 默认情况下,this的类型是指向类类型非常量版本的常量指针,this的类型是Sales_data *const。
  • 可以把isbn的函数体想象成如下的形式:
//伪代码,说明隐式的this指针是如何使用的
//下面的代码是非法的:因为我们不能显式地定义自己的this指针
//谨记此处的this是一个指向常量的指针,因为isbn是一个常量成员
std::string Sales_data::isbn(const Sales_data *const this) { return this->isbn; }
  • 类本身就是一个作用域。
  • 编译器分两步处理类:首先编译成员的声明,然后才轮到成员函数体。
  • 当我们在类的外部定义成员函数时,成员函数的定义必须与它的声明匹配。
double Sales_data::avg_price() const
{
    if (units_sold)
    {
        return revenue / units_sold;
    }
    else
    {
        return 0;
    }
}
  • 定义一个返回this的对象的函数。
Sale_data &Sales_data::combine(const Sales_data &rhs)
{
    units_sold += rhs.units_sold; //把ths的成员加到this对象的成员上
    revenue += rhs.revenue;
    return *this; //返回调用该函数的对象
}

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

  • 类常常需要定义一些辅助函数,尽管这些函数定义的操作从概念上来说属于类的接口的组成部分,但是它们实际上并不属于类本身。
  • 一般来说,如果非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个头文件内。
  • 定义read和print函数。
//输入的交易信息包括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;
}
  • 定义add函数
Sales_data add(const Sales_data &lhs, const Sales_data &rhs)
{
    Sales_data sum = lhs; //把lhs的数据成员拷贝给sum
    sum.combine(rhs);     //把rhs的数据成员加到sum当中
    return sum;
}

7.1.4 构造函数

  • 每个类都分别定义了它的对象被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数
  • 构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。
  • 构造函数不能被声明成const。
  • 现在的Sales_data类并没有定义任何构造函数,类通过一个特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数,默认构造函数无需任何实参。
  • 编译器创建的构造函数又被称为合成的默认构造函数,会按照如下规则初始化类的数据成员:
    1. 如果存在类内的初始值,用它来初始化成员。
    2. 否则,默认初始化该成员。
  • 合成的默认构造函数只适合非常简单的类,对于一个普通的类来说,必须定义它自己的默认构造函数,原因有三个:
    1. 只有在类不包含任何构造函数的情况下才会替我们生成一个默认的构造函数,一旦自己定义了一些其它的有参构造,那么除非我们生成一个默认的构造函数,否则类将没有默认构造函数。
    2. 合成的默认构造函数可能执行错误的操作,定义在块中的内置类型或者复合类型对象被默认初始化,这它们的值将是未定义的。
    3. 编译器不能为某些类合成默认的构造函数,如一个类中包含一个其它类类型的成员且这个成员的类型没有默认构造函数,则编译将无法初始化改成员。
  • 定义Sales_data的构造函数
    • 一个istream&,从中读取一个交易信息。
    • 一个const string&,表示ISBN编号;一个unsigned,表示售出的图书数量;以及一个double,表示图书的售出价格。
    • 一个const string&,表示ISBN编号;编译器将赋予其他成员默认值。
    • 一个空参数列表(及默认构造函数),定义了其他构造函数,那么必须定义一个默认构造函数。
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;
}
  • C++11标准中,=defalut来要求编译器生成构造函数,可以声明在类内(内联),也可以声明在类外(不内联)。
  • 如果编译器不支持类内初始值,那默认构造函数就应该使用构造函数初始值列表。
  • 以下两个定义出现的新的部分,即冒号和花括号之间的代码称为构造函数初始值列表
    Sales_data(const std::string &s) : bookNo(s) {}
    //上面定义等价于Sales_data(const std::string &s) : bookNo(s),unit_sold(0),revenue(0) {}
    Sales_data(const std::string &s,unsigned n,double p) : bookNo(s),units_sold(n),revenue(p*n) {}
  • 在类的外部定义构造函数,构造函数初始值列表为空,但是执行了构造函数体。
Sales_data::Sales_data(std::istream &is)
{
    read(is, *this); // read函数的作用是从is中读取一条交易信息然后存入this对象中
}

7.1.5 拷贝、赋值和析构

  • 类还需要控制拷贝、赋值和销毁对象时发生的行为,如果我们不主动定义这些操作,则编译器将替我们合成它们。
  • 尽管编译器能替我们合成拷贝、赋值和销毁的操作,但是对于某些类来说合成的版本无法正常工作。

7.2 访问控制与封装

  • 使用访问说明符加强类的封装性:
    • 定义在public说明符之后的成员在整个程序内可被访问,public成员定义类的接口。
    • 定义在private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,隐藏了类的实现细节。
class Sales_data
{
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(p * n) {}
    Sales_data(std::istream &);
    std::string isbn() const { return bookNo; }
    Sales_data &combine(const Sales_data &);

private: //添加了访问说明符
    double avg_price() const { return units_sold ? revenue / units_sold : 0; }
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
}
  • 使用了class代替struct,它们的默认权限不一样,class关键词默认成员是private,struct关键词默认成员是public。

7.2.1 友元

  • 类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元
  • 如果类想把一个函数作为它的友元,只需要增加一条以friend关键字开始的函数声明语句即可。
  • 友元声明只能出现类定义的内部,但是在类内出现的具体位置不限。
  • 友元不是类的成员也不受它所在区域访问控制级别的约束。
class Sales_data
{
    //为Sales_data的非成员函数所做的友元声明
    friend Sales_data add(const Sales_data &, const Sales_data &);
    friend std::ostream &print(std::ostream &, const Sales_data &);
    friend std::istream &read(std::istram &, Sales_data &);
    //其他成员及访问说明符与之前一致
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(p * n) {}
    Sales_data(std::istream &);
    std::string isbn() const { return bookNo; }
    Sales_data &combine(const Sales_data &);

private:
    double avg_price() const { 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::ostream &print(std::ostream &, const Sales_data &);
std::istream &read(std::istram &, Sales_data &);

7.3 类的其他特性

  • 包括:类型成员、类的成员的类内初始值、可变数据成员、内联成员函数、从成员函数返回*this、关于如何定义并使用类类型及友元类的更多知识。

7.3.1 类成员再探

  • 定义一对相互关联的类分别是Screen和Window_mgr。
  • Screen表示显示器中的一个窗口。
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内容
};
  • 添加一个构造函数令用户能够定义屏幕的尺寸和内容,以及其他两个成员,分别负责移动光标和读取给定位置的字符。
  • 在类中,常有一些规模较小的函数适合于被声明成内联函数
//在.h文件中
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;      //保存Screen内容
};

//可以在函数的定义处指定inline
inline Screen &Screen::move(pos r, pos c)
{
    pos row = r * width; //计算行的位置
    cursor = row + c;    //在行内将光标移动到指定的列
    return *this;        //以左值的形式返回对象
}
//.cpp
char Screen::get(pos r, pos c) const //在类的内部声明成inline
{
    pos row = r * width;      //计算行的位置
    return contents[row + c]; //返回给定列的字符
}
  • 重载成员函数也可以被重载。
screen myscreen;
char ch = myscreen.get(); //调用Screen::get()
ch = myscreen.get(0, 0);  //调用Screen::get(pos,pos)
  • 有时希望能修改类的某个数据成员,即使式在一个const成员函数内,可以在变量的声明中加入mutable关键词成为一个可变数据成员
//.h
class Screen
{
public:
    void some_member() const;
    //......
private:
    mutable size_t access_ctr; //即使在一个const对象内也能被修改
    //......
};

//......
//.cpp
//......

void Screen::some_member() const
{
    ++access_ctr; //保存一个计数值,用于记录成员函数被调用的次数
    //该成员需要完成的其他工作
}
  • 在C++ll新标准中,最好的方式就是把这个默认值声明成一个类内初始值。
class Window_mgr
{
private:
    //这个Window_mgr追踪的Screen
    //默认情况下,一个Window_mgr包含一个标准尺寸的空白Screen
    std::vector<Screen> screens{Screen(24, 80, ' ')};
}

7.3.2 返回*this的成员函数

  • 继续添加一些函数,它们负责设置光标所在位置的字符或者其他任一给定位置的字符。
//.h
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 ch)
{
    contents[r * width + col] = ch; //设置给定位置的新值
    return *this;                   //将this对象作为左值返回
}
//将光标移动到一个指定的位置,然后设置该位置的字符值
myScreen.move(4, 0).set('#');
//等价于
myScreen.move(4, 0);
myScreen.set('#');
  • 假如当初我们定义的返回类型不是引用,则move的返回值将是*this的副本,因此调用set只能改变临时副本,而不能改变myScreen的值。
//如果move返回Screen而非Screen&
Screen temp = myScreen.move(4, 0); //对返回值进行拷贝
temp.set('#');                     //不会改变myScreen的contents
  • 当一个成员调用另外一个成员时,this指针在其中隐式地传递。
  • 当display的非常量版本调用do_display时,它的this指针将隐式地从指向非常量的指针转换成指向常量的指针。
//.h
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(ste::ostream &os) const { os << contents; }
};
  • 当do_display完成后,display函数各自返回解引用this所得的对象。
  • 在非常量版本中,this指向一个非常量对象,因此display返回一个普通的(非常量)引用,而const成员则返回一个常量引用。
Screen myScreen(5, 3);
const Screen blank(5, 3);
myScreen.set('#').display(cout); //调用非常量版本
blank.display(cout);             //调用常量版本

7.3.3 类类型

  • 每个类定义了唯一的类型,对于两个类来说,即使它们的成员完全一样,这两个类也是两个不同的类型。
Struct First
{
    int memi;
    int getMem();
};

Struct Second
{
    int memi;
    int getMem();
};
First obj1;
Second obj2=obj1;//错误:obj1和obj2的类型不同
  • 就像可以把函数的声明和定义分离开来一样,也能仅仅声明类而暂时不定义它,如class Screen;,这种声明被称作前向声明
  • 对于类型Screen来说,在它声明之后定义之前是一个不完全类型,也就是说,此时我们已知Screen是一个类类型,但是不清楚它到底包含哪些成员。

7.3.4 友元再探

  • 关于类之间的友元关系,假设需要为Window_mgr添加一个名为clear的成员,它负责把一个指定的Screen的内容都设为空白。
class Screen
{
    // Window_mgr的成员可以访问Screen类的私有部分
    friend class Window_mgr;
    //......
}; 
  • 如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。
//.h
class Window_mgr
{
public:
    //窗口中每个屏幕的编号
    using ScreenIndex = std::vector<Screen>::size_type;
    //按照编号将指定的Screen重置为空白
    void clear(ScreenIndex);

private:
    std::vector<Screen> screens{Screen(24, 80, ' ')};
};
//.cpp
void Window_mgr::clear(ScreenIndex i)
{
    // s是一个Screen的引用,指向我们想清空的那个屏幕
    Screen &s = screens[i];
    //将那个选定的Screen的引用,指向我们想清空的那个屏幕
    s.contents = string(s.height * s.width, ' ');
}
  • 除了令整个Window_mgr作为友元之外,Screen还可以只为clear提供访问权限。
  • 关于令某个成员函数作为友元:
    1. 首先定义Window_mgr类,其中声明clear函数,但是不能定义它,在clear使用Screen的成员之前必须先声明Screen。
    2. 接下来定义Screen,包括对于clear的友元声明。
    3. 最后定义clear,此时它才可以使用Screen的成员。
class Screen
{
    // Window_mgr::clear必须在Screen类之前被声明
    friend void Window_mgr::clear(ScreenIndex);
    // ......
};
  • 尽管重载函数的名字相同,但它们仍然是不同的函数,因此,如果一个类想把一组重载函数声明成它的友元,它需要对这组函数中的每一个分别声明。
//重载的storeOn函数
extern std::ostream& storeOn(std::ostream &, Screen &);
extern BitMap& storeOn(BitMap &, Screen &);
class Screen
{
    // storeOn的ostream版本能访问Screen对象的私有部分,但是接受BitMap&作为参数版本仍然不能访问Screen。
    friend std::ostream &storeOn(std::ostream &, Screen &);
    // ......
};
  • 当一个名字第一次出现在一个友元声明中时,我们隐式地假定该名字在当前作用域中是可见的,然而,友元本身不一定真的声明在当前作用域中。
  • 即使我们仅仅是用声明友元的类的成员调用该友元函数,它也必须是被声明过的。
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的声明在作用域中了

7.4 类的作用域

  • 每个类都会定义它自己的作用域。
  • 在类的作用域之外,普通的数据和函数成员只能由对象、引用或者指针使用成员访问运算符来访问。
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成员
  • 在类的外部,成员的名字被隐藏起来了。
  • 因为编译器在处理参数列表之前已经明确了我们当前正位于某一个类的作用域中,所以不必再专门说明这个类成员是这个类定义的。
  • 但是函数的返回类型通常出现在函数名之前,因此当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外,这时,返回类型必须指明它是哪个类的成员。
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;
}

7.4.1 名字查找与类的作用域

  • 编译器处理完类中的全部声明后才会处理成员函数的定义。
  • 名字查找的过程:
    1. 首先,在名字所在的块中寻找其声明语句,只考虑在名字的使用之前出现的声明。
    2. 如果没找到,继续查找外层作用域。
    3. 如果最终没有找到匹配的声明,则程序报错。
  • 类的定义分两步处理。
    1. 首先,编译成员的声明。
    2. 直到类全部可见后才编译函数体。
  • 这种两阶段的处理方式只适用于成员函数中使用的名字。
  • 声明中使用的名字,包括返回类型或者参数列表中使用的名字,都必须在使用前确保可见。
typedef double Money;
string bal;
class Account
{
public:
    Money balance() { return bal; }

private:
    Money bal;
    //......
};
  • 一般来说,内层作用域可以重新定义外层作用域中的名字,即使该名字已经在内层作用域中使用过。
  • 然而在类中,如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字。
typedef double Money;
class Account
{
public:
    Money balance() { return bal; } //使用外层作用域的Money

private:
    typedef double Money; //错误:不能重新定义Money
    Money bal;
    //......
};
  • 类型名的定义通常出现在类的开始处,这样就能确保所有使用该类型的成员都出现在类名的定义之后。
  • 成员函数中使用的名字按照如下方式解析:
    1. 首先,在成员函数内查找该名字的声明,和前面一样,只有在函数使用之前出现的声明才被考虑。
    2. 如果在成员函数内没有找到,则在类内继续查找,这时类的所有成员都可以被考虑。
    3. 如果类内也没找到该名字的声明,在成员函数定义之前的作用域内继续查找。
  • 成员定义中的普通块作用域的名字查找,在此例中,height参数隐藏了同名的成员。
//注意:这段代码仅为了说明而用,不是一段很好的代码
//通常清空下不建议为参数和成员使用同样的名字
int height; //定义了一个名字,稍后将在Screen中使用
class Screen
{
public:
    typedef std::string::size_type pos;
    void dummy_fun(pos height)
    {
        cursor = width * height; 
    }

private:
    pos cursor = 0;
    pos height = 0, width = 0;
};
  • 如果想绕开上面的查找规则,应该将代码变为:
//不建议的写法,成员函数中的名字不应该隐藏同名的成员
void Screen::dummy_fun(pos height)
{
    cursor = width * this->height; //成员height
    //另外一种表示该成员的方式
    cursor = width * Screen::height; //成员height
}
  • 其实最好的确保我们使用height成员的方法是给参数起个其他名字:
void Screen::dummy_fun(pos ht)
{
    cursor = width * height; //成员height
}
  • 类作用域之后,在外层的作用域中查找。
//不建议的写法:不要隐藏外层作用域中可能被用到的名字
void Screen::dummy_fun(pos height)
{
    cursor = width * ::height; //显式调用全局的
}
  • 当成员定义在类的外部时,名字查找的第三步不仅要考虑类定义之前的全局作用域中的声明,还需要考虑在成员函数定义之前的全局作用域中的声明。
int height; //定义了一个名字,稍后将在Screen中使用
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);
}

7.5 构造函数再探

  • 对于任何C++的类来说,构造函数都是其中重要的组成部分。

7.5.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;
}
  • 如果成员是const或者是引用的话,必须将其初始化。
class ConstRef
{
public:
    ConstRef(int ii);

private:
    int i;
    const int ci;
    int &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(i) {}
  • 构造函数的初始值列表效率更高,因为这是初始化不是赋值。
  • 构造函数初始值列表只说明用于初始化成员的值,而不限定初始化的具体执行顺序。
class X
{
    int i;
    int j;
public:
    //未定义的:i在j之前被初始化
    X(int val) : j(val), i(j) {}
}
  • 最好令构造函数初始值的顺序与成员声明的顺序保持一致。尽量避免使用某些成员初始化其他成员。
X(int val): i(val), j (val) { } 
  • 默认实参指的是当函数调用中省略了实参时,自动使用一个值。
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); }
    //......
};

7.5.2 委托构造函数

  • C++11新标准扩展了构造函数初始值的功能,使得可以定义所谓的委托构造函数
  • 当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行。
class Sales_data
{
public:
    //非委托构造函数使用对应的实参初始化成员
    Sales_data(std::string s, unsigned cnt, double rev) : bookNo(s), units_sold(cnt), revenue(rev * cnt) {}
    //其余构造函数全部委托给另一个构造函数
    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); }
    //......
};

7.5.3 默认构造函数的作用

  • 在实际中,如果定义了其他构造函数,那么最好也提供一个默认的构造函数。
  • 这部分挠头?

7.5.4 隐式的类类型转换

  • C++在内置类型之间定义了几种自动转换规则。
  • 如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,有时我们把这种构造函数称作转换构造函数
  • 书上例子看吐了,换个例子理解。
  • 值得注意的是转换构造函数只接受一个参数,如果有提供默认参数的也可以,将一般参数类型隐式转为类类型。
class a
{
public:
    int x, y;
    a(int i, int j = 2) : x(i), y(j)
    {
    }
};
int main()
{
    a t1(4);
    a t2 = 3; //转换构造函数
    return 0;
}
  • 在要求隐式转换的程序上下文中,我们可以通过将构造函数声明为explicit加以阻止:
class a
{
public:
    int x, y;
    explicit a(int i, int j = 2) : x(i), y(j)
    {
    }
};
int main()
{
    a t1(4);
    a t2 = 3; //不能将explicit构造函数用于拷贝形式的初始化过程
    return 0;
}
  • 尽管编译器不会将explicit的构造函数用于隐式转换过程,但是可以将构造函数显式地强制进行转换:
int main()
{
    a t1(4);
    a t2 = a(3); //实参是一个显式构造的a对象
    a t3 = static_cast<a>(3);
    return 0;
}

7.5.5 聚合类

  • 聚合类使得用户可以直接访问其成员,并且具有特殊的初始化语法形式。
    • 所有成员都是public的。
    • 没有定义任何构造函数。
    • 没有类内初始值。
    • 没有基类,也没有virtual函数。
struct Data
{
    int ival;
    string s;
};
  • 可以提供一个花括号括起来的成员初始值列表,并用它初始化聚合类的数据成员。
// val1.ival = 0; val1.s = string ("Anna")
Data val1 = {0, "Anna"};
  • 初始值的顺序必须与声明的顺序一致。
//错误:不能使用"Anna"初始化ival,也不能使用1024初始化s
Data val1 = {"Anna", 0};
  • 显式地初始化类的对象的成员存在三个明显的缺点:
    1. 要求类的所有成员都是public的。
    2. 将正确初始化每个对象的每个成员的重任交给了类的用户。因为用户很容易忘掉某个初始值,或者提供一个不恰当的初始值,所以这样的初始化过程冗长乏味且容易出错。
    3. 添加或删除一个成员之后,所有的初始化语句都需要更新。

7.5.6 字面值常量类

  • 但它符合下述要求,则它是一个字面值常量类:
    • 数据成员都必须是字面值类型。
    • 类必须至少含有一个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;    // io错误
    bool other; //其他错误
}
constexpr Debug io_sub(false, true, false); //调试IO
if (io_sub.any())
{
    cerr << "print appropriate error messages" << endl;
}
constexpr Debug prod(false);
if (prod.any())
{
    cerr << "print an error message" << endl;
}

7.6 类的静态成员

  • 有的时候类需要它的一些成员与类本身直接相关,而不是与类的各个对象保持关联。
  • 通过成员的声明之前加上关键词static使得其与类关联在一起。并且可以有访问保护,可以是常量、引用、指针、类类型。
class Account
{
    //静态成员函数也不与任何对象绑定在一起,它们不包含this指针。
    //静态成员函数不能声明成const的,而且我们也不能在static函数体内使用this指针
public:
    void calculate() { amount += amount * interestRate; }
    static double rate() { return interestRate; }
    static void rate(double);

private:
    std::string owner;
    double amount;
    static double interestRate;//只存在于interestRate对象而且它被所有Account对象共享。
    static double initRate();
};
  • 类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。
  • 使用类的静态成员。
double r;
r = Account::rate(); //使用作用域运算访问静态成员
  • 虽然静态成员不属于类的某个对象,但是我们仍然可以使用类的对象、引用或者指针来访问静态成员。
Account ac1;
Account *ac2 = &ac1;
//调用静态成员函数rate的等价形式
r = ac1.rate();  //通过Account对象或引用
r = ac2->rate(); //通过指向Account对象的指针
  • 成员函数不用通过作用域运算符就能直接使用静态成员:
void Account::rate(double newRate)
{
    interestRate = newRate;
}
  • 和类的所有成员一样,当我们指向类外部的静态成员时,必须指明成员所属的类名,static关键字则只出现在类内部的声明语句中。
  • 不是由类的构造函数初始化的,类似于全局变量,静态数据成员定义在任何函数之外,因此一旦被定义,就将一直存在于程序的整个生命周期中。
  • 的静态成员不应该在类的内部初始化,但是我们可以为静态成员提供const整数类型的类内初始值,静态成员必须是字面值常量类型的constexpr的常量表达式。
class Account
{
public:
    static double rate() { return interestRate; }
    static void rate(double);

private:
    static constexpr int period = 30; //常量表达式
    double daily_tbl[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;
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Flame老唐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值