C语言指针部分学习总结(2)

目录

一.补充知识

(一)二级指针

(二)数组名在不同场合的含义

(三)sizeof和strlen

二.指针和数组的关系

(一)下标访问符[ ]和解引用符*

        1.[ ]和*的关系

        2.[ ],*操作符的链式访问

​编辑

(二)一维数组的传参本质

(三)二维数组的传参本质

三.几种特殊类型的指针及其语法

(一)字符指针

1.字符数组和字符串

2.字符指针的初始化和字符串的存储

3.浅谈数据在内存中的存储

4.例题

(二)数组指针

        1.数组指针和指针数组的区分

        2.数组指针的初始化及类型

        3.例题

(三)函数指针

        1.函数的地址

        2.函数指针的语法及其类型

        3.函数指针的应用

        4.函数指针数组及其运用


一.补充知识

(一)二级指针

        二级指针是一种特殊的指针变量,与一级指针(普通指针)所存储变量的地址相一致的是,二级指针存储的是对应一级指针的地址。可以说,二级指针指向一级指针,一级指针指向对应变量。通过对二级指针的解引用可以得到一级指针,从而去修改一级指针的内容(变量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;
}

他们的关系如下图(地址是随便写的):

1597ae465a9a481bbfaf70ecbc80fa7d.jpeg

        其中,二级指针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表示的并不是数组首元素的地址:

  1. .sizeof(arr):这里的数组名是表示整个数组,计算的是整个数组的大小,单位是字节
  2. &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环境下的打印结果:

ed53b9806ec64ed392ece5e8e0343a34.jpeg

        从本次打印结果可以看出,从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;
}

        打印的结果是:

7304efb8fd8c4ed68b22ee7e044bf329.png

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

       打印结果:

8064964a62af46ad9ba87ee2b5482cc1.png

         Ⅱ.在进行数组元素访问的时候,我们通常采用下标访问操作符[],但实际上,计算机真实进行运算的时候,总是会把[]转化为解引用操作符*。

#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 ,打印结果:

63452886d44947a8b86b6e6f3a4c1546.png

        和上面一次输出了同样的结果,说明*(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;
}

9350843546d34dd49cd70362590061a3.png

        这组代码中,我们以代码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环境下输出结果:

a5216068ad53489688a50d58c6d5dbaf.png

        由结果可以看出,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;
}

        打印结果:

d21cb7d2567b4b1197fc00afb4e7a3ee.png

        将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'。下面有一张简单的示意图:

0c9d17a1fbfb46f38d6fed495ff53dca.png

       下面这个例子能够加强对这两种存储方式的理解:

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》,改编于比特就业课
}

        输出结果:

f7ea4b05687d413789ffba43964f5cab.png

        造成这个结果的原因是:

        ①str3和str4是两个指向同一字符串“abcde”的字符指针。而这个字符串存储于内存中的一个只读数据段中。这个数据段位于全局区,其生命周期是与整个程序一样的。因此str3和str4指向的实际上是同一块内存空间。

        ②str1和str2本质上是两个数组。其内容是由包含'\0'在内的六个字符组成的。这两个数组在内存中的栈区分别创建。数组名str1和str2表示的地址,即两个数组的首元素地址,必定是不一样的。

3.浅谈数据在内存中的存储

        在C语言中,前面提到的字符串存储在文字常量区或只读数据段中。这是一块非常特殊的区域。那么内存中存储数据的区域是如何划分的呢?

        C语言程序分配的内存区域:

  1. 栈区(stack):主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址
  2. 堆区(heap):一般由程序员手动创建手动释放,如果程序员在程序结束前未释放由OS回收。
  3. 数据段(静态区)(data segment):存放全局变量,静态数据(包含只读常量,如字符串常量),程序结束后由系统释放。
  4. 代码段(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;
}

        画图理出上述三级指针,二级指针,一级指针的关系(每个箭头均表示一个指针):

fb8bcf3d0ca44c778f7ae22e0194f3c7.png

        Ⅰ.printf("%s\n", **++cpp);

        解析: cpp一开始指向cp数组的第一个元素,++后指向第二个,两次解引用后打印“POINT”。

        执行完这一步操作后,各级指针位置如下:

9eab6782c81848399c6a2cad38168520.png
        Ⅱ.printf("%s\n", *-- * ++cpp + 3);

        解析:cpp++后指向cp数组的第三个元素(注意前面cpp指针指向已经变化了),解引用一次指向c数组的第二个元素,进行--操作后该处指针指向c数组的第一个元素,再次解引用指向"ENTER"的首字符‘F’,+3最后计算,指针访问"ENTER"的第四个字符'E'处,打印“ER”。

        执行完这一步操作后,各级指针位置如下:

63929aea129849139f321a9004c9b3b2.png
        Ⅲ.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;
}

       我们得到:

4ee89dfc5b6a454f8c4043366b0ebe07.png

        我们可以看出,&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;
}

        15bc1561018b4588aa186392a075758a.png

        从打印结果来看我们确实成功将一个指向拥有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;
}

       打印结果:

0fb255f081df4453a816db4379f57e8e.png

        有了上面的两行结果,我们肯定:函数是有地址的,函数名就是函数的地址,也可使用&函数名的形式。

        既然函数有地址,我们就需要创建函数指针把函数的地址存起来。

        2.函数指针的语法及其类型

        函数指针的写法其实和数组指针很类似。只是在下标引用符的地方换上了参数列表:

15dcfc649e394e658f2785333916346c.png

       再举几个例子:

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]优先结合,前面加上*,用括号括起来。然后在外围按数组成员函数指针的格式组成。

aa138fa129fb49c5ab10939a0b7a9290.png

       2aa70bd3cca246edb96d6a5ef63ccf76.png

        函数指针数组的一大应用便是转移表。转移表的定义是利用函数指针数组调用函数,从而代替繁杂的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更新:由于博主转专业失败,接下来可能专注于原专业学习,剩余部分可能弃坑了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值