【C初阶】指针

简介: 初阶指针 详解 野指针形成原因,指针数组




🎈今日心语:男人的精致和自信,从来和脸蛋没有关系


前言:

本章主要介绍:

  1. 指针是什么
  2. 指针和指针类型
  3. 野指针
  4. 指针运算
  5. 指针和数组
  6. 二级指针
  7. 指针数组

目录:


1. 指针是什么?

指针是什么?

指针理解的2个要点:

1. 指针是内存中一个最小单元的编号,也就是地址
2. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量

总结:指针就是地址,口语中说的指针通常指的是指针变量。

那么想要学习指针,先得了解内存

内存是存储区域:

指针变量

我们可以通过&(取地址操作符)取出变量的内存起始地址把地址可以存放到一个变量中,这个变量就是指针变量

int main()
{
  int a = 10;//创建变量的本质是向内存申请了空间,这里a申请了4个字节的内存空间
  char ch;
  //&a;//拿出a变量的起始地址,但是能够看到后面的地址
  int* pa = &a;//pa是整型指针变量,向内存申请了空间存放了a的地址
  char* pc = ch;//pa的类型是int*,int告诉我们a指向的变量是int型,*告诉我们pa是指针变量
  return 0;
}

从上面对程序内存的监控可知,pa,pc存放的是&pa,&pc,而pa,pc又有自己的地址

总结:

指针变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。

那这里的问题是:

  • 一个小的单元到底是多大?(1个字节)
  • 如何编址?
    经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。
    对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电
    平(低电压)就是(1或者0);
    那么32根地址线产生的地址就会是:

00000000 00000000 00000000 00000000

00000000 00000000 00000000 00000001

11111111 11111111 11111111 11111111

这里就有2的32次方个地址。

每个地址标识一个字节,那我们就可以给 (2^32Byte == 2^32/1024KB ==

232/1024/1024MB==232/1024/1024/1024GB == 4GB) 4G的空间进行编址。

同样的方法,那64位机器,如果给64根地址线,那能编址多大空间,自己计算。

这里我们就明白:

在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。
那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。

总结:

指针变量是用来存放地址的,地址是唯一标示一个内存单元的。
指针的大小在32位平台是4个字节,在64位平台是8个字节

解引用操作符

上面的代码,如果我们想通过pa间接改变a的值,那么就需要用到解引用操作符了

如下:

#include<stdio.h>
int main()
{
  int a = 10;//创建变量的本质是向内存申请了空间
  char ch;
  //&a;//拿出a变量的起始地址,但是能够看到后面的地址
  int* pa = &a;//pa是整型指针变量,向内存申请了空间存放了a的地址
  /*char* pc = ch;*///pa的类型是int*,a指向的变量是int型,*告诉我们pa是指针变量
  *pa = 20;//解引用操作符(间接访问操作符)
  printf("%d", a);
  return 0;
}

2. 指针和指针类型

这里我们在讨论一下:指针的类型

我们都知道,变量有不同的类型,整形,浮点型等。那指针有没有类型呢?

准确的说:有的。

当有这样的代码:

int a = 10;
p = &a;

要将&a(a的地址)保存到p中,我们知道p就是一个指针变量,那它的类型是怎样的呢?

我们给指针变量相应的类型。

char *pc = NULL;

int *pi = NULL;

short *ps = NULL;

long *pl = NULL;

float *pf = NULL;

double *pd = NULL;

这里可以看到,指针的定义方式是: type + * 。

其实:

char 类型的指针是为了存放 char 类型变量的地址。
short
类型的指针是为了存放 short 类型变量的地址。

int* 类型的指针是为了存放 int 类型变量的地址。**

那指针类型的意义是什么?

2.2 指针的解引用

分析问题1:

仔细观察下面图片中解引用操作对a的作用

不难发现,不同的指针类型,对a的作用不同,因此指针类型是有意义的。

得出结论1:

指针类型的意义仅仅是这些吗?

当然不是!

2.1 指针±整数

分析问题2:

观察下图中pa,pc分别加1后的变化

发现变化有所不同

得出结论2:

总结:

1.指针的类型决定了指针向前或者向后走一步有多大(距离)总结:
2.指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。

比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。

那指针类型该如何使用呢?

指针类型的使用

示例:

int main()
{
  int arr[10] = { 0 };
  //把1~10放入数组中
  int* p = arr;//arr是数组元素的首地址
  int i = 0;
  for (i = 0; i < 10; i++)
  {
    *p = i + 1;
    p++;
  }
  return 0;
}

如下,这里如果把intp改为charp,数组的守元素如果是4个字节,p+1每次向后移动1个字节,达不到理想的效果

反例:

知道了指针类型的意义之后,以后写代码就可以选择性地使用;

代码简化:

上面示例中的代码还可以这样写

int main()
{
  int arr[10] = { 0 };
  int* p = arr;
  int i = 0;
  for (i = 0; i < 10; i++)
  {
    *(p + i) = i + 1;
  }
  return 0;
}

3. 野指针

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

3.1 野指针成因

1. 指针未初始化

int main()
{
  int* p;//p就是野指针,变量p没有初始化,放的是随机值,被当成了地址,把20放进去
  //而这个地址的空间不属于这个程序,程序就会出现问题
  *p = 20;
  return 0;
}

2. 指针越界访问

#include <stdio.h>
int main()
{
int arr[10] = {0};
int *p = arr;
int i = 0;
for(i=0; i<=10; i++)//这里访问了11个元素
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}

3. 指针指向的空间释放

这里放在动态内存开辟的时候讲解,这里可以简单提示一下。

3.2 如何规避野指针

  1. 指针初始化
  2. 小心指针越界 //需要在写程序时思考
  3. 指针指向空间释放,及时置NULL
  4. 避免返回局部变量的地址/避免返回栈空间的地址
  5. 指针使用之前检查有效性
//指针初始化
int main()
{
  int a = 10;
  int* p = &a;//明确初始化
  //NULL  -0,就是用来初始化指针的
  int* p = NULL;//相当于int a = 0;
  return 0;
}

这里需要注意:不为空指针才能访问

4. 指针运算

  • 指针± 整数
  • 指针-指针
  • 指针的关系运算

4.1 指针±整数

示例1:

#define N_VALUES 5
float values[N_VALUES];
float *vp;
//指针+-整数;指针的关系运算
for (vp = &values[0]; vp < &values[N_VALUES];)
{
*vp++ = 0;
}

代码详解:

示例2:

int main()
{
  double arr[5] = { 0 };
  double* p = arr;
  int i = 0;
  for (i = 0; i < 5; i++)
  {
    printf("%lf ", *(p + i));//p+i下标为i元素的地址
    //可以通过指针+/-整数随意移动,来达到内存访问的效果
  }
  return 0;
}

我们可以通过指针+/-整数随意移动,来达到内存访问的效果


4.2 指针-指针

int main()
{
  int arr[10] = { 0 };
  printf("%d\n", &arr[9] - &arr[0]);
  printf("%d\n", &arr[0] - &arr[9]);
  return 0;
}

指针和指针相减的绝对值是两个指针之间的元素个数

错误示例:

int main()
{
  //两个指针相减的前提是指针指向的是同一块连续的空间
  int a = 10;
  char c = 'w';
  printf("%d\n", &a - &c);//这种写法是错误的
}

两个指针相减的前提是指针指向的是同一块连续的空间

指针-指针的使用:

求字符串长度

int my_strlen(char* arr)
{
  char* start = arr;
  while (*arr != '\0')
  {
    arr++;
  }
  return arr - start;//当*arr='\0'时,arr指向'\0',跳出循环
}
int main()
{
  char arr[] = "abcdef";
  int len = my_strlen(arr);
  printf("%d\n", len);
  return 0;
}

上面代码用’\0‘的地址减去了a的地址,求得了字符串的长度;

这里有指针减指针

那么是否有指针加指针呢?

答案是没有

指针的本质是地址

地址-地址是两个地址之间的距离

地址+地址没有任何意义

4.3 指针的关系运算

for(vp = &values[N_VALUES]; vp > &values[0];)
{
*--vp = 0;
}

代码简化, 这将代码修改如下:

for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--)
{
*vp = 0;
}

实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。

标准规定:

允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。

5. 指针和数组

我们看一个例子:

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

运行结果:

可见数组名和数组首元素的地址是一样的。

结论:数组名表示的是数组首元素的地址。(2种情况除外,数组章节讲解了)

那么这样写代码是可行的:

int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr;//p存放的是数组首元素的地址

既然可以把数组名当成地址存放到一个指针中,我们使用指针来访问一个就成为可能。

例如:

#include <stdio.h>
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr; //指针存放数组首元素的地址
int sz = sizeof(arr)/sizeof(arr[0]);
for(i=0; i<sz; i++)
{
printf("&arr[%d] = %p <====> p+%d = %p\n", i, &arr[i], i, p+i);
}
return 0;
}

运行结果:

所以 p+i 其实计算的是数组 arr 下标为i的地址。

那我们就可以直接通过指针来访问数组。

如下:

int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
int *p = arr; //指针存放数组首元素的地址
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i<sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}

6. 二级指针

指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?

这就是 二级指针 。

当我们把代码该成下面这样,便通过解引用操作*p改变了变量a的值

解引用拿到的是a的内容

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

同样我们也可以通过对二级指针的两次解引用操作来改变

理论上也有三级指针,四级指针,思考方式是相同的,但是三级指针一般都用得很少了。

7. 指针数组

指针数组是指针还是数组?

答案:是数组。是存放指针的数组。

数组我们已经知道整形数组,字符数组。

//整型数组
  int arr[10];
//字符数组
  char arr2[5];

那么我们可以得出指针数组

那指针数组是怎样的?

//指针数组-存放指针的数组
  int* arr3[5];//存放整型指针的数组
  char* arr4[6];//存放字符指针的数组
int* arr3[5];//是什么?

arr3是一个数组,有五个元素,每个元素是一个整形指针。

指针数组的使用

示例:

int main()
{
  int a = 10;
  int b = 20;
  int c = 30;
  int d = 40;
  int e = 50;
  int* arr3[5] = {&a,&b,&c,&d,&e};//存放整型指针的数组
  int i = 0;
  for (i = 0; i < 5; i++)
  {
    printf("%d ", *(arr3[i]));
  }
  return 0; 
}

示例2:

用一维数组模拟一个二维数组

int main()
{
  //用一维数组模拟一个二维数组
  int arr1[] = { 1,2,3,4,5 };
  int arr2[] = { 2,3,4,5,7 };
  int arr3[] = { 3,4,5,6,7 };
  int arr4[] = { 4,5,6,7,8 };
  int* arr[4] = { arr1,arr2,arr3,arr4 };这里的元素都是首元素地址
  int i = 0;
  for(i=0;i<4;i++)
  { 
    int j = 0;
    for (j = 0; j < 5; j++)
    {
      printf("%d ", arr[i][j]);//利用for循环,i,j控制行数和列数
      //printf("%d ", *(*(arr + i) + j));
    //arr[j]==*(arr+j)
    //arr+i,是下标为i的元素,解引用之后是取arr1~arr4中某个数组,再加j后解引用是取到下标为j的元素,
    }
    printf("\n");
  }
  return 0;
}

结语:

这里我们关于初阶指针的内容就介绍完了,如果小伙伴还有不理解的内容,也不要担心,之后还会有指针进阶的介绍。
文章中某些内容我们之前有介绍,所以只是一笔带过,还请谅解。
希望以上内容对大家有所帮助👀,如有不足望指出🙏


加油!!!




相关文章
|
存储 编译器
初阶指针(详解)
初阶指针(详解)
33 1
C初阶--指针初阶(上):什么是指针+指针类型+野指针(下)
C初阶--指针初阶(上):什么是指针+指针类型+野指针(下)
|
存储 Perl
C初阶--指针初阶(上):什么是指针+指针类型+野指针(上)
C初阶--指针初阶(上):什么是指针+指针类型+野指针(上)
|
4月前
|
存储 程序员 C语言
【C初阶】初识指针
【C初阶】初识指针
|
5月前
|
编译器 C语言
|
6月前
|
存储 编译器 Perl
指针初阶(1)
指针初阶(1)
35 0
|
6月前
指针初阶(2)
指针初阶(2)
32 0
|
存储
初阶指针(纯干货!!!)上
初阶指针(纯干货!!!)
71 0
|
存储 编译器
指针(初阶)
指针(初阶)
64 0