C语言:大战指针“哥斯拉”(1)

前言

相信每个人开始学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。

六、总结

通过上面对指针的初步学习,相信大家已经掌握了战胜指针“哥斯拉”的第一招,后面再和蜡笔小欣一起学习战胜指针“哥斯拉”的其他招式,我们下期再见!