C++ primer学习笔记——第七章 类

类的基本思想是数据抽象封装

数据抽象是一种依赖于接口实现分离的编程技术。接口包括用户所能执行的操作;实现则包括类的数据成员、负责接口实现的函数体以及实现类所需的各种私有函数。

封装后的类隐藏了它的实现细节,实现了类的接口和实现的分离。

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

一、定义抽象数据类型

1、设计Sales_data类

2、定义改进的Sales_data类

成员函数的声明必须在类的内部,它的定义既可以在类的内部也可以在类的外部。作为接口组成部分的非成员函数,例如add、read和print等,他们的定义和声明在类的外部:

using namespace std;

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

};
//Sales_data的非成员接口函数
Sales_data add(const Sales_data&,const Sales_data&);
ostream &print(ostream&, const Sales_data&);
istream &read(istream&,Sales_data&);

定义在类内部的函数时隐式的inline函数。

引入this

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

对于我们来说,this形参是隐式定义的:

//没有必要在内部使用this
std::string isbn() const (return this->bookNo;)

因为this的目的总是指向“这个对象”,所以this是一个常量指针,我们不允许改变this中保存的地址。

引入const成员函数

参数列表之后的const关键字用来修改隐式this指针的类型。

默认情况下,this的类型是指向类类型非常量版本的常量指针。默认情况下,我们不能把this绑定到一个常量对象上,这一情况使得我们不能在一个常量对象上调用普通的成员函数。将this设置为指向常量的指针有助于提高函数的灵活性。

此时,紧跟在参数列表后面的const表示this是一个指向常量的指针。像这样使用const的成员函数被称作常量成员函数

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

通常,如果函数只是读取数据或成员的值,而不做任何改变,这时的成员函数应该定义成const的

类作用域和成员函数

编译器分两步处理类:首先编译成员的声明,然后才轮到成员函数体(如果有的话)。因此,成员函数体可以随意实用类中的其他成员而无须在意这些成员出现的次序。

在类的外部定义成员函数

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

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;                 //返回调用该函数的对象
}

调用一个返回引用的函数得到左值,其他返回类型得到右值

3、定义类相关的非成员函数

尽管非成员函数定义的操作从概念上来说属于类的接口的组成部分,但是实际上它们并不属于类本身。

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

4、构造函数

类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数

构造函数的名字和类相同。和其他函数不一样的是,构造函数没有返回类型。

不同于其他成员函数,构造函数不能被声明成const的。

合成的默认构造函数

类通过一个特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数。默认构造函数无须任何实参。

编译器创建的构造函数又被称作合成的构造函数函数。

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

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

第一,只有当类没有声明任何构造函数时,编译器才会自动地生成默认构造函数。如果我们定义了其他构造函数,那么也必须定义一个默认构造函数。

第二,如果类包含有内置类型或者符合类型的成员,则只有当这些成员全都被赋予了类内的初始值时,这个类才适合于使用合成的默认构造函数。

第三,有的时候编译器不能为某些类合成默认的构造函数。比如类内包含一个没有默认构造函数的类类型的成员,编译器无法初始化该成员。

定义Sales_data的构造函数

using namespace std;
struct Sales_data{
    //新增的构造函数
    Sales_data()=default;
    Sales_data(const string &s):bookNo(s){}
    Sales_data(const string &s,unsigned n,double p):
               bookNo(s),units_sold(n),revenue(p*n){}
    Sales_data(istream &);
    //之前已有的成员
	string isbn() const{ return bookNo; }
	Sales_data& combine(const Sales_data&);
	double avg_price() const;
	string bookNo;
	unsigned units_sold = 0;
	double revenuw = 0.0;
};

在C++11新标准中,如果我们需要默认的行为,那么可以通过在参数列表后面写上=default来要求编译器生成构造函数。

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

上面的默认构造函数之所以对Sales_data有效,是因为我们为内置类型的数据成员提供了初始值。如果你的编译器不支持类内初始值,那么你的默认构造函数就应该使用构造函数初始值列表来初始化类的每个成员。

构造函数初始值列表

构造函数定义中冒号以及冒号和花括号之间的部分,叫做构造函数初始值列表,它负责为新创建的对象的一个或几个数据成员赋初值。

构造函数不应该轻易地覆盖掉类内的初始值,除非新赋的值与原值不同。如果不能使用类内初始值,则所有构造函数都应该显式的初始化每个内置类型的成员。

在类的外部定义构造函数

在类的外部定义构造函数时,必须指明该构造函数是哪个类的成员:

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

5、拷贝、赋值和析构

如果我们不主动定义这些操作,则编译器将替我们合成他们。

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

二、访问控制和封装

在C++语言中,我们使用访问说明符来加强类的封装性:

  • 定义在public说明符之后的成员在整个程序内可被访问,public成员定义类的接口
  • 定义在private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装(即隐藏了)类的实现细节。
using namespace std;
class Sales_data{
public:
    Sales_data()=default;
    Sales_data(const string &s):bookNo(s){}
    Sales_data(const string &s,unsigned n,double p):
               bookNo(s),units_sold(n),revenue(p*n){}
    Sales_data(istream &);
	string isbn() const{ return bookNo; }
	Sales_data& combine(const Sales_data&);
private:
	double avg_price() const
        {return units_sold?revenue/units_sold:0;}
	string bookNo;
	unsigned units_sold = 0;
	double revenuw = 0.0;
};

public后面:构造函数、一部分成员函数

private后面:数据成员,作为实现部分的函数

使用class或struct关键字

如果我们使用struct关键字,则定义在第一个访问说明符之前的成员是public的,相反,如果我们使用class关键字,则这些成员是private的。

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

1、友元

类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元

using namespace std;
class Sales_data{
//为Sales_data的非成员接口函数所做的友元声明
friend Sales_data add(const Sales_data&,const Sales_data&);
friend ostream &print(ostream&, const Sales_data&);
friend istream &read(istream&,Sales_data&);

public:
    Sales_data()=default;
    Sales_data(const string &s):bookNo(s){}
    Sales_data(const string &s,unsigned n,double p):
               bookNo(s),units_sold(n),revenue(p*n){}
    Sales_data(istream &);
	string isbn() const{ return bookNo; }
	Sales_data& combine(const Sales_data&);
private:
	double avg_price() const
        {return units_sold?revenue/units_sold:0;}
	string bookNo;
	unsigned units_sold = 0;
	double revenuw = 0.0;
};
//Sales_data的非成员接口函数的声明
Sales_data add(const Sales_data&,const Sales_data&);
ostream &print(ostream&, const Sales_data&);
istream &read(istream&,Sales_data&);

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

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

封装有两个重要的优点:

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

友元的声明

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

为了使友元对类的用户可见,我们通常把友元的声明与类本身放置在同一个头文件中。

三、类的其他特性

定义一个类型成员

class Screen{
public:
    using pos=std::string::size_type;
    //其他成员
    ...
}

用来定义类型的成员必须先定义后使用,这一点与普通成员有所区别

令成员作为内联函数

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

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

定义在类内部的成员函数是自动inline的,我们也可以在类内部显式的用inline声明成员函数,同样的,也能在类的外部用inline关键字修饰函数的定义;

虽然我们可以在声明和定义的地方同时声明inline,但是最好只在类外部定义的地方说明inline,这样可以使类更加容易理解。

重载成员函数

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

可变数据成员

在变量的声明中加入mutable关键字,说明该成员是可变成员,在任何成员函数中,即使是const函数中也能改变它的值。

类数据成员的初始值

当我们提供一个类内初始值时,必须以符号=或者花括号表示。

2、返回*this的成员函数

move成员函数返回值是调用move的对象的引用。返回引用的函数是左值的,意味着这些函数返回的是对象本身而非对象的副本

从const成员函数返回*this

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

一个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)  const
                    {do_display(os);return *this;}
private:
    void do_display(std::ostream &os) const {os<<contents;}
    //其他成员与之前版本一致
};

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

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

3、类类型

即使两个类的成员列表完全一样,它们也是不同的类型。对于一个类来说,它的成员和其他任何类(或者任何其他作用域)的成员都不是一回事。

类的声明

我们可以仅仅声明类而暂时不定义它,这种声明被称作前向声明,在它声明之后定义之前是一个不完全类型

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

class Screen;  //Screen类的声明

对于一个类来说,在我们创建它的对象之前该类必须被定义过,而不能仅仅被声明。类似,类必须首先被定义,然后才能用引用或者指针访问其成员。

3、友元再探

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

类之间的友元关系

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

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

class Window_mgr{
public:
    //窗口中每个屏幕的编号
    using ScreenIndex=std::vector<Screen>::size_type;
    //按照编号将指定的Screen重置为空白
    void clear(ScreenIndex);
private:
    std::vector<Screen> screens{Screen(24,80,' ')};
};

void Window_mgr::clear(ScreenIndex i)
{
    //s是一个Screen的引用
    Screen &s=screens[i];
    //将那个选定的Screen重置为空白
    s.contents=string(s.height*s.width,' ');
}

必须要注意的一点,友元关系不存在传递性。也就是说,如果Window_mgr有它自己的友元,则这些友元并不能理所当然地具有访问Screen的特权

令成员函数作为友元

class Screen{
    //Window_mgr::clear必须在Screen类之前被声明
    friend void Window_mgr::clear(ScreenIndex);
    //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的声明在作用域中了

四、类的作用域

当我们在类的外部定义成员函数时必须同时提供类名和函数名。一旦提供了类名,定义的剩余部分就在类的作用域之内了,这里的剩余部分包括参数列表和函数体。结果就是,我们可以直接使用类的其他成员而无需再次授权了。

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

函数的返回类型通常出现在函数名之前。因此当成员函数定义在类的外部时,返回类型中使用的名字都位于作用域之外。这时,返回类型必须指明它是哪个类型的成员:

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

1、名字查找与类的作用域

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

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

声明中使用的名字,包括返回类型或者参数列表中使用的名字,都必须在使用前确保可见。如果某个成员的声明使用了类中尚未出现的名字,则编译器将会在定义该类的作用域中继续查找。

类型名要特殊处理

在类中,如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字

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

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

  • 首先,查找成员函数内的名字
  • 再在类内继续查找
  • 最后在成员函数定义之前的作用域内继续查找

一般来说,不建议使用其他成员的名字作为某个成员函数的参数

//通常情况下不建议为参数和成员使用同样的名字
int height;
class Screen{
public:
    typedef std::string::size_type pos;
    void dummy_fcn(pos height) {
        cursor=width*height;   //哪个height?是那个参数
    }
private:
    pos cursor=0;
    pos height=0,width=0;
};

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

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

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

尽管外层的对象被隐藏掉了,但是我们仍然可以用作用域运算符访问它:

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

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

当成员定义在类的外部时,名字查找的第三步不仅要考虑类定义之前的全局作用域中的声明,还要考虑在成员函数定义之前的全局作用域中的声明。

五、构造函数再探

1、构造函数初始值列表

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

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

如果成员是const、引用,或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初始值

class ConstRef{
public:
    ConstRef(int ii):i(ii),ci(ii),ri(i) {}
private:
    int i;
    const int ci;   //const成员必须初始化
    int &ri;        //const成员必须初始化
};

成员初始化的顺序

显然,在构造函数初始值中每个成员只能出现一次。

构造函数初始值列表只说明用于初始化成员的值,而不限定初始化的具体执行顺序。

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

默认实参和构造函数

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

class Sales_data{
public:
    //定义默认构造函数,令其与只接受一个string实参的构造函数功能相同
    Sales_data(std::string s=' '):bookNo(s) { }
    //其他构造函数与之前一致
    Sales_data(const string &s,unsigned n,double p):
               bookNo(s),units_sold(n),revenue(p*n){}
    Sales_data(istream &) {read(is,*this);}
    
};

3、委托构造函数

一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它自己的一些(或者全部)职责委托给了其他构造函数

class Sales_data{
public:
    //非委托构造函数使用对应的实参初始化成员
    Sales_data(const string &s,unsigned n,double p):
               bookNo(s),units_sold(n),revenue(p*n){}
    //其余构造函数全部委托给另一个构造函数
    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);}
    //其他成员与之前版本一致
};

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

3、默认构造函数的作用

在实际中,如果定义了其他构造函数,那么最好也提供一个默认构造函数。否则可能会出现错误:

class NoDefault{
public:
    NoDefault(const std::string&);
    //还有其他成员,但是没有其他构造函数了    

};
struct A{
    NoDefault my_eme;
};
A a;        //错误:不能为A合成构造函数

struct B{
    B() {}      //错误:b_member没有初始值
    NoDefault b_member;
};

使用默认构造函数

注意以下两种区别:

Sales_data obj();         //声明了一个函数而非对象
Sales_data obj2;          //obj2是一个对象而非函数

4、隐式的类类型转换

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

需要多个实参的构造函数不能用于隐式转换

string null_book="9-999-99999-9";
item.combine(null_book);

编译器用给定的string自动创建了一个Sales_data对象,新生成的这个(临时)Sales_data对象被传递给combine

只允许一步类类型转换

下面的代码隐式地使用了两种转换规则,所以它是错误的:

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

如下改正则是正确的:

item.combine(string("9-999-99999-9"));     //显式地转换成string,隐式地转换成Sales_data
item.combine(Sales_data("9-999-99999-9"));   //隐式的转换成string,显式地转换成Sales_data

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

在要求隐式转换的程序上下文中,我们可以通过将构造函数声明为explicit加以阻止。

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

using namespace std;
class Sales_data{
public:
    Sales_data()=default;
    Sales_data(const string &s,unsigned n,double p):
               bookNo(s),units_sold(n),revenue(p*n){}
    explicit Sales_data(const string &s):bookNo(s){}
    explicit Sales_data(istream &);
    //其他成员与之前版本一致
};

item.combine(null_book);   //错误:不可隐式转换
item.combine(cin);         //错误:不可隐式转换

explicit构造函数只能用于直接初始化,而不能用于拷贝形式的初始化

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

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

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

举个例子:

//如果vector不是explicit的,下面的语句将令人困惑
int getSize(const std::vector<int>&);
getSize(34);

//对string却可以不用是explicit
void setYourName(std::string); // 声明
setYourName("pezy"); // 正确

5、聚合类

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

聚合类满足以下条件:

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

例如:

struct Data{ 
    int ival;
    string s;
};

可以提供一个花括号括起来的成员初始值列表,并用它初始化聚合类的数据成员,初始值的顺序必须与声明的顺序一致:

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

6、字面值常量类

六、类的静态成员

静态成员与类本身直接相关,而不是与类的各个对象保持关联

声明静态成员

使用static关键字声明静态成员:

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

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

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

使用类的静态成员

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

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 interestRate;}
    static void rate(double);
private:
    static constexpr int period=30;   //period是常量表达式
    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;
};

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

猜你喜欢

转载自blog.csdn.net/oil_you/article/details/82778030