C语言 指针
引言
1. 什么是指针
1. 指针是内存中一个最小单元的编号,通俗的说,指针也就是地址。
2. 平时口语中说的指针,通常指的是指针变量,即存储一块内存地址的一个变量。
3. 在 32位 的机器上,地址是 32个 0或1 组成二进制序列,此时地址就得用 4 个字节的空间来存储,所以此时一个指针变量的大小就应该是 4 个字节。同样地,在 64位 的机器上,地址是 64个 0或1 组成二进制序列,此时地址就得用 8 个字节的空间来存储,所以此时一个指针变量的大小就应该是 8 个字节。
综上所述,指针即指针变量,它占用内存大小要么为 4,要么为 8.
2. 简单认识指针
经过上面的介绍,我们就来简单地认识下图的指针。
下面的两行代码,我画了一幅图来解释它。我们可以说,指针变量 pa 指向 整型变量 a. 也可以说,指针变量 pa 存储了变量 a 的地址。
备注: 0x11332244 是 a 的十六进制地址,0x00001111 是 pa 的十六进制地址,这两者不要混淆了。因为指针本质上也是一个变量,既然是变量,那么它在创建的时候,底层就会为其开辟内存。
3. 取地址符 & 和解引用 * 符
int a = 10;
int* pa = &a; // 将 a 的地址赋给 pa
*pa = 20; // 将 a 的值改为 20
① int* 表示 pa 是一个整型指针变量。
② *pa 表示解引用 指针变量 pa,*pa 就等价于 a.
③ 通俗的来说,解引用符和取地址符是可以充当 " 抵消的作用 " 。
*pa <==> *(&a) <==> a
一、指针与内存
指针就是地址,有了地址,就能帮助我们快速地找到一块内存空间。
程序清单:
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a; // 取出 a 的地址赋值给指针变量 pa
*pa = 20; // *pa == a
printf("%d\n", a);
return 0;
}
// 输出结果:20
在上面的程序中,&a 表示取出 int变量 a 的地址 (取出的是 变量a 的第一个字节地址);*pa 表示解引用 pa,*pa 就等价于 a.
如下图所示:(假设虚拟地址空间为 32位)
注意事项:
内存是电脑上特别重要的存储器,计算机中程序的运行都是在内存中进行的 。所以为了有效的使用内存,就把内存划分成一个个小的内存单元,每个内存单元的大小是1个字节。为了能够有效访问到内存的每个单元,就给内存单元进行了编号,这些编号被称为该内存单元的地址。在 C语言中,每创建一个变量就会在底层开辟地址。
① 内存会被划分为小的内存单元,一个内存单元的大小是1个字节。
② 每个内存单元都有编号,这个编号也被称为:地址 / 指针。
③ 地址 / 指针可以存放在一个变量中, 这个变量称为指针变量,指针变量也是一个变量,它也有自己的地址。
④ 通过指针变量中存储的地址,就能找到指针指向的空间。
二、指针类型的存在意义
1. 指针变量的大小
程序清单:
#include <stdio.h>
int main()
{
int a = 10;
char ch = 'a';
double d = 3.14;
int* pa = &a;
char* pc = &ch;
double* pd = &d;
printf("%d\n", sizeof(pa)); //4
printf("%d\n", sizeof(pc)); //4
printf("%d\n", sizeof(pd)); //4
return 0;
}
结论:
指针变量是用来存放地址的。所以,地址的存放需要多大空间,指针变量的大小就应该是多大。
① 32位 机器,支持 32位 虚拟地址空间,其产生的地址就是 32位,所以此时指针变量就需要 32位 的空间存储,即 4字节。
② 64位 机器,支持 64位 虚拟地址空间,其产生的地址就是 64位,所以此时指针变量就需要 64位 的空间存储,即 8字节。
2. 指针移动
程序清单:
#include <stdio.h>
int main() {
int a = 3;
char ch = 'a';
int* pa = &a;
char* pc = &ch;
printf("%p\n", pa);
printf("%p\n", pa + 1);
printf("%p\n", pc);
printf("%p\n", pc + 1);
return 0;
}
输出结果:
总结:
从输出结果来看,指针类型决定了指针向前或者向后走一步有多大距离。当一个整型指针进行挪动的时候,移动 4 个字节;当一个字符指针进行挪动的时候,移动 1 个字节。这是一个很重要的知识点,因为这决定了一个指针一次性访问多少个字节。
3. 不同指针类型的解引用
① 对一个整型指针变量解引用后,并为之赋值。
② 对一个字符指针变量解引用后,并为之赋值。
总结:
从输出结果来看,指针类型也决定了指针进行解引用时能操作几个字节。当对一个整型指针变量解引用后,能操作 4 个字节;当对一个字符指针变量解引用后,能操作 1 个字节。
三、指针运算
1. 指针加减整数
程序清单1
#include <stdio.h>
int main() {
int a = 3;
int* pa = &a;
printf("%p\n", pa);
printf("%p\n", pa + 1);
printf("%p\n", pa - 1);
return 0;
}
输出结果:
程序清单2
#include <stdio.h>
void print(int arr[]) {
for (int i = 0; i < 10; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
print(arr);
for (int i = 0; i < 10; i++) {
*p = 0; // 将数组的每一个元素都设置成 0
p++; // 将指针往后挪动一个元素
}
print(arr);
return 0;
}
输出结果:
2. 指针 - 指针
程序清单:
#include <stdio.h>
int main() {
int arr[10] = { 0 };
int* p = NULL;
printf("%d\n", &arr[8] - &arr[1]);
printf("%d\n", &arr[1] - &arr[8]);
printf("%d\n", &arr[8] - p);
return 0;
}
输出结果:
注意事项:
① 从上面的输出结果来看," 指针 - 指针 " 运算适用于两个指针指向同一块空间才有意义。由于数组的内存地址是连续的,且由低到高变化,所以 " 指针 - 指针 " 运算就相当于数组下标之差。
② " 指针 - 指针 " 也可以理解为两个指针之间隔了多少个元素,其差值结果是一个数值,而不是字节。 这一点不能单纯的与指针变量 " 所占用内存的大小之差 " 的概念弄混淆了。
3. 指针关系运算
程序清单:
#include <stdio.h>
int main() {
int arr[10] = { 10,9,8,7,6,5,4,3,2,1 };
if (arr[1] >= arr[3]) {
printf("haha\n");
}
else {
printf("hehe\n");
}
// 指针关系运算
// 随着数组下标增长,数组的地址由低到高变化
if (&arr[1] >= &arr[3]) {
printf("haha\n");
}else {
printf("hehe\n");
}
return 0;
}
// 输出结果:
// haha
// hehe
四、二级指针
二级指针即指针的指针,它存放的是指针变量的地址。一级指针的取地址、解引用等操作,也可以类比到此处的二级指针。
程序清单:
#include <stdio.h>
int main() {
int a = 10;
int* pa = &a;
int** ppa = &pa;
**ppa = 20;
printf("%d\n", a);
return 0;
}
// 输出结果:20
五、野指针
野指针:指针指向的位置是不可知的、随机的、不正确的、没有明确限制的。
程序清单1
指针 p 没有指向任何地址。所以 p 中存放的可能是一个随机地址,或者说, 指针 p 随意指向了内存中的一块区域,如果我们再次对指针 p 解引用,就会造成非法访问。
#include <stdio.h>
int main()
{
int* p; //局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
程序清单2
指针访问数组越界。这就好像我们给指针 p 只规定了一块限定区域,超出这个区域,它就访问到了 " 无名区 "。
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int* p = arr;
int i = 0;
for (i = 0; i < 20; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
如何避免野指针问题
1. 当指针在定义时,不知道指向谁,初始化为 NULL.
2. 预防指针越界。
3. 使用指针前,进行 assert 断言。
#include <stdio.h>
#include <assert.h>
int main() {
int a = 10;
int* pa = &a;
int* p = NULL;
assert(pa != NULL);
assert(p != NULL); // 编译器直接提示报错信息
}
注意事项:
① assert 在使用时需要引入头文件 <assert.h>
② 如果 assert 括号内的条件为真,则程序正常执行;如果它括号内的条件为假,则会直接报错,并提示错误信息,精确到行。
六、字符指针
字符指针通常与字符串相关联,这里需要明确的是,字符指针通常存储的是字符串中的首个字符的地址,而不是整个字符串的地址。
程序清单1
#include <stdio.h>
int main() {
char* p = "abcdef";
// p 指向字符串的第一个字符
printf("%c\n", *p);
printf("%s\n", p);
return 0;
}
// 输出结果:
// a
// abcdef
注意事项:
① 需要明确: 指针 p 指向 " abcdef " 的第一个字符 ’ a ’ 的地址,而不是整个字符串的地址。或者说,指针 p 中存放的字符 ’ a ’ 的地址。
② 针对上面的第二个输出结果,为什么对一个字符指针变量打印就能够输出整个字符串呢?原因在于:指针 p 指向第一个字符,就能够找到整个字符串后面的所有字符。这和顺藤摸瓜是一个道理。
③ 我们日常所说的字符串其实是一个常量字符串,放在常量区,不可被修改。所以当我们创建一个字符指针,用于指向一个字符串时,就可以将这个指针变量添加 const 修饰符,这样更加规范。
const char* p = "abcdef";
程序清单2
#include <stdio.h>
int main() {
char* p1 = "abcdef";
char* p2 = "abcdef";
char arr1[] = "abcdef";
char arr2[] = "abcdef";
if (p1 == p2) {
printf("p1 == p2\n");
}else {
printf("p1 != p2\n");
}
if (arr1 == arr2) {
printf("arr1 == arr2\n");
}else {
printf("arr1 != arr2\n");
}
return 0;
}
// 输出结果:
// p1 == p2
// arr1 != arr2
注意事项:
① 分析第一个输出结果,当我们创建两个字符指针时,它们指向的都是字符串的首字符地址,而字符串又是常量字符串,不可被更改,所以,p1 和 p2 都指向同一份 ’ a ’ 的地址。
② 分析第二个输出结果,当我们创建两个字符数组时,同样的常量字符串中的字符被放入了不同的数组,数组在栈区开辟了新的内存,所以两个数组首元素的地址是不同的。