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