指针进阶篇

指针的基本概念:

  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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/226980.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

赛氪为第五届全球校园人工智能算法精英大赛决赛选手保驾护航

12月10日&#xff0c;以“智青春算未来”为主题的2023年第五届全球校园人工智能算法精英大赛全国总决赛在河海大学江宁校区举行。本次大赛由江苏省人工智能学会主办&#xff0c;自9月份启动以来&#xff0c;共吸引了全国近400所高校的3000多支参赛团队参加。经过校赛、省赛选拔…

nlp与cv的发展

Transformer的出现,促进了更高容量模型的建立,为大模型的出现奠定基础. &#x1f9d0;大模型通常具有十亿个以上参数(仅供参考) &#x1f62e;左边的蓝色是CV领域、右下绿色是NLP、右上蓝色是多模态&#x1f603;基础模型(Foundational Models)首次由Bommasani等人在《Stanford…

HTTP代理服务器脚本录制

1、报错1 target controller is configured to “use recording Controller“ but no such controller exists,ensure_target controller is configured to "use recording -CSDN博客

等等Domino 14.0FP1

大家好&#xff0c;才是真的好。 节奏确实太快了&#xff0c;有时候我深感我也追不上。 以前Notes Domino是三年磨一剑&#xff0c;也就说每三年才发一个大版本&#xff0c;从2019年开始&#xff0c;进行了高频提速&#xff0c;居然一年一个大版本&#xff01; 周末&#xf…

NAT——网络地址转换

目录 一、概念 二、NAT的分类 1.静态NAT 1.1 静态NAT的配置 1.2 利用eNSP小实验加强对静态NAT的理解 2、动态NAT 三、NAPT——端口映射 四、Easy IP 使用一个公网地址可以让所有人都上公网 一、概念 随着Internet的发展和网络应用的增多&#xff0c;IPv4地址枯竭已经成为…

jmeter 如何循环使用接口返回的多值?

有同学在用jmeter做接口测试的时候&#xff0c;经常会遇到这样一种情况&#xff1a; 就是一个接口请求返回了多个值&#xff0c;然后下一个接口想循环使用前一个接口的返回值。 这种要怎么做呢&#xff1f; 有一定基础的人&#xff0c;可能第一反应就是先提取前一个接口返回…

在Node.js中MongoDB排序的方法

本文主要介绍在Node.js中MongoDB排序的方法。 目录 Node.js中MongoDB排序使用原生的mongodb驱动程序进行排序使用Mongoose库中的排序 Node.js中MongoDB排序 在Node.js中使用MongoDB进行排序&#xff0c;可以使用原生的mongodb驱动程序或者Mongoose库。 使用原生的mongodb驱动…

改进lora-scripts,支持SDXL训练,以及启动脚本

分享下自己改进的一个lora训练脚本&#xff0c;在ubuntu下如果SD-WEBUI的环境已经搭好的话&#xff0c;只需要下载lora-script就可以支持训练了&#xff0c;直接命令行方式训练。 首先&#xff0c;我们需要克隆下项目&#xff1a; git clone https://github.com/Akegarasu/lo…

黑色翻页时钟HTML源码-倒计时单页翻页时钟

黑色翻页时钟HTML源码-倒计时单页翻页时钟这是一个类似fliqlo的黑色翻页时钟HTML源码&#xff0c;它仅包含一个HTML文件&#xff0c;上传到网站后即可使用。该时钟具有查看当前时间、秒表和倒计时功能&#xff0c;并且可以在页面的右下角进行设置。 红色动态炫酷数字时钟html网…

【已解决】在使用poi-tl生成的word文档时候,怎么添加目录?poi-tl生成目录解决办法

需求&#xff1a; 需求的报告模板中大概包括标题、目录、前言、章节&#xff08;根据模板动态生成的标题文字表格图片&#xff09;&#xff0c;其中目录需要根据章节的实际情况动态生成。在网上没有找到什么好的解决方案&#xff0c;请教一下实现思路&#xff0c;非常感谢。 …

MATLAB 计算两片点云间的最小距离(2种方法) (39)

MATLAB 计算两片点云间的最小距离 (39) 一、算法介绍二、算法实现1.常规计算方法2.基于KD树的快速计算一、算法介绍 假设我们现在有两片点云 1 和 2 ,需要计算二者之间的最小距离,这里提供两种计算方法,分别是常规计算和基于KD树近邻搜索的快速计算方法,使用的测试数据如…

为什么选择国产WordPress:HelpLook的优势解析

如今网站建设可以说已经是企业必备。而在众多的网站建设工具中&#xff0c;WordPress无疑是其中的佼佼者。作为一款开源的CMS&#xff08;内容管理系统&#xff09;&#xff0c;WordPress拥有丰富的插件和主题&#xff0c;以及强大的功能&#xff0c;使得用户可以轻松地构建出符…

vivado约束方法8

无交互的逻辑互斥时钟组 逻辑排他性时钟是指在不同源点上定义但共享部分的时钟由于多路复用器或其他组合逻辑&#xff0c;它们的时钟树。时间限制向导识别此类时钟&#xff0c;并建议在它们这样做时直接对其进行时钟组约束除了连接到其共享时钟的逻辑之外&#xff0c;彼此之间…

半导体:Gem/Secs基本协议库的开发(5)

此篇是1-4 《半导体》的会和处啦&#xff0c;我们有了协议库&#xff0c;也有了通讯库&#xff0c;这不得快乐的玩一把~ 一、先创建一个从站&#xff0c;也就是我们的Equipment端 QT - guiCONFIG c11 console CONFIG - app_bundle CONFIG no_debug_release # 不会生…

Python 直观理解基尼系数

基尼系数最开始就是衡量人群财富收入是否均衡&#xff0c;大家收入平平&#xff0c;那就是很平均&#xff0c;如果大家收入不平等&#xff0c;那基尼系数就很高。 还是给老干部们讲的言简意赅。 什么是基尼系数 我们接下来直接直观地看吧&#xff0c;程序说话 # -*- coding:…

Chart.js 实现实时动态折线图 并限制最大长度

<!DOCTYPE html> <html><head><title>模拟</title><script src"https://lib.sinaapp.com/js/jquery/3.1.0/jquery-3.1.0.min.js"></script><script src"https://cdn.staticfile.org/Chart.js/3.9.1/chart.js"…

12345、ABCDE项目符号列表文字视频怎么制作?重点内容介绍PR标题模板项目工程文件

Premiere模板&#xff0c;包含10个要点标题12345、ABCDE项目符号列表文字模板PR项目工程文件。可以根据自己的需要定制颜色。在视频的开头、中间和结尾使用。包括视频教程。 适用软件&#xff1a;Premiere Pro 2019 | 分辨率&#xff1a;19201080 (HD) | 文件大小&#xff1a;9…

基于Java SSM框架实现疫情居家办公OA系统项目【项目源码+论文说明】

基于java的SSM框架实现疫情居家办公OA系统演示 摘要 21世纪的今天&#xff0c;随着社会的不断发展与进步&#xff0c;人们对于信息科学化的认识&#xff0c;已由低层次向高层次发展&#xff0c;由原来的感性认识向理性认识提高&#xff0c;管理工作的重要性已逐渐被人们所认识…

加油站“变身”快充站,探讨充电新模式——安科瑞 顾烊宇

摘要&#xff1a;新能源汽车规模化发展的同时&#xff0c;充电不便利的痛点愈发明显。在未来的新能源汽车行业发展当中&#xff0c;充电的矛盾要远远大于造车的矛盾&#xff0c;解决好充电的问题成为电动汽车行业发展的一个突出问题。解决充电补能问题&#xff0c;重要的方式之…

分库分表以后,如何实现扩容?

在实际开发中&#xff0c;数据库的扩容和不同的分库分表规则直接相关&#xff0c;今天我们从系统设计的角度&#xff0c;抽象了一个项目开发中出现的业务场景&#xff0c;从数据库设计、路由规则&#xff0c;以及数据迁移方案的角度进行讨论。 从业务场景出发进行讨论 假设这…