原由
Java 中的代码:
Object nullObj = null;
System.out.println(nullObj.toString());
此时就会出现注明的空指针异常,可是 OC 中的 nil 并不会如此。其原因是汇编方法 msg_send 处理了消息调用者为 nil 时的情况,大概处理如下:
LNilReceiver:
// r0 is already zero
mov r1, #0
mov r2, #0
mov r3, #0
FP_RETURN_ZERO
bx lr
END_ENTRY _objc_msgSend
那么,问题来了:
- OC 中的 nil 是什么?
- 为什么 Java 中会崩溃?
- C/C++ 中 null 会崩溃吗?
一、OC 中的空
OC 中可以使用 4 种方式来代表空:
- nil;
- Nil;
- NULL;
- NSNull;
OC 中没有 null;
查阅 objc4 源码可知,
nil 的声明:
#ifndef nil
# if __has_feature(cxx_nullptr)
# define nil nullptr
# else
# define nil __DARWIN_NULL
# endif
#endif
Nil 的声明:
#ifndef Nil
# if __has_feature(cxx_nullptr)
# define Nil nullptr
# else
# define Nil __DARWIN_NULL
# endif
#endif
通过源码可知,其实 Nil 完全等价于 nil,要弄清楚本质,关键点有三个:
- __has_feature(cxx_nullptr);
- nullptr;
- __DARWIN_NULL
关键点1:__has_feature
根据Clang 3.7 文档对__has_feature的描述:
__has_feature evaluates to 1 if the feature is both supported by Clang and standardized in the current language standard or 0 if not.
因此,__has_feature(cxx_nullptr)
是用来判断是否支持 C++11 中的 nullptr 特性的。所以,在 Objective-C 中 nil 和 Nil 都是 __DARWIN_NULL
宏定义。
其实,验证这一点也很简单,在 iOS 工程中运行以下代码即可:
# if __has_feature(cxx_nullptr)
# define xk_test 0
# else
# define xk_test 1
# endif
NSLog(@"%d",xk_test); // 输出为1,表示iOS中不支持cxx_nullptr
那么来看看__DARWIN_NULL
是个啥,仍然在源码中找到:
#ifndef __DARWIN_NULL
#define __DARWIN_NULL NULL
#endif
因此:
- iOS 中,Nil、nil、NULL 完全等价;
另外,NSNull 就是一个对象,只有一个 [NSNull null]
方法,就不多说了吧~~~
二、C++ 和 C 中的 NULL
既然 OC 中的 nil、Nil 都是 NULL,那么 NULL 到底是个啥?
NULL 是 C/C++ 中定义的宏,但是两者却不是完全等价的,在 VC 的 runtime.h 中,其定义如下:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
由此可知:
- C 中的 NULL 为 (void *) 0;
- C++ 中的 NULL 就是 0;
其本质区别就是 C 中将 0 强制转化成了一个空指针;
那为什么在 C++ 中会直接定义 NULL 为 0 呢?因为 C 中可以隐式转换,如:
int a = (void *)0;
而 C++ 却是需要显示的写出类型转换的,如:
int *p = (void *) 0; // error
Xcode 中,cpp 文件下,报错如下:
![](https://img.haomeiwen.com/i4044896/f4640fc04f418c21.png)
.m 文件中转换只会报警告:
![](https://img.haomeiwen.com/i4044896/f9dc66931bd8b3c5.png)
所以,NULL 在 C++ 中直接定义为 0;
注意:.m 文件和 .cpp 文件的说法并不准确,是否编译通过取决于编译器的设置。可以使用 ifdefine 来测试当前编译器是否支持 __cplusplus;
三、C++ 中的NULL 延伸
因为 C++ 中,NULL 定义为 0,那么在函数重载时,使用 gcc 编译器运行时,就会发生偏差,NULL 本来是代表空指针的,却调用到 int 类型的函数:
#include <iostream>
using namespace std;
void func(int a)
{
cout << "func int" << endl;
}
void func(char* a)
{
cout << "func char*" << endl;
}
int main()
{
func(NULL);
return 0;
}
输出:
func int
使用 C++ 编译器编译时,C++ 会直接禁止使用 NULL 作为重载函数的入参,会直接报错:
![](https://img.haomeiwen.com/i4044896/fb82deefe699c0eb.png)
另外,需要特殊说明的一点是:
- NULL 在 C++ 中严格来讲是 long 类型;
起初,觉得 NULL 既然是 0,那么:
int *a = NULL;
*a = 10;
一开始觉得这种写法是不对的,因为将整数赋值给了指针。但是其实这么写也正确,因为指针接收一个地址,地址本质上就是一个表示内存位置的数字。只不过被赋值为 NULL ,指针的地址为 0 而已。因此,继续使用 a 这个指针就会出现坏内存访问( EXC_BAD_ACCESS)。这也就解释了第三个问题:同 Java 一样,使用空指针在 C/C++ 中会崩溃;
另外,还想更具体看 NULL 到底是个啥类型,作进一步验证。想到了使用 type of 方法:
printf("NULL:%lu\n",sizeof(typeof(NULL)));
但是结果确是 8,难道 C++ 后面的版本 NULL 被改成了 void * ?
于是继续对比:
printf("NULL:%lu\n",sizeof(typeof(NULL)));
printf("nullptr:%lu\n",sizeof(typeof(nullptr)));
输出为:
8
8
这不对啊,解释不通啊。后来想到,Java 中的字面量具有默认类型的特性,难道 C++ 中 0 字面量的类型为 long 类型:
printf("int:%lu\n",sizeof(typeof(0)));
输出为:
4
那么就只有一个解释了:
- NULL = 0L;
验证:
if(typeid(NULL) == typeid(int)){
cout << "NULL的数据类型是:int型" << endl;
}
if(typeid(NULL) == typeid(long)){
cout << "NULL的数据类型是:long型" << endl;
}
if(typeid(NULL) == typeid(void *)){
cout << "NULL的数据类型是:指针型" << endl;
}
结果:
NULL的数据类型是:long型
由此证明:
*当前版本 C++ 中的 null 并不是 int 类型的 0,而是 long 类型的 0;
四、nullptr
因为 NULL 的定义不够清晰,所以推出了 nullptr 来代指空指针。如果编译器支持 nullptr,理论上不应该再使用 NULL 来给空指针赋值或者清空指针,而应该使用 nullptr,而给 int 类型赋值则直接使用 0 即可。
将 nullptr 赋值给非指针类型,则直接报错:
![](https://img.haomeiwen.com/i4044896/efc199e7ca85e4ae.png)
- nullptr 本质上是一个编译器特性,是为了代码的书写规范而产生,让空指针的赋值更加清晰;
五、Java 中的 null
起初,因为 OC 中习惯了消息转发机制,在调用者为 nil 时,消息转发机制不会做任何事情,所以使用空指针并不会引起崩溃。因此,不理解 java 中的 null 为什么直接奔溃,其实 C++ 本身就是不可以使用空指针的,只是 OC 做了特殊处理罢了:
int i = NULL;
i = 10;
int *a = NULL;
*a = 1; // EXC_BAD_ACCESS
除此之外,Java 中的 null 具备几个特点:
- null 是一个关键字,不是编译器特性;
- null 不是一种类型,而是一个值;
- null 是所有引用类型的默认值;
- null 不能赋值给基础数据类型;
- 包装类(Integer)在自动拆箱(Integer赋值给Int)时,如果为 null ,不会自动转换成对应基础类型的默认值,而是会报空指针异常引起崩溃;
Integer integer1 = 10;
int int1 = integer1;
Integer integer2 = null;
int int2 = integer2; // NullPointerException
- null 调用 instanceof 方法永远返回 false;
Object obj = null;
if (obj instanceof Object){
System.out.println("True");
} else {
System.out.println("False");
}
- null == null 返回 true 可以使用 == 和 != 来判断是否为 null;
Object obj = null;
if (obj == null){
System.out.println("True");
} else {
System.out.println("False");
}
-
null 是值而不是类型,所以不能当做 instanceof 的参数;
null不是一种类型
-
不能使用值为 null 的引用类型变量来调用非静态方法;
-
可以使用值为 null 的引用类型变量来调用静态方法;
class XKPerson {
static final String COUNTRY = "CHINA";
static public void StaticFunc(){
System.out.println("123");
}
public void InstanceFunc(){
System.out.println("456");
}
}
public class Main {
public static void main(String[] args) {
XKPerson p1 = null;
p1.StaticFunc(); // correct
p1.InstanceFunc(); // error, NullPointerException
}
}
静态方法:实例对象可以调用静态方法,也可以访问静态成员变量,但是静态方法的调用不需要实例对象,所以对象为 null 时调用静态方法正常执行,调用实例方法则会空指针异常;
- null 可以作为参数传递给方法,但是否崩溃取决于方法内部的处理;
六、总结
- OC 中的 Nil、nil、NULL 完全等价;
- C++ 不支持隐式类型转换而 C 支持,所以 C++ 中对 NULL 的定义做了特殊处理,直接定义为 0;
- C++ 支持方法重载,NULL 在 GCC 编译器中会对重载方法的调用产生误差,调用到 int 类型,在 C++ 编译器中直接报错;
- 因为 NULL 的定义不是很清晰所以出现了 nullptr 代替 NULL,表示空指针,nullptr 用于指针类型,赋值给非指针类型时直接编译期报错;
- NULL 虽然是 0,但是是 long 类型;
- Java 中的 null 是一种特殊的值,表示引用类型的指针为空;
网友评论