【C语言进阶】指针进阶的详细讲解--(数组指针、指针数组、函数指针、函数指针数组、指向函数指针数组)

简介: 【C语言进阶】指针进阶的详细讲解--(数组指针、指针数组、函数指针、函数指针数组、指向函数指针数组)

1.字符指针

1.1字符指针的使用:

形式一:

int main()
{
  char ch = 'w';
  char* pc = &ch;
  *pc = 'w';
  printf("%c\n", *pc);
}

形式二:

int main()
{
  const char* pstr = "hello bit.";
  printf("%s\n", *pstr);
  return 0;
}

第二种用法的本质是把常量字符串 ’ hello bit. ’ 的首字符 ’ h ’ 的地址放到了pstr中,不是把一个字符串放到pstr指针变量里。

1.2.常量字符串:

注意,常量字符串作为常量,它们不可被修改

int main()
{
    char* p = "abcdef";
    //使指针指向常量区的常量字符串abcdef
    *p = 'A';
    //按照我们的理解,*p中存放的是首元素a的地址,我们尝试改变它
    //我们运行起来发现无法完成编译运行,程序会直接卡住
    //于是我们可以得出,当使用常量字符串时,常量字符串abcdef不可修改
    return 0;
}

这些常量字符串中的常量字符,是原本就储存在常量区(只读数据区)的一些常量,不同于需要我们输入的字符变量。所以当使用字符指针直接指向常量字符串时,可以直接进行调用但无法对其进行修改。


当出现这种错误时,程序往往是可以正常编译运行但会卡住而得不出结果,会给我们的代码书写造成很大的困扰。为了尽可能的避免这种情况的发生,我们通常在指向常量字符串时,使用 const 对指针变量进行修饰,进行这样的书写操作之后,当我们试图对常量字符串进行改动时,将会直接报错而无法进行编译运行, 便于我们找到问题的所在

1.3.相关面试题:

int main()
{
    char str1[] = "hello bit.";
    char str2[] = "hello bit.";
    //创建两个数组、两个字符型指针变量
    const char* str3 = "hello bit.";
    const char* str4 = "hello bit.";
//判断数组内容是否相同:
    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;
}

这道面试题中定义了两个字符型数组和两个字符型指针变量,将两个字符型数组进行对比,又将两个字符型指针变量尽行了对比,目的是为了验证它们在存储时内存的开辟。


这里 str3 和 str4 指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域(在我们的内存中存在着常量区用于存放一些常量),当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4相同

2.指针数组:

指针数组的本质是数组,是用于存放指针变量的数组:

int main()
{
    //创建变量:
    int a = 1;
    int b = 2;
    int c = 3;
 //创建指针数组,用于存放指针(即变量的地址):
    int* arr[3] = { &a,&b,&c }; 
//打印验证内容:
    int i = 0;
    for (i = 0; i < 3; i++)
    {
        printf("%p\n", arr[i]);
    }
//也可以通过指针数组(存放的是各个变量的地址)来访问变量内容:
    for (i = 0; i < 3; i++)
    {
        printf("%d\n", *(arr[i]));
    }
    return 0;
}

更常用的却是另外一种情况:

int main()
{
 //创建数组:
    int arr1[3] = { 1,2,3 };
    int arr2[3] = { 4,5,6 };
    int arr3[3] = { 7,8,9 };
  int* parr[3] = { arr1,arr2,arr3 };
    //我们都知道,数组名即为数组内首元素地址
    //即此处的数组名为地址,故可以存放在指针变量内
//验证指针数组内存放数据:
    int i = 0;
    for (i = 0; i < 3; i++)
    {
        printf("整型数组arr%d[3]首元素地址为:%p\n", i + 1, parr[i]);
    }
    printf("\n");
 //通过指针数组访问整型数组内的数据:
    for (i = 0; i < 3; i++)
    {
        printf("arr%d[3]中存储的数据为:", i + 1);
        int j = 0;
        for (j = 0; j < 3; j++)
        {
            printf("%d ", *(parr[i] + j));
        }
        printf("\n");
    }
    return 0;
}

3.数组指针:

3.1 数组指针的定义:

数组指针的本质是指针。不同于其他指针,数组指针指向的不是地址而是数组

int main()
{
    int* p1[10];
    //p1是数组,指针数组
  int(*p2)[10];
    //p2是指针。数组指针
    return 0;
}

上面的示例中,p2先和*进行结合,说明p2是一个指针变量,再指向大小为10的整型数组。即p2是一个指向数组的指针,叫做数组指针。简单来说,数组指针就是专门用来存放一个数组的地址的指针。

3.2.&数组名VS数组名:

我们都知道,除一些特殊情况外,大多数的数组名都表示数组首元素的地址,那么既然数组名已经是地址了,&+数组名的组合又代表着什么呢

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

我们如果通过打印,会发现 arr 的打印结果与 &arr 相同:

那它们真的是一回事吗?

我们将上面这段代码进行改写:

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

我们使 arr 与 &arr 同时向后走一步,这时我们看到了不一样的结果:

实际上,&arr 表示的是整个数组的地址,而 arr 仅表示其中首元素的地址,这就导致了我们在使它们在向后走的时候,它们向后走的步幅不同,&arr 跨过了整个数组的长度而 arr 仅仅只跳过了一个数据元素。

3.3.数组指针的使用:

在上面的介绍中,我们看到了数组指针的最基础用法:

int arr[10] = { 0 };
int (*p2)[10] = &arr;

但我们却很少会这样去使用它。更多的是在我们进行函数调用,进行数组传参时,可以通过数组指针来进行接收:

void print_arr1(int arr[3][5], int row, int col)
{
    int i = 0;
    for (i = 0; i < row; i++)
    {
        int j = 0;
        for (j = 0; j < col; j++)
        {
            printf("%02d ", arr[i][j]);
        }
        printf("\n");
    }
}
//使用数组指针来进行接收:
void print_arr2(int(*arr)[5], int row, int col)
{
    int i = 0;
    for (i = 0; i < row; i++)
    {
        int j = 0;
        for (j = 0; j < col; j++)
        {
            printf("%02d ", arr[i][j]);
        }
        printf("\n");
    }
}
int main()
{
    int arr[3][5] = { 1,2,3,4,5,6,7,8,9,10 };
    print_arr1(arr, 3, 5);
    //数组名arr,表示首元素的地址
    //但是二维数组的首元素是二维数组的第一行,所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
    printf("\n");
   //可以使用数组指针来进行接收:
    print_arr2(arr, 3, 5);
    return 0;
}

4.数组参数:

4.1一维数组传参:

我们都知道,在对数组进行传参时并不会真实的在内存中创建临时数组,数组名的意义是作为数组内首元素的地址,因此当函数参数为数组名时,实际上传递的是数组中首元素的地址,于是我们可以发现下面三种传参方式都是可行的

//方式1:标准传参方式
//完整的传递数组内容
void test1(int arr[3])
{
  int i = 0;
  for (i = 0; i < 3; i++)
  {
    printf("%d ", arr[i]);
  }
  printf("\n");
}
//方式2:省略数组大小
//形参部分的数组大小可以省略
void test2(int arr[])
{
  int i = 0;
  for (i = 0; i < 3; i++)
  {
    printf("%d ", arr[i]);
  }
  printf("\n");
}
//方式3:扩大形参数组大小
//实际仍为传递首元素地址,故可行,但不建议
void test3(int arr[100])
{
  int i = 0;
  for (i = 0; i < 3; i++)
  {
    printf("%d ", arr[i]);
  }
  printf("\n");
}
int main()
{
  int arr[3] = { 1,2,3 };
 //调用函数:
  test1(arr);
  test2(arr);
  test3(arr);
  return 0;
}

这三种将形参写作数组形式的方式都是可行的,但是为了避免出现错误,推荐大家尽可能的使用第一种方式,其次是第二种方式。第三种方式虽然也可以运行,但有可能会出现难以预料的错误,不建议使用。

同时,以上三种将形式参数写成数组形式的写法,也可以改写为使用指针做形式参数的形

//使用指针作为形式参数:
//一级指针:
void test1(int* p)
{
  int i;
  for (i = 0; i < 3; i++)
  {
    printf("%d ", *(p + i));
  }
  printf("\n");
}
//二级指针_形式1:
void test2(int** p)
{
  int i;
  for (i = 0; i < 3; i++)
  {
    printf("%d ", *(p + i));
  }
  printf("\n");
}
//二级指针_形式2:
void test3(int** p)
{
  int i = 0;
  for (i = 0; i < 3; i++)
  {
    printf("%d ", **(p + i));
  }
}
int main()
{
  int arr[3] = { 1,2,3 };
    int* parr = &arr;
   int arrA[3] = { 1,2,3 };
  int arrB[3] = { 4,5,6 };
  int arrC[3] = { 7,8,9 };
  int* arrD[3] = { &arrA,&arrA,&arrC };
  //调用函数:
  test1(arr);
    test2(parr);
  test3(arrD);
  return 0;
}

并且同理,使用指针作为形式参数的另外两种形式也是可以(但最后一种方式同样不建议)的

void test(int* p[3])
{
    ...
}
 void test(int* p[100])
//可以但不建议使用
{
    ...
}

4.2 二维数组传参:

我们在前面初阶的部分学习二维数组传参时就知道了,二维数组在进行传参时可以不知道有多好行,但必须知道有多少列,这样计算机才知道应该在何时进行换行。如此只要知道了什么时候进行换行,对于行数就不需要再进行强制要求了,所以在数组进行传参时,允许写成以下三种形式:

//二维数组传参:
//方式1:标准传参
void test1(int arr[3][3])
{
  int i = 0;
  for (i = 0; i < 3; i++)
  {
    int j = 0;
    for (j = 0; j < 3; j++)
    {
      printf("arr[%d][%d] = %d ", i, j, arr[i][j]);
    }
    printf("\n");
  }
}
//方式2:行数可以省略
void test2(int arr[][3])
{
  int i = 0;
  for (i = 0; i < 3; i++)
  {
    int j = 0;
    for (j = 0; j < 3; j++)
    {
      printf("arr[%d][%d] = %d ", i, j, arr[i][j]);
    }
    printf("\n");
  }
}
//方式3:行数可以超出原数组上限
void test3(int arr[100][3])
{
  int i = 0;
  for (i = 0; i < 3; i++)
  {
    int j = 0;
    for (j = 0; j < 3; j++)
    {
      printf("arr[%d][%d] = %d ", i, j, arr[i][j]);
    }
    printf("\n");
  }
}
int main()
{
  int arr[3][3] = { {1,2,3},{4,5,6},{7,8,9} };
 //函数调用:
  test1(arr);
  test2(arr);
  test3(arr);
  return 0;
}

同样的,二维数组除了可以使用数组作为函数的参数以外,也可以使用指针作为函数的参数进行传参,区别于一维数组,二维数组在传参时传递的不是首元素的地址而是首行元素的地址

void test(int(*p)[3])
//此处的数组大小3为列数
{
  int i = 0;
  for (i = 0; i < 3; i++)
  {
    int j = 0;
    for (j = 0; j < 3; j++)
    {
      printf("%d ", (*p + i * 3)[j]);
            //这里注意p+(i*3)是因为i为行号,跳过行时需要跳过i*3个数据元素
    }
    printf("\n");
  }
}
int main()
{
  int arr[3][3] = { {1,2,3},{4,5,6},{7,8,9} };
 test(arr);
return 0;
}

但是要格外注意,二维数组与一维数组不同,不可以使用二级指针进行传参。其原理是,二级指针的作用是用于存储一级指针的的地址,而传递过来的参数是二维数组第一行(这里可以简单理解为一个一维数组,但本质上不是)的地址,无法使用二级指针进行存储。

相关文章
|
22天前
|
C语言
【c语言】指针就该这么学(1)
本文详细介绍了C语言中的指针概念及其基本操作。首先通过生活中的例子解释了指针的概念,即内存地址。接着,文章逐步讲解了指针变量的定义、取地址操作符`&`、解引用操作符`*`、指针变量的大小以及不同类型的指针变量的意义。此外,还介绍了`const`修饰符在指针中的应用,指针的运算(包括指针加减整数、指针相减和指针的大小比较),以及野指针的概念和如何规避野指针。最后,通过具体的代码示例帮助读者更好地理解和掌握指针的使用方法。
44 0
|
24天前
|
存储 编译器 C语言
【c语言】数组
本文介绍了数组的基本概念及一维和二维数组的创建、初始化、使用方法及其在内存中的存储形式。一维数组通过下标访问元素,支持初始化和动态输入输出。二维数组则通过行和列的下标访问元素,同样支持初始化和动态输入输出。此外,还简要介绍了C99标准中的变长数组,允许在运行时根据变量创建数组,但不能初始化。
35 6
|
21天前
|
C语言
【c语言】指针就该这么学(3)
本文介绍了C语言中的函数指针、typedef关键字及函数指针数组的概念与应用。首先讲解了函数指针的创建与使用,接着通过typedef简化复杂类型定义,最后探讨了函数指针数组及其在转移表中的应用,通过实例展示了如何利用这些特性实现更简洁高效的代码。
15 2
|
22天前
|
C语言
如何避免 C 语言中的野指针问题?
在C语言中,野指针是指向未知内存地址的指针,可能引发程序崩溃或数据损坏。避免野指针的方法包括:初始化指针为NULL、使用完毕后将指针置为NULL、检查指针是否为空以及合理管理动态分配的内存。
|
22天前
|
C语言
C语言:哪些情况下会出现野指针
C语言中,野指针是指指向未知地址的指针,通常由以下情况产生:1) 指针被声明但未初始化;2) 指针指向的内存已被释放或重新分配;3) 指针指向局部变量,而该变量已超出作用域。使用野指针可能导致程序崩溃或不可预测的行为。
|
28天前
|
存储 C语言
C语言32位或64位平台下指针的大小
在32位平台上,C语言中指针的大小通常为4字节;而在64位平台上,指针的大小通常为8字节。这反映了不同平台对内存地址空间的不同处理方式。
|
28天前
|
存储 算法 C语言
C语言:什么是指针数组,它有什么用
指针数组是C语言中一种特殊的数据结构,每个元素都是一个指针。它用于存储多个内存地址,方便对多个变量或数组进行操作,常用于字符串处理、动态内存分配等场景。
|
28天前
|
存储 C语言
C语言指针与指针变量的区别指针
指针是C语言中的重要概念,用于存储内存地址。指针变量是一种特殊的变量,用于存放其他变量的内存地址,通过指针可以间接访问和修改该变量的值。指针与指针变量的主要区别在于:指针是一个泛指的概念,而指针变量是具体的实现形式。
|
29天前
|
C语言
C语言指针(3)
C语言指针(3)
11 1
|
21天前
|
编译器 C语言
【c语言】指针就该这么学(2)
本文详细介绍了指针与数组的关系,包括指针访问数组、一维数组传参、二级指针、指针数组和数组指针等内容。通过具体代码示例,解释了数组名作为首元素地址的用法,以及如何使用指针数组模拟二维数组和传递二维数组。文章还强调了数组指针与指针数组的区别,并通过调试窗口展示了不同类型指针的差异。最后,总结了指针在数组操作中的重要性和应用场景。
16 0