C语言:数组与指针

36 阅读11分钟

C语言:数组与指针

1.前言:

在我们生活中,当我们需要将相同类型的物品放在到一起时,我们可以使用篮子,竹筐之类的作为分类的载体,盛放在一起,便于之后的查找,在编程中为了达到这一目的,便有了数组

众所周知,内存被分为了很多个内存单元,但假如这些内存单元没有了自己的编号时,那编译器就很难寻找其中的元素,就如生活中当我们需要进入一个大楼中访问自己朋友家时,倘若没有门牌号,我们就需要一间一间的寻找,但倘若有了门牌号,我们就只需要到相应的楼层去找相应的房间,同理,内存单元当有了相应的编号,便可以提高效率,所以便有了地址,即指针

2.数组

1.一维数组

1.数组的概念

相同数据类型的集合就是数组

何为一维数组呢?

首先,一维数组就是像下面这个例子一样

int arr[10]={1,2,3,4,5,6,7,8,9,10}

就是只有如一条线一样的排布,就像我们所知的一维,点和线一样

但是,在这里有一点需要我们注意的是:数组的每一个数据都有自己的小标,并且下标是从0开始的

#include <stdio.h>int main()
{
    int arr[10]={1,2,3,4,5,6,7,8,9,10};
    printf("%d %d\n",arr[0],arr[8]);
    return 0;
}

屏幕截图 2024-10-18 124629.png

由此,我们可以做到将整个数组打印出来

#include <stdio.h>int main()
{
    int arr[10]={1,2,3,4,5,6,7,8,9,10};
    int sz=sizeof(arr)/sizeof(arr[0]);
    for(int i=0;i<sz;i++)
    {
        printf("%d ",arr[i]);
    }
    printf("\n");
    return 0;
}

屏幕截图 2024-10-18 125450.png

在此需要说明一点数组通常是指该元素的首地址,除了一下两种情况

  • 使用了sizeof(数组名)
  • &数组名,此时取的是整个数组的地址
2.解释及要求

那么接下来我们将要对上面的数组开始解释

首先,[ x ],这里的x是指这个数组的最大能容纳的元素数量,例如上面的arr[10],10指的就是arr这个数组最多能容纳10个元素

对此,当然 [ ] 中可以不写数值,让编译器自己去完成这项工作

例如:

#include <stdio.h>int main()
{
    int arr[]={1,2,3,4,5};
    //这时在将鼠标放在arr上,就会发现[]中,已经由编译器填入应有的数据
    return 0;
}

屏幕截图 2024-10-18 130126.png

当然,既然有[ ]对数组的容纳量有要求,就不能使后面的元素大于其最大容纳量,否则会报错

初始值设定值太多

就是指我们的放入这个数组的元素已经超过了数组的最大容纳量了,这种也称为访问违规

当我们填入的数据不足填满时,编译器会自动帮我们在该数组剩下的空位置填0

#include <stdio.h>int main()
{
    int arr[10]={1,2,3,4,5};
    return 0;
}

通过监视,我们可以知道,剩下空位被填0了

屏幕截图 2024-10-18 131323.png

3.数组类型

接下来,就是定义数组前的数据类型是指该数组所存放的元素的类型

int arr1[5]={1,2,3,4,5};
char arr2[5]="abcde";
float arr3[5]={1.0,2.0,3.0,4.0,5.0};

数组后面存放的内容是什么,前面的数据类型就是对应的

假如不这样做,可能会导致发生一些错误,众所周知,int的字节是4,而char的字节是1,其范围是-128~127,若用char定义的数组去存放int类型的数据,会使该int类型的数据从32位直接被截成8位,这时就可能出现错误

例如:

#include <stdio.h>int main()
{
    char arr[]={5000};
    return 0;
}

屏幕截图 2024-10-18 132604.png

除此之外,前面的数据类型也从一方面写出了该数组的大小

#include <stdio.h>int main()
{
    int arr1[10]={0};//4*10=40
    char arr2[10]={0};//2*10=20
    double arr3[10]={0};//8*10=80
    printf("%zu\n",sizeof(arr1));
    printf("%zu\n",sizeof(arr2));
    printf("%zu\n",sizeof(arr3));
    return 0;
}

屏幕截图 2024-10-18 133013.png

还有就是,这些数据类型也从一方面写出了当我们下标+1时,数组中地址增加的量

#include <stdio.h>

int main()
{
    int arr1[10]={0};
    printf("%p\n",&arr1[0]);
    printf("%p\n",&arr1[1]);
    char arr2[10]={0};
    printf("%p\n",&arr2[0]);
    printf("%p\n",&arr2[1]);
    return 0;
}

屏幕截图 2024-10-18 190255.png

这里,我们可以看见int类型的数组中每个元素的地址差4个字节,char类型的数组中每个元素的地址差1个字节

2.二维数组

二维数组正如我们的二维空间一样,是一个面,所以其相较于一维数组,多了行,列的概念

#include <stdio.h>

int main()
{
    int arr[3][4] = { 1,2,3,4,2,3,4,5,3,4,5,6 };
    int sz1 = sizeof(arr) / sizeof(arr[0]);//行
    int sz2 = sizeof(arr[0]) / sizeof(arr[0][0]);//列
    for (int i = 0; i < sz1; i++)
    {
        for (int j = 0; j < sz2; j++)
        {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
    return 0;
}

该二维数组打印出来的结果是

屏幕截图 2024-10-18 191247.png

二维数组与一维数组几乎无差别,像二维数组的功能,也能通过一维数组达成

例如:

#include <stdio.h>

int main()
{
    int arr1[4]={1,2,3,4};
    int arr2[4]={2,3,4,5};
    int arr3[4]={3,4,5,6};
    int sz=sizeof(arr)/sizeof(arr[0]);
    for(int i=0;i<sz;i++)
    {
        printf("%d ",arr1[i]);
    }
    printf("\n");
        for(int i=0;i<sz;i++)
    {
        printf("%d ",arr2[i]);
    }
    printf("\n");
        for(int i=0;i<sz;i++)
    {
        printf("%d ",arr3[i]);
    }
    printf("\n");
    return 0;
}

这就是用一维数组来达成二维数组的方法,但是二维数组对于这种行列更适合,效率更高

但是这两种方法也是有所区别的:二维数组的元素在内存存储中是连续的,而一维数组不是连续的(大概率)

下标跟一维数组一样,都是从0开始的,但是前面[ ]表示行,后面的[ ]表示列

1.二维数组的用法
1.可以通过{ }来分组
#include <stdio.h>

int main()
{
    int arr[3][4]={{1,2},{3,4},{5,6}};
    for(int i=0;i<3;i++)
    {
        for(int j=0;j<4;j++)
        {
            printf("%d ",arr[i][j]);
        }
        printf("\n");
    }
    return 0;
}

屏幕截图 2024-10-18 193428.png

从这里,我们也可以知道,当该数组的一行的元素没满的时候,编译器会自动补0

2.{ }中的元素不能超过数组表示列的[ ]中的最大容量
#include <stdio.h>

int main()
{
    int arr[2][2] = { 1,{2,3,4} };
    return 0;
}

此时编译器会报错:

初始值设定项值太多

3.定义一个二维数组的时候,其行可以不定义,但列要求一定要定义
# include <stdio.h>

int main()
{
    int arr[][]={1,2,3,4,5,6,7,8};
    return 0;
}

此时编译器会报错:

数组不能包含此类型的元素

因为,当我们不对二维数组的列进行定义的话,编译器无法知道我们的想法,最终就会导致编译器凌乱,也就是报错

这也变相地告诉我们,arr[ ][ ]相当于将前面的arr[ ]当作了数组名,而后面[ ]就是该数组的最大容纳量,只不过该数组的全部元素在内存中是连续的

当然有一维数组,二维数组,那当然存在三维数组,甚至更高维的数组,这就不进行讲述了

3.指针

我们从前言中知道了指针就是存放一个元素的地址的,而用来保存该指针的变量就将其称为指针变量,但在我们通常的对话中,会将指针变量念作指针,但本质上其实还是指针变量

#include <stdio.h>

int main()
{
    int a=0;
    int* pa=&a;//这里pa就是指针变量,*在这里的作用是说明pa是一个指针,int用于说明pa指向的对象a的数据类型是int
    return 0;
}

1.指针的大小

在不同操作系统下的编译器中,指针的大小其实也不一样

在32位操作系统下,指针的大小是4个字节,而在64位的操作系统下,指针的大小是8个字节

这里我们可以验证一下:

#include <stdio.h>

int main()
{
    printf("%zu\n",sizeof(int*));
    printf("%zu\n",sizeof(char*));
    printf("%zu\n",sizeof(double*));
    printf("%zu\n",sizeof(float*));
    return 0;
}

32位操作系统:

屏幕截图 2024-10-18 200104.png 64位操作系统:

屏幕截图 2024-10-18 200120.png 但有人就会发现,这些指针前面的数据类型不管是什么,似乎大小都是一样的,这是事实,因为这里的数据类型只能说明其指向的对象的数据类型是什么,但除了这之外就没有别的区别了吗?当然是有的,但把这留到一会讲

2.指针的使用

我们既然知道指针能存放地址,那么只要对指针变量使用*(解引用),然后对该整体进行操作,这时就能对指针指向的对象造成影响

#include <stdio.h>

int main()
{
    int a=0;
    int* pa=&a;
    *pa=10;
    printf("%d\n",a);
    return 0;
}

屏幕截图 2024-10-18 201231.png

这就是指针的作用,其可以用在一些函数中,因为我们知道函数中的形参只是实参的一份临时拷贝,当函数结束时,形参就会销毁,故无法对实参造成影响,所以这时候就能使用指针,从而对实参造成影响

用函数将两个数字进行交换

#include <stdio.h>

void Switch(int* pa,int* pb)
{
    int tmp=*pa;
    *pa=*pb;
    *pb=tmp;
}

int main()
{
    int a=1,b=2;
    Switch(&a,&b);
    printf("%d %d\n",a,b);
    return 0;
}

屏幕截图 2024-10-18 201751.png

注意:使用指针的时候记得初始化,否则会形成野指针,即指针里存放的是随机值

#include <stdio.h>

int main()
{
    int a;
    int* pa=&a;//不能这样写,这里就是形成的是野指针,此时pa里存放的是随机值
    return 0;
}

避免野指针的方法:

1.指针初始化

2.小心指针越界(在像数组和指针使用时,指针不要指向数组外的空间)

3.指针指向空间放置即使是NULL

4.避免返回局部变量的地址(像函数中变量)

5.指针使用之前检查其有效性

3.指针间的运算

指针之间能够进行减法运算,这样便可以得到两个指针之间的元素总数,多用于数组中求其数组中的两元素间有多少的元素

#include <stdio.h>

int main()
{
    int arr[] = { 1,2,3,4 };
    int* parr = arr;
    for (int i = 0;; i++)
    {
        if (arr[i] == 4)
        {
            printf("%d", (parr + i) - (parr));
            break;
        }
    }
    return 0;
}
#include <stdio.h>

int main()
{
    char arr[]="abcdef";
    int* parr=arr;
    for(int i=0;;i++)
    {
        if(arr[i]=='\0')
        {
            printf("%d",(parr+i)-(parr));
            break;
        }
    }
    return 0;
}

4.指针的类型

指针的类型有像:int*,short*,long*,long long*,char*,double*,float*

而指针的类型主要取决于指针指向的对象的数据类型

当我们的数据类型用错时,可能会发生错误

还有指针中对常量的加减法与数组中相似

#include <stdio.h>

int main()
{
    int arr1[10]={0};
    int* parr1=arr1;
    printf("%p\n",parr1);
    printf("%p\n",parr1+1);
    char arr2[10]={0};
    char* parr2=arr2;
    printf("%p\n",parr2);
    printf("%p\n",parr2+1);
    return 0;
}

屏幕截图 2024-10-18 230457.png

因此,我们拥有了灵活使用的方法,可以根据我们对于地址是1个1个字节的移动,还是4个4个字节的移动

4.指针和数组

数组和指针通常可以混合使用

因为我们知道数组名通常指的是该数组的首元素地址,有地址通常就会与指针有交集

例如:

#include <stdio.h>

int main()
{
    int arr[] = { 1,2,3,4,5,6,7 };
    int* parr = arr;
    int sz = sizeof(arr) / sizeof(arr[0]);
    for (int i = 0;i<sz; i++)
    {
        printf("%d ", *parr + i);//也可以使用arr[i]
        //*(parr+i)实际上是=arr[i]
    }
    printf("\n");
    return 0;
}

屏幕截图 2024-10-18 232002.png

这也能够很好地访问数组中的内容

5.二级指针

二级指针顾名思义,就i是用于存放指针的指针变量

#include <stdio.h>

int main()
{
    int a=1;
    int* pa=&a;
    int** ppa=&pa;//这里ppa就是一个二级指针
    **ppa=2;
    printf("%d\n",a);
    return 0;
}

屏幕截图 2024-10-18 232955.png

二级指针跟普通指针的要点几乎差不多,就是得解释一下,第二个是指ppa是一个指针变量,而int指的是ppa指向的对象是一个指针,而这个指针指向的对象的数据类型是int

其与一级指针一样都能对真正的对象造成影响

6.总结

数组和指针在编程中十分常见,应用范围也很广泛,这两者也通常会一起搭配着使用