于是有一帮各领域的大牛们出来对喷互黑,当然主要是C、C++和Java三大阵营。像@左耳朵耗子,@GeniusVczh,@简悦云风等牛人基本上把问题说清楚了。@GeniusVczh作为一个编译学的狂热分子,主要从语言设计的角度阐述了本质,不过我想有些人看了之后不一定知道他的文章与上面的微博有什么关系(详细请看这里)。他的文章可以看做@左耳朵耗子文章的一个进阶。@左耳朵耗子主要从基础方面给出了说明,详细请看。我的看法基本与@左耳朵耗子的评论一致:
因此,我主要是对于@左耳朵耗子进行一点点补充说明。
没错!这哥们就是Java写多了。
因为在Java里,下面的代码完全是符合@丑芹的期望:
-
Base b = new Derived [10];
-
//....
- delete [] b;
Java中,所有对象变量都是指针。
因此,上面的Java代码本质上就是:
-
typedef base* Base;
-
typedef derived* Derived;
-
-
// ....
-
Base* b = new Derived [10];
-
// ...
-
delete [] b;
-
-
// C++中,则需要显式调用下面这段代码:
-
for (int i = 0; i < 10; ++i)
-
delete b[i];
-
- // 而在Java中,此时b数组里元素所指向的内存依然没有被释放,因为需要等到垃圾收集机制来处理。其收集过程与上面的C++循环的道理是一样的
为什么说基础知识很重要呢?因为@丑芹第一句话就是错的,明显是基础知识不牢靠导致的。他说:
我真的不知道数组的多态性是什么东西,C++和Java里也没有提过相关的概念。顺带黑一下国产的C/C++/Java教材,总是编造一些根本就不存在的概念然后把学生们搞到脑残。
那么,为什么C++里面,下面这段代码就是错的呢:
-
Base* b = new Derived [10];
-
//...
- delete [] b;
假定sizeof(Base) == 60,sizeof(Derived) == 64
new Derived [10];得到的是一个数组,这个数组的元素类型是Derived,那么整个数组占用的内存为640Bytes。而b是一个指向Base的指针,因此,Base* b = new Derived [10];的意思就是说,我有一块640字节的内存,通过类型为Base的指针b来访问。那么通过b来访问这640字节中包含的第i个元素时,需要进行指针算术,才能够访问到第i个元素。很不幸的是,按照C++的内存模型,指针算术p + i的做法是:将640字节视为一坨以字节为单位的线性存储区域,访问类型为Base的第i个元素等价于(char*)p + i * sizeof(*p),这个操作将p指向了这一坨内存的第 i * 60字节的地方。毫无疑问,这个位置肯定不是某个Derived元素的首地址。因此,数组中只有在访问第一个元素时,地址才是对的,访问其他所有元素时都会导致内存错误——当然,地址偶然也会正确,在这个例子里,p[64]就是正确的地址(类的开始位置),它指向第60个Derived的首地址。
因此,为了让指针算术得到正确的结果,并且使用多态机制,那么正确的写法应该是:
-
Base** b = new Derived*[10];
-
for (int *i = 0; i < 10; ++i)
- *(b + i) = new Derived();
这里面,只有在析构时跟C++的虚拟机制有关,其他的都是C++内存模型相关的,由于C++要兼容C,因此在内存模型上与C模型需要完全兼容。
现在大家应该都看到了,其实,内存模型和指针算术才是这个问题最核心的,最基础的知识点。