C-C++到底支不支持VLA以及两种语言中const的区别
C-C++到底支不支持VLA以及两种语言中const的区别
到底支不支持VLA
VLA就是variable-length array,也就是变长数组。
最近写程序的时候无意间发现,gcc中竟然支持下面这种写法:
int n = 10;
int a[n];
注意上面的语句是在函数内部写的,也就是n和a都是自动变量。
当时十分疑惑,C语言中数组的长度不应该是常量或常量表达式吗?为什么变量也可以。我将代码在VC中跑了一下,发现编译出错,提示数组的大小未知,说明VC中是不支持VLA的。
那既然有的编译器支持VLA,又有的编译器不支持VLA,那么C标准到底是怎样规定的呢?然后我看是看书、在网上查资料。
C Primer Plus一书中是这样描述的:
C90标准中并不支持VLA,C99开始支持VLA,很大的一个原因:FORTRAN中支持这种写法。C99中对对VLA有一些限制,比如变长数组必须是自动存储类型,也就是说,如果我上面两句放在函数外面就就不能通过编译了,这是因为在函数外面定义的是全局变量,此外,使用VLA不能对数组进行初始化,因为它的长度在运行时才能确定。
此外VLA并不是真正的变长,它实际上只是将数组的长度推迟到运行时确定而已,也就是说C90标准中,数组的长度必须在编译时期就知道,但C99支持VLA后,数组的长度可以推迟到运行时知道,但是,一旦长度确定,数组的长度就不能变了。
此外,网上的大神说,C++的标准中无论是C++90还是C++99还是C++11都不支持VLA的这种写法。
鉴于以上原因,在C语言中,如果想用变长的数组,还是老老实实用malloc分配吧,在C++中当然有更好的选择,就是vector,当然C++11中又推出了一个array,而且这两种都是真正的变长,也就是数组的长度随时都可以改变。
下面我还想说一下C和C++中const关键字的区别。
const关键字最早是C++中的产物,后来才引入到C语言中。const在C语言中和在C++中是相当不一样的。
在C语言中const修饰的,被认为是一个只读的、或者叫不可改变的变量,它实际上只是一个变量,只不过对这个变量做了一些限制。而C++中const修饰的才是真正的常量。当然这其中还有很多细节的地方,有些地方我也还有些模糊,下面,我就通过例子,把自己已经理解的东西写出来。
先来一个例子:
const int a = 10; int array[a]; int main() { return 0; }
用gcc和g++编译的结果分别如下:
可以看到gcc下不能通过编译,但是g++下可以通过,说明C语言中有错,在C++中没错。
原因解释:
首先说C语言中:
首先说明,即使在支持VLA的编译器下,(我的gcc是支持的),前面提到了VLA数组是有限制的,VLA必须是自动存储类型,而上面的代码中数组是全局变量,所以并不存在是否支持VLA的问题。上面提到,在C语言中const被认为是一个受到一定限制的变量,是变量就要被分配数据区(或者运行时的栈区等)内存空间,由于a是全局变量,全局变量位于数据区,空间在编译时期就分配了。而,需要注意,编译时期,编译器是不能读取数据区的内存的(它可以分配数据区的内存,并初始化内存,但是不能从数据区的牛叉女内存中读取数据)。所以在编译时期,编译器其实并不知道a的值是什么,因为它不能读数据区的内存而a的值是在内存中的。但是,对于数组array编译器是一定要知道数组的长度才行的,也就是必须要知道a的值,这样就矛盾了,所以编译器就报错了!
那在C++中有为什么能够通过呢?
原因就是C++真的把const当成常量看待。
详细解释一下:
const int a = 10;这条语句中10是我们所说的字面量,无论是在C中还是在C++中字面量都是保存在代码段中,编译初期会将其保存在符号表中。C++尽量不对const分配数据区(或者运行时的栈区)的内存空间,只在必须分配内存时才分配(这个后面再说)。下面一条语句int array[a],编译器一定要知道a的值的,C语言要想知道a的值,必须读内存,但是C++却不需要,直接读取代码段中的的符号表即可,编译时期访问符号表是没有任何问题的,但是访问数据区的内存是做不到的。所以上面的语句在C++中是没有问题的。
再来说说,什么叫尽可能不为const分配内存。
如果代码是这样
const int a = 10; const int *p = &a; int array[a]; int main() { return 0; }
注意 const int *p = &a;这句,对a取地址操作,我们知道位于代码段的数据是不取地址的,所以这个时候,只能给a在数据区分配空间了。
于是就出现了新的问题,既然给a分配了数据区的空间,那是不是编译时期就不知道a的值了,因为毕竟编译时期是不能读取数据区的内存的,那么后面数组的定义也就不行了吧?但是答案却相反,依然可以,这是因为当编译器读a的值的时候,不是从数据区的内存中,而是程序段的符号表中读取的那个字面常量10。所以在编译实际依然能够确定数组的长度。
下面的例子应该更能说明这个问题:
#include <stdio.h> int main() { const int a = 10; int *pa = (int *)&a; printf("pa指向的地址为:%p a的地址为:%p\n",pa,&a); (*pa)++; printf("a = %d,*pa = %d\n",a,*pa); return 0; }
我们分别用gcc和g++编译他,然后分别看结果,如下:
惊奇地发现,虽然都能顺利通过编译,但是C的执行和C++的执行竟然不一样!
好吧,下面解释原因。
还是要声明一下,C语言中const就是一个值不能改变的变量,就是个受限制的变量,但是,我们虽然我们不能通过a修改那块内存的值,但是我们可以通过指针间接去修改。这里要注意那个强制类型转换,如果不写强制类型转换,编译器就会报错,是允许将const int *赋值给int*的。在C++中,这一点和C是一样的,就是虽然我们不能通过a本身修改那块内存的值,但是我们可以通过指针间接去修改。但是为什么C和C++中的输出不一样呢?原因就是C++在读a的时候,其实是去代码段中读字面常量10去了,而C是读a所标识的那块栈区的内存。其实a所标识的内存的内容都已经变成11了,无论是C还是C++都是一样,区别就在于C读const数据和读普通变量一样,都是从数据段(如果是局部变量就是从栈区)读取数组,而C++却是读取代码段的字面常量!(间接修改const的时候,当然都是修改的数据区或栈区的内存,而不是代码段,因为代码段是只读的)
所以C++中const修饰的可以认为就是常量!但是C语言中却不能这么认为。
最后要小心C++中的const蜕变成C语言中的const。
其实通过上面的分析,我们应该可以得出一个结论:C++中的const之所以和C语言中的const不一样,C++中的const之所以能够看成常量,就是因为C++在读取const的时候,实际上读取的是代码段的字面常量,而不是数据区(对于全局变量来说是静态区,对于局部变量来说是栈区)的内存中的数值。
那么问题来了:如果const保存的不是一个字面常量呢?
看下面代码:
#include <stdio.h> int main() { int i = 10; const int a = i; int *pa = (int *)&a; printf("pa指向的地址为:%p a的地址为:%p\n",pa,&a); (*pa)++; printf("a = %d,*pa = %d\n",a,*pa); return 0; }
几乎还是同样的代码,只是先把字面常量10赋值给了变量i,然后用i初始化const int a,但是我们发现,在C++中,执行结果却变了。
为什么呢?前面强调了,C++读取const的值,实际上是读取代码段的字面常量,那么,如果我们初始化const的时候,给它的不是字面量(或者是常量表达式),那么他就没有字面量可以读啦!这时候就只能退而求其次,去读数据区内存中的值啦!这个时候,C++中的const和C语言中的const就一样了。
需要注意的是 sizeof是C和C++中的运算符,而且他的值通常都是在编译时确定的,可以认为是一个字面常量。
比如:
#include <stdio.h> int main() { const int a = sizeof(int); int *pa = (int *)&a; printf("pa指向的地址为:%p a的地址为:%p\n",pa,&a); (*pa)++; printf("a = %d,*pa = %d\n",a,*pa); return 0; }
此外在类中使用const修饰类成员变量的时候也要小心,因为也会退化成C语言中的const
比如:
#include <stdio.h> class A{ public: const int a; int array[a]; A(int i):a(i) { } }; int main() { return 0; }
编译器会报错:
首先,类只是定义类型的地方,不能再类中初始化成员变量,所以在类定义中写const int a = 10是不对的,const成员的初始化要放在初始化列表中。我们知道,在对象创建之前才会调用构造函数进行对象的初始化,所以在编译时期我们根本就不知道a的值。所以把a当做数组的长度是有问题的。
我们可以这样:
#include <stdio.h> class A{ public: static const int a = 10; int array[a]; A() { } }; int main() { return 0; }
这样定义的a就可以当成一个常量来使用了。注意的是,这时候,a的语句不是声明了,而是定义+初始化,而且C++只允许这种情况可以在类内初始化,就是当变量被 static 和const修饰,且为int类型时。(当然这种情况依然可以在类外定义和初始化,只不过后面就不能用a定义数组的长度了,也会提示数组的长度不确定)
简单总结一下就是:
C语言中const就是一个受限制的变量,读取const时是从数据区的内存读取的(全局从静态去,局部从栈区),可以用指针间接修改其标识的数据区的内存区域,在读取const的值时,也是读取它标识的数据区的内存中的值。
在C++中,const大多数情况下可以当成常量来使用,这是因为,虽然C++也会为const在数据区开辟内存(C++尽量不这样左),我们也可以通过指针(或者非常量的引用)来简介修改其标识的数据区的内存区域的值,但是,在读取const时,不是读取数据区的内存区域中的值(也很有可能根本就没有分配内存),而是读取代码段的字面常量。所以可以达到常量的效果。
最后要警惕C++中const退化成C语言中的const,有两种情况,一种是初始化的const的时候是用变量初始化的,而不是字面常量(或常量表达式)。第二种情况就是const修饰类中成员变量的时候。
给一个建议:如果我们想用const定义常量,那么我们就要用字面常量或常量表达式初始化const,而且要将const用static修饰(注意在C++中如果定义的是全局的const,默认为static修饰,但是在类中并不是这样的,这也是为什么我们在类中定义常量要用static修饰)。在类中定义常量的时候要同时用cosnt和static修饰,而且尽量在类的内部进行初始化。
如果你觉得对你有用,请赞一个吧