目录
前言
在这篇文章中,我们一起来探讨一下C语言中很重要的一个知识——指针。指针为何如此重要?这是因为正是指针使得C语言威力无穷。有些任务用用其他语言也可以实现,但C语言能够更有效地实现;有些任务其他语言无法实现(例如直接访问硬件),但是C语言可以实现。然而,指针虽然很强大,与之相伴的风险也不小,其使用得当它们可以简化算法的实现;如果使用不当,它们就会引起错误,导致一些极难发现的问题。
一、指针是什么
1、内存
在学习指针之前我们先来了解一个知识—内存。在计算机中内存被划分为一块一块的小格子,即内存单元(可以理解成内存是一栋酒店,而内存单元就是酒店中的一个一个房间),为了更好地区分这些内存单元我们给内存单元编号。我们接下来讨论两个问题:
- 1)一个内存单元的大小是多大
- 2)如何给内存单元编号
1)一个内存单元的大小是多大
我们先来看第一个问题:一个内存单元的大小是多大?——根据内存的利用效率分析可以得出:一个内存单元的大小为:一个字节。
2)如何给内存单元编号
再来看来看第二个问题:计算机是如何给内存单元编号的呢? 是由电脑上的地址线通电产生电信号,再把电信号转换成数字信号,该数字信号为二进制序列。对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是1或者0;
那么32根地址线产生的地址就为:
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001
...
11111111 11111111 11111111 11111111
这里就有2的32次方个地址。每个地址标识一个字节,那我们就可以给: (2^32Byte == 2^32/1024KB ==2^32/1024/1024MB==2^32/1024/1024/1024GB == 4GB) 4G的空闲进行编址。
对于64位的机器来说运算过程同上。
到这里我们明白:
- 在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。
- 那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。
根据上面的知识我们就可以很好地理解了内存是什么。
2、指针变量
指针是什么?指针理解的2个要点:
- 指针是内存中一个最小单元的编号,也就是地址
- 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
指针变量——存放地址的变量,存放在指针中的值都会被当成地址处理。我们可以通过&(取地址操作符)取出变量的内存其实地址,把地址可以存放到一个变量中,这个变量就是指针变量,例如见下面的代码:
#include <stdio.h>
int main()
{
int a = 10;//在内存中开辟一块空间
int* p = &a;//这里我们对变量a,取出它的地址,可以使用&操作符。
//a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量中
//p就是一个之指针变量。
return 0;
}
总结
- 指针是用来存放地址的,地址是唯一标示一块地址空间的。
- 指针的大小在32位平台是4个字节,在64位平台是8个字节。
二、指针和指针类型
1、指针类型
我们都知道,变量有不同的类型,整形,浮点型等。那指针有没有类型呢?其实指针也是有类型的,例如在上面的例子中的指针变量p是int*类型的指针变量。指针变量有如下类型:
char* pc = NULL; — char*类型用于存放char类型变量的地址
int* pi = NULL; — int*类型用于存放int类型变量的地址
short* ps = NULL; — short*类型用于存放short类型变量的地址
long* pl = NULL; — long*类型用于存放long类型变量的地址
float* pf = NULL; — float*类型用于存放float类型变量的地址
double* pd = NULL; — double*类型用于存放double类型变量的地址
由此可以得出:指针的定义方式为:type + *
我们来看看每种指针类型的大小是多大:
通过上图我们发现所有的指针类型的大小都是4字节的,并且int类型的变量是可以存放于char*类型的指针类型的,那么指针类型存在的意义是什么呢?
2、指针类型的意义
1)解引用操作
指针类型存在的第一个意义是:
指针类型决定了指针进行解引用操作的时候能够访问空间的大小,如:
int* p; *p —> 4字节
char* p; *p —> 1字节
double* p; *p —> 8字节
我们通过代码来看看是否如上面所说:
a.我们先来看int类型的变量存入int*类型的指针类型后的解引用操作
解析:在调试中通过内存窗口进行观察,通过输入&num得到num的地址为0x007DF9A0,通过对其观察可得4个字节均发生了变化。
b.我们再来看int类型的变量存入char*类型的指针类型后的解引用操作
由上图可知当存入char*指针类型时,解引用操作后只能访问一个字节。
未来在给指针赋值的时候可根据情况选择合适的指针类型,例如:你想向后访问一个字节,那么就使用char*类型。对于为什么是倒着存储的,这涉及到大小端的知识这里不做过多讲解,后面的文章会详细介绍。
2)指针+-整数
指针类型的第二个意义:
指针类型决定了指针+1向后跳几个字节(步长),如:
int* —> 4字节
char* —> 1字节
见下面代码及运行结果:
#include <stdio.h>
int main()
{
int num = 0;
int* p1 = #
printf("%p\n", p1);
printf("%p\n", p1 + 1);
printf("----------\n");
char* p2 = #
printf("%p\n", p2);
printf("%p\n", p2 + 1);
return 0;
}
当int*类型+1时向后跳过4字节,而char*类型只跳过1字节。这就是指针类型在指针+-整数的使用。
3)指针类型意义的价值
根据我们所说的以上两条指针类型意义,我们可以使用在如下场景:
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int* p = arr;
//赋值
for (int i = 0; i < 10; i++)
{
*(p + i) = i;
}
//打印
for (int j = 0; j < 10; j++)
{
printf("%d ", arr[j]);
}
return 0;
}
上述代码便是应用了指针加整数的特点对数组进行赋值。
三、野指针
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
接下来我们一起来探讨野指针出现情况及解决方法
1、指针未初始化
1)错误代码实例
#include <stdio.h>
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
当指针默认为随机值,我们进行解引用操作时其访问的地址是随机的
2)解决方法
当我们定义一个指针变量时要记得初始化。不知道给新定义的指针赋什么初始值时,我们一般赋值为NULL。
2、指针越界访问
1)错误代码实例
#include <stdio.h>
int main()
{
int arr[10] = {0};
int *p = arr;
int i = 0;
for(i=0; i<=11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
当for循环进行到i=11时,指针p已经访问到数组外面,越界访问,如下图:
2)解决方法
小心指针越界,在使用for循环时注意条件的设置。
3、指针指向的空间未释放
1)错误代码示例
这个是由于我们自己开辟的空间(如使用malloc)使用后未进行释放(使用free)所导致的。比如该代码:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p=NULL;
p = (int*)malloc(sizeof(int));
*p = 10;
printf("%p,%d\n", p, *p);
printf("%p,%d\n", p, *p);
return 0;
}
2)解决方法
当我们使用如malloc等内存开辟函数在堆上开辟空间时,要记住使用free。可以养一个好的习惯,即写完malloc等内存开辟函数,紧接着写free并置指针为空后,再往中间写入其他代码。
4、使用前未验证指针的有效性
1)错误代码示例
#include <stdio.h>
int main()
{
int* p = NULL;
//....
int a = 10;
p = &a;
free(p);
*p = 20;
return 0;
}
2)解决方法
当代码比较长时,我们可能在前面代码中已经对指针进行制空,这使得在后面的代码中常常会忘记指针的有效性,因此在使用指针时我们可以进行验证指针有效性的操作。代码如下:
#include <stdio.h>
int main()
{
int* p = NULL;
//....
int a = 10;
p = &a;
if (p != NULL)
{
*p = 20;
}
return 0;
}
5、返回局部变量的地址
1)错误代码示例
我们都知道当代码运行到局部变量时会给变量在栈上开辟一块空间,当出了变量的作用域后会马上销毁,这就可能会发生:返回局部变量的地址后,该局部变量空间已被销毁。
#include <stdio.h>
int* test()
{
int a = 5;
return &a;
}
int main()
{
int* p = test();
*p = 20;
return 0;
}
2)解决方法
明白局部变量的作用域,避免返回函数中局部变量的地址。
四、指针的运算
1、指针 +- 整数
指针+-整数的知识我们已经在指针类型意义的应用提过一次了,这里我们使用例子为大家演示:
#include <stdio.h>
int main()
{
int arr[5] = { 0,1,2,3,4 };
int* p = arr;
for (int i = 0; i < 5; i++)
{
printf("%d ", *p);
p = p + 1;
}
return 0;
}
2、指针 - 指针
指针与指针相减得到的是两个指针之间的元素个数,但前提是这两个指针必须指向同一块空间,否则不能进行相减。
#include <stdio.h>
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
int num = 0;
num = &arr[9] - &arr[0];
printf("%d\n", num);
return 0;
}
该运行结果为:9,见下面的图示:
根据这一性质我们可以模拟实现一个库函数——strlen
#include <stdio.h>
int my_strlen(char* str)
{
char* star = str;
char* tail = str;
while (*tail != '\0')
{
tail = tail + 1;
}
return tail - star;
}
int main()
{
char arr[] = "hello world";
int num = 0;
num = my_strlen(arr);
printf("%d\n", num);
return 0;
}
3、指针的关系运算
指针的关系运算指的是两个指针之间比大小。比如下面的代码块:
for(vp = &values[N_VALUES]; vp > &values[0];)
{
*--vp = 0;
}
注意:
C语言标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
五、指针和数组
我们先来看一个例子:
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
printf("%p\n", arr);
printf("%p\n", &arr[0]);
return 0;
}
该代码的运行结果为:
由此我们可以得出一个结论:数组名表示的是数组首元素的地址
即:arr = &arr[0]
但有两种情况例外:
第一种情况:
&arr—&数组名—数组名不是首元素的地址—数组名表示整个数组—&数组名取出的是整个数组的地址。
第二种情况:
sizeof(arr)—sizeof(数组名)—数组名表示的整个数组—sizeof(数组名)计算的是整个数组的大小。
根据代码来测试以上内容的准确性:
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
printf("%p\n", arr);
printf("%p\n", arr + 1);
printf("----------\n");
printf("%p\n", &arr[0]);
printf("%p\n", &arr[0] + 1);
printf("----------\n");
printf("%p\n", &arr);
printf("%p\n", &arr + 1);
return 0;
}
由此可见我们上面的分析是正确的。
数组可以提过指针很好地进行访问。
六、二级指针
指针变量也是变量,是一个变量就有地址,那么存放指针变量的地址就需要二级指针来存放。
#include <stdio.h>
int main()
{
int a = 10;
int* p1 = &a; //p1是一个指针变量,存放a的地址
int** p2 = &p1;
//p2是一个二级指针,存放一级指针p1的地址
return 0;
}
我们可以提过下面的图示更好地理解二级指针:
对于二级指针的运算有:
- *p2 通过对p2中的地址进行解引用,这样找到的是 p1 , *p2 其实访问的就是 p1
int b = 20;
*p2 = &b;//等价于 p1 = &b;
- **p2 先通过 *p2 找到 p1 ,然后对 p1 进行解引用操作: *p1 ,那找到的是 a
**p2 = 30;
//等价于*p1 = 30;
//等价于a = 30;
七、指针数组
首先我们应该明白:指针数组到底是指针还是数组?— 指针数组是一个数组,用于存放指针的数组。
对比之前整型数组、字符数组我们来学习指针数组。
我们之前已经学过整型数组、字符数组,分别是用于存放整型变量、字符型变量:
int arr1[5];
char arr2[6];
一个小的知识点:一个数组的定义,去掉其数组名后剩下的部分为该数组元素个数以及该数组元素的类型。
那么同理,指针数组的定义为:
int* arr3[5];
根据上面上面的知识点可得:去掉数组名arr3得到的为,该数组元素个数为5个,每个元素的类型为int*,如下图:
指针数组的使用如下:
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
int c = 30;
int* arr[3] = { &a,&b,&c };
for (int i = 0; i < 3; i++)
{
printf("%d ", *(arr[i]));
}
return 0;
}
总结
在这篇初阶的文章中我们主要介绍了指针的一些基本概念以及一些基本用法,在下一篇文章中我们将会更深入地学习指针相关的知识。比如字符指针、数组指针、函数指针、函数指针数组等内容。