详解C指针+小型计算器项目

简介: 详解C指针+小型计算器项目

1.指针

1. 指针是什么

我们口头说的指针就是地址,

指针变量是变量,是用来储存地址的。

2. 指针和指针类型

#include <stdio.h>
int main()
{
  int a = 0;
  int* p = &a;
  *p = 20;
  printf("%d\n",a);
  printf("%d\n",*p);
  return 0;
}

对于上面的代码

这里的p是指针变量——是用来存放地址的变量;

可以这样理解从*可以看出p是个指针变量,p指向的内容是int类型的。

*p = 20,此处的 * 是解引用操作符。

&为取地址操作符

指针的类型是根据原来值的类型来确定用什么类型的指针。如:char类型,那就用char* 。

去掉指针变量名剩下的就是指针的类型


关于指针类型的声明有人可能会问用不同的类型声明可以吗?当然可以,但是会出现一些问题。

不同的指针类型决定了指针解引用所移动的步长

如:char类型的,那么它+1就指向下一个字节(向后走一步)

int 类型的,那么它+1就指向到第4个字节处。(向后走4步)

指针的类型代表它所能访问几个字节大小的空间

看下面这个代码

int main()
{
  int a = 0x11223344;
  char* p = (char*)&a;
  *p=0;
  printf("%x", a);
  return 0;
}

此处的&a应该是int*类型的,但强转成char*类型的指针之后,对p进行解引用赋值成0只能改变一个字节,所以输出的结果是11223300

看计算机输出的结果

指针的大小由电脑的平台所决定的而不是由指针类型决定的。

如果平台上是32位的,那就是4个字节的大小;64位的平台就是8个字节的大小。

看下面这个代码可以看出来:

#include <stdio.h>
int main()
{
  printf("%d\n", sizeof(int*));
  printf("%d\n", sizeof(char*));
  printf("%d\n", sizeof(long*));
  printf("%d\n", sizeof(float*));
  return 0;
}

在我的32位平台上上面的输出结果都是4。

3. 野指针

野指针就是指针访问了位置不可知的地方,造成非法访问

🥇1. 指针没有初始化

比如这样:

  int* i;
  这样写是不对的,指针i指向的空间不知道
  可以这样写:
  int* i=NULL;

🥈2. 指针越界访问

看下面这个代码:

  int arr[5]={1,2,3,4,5};
  int* p=arr;
  int i=0;
  for(i=0;i<=5;i++)
  {
    *(p++)=i;
  }

该数组里面有5个元素,当i为5时,p访问的空间超过了数组的范围

造成越界访问,这时p就是野指针.

🥉3. 指针指向的空间被释放

int* f(int* a,int* b)
{
  int c = 0;
  c = *a + *b;
  return &c;

}
int main()
{
  int a = 10, b = 20;
  int* p=f(&a, &b);
  printf("%d\n", *p);
  return 0;
}

在调用函数的过程中开辟的空间,出了这个了函数,则开辟的空间返还给操作系统,导致p指针接受的地址被释放,次数p为野指针。

虽然在vs 上面依然可以输出,但是确实是错误的。需要注意。

还有这种,和上面的差不多:

int main()
{
  int* p = (int*)malloc(4 * sizeof(int));
  int i = 0;
  if (p != NULL)
  {
    for (i = 0; i < 4; i++)
    {
      *(p + i) = i;
    }
  }
  free(p);
  for (i = 0; i < 4; i++)
  {
    printf("%d ", *(p + i));
  }
  return 0;
}

在p指向的空间被释放后,它所指向的空间被返还给操作系统

再次对它解引用进行访问就造成非法访问。

通常在释放空间后加上p=NULL

4. 指针运算

  • 🚗 指针加减整数

指针加一个整数表示该指针向后走几个该指针所指类型字节的个数。

比如:

int main()
{
  int arr[] = { 1,2,3,4,5,6,7,8 };
  int p=*(arr + 4);
  return 0;
}

p就是5,因为数组的每个元素都是int类型的,所以arr+4表示arr向后走4个int类型的字节数,即访问数组第5个元素。


指针减整数也是同理,注意不要越界访问

  • 🚓指针相减

指针相减得到的是两个指针之间的元素个数,不要认为是地址相减的结果,不要问为什么,这是这么规定的。

看下面的代码帮助你理解:

int main()
{
  int arr[] = { 1,2,3,4,5,6,7,8 };
  int* p1 = arr;
  int* p2 = arr + 4;
  printf("%d\n", p2 - p1);
  return 0;
}

输出的结果是4哦😁

  • 🚕指针做比较

指针也是有大小的,就比如有高地址与低地址这么一说

c语言标准规定

允许指针与指针指向数组的最后一个元素后面的那个地址进行比较,不允许和指针指向数组第一个元素前面的那个地址进行比较。

再次说明不要问为什么,规定就是规定

例子:

int main()
{
  int arr[] = { 1,2,3,4,5,6,7,8 };
  int* p;
  for (p=arr;p<=&arr[7];p++)
  {
    ;
  }
  上面这个可以,下面这个不可以,原因上面说的非常清楚
  for (p = &arr[7]; p >= arr; p--)
  {
    ;
  }
  return 0;
}

5. 二级指针

指针的地址怎么储存呢?——用二级指针变量进行储存

  int i=0;
  int* p=&i;
  int** p1=&p;

指针的指针就是二级指针,用于储存一级指针的地址。要改变i的值p1要解引用2次才可以改变

2. 字符指针

能够指向字符数据的指针

形如这样的char* p

这里就讲比较难的地方吧!看下面这个代码

void pd(char* s1,char* s2)
{
  if (s1 == s2)
    printf("相等\n");
  else
    printf("不相等\n");
}
int main()
{
  char arr1[] = "abcef";
  char arr2[] = "abcef";
  pd(arr1,arr2);
  char* p1 = "abcef";
  char* p2 = "abcef";
  pd(p1,p2);
  return 0;
}

这里的arr1和arr2,p1和p2相等吗?看结果

arr1与arr2不相等,p1和p2相等。在数组储存数据时,即使储存的字符串相等也开辟不同的空间。但是p1,p2可不能储存"abcef"这样的常量字符串,它们只是储存了首字符的地址,所以p1和p2相等.并且p1,p2所指向内存的数据不能更改,因为初始化的常量字符串是不能更改的,可以这样写

  const char* p1 = "abcef";
  const char* p2 = "abcef";

3. 数组指针

数组指针本质上是指针,例如:

  int (*p)[5]
  *说明p是一个指针,指针指向一个数组,
  数组中有5个元素,每个元素是int类型

这里的p的类型为 int (*)[5] 类型。

这样使用,例子:

void f(int (*p)[5],int n)
{
  int i, j;
  for (i=0;i<5;i++)
  {
    for (j=0;j<5;j++)
    {
      *(*(p + i) + j) = 1;
    }
  }
}
int main()
{
  int arr[5][5]={1,2,3,4,5,6,7};
  f(arr,5);
  return 0;
}
  • 这里的二维数组名表示第一行数组的地址,数组的地址要用数组指针进行接收。

下面这个对应着内存布局:

4. 指针数组

指针数组本质上是数组。例如:

int *p[5]
根据运算符的结合性可知,
p先与[]结合,说明p是一个数组,数组里面有5个元素,
每个元素是int*类型,也就是指针指向int类型

给个简单的例子:

看代码:简单

int main()
{
  int arr1[] = { 1,2,3 };
  int arr2[] = { 4,5,6 };
  int arr3[] = { 7,8,9 };
  int* p[] = { arr1,arr2,arr3 };
  int i = 0, j = 0;
  for (i = 0; i < 3; i++)
  {
    for (j = 0; j < 3; j++)
    {
      printf("%d ", p[i][j]);
    }
    printf("\n");
  }
  return 0;
}

5.数组传参和指针传参

🥳1️⃣数组名表示的含义

  • 一维数组名
    一维数组名表示数组首元素地址
  • 二维数组名
    二维数组名表示首行数组的地址
    每一行的数组都有自己的名字,对与下面的代码arr2[0]为第一行数组的名字

sizeof(数组名)求的是整个数组的大小。

&数组名取出的是整个数组的地址

例子:看代码

int main()
{
  int arr1[] = { 1,2,3,4,5,6 };
  int arr2[][3] = { 1,2,3,4,5,6 };
  printf("%d\n",sizeof(arr1));求的整个数组的大小,单位字节24
  printf("%d\n", sizeof(arr2));求的整个数组的大小24
  printf("%d\n\n", sizeof(arr2[0]));求的是第一行数组的大小12

  printf("%p\n", arr1);首元素的地址
  printf("%p\n", arr1+1);第2个元素的大小
  printf("%p\n", &arr1);这个元素的大小
  printf("%p\n\n", &arr1+1);越过整个数组后的地址

  printf("%p\n", arr2);首行的地址
  printf("%p\n", &arr2 + 1);越过这个数组后的地址
  printf("%p\n", arr2[0]);首行首元素的地址
  printf("%p\n", &arr2[0] + 1);第二行数组的首元素的地址
  return 0;
}

  • 运行的结果

🥳2️⃣数组传参

  • 一维数组传参
f(int arr[])
{}
f(int arr[3])
{}
f(int* arr)
{}
上面这三种都可以,但是最后一种比较好,它能反应出数组名字为数组首元素地址,
像第1与第2种里面[]里面有没有数值的没有问题。
f1(int* arr1[])
{}
f1(int* arr1[3])
{}
上面这两种大家应该可以理解
f1(int** arr1)
{}
arr是数组名,表示首元素地址,每个元素是指针,即指针的地址要用二级指针接受
int main()
{
  int arr[3] = { 1,2,3 };
  int* arr1[3];
  f(arr);
  f1(arr1);
  return 0;
}
f(int

对于一维数组传参,上面的几种都正确。

  • 二维数组传参
f(int p[2][2] )
{}
f(int p[][2] )
{}
上面这两种都可以,也都可以理解,前一个数值可以省略,
但是第二个[]的数值不能去掉.
f(int(*p)[2] )
{}
二维数组名表示首行数组的地址,要用数组指针进行接收
int main()
{
  int arr[2][2];
  f(arr);
  return 0;
}

🥳3️⃣指针传参

  • 一级指针传参
f(int* p)
{}
int main()
{
  int a = 0;
  int* p = &a;
  f(p);
  return 0;
}

一级指针传参用一级指针进行接收

思考形参为一级指针的时候,实参可以为什么样的类型

1️⃣实参可以为一维数组的数组名

2️⃣可以为一级指针

  • 二级指针传参
f(int** p1)
{}
int main()
{
  int a = 0;
  int* p = &a;
  int** p1 = &p;
  f(p1);
  return 0;
}

二级指针传参用二级指针进行接收

思考形参为一级指针的时候,实参可以为什么样的类型

1️⃣实参可以为指针数组

2️⃣可以为二级指针

3️⃣可以为一级指针的地址

6. 函数指针

引例:

int add(int x,int y)
{
  return x + y;
}
int main()
{
  &add;
  return 0;
}

对于函数的地址应该怎么储存呢?那就需要用函数指针类型进行接收。

对于上面的代码,我们可以这样写

int (*p)(int,int)

对于上面这个可以这么理解:

* 告诉我们P是个指针,从后面的小括号可以看出是个函数,函数的参数有两个,都是int 类型的,返回类型是int

下面这样进行使用:

  int(*p)(int,int)=&add;
  (*p)(1, 2);
  (**p)(1, 2);
  p(1, 2);
  add(1, 2);

我们发现上面这3种都正确,说明p变量前面的星号没有任何用处,p就相当与add。

看下面的这个代码可以更好的理解:我们发现打印的结果相同

printf("%p\n",add);
  printf("%p\n", &add);

  • 看着两个表示的是什么:
  • (*(void (*)())0)()
  • void (*signal(int , void(*)(int)))(int)
    我们一点一点拆分看
(*(void (*)())0)(); 
1.void (*)()这是一个函数指针类型,无参数,返回类型为空(void)

2.(void (*)())0这个是强制类型转换,把0强转为函数指针类型

3.*解引用变成函数,调用函数,函数无参数


void (*signal(int , void(*)(int)))(int);

1.void(*)(int)这是一个函数指针类型,返回类型为空,参数为int类型的

2.signal(int , void(*)(int))这是一个函数,函数名为signal,
参数有俩个,分别为int类型和函数指针类型。

3.函数要有返回类型,去掉signal(int , void(*)(int)),就是该函数的返回类型
该函数的类型为void (*)(int),函数指针类型。

对这个函数可以进行一下好看的改变
typedef void (*v_i)(int)
然后这样写:v_i signal(int,v_i);这样看着是不是非常舒服

7. 函数指针数组

函数指针数组

是个数组,里面的元素是是函数指针

给个例子:

void f1()
{}
void f2()
{}
int main()
{
  void (*p[2])()={f1,f2};
  这个就是函数指针数组,
  这样理解p先和[]结合说明p是一个数组,数组有2个元素,
  每个元素的个数是void (*)()函数指针类型
  return 0;
}

8. 指向函数指针数组的指针

指向函数指针数组的指针

它是一个指针,指向一个数组,数组的每个元素是函数指针类型

看下面的这个代码:

void f1()
{}
void f2()
{}
int main()
{
  void (*p[2])()={f1,f2};
  void(*(*p1)[2])()=&p;
  p1就是一个函数指针数组的指针
  return 0;
}

9. 回调函数

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。

回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应


下面有个例子:

void f1()
{}
f(int a,void (*p)())
{
  p();
}
int main()
{
  int a;
  f(a, f1);
  把f1函数作为参数传给f函数,通过函数指针来调用f1函数。
  return 0;
}

10. 小型计算器项目(对7,8,9等知识点的运用)

#include <stdio.h>
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;
}
void menu()
{
  printf("****    请选择        ****\n");
  printf("**** 1.加      2.减 **********\n");
  printf("**** 3.乘           4.除 **********\n");
  printf("************* 0退出 ***************\n");
}
f(int (*p)(int, int))
{
  int a, b;
  printf("请输入两个整数:>");
  scanf("%d%d", &a, &b);
  printf("结果为%d\n", p(a, b));
}
int main()
{

  int n;
  int (*gather[5])(int, int) = { 0,add,sub,mul,div };
  该函数指针数组里面存的是各个函数的地址,首个元素设置为0,
  方便用下标访问
  do
  {
    menu();
    该函数打印菜单,供用户选择
    scanf("%d", &n);
    if (n == 0)
    {
      printf("退出程序\n");
    }
    else if (n > 0 && n < 5)
    {
      f(gather[n]);
      这就是一个回调函数
    }
    else
    {
      printf("选择错误请从新选择\n");
    }
  } while (n);
  return 0;
}

运行的基本情况如下:

相关文章
|
1月前
|
C语言
【C语言】简易计算器转移表(函数指针简化)
【C语言】简易计算器转移表(函数指针简化)
27 0
|
1月前
|
存储 C语言 索引
函数指针数组在实现转移表时的应用:以计算器为例
函数指针数组在实现转移表时的应用:以计算器为例
38 0
|
8月前
|
C语言
通过模拟实现计算器介绍函数指针数组和回调函数的用法【C语言/指针/进阶】
通过模拟实现计算器介绍函数指针数组和回调函数的用法【C语言/指针/进阶】
55 0
|
1月前
|
C语言
指针数组以及利用函数指针来实现简易计算器及typedef关键字(指针终篇)
指针数组以及利用函数指针来实现简易计算器及typedef关键字(指针终篇)
项目中常见NPE空指针异常
项目中常见NPE空指针异常
|
1月前
|
小程序
利用函数指针数组写计算器(转移表)
利用函数指针数组写计算器(转移表)
30 1
|
9月前
|
存储 小程序
计算器小程序+函数指针数组介绍
计算器是我们生活中重要的使用工具,那么我们要怎样写出计算机小程序呢?想必大家有很多种写出计算器程序的代码,在这里小编将用函数指针数组写出计算器程序.
39 0
|
10月前
函数指针、函数指针数组、计算器+转移表等归纳总结
函数指针、函数指针数组、计算器+转移表等归纳总结
|
11月前
|
Serverless C语言
头歌C语言实训项目-数组、指针和函数综合编程练习
头歌C语言实训项目-数组、指针和函数综合编程练习
227 0
|
11月前
|
存储 编译器 C语言
头歌c语言实训项目-指针进阶
头歌c语言实训项目-指针进阶
306 0