【C语言期末不挂科——指针进阶篇】【上】

简介: 【C语言期末不挂科——指针进阶篇】【上】



前言:

  我们在指针初阶篇学习了:

1、指针就是个变量,用来存放地址,地址唯一标识一块空间。

2、指针的大小是固定的4/8个字节(32位平台/64位平台)

3、指针是有类型,指针的类型决定指针±整数的步长,指针解引用操作时候的权限。

4、指针的运算。

  快要期末了,祝各位小伙伴们期末考试顺利,那么话不多说,进入我们今天的主题!


字符指针

  指针的类型里面我们知道有一种指针 类型为 字符指针(char *),经过初阶的学习我们已经能用:

#include<stdio.h>
int main()
{
  char ch = 'a';
  char *p = &ch;
  *p = 'b';
  return 0;
}

  一般我们在存储字符串的时候,我们都会把字符串放进字符数组里面,其实还有一种方式可以用来存储字符串:

#include<stdio.h>
int main()
{
  char ch[10] = { 'a', 'b', 'c', 'd', 'e', 'f', '\0' };
  const char *p = "abcdef";//常量字符串
  printf("%s",p);
  return 0;
}

  我们可以用字符指针来接收字符串,有人可能要问了:“指针接收?那是把字符串存储到指针里吗?”,实则不然,想一下,如果是在x86的环境下,指针只有4个字节大小,而这个字符串已经超出4字节的范围。

  还记得我们在C语言中是如何打印字符串的吗?

printf("%s\n",ch);//上面的例子

  我们当时不知道为什么这样写,现在看来,我们是将数组首元素地址传入到printf函数里,然后printf函数通过寻址来访问字符数组从而打印字符串。

  其实我们指针也是如此,指针并不是存储了字符串,而是存储了字符串首元素的地址,这样就能通过寻址打印了:

  注意:被双引号引用的字符串已经变成了常量,所以要加const,在C语言内存布局里面有个叫做代码区的区域,是专门用来存储常量以及代码的地方。

我们来看下面这道题:(请问输出结果是什么?)

#include <stdio.h>
int main()
{
  char str1[] = "hello world";
  char str2[] = "hello world";
  
  const char *str3 = "hello world";
  const char *str4 = "hello world";
  
  if(str1 == str2)
    printf("str1 and str2 are same\n");
  else
    printf("str1 and str2 are not same\n");
 
  if(str3 == str4)
    printf("str3 and str4 are same\n");
  else
    printf("str3 and str4 are not same\n");
 
  return 0;
}

  怎么样,你相对了没有?下面我们来仔细分析是如何将打印的。

  首先我们来看str1[]与str2[],我们知道,数组名代表首元素地址,而这是两个数组,str1与str2那么他们两个的地址一定是不同的,所以第一次if判断的时候他们是不相等的。

  而str3与str4为什么又是相同的呢?首先,与前两个不同,str3与str4并没有单独开一个空间存储字符串,实际上,常量字符串存储在代码区里时,如果你的常量是出现过的,编译器不会在生成另一份相同的常量,而是直接取同一块常量的首地址给你的指针,所以str3与str4是相同的。


数组指针

  在学习数组指针之前,我们先复习一下上节课所讲的指针数组:

  如果想不明白也可以类比,整形数组——是存放整形的数组字符数组——是存放字符的数组,那么指针数组——是存放指针的数组

int *arr[10];//整形指针的数组 
  char *arr2[4];//一级字符指针的数组
  char *arr3[5];//二级字符指针的数组

  当然也可以用指针数组来模拟二维数组:

#include <stdio.h>
int main()
{
  int arr1[] = { 1, 2, 3, 4, 5 };
  int arr2[] = { 2, 3, 4, 5, 6 };
  int arr3[] = { 3, 4, 5, 6, 7 };
  
  int *arr[3] = { arr1, arr2, arr3 };//指针数组
  
  int i = 0;
  for(i = 0 ; i < 3 ; i++)//打印每一列
  {
    int j = 0;
    for(j = 0 ; j < 5 ; j++)//打印每一行
    {
      printf("%d ",arr[i][j]);  
    } 
    printf("\n");
  }
  return 0;
}


  话不多说,进入正题,数组指针是什么?

  同样的,我们可以先来类比:

整形指针——指向整型变量的指针,存放整形变量地址的指针变量

字符指针——指向字符变量的指针,存放字符变量地址的指针变量

数组指针——指向数组的指针,存放的是数组的地址的指针变量

  下面哪个是数组指针?

int *p1[10];
int (*p2)[10];

  在C语言中’[]‘的优先级是要比’*'高的,所以第一个语句,p1是先与[]结合,所以是数组,而数组的类型是int *整形指针类型。

  我们再来看第二个,*p2被括号括起来了,那么他的优先级就要比[]高,所以p2就是指针类型,还记得之前说的吗?int *p,*说明了p是指针,而int说明了p指向的是整形,那么去掉(*p2)剩下的int [10],也就是数组类型,所以第二条语句是一个数组指针。


  我们知道了什么是数组指针,但是我们该如何给数组指针赋值呢?这个时候就需要再次研究一下我们的数组名了。

#include<stdio.h>
int main()
{
  int arr[10] = { 0 };
  printf("%p\n", arr);
  printf("%p", &arr[0]);
  return 0;
}

  我们前面说过,数组名表示首元素地址,但是我们有两个例外:

1、sizeof(数组名),这里的数组名不是首元素地址,数组名表示整个数组,其计算的是数组的整个大小,单位为字节。

2、&数组名 这里的数组名也表示整个数组,&所取出的是数组的整个地址。

  除此之外,所有情况的数组名均表示首元素地址。

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

  我们可以看到,1 2和3 4运行结果相同,都是相差四个字节,虽然5的地址和1与3的地址相同,但是6的地址却与1 3 5 相差了40个字节,而我们数组的大小刚好是40个字节。这也能说明&数组名是取整个数组的地址。

  那么我们就可以写出数组指针了:

#include<stdio.h>
int main()
{
  int arr[10] = { 0 };
  int (*p)[] = &arr;//&arr是整个数组的地址
  
  return 0;
}

  我们也可以通过编译来看数组指针的类型:

  上面已经声明了数组指针,那么我们到底该怎么用数组指针呢?

#include<stdio.h>
int main()
{
  int arr[10] = { 1, 2, 3, 4 ,5, 6, 7, 8, 9, 10 };
  int (*p)[10] = &arr;
  int len = sizeof(arr)/sizeof(int);
  int i = 0;
  for(i = 0 ; i < len ; i++)
  {
    printf("%d ", *((*p) + i));//数组指针使用方式
  }
  return 0;
}

  数组指针使用时首先对p进行解引用找到这个数组首元素地址,然后首元素加上偏移量之后再解引用,得到具体的值。当然还可以这样写:

for(i = 0 ; i < len ; i++)
{
  printf("%d ", (*p)[i]);
}

  实际上数组指针并不是以上情况下使用的,一般我们数组指针用来对二维数组传参,因为二维数组传参,形参是指针形式。例如:

#include<stdio.h>
void Print(int (*p)[5], int r, int c)//这里数组指针指向的就是二维数组
{
  int i = 0;
  for(i = 0 ; i < r ; i++)//每一行
  {
    int j = 0;
    for(j = 0 ; j < c ; j++)//每一列
    {
      printf("%d ", *(*(p + i) + j));//*(p+i)表示找到二维数组的i行首元素地址,+j表示这一行的第j个元素的地址,最后在最最外面解引用,最后得到这个元素
    }
    printf("\n");
  }
  return;
}
int main()
{
  int arr[3][5] = { 1,2,3,4,5 ,2,3,4,5,6 , 3,4,5,6,7 };
  Print(arr, 3, 5);
  return 0;
}

  虽然在main函数里我们是使用二维数组数组名来传参,但是我们知道,数组名表示首元素地址,这个时候使用数组指针就比较符合场景了。

  虽然看起来指针数组不如直接将二维数组传入来的实在,但是其实我们直接传数组的时候,编译器还是会将数组转化为上面写的那种形式。


数组传参和指针传参

  数组传参

  既然说到了数组指针问题,上面函数参数可能会让人捉摸不透,变来变去,我们来总结一下:

我们看下面一段代码:(我们可以先思考下面函数部分是不是都是对的?)

//一维数组各种传参
#include<stdio.h>
void test(int arr[]);//y or n?
void test(int arr[10]);//y or n?
void test(int *arr);//y or n?
void Test2(int *arr[20]);//y or n?
void Test2(int **arr);//y or n?
int main()
{
  int arr[10] = {0};
  int *arr2[20] = {0};
  test(arr);
  Test2(arr2);
  return 0;
}

  我们依次看每个函数:

  也就是说以上五个一维数组传参的形式全部都是正确的,怎么样,你理解了吗?那么二维数组的传参会是怎样呢?来看看下面哪些是正确的二维数组传参形式:

void test(int arr[3][5]);//y or n?
void test(int arr[][]);//y or n?
void test(int arr[][5]);//y or n?
void test(int *arr);//y or n?
void test(int *arr[5]);//y or n?
void test(int (*arr)[5]);//y or n?
void test(int **arr);//y or n?
int main()
{
  int arr[3][5] = {0};
  test(arr);
  return 0;
}

  所以,二级指针传参,如果用数组传参只能将行省略,如果用指针传参,只能使用数组指针的形式传参。


  一级指针传参

  既然我们说到了传参,而数组又与指针密不可分,我们顺带来分析一下,指针的传参方式又是什么?

  我们来看下面代码:

#include<stdio.h>
void Test(int *p)
{
  return;
}
int main()
{
  int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
  int a;
  
  return 0;
}

  思考一下,我们有哪些方式可以传参一级指针呢?你可以把你想的全部写下来,与我写的对比,看看有什么不同:

  1、可以传整形变量的地址。

  2、可以传一级指针。

  3、可以传一维数组的首元素。

#include<stdio.h>
void test(int *p)
{
  return;
}
int main()
{
  int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
  int a = 1;
  int *p = &a;
  
  test(arr);
  test(&a);
  test(p); 
  return 0;
}

  我们可以看到是可以通过编译的,可能还存在其他形式的一级指针传参,但是这三个是我们最常用的方式。


  二级指针传参
#include<stdio.h>
void test(int **p)
{
  return;
}
int main()
{
  //哪些传参方式? 
  return 0;
}

  同样,我们思考一下有哪些方式可以给二级指针传参呢?

  1、一级指针变量的地址

  2、二级指针

  3、指针数组的数组名

#include<stdio.h>
void test(int **p)
{
  return;
}
int main()
{
  int n = 10;
  int *p = &n;
  int **pp = &p;
  int *arr[10];
  
  test(&p);//二级指针就是取一级指针的地址 
  test(pp);//传入二级指针 
  test(arr); //函数名表示首元素地址,而指针数组的每个元素都是指针,所以这个数组名就是二级指针 
  return 0;
}

  当然,二级以上的指针和二级指针类似,类比就行。


  这就是今天的内容了,如果对各位有帮助还望能三连支持~~

相关文章
|
1月前
|
存储 NoSQL 编译器
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
指针是一个变量,它存储另一个变量的内存地址。换句话说,指针“指向”存储在内存中的某个数据。
84 3
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
|
1月前
|
存储 编译器 C语言
【C语言】指针大小知多少 ?一场探寻C语言深处的冒险 !
在C语言中,指针的大小(即指针变量占用的内存大小)是由计算机的体系结构(例如32位还是64位)和编译器决定的。
54 9
|
1月前
|
安全 程序员 C语言
【C语言】指针的爱恨纠葛:常量指针vs指向常量的指针
在C语言中,“常量指针”和“指向常量的指针”是两个重要的指针概念。它们在控制指针的行为和数据的可修改性方面发挥着关键作用。理解这两个概念有助于编写更安全、有效的代码。本文将深入探讨这两个概念,包括定义、语法、实际应用、复杂示例、最佳实践以及常见问题。
44 7
|
2月前
|
存储 C语言
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
148 13
|
2月前
|
存储 程序员 编译器
C 语言数组与指针的深度剖析与应用
在C语言中,数组与指针是核心概念,二者既独立又紧密相连。数组是在连续内存中存储相同类型数据的结构,而指针则存储内存地址,二者结合可在数据处理、函数传参等方面发挥巨大作用。掌握它们的特性和关系,对于优化程序性能、灵活处理数据结构至关重要。
|
2月前
|
算法 C语言
C语言中的文件操作技巧,涵盖文件的打开与关闭、读取与写入、文件指针移动及注意事项
本文深入讲解了C语言中的文件操作技巧,涵盖文件的打开与关闭、读取与写入、文件指针移动及注意事项,通过实例演示了文件操作的基本流程,帮助读者掌握这一重要技能,提升程序开发能力。
126 3
|
2月前
|
存储 算法 程序员
C 语言指针详解 —— 内存操控的魔法棒
《C 语言指针详解》深入浅出地讲解了指针的概念、使用方法及其在内存操作中的重要作用,被誉为程序员手中的“内存操控魔法棒”。本书适合C语言初学者及希望深化理解指针机制的开发者阅读。
|
2月前
|
程序员 C语言
C语言中的指针既强大又具挑战性,它像一把钥匙,开启程序世界的隐秘之门
C语言中的指针既强大又具挑战性,它像一把钥匙,开启程序世界的隐秘之门。本文深入探讨了指针的基本概念、声明方式、动态内存分配、函数参数传递、指针运算及与数组和函数的关系,强调了正确使用指针的重要性,并鼓励读者通过实践掌握这一关键技能。
44 1
|
2月前
|
存储 C语言 计算机视觉
在C语言中指针数组和数组指针在动态内存分配中的应用
在C语言中,指针数组和数组指针均可用于动态内存分配。指针数组是数组的每个元素都是指针,可用于指向多个动态分配的内存块;数组指针则指向一个数组,可动态分配和管理大型数据结构。两者结合使用,灵活高效地管理内存。
|
2月前
|
存储 NoSQL 编译器
C 语言中指针数组与数组指针的辨析与应用
在C语言中,指针数组和数组指针是两个容易混淆但用途不同的概念。指针数组是一个数组,其元素是指针类型;而数组指针是指向数组的指针。两者在声明、使用及内存布局上各有特点,正确理解它们有助于更高效地编程。