【搞透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)的简化


目录
相关文章
|
7天前
|
安全 C语言
【C语言】如何规避野指针
【C语言】如何规避野指针
16 0
|
8天前
|
C语言
C语言:数组和指针笔试题解析(包括一些容易混淆的指针题目)
C语言:数组和指针笔试题解析(包括一些容易混淆的指针题目)
|
2天前
|
存储 程序员 C语言
【C 言专栏】C 语言指针的深度解析
【4月更文挑战第30天】C 语言中的指针是程序设计的关键,它如同一把钥匙,提供直接内存操作的途径。指针是存储其他变量地址的变量,通过声明如`int *ptr`来使用。它们在动态内存分配、函数参数传递及数组操作中发挥重要作用。然而,误用指针可能导致错误,如空指针引用和内存泄漏。理解指针的运算、与数组和函数的关系,以及在结构体中的应用,是成为熟练 C 语言程序员的必经之路。虽然挑战重重,但掌握指针将增强编程效率和灵活性。不断实践和学习,我们将驾驭指针,探索更广阔的编程世界。
|
3天前
|
算法 搜索推荐 程序员
C语言中的函数指针和回调函数
C语言中的函数指针和回调函数
9 2
|
7天前
|
存储 编译器 C语言
【C语言】初步解决指针疑惑
【C语言】初步解决指针疑惑
6 0
|
8天前
|
存储 C语言
指针深入解析(C语言基础)带你走进指针,了解指针
指针深入解析(C语言基础)带你走进指针,了解指针
|
8天前
|
C语言 C++
C语言:指针运算笔试题解析(包括令人费解的指针题目)
C语言:指针运算笔试题解析(包括令人费解的指针题目)
|
9天前
|
存储 安全 编译器
C语言怎样定义指针变量
C语言怎样定义指针变量
7 0
|
10天前
|
安全 C语言
指针与字符串:C语言中的深入探索
指针与字符串:C语言中的深入探索
15 0
|
22天前
|
存储 程序员 编译器
爱上C语言:指针很难?来来来,看看这篇(基础篇)
爱上C语言:指针很难?来来来,看看这篇(基础篇)