指针:
一、 指针是什么?
- 在计算机科学中,指针是编程语言中的一个对象,利用地址,它的值直接指向存在电脑存储器中另一个地方的值。
- 由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。
- 因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址的内存单元。
1、内存(地址)
- 内存是电脑上特别重要的存储器,计算机中程序的运行都是在内存中进行的 。
- 所以为了有效的使用内存,就把内存划分成一个个小的内存单元,每个内存单元的大小是1个字节。
- 为了能够有效的访问到内存的每个单元,就给内存单元进行了编号,这些编号被称为该内存单元的地址。
指针是个变量,存放内存单元的地址(编号)。
那对应到代码:
#include <stdio.h>
int main()
{
int a = 10; //在内存中开辟一块空间
int *p = &a; //这里我们对变量a,取出它的地址,可以使用&操作符。
//将a的地址存放在p变量中,p就是一个指针变量。
return 0;
}
总结:
指针就是变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。
2、如何编址
经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。
-
前面有0x的数 —— 表示16进制 —— 2个位数的16进制数(FF) = 8个位数的2进制数(11111111)(举例:16进制 —— FF 和 2进制 —— 11111111 表示的数相等)
-
在32位平台下:有32个地址线,每个地址线能够给出0或1,则能够给出2的33次方 - 1个 编号地址2的33次方字节 = 4,294,967,296byte = 4,194,304kb = 4096mb = 4gb
-
所以在32位平台下(32个地址线):内存最大为4GB。
-
如果想要增大内存就要增加地址线,可以用64位的平台。
这里我们就明白:
- 在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。
- 那如果在64位机器上,如果有64个地址线,一个指针变量的大小是8个字节,才能存放一个地址。
总结:
- 指针是用来存放地址的,地址是唯一标示一块地址空间的。
- 指针的大小在32位平台是4个字节,在64位平台是8个字节。
二、 指针和指针类型
这里我们在讨论一下:指针的类型
我们都知道,变量有不同的类型,整形,浮点型等。那指针有没有类型呢?
准确的说:有的。
指针为什么会有类型呢?
我们在前面讲过指针在32位平台下是4个字节,在64位平台下是8个字节。
既然指针的大小是相同的为什么我们不统一定一个类型为指针类型呢?
char* 类型的指针是为了存放 char 类型变量的地址。
short* 类型的指针是为了存放 short 类型变量的地址。
int* 类型的指针是为了存放 int 类型变量的地址。
下面我们讲指针类型的意义:
1、指针 + - 整数
#include<stdio.h>
int main()
{
int n = 0;
char* pa = &n;
int* pb = &n;
printf("%p\n", &n);
printf("char类型的地址:%p\n", pa);
printf("char类型+1的地址:%p\n", pa + 1);
printf("int类型的地址:%p\n", pb);
printf("int类型+1的地址:%p\n", pb + 1);
return 0;
}
总结:
指针的类型决定了指针向前或者向后走一步有多大(距离)。
2、指针的解引用
#include<stdio.h>
int main()
{
int n = 0x11223344;
char* pa = (char*)&n;
*pa = 0;
return 0;
}
#include<stdio.h>
int main()
{
int n = 0x11223344;
int* pa = &n;
*pa = 0;
return 0;
}
总结:
指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
比如:
char* 的指针解引用就只能访问一个字节,
而 int* 的指针的解引用就能访问四个字节。
三、野指针
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
1、野指针成因
1.1 指针未初始化
#include<stdio.h>
int main()
{
int* p;
*p = 10; //指针没有初始化 —— 指向的是随机值 —— 随机值是不在main函数的控制范围内 —— 野指针
return 0;
}
1.2 指针的越界访问
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
int* p = arr;
int i = 0;
for (i = 0; i <= 10; i++)
{
*p = i;
p++; //当指针指向的范围超出数组arr的范围时,p就是野指针 —— *p + 10 超过了数组的范围
}
return 0;
}
1.3 指针指向的空间释放
#include<stdio.h>
int* test()
{
int a = 10;
int* p = &a;
return p;
}
int main()
{
int* p = test(); //这一步执行完 —— test所创建的空间已经被释放了
printf("hehe\n");
printf("%d", *p); //打印值为不确定的值
}
2、 如何规避野指针
- 指针初始化
- 小心指针越界
- 指针指向空间释放即使置NULL
- 避免返回局部变量的地址
- 指针使用之前检查有效性
#include <stdio.h>
int main()
{
int *p = NULL; //当不知道初始化的值时,可以用NULL空指针来赋值
int a = 10;
p = &a; //如果有初始化的值,尽量初始化
if(p != NULL)
{
*p = 20;
}
return 0;
}
四 、指针运算
1、指针 + - 整数
int main()
{
int arr[5];
int* p;
for (p = &arr[0]; p < &arr[5]; *p++ = 0)
{
; //循环5次
}
return 0;
}
数组地址是连续的,随着下标由小到大,地址是由小到大。
2、指针 - 指针
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
printf("%d\n", &arr[9] - &arr[0]); //打印值为 9 —— 不是36
return 0;
}
指针 - 指针算的是指针元素之间的个数,不是他们之间相差的字符个数
实现库函数 —— strlen
#include<stdio.h>
#include<string.h>
int my_strlen1(char* p) //一般方法 —— 实现库函数
{
int count = 0;
while (*p != '\0')
{
count++;
p++;
}
return count;
}
int my_strlen2(char* p) //利用指针 - 指针的方法 —— 实现库函数
{
char* s = p;
while (*p != '\0')
{
p++;
}
return p - s;
}
int main()
{
char arr[] = "abcdef";
printf("%d\n", strlen(arr)); //打印值为6
printf("%d\n", my_strlen1(arr)); //打印值为6
printf("%d\n", my_strlen2(arr)); //打印值为6
return 0;
}
3、指针的关系运算符
for(p = &arr[5]; p > &arr[0];)
{
*--p = 0;
}
这个代码不会访问到 arr[ 0 ] 前面的地址
代码简化, 这将代码修改如下:
for(p = &arr[5]; p >= &arr[0]; p--)
{
*p = 0;
}
这个代码会访问到 arr[ 0 ] 前面的地址
第二种方法 —— 实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。
标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,
但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
五、指针和数组
数组 —— 是一串连续的空间 —— 放相同类型的变量
数组大小 —— 和数组类型和元素个数有关
指针(变量) —— 是一个变量 —— 用来存放地址
指针大小 —— 和使用的平台有关 —— 在32位机器下为4个字节 —— 在64位平台下8个字节
1、数组名
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
printf("%p\n", arr);
printf("%p\n", &arr[0]);
return 0;
}
数组名表示首元素的地址 —— 但是有两种例外(表示整个数组) —— 其他的都表示首元素地址
第一种:&arr —— 表示整个数组
第二种:sizeof(arr) —— arr表示整个数组 —— 计算整个数组的大小
2、指针
既然可以把数组名当成地址存放到一个指针中,我们使用指针来访问数组就成为可能。
#include <stdio.h>
int main()
{
int i = 0;
int arr[] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr; //指针存放数组首元素的地址
int sz = sizeof(arr)/sizeof(arr[0]);
for(i=0; i<sz; i++)
{
printf("&arr[%d] = %p <====> p+%d = %p\n", i, &arr[i], i, p+i);
}
return 0;
}
从上面的结果就可以知道:arr[ i ] = *(arr + i )
那我们就可以直接通过指针来访问数组。
#include<stdio.h>
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
int *p = arr; //指针存放数组首元素的地址
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i)); //*(p + i) = p[i]
}
return 0;
}
六、二级指针
指针变量也是变量,是变量就有地址。
对于二级指针的运算:
1. *ppa通过对ppa中的地址解引用 —— 找到pa —— *ppa = pa = &a
2. **ppa —— *(*ppa) = *pa = *(&a) = a = 10
七、指针数组
int arr —— 整型数组
char ch —— 字符数组
int* parr —— 整型指针数组
char* pch —— 字符指针数组
#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
int c = 30;
int* arr[3] = { &a, &b, &c }; //int* arr[3]其中arr先和[ ]结合 —— 说明这是一个数组 ——
//*表示数组中存放的是指针 —— int表示指针所指向的数是int类型
//打印a, b, c的值
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%d ", *(arr[i]));
}
return 0;
}