15-7 静态成员函数

在上一节关于15.6——静态成员变量的内容中,你了解到静态成员变量属于类本身而非类的实例。若静态成员变量为public类型,可通过类名配合作用域解析运算符直接访问:

#include <iostream>

class Something
{
public:
    static inline int s_value { 1 };
};

int main()
{
    std::cout << Something::s_value; // s_value is public, we can access it directly
}

image

但如果静态成员变量是私有的呢?请看以下示例:

#include <iostream>

class Something
{
private: // now private
    static inline int s_value { 1 };
};

int main()
{
    std::cout << Something::s_value; // error: s_value is private and can't be accessed directly outside the class
}

image

在此情况下,我们无法直接从 main() 中访问 Something::s_value,因为它是私有成员。通常我们需要通过公有成员函数访问私有成员。虽然可以创建一个普通的公有成员函数来访问 s_value,但这样就必须实例化该类型的对象才能使用该函数!

#include <iostream>

class Something
{
private:
    static inline int s_value { 1 };

public:
    int getValue() { return s_value; }
};

int main()
{
    Something s{};
    std::cout << s.getValue(); // works, but requires us to instantiate an object to call getValue()
}

image

我们可以做得更好。


静态成员函数

成员变量并非唯一可设为静态的成员类型。成员函数同样可以设为静态。以下是使用静态成员函数访问器的上述示例:

#include <iostream>

class Something
{
private:
    static inline int s_value { 1 };

public:
    static int getValue() { return s_value; } // static member function
};

int main()
{
    std::cout << Something::getValue() << '\n';
}

image

由于静态成员函数不与特定对象关联,可直接通过类名和作用域解析运算符调用(例如 Something::getValue())。与静态成员变量类似,它们也可通过类型的对象进行调用,但这种做法并不推荐。


静态成员函数没有this指针

静态成员函数有两个值得注意的特性。首先,由于静态成员函数不属于任何对象,因此它们没有this指针!仔细想想这很合理——this指针始终指向成员函数正在操作的对象。静态成员函数不操作对象,因此不需要this指针。

其次,静态成员函数可直接访问其他静态成员(变量或函数),但无法访问非静态成员。这是因为非静态成员必须属于某个类对象,而静态成员函数本身没有对应的类对象可操作!


在类定义外部定义的静态成员

静态成员函数也可以在类声明外部定义。其工作方式与普通成员函数相同。

#include <iostream>

class IDGenerator
{
private:
    static inline int s_nextID { 1 };

public:
     static int getNextID(); // Here's the declaration for a static function
};

// Here's the definition of the static function outside of the class.  Note we don't use the static keyword here.
int IDGenerator::getNextID() { return s_nextID++; }

int main()
{
    for (int count{ 0 }; count < 5; ++count)
        std::cout << "The next ID is: " << IDGenerator::getNextID() << '\n';

    return 0;
}

该程序输出:

image

请注意,由于该类中的所有数据和函数均为静态,我们无需实例化该类的对象即可使用其功能!该类通过静态成员变量存储待分配的下一个ID值,并提供静态成员函数来返回该ID并递增。

正如第15.2节——类与头文件所述,在类定义内部定义的成员函数默认具有内联特性。在类定义外部定义的成员函数则不具备默认内联性,但可通过inline关键字显式实现内联。因此,若将静态成员函数定义在头文件中,当该头文件被包含到多个翻译单元时,应显式声明为inline,以避免违反单一定义规则(ODR)。


关于全静态成员类的警示

编写全静态成员类时需谨慎。尽管此类“纯静态类pure static classes”(亦称“单态类monostates”)具有实用价值,但也存在潜在弊端。

首先,由于所有静态成员仅实例化一次,纯静态类无法生成多个副本(除非克隆类并重命名)。例如,若需创建两个独立的ID生成器,纯静态类无法实现此需求。

其次,在全局变量章节中我们已知晓全局变量的危险性——任何代码都可能修改全局变量值,进而破坏看似无关的其他代码。纯静态类同样存在此隐患。由于所有成员都属于类本身(而非类对象),且类声明通常具有全局作用域,纯静态类本质上等同于在全局可访问命名空间中声明函数和全局变量,因此具备全局变量所有固有的弊端。

与其编写全静态成员的类,不如考虑创建普通类并实例化其全局实例(全局变量具有静态生命周期)。这样既能在需要时使用全局实例,又可在特定场景下灵活创建局部实例。


纯静态类 vs 命名空间

纯静态类与命名空间存在诸多重叠之处。二者均允许在作用域范围内定义具有静态生命周期的变量和函数。但关键区别在于:类具备访问控制机制,而命名空间则不具备。

通常情况下,当存在静态数据成员和/或需要访问控制时,应优先选用静态类;否则则推荐使用命名空间。


C++ 不支持静态构造函数

既然普通成员变量可通过构造函数初始化,那么通过静态构造函数初始化静态成员变量似乎也合乎逻辑。虽然某些现代语言确实为此目的支持静态构造函数,但遗憾的是 C++ 并不在列。

若静态变量可直接初始化,则无需构造函数:您可在定义处初始化静态成员变量(即使其为私有)。我们在前文的IDGenerator示例中采用了此方法。以下是另一个示例:

#include <iostream>

struct Chars
{
    char first{};
    char second{};
    char third{};
    char fourth{};
    char fifth{};
};

struct MyClass
{
	static inline Chars s_mychars { 'a', 'e', 'i', 'o', 'u' }; // initialize static variable at point of definition
};

int main()
{
    std::cout << MyClass::s_mychars.third; // print i

    return 0;
}

image

如果初始化静态成员变量需要执行代码(例如循环),存在多种实现方式,但都略显晦涩。一种适用于所有变量(无论是否静态)的方法是:通过函数创建对象,填充数据后将其返回给调用方。该返回值可被复制到待初始化的对象中。

#include <iostream>

struct Chars
{
    char first{};
    char second{};
    char third{};
    char fourth{};
    char fifth{};
};

class MyClass
{
private:
    static Chars generate()
    {
        Chars c{}; // create an object
        c.first = 'a'; // fill it with values however you like
        c.second = 'e';
        c.third = 'i';
        c.fourth = 'o';
        c.fifth = 'u';

        return c; // return the object
    }

public:
	static inline Chars s_mychars { generate() }; // copy the returned object into s_mychars
};

int main()
{
    std::cout << MyClass::s_mychars.third; // print i

    return 0;
}

image

相关内容:
也可以使用lambda表达式实现此功能。

我们在第8.15节——全局随机数(Random.h)中展示了该方法的实际应用示例(虽然我们使用命名空间而非静态类实现,但工作原理相同)。


测验时间

问题 #1

将以下示例中的 Random 命名空间转换为具有静态成员的类:

#include <chrono>
#include <random>
#include <iostream>

namespace Random
{
	inline std::mt19937 generate()
	{
		std::random_device rd{};

		// Create seed_seq with high-res clock and 7 random numbers from std::random_device
		std::seed_seq ss{
			static_cast<std::seed_seq::result_type>(std::chrono::steady_clock::now().time_since_epoch().count()),
				rd(), rd(), rd(), rd(), rd(), rd(), rd() };

		return std::mt19937{ ss };
	}

	inline std::mt19937 mt{ generate() }; // generates a seeded std::mt19937 and copies it into our global object

	// Generate a random int between [min, max] (inclusive)
	inline int get(int min, int max)
	{
		return std::uniform_int_distribution{min, max}(mt);
	}
}

int main()
{
	// Print a bunch of random numbers
	for (int count{ 1 }; count <= 10; ++count)
		std::cout << Random::get(1, 6) << '\t';

	std::cout << '\n';

	return 0;
}

image

显示答案

#include <chrono>
#include <random>
#include <iostream>

class Random
{
private: // could be public if we want these to be accessible
	static std::mt19937 generate()
	{
		std::random_device rd{};

		// Create seed_seq with high-res clock and 7 random numbers from std::random_device
		std::seed_seq ss{
			static_cast<std::seed_seq::result_type>(std::chrono::steady_clock::now().time_since_epoch().count()),
				rd(), rd(), rd(), rd(), rd(), rd(), rd() };

		return std::mt19937{ ss };
	}

	static inline std::mt19937 mt{ generate() }; // generates a seeded std::mt19937 and copies it into our global object

public:
	// Generate a random int between [min, max] (inclusive)
	static int get(int min, int max)
	{
		return std::uniform_int_distribution{min, max}(mt);
	}
};

int main()
{
	// Print a bunch of random numbers
	for (int count{ 1 }; count <= 10; ++count)
		std::cout << Random::get(1, 6) << '\t';

	std::cout << '\n';

	return 0;
}

在命名空间中声明的任何对象都是全局变量。当我们在命名空间内声明内联 std::mt19937 mt 时,实际上是在声明一个内联全局变量 mt。这使得整个程序能够通过全局变量 mt 使用 std::mt19937 的单一实例。

相比之下,当类对象实例化时,该对象会包含每个非静态数据成员的副本。我们不希望每个类对象都拥有独立的 std::mt19937 副本。因此采用 static inline std::mt19937 mt 的声明方式,告知编译器应在 Random 类内部存在 std::mt19937 的单一实例。将mt定义为static inline使其成为内联变量,允许我们在类定义内部初始化它(本点将在第15.6节——静态成员变量中详述)。

同时将成员函数设为static,使其无需实例化Random对象即可被访问。

posted @ 2026-01-02 22:52  游翔  阅读(24)  评论(0)    收藏  举报