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

C 进阶 — 指针的使用

C 进阶 — 指针的使用

主要内容

1、字符指针

2、数组指针

3、指针数组

4、数组传参和指针传参

5、函数指针

6、函数指针数组

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

8、 回调函数

9、指针和数组练习题

前节回顾

1、指针就是个变量,用来存放地址,地址唯一标识一块内存空间

2、指针大小是固定的 4/8 个字节(32 位平台/64 位平台)

3、指针有类型,指针的类型决定了指针的 ± 整数的步长和指针解引用操作时的解释

4、指针的运算

一 字符指针

在指针的类型中我们知道有一种指针类型为字符指针 char*

//使用方式一
int main()
{char ch = 'w';char *pc = &ch;*pc = 'x';return 0;
}//使用方式二
int main()
{const char* pstr = "HELLO WORLD"; //如何理解该行printf("%s\n", pstr);return 0;
}

代码 const char* pstr = "HELLO WORLD"; 是把字符串 HELLO WORLD 首字符的地址放到了 pstr 中(可不能理解成把字符串 HELLO WORLD 放到字符指针 pstr 里了)

练习题

下面代码的最终输出是

#include <stdio.h>
int main()
{char str1[] = "HELLO WORLD";char str2[] = "HELLO WORLD";const char *str3 = "HELLO WORLD";const 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) //这里比较的是 STR 存放的地址printf("str3 and str4 are same\n"); //√elseprintf("str3 and str4 are not same\n");return 0;
}

这里 str3 和 str4 指向的是一个同一个常量字符串。C/C++ 会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串的时候,他们实际会指向同一块内存

但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以 str1 和 str2 不同,str3 和 str4 相同

这里可以结合内存分配会更好理解,不同的数组在栈区分配的内存空间是不同的,而两个 str 指针在栈区的内存空间当然也不同,可是它们内存空间的值相同(指向常量区的同一个位置)

二 指针数组

指针数组是一个存放指针的数组,操作符 [] 的优先级大于 *

//复习 下面的数组表示的意义
int* arr1[10];		 //整形指针数组
char *arr2[4]; 		 //一级字符指针数组
char **arr3[5];		 //二级字符指针数组

三 数组指针

3.1 数组指针定义

数组指针是指针,即指向数组的指针

// p1, p2 分别是什么
int *p1[10];  //指针数组, 表示十个元素的整型指针数组
int (*p2)[10]; //数组指针, 表示指向十个整型元素数组的指针//[] 优先级要高于 * 号, 加上 () 保证结合律
3.2 & 数组名和 数组名

int arr[10];

arr 和 &arr 分别的含义是 ?arr 是数组名,数组名表示数组首元素的地址。那 &arr 数组名是 ?

#include <stdio.h>
int main()
{int arr[10] = {0};printf("%p\n", arr);printf("%p\n", &arr);return 0;
}

打印的内容是一样的,即值相同,但它们代表的含义却并不相同

image-20241206162530400

#include <stdio.h>
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    = 000000D344CFF7D8
&arr   = 000000D344CFF7D8
arr+1  = 000000D344CFF7DC
&arr+1 = 000000D344CFF800

&arr 和 arr,虽然值相同,但意义不同。&arr 表示的是 数组的地址,而不是数组首元素的地址

本例中 &arr 的类型是: int(*)[10] ,是一种数组指针类型。数组的地址 + 1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是 40

3.3 数组指针的使用
//代码一
#include <stdio.h>
int main()
{int arr[10] = {1,2,3,4,5,6,7,8,9,0};int (*p)[10] = &arr;//把数组 arr 的地址赋值给数组指针变量 preturn 0;
}
//代码二
#include <stdio.h>//普通二维数组的打印
void print_arr1(int arr[3][5], int row, int col)
{for(int i=0; i<row; i++){for(int j=0; j<col; j++)printf("%d ", arr[i][j]);printf("\n");}
}void print_arr2(int (*arr)[5], int row, int col)
{for(int i=0; i<row; i++){for(int j=0; j<col; j++)printf("%d ", arr[i][j]);printf("\n");}
}int main()
{int arr[3][5] = {1,2,3,4,5,6,7,8,9,10};print_arr1(arr, 3, 5);//数组名 arr, 表示首元素的地址, 但二维数组的首元素是二维数组的第一行. 所以这里传递的 arr, 其实相当于第一行的地址, 是一维数组的地址. 可以数组指针来接收print_arr2(arr, 3, 5); //传递的是第一行的地址, 这里联想二维数组的内存分布会好理解return 0;
}

练习,解释下面代码的意思

int arr[5];				//有五个元素的整型数组
int *parr1[10];			//有十个整型指针元素的数组
int (*parr2)[10];   	//指向十个整型元素数组的指针
int (*parr3[10])[5];	//有十个数组指针元素的数组(数组指针指向有五个元素的整型数组)

四 数组参数、指针参数

4.1 一维数组传参
#include <stdio.h>
void test(int arr[]) {}     	// √
void test(int arr[10]) {}   	// √
void test(int *arr) {}      	// √
void test2(int *arr[20]) {} 	// √
void test2(int **arr) {}    	// √int main()
{int arr[10] = {0};   //数组int *arr2[20] = {0}; //指针数组test(arr);test2(arr2);
}

第一个 test() 函数传参是一个数组,第一个 test() 函数用一个没有元素个数的数组来接收没问题(一维数组传参,函数形参可以省略数组元素个数)

第二个 test() 函数加上了数组元素个数自然也没问题

第三个 test() 函数用一级指针来接收一维数组也是没什么问题,数组名就是数组首元素的地址

第一个 test2() 函数参数是一个指针数组,就是一个存储一级指针的数组。函数用一个相同结构的指针数组来接收没问题,这里也可以省略数组的元素个数

第二个 test2() 函数用一个二级指针来接收也可以,因为数组名是首元素地址,而数组里面元素存放的也是一个地址,地址的地址用一个二级指针来接收没问题

总结一下:一维数组传参,函数的形参用相同的结构来接收没问题,数组元素个数可以省略,函数的形参用指针来接收就需要考虑指针和地址之间的关系了

4.2 二维数组传参
void test(int arr[3][5]) {}   			//√
void test(int arr[][]) {}				//×
void test(int arr[][5]) {}				//√
void test(int *arr) {}					//√
void test(int* arr[5]) {}				//×
void test(int (*arr)[5]) {}				//√
void test(int **arr) {}					//×int main()
{int arr[3][5] = {0};test(arr);return 0;
}

第一个 test() 函数的传参是一个二维数组,第一个 test() 函数用相同结构的二维数组接收没问题

第二个test() 函数用省略了元素个数的二维数组来接收,二维数组初始化时可以省略行数,不能忽略列数

第三个 test() 函数省略了行数,没有省略列数没问题

第四个 test() 函数用一级指针来接收二维数组是可行的,数组名是首元素地址,地址用一级指针接收没问题

第五个 test() 函数用一维指针数组来接收不行,指针数组本质是数组,用数组来接收数组需要用相同结构的数组

第六个 test() 函数用数组指针来接收二维数组是可以的,因为数组名是首元素地址就是第一行的地址,相当于是一个一维数组的地址,而数组指针也是一个一级指针,可以用来接收数组的地址,并且每一行有5个元素,数组指针也是接收 5 个元素

第七个 test() 函数用二级指针来接收二维数组不行,因为数组名是首元素地址是第一行的地址,而二级指针是需要接收地址的地址,匹配不上

4.3 一级指针传参
#include <stdio.h>
void print(int *p, int sz)
{for(int 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]);print(p, sz); //一级指针 p,传给函数return 0;
}
//思考: 当一个函数参数部分为一级指针时, 函数能接收什么参数 ?
void test1(int *p) {} //test1 函数能接收什么参数
void test2(char* p) {} //test2 函数能接收什么参数//地址传参用一级指针
//数组名, 指针, 地址
4.4 二级指针传参
#include <stdio.h>
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;
}
//思考: 当函数的参数为二级指针时, 可以接收什么参数 ?
void test(char **p) {}int main()
{char c = 'b';char*pc = &c;char**ppc = &pc;char* arr[10];test(&pc);test(ppc);test(arr); //√return 0;
}//地址的地址传参用二级指针

总结:指针传参只需要判断是地址还是地址的地址,只要是地址就可以用一级指针接收,是地址的地址就可以用二级指针来接收。相反用一级指针来接收就需要传地址,二级指针来接收就需要传地址的地址

五 函数指针

顾名思义就是一个指针指向函数

#include <stdio.h>
void test() {}int main()
{printf("%p\n", test);  // 00007FF7AFAB1159printf("%p\n", &test); // 00007FF7AFAB1159return 0;
}

上述是 test 函数的地址, 那如何保存函数地址 ?

// pfun1 和 pfun2 哪个可以存放 test 函数的地址
void (*pfun1)();
void *pfun2();// pfun1 可以, pfun1 先和 * 结合,说明 pfun1 是指针, 指针指向的是一个函数,指向的函数无参数,返回值类型为 void
函数指针经典代码

C 陷阱和缺陷中提及该代码,解释下述两行代码

(*(void (*)())0)();  // 代码一
void ( *signal(int , void(*)(int)) )(int);  //代码二

代码一

void (*)() 中的 * 表明该类型为函数指针 (它指向的函数参数为空,返回值为空),(void (*)()) 0 表示把 0 地址强转为上述的函数指针类型,*(void (*)())0 表示对函数指针解引用 (使用函数指针的方式调用函数 (*ptr)() )

综上上述代码意思为,把 0 地址强转为指向参数为空,返回值为空类型的函数指针,并进行函数调用

代码二

void(*)(int) 和上述一样,是一个函数指针类型 (它指针的函数参数类型为 int,返回值为空 ),signal(int , void(*)(int)) 表明 signal 是一个函数,参数一类型是 int,参数二类型是函数指针,void ( *signal(int , void(*)(int)) )(int); 表示 signal 的返回类型为 void(*)(int)

综上上述代码意思为,signal 是一个函数,参数一类型为 int,参数二类型是函数指针且类型为 void(*)(int),返回值的类型为 void(*)(int)

代码二如何简化

typedef void(*func_ptr)(int);
func_ptr signal(int, func_ptr);

六 函数指针数组

数组是一个存放相同类型数据的存储空间,指针数组比如 int *arr[10]; /数组的每个元素是 int*。把一组函数地址存到一个数组中,这个数组就叫函数指针数组

函数指针数组的定义,例如 int (*parr[10])();

parr先和 [] 结合,说明 parr 是数组,数组的内容是 int (*)() 类型的函数指针

函数指针数组的用途:转移表

#include <stdio.h>
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div(int a, int b) { return a / b; }int main()
{int x, y;int input = 1;int ret = 0;do{printf( "*************************\n" );printf( " 1:add 2:sub \n" );printf( " 3:mul 4:div \n" );printf( "*************************\n" );printf( "请选择:" );scanf( "%d", &input);switch (input){case 1:printf( "输入操作数:" );scanf( "%d %d", &x, &y);ret = add(x, y);printf( "ret = %d\n", ret);break;case 2:printf( "输入操作数:" );scanf( "%d %d", &x, &y);ret = sub(x, y);printf( "ret = %d\n", ret);break;case 3:printf( "输入操作数:" );scanf( "%d %d", &x, &y);ret = mul(x, y);printf( "ret = %d\n", ret);break;case 4:printf( "输入操作数:" );scanf( "%d %d", &x, &y);ret = div(x, y);printf( "ret = %d\n", ret);break;case 0:printf("退出程序\n");breark;default:printf( "选择错误\n" );break;}} while (input);return 0;
}

将上述计算器例子用函数指针数组改写

#include <stdio.h>
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div(int a, int b) { return a / b; }int main()
{int x, y;int input = 1;int ret = 0;do{printf( "*************************\n" );printf( " 1:add 2:sub \n" );printf( " 3:mul 4:div \n" );printf( "*************************\n" );int(*arr[5])(int x, int y) = {0, add, sub, mul, div}; //转移表printf( "请选择:" );scanf( "%d", &input);if (input >= 1 && input <= 4){printf( "输入操作数:" );scanf( "%d %d", &x, &y);ret = (*arr[input])(x,y);}elseprintf( "输入有误\n" );printf( "ret = %d\n", ret);} while (input);return 0;
}

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

指向函数指针数组的指针,是一个 指针。它指向一个数组,数组的元素都是函数指针

void test(const char* str)
{printf("%s\n", str);
}int main()
{//函数指针 pfunvoid (*pfun)(const char*) = test;//函数指针的数组 pfunArrvoid (*pfunArr[5])(const char* str);pfunArr[0] = test;//指向函数指针数组 pfunArr 的指针 ppfunArrvoid (*(*ppfunArr)[5])(const char*) = &pfunArr;return 0;
}//在函数指针的数组的类型最里面加上 (*) 即可表明是指向它的指针类型

八 回调函数

回调函数就是一个通过函数指针调用的函数。把函数指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,就称为回调

回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应

演示一下 qsort 函数的使用

#include <stdio.h>//qosrt 函数的使用者得实现一个比较函数
int int_cmp(const void * p1, const void * p2)
{return (*( int *)p1 - *(int *) p2);
}
int main()
{int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };int i = 0;qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)printf( "%d ", arr[i]);return 0;
}

使用回调函数,模拟实现 qsort(采用冒泡的方式)

#include <stdio.h>
int int_cmp(const void * p1, const void * p2)
{return (*( int *)p1 - *(int *) p2);
}void _swap(void *p1, void * p2, int size)
{for (int i = 0; i< size; i++) //循环 swap 的内存空间大小{char tmp = *((char *)p1 + i);*(( char *)p1 + i) = *((char *) p2 + i);*(( char *)p2 + i) = tmp;}
}void bubble(void *base, int count , int size, int(*cmp )(void *, void *))
{for (int i = 0; i< count - 1; i++){for (int j = 0; j< count - i - 1; j++){if (cmp ((char *) base + j * size , (char *)base + (j + 1) * size) > 0) //转成 char * 指针, 进行指针偏移{_swap(( char *)base + j * size, (char *)base + (j + 1) * size, size);}}}
}int main()
{int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };//char *arr[] = {"aaaa","dddd","cccc","bbbb"};int i = 0;bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)printf( "%d ", arr[i]);return 0;
}

九 指针和数组练习题

通常情况下数组名就是数组首元素的地址,但是有两个特殊情况

1、sizeof(数组名) —— 表示整个数组,计算的是整个数组的大小,单位是字

2、& + 数组名 —— 数组名表示整个数组,取出的是整个数组的地址

数组练习题

一维数组

int a[] = {1,2,3,4};
printf("%d\n",sizeof(a));		
//数组有 4 个元素,每个元素的类型都是 int, int 类型占 4 个字节, 所以打印出来的值是 16printf("%d\n",sizeof(a+0));
//(数组名 + 0) 并不是单独的数组名, 那么此时表示的就不是整个数组,(数组名 + 0) 表示的就是首元素的地址, 所以打印出来的值是 8 (64 位下)printf("%d\n",sizeof(*a));		
//不符合两种特殊情况, 所以 a 是首元素地址, *a就是首元素. 因为元素类型是 int, 所以打印值是 4printf("%d\n",sizeof(a+1));
//sizeof(a+1) 中的 a 是数组首元素地址, 因为 + 1 跳过 1 个整型. 所以a + 1 是第二个元素的地址, 所以打印值是 8 (64位下)printf("%d\n",sizeof(a[1]));	
//a[1] 表示第二个元素, 所以它的大小就是 4 个字节printf("%d\n",sizeof(&a));		
//sizeof(&a) 中 &a 是数组的地址, 数组的地址也是一个地址, 只要是地址它的大小就是 8 个字节 (64 位下)
//数组的地址和首元素的地址只在类型上存在差别, 数组的地址为 int(*)[4],首元素地址的类型为 int *, 类型的差异仅仅决定了 +- 操作跳过几个地址. 因为它们都是指针,所以 sizeof 的值都是一样的printf("%d\n",sizeof(*&a));
//* 和 & 符号相互抵消, 该代码等价于 sizeof(a), 所以打印值是 16printf("%d\n", sizeof(&a + 1));
//&a + 1 跳过了整个数组, 指向了数值最后一个元素的下一个元素. 因为指针只是指向了最后一个元素的下一个元素, 并没有解引用, 所以不存在指针的越界访问. &a + 1 表示的仍然是一个指针, 打印值是 8(64 位下)printf("%d\n",sizeof(&a[0]));
//&a[0] 表示取出数组首元素地址, 所以打印值应该是 8(64位)printf("%d\n",sizeof(&a[0]+1));
//&a[0] + 1 表示数组第二个元素地址, 所以值是 8(64位)

字符数组

char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr));
//数组名单独放到 sizeof 内部, 计算整个数组的大小 单位字节, 所以打印值应该是 6
printf("%d\n", sizeof(arr+0));
//arr+0 不满足两种特殊情况, arr 表示首元素地址, 所以打印出来的值应该是 8 (64位)
printf("%d\n", sizeof(*arr));
//*arr 表示对数组首元素地址进行解引用, 所以表示的是首元素, 大小为 1 个字节
printf("%d\n", sizeof(arr[1]));
//arr[1] 是第二个元素, 大小是 1 个字节
printf("%d\n", sizeof(&arr));
//&arr 表示整个数组的地址, 数组的地址也是一个地址, 所以打印值是 8
printf("%d\n", sizeof(&arr+1));
//&arr + 1 表示跳过整个数组, 指向数组最后一个元素下一个元素的地址, 所以打印值是 8
printf("%d\n", sizeof(&arr[0]+1));
//&arr[0]+1 表示数组第二个元素地址, 所以打印值是 8printf("%d\n", strlen(arr));
//arr 是首元素地址, strlen 在求长度时只有遇到 \0 才会结束. 原数组中没有 \0 , 所以打印值是一个随机值
printf("%d\n", strlen(arr+0));
//arr+0 表示数组首元素地址, 原数组中没有 \0 , 所以打印值是一个随机值
printf("%d\n", strlen(*arr));
//*arr 表示数组首元素 'a', 因 strlen 必须传入地址(入参是指针类型) , 字符 a ASCII 码值是 97. 此时 strlen 函数会把 97 当作一个地址, 那么在打印时会存在多种情况 : 1. 打印随机值 2. 访问到不允许访问的数据, 程序崩溃
printf("%d\n", strlen(arr[1]));
//arr[1] 表示第二个元素 'b', 其余同上
printf("%d\n", strlen(&arr));
//&arr 表示取出整个数组的地址, 数组的地址也是从数组的第一个元素的地址开始的, 此时打印值是一个随机值
printf("%d\n", strlen(&arr+1));
//&arr + 1 指向数组最后一个元素下一个元素的地址, 得到一个随机值, 但与第一行打印中的随机值存在差异 (随机值少 6, 因为跳过了 6 个元素)
printf("%d\n", strlen(&arr[0]+1));
//&arr[0] + 1 表示数组第二个元素地址, 从 b 开始向后统计的, 打印值是一个随机值 (比第一行打印的随机值小 1)char arr[] = "abcdef";
printf("%d\n", sizeof(arr));
//arr 取出的是数组中所有的元素, 所以打印值是 7
printf("%d\n", sizeof(arr+0));
//arr + 0 中 arr 表示数组首元素, arr + 0 表示数组首元素地址, 所以打印值是 8
printf("%d\n", sizeof(*arr));
//*arr 表示数组首元素, 大小是一个字节
printf("%d\n", sizeof(arr[1]));
//arr[1] 表示第二个元素, 大小是一个字节
printf("%d\n", sizeof(&arr));
//&arr 表示取出的是数组的地址, 是地址就是 8 个字节
printf("%d\n", sizeof(&arr+1));
//&arr + 1 表示跳过了整个数组, 它还是一个地址, 是地址就是 8 个字节
printf("%d\n", sizeof(&arr[0]+1));
//&arr[0] + 1 表示的是第二个元素的地址, 大小是 8 个字节
printf("%d\n", strlen(arr));
//取出的是数组首元素地址, 所以打印值是 6
printf("%d\n", strlen(arr+0));
//取出的是数组首元素地址, 所以打印值是 6
printf("%d\n", strlen(*arr));
//取出的是首元素 'a', a 的 ASCII码值是 97,此时 strlen 会把 97 当作一个地址, 所以打印值是一个随机值或报错
printf("%d\n", strlen(arr[1]));
//取出的是首元素 'b', b 的 ASCII码值是 98,此时 strlen 会把 98 当作一个地址, 所以打印值是一个随机值或报错
printf("%d\n", strlen(&arr));
//取出来的是数组的地址, 数组的地址也是首元素的地址, 所以打印值是 6
printf("%d\n", strlen(&arr+1));
// +1 跳过了整个数组, 此时指向的是数组最后一个元素下一个元素的地址, 所以打印值是一个随机值
printf("%d\n", strlen(&arr[0]+1));
//第二个元素的地址, 所以打印值是 5char *p = "abcdef";
printf("%d\n", sizeof(p));
//p 是一个指针变量, 是首元素地址. 计算的就是一个指针变量的大小, 所以打印值是 8
printf("%d\n", sizeof(p+1));
//是第二个元素的地址, 所以打印值是 8
printf("%d\n", sizeof(*p));
//p 类型是 char*, *p 是 char 类型, 所以打印值是 1
printf("%d\n", sizeof(p[0]));
//表示首元素 'a', 打印值是 1
printf("%d\n", sizeof(&p));
//表示一个二级指针变量, 所以打印值是 8
printf("%d\n", sizeof(&p+1));
//表示一个二级指针变量, &p + 1 表示跳过 p 指针变量后的地址, 所以打印值是 8
printf("%d\n", sizeof(&p[0]+1));
//表示数组第二个元素地址, 所以打印值是 8
printf("%d\n", strlen(p));
//取出的是首元素地址, 所以打印值是 6
printf("%d\n", strlen(p+1));
//取出的是第二个元素地址, 所以打印值是 5
printf("%d\n", strlen(*p));
//*p 取出的是第一个元素, a 的 ASCII 码值为 97, strlen 会把 97 当作一个地址, 所以打印值是一个随机值或报错
printf("%d\n", strlen(p[0]));
//*p 取出的是第一个元素, a 的 ASCII 码值为 97, strlen 会把 97 当作一个地址, 所以打印值是一个随机值或报错
printf("%d\n", strlen(&p));
//表示一个二级指针变量, 所以打印值是一个随机值
printf("%d\n", strlen(&p+1));
//表示一个二级指针变量,所以打印值是一个随机值
printf("%d\n", strlen(&p[0]+1));
//表示第二个元素地址, 所以打印值是 5

二维数组

int a[3][4] = {0};
printf("%d\n",sizeof(a));
//二维数组的数组名表示数组的大小, 打印值是 48
printf("%d\n",sizeof(a[0][0]));
//表示第一行的第一个元素, 所以打印值是 4
printf("%d\n",sizeof(a[0]));
//表示第一行所有元素, 所以打印出值是 16 (a[0] 是第一行的数组名, 数组名单独放到 sizeof 内部, 计算是第一行数组的总大小)
printf("%d\n",sizeof(a[0]+1));
//数组名没有单独放到 sizeof 内部, 所以数组名 a[0] 就是数组首元素 a[0][0] 的地址, +1 后是 a[0][1] 的地址, 所以打印值是 8
printf("%d\n",sizeof(*(a[0]+1)));
//表示第一行的第二个元素, 所以打印值是 4
printf("%d\n",sizeof(a+1));
//数组名没有单独放到 sizeof 的内部, a 表示数组首元素地址,是二维数组的首元素的地址, 也就是第一行的地址(&a[0]). +1 跳过一行就是第二行的地址, 是一个数组指针变量, 所以打印值是 8
printf("%d\n",sizeof(*(a+1)));
//表示 a[1] 第二行所有元素, 所以打印值是 16
printf("%d\n",sizeof(&a[0]+1));
//a[0] 是第一行数组名, &a[0] 取出的就是数组的地址, 第一行的地址. 所以+ 1 就是第二行的地址, 所以打印值是 8
printf("%d\n",sizeof(*(&a[0]+1)));
//表示 a[1] 第二行所有元素, 所以打印值是 16
printf("%d\n",sizeof(*a));
//a 作为数组名没有单独放到 sizeof 内部, a 表示数组首元素地址, 是二维数组首元素的地址, 也就是第一行的地址, *a 就是第一行所有元素, 所以打印值是 16
printf("%d\n",sizeof(a[3]));
//a[3] 表示第四行数组名, 因为 sizeof 并不会计算, 也没有访问. 所以不存在越界访问, 所以打印值是 16. a[3]无需真实存在, 仅仅是通过类型的推断算出的长度

总结: 数组名的意义

1、sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。

2、&数组名,这里的数组名表示整个数组,取出的是整个数组的地址。

3、除此之外所有的数组名都表示首元素的地址

指针练习题

以下程序的结果是什么

int main()
{int a[5] = { 1, 2, 3, 4, 5 };int *ptr = (int *)(&a + 1); //&a 表示的是数组的地址printf( "%d,%d", *(a + 1), *(ptr - 1)); //2 5return 0;
}

*(a+1) 输出 2 ,a 数组名为数组首元素地址,再加一解引用就是数组第二位元素

*(ptr - 1) 输出5,&a 是取整个数组的地址,然后加一跳过了数组 a,ptr 指向数组元素 5 的后面,再强制转换成 int*(本来类型应该为 int(*)[5] ) ,然后 ptr - 1 解引用就指向数组元素 5

struct Test
{int Num;	  //4char *pcName; //4short sDate;  //2char cha[2];  //2short sBa[4]; //8
}*p;
//假设 p 的值为 0x100000, 如下表表达式的值分别为多少 ?
//已知结构体 Test 类型的变量大小是 20 个字节
int main()
{printf("%p\n", p + 0x1);  // 20 -> 2 进制 1100 -> 16 进制 = 14//0x100014printf("%p\n", (unsigned long)p + 0x1);//0x100001printf("%p\n", (unsigned int*)p + 0x1);//0x100004return 0;
}

考察 指针 ± 整数,结构体指针 +1 会跳过一个结构体,所以 +1 就会跳过20 个字节。 p + 0x1 表示 0x100000 + 20 = 0x100014,因为打印的值是一个地址,所以要补满8位,所以打印出来的值应该是 00100014

(unsigned long) p + 0x1 中,p 被强制类型转换为 unsigned long 类型,此时 p 就不是一个指针变量了,所以此时整型值 +1 就是 +1 本身,所以打印出来的值应该是 00100001

(unsigned int*)p + 0x1 中,p 被强制类型转换为 unsigned int* 类型,所以+1 就会跳过 4 个字节,所以 p + 0x1 就表示 0x100000 + 4 = 0x100004,因为打印的值是一个地址要补满 8 位,所以打印值是 00100004

image-20241210231316306

int main()
{int a[4] = { 1, 2, 3, 4 };int *ptr1 = (int *)(&a + 1); //&a 整个数组的地址int *ptr2 = (int *)((int)a + 1); //数组首元素地址值转整型+1 printf( "%x,%x", ptr1[-1], *ptr2); //%x 用于格式化的输出符号, 以十六进制形式输出整数//4, 2000000(显示数据时也需要倒着读)return 0;
}

数组首元素地址值转整型 + 1 ,在小端机器上,以十六进制显示

01 00 00 00
02 00 00 00

*ptr2 的内容就是 00 00 00 02 这部分,显示数据时也需要倒着读,即为 2000000

#include <stdio.h>
int main()
{int a[3][2] = { (0, 1), (2, 3), (4, 5) };int *p;p = a[0]; //a[0] 是第一个行的数组名, 数组名表示首元素的地址, 即 a[0][0] 的地址printf( "%d", p[0]); //*(p+0) = *preturn 0;
}

注意第一行代码中的二维数组并不是如下形式

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

它是用括号连接起来的,表示的是一个逗号表达式,逗号表达式从左向右依次计算,最后一个计算的值就是表达式的取值,所以数组的真实情况应该如下:

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

因为数组是三行两列的,所以数据 1 3 放到第一行,数据 5 放到第二行第一列,其它的三个位置上放的都是0 。因为 a[0] 是第一行的数组名,数组名表示首元素的地址,其实就是 &a[0 ][0] 的地址, p[0] = *(p+0) = *p ,所以打印值是 1

int main()
{int a[5][5]; //二维数组, 5 行 5 列int(*p)[4];p = a; //a 表示 &a[0] , 类型为 int(*)[5]printf( "%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]); // &p[4][2] = p + 4 后再找第三个元素return 0;
}

首先创建了一个 5 行 5 列的数组,再创建了一个数组指针变量,p 指向的是 4 个整型元素的地址

接着进行了 &p[4][2] - &a[4][2] , &p[4][2] - &a[4][2] 两个操作,指针-指针得到的是两个指针之间的元素的个数

分析一下:a 的类型是 int(*)[5],p 的类型是 int(*)[4]

当把 a 赋予 p 时,两者的首地址都是指向 a 数组中的第一行的第一个元素,两者会有类型的差异,所以 ± 整数二者跳过的字节数不同

a 每次 +1 跳过的是 5 个整型,而 p 每次 +1 跳过的是 4 个整型,我们画图分析如下

image-20241210234524099

由图可知,两个指针相减得到的值是 -4 ,所以 %d 打印出来的值是 -4 ;

而 %p 是打印地址, -4 在内存中是以补码的形式存放的,

-4 的原码为:10000000000000000000000000000100

-4 的补码是:111111111111111111111111111111111100

所以 %p 此时就把 -4 的补码当作一个地址打印出来,把它的值换算成 16 进制,得到的是:FFFFFFFC

所以打印值是 FFFFFFFC 和 -4

int main()
{int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };int *ptr1 = (int *)(&aa + 1); //&aa + 1 整个数组int *ptr2 = (int *)(*(aa + 1)); //a[1][0] = 6printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1));return 0;
}

image-20241211000013898

*(aa+1) 等价于 aa[1],aa[1] 是第二行的数组名,因为数组名表示的是首元素的地址,所以 aa[1] == &aa[1][0]

由图可知,打印值分别是 10 和 5

#include <stdio.h>
int main()
{char *a[] = {"work","at","alibaba"};char**pa = a;pa++; //pa[1]printf("%s\n", *pa); return 0;
}

a 是一个字符指针数组,数组里面一共有三个元素,数组的每个元素都是 char* 类型。因为二级指针变量 pa 被赋予了 a,a 是一个数组名,表示的就是首元素的地址, pa++ 即为 pa[1] 因此,打印结果是 at

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

题目比较复杂,试着画图像来分析

image-20241211001444818

因为指针优先级的问题 ++cpp 会率先进行,此时 cpp 的指向就会改变

image-20241211001514873

此时*++cpp 拿到的值就是c+2,所以**++cpp 表示的值就是*(c+2),此时打印出来的值就是 POINT

第二个代码:* -- * ++ cpp + 3

这个代码中,+的优先级是最低的,所以应该先计算 ++cpp

因为上一个代码已经进行了 ++cpp 的操作指向了c+2,此时在进行 ++ 操作指向的应该是 c+1
image-20241211001644991

再依照优先级顺序进行解引用操作,此时拿到的是c+1,原代码就可以转化为*--(c+1)+3

接下来应该要执行 – 操作,因为 – 的对象是 c+1,所以在执行完–操作以后,c+1的值会变成 c

image-20241211001717639

现在的原代码相当于 *c+3,所以*c+3打印出来的值应该是 ER

第三个代码:* cpp [-2] + 3

先把代码转换一下:* *(cpp-2) + 3

应该先算(cpp-2),此时应该拿到的是 c+3,再对 (c+3) 解引用拿到的是 FIRST 处的地址,再进行 +3 操作,所以打印出来的值应该是 ST

第四个代码:cpp[-1][-1] + 1

再来把代码转换一下:*(*(cpp - 1) - 1) + 1

此时的逻辑和之前的代码一模一样,所以打印值是 EW

相关文章:

C 进阶 — 指针的使用

C 进阶 — 指针的使用 主要内容 1、字符指针 2、数组指针 3、指针数组 4、数组传参和指针传参 5、函数指针 6、函数指针数组 7、指向函数指针数组的指针 8、 回调函数 9、指针和数组练习题 前节回顾 1、指针就是个变量&#xff0c;用来存放地址&#xff0c;地址唯一…...

【经验分享】容器云运维的知识点

最近忙于备考没关注&#xff0c;有次点进某小黄鱼发现首页出现了我的笔记还被人收费了 虽然我也卖了一些资源&#xff0c;但我以交流、交换为主&#xff0c;笔记都是免费给别人看的 由于当时刚刚接触写的并不成熟&#xff0c;为了避免更多人花没必要的钱&#xff0c;所以决定公…...

MFC学习笔记专栏开篇语

MFC&#xff0c;是一个英文简写&#xff0c;全称为 Microsoft Foundation Class Library&#xff0c;中文翻译为微软基础类库。它是微软开发的一套C类库&#xff0c;是面向对象的函数库。 微软开发它&#xff0c;是为了给程序员提供方便&#xff0c;减少程序员的工作量。如果没…...

电子科技大学《高级算法设计与分析》期末复习问题汇总(客观题-选择题、判断题)

电子科技大学《高级算法设计与分析》问题汇总_已知背包问题的动态规划算法时间复杂度为o(nw),其中n为物品数目,w为背包容量。请-CSDN博客 转载自上面这个链接&#xff0c;古希腊掌管成电专业课的神&#xff01;&#xff01;为了防止他的链接失效&#xff0c;自己也转存一份 &…...

GPTcelltype——scRNA-seq注释

#安装包 install.packages("openai") remotes::install_github("Winnie09/GPTCelltype") #填写API Sys.setenv(OPENAI_API_KEY your_openai_API_key) #加载包 #Load packages library(GPTCelltype) library(openai) #准备文件 #Assume you have already r…...

AI与大数据的深度结合:驱动决策的革命性力量

引言&#xff1a;数字时代的决策挑战 在这个信息爆炸的数字时代&#xff0c;数据早已渗透到我们生活的方方面面。全球每天产生的数据量呈指数级增长&#xff0c;无论是用户的消费行为、设备的运行状态&#xff0c;还是社会热点的实时动态&#xff0c;这些信息的规模和复杂性前所…...

Java多线程与线程池技术详解(九)

面对苦难的态度&#xff1a;《病隙碎笔》“不断的苦难才是不断地需要信心的原因&#xff0c;这是信心的原则&#xff0c;不可稍有更动。” 孤独与心灵的成长&#xff1a;《我与地坛》“孤独的心必是充盈的心&#xff0c;充盈得要流溢出来要冲涌出去&#xff0c;便渴望有人呼应他…...

【常考前端面试题总结】---2025

React fiber架构 1.为什么会出现 React fiber 架构? React 15 Stack Reconciler 是通过递归更新子组件 。由于递归执行&#xff0c;所以更新一旦开始&#xff0c;中途就无法中断。当层级很深时&#xff0c;递归更新时间超过了 16ms&#xff0c;用户交互就会卡顿。对于特别庞…...

什么是大语言模型(LLM)

1. 什么是大语言模型&#xff08;LLM&#xff09;&#xff1f; LLM 是一种基础模型&#xff08;Foundation Model&#xff09;的实例。 基础模型的特点&#xff1a; 使用大量未标注的自监督数据进行预训练。通过学习数据中的模式&#xff0c;生成具有普适性和可适应性的输出…...

柚坛工具箱Uotan Toolbox适配鸿蒙,刷机体验再升级

想要探索智能设备的无限可能&#xff1f;Uotan Toolbox&#xff08;柚坛工具箱&#xff09;将是您的得力助手。这款采用C#语言打造的创新型开源工具箱&#xff0c;以其独特的设计理念和全面的功能支持&#xff0c;正在改变着用户与移动设备互动的方式。 作为一款面向专业用户的…...

supervisor使用详解

参考文章&#xff1a; Supervisor使用详解 Supervisor 是一个用 Python 编写的客户端/服务器系统&#xff0c;它允许用户在类 UNIX 操作系统&#xff08;如 Linux&#xff09;上监控和控制进程。Supervisor 并不是一个分布式调度框架&#xff0c;而是一个进程管理工具&#x…...

win11电源设置在哪里?控制面板在哪里?如何关闭快速启动?

不知道微软咋想的&#xff0c;从win10&#xff08;win8&#xff09;开始搞事情&#xff0c;想把windows娱乐化。 娱乐化的特点就是只照顾傻子不考虑专家&#xff0c;系统设置统统藏起来&#xff0c;开机即用——也只能那么用。 搞两套界面做不到吗&#xff1f; win11非常头疼的…...

【论文阅读笔记】One Diffusion to Generate Them All

One Diffusion to Generate Them All 介绍理解 引言二、相关工作三、方法预备知识训练推理实现细节训练细节 数据集构建实验分结论附录 介绍 Paper&#xff1a;https://arxiv.org/abs/2411.16318 Code&#xff1a;https://github.com/lehduong/onediffusion Authors&#xff1…...

SpringCloud和Nacos的基础知识和使用

1.什么是SpringCloud ​ 什么是微服务&#xff1f; ​ 假如我们需要搭建一个网上购物系统&#xff0c;那么我们需要哪些功能呢&#xff1f;商品中心、订单中心和客户中心等。 ​ 当业务功能较少时&#xff0c;我们可以把这些功能塞到一个SpringBoot项目中来进行管理。但是随…...

人工智能技术的深度解析与推广【人工智能的应用场景】

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c; 忍不住分享一下给大家。点击跳转到网站 学习总结 1、掌握 JAVA入门到进阶知识(持续写作中……&#xff09; 2、学会Oracle数据库入门到入土用法(创作中……&#xff09; 3、手把…...

md5sum -c

md5sum -c xxx 命令用于验证文件的 MD5 校验和是否匹配。具体来说&#xff0c;-c 选项告诉 md5sum 命令去读取指定文件&#xff08;通常是一个包含 MD5 校验和的文件&#xff09;&#xff0c;并与实际文件的 MD5 校验和进行比较。 工作原理&#xff1a; 生成校验和文件&#x…...

excel使用笔记

1.工作表1计算工作表2某列的和 假设我们有两个工作表&#xff0c;分别命名为“Sheet1”和“Sheet2”&#xff0c;我们想要求和这两个工作表中A1到A**单元格的数据&#xff0c;可以在任意一个工作表的单元格中输入以下公式&#xff1a; SUM(Sheet1!A1:A10, Sheet2!A1:A10) SUM…...

keepalived+nginx实现web高可用

目录 高可用集群搭建 Keepalived&#xff0b;nginx实现web高可用 一.节点规划 二.基础准备 1.修改主机名 2.关闭防火墙和selinux服务 三.用keepalived配置高可用 1.安装nginx服务 2.修改nginx配置文件 3.启动nginx 4.访问nginx 5.安装keepalived服务 6.编辑配置文件…...

边界层气象:脉动量预报方程展开 | 湍流脉动速度方差预报方程 | 平均湍流动能收支方程推导

写成分量形式 原始式子&#xff1a; ∂ u i ′ ∂ t u ‾ j ∂ u i ′ ∂ x j u j ′ ∂ u ‾ i ∂ x j u j ′ ∂ u i ′ ∂ x j − 1 ρ ‾ ⋅ ∂ p ′ ∂ x i g θ v ′ θ ‾ v δ i 3 f ϵ i j 3 u j ′ v ∂ 2 u i ′ ∂ x j 2 ∂ ( u i ′ u j ′ ‾ ) ∂ x j…...

TOSUN同星TsMaster使用入门——2、使用TS发送报文,使用graphics分析数据等

在第一章里面已经介绍了关于同星工程的创建和最基础的总线分析&#xff0c;接下来看看怎么使用TS发送报文以及图形化分析数据。 目录 一、使用Graphics分析报文信号/变量&#xff08;对标CANoe Graphics&#xff09; 二、使用数值窗口统计信号值/变量 三、使用TS发送报文 3…...

docker详细操作--未完待续

docker介绍 docker官网: Docker&#xff1a;加速容器应用程序开发 harbor官网&#xff1a;Harbor - Harbor 中文 使用docker加速器: Docker镜像极速下载服务 - 毫秒镜像 是什么 Docker 是一种开源的容器化平台&#xff0c;用于将应用程序及其依赖项&#xff08;如库、运行时环…...

通过Wrangler CLI在worker中创建数据库和表

官方使用文档&#xff1a;Getting started Cloudflare D1 docs 创建数据库 在命令行中执行完成之后&#xff0c;会在本地和远程创建数据库&#xff1a; npx wranglerlatest d1 create prod-d1-tutorial 在cf中就可以看到数据库&#xff1a; 现在&#xff0c;您的Cloudfla…...

论文解读:交大港大上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(一)

宇树机器人多姿态起立控制强化学习框架论文解析 论文解读&#xff1a;交大&港大&上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架&#xff08;一&#xff09; 论文解读&#xff1a;交大&港大&上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化…...

Ascend NPU上适配Step-Audio模型

1 概述 1.1 简述 Step-Audio 是业界首个集语音理解与生成控制一体化的产品级开源实时语音对话系统&#xff0c;支持多语言对话&#xff08;如 中文&#xff0c;英文&#xff0c;日语&#xff09;&#xff0c;语音情感&#xff08;如 开心&#xff0c;悲伤&#xff09;&#x…...

爬虫基础学习day2

# 爬虫设计领域 工商&#xff1a;企查查、天眼查短视频&#xff1a;抖音、快手、西瓜 ---> 飞瓜电商&#xff1a;京东、淘宝、聚美优品、亚马逊 ---> 分析店铺经营决策标题、排名航空&#xff1a;抓取所有航空公司价格 ---> 去哪儿自媒体&#xff1a;采集自媒体数据进…...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

网站指纹识别

网站指纹识别 网站的最基本组成&#xff1a;服务器&#xff08;操作系统&#xff09;、中间件&#xff08;web容器&#xff09;、脚本语言、数据厍 为什么要了解这些&#xff1f;举个例子&#xff1a;发现了一个文件读取漏洞&#xff0c;我们需要读/etc/passwd&#xff0c;如…...

springboot整合VUE之在线教育管理系统简介

可以学习到的技能 学会常用技术栈的使用 独立开发项目 学会前端的开发流程 学会后端的开发流程 学会数据库的设计 学会前后端接口调用方式 学会多模块之间的关联 学会数据的处理 适用人群 在校学生&#xff0c;小白用户&#xff0c;想学习知识的 有点基础&#xff0c;想要通过项…...

Go 语言并发编程基础:无缓冲与有缓冲通道

在上一章节中&#xff0c;我们了解了 Channel 的基本用法。本章将重点分析 Go 中通道的两种类型 —— 无缓冲通道与有缓冲通道&#xff0c;它们在并发编程中各具特点和应用场景。 一、通道的基本分类 类型定义形式特点无缓冲通道make(chan T)发送和接收都必须准备好&#xff0…...

MySQL 知识小结(一)

一、my.cnf配置详解 我们知道安装MySQL有两种方式来安装咱们的MySQL数据库&#xff0c;分别是二进制安装编译数据库或者使用三方yum来进行安装,第三方yum的安装相对于二进制压缩包的安装更快捷&#xff0c;但是文件存放起来数据比较冗余&#xff0c;用二进制能够更好管理咱们M…...