最近在微博上看到一个关于“C++的数组不支持多态”的问题的讨论,觉得很有意思。
其实一些写java较多的程序员
由于java的一些特性而使得他们不必太过于操心底层的一些东西,比方说内存问题。所以当java程序员转手写C、C++ 的时候会遇到一些困惑,进而就有人开始在微博上无敌的黑C++。事实上那些说C++不好用的人其实是连C语言都没学好,呵呵。
今天说两个问题,一是C语言的内存对齐问题;二是C中数组名跟指针的区别;分别举简单的例子。
如果这里struct A* pa =(struct A*) malloc(sizeof(struct B)*2) 并用pa对B中的变量赋值,然后struct B*pb = (struct B*)pa,那么pb所指向的内存就乱了。如果内存对齐了,而且struct A中的成员的顺序在struct B中是一样的而且在最前面话,那么就没有问题。这里就是 C语言里乱转型容易造成内存的混乱,这跟C++没关系。并且在C++相关书中也说过,父类对象和子类对象的转型会带来严重的内存问题。C++的标准会把虚函数表的指针放在类实例的最前面,这样做就是为了转型后不会找不到虚表了。
但是,如果你在class B中再加一个int成员的问题,这个程序就Segmentation fault了。(当然如果你用的微软的编译器可能A和B的size不一样也可以调用到析构函数,有人说这是微软对其的一种改进,呵呵。。。)
今天说两个问题,一是C语言的内存对齐问题;二是C中数组名跟指针的区别;分别举简单的例子。
内存对齐问题:
先看这段代码:
struct A { int a; char b; int c; };
printf("%d,", sizeof(struct A));
struct B { int a; char b; int c; char d;};
printf("%d\n", sizeof(struct B));
假设在32位的系统上,sizeof(int)=4,sizeof(char)=1那么输出结果是什么呢?
答案是:12,16。
有些人可能会感到困惑吧,这就是要说的C语言内存对齐或者说是字节对齐的问题。一般情况下内存对齐是向着4的倍数对齐的。(当然也有例外,不同的编译器会有不同的写法。)
- 为什么要字节对齐呢?为了提高性能。因为这些东西都在内存里,如果不对齐的话,编译器就要向内存一个字节一个字节的取,这样一来,struct A,就需要取9次,太浪费性能了,而如果一次取4个字节,那么三次就搞定了。
- 但是,为什么struct B不向12 对齐,却要向16对齐,因为char d; 被加在了最后,当编译器计算一个结构体的尺寸时,是边计算,边对齐的。也就是说,编译器先看到了int,那么是4字节,然后是 char,一个字节,而后面的int又不能填上还剩的3个字节,所以把char b对齐成4,于是计算到d时,就是13 个字节,于是就是16啦。但是如果换一下d和c的声明位置,就是12了。
如果这里struct A* pa =(struct A*) malloc(sizeof(struct B)*2) 并用pa对B中的变量赋值,然后struct B*pb = (struct B*)pa,那么pb所指向的内存就乱了。如果内存对齐了,而且struct A中的成员的顺序在struct B中是一样的而且在最前面话,那么就没有问题。这里就是 C语言里乱转型容易造成内存的混乱,这跟C++没关系。并且在C++相关书中也说过,父类对象和子类对象的转型会带来严重的内存问题。C++的标准会把虚函数表的指针放在类实例的最前面,这样做就是为了转型后不会找不到虚表了。
将内存对齐那么下面这段代码就可以正确执行,包括调用子类的虚函数。
class A
{
int b;
public:
virtual ~A(){ cout <<"A::~A()"<<endl; }
};
class B: public A
{
int i;
public:
virtual ~B() { cout <<"B::~B()"<<endl; }
};
int main(void)
{
cout << "sizeA:" << sizeof(A) << " sizeB:"<< sizeof(B) <<endl;
A *pb = new B[2];
delete [] pb;
return 0;
}
但是,如果你在class B中再加一个int成员的问题,这个程序就Segmentation fault了。(当然如果你用的微软的编译器可能A和B的size不一样也可以调用到析构函数,有人说这是微软对其的一种改进,呵呵。。。)
C中数组名跟指针的区别:
还是来看个例子:
int a[5];
printf("%x\n", a);
printf("%x\n", a+1);
printf("%x\n", &a);
printf("%x\n", &a+1);
这四个printf会输出什么呢?假设a的地址为:0x12ff34,32位系统。
第一个printf输出是0x12ff34,这个应该没什么问题。
第二个printf输出0x12ff38,如果你认为是0x12ff35那就错了,编译器会编译成 a+ 1*sizeof(int),int在32位下是4字节,所以是加4,也就是0x12ff38
第三个printf输出是什么呢?可能有些人对这个是最迷惑的,&a?那不就是a的地址吗,我怎么知道a的地址是多少?!好吧,我来告诉你,是0x12ff34。很多人会觉得指针和数组是一回事,其实不然。如果是 int *a,那么没有问题,因为a是指针,所以 &a 是指针的地址,a 和 &a不一样。但是这是数组a[],所以&a其实是被编译成了 &a[0],所以答案就是0x12ff34。
第四个printf输出也比较有意思,有人会说是0x12ff38,不对!因为是&a是数组,被看成int(*)[5],所以sizeof(a)是5,也就是5*sizeof(int),也就是0x12ff48。(这里地址是以16进制表示,所以5*sizeof(int)的16进制结果就是14)
现在看来写C语言程序不是你想像的那么简单的,起码C/C++程序员要留个心眼操心底层的东西,相对而言java程序员可能在这方面就比较轻松一点了。这里并不是说C不好,相反像java这样封装的过好的高级语言容易让程序员养成对其垃圾回收机制的过度依赖和惰性。
Dennis当初设计C语言的初衷:
1)相信程序员,不阻止程序员做他们想做的事。
2)保持语言的简洁,以及概念上的简单。
3)保证性能,就算牺牲移植性。
现在有很多高级的编程语言,语法也越来越复杂和强大,但是C语言依然光芒四射,Dennis离世了,但是C语言的这些设计思路将永垂不朽。
第一个printf输出是0x12ff34,这个应该没什么问题。
第二个printf输出0x12ff38,如果你认为是0x12ff35那就错了,编译器会编译成 a+ 1*sizeof(int),int在32位下是4字节,所以是加4,也就是0x12ff38
第三个printf输出是什么呢?可能有些人对这个是最迷惑的,&a?那不就是a的地址吗,我怎么知道a的地址是多少?!好吧,我来告诉你,是0x12ff34。很多人会觉得指针和数组是一回事,其实不然。如果是 int *a,那么没有问题,因为a是指针,所以 &a 是指针的地址,a 和 &a不一样。但是这是数组a[],所以&a其实是被编译成了 &a[0],所以答案就是0x12ff34。
第四个printf输出也比较有意思,有人会说是0x12ff38,不对!因为是&a是数组,被看成int(*)[5],所以sizeof(a)是5,也就是5*sizeof(int),也就是0x12ff48。(这里地址是以16进制表示,所以5*sizeof(int)的16进制结果就是14)
现在看来写C语言程序不是你想像的那么简单的,起码C/C++程序员要留个心眼操心底层的东西,相对而言java程序员可能在这方面就比较轻松一点了。这里并不是说C不好,相反像java这样封装的过好的高级语言容易让程序员养成对其垃圾回收机制的过度依赖和惰性。
Dennis当初设计C语言的初衷:
1)相信程序员,不阻止程序员做他们想做的事。
2)保持语言的简洁,以及概念上的简单。
3)保证性能,就算牺牲移植性。
现在有很多高级的编程语言,语法也越来越复杂和强大,但是C语言依然光芒四射,Dennis离世了,但是C语言的这些设计思路将永垂不朽。