当前位置: 首页 > news >正文

指针进阶篇

指针的基本概念:

  1. 指针是一个变量,对应内存中唯一的一个地址
  2. 指针在32位平台下的大小是4字节,在64位平台下是8字节
  3. 指针是有类型的,指针类型决定该指针的步长,即走一步是多长
  4. 指针运算:指针-指针表示的是两个指针之间的元素个数

回顾完指针的初阶用法,今天我们要来讲讲指针更高阶的用法

一、字符指针

字符指针就是一个指针,指向一个字符

int main()
{char ch = 'a';char* pc = &ch;printf("%c\n", *pc);//打印areturn 0;
}

还有另一种用法:

int main()
{char* p = "abcdefg";printf("%c\n", *p);//打印areturn 0;
}

该怎么理解呢?其实右边的"abcdefg"是一个字符常量,p中存放的是该字符常量首元素的地址。也就是'a'的地址。

来看看一组练习:

int main()
{char arr1[] = "abcdefg";char arr2[] = "abcdefg";char* arr3 = "abcdefg";char* arr4 = "abcdefg";if (arr1 == arr2)printf("arr1和arr2相等\n");elseprintf("arr1和arr2不相等\n");if(arr3 == arr4)printf("arr3和arr4相等\n");elseprintf("arr1和arr2不相等\n");return 0;
}

解答:

二、指针数组

首先,我们得明白指针数组是一个数组,数组中的每个元素是指针类型

int main()
{int a = 1;int b = 2;int c = 3;int d = 4;int* p1[] = { &a,&b,&c,&d };//数组中的每个元素是整型指针char ch1 = 'a';char ch2 = 'b';char ch3 = 'c';char ch4 = 'd';char* p2[] = { &ch1,&ch2,&ch3,&ch4 };//数组中的每个元素是字符指针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* 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;
}

打印时,arr[i]相当于每行首元素的地址,通过下标j就可以依次访问每个整型数组中的元素了。 

在内存中对应的关系如下:

指针数组还有另一种用法:

int main()
{char* arr[] = { "baiyahua","dashidai","tassel" };for (int i = 0; i < 3; i++){printf("%s ", arr[i]);}return 0;
}

我们得知道,arr中存的是字符'b','d','t'的地址,因此分别打印三个字符长常量。

三、数组指针

3.1数组指针的定义

要同前面的指针数组区别开,数组指针本质上是一个指针,该指针指向一个数组。

int arr[] = { 1,2,3,4,5 };

拿这个代码举例,我们想要将该数组的地址存一个指针当中,得到的那个指针就是一个数组指针。要完成这个操作,需要解决两个问题。

1.怎么取出数组的地址?

2.数组指针的类型改怎么写?

对于第一个问题,我们先来梳理一下数组名的概念:

数组名表示首元素的地址,两个情况除外:

  • sizeof(数组名):表示整个数组的大小,单位是字节
  • &数组名:表示取出整个数组的地址

到这里,第一个问题就解决了,要得到一个数组的地址,只需要&数组名就行了。

对于第二个问题,C语言规定的语法是这样写的:

int(*p)[5] = &arr;int *p[5] = &arr;//错误的写法

注意:不能写成下面的形式,因为p首先会跟[]结合,表明这是一个数组,数组中的每个元素是int*类型,p就变成了一个指针数组了。所以一定要将*和p括号起来

对于上面的形式,我们可以这样理解,p首先跟*结合,表明这是一个指针,在跟[]结合,表明指针指向一个数组,数组中的每个元素是int类型,就是一个数组指针了。

3.2&数组名和数组名

刚刚我们提到,平常的数组名表示的是数组首元素的地址,而&数组名表示的是整个数组的地址,在内存中怎么体现出来呢?

还记得我们最开始讲的指针类型决定该指针的步长,如果是数组指针,那么+1会跳过一个数组。

int main()
{int arr[5] = { 1,2,3,4,5 };printf("%p\n", arr);printf("%p\n", arr + 1);printf("\n");printf("%p\n", &arr[0]);printf("%p\n", &arr[0] + 1);printf("\n");printf("%p\n", &arr);printf("%p\n", &arr + 1);printf("\n");return 0;
}

 可以看到,虽然&arr和arr,&arr[0]的值相等,当时它们+1的步长不同,这是因为&arr取出数组的地址,+1跳过了整个数组。

3.3数组指针的使用

理解完数组指针,就该谈谈数组指针的使用了,实际写代码中,数组指针一般用在二维数组。

一般的二维数组打印:

void Print(int arr[3][5], int row, int col)
{int i = row;for (i = 0; i < row; i++){int j = col;for (j = 0; j < col; 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} };Print(arr,3,5);return 0;
}

这里的数组传参,形参我们用了数组的形式,实参部分,我们传了二维数组的数组名,数组名是首元素的地址,只不过对于二维数组,数组的首元素是第一行的地址,也就是说,实参传了一个数组的地址,那形参是不是就可以用我们刚刚学的数组指针来接受了。于是就有了第二种写法:

void Print(int(*p)[5] , int row, int col)
{int i = row;for (i = 0; i < row; i++){int j = col;for (j = 0; j < col; j++){printf("%d ", p[i][j]);}printf("\n");}
}

这里来解释一下p[i][j]

p[i]相当于*(p+i),p+i是每行的地址

*(p+i)就是每行的数组名,而数组名又是数组首元素的地址

因此p[i]就相当于每行首元素的地址,通过j就可以依次访问每行的元素。

四、数组传参和指针传参

写代码时,我们难免要将一个数组或指针传参,那么形参部分该如果设计?

判断下面的传参是否正确:

4.1一维数组传参

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);
}

 4.2二维数组传参

void test(int arr[3][5])//ok?
{}
void test(int arr[][])//ok?
{}
void test(int arr[][5])//ok?
{}
void test(int* arr)//ok?
{}
void test(int* arr[5])//ok?
{}
void test(int(*arr)[5])//ok?
{}
void test(int** arr)//ok?
{}
int main()
{int arr[3][5] = { 0 };test(arr);
}

 4.3一级指针传参

void Print(int* p, int sz)
{int i = 0;for (i = 0; i < sz; i++){printf("%d ", *(p + i));}
}int main()
{int arr[] = { 1,2,3,4,5,6 };int sz = sizeof(arr) / sizeof(arr[0]);int* p = arr;Print(p, sz);return 0;
}

思考:当函数的形参是一级指针的时候,可以接受什么参数?

void test(int* p)
{}int main()
{int a = 1;int* p = &a;int arr[] = { 1,2,3 };test(p);//传整型指针test(&a);//传整型变量的地址test(arr);//传整型一维数组的数组名return 0;
}

4.4二级指针传参

void test(int** pp)
{printf("%d\n", **pp);
}int main()
{int a = 1;int* p = &a;int** pp = &p;test(pp);test(&p);return 0;
}

思考:当函数的参数是二级指针的时候,可以传什么参数?

void test(int** pp)
{}int main()
{int a = 1;int b = 2;int c = 3;int* p = &a;int** pp = &p;int* arr[] = { &a,&b,&c };test(pp);//传二级指针变量test(&p);//传一级指针变量的地址test(arr);//传整型指针数组的数组名return 0;
}

五、函数指针

5.1函数指针的定义

函数指针就是存放一个函数地址的指针,同理解数组指针一样,我们得弄明白

1.怎么得到函数的地址?

2.函数指针变量怎么写?

这里就直接给出结论了,&函数名==函数名,都是取出函数的地址。

函数指针变量的写法:

int test(int x ,int y)
{}int main()
{int(*p)(int, int) = test;int(*p)(int, int) = &test;//两种写法都可以return 0;
}

5.2函数指针的使用

int Add(int x ,int y)
{return x + y;
}int main()
{int a = 1;int b = 2;int c = Add(1, 2);int d = (*Add)(1, 2);printf("c = %d d = %d", c, d);return 0;
}

对于函数指针,*我们可以加也可以不加,两者是同等的,*并没有实际的作用,但是如果要加*,必须跟p括号起来。

来看看两个有趣的代码:

//代码1
(*(void (*)())0)();//代码2
void (*signal(int , void(*)(int)))(int);

代码1解读:

该代码我们从0开始,一个整数前面加上括号,第一想到的应该是强制转换。

再看括号里面的部分,void (*)(),是一个函数指针类型,该函数没有参数,返回值是void类型。

加上*,就是引用该函数。

于是,代码的总体意思就是:引用0地址处的函数,该函数没有参数,返回值为void类型

代码2解读:

首先来看signal,后面加上(),容易想到是函数的定义或声明。

再看参数只有类型,没有参数,能确定是函数的声明。

将signal(int,void(*)(int))去掉,余下的void (*)(int)就是函数的返回值类型了。

该代码的意思是:声明一个函数名为signal的函数,该函数的参数类型分别是int和void(*)(int),返回值类型是void(*)(int)

我们还可以对该代码进行一个简化

typedef void (*ptr)(int);

将void(*)(int)类型重命名为ptr,以后我们想用该类型创建变量时,可以直接用ptr。

ptr signal(int, void(*)(int));

六、函数指针数组

函数指针数组,本质上是数组,数组中每个元素都是函数指针。

6.1函数指针数组的定义

int (*arr[])(int, int);

arr首先与[]结合,表明arr是个数组,数组中每个元素的类型是int (*)(int, int)。

6.2函数指针数组的使用

首先我们来模拟一个简单的计算器,可以实现加减乘除法

//计算器version1
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 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");break;default:printf("输入错误,请重新输入\n");break;}}while (input);return 0;
}

该代码虽然能完成我们的要求,但试想,如果我们要更多的运算,一方面我们得增加运算的定义,另一方面我们还要增加case语句的长度,代码就会变得非常长,这时就可以使用函数指针数组来优化我们的代码。

优化后的代码如下:

//计算器version2
int main()
{int input = 0;int x = 0;int y = 0;int ret = 0;int(*pf[5])(int, int) = { NULL, Add, Sub, Mul, Div };//将运算函数的地址存到一个函数指针数组中//输入数字,通过数组下标执行相应的函数//为了保证输入的数字和我们想要执行的函数对应//加上NULLdo{menu();printf("请输入操作:>");scanf("%d", &input);if (input >= 1 && input <= 4){printf("请输入两个操作数:>");scanf("%d%d", &x, &y);ret = pf[input](x, y);printf("%d\n", ret);}else if (input == 0){printf("退出程序\n");}else{printf("输入错误,请重新输入\n");}} while (input);return 0;
}

这时,我们想要增加运算时,只要添加运算的定义,在函数指针数组中增加新增函数的地址,修改一下if条件,而我们的代码长度不变,大大增加了代码的可读性和维护性。

需要注意的是,由于数组是存储一系列相同类型的元素,因此所有函数的参数类型和返回值必须相同才能使用函数指针数组。

七、指向函数指针数组的指针

首先,指向函数指针数组的指针是一个指针,指向一个数组,数组中每个元素的类型都是函数指针类型。

定义:

int (*pf[5])(int, int) = { NULL,Add,Sub,Mul,Div };int (*(*p)[5])(int, int) = &pf;
//*先跟p结合,表明这是个指针
//往后看到[],表明指针指向一个数组,数组中有5元素
//剩下的int (* )(int, int)就是元素的类型

当然,我们日常写代码时很少用到指向函数指针数组的指针,大家可以做个了解。

八、回调函数

8.1回调函数的定义和使用

回调函数的定义:回调函数不是由函数的实现方直接调用,而是处于某种特定的情境下,由另一个函数通过函数指针的方式调用的。

这里用上面计算器 version1举例,我们发现代码有很多冗余的部分。

这些部分能不能只写成一份?于是就有了我们的计算器version3。

//计算器version3
void Calc(int (*p)(int, int))
{int x = 0;int y = 0;int ret = 0;printf("请输入两个操作数:>");scanf("%d%d", &x, &y);ret = p(x, y);printf("%d\n", ret);
}int main()
{int input = 0;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);return 0;
}

 计算器version3中的Add,Sub,Mul,Div就是回调函数,这些函数不是本身直接调用,而是作为一个参数,传到Calc函数中,由Calc函数调用的。

这里再用库中的qsort函数来帮助大家更好的理解回调函数

8.2qsort的使用

在讲qsort函数之前,简单回顾一下冒泡排序,由于该排序比较简单,就直接给出代码了。

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(int arr[], int sz)
{int i = 0;for (i = 0; i < sz; i++){printf("%d ", arr[i]);}printf("\n");
}int main()
{int arr[] = { 9,8,7,6,5,4,3,2,1 };int sz = sizeof(arr) / sizeof(arr[0]);Print(arr, sz);Bubble_Sort(arr, sz);Print(arr,sz);return 0;
}

该代码的缺陷是只能排序整型数据,当我们想要排序结构体类型,字符类型等的数据,它就不能完成要求,但qsort函数是可以做到的。

qsort函数是C语言的库函数,大家可以自行查找具体用法,这里就给出它大致的用法。

void qsort(void* base, size_t num, size_t size,int (*compar)(const void* p1, const void* p2));
//base指向待排序的数组
//num是待排序数组元素的个数
//size是待排序数组一个元素的大小
//compar是一个函数指针,p1和p2是两个待比较的元素

void*类型可以接受任意类型的数据,但不能进行解引用和+-1操作,它没有具体的类型

为什么base,p1和p2要写成void*类型呢?

因为qsort要实现的是对任意类型的数据进行排序,如果是int*,char*等具体类型的指针,就只能接收对应类型的数据,想要排序其他类型的数据,就得改变base,p1和p2的类型,而我们的库函数肯定是不能随便乱动的。

对于qsort,我们只需要实现数据的比较方法函数,再将该函数作为函数指针参数,传到qsort函数中,qsort就会自动为我们排序。

而对于compar函数,我们要完成的功能是:

当p1所指对象<p2所指对象,返回负数

当p1所指对象>p2所指对象,返回正数

当p1所指对象==p2所指对象,返回0

下面使用qsort来排序一个结构体

struct Stu
{char name[20];int age;
};void PirntStruct(const void* arr, int sz)
{int i = 0;for (i = 0; i < sz; i++){printf("%s %d\n", (((struct Stu*)arr)+i)->name, ((struct Stu*)arr+i)->age);}printf("\n");
}//以年龄排序
int compar_by_age(const void* p1, const void* p2)
{return (((struct Stu*)p1)->age - ((struct Stu*)p2)->age);
}//以名字排序
int compar_by_name(const void* p1, const void* p2)
{return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);//字符串的比较用strcmp函数,strcmp函数的返回值正好与我们要求的返回值一致
}int main()
{struct Stu stu[] = { {"baiyahua",20},{"zhangsan",18},{"lisi",13} };int sz = sizeof(stu) / sizeof(stu[0]);printf("排序前:\n");PirntStruct(stu, sz);printf("以名字排序后:\n");qsort(&stu, sz, sizeof(stu[0]), compar_by_name);PirntStruct(stu, sz);printf("以年龄排序后:\n");qsort(&stu, sz, sizeof(stu[0]), compar_by_age);PirntStruct(stu, sz);return 0;
}

8.3qsort的模拟实现

知道了qsort的使用方法,我们能不能自己实现一个qsort函数,这里底层用冒泡排序来模拟一个qsort函数。

需要解决的问题:

函数的参数是怎么设计的?这里可以参考库中的qsort函数

怎么比较两个数据?由于需要排序不同类型的数据,就需要不同类型数据的比较方法,我们将方法独立写成一个函数,将该函数的指针传为我们的qsort函数,再我们的qsort函数中调用

怎么交换两个数据?我们将两个数据的内存全部交换相当于交换的两个数据

int compar(const void* p1, const void* p2)
{return *(int*)p1 - *(int*)p2;
}void Swap(char* e1, char* e2, int size)
{int i = 0;for (i = 0; i < size; i++){char tmp = e1[i];e1[i] = e2[i];e2[i] = tmp;}
}void my_qsort(void* base, size_t num, size_t size, int compar(const void*,const void*))
{int i = 0;for (i = 0; i < num - 1; i++){int j = 0;for (j = 0; j < num - 1 - i; j++){if (compar((char*)base + j * size, (char*)base + (j + 1) * size) > 0){Swap((char*)base + j * size, (char*)base + (j + 1) * size, size);}}}
}

这里解释一下两个地方

 想要排序结构体数据时只要给出结构体数据的比较方法即可

九、指针和数组笔试题解析

int main()
{//一维数组int a[] = { 1,2,3,4 };printf("%d\n", sizeof(a));// 16,sizeof(数组名),计算整个数组的大小printf("%d\n", sizeof(a + 0));// 4/8,32位机器下4字节,64位机器下8字节printf("%d\n", sizeof(*a));// 4,sizeof(1),整形数据的大小printf("%d\n", sizeof(a + 1));// 4/8,地址的大小printf("%d\n", sizeof(a[1]));// 4,sizeof(2)printf("%d\n", sizeof(&a));// 4/8,地址的大小printf("%d\n", sizeof(*&a));// 16,*和&相抵消,相当于sizeof(a)printf("%d\n", sizeof(&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));//6,计算数组所占空间printf("%d\n", sizeof(arr + 0));//4/8,地址的大小printf("%d\n", sizeof(*arr));//1,字符'a'的大小printf("%d\n", sizeof(arr[1]));//1,字符'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,地址的大小printf("%d\n", strlen(arr));//随机值printf("%d\n", strlen(arr + 0));//随机值printf("%d\n", strlen(*arr));//程序报错,'a'的ASCII码值是97,strlen认为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));//随机值return 0;
}
int main()
{char arr[] = "abcdef";printf("%d\n", sizeof(arr));// 7printf("%d\n", sizeof(arr + 0));// 4/8printf("%d\n", sizeof(*arr));//1printf("%d\n", sizeof(arr[1]));// 1printf("%d\n", sizeof(&arr));// 4/8printf("%d\n", sizeof(&arr + 1));// 4/8printf("%d\n", sizeof(&arr[0] + 1));// 4/8printf("%d\n", strlen(arr));// 6printf("%d\n", strlen(arr + 0));//6printf("%d\n", strlen(*arr));//程序报错printf("%d\n", strlen(arr[1]));//程序报错printf("%d\n", strlen(&arr));//6printf("%d\n", strlen(&arr + 1));//随机值printf("%d\n", strlen(&arr[0] + 1));//5return 0;
}
int main()
{char* p = "abcdef";printf("%d\n", sizeof(p));// 4/8printf("%d\n", sizeof(p + 1));// 4/8printf("%d\n", sizeof(*p));// 1printf("%d\n", sizeof(p[0]));// 1printf("%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));//程序报错//printf("%d\n", strlen(p[0]));//程序报错printf("%d\n", strlen(&p));//随机值printf("%d\n", strlen(&p + 1));//随机值printf("%d\n", strlen(&p[0] + 1));//5return 0;
}

10.指针笔试题

笔试题1:

int main()
{int a[5] = { 1, 2, 3, 4, 5 };int *ptr = (int *)(&a + 1);printf( "%d,%d", *(a + 1), *(ptr - 1));return 0;
}

代码解析:

  • a是数组名,既没有sizeof(数组名),也没有&数组名,所以就表示数组首元素地址;
    *(a+1)就表示数组第二个元素,也就是2
  • &a取出整个数组的地址,(&a+1)跳过一个数组,再强转成一个int*类型;因此ptr-1就指向数组倒数第一个元素,*(ptr-1)就是5

笔试题2: 

//由于还没学习结构体,这里告知结构体的大小是20个字节
struct Test
{int Num;char* pcName;short sDate;char cha[2];short sBa[4];
}*p;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
//已知,结构体Test类型的变量大小是20个字节
int main()
{p = 0x100000;printf("%p\n", p + 0x1);printf("%p\n", (unsigned long)p + 0x1);printf("%p\n", (unsigned int*)p + 0x1);return 0;
}

题目解析:

  • p是一个结构体指针,+1跳过一个结构体的大小,因此(p+1)跳过20个字节,转换成16进制就是0x00000014,(p+0x1)就是00100014
  • p被强转成无符号长整型,+1就是整型+1,因此((unsigned long)p+0x1)就是00100001
  • p被强转成无符号整型指针,+1跳过一个整型指针类型,也就是4字节,((unsigned int*)p+0x1)就是00100004

笔试题3:

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;
}

题目解析:

  • (&a+1)跳过数组,再强转成整型;ptr[-1]相当于*(ptr-1),答案是4
  • a被强转成整型,+1就是数值+1,再被强转整型指针,这里我们需要画出a数组的详细内存图
    *ptr2就是图中的红色部分,但内存中是以小端字节序存储的,拿出来要还原,因此答案就是02000000

笔试题4:

int main()
{int a[3][2] = { (0, 1), (2, 3), (4, 5) };int* p;p = a[0];printf("%d", p[0]);return 0;
}

题目解析:

  • 需要注意,数组的初始化中有(),不要误以为这是在初始化数组,()中是一个表达式,其结果是最后一个表达式的结果,因此(0,1)是1,(2,3)是3,(4,5)是5,实际的数组中的数据

    a[0]是数组首行的地址,也是第一行的数组名,数组名表示首元素的地址,a[0]就是第一行第一个元素的地址,p[0]相当于*(p+0);答案是1

笔试题5:

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;
}

题目解析:

  • 这题需要我们画出数组内存图
    (&p[4][2]-&a[4][2])是一个高地址-低地址,得出的结果是-4;
    -4的补码是:11111111 11111111 11111111 11111100,表示成16进制就是FFFFFFFC
    以%d打印就是-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;
}

代码解析:

  • (ptr1-1)是数组倒数第一个元素的地址,*(ptr1-1)就是10
  • *(aa+1)可以看成a[1],是第二行的数组名,也就是第二行首元素的地址,被强转成了int*;因此(ptr2-1)是第一行倒数第一个元素的地址,答案是5

笔试题7:

int main()
{char* a[] = { "work","at","alibaba" };char** pa = a;pa++;printf("%s\n", *pa);return 0;
}

题目解析:

  • 内存分布图
    答案就是at

面试题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;
}

题目解析:

  • 内存分布图
    需要注意:++会改变cpp的值
    因此答案就是POINT,ER,ST,EW

关于指针的内容就讲到这!

相关文章:

指针进阶篇

指针的基本概念&#xff1a; 指针是一个变量&#xff0c;对应内存中唯一的一个地址指针在32位平台下的大小是4字节&#xff0c;在64位平台下是8字节指针是有类型的&#xff0c;指针类型决定该指针的步长&#xff0c;即走一步是多长指针运算&#xff1a;指针-指针表示的是两个指…...

C语言之单链表理解与应用

其实网上有好多关于单链表理解&#xff0c;其实知乎上有一篇写的很好&#xff0c;利用图形与代码结合&#xff0c;我觉得写的很好&#xff0c;大家也可以去查一下&#xff0c;每个人都有自己的想法与理解&#xff0c;这里主要看单链表概念&#xff0c;应用场景&#xff0c;举例…...

SpringBoot对PDF进行模板内容填充、电子签名合并

1. 依赖引入–这里只包含额外引入的包 原有项目包不含括在内 <!-- pdf编辑相关--> <dependency><groupId>com.itextpdf</groupId><artifactId>itextpdf</artifactId><version>5.5.13.3</version> </dependency><de…...

Vue3快速上手笔记

Vue3快速上手 1.Vue3简介 2020年9月18日&#xff0c;Vue.js发布3.0版本&#xff0c;代号&#xff1a;One Piece&#xff08;海贼王&#xff09;耗时2年多、2600次提交、30个RFC、600次PR、99位贡献者github上的tags地址&#xff1a;https://github.com/vuejs/vue-next/release…...

LLM中的Prompt提示

简介 在LLM中&#xff0c;prompt&#xff08;提示&#xff09;是一个预先设定的条件&#xff0c;它可以限制模型自由发散&#xff0c;而是围绕提示内容进行展开。输入中添加prompt&#xff0c;可以强制模型关注特定的信息&#xff0c;从而提高模型在特定任务上的表现。 结构 …...

【算法Hot100系列】最长回文子串

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…...

KaiwuDB × 国网山东综能 | 分布式储能云边端一体化项目建设

项目背景 济南韩家峪村首个高光伏渗透率台区示范项目因其所处地理位置拥有丰富的光照资源&#xff0c;该区域住户 80% 以上的屋顶都安装了光伏板。仅 2022 年全年&#xff0c;光伏发电总量达到了百万千瓦时。 大量分布式光伏并网&#xff0c;在输出清洁电力的同时&#xff0c…...

elasticsearch查询出现Limit of total fields 1000 has been exceeded

项目场景&#xff1a; 在项目中使用elasticsearch保存日志等相关数据&#xff0c;查询页面查询这些日志数据 问题描述 提示&#xff1a;这里描述项目中遇到的问题&#xff1a; 今天在检查日志数据时&#xff0c;发现数据出不来&#xff0c;检查后端日志&#xff0c;发现一直…...

TCP/IP详解——DHCP 协议

文章目录 1. DHCP 协议1.1 DHCP 概念1.2 DHCP 原理1.3 DHCP 续约1.4 DHCP 报文种类1.5 DHCP 报文格式1.6 DHCP 协议抓包分析1.6.1 Wireshark 抓包查看1.6.2 CSNAS 抓包分析 1.7 DHCP 的 Option1.8 思考 1. DHCP 协议 1.1 DHCP 概念 DHCP协议称为动态主机配置协议。 DHCP作用…...

牛客后端开发面试题3

阿里巴巴2021 1、通配符的含义 在字符串匹配时可以代替一定范围的字符。 2、死锁的基本知识 产生死锁的原因&#xff1a; 1.系统资源不足 2.进程运行推进方式不合理 3.分配资源不合理 &#xff08;把幼儿园老师比作操作系统&#xff0c;幼儿园里的玩具比作系统资源&#xff0c…...

Postman-脚本自动化及定时执行脚本(7)

一.postman脚本自动化&#xff08;从postman至Newman可以一键执行脚本并生成报告&#xff1a;&#xff09; Postman Newman 是一个 CLI&#xff08;命令行界面&#xff09;工具&#xff0c;可以使用它来运行 Postman 中的集合&#xff08;Collection&#xff09;和环境&#xf…...

基于SSM的影视企业全渠道会员管理系统(有报告)。Javaee项目

演示视频&#xff1a; 基于SSM的影视企业全渠道会员管理系统&#xff08;有报告&#xff09;。Javaee项目 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&#xff0c;通过Spring S…...

【C++】 C++11 新特性探索:decltype 和 auto

▒ 目录 ▒ &#x1f6eb; 问题描述环境 1️⃣ decltype推导变量类型推导函数返回类型 2️⃣ auto自动推导变量类型迭代器和范围循环 3️⃣ decltype 和 auto 同时使用&#x1f6ec; 结论&#x1f4d6; 参考资料 &#x1f6eb; 问题 描述 C11 引入了一些强大的新特性&#xff…...

【Jeecg Boot 3 - 第二天】1.2、jar 包和 lib 依赖分离,部署包缩小100倍

一、场景 二、思路 三、实战 ▶ 2.1、项目 jar 包解压获取 lib config Stage 1&#xff1a;正常打包获取 jeecg-system-start-3.6.0.jar Stage 2&#xff1a;解压 获取如下文件 Stage 3&#xff1a;获取 lib config ▶ 2.2、获取简化版项目jar包 Stage 1&#xff1…...

电商平台的易聊集成:无代码开发,API连接,CRM支持

连接电商与客服&#xff1a;易聊的创新解决方案 在迅速变化的电子商务市场中&#xff0c;企业要想保持竞争力&#xff0c;就必须拥有高效灵活的客服体系。易聊&#xff0c;一家领先的AISaaS服务商&#xff0c;正是基于这一需求&#xff0c;推出了一系列创新产品。它们通过智能…...

Draw.io or diagrams.net 使用方法

0 Preface/Foreword 在工作中&#xff0c;经常需要用到框图&#xff0c;流程图&#xff0c;时序图&#xff0c;等等&#xff0c;draw.io可以完成以上工作。 official website:draw.io 1 Usage 1.1 VS code插件 draw.io可以扩展到VS code工具中。...

CAPL——发送自定义报文

文章目录 一、前言二、CANoe操作二、CAPL程序三、Trace结果一、前言 CAPL是CANoe自带的一个编程语言,基本语法基于C语言,通过CAPL可以发挥CANoe更高效、更强大的功能。CAPL最大的特点就是可以编程灵活的完成报文的发送,报文包括通信报文及诊断报文 本文讲述模拟一个报文周…...

接口自动化测试实操【设置断言思路】

1 断言设置思路 这里总结了我在项目中常用的5种断言方式&#xff0c;基本可能满足90%以上的断言场景&#xff0c;具体参见如下脑图&#xff1a; 在这里插入图片描述 下面分别解释一下图中的五种思路&#xff1a; 1&#xff09; 响应码 对于http类接口&#xff0c;有时开发人…...

windows redis 允许远程访问配置

安装好windows版本的redis&#xff0c;会以服务方式启动&#xff0c;但是不能远程访问&#xff0c;这个时候需要修改配置。redis安装路径下会有2个配置文件&#xff0c;究竟需要怎么修改才能生效呢&#xff1f;看下图 这里的redis服务指定了是redis.windows-service.conf文件&…...

三、JS逆向

一、JS逆向 解释&#xff1a;在我们爬虫的过程中经常会遇到参数被加密的情况&#xff0c;这样只有先在前端搞清楚加密参数是怎么生成的才能继续我们的爬虫&#xff0c;而且此时我们还需要用python去执行这个加密的过程。本文主要讲怎么在浏览器调试JS&#xff0c;以及Python执…...

HBase 高可用集群详细图文安装部署

目录 一、HBase 安装部署 1.1 Zookeeper 正常部署 1.2 Hadoop 正常部署 1.3 HBase 安装 1.4 HBase 的配置文件 1.4.1 hbase-env.sh 1.4.2 hbase-site.xml 1.4.3 regionservers 1.4.4 创建目录 1.5 HBase 远程发送到其他节点 1.6 HBase 服务的启动 1.6.1 单点…...

现代雷达车载应用——第2章 汽车雷达系统原理 2.6节 雷达设计考虑

经典著作&#xff0c;值得一读&#xff0c;英文原版下载链接【免费】ModernRadarforAutomotiveApplications资源-CSDN文库。 2.6 雷达设计考虑 上述部分给出了汽车雷达基本原理的简要概述。在雷达系统的设计中&#xff0c;有几个方面是必不可少的&#xff0c;它们决定了雷达系…...

【JVM从入门到实战】(五)类加载器

一、什么是类加载器 类加载器&#xff08;ClassLoader&#xff09;是Java虚拟机提供给应用程序去实现获取类和接口字节码数据的技术。 类加载器只参与加载过程中的字节码获取并加载到内存这一部分。 二、jdk8及之前的版本 类加载器分为三类&#xff1a; 启动类加载器-加载Ja…...

计算机毕业设计 基于Web的城市旅游网站的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…...

【人工智能革命】:AIGC时代的到来 | 探索AI生成内容的未来

&#x1f3a5; 屿小夏 &#xff1a; 个人主页 &#x1f525;个人专栏 &#xff1a; IT杂谈 &#x1f304; 莫道桑榆晚&#xff0c;为霞尚满天&#xff01; 文章目录 &#x1f4d1;前言一. AIGC 技术的概述和发展趋势1.1 AIGC 技术的概述1.2 AIGC 技术的发展趋势 二. AIGC 与元宇…...

spring-boot-data-jpa、JPA实现分页

spring-boot-data-jpa、JPA实现分页 JPA越来越丰富了&#xff0c;下面使用springboot3.x实现JPA分页 通过传入PageRequest pageRequest PageRequest.of(page, size);到接口查询&#xff0c;返回Page拿到分页数据。 转自 https://lingkang.top/archives/jpa-shi-xian-fen-ye …...

云原生之深入解析如何在Kubernetes中快速启用Cgroup V2支持

一、cgroup v2 有哪些优势&#xff1f; Linux 中有两个 cgroup 版本&#xff1a;cgroup v1 和 cgroup v2。cgroup v2 是新一代的 cgroup API。Kubernetes 自 v1.25 起 cgroup2 特性正式 stable。cgroup v2 提供了一个具有增强资源管理能力的统一控制系统&#xff0c;cgroup v2…...

QT实现四则运算计算器

#include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this);this->setMaximumSize(240,300);this->setMinimumSize(240,300);this->setWindowTitle("计算器&…...

mysql的redolog、undo、binlog的作用

概览&#xff1a; MySQL三大日志包括&#xff1a;undolog&#xff0c;redo log&#xff0c;binlog&#xff0c;它们分别有以下作用&#xff1a; undolog&#xff1a;是Innodb存储引擎事务生成的日志。用于事务的回滚和MVCC&#xff0c;保证了事务的原子性。 redo log&#x…...

京东大数据-10月京东咖啡机市场销售数据分析-销售额增长41%,德龙等海外头部品牌店铺数据分析

如今&#xff0c;咖啡已经成为了人们日常生活中流行的生活饮品之一&#xff0c;消费量较大。随着咖啡的受众人群越来越多&#xff0c;消费者们对咖啡品质的要求也愈来愈高&#xff0c;而咖啡品质除了受咖啡豆质量影响外&#xff0c;还受制作过程中煮泡时间、水温和物料数量等因…...

网站建设公司shundeit/edm营销

作者&#xff1a;朱金灿 来源&#xff1a;http://blog.csdn.net/clever101/ 有些东西或许只有失去你才会发现它在你的生活有着重大影响&#xff0c;比如google搜索。现在基本可以肯定g.cn是我最常用的网站。就在前些日子&#xff0c;我和朋友讨论google退出的事&#xff0c;他…...

百度做网站和推广效果怎么样/国内建站平台

以下几种方法可以都试用一下&#xff0c;思路都差不多。 EXE文件关联被修改后&#xff0c;情形是很吓人的。某回给一个企业修理此类状况&#xff0c;公司财务吓得几乎面无人色&#xff0c;以为所有的东西都完蛋了。经我一番巧妙折腾&#xff0c;症状消失&#xff0c;才如释重负…...

wordpress keshop/凡科建站app

1256 打鼹鼠 时间限制: 1 s空间限制: 128000 KB题目等级 : 钻石 Diamond题解查看运行结果题目描述 Description鼹鼠是一种很喜欢挖洞的动物&#xff0c;但每过一定的时间&#xff0c;它还是喜欢把头探出到地面上来透透气的。 根据这个特点阿Q编写了一个打鼹鼠的游戏&#xff1a…...

哪个网站做螺丝生意好/小红书推广价目表

总算是把卡方检验的思想看懂了 用来判断概率分布X与Y是否有关。 所以在文本特征选择中&#xff0c;有如下的计算公式&#xff1a; 转自Hackerzer的博客&#xff1a;http://blog.csdn.net/hackerzer/article/details/76713829...

武汉免费做网站/怎么自己制作一个网站

相信很多童鞋在朋友圈总能看到类似的消息&#xff1a;“手机丢失&#xff0c;请大家重新给我发下手机号吧”或“换新卡联系人没了&#xff0c;兄弟们发下电话给我”&#xff0c;而这些尴尬都是没能养成良好的备份通讯录习惯造成的。那么&#xff0c;现阶段如何备份最为靠谱呢&a…...

最近在线观看免费完整版高清电影/seo公司排名

1. kafka介绍1.1. 主要功能根据官网的介绍&#xff0c;ApacheKafka是一个分布式流媒体平台&#xff0c;它主要有3种功能&#xff1a;1&#xff1a;It lets you publish and subscribe to streams of records.发布和订阅消息流&#xff0c;这个功能类似于消息队列&a…...