笔记 | 计算机系统基础:07-一次搞懂数组和指针!

零. 课程要点:
  • 数组
  • 指针
  • 指针数组
  • 数组指针

指针一直以来都是C语言学习者头疼的东西,&和*单个看的时候都还好,组合起来复杂一点就容易头晕,而且一旦用错了,就容易core dump。所以这次我们彻底把它给理清楚,一次搞懂,终身受用。而由于数组和指针又有许多相似之处,所以将二者统一起来学习。并要学会区分“指针数组”和“数组指针”这两个容易混淆的概念。

一. 数组

首先,什么是数组?
数组是用来存储具有相同类型的元素的集合。
因此要定义一个数组,就要定义这个数组中储存元素的类型,以及元素的个数,然后给这个数组起个名字

1. 数组的声明

type arrayName[arraySize];

例如double balance[5];表示声明一个类型为 double 的包含 5个元素的名称为 balance的数组。

数组声明后,下一步就要进行初始化。

如果不进行初始化的话会怎么样?
并不是所有未初始化的数组的值都是随机的。全局数组,也就是定义在main函数外面的数组,元素的默认值是全部为0的。局部数组,也就是定义在函数内部的数组,其值默认是随机的。为什么局部数组未初始化就会有随机值?
之前在介绍函数调用的时候,我们提到局部数组是放在栈区的,而指令是通过移动栈顶指针完成,由于栈不会清空,所以栈内的数据很有可能是上一次出栈时候遗留的数据,因此数据是随机的。

注:如果声明时不进行初始化,则必须给出数组元素的个数!double balance[ ];是会报错的。因为系统不懂该为它分配多少内存空间。

2. 数组的初始化
  • 第一种方式:声明时初始化
    double balance[5] = {1.1, 2.2, 3.3, 4.4, 5.5};
    a. 如果数组想要初始化为相同的元素,可以只给出一个值,如:
    double balance[5] = {1.1};
    b. 声明时初始化可以不给出数组大小,取初始化时元素的个数,如
    double balance[ ] = {1.1, 2.2, 3.3, 4.4, 5.5};

  • 第二种方式:逐个元素初始化。(记得数组的第一个元素下标为0!)
    balance[0] = 1.1balance[1] = 2.2balance[2] = 3.3balance[3] = 4.4balance[4] = 5.6
    注:若只初始化了部分元素,剩下的元素仍为随机值。

3. 数组的访问
  • 第一种方式:下标法:数组名[下标]
    例如double salary = balance[2];表示取balance数组的第3个元素给salary变量。

  • 第二种方式:指针法:*(数组名+下标)
    例如double salary = *(balance+2);表示取balance数组的第3个元素给salary变量。
    为什么可以这么访问?在C语言中,数组名a表示的是数组的首地址,也就是第一个元素的地址!从某种程度上来说,数组名就相当于一个指针。因此*a表示访问第一个元素,*(a+i)表示访问第i个元素。
    注意,这里a+i并不是表示把a的首地址直接加上整数i,而是a + sizeof(type)*i,如果数组元素的类型为int,那么比例因子就为4。

4. 二维数组

虽然可以创建多维数组,但一般也只用到二维数组。

  • 二维数组的声明
    type arrayName[x][y];,如int x[3][4];

  • 二维数组的初始化
    a) 第一种方式:声明时初始化
    int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7} , {8, 9, 10, 11} };
    ① 内部嵌套的括号可省略,所以上面语句等同于
    int a[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};
    ② 声明时初始化可以不给出第一个维度的大小,如:
    int a[ ][4] = { {0, 1, 2, 3}, {4, 5, 6, 7} , {8, 9, 10, 11} };
    注:不能使用int a[ ][ ] = { {0, 1, 2, 3}, {4, 5, 6, 7} , {8, 9, 10, 11} };
    ③ 如果数组想要初始化为相同的元素,可以只给出一个值,如
    int a[3][4] = {2};
    b) 第二种方式:逐个元素初始化
    a[0][0]=0, a[0][1]=1, a[0][2]=2, ......
    注:那可以使用a[0]={0,1,2,3}形式来初始化吗?不行!

  • 二维数组的访问
    a) 第一种方式:下标法。如int temp = a[2][3];
    a) 第二种方式:指针法。如int temp = *(*(a+2)+3);或者*(a[2]+3)
    第二种方式理解起来稍微有一点绕,二维数组中行的首地址取一次*或者[]等于该行首列元素的地址,也就是*(a+i)等价于a[i],该地址加上列的偏移量j后就定位到了第(i, j)个元素的地址,再进行一次加星号就可以访问其数据。

二. 指针

那么指针是什么?
指针本身是一个变量,它的值表示一个地址,这个地址是另外一个变量的内存地址
例如,a是一个int变量,值为23,存储在0x08048A08的内存地址处。那么如果p是一个指针变量,其值为0x08048A08,那么就表示p中存储的是a的地址,或者p指向a。为了表示这种关系,我们之后需要使用*&符号。

笔记 | 计算机系统基础:07-一次搞懂数组和指针!_第1张图片
指针

既然指针本身是一个地址变量,在IA-32中,它的长度就是32位。但是指针指向的内存地址所存放的变量类型和长度不确定,可能是int,可能是double。
因此要定义一个指针,就要定义这个指针中储存地址指向的元素的类型,然后给这个指针起个名字

1. 指针的声明

type *var-name;

例如int *ip;表示声明一个指向类型为 int的名称为ip的数组。也就是ip中地址指向的内存空间存放的是一个int型变量(占32位)。
又如char *cp;表示声明一个指向类型为char的名称为cp的数组。也就是cp中地址指向的内存空间存放的是一个char型变量(占8位)。由于C语言没有字符串类型,所以通常是将字符串放在一个字符数组中,再用指针取访问它。char str[] = "string"; char *pstr = str;表示pstr指向这个字符串的第一个字符s。

声明一个指针变量并不会自动分配任何内存。在对指针进行间接访问之前,指针必须进行初始化:或是使它指向现有的内存,或者给它动态分配内存,否则我们并不知道指针指向哪儿。

它可能指向一个非法地址,这时,程序会报错,在 Linux 上,错误类型是 Segmentation fault(core dumped)。它也可能指向一个合法地址,实际上,这种情况更严重,你的程序或许能正常运行,但是这个没有被初始化的指针所指向的那个位置的值将会被修改,而你并无意去修改它。

2. 指针的初始化
  • 第一种方式:声明时初始化
    int a = 23;
    int *p = &a;

  • 第二种方式:声明后初始化
    int a = 23;
    int *p;
    p = &a;
    注意,这里不要写成了*p = &a;!!!

为了更好的记忆,不至日子久了就忘记混淆,我们有必要来区分一下*&符号。

*有两层含义:
① 在声明时使用,表示该变量是一个指针,这是为了和普通变量的声明区分开(不过也确实容易让人糊涂)。所以你不能int *p = &a;表示对p间接访问,令它指向的内存空间赋值为a的地址,这是错误的!
② 在间接访问时使用,表示访问指针指向的数据,例如int a = 23; int *p = &a; *p = 100;,表示将p所指向的内存空间(a变量所在地址)赋值为100,也就是执行之后a=100。

&表示取一个变量的地址,所以既然p是一个地址变量,那么初始化时就应该给它赋值一个地址,即p = &a;

※ 巩固时间:请回答*&a&*p&p表示什么意思?应该很简单吧,不过多解释了,直接看运行结果吧。

#include 

int main () {
    int a = 23;
    int *p = &a;
    
    printf("&a = %p, a = %d \n", &a, a);
    printf("p = %p, *p = %d \n", p, *p);
    printf("*&a = %d, &*a = %p \n", *&a, &*p);
    printf("&p = %p \n", &p);
    
    return 0;
}
&a = 0xc8b4994, a = 23 
p = 0xc8b4994, *p = 23 
*&a = 23, &*a = 0xc8b4994 
&p = 0xc8b4998 

为什么p的地址刚好和a的地址差了4个字节?这个学习过前面关于函数栈文章的同学应该能一下明白。另外可以看出其实*&组合使用时可以互相抵消。

3. 指针的使用

一个指针变量其实有三个属性:

指针变量所在的地址&p
指针变量的值(指向的地址)p
指针变量指向的地址所存储的数据(间接访问)*p

所以在使用指针时,一定要牢牢记住想要使用的是哪一个属性,它表示什么意思,是什么类型?这样就不会搞不清楚究竟是要用&,还是*,还是什么符号都不加。

第①个的话,其实在代码中我们很少会需要访问或改变一个指针的存放地址,所以如果你在代码中看到对一个指针取地址,就要格外留意一下,是不是不小心写错了;
第②个的话,一般初始化时使用,或者想令指针重新指向一个新的数据,那么就需要使用这种格式(这时赋给它的是一个地址);

int a = 23, b=24;
int *p = &a;
printf("p = %p \n", p);
   
p = &b;
printf("p = %p \n", p);
p = 0x4cc44310 
p = 0x4cc44314

第③个的话,如果你目的就是访问或者改变指针所指向的数据的值,那就应该对它进行间接访问,正如在函数调用时按址传参一样。

int a = 23;
int *p = &a;   
*p = 24;
printf("a = %d \n", *p);
a = 24 

※ 发散思维:int **p;表示什么意思?
遇到这种复合的,首先要淡定,我们把它写成标准的形式type *var-name;,即int * *p;,那么这句代码还是声明了一个名称为p的指针,只是它现在指向的数据类型也是一个指针int *,即指向指针的指针,只要我们牢牢记住指针的含义,脑海中有下面这张图,这种形式就没什么特别不同。

笔记 | 计算机系统基础:07-一次搞懂数组和指针!_第2张图片
指向指针的指针

那怎么用呢?同样的,只要牢牢 记住*&的用法,该间接访问就间接访问,该取地址就取地址。

int a = 23;
int *p;
int **p1;

p = &a;
p1 = &p;

printf("a = %d, &a = %p \n", a, &a);    
printf("p = &a = %p, *p = a = %d \n", p, *p);

printf("p = %p, &p = %p \n", p, &p);
printf("p1 = &p =%p, *p1 = p = %p, **p1 = *p = a =%d\n", p1, *p, **p1);
a = 23, &a = 0xaecb0d2c 
p = &a = 0xaecb0d2c, *p = a = 23 
p = 0xaecb0d2c, &p = 0xaecb0d30 
p1 = &p =0xaecb0d30, *p1 = p = 0x17, **p1 = *p = a =23

可以看出,访问p的内容,要用一个星号*,访问a的内容要用两个星号**,其实只是绕了一下,并没什么特殊。

那可不可以定义一个指向一个指向指针的指针?甚至更多层的指针?留给大家试验。

4. 指针的运算

指针其实也是一个变量,只不过这个变量存储的是地址而已,既然是变量,那就可以对其执行算术运算:++、--、+、-。

不过要记住,指针在算术运算时跳跃的字节数取决于指针所指向变量数据类型长度,比如 int变量就是 4 个字节,那么p++不是表示把地址加1,而是把地址加上1个int的数据长度,即加上4。

int a = 23;
int *p = &a;   
printf("p = %p \n",  p);
p++;
printf("p++ = %p \n", p);
p = 0xc665c2c4 
p++ = 0xc665c2c8

记得我们上面在介绍数组的指针法访问时也提到过这个概念:**数组名a表示的是数组的首地址,相当于一个指针。因此*a表示访问第一个元素,*(a+i)表示访问第i个元素。但a+i并不是表示把a的首地址直接加上整数i,而是a + sizeof(type)*i,如果数组元素的类型为int,那么比例因子就为4。

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

p = a;
printf("a[0] = %d, *a = %d, *p = %d \n", a[0], *a, *p);
printf("a[1] = %d, *(a+1) = %d, *(p+1) = %d \n", a[1], *(a+1), *(p+1));
a[0] = 1, *a = 1, *p = 1 
a[1] = 2, *(a+1) = 2, *(p+1) = 2 

同样,两个指针之间也可以比较大小,不过这里比较的是存储的地址的大小,如果你要比较两个指针指向的数据之间的大小,记得用上*来间接访问。


-----(分割线)以上就是数组和指针的基本概念和用法,一定要掌握扎实了再继续学习哦,不然容易犯晕,接下去要介绍两个看起来长得很像的数据类型:指针数组和数组指针-----


三. 指针数组

念这个组合短语时要自己补充完整:

  • 指针数组是一个存放指针数组

所以这东西它其实就是一个数组,只不过数组里的元素都是指针而已。

1. 指针数组的声明

既然指针数组是一个数组,那它的声明格式应该和数组一样,type arrayName[arraySize];,只是指针的type是什么?其实上面我们介绍int **p;时已经提到过,指针的类型可以写成int *这种形式。

因此,可以用int * a[3];来声明一个存储3个指针元素的数组,其中每个指针都指向int类型的数据。

大部分人都喜欢写成int *a[3];,但是星号和变量凑在一起,很容易让你分不清楚到底是声明一个数组指针还是声明一个指针数组,除非你心中清楚[ ]的优先级高于*,因此这个声明首先是个数组。

2. 指针数组的初始化

指针数组里存放的都是指针,那有什么用?定义了几个指针,然后把他们都存进这个数组里的意义何在?
其实指针数组比较适合用来指向若干个字符串,使字符串处理更加方便、灵活。
更常用的是定义成const变量,作为数据字典使用。

const char *fruit[] = {
               "Apple",
               "Orange",
               "Banana",
               "Grape",
};

printf("I like  %s\n", fruit[2]);
I like  Banana

以上的代码,用二维数组也能实现同样的功能:

const char fruit[4][8] = {
               "Apple",
               "Orange",
               "Banana",
               "Grape",
};

但是二者还是不同的,指针数组声明时内存只是分配了4个32位的空间用来存放4个指针,里面暂时是空的(也有可能是脏数据)。等到初始化的时候,就为每个数组元素赋值为真正存放字符串的首地址。注意到这里每个字符串后面追加一个“\0”后按地址连续存放。

笔记 | 计算机系统基础:07-一次搞懂数组和指针!_第3张图片
指针数组 vs 二维数组

而二维数组在声明时就分配了4x8=32个字节的空间,初始化时按字符串的大小为对应地址空间赋值,这里每个字符串不论有效数据多少,都占8个字节,因此比较不灵活也比较浪费。

3. 指针数组的访问

既然是数组,那就可以用数组的通用访问方式,不过这样访问到的只是数组的元素内容,也就是一个个指针。

  • 第一种方式:下标法
    例如const char *p = fruit[2];表示取fruit数组的第3个元素(字符串或字符数组“Banana”的首地址)给p指针。(注意,这里定义p的类型时最好用const char *,而不是char *,否则会有编译警告,因为你试图修改一个变量的首地址)

  • 第二种方式:指针法
    例如const char *p = *(fruit +2); 这里容易迷糊,要记住,fruit+2并不是fruit[2],而是fruit[2]的地址,在上图中就是0xf6e710e0,要访问数组元素的内容,就应该再加一个星号。

那如果想访问指针元素所指向的内容呢?只需要再加个星号就行了。例如:
char c = *(fruit[2] + 4);或者char c = *(*(fruit+2) + 4);表示取第3中水果的第5个字符,即‘n’。
这个跟二维数组的元素访问方式是一样的。

看到这是不是觉得这些指针符号,数组符号,加减号,相互组合后也没有看上去那么复杂,弄清楚了数据类型在内存中的存放方式,就不难看懂这些表达式指的是什么。无非就是取地址,或访问地址,相互组合。那么再接再厉!来看看数组指针。

四. 数组指针

同样的,念这个组合短语时要自己补充完整:

  • 数组指针是一个指向数组指针

所以这东西它其实就是一个指针,只不过指针指向的内存空间里存放的是数组而已。而这个指针的值就是该数组的首地址。

1. 数组指针的声明

既然数组指针是一个指针,那它的声明格式应该和指针一样,type *var-name;,只是数组的type是什么?数组的类型可以看成int []这种形式。

因此,可以用int (*p)[3];来声明一个指针p,它指向的是一个包含3个int元素的一维数组。

因为( )的优先级高于[ ],因此这个声明首先是个指针。

2. 数组指针的初始化

既然数组指针是一个指针,那初始化的时候就应该给它赋一个地址,而且是一个数组的首地址。

int a[3][4]={ {1,2,3,4}, {5,6,7,8}, {9,10,11,12} };
int (*p)[4];
p=(int(*)[4])a;

for(int i=0; i<3; i++)
{
    printf("a[%d] = %p \t p+%d = %p \n", i, a[i], i, p); 
    printf("&p = %p \n", &p); 
    p++;
}
a[0] = 0x7fffdb136fd0    p+0 = 0x7fffdb136fd0 
a[1] = 0x7fffdb136fe0    p+1 = 0x7fffdb136fe0 
a[2] = 0x7fffdb136ff0    p+2 = 0x7fffdb136ff0 

定义了一个指向包含4个int元素的p,并把二维数组a的首地址赋给p,那么p的值就是二维数组第一行的地址,当对p进行加1操作时,根据前面指针的内容,我们知道应该要跨越sizeof(type)个地址,也就是一个4个int变量组成的一维数组。

这段形容跟之前的指针数组听起来好像,都是指针指向一个数组,不过两者还是不一样的,看一下它们中的存放方式就能明白二者的区别:

笔记 | 计算机系统基础:07-一次搞懂数组和指针!_第4张图片
数组指针
3. 数组指针的使用

如果要用数组指针来访问上面我们定义的二维数组的元素呢?
可以用下面三种形式:p1[i][j]*(*(p1+i)+j)*(p1[i]+j))

int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12};
int (*p1)[4];
p1=a;

for(int i=0;i<3;i++)
{
    for(int j=0;j<4;j++) 
    {
        printf("a[%d][%d] = %d = %d = %d\n", \
            i, j, p1[i][j], *(*(p1+i)+j), *(p1[i]+j));
    }
    printf("\n");
}

可以看出,同时用来指向二维数组时,形式看起来和指针数组好像一样,但其实它们的内涵还是略有不同的。

你可能感兴趣的:(笔记 | 计算机系统基础:07-一次搞懂数组和指针!)