摘要
在计算机的底层逻辑中,CPU和内存之间通过数据总线来传输数据,把数值存储到地址中。我我们通过地址来获取数值可以加快我们程序的运行效率,而所用到的就是指针。
指针是一种变量,通过存储地址,再通过解引用符来调用数据,根据调用方式的不同,分别为值传递,址传递。
目录
指针的应用
假设我们住在同一个大公寓中,每个房间都有一个房间号,如果要找到小明,那么我们总不可能说在这一大栋公寓说:“我们要找小明”,很显然,这是吃力不讨好的事情,效率最高的事情就是通过服务员来知道房间号,通过房间号来找小明——我们也将门牌号称作地址
C语言中,我们给房间号起了新的名字:指针
而指针就是这样的作用,标记内存单元的编号,给数据地址,根据数据类型的不同,其内存单元所占的体积不同,比如double类型,为综合的办公室,双精准浮点类型,占据8个字节单位;再比如char类型,可以类比成小巧的办公间,有8个人的办公位置,其大小为1个字节单位。
我们可以理解成:
房间号==内存单元的编号==地址==指针
指针的底层原理(硬件方面)
CPU | 1 | 内存 |
0 | ||
0 | ||
0 | ||
1 | ||
1 | ||
地址总线 | ||
1 | ||
0 | ||
0 | ||
0 | ||
1 | ||
数据总线 | ||
控制总线 | ||
我们这一点要从计算机的编址讲起,计算机中的编址——通过硬件设计完成的,像前台服务员要给我们置办房间号一样,它是通过“服务端”来查看“空余房号”给我们安排房间号。
但是计算机的安排地址不像前台服务员简单给我们安排“空余房号”那样简单,单单通过操作就能完成安排,而是要通过硬件单元相互协同工作。所谓的协同,就是硬件间能够惊醒数据传递。
硬件与硬件之间是相互独立存在的,我们通过“线”连接起来,。
CPU与内存之间也有大量数据交互,所以,二者间也是用“线”连接起来
本文重点讲“指针”,所以只强调地址总线。
32位机器有32根地址总线,由于计算机从晶体管发展以来,通过1,0来表示【电脉冲的有无】,32根地址总线就有2^32含义,而每一种含义都代表一个地址。
地址信息被下达传给内存,在内存上,就可以找到该地址对应的数据,将数据在通过数据总线传入CPU内寄存器
我们通过前台服务员来获取小明的房间号,这在计算机中,就是我们通过CPU内的MAR获取地址,我们有了房间号,找到小明的速度就加快了,又或者说,我们找到了地址,找到我们想要的数据就变得快捷。
#include<stdio.h>
int main() {
int a = 10;
int * pa = &a;//取出 a 的地址并存储到指针变量 pa 中
printf(“%p\n”,&a);//%p指的是打印出16进制的地址
return 0;
}
而我们通过服务员(也就是取地址操作符&)来获取小明的地址,我们记得小明的房间号,这是一种存储方式,知道小明房间号的我们,就是一份指针变量。
地址的存放规律:
0XFFFFFFFF |
| <——高地址 |
0XFFFFFFFE |
| |
| ||
| ||
……… | ||
| ||
| ||
0X006FFD73 |
| int a存放 |
0X006FFD72 |
| |
0X006FFD71 |
| |
0X006FFD70 |
| |
| ||
| ||
……… |
| |
| ||
0X00000002 | 8bit | |
0X00000001 | 1字节 | |
0X00000000 | 1byte | <——低地址 |
←低地址
指针变量和解引⽤操作符(*)
从上文我们知道,进行取地址操作(&)后,获得的地址是一个数值,&a获得的是0x006FFD70,这个数值有时候需要存储起来——也就是我们有时候要找小明,那么我们就必须记住他的地址,以备不时之需,而指针变量就是这样的作用。
拆解指针类型
int a = 10;
int * pa = &a;
我们看到pa的类型是int*
如果有一个char类型的变量ch,ch的地址,要存放在char*类型的指针变量中。
即
char ch=’w’;
char* pc=*ch;
解引用操作符
我们要用到这个信息——小明的房间号,去找小明拿取物品或者存放物品,我们本身需要通过大脑神经突触连接找到关于小明的房间号,而计算机是通过解引用操作符(*)指针来表达pa指向a的地址的数值。
int a = 100;
int* pa = &a;
*pa = 0;
上段代码*pa的意思就是通过pa中存放的地址,找到指向的空间,*pa实际上就是a本身,我们找到小明的房间号,找到的就是小明本人,我们这时候跟他商讨事情,小明知道了我们的想法(赋值为0),所以*pa=0;
我们在这里埋下一个伏笔,既然*pa==a;直接写成a=0;就可以了,为什么要大费周章来引入指针?对此,我只能暂时替你回答——我们多了一个路径来修改a的数值,写代码会更加灵活多变——到后面我们再一一揭晓。
指针变量的⼤⼩
上文提到,32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产⽣的2进制序列当做⼀个地址,那么⼀个地址就是32个bit位,需要4个字节才能存储。
指针变量是用来存放地址的,那么指针变量的大小就需要4个字节的空间才行。
同理,64位的机器中,有64根地址线,一个地址就是64个二进制位组陈大哥二进制序列,存储起来就需要8个字节的空间,指针变量的大小就是8个字节。
printf("%zd\n", sizeof(char *));
printf("%zd\n", sizeof(short *));
printf("%zd\n", sizeof(int *));
printf("%zd\n", sizeof(double *));
在x86的环境下,输出的数值都是4,而在x64环境的环境下,输出的数值都是8.
需要注意到的点是:指针变量的大小和类型是无关的,只要是指针类型的变量,在相同的环境(平台,位数)下,大小都是相同的。
指针变量类型的意义
指针变量的大小与其类型无关,只要是指针变量,在同一个平台下其字节数都是一样的,为什么还要有各种各样的指针类型?
/代码段1/
#include <stdio.h>
int main() {
int a = 0x11223344;
int *pi = &a;
*pi = 0;
return 0;
}
/代码段2/
#include <stdio.h>
int main() {
int a = 0x11223344;
char *pc = (char *)&a;
*pc = 0;
return 0;
}
通过对这两段代码段的监测我们可以知道,代码1会将a的4个字节全部更改为0,但是代码2只将n的第一个字节改为0.
于是,我们有这样一种说法:指针的类型决定了,对指针解引用的时候一次能操作几个字符——我们在这里就需要发出疑问,int类型被char*指向是有用的吗?答案是肯定的,char*可以修改int类型中一个字节,这对数据的修正是有利的。
指针+-整数
#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个字节。这就是指针变量的类型差异带来的变化。指针+1,其实跳过1个指针指向的元素。指针可以+1,那也可以-1。
结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)。
void*指针
在指针类型中有⼀种特殊的类型是 void * 类型的,可以理解为⽆具体类型的指针(或者叫泛型指针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进⾏指针的+-整数和解引⽤的运算。
使⽤void*类型的指针接收地址:
#include <stdio.h>
int main() {
int a = 10;
int* pa = &a;
int* pc = &a;
*pa = 10;
*pc = 0;
return 0;
}
void* 类型的指针可以接收不同类型的地址,但是⽆法直接进⾏指针运算。
⼀般 void* 类型的指针是使⽤在函数参数的部分,⽤来接收不同类型数据的地址,这样的设计可以 实现泛型编程的效果。使得⼀个函数来处理多种类型的数据
指针运算
指针的基本运算有三种,分别是:
- 指针+- 整数
- 指针-指针
- 指针的关系运算
指针+- 整数
由于数组在内存中是连续存放的,并且不间断,所以只要知道首元素的地址,就能顺藤摸瓜知道后面所有元素,可以理解成一家子,住在连续的房间号,我们只要知道一家子谁的房间号最小,根据人口数,就可以知道其他人的房间号了。
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
数组 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
地址(*int) | p | p+1 | p+2 | p+3 | p+4 | p+5 | p+6 | p+7 | p+8 | p+9 |
#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;
}
指针-指针
//指针-指针
#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;
}
指针的关系运算
#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;
}
指针的使⽤和传址调⽤
strcat的模拟
要清楚strcat的工作原理:将字符串dest,src传入,src每个下标上的字符赋给与dest相同下标的字符。
Appends a copy of the source string to the destination string. The terminating null character in destination is overwritten by the first character of source, and a null-character is included at the end of the new string formed by the concatenation of both in destination.
——legacy.cplusplus.com
char* my_strcat(char* dest, const char* src){
char* ret = dest;
assert(dest != NULL);
assert(src != NULL);
while(*dest){
dest++;
}
while((*dest++ = *src++)){
;
}
return ret;
}
传值调⽤和传址调⽤
#include <stdio.h>
void Swap1(int x, int y) {
int tmp = x;
x = y;
y = tmp;
}
int main() {
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
Swap1(a, b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
/代码段2:/
#include <stdio.h>
void Swap2(int *x, int *y) {
int tmp = *x;
*x = *y;
*y = tmp;
}
int main() {
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
Swap2(&a, &b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
我们发现,在main函数的内部,创建了a和b,a的地址是0x,b的地址是,在调用Swap1函数时,将a和b传递给了Swap1函数,而Swap1函数内部船舰了形参x和形参y接受a和b的数值,但是形参x和形参y分别于a,b的地址不一样,相当于x和y是独立的空间,那么在Swap1函数内部交换x和y的数值,不会影响到a和b。该代码段是典型的传值调用。
在这里提一下,由于形参是暂时存在,每一次函数的调用,都要在栈区创建一份临时空间,而当我们利用完这一份临时空间后,这个空间就会被销毁——这是为了保护内存不爆炸而做的。
而代码段2解决的是1当调用Swap函数的时候,Swap2函数内部操作的就是main函数中的a和b,直接将a和b的数值交换。那么就可以使用指针了,在main函数中将a和b的地址传递给Swap2函数,Swap2函数里边通过地址间接地操作main函数中的a和b,并且达到交换的效果。我们将这种把变量的地址传递给了函数,这种函数调用方式叫:传址调用。
传址调用能将函数和主调函数(main)之间建立真正的联系,在函数内部可以修改主调函数的变量。所以我们如果要在接下来的工作生产中,通过主调函数的变量值来实现函数的计算,那么就采用传值调用。而如果是函数内部要修改主调函数中的变量的数值,就需要传值调用。
由于篇幅问题,我们再次另开新章:
指针(2)——数组指针,指针数组http://t.csdnimg.cn/DPhLx