面向对象语言还需要指针么?

时间:2022-04-12 19:48:54

     大三那会还在搞单片机和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)就行了。   这句话是错的,交换对象,也必须使用ref关键字,因为a,b这两个引用,依旧处在栈上,它不能影响原有状态

     点评:你会发现,C++中根本就没有值类型和引用类型这回事,加上*,&才是“引用类型”,否则就是值类型,值类型传递,就会调用拷贝构造函数,造成一定的性能损失。而C#不存在这个问题。

4.函数返回值与工厂模式

      设计模式的构造模式中,工厂是最常见的,你可以非常方便的写一个C#版本的工厂,但在C++中,怎么实现工厂?

Point GetProduct()
{
Point p; //设置为默认值就可以了。
return p;
}

   调用时 Point *p= &GetProduct();

   你觉得这样对么?留给读者讨论。

5.类的组合和对象拷贝

     类可以组合,也可以继承。但在C++中,类组合会带来额外的问题,考虑如下的结构:

class PointEx
{
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#中:类是引用类型,结构体是值类型。结构体作为参数或返回值,都需要做拷贝。结构还可以包含构造函数常量字段方法属性索引器运算符事件嵌套类型,但如果同时需要上述几种成员,则应当考虑改为使用类作为类型。结构可以实现接口,但它们无法继承另一个结构。 因此,结构成员无法声明为 protected。(来自MSDN, 感谢冲杀同学的纠错)

  导致这些区别的原因:内存分配

   其实,说这么多,导致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++,那么这种内存管理是如何完成的?

     欢迎大家讨论!

 

     夜深了, 博主有点晕,给大家讲个笑话:烫烫烫烫烫烫,屯屯屯屯屯屯屯,笑话讲完了。