指针可以说是整个C语言学习过程中最为重要同时难度也较大的部分,因此整个指针内容我分为三部分进行记录,分别是初识指针、进阶认识和相关扩展
一、指针是什么?
在了解指针之前我们需要了解一下计算机的一些组成原理。
1、内存和地址
(1)内存
计算机中的内存通常有8GB/16GB/32GB/64GB,为了计算机的高校运行,通常将内存划分为一个个内存单元,每个内存单元取一个字节,一个字节有八个比特位。为了方便理解,下面补充一下关于计算机常见单位的知识。
比特位是计算机中最小的内存单元,一个比特位可以存储一个二进制位的0或1。
bit - 比特位
byte - 字节
KB
MB
GB
TB
PB
1byte = 8bit
1KB = 1024byte
1MB = 1024KB
1GB = 1024MB
1TB = 1024GB
1PB = 1024TB
计算机将内存以字节为单位划分成一个个内存单元后,对每个内存单元进行编号,通过这个编号计算机可以迅速提取到对应内存单元中存储的数据,这个编号在计算机中称为地址,而在C语言中称为指针。
所以可以认为:内存单元的编号==地址==指针
0XFFFFFFFF | 一个字节 |
0XFFFFFFFE | 一个字节 |
一个字节 | |
一个字节 | |
0X00000001 | 一个字节 |
(2)编址
在CPU处理数据时需要访问内存单元,这就要知道内存单元在什么位置,所以要给内存进行编排地址。
在计算机中有很多的硬件单元,而计算机要工作时就需要这些硬件单元协同工作,它们协同工作的渠道就是“线”,通过“线”进行数据交互。在编址中需要用到三个线,分别是地址总线、数据总线、控制总线。
在32位的机器上有32根地址总线,每根线的状态有1和0,分别代表电脉冲有无。一根线代表两种含义,那么32根线就代表了2^32种含义,每种含义代表一个地址。
在CPU需要访问内存时,通过控制总线发送一个“读”的指令给内存,再从地址总线传输该内存的地址到内存,就可以从内存上读取该地址的数据,然后从数据总线传输该内存的数据给CPU;当CPU需要保存数据到内存时,CPU将会通过控制总线发送一个“写”的指令给内存,同时使用数据总线将需要存储的数据传输给内存,此时内存会分发一个内存单元用于存储数据,并将该内存的地址通过地址总线传输给CPU。
2、指针变量和地址的关系
在C语言创建变量的时候会向内存申请一块空间,这块空间就需要有对应的地址。比如申请一个整型变量a,内存会分配四个字节的空间,每个字节的地址如下:
0x006FFD70
0x006FFD71
0x006FFD72
0x006FFD73
要获得变量a的地址就需要使用取地址操作符&,下面会详细介绍
int a = 10;
int* p = &a;
printf("%p\n",p);
这里将a的地址使用取地址操作符&取出来后需要存储起来,以便后续使用,而在上面代码中a的地址就存储在变量p中。
这个p就是指针变量,p前面的int代表p指针的地址所指向的对象类型是int,*代表这个变量p是个指针变量。
打印结果是0x006FFD70,打印地址需要使用%p。
二、指针的相关操作
1、解引用与取地址
正如上面所说一个变量的地址使用取地址操作符&获得之后需要放到一个指针变量之中,那么在后续使用中如何获得这个指针变量所指向的对象呢,这就需要使用解引用操作符。
int a = 100;
int* pa = &a;
*pa = 0;
return 0;
上面这个代码中*pa就是通过pa所存放的地址找到该地址指向的变量a,也就是*pa相当于a变量,所以*pa=0,就是把a改成了0。
大家都使用过快递柜,在这里a就相当于快递,地址就相当于快递所在柜子的编号,而取地址操作符 * 就相当于取件码。在快递柜输入取件码得到柜号,就能拿到快递。
2、指针运算
(1)指针+-整数
指针+-一个整数会跳过 整数*指针指向对象的类型大小 个字节,下面直接使用代码演示
#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跳过了一个字节,int*类型的指针变量+1跳过4个字节,如果+2就对应翻倍。
使用画图更为直观理解
总结:指针的类型决定了指针向前或者向后跳过多少空间
(2)指针-指针
指针-指针得到的绝对值是两个指针所指向的空间内(必须是同一空间)他们的元素个数
使用代码演示:
#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;
}
上面代码运用了指针相减获得两个指针之间元素个数的原理 模拟实现了strlen函数,运行结果如下
(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;
}
上述代码可以打印出数组arr的全部元素。
3、const修饰指针
变量本身是可以被修改的,但使用const修饰变量之后,变量就变得不可修改了,对变量进行限制,这样的变量称为常变量。
#include <stdio.h>
int main()
{
int m = 0;
m = 20;//m是可以修改的
const int n = 0;
n = 20;//n是不能被修改的
return 0;
}
比如上述代码中的m未添加修饰,可以被修改;而n添加了const修饰,就不可被修饰,代码中对n进行修改不符合语法规则,会报错。
但是如果绕过n,用n的地址就可以修改n。
#include <stdio.h>
int main()
{
const int n = 0;
printf("n = %d\n", n);
int*p = &n;
*p = 20;
printf("n = %d\n", n);
return 0;
}
为了防止这种绕开变量修改内容的行为,我们可以使用const修饰指针
#include <stdio.h>
//代码1
void test1()
{
int n = 10;
int m = 20;
int *p = &n;
*p = 20;//ok?
p = &m; //ok?
}
void test2()
{
//代码2
int n = 10;
int m = 20;
const int* p = &n;
*p = 20;//ok?
p = &m; //ok?
}
void test3()
{
int n = 10;
int m = 20;
int *const p = &n;
*p = 20; //ok?p = &m; //ok?
}
void test4()
{
int n = 10;
int m = 20;
int const * const p = &n;
*p = 20; //ok?
p = &m; //ok?
}
int main()
{
//测试⽆const修饰的情况
test1();
//测试const放在*的左边情况
test2();
//测试const放在*的右边情况
test3();
//测试*的左右两边都有const
test4();
return 0;
}
从上面代码是运行结果可以知道:
1.const如果放在 * 的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。 但是指针变量本⾝的内容可变。
2.const如果放在 * 的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指 向的内容,可以通过指针改变。
3.如果const同时放在 * 的两边,既保证指针指向的内容不能通过指针来改变,又保证了指针变量的内容不能修改。
4、传址调用
在刚才提到的strlen模拟实现的函数中传的参数s就是指针,这种调用函数的方式就称为传址调用。
#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>
//写⼀个函数,交换两个整型变量的值
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;
}
运行代码结果如下:
我们打开监视窗口查看变量的内存分布情况
发现没产生交换的效果 ,这是因为在main函数内部,创建了a和b,a的地址是0x0135fafc,b的地址是0x0135faf0,在调⽤ Swap1函数时,将a和b传递给了Swap1函数,在Swap1函数内部创建了形参x和y接收a和b的值,但是 x的地址是0x0135fa18,y的地址是0x0135fa1c,x和y确实接收到了a和b的值,不过x的地址和a的地址不 ⼀样,y的地址和b的地址不⼀样,相当于x和y是独⽴的空间,那么在Swap1函数内部交换x和y的值, ⾃然不会影响a和b,当Swap1函数调⽤结束后回到main函数,a和b的没法交换。Swap1函数在使⽤ 的时候,是把变量本⾝直接传递给了函数,这种调⽤函数的⽅式我们之前在函数的时候就知道了,这 种叫传值调⽤。
结论:实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实 参。
要将a、b的值传递给函数并对a、b的值进行修改,这个时候我们就可以使用传址调用了
#include <stdio.h>
void Swap2(int*px, int*py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = 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;
}
运行结果:
传址调⽤,可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量;所 以未来函数中只是需要主调函数中的变量值来实现计算,就可以采⽤传值调⽤。如果函数内部要修改 主调函数中的变量的值,就需要传址调⽤。
5、二级指针
指针也是一种变量,创建指针变量时内存也会分配内存单元和地址,而指针变量的地址就存放在二级指针中。
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a;
int** ppa = &pa;
return 0;
}
将a、pa、ppa之间的关系画图表示
*ppa就是对ppa中存储的地址解引用,找到pa
**ppa就是对ppa中存储的地址解引用,找到pa,再对pa解引用找到a