C语言进阶:万字详解带你搞懂指针那些事

请叫我鸽子王!已经很久没有发文章了哈哈哈,终于回归了不是吗

今天带来的是指针的进阶篇,指针是C语言中最复杂的部分之一,今天我们围绕的是指针的进阶部分来学习。

C语言:指针(基础全介绍)_m0_62319039的博客-CSDN博客https://blog.csdn.net/m0_62319039/article/details/121648203?spm=1001.2014.3001.5501可以回顾一下博主以前关于指针基础的介绍,话不多说,我们开始进阶篇的学习。

目录

1.字符指针

2.指针数组

3.数组指针

4.数组参数、指针参数

4.2 二维数组传参

4.3 一级指针传参

4.4 二级指针传参

5.函数指针

6.回调函数:qsort (快速排序)

6.2 qsort 排序结构体

7.指针和数组练习题解析

7.1 一维数组

 7.2 字符数组

7.3 二维数组

8.指针笔试题

8.1

8.2

8.3 

8.4

8.5

8.6

8.7

8.8


1.字符指针

char ch = 'w';
char* p = &ch;

char* p = "abcdef";

字符指针其实就是指针中存放的是字符的地址。前者我们在这假设w的地址是0x12ff80,p的地址是0x124440,那么p里面存放的就是0x12ff80,

后者p中存放的是首字符a的地址。

int main()
{
    char arr1[] = "abcdef";
    char arr2[] = "abcdef";

    const char* str1 = "abcdef";
    const char* str2 = "abcdef";

return 0;
}

结论就是:arr1!=arr2,str1==str2。本质上,str1和str2指向的是同一份"abcdef"。

arr1和arr2却是不同的两个字符串。

char* arr[] = {"abc","qwer","zhangsan"};

同样,字符指针数组也可以这么表示。

2.指针数组

存放指针的数组就是指针数组。

基本形式:

int main()
{
    int arr1[] = {1,2,3,4,5};
    int arr2[] = {2,3,4,5,6};
    int arr3[] = {3,4,5,6,7};

    int* arr[] = {arr1,arr2,arr3};

return 0;
}

其中,arr就是一个指针数组。

每一个元素都表示对应的数组的首元素地址。例如,arr1就是一个指针,指向arr1这个数组的首元素。

3.数组指针

是一种指针。

char* p = &ch;//字符指针,指向字符的指针
int* p = &ch;//整形指针,指向整型的指针

那么数组指针就是指向数组的指针。

int * p1[10];//p1先和数组结合,每个元素存放的是int*类型
int (*p2)[10];//p2先和*结合,再和数组结合,每个元素就是int
//p2就是一种数组指针

但是要弄清楚p2,我们先要了解数组名。

我们知道,数组名就是首元素地址。对于一个数组arr,arr和&arr的区别是什么呢?

 可以看到,arr和arr+1差的是一个4个字节,但是&arr和&arr+1差的是整个数组的大小。

其实后者中间的就是一个数组指针。

int main()
{
    char arr[5];
    char (*pa)[5] = &arr;
}

通俗易懂的说,pa就是指向arr的地址的一个元素,这个元素是一个int类型的指针(int*)。

重要等式:

arr[i] == *(arr+i) == p[i] == *(p+i)

接下来我们来理解一段代码:

int arr[5];arr是一个整形数组,有5个元素,每个元素是int类型

int *p1[10];p1是一个数组,数组有10个元素,每个元素的类型是int*,p1是是指针数组

int (*p2)[10];p2和*结合,说明p2是一个指针,该指针指向一个数组,数组是10个元素,每个元素是int类型的。p2是一个数组指针

int (*p3[10])[5];p3先和方块结合,是一个数组,数组有10个元素,也就是int(*)[5],所以p3是一个存放了10个int(*)[5]类型的数组

p3如图

4.数组参数、指针参数

#include<stdio.h>
void test(int arr[])
{}
void teat(int arr[10])
{}
void teat(int *arr)
{}
void test2()int *arr[20])
{}
void test2(int **arr)
{}

int main()
{
    int arr[10] = {0};
    int *arr2[20] = {0};
    test(arr);
    test2(arr);

    return 0;
}

可以看到每一个传参接收的形式都是正确的。

4.2 二维数组传参

二维数组传参,行可以省略,列不能省略(就是横着的不能省略),int[][5]是正确的

void test(int *arr)
{}
void test(int* arr[5])
{}
void test(int (*arr)[5])只有这一种方法是正确的
{}
void test(int **arr)
{}

int main()
{
    int arr[3][5] = {0};
    test(arr);
    return 0;
}

4.3 一级指针传参

void print(int * p)

当一个函数的参数部分为一级指针的时候,函数能接收什么参数?

void test(int* p)
{}

int main()
{
    int a = 10;
    int*ptr = &a;
    int arr[10] = {0};
    test(&a);
    test(ptr);
    test(arr);
}

以上这些都可以。

4.4 二级指针传参

void test(int** ptr)

当一个函数的参数部分为二级指针的时候,函数能接收什么参数?

void test(char** p)
{}

int main()
{
    char ch = 'w';
    char* p = &ch;
    char** pp = &p;
    char* arr[5];

    test(&p);
    test(pp);
    test(arr);

    return 0;
}

5.函数指针

函数是不存在首元素的地址这一说法的,也就是说&ADD和ADD是一样的意思。

函数指针的基本形式:

int ADD(int x,int y)
{
    return x+y;
}

int main()
{
    int (*pf)(int,int) = &ADD;
    return 0;
}

其中,int(*pf)()int就是一个函数指针

pf跟*结合,说明这是指针,后面跟着括号,说明这是个函数指针。函数参数是int int,返回类型也是int。

 如何调用?

int Add(int x,int y)
{
    return x+y;
}

int main()
{
    int (*pf)(int,int) = &Add;*pf就是一个函数指针,指向Add函数的地址

    int sum = (*pf)(2,3);pf就是函数指针解引用
    printf("%d",sum);
    return 0;
}

实例1:

(*(void(*)())0)();

1.把0强制类型转换为void(*)()这种类型的函数指针

2.再去调用0地址处这个参数为无参,返回类型是void的函数

这是依次函数调用,调用0地址处的函数。

实例2:

void(*signal(int,void(*)(int)))(int);

1.signal是一个函数声明

这个函数的参数有两个,第一个是int类型,第二个是函数指针,该指针指向的函数参数int,返回类型是void

2.signal函数的返回类型也是函数指针,该指针指向的函数参数是int,返回类型是void。

利用函数指针实现加法减法计算器:

我们先完成加减部分的代码,十分简单:

int ADD(int x,int y)
{
    return x+y;
}

int SUB(int x,int y)
{
    return x-y;
}

主函数:

#include<stdio.h>
int main()
{    
    int input = 0;
    int x = 0;
    int y = 0;
    int ret = 0;
    
    int(*pf[3])(int,int) = {0,ADD,SUB};这里是初始化函数指针数组,每一个函数指针都指向相对应函数的地址
    
    scanf("%d",&input);
    ret = pf[input](x,y);
    printf("%d",ret);
    
    return 0;
}

6.回调函数:qsort (快速排序)

qsort是C语言的一个库函数,基于快速排序算法。

我们先来看qsort的基本形式:

void qsort(void* base,指针
           size_t num,数组大小,单位是字节
           size_t width,每个元素的大小
           int(* compare)(const void* e1,const void* e2)
           );
比如int arr[10],那么num就是10,width就是4

转到MSDN,

 这是什么呢?这是说e1指向的元素比e2指向的元素小的话,返回值就<0,其他同理。

具体来说,

int main()
{
    int arr[] = {1,4,2,6,5,3,7,9,0,8};
    int sz = sizeof(arr)/sizeof(arr[0]);
    
    qsort(arr,sz,sizeof(arr[0],compare)

    return 0;
}

其中compare函数是传进来的一个函数,我们来完成这个函数.

这个函数的作用就是,比较e1和e2的大小。

int compare(const void*e1,const void* e2)
{
    *e1

此时对e1进行解引用操作,编译器会报警告:非法的间接寻址

为什么会出现这个状况呢?因为我们给出的需要排序的类型是int,但是e1给出的类型却是void。我们把整型变量放到void类型中,编译器会报警告。

解决办法就是:我们把需要排序的内容放到一个void*类型中

int a = 0;
void* p = &a;

问题来了,我们为什么要用void* 这个类型呢?

在最初创作qsort函数的时候,创作者并不知道排序的内容的类型是什么,也许是整形数组,字符数组,浮点型数组,结构体数组。写成void*就可以全部接受。

1.void* 是一种无类型的指针,无具体类型的指针。

2.void* 的指针变量可以存放任意类型的地址。

3.void* 的指针不能直接进行解引用操作。

4.void* 的指针不能直接进行加减操作。

用法:

用void* 时应该把其强制类型转换成相应的形态,再解引用。

int compare(const void*e1,const void*e2)
{
    if( *(int*)e1  > *(int*)e2)
    return 1;
    else if( *(int*)e1  > *(int*)e2)
    return -1;
    else
    return 0;
}

这样子e1,e2就能正常解引用操作了。

但其实这段代码比较冗余,可以适当简化:

int compare(const void*e1,const void*e2)
{
    return *(int*)e1 - *(int*)e2;   
}

这样子就跟MSDN中qsort的定义是一样的了。 

至于具体是怎么完成快速排序的,这里就不进行深究了。过程和冒泡排序是比较相似的,大家有兴趣可以自己尝试编写一下。

6.2 qsort 排序结构体

我们先给定一个结构体:

struct Stu
{
    char name[20];
    int age;
    float score;
};

再把qsort函数写出来:

void test()
{
    struct Stu arr[] = {{"zhangsan",20,87.5f},{"lisi",22,99.0f},{"wangwu",10,68.5f}};
    int sz = sizeof(arr)/sizeof(arr[0]);
    qsort(arr,sz,sizeof(arr[0]),compare);
}

只要把compare函数写出来,跟之前的整型比较相似,只要改变强制类型转换的形态就行了:

int compare(const void*e1,const void*e2)
{
    if(((struct Stu*)e1)->score > ((struct Stu*)e2)->score)
    {
        return 1;
    }
    else if(((struct Stu*)e1)->score < ((struct Stu*)e2)->score)
    {
        return -1;
    }
    else
    {
        return 0;
    }
}

要注意,本次排序是通过score的升序排序的!

最后再完成打印函数:

void print_stu(struct Stu arr[],int sz)
{
    int i = 0;
    for(i = 0; i < sz; i++)
    {
        printf("%s %d %f\n",arr[i].name,arr[i].age,arr[i].score);
    }
    printf("\n");
}

每一步都是遵循着qsort函数的基本形式来完成,利用函数指针完成地址的传递。结构体的一点特点多注意一下,这样一个快速排序的函数就完成了。

7.指针和数组练习题解析

7.1 一维数组

数组名是数组首元素的地址,但是有两个例外:

1.sizeof(数组名),这里的数组名表示整个数组

2.&数组名,这里的数组名也表示整个数组

		int a[] = { 1,2,3,4 };

		printf("%d\n", sizeof(a)); 
数组名单独放在sizeof内部,计算的是整个数组的大小,单位是字节,16

		printf("%d\n", sizeof(a + 0));         
a表示的是首元素的地址,a + 0也是数组首元素的地址,地址大小就是4 / 8

		printf("%d\n", sizeof(*a));           
a表示的首元素地址,* a就是对首元素的地址的解引用,就是首元素,大小是4

		printf("%d\n", sizeof(a + 1));         
a表示的首元素地址,a + 1表示第二个元素的地址,地址大小就是4 / 8

		printf("%d\n", sizeof(a[1]));         
a[1]是数组的第二个元素,大小是4

		printf("%d\n", sizeof(&a));          
&a取出的是数组的地址,地址的大小就是4 / 8

		printf("%d\n", sizeof(*&a));        
*和& 相当于抵消了,sizeof(a),大小是16

		printf("%d\n", sizeof(&a + 1));       
&a + 1直接跳过这个数组,取到的是后面的地址,地址的大小就是4 / 8

		printf("%d\n", sizeof(&a[0]));      
&a[0]取出数组第一个元素的地址,大小就是4 / 8

		printf("%d\n", sizeof(&a[0] + 1)); 
&a[0] + 1就是第二个元素的地址,大小就是4 / 8

 7.2 字符数组

    char arr[] = { 'a','b','c','d','e','f' };

	printf("%d\n", sizeof(arr));         
arr作为数组名单独放在sizeof内部,计算的是整个数组的大小,单位是字节,6

	printf("%d\n", sizeof(arr + 0));   
arr就是首元素的地址,+0也还是地址,大小4/8

	printf("%d\n", sizeof(*arr));       
arr就是首元素的地址,*arr就是首元素,是一个字符,大小是1

	printf("%d\n", sizeof(arr[1]));     
arr[1]就是数组的第二个元素,是一个字符,大小是1

	printf("%d\n", sizeof(&arr));      
&arr取出的是地址,大小是4/8

	printf("%d\n", sizeof(&arr + 1));
&arr取出的是数组的地址,&arr+1跳过的是整个数组,但是还是地址,大小是4/8

	printf("%d\n", sizeof(&arr[0] + 1));
&arr[0]是第一个元素的地址,+1就是第二个元素的地址,大小就是4/8
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", strlen(arr));         
因为无法知道\0的位置,所以这是个随机值

printf("%d\n", strlen(arr + 0));  
arr是首元素的地址,arr+0还是首元素的地址,结果还是随机值

printf("%d\n", strlen(*arr));      
&arr取到的是a,但是会识别为a的ASCII码值,传给strlen的就是97,strlen会把97作为起始位置开始统计,会形成内存访问冲突

printf("%d\n", strlen(arr[1]));    
和上一个一样

printf("%d\n", strlen(&arr));     
&arr是arr数组的地址,虽然类型和strlen的参数类型有所差异,但是传参过去后,还是从第一个字符的位置向后数位置,结果还是随机

printf("%d\n", strlen(&arr + 1));
随机值

printf("%d\n", strien(&arr[0] + 1));
随机值

char arr[] = "abcdef";           
   
printf("%d\n", sizeof(arr));        
7字节

printf("%d\n", sizeof(arr + 0));  
地址,4/8

printf("Xd\n", sizeof(*arr));       
a,就是1

printf("%d\n", sizeof(arr[1]));   
1

printf("Xd\n", sizeof(&arr));    
地址4/8

printf("%d\n", sizeof(&arr + 1));  
地址,4/8

printf("%d\n", sizeof(&arr[0] + 1)); 
地址,4 / 8
printf("%d\n", strlen(arr));    
首元素地址,到\0,大小是6

printf("%d\n", strlen(arr + 0)); 
6

printf("%d\n", strien(*arr));  
err

printf("%d\n", strien(arr[1])); 
err

printf("Xd\n", strlen(&arr)); 
6

printf("%d\n", strlen(&arr + 1));
随机值

printf("%d\n", strlen(&arr[0] + 1));
5
char* p = "abcdef";   
p指向的是字符串的地址

printf("%d\n", sizeof(p));
p是一个指针变量,大小是地址的地址4/8

printf("%d\n", sizeof(p + 1));
地址+1还是一个地址,大小还是4/8

printf("%d\n", sizeof(*p));
p是char*的指针,解引用访问一个字节,所以大小是1

printf("%d\n", sizeof(p[0]));
p[0]  等价于 *(p+0)也就是*p,大小是1

printf("%d\n", sizeof(&p));
p是个变量,取出的是p的地址,是地址大小就是4/8

printf("%d\n", sizeof(&p + 1));
类型是char**,还是一个地址,大小就是4/8

printf("%d\n", sizeof(&p[0] + 1));
&p[0]就是a的地址,那么最终取出的就是b的地址,地址大小就是4/8
char* p = "abcdef";
printf("%d\n", strlen(p));     
从p开始数字符串的长度,长度就是6

printf("%d\n", strlen(p + l));
跟上面的一样,长度是5

printf("%d\n", strlen(*p));
传过去的是ASCII码值97,,err

printf("%d\n", strien(p[0]));
err

printf("%d\n", strlen(&p));
随机值

printf("%d\n", strien(&p + 1));
随机值

printf("%d\n", strlen(&p[0] + 1));
p[0]等价于*(p+0)等价于*p,就是从第二个字符的位置向后数字符串,大小是5 

7.3 二维数组

int main()
{
	int a[3][4] = { 0 };
	printf("%d\n", sizeof(a));         
计算的是整个数组的大小,3*4*4=48

	printf("%d\n", sizeof(a[0][0]));
第一个元素,大小是4

	printf("%d\n", sizeof(a[0]));
表示的是第一行的数组名,计算的是第一行的大小,大小是16

	printf("%d\n", sizeof(a[0] + 1));
a[0]作为第一行的数组名,没有&,没有单独放在sizeof内部,所以a[0]表示的就是首元素的地址,
即a[0][0]的地址,a[0]+1就是第一行第二个元素的地址,大小就是4/8

	printf("%d\n", sizeof(*(a[0] + 1)));
第一行第二个元素,大小是4

	printf("%d\n", sizeof(a + 1));
a是二维数组的数组名,没有&,没有单独放在sizeof内部,a表示首元素的地址,即第一行的地址,a+1就是第二行地址,是类型为int(*)[4]的数组指针

	printf("%d\n", sizeof(*(a + 1)));
*(a+1)就是第二行,相当于第二行的数组名,sizeof计算的是第二行的大小,就是16

	printf("%d\n", sizeof(&a[0] + 1));
a[0]时第一行的地址,&a[0]是第一行的地址,+1就是第二行的地址,4/8

	printf("%d\n", sizeof(*(&a[0] + 1)));
上面的例子再解引用,拿到的是第二行,就是a[1],大小是16

	printf("%d\n", sizeof(*a));
a是二维数组的数组名,没有&,没有单独放在sizeof内部,a表示首元素的地址,*a也就是二维数组的首元素,也就是第一行,大小就是16

	printf("%d\n", sizeof(a[3]));
int[4],就是16

	return 0;
}

二维数组的相关理解比较难,特别是要清楚二维数组的首元素就是第一行,+1就直接拿到了第二行。

8.指针笔试题

8.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+1跳过的是真个数组,所以ptr是在5的后面的那个地址,所以最终打印的是2和5

8.2

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

这个题我们一个一个看,

printf("%p\n", p + 0x1);
在这里,p的结构体类型的指针,p作为一个指针+1,
那么就是跳过整个结构体,结构体的大小是20个字节,
以16进制打印出来就是0x100014
printf("%p\n", (unsigned long)p + 0x1);
这里把p强制类型转换成为了无符号的整型,一个整型加上一个1,
就变成了简单的数字加减,所以打印出来的是0x100001
printf("%p\n", (unsigned int*)p + 0x1);
p转换成了整形指针,整形指针+1就是+4个字节,
打印出来就是0x100004

8.3 

int main()
{
	int a[4] = { 1, 2, 3, 4 };
	int* ptr1 = (int*)(&a + 1);
	int* ptr2 = (int*)((int)a + 1);
	printf("%x", ptr1[-1]);
    printf("%x",*ptr2);
	return 0;
}
printf("%x", ptr1[-1]);
ptr1[-1]等价于*(ptr1-1),ptr1是&a+1也就是4后面的地址,
那么-1以后就是4
printf("%x",*ptr2);

至于这个,我们慢慢分析:

首先把a转换为整型类型,再+1,我们假设这个地址是0x10,那么+1就是0x11,再强制类型转换为整型指针,也就是说只是把地址往后移动了一个字节,我们在这里以小端为例:

 那么读取的时候读取到的就是00 00 00 02,所以还原出来的数字就是0x02000000,所以这个题最终打印出来的结果就是4和2000000

8.4

int main()
{
	int a[3][2] = { (0, 1),(2, 3),(4, 5) };
	int* p;
	p = a[0];
	printf("%d", p[0]);
	return 0;
}

首先,这是个逗号表达式,也就是说,这个二维数组是int a[3][2] = {1,3,5,0,0,0}

分析到这里就容易了,p指向的就是1,打印的结果是1

8.5

int main()
{
	int a[5][5];
	int(*p)[4];
	p = a;
	printf("%p", &p[4][2] - &a[4][2]);
	printf("%d", &p[4][2] - &a[4][2]);
	return 0;
}

首先要知道,&p[4][2] 和 &a[4][2]是不同的含义,前者是一个数组指针,而后者确实二维数组访问的元素的首地址,&p[4][2]意思就是*(*(p+4)+2)

绿色的是&p[4][2],其中p认为它指向的是4个整型的数组,p+1跳过的就是4个字节,所以绿色的方块就是&p[4][2],红色的方块就是&a[4][2]

printf("%p", &p[4][2] - &a[4][2]);
printf("%d", &p[4][2] - &a[4][2]);

%p打印的是地址,两个元素中间差了-4,所以%d打印出来的就是-4,那么-4在内存中通过转换,补码就是11111111111111111111111111111100,这个值16进制打印出来的就是ff ff ff fe,所以两个结果分别是 fffffffe,-4

8.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+1拿到的是10后面那个地址,再-1拿到的就是10

ptr2就是aa+1再解引用,因为aa是一个二维数组,aa+1拿到的就是第二行的地址,也就是6,再-1拿到的就是5,打印出来就是10和5

8.7

int main()
{
	char* a[] = { "work","at","alibaba" };
	char** pa = a;
	pa++;
	printf("%s\n",* pa);
	return 0;
}

这个其实很简单,pa指向的就是work,+1就是at

8.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;
}

 首先这是简单的分析,指针指向的位置

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

++cpp后,cpp指向的位置也发生了变化:

 这时解引用拿到的是c+2,最后打印出来的就是point

printf("%s\n", *-- * ++cpp + 3);

这个乍一看,*--是什么东西?我们研究的是cpp这个对象,cpp++后再解引用,拿到的是c+1,再--,c+1变成了c,于是指向的是原来c指向的地址,所以是ENTER,但是最后还+3,所以打印的就是 ER

printf("%s\n", *cpp[-2] + 3);

cpp[-2]+3也就是*(cpp-2)+3,指向的是FIRST,+3就是ST

printf("%s\n", cpp[-1][-1] + 1);

cpp[-1][-1]等价于*(*(cpp-1)-1),,最终指向的是EW

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值