21 静态成员变量和静态成员函数

1 对象在内存中的分配存储方式

1.1 this 指针

我们知道,创建一个类的对象,程序就会开辟一块内存空间,用于存储这个对象。那么,对象的成员变量和成员函数在内存中,是如何存储的呢?

是像下面这张图一样,每个对象的内存空间都存储着类的成员变量和成员函数吗?

对象内存模型猜想

这样存储,是可以的,但这有一个巨大问题,成员函数占用的空间往往比成员变量大,而且,这些成员变量的内容,往往是相同的。这就会导致程序在内存中存放大量的重复内容。对应追求性能的 C++ 语言来说,这种问题自然是无法忍受的。

因此,实际上,类的成员函数的内存空间,是由所有同类对象共享的。

对象内存模型

大家有没有发现一个问题:类的所有对象的成员函数都是共享的,那么,我们在调用对象的成员函数时,成员函数中如果要使用成员变量时,程序是如何找到实际要用的成员变量的呢?

其实,在成员函数中,都隐含着一个特殊的指针,成为 this 指针 。该指针存放着该类对象的地址。

在程序执行 stu1.say() 时,实际上,程序会把存储着 stu1 对象地址的 this 指针,传递给 say() 这个成员函数。因此,我们可以正常输出 stu1 对象中存储的数据。

1.2 在成员函数中调用成员函数

我们在类外调用对象的成员函数时,因为要区分究竟调用的是哪个对象的成员函数,所以需要使用 对象名 或者 对象的指针 ,去调用对象的成员函数,为其 this 指针赋值。

但我们在类的成员函数中,调用成员函数时,因为成员函数本身在被调用时就肯定获得了 this 指针,而 this 指针可以被隐式传递给被调用的成员函数,所以,我们直接使用函数名即可,不需要指定具体对象。

(ps.即使我们想写出具体对象,在类声明时,对象都是不存在的,我们又如何未卜先知地知道究竟会用到什么对象呢?)

在下面的例子中,学生对象的年龄,不再输入确切的数字,而是由 当前年份 - 出生年份 计算得到。

#include<iostream>

using namespace std;

class Student {
private:
    string name;
    int birthYear;
    int score;
public:
    Student(string _name, int _birthYear, int _score) : name(_name), birthYear(_birthYear > 1900 ? _birthYear : 1900), score(_score) {}

    // 计算学生年龄的方法
    int getAge() {
        return 2023 - birthYear;
    }

    void say() {
        cout << "我是" << name << ",我今年" << getAge() << "岁了,我的成绩是" << score << "分。" << endl;
    }
};

int main() {
    Student *stu1 = new Student("张三", 2000, 85);
    stu1->say();
    delete stu1;
    return 0;
}

程序输出:我是张三,我今年23岁了,我的成绩是85分。

2 静态成员变量

从上面的内容可知,对象的内存中包含了成员变量,不同的对象占用不同的内存,这使得不同对象的成员变量相互独立,它们的值不受其他对象的影响。例如有两个相同类型的对象 ab,它们都有一个成员变量 name ,那么修改 a.name 的值不会影响 b.name 的值。

可是有时候我们希望在多个对象之间共享数据,对象 a 改变了某份数据后对象 b 可以检测到。共享数据的典型使用场景是计数,以前面的 Student 类为例,如果我们想知道班级中共有多少名学生,就可以设置一份共享的变量,每次创建对象时让该变量加 1

在 C++ 中,我们可以使用 静态成员变量 来实现多个对象共享数据的目标。静态成员变量是一种特殊的成员变量,它被关键字 static 修饰,例如:

class Student {
private:
    string name;
    int birthYear;
    int score;
public:
    // 记录学生总数的静态成员变量
    static int total;
    
    Student(string _name, int _birthYear, int _score) : name(_name), birthYear(_birthYear > 1900 ? _birthYear : 1900), score(_score) {}

    // 计算学生年龄的方法
    int getAge() {
        return 2023 - birthYear;
    }

    void say() {
        cout << "我是" << name << ",我今年" << getAge() << "岁了,我的成绩是" << score << "分。" << endl;
    }
};

这段代码声明了一个静态成员变量 total ,用来统计学生的人数。

static 成员变量属于类,不属于某个具体的对象 ,即使创建多个对象,也只为 total 分配一份内存,所有对象使用的都是这份内存中的数据。当某个对象修改了 total ,也会影响到其他对象。

static 成员变量必须在类声明的外部初始化 ,具体形式为:

数据类型 类名::name = value;

name 是变量名,value 是初始值。将上面的 total 初始化:

int Student::total = 0;

静态成员变量在初始化时不能再加 static,但必须要有数据类型。被 privateprotectedpublic 修饰的静态成员变量都可以用这种方式初始化。

注意:static 成员变量的内存既不是在声明类时分配,也不是在创建对象时分配,而是在(类外)初始化时分配。反过来说,没有在类外初始化的 static 成员变量不能使用。 初始化时可以赋初值,也可以不赋值。如果不赋值,那么会被默认初始化为 0。

int Student::total;

static 成员变量既可以通过对象来访问,也可以通过类来访问。请看下面的例子:

int Student::total = 10;

int main() {
    //通过类类访问 static 成员变量
    cout << "当前共有" << Student::total << "名学生。" << endl;
    Student *stu1 = new Student("张三", 2000, 85);
    //通过对象指针来访问 static 成员变量
    cout << "当前共有" << stu1->total << "名学生。" << endl;
    delete stu1;
    return 0;
}

注意:static 成员变量不占用对象的内存,而是在所有对象之外开辟内存,即使不创建对象也可以访问。具体来说,static 成员变量和普通的 static 变量类似,都在内存分区中的全局数据区分配内存。

静态成员变量内存分配

接下来,让我们实现 静态成员变量 total ,统计学生人数的功能。

#include<iostream>

using namespace std;

class Student {
private:
    string name;
    int birthYear;
    int score;
public:
    static int total;
    
    Student(string _name, int _birthYear, int _score) : name(_name), birthYear(_birthYear > 1900 ? _birthYear : 1900), score(_score) {
        total++;
    }
    
    // 计算学生年龄的方法
    int getAge() {
        return 2023 - birthYear;
    }

    void say() {
        cout << "我是" << name << ",我今年" << getAge() << "岁了,我的成绩是" << score << "分。" << endl;
    }

    void showTotal() {
        cout << "当前共有" << total << "名学生。" << endl;
    }
};

int Student::total = 0;

int main() {
    cout << "当前共有" << Student::total << "名学生。" << endl;
    Student *stu1 = new Student("张三", 2000, 85);
    stu1->showTotal();
    delete stu1;
    return 0;
}

程序输出:
当前共有0名学生。
当前共有1名学生。

本例中将 total 变量在构造函数中被修改,每次创建对象时,会调用构造函数使 total 的值加 1。

3 静态成员函数

在类中,static 除了可以声明静态成员变量,还可以声明静态成员函数。

语法:static 返回值类型 函数名(参数列表);

普通成员函数可以访问所有成员(包括成员变量和成员函数),静态成员函数只能访问静态成员。

静态成员函数没有 this 指针,无法在函数体内部访问某个对象,所以不能调用普通成员函数,只能调用静态成员函数。

静态成员函数,也是属于类,而不是属于某个具体对象的,因此,调用它的方式,与静态成员变量相同,既可以通过对象来访问,也可以通过类来访问。

通过类访问的语法为:类名::静态成员函数名();

静态成员函数,往往被用于访问被 private 修饰的静态成员变量,或者作为工具类的方法使用。

#include<iostream>

using namespace std;

class Student {
private:
    string name;
    int birthYear;
    int score;
public:
    static int total;
    Student(string _name, int _birthYear, int _score) : name(_name), birthYear(_birthYear > 1900 ? _birthYear : 1900), score(_score) {
        total++;
    }

    ~Student() {
        cout << "调用" << name << "的析构函数" << endl;
    }

    // 计算学生年龄的方法
    int getAge() {
        return 2023 - birthYear;
    }

    void say() {
        cout << "我是" << name << ",我今年" << getAge() << "岁了,我的成绩是" << score << "分。" << endl;
    }

    static void showTotal() {
        cout << "当前共有" << total << "名学生。" << endl;
    }
};

int Student::total;

int main() {
    Student::showTotal();
    Student *stu1 = new Student("张三", 2000, 85);
    stu1->showTotal();
    delete stu1;
    return 0;
}