前言
相信每个人开始学C语言的时候,指针是最令人头疼的部分,今天蜡笔小欣带大家一起来大战指针“哥斯拉”,让你对指针有初步的了解。
一、指针
(一)指针定义
指针是内存中一个最小单元的编号,也就是地址,通过它能找到以它为地址的内存单元。简单来说指针就相当于一个门牌号,里面存的的是住户的编号。
(二)指针变量
我们平时口头说的指针,通常指的是指针变量,它是用来存放内存地址的变量。当你定义一个变量的时候,实际上是向内存申请了一块空间来存放你的变量。我们都知道 int 类型占 4 个字节,在计算机中数字都是用补码表示的。
int a = 666;
例如:666在计算机中换算成补码为:0000 0010 1001 1010
这里有 4 个byte,因此需要用四个单元格来存储。
如果我们想知道这个变量一开始存储的地址,就可以通过运算符&来取得变量实际的地址,这个值就是变量所占内存块的初始地址。
printf("%x",&a);
运行之后就会发现打印出来一串数字f3cffca4,这个就是定义整型变量a的初始地址。
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> int main() { int a = 666;//在内存中开辟一块空间 int* pa = &a;//对变量a,取出它的地址,使用&操作符 //a变量占用4个字节的空间,将a的4个字节中的第一个字节的地址存放在pa变量中, //pa就是一个之指针变量。 return 0; }
如上面代码所示,pa中存储的是a变量的内存地址,那我们该如何通过地址去获取a的值呢?
我们需要通过解引用的操作,在 C 语言中通过 * 就可以找到一个指针所指向地址的内容了。
简单来说pa里面是用户的门牌号(地址),而 *pa是通过这个地址找到了里面的住户(内容)。
(三)void*指针
void*类型的指针我们可以理解为没有具体类型的指针,它可以用来接受任何类型的地址,但无法直接进行指针的+-整数和解引用的运算。
int main() { int a = 6; char b = "bit"; void* p1 = &a;//int* void* p2 = &b;//char* *p1 = 20;//err void*类型的指针不能直接进行解引用操作 p1++;//err void*类型的指针也不能进行+-整数操作 return 0; }
二、指针类型和意义
(一)指针的类型
我们前面学过变量有许多类型,比如整型,浮点型等,指针变量也不例外,它有许多类型。
char* pa = NULL;
int* pb = NULL;
short* pc = NULL;
long* pd = NULL;
float* pe = NULL;
double* pf = NULL;
从上面我们可以看出,指针的定义方式是:type + *。
char*类型的指针是为了存放char类型变量的地址。
int*类型的指针是为了存放int类型变量的地址。
short*类型的指针是为了存放short类型变量的地址。
long*类型的指针是为了存放long类型变量的地址。
float*类型的指针是为了存放float类型变量的地址。
double*类型的指针是为了存放double类型变量的地址。
(二)指针类型的意义
我们一起运行下面的代码:
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> int main() { int x = 5; int* pa = &x; char* pb = (char*)&x;//将int*强转为char* printf("%p ", &x); printf("%p ", pa); printf("%p ", pa + 1); printf("%p ", pb); printf("%p ", pb + 1); return 0; }
运行结果:
我们发现 &x、pa、pb所得到的地址相同,因为&x 本来就是取x的地址,而pa、pb 是指针,保存了x的地址。与此同时你们是否有疑问:上面定义的指针变量明明一个是int类型,一个是char类型,为什么地址会一样呢?
为什么pa+1和pb+1的地址不同呢?
这就和指针的类型有关了。char* 类型的指针变量+1是跳过1个字节, 而int* 类型的指针变量+1就不一样,它是跳过4个字节。因此我们可以得出一个结论:指针的类型决定了指针向前或者向后??步有多?(距离)。
三、const修饰指针
(一)const修饰变量
const修饰的变量是不能被修改的。
const int a = 10; //a不能被修改了,但是a的本质还是变量,const只是在语法上做了限制,我们习惯上叫a为常变量 a = 20;
(二)const修饰指针变量
const修饰指针变量可以分为以下两种情况。
第一种情况:
int main() { const int a = 5; int const* p = &a;//const限制的是*p *p = 10;//err printf("a=%d", a); return 0; }
const放在 * 的左边,限的是*p,表示不能通过指针变量p去修改p指向的空间的内容
*p = 10; //err
但是p是没有受限制的
p=&b;//ok
第二种情况:
int main() { const int a = 5; int* const p = &a;//const限制的是p *p = 10;//ok printf("a=%d", a); return 0; }
const放在 * 的右边,限制的是p变量,它不能被修改,无法再指向其他的变量
p = &b;//err
但是*p是没有限制的,可以通过p修改p所指向的对象的内容
*p = 10;//ok
四、指针的基本运算
(一)指针+-整数
结合前面所学的知识我们知道数组是连续存放的,地址由低到高,我们只要知道第一个元素的地址,就能找到该数组其他元素的地址,下面举个栗子:
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 |
#define _CRT_SECURE_NO_WARNINGS #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加的是i*sizeof(int) //通过指针+-整数来找到数组后面的其他元素 } return 0; }
打印结果:1 2 3 4 5 6 7 8 9 10
(二)指针-指针
指针-指针其实就是地址-地址,在两个指针指向同一块空间的前提下,指针-指针的绝对值是两个指针之间的元素个数。
我们利用指针-指针来写一个my_strlen函数来求字符串长度。
代码如下所示:
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> int my_strlen(char* p) { char* start = p; while (*p != ' ') { p++; } return p - start;//指针-指针 } int main() { int len = my_strlen("abc"); printf("%d", len); return 0; }
运行结果如下 :
(三)指针的关系运算
指针的关系运算其实就是指针比较大小,也是地址比较大小,举个栗子:打印数组的内容
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> int main() { int arr[] = { 1,2,3,4,5,6,7,8,9,10 }; int sz = sizeof(arr) / sizeof(arr[0]); //使用while循环打印arr的内容 int* p = &arr[0]; //arr是数组名,数组名就是数组首元素的地址,arr<==>&arr[0] while (p < arr + sz) { printf("%d ", *p); p++; } return 0; }
运行结果: 1 2 3 4 5 6 7 8 9 10
通过指针的关系运算,我们可以更加方便地打印数组的内容。
五、野指针
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。
(一)野指针成因
1.指针未初始化
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> int main() { int* p; //指针变量未初始化,系统默认为随机值 *p = 10; return 0; }
局部变量p没有初始化,变量的值是随机的,无法通过p找到相应的空间地址,就变成野指针。
2.数组越界访问
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> int main() { int arr[5] = { 0 }; int* p = arr; int i = 0; for (i = 0; i < 10; i++) { *p = 1; p++;//指针指向的范围超出数组arr的范围,p就变成野指针 } return 0; }
运行结果:
由于数组arr定义有5个元素,对这5个元素(下标为0 到4的元素)的访问都合法,如果对这5个元素之外的访问,就是非法的,导致数组越界访问,也会造成p变为野指针。
3.指针指向空间释放
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> int test() { int n = 6; return &n; } int main() { int* p = test(); printf("%d", *p); return 0; }
我们在函数中定义的变量是临时变量,只要出了这个函数的作用域就会自动销毁。销毁后系统没办法访问这个空间地址,但我们通过指针还能在内存里找到这个空间,这就会非法访问,造成野指针。
(二)规避野指针的方法
1.指针初始化
#include <stdio.h> int main() { //第一种情况 int a = 6; int* p = &a;//明确p应该指向a,把a的地址初始化 //第二种情况 //不知道给指针初始化谁的地址,直接用空指针初始化 int* p = NULL; return 0; }
2.小心指针越界
明确一个程序向内存申请了哪些空间,使用指针访问空间时不能超出访问范围,超出了就会造成越界访问。
3.指针指向空间释放后,要及时置NULL
我们在平时编程时,对空指针很容易检测(if(p==NULL)),但是对于非法指针p不为空,我们是无法检测到的。防止对一个已经释放的指针多次释放造成程序崩溃,但是对一个NULL指针多次释放是合法的。因此我们在指针指向空间释放后,要及时置NULL。
六、总结
通过上面对指针的初步学习,相信大家已经掌握了战胜指针“哥斯拉”的第一招,后面再和蜡笔小欣一起学习战胜指针“哥斯拉”的其他招式,我们下期再见!