目录
1指针的大小
2指针类型
2.1字符指针
2.1.1指向字符串常量
2.1.2指向字符数组
2.1.3访问字符串
2.1.4使用指针遍历字符串
2.2指针数组
2.3数组指针
2.4函数指针
2.4.1来看两段代码
2.4.1回调函数
2.5函数指针数组
3数组参数,指针参数
3.1&数组名VS数组名
3.2一维数组传参
3.3二维数组传参
3.4一级指针传参
3.5二级指针传参
4指针笔试题
4.1对于数组名和字符串指针的理解
4.2程序的结果是什么?
4.3如下表表达式的值分别为多少?
4.4程序的结果是什么?
4.5小心有坑!
4.6打印结果为?
4.7Work at alibaba
1指针的大小
指针是一种特殊的变量,用于存储内存地址。可以直接访问内存中的数据,而无需知道变量的名称。指针变量包含一个内存地址,该地址指向存储在计算机内存中的数据位置。
指针的大小即地址的大小,地址是物理的电线上产生的,对于32位机器,有32根地址线,即有32个1和0组成的二进制序列,这个二进制序列就叫做地址,需要32个比特位才能存储这个地址。许多32位操作系统上,指针通常是4个字节(32位),而在64位操作系统上,指针通常是8个字节(64位)。
虽然指针的大小都是4/8字节,但指针仍有许多类型,不同类型对应的指针在存放、解引用、指针运算上有差异。
2指针类型
2.1字符指针
字符指针是指向字符数据的指针,在处理字符串和字符数组中有重要作用。
字符指针的用途包括但不仅限于:
2.1.1指向字符串常量
char *strPtr = “Hello, world!”;
要注意,这里不是把一个字符串放到strPtr指针变量里了,本质是把字符串”Hello World”的首字符的地址放到了strPtr中。
有一道面试题考察了这个知识点:
#include int main(){char str1[] = "hello world.";char str2[] = "hello world.";char *str3 = "hello world.";char *str4 = "hello world.";if(str1 == str2)printf("str1 and str2 are same\n");elseprintf("str1 and str2 are not same\n"); if(str3 == str4)printf("str3 and str4 are same\n");elseprintf("str3 and str4 are not same\n"); return 0;}
运行结果:
str1 and str2 are not same
str3 and str4 are same
数组名是首地址,str1和str2的首地址不一样,所以str1和str2显然不相等;*str3和*str4都指向了同一个字符串,两个字符指针存放的地址是相同的,所以是str3和str4是相同的。
2.1.2指向字符数组
char str[] = “Hello”;
char *strPtr = str;
2.1.3访问字符串
printf(“%s”, strPtr);
2.1.4使用指针遍历字符串
while (*strPtr != ‘\0’) {
printf(“%c”, *strPtr);
strPtr++;
}
字符指针在中经常用于字符串的处理和操作,例如字符串的拷贝、比较、连接等。这些功能包含在string.h中,详见:cplusplus.com/reference/cstring/?kw=string.h
2.2指针数组
指针数组,即存放指针的数组。数组的每一个元素都是一个指针类型的变量。示例如下:
int *p[10];
这里要注意:[]的优先级要高于*的,所以p先与[]结合,说明他的一个存放10个变量的数组,而数组的每一个变量类型是 int*。
数组指针的使用:
指针数组的使用场景很多,其中一个典型的用法是处理字符串数组。比如:
#include int main() {char *strings[] = {"apple", "banana", "orange"};for (int i = 0; i < 3; i++) {printf("%s\n", strings[i]);}return 0;}
还可以通过指针数组来模拟实现二维数组:
#include int main(){int arr1[] = { 1,2,3,4,5 };int arr2[] = { 2,3,4,5,6 };int arr3[] = { 3,4,5,6,7 };int* arr[3] = { arr1, arr2, arr3 };for (int i = 0; i < 3; i++){for (int j = 0; j < 5; j++){printf("%d ", arr[i][j]);}printf("\n");}return 0;}
2.3数组指针
数组指针,即指向数组的指针。与指针数组不同,数组指针指向的是一个完整的数组,而不是数组中的单个元素。示例如下:
int (*p)[10];
由于()优先级最高,所以 p 先与*结合,说明他是一个指针,指向的是一个大小为10,元素类型为int的数组。
数组指针的一个重要运用就是传递多维数组给函数
#includevoid printArray(int (*arr)[3], int rows) {for (int i = 0; i < rows; i++) {for (int j = 0; j < 3; j++) {printf("%d ", arr[i][j]);//也可以写为:printf("%d ", *(*(arr + i) + j));}printf("\n");}}int main() {int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};printArray(arr, 2);return 0;}
二维数组的数组名表示二维数组第一行的地址,是一个一维数组的地址,类型为数组指针。所以这里 printArray 函数接收的是二维数组第一行的地址,通过arr[i][j]的方式来访问arr数组的第i行第j个元素。
2.4函数指针
函数指针可以用来存储函数的地址,使得可以动态地选择调用不同的函数,从而实现一些高级的编程技巧和模式。
若定义有:int add(int x, int y)
则int (*p)(int int) –> pf就是函数指针变量
2.4.1来看两段代码
(*(void (*)())0)()
从里层括号开始看:
- void(*)()是函数指针类型
- 括号里面放类型 就相当于强制类型转换,相当于将0(地址处)强制类型转化位一个这样类型的函数
- 然后再解引用,调用这个该函数,因为函数没有参数,所以只给了一个括号
综上:这句代码是在先将0强制类型转换为void(*)()的函数指针,再调用0地址处的函数。
void (*signal(int , void(*)(int)))(int);
- signal(int , void(*)(int))是在申明函数
- signal的形参类型为int和一个函数指针,该函数指针的类型为void(*)(int)
- void(*)(int)是signal函数的返回类型, 也是一种函数指针类型
可以通过typedef重定义函数指针来简化这段代码:
//简化:typedef void(* pf )(int);pf signal(int, pf);
函数指针有一个非常大的用途,就是实现回调函数
2.4.1回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
通过使用回调函数,可以实现程序的灵活性和可扩展性,使得程序能够根据不同的情况执行不同的逻辑。
一个典型的回调函数应用就是qsort函数
(图片来源:qsort – C++ Reference (cplusplus.com))
qsort函数提供了一种方便且高效的方法来对数组进行排序,用户可以根据自己的需求提供不同的比较函数来实现不同的排序逻辑(升序或降序,任意类型的数据排序)。
qsort的最后一个参数就是一个函数指针,在sort函数中调用时qsort时,我们传入写好的compare函数的地址,然后在qsort中会通过调用compare函数来实现排序,这就是回调函数。
#include#includestruct data{char n[10];char num;};void print_int(int arr[], int size){for (int i = 0; i < size; i++){printf("%d ", arr[i]);}}void print_struct(struct data arr[], int size){for (int i = 0; i num - ((struct data*)p1)->num; //降序}sort1(){int arr[10] = { 7,4,6,2,6,1,3,8,9,0 };int size = sizeof(arr) / sizeof(arr[0]);qsort(arr, size, sizeof(arr[0]), compare_int);print_int(arr, size);}sort2(){struct data arr[] = { {"ChinaLYB", 14}, {"Buider", 18}, {"haisai233", 12} };int size = sizeof(arr) / sizeof(arr[0]);qsort(arr, size, sizeof(arr[0]), compare_struct);print_struct(arr, size);}int main(){sort1();printf("\n\n");sort2();return 0;}
2.5函数指针数组
函数指针数组是指一个数组,其中的每个元素都是函数指针。换句话说,函数指针数组存储了一组函数指针,使得可以通过数组索引来访问和调用不同的函数。
以下是一个使用函数指针数组实现的简单的计算器程序
#includevoid lobby(){printf("**********Caculator***********\n");printf("*******1.add2.sub*******\n");printf("*******3.div4.mul*******\n");printf("************0.exit************\n");}int add(int x, int y){return x + y;}int sub(int x, int y){return x - y;}int div(int x, int y){return x / y;}int mul(int x, int y){return x * y;}int main(){int input = 0;int x = 0;int y = 0;int result = 0;do{lobby();printf("Please choose :>");scanf("%d", &input);//函数指针数组int(*pfArr[5])(int, int) = { NULL, add, sub, div, mul };if (input >= 1 && input ");scanf("%d %d", &x, &y);result = pfArr[input](x, y);printf("result = %d\n", result);}else if (input == 0){printf("Exiting... ...\n");}else{printf("Invalid input, please choose again:>\n");}} while (input);}
3数组参数,指针参数
3.1&数组名VS数组名
对于下面的数组:
int arr[10];
arr 和 &arr 分别是啥?
首先对于数组名(arr)的理解:
- 数组名是数组首元素的地址
- 另外有两个例外:
- sizeof(数组名) :数组名表示整个数组, sizeof(数组名)是计算整个数组的大小,单位是字节
- &数组名 :数组名表示整个数组,&数组名取出的是整个数组的地址
- 除此之外,数组名都表示数组首元素的地址
对于&数组名(&arr)的理解:
&arr表示的是数组的地址,而不是数组首元素的地址。
可以通过下面这个例子深刻体会:
#include 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;}
运行结果:
arr = 00B1F900
&arr= 00B1F900
arr+1 = 00B1F904
&arr+1= 00B1F928
可见,虽然arr与&arr存放的地址相同,但指针的类型不同,性质也就不同。对于arr+1,指针类型为int*,所以指针向后移动4个字节(一个int的大小,即arr数组一个元素的大小),指向指针第二个元素;而对于&arr+1,指针类型为int(*)[],所以指针向后移动40个字节(十个int的大小,一个数组的大小)
3.2一维数组传参
在传递一维数组给函数时,并不会复制整个数组,而是传递数组的地址(指针)。这意味着在函数内部对数组进行的任何修改都会影响到原始的数组。
下面来看一些一维数组传参的示例判断是否可行:
#include
int main()
{
int arr[10] = {0};
int *arr2[20] = {0};
test(arr);
test2(arr2);
}void test(int arr[]) //ok?
{}
//ok -> 为什么可以省略大小?因为函数正如上述所说,数组传参传递的是数组的地址,而地址得到的大小时确定的,所以大小可以省略
void test(int arr[10]) //ok?
{}
//ok -> 也可以,同样由于地址得到的大小时确定的,编译器会自动忽略这个大小
void test(int *arr) //ok?
{}
//ok
void test2(int *arr[20]) //ok?
{}
//ok
void test2(int **arr) //ok?
{}
//ok -> arr2可以理解为指针数组的首元素地址,是地址的地址,故用一个二级指针来接收是合理的
3.3二维数组传参
传递二维数组作为参数与传递一维数组类似,但有一些细微的区别。
二维数组实际上是以一维数组的形式存储的连续内存块。因此,在将二维数组传递给函数时,需要明确指定数组的列数。
二维数组的数组名代表的是第一行(相当于一个一维数组)的地址,是一个数组的地址,类型为数组指针(*)[],这意味着如果直接使用一个二维数组的数组名作为参数传递时,需要用一个数组指针类型的形参来接收。
下面来看一些二维数组传参的示例判断是否可行:
#include
int main()
{
int arr[3][5] = {0};
test(arr);
}void test(int arr[3][5]) //ok?
{}//ok
void test(int arr[][]) //ok?
{}
//not ok -> 行可以省略但列不能省略
void test(int arr[][5]) //ok?
{}
//ok
void test(int *arr) //ok?
{}
//not ok -> arr是第一行的地址,而不是首元素的地址
void test(int* arr[5]) //ok?
{}
//not ok -> 形参是指针数组,而不是数组指针
void test(int (*arr)[5]) //ok?
{}
//ok -> 数组指针
void test(int **arr) //ok?
{}
//not ok -> 传过来的第一行的地址不能用二级指针来接收,二级指针是接收一级指针
3.4一级指针传参
通过传递指针作为参数,函数可以修改指针所指向的值,而不仅仅是传递该值的副本。这使得我们可以在函数内部更改调用函数的作用域中的变量。
也可以通过一级指针将一维数组的地址传递给打印函数,再在函数中访问函数,比如:
#include 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;}
3.5二级指针传参
#includevoid 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;}
4指针笔试题
4.1对于数组名和字符串指针的理解
下列程序会打印什么?
#include#includeint main(){int a[] = { 1,2,3,4 };printf("%d\n", sizeof(a));//4*4 = 16printf("%d\n", sizeof(a + 0));//只有sizeof里面只放数组名时数组名才表示整个数组,这里的数组名是数组的首地址//首元素地址+0 -> 还是首元素的地址//地址大小 -> 4/8(4(x86),8(x64))printf("%d\n", sizeof(*a));//数组名是数组的首地址//解引用是个int -> 4字节printf("%d\n", sizeof(a + 1));//同上, 4/8printf("%d\n", sizeof(a[1]));//4printf("%d\n", sizeof(&a));//取出的是数组的地址 -> 4/8字节printf("%d\n", sizeof(*&a));// == sizeof(a) -> 16//or : &a -> int(*)[4]对一个数组指针解引用 -> 访问的是一个数组,sizeof:16字节printf("%d\n", sizeof(&a + 1));//&a + 1相当于是 &a跳过了一个数组,仍然是地址 ->4/8printf("%d\n", sizeof(&a[0]));//首元素地址 -> 4/8printf("%d\n", sizeof(&a[0] + 1));//4/8char arr[] = { 'a','b','c','d','e','f' };printf("%d\n", sizeof(arr));//数组名单独放在sizeof内部,数组名表示整个数组,6字节printf("%d\n", sizeof(arr + 0));//数组名首元素(char*)地址 -> 4/8printf("%d\n", sizeof(*arr));//*arr -> 首元素 - char - 一字节printf("%d\n", sizeof(arr[1]));//一字节printf("%d\n", sizeof(&arr));//&arr -> 数组的地址 -> 4/8printf("%d\n", sizeof(&arr + 1));//跳过整个数组后的地址 - 4/8printf("%d\n", sizeof(&arr[0] + 1));//第二个元素的地址 4/8个字节printf("%d\n", strlen(arr));//随机值 直到遇到 '\0'printf("%d\n", strlen(arr + 0));//arr+0 -> 首元素地址 和第一个一样//printf("%d\n", strlen(*arr));//strlen需要传入一个地址,而这里传入的是一个字符'a' - 97,就是将97作为地址传参,strlen就会从97这个地址传参开始直到遇到'\0'//error//printf("%d\n", strlen(arr[1]));//errorprintf("%d\n", strlen(&arr));//数组的地址和数组首元素的地址,值是一样的.依然从数组的第一个元素开始算,是个随机数printf("%d\n", strlen(&arr + 1));//同上printf("%d\n", strlen(&arr[0] + 1));//第二个元素的地址 - 4/8字节char* p = "abcdef";//'a' 'b' 'c' 'd' 'e' 'f' '\0'printf("%d\n", sizeof(p));//p是一个指针变量, 4/8printf("%d\n", sizeof(p + 1));//p+1是b的地址, 地址大小4/8printf("%d\n", sizeof(*p));//首元素 1字节printf("%d\n", sizeof(p[0]));//一个字节printf("%d\n", sizeof(&p));//字符串的地址 4/8printf("%d\n", sizeof(&p + 1));//跳过一个字符串的地址 4/8printf("%d\n", sizeof(&p[0] + 1));//第二个元素的地址 4/8printf("%d\n", strlen(p));//6printf("%d\n", strlen(p + 1));//5//printf("%d\n", strlen(*p));//error//printf("%d\n", strlen(p[0]));//errorprintf("%d\n", strlen(&p));//随机数 &p ->一个地址printf("%d\n", strlen(&p + 1));//随机数printf("%d\n", strlen(&p[0] + 1));//5return 0;}
4.2程序的结果是什么?
int main(){int a[5] = { 1, 2, 3, 4, 5 };int *ptr = (int *)(&a + 1);printf( "%d,%d", *(a + 1), *(ptr - 1));return 0;}
答案:2,5
解析:&a取出的是整个数组的地址,加1代跳过一整个数组的大小,即&a+1代表数组a后4个字节的地址。然后将这个地址强制类型转化为(int*)并用*ptr储存。a是首元素地址,加1代表第二个元素的地址,解引用得到 2, ptr – 1得到a数组最后一个元素的地址,解引用得到 5。
4.3如下表表达式的值分别为多少?
struct Test{ int Num; char *pcName; short sDate; char cha[2]; short sBa[4];}*p;//假设p 的值为0x100000。int main(){ printf("%p\n", p + 0x1); printf("%p\n", (unsigned long)p + 0x1); printf("%p\n", (unsigned int*)p + 0x1); return 0;}
答案:0x100014
0x100001
0x100004
解析:首先算出结构体的大小为(4+4+2+1*2+2*4=)20字节。由于指针+1是加几取决于指针的类型,所以这里先明确指针的类型:
p为结构体指针,大小为20,故p + 0x1 -> 0x100000+20 = 0x100014
(unsigned long)p为无符号长整型,不是指针,所以+0x1正常运算即可 = 0x100001
(unsigned int*)p为无符号整形指针,大小为4,故(unsigned int*)p + 0x1 -> 0x100000 + 4 =0 0x100004
4.4程序的结果是什么?
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;}
答案:0x4,0x200000
解析:ptr[-1]的情况与题4.2相似(如图4.4.1);对于ptr2储存的地址,先是将数组首元素地址强制类型转化为int类型,+1代表地址+1字节,指向的位置如图4.4.2,这时再转换为int*类型,解引用时顺序访问4个字节的数据,由于是小端程序,还原时为:0x0200000。
(图 4.4.1)
(图4.4.2)
4.5小心有坑!
#include int main(){ int a[3][2] = { (0, 1), (2, 3), (4, 5) };int *p;p = a[0];printf( "%d", p[0]);return 0;}
答案:1
解析:用小括号初始化???——>逗号表达式,实际上存的是1 3 5 0 0 0,故p[0] 为 1
4.6打印结果为?
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;}
答案:0xFFFFFFFC,-4
解析:
首先可以由图确定的是第二个的答案是-4,而-4的原码反码补码分别为:
10000000000000000000000000000100
11111111111111111111111111111011
1111 1111 1111 1111 1111 1111 1111 1100
以%p(16进制)打印:
F F F FF F FC
4.7Work at alibaba
#include int main(){ char *a[] = {"work","at","alibaba"}; char**pa = a; pa++; printf("%s\n", *pa); return 0;}
答案:at
解析:char *a[]定义了一个指针数组,其中每个元素都是指向字符数组的指针。char **pa是指向指针的指针,初始化为指向数组a的第一个元素。然后pa++将pa指向数组a的第二个元素。因此,*pa等价于a[1],即指向字符串 “at” 的指针。