第7章:类

  • #1.定义抽象数据类型
    • 1.1 设计Sales_data类
    • 1.2 定义改进的Sales_data类
    • 1.3 定义类相关的成员函数
    • 1.4 构造函数
    • 1.5 拷贝、赋值和析构
  • #2.访问控制和封装
    • 2.1 友元
  • #3.类的其他特性
    • 3.1 类成员再探
    • 3.2 返回*this的成员函数
    • 3.3 类类型
    • 3.4 友元再探
  • #4.类的作用域
    • 4.1 名字查找和类的作用域
  • #5.构造函数再探
    • 5.1 构造函数初始值列表
    • 5.2 委托构造函数
    • 5.3 默认构造函数的作用
    • 5.4 隐式的类类型转换
    • 5.5 聚合类
    • 5.6 字面值常量类
  • #6.类的静态成员

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

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

类要想实现数据抽象和封装,需要首先定义一个抽象数据类型

#1. 定义抽象数据类型

要想定义抽象数据类型,我们需要定义一些操作以供类的用户使用。一旦类定义了自己的操作,我们就可以封装它的数据成员。

1.1 设计Sales_data类

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

1.2 定义改进的Sales_data类

定义和声明成员函数的方式和普通函数类似差不多。成员函数的声明必须在类的内部,它的定义则既可以在类的内部也可以在类的外部。作为接口组成部分的非成员函数,它们的定义和声明都在类的外部。

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::istream &,Sales_data &);

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

定义成员函数

所有的成员都必须在类的内部声明,但是成员函数体可以定义在类内也可以定义在类外。以Sales_data的成员函数isbn为例来引出this指针:

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

isbn函数返回Sales_data的数据成员bookBo。

引入this

成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化this。例如,如果调用:

total.isbn();

则编译器负责把total的地址传递给isbn的隐式形参this。可以等价地认为编译器将该调用重写成了如下形式:

Sales_data::isbn(&total);

其中,调用Sales_data的isbn成员时传入了total地址。在成员函数内部,我们可以直接使用调用该函数的对象的成员,而不须通过成员访问运算符来做到这一点,因为this所指向的正是这个对象。任何对类的成员的直接访问都被看作this的隐式引用。对于我们来说,this形参是隐式定义的。我们可以把isbn定义成如下形式:

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

因为this的目的总是指向这个对象,所以this是一个常量指针。

引入const成员函数

isbn函数的另一个关键之处是紧随参数列表之后的const关键字,这里,const关键字的作用是修改隐式this指针的类型。默认情况下,this的类型是指向类类型非常量版本的常量指针。C++语言允许我们把const关键字放在成员函数的参数列表之后,此时,紧跟在参数列表后面的const表示this是一个指向常量的指针。像这样使用const的成员函数被称作常量成员函数

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

==常量对象,以及常量对象的引用或指针都只能调用常量成员函数。==

类作用域和成员函数

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

在类的外部定义成员函数

当我们在类的外部定义成员函数时,成员函数的定义必须与它的声明匹配,同时,类外部定义的成员的名字必须包含它所属的类名:

double Sales_data::avg_price() const {
    if (units_sold) {
        return revenue / units_sold;
    }else {
        return 0;
    }
}
定义一个返回this对象的函数
Sales_data &Sales_data::combine(const Sales_data &rhs) {
    units_sold += rhs.units_sold; //把rhs的成员加到this对象的成员上
    revenue += rhs.revenue;
    return *this; //返回调用该函数的对象
}

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

我们定义非成员函数的方式和其他函数一样,通常把函数的声明和定义分离开来。如有函数在概念是属于类但是不定义在类中,则它一般应与类声明在同一个头文件内。

定义read和print函数
std::istream &read(std::istream &is, Sales_data &item) {
    double price = 0.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 << " " << item.avg_price();
    return os;
}
定义add函数
Sales_data add(const Sales_data &lhs, const Sales_data &rhs) {
    Sales_data sum = lhs;
    sum.combine(rhs);
    return sum;
}

1.4 构造函数

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

构造函数的名字与类名相同。和其他函数不一样的是,构造函数没有返回类型;构造函数不能被声明成const。

合成的默认构造函数

类通过一个一个特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数。默认构造函数无须任何实参。编译器创建的构造函数又被称为合成的默认构造函数

==只有当类没有声明任何构造函数时,编译器才会自动生成默认构造函数。==

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

对于一个普通的类来说,必须定义它自己的默认构造函数,原因有三:

  1. 只有当类没有声明任何构造函数时,编译器才会自动地生成默认构造函数。一旦在类中定义了其他构造函数,除非我们再定义默认构造函数,否则类没有默认构造。
  2. 对于某些类而言,合成的默认构造可能执行错误的操作。如果类中的内置类型或复合类型的对象被默认初始化,它们的值将是未定义的。
  3. 编译器有时候不能为类合成默认的构造函数。例如:类中包含了一个其他类型的成员,而这个成员没有默认构造函数。
定义Sales_data的构造函数
struct Sales_data {
    //C++11新标准中,如果我们需要默认的行为,可以在参数列表后写上=default来要求编译器生成默认构造
    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::iostream &);
    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的含义

在C++11新标准中,如果我们需要默认的行为,可以通过在参数列表后写上=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) {}

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

在类的外部定义构造函数

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

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

当我们在类的外部定义构造函数时,必须指明该构造函数是哪个类的成员。因此,需要指定具体作用域。

1.5 拷贝、赋值和析构

除了定义类的对象如何初始化之外,类还需要控制拷贝、赋值和销毁对象时发生的行为。

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

尽管编译器能替我们合成拷贝、赋值和销毁的操作,但是必须清楚的一点是,对于某些类来说合成的版本无法正常工作。


#2. 访问控制与封装

c++语言中,使用访问说明符加强类的封装性:

  • 定义在public说明符之后的成员在整个程序内可被访问,public成员定义类的接口。
  • 定义在private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了类的实现细节。
使用class或struct关键字

使用class和struct定义类唯一的区别就是默认的访问权限。使用struct关键字,则定义在第一个访问说明符之前的成员是public;如果使用class关键字,则成员是private的。

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

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::istream &, Sales_data &);
public:
    Sales_data() = default;
    Sales_data(const std::string &s, unsigned n, double p) :bookNo(s), units_sold(n), revenue(n*p) {}
    Sales_data(const std::string &s) :bookNo(s) {}
    Sales_data(std::iostream &);
    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::istream &, Sales_data &); 

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

友元的声明

友元的声明仅仅指定了访问的权限,而非一个通常意义上的函数声明。如果我们希望用户能够调用某个友元函数,那么我们必须在友元声明之外再专门对函数进行一次声明。


#3. 类的其他特性

3.1 类成员再探

定义一个类型成员
class Screen {
public:
    typedef std::string::size_type pos; //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(pos ht,pos wd) const; //显示内联
    Screen &move(pos r,pos c);

private:
    pos cursor = 0;
    pos height = 0,width = 0;
    std::string contents;
};
令成员作为内联函数

在类中,常有一些规模较小的函数适合于被声明成内联函数。定义在类内部的函数是自动inline的。我们也可以在类的外部用inline关键字修饰函数的定义:

inline Screen &Screen::move(pos r,pos c) {
    pos row = r * width; //计算行的位置
    cursor = row + c; //在行内将光标移动到指定的列
    return *this; //以左值的形式返回对象
}

char Screen::get(pos r,pos c) const { //在类的内不声明成inline
    pos row = r * width; //计算行的位置
    return contents[row + c]; //返回给定列的字符
}
重载成员函数

和非成员函数一样,成员函数也可以被重载。只要函数之间在参数的数量和\或类型上有区别就行。

可变数据成员

有时会发生这样一种情况,我们希望修改类的某个数据成员,即使是在一个const成员函数内。可以通过在变量的声明中加入mutable关键字做到这一点。

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

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

3.2 返回*this的成员函数

class Screen {
public:
    typedef std::string::size_type pos;
    Screen &set(char);
    Screen &set(pos,pos,char);
private:
    pos cursor;
    pos height = 0, width = 0;
    std::string contents;
};
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成员的返回值是调用set的对象的引用。返回引用的函数是左值的,意味着这些函数返回的是对象本身而非对象的副本。

从const成员函数返回*this

==一个const成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用。==

基于const的重载

通过区分成员函数是否是const的,我们可以对其进行重载。

class Screen {
public:
    //根据对象是否是const重载了display函数
    Screen &display(std::ostream &os){
        do_display(os);
        return *this;
    }
    const Screen &display(std::ostream &os) {
        do_display(os);
        return *this;
    }

private:
    //该函数负责显示Screen的内容
    void do_display(std::ostream &os) const {
        os << contents;
    }
};

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来说,在它声明之后定义之前是一个不完全类型

3.4 友元再探

类还可以把其他类定义成友元,也可以把其他类的成员函数定义成友元。此外,友元函数能定义在类的内部,这样的函数是隐式内联的。

类之间的友元关系
class Screen {
    //Window_mgr的成员可以访问Screen类的私有部分
    friend class Window_mgr;
};

如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。
每个类负责控制自己的友元类和友元函数。

令成员函数作为友元

除了令整个类作为友元之外,还可以只为类的某个成员函数提供访问权限。

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对象的私有部分
    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的声明在作用域中了

#4. 类的作用域

每个类都会定义它自己的作用域。在类的作用域之外,普通的数据和函数成员只能由对象、引用或者指针使用成员访问运算符来访问。对于类类型成员则使用作用域运算符访问。

4.1 名字查找与类的作用域

在目前为止,我们编写的程序中,名字查找的过程比较直截了当:

  • 首先,在名字所在的块中寻找其声明的语句,只考虑在名字的使用之前出现的声明。
  • 如果没找到,继续查找外层作用域。
  • 如果最终没有找到匹配的声明,则程序报错。
    对于定义在类外部的成员函数来说,解析其中名字的方式和上述查找规则有所区别。类的定义分两步处理:
  • 首先,编译成员的声明。
  • 直到类全部可见后才编译函数体。

==编译器处理完类中的全部声明后才会处理成员函数的定义。==

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

这种两阶段的处理方式只使用与成员函数中使用的名字。如果某个成员的声明使用了类中尚未出现的名字,则编译器将在定义该类的作用域中继续查找。例如:

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

当编译器看到balance函数的声明语句时,它将在Account类的范围内寻找对Money的声明。如果没找到,编译器会接着到Account的外层作用域中查找。

类型名要特殊处理

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

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

#5. 构造函数再探

5.1 构造函数初始值列表

如果没有在构造函数的初始值列表中显式地初始化成员,则该成员将在函数体之前执行默认初始化。例如:

//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) : i(ii),ci(ii),ri(i) {}
private:
    int i;
    const int ci;
    int &ri;
};

和其他常量对象或者引用一样,成员ci和ri都必须被初始化。因此,如果我们没有为它们提供构造函数初始值的话将引发错误:

ConstRef::ConstRef(int ii) {
    //赋值
    i = ii; //正确
    ci = ii; //错误:不能给const赋值
    ri = ii; //错误:ri没被初始化
}
//正确:显示地初始化引用和const成员
ConstRef::ConstRef(int ii) :i(ii),ci(ii),ri(i) {}
成员初始化顺序

成员的初始化顺序与它们在类定义中的出现顺序一致:第一个成员先被初始化,然后第二个,依次类推。构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序。

class X {
private:
    int i;
    int j;
public:
    //i的值为undefined,i在j之前初始化
    X(int val) :j(val), i(j) {}
};

==最好令构造函数的初始值顺序和成员声明的顺序保持一致。而且如果可能的话,尽量避免使用成员初始化其他成员。==

默认实参和构造函数

Sales_data默认构造函数的行为与只接受一个string实参的构造函数差不多。唯一的区别是接受string实参的构造函数使用这个实参初始化bookNo,而默认构造函数使用string的默认构造函数初始化bookNo。

class Sales_data {
public:
    //定义默认构造函数,令其与只接受一个string实参的构造函数功能相同
    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);
    }
};

==如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。==

5.2 委托构造函数

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

5.3 默认构造函数的作用

当对象被默认初始化或值初始化时自动执行默认构造函数。默认初始化在以下情况下发生:

  • 当我们在块作用域内不使用任何初始值定义一个非静态变量或数组时。
  • 当一个类本身含有类类型的成员且使用合成的默认构造函数时。
  • 当类类型的成员没有在构造函数初始值列表显式的初始化时。

值初始化在以下情况发生:

  • 在数组初始化的过程中如果我们提供的初始值数量少于数组的大小时。
  • 当我们不使用初始值定义一个局部静态变量时。
  • 当我们通过书写表达式显示地请求值初始化时,其中T是类型名。
class NoDefault {
public:
    NoDefault(const std::string&);
};
struct A {
    NoDefault my_mem; //默认情况下my_mem是public的
};
A a; //错误:不能为A合成构造函数
struct B {
    B(){} //错误:b_member没有初始值
    NoDefault b_member;
};

==在实际中,如果定义了其他构造函数,那么最好也提供一个默认构造函数。==

使用默认构造函数
Sales_data obj(); //错误:声明一个函数而非对象
Sales_data obj2; //正确:obj2是一个对象而非函数

5.4 隐式的类类型转换

如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,有时我们把这种构造函数称为转换构造函数

只允许一步类类型转换

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

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

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

//正确:显示地转换成string,隐式地转换成Sales_data
item.combine(string("9-999-99999-9"));
//正确:隐式地转换成string,显示地转换成Sales_data
item.combine(Sales_data("9-999-99999-9"));
抑制构造函数定义的隐式转换

在要求隐式转换的程序上下文中,我们可以通过将构造函数声明为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 &);
private:
    std::string bookNo;
    unsigned units_sold;
    double revenue;
};

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

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

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

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

发生隐式转换的一种情况是当我们执行拷贝形式的初始化时。此时,我们只能使用直接初始化而不能使用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));

5.5 聚合类

聚合类使得用户可以直接访问其成员,并且具有特殊的初始化语法形式。当一个类满足如下条件时,我们说它是聚合的:

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

例如,下面类是一个聚合类:

strcuct Data {
    int ival;
    string s;
};

5.6 字面值常量类

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

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

  • 数据成员都必须是字面值类型。
  • 类必须含有一个constexpr构造函数。
  • 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数。
  • 类必须使用析构函数的默认定义,该成员负责销毁类的对象。
constexpr构造函数

尽管构造函数不能是const的,但是字面值类型的构造函数可以是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构造函数必须初始化所有数据成员,初始值或者使用constexpr构造函数,或者是一条常量表达式。


#6. 类的静态成员

有的时候类需要它的一些成员与类本身直接相关,而不是与类的各个对象保持关联。

声明静态成员

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

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

类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。

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

使用静态成员

我们使用作用域运算符直接访问静态成员:

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

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

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; //period是常量表达式
    double daily_tbl[period];
};
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • 使用类定义自己的数据类型 本章主要关注数据抽象→将对象的具体实现与对象所能执行操作分离开来 零、术语表 聚合类 类...
    菜鸡也会飞阅读 238评论 0 1
  • 数据抽象:是一种依赖于接口和实现分离的变成技术。 封装:分离接口(用户所能执行的操作)和实现(数据成员、实现接口的...
    咸鱼翻身ing阅读 199评论 0 0
  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy阅读 9,500评论 1 51
  • 这是16年5月份编辑的一份比较杂乱适合自己观看的学习记录文档,今天18年5月份再次想写文章,发现简书还为我保存起的...
    Jenaral阅读 2,727评论 2 9
  • 二十三儿 喝面叶儿 其实不是说特别喜欢某种食物 像二十三的面叶儿,年三十的饺子,只是特定的时间吃特定的食物已经成了...
    欢喜_b254阅读 209评论 0 0