10.【C语言学习笔记】指针(二)
目录
1. 数组名的理解(数组首元素地址还是整个数组)
2. 使用指针访问数组
3. 一维数组传参的本质
4. 二级指针(指针变量的指针,存放指针变量的地址)
5. 指针数组
6. 数组指针变量
6.1 数组指针变量的初始化
7. 二维数组传参的本质(传递的是第一行这个一维数组的地址)
8. 字符指针变量
8.1 字符数组和常量字符串的区别
8.2 常量字符串的存储
9. 函数指针变量
9.1 函数指针变量的创建
9.2 函数指针变量的使用
9.3 两个代码(难理解)
9.4 typedef 关键字(类型重命名)
10. 函数指针数组
11. 函数指针数组实现计算器(函数指针数组—转移表)
11.1 计算器一般实现
11.2 改进1—函数指针数组方式
11.3 改进2—函数指针作参数(回调函数)
1. 数组名的理解(数组首元素地址还是整个数组)
数组名就是数组首元素(第一个元素)的地址是对的,但是有两个例外:
• sizeof(数组名),sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节
• &数组名,这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址是有区别的:地址相同,但是类型不同)
除此之外,任何地方使用数组名,数组名都表示首元素的地址。
数组和指针的关系:
1.数组就是数组,是一款连续的空间(数组的大小和数组元素的个数和类型都有关系)
2. 指针(变量)就是指针(变量),是一个变量(4/8个字节
3. 数组名是地址,是首元素的地址
4. 可以使用指针来访问数组
2. 使用指针访问数组
#include <stdio.h>
int main()
{int arr[10] = { 0 };int i = 0;int sz = sizeof(arr) / sizeof(arr[0]);//输入int* p = arr;for (i = 0; i < sz; i++){scanf("%d", p + i);//scanf("%d", arr+i);//也可以这样写}//输出for (i = 0; i < sz; i++){//printf("%d ", *(p + i));printf("%d ", p[i]);}return 0;
}
将 *(p+i) 换成 p[i] 也是能够正常打印的,所以本质上 p[i] 是等价于 *(p+i) 。
同理 arr[i] 应该等价于 *(arr+i) ,数组元素的访问在编译器处理的时候,也是转换成首元素的地址+偏移量求出元素的地址,然后解引用来访问的。
即:arr[i] <=> *(arr+i) <=> p[i] <=> *(p+i)
其实:arr[i] <=> *(arr+i) <=> *(i+arr) <=> i[arr] (但是后面的方式不推荐使用)
[ ] 是下标引用操作符,是下标引用操作符:arr[i] => *(arr+i) == i[arr] => *(i+arr)
3. 一维数组传参的本质
#include <stdio.h>// 数组传参的时候,形参是可以写成数组的形式的
// 但是本质上还是指针变量
void Print(int arr[10]) // int* arr
{int sz2 = sizeof(arr) / sizeof(arr[0]); // 得不到元素的个数// 这里的arr已经单纯是数组首元素的地址了,sizeof(arr)就是指针的大小printf("sz2 = %d\n", sz2);
}int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int sz1 = sizeof(arr) / sizeof(arr[0]); printf("sz1 = %d\n", sz1);Print(arr);// 只有sizeof(arr),&arr 这两种情况才数组名才代表的是整个数组,所以此处给Print的参数只是数组首元素的地址!!!return 0;
}
我们发现在函数内部是没有正确获得数组的元素个数。
只有sizeof(arr),&arr 这两种情况才数组名才代表的是整个数组,所以此处给Print的参数给的数组名只是数组首元素的地址!!!那么在数组传参的时候,传递的是数组名,也就是说数组传参本质上传递的是数组首元素的地址。
形参的数组是不会单独再创建数组空间的,所以形参的数组是可以省略掉数组的大小的。(即形参可以写成:int arr[ ] )
所以函数形参的部分理论上应该使用指针变量来接收首元素的地址。那么在函数内部我们写sizeof(arr) 计算的是一个地址的大小(单位字节)而不是数组的大小(单位字节)。正是因为函数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的。(下面代码为:指针变量来接收首元素的地址)
#include <stdio.h>// 数组传参的时候,形参是可以写成数组的形式的
// 但是本质上还是指针变量
//void Print(int arr[10]) // int* arr
//{
// int sz2 = sizeof(arr) / sizeof(arr[0]); // 得不到元素的个数
// printf("sz2 = %d\n", sz2);
//}void Print(int* p, int sz)
{for (int i = 0; i < sz; i++){printf("%d ", *(p + i));}
}
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int sz1 = sizeof(arr) / sizeof(arr[0]); printf("sz1 = %d\n", sz1);Print(arr);return 0;
}
总结:一维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。
4. 二级指针(指针变量的指针,存放指针变量的地址)
#include <stdio.h>int main()
{int a = 10;int* p = &a; // 一级指针int** pp = &p; // 二级指针// int* 说明pp指向的p的类型是int*,*说明pp是指针变量printf("%p\n", *pp); // pp解一层引用是p,即&aprintf("%p\n", p);printf("%p\n", &a);printf("%d\n", **pp); // pp解两层引用是*p,即aprintf("%d\n", a);return 0;
}
5. 指针数组
整型数组,是存放整型的数组,字符数组是存放字符的数组。那指针数组则是存放指针的数组。
指针数组的每个元素都是用来存放地址(指针)的。指针数组的每个元素是地址,又可以指向一块区域。
6. 数组指针变量
前面讲的 指针数组 是一种数组,数组中存放的是地址(指针)。
数组指针变量是指针变量。存放的是数组的地址,能够指向数组的指针变量。
int *p1[10]; // 指针数组,p1[10]是数组,int*是数组类型
int (*p2)[10]; // 数组指针,int (*)[10] 是指针类型
p先和*结合,说明p是一个指针变量变量,然后指着指向的是一个大小为10个整型的数组。
所以p是一个指针,指向一个数组,叫 数组指针。
注意:[ ]的优先级要高于*号的,所以必须加上()来保证p先和*结合。
6.1 数组指针变量的初始化
#include<stdio.h>
int main()
{int arr[10] = { 0 };int(*p)[10] = &arr; //&arr得到的就是数组的地址return 0;
}
int (*p) [10] = &arr;
| | |
| | |
| | p指向数组的元素个数
| p是数组指针变量名
p指向的数组的元素类型
7. 二维数组传参的本质(传递的是第一行这个一维数组的地址)
二维数组起始可以看做是每个元素是一维数组的数组。那么二维数组的首元素就是第一行,是个一维数组。
根据数组名是数组首元素的地址这个规则,二维数组的数组名表示的就是第一行的地址,是一维数组的地址。根据上面的例子,第一行的一维数组的类型就是int [5] ,所以第一行的地址的类型就是数组指针类型int(*)[5] 。那就意味着二维数组传参本质上是:传递的是第一行这个一维数组的地址,那么形参也是可以写成指针形式的。如下:int(*arr)[5]
总结:二维数组传参,形参的部分可以写成数组,也可以写成指针形式。
#include<stdio.h>//void Print(int a[3][5], int r, int c)
void Print(int(*arr)[5], int r, int c)
{int i = 0;for (i = 0; i < r; i++){int j = 0;for (j = 0; j < c; 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} };// 数组名是数组首元素的地址// 1.&数组名// 2.sizeof(数组名)Print(arr, 3, 5);return 0;
}
8. 字符指针变量
8.1 字符数组和常量字符串的区别
常量字符串不能修改!!!
#include <stdio.h>int main()
{char ch = 'w';char* pc = &ch;char arr[10] = "abcdef";char* p1 = arr; // p1指向的是字符数组的首元素,可以修改*p1 = 'w'; // 可以修改//char* p2 = "abcdef"; const char* p2 = "abcdef";// p2指向常量字符串的首元素,所以不能修改,最好用const修饰//*p2 = 'w';// 不能修改printf("%s\n", p1); // 都可以用%s打印printf("%s\n", p2);return 0;
}
8.2 常量字符串的存储
#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)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相同。
9. 函数指针变量
9.1 函数指针变量的创建
函数指针变量是用来存放函数地址的,未来通过地址能够调用函数的。
函数名就是函数的地址,也可以通过&函数名的方式获得函数的地址。
注意:这里和数组的区别,数组名是数组首元素地址,&数组名是整个数组的地址,两个地址相同,但是类型不同!!!而函数名和&函数名完全相同
#include <stdio.h>
void test()
{printf("hehe\n");
}
int main()
{printf("test: %p\n", test);printf("&test: %p\n", &test);return 0;
}
// 输出结果
// test: 005913CA
// &test: 005913CA
函数指针变量的写法其实和数组指针非常类似。如下:
void test()
{printf("hehe\n");
}
void (*pf1)() = &test;
void (*pf2)() = test;int Add(int x, int y)
{return x + y;
}
int(*pf3)(int, int) = Add;
int(*pf3)(int x, int y) = &Add;//x和y写上或者省略都是可以的
函数指针类型解析:
int (*pf3) (int x, int y)
| | ------------
| | |
| | pf3指向函数的参数类型和个数的交代
| 函数指针变量名
pf3指向函数的返回类型int (*) (int x, int y) // pf3函数指针变量的类型
int (*) (int, int) // x和y写上或者省略都是可以的
9.2 函数指针变量的使用
通过函数指针调用指针指向的函数
#include <stdio.h>
int Add(int x, int y)
{return x + y;
}
int main()
{int(*pf3)(int, int) = Add;printf("%d\n", Add(2, 3));printf("%d\n", (*pf3)(2, 3));printf("%d\n", (&Add)(2, 3));printf("%d\n", pf3(2, 3));return 0;
}
9.3 两个代码(难理解)
#include <stdio.h>
int main()
{( * ( void (*)() ) 0 )();// void (*)() -- 函数指针类型// ( void (*)() ) -- 强制类型转换// ( void (*)()) ) 0 -- 将0强制类型转换位void (*)()的函数指针类型// 这就意味着我们假设0 地址处存放着 无参,返回类型是void的函数// 最终调用0地址处存放的这个函数return 0;
}
// 函数声明
void (* signal(int, void(*)(int)))(int);
//void (*)(int) signal(int, void(*)(int)); //err
//上一行写法错误,但是好理解,signal是函数名,void (*)(int)是函数的返回类型
9.4 typedef 关键字(类型重命名)
typedef unsigned int uint; //将 unsigned int 重命名为 uint
typedef int* ptr_t; //将 int* 重命名为 ptr_t; //对于数组指针和函数指针稍微有点区别:
typedef int(*parr_t)[5]; //将 int(*)[5] 重命名为 parr_t , 新的类型名必须在*的右边,
typedef void(*pfun_t)(int); //将 void(*)(int) 重命名为 pfun_t , 新的类型名必须在*的右边// 简化9.3的代码2可以写成:
typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);
10. 函数指针数组
把函数的地址存到一个数组中,那这个数组就叫函数指针数组
#include <stdio.h>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 (*pf1)(int, int) = Add; int (*pfarr[4])(int, int) = { Add, Sub, Mul, Div };// int (*)(int, int)类型的数组for (int i = 0; i < 4; i++){int ret = pfarr[i](8, 4);printf("%d\n", ret);}return 0;
}
11. 函数指针数组实现计算器(函数指针数组—转移表)
11.1 计算器一般实现
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>void menu()
{printf("*************************\n");printf("****** 1:add 2:sub ******\n");printf("****** 3:mul 4:div ******\n");printf("****** 0:exit ******\n");printf("*************************\n");printf("请选择:");
}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 input = 0;int x = 0;int y = 0;int ret = 0;do{menu();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");break;default:printf("选择错误\n");break;}} while (input);return 0;
}
11.2 改进1—函数指针数组方式
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>void menu()
{printf("*************************\n");printf("****** 1:add 2:sub ******\n");printf("****** 3:mul 4:div ******\n");printf("****** 0:exit ******\n");printf("*************************\n");printf("请选择:");
}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;int(*p[5])(int x, int y) = { 0, Add, Sub, Mul, Div }; //转移表do{menu();scanf("%d", &input);if ((input <= 4 && input >= 1)){printf("输入操作数:");scanf("%d %d", &x, &y);ret = (*p[input])(x, y);printf("ret = %d\n", ret);}else if (input == 0){printf("退出计算器\n");}else{printf("输入有误\n");}} while (input);return 0;
}
11.3 改进2—函数指针作参数(回调函数)
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>void menu()
{printf("*************************\n");printf("****** 1:add 2:sub ******\n");printf("****** 3:mul 4:div ******\n");printf("****** 0:exit ******\n");printf("*************************\n");printf("请选择:");
}
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;
}// 函数指针作 calc函数的参数
void calc(int (*pf)(int, int))
{int x = 0;int y = 0;int ret = 0;printf("输入操作数:");scanf("%d %d", &x, &y);ret = pf(x, y);printf("ret = %d\n", ret);
}int main()
{int input = 0;do{menu();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;
}