【搞透C语言指针】那年我双手插兜, 不知道指针是我的对手(一)

简介: 【搞透C语言指针】那年我双手插兜, 不知道指针是我的对手

一、字符指针

在指针的类型中我们知道有一种指针类型为字符指针 char*

一般这么使用:

int main()
{
    char ch = 'w';
    char *pc = &ch;
    *pc = 'w';
    return 0;
}

但是还有一种方式

字符串常量

int main()
{
    const char* pstr = "hello world.";//这里是把一个字符串放到pstr指针变量里了吗?
    printf("%s\n", pstr);
    return 0;
}

以上特别容易以为是把字符串 hello world.放到字符指针 pstr 里了,其实是把字符串的首字符地址h的地址存放到字符指针变量 pstr当中,如果当32位平台,4个字节大小的指针变量pstr能够装得下字符串吗?显然,不能。

需要注意的一点,观察以下图:

image.png

好了,小伙伴们理解了吗?下面来看一道来自于《剑指offer》当中的一道题:

#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
}

image.png

image.png

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


二、指针数组

指针数组就是存放指针的数组,int* arr1[10] 表示存放整型指针的数组,char* arr2[20] 表示存放字符指针的数组。


应用:

int main()
{
    /* char* arr[] = { "abcdef", "hehe", "qwer" };
  int i = 0;
  for (i = 0; i < 3; i++)
  {
    printf("%s\n", arr[i]);
  } */
  int arr1[] = { 1,2,3,4,5 };
  int arr2[] = { 2,3,4,5,6 };
  int arr3[] = { 3,4,5,6,7 };
  //arr[i] == *(arr+i)
  //arr是一个存放整型指针的数组
  int* arr[] = { 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("%d ", *(arr[i]+j));
    }
    printf("\n");
  }
  return 0;
}

三、数组指针

1.数组指针的定义

整形指针: int * pint; 能够指向整形数据的指针。

浮点型指针: float * pf; 能够指向浮点型数据的指针。

那数组指针应该是:能够指向数组的指针。

//下面代码哪个是数组指针?
int main()
{
    int *p1[10];
    int (*p2)[10];
    //p1, p2分别是什么?
    return 0;
}

解释:int (*p)[10]; //解释:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个 指针,指向一个数组,叫数组指针。


//这里要注意:[ ]的优先级要高于*号的,所以必须加上()来保证p先和*结合。


2.数组名和&数组名

我们知道,数组名绝大部分情况下是数组首元素的地址。


但是有两种特殊情况:


1. sizeof(数组名) - sizeof内部单独放一个数组名的时候,数组名表示的整个数组,计算得到的是数组的总大小。

2. &arr - 这里的数组名表示整个数组,取出的是整个数组的地址,从地址值的角度来讲和数组首元素的地址是一样的,但是意义不一样。


观察以下代码

#include<stdio.h>
int main()
{
  int arr[10] = { 0 };
  //printf("%d\n", sizeof(arr));
  printf("%p\n", arr);//其类型是int * 
  printf("%p\n", arr+1);//4
  printf("%p\n", &arr[0]);//int* 
  printf("%p\n", &arr[0]+1);//4
  printf("%p\n", &arr);//其类型是int(*)[10]
  printf("%p\n", &arr+1);
  int (*p)[10] = &arr;
  //p是一个数组指针
  //int(*)[10] 
  return 0;
}

image.png

解释:根据上面的代码我们发现,其实&arr和arr,虽然值是一样的,但是意义应该不一样的。 实际上: &arr 表示的是数组的地址,而不是数组首元素的地址。(细细体会一下)


本例中 arr 和 &arr[0] 表示的都是数组首元素的地址,arr + 1和&arr[0] +1 需要看数组首元素地址的类型是int *,所以加一跳过一个整型的大小。而&arr 的类型是: int(*)[10] ,是一种数组指针类型 数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40。


3.数组指针的使用

那数组指针是怎么使用的呢? 既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。

#include<stdio.h>
int main()
{
  int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
  int sz = sizeof(arr) / sizeof(arr[0]);
  int (* p)[10] = &arr;
  int i = 0;
  //p  --- &arr
  //*p --- *&arr
  //*p --- arr
  for (i = 0; i < sz; i++)
  {
    printf("%d ", *((*p) + i));
  }
  //虽然对,但是有点别扭,不推荐 
  //一般还是将数组名存放到一个整型指针中再访问使用
  //int* p = arr;
  //for (i = 0; i < sz; i++)
  //{
  //  printf("%d ", *(p + i));
  //}
  return 0;
}

一个数组指针的使用(二维数组传参):

#include<stdio.h>
void print(int(*arr)[5], int row, int col)
{
  int i = 0;
  for (i = 0; i < 3; i++)
  {
    int j = 0;
    for (j = 0; j < 5; j++)
    {
      //*(arr + 0) == arr[0] == &arr[0][0] 为第一行数组首元素的地址……以此类推
      printf("%d ", *(*(arr + i) + j));
      //printf("%d ", arr[i][j]);//和以上代码表示一样
    }
    printf("\n");
  }
}
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;
}

辨别一下以下代码

int arr[5];   //整型数组
int *parr1[10]; //指针数组
int (*parr2)[10]; //数组指针
int (*parr3[10])[5]; //存放数组指针的数组

四. 数组传参和指针传参

在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?


1.一维数组传参

对于函数test(),一维整型数组传参,形参部分可以是数组的形式接收,也可以是指针的形式接收。


对于函数test2(),指针数组传参,形参部分可以是指针数组的形式接收,也可以是二级指针的形式接收。

#include <stdio.h>
void test(int arr[])// ture
{}
void test(int arr[10])// ture
{}
void test(int* arr)// true
{}
void test2(int* arr[20])// true 
{} 
void test2(int** arr)// true 
{}
int main()
{
  int arr[10] = { 0 };
  int* arr2[20] = { 0 };
  test(arr);
  test2(arr2);
}

2.二维数组传参

如果是数组,行可以省略,但是列不能省略。如果是指针,传过去的是第一行的地址,形参部分接收的就应该是数组指针。

void test(int arr[3][5])//ture
{}
void test(int arr[][])//false
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int arr[][5])//ture
{}
void test(int *arr)//false
{}
void test(int* arr[5])//false
{}
void test(int (*arr)[5])//ture
{}
void test(int **arr)//false
{}
int main()
{
 int arr[3][5] = {0};
 test(arr);
}

3.一级指针传参

#include <stdio.h>
void print(int *p, int sz)
{
     int i = 0;
     for(i=0; i < sz; i++)
     {
     printf("%d\n", *(p+i));
     }
}
int main()
{
     int arr[10] = {1,2,3,4,5,6,7,8,9};
     int *p = arr;
     int sz = sizeof(arr)/sizeof(arr[0]);
     //一级指针p,传给函数
     print(p, sz);
     return 0;
}

思考: 当一个函数的参数部分为一级指针的时候,函数能接收什么参数?

 void test1(int *p)

 {}

//test1函数能接收什么参数?

  1. 一级指针
  2. 整型变量的地址
  3. 整型的一维数组名

4.二级指针传参

#include <stdio.h>
void test(int** ptr)
{
     printf("num = %d\n", **ptr); 
}
int main()
{
     int n = 10;
     int*p = &n;
     int **pp = &p;
     test(pp);
     test(&p);
     return 0;
}

思考:当函数的参数为二级指针的时候,可以接收什么参数?

 void test2(int **p)

 {}

//test2函数能接收什么参数?

  1. 二级指针变量
  2. 一级指针变量的地址
  3. 整型的指针数组的数组名

五. 函数指针

1.函数指针

首先看一段代码

#include <stdio.h>
void test()
{
     printf("hehe\n");
}
int main()
{
     printf("%p\n", test);
     printf("%p\n", &test);
     return 0;
}

输出的是两个地址,这两个地址是 test 函数的地址。 那我们的函数的地址要想保存起来,怎么保存?


void (*pfun)();


解释:pfun就是一个函数指针。pfun先和*结合,说明pfun是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void。


⚠️注意:函数名和&函数名是一样的,都表示的是函数的地址。


2.利用函数指针

int Add(int x, int y)
{
  return x + y;
}
#include<stdio.h>
int main()
{
  //函数指针变量
  int (*pfun)(int, int) = Add;
  //调用函数
  int ret1 = (*pfun)(3, 5); //函数的地址存放到函数指针当中
  printf("%d\n", ret1);
  //我们平常写的函数调用
  int ret2 = Add(4, 7);
  printf("%d\n", ret2);
  //那么就有以下表示方法
  int ret3 = pfun(8, 9);
  //所以我们知道 pfun和*pfun是一样的,这里的*只是帮助理解,加多少个*都只是"摆设"
  printf("%d\n", ret3);
  return 0;
}

那么假设一个函数是这么写的 char* test(int c,float* pf),那么这个函数指针该如何表示的呢?


char*(*pt)(int,float*) = test;


讲到这里,有人会质疑,以上的应用不相当于脱裤子放p,多此一举吗?那函数指针到底是如何应用的呢,答案当然是有的,在函数需要调用另一个函数的时候,那么就需要传入函数的地址了,在形参部分利用函数指针来接收,就需要具体用到函数指针了,这里就不多细讲,感兴趣的同学可以写一段代码,深切体会一下。


3.阅读两段有趣的代码

两段代码源于《C陷阱与缺陷》这本书

  (*(void (*)())0)(); //code1
  void (*signal(int , void(*)(int)))(int); //code2

解释code1:对于code1代码,(*(void (*)())0)(),是将0强制类型转换为void(*)()类型的函数指针,这就意味着0地址处放置着一个函数,该函数没有参数,返回类型为void,然后调用0地址处的这个函数。


解释code2:对于code2代码,void (* signal(int , void(*)(int)) )(int),这个代码是一个函数的声明。


分析:首先我们可以确定signal这个符号按优先级来说,先和右边的括号结合,里面放了一个参数 int,另一个参数void(*)(int),由此我们可以判断,signal是一个函数,它的一个参数为整型变量,另一个参数为一个函数指针,该函数指针指向的是一个参数为int,返回类型为void的函数。那么signal函数还缺什么呢?缺返回类型,绿色的加粗部分的void(*)(int)表示的就是signal的返回类型。


其实大概可以理解为:// void (*) (int) signal(int , void(*)(int)),但是语法格式并不支持,于是signal只能和*结合在一起。


那么我们可以用typedef这个关键字来简化这段代码,使代码变得容易理解。

typedef int* ptr_t; 
typedef void(*pf_t)(int);//将void(*)(int) 类型重命名为pf_t
pf_t signal(int, pf_t); //将 void(*signal(int,void(*)(int)))(int)的简化


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