一.指针的基本定义及相关操作
1.指针变量的理解和基本操作
int main()
{
int a = 10;
&a;//用&取地址操作符取出a的地址
int* pa = &a;
*pa = 200;
return 0;
}
创建变量的本质就是在内存中申请空间。
如:int型申请4个字节,用于存放数值。这四个字节都有自己的编号(即地址),可以通过编号找到内存单元。
pa是一个用于存放地址的变量,其类型为int*(其他数据类型同理)。
注意:
1.指针变量是存放地址的变量,地址的大小多大,指针变量的大小就是多大(大小为4(32位机器下)/8(64位机器下)个字节,与类型无关)。
2.只要是指针变量,在相同平台下大小相等。
3.void* -----> 无具体类型的指针,可以接收任意类型的指针。
2.解引用操作符
int main()
{
int a = 10;
int* pa = &a;
*pa = 200;
printf("%d", a);//输出结果为200
return 0;
}
在上述代码中,我们可以通过 * — 解引用操作符,利用*pa(pa必须是指针变量)a所在的地址,并将200存放到a的地址中。
(int类型的 * (解引用)可以访问4个字节,char类型的 * 只能访问一个字节,void类型的 * 不能进行解引用操作!!!)
const修饰变量及其与指针的关系
int main()
{
//const修饰普通变量:
const int n = 100;//num的值不可被修改
int arr[n];//错误代码,编译器会报错
int* p = &n;
*p = 200;//此时可以通过n的地址绕过const修改n的值
printf("%d ",n);//输出值为200
return 0;
}
const修饰的变量叫:常变量
本质上还是变量,只是不能被修改。
常变量不可用在数组的定义中!!!
运用指针,可以通过查找n所在的地址,直接修改地址中的元素以达到绕过const修改其修饰的变量的目的(最好不使用)。
int main()
{
//const修饰指针变量:
int n = 10;
int m = 20;
//当const位于*的左边时
int const* p = &n;//也可以写成const int* p = &n;
*p = 20;//报错
p = &m;//可以实现
//当const位于*的右边时
int* const pa = &n;
*pa = 20;//可以实现
pa = &m;//报错
return 0;
}
当const位于 * 左侧时:
限制指针所指向的内容,也就是不能通过指针变量来修改其所指向的内容。
但是指针指向的地址是可以改变的。
当const位于 * 右侧时:
限制指针变量本身,即指针不能改变其所指向的地址。
但是可以通过指针变量修改它所指向的内容。
3.指针运算
1.指针加减整数
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
int i=0;
for(i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
//1 2 3 4 5 6 7 8 9 10
}
int* p = &arr[0];
for(i = 0; i < sz; i++)
{
printf("%d ",*p);
p++;//效果与printf("%d ", *(p + i));相同
//1 2 3 4 5 6 7 8 9 10
}
return 0;
}
上下两个for循环的函数实现相同的功能的原因:
(1)指针类型决定指针+1的步长,决定了指针解引用的权限。
(2)数组再内存中连续存放。
(加减原理一样,都可以通过指针实现)
2.指针 - 指针(指针 + 指针没有意义)
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
// 0 1 2 3 4 5 6 7 8 9
printf("%d\n", &arr[9] - &arr[0]);
//输出结果为9,即{ 1,2,3,4,5,6,7,8,9 }9个元素
return 0;
}
指针-指针的绝对值是指针和指针之间元素的个数。
(指针-指针的前提条件是两个指针指向同一块空间)
1.指针 - 指针的实际应用之模拟实现strlen函数
strlen函数:
int main()
{
char arr[] = "abcedfg";
size_t len = strlen(arr);//数组名为数组首元素的地址
printf("%zd\n", len);//strlen统计的是字符串中'\0'之前的字符个数
return 0;
}
模拟实现:
size_t my_strlen1(char* p)//遍历数组的方式
{
size_t count = 0;
while (*p != '\0')//=while(*p)
{
count++;
p++;
}
return count;
}
size_t my_strlen2(char* p)//指针 - 指针的方式
{
char* start = p;//start == p[0];
char* end = p;
while (*end != '\0')//=while(*end)
{
end++;
}
return end - start;
}
int main()
{
char arr[] = "abcdefg";
size_t len = my_strlen1(arr);
size_t len1 = my_strlen2(arr);
return 0;
}
上述两种方法都可以模拟实现strlen函数。
3.指针的关系运算(即比较大小)
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = &arr[0];
while (p < &arr[sz])//指针的关系运算
{
printf("%d ", *p);
p++;
}
return 0;
}
前提是两个指针指向同一块空间。
在此前提下:比较 &arr[n] 和 &arr[m] 中 n 和 m 的大小关系即可,n 和 m 的大小关系即是 &arr[n] 和 &arr[m] 的大小关系。
4.野指针
成因:
1.如果一个局部变量不初始化,它的值是随机的。
int main()
{
int* p;
*p = 20;//此时p就是野指针
return 0;
}
int* p;
中p是局部变量,但没有初始化,其值是随机的。如果把p中存放的值当作地址,解引用操作符就会形成非法访问。
2.指针的越级访问也会形成野指针
int main()
{
int arr[5] = { 1,2,3,4,5 };
int* str = arr;
printf("%d\n", *(str + 10));
//此时,str越界访问,形成野指针
//会导致数据异常甚至程序崩溃
return 0;
}
3.指针指向空间释放
int* test(void)
{
int n = 100;//n是局部变量,进函数时生成,出函数销毁
return &n;
}
int main()
{
int* p = test();
printf("%d\n", *p);//p访问已经销毁的空间,造成非法访问,即野指针
return 0;
}
主函数中*p访问的空间在出 test 函数时就已经被销毁,此时这段空间不属于p了,有可能会篡改内存,会造成非法访问。即形成野指针。
如何避免野指针的形成:
1.如果找到指针指向哪里,就直接赋地址,如果不知道,可以赋值NULL(NULL是C语言中定义的标识符常量,值为0,但这个地址无法使用,读写该地址时会报错)int* p=NULL;
。
2.小心指针越界。
3.指针不再使用时,及时置NULL,指针使用前检查其有效性if(p != NULL)
才使用。
4.避免返回局部变量的地址。
assert断言来检查有效性
#include<assert.h>
头文件定义了宏assert(),用于在运行时保证程序符合条件,如果不符合,就报错终止运行。这个宏常常被称为“断言”。
assert(p != NULL);
p如果等于NULL,条件成立,报错。如果不为NULL,则正常运行。
可以在assert头文件前添加一个宏:#define NDEBUG
来控制assert的禁用与否(有就禁用)。