指针三部曲(一)

本文详细介绍了指针的概念,包括内存管理、取地址操作、指针变量、解引用、const修饰、指针运算(如指针加减整数、指针间的运算)、野指针的产生与避免、使用assert断言检查指针状态以及函数中的传址调用。
摘要由CSDN通过智能技术生成


指针第一曲即将开讲,别走神~

1. 内存

  在计算机中,为了高效管理内存空间,就把内存空间划分为一个个的小内存单元,一个内存单元占用一个字节(1 byte = 8 bit )。
举个简单的例子,一栋宿舍楼就是整个内存,每一间学生宿舍就是一个内存单元( 1 byte ),学生宿舍里面的床位就是比特位( bit )。

2. 取地址操作符(&)

  现在大多数人都有网购的行为,而想要拿到网购的物品,就需要商家通过快递的方式邮寄。邮寄最重要的一点就是填写地址,快递员会根据地址送到指定位置。计算机也同样会通过地址对变量进行操作。(创建变量就是向内存申请空间,变量存放的位置就是地址)

所以可以说指针就是地址

  想要查看变量的地址,就要用到取地址操作符( & ),看到 & 可不要忘记操作符中的双目操作符 & (按位与),逻辑运算符逻辑与( && )。自己要注意区分哦
  
下面看代码:

#include<stdio.h>
int main()
{
	int x = 1;
	&x;     //取 x 的地址
	printf("%p", &x); //%p是打印地址的占位符
	return 0;
}

运行结果:

00000044C47BF594

图解:
取地址操作符

创建一个整型变量( int ),向内存申请一块空间,占用四个字节,而 x 取地址取的是地址较小的地址,并不会全部取出(计算机会根据取到的地址接着向后访问,访问到 4 个字节)。

3. 指针变量

  当我们取了地址之后,为了方便后续的使用,就需要创建变量存储这个地址,这个变量叫做指针变量
指针变量是变量,作用是存放地址,一般表现形式为:

类型 * 变量名

例如:int* p 、char* p 、void* p ,重点是变量名前的 *

#include<stdio.h>
int main()
{
	int x = 1;
	int* p = &x;   //存放 x 的地址
	printf("%p", p); // p 中是 x 的地址
	return 0;
}

3.1. 解引用操作符( * )

  当我们有了变量的地址,要找到该变量就轻而易举了,更改变量的值也可以实现,不过需要借助解引用操作符( * )。

#include<stdio.h>
int main()
{
	int x = 1;
	int* p = &x;
	*p = 666;    //解引用 p 赋值
	printf("*p=%d\n", *p);
	printf(" x=%d\n",  x);
	return 0;
}

输出结果为:

*p=666
 x=666  // *p = x

由此可见,对地址解引用之后就可以对该地址所对应的变量进行赋值了。

3.2 指针变量大小

  指针变量作为一个变量,肯定是有大小的。但是指针变量的大小与类型无关,指针类型在相同平台上,大小是相同的。

#include<stdio.h>
int main()
{
	printf("%zd\n", sizeof(short*));//sizeof 计算字节数
	printf("%zd\n", sizeof(char*));
	printf("%zd\n", sizeof(int*));
	printf("%zd\n", sizeof(double*));
	return 0;
}

32位平台运行结果:

4
4
4
4

64位平台运行结果:

8
8
8
8

4. const

  const是一个关键字,const 可以修饰指针变量,起到一定的限制作用。

const

  1. 当 const 在 * 左边时,限制了解引用操作,不能修改指针指向的内容(值)。
  2. 当 const 在 * 右边时,限制了解引用操作,不能修改指针变量的内容(地址)。
  3. 如果 * 两边都有 const ,那就都不能修改了。

判断小 tips:1.只需观察 const 右边,从 const 右边开始向后观察,如果先看到 * ,那么就是限制 *p;
      2.如果先看见变量名 p ,那么就限制了指针变量 p 。

5.指针的运算

5.1 指针 ± 整数

例1:

#include<stdio.h>
int main()
{
	int a = 10;
	int* pa = &a;

	char b = 'x';
	char* pb = &b;

	printf("pa  =%p\n", pa);
	printf("pa+1=%p\n", pa+1);
	printf("pb  =%p\n", pa);
	printf("pb+1=%p\n", pb+1);

	return 0;
}

运行结果1:

pa  =0000000158BBF654
pa+1=0000000158BBF658  //整型 pa 加一后跳过 4 个字节;
pb  =0000000158BBF654
pb+1=0000000158BBF695  //字符型 pb 加一后跳过 1 个字节;

int 是 4 个字节,char 是 1 个字节。

例2:

#include<stdio.h>
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]); //计算数组中元素个数
	int* p = arr;     //arr 代表首元素地址
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));  //首元素地址加 i 逐个遍历数组,最后解引用
	}
	return 0;
}

运行结果2:

1 2 3 4 5 6 7 8 9 10

*( p + i )等同于 p [ i ] 等同于 arr [ i ] (而 p 又等同于 arr ,p 与 arr 可以互换)

以上只演示了加法,减法可以类推得到,也可以自行尝试。

5.2 指针 - 指针

注意:指针 - 指针的前提是指针指向同一空间。
例:

#include<stdio.h>
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int p = &arr[9] - &arr[5];    //指针 - 指针
	printf("%d", p);
	return 0;
}

运行结果:

4

5.3 指针关系运算

例:

#include<stdio.h>
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;
	int sz = sizeof(arr) / sizeof(arr[0]);  //计算数组元素个数
	while (p < arr + sz)  //指针的关系运算
	{
		printf("%d ", *p);
		p++;
	}
	return 0;
}

运行结果:

1 2 3 4 5 6 7 8 9 10

6. 野指针

  野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
既然称之为野指针,表面意思上就表明该指针是 “ 野生 ” 的,不规范,不标准。

6.1 指针未初始化

#include<stdio.h>
int main()
{
	int* p;  //指针未初始化
	*p = 10;

	printf("%d", *p);
	return 0;
}

既然要存放数值,就应该向内存申请一块空间,而指针可没有这功能。最后运行就会报错(使用了未初始化的局部变量 “ p ”)。

6.2 指针越界访问

#include <stdio.h>
int main()
{
	int arr[10] = { 0 };
	int* p = &arr[0];
	for (int i = 0; i < 11; i++)//超出数组范围
	{
		*(p++) = i;   // p[10] 就是野指针
	}
	return 0;
}

指针越界访问,运行后会崩!!!

6.3 指针指向已销毁的空间

#include<stdio.h>
int ret()
{
	int n = 10;
	return &n; //返回了 n 的地址后,内存还给系统
}
int main()
{
	int* p = ret();  //调用 ret 函数
	printf("%p", *p);//想要打印 n 的值,会不会成功呢?
	return 0;
}

以上代码最后什么都不会打印,因为在 ret 函数中,最后返回了 n 的地址,而 n 的生命周期只在 ret 函数内,出了 ret 函数就销毁了,内存还给系统,主函数中的 *p 找不到 n 。所以 printf 想要打印 n 的值,就不会成功!

6.4 规避野指针

6.4.1 初始化野指针

对野指针初始化,只需要给野指针赋值NULL即可。

int* p = NULL;

6.4.2 空间释放时置NULL

#include<stdio.h>
int ret()
{
	int n = 10;
	return &n;
}
int main()
{
	int* p = ret();
	p = NULL;  //及时置NULL
	printf("%d ", *p);
	return 0;
}

以上方法虽然规避了野指针,但是本质上还是野指针,置 NULL 只是将野指针限定了一个范围,但是任然改变不了野指针危险的本质。所以我们在使用指针时一定要擦亮眼睛,遇到野指针 “绕着走” 。

7. assert断言

  assert宏定义在头文件 <assert.h> 中,我们先了解代码的使用。

代码1:

#include<stdio.h>
//#define NDEBUG
#include<assert.h>
int main()
{
	int a = 10;
	//int* p = &a;
	int* p = NULL;
	assert(p != NULL);  //assert断言
	printf("%d", a);
	return 0;
}

运行结果1:

Assertion failed: p != NULL, file ( 此文件路径 ), line ( assert 当前所在行数)

error
  
  
代码2:

#include<stdio.h>
#define NDEBUG
#include<assert.h>
int main()
{
	int a = 10;
	//int* p = &a;
	int* p = NULL;
	assert(p != NULL);
	printf("%d", a);
	return 0;
}

运行结果2:

10

  
  
代码3:

#include<stdio.h>
//#define NDEBUG
#include<assert.h>
int main()
{
	int a = 10;
	int* p = &a;
	//int* p = NULL;
	assert(p != NULL);
	printf("%d", a);
	return 0;
}

运行结果3:

10

综上,

  1. assert 语句中的表达式如果为真,assert 语句就会执行它断言的功能。
  2. assert 语句中的表达式如果为,assert 语句就不会执行它断言的功能。
  3. assert 语句还有一个开关,就是代码 2 中的 NDEBUG ,NDEBUG 必须定义在 <assert.h> 头文件之前才能正常使用,有了 #define NDEBUG,相当于就没有了 assert 断言语句。

8. 指针的传址调用

  之前在函数一章中讲到的是传值调用,而指针自然是进行传地址调用。
下面就观察一下两组代码(使用函数实现常量的交换):

代码1:

#include<stdio.h>
void swap(int x,int y)
{
	int tmp = x;
	x = y;
	y = tmp;
}
int main()
{
	int a = 30;
	int b = 60;
	printf("交换前:");
	printf("  a=%d  ", a);
	printf("  b=%d  ", b);
	swap(a, b);
	printf("\n");
	printf("交换后:");
	printf("  a=%d  ", a);
	printf("  b=%d  ", b);
	return 0;
}

代码2:

#include<stdio.h>
void swap(int* x, int* y)  //使用 int* 接收
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}
int main()
{
	int a = 30;
	int b = 60;
	printf("交换前:");
	printf("  a=%d  ", a);
	printf("  b=%d  ", b);
	swap(&a, &b);  //传地址
	printf("\n");
	printf("交换后:");
	printf("  a=%d  ", a);
	printf("  b=%d  ", b);
	return 0;
}

以上两组代码唯一的区别在于 swap 函数中形参和实参,代码1中是传的是数值,代码2中是传的是地址。
解析:
代码1中 x、y 的地址和主函数中 a、b 的地址不同,x、y 处在一个独立的空间,并不会影响函数外 a、b 的值,所以交换不会成功。而代码2传递地址,就间接地交换了 a、b 的值了。
最后真正能够实现交换的是代码2

💖指针三部曲传送门

  1. 指针(一)
  2. 指针(二)
  3. 指针(三)
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值