总言
C语言:指针的使用介绍。
文章目录
- 总言
- 1、指针初阶
- 1.1、是什么
- 1.2、指针和指针类型
- 1.2.1、指针类型介绍
- 1.2.2、作用一:指针解引用
- 1.2.3、作用二:指针±整数
- 1.3、野指针
- 1.3.1、野指针是什么
- 1.3.2、为什么存在野指针
- 1.3.3、如何避免野指针
- 1.4、指针运算
- 1.4.1、指针±整数
- 1.4.2、指针-指针
- 1.4.3、指针和数组
- 1.5、二级指针和指针数组
- 1.5.1、二级指针
- 1.5.2、指针数组
- 2、指针进阶
- 2.1、字符指针
- 2.1.1、字符指针的两种常见用法
- 2.1.2、例题演示
- 2.2、指针数组和数组指针
- 2.2.1、指针数组
- 2.2.2、数组指针
- 2.2.2.1、数组指针的认识
- 2.2.2.2、数组指针的使用
- 2.3、数组参数、指针参数
- 2.3.1、一维数组传参
- 2.3.2、二维数组传参
- 2.3.3、一级指针传参
- 2.3.4、二级指针传参
- 2.4、函数指针
- 2.5、函数指针数组
- 2.5.1、整体介绍
- 2.5.2、实例演示:简单模拟计算器
- 2.5.2.1、模拟实现1.0
- 2.5.2.2、模拟实现2.0:转移表
- 2.6、指向函数指针数组的指针
- 2.7、回调函数
- 2.7.1、回调函数基本介绍
- 2.7.2、回调函数用途举例:解决2.5.2.1中模拟实现冗余问题
- 2.7.3、回调函数用途举例:qsort快排与冒泡排序
- 2.7.3.1、qsort函数的简单介绍
- 2.7.3.2、qsort函数的使用演示:内置类型、自定义类型
- 2.7.3.3、 仿照qsort实现任意类型数据比较的冒泡排序
- 2.8、相关练习
- 2.8.1、指针和数组理解
- 2.8.1.1、sizeof与一维数组(int)
- 2.8.1.2、sizeof与一维数组(char)(一)
- 2.8.1.3、strlen与一维数组(char)(一)
- 2.8.1.4、sizeof与一维数组(char)(二)
- 2.8.1.5、strlen与一维数组(char)(二)
- 2.8.1.6、sizeof与一维数组(char)(三)
- 2.8.1.7、strlen与一维数组(char)(三)
- 2.8.1.8、sizeof和二维数组(int)
- 2.8.1.9、一个小结
- 2.8.2、指针相关笔试题
- 2.8.2.1、练习一
- 2.8.2.2、练习二
- 2.8.2.3、练习三
- 2.8.2.4、练习四
- 2.8.2.5、练习五
- 2.8.2.6、练习六
- 2.8.2.7、练习七
- 2.8.2.8、练习八
1、指针初阶
1.1、是什么
1)、关于指针
1、指针是内存中一个最小单元的编号,也就是地址。
2、平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量。
2)、关于内存
3)、关于指针变量
int main()
{
int a = 10;//创建一个变量a,其内存中开辟一块空间,占用4个字节的空间
int* p = &a;//对变量a,使用&操作符取出它的地址,即将a的4个字节的第一个字节的地址存放在p变量中,p就是一个之指针变量。
return 0;
}
1.2、指针和指针类型
1.2.1、指针类型介绍
由下述可知,指针类型的变量,其定义方式是: type + *
,即对应变量类型+*
号。
例如:
char*
类型的指针是为了存放 char
类型变量的地址。
short*
类型的指针是为了存放 short
类型变量的地址。
int*
类型的指针是为了存放 int
类型变量的地址。
那么问题来了,这些不同类型的指针变量,其空间大小一致吗?
int main()
{
int a = 10;//整型变量
int* pa = &a;//整型指针
char c = 'c';//字符变量
char* pc = &c;//字符指针
printf("%d\n", sizeof(pa));
printf("%d\n", sizeof(pc));
return 0;
}
验证如下:根据之前所学可知,指针的大小在32位平台是4个字节,在64位平台是8个字节。不因其类型而变化。
既然如此,为什么我们需要不同类型的指针变量?
以下我们将介绍不同类型的指针在解引用和±整数时的意义。
1.2.2、作用一:指针解引用
演示代码如下:
int main()
{
int a = 0X11223344;
int* pa = &a;
*pa = 0;
int b = 0X11223344;
char* pb = &b;
*pb = 0;
return 0;
}
同样是int
类型的变量,使用int*
的指针和char*
的指针来存储该变量地址,然后解引用修改它,结果有何不同?
假如是Int类型的指针变量:
假如是char类型的指针变量:
结果说明:
指针类型决定了指针在解引用时,能够访问的权限。
比如: char*
的指针,解引用能访问一个字节;而 int*
的指针,解引用能访问四个字节。
1.2.3、作用二:指针±整数
演示代码如下:
int main()
{
int a = 10;
int* pa = &a;
char* pc = &a;
printf("%p\n", pa);
printf("%p\n", pa+1);
printf("\n");
printf("%p\n", pc);
printf("%p\n", pc+1);
return 0;
}
如下所示,可以得知,指针的类型决定了指针向前或者向后走一步有多大(距离)。
int * +1
,即int* + 1*sizeof(int) == int* +4(字节)
char * +1
,即char* + 1*sizeof(char) == char* +1(字节)
int * +n
,即int* + n*sizeof(int) == int* +4*n(字节)
char * +n
,即char* + n*sizeof(char) == char* +1*n(字节)
1.3、野指针
1.3.1、野指针是什么
野指针:存在一个指针,其指向的位置是不可知的(随机的、不正确的、没有明确限制的)。
1.3.2、为什么存在野指针
1)、原因一:指针未初始化
演示代码如下:
int main()
{
int* p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
2)、原因二:指针越界访问
此类情况常出现在数组等大型内存空间中,演示代码如下:
int main()
{
int arr[10] = { 0 };
int* p = arr;
int i = 0;
for (i = 0; i <= 11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
3)、原因三:指针指向的空间释放
演示代码如下:
int* test()
{
int a = 100;
return &a;
}
int main()
{
int* p = test();
printf(" %d\n", *p);
printf(" %d\n", *p);
return 0;
}
演示结果如下:
说明:此处的错误,是由于局部变量有其生命周期,出了函数体外,属于a的存储空间权限被收回,此时这块空间会被系统分配给别的对象,则指针访问的就是未知对象,即野指针。
1.3.3、如何避免野指针
1、指针初始化
int a = 10;
int* pa = &a;//若有确定变量,则&对应变量
int* pb = NULL;//若无,则赋值为空(空指针)
2、小心指针越界
3、指针指向空间释放时,及时置NULL
4、避免返回局部变量的地址
5、指针使用之前检查有效性
int* p = NULL;
//....
int a = 10;
p = &a;
if (p != NULL)
{
*p = 20;
}
6、等等
1.4、指针运算
1.4.1、指针±整数
演示代码如下:
#define N 5
int main()
{
float values[N];
float* vp;
//指针+-整数;指针的关系运算
for (vp = &values[0]; vp < &values[N];)
{
*vp++ = 0;
}
return 0;
}
*vp++ = 0;
:指针±整数,vp < &values[N];
:指针的关系运算
标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
#define N 5
int main()
{
float values[N];
float* vp;
for (vp = &values[N - 1]; vp >= &values[0]; vp--)//往前遍历
{
*vp = 0;
}
return 0;
}
如上述这段代码,其在绝大部分的编译器上是可以顺利运行,但还是应该避免这样写,因为标准并不保证它绝对可行。
1.4.2、指针-指针
演示代码如下:
int my_strlen(char* s)
{
char* p = s;
while (*p != '\0')
p++;
return p - s;
}
指针-指针:必须指向同一块空间,得到的是两指针之间的元素个数。
1.4.3、指针和数组
前提:除两个特例外,数组名是首元素地址(具体见数组章节)。
由上述内容可知:既然可以把数组名当成地址存放到一个指针中,就可以使用指针来访问数组。
演示一:用于验证数组和指针的关系
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,0 };
int* p = arr; //指针存放数组首元素的地址
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < sz; i++)
{
printf("&arr[%d] = %p <====> p+%d = %p\n", i, &arr[i], i, p + i);
}
return 0;
}
演示二:如何使用指针遍历数组
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
int* p = arr; //指针存放数组首元素的地址
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
1.5、二级指针和指针数组
1.5.1、二级指针
对于一个普通变量,我们可以用指针存放它的地址,这里的指针是一级指针。
int a = 0;
int* pa = &a;
同理的,指针变量是一个变量,其在内存空间也有相应地址,那么我们怎么存放指针的地址呢?
同样使用一个指针即可,只是这里的指针是二级指针,其含义是这是一个指针,这个指针指向的对象仍旧是一个指针类型的数据。
int a = 0;
int* pa = &a;
int** ppa = &pa;
1.5.2、指针数组
我们知道数组有各种类型:
int arr[5];//整型数组:存放整型变量的数组
char ch[6];//字符数组:存放字符变量的数组
那么我们说指针数组,其指的是指针还是数组?
回答:指针数组是存放指针类型变量的数组。
int* arr2[5];//该数组是指针数组,其存放的元素类型是int*
int a = 1, b = 2, c = 3, d = 4, e = 5;
int* arr3[5] = { &a,&b,&c,&d,&e };//指针数组
一个运用演示:
int main()
{
int data1[] = { 1,2,3,4,5 };
int data2[] = { 2,3,4,5,6 };
int data3[] = { 3,4,5,6,7 };
int* arr[3] = { data1,data2,data3 };
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 5; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
2、指针进阶
2.1、字符指针
2.1.1、字符指针的两种常见用法
1)、用法一演示
创建了一个字符,则用对应的字符指针指向该字符变量的地址。可以通过字符指针修改该字符变量。
int main()
{
char ch = 'w';
char* p = &ch;
printf("%c\n", *p);
*p = 'h';
printf("%c\n", *p);
return 0;
}
2)、用法二演示
用字符指针指向一个字符串常量,该字符指针实际存储的是字符串首字符的地址。由于是字符串常量,不能通过该字符指针的对原字符串进行修改。
int main()
{
char* p = "merry christmas.";
//*p = 'M';//error
printf("%c\n", *p);
printf("%s\n", p);
return 0;
}
注意事项:
*p = 'M';//error
①:这句代码错误的原因是,"merry christmas."
是字符串字面量,不能被修改,通常情况下,为了防止此类错误发生,可使用const
关键字进行修饰:
const char* p = "merry christmas.";
②:"merry christmas."
这个常量字符串实际上是存储在内存静态常量区中,是只读的。
2.1.2、例题演示
1)、例题演示一
下面这段代码输出结果是什么?
#include <stdio.h>
int main()
{
char str1[] = "happy new years.";
char str2[] = "happy new years.";
const char* str3 = "happy new years.";
const char* str4 = "happy new years.";
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if (str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
演示结果如下:
分析:
str3
和str4
指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。
但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。
所以str1
和str2
不同,str3
和str4
不同。
简言之:一个是用不同指针指向相同字符串常量,一个是用相同的常量字符串初始化不同的数组。
2.2、指针数组和数组指针
2.2.1、指针数组
在1.5.1中我们介绍过指针数组:指针数组是一个存放指针的数组。
int* arr1[10]; //整形指针的数组
char *arr2[4]; //一级字符指针的数组
char **arr3[5];//二级字符指针的数组
演示例子一:
int main()
{
int a = 10;
int b = 20;
int c = 30;
//可以使用单独的指针存放a、b、c
int* p1 = &a;
int* p2 = &b;
int* p3 = &c;
//也可以使用一个指针数组
int* arr[3] = { &a, &b, &c };
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%d ", *(arr[i]));
}
return 0;
}
以上只是一个简单的示范,实际中这样写的很少,因为a、b、c是毫无关系的数据。
演示例子二:
int main()
{
int data1[] = { 1,2,3,4,5 };
int data2[] = { 2,3,4,5,6 };
int data3[] = { 3,4,5,6,7 };
int* arr[3] = { data1,data2,data3 };
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 5; j++)
{
printf("%d ", arr[i][j]);//*(arr[i]+j)
}
printf("\n");
}
return 0;
}
这里类似于模拟出了一个二维数组,效果相同,但实际上二维数组中的每个一维数组data1、data2、data3
空间不连续。
2.2.2、数组指针
2.2.2.1、数组指针的认识
1)、问题引入
int *p1[10];
int (*p2)[10];
问题: 如上述代码,p1, p2
分别是什么?
分析:
①要注意运算符的优先级,[ ]
的优先级高于*
,故int *p1[10];
中,p1[10]
为先,可知其为数组,该数组的元素个数有10个,元素类型为int *
;
②()
和[]
优先级一致,此时看结合,二者都是左结合。故在int (*p2)[10];
中,(*p2)
为先,此处的*
非解引用操作符,在此处起到的作用是:提醒我们这是一个指针变量。那么这里的整体含义是:p2
为指针,指向对象为一个数组int[10]
,该数组元素类型为int,元素个数为10。
说明:
在写指针数组时,由于[]
的优先级要高于*号
的,所以必须加上()
来保证p
先和*
结合。
2)、相关说明:关于数组指针的类型
之前我们学习数组时,一直在强调数组名的含义:数组名
是数组首元素地址,&数组名
取出的是整个数组的地址。
并且我们还为此进行了相关验证:分别让数组名+1
,&数组名+1
。
int main()
{
int a = 10;
int* p = &a;
//数组是首元素的地址
int arr[10] = {0};
printf("%p\n", arr);
printf("%p\n\n", arr+1);
printf("%p\n", &arr[0]);
printf("%p\n\n", &arr[0]+1);
printf("%p\n", &arr);
printf("%p\n\n", &arr+1);
return 0;
}
如上述代码,我们讨论了数组名
,&数组名
所代表的含义,并知晓了其与地址间的关系,可以用一个指针来存储相关地址。但我们并没有讨论对应指针的类型。
对于arr
、或者&arr[0]
,其代表数组中单个元素的地址。若使用指针来存储该地址,我们能很容易的得出该指针的类型:int *
。
而对于&arr
,用指针来存储,该指针的类型要如何写呢?
首先,其是一个指针,故有:*p
;
其次,其指向的是含有10个元素的数组的地址,倘若直接写为*p[10]
,优先级不对,因此需要加上()
:(*p)[10]
;
最后,这个数组的元素类型是int
,故有:int (*p)[10]
,即:
int(*p)[10] = &arr;
那么,这个指针(数组指针)的类型即为:int(*)[10]
(去掉指针名称)。
小练习:
如下述代码,有一个数组,现有一个指针p
指向该数组,即p=&arr
,请写出这个数组指针的类型。
char* arr[5];
回答:char* (*)[5]
char* (*p)[5] = &arr;
含义:这是一个指向指针数组的数组指针。
这是一个指针,该指针指向一个数组,该数组元素个数为5,元素类型为char*
。即该数组是一个存放指针的数组。
2.2.2.2、数组指针的使用
1)、在一维数组中的使用举例
任务要求: 写一个函数,用于遍历数组。
写法1.0: 实际上我们可以直接用数组名或指针解决。根据之前所学,传参中,int arr[]
和int* arr
并无区别,因为传入数组名,常规情况下,其代表数组首元素地址,故可以使用对于元素类型的指针解决。同样的,arr[i]
和*(arr + i)
并无区别。我们讲解过[]
的含义。
arr[i]
等价于p[i]
等价于*(p+i)
等价于*(arr+i)
。
void print1(int arr[], int sz)
{
for (int i = 0; i < sz; ++i)
{
printf("%d ", arr[i]);
}
printf("\n");
}
void print2(int* arr, int sz)
{
for (int i = 0; i < sz; ++i)
{
printf("%d ", *(arr + i));
}
printf("\n");
}
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
int sz = sizeof(arr) / sizeof(arr[0]);
print1(arr, sz);
print2(arr, sz);
return 0;
}
写法2.0: 传参时使用&数组名
,即数组指针。
void print3(int(*p)[10], int sz)
{
for (int i = 0; i < sz; ++i)
{
printf("%d ", *(*p + i));
}
printf("\n");
}
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
int sz = sizeof(arr) / sizeof(arr[0]);
print3(&arr, sz);
return 0;
}
概括: *p
相当于数组名,数组名表述首元素地址,故*p==&arr[0]
。
解析: 对数组指针,其指向的是整个数组的地址,指针±整数会跳过整个数组。要用其表示数组中某一项元素,则需要先对p
(该行数组的地址)进行解引用,得到*p
为该行数组的数组名(或者说该行数组的首元素地址),再对其进行指针运算(±整数),即可得到对应后续数组元素的地址,而要得到后续数组元素,需要在得到地址后对其解引用,即*((*p+i))
,才得到该行数组中某一元素的值。
*((*p)+i)
等价于*((*(&arr)+i))
等价于*(arr+i)
等价于arr[i]
。
事实上,在一维数组中很少使用数组指针这样写。
2)、在二维数组中的使用举例
任务要求: 写一个函数,用于遍历二维数组。
写法一: 传参时直接使用数组形式
void print1(int arr[][6], int r, int c)
{
for (int i = 0; i < r; ++i)
{
for (int j = 0; j < c; ++j)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
printf("\n");
}
int main()
{
int arr[3][6] = { {1,2,3,4,5,6},{2,3,4,5,6,7},{3,4,5,6,7,8} };
print1(arr, 3, 6);
return 0;
}
写法二: 假如使用数组指针。
要使用数组指针做参数,首先要理解二维数组中,数组名的含义。
如上述解释,二维数组int arr[row][col]
的数组名arr
表示整个第一行(一维数组arr[0]
)的地址。故有:
①对应的指针是数组指针:int(*p)[col]
,只不过这里的数组是一位数组。在二维数组中是&arr[0]
。
②使用指针进行±运算,则跳过整个一位数组,即二维数组中的一行元素。
③那么有:
p+i
:指向二维数组的第i
行的指针(数组指针),是地址。
*p
:得到的是数组名,数组名在一维数组中表示首元素地址。
*(p+i)
:则表示第i行一维数组,第一个元素的地址。
(*(p+i)+j)
:则表示第i行数组,从首元素往后走j个,即第i行,第j列元素的地址。
void print2(int(*p)[6], int r, int c)
{
for (int i = 0; i < r; ++i)
{
for (int j = 0; j < c; ++j)
{
printf("%d ", *(*(p+i)+j));//等价于p[i][j]
}
printf("\n");
}
printf("\n");
}
int main()
{
int arr[3][6] = { {1,2,3,4,5,6},{2,3,4,5,6,7},{3,4,5,6,7,8} };
print2(arr, 3, 6);
return 0;
}
注意:
假如我们对二维数组取地址,则其对应指针如下:
void test1(int(*p)[5])
{
}
void test2(int(*p)[3][5])
{
*p;//解引用得到的是二维数组的数组名,即第一行的整体地址
}
int main()
{
int arr[3][5];
test1(arr);//传递二维数组数组名:二维数组首元素/第一行地址/整个一维数组地址
test2(&arr);//传递整个二维数组地址
return 0;
}
小练习: 解释以下代码
int arr[5];
int* parr1[10];
int(*parr2)[10];
int(*parr3[10])[5];
int arr[5];
:arr是一个整型数组,元素个数为5,元素类型为int
;
int* parr1[10];
:parr1是一个指针数组,元素个数为10,元素类型为int*
;
int(*parr2)[10];
:parr2是一个数组指针,指向一个数组。该指针类型为int(*)[10]
,指向的数组元素类型为int,元素个数为10;
int(*parr3[10])[5];
:parr3是一个数组,用于存储数组指针,该数组的元素类型为int(*)[5]
,元素个数为10。每个数组指针指向一个元素类型为int,元素大小为5的数组。
2.3、数组参数、指针参数
2.3.1、一维数组传参
一维数组传参,单独数组名的情况下,表示数组首元素地址。
1)、对于普通类型
void test(int arr[])//ok?
{}
void test(int arr[10])//ok?
{}
void test(int* arr)//ok?
{}
int main()
{
int arr[10] = { 0 };
test(arr);
}
形参写成数组形式:
void test(int arr[])
、void test(int arr[10])
:一维数组传参,形参大小可写可不写,写错也无问题(但不建议这样写)。
形参写成指针形式:
void test(int* arr)
:一维数组中,数组传参,形参可以写成数组形式,也可以写成指针形式。而本质上,数组在函数传参时传递的是数组名,即数组首元素地址,故为址传递,可用指针接收。至于需要用什么样类型的指针来接收,需要根据实参来确定。
2)、对于指针数组
void test2(int* arr[20])//ok?
{}
void test2(int** arr)//ok?
{}
int main()
{
int* arr2[20] = { 0 };
test2(arr2);
}
形参写成数组形式:
void test2(int* arr[20])
:指针数组传参,形参可以写成数组的形式,故此处写成了指针数组,数组元素可以忽略。
形参写成指针形式:
void test2(int** arr)
:指针数组,每个元素的类型为int *
,传递数组名时,相当于传递了该数组首元素地址,即int*
一级指针的地址,此时需要拿一个二级指针int**
来接收。
2.3.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);
}
形参写成数组形式:
void test(int arr[3][5])
、void test(int arr[][5])
:数组传参,形参可以写成数组的形式。不过对于二维数组来说,二维数组传参,可省略行,不可省略列。
void test(int arr[][])
:错误写法。若省略二维数组的列,由于二维数组在内存空间也是连续存放的,就不知道该二维数组具体的行数、列数分布规律,有时可用得到很多解。
形参写成指针形式:
二维数组做实参,数组名传递过去的是二维数组的一行元素的地址,即整个一维数组的地址,故需要用数组指针来接收。这里即 int (*p)[5]
,表示指向元素个数为5,类型为int型的数组。
void test(int* arr)
:错误,实参传递的是二维数组的首行元素,无法用一级指针来接收。
void test(int* arr[5])
:错误,int* arr[5]
这里arr先和[]
结合,表示一个指针数组,用来接收元素个数为5,元素类型为int*型的指针。而我们需要的数组指针。
void test(int(*arr)[5])
:正确,int(*arr)[5]
是一个数组指针,指向一行元素个数为5,类型为int
的数组。
void test(int** arr)
:错误,只有当传递一级指针的地址时,才能用二级指针来接收。
2.3.3、一级指针传参
1)、基本内容
一级指针传参,用一级指针来接收,该一级指针可以改变所指向对象的内容。但不能改变该一级指针本身(本质上还是传值传参,如果要改变该一级指针本身,需要用二级指针)。
void print(int* ptr, int sz)
{
for (int i = 0; i < sz; i++)
{
//++ptr[i];
(*(ptr + i))++;
printf("%d ", *(ptr+i));
}
printf("\n");
}
int main()
{
int arr[10] = { 0,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;
}
上述代码中,ptr
和p
所指向的内容,其地址相同,都是arr首元素地址。而&ptr,&p
本身不同,它们是不同的地址空间。
2)、思考:当一个函数的参数部分为一级指针的时候,函数能接收什么参数?
回答:
①可以普通变量的地址
int a;
test(&a);//test(int* a);
②可以是一级指针本身
int* p;
test(p);//test(int* p);
③可以是一维数组名
int arr[10];
test(arr);//test(int* arr);
④……
2.3.4、二级指针传参
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;
}
2.4、函数指针
C语言并没有要求指针只能指向数据,它还允许指针指向函数。毕竟函数占用内存单元,所以每个函数都有地址,就像每个变量都有地址一样。
1)、函数地址介绍
函数不像数组,没有首元素的概念,函数名和取地址函数名,得到的都是函数的地址。函数的地址表示函数存放的位置。
#include <stdio.h>
void test()
{
printf("Happy Chinese New Year.\n");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
2)、如何存放一个函数地址:函数指针类型说明
根据1)中代码,问:下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();
void *pfun2();
回答:pfun2
会先和函数调用操作符结合,表示pfun2
是一个返回类型为void*
的函数。而由于括号,pfun1
会先和*
结合,说明pfun1
是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void
。
演示代码一:
int Add(int x, int y)
{
return x + y;
}
如上述代码,若用一个函数指针来存储该函数地址,该如何写其指针类型?
int (*pf)(int, int) = Add;
如上述,①pf
就是函数指针变量,其指向一个参数为(int,int)
,返回类型为int
的函数。②该函数指针类型为:int (*)(int, int)
。
代码演示二:
int test(char* str)
{
}
如上述代码,若用一个函数指针来存储该函数地址,该如何写其指针类型?
int (*pt)(char*) = test;
3)、函数指针变量的使用
以2)中代码演示一为例:如何通过pf
函数指针变量来找到对应函数进而调用它?
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*pf)(int, int) = Add;
return 0;
}
回答:
①如下,此处*p
需要加上括号,因为函数调用操作符的优先级比解引用操作符高。
printf("%d\n", (*pf)(2, 3));
int ret = (*pf)(7, 8);
②我们也可以不加*
号,事实上,函数指针中,*
在此处只是一种形式,此处p
的括号可以去掉。
printf("%d\n", pf(4, 4));
printf("%d\n", (******pf)(6, 9));
③但需要注意,有*
号时,函数指针变量必须要加括号;无*
号时,函数指针变量不用加括号。
4)、一些特殊代码演示:源自《C陷阱和缺陷》
代码1:
(*(void (*)())0)();
void (*)()
:这是一个函数指针类型:void (p*)()
,含函数指针指向一个无参,无返回值的函数。
(void (*)())
:将上述的函数指针类型用一个括号括起。类型括起来,一般用于强制类型转换比如:(float)5/2
。
(void (*)())0)
:将0进行强制类型转换,让其类型为void (*)()
,即一个函数指针。
*(void (*)())0
:对该函数指针解引用,根据上面所学(*p)(2,3)
,这表示找到相应的函数地址。
(*(void (*)())0)()
:由于还函数无参,故后续添加一个()即可,表示一次函数调用。
总述: 将0强制类型转换为void(*)( )
类型的函数指针,然后调用0地址处的函数,该函数无参、返回类型为void
。
代码2:
void (*signal(int, void(*)(int)))(int);
signal(int, void(*)(int))
:这是一个函数,函数有两个参数,int
和void(*)(int)
。
void (*)(int);
:这是该函数的返回类型,也是一个函数指针。
总述: ①signal
是一个函数声明。②其有两个参数,第一个参数类型是int
,第二个参数类型是 void(*)(int)
,即函数指针变量的类型。③signal
函数的返回值的也是一个函数指针变量,其类型为:void (*)(int)
。
将上述代码稍微修改,我们理解起来更容易:(此处只是便于理解,实际中不能这样写)
void (*)(int) signal(int, void(*)(int));
上述代码的简化版:
我们使用typedef
,将void(*)(int)
这个函数指针类型重命名为pf_t
,那么我们就可以使用pf_t
来代替void(*)(int)
。
typedef void(*pf_t)(int);
pf_t signal(int, pf_t);
此处pf_t
写在中间是语法要求。
2.5、函数指针数组
2.5.1、整体介绍
1)、基本介绍
数组是一个存放相同类型数据的存储空间,要把函数的地址存到一个数组中,那这个数组就叫函数指针数组。
//字符指针数组
char* arr1[4];
//整型指针数组
int* arr2[4];
函数指针数组的一个用途是:转移表。
以下为演示代码:
如下述,有四个函数,其分别用于加减乘除。
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 (*pf1)(int, int) = Add;
int (*pf2)(int, int) = Sub;
int (*pf3)(int, int) = Mul;
int (*pf4)(int, int) = Div;
分别使用四个函数指针来存储即可。
但是这样相对比较分散,因这四个函数指针具有相同类型,我们能否使用一个数组来直接存放它们?即使用函数指针数组。
那么这个函数指针数组的类型该如何写呢?
int(*pf[4])(int, int) = { Add,Sub,Mul,Div };
如上述,由于优先级,pf
先与数组下标运算符[]
结合,表示pf
是一个元素个数为4的数组。去掉数组名称,即为数组元素类型:int(*)(int, int)
。
当然,由于我们后面初始化了,数组元素大小是可以省略的。
int(*pf[])(int, int) = { Add,Sub,Mul,Div };
2)、如何使用
基于1),如何调用这个函数指针数组呢?
要知道,该函数指针数组,其本身是一个数组,该数组中的每一个元素为函数指针,指向一个函数。那么与普通数组一致,我们可以通过下标访问数组的元素,得到对应的函数指针。而函数指针如何访问函数,我们在2.4中讲述过。
以下为演示代码:
int main()
{
int(*pf[4])(int, int) = { Add,Sub,Mul,Div };
for (int i = 0; i < 4; ++i)
{
printf("%d ", pf[i](8, 2));
}
printf("\n");
return 0;
}
2.5.2、实例演示:简单模拟计算器
2.5.2.1、模拟实现1.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:加法 2:减法 \n");
printf(" 3:乘法 4:除法 \n");
printf(" 0:退出 \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");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}
上述1.0中存在的缺陷:代码冗余。
①首先是每个功能中都要单独scanf、printf
相关数据。因为假如把这串代码放置在switch
语句外,循环语句do while
内,那么当选择错误和退出时,仍旧会打印下述代码,不符逻辑。
printf("输入操作数:");
scanf("%d %d", &x, &y);
printf("ret = %d\n", ret);
②假如之后该计算器功能增加,那么switch case
语句将会变得很庞大。
因此有了下述2.0版本,实际上是使用了函数指针数组。
2.5.2.2、模拟实现2.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;
int(*p[])(int x, int y) = { 0, add, sub, mul, div }; //转移表
while (input)
{
printf("*************************\n");
printf(" 1:加法 2:减法 \n");
printf(" 3:乘法 4:除法 \n");
printf(" 0:退出 \n");
printf("*************************\n");
printf("请选择:");
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");
break;
}
else
printf("输入有误\n");
}
return 0;
}
下述即为转移表。这里函数指针数组的首元素为0,是为了方便后续输入ret = (*p[input])(x, y)
能直接对应上菜单选项。
int(*p[])(int x, int y) = { 0, add, sub, mul, div }; //转移表
使用函数指针数组的劣势:存储的函数指针类型必须一致。
2.6、指向函数指针数组的指针
1)、基本介绍
如下述代码,假如是一个数组指针,根据之前所学,可以很容易的写出它们的对应类型。假如是一个函数指针,或者函数指针数组,我们也能相对写出类型。
int arr[10] = { 0 };
int(*p)[10] = &arr;//数组指针
int* arr2[5];
int* (*p)[5] = &arr2;//数组指针
int(*pf)(int, int) = &Add;//函数指针
int(*pfarr[4])(int, int);//函数指针数组
那么,依次类推,假如是一个指向函数指针数组的指针呢?
//指向函数指针数组的指针:
//它是一个数组指针,指向一个数组,该数组元素为函数指针
int(*(*p3)[4])(int, int) = &pfarr;
该指针对应的类型:int(*(*)[4])(int, int)
为什么要有上述这个指向函数指针数组的指针?
我们学了函数指针数组,本质是一个数组,而我们也知晓&数组名
得到数组整个元素。那么就要有对应的指针类型来存储它。
2)、上述指向函数指针数组的指针的一个使用演示
#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 (*pfarr[4])(int, int) = { add,sub,mul,div };
//指向函数指针数组的指针:
int(*(*p)[4])(int, int) = &pfarr;
//使用上述指针遍历数组,从而访问函数
for (int i = 0; i < 4; ++i)
{
printf(" %d\n", (*p)[i](8, 4));
}
printf("\n");
return 0;
}
2.7、回调函数
2.7.1、回调函数基本介绍
1)、基本介绍
回调函数: 一个通过函数指针调用的函数。
①如果你把函数的指针(地址) 作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
②回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
例如:有A、B两个函数,将A函数取地址得其函数指针P,其中,让P作为B函数的参数,在B函数中使用时,就能够达成B函数中调用A的效果。
void A()
{
printf("回调函数\n");
}
void B(void(*P)())//B的参数为函数指针
{
if (1)//进行一些条件操作等
A();
}
int main()
{
B(A);//对B传入A的函数名称,即对应函数地址。
return 0;
}
2.7.2、回调函数用途举例:解决2.5.2.1中模拟实现冗余问题
在2.5.2.1中我们说使用switch case
语句造成了代码冗余问题,而后我们提出的一种解决方案是使用函数指针数组制作转移表,来达到目的。
除了上述这种方案外,我们还可以使用回调函数来解决它。其本身仍旧涉及函数指针问题:
void calc(int(*pf)(int, int))
{
int x, y;
printf("输入操作数:");
scanf("%d %d", &x, &y);
printf("ret = %d\n", pf(x,y));
}
calc
是一个函数,其参数为一个函数指针:int(*pf)(int, int)
。我们只用在每个switch case
语句中,对应选项处传入相关函数的地址,就能达到使用一个函数同时调用多个函数的目的。
整体代码如下:
#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;
}
void calc(int(*pf)(int, int))
{
int x, y;
printf("输入操作数:");
scanf("%d %d", &x, &y);
printf("ret = %d\n", pf(x,y));
}
int main()
{
int input = 1;
do
{
printf("*************************\n");
printf(" 1:加法 2:减法 \n");
printf(" 3:乘法 4:除法 \n");
printf(" 0:退出 \n");
printf("*************************\n");
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;
}
2.7.3、回调函数用途举例:qsort快排与冒泡排序
2.7.3.1、qsort函数的简单介绍
qosrt函数链接
注意事项一:
void* base
①qsort
函数第一参数(数组),为什么其类型为viod*
?
回答:我们会用qsort
函数调用各种类型的数据,使用viod*
的指针,可以接受任意类型的数据。
size_t num
size_t size
②为什么给出了数组总体元素个数num
,还需要单独一个元素的大小size
?
回答:仍旧是上述问题,因为传入的数据类型不定,我们只知道元素个数时,无法判断每个元素的大小。
注意事项二:
int (*compar)(const void*,const void*)
①qsort
函数中的最后一个参数,其是一个函数指针,类型为:int (*)(const void*,const void*)
。说明我们在使用qsort
函数时,最后这个参数是要传入一个函数的地址(函数名/函数指针/等)。这就构成了回调函数。
②我们写的比较函数,其返回值、参数类型需要按照上述函数指针的要求来。
2.7.3.2、qsort函数的使用演示:内置类型、自定义类型
1)、演示一:qsort函数作用于整型数组
#include<stdlib.h>
int cmp_by_intarr(const void* e1, const void* e2)
{
return (*(int*)e1 - *(int*)e2);//排升序
}
void test01()
{
int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), cmp_by_intarr);//比较函数函数名:此处构成回调函数
}
int main()
{
test01();
return 0;
}
注意事项:
①:如何自定义写比较函数?
②:比较函数中的返回值该怎么写?
return (*(int*)e1 - *(int*)e2);
需要注意根据我们的需求,强制类型转换const void*
再进行比较。
2)、演示二:qsort函数作用于结构体数组
#include<stdlib.h>
#include<string.h>
typedef struct stu
{
char name[20];
int age;
double score;
}stu;
int cmp_by_age(const void* e1, const void* e2)
{
return ((stu*)e1)->age - ((stu*)e2)->age;
}
int cmp_by_name(const void* e1, const void* e2)
{
return strcmp(((stu*)e1)->name, ((stu*)e2)->name);
}
void test02()
{
//定义一个结构体数组:数组元素为结构体
stu arr[3] = { {"张龙",20,98},{"王朝",22,92},{"马汉",19,88} };
int sz = sizeof(arr) / sizeof(arr[0]);
//test1:以结构体中成员年龄来排序:
qsort(arr, sz, sizeof(arr[0]), cmp_by_age);
//test2:以结构体中成员名称来排序:
qsort(arr, sz, sizeof(arr[0]), cmp_by_name);
}
int main()
{
test02();
return 0;
}
仍旧需要注意这里比较函数返回值的写法。e1、e2表述数组元素,结构体数组中的元素类型为自定义的结构体,但我们比较时是根据结构体中某一项成员来比较的,因此此处使用了结构体成员访问运算符,另外需要注意的是对字符类型的数据的比较。
return ((stu*)e1)->age - ((stu*)e2)->age;
return strcmp(((stu*)e1)->name, ((stu*)e2)->name);
2.7.3.3、 仿照qsort实现任意类型数据比较的冒泡排序
我们在函数与数组章节中讲解过冒泡排序的设计实现,现在,我们要在此基础上进行进一步扩展:
整体实现:
void swap(char* num1, char* num2, size_t size)
{
for (size_t i = 0; i < size; ++i)
{
//一个字符一个字符的交换
char tmp = *num1;
*num1 = *num2;
*num2 = tmp;
//注意字符指针的迭代
num1++;
num2++;
}
}
void bubble_sort(void* base, size_t num, size_t size, int (*compar)(const void*, const void*))
{
for (size_t i = 0; i < num - 1; ++i)
{
for (size_t j = 0; j < num - 1 - i; ++j)
{
//arr[j]>arr[j+1]
if (compar((char*)base + size * j, (char*)base + size * (j + 1)) > 0)//排升序
{
swap((char*)base + size * j, (char*)base + size * (j + 1),size);//交换元素
}
}
}
}
分细节说明:
step1: 根据之前所学,冒泡排序的基本框架如下:
void bubble_sort(void* base, size_t num, size_t size, int (*compar)(const void*, const void*))
{
for (size_t i = 0; i < num - 1; ++i)//总趟数
{
for (size_t j = 0; j < num - 1 - i; ++j)//单趟
{
if (compar() > 0)//排升序
{
swap();//交换元素
}
}
}
}
1、bubble_sort
中,形参是参考qsort得来的:需要排序的数组,需要排序的元素个数,每个元素的大小,比较函数(回调函数)。这样设计的逻辑是为了实现任意类型的数组比较。因此采用了void*
形式。
2、另外,bubble_sort
中需要注意if语句的实现,此处涉及两个操作:①数组元素比较;②数组元素交换。
step2: compar()
的实现:(以升序为例)
if (compar((char*)base + size * j, (char*)base + size * (j + 1)) > 0)
之前实现的普通版本中,if语句数组元素比较的写法为:if (arr[i] > arr[i + 1])
此处为实现任意类型数组的排序,需要自己实现比较函数,根据比较函数的返回值,>0、<0、=0
来判断。
要比较数组任意下标处的两个元素,而我们知道base
指向数组首元素地址,在指针初阶中,我们学习了指针±整数
的含义,因此此处可以采用base ± size
来进行指针指向的改变。
但直接使用base ± size
是不行的,因为base
指针类型为viod*
,故我们可以将其强制转换类型为char *
,所以得(char*)base + size
。
base ± size
和(char*)base + size
我们能够访问的是数组下标为0和下标为1位置处的元素,如果要访问后续元素,就需要进一步优化±整数的长度
,而刚好for
循环中的j
可以为我们提供这一条件,以达到访问下标为j
和j+1
处元素的目的。
step3: swap()
的实现:
需要注意,因为void*
任意类型,此处swap
交换,我们采取在内存空间中一个字符一个字符的交换,从而达到元素整体进行交换的目的。
void swap(char* num1, char* num2, size_t size)
{
for (size_t i = 0; i < size; ++i)
{
//一个字符一个字符的交换
char tmp = *num1;
*num1 = *num2;
*num2 = tmp;
//注意字符指针的迭代
num1++;
num2++;
}
}
2.8、相关练习
2.8.1、指针和数组理解
1)、前提认识:
数组名通常来说是数组首元素地址,但是有两个例外
1、sizeof(数组名),此处数组名表示整个数组,计算的是整个数组的大小;
2、&数组名,此处的数组名表示整个数组,取出的是整个数组的地址。
2.8.1.1、sizeof与一维数组(int)
题目如下:
int main()
{
int a[] = { 1,2,3,4 };
printf("%d\n", sizeof(a));//1
printf("%d\n", sizeof(a + 0));//2
printf("%d\n", sizeof(*a));//3
printf("%d\n", sizeof(a + 1));//4
printf("%d\n", sizeof(a[1]));//5
printf("%d\n", sizeof(&a));//6
printf("%d\n", sizeof(*&a));//7
printf("%d\n", sizeof(&a + 1));//8
printf("%d\n", sizeof(&a[0]));//9
printf("%d\n", sizeof(&a[0] + 1));//10
return 0;
}
分析如下:
1、sizeof(a)
分析:16,sizeof内单独放数组名,计算的是整个数组的大小
2、sizeof(a + 0)
分析:4/8 ,sizeof内部放的是a+0,非单独数组名,此处a表示首元素地址。a+0:仍旧是首元素地址。
3、sizeof(*a)
分析:4,sizeof内部放的是*a,非单独数组名,此处a表示首元素地址。*a:数组首元素,故此处sizeof(*a)计算的是首元素大小。
4、sizeof(a + 1)
分析:4/8,同sizeof(a + 0),此处计算的是数组第二个元素的地址大小。
5、sizeof(a[1])
分析:4,sizeof内部非单独数组名,a表述首元素地址,a[1]表示数组下标为1的元素,sizeof(a[1])计算的是该元素大小。
6、sizeof(&a)
分析:4/8,&数组名,取出整个数组元素的地址,但其仍旧是地址,因此sizeof计算的是地址大小。
7、sizeof(*&a):
分析:16,&a取到的是整个数组的地址,对其*得到整个数组,故sizeof计算整个数组的大小。
-----------
int b=10;
int* p = &b; //p中存储的是b的地址
*p;//得到的是变量b
//此处数组a和变量b一致。
-----------
8、sizeof(&a + 1):
分析:4或8,&a表示整数数组的地址,+1跳过整个数组,得到后续内存空间的地址,但sizeof仍旧计算的是地址大小。
9、sizeof(&a[0]):
分析:4或8,对数组首元素取地址,sizeof计算的是数组首元素地址的大小。
10、sizeof(&a[0] + 1):
分析:4或8,sizeof计算的是数组第二个元素地址的大小
结果如下:
2.8.1.2、sizeof与一维数组(char)(一)
题目如下:
int main()
{
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", sizeof(arr));//1
printf("%d\n", sizeof(arr + 0));//2
printf("%d\n", sizeof(*arr));//3
printf("%d\n", sizeof(arr[1]));//4
printf("%d\n", sizeof(&arr));//5
printf("%d\n", sizeof(&arr + 1));//6
printf("%d\n", sizeof(&arr[0] + 1));//7
return 0;
}
分析如下:
char arr[] = { 'a','b','c','d','e','f' };
,实际数组中存储的是'a','b','c','d','e','f'
。
1、sizeof(arr)
分析:6,sizeof内单独放置数组名,计算整个数组的大小。
2、sizeof(arr + 0)
分析:4/8 ,sizeof内部放的是arr+0,非单独数组名,此处arr表示首元素地址。arr+0:仍旧是首元素地址。
3、sizeof(*arr)
分析:1,sizeof内部放的是*arr,非单独数组名,此处arr表示首元素地址。*arr:数组首元素,故此处sizeof(*arr)计算的是首元素大小。
4、sizeof(arr[1])
分析:1,sizeof内部非单独数组名,arr表述首元素地址,arr[1]表示数组下标为1的元素,sizeof(arr[1])计算的是该元素大小。
5、sizeof(&arr)
分析:4/8,&数组名,取出整个数组元素的地址,但其仍旧是地址,因此sizeof计算的是地址大小。
6、sizeof(&arr + 1)
分析:4或8,&arr表示整个数组的地址,+1跳过整个数组,得到后续内存空间的地址,但sizeof仍旧计算的是地址大小。
7、sizeof(&arr[0] + 1)
分析:4或8,sizeof计算的是数组第二个元素地址的大小
结果如下:
2.8.1.3、strlen与一维数组(char)(一)
一些前提先知:
我们模拟实现过strlen函数,其参数是指针,指向内存地址空间。
题目如下:
int main()
{
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", strlen(arr));//1
printf("%d\n", strlen(arr + 0));//2
printf("%d\n", strlen(*arr));//3
printf("%d\n", strlen(arr[1]));//4
printf("%d\n", strlen(&arr));//5
printf("%d\n", strlen(&arr + 1));//6
printf("%d\n", strlen(&arr[0] + 1));//7
return 0;
}
分析如下:
1、strlen(arr)
分析:随机值,arr是数组首元素地址,strlen统计直至遇到‘\0’前的字符个数。
2、strlen(arr + 0)
分析:随机值,arr是数组首元素地址,+0效果不变,strlen统计直至遇到‘\0’前的字符个数。
3、strlen(*arr)
分析:错误,非法访问。
----------
arr是数组首元素地址,*arr得到数组首元素,即字符‘a’,对应AScii码为97,
strlen从地址编号为97处开始计数直至遇到‘\0’,属于非法访问。
----------
4、strlen(arr[1])
分析:效果同上,属于非法访问。
5、strlen(&arr)
分析:随机值,&arr取出的是整个数组的地址,但其和首元素地址相同(我们验证过,同时演示了+1后的效果),
----------
strlen不像sizeof计算数组大小,故其仍旧从arr首元素开始,向后统计直至遇到‘\0’前的字符个数。
----------
6、strlen(&arr + 1)
分析:随机值,但与上述5中相差6个字符。因为&arr取出的是整个数组的地址,+1跳过整个数组。
7、strlen(&arr[0] + 1)
分析:随机值,但与上述5中相差1个字符。因为&arr[0]取出的是数组首元素的地址,+1跳过一个元素。
结果如下:
2.8.1.4、sizeof与一维数组(char)(二)
题目如下:
int main()
{
char arr[] = "abcdef";
printf("%d\n", sizeof(arr));//1
printf("%d\n", sizeof(arr + 0));//2
printf("%d\n", sizeof(*arr));//3
printf("%d\n", sizeof(arr[1]));//4
printf("%d\n", sizeof(&arr));//5
printf("%d\n", sizeof(&arr + 1));//6
printf("%d\n", sizeof(&arr[0] + 1));//7
return 0;
}
分析如下:
char arr[] = "abcdef";
实际数组中存储的是'a','b','c','d','e','f' ,'\0'
,共七个字符。
1、sizeof(arr)
分析:7,sizeof内单独放置数组名,计算整个数组的大小。
2、sizeof(arr + 0)
分析:4/8 ,sizeof内部放的是arr+0,非单独数组名,此处arr表示首元素地址。arr+0:仍旧是首元素地址。
3、sizeof(*arr)
分析:1,sizeof内部放的是*arr,非单独数组名,*arr得到的是数组首元素,sizeof计算数组首元素大小。
4、sizeof(arr[1])
分析:1,同上,sizeof计算的是数组下标为1的元素大小。
4、sizeof(arr[1])
分析:1,同上,sizeof计算的是数组下标为1的元素大小。
5、sizeof(&arr)
分析:4/8,&arr得到的是整个数组的地址,sizeof计算的是整个数组的地址大小,但其仍旧是地址。
6、sizeof(&arr + 1)
分析:4/8,&arr得到的是整个数组的地址,+1跳过整个数组,但sizeof计算的仍旧是地址。
7、sizeof(&arr[0] + 1)
分析:4/8,&arr得到的是数组首元素的地址,+1到数组下一个元素地址,但sizeof计算的仍旧是地址。
结果如下:
2.8.1.5、strlen与一维数组(char)(二)
题目如下:
int main()
{
char arr[] = "abcdef";
printf("%d\n", strlen(arr));//1
printf("%d\n", strlen(arr + 0));//2
printf("%d\n", strlen(*arr));//3
printf("%d\n", strlen(arr[1]));//4
printf("%d\n", strlen(&arr));//5
printf("%d\n", strlen(&arr + 1));//6
printf("%d\n", strlen(&arr[0] + 1));//7
return 0;
}
分析如下:
1、strlen(arr)
分析:6,arr是数组首元素地址,strlen统计直至遇到‘\0’前的字符个数。
2、strlen(arr + 0)
分析:6,arr是数组首元素地址,+0效果不变,strlen统计直至遇到‘\0’前的字符个数。
3、strlen(*arr)
分析:错误,非法访问。
----------
arr是数组首元素地址,*arr得到数组首元素,即字符‘a’,对应AScii码为97,
strlen从地址编号为97处开始计数直至遇到‘\0’,属于非法访问。
----------
4、strlen(arr[1])
分析:效果同上,属于非法访问。
5、strlen(&arr)
分析:6,&arr取出的是整个数组的地址,但其和首元素地址相同(我们验证过,同时演示了+1后的效果),
----------
strlen不像sizeof计算数组大小,故其仍旧从arr首元素开始,向后统计直至遇到‘\0’前的字符个数。
----------
6、strlen(&arr + 1)
分析:随机值,&arr取出的是整个数组的地址,+1跳过整个数组,strlen从这里开始向后统计至遇到‘\0’前的字符个数。
7、strlen(&arr[0] + 1)
分析:5,&arr[0]取出的是数组首元素的地址,+1跳过一个元素,strlen从这里开始向后统计至遇到‘\0’前的字符个数。
结果如下:
2.8.1.6、sizeof与一维数组(char)(三)
题目如下:
int main()
{
char* p = "abcdef";
printf("%d\n", sizeof(p));//1
printf("%d\n", sizeof(p + 1));//2
printf("%d\n", sizeof(*p));//3
printf("%d\n", sizeof(p[0]));//4
printf("%d\n", sizeof(&p));//5
printf("%d\n", sizeof(&p + 1));//6
printf("%d\n", sizeof(&p[0] + 1));//7
return 0;
}
分析如下:
char* p = "abcdef";
,有一块内存空间存放的是'a','b','c','d','e','f' ,'\0'
,有一个指向该内存空间首地址的指针p,能访问一个字节。
1、sizeof(p)
分析:4/8,p是指针,sizeof计算指针变量的大小。
2、sizeof(p + 1)
分析:4/8,p是指针,p+1向后走一个字节仍旧是指针,指向数组第二个元素,sizeof计算指针变量的大小。
3、sizeof(*p)
分析:1,*p得到的是数组首元素,sizeof计算的是数组首元素大小。
4、sizeof(p[0])
分析:1,同上。
5、sizeof(&p)
分析:4/8,p是指针,&p得到的是p的地址(二级指针),sizeof计算的是地址的大小。
6、sizeof(&p + 1)
分析:4/8,p是指针,&p得到的是p的地址(二级指针),&p+1,跳过p但仍旧是地址,sizeof计算的是地址的大小。
7、sizeof(&p[0] + 1)
分析:4/8,&p[0]是数组首元素地址,+1是数组第二个元素地址,sizeof计算的是地址的大小。
结果如下:
2.8.1.7、strlen与一维数组(char)(三)
题目如下:
int main()
{
char* p = "abcdef";
printf("%d\n", strlen(p));//1
printf("%d\n", strlen(p + 1));//2
printf("%d\n", strlen(*p));//3
printf("%d\n", strlen(p[0]));//4
printf("%d\n", strlen(&p));//5
printf("%d\n", strlen(&p + 1));//6
printf("%d\n", strlen(&p[0] + 1));//7
return 0;
}
分析如下:
1、strlen(p)
分析:6,strlen参数为char*,p是首元素地址,故strlen从p指向位置起,统计直至遇到‘\0’前的字符个数。
2、strlen(p + 1)
分析:5,strlen从p+1指向位置起,统计直至遇到‘\0’前的字符个数。
3、strlen(*p)
分析:非法访问,*p得到字符'a',strlen从其ascill码对应地址开始访问
4、strlen(p[0])
分析:非法访问,p[0]得到字符'a',strlen从其ascill码对应地址开始访问
-----------
5、strlen(&p)
分析:随机值,&p得到p的地址,strlen从p所在地址处开始,统计直至遇到‘\0’前的字符个数。
6、strlen(&p + 1)
分析:随机值,&p得到p的地址,+1跳过p,strlen其后地址处统计直至遇到‘\0’前的字符个数。
-----------
7、strlen(&p[0] + 1)
分析:5,&p[0]得到的是数组首元素地址,+1到数组第二个元素,strlen统计直至遇到‘\0’前的字符个数。
结果如下:
2.8.1.8、sizeof和二维数组(int)
对二维数组的理解:
1、可以把二维数组想象成一维数组,则二维数组的每个元素是一行一维数组。
2、二维数组数组名仍旧遵循两个特例。sizeof(数组名)、&数组名
3、除了上述特例外,二维数组数组名表示数组首元素地址,这里的首元素地址是二维数组第一行元素整体地址。
题目如下:
int main()
{
int a[3][4] = { 0 };
printf("%d\n", sizeof(a));//1
printf("%d\n", sizeof(a[0][0]));//2
printf("%d\n", sizeof(a[0]));//3
printf("%d\n", sizeof(a[0] + 1));//4
printf("%d\n", sizeof(*(a[0] + 1)));//5
printf("%d\n", sizeof(a + 1));//6
printf("%d\n", sizeof(*(a + 1)));//7
printf("%d\n", sizeof(&a[0] + 1));//8
printf("%d\n", sizeof(*(&a[0] + 1)));//9
printf("%d\n", sizeof(*a));//10
printf("%d\n", sizeof(a[3]));//11
return 0;
}
分析如下:
1、sizeof(a)
分析:48,二维数组数组名,单独放置在sizeof内部,表示整个数组的地址,计算整个数组的大小。
2、sizeof(a[0][0])
分析:4,sizeof计算二维数组(0,0)位置处元素大小。
-----------
3、sizeof(a[0])
分析:16,二维数组中,a[0]可以表示第一行的数组名,sizeof(a[0])表示将第一行数组名单独放在sizeof中,则计算的是整个第一行的大小。
4、sizeof(a[0] + 1)
分析:4/8,二维数组中,a[0]可以表示第一行的数组名,
sizeof(a[0]+1),并没有把第一行数组名单独放入sizeof中,则a[0]+1表示第一行第二个元素的地址,sizeof计算的是地址大小。
5、sizeof(*(a[0] + 1))
分析:4,根据4,对a[0] + 1解引用,得到第一行第二个元素,sizeof计算的是数组元素大小。
-----------
-----------
6、sizeof(a + 1)
分析:4/8,a是二维数组的数组名,但没有单独放置在sizeof中,则表述二维数组首元素地址,
在二维数组为第一行元素地址,+1跳过一整行,的带二维数组第二行整体元素地址,sizeof计算的是地址大小。
7、sizeof(*(a + 1))
分析:16,根据6,对第二行整体元素地址解引用,得到的是第二行元素,sizeof计算的是第二行整体元素的大小。
另解:a[1]
-----------
-----------
8、sizeof(&a[0] + 1)
分析:4/8,a[0]是第一行的数组名,没有单独放置在sizeof中,但对数组名进行了取地址操作,故得到的是第一行整体地址,+1跳过一整行,
sizeof计算的是第二行的地址大小。
9、sizeof(*(&a[0] + 1))
分析:16,对第二行的地址解引用,拿到的是第二行整体元素。
-----------
10、sizeof(*a)
分析:16,a没有单独放入sizeof内部,也没有对它&,那么其表示二维数组首元素地址,即二维数组第一行地址,解引用,得到的是整个第一行。
另解:*a == *(a+0) == a[0]
11、sizeof(a[3])
分析:16,不会实际访问,但能知道对应类型及与大小。
结果如下:
2.8.1.9、一个小结
sizeof
是一个操作符
计算的是对象所占内存的大小,单位是字节,返回类型size_t
,不在乎内存中存放的是什么,只在乎内存大小。
strlen
是一个库函数
用于求字符串长度,从给定的地址向后访问字符,统计\0
之前出现的字符个数。
2.8.2、指针相关笔试题
2.8.2.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
得到整个数组的地址,类型为int(*)[5]
,&a + 1
跳过整个数组,又将其强制类型转换为int*
,ptr
指针的访问权限是int
大小。故ptr - 1
得到的是数组最后一个元素的地址,解引用得到数组最后一个元素。
②a
表述数组首元素地址,+1到数组下一个元素地址,解引用得到数组第二个元素。
2.8.2.2、练习二
以下程序执行结果是什么?
//这里告知结构体的大小是20个字节
struct Test
{
int Num;
char* pcName;
short sDate;
char cha[2];
short sBa[4];
}*p;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
//已知,结构体Test类型的变量大小是20个字节
int main()
{
p = (struct Test*)0x100000;
printf("%p\n", p + 0x1);
printf("%p\n", (unsigned long)p + 0x1);
printf("%p\n", (unsigned int*)p + 0x1);
return 0;
}
分析:
此题考查的是指针±整数的含义。
p + 0x1
①p
表示一个结构体指针,对应类型为 struct Test *
,控制着该指针p的访问权限。② 0x1
十六进制转换后为十进制1,则此处为指针加减整数,+1跳过对应类型大小的内存单元,即题目给出的20字节。③但打印时是以十六进制显示的,故最后需要注意输出结果的类型转换。
(unsigned long)p + 0x1
①(unsigned long)p
强制类型转换,将指针变量p从结构体指针转换为无符号长整型,p从而变成了一个整型变量(内存空间中的0x100000
不变,只是被视为了对应的整数)②整数±整数,是算术运算。③最后%p
以地址的形式打印,注意十六进制。
(unsigned int*)p + 0x1)
①(unsigned int*)p
强制类型转换,将结构体指针变量转换为无符号整型指针变量,内存大小访问权限改变。②(unsigned long)p + 0x1
为指针运算,+1
32位下跳过unsigned int*
四字节的内存单元。③注意最后以地址的形式打印,且为十六进制形式。
2.8.2.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;
}
分析:
2.8.2.4、练习四
以下程序执行结果是什么?
#include <stdio.h>
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), (2, 3), (4, 5) }
使用的是()
,在这里是逗号表达式。故实际上数组初始化结果为{ (1, 3, 5 }
。
2.8.2.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;
}
分析:
int(*p)[4]
,这里告诉我们p是一个数组指针,指向元素个数为4,类型为int的数组,p+1则跳过int(*)[4]
,即4个数组元素大小。
int a[5][5]
,则表示这个二维数组为五行五列。故p[4][2]
和a[4][2]
所指向的位置是不同的。
%p,%d
:题目要求以不同的形式打印,我们计算出的结果-4
,若是以%p
的形式打印,则将内存中的补码看做一个地址,若是以%d
的形式打印,则打印的是-4
的原码,即本身。
2.8.2.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;
}
分析:
要注意&aa
和aa
的含义,此外就是强制类型转换int*
后,ptr1、ptr2
所能访问的内存大小权限。
2.8.2.7、练习七
以下程序执行结果是什么?
#include <stdio.h>
int main()
{
char* a[] = { "work","at","alibaba" };
char** pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}
分析:
char** pa = a
,这里a是首元素地址。
2.8.2.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;
}
输出结果:
分析:
char* c[] = { "ENTER","NEW","POINT","FIRST" };
char** cp[] = { c + 3,c + 2,c + 1,c };
char*** cpp = cp;
printf("%s\n", **++cpp);
cpp
先自增,再解引用两次:
printf("%s\n", *-- * ++cpp + 3);
优先级:(*(-- (*( ++cpp)))) + 3
1、cpp先自增,再解引用得到cp
2、cp自减,再解引用,得到c
3、对c+3
printf("%s\n", *cpp[-2] + 3);
1、先找到cpp[-2]
,再解引用得到cp
2、对得到的cp+3,打印对应指针指向的字符串
注意,上述*cpp[-2]
,只是通过指针访问到*(cpp-2)
的位置,实际上并没有改变cpp原先指向。(不像自增自减,确切改变变量本身)
printf("%s\n", cpp[-1][-1] + 1);
1、cpp[-1][-1]
实际为:(*(cpp-1)-1)
2、cpp先-1访问到对应的cp,cp再-1访问到对应的c,对应的c+1打印。