【C语言】--指针超详解(一)
目录
一.内存和地址
1.1--内存
1.2--如何理解编址
二.指针变量和地址
2.1--取地址操作符(&)
2.2--指针变量和解引用操作符(*)
2.2.1--指针变量
2.2.2--如何理解指针类型
2.2.3--解引用操作符
2.3--指针变量的大小
三.指针变量类型的意义
3.1--从指针的解引用方面看
3.2--从指针加减整数方面看
3.3--void * 指针
四.指针运算
4.1--指针加减整数
4.2--指针减指针
4.3--指针的关系运算
前言:从这篇文章开始,笔者将要开始更新指针系列的知识分享,这一块也是C语言学习中的重难点,希望能给大家带来一些帮助,一起学习进步
一.内存和地址
--提到指针,我们就先需要对内存和地址有一定的了解,理清这两者和指针之间的关系。
1.1--内存
--我们生活中的许多事情都离不开编号,比如我说我们刚开学想找到自己的宿舍,如果不知道宿舍的门牌号的话,就很麻烦了,而有了门牌号,我们就可以快速找到宿舍的地址。那么同理,在计算机上我们如果想要高效的管理内存空间的话,其实也是把内存划分为一个个的内存单元(它们也有自己的编号),每个内存单元的大小取1字节。
在这里先补充一下计算机中的常见单位:一个比特位可以存储一个二进制的位1或者0
1.bit--比特 1Byte=8bit
2.Byte--字节 1KB=1024Byte
3.KB 1MB=1024KB
4.MB 1GB=1024MB
5.GB 1TB=1024GB
6.TB 1PB=1024TB
7.PB
我们可以把每个内存单元理解为一个学生宿舍
一个字节空间里面可以存放8个比特位,就好比一个宿舍住了8个人一样,每个人都是一个比特位。 每个内存单元又都有一个编号(这个编号就相当于宿舍房间的门牌号),有了这个内存单元的编号,CPU就可以快速找到一个内存空间。 在生活中,我们把门牌号叫做地址,在计算机我们把内存单元的编号也叫做地址。C语言中给地址起了新的名字叫做指针。 所以我们可以这样理解: 内存单元的编号==地址==指针
1.2--如何理解编址
--计算机中各个硬件单元都是要互相协同工作的,但硬件与硬件之间又是相互独立的,所以硬件与硬件之间的协同工作是用一个个线连起来的 ,今天主要介绍的是地址总线。
CPU访问内存中的某个字节空间,必须知道这个字节空间在内存的什么位置,而因为内存中字节很多,所以需要给内存进行编址。 计算机中的编址,并不是把每个字节的地址记录下来,而是通过硬件设计完成的。我们可以简单理解为,32位机器有32根地址总线,每根线只有两态,表示0,1【电脉冲有无】,那么一根线能表示两种含义,2根线就是4中。依次类推32根地址线就能表示2^32种含义,每一种含义都代表一个地址。 整体过程大概就是控制总线下达一个命令,地址信息通过地址总线被下达给内存,在内存上就可以找到该地址对应的数据,最后将数据通过数据总线传入CPU内寄存器。
二.指针变量和地址
2.1--取地址操作符(&)
--理解了内存和地址的关系,我们再回到C语言中,在C语言中创建变量其实就是向内存申请空间,比如:
#include <stdio.h>
int main()
{int a = 10;return 0;
}
上述的代码就是创建了整型变量a,内存中申请4个字节,用于存放整数10,其中每个字节都有地址,那我们该如何得到a的地址呢,这里我们就需要取地址操作符(&)了。
#include <stdio.h>
int main()
{int a = 10;&a;//取出a的地址printf("%p\n", &a);return 0;
}
按上述代码执行后,&a取出的是a所占字节中地址较小的字节的地址,所以打印出来也是地址较小字节的地址。虽然整型变量占用4个字节,但我们只需要知道了第一个字节地址,顺藤摸瓜访问到4个字节的数据也是可行的。
2.2--指针变量和解引用操作符(*)
2.2.1--指针变量
--我们通过取地址操作符(&)拿到的地址是一个数值,这个数值我们有时需要存储起来,方便后期使用,这个时候就得用上指针变量了。 比如:
#include <stdio.h>
int main()
{int a = 10;int* pa = &a;//取出a的地址并存储到指针变量pa中return 0;
}
指针变量也是一种变量,这种变量就是用来存放地址的 ,存放在指针变量中的值都会理解为地址。
2.2.2--如何理解指针类型
我们看到指针变量的类型是int*,我们该如何理解指针的类型呢?
int a = 10;
int * pa = &a;
这里pa左边写的是int*,*说明了pa是指针变量,而前面的int是在说明pa指向的是整型类型的对象
同样的,如果有一个char类型的变量ch,ch的地址我们就可以用char * pc = &ch来存起来。其他类型也是同样的道理。
2.2.3--解引用操作符
--我们将地址保存之后,那我们怎么去使用他呢,这里我们就需要解引用操作符(*)了。
#include <stdio.h>
int main()
{int a = 100;int* pa = &a;*pa = 0;return 0;
}
上述代码就使用了解引用操作符,*pa的意思就是就是通过指针变量pa中存放的地址,找到指向的空间,*pa其实就是a变量了;所以*pa=0,这个操作就是把a改成了0;
那么我们为什么不直接修改a呢,还非要运用指针,其实这里把a的修改的操作交给了pa,这样就多了一种途径,写代码也更加灵活。
2.3--指针变量的大小
--从前面的内容我们可以了解到,32位机器假设有32根地址总线,每根地址总线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产生的2进制序列当做一个地址,那么一个地址就是32个bit位,需要4个字节才能存储。如果指针变量是用来存放地址的,那么指针变量的大小就得是4个字节的空间才可以,同理,如果是64位机器,指针变量的大小就是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;
}
我们将这串代码分别放在X86和X64环境运行一下,可以发现一个全是4,一个全是8。
结论:
- 32位平台下地址是32个bit位,指针变量的大小是4个字节
- 64位平台下地址是64个bit位,指针变量的大小是8个字节
- 指针变量的大小和类型无关,只要是指针类型的变量,在相同的平台下,大小都是相同的。
三.指针变量类型的意义
--在上面的学习中我们知道了指针变量的大小和类型无关,那为什么还有各种各样的指针类型呢,其实指针类型是有其特殊意义的,我们接着往下看把。
3.1--从指针的解引用方面看
--我们对比下面两串代码,在调试时观察其内存的变化
//代码1
#include <stdio.h>
int main()
{int n = 0x11223344;int* pi = &n;*pi = 0;return 0;
}//代码2
#include <stdio.h>
int main()
{int n = 0x11223344;char* pc = (char*)&n;*pc = 0;return 0;
}
通过调试观察其内存变化我们可以发现,代码1会将n的4个字节全部改为0,但是代码2只是将你的第一个字节改为0.
结论:指针的类型决定了对指针解引用的时候有多大权限(一次能操作几个字节)。
比如:从上面的例子可以知道,char*的指针解引用就只能访问1个字节,而int*的指针解引用就能访问4个字节
3.2--从指针加减整数方面看
--我们直接通过一串代码,运行起来观察其地址的变化来直观感受一下。
#include <stdio.h>
int main()
{int n = 10;printf("%p\n", &n);char* pc = (char*)&n;printf("char * 类型\n");printf("%p\n", pc);printf("%p\n", pc + 1);int* pi = &n;printf("int * 类型\n");printf("%p\n", pi);printf("%p\n", pi + 1);return 0;
}
代码运行结果如下:
我们可以看出,char*类型的指针变量+1跳过一个字节,int*类型的指针变量+1跳过了4个字节。这就是指针变量类型差异带来的变化。指针+1,其实就是跳过1个指针指向的元素。指针可以+1,那也可以-1。
结论:指针的类型决定了指针向前或者向后走一步有多大(距离)。
3.3--void * 指针
--在指针类型中有一种特殊的类型是void*类型的,可以理解为无具体类型的指针(或者叫泛型指针),这种类型的指针可以用来接受任意类型地址。但是也有局限性,void*类型的指针不能直接进行指针的+-整数和解引用的运算。
举例:
#include <stdio.h>
int main()
{int a = 10;int* pa = &a;char* pc = &a;//errreturn 0;
}
在上面的代码中,将一个int类型的变量地址赋值给一个char*类型的指针变量。编译器给出了一个警告(如下图),是因为类型不兼容。但是用void*类型就不会有这样的问题了。
使用void*类型的指针接收地址:
#include <stdio.h>
int main()
{int a = 10;void* pa = &a;void* pc = &a;*pa = 10;//err*pc = 0;//errreturn 0;
}
运行代码的结果:
从这里我们可以看到,void*类型的指针可以接收不同类型的地址,但无法直接进行指针运算。
那void*指针到底有啥用呢,一般void*类型的指针是使用在函数参数的部分,用来接受不同类型数据的地址,这样的设计可以实现泛型编程的效果,使得一个函数来处理多种类型的数据,这个在后续的指针超详解篇中会给大家分享。
四.指针运算
指针的基本运算有三种:
- 指针+-整数
- 指针-指针
- 指针的关系运算
4.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;
}
大家可以自己下去尝试一下逆序打印和用指针+-整数实现strlen函数等问题,体会指针加减整数是如何运用的。
4.2--指针减指针
--【指针-指针】的绝对值得到的是两个指针之间的元素个数
前提:两个指针指向了同一块空间,否则不能相减
我们来看一个用指针-指针实现strlen函数作用的代码吧:
//指针-指针
#include <stdio.h>
int my_strlen(char* s)
{char* p = s;while (*p != '\0')p++;return p - s;
}
int main()
{printf("%d\n", my_strlen("abc"));return 0;
}
4.3--指针的关系运算
--我们直接用指针的关系运算打印数组元素这个例子来直观感受一下吧:
#include <stdio.h>int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int* p = &arr[0];int sz = sizeof(arr) / sizeof(arr[0]);while (p < arr + sz) //指针的⼤⼩⽐较{printf("%d ", *p);p++;}return 0;
}
结语:本篇文章就到此结束,对指针这块内容感兴趣的朋友可以期待一下后续的指针相关知识分享,感谢大家的关注和支持。