九、指针
1. 内存单元
4G=2^32 * 8bit ,每8bit 是一个字节,也就是一个存储单元。
所谓32位和64位系统,以32位为例,最多支持4G的内存空间,指的是CPU的地址总线是32根,高电平1,低电平0,最多能存储2^32个地址,每个地址都有其唯一独立的编号。
4G内存的地址编码范围就是0–>2^32,二进制4位——十六进制1位:0–>0xffffffff
那么64位系统最多支持2^32G,但在目前的计算机的寻址能力根本不可能达到,不过128G还是可以达到的。
2. 指针就是地址
*号作用
***:**定义指针变量的标志。可以存地址。int *d = &a
***:**第二个作用,解引用,取内容。
例如
int main() { int a = 10; // a在内存中要分配地址- 4字节 printf("%p\n", &a); //%p 专门用来打印地址 // 定义指针变量的标志 int *pa = &a; // pa是用来存放地址的,在C语言中pa叫指针变量 // * 说明 pa是指针变量 // int 说明pa 执行的对象是int类型的 char ch = 'w'; // 同理 char *pc = &ch; // 解引用 *pa = 20; printf("%d\n", a); //输出20,*pa通过pa里面的地址,找到a,并改变a的值 return 0; }
指针大小
printf("%d\n", sizeof(char *)); printf("%d\n", sizeof(int *)); printf("%d\n", sizeof(long *)); printf("%d\n", sizeof(short *)); printf("%d\n", sizeof(float *));
输出全是8,为什么呢?
指针是用来存地址的,指针需要多大的空间,取决于地址的存储需要多大空间。32位 32bit组成的地址 =4byte 64位 64bit 组成的地址 = 8byte。
3.指针类型的意义
决定了指针解引用的权限有多大(能操作几个字节)。比如char* 指针解引用就只能访问一个字节,而int* 的指针的解引用就能访问四个字节。
int num = 0x01020304; // 0x1020304,会自动省略掉0 int *p1 = # printf("num = %#x\n", num); // 内存中存储方式 0x04 0x03 0x02 0x01 printf("*p1 = %#x\n", *p1); // int型 4字节 取完 short *p2 = # printf("*p2 = %#x\n", *p2); // short型 2字节 取前两个 0x04 0x03,输出0x304 char *p3 = # printf("*p3 = %#x\n", *p3); // char型 1字节 取第一个 0x04,输出0x4 short *p4 = # p4 += 1; printf("*p4+1 = %#x\n", *p4); //+1跳过前两个字节,输出0x102
4.野指针
- 指针必须指定内容,或者不知道初始化什么就先初始化为NULL,否则为野指针,会报错。
//这里p就是一个野指针 // p是一个局部的指针变量,局部变量不初始化的话,默认是随机值 int *p; *p = 20; // 非法访问内存 printf("%d\n", *p); //报错
越界访问,导致野指针问题。
int arr[10] = {0}; int *p = arr; for (int i = 0; i <= 20;i++) { *p = i; //解引用,当i=10,越界访问,报错 p++; }
指针指向的空间释放,导致野指针问题。
so,指针使用之前要检查有效性。
注:随机初始化变量值或 指针,养成好的编程习惯。
5. 指针运算
- 指针±整数
int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; int *p = arr; int *pend = arr + 9; while (p <= pend) { printf("%d\n", *p); p++; }
指针- 指针 = 指针和指针之间元素的个数。指针+指针没有什么意义。
前提:两个指针指向同一块空间。
int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; printf("%d\n", &arr[9] - &arr[0]); //输出9
函数返回
//计数器写法 int my_strlen(char *str) { int count = 0; while (*str != '\0') //不能用if判断(if只执行一次,返回值是1) { count++; str++; } return count; } int main() { int len = my_strlen("abc"); printf("%d\n", len); return 0; } //递归写法 int my_strlen(char *str) { char *start = str; while (*str != '\0') { str++; } return str - start; }
指针&数组运算
int arr[5] = {1, 2, 3, 4, 5}; int *p = NULL; for (p = &arr[5]; p > &arr[0];) *--p = 0; for (int i = 0; i < 5; i++) printf("%d\n", arr[i]); //输出全为0 // 等价,但实际中应该避免此类写法,因为标准并不保证它可行 for (p = &arr[4]; p >= &arr[0]; p--) *p = 0;
标准规定:允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
6. 指针等价关系
指针指向的是数组首元素的地址。
int arr[10] = {0}; int *p = arr; int i = 0; for (i = 0; i < 10; i++) printf("%p======>%p\n", &arr[i], p + i);//输出一致
等价交换
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int *p = arr; printf("arr[2]=%d-----p[2]=%d\n", arr[2], p[2]); //输出相等 //[]是一个操作符 2和arr是两个操作数 printf("2[arr]=%d-----arr[2]=%d\n", 2 [arr], arr[2]); //输出相等 //等价关系 //arr[2] = *(arr+2) = *(2+arr) = 2[arr] //arr[2] = *(arr+2) = *(p+2) = *(2+p) = *(2+arr) = 2[arr]
二级指针
*ppa = pa
*pa = arr
所以 **ppa=arr
int arr[10] = {0}; int *pa = arr; //pa是指针变量,一级指针 int **ppa = &pa; //pa也是变量,&pa取出pa在内存中的起始地址
7.指针数组
存放指针的数组
int arr[10]; // 整形数组 = 存放整形的数组 char str[5]; // 字符数组 = 存放字符的数组 // 指针数组 = 存放指针的数组 int *parr[5]; // 整形指针的数组 char *pch[5]; // 字符指针的数组
8.字符指针
指针是可以指向字符串的,且指向的是首个字符串的地址,字符串是常量字符串,严格编程中,需要加关键字const,是无法进行更改的。
char str1 = "hello str"; // 字符串初始化,后期可以改 char str2 = "hello str"; const char *str3 = "hello bit"; // 常量字符串,不可更改 const char *str4 = "hello bit"; // 因为str3和str4内容相同,在内存空间中只保留一个就好了 if (str1 == str2) printf("%d\n", 1); else printf("%d\n", 2); if (str3 == str4) printf("%d\n", 3); else printf("%d\n", 4);
9. 指针模拟二维数组
模拟二维数组并不是真的说该指针是二维数组,因为指针指向的是独立的,只是因为a,b,c由arr[i]指向,看起来是连续的(实际上,二维数组存放的地址是连续的)。
int a[5] = {1, 2, 3, 4, 5}; int b[] = {2, 3, 4, 5, 6}; int c[] = {3, 4, 5, 6, 7}; int *arr[3] = {a, b, c}; //模拟二维数组 for (int i = 0; i < 3; i++) { for (int j = 0; j < 5; j++) { printf("%d", arr[i][j]); //输出12345,23456,34567 } printf("\n"); }
10.数组指针
数组指针是一种指针哦~
整型指针 - 是指向整型的指针
字符指针 - 是指向字符的指针
数组指针 - 是指向数组的指针
int arr[10] = {1, 2, 3, 4}; int(*parr)[10] = &arr; //取出的是数组的地址,parr就是一个数组指针,数组10个元素,每个元素的类型是int double* d[5]; double* (*pd)[5] = &d; //(*pd)指 指向d的数组的指针,指针类型是double*
int arr[10] = {0}; int *p1 = arr; // 整型指针 int(*p2)[10] = &arr; // 数组指针 printf("%p\n", p1); // 虽然结果相同,但是意义不同 printf("%p\n", p1 + 1); // 跳过一个整型,就是4个字节 printf("%p\n", p2); printf("%p\n", p2 + 1); // 跳过一整个数组
注:数组名是数组首元素的地址,但是有两个例外:
sizeof(数组名) :数组名表示整个数组,计算的是整个数组大小,单位是字节;
&数组名:数组名表示整个数组,取出的是整个数组的地址。
实例分析
void print(int (*p)[5], int r, int c) //一维指针 { for (int i = 0; i < r; i++) { for (int j = 0; j < c; j++) // p+i:找到第i行的地址; //*(p+i)解引用找到第i行的数组名,即第i行的首地址; //*(p + i) + j):第i行第j列的地址; // 再解引用,*(*(p + i) + j),找到第i行j列元素 printf("%d ", *(*(p + i) + j)); printf("\n"); } } int main() { int arr[3][5] = {{1, 2, 3, 4, 5}, {2, 3, 4, 5, 6}, {3, 4, 5, 6, 7}}; print(arr, 3, 5); //arr为首元素的地址,即数组第一行,所以print函数参数为一维指针 return 0; }
一些语句意思
int (*parr1)[10]; //数组指针,该指针能够指向一个数组,数组有10个元素,每个元素的类型是int int(*parr2[10])[5]; //parr2是一个存储数组指针的数组,该数组能够存放10个数组指针,每个数组指针能够指向一个数组,数组5个元素,每个元素是int类型。
11.数组参数、指针参数
在写代码的时候难免要把 数组 、指针 传给函数,函数的参数也需要相应的设定。
- 一维数组传参
void test(int arr[]) //数组传参,没毛病 {} void test(int arr[10]) //形参,数组长度没有意义,写不写都行 {} void test(int *arr) //int类型 数组首地址,完美适配 {} void test2(int *arr[20]) //指针数组传参,没毛病 {} void test2(int **arr) //与上面类似,只不过原数组是指针数组,这里用二级指针表示数组首地址 {} int main() { int arr[10] = {0}; int *arr2[20] = {0}; //存放int*的数组 test(arr); //一维数组数组名,相当于数组首元素的地址,int类型 test2(arr2); return 0; }
二维数组传参
注意:二维数组传参,函数形参的设计只能省略第一个[ ]的数字
因为对于一个二维数组,可以不知道有多少行,但是必须知道一行多少元素,这样才方便预算。
void test3(int (*arr)[5]) // ok, arr指针数组指向5个int型元素 { } void test3(int arr[3][5]) // ok,行数可以省略,但是列数不能省略 { } int main() { int arr3[3][5] = {0}; test3(arr); // arr表示首元素是二维数组的第一行,即5个int类型元素 return 0; }
12. 函数指针
指向函数的指针。存放函数地址的指针。
!!!注意:数组名!=&数组名
But: 函数名 = &函数名
int ADD(int x, int y) { return x + y; } void test(char *str) { } int main() { //创建函数指针变量 // int:函数类型, int(*pt)():说明是指针函数, int(*pt)(int, int):形参类型 int (*pt)(int, int) = &ADD; void (*pl)(char *) = &test; int res = (*pt)(3, 5); int ret = pt(3, 5); int rea = ADD(3, 5); printf("%d\n", res); // 三种方式输出相同,且(*pt)(3, 5) 中 *没有实际意义,只是为了好理解 printf("%d\n", res); printf("%d\n", rea); printf("%p\n", &ADD); printf("%p\n", ADD); return 0; }
13. 特殊写法理解
- //2为第二种写法
14. 函数指针数组
int add(int x, int y); int sub(int x, int y); int (*paa[2])(int, int) = [add, sub];
15. 回调函数机制
回调函数就是一个通过函数指针调用的函数。即把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,就说这是回调函数。
回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用,用于对该事件或条件进行响应。
可以简化很多操作。
// 以冒泡排序为例 #include <stdio.h> #include <string.h> #include <stdlib.h> int cmp_int(const void* e1, const void* e2) //void* 是一种无类型的指针,直接解引用*e1,不知道访问几个字符 //所以对其强制类型转换一下*(int *)e1,从e1首地址往后访问4个字节即可 { return *(int*)e1 - *(int*)e2; //解引用值相等:返回0; e1<e2:返回<0; e1>e2:返回>0 } void print(int arr[], int sz) { for (int i = 0; i < sz; i++) { printf("%d ", arr[i]); } printf("\n"); } int main() { int arr[10] = {2,4, 1, 7, 5, 9, 6, 3, 8, 0}; int sz = sizeof(arr)/ sizeof(arr[0]); // 排序 qsort(arr, sz, sizeof(arr[0]), cmp_int); //qsort 头文件是stdlib.h // 打印 print(arr, sz); return 0; }
写一个通用的冒泡排序算法???
16.一些写法解释
/*
sizeof(数组名):数组名表示整个数组,计算的是整个数组的大小
&数组名:数组名表示整个数组,取出的是整个数组的地址
除此之外,所有的数组名都是数组首元素的地址
*/
int a[4] = {0, 1, 2, 3};
printf("%d\n", sizeof(a)); //整个数组的大小
printf("%d\n", sizeof(a + 0)); //第一个元素的地址 计算的是地址的大小
printf("%d\n", sizeof(a +1)); //第二个元素的地址 计算的是地址的大小
printf("%d\n", sizeof(*a)); //*a是数组的首元素,计算的是第一个元素的大小
printf("%d\n", sizeof(a[1])); //计算的是第二个元素的大小
printf("%d-----%d\n", sizeof(&a)); //数组的地址,32位是4byte,64位是8byte
printf("%d-----%d\n", sizeof(*&a)); //&a 数组的地址, *&a 解引用=a,即找到整个数组 所以结果是整个数组的大小:16
printf("%d-----%d\n", sizeof(&a+0));
printf("%d-----%d\n", sizeof(&a+1)); //跳过整个数组,下一块空间的起始位置的地址,也是4or8
printf("%d-----%d\n", sizeof(&a[0]));
printf("%d-----%d\n", sizeof(&a[0]+1)); //a[0]:第一个元素,&a[0]:第一个元素地址,&a[0]+1:第二个元素地址
char arr[] = {'a', 'b', 'c', 'd', 'e', 'f'};
printf("%d\n", strlen(arr)); //数组长度
printf("%d\n", strlen(arr + 0)); //数组长度
//printf("%d\n", strlen(*arr)); //因为strlen函数在设计的时候 int my_strlen(const char* str) *a:数组首元素,a:97,
//把97当成了一个地址,这样再找字符串就找不到了,会报错。
//printf("%d\n", strlen(arr[1])); //问题同上。
printf("%d\n", strlen(&arr)); //数组长度
printf("%d\n", strlen(&arr + 1));
printf("%d\n", strlen(&arr[0] + 1));
char arr1[] = "abcdef"; // a,b,c,d,e,f,\0
printf("%d\n", sizeof(arr1));
printf("%d\n", sizeof(arr1 + 0));
printf("%d\n", sizeof(*arr1));
printf("%d\n", sizeof(arr1[1]));
printf("%d\n", sizeof(&arr1)); //数组地址,类型char(*)[6]
printf("%d\n", sizeof(&arr1 + 1));
printf("%d\n", sizeof(&arr1[0] + 1));
printf("%d\n", strlen(arr1)); // 输出6, 因为遇到\0,就停止。
printf("%d\n", strlen(arr1 + 0));
// printf("%d\n", strlen(*arr1)); //err
// printf("%d\n", strlen(arr1[1])); //err
printf("%d\n", strlen(&arr1)); // 数组地址,类型char(*)[6]
printf("%d\n", strlen(&arr1 + 1)); // 跳过整个数组,下一块空间的起始地址,那什么时候遇到\0是未知的,所以应该是随机值,但我 //运行出来是6(不清楚为啥)
printf("%d\n", strlen(&arr1[0] + 1));
char *p = "abcdef"; // a,b,c,d,e,f,\0 p是一个指针,指向数组首元素的地址。
printf("%d\n", sizeof(p)); //数组地址大小
printf("%d\n", sizeof(p + 1)); //p指向a,p+1就指向b,还是地址
printf("%d\n", sizeof(*p)); //p指向a,解引用就是a的大小:1
printf("%d\n", sizeof(p[0])); //等价 *(p+0)
printf("%d\n", sizeof(&p)); //取p的地址
printf("%d\n", sizeof(&p + 1)); //跳过整个数组
printf("%d\n", sizeof(&p[0] + 1)); //第二个元素的地址
int b[3][4] = {0};
printf("%d\n", sizeof(b[0])); // 16, 整个一行的元素的大小 b[0]放在sizeof()内部,代表整行元素的大小
printf("%d\n", sizeof(b[0] + 1)); // 这里b[0]表示数组第一行首元素的地址,且没有放在sizeof() 内部,所以b[0]+1 代表第一 //行第二个元素的地址
printf("%d\n", sizeof(b + 1)); // 因为是二维数组,所以b代表第一行地址,b+1代表第二行第地址
printf("%d\n", sizeof(*(b + 1))); // 第二行数组大小,16
printf("%d\n", sizeof(&b[0] + 1)); // b[0]:第一行的数组名,&b[0]:第一行的地址,&b[0]+1 :第二行的地址
printf("%d\n", sizeof(*b)); // 16, b:二维数组数组名,没有单独放在sizeof里面即sizeof(b),也没有&b,所以b是首元 //素的地址,解引用,代表第一行的数组大小。
printf("%d\n", sizeof(b[3])); // b[3]是第四行的数组名(如果有的话),其实在这个例子里不存在,但也可以通过类型计算其大 //小的。
short aa = 5;
int b = 5;
printf("%d\n", sizeof(aa = b + 6)); // 2, 虽然b是int型,但是放到了a中(short型),结果还是short类型
printf("%d\n", aa); // 5, 因为上面sizeof内部并没有运算,a就是个short类型。
int s = {1, 2, 3, 4, 5};
int *ppt = (int *)(&s + 1); //&s表示数组s的地址,&s +1表示跳过整个数组,下一个空间的首地址,int(*)[]类型,需要强制转换成 //(int*)类型,赋给int*p,指向5后面一个地址。
printf("%d %d\n", *(s + 1), *(ppt - 1)); //a+1表示第二个元素的地址,解引用就是2, p-1表示向前移动一个元素,即5的地址,解 //引用就是5
17. 总结
指针是特殊变量,存储的是内存里的一个地址。要搞清楚指针就要搞清楚四个内容:
指针的类型:把指针名字去掉,剩下的都是( 包括() )
指针所指向的类型:把指针名字,和指针左边的*去掉,剩下的都是
指针的值:指针所指向的内存区或地址,如果说一个指针的值是XX,就相当于说该指针指向了以XX为首地址的一片内存区域;我们说 一个指针指向了某块内存区,就相当于说该指针的值是这块内存区域的首地址。
指针本身所占据的内存区:64为平台,用sizeof(指针的类型)=8byte.
int p; // 普通的整型变量 int *p; // p是一个返回整型数据的指针,指针的类型是int* int p[3]; //p是一个由整型数据组成的数组 int *p[3]; //p先与[],再与*,再与int,so p是整型数据指针组成的数组 int(*p)[3]; //p先与*,再与[],再与int,so p是指向 整型数据组成的数组 的指针 int **p;// p先与*,再与*,再与int,so 指针所指向的元素是整型数据的指针, int p(int); //p先与(),即p是一个函数,()里int,即该函数有一个整型变量的参数,最后与最外层的int,so 函数的返回值是一个整型数据 int(*p)(int); //p是一个指针,指向函数,函数有个int型参数,最后与最外层int结合,so p指向一个有整型参数且返回类型为整型的函数的指针 int *(*p(int))[3]; //p先与(),即p是一个有整型变量参数的函数; //与最近的*结合,即函数的返回的是一个指针; //再到最外面一层,先于[]结合,即指针指向的是一个数组; //再与最外面的*结合,即数组里的元素是指针; //最后与最外面的int,说明指针指向的内容是整型。 //so p是一个参数为一个整型数据 且返回一个指向 由整型指针变量组成的数组 的指针变量的函数