目录
一.补充知识
(一)二级指针
二级指针是一种特殊的指针变量,与一级指针(普通指针)所存储变量的地址相一致的是,二级指针存储的是对应一级指针的地址。可以说,二级指针指向一级指针,一级指针指向对应变量。通过对二级指针的解引用可以得到一级指针,从而去修改一级指针的内容(变量a的地址)。
语法:以字符类型指针为例,二级指针的类型写做char**。
例子:
#include<stdio.h>
int main()
{
int a=20;
int* pa=&a;
int** ppa=&pa;
//*ppa == pa
//**ppa == a
printf("**ppa=%d\n", **ppa);//打印出的结果是20
return 0;
}
他们的关系如下图(地址是随便写的):
其中,二级指针ppa地址未给出,存储内容是0x1234;一级指针地址为0x1234,存储内容是0x9876;变量a地址为0x9876,存储内容是20。
类似二级指针的定义,可以衍生出三级指针,存储的是二级指针的地址,写做char***;四级指针,存储的是三级指针的地址,写做char****等等...但从后续应用上来说,三级之后的指针几乎不会被使用了。(注:二级指针和二维数组没有对应关系)
多级指针实质上是一种指向指针值的指针。
(二)数组名在不同场合的含义
正常情况下,单独的数组名表示的都是数组首元素的地址。如一个数组arr[10],arr等价于&arr[0]。也就是说,arr的类型是一种指针(如果arr[10]是整型数组,那么arr就是整型指针),存储的是数组的首元素。那么它也应该有指针的一些特性(用%p打印其存储的地址,对其进行解引用操作,进行指针+1-1操作)。下面这一段代码验证了这一点:
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%p\n", arr);//这里的arr相当于&arr[0]
printf("%d\n", *arr);//对数组第一个元素的地址进行解引用//1
printf("%d\n", *(arr+1));//对数组第二个元素的地址进行解引用//2
printf("%d\n", *(arr+2));//对数组第三个元素的地址进行解引用//3
return 0;
}
但在两种情况下,数组名arr表示的并不是数组首元素的地址:
- .sizeof(arr):这里的数组名是表示整个数组,计算的是整个数组的大小,单位是字节
- &arr:这里的数组名也表示整个数组,取出的是整个数组的地址
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
printf("%p\n",&arr[0]); //a(与b是等价的)
printf("%p\n",arr); //b
printf("%d\n", sizeof(arr));//这里的数组名arr代表的是整个数组 //c
printf("%p\n",&arr);//这里的数组名arr代表的也是整个数组 //d
return 0;
}
第一点很好理解,sizeof计算的是后面arr数组的总大小,此时arr表示整个数组。通过打印的结果可以看到a,b和d的结果是一致的,那整个数组的地址和数组首元素地址有什么区别呢?
实际上,&arr对应一种特殊的指针类型——数组指针。它指向的是整个数组,当这种指针+1或者-1的时候,跨过的是整个数组。可以从下边这个例子看出&arr的含义:
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("arr =%p\n", arr);
printf("&arr =%p\n", &arr);//数组的地址
printf("变化后\n");
printf("arr+1 =%p\n", arr+1);
printf("&arr+1 =%p\n", &arr+1);//数组的地址
//arr本质是数组首元素地址,+1跳过一个整型大小
//&arr到&arr+1 跳过了整个数组,10个整型,40个字节
return 0;
}
x86环境下的打印结果:
从本次打印结果可以看出,从00FBFA70到00FBFA74跳过4个字节;从00FBFA70到00FBFA98跳过40个字节(00开头代表八进制,其实跳过2*8^1+8*8^0=40个字节)。
对于广义上的sizeof(m)表达式,其中m也是一个表达式,这个表达式的值会作为一个地址传给sizeof进行运算。一般情况下,我们计算的都是sizeof(arr)这样可以直接看出是数组大小的表达式,但有时候,m的位置经过运算后是一个数字,此时就是把这个数字当作一个地址送给sizeof运算,这种情况下,经常出现访问位置地址而引发异常的现象,容易造成程序崩溃。
(三)sizeof和strlen
1.sizeof是一种操作符,sizeof 计算变量所占内存内存空间大小的,单位是字节,如果操作数是类型的话,计算的是使⽤类型创建的变量所占内存空间的大小。sizeof 只关注占用内存空间的大小,不在乎内存中存放什么数据。因此,sizeof后面的表达式是不会真实计算的,例如:
int main()
{
short s = 8;//短整型占两个字节
int n = 12;
//下面测试sizeof(s = n+5)这个表达式的值
printf("sizeof(s = n + 5)=%zd\n", sizeof(s =n + 5));
printf("s=%d\n", s);
//输出结果是什么呢
return 0;
}
打印的结果是:
如果sizeof里面的表达式真实计算,s会变成17,后续输出s的值将会是17,但最终结果是8,仍是s的初始值,即这个表达式并没有计算。
既然表达式“s=n+5"并没有算,怎么知道sizeof的结果呢?
n和5都是整型,而s是短整型类型,表达式把n和5两个整形的和赋值给s,因此这个表达式的最终结果是s决定的,长度是s的长度两个字节。因此sizeof(s = n+5)的结果是2。
2.strlen 是C语言的库函数,功能是求字符串长度。函数原型如下:
size_t strlen ( const char * str );
strlen统计的是 从 strlen 函数的参数 str 存的这个地址开始 向后直到‘ \0 ’出现之前 字符的个数。strlen 函数会⼀直向后找 \0 字符,直到找到为止,所以可能存在越界查找。
最后,做好sizeof和strlen的区分需要进行一些相关笔试题的专题训练。
二.指针和数组的关系
(一)下标访问符[ ]和解引用符*
1.[ ]和*的关系
Ⅰ.前面的补充知识里面提到,数组名arr表示的是数组首元素的地址(除sizeof(arr)和&arr外)。也就是说,数组名的本质是一种指针。我们在初学数组的时候知道,可以用arr[i]来访问arr数组的第i个元素。实际上,如果将arr赋值给一个指针p,同样也能用p[i]来访问数组的元素。
#include<stdio.h>
int main()
{
//使用指针
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);//计算数组元素的个数
int* p = arr;
for (int i = 0; i < sz; i++)
{
printf("%d ", p[i]);
}
printf("\n");
//实际上,下表访问操作符配合数组名还有一种比较少见的写法
for (int i = 0; i < sz; i++)
{
printf("%d ",i[p]);
}
printf("\n");
return 0;
}
打印结果:
Ⅱ.在进行数组元素访问的时候,我们通常采用下标访问操作符[],但实际上,计算机真实进行运算的时候,总是会把[]转化为解引用操作符*。
#include<stdio.h>
int main()
{
//使用指针
int arr[10] = { 0 };//下面将使用手动输入数组元素
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr;
for (int i = 0; i < sz; i++)
{
/*scanf("%d", p + i);*/ //在前面已经知道arr和p其实是一模一样的东西了,因此后面仅用arr
scanf("%d", arr + i);//首元素的地址+i(指针)
}
//产生一行输入
for (int i = 0; i < sz; i++)
{
/*printf("%d ", *(p + i));*/
printf("%d ", *(arr + i));
}
//产生一行输出
return 0;
}
键盘输入1 2 3 4 5 6 7 8 9 10 ,打印结果:
和上面一次输出了同样的结果,说明*(p+i)等价于p[i]等价于i[p]。
//总结: *(arr+i) == arr[i]
//编译器运算时,也是转化为指针来运算的,即化成左边那个
//*(arr+i) == *(i+arr) == i[arr]
// arr[i] == i[arr]
// []仅仅是操作符
值得一提的是,因为下标访问操作符实质上被系统替换成了解引用操作符的格式,因此,形如arr[-2]的形式,其等价的形式就是*(arr-2),这也是一种合法的表达。这是我们先前仅仅从数组层面理解时无法理解的。
2.[ ],*操作符的链式访问
在实际的指针运算中,[ ],*操作符的使用往往不像1.里面介绍的那样单纯。操作符[ ],*实际上是存在链式访问的,因为这部分将会涉及到数组指针和字符指针,如还不了解数组指针和字符指针可以跳转三.(一)——字符指针和三.(二)——数组指针。
来看下面几个例子:
int main()
{
int arr1[][3] = { {1,2},{3,4},{5,6} };//这是一个二维数组
char* arr2[] = { "abcde","fghi" };//这是一个指针数组
char** pa[] = { arr2 + 1,arr2 };//二级指针数组pa
char*** ppa = pa;//三级指针ppa
//如果我们要访问arr1里的元素4,我们要怎么做呢?
printf("访问 4 有以下几种方式:\n");
printf("%d\n", arr1[1][1]); //代码1
printf("%d\n", *(*(arr1 + 1) + 1));
printf("%d\n", *(arr1[1] + 1));
printf("%d\n", (*(arr1 + 1))[1]);
//如果我们要打印arr2数组里的部分"hi",我们要怎么做呢?
printf("打印 'hi' 有以下几种方式:\n");
printf("%s\n", **ppa+2 );
printf("%s\n", ppa[0][0] + 2);//更多版本可看字符指针后的例题③ //代码2
return 0;
}
这组代码中,我们以代码1和代码2为例,其他的都可由前面[ ]和*的关系导出。
代码1中,我们可以把arr1[1][1]拆开看,先是arr1[1]得到的其实也是一个数组{3,4}.此时我们可以再利用数组名+[访问元素序号]的形式再次用[1]得到4;
代码2中,ppa[0]等价于pa[0]即arr2+1,得到了"fghi"这个数组,使用[0]得到首元素f地址,+2访问到了元素h,打印"hi"。
(二)一维数组的传参本质
之前学习一个函数调用一维数组数组的时候,我们是这样做的
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
func(arr);//调用func函数
在学习函数传参的时候我们知道,传入的参数是实参,而.c文件在函数参数调用的时候会创建形参,形参是实参的一份临时拷贝。那我们往函数参数传入数组,编译器会再次拷贝一份数组吗?
事实上,学完前面的前置知识我们就知道了,这里的arr是一个指针,其存储内容是数组首元素的地址。后续在函数调用时,拷贝的形参也是一个指针,并不像我们之前想的一样再次创建了一个数组。下面这个例子也佐证了这一点:
void compare_sz1_sz2(int arr[])
{
int sz2 = sizeof(arr)/sizeof(arr[0]);
printf("sz2=%d\n", sz2);
//如果函数传参的时候传入的是一整个数组,那sz1应该等于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);
compare_sz1_sz2(arr);//输出sz2的大小
return 0;
}
x86环境下输出结果:
由结果可以看出,compare_sz1_sz2函数并没有获得真正的数组元素个数。其得到的结果1实际上是sizeof(arr)/sizeof(arr[0])得出的,因为在x86环境下,指针arr的大小是4字节,整型arr[0]大小也是4字节,4/4=1。
这样以后,上面的compare_sz1_sz2函数的函数定义也可以写做
void compare_sz1_sz2(int* arr);
//参数列表的int arr[],int* arr是等价的
总之,一维数组传参本质上传递的是数组首元素的地址,传递的是指针。
(三)二维数组的传参本质
因为这部分将会涉及到数组指针,如还不了解数组指针可以跳转三.(二)——数组指针。
所谓二维数组,实质上是一种一维数组的数组。即二维数组的每一个元素都是一个一维数组。参照一维数组,二维数组的数组名也代表数组首元素的地址,即第一个一维数组的地址。
如下面这个数组,数组名arr即首元素地址就是{1,2}这个数组的地址。
int arr[3][5] = {{1, 2}, {3, 4}, {5, 6}};
因此二维数组在传参的时候,传入的参数arr的本质就是一个一维数组的地址。这个arr的本质就是一个数组指针(我们在后面会更详细地讨论这种指针)。因此,在再次对其传参本质的案例分析时,就需要动用数组指针,它的基本语法为(以一个有5个元素的数组对应的指针为例):
int (*arr)[5];
来看下面这个例子:
void print(int arr[3][5], 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");
}
}
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;
}
打印结果:
将print函数的形参列表换成数组指针,也能得到相同的结果。
void print(int (*arr)[5], 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");
}
}
打印结果和第一种情况一致。
再使用指针的方式替换下标访问操作符[]:
//arr+0
//arr arr+1 arr+2 arr+i
// ↓ ↓ ↓ ↓
// [1,2,3,4,5][2,3,4,5,6][3,4,5,6,7]
//写成指针形式
void print(int(*arr)[5], int r, int c)
{
for (int i = 0; i < r; i++)
{
for (int j = 0; j < c; j++)
{
printf("%d ", *((*(arr + i)) + j));
//*(arr+i) == arr[i]
//*((*(arr+i))+j) == arr[i][j]
}
printf("\n");
}
}
打印结果和第一种情况一致。
总之,二维数组的传参本质也是指针,只不过改成数组指针了。
三.几种特殊类型的指针及其语法
(一)字符指针
在前面我们已经知道,字符指针是存储字符变量char地址的一种指针变量。
1.字符数组和字符串
简单回顾前面我们对于字符数组和字符串的定义:
//字符数组
char arr1[]={'a','b','c','d','e'};//由字符组成的普通数组
//字符串
char arr2[]="abcde";//一种类似数组数据形式,'\0'是字符串的结束标志
这两个定义中,数组arr1有5个元素,而数组arr2有6个元素,包含一个'\0'。因为在之前大多的应用中,字符串和字符数组的应用情景相似。因此字符串可以视为一种特殊的,写法更方便的字符数组。使用字符串能更方便的使用strlen计算元素个数,而使用字符数组还需要手动往数组添加元素'\0'。
从上面这两种数据类型:字符数组和字符串,可以衍生出第三种数据类型:字符串数组,即数组里面的元素是字符串。但因为它在赋值上具有特殊性,将在第4部分涉及。
2.字符指针的初始化和字符串的存储
字符指针的基本使用方法是这样的:
int main()
{
char c='c';
char* pc=&c;
*pc='d';
return 0;
}
这里我们把字符c的地址赋值给字符指针pc;今天我们要提到的是下面这种赋值方式:
int main()
{
char* pa="abcde";
printf("%s\n",pa);
return 0;
}
这样的赋值方式有什么含义呢?是把一个字符串赋值给一个指针吗?
后面的打印代码输出了字符串的值——“abcde”。其实这样的赋值操作是将字符串首字符'a'的地址赋值给字符型指针pa。
以上的操作从另一个角度来看便是借助一个字符指针pa“储存”了一个常量字符串。这里提到了一个词——常量,没错,字符串实际上是一种只读常量,是不可被修改的。于是我们也不希望字符指针pa对其进行修改,所以上面对字符指针的赋值操作的规范写法应是。
const char* pa="abcde";
回到正题,我们比对一下我们已经涉及的存储字符串的两种形式:
const char* pa="abcde";//1
char a[]="abcde";//2
对于第一种存储方式,本质上我们在内存中开辟了1个字节的空间给指针,存储了只读常量字符串首字符的地址;而对于第二种存储方式,我们在内存中开辟了连续的6个字节的空间给数组,并分批存入了6个字符:'a','b','c','d','e','\0'。下面有一张简单的示意图:
下面这个例子能够加强对这两种存储方式的理解:
int main()
{
char str1[] = "abcde";
char str2[] = "abcde";
const char *str3 = "abcde";
const char *str4 = "abcde";
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;//例子来自于《剑指offer》,改编于比特就业课
}
输出结果:
造成这个结果的原因是:
①str3和str4是两个指向同一字符串“abcde”的字符指针。而这个字符串存储于内存中的一个只读数据段中。这个数据段位于全局区,其生命周期是与整个程序一样的。因此str3和str4指向的实际上是同一块内存空间。
②str1和str2本质上是两个数组。其内容是由包含'\0'在内的六个字符组成的。这两个数组在内存中的栈区分别创建。数组名str1和str2表示的地址,即两个数组的首元素地址,必定是不一样的。
3.浅谈数据在内存中的存储
在C语言中,前面提到的字符串存储在文字常量区或只读数据段中。这是一块非常特殊的区域。那么内存中存储数据的区域是如何划分的呢?
C语言程序分配的内存区域:
- 栈区(stack):主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等
- 堆区(heap):一般由程序员手动创建手动释放,如果程序员在程序结束前未释放由OS回收。
- 数据段(静态区)(data segment):存放全局变量,静态数据(包含只读常量,如字符串常量),程序结束后由系统释放。
- 代码段(text segment):存放函数体(全局函数,类成员函数)的二进制代码。
4.例题
①求代码的输出。(递归函数)
//改编自牛客网
void print(char* s)
{
if (*s)
{
print(++s);
printf("%c", *s);
}
}
int main()
{
char str[] = "Geneius";
print(str);
return 0;
}
解析:当str指向'\0'时,递归停止。开始打印字符,循环从内层到外层,因此打印suiene。注意不打印G,因为函数调用里一开始指针s就++了。print函数里面的递归逻辑如下:
void print(char* s)
{
if (*s) //G
{
s++;
if (*s)//e
{
s++;
if (*s)//n
{
s++;
if (*s)//e
{
s++;
if (*s)//i
{
if (*s)//u
{
s++;
if (*s)//s
{
s++;
//if (*s)//\0,ASCII码为0,这层循环不执行
//{
// printf("%c", *s);
//}
printf("%c", *s);//s
}
printf("%c", *s);//u
}
printf("%c", *s);//i
}
printf("%c", *s);//e
}
printf("%c", *s);//n
}
}
printf("%c", *s);//e
}
}
②求代码的输出。(字符串数组对应由字符指针数组进行接收,是指针试题的经典)
//链接:https://www.nowcoder.com/questionTerminal/3477151994f447b2ad5c62ce7324acba
//来源:牛客网
int main()
{
char *str[3] ={"stra", "strb", "strc"};
char *p =str[0];
int i = 0;
while(i < 3)
{
printf("%s ",p++);
i++;
}
return 0;
}
解析:i=0时,p指向第一个字符串"stra"的首字符s,打印stra;然后p移至"stra"的第二个字符t
i=1时,p指向"stra"的第二个字符t,打印tra;然后p移至"stra"的第三个字符r
同理,最后打印结果:stra,tra,ra
注意①p的类型是字符指针,+1只跳过一个字节;②循环只进行3次
③求代码的输出(本题涉及字符指针数组)
//题目来自比特就业课
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;
}
画图理出上述三级指针,二级指针,一级指针的关系(每个箭头均表示一个指针):
Ⅰ.printf("%s\n", **++cpp);
解析: cpp一开始指向cp数组的第一个元素,++后指向第二个,两次解引用后打印“POINT”。
执行完这一步操作后,各级指针位置如下:
Ⅱ.printf("%s\n", *-- * ++cpp + 3);
解析:cpp++后指向cp数组的第三个元素(注意前面cpp指针指向已经变化了),解引用一次指向c数组的第二个元素,进行--操作后该处指针指向c数组的第一个元素,再次解引用指向"ENTER"的首字符‘F’,+3最后计算,指针访问"ENTER"的第四个字符'E'处,打印“ER”。
执行完这一步操作后,各级指针位置如下:
Ⅲ.printf("%s\n", *cpp[-2] + 3);
解析:首先需要明确的是,cpp[-2]等价于*(cpp-2),也就是指针cpp从原本指向cp数组的第三个元素,-2并解引用访问cp数组的第一个元素,进行两次次解引用后,指针指向字符串“FIRST”的首元素,最后+3,指针访问字符'S'处,打印“ST”。
执行完这一步操作后,各级指针位置没有变化。
Ⅳ.printf("%s\n", cpp[-1][-1] + 1);
解析:这一步使用了一个类似二维数组的形式,我们可以逐层拆解,可将cpp[-1][-1]化为*(*(cpp-1)-1)。需要注意的是,上一步完成后,cpp没有自增自减,cpp仍指向cp数组的第三个元素。因此,cpp执行-1操作,解引用访问cp数组第二个元素,此时调用此处指针,再执行-1操作访问c数组第二个元素,解引用访问字符串“NEW”的第一个元素,最后的最后,执行+1操作访问字符'E',打印“EW”。
执行完这一步操作后,各级指针位置没有变化。
(二)数组指针
1.数组指针和指针数组的区分
数组指针和指针数组的形式是很相似的:
char* p[20];//1
char (*p)[20];//2
上面这段代码中,1是有10个字符指针的指针数组,2是指向一个10个元素的字符数组的数组指针。之所以2的形式里要把“*p”括起来,是因为操作符的优先级问题:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。
2.数组指针的初始化及类型
数组指针的用途在于存放数组的地址,那我们如何得到数组的地址呢,答案就是前面学到的&数组名。以下就是对一个数组指针进行初始化的一小段代码。对下面这段代码进行调试:
int main()
{
int arr[10]={0};
int(*p)[10]=&arr;
return 0;
}
我们得到:
我们可以看出,&arr和p的类型是一致的。我们成功地将数组arr的地址传给了指针p。
但编译器这里显示的类型名是错误的,不是有效的数组指针类型名。前面我们知道,字符指针,整型指针的类型是char*,int*,那数组指针的类型是什么呢?
其实,一个指针的定义去掉它的名字后的部分就是它的类型。那么对于上面那段代码,这个数组指针的类型就是char (*)[20]。这是我们第一次学到的比较别扭的类型名称。
知道了数组指针的类型名是 数组类型名 (*)[数组元素个数],后我们就可以利用这个类型名进行强制类型转换的操作。
int main()
{
int arr[10] = { 0 };
int(*p)[10] = &arr;
int brr[20] = { 1,2,3,4,5,6,7,8,9,10,11 };
p=(int(*)[10])brr;//将brr强转为有是个元素的整型数组的指针
for (int i = 0; i < 10; i++)
{
printf("%d ",(*p)[i]);
}
return 0;
}
从打印结果来看我们确实成功将一个指向拥有20个元素数组的指针brr 强制转换为 一个指向拥有10个元素数组的指针,并将其赋值给p。
但上面这个运用其实有些把问题复杂化了,实际情况下我们几乎不会这样使用数组指针。
数组指针的重要应用其实就是本文的二维数组传参部分,可以回到前面看看具体例子。
3.例题
①求程序的输出结果
//来自比特就业课
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是整形指针,&a是数组的地址,可以看作数组指针;二者起始都指向1;对于ptr的初始值,看作数组指针a+1跨过1个数组,指向数组后面的区域;然后我们把它强制类型转化为整形指针int*。因此打印操作中ptr-1操作跳过1个整形,指向了5,解引用打印5。前面的*(a+1)则是打印2。
因此最后输出2,5。
②求程序的输出结果
//来自牛客网
int main()
{
int x[5] = { 2,4,6,8,10 }, * p;
int (*pp)[5];
p = x;
pp = &x;
printf("%d\n", *(p++));
printf("%d\n", *pp);
return 0;
}
第一个打印,p起始指向数组首地址,由于++是后置++,因此先使用值再进行指针移动,解引用打印得到2。
第二个打印,因为数组指针pp=&x,因此取地址后=>*pp=x;我们知道单独的数组名表示的是数组首元素的地址,因此输出的实际上是被转化为整形输出的地址。对于每次打印,首元素的地址,也就是数组其实分配的内存地址是相对随机的。因此打印随机值,且编译器会报类型不符的警告⚠(vs编译器)。
因此最后输出2,随机值。
③求程序的输出结果
//来自比特就业课
int main()
{
int a[5][5];
int(*p)[4];
p = a;
printf("%d\n", &p[4][2] - &a[4][2]);
return 0;
}
int a[5][5]是一个二维数组的声明,a作为数组名表示的是数组首元素的地址(二维数组的首元素是a[0])。因此a是一个类型为int (*)[5]的数组指针(这里的5是第二个括号的5,后面的4也是二维数组第二个括号的4),而p是一个类型为int (*)[4]的数组指针,将a的值赋给p的时候,p的最终类型由p自己说了算,因此为int (*)[4]类型。(注意二维数组第一个括号为5的结构不变)
&p[4][2]和&a[4][2]可以看作是p[4][2]和a[4][2]两个元素地址的差值。示意图如下:
因此,p[4][2]的地址比a[4][2]的地址低4个字节(4×5+3-4×4+3=4);因此本题输出为4。
(三)函数指针
1.函数的地址
函数指针是继数组指针之后,我们遇到的第二个较难理解的指针类型。类似数组指针,我们同样可以用类比的方法去理解函数指针。很显然,函数指针的用途应当是存储函数的地址。那我们首先遇到的一个问题就是:函数的地址真的存在吗?如果存在,函数的地址又是什么呢?
下面我们将以加法函数ADD为示例函数,验证函数是否有地址。
int ADD(int x, int y)
{
return x + y;
}
int main()
{
printf("%p\n", &ADD);
printf("%p\n", ADD);//准备验证另一个规律:根据类比,单独的函数名是否表示函数的地址?
return 0;
}
打印结果:
有了上面的两行结果,我们肯定:函数是有地址的,函数名就是函数的地址,也可使用&函数名的形式。
既然函数有地址,我们就需要创建函数指针把函数的地址存起来。
2.函数指针的语法及其类型
函数指针的写法其实和数组指针很类似。只是在下标引用符的地方换上了参数列表:
再举几个例子:
void print()
{
printf("hi\n");
}
char* write(char* str)
{
return str++;
}
int main()
{
void (*pa)() = print;//也可以用&print
char* (*pb)(char*) = write;//形参str可写可不写
return 0;
}
同样的,类比数组指针,函数指针的类型也可以写作:(以ADD函数为例)
int (*)(int int);
3.函数指针的应用
下面是一个函数指针应用于两个数比大小的例子:
int Max(int x, int y)
{
return x > y ? x : y;
}
int main()
{
//类比数组指针 int (*pa)[5]=arr;
int (*pf)(int, int) = Max;
//类比数组指针 printf("%d ",(*pa)[i]);
printf("MAX=%d\n", (*pf)(2, 4));
printf("MAX=%d\n", (*pf)(6, -9));
return 0;
}
输出:4
2
有趣的是,由于在函数指针的初始化中用Max和&Max等价,因此用于解引用的*也用不上。因此上面的代码还可以简化为:
int main()
{
int (*pf)(int, int) = Max;
printf("MAX=%d\n", pf(2, 4));
printf("MAX=%d\n", pf(6, -9));
return 0;
}
当然,从另外一种角度来看,这段代码也是正确的:
printf("MAX=%d\n", (********pf)(2, 4)); //*无实质性作用
4.函数指针数组及其运用
函数指针数组是以函数指针为元素的数组,里面存了一个或者多个相同类型(返回类型和形参类型,个数均相同)的函数的地址。以 函数 int Add(int,int) 和 有10个元素的数组 为例,其函数指针数组声明写做:
int (*padd[10])(int,int);
这个定义的结合逻辑是这样的:里面指针名padd和[10]优先结合,前面加上*,用括号括起来。然后在外围按数组成员函数指针的格式组成。
函数指针数组的一大应用便是转移表。转移表的定义是利用函数指针数组调用函数,从而代替繁杂的switch语句,实现程序执行的高效率。下面将从一个制作计算器的例子切入。
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 input, x, y;
int main()
{
do
{
printf("1.add 2.sub 3.mul 4.div 0.exit\n");
printf("请选择:");
scanf("%d", &input);
int (*p[5])(int x, int y) = { 0,ADD,SUB,MUL,DIV };//函数名即函数地址
//将第一个元素定为0,方便操作数和选项一一对应
if ((input <= 4 && input >= 1))
{
printf("输⼊操作数:");
scanf("%d %d", &x, &y);
printf("ret = %d\n", (*p[input])(x, y));
}
else if (input == 0)
{
printf("退出计算器\n");
}
else
{
printf("输⼊有误\n");
}
} while (input);
return 0;
}
由于篇幅长度考虑,结构体指针将在下一篇博文——C语言指针部分学习总结(3)里提到。函数指针剩余部分回调函数和例题也会放在后面。
9.16更新:由于博主转专业失败,接下来可能专注于原专业学习,剩余部分可能弃坑了。