百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

C语言之指针与数组(c语言数组指针和指针数组的区别)

zhezhongyun 2025-04-06 23:33 44 浏览

一维数组中的指针

数组(Array)是一系列具有相同类型的数据的集合,每一份数据叫做一个数组元素(Element)。数组中的所有元素在内存中是连续排列的,整个数组占用的是一块内存。以int arr[] = { 99, 15, 100, 888, 252 };为例,该数组在内存中的分布如下图所示:

一维数组名:可以隐式转换为指向数组首地址的指针

定义数组时,要给出数组名和数组长度,数组名可以认为是一个指针,它指向数组的第 0 个元素。在C语言中,我们将第 0 个元素的地址称为数组的首地址。以上面的数组为例,下图是 arr 的指向:

数组下标为啥从0开始?

数组下标实际上是每个元素的地址相对于第一个元素地址的偏移量。如下图所示:

一维数组名可以隐式转换为指向数组首地址的指针,但是不可对数组名直接自增自减操作,数组名的位置不可改变。所以一般会用指针指向数组去操作,如下测试代码:

#include 
int main() 
{
  int arr[5] = { 99, 15, 100, 888, 252 };
  int* p = arr;
  //等效 p=&arr[0]
  //错误,数组名不可改变位置
  //arr++;
  for (int i = 0; i < 5; i++) 
  {
    //*(arr + i)等效arr[i] 等效p[i]等效(arr+i)[0]等效(p+i)[0]
    printf("%d\t", *(arr + i));
  }
  printf("\n");
  return 0;
}

&一维数组名:可以隐式转换为指向整个数组的数组指针

数组指针是指向整个数组的指针,在做偏移的时候就是整个数组占用的字节数。如下测试代码:

#include 
int main() 
{
  int arr[5] = { 99, 15, 100, 888, 252 };
  int(*p)[5] = &arr;
  printf("p:%p\t&arr:%p\n", p, arr);
  printf("p+1:%p\t&arr+1:%p\n", p + 1, arr + 1);
  for (int i = 0; i < 5; i++)
  {
    printf("%d\t", p[0][i]);
  }
  printf("\n");
  return 0;
}

运行结果如下:

0000001512BEFB5C- 0000001512BEFB48=0000000000000014 ,十六进制0000000000000014 就是十进制20,刚好是sizeof(int)*5 个字节,自己电脑上运行的地址肯定会改变,但是偏移量肯定是一样的。

数组名和普通指针的区别

虽然说数组名可以当做指针使用,但实际上数组名并不等价于指针。

  • 数组名代表的是整个数组,具有确定数量的元素
  • 指针是一个标量,不能确定指向的是否是一个数组
  • 编译器会把数组名转换为一个指针常量,是数组中的第一个元素的地址

如下测试代码:

#include 
int main() 
{
  int arr[5] = { 99, 15, 100, 888, 252 };
  //隐式转换
  int* p = arr;
  //指针变量64位中占用8个字节
  printf("sizeof(p):%zd\n", sizeof(p));
  //整个数组占用内存
  printf("sizeof(arr):%zd\n", sizeof(arr));
  //数组名取地址并不是二级指针,而是数组指针
  int(*parr)[5] = &arr;
  for (int i = 0; i < 5; i++) 
  {
    printf("%d\t", parr[0][i]);
  }
  printf("\n");
  return 0;
}

运行结果如下:

指针操作一维数组

示例程序| 各种方式遍历一维数组

在一维数组中以下访问元素的方式都是等效的,不过在用指针操作的时候强烈建议新手采用下标法。

  • *(arr+i)
  • arr[0]
  • (arr+i)[0]

当指针指向一维数组的首地址的时候,也存在上述一样的用法,并且还有其他的使用方式,如下测试代码:

#include 
int main() 
{
  int arr[5] = { 99, 15, 100, 888, 252 };
  //隐式转换
  printf("数组名指针方式:\n");
  for (int i = 0; i < 5; i++)
  {
    //*(arr+i)等效arr[i]
    printf("%d\t",*(arr+i));
  }
  int* p = arr;
  printf("\n指针方式:\n");
  for (int i = 0; i < 5; i++)
  {
    printf("%d\t", *(p + i));
  }
  printf("\n指针下标方式1:\n");
  for (int i = 0; i < 5; i++)
  {
    printf("%d\t", p[i]);
  }
  printf("\n指针下标方式2:\n");
  for (int i = 0; i < 5; i++)
  {
    printf("%d\t", (p+i)[0]);
  }
  printf("\n指针偏移方式:\n");
  for (int i = 0; i < 5; i++)
  {
    //等效*(p++);
    printf("%d\t",(p++)[0]);
  }
  printf("\n数组指针方式:\n");
  int(*parr)[5] = &arr;
  for (int i = 0; i < 5; i++) 
  {
    printf("%d\t", parr[0][i]);
  }
  printf("\n");
  return 0;
}

运行结果如下:

示例程序| 指针操作一维数组负下标理解

在一维数组中如果用指针指向数组首地址后,在操作过程中对指针对了一些偏移后,或者指针不是指向首地址,下标法的含义是完全不同的,如下测试代码:

#include 
int main() 
{
  int arr[5] = { 99, 15, 100, 888, 252 };
  int* p = &arr[2];
  //p[0]等效 *(p+0)
  printf("%d\n", p[0]);
  //p[-2]等效*(p-2);
  printf("%d\n", p[-2]);
  return 0;
}

运行结果与图解如下:

示例程序| 指针操作字符串(字符数组)

指针操作字符类数组,要注意的是边界问题,尤其是在字符串中的字符调整,例如反转字符串操作,统计字符串长度等操作。指针操作字符串,注意常量字符串,建议养成const修饰指针的写法,C++对于const要求更为严格,如下测试代码:

#include 
int main() 
{
  //表示字符串常量,字符串常量的首地址赋值给指针了
  //建议加上const修饰 C++中没有const是错误的
  const char* cpstr = "coolmoying";
  //错误常量无法被修改
  //cpstr[0] = 'A';
  puts(cpstr);
  //指针单纯存储地址功能,可更改存储内容
  cpstr = "Iloveyou";
  puts(cpstr);
  //把"coolmoying"拷贝到了str中
  char str[] = { "coolmoying" };
  char* pstr = str;
  //可修改变量的数据
  pstr[0] = 'A';
  puts(pstr);
  //统计字符串可见长度
  int count = 0;
  while (*pstr++ != '\0')
    count++;
  printf("length:%d\n", count);
  return 0;
}

运行结果如下:

二维数组中的指针

二维数组可以理解为每一个元素都是一个一维数组的数组,这样就可以很好的理解二维数组与指针了。下面定义了一个2行3列的二维数组,内存模型如下:

二维数组名:可以隐式转换指向数组的指针

数组指针在做偏移的时候,每次都是偏移一行,如下测试代码:

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

运行结果如下:

二维数组名[下标]:可以隐式转换一级指针

二维数组名[i]:代表第i行的首地址,可以隐式转换为一级指针。

&二维数组名[i] :代表的是第i行的首地址,并且可以隐式转换为数组指针。

&二维数组名[i][j]:当前行列元素的地址,

如下测试代码:

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

    for (int i = 0; i < 2; i++)
    {
        int* p = arr[i];
        for (int j = 0; j < 3; j++)
        {
            printf("%d\t", p[j]);
        }
        printf("\n");
    }

    int(*pp)[3] = &arr[0];
    for (int i = 0; i < 2; i++) 
    {
        for (int j = 0; j < 3; j++) 
        {
            printf("%d\t", pp[i][j]);
        }
        printf("\n");
    }
    return 0;
}

运行结果如下:

行列指针

行指针:是指向一整行,并不是指某个具体元素

  • 二维数组名[下标]

列指针:是指向某个具体元素

  • 二维数组名
  • 二维数组名+下标

指针操作二维数组

示例程序| 一级指针遍历二维数组

在显存指针操作的时候就需要使用一级指针操作二维数组,由于二维数组的内存是连续的,所以一级指针依然可以操作,只需要把行列转换为序号即可,如下测试代码:

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

    return 0;
}

运行结果如下:

示例程序| 数组指针遍历二维数组

二维数组访问元素的方式比较多,以下方式都可以:

  • arr[i][j]
  • *(*(arr+i)+j)
  • *(arr[i]+j)
  • *((arr+i)[0]+j)
  • (arr[i]+j)[0]
  • ((arr+i)[0]+j)[0]

当数组指针指向一维数组的首地址的时候,也存在上述一样的用法,并且还有其他的使用方式,如下测试代码:

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

示例程序| 数组指针操作多个字符串

多个字符串可以采用二维数组存储,每一行存储一个字符串,操作每一行即可操作每个字符串,例如多个字符串的排序问题,如下测试代码:

#include
#include
int main()
{
    char str[][10] = { "coolmoying","moying","ying" };
    char (*p)[10] = str;
    for (int i = 0; i < 3; i++) 
    {
        for (int j = 0; j < 3 - i - 1 j if strcmppj pj1>0)
            {
                char temp[10] = "";
                strcpy(temp, p[j]);
                strcpy(p[j], p[j + 1]);
                strcpy(p[j + 1], temp);
            }
        }
    }
    for (int i = 0; i < 3; i++)
    {
        puts(p[i]);
    }
    return 0;
}

动态内存申请

C语言内存四区

C语言在程序执行的时候,内存划分为4个区域

  • 代码区:存放函数体的二进制代码,由操作系统进行管理的。
  • 全局区:存放全局变量和静态变量以及常量
  • 栈区:由编译器自动分配释放,存放函数的参数值,局部变量等。
  • 堆区:由程序员分配和释放,若程序员不释放,程序结束后由操作系统回收。

C语言内存申请和释放函数

C语言使用内存申请和释放函数一定要记得包含stdlib.h头文件。不包含可能会导致申请内存失败

  • malloc函数:void* malloc(size_t _Size);_Size:申请内存字节数返回申请内存的首地址申请内存不会做初始化
  • calloc函数:void* calloc(size_t _Count,size_t _Size);_Count:申请几个_Size:每个占用字节数申请内存并且默认初始化为0
  • realloc函数:void* realloc(void* _Block,size_t _Size);_Blcok: 重新申请内存的首地址,含原数据_Size:重新申请内存大小,要大于原大小申请失败返回空, 申请成功返回申请内存首地址
  • free函数:void free(void* _Block);_Block:释放的内存首地址同一段内存不能被重复释放哪里申请的内存就要从哪里释放,指针做了偏移一定要还原释放内存后,指针要置空,防止悬浮指针存在

动态申请内存基本操作

示例程序| 一级指针申请单个变量内存

动态申请内存过程其实是分为两个过程

  • 在堆区创建变量
  • 把变量首地址赋值给指针

如下测试代码:

#include
#include
#include
int main()
{
    int* pInt = (int*)malloc(sizeof(int));
    //申请内存失败判定
    assert(pInt);
    *pInt = 123;
    char* pChar = (char*)malloc(sizeof(char));
    //申请内存失败判定
    if (pChar == NULL)
        return 0;
    putchar(*pChar = 'A');
    free(pInt);
    pInt = NULL;
    free(pChar);
    pChar = NULL;
    return 0;
}

示例程序| 一维数组动态申请内存

一维数组动态内存申请其实就是申请一段内存,然后把一段内存的首地址赋值给指针,通过指针偏移操作多个内存。不过一般操作方便,推荐使用数组下标方式访问,一维数组动态主要有一下三种方式:

  • 直接申请一段内存
  • 通过传参方式,子函数修改指针指向,需要传入二级指针
  • 通过返回值方式

测试代码如下:

#include
#include
#include
int main()
{
    int* pInt = (int*)malloc(sizeof(int));
    //申请内存失败判定
    assert(pInt);
    *pInt = 123;
    char* pChar = (char*)malloc(sizeof(char));
    //申请内存失败判定
    if (pChar == NULL)
        return 0;
    putchar(*pChar = 'A');
    free(pInt);
    pInt = NULL;
    free(pChar);
    pChar = NULL;
    return 0;
}

运行结果如下:

示例程序| 一维数组动态申请之内存自动扩增

realloc函数可以实现内存的自动扩增,当用户数据超过限定的时候,可以做到自动增长,如下测试代码:

#include
#include
#include
int main()
{
    //申请一个内存大小
    int* arr = (int*)malloc(sizeof(int));
    assert(arr);
    //最大存储数
    int max = 1;
    //当前元素个数
    int curSize = 0;
    int temp=0;
    while (1) 
    {
        //内存自动扩增
        if (curSize >= max)
        {
            max *= 2;
            int* p = realloc(arr, sizeof(int) * max);
            assert(p);
            arr = p;
        }
        int result=scanf("%d", &temp);
        //输入0退出输入状态
        if (temp == 0)
            break;
        arr[curSize++] = temp;
    }
    for (int i = 0; i < curSize; i++) 
    {
        printf("%d\t", arr[i]);
    }
    free(arr);
    arr=NULL;
    return 0;
}

运行结果如下:

示例程序| 二维组动动态申请内存

二维数组的动态内存申请主要有一下两种方式

  • 数组指针实现
  • 二级指针实现(如果用子函数实现需要传入三级指针)

测试代码如下:

#include
#include
#include

int main()
{
    int row = 3;
    int cols = 4;
    //No.1 数组指针申请
    int(*p)[4] = NULL;
    p = (int(*)[4])calloc(row, sizeof(int[4]));
    assert(p);
    for (int i = 0; i < row; i++)
    {
        for (int j = 0; j < cols; j++)
        {
            printf("%d ", p[i][j]);
        }
        printf("\n");
    }
    //No.2 二级指针申请
    int** parr = (int**)calloc(row, sizeof(int*));
    assert(parr);
    for (int i = 0; i < row; i++)
    {
        parr[i] = (int*)calloc(cols, sizeof(int));
        assert(parr[i]);
    }
    for (int i = 0; i < row; i++) 
    {
        for (int j = 0; j < cols; j++) 
        {
            printf("%d ", parr[i][j]);
        }
        printf("\n");
    }
    for (int i = 0; i < row; i++)
    {
        free(parr[i]);
        parr[i] = NULL;
    }
    free(parr);
    parr = NULL;
    free(p);
    p= NULL;
    return 0;
}

示例程序| 字符指针申请内存并且赋值字符串

字符指针操作字符串的时候,主要是要注意字符串长度问题,一级字符串操作必须采用字符串处理函数去完成,如下测试代码:

#include
#include
#include
char* initStr(const char* initString) 
{
    int length = strlen(initString);
    //申请内存长度加1,要考虑字符串结束标记
    char* pstr = (char*)malloc(length + 1);
    //字符串赋值用strcpy函数
    strcpy(pstr, initString);
    return pstr;
}
int main()
{
    char* pstr = NULL;
    pstr = initStr("coolmoying");
    puts(pstr);
    return 0;
}

相关推荐

激光手术矫正视力对眼睛到底有没有伤害?

因为大家询问到很多关于“基质不能完全愈合”的问题,有必要在这里再详细解释一下。谢谢@珍惜年少时光提出的疑问:因为手头刚好在看组织学,其中提到:”角膜基质约占角膜的全厚度的90%,主要成分是胶原板层,...

OneCode核心概念解析——View(视图)

什么是视图?在前面的章节中介绍过,Page相关的概念,Page是用户交互的入口,具有Url唯一性。但Page还只是一个抽象的容器,而View则是一个具备了具体业务能力的特殊的Page,它可以是一个...

精品博文图文详解Xilinx ISE14.7 安装教程

在软件安装之前,得准备好软件安装包,可从Xilinx官网上下载:http://china.xilinx.com/support/download/index.html/content/xilinx/z...

卡片项目管理(Web)(卡片设计的流程)

简洁的HTML文档卡片管理,简单框架个人本地离线使用。将个人工具类的文档整理使用。优化方向:添加图片、瀑布式布局、颜色修改、毛玻璃效果等。<!DOCTYPEhtml><html...

GolangWeb框架Iris项目实战-JWT和中间件(Middleware)的使用EP07

前文再续,上一回我们完成了用户的登录逻辑,将之前用户管理模块中添加的用户账号进行账号和密码的校验,过程中使用图形验证码强制进行人机交互,防止账号的密码被暴力破解。本回我们需要为登录成功的用户生成Tok...

sitemap 网站地图是什么格式?有什么好处?

sitemap网站地图方便搜索引擎发现和爬取网页站点地图是一种xml文件,或者是txt,是将网站的所有网址列在这个文件中,为了方便搜索引擎发现并收录的。sitemap网站地图分两种:用于用户导...

如何在HarmonyOS NEXT中处理页面间的数据传递?

大家好,前两天的Mate70的发布,让人热血沸腾啊,不想错过,自学的小伙伴一起啊,今天分享的学习笔记是关于页面间数据伟递的问题,在HarmonyOSNEXT5.0中,页面间的数据传递可以有很多种...

从 Element UI 源码的构建流程来看前端 UI 库设计

作者:前端森林转发链接:https://mp.weixin.qq.com/s/ziDMLDJcvx07aM6xoEyWHQ引言由于业务需要,近期团队要搞一套自己的UI组件库,框架方面还是Vue。而业界...

jq+ajax+bootstrap改了一个动态分页的表格

最近在维护一个很古老的项目,里面是用jq的dataTable方法实现一个分页的表格,不过这些表格的分页是本地分页。现在想要的是点击分页去请求数据。经过多次的修改,以失败告终。分页的不准确,还会有这个错...

学习ES6- 入门Vue(大量源代码及笔记,带你起飞)

ES6学习网站:https://es6.ruanyifeng.com/箭头函数普通函数//普通函数this指向调用时所在的对象(可变)letfn=functionfn(a,b){...

青锋微服务架构之-Ant Design Pro 基本配置

青锋(msxy)-Gitee.com1、更换AntDesignPro的logo和名称需要修改文件所在位置:/config/defaultSetting.jsconstproSett...

大数据调度服务监控平台(大数据调度服务监控平台官网)

简介SmartKettle是针对上述企业的痛点,对kettle的使用做了一些包装、优化,使其在web端也能具备基础的kettle作业、转换的配置、调度、监控,能在很大一定程度上协助企业完成不同...

Flask博客实战 - 实现博客首页视图及样式

本套教程是一个Flask实战类教程,html/css/javascript等相关技术栈不会过多的去详细解释,那么就需要各位初学者尽可能的先去掌握这些基础知识,当然本套教程不需要你对其非常精通,但最起码...

Web自动化测试:模拟鼠标操作(ActionChains)

在日常的测试中,经常会遇到需要鼠标去操作的一些事情,比如说悬浮菜单、拖动验证码等,这一节我们来学习如何使用webdriver模拟鼠标的操作首页模拟鼠标的操作要首先引入ActionChains的包fro...

DCS F-16C 中文指南 16.9ILS仪表降落系统教程

10–ILS教程我们的ILS(仪表着陆进近)将到达Batumi巴统机场。ILS频率:110.30跑道航向:120磁航向/126真航向无线电塔频率:131.0001.设置雷达高度表开关打开(前)并...