c语言|C语言中的指针(进阶)

目录
指针的概念
1.字符指针
2.指针数组
3.数组指针
3.1数组指针的定义
3.2&数组名和数组名
3.3数组指针的使用
4.数组参数、指针参数
4.1一维数组传参
4.2二维数组传参
4.3一级指针传参
4.4二级指针传参
5.函数指针
6.函数指针数组
7.指向函数指针数组的指针
8.回调函数:
9.指针和数组笔试题解析
10.指针笔试题

内存当中分为很多个内存单元,而这些内存单元都有一个编号,这个编号也叫做地址,这个地址既是指针,我们平常所说的指针指的是指针变量。
内存单元编号 == 地址 == 指针
指针的概念
1.指针就是个变量,是用来存放地址的,地址唯一标识一块内存空间
2.指针的大小是固定的4/8个字节(32位平台/64位平台)
3.指针是有类型的,指针的类型决定了指针+-整数的步长,也决定了指针解引用的时候的权限。
4.指针的运算:
1)指针+-整数
2)指针-指针
1.字符指针 指针类型中有一种指针叫做字符指针char*
我们一般这样用:
#include int main() { char ch = 'b'; char* p = &ch; *p = 'a'; return 0; }

还有一种使用的方式
#include int main() { const char* p = "abcdef"; printf("%s\n", p); return 0; }

代码const char* p = "abcdef"; 的意思是把abcdef这个字符串的首字符a的地址放到了字符指针p里面。
c语言|C语言中的指针(进阶)
文章图片

这里有一道练习:
#include int main() { char* p1 = "abcdef"; char* p2 = "abcdef"; char arr1[] = "abcdef"; char arr2[] = "abcdef"; if (p1 == p2) { printf("p1 == p2\n"); } else { printf("p2 != p2\n"); } if (arr1 == arr2) { printf("arr1 == arr2\n"); } else { printf("arr1 != arr2\n"); } return 0; }


代码的执行结果是:

c语言|C语言中的指针(进阶)
文章图片

这里p1和p2指向的是同一个常量字符串,C/C++会把常量字符串存储到单独的一个内存区域,称为常量区,当几个指针指向同一个常量字符串时,它们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组时就会在内存中开辟不同的内存块,所以p1和p2相同,arr1和arr2不同。
2.指针数组 指针数组就是存放指针的数组,数组中的每一个元素都是一个指针。
int* arr1[10] //整型指针的数组
char* arr2[4] //一级字符指针的数组
char** arr3[5] //二级字符指针的数组
#include int main() { int a = 1; int b = 2; int c = 3; int* pa = &a; int* pb = &b; int* pc = &c; int* arr1[] = { pa, pb, pc }; int** arr2[] = { arr1, arr1+1, arr1+2 }; printf("%d\n", **arr2[0]); printf("%d\n", **arr2[1]); printf("%d\n", **arr2[2]); return 0; }

代码执行结果是:
c语言|C语言中的指针(进阶)
文章图片

如图:
c语言|C语言中的指针(进阶)
文章图片

数组名表示数组首元素的地址
3.数组指针 3.1数组指针的定义 我们知道:
整型指针:int* p; 指向整型的指针
浮点型指针:char* p; 指向浮点型的指针
那么数组指针就是指向数组的指针
数组指针是这么定义的:
int (*p)[10]
解释:
p先和*结合,说明p是一个指针变量,然后指针指向的是一个大小为10个整型的数组,p是一个指针,指向一个数组,叫做数组指针。
因为[]的优先级要高于*号,所以必须加上()来保证p先和*结合
3.2&数组名和数组名 先来看一段代码:
#include int main() { int arr[10] = { 0 }; printf("%p\n", arr); printf("%p\n", &arr); return 0; }

代码的执行结果是:
c语言|C语言中的指针(进阶)
文章图片

可以看到,&数组名和数组名打印的地址是一样的。
再来看一段代码
#include int main() { int arr[10] = { 0 }; printf("%p\n", arr); printf("%p\n", &arr); printf("%p\n", arr + 1); printf("%p\n", &arr + 1); return 0; }

代码的执行结果是:
c语言|C语言中的指针(进阶)
文章图片

由上面的代码可知,虽然&arr和arr值是一样的,但是意义是不一样的。
实际上:&arr表示的是数组的地址,而不是数组首元素的地址。
本例中&arr的类型是int(*)[10],是一种数组指针类型。
数组的地址+1,跳过整个数组的大小,所以&arr+1相对于&arr的差值是40
3.3数组指针的使用 数组指针指向的是数组,那么数组指针中存放的是数组的地址。
这么使用:
#include int main() { int arr[10] = { 0 }; int (*p)[10] = &arr; return 0; }

比如:
#include void print1(int* arr) { int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr[i]); } }void print2(int (*p)[5]) { int i = 0; for (i = 0; i < 5; i++) {//*p相当于数组名,数组名又是首元素的地址,所以*p相当于&arr[0] printf("%d ", (*p)[i]); } }int main() { int arr[5] = { 1,2,3,4,5 }; print1(arr); printf("\n"); print2(&arr); return 0; }

在二维数组中的使用:
#include void print(int (*p)[3]) { int i = 0; for (i = 0; i < 3; i++) { int j = 0; for (j = 0; j < 3; j++) { printf("%d ", p[i][j]); //p[i][j]可以写成*(*(p + i) + j) //p+i是指向第i行的 //*(p+i)相当于拿到了第i行,也相当于第i行的数组名,也是第i行首元素的地址 } printf("\n"); } }int main() { int arr[3][3] = { {1,2,3}, {3,4,5}, {5,6,7} }; print(arr); //数组名arr,表示数组首元素的地址 //二位数组的首元素是二维数组的第一行 //所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址 //可以用数组指针来接收 return 0; }

4.数组参数、指针参数 4.1一维数组传参 先看下面的一串代码,哪个函数的形式参数可以正确接受主函数中传递的实参呢?
void test1(int arr[]) {}void test1(int arr[10]) {}void test1(int* arr) {}void test2(int* arr[10]) {}void test2(int** arr) {}int main() { int arr1[10] = { 0 }; int* arr2[10] = { 0 }; test1(arr1); test2(arr2); return 0; }

只有void test2(int* arr[10]){}这个是不行的,这里的int* arr[10]是个指针数组(这里的指针数组只是一个形参的形式,实际上编译器会将其解析成二级指针),而test2(arr2); 所传递的是个一级指针的地址,要用二级指针来接收。
再来看test1(arr1); 传递的是数组首元素地址,理论上应该用int* arr这样的指针来接受,但为什么可以用int arr[]或这int arr[10]来接受呢?
其实数组在进行传参的时候函数里的形参可以写成数组的形式也可以写成指针的形式,因此形参int arr[]中的[]里面可以写数字也可以不写数字,因为最后编译器都会将其解析成int* arr的形式。
4.2二维数组传参 来看看下面的代码,哪个函数的形参可以正确接受呢?
void test(int arr[3][5])//1 {}void test(int arr[][5])//2 {}void test(int arr[][])//3 {}void test(int(*p)[5])//4 {}void test(int** p)//5 {}void test(int* p)//6 {}void test(int* arr[5])//7 {}int main() { int arr[3][5] = { 0 }; test(arr); return 0; }

只有1和2和4是可以正确接受的。
二维数组传参,函数形参的设计只能省略第一个[]的数字,因为对一个二维数组来说,可以不知道有几行,但是一定要知道一行有几个元素,这样才方便运算。
4.3一级指针传参 看代码:
#include void print(int* p, int sz) { int i = 0; for (i = 0; i < sz; i++) { printf("%d ", *(p + i)); } }int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int* p = arr; int sz = sizeof(arr) / sizeof(arr[0]); print(p, sz); return 0; }

一级指针的传递用一级指针来接收。
思考:
当一个函数的参数部分是一级指针的时候,函数可以接收什么参数?
比如:
void test1(int* p) {} //test1能接收什么参数? void test2(char* p) {} //test2能接收什么参数?

答案是,test1可以接收int类型的地址,test2可以接收char类型的地址。
4.4二级指针传参 看代码:
#include void test(int** ptr) { printf("%d\n", **ptr); }int main() { int a = 10; int* p = &a; int* pp = &p; test(pp); test(&p); return 0; }

二级指针传参传的是一级指针的地址,所以要用二级指针来接收。
思考:
当函数的参数为二级指针时,函数可以接收什么样的参数?
比如:
void test(char** p) {}int main() { char ch = 'a'; char* p = &ch; char** pp = &p; char* arr[10]; test(pp); test(arr); return 0; }

其中的test(arr); 也是可以的,因为arr是数组首元素的地址,而arr是一个指针数组,所以arr即为一个一级指针的地址。
5.函数指针 先来看一段代码:
#include int Add(int x, int y) { int z = 0; return z = x + y; }int main() { printf("%p\n", &Add); printf("%p\n", Add); return 0; }

执行结果为:
c语言|C语言中的指针(进阶)
文章图片

可以看到输出的是两个地址,这两个都是test函数的地址。
如果我们想要将函数的地址保存起来,该怎么保存?
可以这样写:
int (*p)(int, int) = &Add;

*先和p结合,说明p是一个指针,右边是一个(),说明p指向的是个函数,而()里面无参数,说明指针指向的函数无参数,并且是个返回值是int类型的,所以p是一个函数指针。
把其中的p去掉,得到的int (*)(int, int)即为函数指针的类型。
而用函数指针去调用函数可以这么写:
int *p(1, 2);

*p即对p进行解引用可以得到Add。
也可以这么调用:
int p(1, 2);

要注意,这只是一种简写方式,实际上编译器最终还是会将其解析成第一种写法。

下面来阅读两段出自《C陷阱和缺陷》这本书中的代码
代码1
(*(void (*)())0)();

代码2
void (*signal(int , void(*)(int)))(int);

对于代码1,可以一层一层往里面看,先看外面的(*)()可知,这是对一个函数指针的调用,再往里面看,可以得知void(*)()是一个函数指针的类型,有void(*p)()去掉其中的p得到,而把类型放到()里面,意为强制类型转换,(void (*)())0即为对0进行强制类型准换,而后对0指针进行调用。
对于代码2,是对于signal这个函数的声明,这个函数的参数是int 和void(*)(int),函数的返回值类型是void(*)(int)。可以对这个代码进行简化:
typedef int (*p_fun)(int); p_fun signal(int, p_fun);

当然了,在《C陷阱和缺陷》这本书中有更详细的解释,推荐大家去看。
6.函数指针数组 数组是一个存放相同类型数据的存储空间,把函数的地址存放到一个数组中,那这个数组就叫做函数指针数组,那函数指针的数组该如何定义呢?
int (*parr1[10])(); int *parr2[10](); int (*)() parr3[10];

只有parr1才是正确的定义方式,parr1先和[10]结合,说明parr1是个数组,数组的内容是int (*)()类型的函数指针。
函数指针的用途是:转移表
来看一个例子:(计算器)
#include int add(int x, int y) { int z = 0; return z = x + y; }int sub(int x, int y) { int z = 0; return z = x - y; }int mul(int x, int y) { int z = 0; return z = x * y; }int div(int x, int y) { int z = 0; return z = x / y; }void menu() { printf("**********1.add2.sub**********\n"); printf("**********3.mul4.div**********\n"); printf("**********0.exit*********\n"); printf("************************************\n"); }int main() { int input = 0; int x = 0; int y = 0; int ret = 0; do { menu(); printf("请选择>:"); scanf("%d", &input); switch (input) { case 1: printf("请输入两个数字>:"); scanf("%d %d", &x, &y); ret = add(x, y); printf("%d\n", ret); break; case 2: printf("请输入两个数字>:"); scanf("%d %d", &x, &y); ret = sub(x, y); printf("%d\n", ret); break; case 3: printf("请输入两个数字>:"); scanf("%d %d", &x, &y); ret = mul(x, y); printf("%d\n", ret); break; case 4: printf("请输入两个数字>:"); scanf("%d %d", &x, &y); ret = div(x, y); printf("%d\n", ret); break; case 0: printf("退出\n"); default: printf("请重新输入\n"); break; } } while (input); return 0; }

然而这样的代码会比较冗余
来看使用函数指针数组的写法:
#include int add(int x, int y) { int z = 0; return z = x + y; }int sub(int x, int y) { int z = 0; return z = x - y; }int mul(int x, int y) { int z = 0; return z = x * y; }int div(int x, int y) { int z = 0; return z = x / y; }void menu() { printf("**********1.add2.sub**********\n"); printf("**********3.mul4.div**********\n"); printf("**********0.exit*********\n"); printf("************************************\n"); }int main() { int (*p[5])(int, int) = { 0, add, sub, mul, div }; //转移表 int input = 0; int x = 0; int y = 0; int ret = 0; do { menu(); printf("请选择>:"); scanf("%d", &input); if (input <= 4 && input >= 1) { printf("请输入两个数字>:"); scanf("%d %d", &x, &y); ret = ( * p[input])(x, y); printf("%d\n", ret); } else if (input == 0) { printf("退出\n"); continue; } else { printf("请重新输入\n"); continue; } } while (input); return 0; }

这种写法是不是会更简洁一些呢?
7.指向函数指针数组的指针 指向函数指针数组的指针是一个指针,指针指向的是一个数组,数组的元素都是函数指针。
这样定义:
void test() { printf("hehe\n"); }int main() { //函数指针 void (*pfun)() = test; //函数指针数组 void(*pfunarr[5])(); pfunarr[0] = pfun; //指向函数指针数组的指针 void (*(*ppfunarr)[5])() = &pfunarr; return 0; }

再来看这样的一段代码:
int add(int x, int y) { int z = 0; return z = x + y; }int sub(int x, int y) { int z = 0; return z = x - y; }int mul(int x, int y) { int z = 0; return z = x * y; }int div(int x, int y) { int z = 0; return z = x / y; }int main() { int (*pfunarr[])(int, int) = { add, sub, mul, div }; int (*(*ppfunarr)[4])(int, int) = &pfunarr; int i = 0; for (i = 0; i < 4; i++) { int ret = (*ppfunarr)[i](1, 5); printf("%d\n", ret); } return 0; }

函数的执行结果是:
c语言|C语言中的指针(进阶)
文章图片

这是通过对指向函数指针的数组的指针进行一个解引用,得到函数指针数组的首元素地址,然后就能够依次通过不同的函数指针来调用对应的函数了。
如图所示:
c语言|C语言中的指针(进阶)
文章图片

8.回调函数: 回调函数就是一个通过函数指针调用的函数。如果把一个函数的指针(地址)作为一个实参传递给一个另一个函数,当在另一个函数内部用这个指针来调用其所指向的函数时,那么被这个指针所指向的函数就叫做回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外一方所调用的,用于对该事件或条件进行响应。
前面讲到函数指针数组的时候,用了转移表的方式来对代码进行简化,其实也可以利用回调函数的方法将重复的代码封装到一个函数里面。
c语言|C语言中的指针(进阶)
文章图片

那么代码就变成了这样:
void test(int (*pfun)(int, int)) { int x = 0; int y = 0; int ret = 0; printf("请输入两个数字>:"); scanf("%d %d", &x, &y); ret = pfun(x, y); printf("%d\n", ret); }int main() { int input = 0; do { menu(); printf("请选择>:"); scanf("%d", &input); switch (input) { case 1: test(add); break; case 2: test(sub); break; case 3: test(mul); break; case 4: test(div); break; case 0: printf("退出\n"); default: printf("请重新输入\n"); break; } } while (input); return 0; }

可以用qsort函数来模拟实现一下qsort,打开MSDN,看一下qsort是什么
c语言|C语言中的指针(进阶)
文章图片

这是一个排序函数,有4个参数,返回类型是void
再来看每个参数的意思:
c语言|C语言中的指针(进阶)
文章图片

先来看看qsort函数具体怎么使用
用qsort排序一个整型数组:
int main() { int arr2[] = { 9,8,7,6,5,4,3,2,1,0 }; qsort(arr2, sizeof(arr2) / sizeof(arr2[0]), sizeof(arr2[0]), int_cmp); return 0; }

其中作为第4个参数传的是int_cmp的地址,而这个函数是需要我们自己去定义的,因为数组的排序是按照大小顺序来进行排序的,所以我们要写一个函数来比较数组里面两个元素的大小,由于qsort函数的设计者不清楚使用者要对什么样的数组元素进行一个排序,这个排序是升序还是降序,所以这个比较函数由使用者来自行设计。顺带一提,比较函数返回的值>0,那么进行升序排序,反之进行降序排序。所以比较函数可以这么写:
int int_cmp(const void* e1, const void* e2) { if (*(int*)e1 > *(int*)e2) { return 1; } else if (*(int*)e1 == *(int*)e2) { return 0; } else { return -1; } }

其可以简化为:
int int_cmp(const void* e1, const void* e2) { return ( *(int*)e1 - *(int*)e2); }

以上就是qsort函数的基本用法了。
对char类型的数组进行排序:
int char_cmp(const void* e1, const void* e2) { return strcmp((char*)e1, (char*)e2); }int main() { char arr1[] = "piqhrenjv"; qsort(arr1, sizeof(arr1) / sizeof(arr1[0]), sizeof(arr1[0]), char_cmp); return 0; }

对float类型的数组进行排序:
int float_cmp(const void* e1, const void* e2) { return *(float*)e1 - *(float*)e2; }int main() { float arr3[] = { 9.0,8.0,7.0,6.0,5.0,4.0,3.0,2.0,1.0,0.0 }; qsort(arr3, sizeof(arr3) / sizeof(arr3[0]), sizeof(arr3[0]), float_cmp); return 0; }

对结构体数组进行排序:
int cmp_stu_by_age(const void* e1, const void* e2) { return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age; }int main() { struct Stu arr4[3] = { {"zhangsan", 20, 100}, {"lisi", 50, 90}, {"wangwu", 30, 80} }; qsort(arr4, sizeof(arr4) / sizeof(arr4[0]), sizeof(arr4[0]), cmp_stu_by_age); return 0; }


qsort的函数内部逻辑用的是快速排序的算法来实现的,这里我们采用冒泡排序的算法来实现:
我们模拟实现的bubble_sort函数可以这么写:
void bubble_sort(void* base, int num, int width, int (*cmp)(const void* e1, const void*e2)) { int i = 0; //冒泡比较的趟数 for (i = 0; i < num - 1; i++) { int j = 0; //每趟的比较次数 for (j = 0; j < num - 1 - i; j++) { //先将数组的首元素指针转换为char类型,然后通过元素的字节大小来决定指针每次跳过的距离 if (cmp((char*)base + (j * width), (char*)base + ((j + 1) * width)) > 0) { swap((char*)base + (j * width), width); } } } }

先以排序整型数组为例来进行讲解:
int main() { int arr[10] = { 9,8,7,6,5,4,3,2,1,0 }; int sz = sizeof(arr) / sizeof(arr[0]); bubble_sort(arr, sz, sizeof(arr[0]), int_cmp); //传输的是函数的地址 return 0; }

第一个实参传的是数组首元素地址,却用void*类型的指针来接收,那么该如何找到数组每个元素的地址呢?可以先对指针进行(char*)的强制类型转换,那么指针+或-所走过的距离就只有一个字节大小了,所以参数里面还有一个是width,表示的是数组每个元素的字节大小,比如int类型的元素,它的字节大小是4,那么int类型元素的指针+4,就能找到下一个int类型的元素了。
如图:
c语言|C语言中的指针(进阶)
文章图片

找到每个元素的地址之后,这时候就要自己写一个比较函数了:
int int_cmp(const void* e1, const void* e2) { //比较整型数组两个元素的大小,先将数组的元素强制转换为int类型的指针 return ( *(int*)e1 - *(int*)e2); }

比较完之后就要按照元素的大小来对元素进行交换了,由于qsort函数是可以对任意类型的数组进行排序的,所以这里就要考虑到各种不同大小的元素都能够进行交换,又知道,内存中数据的基本存储单元是一个字节大小,那么如果要对两个4字节大小的整型数据进行交换,可以拆分成两个整型数据中每一个内存单元中的数据进行交换。
如图:
c语言|C语言中的指针(进阶)
文章图片

代码如下:
void swap(const void* p, int width) { int i = 0; for (i = 0; i < width; i++) { //将元素的交换拆分成每一个内存单元上的交换 char tmp = *((char*)p + i); *((char*)p + i) = *((char*)p + width + i); *((char*)p + width + i) = tmp; } }

最后可以再写一个函数将数组打印出来:
void print(int* arr, int sz) { int i = 0; for (i = 0; i < sz; i++) { printf("%d ", arr[i]); } }

除此之外,模拟实现的qsort函数也可以排序结构体数组:
struct Stu { char name[20]; int age; int score; }; int cmp_stu_by_age(const void* e1, const void* e2) { return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age; }int cmp_stu_by_score(const void* e1, const void* e2) { return ((struct Stu*)e1)->score - ((struct Stu*)e2)->score; } int cmp_stu_by_name(const void* e1, const void* e2) { return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name); }int main() { struct Stu arr2[3] = { {"zhangsan", 50, 100}, {"lisi", 20, 90}, {"wangwu", 30, 80} }; bubble_sort(arr2, sizeof(arr2) / sizeof(arr2[0]), sizeof(arr2[0]), cmp_stu_by_age); bubble_sort(arr2, sizeof(arr2) / sizeof(arr2[0]), sizeof(arr2[0]), cmp_stu_by_score); bubble_sort(arr2, sizeof(arr2) / sizeof(arr2[0]), sizeof(arr2[0]), cmp_stu_by_name); return 0; }

9.指针和数组笔试题解析 先了解一个知识点,数组名表示的是数组首元素的地址,但是有两个例外:
1.sizeof(数组名)中的数组名表示的是整个数组,sizeof(数组名)计算的是整个数组的大小;
2.&数组名中的数组名表示的整个数组,取出来的是整个数组的地址,也可以认为是指向数组的指针;
【c语言|C语言中的指针(进阶)】其中sizeof是个操作符,是用来计算对象在内存中所占空间的大小,单位是字节,不会直接对内存中的空间进行访问。
下面有一些练习题:
#include int main() { //一维数组 int a[] = { 1,2,3,4 }; printf("%d\n", sizeof(a)); //16 sizeof(a)计算的是整个a数组的大小,a数组有4个int类型的元素,大小为16个字节 printf("%d\n", sizeof(a + 0)); //4或8 a+0是数组首元素的地址,整型的地址,是指针类型,指针的大小取决于平台是32位还是64位 printf("%d\n", sizeof(*a)); //4 对数组首元素地址进行解引用,得到数组首元素,首元素是个整型,占4个字节 printf("%d\n", sizeof(a + 1)); //4或8 数组首元素地址+1,跳过一个整型,还是一个指针 printf("%d\n", sizeof(a[1])); //4 数组的第二个元素的大小 printf("%d\n", sizeof(&a)); //4或8 &a是整个数组的地址,虽然值和数组首元素地址的值一样,但是意义不同,一个是数组指针,一个是整型指针,指针的大小是4或8个字节 printf("%d\n", sizeof(*&a)); //16 *&a是先对a取地址,再解引用,得到的是a的数组名,但是此时数组名在sizeof里面,所以表示的是整个数组,sizeof计算的是整个数组的大小 printf("%d\n", sizeof(&a + 1)); //4或8 整个a数组的地址+1,即为指向a数组的数组指针跳过了1个数组的大小,但是指针的类型依旧为数组指针类型 printf("%d\n", sizeof(&a[0])); //4或8 取的是数组首元素的地址 printf("%d\n", sizeof(&a[0] + 1)); //4或8 数组首元素地址+1,跳过一个整型的大小 return 0; }

这种题主要是看sizeof里面是数组名,还是数组的地址,或是数组元素的地址,或者是数组中的元素,只要分辨清楚就行了。
对数组的指针解引用的话,得到的是指向的那个数组的首元素的地址,也是数组名。
int main() { //字符数组 char arr[] = { 'a','b','c','d','e','f' }; printf("%d\n", sizeof(arr)); //6 数组里面有6个元素,每个元素都是字符类型的,为1个字节大小 printf("%d\n", sizeof(arr + 0)); //4或8 字符指针也是指针,同样是4或8个字节大小 printf("%d\n", sizeof(*arr)); //1 数组首元素 printf("%d\n", sizeof(arr[1])); //1 数组第二个元素 printf("%d\n", sizeof(&arr)); //4或8 整个数组的地址 printf("%d\n", sizeof(&arr + 1)); //4或8 数组指针+1,跳过1个数组,但它还是一个指针 printf("%d\n", sizeof(&arr[0] + 1)); //4或8 首元素地址+1,跳过一个字符的大小 return 0; }

下面这道题,是计算strlen的大小,strlen函数的参数是char*类型的,从给定的指针开始向后找到\0,然后统计\0之前到指针所指向的那块空间之间的字符个数,也可以认为是内存单元的数量。
int main() { char arr[] = { 'a','b','c','d','e','f' }; printf("%d\n", strlen(arr)); //随机值 从'a'开始向后统计,直到遇到\0为止 printf("%d\n", strlen(arr + 0)); //随机值 //printf("%d\n", strlen(*arr)); //err a的ASCII码值是97,相当于把这个当作地址传给了strlen,而这个指针所指向的空间没有申请访问的权限,所以这个代码是不能这么写的。 //printf("%d\n", strlen(arr[1])); //err 和上面的一样 printf("%d\n", strlen(&arr)); //随机值 数组的指针,传给strlen函数时会转换为char*类型的指针 printf("%d\n", strlen(&arr + 1)); //随机值 数组的指针+1跳过1个数组,从'f'后面的那个元素开始进行统计 printf("%d\n", strlen(&arr[0] + 1)); //随机值 数组首元素地址+1,从第二个元素开始向后统计 return 0; }

创建数组时会申请多一部分空间,所以strlen就算发生了数组越界,编译器也只是给出一个警告。

int main() { char arr[] = "abcdef"; printf("%d\n", sizeof(arr)); //7 字符串后面默认有一个\0,所以数组有7个元素 printf("%d\n", sizeof(arr + 0)); //4或8 数组首元素地址 printf("%d\n", sizeof(*arr)); //1 数组首元素的地址 printf("%d\n", sizeof(arr[1])); //1 数组第2个元素,即'b' printf("%d\n", sizeof(&arr)); //4或8 数组的地址,即数组指针 printf("%d\n", sizeof(&arr + 1)); //4或8 printf("%d\n", sizeof(&arr[0] + 1)); //4或8 数组第2个元素的地址 return 0; }

int main() { char arr[] = "abcdef"; printf("%d\n", strlen(arr)); //6 printf("%d\n", strlen(arr + 0)); //6 //printf("%d\n", strlen(*arr)); //err //printf("%d\n", strlen(arr[1])); //err printf("%d\n", strlen(&arr)); //6 printf("%d\n", strlen(&arr + 1)); //随机值 数组指针跳过一个数组,此时指针指向的是\0后面的那个字符,从那个字符开始向后统计 printf("%d\n", strlen(&arr[0] + 1)); //5 return 0; }

int main() { char* p = "abcdef"; printf("%d\n", sizeof(p)); //4或8 p是个字符指针,存放的是"abcdef"字符串首字符的地址 printf("%d\n", sizeof(p + 1)); //4或8 指向了'b' //printf("%d\n", sizeof(*p)); //err 非法访问 //printf("%d\n", sizeof(p[0])); //err printf("%d\n", sizeof(&p)); //4或8 二级指针也是指针 printf("%d\n", sizeof(&p + 1)); //4或8 指向一级指针的二级指针+1,跳过了一个指针的大小,指向了p指针的后面 printf("%d\n", sizeof(&p[0] + 1)); //4或8 p解引用找到'a',再取a的地址,再+1,此时指针指向了'b' return 0; }

int main() { char* p = "abcdefgh"; printf("%d\n", strlen(p)); //8 printf("%d\n", strlen(p + 1)); //7 //printf("%d\n", strlen(*p)); //err //printf("%d\n", strlen(p[0])); //err printf("%d\n", strlen(&p)); //随机值 printf("%d\n", strlen(&p + 1)); //随机值 printf("%d\n", strlen(&p[0] + 1)); //7 return 0; }

int main() { //二维数组 int a[3][4] = { 0 }; printf("%d\n", sizeof(a)); //48 计算的是整个二维数组的大小 printf("%d\n", sizeof(a[0][0])); //4 a[0][0]即*(*(a+0)+0),先对二维数组首元素即第一行数组的地址,一维数组的地址进行解引用,得到第一行数组的首元素地址,首元素地址+0解引用,得到首元素,首元素的大小是4个字节 printf("%d\n", sizeof(a[0])); //16 对二维数组第一行数组进行解引用,得到的是第一行数组即一维数组的数组名,所以这里计算的是整个第一行数组的大小 printf("%d\n", sizeof(a[0] + 1)); //4或8 数组首元素的地址+1,得到第二个元素的地址 printf("%d\n", sizeof(*(a[0] + 1))); //4 数组第二个元素 printf("%d\n", sizeof(a + 1)); //4或8 第二行数组的地址,是个数组指针 printf("%d\n", sizeof(*(a + 1))); //16 第二行数组的数组名 printf("%d\n", sizeof(&a[0] + 1)); //4或8 二级指针+1 printf("%d\n", sizeof(*(&a[0] + 1))); //16 &第一行数组名,得到的是整个一维数组的地址,+1再解引用之后,得到的是第二行数组的数组名 printf("%d\n", sizeof(*a)); //16 一维数组的数组名 printf("%d\n", sizeof(a[3])); //16 一维数组的指针跳过了3个一维数组的大小(一维数组是二维数组的其中一行) return 0; }

要注意sizeof里面的是不是数组名,如果是数组名,则计算的是整个数组的大小。
10.指针笔试题1.
int main() { int a[5] = { 1, 2, 3, 4, 5 }; int *ptr = (int *)(&a + 1); //数组指针+1跳过一个数组,然后强制类型转换成int*类型的 printf( "%d,%d", *(a + 1), *(ptr - 1)); return 0; } //程序的结果是什么?

2.
//结构体的大小是20个字节 struct Test { int Num; char* pcName; short sDate; char cha[2]; short sBa[4]; }*p; //假设p 的值为0x100000。 如下表表达式的值分别为多少? //已知,结构体Test类型的变量大小是20个字节 int main() { printf("%p\n", p + 0x1); //结构体指针+1,跳过一个结构体,所以地址上应该加了20,因为跳过了20个字节 printf("%p\n", (unsigned long)p + 0x1); //强制类型转换为unsigned long,这里是整型数据+1 printf("%p\n", (unsigned int*)p + 0x1); //强制类型转换为整型指针,然后+1,跳过一个整型,所以地址数值上+4 return 0; }

3.
int main() { int a[4] = { 1, 2, 3, 4 }; int* ptr1 = (int*)(&a + 1); //数组指针+1后转换为int*类型,此时指针指向4后面的那个元素 int* ptr2 = (int*)((int)a + 1); //整型指针转换为整型后+1,这里是数组首元素的地址数值上+1,然后再把类型转换回整型指针 printf("%x,%x", ptr1[-1], *ptr2); //ptr[-1]相当于*(ptr - 1),得到的是数组第四个元素 return 0; }

c语言|C语言中的指针(进阶)
文章图片

4.
#include int main() { int a[3][2] = { (0, 1), (2, 3), (4, 5) }; //这里是逗号表达式,只取逗号表达式中最后一个表达式的值,所以这里往二维数组中存的是1,3,5,其余元素则初始化为0 int* p; p = a[0]; //数组首元素的地址 printf("%d", p[0]); //数组首元素 return 0; }

5.
int main() { int a[5][5]; int(*p)[4]; p = a; //用int(*)[4]类型的数组指针来接收二维数组第一行数组的地址,所以p+1只会跳过4个数组的元素的大小 printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]); return 0; }

c语言|C语言中的指针(进阶)
文章图片

指针-指针得到的是指针之间元素的个数,但是这里是低地址-高地址,所以元素的个数前还要加个负号,得到的是-4,是个整型数据,内存中存放的是它的补码。
6.
int main() { int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int *ptr1 = (int *)(&aa + 1); int *ptr2 = (int *)(*(aa + 1)); printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1)); return 0; }

分辨清楚整个二维数组的地址和二维数组首元素地址的区别。
7.
#include int main() { char *a[] = {"work","at","alibaba"}; char**pa = a; pa++; printf("%s\n", *pa); return 0; }

画个图理解一下:
c语言|C语言中的指针(进阶)
文章图片

pa中存放的是a数组首元素的地址,a数组中存放的是三个常量字符串首字符的地址。
8.
nt main() { char *c[] = {"ENTER","NEW","POINT","FIRST"}; char**cp[] = {c+3,c+2,c+1,c}; char***cpp = cp; printf("%s\n", **++cpp); printf("%s\n", *--*++cpp+3); printf("%s\n", *cpp[-2]+3); printf("%s\n", cpp[-1][-1]+1); return 0; }

同样的,遇到指针的题,画个图来理解:
c语言|C语言中的指针(进阶)
文章图片

对指针解引用可以得到指针指向的那块空间存放的内容,同时要注意指针的类型,这决定了指针进行解引用时访问几个字节的空间。
其中,语句printf("%s\n", *cpp[-2]+3); 中cpp[-2]会被编译器解析为*(cpp - 2)。
最后,关于指针进阶篇的内容就到这里了。
今后也会不定期更新

    推荐阅读