如果这样写c++的单例:
class Singleton { private: static Singleton * singleton; private: Singleton() { } public: static Singleton *getinstance() { if(singleton == NULL) { Singleton::singleton = new Singleton(); } return singleton; } }; int main() { Singleton * p = Singleton::getinstance(); return 0; }
就会有linker error(unsolved external `singleton`)。
顾名思义,unsolved external 就是说linker找不到这个symbol。
注意,这里不是compiler的错误,而是linker的错误。
假如改成这样:
class Singleton { private: static Singleton * singleton; private: Singleton() { } public: static Singleton *getinstance() { if(singleton == NULL) { Singleton::singleton = new Singleton(); } return singleton; } }; Singleton * Singleton::singleton = nullptr; //多加了这个定义 int main() { Singleton * p = Singleton::getinstance(); return 0; }
那么就会没问题了。
为什么? 为什么要加多这一个定义?在 Singleton 类中不是已经定义了吗? 就算要给那个类静态变量赋值, 为什么不直接加上 Singleton::singleton = nullptr; 就行? 毕竟在类中已经有声明了,再在这里声明一次不会造成重复?毕竟,一般情况下,你不能这样: int x; int x;
其实这并不是C++这门语言的问题,而是这C++、C、Pascal这一类static language的问题。Java这种依靠虚拟机的语言就没有这样的问题(后面再说为什么)。
在众多编程语言之中,有一种语言被称为Native Language,比如C、C++、Pascal。之所以称为Native,并不是因为他们可以被编译(Java也可以被编译,Python也可以),而是因为他们编译出来的东西可以直接变成机器码,然后由底层的操作系统执行。Java、Python、Perl、C#这一类依靠虚拟机来运作的语言就不行。这也是Native的英文意思所在。
这就意味着,他们编译出来的OBJ文件是通用的。只要你所用的C++ compiler,C compiler,Pascal compiler遵循同一个OBJ文件格式的规范(比如ELF, PE, COFF等),那么同一个linker就可以将这些OBJ文件链接成一个可执行文件。
所以,换句话说,各个编译单元对其他编译单元(compilation unit)可以一无所知。(即使编译器想知道也不行,因为你根本就不知道和你一起被链接的OBJ文件是不是同一门语言产生的)
你可以用C写一份代码,编译成一个OBJ文件;然后用C++写一个,编译成一个OBJ文件;然后用Pascal写一份,编译成一个OBJ。然后,用一个链接器将他们链接起来,变成一个可执行文件。
实际上,C++语言并不知道linker的存在(只是一般诸如Visual Studio,GCC等编译套件会自动调用linker罢了),它只负责将多份代码编译成多份OBJ文件,然后由一个linker将他们链接起来。
这种编译模型(compilation model)就是所谓的分离编译。
分离编译有它的好处,但是也会引来很多问题。其中一个就是多重定义的问题。
多重定义的基本含义如下:
假如你写了三份C++代码:
//file1.cpp int c ;
//file2.cpp int c;
//file3.cpp int c;
然后一个main入口:
//main.cpp int main() {}
当你单独编译任意一份的时候,都会成功(用 gcc -c file1.cpp... 命令编译, -c 代表compile,但是不链接),生成 file1.o .... 文件。
但是,当你用链接器将他们链接在一起的时候,就会有链接错误,这是因为你这里定义了三次变量 c ,当连接器将这四个编译单元(compilation unit)链接在一起的时候,它就会发现, c 被定义了多次,于是报错。这就是多重定义的问题。
可以看出,这是因为,各个编译单元对其他编译单元(compilation unit)可以一无所知,所以,即使你定义了多次 c ,编译器也不知道。等到linker再来处理。
回到上面的Singleton上面来。
在上面的代码上,之所以要这样子 Singleton * Singleton::singleton = nullptr; 对一个类里的static member “重新” 定义一翻,也是因为这个原因。
要知道,定义一个变量就等于给它分配了一定的内存空间。static member也是。但是static member是所有的 instance(实例)共有的,所以它有且只有一块内存空间。
但是,一个类的声明通常是放在一个头文件中的,而一个头文件可以被包含多次,分成不同的编译单元编译,各个编译单元对另外的编译单元又是一无所知的,所以编译器这时候就不知道怎么处理这个static member了。假如编译器在类声明的时候给这个static member分配了内存空间(承认了这个定义,并且采取了措施),那么,假如包含这个类的头文件又被其他编译单元包含,则,这个static member就会被再次分配一次内存。这样一来,static member 就不static了。
所以,为了解决这个问题,C++语言规定,在编译这个类的时候,对类里面的static member不做处理(定义),程序员应该在另外一个地方,单独地定义这个static member。
那么Java 为什么就可以这样呢?为什么Java就不用对static member单独定义呢?很简单,因为Java都是独立的,自己的虚拟机,只有Java语言编译出来的东西才能在上面跑。即使Java按照上面所说的编译流程编译,发现了重复的static member,它只需要把这些重复的对象整合成一个即可(依靠它的runtime mechanism)。所以,在语言(语法)层面上,Java没有这样的问题(不需要另外对static member进行单独定义)。C#也是。
多说一句,C++中的singleton一般都不这样写,一般是这样:
class Singleton { private: Singleton(){ } Singleton(Singleton&); Singleton& operator=(Singleton&); public: static Singleton& getInstance(){ static Singleton instance; return instance; } };
:)