C语言

6. C语言--指针

Posted on 2022-01-30,10 min read

6.指针

6.1 指针是什么?

在计算机中,所有的数据都是存放在存储器中的,不同的数据类型占有的内存空间的大小各不相同。内存是以字节为单位的连续编址空间,每一个字节单元对应着一个独一的编号,这个编号被称为内存单元的地址。比如:int 类型占 4 个字节,char 类型占 1 个字节等。系统在内存中,为变量分配存储空间的首个字节单元的地址,称之为该变量的地址。地址用来标识每一个存储单元,方便用户对存储单元中的数据进行正确的访问。在高级语言中地址形象地称为指针。

6.1.1 内存与地址

指针相对于一个内存单元来说,指的是单元的地址,该单元的内容里面存放的是数据。在 C 语言中,允许用指针变量来存放指针,因此,一个指针变量的值就是某个内存单元的地址或称为某内存单元的指针。

6.1.2 内存空间的访问

  • 直接访问:变量代表有名字的内存单元,通过变量名直接访问内存空间。
  • 间接访问:指针是内存空间的地址,通过指针解引用间接访问内存空间。

6.1.3 指针声明

type* pointer_name;
// type指明该指针变量的类型
// *说明该变量是一个指针变量

6.1.4 指针大小

int main()
{
  printf("%u\n", sizeof(int*));
  printf("%u\n", sizeof(char*));
  printf("%u\n", sizeof(float*));
  return 0;
}
  • 32位平台,占据4byte

  • 64位平台,占据8byte

6.2 指针和指针类型

6.2.1 指针的类型

指针的类型和指针所指向的对象的类型是两个不同的概。

int main()
{
  int a = 100;
  int* pa = &a; // 其中`*`表示pa是一个指针变量,`int`表示pa是一个int类型指针
  return 0;
}
  • 指针的大小都是一样的,为什么不创建一个通用类型指针?

6.2.2 指针类型的意义

  • 指针类型决定了指针解引用的权限(能访问字节的数目)

    int main()
    {
      int a = 0xffffffff;
      int* pi = &a; // int类型指针
      *pi = 1; // 解引用操作四个字节
      printf("%x\n", a);
      // char* pc = &a;
      // *pc = 1;
      // printf("%x\n", a);
      return 0;
    }
    


    int main()
    {
      int a = 0xffffffff;
      // int* pi = &a; 
      // *pi = 1;
      // printf("%x\n", a);
      char* pc = &a; // char类型指针
      *pc = 1; // 解引用只操作一个字节
      printf("%x\n", a);
      return 0;
    }
    


  • 指针类型决定了指针的步长

    int main()
    {
      int arr[10] = { 0 };
      int* pi = arr;
      char* pc = arr;
      printf("%x", pi);
      printf("%x", pi+1); // int类型指针+1,改变4个字节,步长为4byte
      printf("%x", pc);
      printf("%x", pc+1); // char类型指针+1,改变1个字节,步长为1byte
      return 0;
    }
    


6.2.3 使用

int main()
{
  int arr[10] = { 0 };
  int* pa = arr; // 数组名是数组第一个元素的指针
  for (int i = 0; i < 10; i++)
  {
    *pa = 100;
    pa++;
  }
  for (int i = 0; i < 10; i++)
  {
    printf("%d\n", arr[i]);
  }
  return 0;
}


  • 如果将int类型指针换为char类型指针,如下

6.3 野指针

定义:指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。

6.3.1 野指针的成因

  • 指针变量未初始化

    类似于:去酒店,没有办理入住手续,直接随便找个房间就住。

    int main()
    {
      int* p; // 局部变量指针未初始化,默认为随机值
      *p = 100;
      return 0;
    }
    


  • 指针越界访问

    类似于:酒店房间号只有000~100,而你非要去找房间号为111的房间。

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


  • 指针指向的空间释放

    类似于:都已经办理退房手续,你仍然要在房间里住。

    // 非法访问内存,指针指向的空间不属于该程序
    int* func() 
    {
      int a = 10;
      return &a; // 函数返回之后,将a的内存空间释放,还给操作系统
    }
    
    int main()
    {
      int* p = func();
      *p = 20;
      return 0;
    }
    
    


6.3.2 如何避免野指针

  • 指针初始化

    // 方式一
    int* p = NULL // NULL在stdio.h头文件中
    
    // 方式二
    int a = 100;
    int* p = &a;
    
  • 小心指针越界

    // 数组越界
    // C语言本身是不会检查数组是否越界
    
  • 指针指向空间及时置NULL

    *p = NULL;
    // 此时p依然不能使用
    
    // 例如:
    int main()
    {
      int* p = NULL;
      *p = 100; // NULL属于操作系统,空间地址并没有分配给用户,所以用户不能访问
      return 0;
    }
    
    


  • 指针使用前检查有效性

    int *p = NULL;
    if(p != NULL){
      code...
    }
    

6.3.3 实例

#include <stdio.h>
main() {
    int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, *p = a + 5, *q = NULL;
    *q = *(p+5);
    printf("%d %d\n", *p, *q);
}
// 结果:运行后报错

6.4 指针运算

6.4.1 + -

int main()
{
  int arr[5];
  int* p;
  for (p = &arr[0]; p < &arr[5]; )
  {
    *p++ = 0;
    // 相当于:
    // *p = 0;
    // p++;
  }
  return 0;
}
// 随着数组索引增大,地址由低到高变化


6.4.2 > < ==

指针的关系运算。

// 打印数组中的元素
int main()
{
  int arr[5] = { 1, 2, 3, 4, 5 };
  int* p = arr; // arr第一个元素的地址
  int* pend = p + 4; // arr最后一个元素的地址
  while (p<=pend)
  {
    printf("%d\n", *p);
    p++;
  }

  return 0;
}


6.4.3 指针-指针

两个指针相减的结果是数组中两个元素之间的个数。指针相加是没有意义的(类似日期与日期相加)。

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


指针相减的前提:两个指针指向同一块区域。

6.4.4 应用(求字符串长度len)

  • 方法一:库函数

    #include <string.h>
    int main()
    {
      char s[] = "Hello";
      int len = strlen(s);
      printf("%d\n", len);
    }
    


  • 方法二:计数器

    int my_strlen(char* p)
    {
      int count = 0;
      while (*p != '\0')
      {
        count++;
        p++;
      }
      return count;
    }
    
    int main()
    {
      char s[] = "Hello";
      int len = my_strlen(s); // 传参时,传递的是字符串的第一个字符的地址
      printf("%d\n", len);
    }
    


  • 方法三:指针相减

    int my_strlen(char* p)
    {
      char* p0 = p;
      while (*p != '\0')
      {
        p++;
      }
      return p - p0;
    }
    
    int main()
    {
      char s[] = "Hello";
      int len = my_strlen(s); // 传参时,传递的是字符串的第一个字符的地址
      printf("%d\n", len);
    }
    


6.4.4 其他

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

// 在大部分的编译器上可以正常运行,然而应避免这样编码,因为标准并不保证它可行。

标准规定

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

6.5 指针和数组

6.5.1 数组名是数组首元素的地址

int main()
{
  int arr[5] = { 0 };
  printf("%p\n", arr);
  printf("%p\n", &arr[0]);
  return 0;
}


6.5.2 通过指针操作数组元素

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


6.5.3 下标引用操作符的交换律

int main()
{
    int arr[5] = { 1, 2, 3, 4, 5 };
    int* p = arr;
    // 访问下标为3的元素的底层原理:
    // arr[3] => *(p+3)
    // 根据加法的交换律,可得
    // *(p+3) = *(3+p)
    // 则
    // arr[3] = 3[arr];
    // []下标引用操作符也具有交换律
    printf("%d\n", arr[3]);
    printf("%d\n", 3[arr]);
    printf("%d\n", p[3]);
    printf("%d\n", 3[p]);
}


6.6 二级指针

6.6.1 创建

int main()
{
  int n = 100;
  int* pn = &n; // 一级指针
  int** ppn = &pn; // 二级指针
}
// 依次类推,还有三级、四级...


// 指针变量中存储了地址,跟普通变量一样,需要开辟一块内存空间用来存放数据
// 而指针变量所在的地址,用二级指针来表示
// pnn中存放的是pn的地址
// pn中存放的是n的地址
// n中存放的是数据

6.6.2 访问

int main()
{
  int n = 100;
  int* pn = &n; // 一级指针
  int** ppn = &pn; // 二级指针
  // *pnn = pn;
  // *pn = n;
  // 所以:**pnn = n; 即通过**pnn即可访问变量n
}

6.7 指针数组

int arr1[10]; // 整形数组
float arr2[10]; // 浮点型数组
char arr3[10]; // 字符型数组

int* parr1[10]; // 整形指针数组
float* parr2[10]; // 浮点型指针数组
char* parr3[10]; // 字符型指针数组


下一篇: 5. C语言--操作符→