C语言中的“静态”函数是什么?

657
这个问题是关于普通函数,而不是 static方法,已在评论中澄清。
我知道static变量是什么,但static函数是什么?
如果我在一个.c文件(没有a.h)中声明一个函数,比如void print_matrix,并包含"a.c",那么会出现"print_matrix@@....) already defined in a.obj",但如果我将其声明为static void print_matrix,那么它就可以编译?

更新 为了澄清事情 - 我知道包括 .c 是不好的,正如你们中的许多人指出的那样。我只是这样做,暂时在 main.c 中清除空间,直到我有更好的想法将所有这些函数分组成适当的 .h.c 文件。只是一个临时的、快速的解决方案。

11个回答

852

static函数是指在同一文件中的其他函数中才可见的函数(更准确地说,是在同一翻译单元中)。

编辑:对于那些认为问题作者指的是“类方法”的人:由于该问题标记了C,因此他指的是普通的C函数。对于(C++/Java/...)类方法,static表示可以在类本身上调用此方法,不需要该类的实例。


3
实际上我没有将它标记为C++,可能是一些管理员这样做了,但它与C++有关,那么在C++中有什么区别呢? - Slava V
21
C++方法通常被称为“成员函数”,因此我认为C++引入了一些歧义。这不是你的错 - 语言只是使用同一个关键字来表示两个不同的事物。 - Chuck
2
不,他仍然指的是一个C++函数。一个C++自由函数而不是一个C++成员函数。 - Lightness Races in Orbit
4
C++术语从不使用“method”这个词,那是Java的术语。在C++标准文档中,它总是被称为“member function”(见此答案或这个C++与Java术语对照表(例如,C ++使用“data member”,而Java使用“field”等)。 - ShreevatsaR
7
为了澄清这个答案:函数的名称只对同一翻译单元中该名称的第一个声明以下的部分可见。该函数可以通过其他方式(例如函数指针)从其他单元(和同一单元的较早部分)调用。 - M.M
显示剩余6条评论

229

C语言中的静态函数与C++中的静态成员函数有很大的区别。在C语言中,静态函数在其编译链接单元之外是不可见的,也就是说,将一个函数声明为静态可以限制其作用域。你可以将静态函数视为“私有”的 *.c 文件(尽管这并不严格正确)。

在C++中,“static”也适用于类的成员函数和数据成员。静态数据成员也称为“类变量”,而非静态数据成员则称为“实例变量”。这是Smalltalk术语。这意味着静态数据成员只有一个副本,由该类的所有对象共享,而每个对象都有自己的非静态数据成员的副本。因此,静态数据成员本质上是一个全局变量,属于类的成员。

非静态成员函数可以访问类的所有数据成员:静态和非静态。静态成员函数只能操作静态数据成员。

可以这样理解,在C++中,静态数据成员和静态成员函数不属于任何对象,而是整个类的成员。


51
C++ 也有文件作用域,不需要涉及 C 语言。 - Lightness Races in Orbit
23
在C++中,静态函数是静态函数。静态成员函数是静态成员函数,也称为方法。C语言没有成员这一概念并不意味着函数就是“C”的特性。 - Gerasimos R
4
除了名称空间以外,全局变量和类静态变量有什么区别吗? - Alexander Malakhov
4
命名空间是主要的区别。另一个区别是您可以将静态数据成员设置为私有,从而只能从类的成员函数中访问。换句话说,与全局变量相比,您对静态数据成员拥有更多的控制权。 - Dima
4
为什么将静态函数视为其 .c 文件中的私有函数并不严格正确?还有什么需要补充的? - YoTengoUnLCD
2
@YoTengoUnLCD,如果你的.c文件#include另一个.c文件会怎样?顺便说一句,这将成为一个很好的SO问题。你应该去问问。 - Dima

78

最小化可运行的多文件作用域示例

这里我演示了如何使用 static 跨多个文件定义函数作用域的影响。

a.c

#include <stdio.h>

/* Undefined behavior: already defined in main.
 * Binutils 2.24 gives an error and refuses to link.
 * https://dev59.com/CYbca4cB1Zd3GeqPVWhF
 */
/*void f() { puts("a f"); }*/

/* OK: only declared, not defined. Will use the one in main. */
void f(void);

/* OK: only visible to this file. */
static void sf() { puts("a sf"); }

void a() {
    f();
    sf();
}

主函数.c

#include <stdio.h>

void a(void);        

void f() { puts("main f"); }

static void sf() { puts("main sf"); }

void m() {
    f();
    sf();
}

int main() {
    m();
    a();
    return 0;
}

GitHub upstream

编译并运行:

gcc -c a.c -o a.o
gcc -c main.c -o main.o
gcc -o main main.o a.o
./main

输出:

main f
main sf
main f
a sf

解释

  • 有两个独立的函数 sf,一个用于每个文件
  • 有一个共享的函数 f

通常情况下,作用域越小越好,所以如果可以的话,始终将函数声明为 static

在 C 编程中,文件通常用于表示“类”,而 static 函数表示类的“私有”方法。

一个常见的 C 模式是将 this 结构体作为第一个“方法”参数传递,这基本上就是 C++ 在幕后执行的操作。

标准对此的要求

C99 N1256 草案 6.7.1 “存储类说明符” 表示 static 是一种“存储类说明符”。

6.2.2/3 “标识符的链接性” 表示 static 意味着 内部链接

如果一个对象或者函数的文件范围标识符的声明包含存储类说明符 static,则该标识符具有内部链接性。

6.2.2/2 表示 内部链接 的行为类似于我们的示例:

在构成整个程序的翻译单元和库集中,具有外部链接性的特定标识符的每个声明都表示相同的对象或函数。 在一个翻译单元内,具有内部链接性的标识符的每个声明都表示相同的对象或函数。

其中“翻译单元”是经过预处理后的源文件。

GCC 如何在 ELF(Linux)上实现它?

使用 STB_LOCAL 绑定。

如果我们编译:

int f() { return 0; }
static int sf() { return 0; }

使用以下命令来反汇编符号表:

readelf -s main.o

输出包含:

Num:    Value          Size Type    Bind   Vis      Ndx Name
  5: 000000000000000b    11 FUNC    LOCAL  DEFAULT    1 sf
  9: 0000000000000000    11 FUNC    GLOBAL DEFAULT    1 f

因此,它们之间唯一的重要区别在于绑定。 Value只是它们进入.bss部分的偏移量,因此我们希望它们不同。

STB_LOCAL在ELF规范中有文档记录,请参见http://www.sco.com/developers/gabi/2003-12-17/ch4.symtab.html:

  

STB_LOCAL本地符号在包含其定义的对象文件外不可见。具有相同名称的局部符号可能存在于多个文件中,而彼此不会干扰

这使得它成为表示static的完美选择。

没有静态函数的函数是STB_GLOBAL,规范如下:

  

当链接编辑器组合多个可重定位目标文件时,它不允许具有相同名称的STB_GLOBAL符号的多个定义。

这与多个非静态定义的链接错误一致。

如果我们使用-O3来提高优化级别,则sf符号将完全从符号表中删除:无论如何,它都不能从外部使用。 TODO在没有优化时为什么仍要将静态函数保留在符号表中?它们可以用于什么?

另请参阅

C++匿名名称空间

在C ++中,您可能希望使用匿名命名空间而不是静态命名空间,这具有类似的效果,但进一步隐藏了类型定义:Unnamed/anonymous namespaces vs. static functions


3
注意:void f() { puts("sf"); }(即f()的两个定义)会导致未定义行为,不需要任何诊断。要看到错误消息是与链接器质量有关的问题。 - M.M

21
以下内容是关于纯C函数 - 在C++类中,修饰符“static”具有另一种含义。
如果您只有一个文件,则此修饰符根本没有任何区别。差异在于具有多个文件的更大项目中:
在C中,每个“模块”(sample.c和sample.h的组合)都是独立编译的,然后将每个已编译的对象文件(sample.o)由链接器连接在一起成为可执行文件。
假设您有几个文件包含在主文件中,并且其中两个文件具有仅用于方便的内部使用的函数add(int a, b) - 编译器会轻松创建这两个模块的对象文件,但链接器会抛出一个错误,因为它发现两个具有相同名称的函数,并且不知道应该使用哪一个(即使没有要链接的内容,因为它们并未在其他地方使用,而是在自己的文件中)。
这就是为什么将此仅在内部使用的函数设置为静态函数。在这种情况下,编译器不会为链接器创建通常的“可以链接此内容”的标记,因此链接器不会看到此函数并且不会生成错误。

17

静态函数定义将该符号标记为内部。因此,它不会在外部链接时可见,但仅对于同一编译单元中的函数(通常是同一文件)可见。


16

首先,通常情况下将一个.cpp文件包含在另一个文件中是一个不好的方法 - 这会导致像这样的问题 :-) 正确的方法是创建单独的编译单元,并为被包含的文件添加头文件。

其次:

C++这里有一些令人困惑的术语 - 在评论中指出之前我并不知道。

a) 静态函数 - 继承自C语言,在这里你所谈论的是这个。位于任何类之外。静态函数意味着它在当前编译单元之外不可见 - 因此在您的例子中,a.obj 有一个副本,而其他代码也有一个独立的副本。(使用多个代码副本会使最终的可执行文件变大)。

b) 静态成员函数 - 在面向对象术语中称为静态方法。位于一个类内部。通过类调用它,而不是通过对象实例调用。

这两种不同的静态函数定义完全不同。要小心 - 这里还有许多需要注意的地方。


好的,我这样做只是为了在 main.cpp 中清理出一些空间,暂时解决问题,直到我决定如何将文件与正确的 .hpp 组织成库。有更好的想法吗? - Slava V
2
在C++中,正确的术语是成员函数,而不是方法。在C++法律术语中没有“方法”。方法是一个通用的面向对象术语。C++通过成员函数来实现它们。 - Brian Neal

16

"C语言中的“static”函数是什么?"

让我们从头开始。

这一切都基于一个叫做“链接”的东西:

"通过一种称为链接的过程,不同作用域或同一作用域中声明多次的标识符可以引用同一个对象或函数。有三种链接类型:外部、内部和无链接。"

来源:C18,6.2.2/1


"在构成整个程序的翻译单元和库集合中,具有外部链接的特定标识符的每个声明表示相同的对象或函数。在一个翻译单元内,具有内部链接的标识符的每个声明表示相同的对象或函数。具有无链接的标识符的每个声明表示唯一的实体。"

来源:C18,6.2.2/2


如果一个函数没有存储类说明符,则默认情况下该函数具有外部链接:

"如果函数的标识符声明没有存储类说明符,则其链接的确定方式与使用存储类说明符extern声明完全相同。"

来源:C18,6.2.2/5

这意味着,如果你的程序由多个翻译单元/源文件(.c.cpp)组成,则该函数在程序的所有翻译单元/源文件中都是可见的。
这在某些情况下可能会成为问题。如果您想在两个不同的上下文中(实际上是文件上下文)使用两个不同的函数(定义),但具有相同的函数名称怎么办?
在C和C ++中,适用于文件范围内的函数的static存储类限定符(而不是C ++类中的静态成员函数或另一个块中的函数)现在有所帮助,并表示相应函数仅在其定义的翻译单元/源文件中可见,而不在其他TLU/文件中。
“如果对象或函数的文件作用域标识符的声明包含存储类别说明符 static ,则该标识符具有内部链接性。”
因此,static函数只有在以下情况下才有意义:
1.您的程序包含多个翻译单元/源文件( .c 或 .cpp )。
并且
  1. 你希望将一个函数的作用域限制在定义该函数的源文件内。

如果不是这两个要求都符合,你就不需要考虑将函数标记为static


附注:

  • 正如已经提到的,static函数在C和C++中没有任何区别,因为这是C++继承自C的特性。

在C++社区中,有关将函数标记为static与使用未命名命名空间替代之间的争论并不重要,这是由C++03标准中一个错误的段落引起的,宣布使用静态函数已被弃用,但很快被委员会本身修改并在C++11中删除。

这成为了各种SO问题的主题:

未命名/匿名命名空间 vs 静态函数

未命名命名空间优于静态的吗?

为什么未命名命名空间是“优越的”替代静态的选择?

static关键字的废弃...不再?

实际上,根据C++标准,它还没有被废弃。因此,仍然可以使用static函数。即使未命名命名空间具有优势,讨论在C++中使用或不使用静态函数是主观的(基于个人意见),因此不适合在本网站上进行。


8

静态函数是可以在类本身上调用的函数,而不是类的实例。

例如,非静态函数如下:

Person* tom = new Person();
tom->setName("Tom");

这种方法适用于类的实例,而不是类本身。但是您可以拥有一个静态方法,它可以在没有实例的情况下工作。这在工厂模式中有时会被使用:

Person* tom = Person::createNewPerson();

3
我觉得你说的是静态“方法”,而不是“函数”? - Slava V
我假设你是在指类中的静态函数。 - Parrots
如果我早知道C++中的“methods”被称为“method functions”,我会更清楚的。现在我知道了 :) 无论如何,还是谢谢。 - Slava V
5
C++ 中没有所谓的“方法”,只有函数。“方法”一词在 C++ 标准中从未被提及,只有“函数”。 - Brian Neal
@BrianNeal 一个方法是一个类函数。如果它不是静态的,它会隐式地接收对象实例(this),这样你就可以访问成员。编译器会隐式地引用成员,而不需要使用this->,除非你在该方法中定义了一个局部变量。我想你指的是C,因为它甚至没有类。 - Puddle
1
@Puddle,我知道你的意思,但在C++标准中没有“方法”的定义。C++只有各种类型的函数。“方法”是一个通用的面向对象术语,在其他语言中使用,在C++中非正式地使用。在C++中,方法被正式称为“成员函数”。 - Brian Neal

7

静态函数的意义取决于语言:

1)在像C这样没有面向对象编程(OOPS)的语言中,这意味着该函数仅在定义它的文件内可访问。

2)在像C++这样有面向对象编程(OOPS)的语言中,这意味着可以直接在类上调用该函数而无需创建其实例。


这是不正确的。你第二段的解释是指类的“静态成员函数”,而不是“静态函数”。在C++中,用static修饰的函数也具有文件作用域,就像在C语言中一样。 - RobertS supports Monica Cellio

7

小细节:静态函数对于大多数实际情况来说是可见的,即函数定义所在的文件就是其所在的翻译单元。您遇到的错误通常被称为违反了一个定义规则。

标准可能会说:

"每个程序必须包含在该程序中使用的每个非内联函数或对象的正好一个定义;不需要任何诊断。"

这是以C方式看待静态函数的方式。然而,在C++中,此方式已被弃用。

在C++中,您还可以声明成员函数为静态函数。这些函数主要是元函数,即它们不描述/修改特定对象的行为/状态,而是作用于整个类本身。此外,这意味着您无需创建对象即可调用静态成员函数。更进一步地,这也意味着您只能从此类函数内部访问静态成员变量。

我要补充Parrot的例子,即Singleton模式,该模式基于此类静态成员函数来获取/使用整个程序生命周期中的单个对象。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接