C语言详解指针(指针海洋的探索,将传值与传址刻在心里)

简介: C语言详解指针(指针海洋的探索,将传值与传址刻在心里)

一、指针的概念

要知道指针的概念,要先了解变量在内存中如何存储的。在存储时,内存被分为一块一块的。每一块都有一个特有的编号。而这个编号可以暂时理解为指针,就像酒店的门牌号一样。

1.1内存与地址

在讲内存和地址之前,我们想有个⽣活中的案例:

假设你要去酒店,酒店有100个房间,但是房间没有编号,你的⼀个朋友来找你玩,如果他想要找到你,就得一个一个去找,这样的效率是很低的,那我们给定每个房间编号

⼀楼:101,102,103...

⼆楼:201,202,203....

...

你的朋友得到房间号,就可以快速找到你的房间,找到你。

如果把上面的例子对找到计算机中:

如: 你的朋友 就相当于计算器

        而你就是房间(地址)的内存了

        101 102 103  就相当于地址          

在计算机中我们把内存单元的编号也称为地址。C语言中给地址起了新的名字叫:指针 

例子:

void main()
 
{
 
          int x = 1, int y = 2;
 
} 

这段代码非常简单,就是两个变量的声明,分别赋值了 1、2。我们把内存当做一个酒店,而每个房间就是一块内存。那么“int x = 1;”和“int y = 2;”的实际含义如下:

去酒店订了两个房间,门牌号暂时用 px、py 表示
让 1 住进 px,让 2 住进 py
其中门牌号就是 px、py 就是变量的地址
x 和 y 在这里可以理解为具体的房间,房间 x 的门牌号(地址)是 px,房间 y 的门牌号(地址)是 py。而 1和 2,通过 px、py 两个门牌,找到房间,住进 x、y。

二、变量的指针与指针变量

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

2.1、指针变量的定义及使用

1、指针变量的定义

指针变量的定义形式如:数据类型 *指针名;例如:

//分别定义了 int、float、char 类型的指针变量

int *x;

float *f;

char *ch;

这里的指针变量是x,f,ch,并非  *x  ,  *f,  *ch.

数据名为int* , float*, char*

2、指针变量的使用

我们要怎样取地址呢?这就要用到——取地址运算符& 和 指针运算符*(间接寻址符)

取地址运算符&:单目运算符&是用来取操作对象的地址。例:&i 为取变量 i 的地址。对于常量表达式、寄存器变量不能取地址(因为它们存储在存储器中,没有地址)。

指针运算符*(间接寻址符):与&为逆运算,作用是通过操作对象的地址,获取存储的内容。例:x = &i,x 为 i 的地址,*x 则为通过 i 的地址,获取 i 的内容。

int main() {
 
    int a = 10;
    //输入一个整型变量a,变量的值为10
    int* pa = &a;
    //输入一个整型的指针变量pa来接收存放a的地址
    printf("%d",*pa);
    //(*)表示对pa存放地址的解引用
    return 0;
}

2.2 指针变量的大小

前⾯的内容我们了解到,32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产生的2进制序列当做⼀个地址,那么⼀个地址就是32个bit位,需要4 个字节才能存储。

如果指针变量是⽤来存放地址的,那么指针变的大小就得是4个字节的空间才可以。

同理64位机器,假设有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要 8个字节的空间,指针变的大小就是8个字节。

#include <stdio.h>
//指针变量的⼤⼩取决于地址的大小
//32位平台下地址是32个bit位(即4个字节)
//64位平台下地址是64个bit位(即8个字节)
int main()
{
printf("%zd\n", sizeof(char *));
printf("%zd\n", sizeof(short *));
printf("%zd\n", sizeof(int *));
printf("%zd\n", sizeof(double *));
return 0;
}

怎么回事呢?想必你也发现些许奥秘,没错,对于指针来说,指针变量的大小与类型无关、只要指针类型的变量,在相同的平台下,大小都是相同的。

2.3、指针+-整数

#include <stdio.h>
int main()
{
int n = 10;
char *pc = (char*)&n;
int *pi = &n;
 
          printf("%p\n", &n);
          printf("%p\n", pc);
          printf("%p\n", pc+1);
          printf("%p\n", pi);
          printf("%p\n", pi+1);
  return 0;
}

让我们打印一下吧!!!

我们可以看得出来 ,char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。 这就是指针变量的类型差异带来的变化。

结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)。

 

2.4、void*指针

在指针类型中有⼀种特殊的类型是 void* 类型的,可以理解为无具体类型的指针(或者叫泛型指

针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进 行指针的+-整数和解引用的运算。

我们来看一下这个代码:

#include <stdio.h>
int main()
{
    int a = 10;
    int* pa = &a;
    char* pc = &a;
    
    return 0;
}

显示从int*到char*的类型不兼容,编译器会给一个报错,用void*则不会出现这个问题

#include <stdio.h>
int main()
{
    int a = 10;
    void* pa = &a;
    void* pc = &a;
    
    return 0;
}

这里我们看到,void*可以用来接收不同类型的指针,但是注意的是void*不能用来指针的运算。

三、指针的运算

指针的基本运算有三种,分别是:

指针+- 整数

指针-指针

指针的关系运算

1、指针+- 整数

因为数组在内存中是连续存放的,只要知道第⼀个元素的地址,顺藤摸⽠就能找到后⾯的所有元素。

#include <stdio.h>
//指针+- 整数
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
for(i=0; i<sz; i++)
{
printf("%d ", *(p+i));     //p+i 这⾥就是指针+整数
}
return 0;
}

2、指针-指针

//指针-指针
#include <stdio.h>
int my_strlen(char *s)
{
char *p = s;
//每次++,到达下一个元素,到最后一个为NULL时,p指向c
while(*p != '\0' )
p++;
return p-s;     
//返回地址的差值,因为是char*类型,每个地址间跳过一个字节,所以返回的
 
}
int main( )
{
printf("%d\n", my_strlen("abc"));
return 0;
}

3、指针的关系运算

//指针的关系运算
#include <stdio.h>
 
 
int main()
{
    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    int* p = &arr[0];
    int i = 0;
    int sz = sizeof(arr) / sizeof(arr[0]);
    while (p < arr + sz) //指针的大小比较
    {
        printf("%d ", *p);
        p++;
    }
 
    return 0;
}

6. 野指针

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

6.1、野指针成因

指针未初始化:
#include <stdio.h>
int main()
{
 
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
 
return 0;
 
}
指针越界访问
#include <stdio.h>
int main()
{
int arr[10] = {0};
int *p = &arr[0];
int i = 0;
for(i=0; i<=11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
指针指向的空间释放
#include <stdio.h>
int* test()
{
int n = 100;
return &n;
}
 
int main()
{
int*p = test();
printf("%d\n", *p);
return 0;
}

那我们如何规避野指针的出现呢

6.2、如何规避野指针

指针初始化,如果我们明确指针的指向的话,该指哪就指向哪;当我们不知道指向哪时, 可以给指针赋值NULL. NULL 是C语⾔中定义的⼀个标识符常量,值是0;0也是地址,这个地址是⽆法使⽤的,读写该地址 会报错。

#include <stdio.h>
int main()
{
int num = 10;
int*p1 = &num
int*p2 = NULL;
 
return 0;
}

6.3、注意指针不要越界

⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是 越界访问。

6.4、当指针不再使用时,可以将其置为NULL,指针使用前,判断其有效性

当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的 时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问, 同时使⽤指针之前可以判断指针是否为NULL。

我们可以把指针想为野狗,不管理的野狗是非常危险的,我们可以将野狗栓在一棵树(NULL),就相对安全了;

6.2、assert函数

assert.h 头文件定义了宏 assert() ,⽤于在运行时确保程序符合指定条件,如果不符合,就报

错终止运行。这个宏常常被称为“断⾔”。

assert(p != NULL );

运行这个语句的时候,判断p是否为空,如果不为空则,程序继续运行,如果为空,则终止程序,并给出错误的信息提示。

四、多级指针及指针数组

(1)多级指针

指针变量作为一个变量也有自己的存储地址,而指向指针变量的存储地址就被称为指针的指针,即二级指针。依次叠加,就形成了多级指针。

int p = 10;
//设置一个变量为p
int* pc = &p;
//取p的地址存进pc
int** pt = &pc;
//取pc的地址存进pt,这里的pt为二级指针

先用一个简单的数组来举例:

int nums[2][2] = {  {1, 2},{2, 3}};
#include<stdio.h>
 
int main()
{
  int arr[2][2] = { {1,2},{2,3} };
  int* pc = &arr;
  printf("%d ", *pc);
  printf("%d\n", pc);
  printf("%d ", *(pc+1));
  printf("%d\n", (pc + 1));
  printf("%d ", *(pc+2));
  printf("%d\n", (pc + 2));
  printf("%d ", *(pc+3));
  printf("%d\n", (pc + 3));
  return 0;
}

我们可以看出二维数组地址的运用,可以看作一维数组;以此来推断,面对多维数组地址的运用时,我们不必害怕,可以当作一维数组

4.3、指向函数的指针

C 语言中,函数不能嵌套定义,也不能将函数作为参数传递。但是函数有个特性,即函数名为该函数的入口地址。我们可以定义一个指针指向该地址,将指针作为参数传递。

对于函数参数传递时,如果是传址函数时,则可以改变该地址的函数值;如为传值,则不然;

#include<stdio.h>
 
void comp_int(int*comp) {
  *comp = 10;
}
 
int main()
{
  int a = 10;
  int b = 20;
  comp_int(&b);
  if (a == b) {
    printf("相等");
  }
  return 0;
}

这里的输出为:相等,则我们可以通过传址改该地址的值;

五、计算器(转移表)的使用

1、计算器的实现(switch)

对于一个计算器,需要有最基础的(加减乘除),来人,上代码:

这时候有人就会觉得这个代码有点冗长,说:小伞,小伞有没有办法,能将代码变得便洁吗?

那当然是有的呀!

#define _CRT_SECURE_NO_WARNINGS
 
#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;
}
 
int main() {
  int a = 0;
  int x, y;
  int ret;
  do
  {
    printf("*************************\n");
    printf("    1:add     2:sub      \n");
    printf("    3:mul     4:div      \n");
    printf("    0:exit               \n");
    printf("*************************\n");
    printf("请选择:");
    scanf("%d", &a);
    switch (a)
    {
    case 1:
      printf("输⼊操作数:");
      scanf("%d %d", &x, &y);
      ret = add(x, y);
      printf("ret = %d\n", ret);
      break;
    case 2:
      printf("输⼊操作数:");
      scanf("%d %d", &x, &y);
      ret = sub(x, y);
      printf("ret = %d\n", ret);
      break;
    case 3:
      printf("输⼊操作数:");
      scanf("%d %d", &x, &y);
      ret = mul(x, y);
      printf("ret = %d\n", ret);
      break;
    case 4:
      printf("输⼊操作数:");
      scanf("%d %d", &x, &y);
      ret = div(x, y);
      printf("ret = %d\n", ret);
      break;
    case 0:
      printf("退出程序\n");
      break;
    default:
      printf("选择错误\n");
      break;
    }
  } while (a);
  return 0;
}

如果我们使用switch语句来实现这样一个简易的计算器我们会发现,每当我要添加一个功能的时候。都需要增加一个case语句,比如我要增加一个&运算,我得再加上一个case语句。因此我们可以使用函数指针数组(转移表)来实现,会简易很多。

什么是转移表?

其实很简单,它所指的就是运用函数指针数组以数组方式去调用里面的函数,从而在某些情况下替代冗长的switch函数,就叫转移表。

单纯的文字说明实在有些单调,这里通过模拟实现计算器来进一步解释说明转移表。

上代码:

#include <stdio.h>
int add(int a, int b)
{
  return a + b;
}
int sub(int a, int b)
{
  return a - b;
}
int mul(int a, int b)
{
  return a * b;
}
int div(int a, int b)
{
  return a / b;
}
int main()
{
  int x, y;
  int input = 1;
  int ret = 0;
  //p[]={add,sub,mul,div}
  //这里我们想要将函数的地址存进来
  //int(*p[])(int,int)
  //返回类型   函数指针  指向函数的参数
  int(*p[5])(int , int )= { 0, add, sub, mul, div }; //转移表
  //函数指针的数组     下标    0   1    2    3    4
  do
  {
    printf("*************************\n");
    printf(" 1:add 2:sub \n");
    printf(" 3:mul 4:div \n");
    printf(" 0:exit \n");
    printf("*************************\n");
    printf("请选择:");
    scanf("%d", &input);
    if ((input <= 4 && input >= 1))
    {
      printf("输⼊操作数:");
      scanf("%d %d", &x, &y);
      ret = (*p[input])(x, y); //
      printf("ret = %d\n", ret);
    }
    else if (input == 0)
    {
      printf("退出计算器\n");
    }
    else
    {
      printf("输⼊有误\n");
    }
  } while (input);
     return 0;
}

对于指针的学习还很多,今天先讲到这里了,点点赞吧!!!



相关文章
|
2月前
|
C语言
【c语言】指针就该这么学(1)
本文详细介绍了C语言中的指针概念及其基本操作。首先通过生活中的例子解释了指针的概念,即内存地址。接着,文章逐步讲解了指针变量的定义、取地址操作符`&`、解引用操作符`*`、指针变量的大小以及不同类型的指针变量的意义。此外,还介绍了`const`修饰符在指针中的应用,指针的运算(包括指针加减整数、指针相减和指针的大小比较),以及野指针的概念和如何规避野指针。最后,通过具体的代码示例帮助读者更好地理解和掌握指针的使用方法。
57 0
|
21天前
|
存储 NoSQL 编译器
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
指针是一个变量,它存储另一个变量的内存地址。换句话说,指针“指向”存储在内存中的某个数据。
74 3
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
|
21天前
|
存储 编译器 C语言
【C语言】指针大小知多少 ?一场探寻C语言深处的冒险 !
在C语言中,指针的大小(即指针变量占用的内存大小)是由计算机的体系结构(例如32位还是64位)和编译器决定的。
46 9
|
21天前
|
安全 程序员 C语言
【C语言】指针的爱恨纠葛:常量指针vs指向常量的指针
在C语言中,“常量指针”和“指向常量的指针”是两个重要的指针概念。它们在控制指针的行为和数据的可修改性方面发挥着关键作用。理解这两个概念有助于编写更安全、有效的代码。本文将深入探讨这两个概念,包括定义、语法、实际应用、复杂示例、最佳实践以及常见问题。
40 7
|
1月前
|
存储 C语言
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
105 13
|
24天前
|
存储 程序员 编译器
C 语言数组与指针的深度剖析与应用
在C语言中,数组与指针是核心概念,二者既独立又紧密相连。数组是在连续内存中存储相同类型数据的结构,而指针则存储内存地址,二者结合可在数据处理、函数传参等方面发挥巨大作用。掌握它们的特性和关系,对于优化程序性能、灵活处理数据结构至关重要。
|
25天前
|
算法 C语言
C语言中的文件操作技巧,涵盖文件的打开与关闭、读取与写入、文件指针移动及注意事项
本文深入讲解了C语言中的文件操作技巧,涵盖文件的打开与关闭、读取与写入、文件指针移动及注意事项,通过实例演示了文件操作的基本流程,帮助读者掌握这一重要技能,提升程序开发能力。
83 3
|
25天前
|
存储 算法 程序员
C 语言指针详解 —— 内存操控的魔法棒
《C 语言指针详解》深入浅出地讲解了指针的概念、使用方法及其在内存操作中的重要作用,被誉为程序员手中的“内存操控魔法棒”。本书适合C语言初学者及希望深化理解指针机制的开发者阅读。
|
1月前
|
存储 C语言 开发者
C 语言指针与内存管理
C语言中的指针与内存管理是编程的核心概念。指针用于存储变量的内存地址,实现数据的间接访问和操作;内存管理涉及动态分配(如malloc、free函数)和释放内存,确保程序高效运行并避免内存泄漏。掌握这两者对于编写高质量的C语言程序至关重要。
52 11
|
25天前
|
程序员 C语言
C语言中的指针既强大又具挑战性,它像一把钥匙,开启程序世界的隐秘之门
C语言中的指针既强大又具挑战性,它像一把钥匙,开启程序世界的隐秘之门。本文深入探讨了指针的基本概念、声明方式、动态内存分配、函数参数传递、指针运算及与数组和函数的关系,强调了正确使用指针的重要性,并鼓励读者通过实践掌握这一关键技能。
36 1