深入理解指针(2)

1.const修饰指针

现有一个变量int a=0;如果我们想改变他的值,直接进行赋值操作a=1即可。但是如果我们在int a的前面用const进行修饰(const可以修饰变量);再想通过赋值操作进行修改,则发现编译不通过,“表达式必须是可修改的左值”,此时的a称作常变量,也就是不能修改的变量。

虽然表面上不可以改,但是如果我们通过&a找到a的地址,在利用解引用去修改是可以的。 但注意虽然这样的操作是可以达到目的的,但是不建议这样操作,是打破语法规则的行为。

#include <stdio.h>

int main()
{
 const int n = 0;
 printf("n = %d\n", n);
 int*p = &n;
 *p = 20;
 printf("n = %d\n", n);
 return 0;
}

接下来介绍重点,此时const修饰的是特殊的变量指针变量。int*pa=&a;有3种修饰方法,分别是

const int*pa=&a;int*const pa=&a; const int*const pa=&a;也就是在*前修饰,*后修饰,*前后都修饰。第一种不允许改变*pa的值,也就是不允许改变a的值;第二种是不允许改变pa的值,也就是不允许改变a的地址;第三种就是前二者的结合,即不允许改变a的值,也不能改变a的地址。

这样一来,我们既没法通过解引用去改变a的值,也没办法修改a的地址,就可以增强函数的鲁棒性。

2.指针的运算

指针的运算分成三类:指针+-整数,指针-指针,指针的关系运算。

1.指针+-整数

因为数组在内存中是连续存放的,只要知道第⼀个元素的地址,顺藤摸⽠就能找到后⾯的所有元素。而不同类型的指针+1时跳过的字节大小是不同的,在32位机器中,int*pa跳过4字节,char*跳过1字节,但是都时跳到数组中的下一个元素,因为在数组内部,地址随下标的增大而变高。,即&arr[0]+1=&arr[1];&arr[5]-2=&arr[3]以此类推。

2.指针-指针

指针减指针绝不是地址相减,而是地址相减之后除以sizeof(arr[0]);也就是两个地址之间元素的个数。下面模拟库函数中strlen的实现。

#include <stdio.h>
int my_strlen(char *s)
{
 char *p = s;
 while(*p != '\0' )
 p++;
 return p-s;
}
int main()
{
 printf("%d\n", my_strlen("abc"));
 return 0;
}

其中的p++不是指针的地址+1,时指针的地址+sizeof(arr[0]),从而跳到下一元素的地址。返回的p-s也就是两个地址之差除以sizeof(arr[0]),就得到第一个元素与\0之间的1元素个数,就是字符的长度。

 3.指针的关系运算

顾名思义,指针的关系运算就是比较两个地址之间的高低,高地址大于低地址。在创建变量的时候,先创建的变量占据高地址,后创建的变量占据低地址,而在一个数组中是相反的,下标小的元素占据低地址,下标大的元素占据高地址。比如我先创建int a,在创建int b,最后创建char arr[3]

就有如下图所示的空间开辟规律:

#include <stdio.h>
int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9,10};
 int *p = &arr[0];
 int sz = sizeof(arr)/sizeof(arr[0]);
 while(p<arr+sz) //指针的⼤⼩⽐较
 {
 printf("%d ", *p);
 p++;
 }
 return 0;
}

3.野指针

野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的),并不是我们拿到的所有的地址都是合法的,int a=0;&a这样得到的地址当然是合法的,但是有一些来路不明的内存中的地址,我们没有访问的权限,但是我们强行解引用或其他操作就是不合法的。

下面举几个野指针的例子:

1.创建指针但未初始化

#include <stdio.h>
int main()
{ 
 int *p;//局部变量指针未初始化,默认为随机值
 *p = 20;
 return 0;
}

这样得到的p就是一个野指针,在vs编译器中无法通过编译,但在其他有一些编译器中可以通过。默认出的随机值时内存中的随机地址,这样的地址我们没有访问权限。

2.数组的越界访问

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

3. 指针指向的空间释放(局部变量的开辟的临时空间归还给内存)

#include <stdio.h>
int* test()
{
 int n = 100;
 return &n;
}
int main()
{
 int*p = test();
 printf("%d\n", *p);
 return 0;
}

这里的n时函数中的局部变量,在栈区中开辟空间后,出了函数便销毁,将这部分空间归还于内存。虽然我们取到了n的地址,但是我们去用它的时候已经丧失了访问的权限了。

4.如何规避野指针

我们可以采取有效的措施防止野指针。

指针初始化 如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL. NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,解引用该地址 会报错。

但是我们不能单纯的让程序出现问题,应该有什么防护措施,这里就需要引入assert断言。assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报 错终⽌运⾏。这个宏常常被称为“断⾔”。程序运⾏到这⼀⾏语句时,验证变量 p 是否等于 NULL 。如果确实不等于 NULL ,程序 继续运⾏,否则就会终⽌运⾏,并且给出报错信息提⽰。 assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零), assert() 不会产⽣ 任何作⽤,程序继续运⾏。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误 流 stderr 中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号。

#include <stdio.h>
#include<assert.h>
//断言assert
int main()
{
	int* pa = NULL;
	assert(pa != NULL);
	*pa = 100;
	return 0;
}

可以看到assert在发现不符时,及时地报错,并且会告诉用户出错的位置(项目,文件与行数)。如果已经确认程序没有问 题,不需要再做断⾔,就在 #include 语句的前⾯,定义⼀个宏 NDEBUG 。这样的话assert就完全失去了效果。

#define NDEBUG
#include <assert.h>

4.传址调用(给函数传地址)

所以说有什么地方一定要用到指针的吗,下面编写一个函数实现两个元素交换值。

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

我们发现swap1函数并没有实现将a与b的值交换,我们取出a,b,x,y的地址就可以发现四者的地址都不相同,所以x与y只是a与b的一份临时拷贝,改变x与y的值并不能改变a与b的值。因此我们就要给函数传递a与b的地址。于是有以下swap2函数。

void Swap2(int*px, int*py)
{
 int tmp = 0;
 tmp = *px;
 *px = *py;
 *py = tmp;
}
int main()
{
 int a = 0;
 int b = 0;
 scanf("%d %d", &a, &b);
 printf("交换前:a=%d b=%d\n", a, b);
 Swap2(&a, &b);
 printf("交换后:a=%d b=%d\n", a, b);
 return 0;
}

传址调⽤,可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量;所 以未来函数中只是需要主调函数中的变量值来实现计算,就可以采⽤传值调⽤。如果函数内部要修改 主调函数中的变量的值,就需要传址调⽤。

  • 12
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值