目录
3.3.3 指针指向空间释放,及时置NULL(涉及动态内存分配)
3.3.5 指针使用之前检查有效性(前提是要初始化,初始化一个值或者空指针)
1. 指针是什么
指针理解的2个要点:1. 指针是内存中一个最小单元的编号,也就是地址2. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
总结:指针就是地址,口语中说的指针通常指的是指针变量。
指针变量我们可以通过&(取地址操作符)取出变量的内存起始地址,把地址可以存放到一个变量中,这个变量就是指针变量#include<stdio.h> int main() { int a = 10; char ch; return 0; }
创建a时,向内存申请四个字节,我们假设是蓝色区域,创建ch时,我们假设是绿色空间,创建变量的本质,其实就是向内存申请空间。
而我们取地址,取的是起始地址
我们创建变量,变量a对应的有自己的地址,我们将其打印出来,我们自然而然写出下列代码:
#include<stdio.h> int main() { int a = 10; char ch; printf("%p\n", &a); return 0; }
如果我们想储存a的地址,我们将其赋值给pa,我们自热而然地想到pa=&a,对于pa的类型,我们对其声明为指针变量,*pa=&a,而我们指向的变量a的类型是int,int *pa=&a;
int *pa = &a;
同理,我们对ch的地址用变量pc进行储存,我们也自然地想到 *pc=&ch;由于这次指向的ch是char类型,我们要声明 char *pc=&ch;
char *pc=&ch;
我们对a的地址进行监测:
我们调整为列1,一次一个字节地看,由此更能直观地看出a在其起始地址往后占四个字节,我们刚刚打印的地址也是其的第一个字节的地址,即打印的是起始地址:
00000000 00000000 00000000 0000000000000000 00000000 00000000 00000001...111111111 111111111 111111111 111111111
指针变量是用来存放地址的,地址是唯一标示一个内存单元的。指针的大小在32位平台是4个字节,在64位平台是8个字节x86环境:
#include<stdio.h> int main() { int a = 10; char ch; int *pa = &a; char* pc = &ch; printf("%d\n", sizeof(pa)); printf("%d\n", sizeof(pc)); return 0; }
我们换成x64的环境试试:
#include<stdio.h> int main() { int a = 10; char ch; int *pa = &a; char* pc = &ch; printf("%d\n", sizeof(pa)); printf("%d\n", sizeof(pc)); return 0; }
pa作为指针变量存放a的地址,但是pa自己也有自己的地址 ,pc同理
当我们想调用pa,或者更改a的数值,我们可以进行解引用操作:
int a=10; int *pa = &a; *pa = 20;
2. 指针和指针类型
2.1 引入
int main()
{
int* pa;
char* pc;
float* pf;
printf("%d\n", sizeof(pa));
printf("%d\n", sizeof(pc));
printf("%d\n", sizeof(pf));
return 0;
}
我们在x86环境下,求各指针的大小,我们发现其都是四个字节,我们会有一些疑惑,既然其大小一样,为什么指针不统一其类型呢 ?
2.2 指针类型的意义
2.2.1访问权限
我们按正常逻辑,定义一个int*的指针去储存int a的地址,我们并对其解引用操作,更改a的数值,我们通过调试,内存窗口里的变化显示其数据确实更改。
int main() { int a = 0x11223344; int* pa = &a; *pa = 0; return 0; }
我们再使用char*指针储存a的内存,由于各类型指针都占四个字节,其可以储存下a的内存,但是我们在解引用的时候发现了问题:
int main() { int a = 0x11223344; char* pc = &a; *pc = 0; return 0; }
所以指针类型是有意义的,其意义之一在于解引用时,不同类型的指针决定了其访问字节数的能力大小,即它的权限。
char*的指针解引用时能够访问1个字节
int*的指针解引用时能够访问4个字节
double*的指针解引用时能够访问8个字节
2.2.2 指针+-整数
int main() { int a = 0x11223344; int* pa = &a; char* pc = &a; printf("%d\n", pa); printf("%d\n", pc); printf("%d\n", pa+1); printf("%d\n", pc+1); return 0; }
pa与pc指针都指向了a地址的起始位置,前两个打印的数值相同,而pa+1我们可以看到其地址数值+4,,pc+1,其地址数值+1
指针类型决定指针的步长(向前or向后,走一步多大距离)
int* 指针+1,意思是跳过一个整型,也就是向后走了4个字节
char*指针+1,意思是跳过一个字符,也就是向后走了1个字节
double*指针+1,意思是跳过一个double,也就是向后走了8个字节
short*指针+1,向后走2个字节
2.3 使用
int main() { int arr[10] = { 0 }; //1~10赋值; return 0; }
如果我们想对数组内部进行1~10的赋值,我们可以在知道首地址的情况下(数组名即数组首元素的地址),使用int*来对其进行操作,具体如下:
nt main() { int arr[10] = { 0 }; //1~10; int*p=arr;//首地址类型int,用int* ; int i = 0; for (i = 0; i < 10; i++) { *p = i + 1; p++; } return 0; }
如果我们使用char*指针进行访问修改,char* p=arr;,我们最多访问蓝色格子区域,
如图所示:
我们也可以换一种方法实现1~10的赋值:
int main() { int arr[10] = { 0 }; //1~10; int*p=arr;//首地址类型int,用int* ; int i = 0; for (i = 0; i < 10; i++) { *(p + i) = i+1; //注意不要越界访问 } return 0; }
3. 野指针
3.1 定义
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
3.2野指针成因
3.2.1 指针未初始化
指针未初始化时,会存一个随机值,这个随机值被当成地址储存在p里,当我们对指针解引用赋值时,例如下面的代码,*p=20,找到随机值这个地址所在的空间,将20放入该空间,此空间不属于我们的空间,就会出现问题。
int main() { int* p; *p = 20; return 0; }
3.2.2 指针越界访问
int main() { int arr[10] = { 0 }; int i = 0; int sz = sizeof(arr) / sizeof(arr[0]); int* p = arr; for (i = 0; i <= sz; i++)//越界访问 { *p = i; p++; } return 0; }
对应的,也可能存在向前越界。
3.2.3 指针指向的空间释放
int* test() { int num = 100; return # } int main() { int*p=test(); *p = 200; return 0; }
当程序执行,*p开辟一个空间,调用函数,函数内num开辟一个空间,假设其地址为
0x0012ff40,该空间存放num的数值100;函数调用结束,返回其地址,并存储在p内,此时该空间被释放,num不存在,其值100丢失,该空间不属于我们,而*p=200的操作,由于p内存储地址,其可以找回地址所指向的空间,但是此时该指针相当于未初始化,所以成了野指针,操作存在不当。
相当于假设你在酒店302,第二天你把房退了,但是你打电话给张三让张三过来住一晚,张三第二天跑来了,说他要住一晚,不论第二天这个房间是否被另一个人使用,这个空间都不属于张三了,不能非法访问,强行入住。即便记住地址,该空间也不能使用。
3.3 如何避免野指针
3.3.1 指针初始化
int main() { int a = 10; int* pa = &a;//明确初始化 //NULL=0,就是初始化指针的 int* p = NULL; return 0; }
3.3.2 小心指针越界
3.3.3 指针指向空间释放,及时置NULL(涉及动态内存分配)
//申请 int* p=malloc(40); //使用 //释放 free(p); p=NULL;
3.3.4 避免返回局部变量的地址(避免返回栈空间的地址)
3.3.5 指针使用之前检查有效性(前提是要初始化,初始化一个值或者空指针)
int main() { int* p = NULL; if (p != NULL) { printf("%d\n", *p); } return 0; }
int main() { int a = 10; int* pa = &a;//明确初始化 if (p != NULL) { printf("%d\n", *p); } return 0; }
空指针不能访问,是系统内核的空间,加一个有效判断即可。
4. 指针运算
4.1 指针+- 整数
int main() { double arr[5] = { 0 }; double* p = arr; int i = 0; for (i = 0; i < 5; i++) { printf("%lf\n", *(p + i)); } return 0; }
#define N_VALUES 5 float values[N_VALUES]; float *vp; //指针+-整数;指针的关系运算 for (vp = &values[0]; vp < &values[N_VALUES];) { *vp++ = 0; }
特别说明:*vp++=0;是*vp=0后,vp++。
4.2 指针-指针
4.2.1 举例
注:指针+指针无意义
指针与指针的相减的绝对值是指针之间相差的元素个数;
指针与指针的相减的前提是两个指针指向同一块连续空间。
int main() { int arr[10] = { 0 }; printf("%d\n", &arr[9] - &arr[0]); printf("%d\n", &arr[0] - &arr[9]); return 0; }
4.2.2 求字符串长度——指针-指针的应用
法一:
#include<string.h> int main() { char arr[] = "abcdef"; int len = strlen(arr); printf("%d\n", len); return 0; }
法二(计数器法——模拟strlen函数实现):
int my_strlen(char* str) { int count = 0; while (*str!='\0') { count++; str++; } return count; } int main() { char arr[] = "abcdef"; int len = my_strlen(arr); printf("%d\n", len); return 0; }
法三——递归
int my_strlen(char *p) { if(*p=='\0') { return 0; } else { return 1+my_strlen(p+1); } } int main() { char arr[]="abcdef"; int ret=my_strlen(arr); printf("%d\n",ret); return 0; }
法四——指针-指针:
int my_strlen(char* str) { char* start = str; while (*str != '\0')//或者写while(*str) str++; return str-start; } int main() { char arr[] = "abcdef"; int len = my_strlen(arr); printf("%d\n", len); return 0; }
4.3 指针的关系运算
for(vp = &values[N_VALUES]; vp > &values[0];) { *--vp = 0; }
代码简化, 这将代码修改如下:for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--) { *vp = 0; }
实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
5. 指针和数组
指针指向同一块连续空间,通过起始地址、偏移量、解引用来访问操作数组内部元素。
//p变化 int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int* p = arr; int i = 0; int sz = sizeof(arr) / sizeof(arr[0]); for (i = 0; i < sz; i++) { printf("%d ", *p); p++; } return 0; }
//p不变 int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int* p = arr; int i = 0; int sz = sizeof(arr) / sizeof(arr[0]); for (i = 0; i < sz; i++) { printf("%d ", *(p + i)); } return 0; }
注:数组与指针不是一个东西;
数组能存放一组数,连续的空间;数组的大小取决于元素的个数;
指针是一个变量,存放地址的;4/8个字节;
数组把首元素的地址,交给一个指针变量后,可以通过指针来访问数组。
p+i 其实计算的是数组 arr 下标为i的地址。
6. 二级指针
6.1 引入
#include<stdio.h> int main() { int a = 10; int* p = &a;//p是指针变量,一级指针变量 int** pp = &p;//pp是二级指针 return 0; }
a是int类型,int* p的*是说明p为指针,int说明p指向的a是int类型,int** pp第二个*说明这个pp是指针,int*说明pp指向的是一级指针。此例中pp的指针类型是int**
6.2 解引用
#include<stdio.h> int main() { int a = 10; int* p = &a;//p是指针变量,一级指针变量 int** pp = &p;//pp是二级指针 //*p = 20; //printf("%d\n", a); *(*pp) = 200; //**pp=200; return 0; }
一次解引用是p,想要找a,对p继续解引用,相当于二级指针两次解引用,我们可以看成a外套了一个p的盒子,p外套了一个pp的盒子,我们要想取出a或者修改a,首先,我们需要打开外面的盒子,然后打开里面的盒子。
7. 指针数组
7.1 定义:
是数组,是存放指针的数组;
int main() { //整型数组——存放整型的数组 int arr1[10]; //字符型数组——存放字符的数组 char arr2[5]; //指针数组——存放指针的数组 int* arr3[5];//存放整型指针的数组 char* arr4[5];//存放字符指针的数组 return 0; }
7.2 使用
7.2.1 例一
int main() { int a = 10; int b = 20; int c = 30; int d = 40; int e = 50; int* arr3[5] = { &a,&b,&c,&d,&e }; int i = 0; for (i = 0; i < 5; i++)//数组遍历 { printf("%d\n", *(arr3[i]));//内存放的是地址,需解引用 } return 0; }
7.2.2 例二——用一维数组模拟二维数组
//用一维数组模拟二维数组 int main() { int arr1[] = { 1,2,3,4,5}; int arr2[] = { 2,3,4,5,6}; int arr3[] = { 3,4,5,6,7}; int arr4[] = { 4,5,6,7,8}; int* arr[4] = { arr1,arr2,arr3,arr4 }; return 0; }
将其打印:
//用一维数组模拟二维数组 int main() { int arr1[] = { 1,2,3,4,5}; int arr2[] = { 2,3,4,5,6}; int arr3[] = { 3,4,5,6,7}; int arr4[] = { 4,5,6,7,8}; int* arr[4] = { arr1,arr2,arr3,arr4 }; int i = 0; for (i = 0; i < 4; i++) { int j = 0; for (j = 0; j < 5; j++) { printf("%d ", arr[i][j]); //同数组的引用,不需要解引用,我们拿到的也是数组首地址,再对其操作 //arr[j]=*(arr+j),可以看成已经解引用过 } printf("\n"); } return 0; }
对于arr[i][j]我们也可以写成,*(*(arr+i)+j);
*(arr+i)相当于通过arr首地址找到内部的arr1\arr2\arr3\arr4的首地址,然后再在子数组的首地址上*(arr1+j),就访问了arr1内的元素。
//用一维数组模拟二维数组 int main() { int arr1[] = { 1,2,3,4,5}; int arr2[] = { 2,3,4,5,6}; int arr3[] = { 3,4,5,6,7}; int arr4[] = { 4,5,6,7,8}; int* arr[4] = { arr1,arr2,arr3,arr4 }; int i = 0; for (i = 0; i < 4; i++) { int j = 0; for (j = 0; j < 5; j++) { printf("%d ", *(*(arr+i) + j)); } printf("\n"); } return 0; }