大三那会还在搞单片机和MFC,玩的纯C系的语言,每天和指针打交道,一切皆指针。有一天,听说JAVA里没有了指针,我大惊失色,指针都没了,这语言还能搞啥?
后来,类似C#,JAVA的高级面向对象语言用得多了。反过来思考,高级面向对象语言没有了指针,到底是好事还是坏事?这种区别体现在哪里?本文以C#和C++为例做个对比,JAVA机制和C#类似。与各位共同探讨。
为了简单,我们先定义一个Point类, 只有X,Y 两个变量。看看C++和C#之间的使用区别
1. 指针和引用
C++中,指针和引用的有一定的区别,指针是一个地址,而引用只是别名,引用使用起来要方便得多。因为指针本身是地址,地址当然可以指向任何地方,所以便有了指针的指针,如果再和数组,函数,结构和类联系上,那简直就是考验人的大脑。 C++的引用,定义了就必须在声明时就初始化,而且不能更改。
C#中,只有引用,一切“引用型变量”都是引用。但这个引用和C++中的引用不同,它更像一个“地址”。如果你声明了 Point p, p就是引用。但是这个p可能没有初始化,你也可以在任何时候改变它。
点评: 指针本来是好东西,但它太灵活,搞得太复杂了。反正我现在不大喜欢看*和&这类符号。
2.类的构造
C++中,可以使用两种方式新建一个变量:
(1) Point p, 你就构造了一个Point变量。 它处在栈区。
(2) Point* p=new Point; 它处在堆区:
最牛的在于,Point points[10]; 这样的声明,会直接产生十个Point对象,处于栈区。
也可以这么定义,Point* ps=new Point[10]; 处于堆区。
C#中,只有一种方式创建:Point p=new Point(); 处于堆区。
3. 交换两个对象
大家一定都记得,初学C++时经典的数字交换问题。
C++中, 两种做法: void Swap(int* a,int *b) 或者是 void Swap(int& a,int& b) ,这个没什么好说的
有意思的是,如果交换两个对象呢? 如果是void Swap(Point a, Point b),那你就已经错了,在执行这个函数时,拷贝构造函数会将你的实参分别拷贝到a,b两个对象中,处在堆区。当函数返回时,你除了浪费了一堆时间做无谓的拷贝工作外,对象还是原来的对象。 因此还得是 void Swap(Point* a, Point *b)或者 void Swap(Point& a, Point& b)。
C#中,交换值类型时,可以使用void Swap(ref int a, ref int b), 如果是引用类型,直接使用void Swap(Point a, Point b)就行了。
点评:你会发现,C++中根本就没有值类型和引用类型这回事,加上*,&才是“引用类型”,否则就是值类型,值类型传递,就会调用拷贝构造函数,造成一定的性能损失。而C#不存在这个问题。
4.函数返回值与工厂模式
设计模式的构造模式中,工厂是最常见的,你可以非常方便的写一个C#版本的工厂,但在C++中,怎么实现工厂?
Point GetProduct(){
Point p; //设置为默认值就可以了。
returnp;
}
调用时 Point *p= &GetProduct();
你觉得这样对么?留给读者讨论。
5.类的组合和对象拷贝
类可以组合,也可以继承。但在C++中,类组合会带来额外的问题,考虑如下的结构:
classPointEx{
public:
Point A;
Point B;
//其他成员和函数
}
那么你声明一个PointEx pex, 那么A,B两个对象就会被自动创建。如果希望能在构造函数中传递A,B两个类的参数,那必须使用“内部对象构造函数”,具体细节参考普通C++教材。而对象拷贝时,也带来了额外的隐患,默认的拷贝构造函数只能拷贝普通类型,但这些组合的成员,如果直接=的话,那么两个PointEx可能就指向同一个Point A。
在C#中,不存在这样的问题,首先A,B在PointEx的构造中,根本就不会被构造。除非你在PointEx中显式的new出A和B。
除此之外,C++中,子类和父类的构造函数与析构函数的执行顺序相当重要。C#中,析构函数被取消了(因为GC),代之以Dispose模式。
6. 函数指针和委托
首先,函数指针是无敌强大的,因为存储区分为堆,栈,代码区和全局数据区。所以代码区也是能被指针访问的,因此函数指针指向代码区,就能把函数流程指向那个区域。这是函数指针的实质。
C++中:你可以这样定义函数指针 int (*sqrt) (int x); (看着真恶心,你还没见函数指针数组,返回函数指针的函数等等丧心病狂的类型) 。可以这样赋值:
先随便定义一个函数sqrt2, 然后func= sqrt2;就可以了,
C#中:可以这样定义委托 delegate int sqrt(int x); 赋值类似。 当然还有更牛的“事件”,实现了多播委托(我理解就是委托数组)
点评: 委托的最大好处是强类型的,而且看着比函数指针优雅的多,功能也更强。
最近,我认识到了函数式编程的强大。如果能认识到“函数也是变量”,那么就是一个很大的进步。
7. 指针类型转换
先说普通值类型的指针类型转换。
C++中:定义两个指针, int*a 和float*b . 两个是不能直接赋值的。要想赋值,只能采用空指针作为中介:
void*pv= a;b= static_cast<float>(pv);
如果是对象,而且是有继承关系的,例如Point3D继承于Point,那么从Point*到Point3D*如何转换呢?
C#中,可以采用强制类型转换或as关键字:
Point3D p3d= (Point3D)p2d; 或者是 Point3D p3d= p2d as Point3D;
点评:类型是任何一门语言中都非常复杂的一部分。 C#有强大得多的类型系统和元数据模型,处理起来要更高级一些。 不过,我有个疑问,C++中,什么区域存储一个变量的类型?机器怎么知道某一块区域内的内存是int而不是double? 难道存储在代码区?
8. 类和结构体的区别
C++中,类和结构体本质几乎没有区别,结构体也能定义函数,定义不同的变量成员,只是所有成员都是公开的;而类有访问控制,可以实现继承和派生。
C#中:类是引用类型,结构体是值类型。结构体作为参数或返回值,都需要做拷贝。而且,结构体无法定义除了构造函数之外的成员函数。用结构体,可以获得比类更高的性能。
导致这些区别的原因:内存分配
其实,说这么多,导致C#和C++的 这些区别的本质原因,在于它们内存分配机制的不同。
C++中,不论是对象还是普通的值,如果是通过 Point p这样的语法生成的话,那么就在栈上。一旦函数结束,栈就被回收。只有new关键字生成的对象,才放在堆上。
放在栈上的数据因为随时可能被回收,才需要这么复杂的指针机制。指针的地址问题,这就像装盒子一样,一个盒子不安全,就多套几层盒子,盒子越套越多,搞得越来越复杂。
C#中,有值类型和引用类型的区别。所有的引用类型,都在堆上,回收靠GC处理。普通函数中定义的值类型都在栈上。如果是对象当中的“值”,那当然也定义在堆上。因为堆比栈方便多了,一个地址就可以了。所以使用起来要比在栈上方便的多。
要说性能,当然是栈比堆快,和内存访问的集中性有关,栈的数据基本都在高速缓存当中,命中率极高。但堆却不一定。 正是因为GC的作用,允许引用类型就定义在堆上。当然,我相信对于C#这样的语言,在编译或运行时应该会计算哪些是数据访问热点,从而优化命中率。 C#这类高级语言,是靠一个强大的“运行时”(runtime)和虚拟机来帮助它实现了这类区别。
我现在还不是很清楚,堆和栈的比例是怎么分配的。以前搞单片机的时候,会有一个编译选项选择它们的比例,我一般会把栈内存(heap)拉到90%,我不愿意用堆,因为觉得麻烦。
那么,高级面向对象语言需要指针么?
有了上面的那一段,估计大家都有答案了。因为有了强大的运行时支持,有了垃圾回收器,使得堆的使用率比栈大的多。虽然性能上会有那么一点点损失吧,但带来的确实是代码的简洁,高效,程序员再也不需要玩指针的游戏了。它带来了以下好处:
1. 省略了拷贝构造函数和析构函数
2. 简化了函数的参数和返回值,不需要再去调用拷贝函数了,性能有所提升。
3. 简化了对象组合的复杂性,C++内存管理本来就够复杂了,面向对象会变得更加复杂,稍微不注意就会造成内存泄露和指针悬挂等。而在C#中,你可以放心大胆的使用组合和继承。思路会清晰的多。
4.C#,JAVA代码好看得多,起码没有那些奇怪的符号。
5. 其他我还没有想到的好处。
从这个角度来说,指针的作用已经被“引用”类型代替了。
但是,C++不需要运行时和虚拟机,直接编译为原生代码,性能肯定更好,但确实C++难学。C++大牛肯定能列出一堆C++内存机制的好处,这个见仁见智了。
还有些遗留的问题,当开启C#的unsafe选项后,可以在C#里直接写C++,那么这种内存管理是如何完成的?