在 C 语言中你所理解的数组并不总是语言概念中的数组。
C 是 70 年代的语言。与其更抽象的后代相比,它非常接近机器实现。因此,如果您想了解令人困惑的相似语法元素之外的内容,则必须考虑实现。
指针和数组都可以通过以下方式访问(方)括号表示法.
括号表示法无疑是有用的,但就指针和数组之间的混淆而言,它是万恶之源。
f[i]
正如我们将看到的,尽管底层机制会有所不同,但它也适用于指针和数组。
指针和数组的关系
让我们从变量声明开始。
Pointers
float * f
只是告诉编译器该符号f
有一天会引用未知数量的浮点数。
f
未初始化。由您决定实际数据的位置并进行设置f
指向他们。
指针算术和括号表示法
请记住,当您向指针添加/减去一个值时,单位是指向类型的大小。
float * f;
float * f3 = f+3; // internal value: f + 3 * sizeof (float)
// these two statements are identical
*f3 = 1;
*(f+3) = 1;
自从写下*(f+i)
当你想从指针引用连续的数据时,这是很尴尬的,括号表示法可以使用:
f[3] = 1; // equivalent to *(f+3) = 1;
无论使用哪种表示法,f[3] 的地址都按如下方式计算:
@f[
3] = f +
3* sizeof (float)
你可以考虑f
功能上作为一个(动态)数组,但在 C 看来,它仍然是一个pointer,通过使其看起来像数组的语法进行引用。
Arrays
float f[10]
仍然告诉编译器f
将引用一些浮点数,但它也
- allocates the requested number of floats at the appropriate location
- 在堆栈上,如果
f
是一个自动局部变量
- 在静态数据(又名 BSS)中,如果
f
是全局变量或静态变量
- 考虑符号
f
as a constant pointer到这些浮点值中的第一个
尽管数组创建语法可能令人困惑,但数组始终具有编译时已知的固定大小。
例如,float f[] = {2,4,8}
声明一个长度为3的数组,相当于float f[
3] = {2,4,8}
。为了方便起见,可以省略维度:长度反映了初始化器的数量,而不强迫程序员明确地重复它。
不幸的是,[]
记法也可以参考pointers在其他一些情况下(稍后会详细介绍)。
括号表示法和数组
括号表示法是访问数组内容的最自然的方式。
当你引用一个数组时,编译器knows它是一个数组。然后它可以根据数组第一个元素访问数据,如下所示:
@f[
3] = f +
3* sizeof (float)
对于一维数组(但仅在这种情况下!),您可以看到地址计算与指针完全相同。
数组作为指针
由于数组也被视为(常量)指针,因此您可以使用数组来初始化指针,但反过来显然是错误的(因为数组是一个constant指针,因此其值不能更改)。
插图
void test (void)
{
float* f1;
float f2[10];
float f3[]; // <-- compiler error : dimension not known
float f4[] = {5,7}; // creates float f4[2] with f4[0]=5 and f4[1]=7
f1[3] = 1234; // <--- write to a random memory location. You're in trouble
f2[3] = 5678; // write into the space reserved by the compiler
// obtain 10 floats from the heap and set f1 to point to them
f1 = (float *) calloc (10, sizeof(float));
f1[3] = 1234; // write into the space reserved by you
// make f1 an alias of f2 (f1 will point to the same data as f2)
f1 = f2; // f2 is a constant pointer to the array data
printf ("%g", f1[3]); // will print "5678", as set through f2
// f2 cannot be changed
f2 = f1; // <-- compiler error : incompatible types ‘float[10]’ / ‘float *’
}
走向多维
让我们将示例扩展到二维情况:
float f2[3][10]; // 2d array of floats
float ** f1; // pointer to pointer to float
f1 = f2; // <-- the compiler should not allow that, but it does!
f2[2][5] = 1234; // set some array value
printf ("%g\n", f2[2][5]); // no problem accessing it
printf ("%g\n",f1[2][5]); // bang you're dead
让我们看看这里发生了什么
当你声明float f2[3][10]
,编译器将所需的 30 个浮点数分配为连续块。前 10 个浮点数代表 f[0],接下来的 10 个浮点数代表 f[1],依此类推。
当你写的时候f2[
2][
5]
,编译器仍然知道f
is an array,因此它可以计算所需浮点数的有效地址,如下所示:
@f2[
2][
5] = f + (
2* 10 +
5) * sizeof (float)
您还可以访问pointers通过多个括号,前提是指针具有适当数量的引用级别:
当引用指针时,编译器简单地连续应用指针算术:
float h = f1[2][5];
相当于:
float * g = f1[2]; // equivalent to g = *(f1+2)
float h = g[5]; // equivalent to h = *(g +5)
f1[
2][
5]
由编译器处理为*(*(f1+
2)+
5)
。
最终地址将按如下方式计算:
@f1[
2][
5] = *(f +
2* sizeof (float *)) +
5* sizeof (float)
你已经要求了,你得到了
除了相同的括号符号之外还有两种截然不同的实现。
显然,当尝试访问f2
数据通过f1
,结果将是灾难性的。
编译器将从中获得第三个浮点数f2[2]
,将其视为一个指针,加 20 并尝试引用结果地址。
如果你通过这种写一些值错误初始化的指针,如果您遇到访问冲突而不是默默地破坏某些随机的四个字节内存,请认为自己很幸运。
不幸的是,即使底层数据结构无法正确访问,除非编译器意识到f2
是一个数组,f2
is still被视为常数float**
pointer