前一段时间,我在 cnblogs 别人的博客中,谈到: java 中的引用/指针,与 c++/C# 中的引用/指针不是一个概念. Java 引用,相当于 c++ 指针(fun3)。Java 引用可以赋值 null, 而 c++ 引用 (见 fun2) 不能赋值 null,c++ 指针可以赋值 null(fun3). Java 中,无 c++ 引用(fun2)对应的语法。 结果引起不必要的质疑,特此,写博客,对c++/java/c# 几种编程语言的指针、引用,进行比较,期望引起更多的人,对此有所关注。
前一段时间,我在 cnblogs 别人的博客中,谈到:
java 中的引用/指针,与 c++/C# 中的引用/指针不是一个概念.
Java 引用,相当于 c++ 指针(fun3)。Java 引用可以赋值 null, 而 c++ 引用 (见 fun2) 不能赋值 null,c++ 指针可以赋值 null(fun3).
Java 中,无 c++ 引用(fun2)对应的语法。
结果引起很多质疑,特此,写博客,对c++/java/c# 几种编程语言的指针、引用,进行比较,期望引起更多的人,对此有所关注。
从语法上看,三种开发语言中,C++ 的指针、引用,最为复杂,因此,下面的举例,都从 C++ 代码开始介绍,然后与 java/c# 的语法进行比较。
1) C++ 简单类型变量,有直接变量定义、指针定义、引用定义。
int aa = 10;//c++
int &bb = aa;//c++
int *cc = &aa;//c++
上述三行代码,最后三个变量指向同一个数据。相比较而言,java/c# 都只有变量定义,无引用定义、指针定义。
-----补充:感谢 xiaotie 、飞浪 的提醒:C#中是有指针的,在unsafe状态下,可以定义和使用指针。特更正。
2) C++ 函数调用参数,简单类型变量,有直接变量定义、指针定义、引用定义,后两个,在函数内部改变数据,退出函数,能看到改变后的数据。
void simple_by_val(int a, const int b)
{
a=15;
//b=13; //error C2166: l-value specifies const object
//a=NULL; //good
//b=NULL; //error C2166: l-value specifies const object
}
void simple_by_ref(int &a, const int &b)
{
a=25;
//b=23; //error C2166: l-value specifies const object
//a=NULL; //good
//b=NULL; //error C2166: l-value specifies const object
}
void simple_by_pointer(int *a, const int *b)
{
*a = 35;
//*b = 33; //error C2166: l-value specifies const object
a = NULL; //ok
b = NULL; //ok
}
java 没有这么多名堂,只有直接变量定义。C# 略为复杂一点,有引用,有 out 参数。
static void M(int a, ref int b, out int c)
{
c = 13;
}
相比较而言,C# 的函数参数( ref int b), 类似于C++的函数参数( int &a),都是调用函数前要赋初值,在函数内部改变数据,退出函数,能看到改变后的数据。
而 C# 的 (out int c),在 C++/Java 中,无对应的语法。这个可以调用函数前,不赋初值。在 C# 之前,也很少见到这种语法,只在一些数据库的存储过程、函数定义中,见过类似语法。估计是从数据库编程语法中抄袭过来的语法。
特别注明:C# 的引用( ref int b),只是用在函数参数变量定义上,不能用在函数内部的局部变量中。C++ 中的引用( int &a),可以用在函数内部的局部变量中。
3) C++ 的类对象变量定义语法,较为复杂,可以定义在stack 上(不用 new),可以定义在 heap(用 new)。
CMyClass obj; //stack
CMyClass *p2 = new CMyClass(); //heap
java/C# 中,没有这么复杂,可以认为是上述两种“综合+简化”了。
4) 在 java/C# 中,如下用法是错误的,会报空指针异常;但是在 C++ 里是合法的。
CMyClass obj;
obj.run();
在 C++ 中,
CMyClass obj;
以上一行代码已经调用了构造函数,完成了变量初始化。而在 java/C# 中,这一行代码相当于:
CMyClass obj = null;
5) C++ 中,stack 变量出了作用范围,内存自动回收;heap 变量,需要手工 delete。
java/C# 中,变量是空闲时自动回收的(理论上的),不是变量出了作用范围,就内存回收。
以下为 c++ 代码:
{
CMyClass obj; //stack
obj.test();
}//此处, stack 变量自动被 delete ,内存自动回收
{
CMyClass *p2 = new CMyClass(); //heap
p2->test();
} //此处,超出变量 p2 的作用范围,下面不能再用 p2 变量了,但是,内存并未释放,有内存泄露。
6) 以下代码在 C++ 中是正确的,在 java/C# 是错误的。在 java/C# 语法中,没有定义变量加 * 的,也不能用 -> 来调用类的函数或类的成员变量,也不能用 delete。
CMyClass *p1 = null;
CMyClass *p2 = new CMyClass();
p2->ab();
delete p2;
p2 = null;
7) 以下代码,在java/C# 语法中,是正确的,在 C++ 是错误的。C++ 中,这种赋值要用指针 (CMyClass *p1 = null;) 。
CMyClass p1 = null;
CMyClass p2 = new CMyClass();
8) 以下代码,在 C++ 代码中,会调用“拷贝构造函数”、"等于号重载函数"。这两个函数,在 C++ 中,默认会由编译器自动生成。
//C++
CMyClass obj; //调用构造函数
CMyClass obj2 = obj; //调用拷贝构造函数
obj2 = obj; //调用 = 操作符重载函数
以上代码,大致相当于 java/C# 中的 克隆"clone"。但更隐蔽(初学者不知道调用了 C++ 构造函数、拷贝构造函数、= 操作符重载函数)、更复杂。java/C# 无操作符重载函数。
以下为 C# 代码:
CMyClass obj = new CMyClass();
CMyClass obj2 = (CMyClass)obj.Clone();
而在 C# 中,Clone 函数并不会自动生成。在 Java 中,可以调用 super.clone() ---- Java 基类 Object 默认有一个 clone 函数。
在 C++ 中,默认会由编译器自动生成“拷贝构造函数”、"等于号(=)的重载函数",这一点,很多时候会造成问题,要特别注意。
在 C++ 中,函数返回值不要用 CMyClass 这种类型,这会造成不必要地调用“拷贝构造函数”、"等于号重载函数";也不要返回引用 CMyClass&, 对函数内局部变量的引用,退出函数后无法继续使用。而要返回指针 CMyClass *(最好用智能指针包装后的指针变量)。这一点很多初学者不明白。
但是 C++ 的 std:string 除外。std:string 的“拷贝构造函数”、"等于号重载函数"经过优化,拷贝后的变量,与拷贝之前的变量,内部使用相同的 char[] 数组,只有当一个 string 变量改变时,才会把 char[] 数组复制成两份。std:string 的“拷贝构造函数” 没有性能上损失,又比 string 指针减少了内存泄露,因此,对 std:string ,使用时尽量用 对象变量、对象引用、对象拷贝构造,避免使用 std:string 指针。
另,java/C# 的 String 变量不可改变(有其它类,比如 java StringBuilder类是可变的),C++ 的 string 变量可以改变。这个细微差异,很多人未曾留意。
9) C++ 引用语法,有一些是 Java/C# 程序员不知道的语法:
//C++
CMyClass &a1; //错误,C++ 引用变量定义的时候就要初始化;//Java/C# 对象变量,没有要求变量定义的时候,就要初始化
CMyClass &a1 = NULL; //错误,C++ 引用变量不能赋值 null
CMyClass &a1 = new CMyClass(); //错误,C++ 引用变量不能赋值给一个 new 对象,这种情况,要用 C++ 指针。
//以下C++ 代码是正确的:
CMyClass a;
CMyClass &a1 = a;
CMyClass *b =new CMyClass();
CMyClass &b1 = *b; //这种写法不常用。
10) Sun 自称 java 中消灭了 C++ 中万恶的指针,自己的对象变量,都是引用。
做个比较:
C++ 引用不能赋值 null, 不能赋值 new XXX();C++ 指针可以赋值 null, 可以赋值 new XXX()。
C++ 引用对象通常在 stack 中,而C++ 指针 new 出来的对象则在 heap 中。
java/C# 中的对象变量,可以赋值 null, 可以赋值 new XXX()。java/C# 中的对象变量在 heap 中。
因此,java/C# 中的对象变量,更像是 C++ 中的指针,而不是 C++ 中的引用。
11) C++ 中,指针变量是一个 long 型整数,可以乱指的:
//C++
CMyClass *obj = (CMyClass *) 99; //compile/run good, should not use
如果我知道一个内存地址,就可以定义一个C++指针变量,指向这个内存地址。C++ 的“引用”没有这个功能。C#/Java 的对象变量更没有这个功能。
“指针乱指” 是 C++ 指针功能强大、灵活的体现(PC 上最早出现播放视频的时候,大概是 intel 486 CPU 时代,C++软件通常都直接写显存,据说这样速度更快),也是最容易出问题的地方。估计是因为这个原因,所以C#/Java 都去掉了这个功能。所谓“万恶的C++指针”,多半,也是指的是“指针乱指”。
12) C++ 有野指针,即已经删除对象,但指针还是指向删除对象,还可以继续操作,但运行结果不保证正确。
//C++
CMyClass *p = new CMyClass();
...//给 p 指向的内存赋值
delete p;
//这时 p 仍然指向之前的内存地址,该内存地址数据,一般情况下、短时间内,并没有被清空或者覆盖,仍然可以读/写。这就是“野指针”。
p->run(); //运行结果可能正确,可能不正确,没有保证。
//此时指针 p 对应的内存,可能被下一个 new XXX() 代码,用了这个内存,因此,理论上讲,delete 之后的指针,不应再用来操作对象。
p= NULL; //将指针指向“空”,可以避免“野指针”问题。
p->run(); //这里会报运行时错误。也就是空指针异常。空指针异常在 java/c# 中都有。
C++ 中,delete 与将变量赋值 null , 理应放在一起,可以认为是一个“数据库事务”一样的,要么都成功、要么都失败。
其实,delete 关键字,是由 C++ 标准定义的,标准中,完全可以要求: delete 所在行的代码,执行之后,把指针变量变成 null(因为 C++ 标准的规范,很多都是规定编译器做什么、怎么做,因此按理是可以加这个规定的)。
这样可以避免野指针问题。可惜,C++ 标准,在这方面没有考虑周全。
另,有人抱怨,面试做题,看不是是 C++ 还是 Java、C# , 期望通过看本文,可以帮助一二。
当然,出题者,最好写明,考题中的代码,是哪一种开发语言。