一篇彻底搞懂指针及数组——C语言指针及数组的部分知识梳理

C语言指针及数组的部分知识梳理


B站有我发布的讲解视频,欢迎各位小伙伴前往观看噢~
C语言指针知识梳理

内存

内存映像象图内容权限
栈区函数中的普通变量可读可写
堆区动态申请的内存可读可写
静态变量区static修饰的变量可读可写
数据区用于初始化变量的常量只读
代码区代码指令只读

指针

一、指针定义

1、概念

一切都是地址

1.1 地址

C语言用变量来存储数据,用函数来定义一段可以重复使用的代码,数据和代码最终都是以二进制形式放到内存中才能供 CPU 使用,CPU 只能通过地址来取得内存中的代码和数据。

代码:拥有读取和执行权限的内存块

数据:拥有读取和写入权限(也可能只有读取权限)的内存块

地址:内存中每一个字节分配一个号码,此号码即为地址(从零开始的八位十六进制数)

CPU 访问内存时需要的是地址,而不是变量名和函数名!

1.2 变量名和函数名

变量名和函数名只是地址的一种助记符,是一种人性化的表达方法,让我们在编写代码的过程中可以使用易于阅读和理解的英文字符串,不用直接面对二进制地址。

当源文件被编译和链接成可执行程序后,它们都会被替换成地址。编译和链接过程的一项重要任务就是找到这些名称所对应的地址。

注意:虽然变量名、函数名、字符串名和数组名在本质上是一样的,它们都是地址的助记符。但在编写代码的过程中,我们认为变量名表示的是数据本身,而函数名、字符串名和数组名表示的是代码块或数据块的首地址。

1.3 指针
  • 指针变量
    • 定义一个变量,存放地址,这样的变量叫做指针变量

  • 指针的本质
    • 指针保存的就是内存地址,指针的本质就是地址

2、定义的两种形式
int* p
int *p
  • 注意:
    • 无论星号在哪里,该定义表明的均是——定义一个类型为int*的的指针变量p
    • 连续定义时,每个变量名前都需要加星号

3、赋值
  • 给指针变量赋值时,要将一份数据的地址赋给它,不能直接赋给一个整数,例如int *p = 1000;是没有意义的,使用过程中一般会导致程序崩溃。

  • 使用指针变量之前一定要初始化,否则就不能确定指针指向哪里,如果它指向的内存没有使用权限,程序就崩溃了。对于暂时没有指向的指针,建议赋值NULL

    int a=0;
    int *p1=&a;
    int *p2=NULL;
    

4、*的不同含义
  • 表示乘法,eg:int a = 3, b = 5, c; c = a * b;

  • 表示定义一个指针变量,以和普通变量区分开,eg:int a = 100; int *p = &a;

  • 表示获取指针(或地址)指向的数据,是一种间接操作。这里的*称为指针运算符,是用来取得某个地址上的数据的。eg:int a, b, *p = &a; *p = 100; b = *p;

    (因此在给指针本身赋值的时候不能加星号,否则就是将值赋给了指针所指向的数据,而非其本身)


5、* 与 &

假设有一个 int 类型的变量 a,pa 是指向它的指针

&是取地址符,*表示获取指针(或地址)指向的数据

  • *&a:可以理解为*(&a)&a表示取变量 a 的地址(等价于 pa),*(&a)表示取这个地址上的数据(等价于 *pa),绕来绕去,又回到了原点,*&a仍然等价于 a。
  • &*pa:可以理解为&(*pa)*pa表示取得 pa 指向的数据(等价于 a),&(*pa)表示数据的地址(等价于 &a),所以&*pa等价于 pa。


二、指针运算

指针变量也是变量,可以对其进行四种算数运算:++、–、+、-

1、递增递减
1.1 概念

C 指针的算术运算 | 菜鸟教程 (runoob.com)

  • 指针的每一次递增,它其实会指向下一个元素的存储单元。

  • 指针的每一次递减,它都会指向前一个元素的存储单元。

  • 指针在递增和递减时跳跃的字节数取决于指针所指向变量数据类型长度,比如 int 就是 4 个字节。

    否则会将先后两个数据的各自的一部分取到,这是没有意义的。

1.2 实际意义
  • 数组:数组中的所有元素在内存中是连续排列的,如果一个指针指向了数组中的某个元素,那么加 1 就表示指向下一个元素,减 1 就表示指向上一个元素
  • 普通变量:不过C语言并没有规定变量的存储方式,如果连续定义多个变量,它们有可能是挨着的,也有可能是分散的,这取决于变量的类型、编译器的实现以及具体的编译模式,所以对于指向普通变量的指针,我们往往不进行加减运算

2、比较运算

指针可以用关系运算符进行比较,如 ==、< 和 >。如果 p1 和 p2 指向两个相关的变量,比如同一个数组中的不同元素,则可对 p1 和 p2 进行大小比较。

当对指针变量进行比较运算时,比较的是指针变量本身的值,也就是数据的地址。如果地址相等,那么两个指针就指向同一份数据,否则就指向不同的数据。

应用:

#include <stdio.h>
const int MAX = 3;
int main ()
{
   int  var[] = {10, 100, 200};
   int  i, *ptr;
 
   /* 指针中第一个元素的地址 */
   ptr = var;
   i = 0;
    
   //只要变量指针所指向的地址小于或等于数组的最后一个元素的地址 &var[MAX - 1],则把变量指针进行递增
   while ( ptr <= &var[MAX - 1] ){
      printf("存储地址:var[%d] = %p\n", i, ptr );
      printf("存储值:var[%d] = %d\n", i, *ptr );
      
       /* 指向下一个位置 */
      ptr++;
      i++;
   }
   return 0;
}


三、数组指针

1、概念

如果一个指针指向了数组,我们就称它为数组指针(Array Pointer)。

数组指针指向的是数组中的一个具体元素,而不是整个数组,所以数组指针的类型和数组元素的类型必须一致。

反过来想,p 并不知道它指向的是一个数组,p 只知道它指向的是一个整数,究竟如何使用 p 取决于程序员的编码。


2、应用
2.1 访问数组元素
2.1.1 使用下标

也就是采用 arr[i] 的形式访问数组元素。如果 p 是指向数组 arr 的指针,那么也可以使用 p[i] 来访问数组元素,它等价于 arr[i]。

2.1.2 使用指针
  • *(p+i)

  • *(arr+i)

    (两者等价)

2.2 取数组中某元素的地址
  • p=&a[i];(i为所需数组元素的下标)
  • p=a+i;


四、指针数组

由指针构成的数组

int n,*p[n];
char *arr[4] = {"hello", "world", "shannxi", "xian"};
//或 char *(arr[4])
//arr就是我定义的一个指针数组,它有四个元素,每个元素是一个char *类型的指针,这些指针存放着其对应字符串的首地址。
//这就相当与定义char *p1 = “hello”,char *p1 = “world”,char *p3 = “shannxi”, char *p4 = “xian”,这是四个指针,每个指针存放一个字符串首地址,然后用arr[4]这个数组分别存放这四个指针,就形成了指针数组。


五、字符串指针

1、字符串的两种表示方法
  1. 字符数组

    获取单个字符:str[i]

  2. 使用一个指针指向字符串

    获取单个字符:

    char *str = "http://c.biancheng.net";
    int len = strlen(str), i;
    //通过strlen得到数组长度,结尾的'/0'不计入
      
    //使用*(str+i),注意是i<len
        for(i=0; i<len; i++){
            printf("%c", *(str+i));
        }
        printf("\n");
    

2、字符数组与字符串常量
  • 相同:

    都可以使用%s输出整个字符串,都可以使用*[ ]获取单个字符

  • 不同:

    它们最根本的区别是在内存中的存储区域不一样,

    • 字符数组:存储在全局数据区或栈区。

      全局数据区和栈区的字符串(也包括其他数据)有读取和写入的权限,字符数组在定义后可以读取和修改每个字符。

    • 字符串常量:存储在常量区。

      常量区的字符串(也包括其他数据)只有读取权限,没有写入权限。字符串常量一旦被定义后就只能读取不能修改,任何对它的赋值都是错误的。

  • 使用的选择

    • 如果只涉及到对字符串的读取,那么字符数组和字符串常量都能够满足要求;

    • 如果有写入(修改)操作,那么只能使用字符数组,不能使用字符串常量。

      eg:获取用户输入的字符串就是一个典型的写入操作,只能使用字符数组,不能使用字符串常量



六、指针变量作为函数参数

1、指针变量

用指针变量作函数参数可以将函数外部的地址传递到函数内部,使得在函数内部可以操作函数外部的数据,并且这些数据不会随着函数的结束而被销毁。(与变量的作用域有关)

像数组、字符串、动态分配的内存等都是一系列数据的集合,没有办法通过一个参数全部传入函数内部,只能传递它们的指针,在函数内部通过指针来影响这些数据集合。

有些初学者可能会使用下面的方法来交换两个变量的值:

#include <stdio.h>
void swap(int a, int b){
    int temp;  //临时变量
    temp = a;
    a = b;
    b = temp;
}
int main(){
    int a = 66, b = 99;
    swap(a, b);
    printf("a = %d, b = %d\n", a, b);
    return 0;
}

运行结果:
a = 66, b = 99

从结果可以看出,a、b 的值并没有发生改变,交换失败。这是因为swap() 函数内部的 a、b 和 main() 函数内部的 a、b 是不同的变量,占用不同的内存,它们除了名字一样,没有其他任何关系,swap() 交换的是它内部 a、b 的值,不会影响它外部(main() 内部) a、b 的值。

改用指针变量作参数后就很容易解决上面的问题:

#include <stdio.h>
void swap(int *p1, int *p2){
    int temp;  //临时变量
    temp = *p1;
    *p1 = *p2;
    *p2 = temp;
}
int main(){
    int a = 66, b = 99;
    swap(&a, &b);
    printf("a = %d, b = %d\n", a, b);
    return 0;
}

运行结果:
a = 99, b = 66

调用 swap() 函数时,**将变量 a、b 的地址分别赋值给 p1、p2,这样 p1、p2 代表的就是变量 a、b 本身,交换 *p1、*p2 的值也就是交换 a、b 的值。函数运行结束后虽然会将 p1、p2 销毁,但它对外部 a、b 造成的影响是“持久化”的,不会随着函数的结束而“恢复原样”。


2、用数组作函数参数
2.1 概念

数组是一系列数据的集合,无法通过参数将它们一次性传递到函数内部,如果希望在函数内部操作数组,必须传递数组指针。

eg:

#include <stdio.h>
int max(int *intArr, int len){
    int i, maxValue = intArr[0];  //假设第0个元素是最大值
    for(i=1; i<len; i++){
        if(maxValue < intArr[i]){
            maxValue = intArr[i];
        }
    }
    return maxValue;
}

int main(){
    int nums[6], i;
    int len = sizeof(nums)/sizeof(int);
    //读取用户输入的数据并赋值给数组元素
    for(i=0; i<len; i++){
        scanf("%d", nums+i);
    }
    printf("Max value is %d!\n", max(nums, len));
    return 0;
}
2.2 形参数组定义的实质

可以写为两中数组定义:int intArr[6]还是int intArr[]

但这两种形式的数组定义都是假象,它们都不会创建一个数组出来,编译器也不会为它们分配内存,实际的数组是不存在的,它们最终还是会转换为int *intArr这样的指针。这就意味着,两种形式都不能将数组的所有元素“一股脑”传递进来,大家还得规规矩矩使用数组指针。

2.3 不能在函数内部求得数组长度

需要强调的是,不管使用哪种方式传递数组,都不能在函数内部求得数组长度,因为 intArr 仅仅是一个指针,而不是真正的数组,所以必须要额外增加一个参数来传递数组长度。

2.4 通过数组指针传参的必要性

C语言为什么不允许直接传递数组的所有元素,而必须传递数组指针呢?

参数的传递本质上是一次赋值的过程,赋值就是对内存进行拷贝。所谓内存拷贝,是指将一块内存上的数据复制到另一块内存上。

对于像 int、float、char 等基本类型的数据,它们占用的内存往往只有几个字节,对它们进行内存拷贝非常快速。而数组是一系列数据的集合,数据的数量没有限制,可能很少,也可能成千上万,对它们进行内存拷贝有可能是一个漫长的过程,会严重拖慢程序的效率,为了防止技艺不佳的程序员写出低效的代码,C语言没有从语法上支持数据集合的直接赋值。



数组地址

1、总结

数组的名字有两个含义:

  1. 表示该数组本身,因此&a表示整个数组的地址

  2. 表示数组首元素的地址

    (因此可以直接将其赋给指针变量p)

  3. 表示一个指针,指向数组首地址

    arrp&arr[0] 这三种写法都是等价的,它们都指向数组第 0 个元素,或者说指向数组的开头。)

  • a是数组首元素地址0x200007a4;
  • a+1为数组元素地址后移一个int型的偏移量,即0x200007a4+4;
  • &a是整个数组的地址0x200007a4;
  • &a+1整个数组的地址后移整个数组空间大小的偏移量,即0x200007a4+4*10=0x200007cc;
  • &a[0]是数组首元素地址0x200007a4,注意:&a[0]是一个指针,编译器要为它分配存储空间,但a不是指针型变量,不会被分配存储空间;
  • &a[0]+1为数组元素地址后移一个int型的偏移量,即0x200007a4+4;
  • sizeof(a)是整个数组所占内存大小:4*10;
  • sizeof(a[0])是首元素1所占内存大小:4;
  • sizeof(&a)是数组的地址所占内存大小:4;
  • sizeof(&a[0])是首元素1的地址所占内存大小:4。

原文链接:https://blog.csdn.net/u014470361/article/details/79352352

2、取数组中某元素的地址

a[i] 指的仍是元素而不是指针,因此取其地址不能直接用数组名

规定:数组a[]、数组指针p

  • p=&a[i];(i为所需数组元素的下标)
  • p=a+i;
  • a+i == &a[i];
  • a == &a[0]

注意:不同的是,数组名是常量,它的值不能改变,而数组指针是变量(除非特别指明它是常量),它的值可以任意改变。也就是说,数组名只能指向数组的开头,而数组指针可以先指向数组开头,再指向其他元素。

3、用指针遍历数组

#include <stdio.h>
const int MAX = 3;
 
int main ()
{
   int  var[] = {10, 100, 200};
   int  i, *ptr;
 
   ptr = var;
   //var表示该数组首元素的地址,而指针变量中所保存的也是地址,因此可以直接将var赋给指针变量ptr,而不需要加取地址符&
   for ( i = 0; i < MAX; i++){
      printf("存储地址:var[%d] = %p\n", i, ptr );
      printf("存储值:var[%d] = %d\n", i, *ptr );
      //*表示访问此指针变量中保存的地址,(即实际变量所保存的值)
      
      ptr++;
      //递增变量指针,以便顺序访问数组中的每一个元素,指向下一个位置 
   }
   return 0;
}


参考链接

  1. 数组的地址问题
  2. C语言指针详解,30分钟玩转C语言指针
  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值