代码改变世界

白话C++系列(31) -- static

2016-06-28 21:01  Keiven_LY  阅读(669)  评论(0编辑  收藏  举报

static

前面的课程我们介绍了:普通的数据成员和普通的成员函数的定义方法,又介绍了const关键字,并且讲解了用const关键字来修饰数据成员和成员函数,我们把这样的数据成员和成员函数称之为常数据成员和常成员函数。

这节课,我们再为大家介绍一个关键字:static(静态的)。在C++中提到静态,就不得不提到两个基本概念:静态数据成员和静态成员函数。我们以一个例子来看一下静态数据成员和静态成员函数的定义方法。

这里,我们定义了一个Tank类(坦克),在这个Tank类中,我们定义了一个普通的数据成员(string m_strCode;),那么如何去定义一个静态数据成员呢?那就是,就在数据成员的前面加上关键字static即可(如:static int s_iCount;),此时我们就称这个数据成员m_iCount为静态数据成员。又如何去定义一个静态成员函数呢?我们就在成员函数的前面加上关键字static即可(如:static int getCount() { return s_iCount; })。

问题:那么什么情况下我们要用到静态数据成员和静态成员函数呢?我们来描述一个场景。

大家都玩过坦克大战的游戏吧,你会发现当自己方的坦克非常多的时候,每辆坦克作战就会很英勇;当自己方坦克比敌方少很多的时候,就会变得很胆怯。那么,这样的话,你就希望每辆坦克作为对象来说,都能够知道自己方还有多少辆坦克的存在,那么此时我们就需要一个静态变量来记录这个值,这个值在我们的这个例子中就是m_iCount。

作为静态变量来说,它并不依赖于对象,而是依赖于类。这如何来理解呢?如果我们不实例化对象,那么作为静态的数据成员s_iCount仍然在内存中是存在的,这个也是静态数据成员与普通数据成员最大的区别。如果是普通的数据成员,则必须要实例化之后,这个数据成员在内存中才能够存在。

那么对于静态数据成员来说,因为它并不依赖于对象的实例化,所以静态的数据成员并不在构造函数中去实例化,它的实例化过程往往是单独进行的。如(int Tank::m_iCount = 0;)请大家注意:为静态数据成员初始化的时候,不要再加static关键字了,而直接写成:类型+类名+数据成员的名称+初值。此外,我们对于这个类来说,大家也可以看一看所书写的算法:我们定义了一个s_iCount(表示坦克的数量),刚初始化的时候坦克数量为0,如果我们将“s_iCount++”写在构造函数中,将“s_iCount--”写在析构函数中,那么,每当我们实例化一个坦克对象的时候,坦克数量就会增加一个;每当销毁一辆坦克的时候,坦克数量就会减少一个;而作为每个对象来说,都可以通过直接访问s_iCount来获取到自己同伴的数量。那么,访问的时候,作为访问方法来说又有两种:一种是不通过对象,而是直接通过类的访问方法;两一种是,如果实例化了一个对象,也可以通过这个对象来访问的方法。如下所示:

下面我们再来从内存当中给大家强调一下普通数据成员和静态数据成员究竟有什么区别。

我们还是以Tank这个类为例,当我们通过Tank这个类实例化t1、t2、t3和t4这4个对象之后,作为普通数据成员code就分别随着t1、t2、t3和t4的产生而诞生了,并且诞生了4个code,每个code都有自己的编号。可是,在这4个对象诞生之前s_iCount就已经诞生了,而且只诞生一次。即:t1、t2、t3和t4这4个对象产生的过程当中,s_iCount的值会变化,但是s_iCount这个静态数据成员的个数不会发生变化

在前面的例子当中,我们是使用的普通的数据成员调用静态的数据成员或成员函数,那么,反之,用静态成员函数去调用普通的数据成员或者是普通的成员函数是不是一样成立呢??答案是:不可行的!!如果这样来写,编译器就会报错!!从逻辑上来讲,静态的数据成员和静态的成员函数都是随类的产生而产生,也就是说,其是依赖于类的;而普通的数据成员是依赖于对象的。如果,一个对象如果产生的话,那么我们在静态的成员函数当中去调用非静态的数据成员,显然是会失败的。因为人家一个对象还没有呢,这就是一个时机的问题。而从原理上来说,也是不成立的。

this指针来谈静态成员函数

我们修改一下Tank这个类,如下:

我们在这里定义了一个fire()的成员函数,还定义了一个静态的成员函数getCount(),另外,还定义了一个普通的数据成员m_strCode和一个静态的数据成员s_iCount。当我们通过fire()函数去调用普通的数据成员m_strCode和静态的数据成员s_iCount的时候,这里fire()函数虽然看上去没有传入参数,实际上它却传了一个隐形的this指针,通过这个隐形的this指针,我们就知道当前所要调用的是哪一个对象对应的数据成员或者是成员函数了。而在调用静态数据成员或静态成员函数的时候,因为它并不与对象相关,而只是与类相关,换句话说,它是一个全局的变量,那么在调用时前面不加this也无所谓,用不着区分,直接修改它的值或者去调用相应的静态成员函数。反之,如果我们使用的是静态的成员函数,那么作为静态的成员函数来说,它并不会传入一个隐形的this指针,那么这个时候你又怎么知道你所要调用的数据成员究竟是哪一个对象的数据成员呢。所以,在静态的成员函数当中,无法调用非静态的数据成员或者非静态的成员函数,可是,我们却可以在静态的成员函数当中去调用静态的数据成员,因为我们可以将静态的成员函数看作是一个全局函数,而静态的数据成员也是一个全局的数据,所以,如果通过静态的成员函数去调用一个静态的数据成员是没有问题的,而当调用一个非静态的数据成员时,就会因为this指针找不到,无法确定其是哪个对象的数据成员而造成编译时报错。

下面介绍一下静态数据成员和静态成员函数的注意事项。

  • 静态数据成员必须单独初始化(因为其并不随着对象的产生而产生,它是随着类的产生就已经产生了,所以说,类产生之后,对象还没有进行实例化时,它就应该已经具有一个初值了,所以它不能够写到构造函数当中去,而只能写到类的外边,直接进行初始化)。
  • 静态成员函数不能调用非静态数据成员和非静态成员函数(反之,非静态的成员函数则可以调用静态的数据成员和静态的成员函数)。
  • 静态数据成员只有一份,并且不依赖于对象而存在(这就是说,如果我们通过sizeof去求一个对象的大小时,那么,请大家记住,它一定不包含静态数据成员的)。

静态代码实践

题目描述:

/*  ***********************************************  */

/*  静态数据成员与静态成员函数

要求:定义Tank类

    数据成员:坦克编号(m_cCode),坦克数量:s_iCount(这是一个静态的数据成员)

        成员函数:构造函数,析构函数,fire,getCount(这是一个静态成员函数)

/*  ***********************************************  */

程序框架:

头文件(Tank.h

#ifndef TANK_H
#define TANK_H
class Tank
{
public:
    Tank(char code);
    ~Tank();
    void fire();
    static int getCount();//定义getCount函数为静态成员函数
private:
    static int s_iCount;//定义s_iCount为静态数据成员
    char m_cCode;
};

#endif

源程序(tank.cpp

#include<iostream>
#include"Tank.h"

using namespace std;

intTank::s_iCount = 0; //首先对s_iCount进行了初始化,注意,这是在构造函数的前面进行了初始化

Tank::Tank(charcode)
{
    m_cCode = code;
    s_iCount++; 
    cout <<"Tank()"<< endl;
}

Tank::~Tank()
{
    s_iCount--;
    cout <<"~Tank()"<< endl;
}
void Tank::fire()
{
    cout <<"Tank --> fire()"<< endl;
}
intTank::getCount() //getCount函数虽然是静态成员函数,但是在这里定义的时候不需要再加上static关键字了
{
    return s_iCount;
}

当我们做完这些之后,就可以在demo.cpp文件中去使用它们了。

如何来使用呢??我们先来尝试一下直接访问静态成员函数如下:

#include<iostream>
#include"stdlib.h"
#include"Tank.h"

using namespace std;

int main()
{
    cout <<Tank::getCount() << endl;

    system("pause");
    return 0;
}

这里直接访问getCount的结果就是将坦克数量s_iCount的值直接返回回来,而s_iCount在初始化的时候已经初始化为0了,所以在这返回的应该就是0。运行结果如下:

我们看到,打印出来的结果的确是0。也许你会说,这个0会不会是某一个初值或者说是一种巧合呢?那么,我们将s_iCount的初始值设为10,再来看一看结果如何?

我们看到,打印出来的结果也是10。可见,这个初始化的动作是非常靠前的,它会在类的编译之初就已经将静态的数据成员全部初始化好了。当我们直接去访问的时候,我们所访问到的结果就是我们初始化的数据。

下面我们来实例化一个坦克的对象t1,并且传入一个坦克编号(’A’),这个还不是重点,重点是当我们实例化了一个对象之后,我们再去调用静态成员函数getCount的时候,我们来看一看这个结果:

int main()
{
    Tank t1('A');
    cout <<Tank::getCount() << endl;

    system("pause");
    return 0;
}

结果如下:

我们看到,打印出了一行“Tank()”,说明构造函数得以执行,并且执行了那一行(s_iCount++;)的操作,因为刚刚我们将s_iCount的初始值改成了10,所以当s_iCount++之后,它的值就变成了11

 不仅如此,我们还可以通过对象t1来直接访问getCount函数,访问的时候用(.)来访问,如下:

int main()
{
    Tank t1('A');
    cout << t1.getCount() << endl;

    system("pause");
    return 0;
}

我们按F5来看一看运行结果:

我们看到,打印出来的结果也是11,跟之前是一样的。可见,无论是通过类名+冒号(tank::)的方式,还是通过对象+点号(t1.)的方式,两者都可以访问静态的数据成员以及静态的成员函数(这里仅以静态的成员函数为例)。

接下来,我们来看一看,当执行完析构函数之后,它的数量有没有减少。为了能够体现出来,我们需要在堆上去实例化一个坦克的对象,如下:

int main()
{
    Tank *p = new Tank('A');
    cout <<Tank::getCount() << endl;
    Tank *q = new Tank('B');
    cout << q->getCount() << endl;
    delete p;
    delete q;
    cout <<Tank::getCount() << endl;
    system("pause");
    return 0;
}

我们按F5看一看运行结果如下:

我们看到,一开始大衣橱“Tank()”,说明Tank的构造函数执行了一次,此时我们去调用getCount函数的时候,我们发现打印出的是11,然后我们又实例化了一个对象,然后打印出的是12,接下来由于分别销毁了对象p和对象q,所以执行了两次析构函数,打印出了两行“~Tank()”,再去调用getCount函数的时候,此时坦克的数量就又回到了初始状态10

好了,关于静态数据成员以及静态成员函数的定义及其使用方法就讲到这里,还有一些内容是需要给大家提醒的。

首先,我们来看一看,对于静态的成员函数来说,能不能加const这个关键字呢??只要大家稍微想一想前面讲到的this指针的相关知识,就可以知道,这是不可以的。这是为什么呢?因为,这是因为,加上const的本质就是给其隐形的this指针加了const,而作为静态的成员函数来说,其根本就没有this指针,所以const嫁给谁呢,肯定是不妥的。为了能够让大家看清楚这一点,我们就不见棺材不掉泪。

修改头文件(Tank.h)中的getCount函数如下:

staticint getCount() const;

修改源程序(Tank.cpp)中getCount函数如下:

int Tank::getCount() const
{
    return s_iCount;
}

然后,在main函数中,我们不需要写什么内容,我们只需要按F7看一看编译能不能通过即可。

我们看到,编译失败。失败的原因就是“getCount”静态成员函数不允许修饰符,也就是说,不允许const关键字来修饰。可见,在静态的成员函数的后面加关键字const是不允许的。我们将程序再改回来,然后再来尝试一下,在普通成员函数fire函数中去调用getCount函数,即在普通的成员函数中去调用静态成员函数可不可以呢?我们说,这是可行的。

修改源程序(Tank.cpp)中的fire函数如下:

void Tank::fire()
{
    getCount();
    cout <<"Tank --> fire()"<< endl;
}

我们按F7看一看编译是否通过

我们看到,编译时成功的。

如果我们把这种;逻辑反过来看可不可以。也就是说,在静态成员函数中去调用普通的成员函数可不可以呢??

修改源程序(Tank.cpp)中的getCount函数如下:

int Tank::getCount()
{
    fire();
    return s_iCount;
}

我们按F7看一看编译是否通过?

我们看到编译失败。失败原因是:Tank中的fire是非静态的成员函数进行了非法调用。这就是我们上节课所讲到的,静态的成员函数只能调用静态的数据成员,而不能调用非静态的数据成员或者是非静态的成员函数