把知识记在小本本上

将零散的知识点放在一个集中的地方,不断递归重构,形成一套为己所用的知识系统。

博客首页 | 小本本首页

数组的不对称边界。

一个小问题

如果一个数组有10个元素,那么这个数组下表的允许取值范围是什么呢?

下面代码1,这段代码的运行结果是什么?为什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
// 代码1
int main()
{
int i = 0;
int arr[10] = {0};

for(i=0; i<12; i++) // 注意这里是 i<12
{
arr[i] = 0;
printf("hey, girl!\n");
}

return 0;
}

结果出人意料。

这段代码在Windows下DevC++下运行结果是:死循环。。。

来看看why:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int main()
{
int i = 0;
int arr[10] = {0};

printf("&i = %p\n", &i);
printf("&arr[0] = %p\n", &arr[0]);
printf("&arr[10] = %p\n", &arr[10]);
printf("&arr[11] = %p\n", &arr[11]);

return 0;
}

从输出的结果可以得到iarr[]在内存中的样子:

&arr[11] == &i,当循环中做arr[11] = 0;这步操作时,也把循环变量i的值改为了0,这样就陷入了一个死循环。

我们把int i = 0;这句话写在数组int arr[10] = {0};这句的后面再试试:

可以看到,这次 i 的地址就在数组arr地址的下面了,这下就不是死循环的情况咯。

另外!对于不同平台下代码1的运行结果是不同的:

  1. Microsoft Visual Studio在debug下是死循环,但是在release下编译运行程序就不是死循环,debug模式下没有进行任何优化,在release模式下将变量在内存中进行了优化,不会出现死循环。

  2. 在MAC OS gcc下编译执行代码1,不会死循环,而且不论i是定义在数组上面还是数组下面,i的地址都是在数组首元素地址的前面,数组向后访问,不会访问到i。

    pic1

    pic2

    嘿嘿,如果i是负数的话,还是会改变到循环变量i的地址滴,下面这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #include <stdio.h>

    int main()
    {
    int i = 1234;
    int arr[10] = {0};

    printf("&i = %p\n", &i);
    printf("&arr[0] = %p\n", &arr[0]);

    for(i=0; i<12; i++)
    {
    arr[-i] = 1;
    printf("nice to meet you!\n");
    }

    return 0;
    }

不对称边界

C语言中数组的这种特别的设计正是其最大优势所在。 ——《C陷阱与缺陷》

在编码过程中,for语句循环控制变量采用半开半闭区间的写法。 ——《高质量C++/C编程指南》的建议【4-5-1】

why?

这个其实是为了增加代码的可读性,看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main()
{
// 1
for(int i=16; i<=37; i++)
{
something1();
}
// 2
for(int j=16; j<38; j++)
{
something2();
}

return 0;
}

问题:在第一个循环中something1()被调用了多少次,在第二个循环中something2()被调用多少次。

答案:37-16+1 = 22次,38-16 = 22。

显然半开半闭区间比较直观。

这种不对称也许从数学上而言并不优美,但是它对于程序设计的简化效果却足以令人吃惊:

  1. 取值范围的大小就是上界与下界的差。38-16的值是22,恰是不对称边界16和38之间所包括的元素数目。
  2. 如果取值范围为空,那么上界等于下界。
  3. 即使取值范围为空,上界也永远不可能小于下界。

像C语言这样的数组下标从0开始的语言,不对称边界给程序设计带来的便利尤为明显:这种数组的上界(即第一个“出界点”)恰是循环执行的次数。

按照这种不对称边界的惯例,有如下语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
char* my_strcpy(char *str_dest, const char *str_src)
{
assert(str_dest != NULL);
assert(str_src != NULL);

char *tmp = str_dest;

while((*tmp++ = *str_src++)) // 这里是重点
{
;
}

return str_dest;
}

另外:在计算字符串长度时候,我们可以用这样的上界指针减去字符串首元素地址的指针然后除每个元素大小。

string_length = (upp - downp)/sizeof(downp[0]);


参考书籍:

《高质量C++/C编程指南》、《C陷阱与缺陷》

为什么C语言从0开始编号