数组与指针被称为C语言里的结合数据类型,所谓结合数据类型是指,它不可单独存在,只能结合一种其他的数据类型进行定义,结合的类型可以是任何其他的类型。

数组与指针关系密切,使用中容易搞混,这节先从数组与指针的基础知识开始,从数组与指针的基本概念和内存图,到数组与指针的访问,再到搞清楚数组与指针的区别。

数组

数组代表了一系列连续存储的内存单元,可以通过以下方式定义数组:

数据类型 数组名[元素个数];

数组的成员叫做数组元素,比如int arr[5]定义了由5个int型元素构成的数组,数组名称是arr。通过数组方式定义的元素有以下特点:

  1. 数组的元素在内存中连续存储
  2. 各数组元素具有同一类型(大小相等)
  3. 数组名代表数组的首地址(第一个元素的地址)
  4. 可以在定义数组的时候给数组元素赋初值,比如:

    int arr[5] = {1, 2, 3, 4, 5};

下面是数组的内存布局。

Alt text

由于数组名代表数组的首地址,所以,可以通过解地址的方式访问数组元素,对于以上定义,可以用如下方式访问:

int arr[5] = {1, 2, 3, 4, 5};
printf("%d", *(arr + 0)); // 1
printf("%d", *(arr + 1)); // 2
printf("%d", *(arr + 2)); // 3
printf("%d", *(arr + 3)); // 4
printf("%d", *(arr + 4)); // 5 

数组与地址

arr代表了数组的首地址,那为什么通过 *(arr + 1) 的方式可以访问到数组的第二个元素呢?这需要了解地址的算术运算规律,这里以地址的加法运算为例进行讲解。
地址做加法与整数做加法是有区别的,整数在进行加法时,是完全按照数值进行相加的,比如1加1等2,而地址做加法时,是以地址的类型大小为单位进行相加的。以int arr[5]为例,arr代表了int类型元素的地址,由于int类型占用4个字节,那么,arr加1,实际上是数值增加的是4,这样,就可以通过*(arr + 1)的方式访问第二个数组元素了,以下是内存图解,注意arr + 1,arr + 2,arr + 3,arr + 4所指的地址与数值大小的变化规律:

Alt text

指针

指针泛指用于存储地址的类型,指针也是一种结合数据类型,定义指针的方式是:

数据类型 *指针变量名;

例如,可以通过 int *p; 的方式定义一个整型指针变量p。与定义整型变量一样,p本身也存储在程序的栈空间上,在32位操作系统下,所有指针都占用4个字节大小。

指针是一种结合数据类型,它希望存储的是它所结合的数据类型的地址,即使你存入别的任何数据,也会被当作该类型的地址对待。对于p而言,它希望存储的是整型变量的地址,如果存储的是其他类型变量的地址,那么编译器会给出警告,提示类型不匹配。而且在通过该地址取出内容时,也会按照p的类型长度来取内容,这样通常结果不是可预知的。

即然指针存储的是变量的地址,那么,就可以通过解地址(*)操作获取该地址处变量的值。以下是指针的一般用法:

int i = 1;
int *p = &i;
printf("%d\n",*p);

数组与指针的关系

数组就是数组,指针就是指针,它们之间没有任何关系,只是它们经常穿着相似的衣服来逗你玩罢了。

指针就是指针,指针变量在32位系统下,永远占4字节,其值为某一个内存的地址。指针可以指向任何地方,但不是任何地方都可以访问到,有的地方是不允许你访问的(比如内核空间)。

数组就是数组,它代表的是一片连续的存储单元,用数组名表示首地址,其大小与元素的类型和个数有关,定义数组时必须指定其元素的类型和个数;数组可以存放任何类型的数据,但不能存放函数。

为什么说数组和指针会经常混淆,因为它们都有相同的访问方式,使得它们看起来长得一样。对于指针和数组,都可以用下标的方式或是地址的方式进行访问。请看下面的例子:

A. int arr[5] = {1, 2, 3, 4, 5};
B. int *p = arr;

对于A,它定义的是一个数组arr,arr有5个int类型的元素,其空间大小为20,数组本身在栈上面。对于B,它定义了一个指针int类型的指针p,它占用4个字节的空间大小,存储在栈上面,对p赋值arr,那么p也指向数组arr的首地址。下面我们分别通过指针和下标的形式来访问数组arr和指针p。

访问数组arr:

  1. 以指针的形式访问:*(arr + 4)。arr代表数组首地址,arr + 4代表从arr开始偏移4个sizeof(int)的偏移量,得到的地址刚好是第5个元素的首地址,再通过(*)解出这个地址上的值是5;
  2. 以下标的形式访问:arr[4]。对于编译器而言,编译器总是把下标形式的操作解析为以指针的形式的操作。arr[4]这个表达式会被解析成:arr代表数组首元素的首地址,再加上中括号中4个元素的偏移量,计算出新的地址,然后从新的地址中取出值。

访问指针p:

  1. 以指针的形式访问:*(p + 4)。先取出p里面存储的地址值,再加上4个sizeof(int)的偏移量,得到新的地址,然后再取出新地址上的值。
  2. 以下标的形式访问:p[4]。编译器总是把以下标形式的操作解析为以指针形式的操作。p[4]这个表达式会被解析成:先取出p中存储的地址值;再加上中括号中4个元素的偏移量,计算出新的地址;然后从就的地址中把值取出来。

通过上面的例子可以看出,以下标的形式访问在本质上与以指针的形式访问没有区别,只是写法不同罢了。

关于数组名

int a[5]为例:
Alt text
注意:

a表示数组第一个元素的地址,&a表示整个数组的首地址,虽然它们的值一样,都是数组的首地址,但是含义完全不一样。举个例子,浙江省的省政府在杭州,杭州市的市政府也在杭州,两个政府都在杭州,但其代表的意义完全不同。

指针数组

指针数组本质还是数组,只不过它存储的成员是指针,比如int *p[3]定义了一个指针数组,它可以存储3个int型的地址值。
Alt text


对于指针数组,同样可以以指针形式和下标形式来访问到每个数组成员指向的值:

指针形式:

*(*(p + 0)) ==> a
*(*(p + 1)) ==> b
*(*(p + 2)) ==> c

下标形式:

p[0][0] ==> a
p[1][0] ==> b
p[2][0] ==> c

数组-其他

数组的定义与初始化

只定义不初始化

int a[5];   //由于未初始化过,所以内容为随机值

全部元素初始化

int a[5] = {1, 2, 3, 4, 5};

部分元素初始化

int a[5] = {1, 2}; //a[0] = 1, a[1] = 2,后续元素的值为0
int a[5] = {1};    //a[0] = 1,后续元素都赋值为0
int a[5] = {0};    //全部元素都赋值为0

全部元素初始化——省略长度

int a[] = {1, 2, 3, 4, 5}; //如果对全部元素都赋初值,则可以省略数组的长度

注意点:

不可以对数组进行整体赋值(数组名不可以作为左值)

int a[5] = {1, 2, 3, 4, 5};
int b[5];
b = a;  //错误写法

变长数组

C99标准支持数组长度为变量的数组,见下例:

int n;
scanf("%d", &n);
int a[n];


注意,这种方式需要编译器支持,并且通过这样的方式定义的数组只能存储在栈上,不能存储在数据段上(对应的类型是静态数组或全局数组)。

数组越界

C语言标准并未规定,对数组的越界访问会有什么后果,它只规定了,对数组的正常访问应该出现的结果。所有对数组越界访问的结果,C语言标准只归结为:未定义行为。未定义行为是指,你的程序可能崩溃,卡死,可能把你的硬盘格掉,等等等等。甚至,如果你运气真的很差,它还可能看起来正常工作(想想为什么正常工作还不是一件好事)。它有可能这次运行是这样的结果,而再次运行是那样的结果,你不能保证每次运行的行为都一样。(搬运自:http://stackoverflow.com/questions/1239938/accessing-an-array-out-of-bounds-gives-no-error-why

野指针问题

指针未指向某一合法地址时,严禁使用*运算符对该地址进行读写:

int *p;   //只定义未赋值,p存储了一个随机的地址
*p = 100; //出错,访问的有可能是任意的地址

这种错误称为访问野指针(悬垂指针suspended pointer)。为了防止以该方式访问了程序关键的数据区导致程序发生各种不可预知的错误,通常在定义的指针的时候,要将指针赋值为NULL,以指向系统的0地址处,如下:

int *p = NULL;

这样,如果还没对p赋过值的话,通过指针访问到的将是系统的0地址,而这个地址是明确不可以读写的,所以程序立即会终止并提示段错误(立即终止比系统出现莫名其妙的错误要好--!)。

指针的运算

当p1、p2指向同一个类型的地址时,两个指针可以进行运算,运算的规则如下:

  • 指针加(p1 + a):地址偏移量为a*sizeof(指针指向的类型)
  • 指针减(p1 – a):地址偏移量为-a*sizeof(指针指向的类型)
  • 指针相加(p2 + p1):无意义
  • 指针相减(p2 – p1):两个指针之间相差的元素个数
  • 指针比较(p2 > p1):比较指针的数值大小

字符数组与字符串

定义

字符数组:元素类型为字符型的数组。

char ch[5] = {'a', 'b', 'c', 'd', 'e'};

字符串:用一对双引号括起来的若干个字符序列。

char *str = "abcde";

字符串的存储形式与字符数组的存储形式是一样的,除了以下两点区别:

  1. 字符串本身存储在程序的只读存储区(非常低的一段地址空间)
  2. 字符串除了所有可见的字符外,末尾还有一个’\0’(ASCII码值为0),作为字符串结束的标志。

字符数组与字符串内存图

Alt text

字符串是存储在数据段的只读数据,只可访问,不可修改,所以像char *str="abcde";这样定义的字符串又叫做字符串常量。

访问字符串时需要知道字符串的首地址,可以把字符串的首地址保存到一个字符指针里面,这样,就可以通过这个字符指针访问字符串了,如下所示:

char *str = "abcde";
printf("%s\n", str);    //打印整个字符串常量
printf("%c\n", *str);   //打印字符串常量的第一个字符
printf("%c\n", str[1]); //打印字符串常量的第二个字符

字符串常量存储的值不可改变,像下面这样的操作会引起程序崩溃:

*(str + 2) = 'g';   //错误,字符串里面的值不可以修改

因为字符串的存储形式与字符数组一致,所以可以用字符串对字符数组进行初始化,如下:

char ch[] = "abcde";
char ch[] = { "abcde" };

与它们等价的写法如下:

char ch[6] = {'a', 'b', 'c', 'd', 'e', '\0'};
char ch[] = {'a', 'b', 'c', 'd', 'e', '\0'};

字符串的输入输出

  1. 字符串输出:
    使用“%s”格式输出字符串,从传入的首地址开始输出,直到遇到’\0’结束,举例:

    char *str = "abcd\0e";
    printf("%s", str); //输出abcd
  2. 字符串输入:
    输入时,需要提供不小于输入字符串长度的存储空间,这个空间可以是一段数组空间,也可是使用malloc分配的一段内存,举例:

    char str[1024] = {0};
    scanf("%s", str);

    输入遇到空白字符(空格、制表符、回车符)时结束,获得的字符中不包含空白字符本身,并且会在字符串的末尾添加’\0’作为结束标志。

字符串数组

由于字符串可以用首地址来表示,那么,多个字符串的首地址存储在数组里,就是字符串数组了,如下:

char *str[2] = {"hello", "world"};

该数组的第一个元素存储的是第一个字符串的首地址,第二个元素存储的是第二个字符串的首地址,以下是它们的内存图。

输出字符串数组里面的元素:

printf("%s", str[0]);
printf("%s", str[1]);







  • 无标签