目录
指针的概念:
1. 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
2. 指针的大小是固定的4/8个字节(32位平台/64位平台)。
3. 指针有类型,指针的类型决定了指针的+-整数的步长,指针解引用操作的时候访问的字节数(权限的不同)。
4. 指针的运算。
void test(int arr[])
{
int sz = sizeof(arr) / sizeof(arr[0]); // 8 / 4 = 2
printf("%d\n", sz);
}
int main()
{
int arr[10] = { 0 };
test(arr);
return 0;
}
32位平台结果固定为1 64位平台结果固定位2
1. 字符指针
int main()
{
char ch = 'w';
char* pc = &ch;
return 0;
}
字符指针:存放字符变量的地址
int main()
{
char arr[] = "abcedf";
char* pc = arr;
return 0;
}
字符串存放到数组里,字符指针存放数组名,数组名是首元素地址。
int main()
{
char arr[] = "abcedf";
char* pc = arr;
printf("%s\n", arr);
printf("%s\n", pc);
return 0;
}
因为把数组名存放到字符指针里,而数组名又等价与首元素地址
所以这里的 arr 和 pc 等价
int main()
{
char* p = "abcdef";
return 0;
}
"abcdef"字符串,包括末尾的'\0'字符 一共是7个字符,把他存入4个字节大小(32位平台)的指针变量p中肯定放不下,但此代码没有报错。
实际上字符串"abcdef"是一个常量字符串
将字符串"abcdef"赋值给p,等价于把a的地址赋值给p
将字符串赋值给字符指针变量,这个指针变量里存的其实是这个字符串的首元素地址
验证方法如下:
打印p解引用操作的结果为a
int main()
{
char* p = "abcdef";
/*printf("%c\n", *p);*/
printf("%s\n", p);
return 0;
}
与上面代码相同,用%s的形式打印字符串的首个元素地址,会打印出整个字符串
内存布局:
常量字符串:
无法被修改
int main()
{
char* p = "abcdef";
*p = 'w';
printf("%s\n", p);
return 0;
}
此代码报错
因为常量字符串并不是通过程序向内存申请空间存放的 所以字符a的地址相当于野指针
报错:Segmentation fault 段错误 访问非法内存时的报错
segmentfault.com(类似Stackoverflow.com)
解决方法:将常量字符串的首元素地址赋值给一个const修饰的字符指针
int main()
{
const char* p = "abcdef";
*p = 'w';
printf("%s\n", p);
return 0;
}
const修饰的是*p,说明p所指向的内容不能被修改,代码就无法编译。
eg:面试题
int main()
{
char arr1[] = "abcdef";
char arr2[] = "abcdef";
char* p1 = "abcdef";
char* p2 = "abcdef";
if (arr1 == arr2)
{
printf("hehe\n");
}
else
printf("haha\n");
return 0;
}
arr1 与 arr2 不相等
原因:arr1和arr2是两个不同的数组,数组名就是不同数组的首元素地址
int main()
{
char arr1[] = "abcdef";
char arr2[] = "abcdef";
char* p1 = "abcdef";
char* p2 = "abcdef";
if (p1 == p2)
{
printf("hehe\n");
}
else
printf("haha\n");
p1 是 == p2的
常量字符串无法被修改,且p1和p2存的字符串一模一样,系统没有必要在内存中存两份,
p1和p2都指向同一块空间的起始位置,所以p1 == p2
但改变p1并不会影响p2的值,p1和p2是两个独立的变量,只是所指的是同一块空间
此代码最准确的写法:const修饰,因为p1和p2所指向的常量字符串无法改变
char arr1[] = "abcdef";
char arr2[] = "abcdef";
const char* p1 = "abcdef";
const char* p2 = "abcdef";
这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4不同
2. 指针数组
int main()
{
int arr[10] = { 0 }; //整型数组 - 存放整型
char ch[5] = { 0 }; //字符数组 - 存放字符
int* parr[4]; //存放整型指针的数组 - 指针数组
char* pch[5]; //存放字符指针的数组 - 指针数组
return 0;
}
指针数组本质是一个数组,用来存放指针
int main()
{
int a = 10;
int b = 20;
int c = 30;
int d = 40;
int* arr[4] = { &a,&b,&c,&d };
return 0;
}
数组里存放的就是对应变量的地址
int main()
{
int a = 10;
int b = 20;
int c = 30;
int d = 40;
int* arr[4] = { &a,&b,&c,&d };
int i = 0;
for (i = 0; i < 4; i++)
{
printf("%d ", *(arr[i]));
}
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* parr[] = { arr1,arr2,arr3 };//数组名等价于首元素地址
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* parr[] = { arr1,arr2,arr3 };//数组名等价于首元素地址
int i = 0;
for (i = 0; i < 3; i++)
{
//打印一行
int j = 0;
for (j = 0; j < 5; j++)
{
printf("%d ", *(parr[i] + j));
}
printf("\n");
}
return 0;
}
温习:
3. 数组指针
数组指针:本质上是指针
int main()
{
int* p = NULL;//p整型指针 - 指向整型的指针 - 可以存放整型的地址
char* pc = NULL;//pc是字符指针 - 指向字符的指针 - 可以存放字符的地址
//数组指针 - 指向数组的指针 - 存放数组的地址
return 0;
}
int* p[10] - //指针数组
int(*p)[10] - //数组指针
与指针数组对比,下行代码就是数组指针,注意(*p)中的*不是解引用操作,这里是定义变量
原因:[]的优先级高于*,上行代码p会先和[]结合,导致p先是一个数组,也就是指针数组
eg:定义一维数组的数组指针
int main()
{
char* arr[5];
char* (*pa)[5] = &arr;
return 0;
}
例如:
int arr2[10] = { 0 };
int(*pa)[10] = &arr2;
&数组名和数组名
数组名:首元素地址
&数组名:整个数组的地址
两个结果在数值上相同(打印出来的结果一样),但意义完全不一样。
int main()
{
int arr[10] = { 0 };
printf("arr = %p\n", arr);
printf("&arr = %p\n", &arr);
printf("arr+1 = %p\n", arr+1);
printf("&arr+1 = %p\n", &arr+1);
return 0;
}
数组名+1:跳过一个元素
&数组名+1:跳过一个数组
数组指针的用法:
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int (*pa)[10] = &arr;
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", (*pa)[i]); // *pa 等价于 数组名 (*pa) = arr 注意要括号
}
return 0;
}
也可以这样写:
for (i = 0; i < 10; i++)
{
printf("%d ", *(*pa + i));//*pa == arr
}
*相当于把&抵消了 *pa == arr
*pa就是数组名,而数组名又是首元素地址,首元素地址+i,相当于向后跳了i个元素
但也可以这样写:最简便
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
直接把数组名(首元素地址)赋值给p,然后(p+i)相当于往后跳了i个元素
但以上的数组指针用法都不常见(数组指针根本不是这样用的),数组指针通常用在二维数组
void Print1(int arr[3][5], int x, int y)
{
int i = 0;
int j = 0;
for (i = 0; i < x; i++)
{
for (j = 0; j < y; j++)
{
printf("%d ", arr[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}};
Print1(arr,3,5);
return 0;
}
二维数组的数组名也是数组名,数组名 == 首元素地址,但这里的首元素地址所指的并不是第一行第一列的元素1。
将二维数组arr[3][5]想象成一维数组,每一行为一个元素,arr一共三个元素,每一个元素是一个五个元素的数组
void Print2(int (*p)[5], int x, int y)
{
}
int main()
{
int arr[3][5] = {{1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7}};
//Print1(arr,3,5);//arr - 数组名 - 数组名就是首元素地址
Print2(arr,3,5);//arr -表示
return 0;
}
*(p + i) + j
p为首行的地址,+i相当于跳过i行,*(p+i) 相当于找到了第i行的地址,也就等价于拿到了第i行数组的数组名,*(p+i)+j 就等价于拿到了第i行j列的元素的地址, *(*(p+i)+j) 对其整体解引用就是i行j列的元素
void Print2(int (*p)[5], int x, int y)
{
int i = 0;
for (i = 0; i < x; i++)
{
int j = 0;
for (j = 0; j < y; j++)
{
printf("%d ", *(*(p + i) + j)); // 加i跳过i行 *(p+i)相当于找到了第i行 == 找到了 这行的数组名 *(p+i) == 当前行的数组名
}
printf("\n");
}
}
int main()
{
int arr[3][5] = {{1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7}};
//Print1(arr,3,5);//arr - 数组名 - 数组名就是首元素地址
Print2(arr,3,5);//arr -表示
return 0;
}
Print1与Print2打印的结果是一样的
void Print1(int arr[3][5], int x, int y)
{
int i = 0;
int j = 0;
for (i = 0; i < x; i++)
{
for (j = 0; j < y; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
void Print2(int (*p)[5], int x, int y)
{
int i = 0;
for (i = 0; i < x; i++)
{
int j = 0;
for (j = 0; j < y; j++)
{
printf("%d ", *(*(p + i) + j)); // 加i跳过i行 *(p+i)相当于找到了第i行 == 找到了 这行的数组名 *(p+i) == 当前行的数组名
}
printf("\n");
}
}
int main()
{
int arr[3][5] = {{1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7}};
Print1(arr,3,5);//arr - 数组名 - 数组名就是首元素地址
Print2(arr,3,5);//arr -表示
return 0;
}
void Print1(int arr[3][5], int x, int y)//参数是数组的形式
void Print2(int (*p)[5], int x, int y)//参数是指针的形式
*(p + i) + j
对于此行代码,*(p+i)相当于拿到第i行的地址,直接利用下标引用操作符,效果相同。
(*(p + i))[j]
(*(p+i))就相当于第i行数组名,注意这里要加括号。
对于下标引用操作符
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
int* p = arr;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i)); // (p+i)相当于第i个元素的地址
}
return 0;
}
因为把arr赋值给了p,所以arr与p就等价
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
int* p = arr;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i)); // (p+i)相当于第i个元素的地址
printf("%d ", *(arr + i));
}
return 0;
}
而数组引用又有另外一种写法
arr[i]
用于p和i等价,所以
p[i]
arr[i] == p[i]
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
int* p = arr;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i)); // (p+i)相当于第i个元素的地址
printf("%d ", *(arr + i));
printf("%d ", arr[i]);
printf("%d ", p[i]);
}
return 0;
}
四种写法全部等价
接下来就是重点:
由于*(p+i) == p[i] => *(*(p+i)+j) == *(p[i]+j) => p[i][j]
void Print2(int (*p)[5], int x, int y)
{
int i = 0;
for (i = 0; i < x; i++)
{
int j = 0;
for (j = 0; j < y; j++)
{
//printf("%d ", (*(p + i))[j]); // 加i跳过i行 *(p+i)相当于找到了第i行 == 找到了 这行的数组名 *(p+i) == 当前行的数组名
//printf("%d ", *(p[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}};
//Print1(arr,3,5);//arr - 数组名 - 数组名就是首元素地址
Print2(arr,3,5);//arr -表示
return 0;
}
总结:
一个数组将其数组名和元素个数去掉,剩下的就是数组元素的类型。
对于其他类型,直接去掉名字,剩下的就是变量的类型。
4. 数组传参和指针传参
一维数组
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 main()
{
int arr[10] = { 0 };
int* arr2[20] = { 0 };
test(arr);
test2(arr2);
}
对于最后一个void test2,参数部分用二级指针接收完全没有问题。
int* arr2[20]; arr2 - 数组名,等价于首元素的地址,而首元素是int* 类型的,也就是一级指针,
arr2就是一级指针的地址,等价于二级指针,所以参数用二级指针接收。
二维数组
void test(int arr[3][5])
{
}
void test1(int arr[][5])
{
}
int main()
{
int arr[3][5] = { 0 };
test(arr);//二维数组传参
return 0;
}
二维数组传参,参数可以写成数组的形式,且参数数组的行可以省略,但列不能省略
如果省略了行,代码就会报错
总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
这样才方便运算。
同样的:
void test3(int* arr) //err
{
}
int main()
{
int arr[3][5] = { 0 };
//test(arr);//二维数组传参
//test1(arr);
//test2(arr);
test3(arr);
return 0;
}
二维数组的数组名表示第一行的数组的地址,不能拿int* arr 用来存放整型的指针来存数组,应该拿指针数组。
同样的:
void test4(int** arr) //err
{
}
int main()
{
int arr[3][5] = { 0 };
//test(arr);//二维数组传参
//test1(arr);
//test2(arr);
//test3(arr);
test4(arr);
return 0;
}
数组的地址也不能放在二级指针
void test5(int(*arr)[5])
{
}
int main()
{
int arr[3][5] = { 0 };
//test(arr);//二维数组传参
//test1(arr);
//test2(arr);
/*test3(arr);*/
//test4(arr);
test5(arr);
return 0;
}
最正确的写法
总结:
一级指针传参
void print(int* p, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d\n", *(p + i)); // 也可以写成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函数能接收什么参数?
void test1(int* p)
{
}
void test2(char* p)
{
}
int main()
{
int a = 10;
int* p1 = &a;
test1(&a);//变量的地址
test1(p1);//一级指针
//字符指针同理
char ch = 'w';
char* p2 = &ch;
test2(&ch);
test2(p2);
return 0;
}
可以传变量的地址/一级指针
二级指针传参
同理:可以传一级指针的地址/二级指针
void test(int** ptr)
{
printf("num = %d\n", **ptr);
}
int main()
{
int n = 10;
int*p = &n;
int **pp = &p;
test(pp);
test(&p);
return 0;
}
不过这里还可能有其他情况:
int* arr[10]; //指针数组的数组名 也可以
test(arr);
还可以传指针数组的数组名
总结:
void test(char **p)
{
}
int main()
{
char c = 'b';
char*pc = &c;
char**ppc = &pc;
char* arr[10];
test(&pc);
test(ppc);
test(arr);//Ok?
return 0;
}
5. 函数指针
数组指针 - 指向数组的指针
函数指针 - 指向函数的指针
int Add(int x, int y)
{
return x + y;
}
int main()
{
int a = 10;
int b = 20;
/*printf("%d\n", Add(a, b));*/
printf("%p\n", &Add);
printf("%p\n", Add);
return 0;
}
在函数里,&函数名和函数名等价,都表示函数的地址
int* pa(int, int) = Add; //err pa是个函数,参数类型为int 返回值为int*
int(*pa)(int,int) = Add; //正确写法
括号(*pa)保证pa先和*结合,使得pa是个指针
函数指针用法:
int Add(int x, int y)
{
return x + y;
}
int main()
{
int(*pa)(int,int) = Add; //正确写法
printf("%d\n", (*pa)(2, 3)); //利用函数指针使用函数
return 0;
}
同理:
void Print(char* str)
{
printf("%s\n",str);
}
int main()
{
void (*p)(char*) = Print; // 函数指针
(*p)("hello bit");
return 0;
}
总结:
void test()
{
printf("hehe\n");
}
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();
void *pfun2();
pfun1先和*结合是一个函数指针
pfun2是一个函数,返回值是void
阅读两段有趣的代码:
//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);
首先先介绍一个方法:
void (*p)(char*) = Print; // 函数指针
void (*)() //函数指针类型
刚刚的函数指针,把变量名字和参数类型去掉,剩下的就是函数指针的类型
我们再来看代码1:
(* ( void (*)() )0 )();
(类型) 0,相当于把0进行强制类型转化为函数指针类型
相当于把0当作某个函数的地址,然后前面的*对其解引用操作,相当于找到这个函数
后面加上()相当于调用这个函数
再来看代码2:
void (*signal(int , void(*)(int)))(int);
代码2相当于在调用signal函数,此函数的第一个参数是int,第二个参数是void(*)(int),与上面相同
这是一个函数指针,函数参数是int,返回值是void
先看int Add(int , int); 把函数名和参数去掉 剩下的就是返回值的类型
同理把void (*signal(int , void(*)(int)))(int); 函数名signal 和参数(int,void(*)(int))去掉
剩下void(*)(int)就是函数返回值,也是函数指针类型
而void (*signal(int , void(*)(int)))(int); 末尾是分号 所以这是一次函数的声明。
返回值为函数指针,void(*)(int)后面的(int)要放在后面
typedef 可以重命名类型,使得此代码更加简洁
typedef unsigned int uint;//重命名无符号整型
typedef void(*)(int) pfun_t; // err
typedef void(*pfun_t)(int);//重命名函数指针类型有所不同
void (*signal(int, void(*)(int)))(int);
typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t); //这两行代码与上面第一行代码等价
代码看起来更加简单易懂
总结:
int Add(int x, int y)
{
return x + y;
}
int main()
{
int a = 10;
int b = 20;
int(*pa)(int, int) = Add;
printf("%d\n", (*pa)(2, 3));
printf("%d\n", (**pa)(2, 3));
printf("%d\n", (***pa)(2, 3));
return 0;
}
结果完全一样,说明*没有用,没有产生效果
那能否不写*呢?
int Add(int x, int y)
{
return x + y;
}
int main()
{
int a = 10;
int b = 20;
int(*pa)(int, int) = Add;
printf("%d\n", (pa)(2, 3));
printf("%d\n", (*pa)(2, 3));
printf("%d\n", (**pa)(2, 3));
printf("%d\n", (***pa)(2, 3));
return 0;
}
结果仍然一样
不写*也非常好理解
printf("%d\n", (pa)(2, 3));
printf("%d\n", Add(2, 3)); //Add == pa
相当于直接用调用Add函数
这就很好的解释了上文的一段代码
printf("%d\n", *p(2, 3));
p(2,3)相当于调用Add函数,结果为5,所以整段代码等价于:
printf("%d\n", *5);
最后再说一个注意点:
使用函数指针的时候,可以不解引用*,*相当于摆设,不起效果 任意加多少个都可以
6. 函数指针数组
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 main()
{
//指针数组
int* arr[5];
int (*pa)(int,int) = Add; //Sub/Mul/Div
//需要一个数组 - 可以存放4个函数的地址 - 函数指针数组
int (*parr[4])(int, int) = {Add,Sub,Mul,Div}; //函数指针数组 放同一个数组的函数指针数组 返回值和参数一定完全一样?
int i = 0;
for (i = 0; i < 4; i++)
{
printf("%d\n",parr[i](2, 3));
}
return 0;
}
int (*parr1[10])(); // 函数指针数组
int *parr2[10](); //err
int (*)() parr3[10]; // err
char* my_strcpy(char* dest,const char* src);
小练习:
1.写一个函数指针pf 能够指向my_strcpy
2.写一个函数指针数组,能够存放4个my_strcpy函数的地址
char* (*pf)(char*, const char*) = my_strcpy;
char* (*pfarr[4])(char*, const char*) = {NULL};
函数指针数组的用途:转移表
模拟计算器
void menu()
{
printf("**************************\n");
printf("*** 1.Add 2.Sub ***\n");
printf("*** 3.Mul 4.Div ***\n");
printf("***** 0.exit ********\n");
printf("**************************\n");
}
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 main()
{
int input = 0;
int x = 0;
int y = 0;
do
{
menu();
printf("请选择:>");
scanf_s("%d", &input);
switch (input)
{
case 1:
printf("请输入两个操作数:>");
scanf_s("%d%d", &x, &y);
printf("%d\n",Add(x, y));
break;
case 2:
printf("请输入两个操作数:>");
scanf_s("%d%d", &x, &y);
printf("%d\n", Sub(x, y));
break;
case 3:
printf("请输入两个操作数:>");
scanf_s("%d%d", &x, &y);
printf("%d\n", Mul(x, y));
break;
case 4:
printf("请输入两个操作数:>");
scanf_s("%d%d", &x, &y);
printf("%d\n", Div(x, y));
break;
case 0:
printf("退出\n");
break;
default:
printf("输入错误\n");
break;
}
} while (input);
return 0;
}
如果还想加入^ 或者 | 等运算 switch语句会越来越长,代码冗余度高(可以利用回调函数解决)
解决方法:使用函数指针数组
void menu()
{
printf("**************************\n");
printf("*** 1.Add 2.Sub ***\n");
printf("*** 3.Mul 4.Div ***\n");
printf("***** 0.exit ********\n");
printf("**************************\n");
}
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 main()
{
int input = 0;
int x = 0;
int y = 0;
int (*pfarr[])(int, int) = { 0,Add,Sub,Mul,Div }; //函数指针数组
//对应下标
do
{
menu();
printf("请选择:>");
scanf_s("%d", &input);
if (input >= 1 && input <= 4)
{
printf("请输入两个操作数");
scanf_s("%d%d", &x, &y);
int ret = pfarr[input](x, y);
printf("%d\n", ret);
}
else if (input == 0)
{
printf("退出");
}
else
{
printf("选择错误");
}
} while (input);
return 0;
}
往后在加运算的时候,比较简便,代码冗余度低
7. 指向函数指针数组的指针
//指向函数指针数组的指针
int Add(int x, int y)
{
return x + y;
}
int main()
{
int arr[10] = { 0 };
int (*p)[10] = &arr;//取出数组的地址
int (*pfArr[])(int, int) = { 0 };//函数指针数组
int (*(*ppfArr)[])(int, int) = &pfArr;
//把名字ppfArr去掉就是 指针所指向的元素
return 0;
}
同理:指针把变量名去掉就是对应的所指的元素类型
数组把数组名和数组元素个数去掉就是数组的类型
int (*(*ppfArr)[])(int, int) = &pfArr;
int (*(*)[])(int, int) = &pfArr;
最内层括号的*代表ppfArr是一个数组指针,它所指向的数组的元素的类型是:函数指针
int (*)(int, int)
8. 回调函数
回调函数概念:
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
void menu()
{
printf("**************************\n");
printf("*** 1.Add 2.Sub ***\n");
printf("*** 3.Mul 4.Div ***\n");
printf("***** 0.exit ********\n");
printf("**************************\n");
}
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;
}
void Calc(int(*pf)(int,int) )
{
int x = 0;
int y = 0;
printf("请输入两个操作数");
scanf_s("%d%d", &x, &y);
printf("%d\n",pf(x,y));
}
int main()
{
int input = 0;
do
{
menu();
printf("请选择:>");
scanf_s("%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);
return 0;
}
利用回调函数解决代码冗余度高的问题
pf通过它所接收到的地址,调用对应的所指向的函数,实现不同的功能
把一个函数的地址传给一个指针,在函数内部,通过这个指针去调用对应的函数,被调用的函数就称为回调函数
void print(char* str)
{
printf("hehe:%s", str);
}
void test(void(*p)(char*))
{
printf("test\n");
p("bit");
}
int main()
{
test(print);
return 0;
}
把print函数的地址传给test,在test函数中,某种情况下调用了函数print,此时print就是回调函数
这种机制称为回调函数机制
接下来介绍一下qsort()函数 - <stdlib.h>
介绍前写讨论一下冒泡排序和结构体数组:
冒泡排序:基于冒泡排序的算法研究(c语言)
结构体数组:
struct Stu
{
char name[20];
int age;
};
int main()
{
struct Stu s[3] = { {"张三",20},{"李四",30}, {"王五",10}};//结构体数组
return 0;
}
冒泡排序局限:只能排序整型数组,无法排序浮点型/结构体数组,无法通用。
qsort函数:
原理:快排(quick sort)
base要排序的数组的起始地址
num是排序数组元素个数
width是每个元素所占的内存大小(单位字节)
cmp是比较函数的地址(对于不同类型的数组,比较方法不同),且参数和返回类型必须一致
警告:int* 到 char*的不兼容
void* pa = &a;
void*类型的指针可以接收任意类型的地址,不会警告 。 void* - 万能桶
所以参数base的类型设置为void*
void*类型的指针是不能进行解引用操作的
void* p = &a;
*p = 0;//报错非法的间接寻址
void*类型的指针不能进行+/-整数的操作
因为不知具体类型,不知道指针访问步长。
int cmp_int(const void* e1,const void* e2)
{
//比较两个整型值的函数
//e1 和 e2 是接收所要比较的两个元素的地址
*(int*)e1;
*(int*)e2;
}
e1和e2为void*类型的指针无法解引用,强制类型转化为要比较的类型进行比较
compare函数的特点,e1 < e2 返回 <0的数,e1 == e2 返回 0 ,e1 > e2返回 >0的数
int cmp_int(const void* e1,const void* e2)
{
//比较两个整型值的函数
//e1 和 e2 是接收所要比较的两个元素的地址
return *(int*)e1 - *(int*)e2;
}
qsort函数具体实现
#include<stdlib.h>
int cmp_int(const void* e1,const void* e2)
{
//比较两个整型值的函数
//e1 和 e2 是接收所要比较的两个元素的地址
return *(int*)e1 - *(int*)e2;
}
int main()
{
int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), cmp_int);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
接下来排序一下浮点数数组
int cmp_float(const void* e1,const void* e2) //注意返回值和参数必须一致 返回值必须int
{
return *(float*)e1 - *(float*)e2;
}
int main()
{
float arr[5] = { 9.0,8.0,7.0,6.0,5.0 };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), cmp_float);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%f ", arr[i]);
}
return 0;
}
注意float arr[] 和%f
注意这里代码会有警告
int cmp_float(const void* e1,const void* e2) //注意返回值和参数必须一致 返回值必须int
{
return *(float*)e1 - *(float*)e2;
}
原因是:return 后面为浮点数相减,结果仍然是浮点数,但返回类型固定是int
不过也无所谓,因为compare函数返回值只要 > 0 ,=0,<0就能得出结果,范围很大,结果通常不会有影响
不过也可以解决:用if语句
或者把相减完的结果再强制类型转化为int
接下来比较结构体数组:年龄排序
struct Stu {
char name[20];
int age;
};
int cmp_stu_by_age(const void* e1,const void* e2)
{
//age为整型 整型比较可以直接用> < =比较
return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
int main()
{
struct Stu s[3] = { {"张三",20},{"李四",30},{"王五",10}};
int sz = sizeof(s) / sizeof(s[0]);
qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
return 0;
}
排序前后对比
名字(字符串)排序
struct Stu {
char name[20];
int age;
};
int cmp_stu_by_name(const void* e1, const void* e2)
{
//比较名字 就是比较字符串
//字符串比较不能直接用 > < =来比较 应该用strcmp函数
return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
//第一个 > 第二个 返回>0的数字 第一个 == 第二个 返回0 第一个 < 第二个 返回 < 0的数字
//strcmp的返回值可以直接用来返回
}
int main()
{
struct Stu s[3] = { {"张三",20},{"李四",30},{"王五",10}};
int sz = sizeof(s) / sizeof(s[0]);
qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
return 0;
}
排序前后对比
这里有一个注意点:
sizeof计算结构体类型的字节大小 >= (name + age)的和的字节大小
总结一下qsort()函数的参数:
1.第一个参数:待排序数组的首元素地址
2.第二个参数:待排序数组的元素个数
3.第三个参数:待排序数组的每个元素的大小,单位是字节
4.第四个参数:函数指针,比较两个元素所用函数的地址 - (这个函数是使用者自己实现的)
这个函数指针的两个参数:待比较的两个元素的地址
接下来自己实现bubble_sort函数,使得此函数能够通用
void bubble_sort(void* base,int sz,int width,int(*cmp)(void* e1 ,void* e2))
{
int i = 0;
for (i = 0; i < sz - 1; i++)
{
int j = 0;
int flag = 1;
for (j = 0; j < sz - 1 - i; j++)
{
//两个元素的比较
if (cmp()>0)
{
//交换
flag = 0;
}
}
if (flag == 1)
{
break;
}
}
}
内层cmp()访问数组元素有三种情况:
但只有第三种是正确的
1.void*类型的base无法进行+/-运算
2.如果是结构体类型的指针+1跳过的字节数 >= (name+age)
3.正确的写法:先把base强制转化为char*类型,然后在+width,等价于向后跳过width个字节的大小
具体代码实现:
struct Stu
{
char name[20];
int age;
};
void Swap(char* buf1,char* buf2,int width)
{
int i = 0;
for (i = 0; i < width; i++)
{
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++;
buf2++;
}
}
//实现bubble_sort函数的程序员,他是否知道未来排序的数据类型 - 不知道
//也不知道待比较的两个元素类型
void bubble_sort(void* base,int sz,int width,int(*cmp)(void* e1 ,void* e2))
{
int i = 0;
for (i = 0; i < sz - 1; i++)
{
int j = 0;
int flag = 1;
for (j = 0; j < sz - 1 - i; j++)
{
//两个元素的比较
if (cmp((char*)base+j*width ,(char*)base+(j+1)*width) > 0)
{
//交换
Swap((char*)base + j * width, (char*)base + (j + 1) * width,width);//传width为了知道两个字符具体是多少字节
}
}
}
}
int cmp_int(const void* e1, const void* e2)
{
//比较两个整型值的函数
//e1 和 e2 是接收所要比较的两个元素的地址
return *(int*)e1 - *(int*)e2;
}
int cmp_stu_by_age(const void* e1, const void* e2)
{
return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age; //age为整型 整型比较可以直接用> < =比较
}
int cmp_stu_by_name(const void* e1, const void* e2)
{
//比较名字 就是比较字符串
//字符串比较不能直接用 > < =来比较 应该用strcmp函数
return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
//第一个 > 第二个 返回>0的数字 第一个 == 第二个 返回0 第一个 < 第二个 返回 < 0的数字
//strcmp的返回值可以直接用来返回
}
int main()
{
int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
//使用bubble_sort的程序员一定知道自己排序的是什么数据
//就应该知道如何比较待排序数组中的元素
struct Stu s[3] = { {"zhangsan",20},{"lisi",30},{"wangwu",10}};
int sz2 = sizeof(s) / sizeof(s[0]);
bubble_sort(arr, sz, sizeof(arr[0]),cmp_int);
//bubble_sort(s, sz2, sizeof(s[0]), cmp_stu_by_age);
bubble_sort(s,sz2,sizeof(s[0]), cmp_stu_by_name);
return 0;
}
难点其实是bubble_sort函数主体的定义和swap函数的定义
void Swap(char* buf1,char* buf2,int width)
{
int i = 0;
for (i = 0; i < width; i++)
{
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++;
buf2++;
}
}
//实现bubble_sort函数的程序员,他是否知道未来排序的数据类型 - 不知道
//也不知道待比较的两个元素类型
void bubble_sort(void* base,int sz,int width,int(*cmp)(void* e1 ,void* e2))
{
int i = 0;
for (i = 0; i < sz - 1; i++)
{
int j = 0;
int flag = 1;
for (j = 0; j < sz - 1 - i; j++)
{
//两个元素的比较
if (cmp((char*)base+j*width ,(char*)base+(j+1)*width) > 0)
{
//交换
Swap((char*)base + j * width, (char*)base + (j + 1) * width,width);//传width为了知道两个字符具体要交换几个字节
}
}
}
}
总结:
1.比较函数cmp是需要独立出来的,每一种类型的比较方式不同
2.swap的原理:把每一对字节的内容交换
9. 指针和数组面试题的解析
数组笔试题
//一维数组
int main()
{
//数组名和&数组名的区别
//数组名为首元素地址 两个例外 sizeof &arr
//1.sizeof(数组名) - 数组名表示整个数组 数组名必须要单独放在 ()里面
//2.&数组名 - 数组名表示整个数组
int a[] = { 1,2,3,4 };
printf("%d\n", sizeof(a)); //16 sizeof(数组名) - 计算的是数组的总大小 - 单位是字节
printf("%d\n", sizeof(a + 0));//指针4/8 数组名表示首元素地址,a+0还是首元素地址 地址的大小就是4/8字节
printf("%d\n", sizeof(*a));// 4 - 数组名表示首元素地址
printf("%d\n", sizeof(a + 1));// 4/8 数组名表示首元素地址 a+1 表示第二个元素的地址
printf("%d\n", sizeof(a[1]));//4
printf("%d\n", sizeof(&a));//4/8 &a 取出的是数组的地址 数组的地址也是地址 数组指针
printf("%d\n", sizeof(*&a));// &a 数组名表示整个数组 *解引用访问整个数组 *和&抵消了 相当于放个a 16
printf("%d\n", sizeof(&a + 1));//4/8 &a表示数组的地址 + 1跳过一个数组 还是一个地址 4/8
printf("%d\n", sizeof(&a[0]));//首个元素的地址 指针的大小 4/8
printf("%d\n", sizeof(&a[0] + 1));//第二个元素的地址 4/8
return 0;
}
int main()
{
//字符数组
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", sizeof(arr)); // sizeof(数组名) 数组名表示整个数组 6
printf("%d\n", sizeof(arr + 0));// 4/8 arr是首元素地址 ,arr+0还是首元素地址
printf("%d\n", sizeof(*arr));//1 arr是首元素地址 *arr是首元素 首元素的大小
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("\n");
/*strlen 求的是字符串长度 遇到\0才会结束计算 */
printf("%d\n", strlen(arr));//随机值 首元素地址
printf("%d\n", strlen(arr + 0));//随机值 首元素地址
printf("%d\n", strlen(*arr));// 首元素 a 传过去 a - 97 等价于把97传过去 strlen会把它当成地址 从97这里 作为起始地址向后数字符,属于非法访问内存 //err
printf("%d\n", strlen(arr[1]));//第二个元素 98 - b 同样 err
printf("%d\n", strlen(&arr));// 从数组开头开始数字符 随机值
printf("%d\n", strlen(&arr + 1));// 跳过当前数组 开始数字符 随机值 并且与上行代码差值为6
printf("%d\n", strlen(&arr[0] + 1));// 第二个元素开始数字符
return 0;
}
int main()
{
char arr[] = "abcdef"; // a b c d e f \0 注意\0字符 \0字符是算作在arr数组里的
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("\n");
printf("%d\n", strlen(arr)); //6 首元素地址
printf("%d\n", strlen(arr + 0));//6 首元素地址
//printf("%d\n", strlen(*arr));// 第一个元素 err 97 非法访问 err
//printf("%d\n", strlen(arr[1]));//第二个元素 err 98 err
printf("%d\n", strlen(&arr));//整个数组的地址 6 &arr 取的是数组的地址 数组指针 char (*p)[7] = &arr
//const char* 与 char(*)[7] 不兼容 会有警告
printf("%d\n", strlen(&arr + 1));// 随机值
printf("%d\n", strlen(&arr[0] + 1));//第二个元素的地址 5
return 0;
}
int main()
{
char* p = "abcdef"; //常量字符串 a的地址放在p里 a b c d e f /n
printf("%d\n", sizeof(p)); //4/8 指针 a的地址
printf("%d\n", sizeof(p + 1)); // 4/8 p+1得到的是b的地址
printf("%d\n", sizeof(*p));// sizeof(a) 1
printf("%d\n", sizeof(p[0])); // p[0] == *(p+0) == 'a' 1
// int arr[10] arr[0] == *(arr+0)
printf("%d\n", sizeof(&p)); //二级指针 4/8
printf("%d\n", sizeof(&p + 1));// 二级指针+1 4/8
printf("%d\n", sizeof(&p[0] + 1)); // &*(p+0) + 1 *和&抵消 相当于 p+1 是个指针 b的地址
printf("%d\n", strlen(p));// a的地址 6
printf("%d\n", strlen(p + 1));// b的地址 5
printf("%d\n", strlen(*p));// err strlen(a) a - 97
printf("%d\n", strlen(p[0]));// *(p+0) err
printf("%d\n", strlen(&p));//二级指针 随机值
printf("%d\n", strlen(&p + 1));//二级指针 随机值
printf("%d\n", strlen(&p[0] + 1));// 相当于b的地址 5
return 0;
}
二维数组:
int main()
{
//二维数组
int a[3][4] = { 0 }; //三行四列的二维数组
printf("%d\n", sizeof(a)); // sizeof(数组名) 数组名表示整个数组 48
printf("%d\n", sizeof(a[0][0]));//第一行第一列的元素 4
printf("%d\n", sizeof(a[0]));//16 第一行的数组名 数组名单独放在sizeof里面 计算的是整个数组的大小
printf("%d\n", sizeof(a[0] + 1));// a[0]没有单独放在()里 所以这里表示首元素地址 也就是第一行第一个元素的地址 再+1 就是 第一行第二个元素的地址
//a[0] 是第一行的数组名 ,数组名此时是首元素的地址 其实就是第一行第一个元素地址
//所以a[0] + 1 就是第一行第二个元素的地址 地址大小为4/8个字节
printf("%d\n", sizeof(*(a[0] + 1)));//第一行第二个元素 等价于上行代码解引用
printf("%d\n", sizeof(a + 1));// 4 数组名是首元素地址 二维数组的首元素地址是第一行的地址 +1 就是第二行的地址
// a是二维数组的数组名 没有sizeof(数组名) 也没有&数组名 , 所以a是首元素地址
//而二维数组的首元素地址就是第一行的地址 所以a就是第一行的地址 +1 就是第二行的地址
printf("%d\n", sizeof(*(a + 1)));//16 第二行数组解引用 相当于把第二行数组的数组名 放在sizeof()里 计算整个数组的大小
//等价于 a[1] 第二行的数组名
printf("%d\n", sizeof(&a[0] + 1));// 第二行的地址 4/8
printf("%d\n", sizeof(*(&a[0] + 1)));// 16 上行代码解引用 相当于把第二行的数组名直接放在sizeof()里 计算整个数组的大小
printf("%d\n", sizeof(*a));//16 首元素地址 首元素地址表示第一行的地址 解引用相当于 第一行的数组名 数组名直接放在sizeof() 里 计算的是整个第一行的大小
printf("%d\n", sizeof(a[3]));// 第四行元素的地址 4/8 等价于 *(a+3)
//sizeof 并不会真正访问 只会计算
return 0;
}
总结:
数组名的意义:
1. sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
2. &数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
3. 除此之外所有的数组名都表示首元素的地址。
指针笔试题
目录
topic1:
int main()
{
int a[5] = { 1, 2, 3, 4, 5 };
int *ptr = (int *)(&a + 1);
printf( "%d,%d", *(a + 1), *(ptr - 1));
return 0;
}
//程序的结果是什么?
topic2:
//由于还没学习结构体,这里告知结构体的大小是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);
printf("%p\n", (unsigned long)p + 0x1);
printf("%p\n", (unsigned int*)p + 0x1);
return 0;
}
topic3:
int main()
{
int a[4] = { 1, 2, 3, 4 };
int *ptr1 = (int *)(&a + 1);
int *ptr2 = (int *)((int)a + 1);
printf( "%x,%x", ptr1[-1], *ptr2);
return 0;
}
地址存储模式:
小端储存,拿的时候也倒着拿
结果就是:
最小的内存单位是一个字节,每个字节都有一个地址,两个相邻的地址之间相差一个字节
(int)地址+1 相当于向后跳过1个字节
topic4:
int main()
{
int a[3][2] = { (0, 1), (2, 3), (4, 5) };
int* p;
p = a[0];
printf("%d", p[0]);
return 0;
}
逗号表达式
p[0] == *(p+0) 数组名表示首元素地址
topic5:
int main()
{
int a[5][5];
int(*p)[4];
p = a;
printf( "%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
return 0;
}
地址相当于是一个无符号数
topic6:
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;
}
topic7:
int main()
{
char* a[] = { "work","at","alibaba" };
char** pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}
char* *pa =a; pa++相当于跳过一个char*的内容
topic8:
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;
}
//topic8:
int main()
{
char* c[] = { "ENTER","NEW","POINT","FIRST" };
char** cp[] = { c + 3,c + 2,c + 1,c };
char*** cpp = cp;//cp是一个数组指针
printf("%s\n", **++cpp); //POINT
printf("%s\n", *--*++cpp + 3); //ER //注意点:++/--导致的地址指向的位置发生变化 和 地址本身++/--的变化
printf("%s\n", *cpp[-2] + 3); // ST
// *(*(cpp-2))+3
printf("%s\n", cpp[-1][-1] + 1); //EW
// *(*(cpp-1)-1)+1
return 0;
}
//前两个有++cpp 导致指针真实位置发生改变 后两个没有 只是单纯移动指针