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 的十六进制地址,这两者不要混淆了。因为指针本质上也是一个变量,既然是变量,那么它在创建的时候,底层就会为其开辟内存。

1-1

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-2

注意事项:

内存是电脑上特别重要的存储器,计算机中程序的运行都是在内存中进行的 。所以为了有效的使用内存,就把内存划分成一个个小的内存单元,每个内存单元的大小是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;
}

输出结果:

1-3

总结:

从输出结果来看,指针类型决定了指针向前或者向后走一步有多大距离。当一个整型指针进行挪动的时候,移动 4 个字节;当一个字符指针进行挪动的时候,移动 1 个字节。这是一个很重要的知识点,因为这决定了一个指针一次性访问多少个字节。

1-4

3. 不同指针类型的解引用

① 对一个整型指针变量解引用后,并为之赋值。

1-5

② 对一个字符指针变量解引用后,并为之赋值。

1-6

总结:

从输出结果来看,指针类型也决定了指针进行解引用时能操作几个字节。当对一个整型指针变量解引用后,能操作 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;
}

输出结果:

1-7

程序清单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;
}

输出结果:

1-8

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;
}

输出结果:

1-9

注意事项:

① 从上面的输出结果来看," 指针 - 指针 " 运算适用于两个指针指向同一块空间才有意义。由于数组的内存地址是连续的,且由低到高变化,所以 " 指针 - 指针 " 运算就相当于数组下标之差。

② " 指针 - 指针 " 也可以理解为两个指针之间隔了多少个元素,其差值结果是一个数值,而不是字节。 这一点不能单纯的与指针变量 " 所占用内存的大小之差 " 的概念弄混淆了。

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 ’ 的地址。

② 分析第二个输出结果,当我们创建两个字符数组时,同样的常量字符串中的字符被放入了不同的数组,数组在栈区开辟了新的内存,所以两个数组首元素的地址是不同的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

十七ing

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值