说明
这是一个完善了但又不完善的笔记,或许以后会更新
可以参考但请务必超越
源文件
Tools
VS2019
VScode
Typora
指针进阶
1.字符指针
2.数组指针
3.指针数组
4.数组传参和指针传参
5.函数指针
6.函数指针数组
7.指向函数指针数组的指针
8.回调函数
9.指针和数组面试题的解析
1.字符指针
面试题:
没有const修饰的指针
str1和str2指向的地址不同
有const修饰的指针
str3和str4指向同一个地址
2.指针数组
int* arr1[10]; //整形指针的数组
char *arr2[4]; //一级字符指针的数组
char **arr3[5];//二级字符指针的数组
[]优先级比*高,应该不用加(),当然加了也没事
指针数组的数组名是用二级指针存放的吗?
3.数组指针
数组指针的定义:
数组指针是指针?还是数组?
答案是:指针。
我们已经熟悉: 整形指针: int pint; 能够指向整形数据的指针。 浮点型指针: float pf; 能够指向浮点型数据的指针。
那数组指针应该是:能够指向数组的指针。
数组指针的类型:
去掉数组名,剩下的就是类型
int (*)[10]
&数组名VS数组名
int* (*)[10] //指针类型
数组指针的使用
...
见code
判断:
int arr[5];//整型数组
int *parr1[10];//parr1是一个数组,10个元素,每个元素是int*的。所以parr1是一个存放指针的数组
int (*parr2)[10];//parr2是一个数组指针,改指针指向的数组又10个元素,每个元素是int
int (*parr3[10])[5];//parr3是一个数组,数组有10个元素
//每个元素是一个数组指针,改指针指向的数组有5个元素
//每个元素是int
4.数组参数、指针参数
一维数组传参
#include <stdio.h>
void test(int arr[])//ok?
{}//没问题
void test(int arr[10])//ok?
{}//没问题,写几都行,虽然语法正确,但是不建议
void test(int *arr)//ok?
{}//没问题
void test2(int *arr[20])//ok?
{}//保持一模一样,没问题,省略数字也没问题
void test2(int **arr)//ok?
{}//因为每个元素都是int*,所以用int*接收也没问题
int main()
{
int arr[10] = {0};
int *arr2[20] = {0};//每个元素都是int*
test(arr);//数组名是首元素地址,传的都是首元素地址
test2(arr2);
}
二维数组的传参
二维数组的首元素,指的是第一行。
想象成一维数组就是,一行一个元素。
void test(int arr[3][8])//ok?
{}//数组传过来,用数组接收,没问题
void test(int arr[][])//ok?
{}//列不能省略,传了个寂寞
void test(int arr[][9])//ok?
{}//行可以省略,知道了一列有几个,才能顺序接收下来,不然不知道第二行从什么地方开始接收。
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int *arr)//ok?
{}//不能用整型指针接收
void test(int* arr[5])//ok?
{}//指针数组,完全不搭边
void test(int (*arr)[5])//ok?
{}//只有这样可以,要传的话必须要传表示一维数组的指针,因为二维数组arr的首元素是一个一维数组。
void test(int **arr)//ok?
{}//二级指针也不行,而且传的也是整型
int main()
{
int arr[3][10] = {0};
test(arr);
}
一级指针传参
#include <stdio.h>
void print(int *p, int sz)//用一级指针接收,没问题
//void print(int p[], int sz)数组接收也行,但是一般不建议这样写,指针就用指针接收比较好
{
int i = 0;
for(i=0; i<sz; i++)
{
printf("%d\n", *(p+i));
}
}
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9};
int *p = arr;
int sz = sizeof(arr)/sizeof(arr[0]);
//一级指针p,传给函数
print(p, sz);
return 0;
}
思考:假设一个函数的参数是一级指针,函数能接受什么参数?
(当一个函数的参数部分为一级指针的时候,函数能接收什么参数?)
void test1(int *p)
{}
//test1函数能接收什么参数?
void test2(char* p)
{}
//test2函数能接收什么参数?
int main()
{
int a = 10;
int* p1 = &a;
int arr[10] = {0};
test1(&a);//整型变量的地址
test1(arr);//数组名,首元素
test1(p1);//一级整型指针
//test(NULL);考虑清楚,当然语法上也支持,但是没意义,传了个寂寞
//当然char同理
}
二级指针传参
void test(int **ppa)
{}
int main()
{
int a = 10;
int* pa = &a;
int** ppa = &pa;
int* arr[5];//指针数组
test(paa);//二级指针
test(&pa);//一级指针变量的地址
test(arr);//数组名,首元素地址
}
5.函数指针
前提注意:
有人会有疑问,函数指针好像没什么卵用
当然,在太简单的代码中可能完全用不到(总不能写上个位数代码,为了用个函数指针而用函数指针吧),杀鸡焉用牛刀?
在这里提前说明:下面的函数指针数组,指向函数指针数组的指针,回调函数等,将阐述标明函数指针是非常重要的!
尤其是在大型的工程中,进行复杂的工程,非常有用!
函数指针变量
//整型指针 - 指向整型
//字符指针 - 指向字符
//数组指针 - 指向数组
//那么 函数指针变量 - 存放函数的地址
//&数组名 - 数组的地址
//数组名 - 数组首元素的地址
//函数名 == &函数名
//函数的地址我们怎么存呢?
int(*pf)(int,int) = &Add;//pf是用来存放函数的地址 - pf就是函数指针变量
//返回类型(指针)(传入类型)= &函数名;
//如果不加括号
//int* pf(int,int) = &Add;err,pf就变成了函数名,返回类型就变成了int*
//类比数组指针
int arr[10] = &arr;
int (*parr)[10] = &arr;//parr就是数组指针变量
函数指针类型
//去掉名,就是类型
int a = 10;//int
int arr[10] = {0};//int [10]
int (*parr)[10] = &arr;//int (*)[10]
//函数指针一样的道理
int(*pf)(int,int) = &Add;//int(*)(int,int) = &Add;
函数指针的应用
//我们有时候并不能直接拿到一个函数或变量,只能拿到他的地址,这时候就可以用指针,通过解引用指针就可以找到这个变量
//下面函数指针数组应用中实现的计算器,就更好的证明了这一点
int(*pf)(int,int) = &Add;
int ret = Add(2, 3);//通过函数调用
printf("%d\n", ret);//5
ret = (*pf)(4, 5);//通过函数指针调用
printf("%d\n", ret);//9
//注意!
//首先函数名也是函数的地址
int(*pf)(int, int) = Add;//去掉&也可以获取
//能把Add赋给pf,而且什么警告都没有
//说明Add == pf
//是不是可以直接不解引用
int ret = pf(2, 3);
printf("%d\n", ret);//5
ret = pf(4, 5);
//而且多写几颗*都可以
//ret = (****pf)(4, 5);多解几次也可以
printf("%d\n", ret);//9
//*在这里就是摆设,既然如此为什么还要放呢? - 让初学者更加容易理解
阅读两段有趣的代码
//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);
注 :推荐《C陷阱和缺陷》
这本书中提及这两个代码。
//代码1拆分开
//可以看到void(*)() - 是一个函数指针类型,0是一个int被强行转换成前面void(*)()类型的地址,*再解引用找回那个函数调用
//代码是一次函数调用,解析:
//1.代码中把0强制类型转换为void(*)()的一个函数的地址。
//2.解引用0地址,就是去调用0地址处的这个函数,被调用的函数是无参,返回类型是void
//在当前所有编译器上,0是个值,可能调用不了,但是对于硬件来说是可以用的。
//代码2从里从外都可以开始解释,是一个有趣的套娃
//这里从signal开始解释
//代码是一次函数声明,解析:
//1.声明的函数名是signal
//2.signal函数有2个参数,第一个是int类型,第二个是void(*)(int)的函数指针类型
//3.signal函数的返回类型依然是void(*)(int)的函数指针类型
//可以把signal提出来理解
//void(*)(int) signal(int, void(*)(int));当然这是错误的,err
//我们可以给void(*)(int)这个类型使用typedef定义一个函数(别名)
//就像typedef int int32 - int32就是int的别名
//typedef void(* pfun_t)(int) - pfun_t就表示把pfun_t去掉后的void(*)(int),这句代码写在main()前
//虽然很别扭,但这样就可以方便的理解,代码2就可以写成
typedef void(*pfun_t)(int);
int main()
{
pfun_t signal2(int, pfun_t);
return 0;
}
代码2太复杂,如何简化:
typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);
以上内容可直接在源代码中运行查看
6.函数指针数组
首先有加减乘除4个函数
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
那么类似于整型,函数指针也可以放进数组中
//int* arr[10];//整型指针的数组
//函数指针数组 - 存放函数指针的数组(废话)
int(*pf1)(int, int) = Add;
int(*pf2)(int, int) = Sub;
int(*pf3)(int, int) = Mul;
int(*pf4)(int, int) = Div;
//想把这4个函数统一管理起来到一个数组中
int(*pfArr[4])(int, int) = { Add,Sub,Mul,Div };//pfArr就是一个函数指针的数组
函数指针数组应用
实现一个计算器
(源代码可查看运行)
简单打印个菜单
void menu()
{ printf("****************************************\n");
printf("*********** 1.add 2.sub ***********\n");
printf("*********** 3.mul 4.div ***********\n");
printf("*********** 0.exit ***********\n");
printf("*************by MyWifeAsuna*************\n");
printf("****************************************\n");
}
使用input变量选择功能,设定x和y为需要运算的数字,sum为结果
int input = 0;
int x = 0;
int y = 0;
int sum = 0;
麻烦的使用Switch
如果使用switch,可以想象重复的代码和参数有很多
do
{
menu();
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
printf("请输入2个操作数:");
scanf("%d %d", &x, &y);
sum = Add(x, y);
printf("sum = %d\n", sum);
break;
case 2:
printf("请输入2个操作数:");
scanf("%d %d", &x, &y);
sum = Sub(x, y);
printf("sum = %d\n", sum);
break;
case 3:
printf("请输入2个操作数:");
scanf("%d %d", &x, &y);
sum = Mul(x, y);
printf("sum = %d\n", sum);
break;
case 4:
printf("请输入2个操作数:");
scanf("%d %d", &x, &y);
sum = Div(x, y);
printf("sum = %d\n", sum);
break;
case 0:
printf("退出计算器\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
可以看到,使用switch有至少三行代码一直在重复
假设,还要添加更多的运算功能
x&y x|y x>>y x<<y x^y ...只要是2个数能运算的都加上,
那这个代码简直是,冗长又难管
里面的代码大多数都是重复的,参数也都是重复的
所以,来改造!
优化(heal ?重点)
do
{
menu();
printf("请选择:");
scanf("%d", &input);
//这里把上面的函数指针数组应用下来
//int(*pfArr[4])(int, int) = { Add,Sub,Mul,Div };
//现在4个函数都有了下标,前面再加个0把下标挤过去
int(*pfArr[5])(int, int) = { 0,Add,Sub,Mul,Div };
//使用下标调用函数 0 1 2 3 4
if (input == 0)
{
printf("退出计算器\n");
}
else if(input>=1 && input<=4)
{
printf("请输入2个操作数:");
scanf("%d %d", &x, &y);
sum = pfArr[input](x, y);//input选择是几就进入下标为几的函数,x和y也传进去
//注意!这里体现了,有时我们并不能使用函数名,而只能使用函数指针,函数指针可以选择运算函数,单写函数名就只是单独的一个运算功能
//如果这里使用Add,Sub ... 就需要写更多的else
printf("sum = %d\n", sum);
}
else
{
printf("选择错误\n");
}
} while (input);
如果以后还要添加的话,只需要扩大数组变更判断就可以了
int(*pfArr[n])(int, int) = { 0,Add,Sub,Mul,Div ... n };
else if(input>=1 && input<=n-1)
优化的使用Switch
之前switch的代码我们可以看到
相同的代码重复出现
思考一下我们写case语句是为了什么,不就是为了实现不同的功能吗。那么我们可以把所有的功能再放进一个函数中统一管理。
写一个Calc()函数把重复的内容放进去,把运算函数Add,Sub ... 也放进去使用指针调用。
void Calc(int (*pf)(int, int))
{
int x = 0;
int y = 0;
int sum = 0;
printf("请输入2个操作数:");
scanf("%d %d", &x, &y);
sum = pf(x, y);//pf == *pf == 运算函数名(Add,Sub ...)
//当然,这里和前面一样只能使用函数指针来调用几个运算函数,而不能使用单独的一个函数名
printf("sum = %d\n", sum);
}
代码就可以优化成
do
{
menu();
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
Calc(Add);
break;
case 2:
Calc(Sub);
break;
case 3:
Calc(Mul);
break;
case 4:
Calc(Div);
break;
case 0:
printf("退出计算器\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
7.指向函数指针数组的指针
虽然感觉很绕,但其实很简单,也很好理解
类比数组指针和指针数组
int arr[10];
int (*p)[10] = &arr;
//p是一个指向整型数组的指针
int* arr[10];//整型指针的数组
int* (*p)[10] = &arr;//整数指针数组的地址
//p是一个指向(整型指针数组)的指针
int(*pf)(int,int) = &Add;//pf是函数指针
int(*pfArr[5])(int, int);//pfArr是一个函数指针的数组
int(*(*p)[5])() = &pfArr;
//p是一个指向函数指针数组的指针
可以看到因为都可以互相放进去,所以其实是能无限套娃的,但再往下套就太没有必要了。
所以,到此为止!
8.回调函数
首先:回调函数必须依赖函数指针
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
上面计算器的Calc()就是一个回调函数。
void Calc(int (*pf)(int, int))//传入运算函数的指针pf
{
int x = 0;
int y = 0;
int sum = 0;
printf("请输入2个操作数:");
scanf("%d %d", &x, &y);
sum = pf(x, y);//通过调用pf来运算
}
qsort函数
void qsort (void* base, size_t num, size_t size, int (*compar)(const void*, const void*));
排序的方法如我们所知:
冒泡排序
选择排序
插入排序
快速排序
qsort是C语言提供的一个快速排序的库函数,包含在C标准库<stdlib.h中>
可以看到qsort有4个参数
void* base,//void*类型的base
size_t num,//size_t类型的num
size_t size,//size_t类型的尺寸
int (*compar)(const void*,const void*)//函数指针,两个参数是const void*类型,返回类型是int
相比较冒泡排序:
void bubble_sort(int arr[], int sz)
{
//趟数
int i = 0;
for (i = 0; i < sz - 1; i++)
{
//一趟
int j = 0;
for (j = 0; j < sz - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
void print_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
int main()
{
int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
//8 9 7 6 5 4 3 2 1 0
//..
//8 7 6 5 4 3 2 1 0 9
//这是一趟冒泡排序,使9移动到了最后
//接下里从8到0又一趟
//7 6 5 4 3 2 1 0 8 9
//10个元素每冒泡排序一趟只能处理一个元素,处理完需要9趟
//且每一趟要处理的对数都不一样
int sz = sizeof(arr) / sizeof(arr[0]);
//升序
bubble_sort(arr, sz);
//打印
print_arr(arr, sz);
return 0;
}
可以看到,这个冒泡排序是固定死的,固定了int类型
qsort的好处就是什么类型都可以
为什么呢?
举个栗子:
可以看到,把int*和float*赋给*虽然在我的编译器上运行了起来,但是会有不兼容的警告(ctrl+f7即可查看)
没有任何警告
void* - 无具体类型的指针
她里面可以放任何类型的指针,也就相当于一个通用类型,所以qsort的第一个参数不是写死的任何类型的指针,而是一个void*
优点:能够接收任意类型的地址
缺点:不能进行运算,不能+-整数,不能解引用
不能进行运算是什么意思呢?
这里int*类型的p1+1,可以得知意思是跳过一个整型int
但是void*不行
运算了个寂寞,p3++也就是p3+1。
size_t num - 待排序的元素个数
知道了要排序的类型,还要知道要排序的个数
有一些版本或者旧版本会使用nitems,完全不影响
void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*));
size_t size - 一个元素的大小,单位是字节
有一些版本或者旧版本会使用width(宽度),当然不影响
void qsort (void* base, size_t num, size_t width, int (*compar)(const void*, const void*));
关键点注意
仅仅有前三个参数是不能进行排序的,前三个参数只是获得了要排序的元素,并没有比较,大于小于等等。
而且比较规则方法也不确定。如果要排序结构体的话,假设一个学生成绩名单:
struct Stu
{
char name[20];
int age;
float score;
}
//有3个学生数据
{
{"张三",20,99.5f},
{"李四",30,65.5f},
{"王五",55,76.5f},
}
这时如果要用qsort排序,这要怎么比呢?是按名字、年龄、分数还是身高体重或者各种奇怪的数据呢?
这时 --
int (*compar)(const void*,const void*) - 指向排序时比较2个元素的函数
qsort抽象出来一个函数:compar(cmp)
有一些版本或者旧版本会使用compare,当然不影响
void qsort(void* base, size_t num, size_t width, int(__cdecl*compare)(const void* elem1, const void* elem2));
int (*compar)(const void* elem1,const void* elem2)//这个地方的elem1和elem2是要比较的2个元素的地址
光说不练假把式
这里的第四个参数内容是这样设计的
- 当elem1小于elem2的时候,返回一个小于0的数字(负数)
- 当elem1等于elem2的时候,返回0
- 当elem1大于elem2的时候,返回一个大于0的数字(正数)
解释比较2个整型函数的代码
int cmp_int(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
//e1和e2都是void*类型的不能直接用
//强制类型转换成int* - int* e1 和 int* e2
//解引用 - *(int*)e1 和 *(int*)e2
//相减返回值就行了
}
//打印
void print_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
int main()
{
int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
qsort(arr, sz, sizeof(arr[0]), cmp_int);
print_arr(arr, sz);
return 0;
}
回到回调函数的定义:
在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
在以上的例子中,cmp_int就是一个回调函数,把指向cmp_int的指针传给qsort作为compar参数使用
多种测试
使用名字排序
struct Stu
{
char name[20];
int age;
};
int cmp_name(const void* e1, const void* e2)
{
return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
void test()
{
struct Stu s[3] = { {"张三", 15},{"李四", 30}, {"王五",10} };
int sz = sizeof(s) / sizeof(s[0]);
//假设按照名字排序
qsort(s, sz, sizeof(s[0]), cmp_name);
}
int main()
{
test();
return 0;
}
这里就不直接运行了,简单写代码测试下
可以看到结构体已经创建好了,接下来排序
按英文字母z最大然后是w最小是l
李四,王五,张三。
使用年龄排序
struct Stu
{
char name[20];
int age;
};
int cmp_age(const void* e1, const void* e2)
{
return (((struct Stu*)e1)->age - ((struct Stu*)e2)->age);
}
void test()
{
struct Stu s[3] = { {"张三", 15},{"李四", 30}, {"王五",10} };
int sz = sizeof(s) / sizeof(s[0]);
//假设按照年龄排序
qsort(s, sz, sizeof(s[0]), cmp_name);
}
int main()
{
test();
return 0;
}
稍微修改名字的函数然后调试
排序前
排序后
10<15<30。
模拟qsort
既然要模拟,那就使用冒泡排序模拟一下,快速排序当然也可以。
简单给定一组数,并且传参到自定义的BubbleSort中
void test1()
{
int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
BubbleSort(arr, sz, sizeof(arr[0]), cmp_int);
print_arr(arr, sz);
}
int main()
{
test1();
return 0;
}
重点来了,接下来会非常硬核
首先创建BubbleSort函数
//使用回调函数实现一个通用的冒泡排序函数
void BubbleSort(void* base, size_t num, size_t size, int(*cmp)(const void* e1, const void* e2))
{
size_t i = 0;//注意int是有符号数,如果i和无符号数的num比较也需要写成size_t,下面的j同理
//趟数
for (i = 0; i < num - 1; i++)
{
size_t j = 0;
//比较的对数
for (j = 0; j < num - 1 - i; j++)
{
if (cmp((char*)base + j * size, (char*)base + (j + 1) * size > 0))//重点
{
//e1>e2,交换
Swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
}
}
}
}
在此代码中可以看到,当两个数进行比较的时候,因为base是void*类型无法使用,强转什么类型都不是很合适,这个地方需要一个最细的力度,所以使用char*来进行比较,加1就跳过1个字符,加n就跳过n个字符,自然加size就跳过size个字符。
if (cmp((char*)base, (char*)base + size > 0))
{
}
这样做只是把下标为0和1的元素进行了比较,但是比较完第一对数后自然要比较第二对数,所以使用base直接加size也不行。于是,改为加一个j*size。那么相对的下一个元素就是(j+1)*size.
if (cmp((char*)base + j * size, (char*)base + (j + 1) * size > 0))
{
}
再然后如果大于0的话,需要交换,也是重点
思考一下,如果要交换的话,只有2个数也是不行的,不知道这2个元素多大,需要size。
Swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
知道了size的话就知道该交换多少个字节了,假设这里使用9,8,7,6,5,4,3,2,1,0。那么首先就是9和8的交换
由低地址到高地址,相对的一个字节一个字节的交换
void Swap(char* buf1, char* buf2, int size)//重点
{
int i = 0;
for ( i = 0; i < size; i++)
{
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++;
buf2++;
}
}
到此位置完成了qsort的模拟。
当然结构体也可以排序,按名字或是年龄都可以,这里只展示名字排序:
排序前:
排序后:
9.指针和数组笔试题解析
有一堆要打印的,计算数组大小的值(注意x86和x64)
//一维数组
int a[] = {1,2,3,4};
printf("%d\n",sizeof(a));//数组名a单独放在sizeof内部,数组名表示整个数组,计算的是整个数组的大小
printf("%d\n",sizeof(a+0));//a表示首元素的地址,a+0还是首元素的地址,地址的大小是4/8字节
printf("%d\n",sizeof(*a));//a表示首元素的地址,*a就是首元素 ==> a[0],大小就是4
//*a <==> *(a+0) <==> a[0]
printf("%d\n",sizeof(a+1));//a表示首元素的地址,a+1是第二个元素的地址,大小就是4/8
printf("%d\n",sizeof(a[1]));//a[1]就是第二个元素 - 4
printf("%d\n",sizeof(&a));//&a - 数组的地址 - 4/8 - int(*)[4]
//数组的地址也还是地址
printf("%d\n",sizeof(*&a));//*&a - &a是数组的地址,对数组的地址解引用拿到的是数组,整个数组的大小,所以大小是16
//取出来地址再解引用,那不就抵消了,就和第1条代码一样了 - printf("%d\n",sizeof(a));
printf("%d\n",sizeof(&a+1));//&a是数组的地址,&a+1是数组的地址+1,也就是跳过整个数组。虽然跳过了数组,还是个地址 - 4/8
printf("%d\n",sizeof(&a[0]));//取出了首元素地址 - 4/8
printf("%d\n",sizeof(&a[0]+1));//&a[0]是第一个元素的地址,&a[0]+1就是取出了第二个元素的地址 - 4/8
//字符数组
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr));//6
printf("%d\n", sizeof(arr+0));//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
printf("%d\n", sizeof(&arr[0]+1));//4/8
printf("%d\n", strlen(arr));//随机值
printf("%d\n", strlen(arr+0));//随机值
printf("%d\n", strlen(*arr));//*arr - a - 97 - err
//strlen以为传进来的'a'的ascii码值97就是地址
printf("%d\n", strlen(arr[1]));//arr[1] - b - 98 - err
printf("%d\n", strlen(&arr));//随机值
printf("%d\n", strlen(&arr+1));//随机值
printf("%d\n", strlen(&arr[0]+1));//随机值
char arr[] = "abcdef";
printf("%d\n", sizeof(arr));//7
printf("%d\n", sizeof(arr+0));//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
printf("%d\n", sizeof(&arr[0]+1));//4/8
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));//随机值
printf("%d\n", strlen(&arr[0]+1));//5
char *p = "abcdef";
printf("%d\n", sizeof(p));//p是一个指针变量,存放的是首元素a的地址 - 4/8
printf("%d\n", sizeof(p+1));//p+1是字符b的地址 - 4/8
printf("%d\n", sizeof(*p));//1
printf("%d\n", sizeof(p[0]));//p[0] -->*(p+0) --> *p - 1
printf("%d\n", sizeof(&p));//4/8
printf("%d\n", sizeof(&p+1));//4/8
printf("%d\n", sizeof(&p[0]+1));//4/8
printf("%d\n", strlen(p));//6
printf("%d\n", strlen(p+1));//5
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));//随机值
//二维数组
int a[3][29] = {0};
printf("%d\n",sizeof(a));//48
printf("%d\n",sizeof(a[0][0]));//4
printf("%d\n",sizeof(a[0]));//a[0]是第一行的数组名,数组名单独放在sizeof内部 - 16
printf("%d\n",sizeof(a[0]+1));//arr[0]是第一行的数组名,并没有单独放在sizeof内部,也没有&,所以arr[0]表示首元素的地址,就是第一行这个数组第一个元素的地址。所以a[0]+1就是第一行,第二个元素的地址 - 4
printf("%d\n",sizeof(*(a[0]+1)));//*(a[0]+1)就是第一行第二个元素 - 4
printf("%d\n",sizeof(a+1));//数组名a,并没有单独放在sizeof内部,也没有&,所以a表示首元素的地址。二维数组的首元素就是他的第一行。所以a+1就是第二行的地址。a他的指针类型就是int(*)[4] - 4/8
printf("%d\n",sizeof(*(a+1)));//*(a+1)就是第二行,*(a+1) --> a[1] - 16
printf("%d\n",sizeof(&a[0]+1));//a[0]是第一行的数组名,&a[0]拿到的是第一行的地址,&a[0]+1,就是第二行的地址,他的类型是int(*)[4] --> a+1 - 4/8
printf("%d\n",sizeof(*(&a[0]+1)));//解引用和取地址抵消 *(&a[0]+1)--> *(&a[1]) --> a[1],就是第二行 - 16
printf("%d\n",sizeof(*a));//a表示首元素的地址,二维数组首元素是第一行,*a就是第一行,也就是第一行的数组名a[0] - 16
printf("%d\n",sizeof(a[3]));//一共就3行,下标0,1,2。但是如果只是看一眼第四行,是可以的,他是有类型的,所以不会出问题。a[3]假设存在,就是第四行的数组名,sizeof(a[3]),就相当于把第四行的数组名单独放在sizeof内部 - 16
//二维数组的数组名是a
//第1行的数组名是a[0]
//第2行的数组名是a[1]
//第3行的数组名是a[2]
//sizeof(a)
//sizeof(a[0]) &a[0]
//sizeof(a[1]) &a[1]
//sizeof(a[2]) &a[2]
//在以上两种情况下,数组名表示整个数组,除此之外
//a - 二维数组的首元素(第一行)地址
//a[0] - 第1行第1个元素的地址
//a[1] - 第2行第1个元素的地址
//a[2] - 第3行第1个元素的地址
//sizeof根据类型计算,不会真实运算。类型也是可以放在sizeof内部的,就比如以前学过的
printf("%d\n",sizeof(int);
数组名的意义
很重要的内容,最好牢牢记住
- sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
- &数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
- 除此之外所有的数组名都表示首元素的地址。
10.指针笔试题
笔试题1:
int main()
{
int a[5] = { 1, 2, 3, 4, 5 };
int *ptr = (int *)(&a + 1);//数组地址+1,跳过这组地址,地址类型是int(*)[5],强制类型转换int*。
printf( "%d,%d", *(a + 1), *(ptr - 1));//首元素+1和数组地址加一过后的首元素+1。解引用访问到2,5
return 0;
}
笔试题2:
struct Test
{
int Num;
char *pcName;
short sDate;
char cha[2];
short sBa[4];
}*p;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
//已知,结构体Test类型的变量大小是20个字节(32位,64位的话会不一样)
int main()
{
p=(struct Test*)0x100000;//p的类型是结构体指针类型是struct Test* -- int,整型赋给指针会有类型的差异,需要强制类型转换
printf("%p\n", p + 0x1);//p+0x1
printf("%p\n", (unsigned long)p + 0x1);//p被强制类型转换成unsigned long后+0x1
printf("%p\n", (unsigned int*)p + 0x1);//p被强制类型转换成unsigned int*后+0x1
return 0; }
p结构体指针+0x1,就是跳过一个结构体,这里就是跳过了1x20个字节。强转unsigned long之后变成了整数,整数+0x1那就是+1。强转unsigned int*之后变成了整型指针,+0x1就是+4
p+0x1就是0x10000014(16进制的20),打印出来应该是00100014
(unsigned long)p + 0x1就是00100001
(unsigned int*)p + 0x1就是00100004
%p以地址的形式打印,默认不打印0x。可以自己加上0x,也可以用%#x以16进制的形式打印,会自动加0x。
笔试题3:
int main()
{
int a[4] = { 1, 2, 3, 4 };
int *ptr1 = (int *)(&a + 1);//取出整个数组的地址,加1跳过整个数组
int *ptr2 = (int *)((int)a + 1);
printf( "%x,%x", ptr1[-1], *ptr2);//ptr[-1] --> *(ptr+(-1)) --> *(ptr-1)。a强制类型转换成int,加1就是加了一个字节。
//4 2000000
return 0;
}
笔试题4:
int main()
{
int a[3][32] = { (0, 1), (2, 3), (4, 5) };//这里使用了小括号()可以说实际上放进去的只有1,3,5。int a[3][33] = { 1, 3, 5 };
int *p;
p = a[0];//首元素地址赋给指针p,指向首元素1的地址
printf( "%d", p[0]);//*(p+0) --> *p 还是1
return 0;
}
笔试题5:
int main()
{
int a[5][34];//a的类型是int(*)[5]
int(*p)[4];//p的类型是int(*)[4]
p = a;//虽然类型不一样但是还是强行放了进去
printf( "%p,%d\n", &p[4][35] - &a[4][36], &p[4][37] - &a[4][38]);//p[4][39] --> *(*(p+4)+2),站在p的角度找到的就是p+4首元素地址+2。指针和指针相减得到的是他们之间的元素个数。低地址减高地址也就是-4。
//10000000000000000000000000000000 -4的源码
//11111111111111111111111111111011
//11111111111111111111111111111100 -4的补码
//%p打印地址,没有原反补概念,-4的补码就会打印成FFFFFFFC
return 0;
}
编译器报警
笔试题6:
int main()
{
int aa[2][42] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
//1 2 3 4 5
//6 7 8 9 10
//本质上是连续存放的
int *ptr1 = (int *)(&aa + 1);//取地址aa+1,跳过整个数组
int *ptr2 = (int *)(*(aa + 1));//*(aa+1) --> aa[1],相当于第二行的首元素地址
printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1));//10 5
return 0;
}
笔试题7:
int main()
{
char *a[] = {"work","at","alibaba"};
char**pa = a;//a现在是首元素地址,放到pa里去,pa指针变量存放的是char**类型的元素
pa++;//跳过一个char*的变量
printf("%s\n", *pa);//at
return 0;
}
笔试题8:
int 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,cp和cpp画出来
printf("%s\n", **++cpp);//++cpp首先移动,这时两次解引用,拿到的值其实就是POINT中P的地址。%s打印到\0其实就是打印了POINT
printf("%s\n", *--*++cpp+3);//前置++,先++,然后解引用,其实就是拿到了c+1。然后再--,就变成了c,再解引用就找到了ENTER。再+3也就是ENTER中第二个E的地址,打印出来就是ER。
printf("%s\n", *cpp[-2]+3);//*cpp[-2] --> *(*(cpp-2))。和上一步类似,但是没有移动回去,解引用得到F的地址,然后+3指向S。打印出来就是ST。
printf("%s\n", cpp[-1][-1]+1);//cpp[-1][-1] --> *(*(cpp-1)-1)。注意,首先cpp-1,解引用,指向的是POINT的首元素地址。然后再-1,解引用,指向的就是NEW的首元素地址。最后+1指向NEW的E。打印出来就是EW。
注意:这道题可能会有几个难点
1.++和--会自增和自减,移动位置。
2.*解引用和c[-1] = *(c-1)一定要搞清楚。
3.最后+3和+1,指的是加一个char*,因为前面都已经有2次*解引用了。