指针的主题,我们在初级阶段已经接触过了,我们知道了指针的概念:
- 指针就是一个变量,用来存放地址,地址唯一标识一块内存空间。
- 指针的大小是固定的 4/8 个字节(32 位平台/ 64 位平台)
- 指针是有类型,指针的类型决定了指针的 +/- 整数的步长,指针解引用操作的时候的权限。
- 指针的计算。
这个章节,我们继续探讨指针的高级主题。
1. 字符指针
在指针的类型中我们知道有一种指针类型为字符指针 char*;
一般使用:
char ch = 'w';
char* p = &ch;
*p = 'b';
还有一种使用方式:
const char* p = "abcdef";//这里是把字符串放在 p 指针变量里了么?
printf("%s\n", p);
很容易以为是把字符串放在了字符指针 p 里了,但是本质上是把字符串首字符 a 的地址,赋值给了 p。
注意:“abcdef” 是一个常量字符串,不能被更改。因此用 const 修饰更安全。
那就有一道这样的面试题:
#include <stdio.h>
int main() {
const char* p1 = "abcdef";
const char* p2 = "abcdef";
char arr1[] = "abcdef";
char arr2[] = "abcdef";
if (p1 == p2)
printf("p1 == p2\n");
else
printf("p1 != p2\n");
if (arr1 == arr2)
printf("arr1 == arr2\n");
else
printf("arr1 != arr2\n");
return 0;
}
//p1 == p2
//arr1 != arr2
这里 p1 和 p2 指向的是同一个常量字符串。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以 arr1 和 arr2 不同,p1 和 p2 相同。
C/C++ 会把常量字符串存储到单独的一个内存区域(常量区/只读区),当几个指针,指向同一个常量字符串时,他们实际会指向同一块内存。
2. 指针数组
在《指针》章节我们介绍了指针数组,指针数组是用来存放指针的数组。
这里我们复习一下,下面指针数组是什么意思?
int* arr1[10];//整型指针的数组
char* arr2[4];//一级字符指针的数组
char** arr3[5];//二级字符指针的数组
3. 数组指针
3.1 数组指针的定义
我们已经熟悉:
整型指针:int* pi; 能够指向整型数据的指针。
浮点型指针:int* pi; 能够指向浮点型数据的指针。
那数组指针应该是:能够指向数组的指针。
下面代码哪一个是数组指针?
int* p1[10];
int (*p2)[10];
解释:
int (*p2)[10];
p2 先和 * 结合,说明 p2 是一个指针变量,然后指针指向的是一个大小为 10 个整型的数组。说明 p2 是一个指针,指向一个数组,叫数组指针。
注意:[ ] 的优先级要高于 * 号的,所以必须加上 ( ) 来保证 p 先和 * 结合。
3.2 数组名和 &数组名
对于下面的数组:
int arr[10];
arr 和 &arr 分别是啥?
我们知道 arr 是数组名,数组名表示数组首元素的地址。
那 &arr数组名 到底是啥?
我们看一段代码:
#include <stdio.h>
int main() {
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
printf("%p\n", arr);
printf("%p\n", arr + 1);
printf("%p\n", &arr[0]);
printf("%p\n", &arr[0] + 1);
printf("%p\n", &arr);
printf("%p\n", &arr + 1);
return 0;
}
可见:&arr数组名 表示整个数组的地址。
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的地址赋值给数组指针变量 p
return 0;
}
但是我们一般很少这样写代码。
一个数组指针的使用:
#include <stdio.h>
void print_arr1(int arr[3][5], int row, int col) {
int i = 0, j = 0;
for (i = 0; i < row; i++) {
for (j = 0; j < col; j++) {
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
void print_arr2(int (*arr)[5], int row, int col) {
int i = 0, j = 0;
for (i = 0; i < row; i++) {
for (j = 0; j < col; j++) {
//printf("%d", *(*(arr + i) + 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_arr1(arr, 3, 5);
print_arr2(arr, 3, 5);
return 0;
}
解释:数组名 arr 表示首元素的地址,但是二维数组的首元素是二维数组的第一行,所以这里传递的 arr,其实相当于第一行的地址,是一维数组的地址,可以用数组指针来接收。
学了指针数组和数组指针我们来一起回顾并看看下面代码的意思:
int arr[5];
int* parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];//存放数组指针的数组
4. 数组参数、指针参数
在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?
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[10] = { 0 };
test(arr);
test2(arr2);
return 0;
}
4.2 二维数组传参
#include <stdio.h>
void test(int arr[3][5]) {}
void test(int arr[][5]) {}
void test(int arr[][]) {} //ok?
void test(int (*arr)[5]) {}
void test(int* arr[5]) {} //ok?
void test(int* arr) {} //ok?
void test(int** arr) {} //ok?
int main() {
int arr[3][5] = { 0 };
test(arr);
return 0;
}
总结:二维数组传参,函数形参的设计只能省略第一个 [ ] 的数字。因为对于一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。这样才方便计算。
4.3 一级指针传参
#include <stdio.h>
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, 0};
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
print(p, sz);
return 0;
}
思考:当一个函数的参数部分为一级指针的时候,函数能接收什么参数?
- 变量的地址 2. 指针变量 3. 一维数组的数组名 ……
4.4 二级指针传参
#include <stdio.h>
void test(int** pp) {
printf("%d\n", **pp);
}
int main() {
int n = 10;
int* p = &n;
int** pp = &p;
test(pp);
test(&p);
return 0;
}
思考:当一个函数的参数部分为二级指针的时候,函数能接收什么参数?
- 一级指针的地址 2. 二级指针变量 3. 指针数组的数组名 ……
5. 函数指针
首先来看一段代码:
#include <stdio.h>
int Add(int x, int y) {
return x + y;
}
int main() {
int arr[5] = { 0 };
//&数组名 - 取出数组的地址
int(*parr)[5] = &arr;//数组指针
//&函数名 - 取出函数的地址
printf("%p\n", &Add);
printf("%p\n", Add);
return 0;
}
//00AA140B
//00AA140B
输出的是两个地址,这两个地址是 Add 函数的地址。
对于函数来说,&函数名 和 函数名 都是函数的地址。
那我们的函数的地址要想保存起来,怎么保存?
int (*pf)(int, int) = &Add; 或者 int (*pf)(int, int) = Add;
解释:首先,能存储地址,就要求 pf 是指针。 pf 先与 * 结合,说明 pf 是指针,指针指向的是一个函数,指向函数的参数为 (int, int),返回值为 int 。
阅读两段有趣的代码:
( *( void (*)() )0 )();
代码 1 是一次函数调用,调用的是 0 作为地址处的函数。
- 把 0 强制类型转换为:无参,返回类型是 void 的函数的地址。
- 调用 0 地址处的这个函数
void (* signal(int, void(*)(int)) )(int);
代码 2 是一次函数声明,声明的 signal 函数的第一个参数类型是 int,第二个参数的类型是函数指针,该函数指针指向的函数的参数是 int,返回类型是 void,signal 函数的返回类型也是一个函数指针,该函数指针指向的函数的参数是 int,返回类型是 void。
代码 2 太复杂,如何简化:
typedef void(* pf_t)(int);//把 void(*)(int)类型重命名为 pf_t
pf_t signal(int, pf_t);
函数指针的用途:
函数指针用于实现回调函数。
6. 函数指针数组
数组是一个存放相同类型数据的存储空间,那我们已经学习了指针数组。比如:
int* arr[10];//数组的每个元素是 int*
那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
int (*parr1[10])();//√
int *parr2[10]();//×
int (*)() parr3[10];//×
parr1 先和 [ ] 结合,说明 parr1 是数组,数组的内容是什么呢?
是 int (*) () 类型的函数指针。
函数指针数组的用途:转移表
例子:计算器
#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 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("请选择:\n");
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;
}
使用函数指针数组实现:
#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 x, y;
int input = 1;
int ret = 0;
int (*p[5])(int, int) = { 0, Add, Sub, Mul, Div };
while (input)
{
printf("**********************\n");
printf(" 1:add 2:sub \n");
printf(" 3:mul 4:div \n");
printf("**********************\n");
printf("请选择:\n");
scanf("%d", &input);
if (input >= 1 && input <= 4) {
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = p[input](x, y);
}
else {
printf("输入有误\n");
}
printf("ret = %d\n", ret);
}
return 0;
}
7. 指向函数指针数组的指针
指向函数指针数组的指针是一个 指针。
指针指向一个 数组,数组的每个元素都是 函数指针。
如何定义?
#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 (*pfArr[])(int, int) = { 0, Add, Sub, Mul, Div };
int (*(*pfArr)[5])(int, int) = &pfArr;
return 0;
}
8. 回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或者条件进行响应。
首先演示一下 qsort 函数的使用:
qsort 函数介绍:
qsort 函数是使用快速排序的思想实现的一个排序函数(可以排序任意类型的数据)。void qsort(void* base, //待排序的数据的起始位置 size_t num, //待排序的数据的个数 size_t size,//待排序的数据元素的大小(单位是字节) int (__cdecl *compar)(const void* e1, const void* e2));//函数指针 - 比较函数
__cdecl:函数调用约定(按 C 的方式调用)
#include <stdio.h>
#include <stdlib.h >
//比较 2 个整型元素
//e1 指向一个整数
//e2 指向另一个整数
int compar_int(const void* e1, const void* e2) {
return *(int*)e1 - *(int*)e2;
}
int main() {
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), compar_int);
int i = 0;
for (i = 0; i < sz; i++) {
printf("%d ", arr[i]);
}
return 0;
}
void* 介绍:
- void* 是无具体类型的指针,可以接受任意类型的地址。
- void* 是无具体类型的指针,所以不能解引用操作,也不能 +/- 整数。
9. 练习
代码 1:
#include <stdio.h>
int main() {
int a[] = { 1, 2, 3, 4 };
printf("%d\n", sizeof(a));//16
printf("%d\n", sizeof(a + 0));//4/8
printf("%d\n", sizeof(*a));//4
printf("%d\n", sizeof(a + 1));//4/8
printf("%d\n", sizeof(a[1]));//4
printf("%d\n", sizeof(&a));//4/8
printf("%d\n", sizeof(*&a));//16
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;
}
- 数组名在没有 &数组名 和 sizeof(数组名) 时,表示数组首元素的地址。
sizeof(*&a);
:&数组名 拿到的是整个数组的地址,类型是 int (*) [4],是一种数组指针。而数组指针解引用后找到的是整个数组。
代码 2:
#include <stdio.h>
#include <string.h>
int main() {
char arr[] = { 'a', 'b', 'c', 'd', 'e', 'f' };
printf("%d\n", strlen(arr));//随机值
printf("%d\n", strlen(arr + 0));//随机值
printf("%d\n", strlen(*arr));//strlen(97);//野指针//崩溃
printf("%d\n", strlen(arr[1]));//strlen(98);//野指针//崩溃
printf("%d\n", strlen(&arr));//随机值
printf("%d\n", strlen(&arr + 1));//随机值 - 6
printf("%d\n", strlen(&arr[0] + 1));//随机值 - 1
return 0;
}
strlen 的函数原型为:
size_t strlen(const char* str);
strlen 函数的参数是一个 char* 类型的指针,接收的是一个地址;当接收一个 字符/整型 的时候,会将整数作为一个地址传给 strlen 函数,此时该地址是一个 野指针,读取会发生访问冲突,导致程序崩溃。
在计算机内部,小地址(较小的地址,0地址等)是留给内核来使用的,是不允许用户直接访问的,所以当编译器看到 小地址 后会报错。
在 C 语言中看到的地址都是假的地址(虚拟地址),虚拟地址最终还要经过硬件和软件相关的计算转换(保证用户不可能访问到内核空间)成物理地址,物理地址才是对应到内存里面每一块空间的地址。
代码 3:
#include <stdio.h>
#include <string.h>
int main() {
char arr[] = "abcdef";
printf("%d\n", strlen(arr));//6
printf("%d\n", strlen(arr + 0));//6
printf("%d\n", strlen(*arr));//strlen(97);//野指针//崩溃
printf("%d\n", strlen(arr[1]));//strlen(98);//野指针//崩溃
printf("%d\n", strlen(&arr));//6
printf("%d\n", strlen(&arr + 1));//随机值
printf("%d\n", strlen(&arr[0] + 1));//5
printf("%d\n", sizeof(arr));//7
printf("%d\n", sizeof(arr + 0));//4/8
printf("%d\n", sizeof(*arr));//1
printf("%d\n", sizeof(arr[1]));//1
printf("%d\n", sizeof(&arr));//4/8
printf("%d\n", sizeof(&arr + 1));//4/8
printf("%d\n", sizeof(&arr[0] + 1));//4/8
return 0;
}
- strlen 是求字符串长度的库函数,计算的是 ‘\0’ 之前出现的字符个数
- sizeof 是求占用内存空间大小的操作符,不在乎内存中放的是什么
代码 4:
#include <stdio.h>
#include <string.h>
int main() {
char* p = "abcdef";
printf("%d\n", sizeof(p));//4/8
printf("%d\n", sizeof(p + 1));//4/8
printf("%d\n", sizeof(*p));//1
printf("%d\n", sizeof(p[0]));//1
printf("%d\n", sizeof(&p));//4/8
printf("%d\n", sizeof(&p + 1));//4/8
printf("%d\n", sizeof(&p[0] + 1));//4/8
printf("%d\n", strlen(p));//6
printf("%d\n", strlen(p + 1));//5
printf("%d\n", strlen(*p));//strlen(97);//野指针//崩溃
printf("%d\n", strlen(p[0]));//strlen(97);//野指针//崩溃
printf("%d\n", strlen(&p));//随机值
printf("%d\n", strlen(&p + 1));//随机值,两随机值无联系
printf("%d\n", strlen(&p[0] + 1));//5
return 0;
}
此时 p 为常量字符串首字符的地址。
代码 5:
#include <stdio.h>
int main() {
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
printf("%d\n", sizeof(a[0] + 1));//4/8
printf("%d\n", sizeof(*(a[0] + 1)));//4
printf("%d\n", sizeof(a + 1));//4/8
printf("%d\n", sizeof(*(a + 1)));//16
printf("%d\n", sizeof(&a[0] + 1));//4/8
printf("%d\n", sizeof(*(&a[0] + 1)));//16
printf("%d\n", sizeof(*a));//16
printf("%d\n", sizeof(a[3]));//16
return 0;
}
- a[0] 表示第一行数组的数组名,sizeof(a[0]); 计算的就是第一行数组的大小。
- a[0]既没有单独放在 sizeof 内部,也没有取地址,a[0] 就表示首元素的地址,也就是第一行这个一维数组的第一个元素的地址,a[0] + 1 就是第一行第二个元素的地址。
- a 既没有单独放在 sizeof 内部,也没有取地址,a 就表示首元素的地址,也就是第一行这个一维数组的地址,a + 1 就是第二行这个一维数组的地址。
sizeof 通过分析变量的类型来计算大小,不会真的访问变量。
总结:
数组名的意义:
- sizeof(数组名); 这里的数组名表示整个数组,计算的是整个数组的大小。
- &数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
- 除此之外所有的数组名都表示首元素的地址。