C语言

10. C语言进阶--指针进阶

Posted on 2022-02-01,44 min read

[TOC]

2. 指针进阶

2.1 字符指针

2.1.1 介绍

char c = 'h';
char* pc = &c;
// 访问权限为1bbyte
// ++/--为1byte

2.1.2 例

int main()
{
  char str1[] = "Hello World!";
  char str2[] = "Hello World!";
  char* str3 = "Hello World!";
  char* str4 = "Hello World!";
  // 将常量字符串的首字母H存放到指针变量str中
  // 常量是不可修改的
  // 在内存中只需要存在一份
  // 所以str3和str4指向的是同一块内存区域
  // 则str3和str4的指针值相等
  if (str1 == str2)
  {
    printf("same\n");
  }else {
    printf("not same\n");
  }

  if (str3== str4)
  {
    printf("same\n");
  }
  else {
    printf("not same\n");
  }
  return 0;
}

![](file://C:\Users\Turbo\Desktop\2. 指针进阶\image\image.png)

2.2 数组指针

指向数组的指针。

2.2.1 格式

int arr[10] = { 0 };
int (*p)[10] = &arr;
// p 变量名
// *表示p是一个指针
// [10]表示该指针指向一个数组
// int 表示该指针指向的数组每个元素的类型为int


 // 注意 int (*p)[10] 与 int *p[10] 区分
 // 如果没有小括号 p先于[]结合 成数组
 // 若有小括号p先于*结合 成指针

2.2.2 数组名

// 数组名是数组首元素的地址
// 但是有2个例外
// 1. sizeof(arr) 数组名表示整个数组,计算的是整个数组大小,单位是字节
// 2. &arr 数组名表示整个数组,取出的是整个数组的地址

2.2.3 arr和&arr

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

// arr是数组首元素的地址
// &arr是数组指针
// 指针值相同,但含义完全不同

![](file://C:\Users\Turbo\Desktop\2. 指针进阶\image\image_1.png)

2.2.4 应用

// 数组指针一般用于二维数组中,在一维数组中体现不出优势

// 普通方法
void print1(int arr[3][4], int r, int c)
{
  int i = 0;
  int j = 0;
  for ( i = 0; i < r; i++)
  {
    for (j = 0; j < c; j++)
    {
      printf("%d ", arr[i][j]);
    }
    printf("\n");
  }
}

// 数组指针
void print2(int(*p)[4], int r, int c)
{
  int i = 0;
  int j = 0;
  for (i = 0; i < r; i++)
  {
    for (j = 0; j < c; j++)
    {
      printf("%d ", *(*(p+i) + j));
    }
    printf("\n");
  }
}

int main()
{
  int arr[3][4] = { 
    { 1, 2, 3, 4 }, 
    { 11, 22, 33, 44 },
    { 111, 222, 333, 444} 
  };
  print1(arr, 3, 4);
  print2(arr, 3, 4); 
  // arr数组名,传递的是数组首元素的地址
  // 对于二维数组,则首元素就是第一行的一维数组
  // 则arr为一维数组指针
  // print2函数的需要使用数组指针接收
  return 0;
}

2.2.5 类型判断

int (*parr1)[10];
// parr1是数组指针变量
// 该指针指向元素个数为10、每个元素为int类型的数组

int (*parr2[10])[5];
// parr2先和[10]结合 所以parr2是一个数组
// 数组的类型为 int (*)[5]; 是数组指针
// 则parr2是一个存储10个数组指针的数组
// 且每个数组指针指向元素类型为int、元素个数为5的数组

2.3 指针数组

2.3.1 介绍

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

2.3.2 例

int main()
{
  int a[] = {1, 2, 3, 4, 5};
  int b[] = {11, 22 ,33, 44, 55};
  int c[] = {111, 222 ,333, 444, 555};
  int* arr[] = { a, b, c };
  for (int i = 0; i < 3; i++)
  {
    for (int j = 0; j < 5; j++) {
      printf("%d ", arr[i][j]);
      printf("%d ", *(arr[i] + j));
    }
    printf("\n");
  }
  return 0;
}
// 模拟二维指针

![](file://C:\Users\Turbo\Desktop\2. 指针进阶\image\image_2.png)

2.4 数组传参和指针传参

2.4.1 一维数组传参

void test1(int arr[]) {} // 数组(省略大小)
// √

void test1(int arr[10]) {} // 数组(未省略大小)
// √

void test1(int *arr) {} // int指针
// √

void test2(int *arr[]) {} // 指针数组(省略大小)
// √

void test2(int **arr) {} // 二级指针
// √

int main()
{
  int arr1[5] = { 0 }; // 一维整形数组
  int* arr2[35] = { 0 }; // 一维整形指针数组
  test1(arr1);
  test2(arr2);
}

2.4.2 二维数组传参

void test(int arr[3][4]) {} // 二维数组(未省略)
// √

void test(int arr[][]) {} // 二维数组(行列都省略)
// ×

void test(int arr[][4]) {} // 二维数组(行省略)
// √
// 总结:二维数组传参,函数形参的设计只能省略第一个[]的数组
//      因为对一个二维数组,可以不知道有多少行,但必须一行有多少个元素
//      方便运算

void test(int *arr) {} // int指针(传递过来的是第一行数组的指针)
// ×

void test(int *arr[4]) {} // 指针数组
// ×

void test(int (*arr)[4]) {} // 数组指针
// √

void test(int **arr) {} // 二级指针
// ×

int main()
{
  int arr[3][4] = { 0 };
  test(arr);
  return 0;
}

2.4.3 一级指针传参

// eg:
void print(int* ptr, int sz)
{
  int i = 0;
  for (i = 0; i < sz; i++)
  {
    printf("%d ", *(ptr + i));
  }
}

int main()
{
  int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
  int* p = arr;
  int sz = sizeof(arr) / sizeof(arr[0]);
  print(p, sz);
  return 0;
}

// 当函数的形参为指针时
// 实参可以是对应类型变量的地址 &var
//    也可以是一级指针变量 ptr

2.4.4 二级指针传参

void print(int** ptr)
{
  // ...
}

int main()
{
  // 指针数组首元素
  int* arr[10] = { 0 };
  int** p = arr;
  print(p);

  // 二级指针变量
  int a = 7;
  int* pa = &a;
  int** ppa = &pa;
  print(ppa);
  print(&pa);
  return 0;
}

// 当函数的形参是二级指针时
// 实参可以是指针数组首元素
//    也可以是二级指针变量

2.5 函数指针

2.5.1 介绍

指向函数的指针。存放函数地址的指针。

int Add(int x, int y)
{
  return x + y;
}

int main()
{
  printf("%p\n", &Add);
  printf("%p\n", Add);
  // 两种打印方式打印结果相同
  // 都是函数的地址
  // Add和&Add完全等价
  // 函数名本质上就是地址
  return 0;
}

![](file://C:\Users\Turbo\Desktop\2. 指针进阶\image\image_3.png)

2.5.2 格式

返回值类型 (*函数指针变量)(参数类型);

// eg:
int (*pf)(int, int) = &Add;
int (*pf)(int, int) = Add;
// * 表示pf是一个指针变量
// () 表示该指针指向一个函数
// (int, int) 表示指针指向的函数的参数类型
// int (*pf) 前面的int表示指针指向的函数的返回值类型

2.5.3 调用

int Add(int x, int y)
{
  return x + y;
}

int main()
{
  int (*pf)(int, int) = Add; // Add === pf
  int ret1 = (*pf)(1, 2); 
  // 其中pf前的*只是摆设 可以省略 也可以有多个
  int ret2 = pf(2, 3);
  int ret3 = Add(3, 4);
  return 0;
}

2.5.4 应用

  • 代码一

    (*(void (*)())0)();
    // 调用0地址处的函数
    // 该函数无参,返回值类型为void
    // 1. void(*)() - 函数指针类型
    // 2. (void(*)())0 - 将0强制类型转换为函数指针类型 即函数地址
    // 3. *(void (*)())0 - 将该函数地址进行指针解引用
    // 4. (*(void (*)())0)() - 调用0地址处的函数
    
  • 代码二

    void (* signal(int, void(*)(int)))(int);
    // 1. signal和()先结合 说明signal是一个函数名
    // 2. signal函数的参数是一个int和函数指针
    //    该函数指针类型 void(*)(int) 指向参数为int 返回值类型为void的函数
    // 3. signal函数的返回类型为函数指针
    //    将void (* signal(int, void(*)(int)))(int); 中的 signal(int, void(*)(int)) 剔除
    //    结果为void (*)(int) 即为函数的返回类型
    //    该函数指针类型 void(*)(int) 指向参数为int 返回值类型为void的函数
    // 总结:signal是一个 参数为int和函数指针 返回类型为函数指针 的函数声明
    
    // 简化如下(含义相同 表达更清晰)
    typedef void(*func_ptr)(int);
    func_ptr signal(int, func_ptr);
    

    ![代码片段来源:C陷阱和缺陷(15页)](file://C:\Users\Turbo\Desktop\2. 指针进阶\image\image_4.png)
    代码片段来源:C陷阱和缺陷(15页)

2.6 函数指针数组

2.6.1 介绍

存放函数指针的数组。

// 定义:
// 函数指针
int (*fp)(int, int);
// fp为指针变量,*表示该变量为指针,其余 int (int, int) 为函数指针指向的函数类型

// 函数指针数组
int (*arr[5])(int, int);
// arr3为数组名,其余 int (* [5])(int, int) 为数组类型,int (* )(int, int) 为数组元素类型

2.6.2 应用

计算器实现

  • 基本实现

    #define _CRT_SECURE_NO_WARNINGS
    #include <stdio.h>
    
    void menu()
    {
      // 打印菜单
      printf("*************************\n");
      printf("**** 两位数加减乘除 *****\n");
      printf("*************************\n");
      printf("**** 1.add    2.sub *****\n");
      printf("**** 3.mul    4.div *****\n");
      printf("****     0.exit     *****\n");
      printf("*************************\n");
    }
    
    int Add(int x, int y)
    {
      return x + y;
    }
    
    int Sub(int x, int y)
    {
      return x - y;
    }
    
    int Mul(int x, int y)
    {
      return x * y;
    }
    
    int Div(int x, int y)
    {
      return x / y;
    }
    
    int main() 
    {
      int input = 0;
      do
      {
        menu(); // 打印菜单
        printf("请输入:> "); // 提示
        scanf("%d", &input); // 获取用户输入 
        int x = 0;
        int y = 0;
        int ret = 0;
        switch (input) // 确定计算规则
        {
        case 1:
          scanf("%d %d", &x, &y); // 获取用户需要计算的数据
          ret = Add(x, y); // 相加
          printf("结果是:%d\n", ret);
          break;
        case 2:
          scanf("%d %d", &x, &y); // 获取用户需要计算的数据
          ret = Sub(x, y); // 相减
          printf("结果是:%d\n", ret);
          break;
        case 3:
          scanf("%d %d", &x, &y); // 获取用户需要计算的数据
          ret = Mul(x, y); // 相乘
          printf("结果是:%d\n", ret);
          break;
        case 4:
          scanf("%d %d", &x, &y); // 获取用户需要计算的数据
          ret = Div(x, y); // 相除
          printf("结果是:%d\n", ret);
          break;
        case 0:
          break;
        default:
          printf("输入错误,请重新输入!\n");
          break;
        }
    
      } while (input);
    }
    
  • 函数指针数组实现

    在上面代码的switch语句中发现有大量的代码冗余,接下来通过函数指针数组对main函数进行重写。

    int main()
    {
      int input = 0;
      do
      {
        menu(); // 打印菜单
        printf("请输入:> "); // 提示
        scanf("%d", &input); // 获取用户输入 
        // 创建函数指针数组
        int (*pfarr[5])(int, int) = { NULL, Add, Sub, Mul, Div };
        // 数组首元素使用NULL补位,使得input值为数组下标 恰好对应相应的函数
        if (input == 1 || input == 2 || input == 3 || input == 4)
        {
          int x = 0;
          int y = 0;
          int ret = 0;
          scanf("%d %d", &x, &y);
          ret = pfarr[input](x, y);
          printf("结果是:%d\n", ret);
        }
        else if (input == 0)
        {
          break;
        }
        else
        {
          printf("输入错误,重新输入!\n");
        }
      } while (input);
    }
    

转移表 -- 函数指针数组。

2.7 指向函数指针数组的指针(了解)

指向函数指针数组的指针 指针指向一个数组 数组的每个元素是函数指针

2.7.1 指向整形数组的指针

// 1.整形数组
int arr1[5];
// arr1为数组名,其余 int [5] 为数组类型,int为数组元素类型

// 2.整形数组的指针
int(*parr1)[5] = &arr1;
// parr1为指针变量,*表示该变量为指针,其余 int [5] 为指针指向的对象的类型

2.7.2 指向整形指针数组的指针

// 1.整形指针数组
int* arr2[5];
// arr2为数组名,其余int* [5] 为数组类型,int*为数组元素的类型

// 2.整形指针数组的指针
int* (*parr2)[5] = &arr2;
// parr2为指针变量,*表示该变量为指针,其余 int* [5] 为指针指向的对象的类型

2.7.3 指向函数指针数组的指针

// 1.函数指针
int (*fp)(int, int);
// fp为指针变量,*表示该变量为指针,其余 int (int, int) 为函数指针指向的函数类型

// 2.函数指针数组
int (*arr3[5])(int, int);
// arr3为数组名,其余 int (* [5])(int, int) 为数组类型,int (* )(int, int) 为数组元素类型

// 3.函数指针数组的指针
int (*(*parr3)[5])(int, int) = &arr3;
// parr3为指针变量,*表示该变量为指针,其余 int (* [5])(int, int) 为指针指向对象的类型

2.8 回调函数

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,这就是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

2.8.1 再次重写计算器

根据回调函数再次重写计算器实例的main函数

int Cal(int (*fp)(int, int)) // 形参为函数指针类型int (* )(int, int)
{
  int x = 0;
  int y = 0;
  scanf("%d %d", &x, &y);
  return fp(x, y);
}

int main()
{
  int input = 0;
  do
  {
    menu(); // 打印菜单
    printf("请输入:> "); // 提示
    scanf("%d", &input); // 获取用户输入 
    int (*pfarr[5])(int, int) = { NULL, Add, Sub, Mul, Div };  // 数组指针
    if (input == 1 || input == 2 || input == 3 || input == 4)
    {
      int ret = Cal(pfarr[input]); // 将函数指针作为参数进行传递
      printf("结果是:%d\n", ret);
    }
    else if (input == 0)
    {
      break;
    }
    else
    {
      printf("输入错误,重新输入!\n");
    }
  } while (input);
}

2.8.2 模拟实现qsort库函数

qsort底层是快速排序。此处只需关注如何使用,即qsort函数的参数和返回值。所以使用冒泡排序实现相同使用方法的该函数。

  • 介绍

    • 头文件<stdlib.h>

    • 语法

      void qsort(
         void *base,
         size_t number,
         size_t width,
         int (__cdecl *compare )(const void *, const void *)
      );
      
    • 参数

      • base:数组的起始地址
      • number:数组中元素的个数
      • width:数组中元素的大小(单位:byte)
      • compare:一个指向用户提供的函数的指针,该函数比较数组两个元素并返回一个值,改值表示他们之间的关系
    • compare

      compare( (void *) & elem1, (void *) & elem2 );
      
      比较返回值 描述
      <0 elem1<elem2
      =0 elem1=elem2
      >0 elem1>elem2
    > 数组按递增排序,如果要对数组进行递减排序,反转 比较返回值 中的`>`和`<`
  • 使用实例

    • 比较整形

      #include <stdio.h>
      #include <stdlib.h>
      
      int compare_int(const void* e1, const void* e2)
      {
        return *(int*)e1 - *(int*)e2;
      }
      
      int main()
      {
        int arr[] = {5,3,1,2,4,6};
        int num = 0;
        num = sizeof(arr) / sizeof(arr[0]);
        qsort(arr, num, 4, compare_int);
        for (int i = 0; i < 6; i++)
        {
          printf("%d ", arr[i]);
        }
        return 0;
      }
      
      // void*类型可以存放任意类型地址
      // 但是不能解引用和计算(解引用时,并不清楚访问几个字节)
      // 使用时需要强制类型转换为需求的类型指针
      
    • 比较字符串

      // 字符串比较大小时,按位比较字符的ASCII码
      // 如果字符串长度不相等,如"ab"和"abc"比较
      // 'a'和'a'比较 相等 右移
      // 'b'和'b'比较 相等 右移
      // '\0'和'c'比较 '\0'<'c' "ab"<"abc"
      #define _CRT_NONSTDC_NO_DEPRECATE
      #include <stdlib.h>
      #include <string.h>
      #include <stdio.h>
      
      int compare_string(const void* e1, const void* e2)
      {
        return stricmp(*(char**)e1, *(char**)e2);
      }
      int main()
      {
        char* arr[] = { "afd","fasf","fadew","fdfa","rqewr","fdasf" };
        int num = 0;
        num = sizeof(arr) / sizeof(arr[0]);
        qsort(arr, num, 4, compare_string);
        for (int i = 0; i < 6; i++)
        {
          printf("%s ", arr[i]);
        }
        return 0;
      }
      
    • 比较结构体

      #include <stdlib.h>
      #include <string.h>
      #include <stdio.h>
      
      struct Student {
        char name[20];
        int age;
      };
      
      int compare_by_name(const void* e1, const void* e2)
      {
        return strcmp(((struct Student*)e1)->name, ((struct Student*)e2)->name);
      }
      
      int compare_by_age(const void* e1, const void* e2)
      {
        return (*((struct Student*)e1)).age - (*((struct Student*)e2)).age;
      }
      
      print_struct_arr(struct Student arr[], int num)
      {
        for (int i = 0; i < 4; i++)
        {
          printf("name: %s, age: %d\n", (arr[i]).name, (arr[i]).age);
        }
      }
      
      int main()
      {
        struct Student arr[4] = { {"listen", 20},{"turbo", 34},{"dummy", 24},{"free", 19}};
        int num = 0;
        num = sizeof(arr) / sizeof(arr[0]);
        qsort(arr, num, sizeof(arr[0]), compare_by_name);
        printf("按照姓名排序如下:\n");
        print_struct_arr(arr, num); // 打印结构体数组
        qsort(arr, num, sizeof(arr[0]), compare_by_age);
        printf("按照年龄排序如下:\n");
        print_struct_arr(arr, num); // 打印结构体数组
        return 0;
      }
      
  • 冒泡排序实现(升序为例)

    • 原理图

      描述:当前元素和下一个元素进行比较。如果小于下一个元素,则保持不变;如果大于下一个元素,则交换位置。然后下标+1,再比较,再下标+1,直到比较完倒数第二个元素和最后一个元素,这个过程称为一趟。接着再从头开始进行比较,如下图所示:

      ![](file://C:\Users\Turbo\Desktop\2. 指针进阶\image\image_5.png)

      容易得出:5个数进行排序需要4趟。

    • 代码

      void bubble_sort(int* p, int num)
      {
        for (int i = 0; i < num-1; i++)
        {
          for (int j = 0; j < num-1; j++)
          {
            if (p[j] > p[j+1])
            {
              int tmp = p[j];
              p[j] = p[j + 1];
              p[j + 1] = tmp;
            }
          }
        }
      }
      
      int main()
      {
        int arr[] = {5,3,1,2,4,6};
        int num = sizeof(arr) / sizeof(arr[0]);
        bubble_sort(arr, num);
        for (int i = 0; i < num; i++)
        {
          printf("%d ", arr[i]);
        }
        return 0;
      }
      
    • 优化

      第二趟6和8比较 与 前面的6和8比较 重复

        第三趟4和6比较、6和8比较 与 前面的4和6比较、6和8比较 重复
        第四趟2和4比较、4和6比较、6和8比较 与 前面的2和4比较、4和6比较、6和8比较 重复
        所以:在第二趟中一次重复的比较;在第三趟中二次重复的比较;在第四趟中三次重复的比较。优化的原理就是让每两个数比较只出现一次。
        如图中,圈中的是重复进行比较的两元素。
        ![](image/image_6.png)
        ![](https://listen2022.github.io/post-images/1643676911293.png)
      
      void bubble_sort(int* p, int num)
      {
        for (int i = 0; i < num-1; i++)
        {
          for (int j = 0; j < num-i-1; j++)
          {
            if (p[j] > p[j+1])
            {
              int tmp = p[j];
              p[j] = p[j + 1];
              p[j + 1] = tmp;
            }
          }
        }
      }
      
      int main()
      {
        int arr[] = {5,3,1,2,4,6};
        int num = sizeof(arr) / sizeof(arr[0]);
        bubble_sort(arr, num);
        for (int i = 0; i < num; i++)
        {
          printf("%d ", arr[i]);
        }
        return 0;
      }
      
    • 增强

      前面的冒泡排序算法只能比较两个整形,现在将冒泡排序功能进行扩展,使其可以比较任何数据类型(参考sqort库函数)。

      void bubble_sort(void* p, int num, int width, int (*compare)(const void*, const void*))
      {
        for (int i = 0; i < num - 1; i++)
        {
          for (int j = 0; j < num - i - 1; j++)
          {
            // 比较大小,前者>后者返回整数
            if (compare((char*)p + j * width, (char*)p + (j+1) * width) > 0)
            {
              // 交换值
              // 确定大小,但不知道类型 
              // 按字节交换 char类型刚好代表一个字节,总共交换width次
              for (int k = 0; k < width; k++)
              {
                char tmp = *((char*)p + j * width + k);
                *((char*)p + j * width + k) = *((char*)p + (j + 1) * width + k);
                *((char*)p + (j + 1) * width + k) = tmp;
              }
            }
          }
        }
      }
      
    • 测试

      // bubble_sort测试
      int compare_int(const void* e1, const void* e2)
      {
        return *(int*)e1 - *(int*)e2;
      }
      
      int main()
      {
        int arr[] = {5,3,1,2,4,6};
        int num = sizeof(arr) / sizeof(arr[0]);
         bubble_sort(arr, num, sizeof(arr[0]), compare_int);
        for (int i = 0; i < num; i++)
        {
          printf("%d ", arr[i]);
        }
        return 0;
      }
      

2.9 习题

2.9.1 指针

  • int a[] = { 1,2,3,4 };

    int main()
    {
      int a[] = { 1,2,3,4 };
      printf("%d\n", sizeof(a)); // 16  
      // 解释:a 单独放在 sizeof() 中时,a 表示整个数组,
      //      计算的是整个数组的大小,整个数组 4 个元素,且数组元素为 int 类型
      //      所以数组大小为 4 × 4 = 16byte
    
      printf("%d\n", sizeof(a + 0)); // 4/8 
      // 解释:a 表示数组首元素的地址,+0 之后依然是数组是首元素的地址,
      //      作为地址在32位计算机中为 4byte,64位系统中为 8byte
    
      printf("%d\n", sizeof(*a)); // 4
      // 解释:a 表示数组首元素的地址,*a 对 a 解引用,得到的是数组首元素 1,
      //      数组元素的类型为 int,所以 *a 的大小为 4byte
    
      printf("%d\n", sizeof(a+1)); // 4/8
      // 解释:a 表示数组首元素的地址,a+1 表示数组第二个元素的地址,
      //      作为地址,其大小为 4byte(32位系统) 或 8byte(64位系统)
    
      printf("%d\n", sizeof(a[1])); // 4
      // 解释:a[1] 表示数组首元素,数组元素的类型为 int,其大小为 4byte
    
      printf("%d\n", sizeof(&a)); // 4/8
      // 解释:&a 中 a 表示的是整个数组,获取的是整个数组的指针
      //     作为地址,其大小为 4/8byte
    
      printf("%d\n", sizeof(*&a)); // 16
      // 解释:&a 获取的是整个数组的地址,再解引用得到的是整个数组
      //      整个数组的大小为 4 × 4 = 16byte
    
      printf("%d\n", sizeof(&a + 1)); // 4/8
      // 解释:&a 获取的是整个数组的指针,&a+1 跳过一个 int [4] 类型的数组
      //      &a+1 指向数组 arr 之后第一个位置的地址
      //      作为指针,其大小为 4/8byte
    
      printf("%d\n", sizeof(&a[0])); // 4/8
      // 解释:a[0] 获取数组首元素,&a[0] 对数组首元素取地址,即数组首元素的地址
      //      作为指针,其大小为 4/8byte
    
      printf("%d\n", sizeof(&a[0] + 1)); // 4/8
      // 解释:易得 &a[0] 为数组首元素的地址,&a[0]+1 跳过一个元素,
      //      即数组第二个元素的地址,作为指针,其大小为 4/8byte
    }
    

    ![](file://C:\Users\Turbo\Desktop\2. 指针进阶\image\image_7.png)

  • char arr[] = { 'a', 'b', 'c', 'd', 'e', 'f' };

    #include <string.h>
    int main()
    {
      char arr[] = { 'a', 'b', 'c', 'd', 'e', 'f' };
      printf("%d\n", sizeof(arr)); // 6
      // 解释:arr 表示整个数组,数组共6个元素,且元素类型为 char
      //      所以,数组的大小为 6byte
    
      printf("%d\n", sizeof(arr + 0)); // 4/8
      // 解释:arr 表示数组首元素的地址,arr+0 表示跳过 0byte,
      //      同样是数组首元素的地址,作为指针,其大小为 4 / 8byte
    
  printf("%d\n", sizeof(*arr)); // 1
  // 解释:arr 表示数组首元素的地址,*arr 对数组首元素的地址解引用
  //      *arr 表示数组首元素,数组元素为char类型,则数组首元素的大小为 1byte

   printf("%d\n", sizeof(arr[1])); // 1
  // 解释:arr[1] 表示数组第二个元素,数组元素为char类型,
  //      则数组第二个元素的大小为 1byte

  printf("%d\n", sizeof(&arr)); //  4/8
  // 解释:arr 表示整个数组,&arr 对整个数组取地址,
  //      作为地址,其大小为 4/8byte

  printf("%d\n", sizeof(&arr + 1)); // 4/8
  // 解释:arr 表示真个数组,&arr 对整个数组取地址,
  //      &arr+1 表示跳过一个 char [6] 类型数组的大小,
  //      则指向数组后第一个位置的地址,
  //      作为地址,其大小为 4/8byte

  printf("%d\n", sizeof(&arr[0] + 1)); // 4/8
  // 解释:arr[0] 表示数组首元素,&arr[0] 获取数组首元素的地址
  //      &arr[0]+1 跳过一个char类型元素的大小,
  //      则表示数组第二元素的地址,作为地址,其大小为 4/8byte

  printf("%d\n", strlen(arr)); // 随机值
  // 解释:arr表示数组首元素的地址,从arr开始向后找 \0 ,
  //      所以,结果为随机值

  printf("%d\n", strlen(arr + 1)); // 随机值
  // 解释:arr表示数组首元素的地址,则 arr+1 表示数组第二个元素的地址
  //      从数组第二个元素开始向后查找 \0 ,所以结果为随机值
  //      但相对于上一个随机值,相差1

  //printf("%d\n", strlen(*arr)); // error
  //// 解释:arr表示数组首元素的地址,*arr 表示对首元素地址解引用,则得到首元素 'a'
  ////      而 strlen 函数的参数是指针类型,所以 error

  //printf("%d\n", strlen(arr[1])); // error
  //// 解释:arr[1] 表示数组第二个元素 'b' ,同样参数类型不否,error

  printf("%d\n", strlen(&arr)); // 随机值
  // 解释:&arr 取真个数组的地址,但是其值与数组首元素地址相等
  //      所以,从数组首元素开始向后找 '\0' ,结果为随机值

  printf("%d\n", strlen(&arr + 1)); // 随机值
  // 解释:&arr 取整个数组的地址,&arr+1 指向数组后第一个位置的地址
  //     从该位置开始向后找 '\0' 结果为随机值
  //     但相对于上一个随机值,相差6

  printf("%d\n", strlen(&arr[0] + 1)); // 随机值
  // 解释:arr[0] 表示数组第一个元素,&arr[0] 对数组第一个元素取地址
  //      &arr[0]+1 跳过一个元素的大小,指向数组第二个元素,
  //      从数组第二个元素开始向后查找 '\0',结果为随机值

  return 0;
}
```
  • char arr[] = "abcdef";

    #include <string.h>
    int main()
    {
      char arr[] = "abcdef";
      // 等价于 
      //char arr[] = { 'a','b','c','d','e','f','\0' };
      printf("%d\n", sizeof(arr)); // 7
      // arr表示真个数组 数组大小为 6×1=7byte
    
      printf("%d\n", sizeof(arr + 0)); // 4/8
      // arr+0=arr 表示数组首元素地址
      // 作为地址 大小为 4/8 byte
    
      printf("%d\n", sizeof(*arr)); // 1
      // arr表示数组首元素地址
      // *arr 表示数组首元素'a' 大小为1byte
       printf("%d\n", sizeof(arr[1])); // 1
      // arr[1] 表示数组第二个元素'b' 大小为1byte
    
      printf("%d\n", sizeof(&arr)); // 4/8
      // &arr 获取真个数组arr的地址
      // 作为地址 大小为 4/8byte
    
      printf("%d\n", sizeof(&arr + 1)); // 4/8
      // &arr 获取真个数组arr的地址
      // &arr+1 跳过一个 char [7] 数组
      // 数组arr后面第一个位置的地址
    
      printf("%d\n", sizeof(&arr[0] + 1)); // 4/8
      // arr[0] 数组首元素
      // &arr[0] 数组首元素地址
      // &arr[0]+1 数组第二个元素地址
      // 作为地址 大小为 4/8byte
    
      printf("%d\n", strlen(arr)); // 6
      // arr数组首元素'a'地址
      // 从该地址开始向后找'\0'
      // strlen 获取从'a'到'\0'的字符串长度6
    
      printf("%d\n", strlen(arr + 1)); // 5
      // arr数组首元素'a'地址
      // arr+1 数组第二个元素'b'地址
      // strlen 获取从'b'到'\0'的字符串长度5
    
      //printf("%d\n", strlen(*arr)); // error
      // arr 数组首元素地址
      // *arr 数组首元素
      // strlen 参数为地址
    
      //printf("%d\n", strlen(arr[1])); // error
      // arr[1] 数组第二个元素
      // strlen 参数为地址
    
      printf("%d\n", strlen(&arr)); // 6
      // &arr 获取整个数组地址 等于数组首元素'a'地址
      // 从该地址开始向后找'\0'
      // strlen函数 从'a'到'\0'的字符串长度6
    
      printf("%d\n", strlen(&arr + 1)); // 随机值
      // &arr 获取整个数组地址 类型为数组指针
      // &arr+1 跳过一个 char [7] 数组 --> 数组arr后第一个位置地址
      // strlen函数 从该位置开始向后找'\0' 而后面的区域未知
      // 所以结果为随机值
    
      printf("%d\n", strlen(&arr[0] + 1)); // 5
      // &arr[0] 等价于 &*(arr+0) 等价于 arr
      // arr+1 表示数组第二个元素'b'地址
      // strlen 从'b'到'\0'的字符串长度5
      return 0;
    }
    
  • char* p = "abcdef";

    #include <string.h>
    int main()
    {
      char* p = "abcdef";
      printf("%d\n", sizeof(p)); // 4/8
      // p 字符指针 字符串首字符 'a' 的地址
      // 大小为 4/8byte
    
      printf("%d\n", sizeof(p + 1)); // 4/8 
      // p 字符指针 字符串首字符 'a' 的地址
      // p+1 跳过一个字符 字符串第二个字符 'b' 的地址
      // 大小为 4/8byte
    
      printf("%d\n", sizeof(*p)); // 1
      // p 字符指针 字符串首字符 'a' 的地址
      // 解引用得到字符'a' 大小为 1byte
    
      printf("%d\n", sizeof(p[0])); // 1 
      // p[0] 等价于 *(p+0)
      // p+0 字符串第1个字符'a'的地址
      // *(p+0) 字符串第1个字符'a' 大小为1byte
    
      printf("%d\n", sizeof(&p)); //  4/8
      // p 字符指针 字符串首字符 'a' 的地址
      // &p 获取字符指针的地址 二级指针
      // 大小为 4/8byte
    
      printf("%d\n", sizeof(&p + 1)); //  4/8
        // &p 获取字符指针的地址 二级指针
      // &p+1 作为指针 大小为 4/8byte
    
      printf("%d\n", sizeof(&p[0] + 1)); // 4/8
      // p[0] 字符串第1个字符'a'
      // &p[0] 字符串第1个字符 'a' 的地址
      // &p[0]+1 跳过1个字符 --> 字符串第2个字符'b'地址
    
      printf("%d\n", strlen(p)); // 6
      // p 字符指针 字符串第1个字符'a'地址
      // strlen 获取从'a'到'\0'字符串长度
    
      printf("%d\n", strlen(p + 1)); // 5 
      // p+1 字符串第2个字符'b'地址
      // strlen 获取从'b'到'\0'字符串长度
    
      //printf("%d\n", strlen(*p)); // error
      // *p 字符'a' strlen函数的参数为指针
    
      //printf("%d\n", strlen(p[0])); // error
      // p[0] 字符'a' strlen函数的参数为指针
    
      printf("%d\n", strlen(&p)); // 随机值
      // &p 字符指针p的地址 从p的地址开始向后查找'\0'
      // '\0' 可能再字符指针p中,也可能在其后面
      // 所以 strlen(&p) 为随机值
    
      printf("%d\n", strlen(&p + 1)); // 随机值
      // &p+1 字符指针p后面一个位置的地址
      // 从该位置向后查找 '\0' 后面的区域是未知的
      // 所以 strlen(&p+1) 为随机值
    
      printf("%d\n", strlen(&p[0] + 1)); // 5 
      // &p[0] 相当于 &*(p+0) = p
      // p+1 字符串第2个字符'b'地址
      // 从该地址向后找 '\0' 
      // strlen 获取从'b'到'\0'字符串长度
    
      return 0;
    }
    

    ![](file://C:\Users\Turbo\Desktop\2. 指针进阶\image\image_8.png)

  • int a[3][4] = { 0 };

 #include <stdio.h>
 int main()
 {
  int a[3][4] = { 0 };
  printf("%d\n", sizeof(a)); // 48
  // a表示整个数组 大小为 3×4×4=48(byte)

  printf("%d\n", sizeof(a[0][0])); // 4
  // a[0][0] 表述数组第一行第一列的元素 int类型 大小为4byte

  printf("%d\n", sizeof(a[0])); // 16
  // a[0]表示二维数组a的第一行,大小为4×4=16(byte)

  printf("%d\n", sizeof(a[0] + 1)); // 4/8
  // a[0]表示二维数组a的第一行
  // a[0]+1 a[0]为二维数组第一行数组首元素地址
  // +1 跳过1字节 --> 二维数组第一行数组第二个元素地址
  // 地址大小为 4/8byte

  printf("%d\n", sizeof(*(a[0] + 1))); // 4
  // *(a[0] + 1) 等价于a[0][1]
  // 表示为二维数组a第一行第二列的元素
  // int类型 大小为 4byte

  printf("%d\n", sizeof(a + 1)); // 4/8
  // a 表示数组首元素地址,二维数组的第一行数组的地址
  // a+1 跳过 1个int [4] 类型一维数组 --> 二维数组的第二行地址 
  // 大小为 4/8(byte)

  printf("%d\n", sizeof(*(a + 1))); // 16
  // a 表示二维数组第一行地址
  // a+1 跳过 1个int [4] 类型一维数组 --> 二维数组第二行地址
  // *(a+1) 解引用得到二维数组第二行
  // 大小为 4×4=16(byte)

  printf("%d\n", sizeof(&a[0] + 1)); // 4/8
  // a[0] 二维数组第一行 
  // &a[0] 二维数组第一行地址
  // &a[0]+1 跳过一个 int [4] 类型一维数组 --> 二维数组第二行地址
  // 大小为 4/8(byte)

  printf("%d\n", sizeof(*(&a[0] + 1))); // 16
  // &a[0]+1 二维数组第二行地址
  // *(&a[0]+1) 解引用得到二维数组第二行
  // 大小为 4×4=16(byte)

  printf("%d\n", sizeof(*a)); // 16
  // a 二维数组第一行地址
  // *a 二维数组第一行
  // 大小为 4×4=16(byte)

  printf("%d\n", sizeof(a[3])); // 16
  // a[3] 类型为int [4]
  // 大小为 4×4=16(byte)
  // sizeof并不会计算括号内的表达式,而是通过类型来确定大小
  // 所以a[3]并不会越界访问

  return 0;
}

2.9.2 练习1

struct Test
{
  int Num;
  char* pcName;
  short sDate;
  char cha[2];
  short sBa[4];
}*p;

// 假设p的值位0x100000 
// 已知结构体Test的大小位20字节

int main()
{
  printf("%p\n", p + 0x1); // 0x100014 结构体指针+1
  printf("%p\n", (unsigned long)p + 0x1); // 0x100001 整形+1
  printf("%p\n", (unsigned int*)p + 0x1); // 0x100004 整形指针+1 
}

2.9.3 练习2

int main()
{
  int a[5] = { 1,2,3,4,5 };
  int* ptr = (int*)(&a + 1);
  // &a 获取真个数组的地址
  // &a+1 跳过 int [5] 数组 --> 数组后第一个位置的地址
  // (int*)(&a + 1) 强制类型转换为整形指针 赋值给ptr
  printf("%d %d\n", *(a + 1), *(ptr - 1)); // 2 5
  // *(a+1) 等价于 a[1] 等于2
  // ptr-1 向前跳过 int 整形 --> 数组a最后一个元素地址
  // *(ptr-1) 数组a最后一个元素
}

2.9.4 练习3

int main()
{
  int a[4] = { 1,2,3,4 };
  int* ptr1 = (int*)(&a + 1);
  // ptr1 整形指针
  // 其中 a 表示整个数组,&a 获取整个数组的地址
  // &a+1 是数组a之后第一个位置的地址
  // (int*)(&a+1) 将数组指针强制类型转换为整形指针,并赋值给ptr1
  // ptr1[-1] 等价于 *(ptr1-1),
  // ptr1-1 将指针向前移动一个int类型的大小(4byte)
  // 即,*(ptr-1) 的值为a[3] = 4;

  int* ptr2 = (int*)((int)a + 1);
  // ptr2 整形指针
  // 其中 a 表示数组首元素的地址,(int)a 将地址强制类型转换为int
  // (int)a+1 则地址数值加1,而内存中每个地址代表1byte
  // 比如:地址是0xFF9A8C11 地址数值 +1 后变为 0xFF9A8C12
  // 即,指向原来指向的地址的后一个字节的地址
  // 所以 (int)a+1 指向如图所示的位置
  // (int*)((int)a+1) 将int型数值强制类型转换为整形指针,并赋值给ptr2
  // 所以 对ptr2解引用 *ptr2 值为 (int*)((int)a+1) 开始向后四个字节的内容

  printf("%x, %x\n", ptr1[-1], *ptr2);
  // 输出结果(输出时将数字前无用的零去除):4,2000000
  return 0;
}

![](file://C:\Users\Turbo\Desktop\2. 指针进阶\image\image_9.png)

2.9.5 练习4

int main()
{
  int a[3][2] = { (0,1), (2,3),(4,5) };
  // 注意:{}中是(),所以是逗号表达式
  // 赋值语句相当于 int a[3][2]={ 1,3,5 };

  int* p; 
  // 声明整形指针

  p = a[0]; 
  // 将数组a首元素赋值给p
  // 数组a为二维数组,其首元素为第一行的数组
  // 并赋值给整形指针p,则p的值为第一行数组的首元素的地址

  printf("%d\n", p[0]); // 1
  // p[0] 等价于 *(p+0) 对第一行数组首元素地址解引用
  // 值为第一行数组的首元素

  return 0;
}

2.9.6 练习5

int main()
{
  int a[5][5];
  int(*p)[4];
  // 定义数组指针 指向对象类型为 int [4]
  p = a;
  // 将数组 a 首元素地址赋值给 p
  printf("%p, %d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
  // &p[4][2] 等价于 *(p+4)+2
  // p+4 跳过 4×4=16 个元素
  // *(p+4) 对数组指针解引用得到一维数组 p[4]
  // *(p+4)+2 跳过2个整形型元素,即第18个元素的地址
  // 
  // &a[4][2] 等价于 *(a+4)+2
  // a+4 跳过 4×5=20 个元素
  // *(a+4) 解引用得到一维数组 a[4]
  // *(a+4)+2 跳过2个整形元素,即第22个元素的地址
  // 
  // &p[4][2] - &a[4][2] 地址相减是两个地址间元素的个数
  // &p[4][2] 第18个元素的地址
  // &a[4][2] 第22个元素的地址
  // 相减结果为 -4 
  // 在内存中十六进制表示为 FFFFFFFC
  // %p 指针形式打印为 FFFFFFFC 
  // %d 将内存中二进制再转换为整形 打印为 -4
  // FFFFFFFC, -4
  return 0;
}


// 另外:运行时警告 “int (*)[4]”和“int (*)[5]”数组的下标不同

![](file://C:\Users\Turbo\Desktop\2. 指针进阶\image\image_10.png)

2.9.7 练习6

int main()
{
  int aa[2][5] = { 1,2,3,4,5,6,7,8,9,10 };
  int* ptr1 = (int*)(&aa + 1);
  // &aa 其中 aa 表示二维数组 获取二维数组的地址
  // &aa+1 跳过整个二维数组 指向二维数组之后第一个位置
  // (int*)(&aa+1)强制类型转换为整形指针 并赋值给 ptr1

  int* ptr2 = (int*)(*(aa + 1));
  // aa 表示二维数组首元素地址--第一行地址
  // aa+1 跳过 int [5] 大小,--第二行地址 
  // *(aa+1) 得到第二行数组--第二行数组首元素地址
  // (int*)(*(aa+1)) 强制类型转换为整形指针 并赋值给 ptr2
  // (不过,本来已经是整形指针,此转换没有必要)

  printf("%d, %d\n", *(ptr1 - 1), *(ptr2 - 1));
  // *(ptr1-1) 向前跳 一个整形大小 并解引用 得到二维数组的最后一个元素
  // *(ptr2-1) 向前跳 一个整形大小 并解引用 得到二维数组第一行最后一个元素

  // 输出结果为:10, 5
  return 0;
}

2.9.8 练习7

int main()
{
  char* a[] = { "work", "at", "home" };
  char** pa = a;
  // 数组 a 中元素的类型时 char*
  // a 表示数组首元素的地址 所以a是二级指针 并赋值给pa

  pa++;
  // pa指向的类型为 char* 自增之后 跳过 1个地址的大小
  // 指向数组a第二个元素的地址

  printf("%s\n", *pa);
  // 对 pa 解引用,得到数组 a 第二个元素(是一个地址)
  // %s 打印 从该地址*pa开始向后查找'/0' 并打印字符串

  // 输出结果为 "at"
  return 0;
}


// 初始化数组a和二级指针pa之后的内存如下:

![](file://C:\Users\Turbo\Desktop\2. 指针进阶\image\image_11.png)

2.9.9 练习8

int main()
{
  char* c[] = { "ENTER", "NEW", "POINT", "FIRST" };
  char** cp[] = { c + 3, c + 2, c + 1, c };
  char*** cpp = cp;
  // 初始化之后的内存情况如果0所示
  printf("%s\n", **++cpp); // POINT
  printf("%s\n", *-- * ++cpp + 3); // ER
  printf("%s\n", *cpp[-2] + 3); // ST
  printf("%s\n", cpp[-1][-1] + 1); // EW
  return 0;
}

![图0](file://C:\Users\Turbo\Desktop\2. 指针进阶\image\image_12.png)
图0

  • 第一个printf

      printf("%s\n", **++cpp); // POINT
      // ++cpp cpp下移动如图1所示
      // 第一次解引用 得到c+2
      // 第二次解引用得到指向字符串"POINT"的字符指针
      // 则打印字符串 "POINT"
    

    ![图1](file://C:\Users\Turbo\Desktop\2. 指针进阶\image\image_13.png)
    图1

  • 第二个printf

      printf("%s\n", *-- * ++cpp + 3); // ER
      // ++cpp cpp下移如图2所示
      // 第一次解引用得到 c+1
      // *-- 得到指向"ENTER"的字符指针
      // +3 跳过3个字符,--> 字符'E'的地址
      // 则打印字符串"ER"
    

    ![图2](file://C:\Users\Turbo\Desktop\2. 指针进阶\image\image_14.png)
    图2

  • 第三个printf

      printf("%s\n", *cpp[-2] + 3); // ST
      // *cpp[-2] 相当于 **(cpp-2),cpp-2 指向如图3所示
      // *(cpp-2) 得到 c+3 
      // **(cpp-2) 得到 指向"FIRST"的字符指针
      // *cpp[-2] + 3 跳过3个字符,--> 字符'S'的地址
    

    ![图3](file://C:\Users\Turbo\Desktop\2. 指针进阶\image\image_15.png)
    图3

  • 第四个printf

      printf("%s\n", cpp[-1][-1] + 1); // EW
      // cpp[-1][-1] 相当于 *(*(cpp-1)-1)
      // cpp-1 指向如图4所示 *(cpp-1) 得到c+2
      // *(*(cpp-1)-1) 向后跳一个指针大小 得到指向"NEW"的字符指针
      // cpp[-1][-1] + 1 向后跳过1个字符,--> 字符'E'的地址
      // 则打印字符串 "EW"
    

    ![图4](file://C:\Users\Turbo\Desktop\2. 指针进阶\image\image_16.png)
    图4


下一篇: 9. C语言进阶--数据存储→