当前位置: 首页 > news >正文

c语言的指针详解

c语言的指针详解

  • 指针
    • 什么是内存
    • 地址的产生
    • 变量的地址
    • 指针变量的大小
  • 指针的类型对指针运算的影响
    • 指针加、减整数
    • 指针作差
    • 指针的关系运算
    • 指针的解引用
    • 野指针
    • 指针和数组
      • 数组名加、减
      • 指针数组
      • 字符指针
      • 数组、指针嵌套定义
    • 多级指针
    • 函数和指针
      • 函数指针的类型
      • 函数指针数组
    • 指针作为函数参数
      • 实参为一维数组
      • 实参为二维数组
      • 形参为一级指针
      • 形参为二级指针
      • 回调函数
  • 指针复习题
    • 数组和指针
      • 案例1
      • 案例2
      • 案例3
      • 案例4
      • 案例5
    • 对指针的理解
      • 案例1
      • 案例2
      • 案例3
      • 案例4
      • 案例5
      • 案例6
      • 案例7
      • 案例8

指针

指针是什么?

  1. 指针是内存中一个最小单元的编号,也就是地址。

  2. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量。

总结:内存的编号是地址,指针变量是用来存储地址的变量。指针一般是指地址,平常说的指针不特别说明,默认是指针变量

可以通过&(取地址操作符)取出变量对应的内存的地址,这个地址可以存放到一个变量中,这个变量就是指针变量。

什么是内存

内存是电脑上特别重要的存储器,计算机中程序的运行都是在内存中进行的 。

所以为了有效的使用内存,就把内存划分成一个个小的内存单元,每个内存单元的大小是1个字节。

为了能够有效的访问到内存的每个单元,就给内存单元进行了编号,这些编号被称为该内存单元的地址。这种地址在c/c++中又把他称作指针

为啥不分到最小单位bit?若这样分配,则相当于指定到某房间的第几平米处处理数据(太细不好处理),1个char类型的变量1个字节,再细的话不合理。

很多时候32位的二进制序列太长,我们将他们用十六进制表示。
请添加图片描述

每个内存单元都有自己的编号,编号称做地址,地址又称作指针。

所以编号、地址、指针三个说法是一回事。

地址的产生

当我们在32位机器上,有32根地址线(有物质形态的电线),该电线是通电的,通电后有高、低电平,高、低电平转换成数字信号就是高电平为1和低电平为0(也可以反逻辑即高电平为0和低电平为1)。

这种32位机器表示的32位二进制数能代表 2 32 2^{32} 232种电信号,此时32根电线产生的电信号,转换成数字信号的二进制序列可作为内存编号,通过内存编号可找到对应的内存空间。

所以内存分为一个个内存单元,每个内存单元都对应一个编号,编号由硬件电路产生。

请添加图片描述

一个内存单元是1个字节,这种内存单元可以给出一个地址。能产生 2 32 2^{32} 232个地址(或 2 32 2^{32} 232个二进制序列),即一个32位机器用于记录地址的数据有 2 32 2^{32} 232个,它们能够管理 2 32 2^{32} 232 byte的空间。

2 32 2^{32} 232byte = 4 =4 =4GB。

在vs界面可发现x86处有一个配置管理器,通过更改配置管理平台,指针变量的地址存储空间会发生变化。

一般 X86 对应 32位(bit),X64 对应 64位(bit)。

变量的地址

#include <stdio.h>
int main()
{int num = 10;&num;//取出num的地址//注:这里num的4个字节,每个字节都有地址,//取出的是第一个字节的地址(较小的地址)printf("%p\n", &num);//打印地址,%p是以地址的形式打印return 0;return 0;
}

请添加图片描述

&num实际占用4byte的空间(X64下是8byte),每个byte都有自己的地址,&num只是取出第1个byte空间的地址。

取出的地址也是十六进制数,也需要内存空间存储,通常用

type* name = &num;

来存储。我们把name称指针变量(存放地址也就是指针的变量),name的类型是type*

*说明name是指针变量,type说明name指向的对象是type类型。*跟随变量还是类型没有具体要求。

指针的使用实例:

#include <stdio.h>
int main()
{int num = 10;int* p = &num;*p = 20;return 0;
}

&num:获得num的地址。&是取地址操作符。

*p*是解引用操作符,解引用是p里存地址,*p就是解引用,它是通过p里的地址找到这个地址指向的变量,对那个变量的存储的数据进行操作。

术语(指针)A指向(地址)B表示A作为一个指针变量,存有某个内存的地址B,在我的理解中这个专业术语可以这样解释。

指针变量的大小

#include <stdio.h>
//指针变量的大小取决于地址的大小
int main()
{printf("%d\n", sizeof(char*));printf("%d\n", sizeof(short*));printf("%d\n", sizeof(int*));printf("%d\n", sizeof(double*));return 0;
}

结论:32位平台下地址是32个bit位(即4个字节)。64位平台下地址是64个bit位(即8个字节)。

因为计算机中每个内存单元(单位:字节 byte)都分配有一个编号(唯一的),一个编号用 32 bit / 64 bit (取决于系统)大小的内存空间存储。所以指针的大小一般是 4或8个字节。在远古时代还有2个字节的指针,这个不做多的深究。

指针的类型对指针运算的影响

指针的定义方式是: type* var=某地址;

一般指针用于存放指定类型的变量的地址。例如char* 类型的指针是为了存放 char 类型变量的地址。

指针的运算和类型有密切的联系。

指针加、减整数

例如这个案例:

#include <stdio.h>
int main()
{int n = 10;char* pc = (char*)&n;int* pi = &n;printf("%p\n", &n);printf("%p\n", pc);printf("%p\n", pc + 1);printf("%p\n", pi);printf("%p\n", pi + 1);pc++;printf("%p\n", pc-1);pc--;printf("%p\n", pc);return 0;
}

输出结果之一:

0072FB20
0072FB20
0072FB21
0072FB20
0072FB24
0072FB20
0072FB20

可以看到,char是1 byte,所以char*指针加1会增加1个单位的地址,而int*则是4个单位。

总结:指针的类型决定了指针向前或者向后走一步有多大(距离)。这个距离一般和原类型的字节数有关。

指针作差

案例:

#include <stdio.h>
int main()
{int a[10];int* p = &a[3];printf("%d", p - a);return 0;
}

p-a相当于a+3-(a+0)

所以同个数组内的地址相减,得到的是指针和指针之间的元素个数。

假设p1p2是数组的两个指针,p1-p2结果为正说明指针p1在高地址,否则p1在低地址。

除此之外的指针的加、减的结果未定义。主要是不同编译器对两个彼此独立的变量的安排并不相同。例如:

#include<stdio.h>int main() {int a = 3;int b = 4;printf("%d", &b - &a);return 0;
}

在vs输出 -3,在Devc++5.11则是 -1,负数只能表明a在高地址,无论哪个编译器都是先用高地址,再使用低地址。

指针的关系运算

案例:

#include <stdio.h>
int main()
{int a[10] = { 1,1,4,5,1,4 };int* p;for (p = &a[9]; p > &a[0]; p--)printf("%d ", *p);return 0;
}

在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。

标准规定:

允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。即仅限数组范围内。尽管标准并没有规定会阻拦这种访问的行为。

请添加图片描述

除了数组以外的指针的关系运算则是普通的数值间的比较。

指针的解引用

案例:

#include <stdio.h>
int main()
{int n = 0x11223344;char* pc = (char*)&n;int* pi = &n;*pc = 0;printf("%08x\n", n);*pi = 0;printf("%08x", n);return 0;
}

输出:

11223300
00000000

可以看到,char*指针仅改变1个字节的内存的数据,但int*却改变了4个字节的所有数据。

总结:指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。

野指针

野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。或野指针指向的位置,当前程序没有使用权限。

野指针的形成原因:

  1. 指针未初始化。

    无论是在什么编译器,都会对未初始化的变量赋予初始值,指针变量也不例外。比如vs系列编译器初始化的值是每个字节都是0xcc

    #include <stdio.h>
    int main()
    {int* p;//局部变量指针未初始化,默认为随机值*p = 20;return 0;
    }
    
  2. 指针越界访问
    最常见的就是数组。例如int a[3]={0};a[3]=4,因为数组只有3个成员,因此数组最多只能访问a[2]a[3]属于越界访问,此时a虽然不是野指针,但因为越权行为被迫成为野指针。

  3. 指针指向的空间释放

    例如这个案例:

    #include<stdio.h>int* f(){int a=3;return &a;
    }int main(){int *p=f();//p=100;return 0;
    }
    

    因为函数在退出时内存会销毁,连带自身内部的局部变量也一起销毁,因此将内部的局部变量的地址返回,此时接收这个地址的指针成了野指针。

如何规避野指针?

  1. 指针变量在申请时就初始化。

  2. 小心指针越界。

  3. 指针指向空间释放,及时置NULL。后面使用之前检测一下即可。
    其中NULL在c语言中的定义:#define NULL ((void *)0),即将0强制转换为地址。

  4. 避免返回局部变量的地址。

  5. 指针使用之前检查有效性。

指针和数组

在数组一篇,曾提到指针和数组的关系。

例如,sizeof(数组名)sizeof(数组成员)

#include <stdio.h>void f() {int a[11];printf("%u\n", sizeof(a));//整个数组的大小printf("%u\n", sizeof(a[0]));//单个int型变量的大小printf("%u\n", sizeof(a[2]));
}int main() {f();return 0;
}

数组名加、减

例如这个案例:

#define _CRT_SECURE_NO_WARNINGS 1#include <stdio.h>
int main()
{int a[4] = { 0,1,2,3 };printf("1.\n");printf("a=%p\n", a);printf("&a=%p\n", &a);printf("2.\n");printf("a+1=%p\n", a+1);printf("&a+1=%p\n", &a+1);int* p1 = a;int* p2 = &a;printf("3.\n");printf("(int*) p1+1=%p\n", p1+1);printf("(int*) p2+1=%p\n", p2+1);int(*p3)[4] = &a;printf("3.\n");printf("(int*) p1+1=%p\n", p1+1);printf("(int(*)[4]) p3+1=%p\n", p3+1);int(*p4)[3] = a;printf("4.\n");printf("(int(*)[3]) p4+1=%p\n", p4+1);printf("(int(*)[4]) p3+1=%p\n", p3+1);return 0;
}

输出结果之一:

1.
a=00EFF728
&a=00EFF728
2.
a+1=00EFF72C
&a+1=00EFF738
3.
(int*) p1+1=00EFF72C
(int*) p2+1=00EFF72C
3.
(int*) p1+1=00EFF72C
(int(*)[4]) p3+1=00EFF738
4.
(int(*)[3]) p4+1=00EFF734
(int(*)[4]) p3+1=00EFF738

可以看到,数组名加1仅跳过1个单位,但是&数组名+1跳过的却是整个数组。

不仅如此,将数组的首元素地址给指向具体数量的数组指针p3时也能得到同样的效果。
请添加图片描述

再次总结:数组名绝大部分情况下是数组的首元素地址。但有2个例外:

  1. sizeof(数组名)表示整个数组的大小,这时数组名不代表首元素地址,而是整个数组的地址。这个数组名不是函数形参代表的指针。
  2. &数组名,取出的是整个数组的地址,尽管在数值上它和首元素地址相同,但意义不同。一般体现在类型上就是这个地址和指向这个地址的指针的类型是type(*)[成员数]。所以&数组名+整数一般是以成员数为单位进行跳转的。

指针数组

指针数组是字面意义上的数组,但是每个成员存放的是指针,也就是地址。

可以做类比:

字符数组——存放字符的数组。

整型数组——存放整型的数组。

因此指针数组——存放指针的数组。比如,存放字符指针的数组:字符指针数组。

案例1:int* a[3]={0};a[]结合表示它是数组,int*是数组的类型,它是一个指针数组,每个成员都存放指针。

#include <stdio.h>void f1() {int a = 1, b = 2, c = 3;int* x[] = { &a,&b,&c };printf("%d ", *x[0]);
}void f2() {int a[3] = { 2,3,4 }, b[3] = { 2,6,4 }, c[3] = { 2,3,4 };int* p[] = { a,b,c };//用指针数组实现类似二维数组的效果for (int i = 0; i < 3; i++) {for (int j = 0; j < 3; j++)printf("%d ", p[i][j]);printf("\n");}
}int main() {//f1();f2();return 0;
}

f2p[i][j]相当于*(*(p+i)+j)*(p+i)相当于在abc三个数组中选,
*(*(p+i)+j)则是选好三个数组中的一个后,对这个数组进行访问。

案例2,字符指针数组:

#include <stdio.h>
int main()
{char* a[] = { "ab","cd","ef" };printf("%s\n", a[1]);//a[0][0] = 'b';//用常量字符串初始化指针变量a[0]return 0;
}

因为是用的常量字符串初始化指针数组,常量字符串不能修改导致a[0][0] = 'b';会使程序崩溃(详见字符指针)。

字符指针

有一种指针类型为字符指针 char* ;

一般使用:

#include <stdio.h>
int main()
{char ch = 'w';char* pc = &ch;*pc = 'w';char st[]="Hello, world!";pc=st;pc[2]='a';printf("%s\n",st);char* ps = "Hello, world!";printf("%c", ps[4]);//*ps='a';//这句不可执行const char* ps2 = "Hello, world!";return 0;
}

代码解析:

  1. char st[]="Hello, world!";,这句的意思是定义了一个数组,用字符串Hello, world!进行初始化,因此即使通过pc去间接修改,它也只是修改数组的某个元素。因此能正常运行通过。

  2. char* ps = "Hello, world!";ps是指针变量,它的大小不支持存储整个字符串,但它有保存字符串首字符的地址,因此可以当数组用。例如ps[4]实际上就是*(ps+4)

    但是Hello, world!常量字符串,本质上不可修改
    因此编译器会“认为”把不可修改的Hello, world!交给ps有危险,char*ps没有被保护起来。
    通过ps取修改里面的字符比如*ps='a';,即使代码能编译通过,程序也会崩(访问到无权访问的内存)。
    所以一些编译器会警告让加const修饰指针ps,对ps的权限进行限制,使*ps代表的内存不可修改,当尝试修改时将这种行为识别为语法错误。这样做让代码看起来更加严谨。例如const char* ps2 = "Hello, world!";

关于字符数组,有这样一个代码出自《剑指offer》:

#include <stdio.h>
int main()
{char str1[] = "hello bit.";char str2[] = "hello bit.";const char* str3 = "hello bit.";const char* str4 = "hello bit.";if (str1 == str2)printf("str1 and str2 are same\n");elseprintf("str1 and str2 are not same\n");if (str3 == str4)printf("str3 and str4 are same\n");elseprintf("str3 and str4 are not same\n");return 0;
}

输出:

str1 and str2 are not same
str3 and str4 are same

分析:因为常量字符串不可修改,即使str3str4指向的内容相同,也没必要放两个相同的常量字符串,它们本身也具有常属性。因此它们共享同一份常量,且它们都不可修改。因此编译器出于节省空间的角度选择了这样做。

数组、指针嵌套定义

之前已经介绍过数组指针,现在介绍另一种指针数组指针指向指针数组的指针),这里是从最小的符号开始理解定义指针数组指针。

首先是一个指针数组:int* a[10]={0};

将数组的地址取出用一个指针变量p存储:p=&a;

*是为了表示指针p也得是一个指针:*p=&a;

这个指针*p指向一个数组,所以需要指定数组的大小,同时用()调整优先级:
(*p)[10]=&a;

这个数组指针指向的数组的每个元素都是int*型,所以
int* (*p)[10]=&a;

int* (*p)[10]=&a;中,p先与*结合表示它是指针,p再与[]结合表示它是指向数组的指针,最后int*表示它的类型。

也可以这样理解:int* (*)[10] p=&aint* (*)[10]p的类型。但编译器不支持这句代码,仅仅作为理解p的本质。

根据这个思路,还可以定义指针数组指针数组:

int* (*p[5])[10]={&a};

案例:

#include <stdio.h>
int main()
{int a[4][4] = {{ 0,1,2,3 },{ 4,5,6,7 },{ 8,9,10,11 },{ 12,13,14,15 }};int* (*p[4])[4] = { &a};//int* (*)[4] p[4]= { &a}for (int i = 0; i < 4; i++) {for (int j = 0; j < 4; j++)printf("%d ", p[0][i][j]);printf("\n");}return 0;
}

它可以和正常的二维数组一样输出:

0 1 2 3
4 5 6 7
8 9 10 11
12 13 14 15

这个嵌套玩法仅作为了解。

多级指针

指针变量也是变量,是变量就有地址,那指针变量的地址存放在二级指针。

请添加图片描述

#include <stdio.h>int main() {int a = 10;int* p1 = &a;int** p2 = &p1;printf("%d", **p2);//两次解引用return 0;
}

同理,二级指针也有地址所以也存在三级指针int*** p3=&p2

在理解类型时可以这样理解:int *p1中,p1先和*结合,表明它是一个指针;int表示它指向的变量的类型,或它能访问的字节数;int* *p2表示p2是个指针,int*表示它指向的变量类型是int*

多级指针加、减整数,跨越的内存也是取决于低一级的指针的情况。

例如:

#include<stdio.h>int main() {char* c[] = { "enter","new","point","first" };char** cp[] = { c + 3,c + 2,c + 1,c };char*** cpp = cp;printf("%s\n",cpp[0][0]);return 0;
}

cchar*型指针数组,内部存储的是字符串常量。

cp是类型为二级指针的数组,可以这样理解:char* *cp[]={};,表明cp是指针数组,存放的地址类型是char*

cpp是三级指针,存放二级指针的地址。这里是将二级指针数组的首元素的地址赋值给cpp,通过对cpp进行一次解引用可访问数组cp,二次解引用可访问常量字符串。

函数和指针

案例:

#include <stdio.h>void f() {printf("void f1(){}\n");
}int main()
{printf("%p\n%p", f, &f);return 0;
}

输出:

001913B6
001913B6

这说明函数名也可以是函数的地址,既然有地址,那就有指向这个函数的指针。这个指针按照之前的经验,就叫函数指针。

函数指针的类型

既然函数存在地址,则说明这个地址可以用指针变量来存储。

假设某函数void f(int){},则存在指针变量p,使得p=&f;能进行。

既然是函数的指针,类型肯定和函数有关,因此尝试:void* p(int)=&f;

但因为(形参列表)在一众操作符中拥有极高优先级,因此用魔法打败魔法,使用更高优先级的(表达式)来使p先与*结合而不是先与()结合。因此void(*p)(int)=&f;

这个表达式同样可以这样理解:void(*)(int) p=&f;,但c语言的标准不支持这样写代码。

此外,f&f得到的都是一样的地址,这说明解引用操作及*p有没有都是一样的效果,无论加多少个**p(int)**p(int))都是一样的。或者可以这样解释:加*是为了帮助程序员理解。

案例:

#include <stdio.h>void f(int x) {printf("void f1(){}\n");
}int main()
{//函数指针类型只有重命名后才能支持连续声明、定义指针typedef void(*pf)(int);void(*p)(int) = &f;pf p2=f, p3=f;p(0);(*p)(0);(**p)(0);(***p)(0);return 0;
}

根据这个原理,我们可以尝试解读者两个代码(出自《c陷阱和缺陷》):

//代码1
(*(void (*)())0)();
//代码2
void (*signal(int, void(*)(int)))(int);

首先是代码1:

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

将这个代码用字符串存储,方便分析:

(*(void (*)())0)();
123     456789 abc

之后以0为突破口进行分析。

易知3和9匹配,3和9之间是一个类型:函数指针void (*)()

在c语言中,类型放括号内一般是将后面的第1个对象强制转换为该类型,这里唯一的对象是0,说明(void (*)())0是想将0转换成函数指针,之前的*也能被解释成解引用。

之后1、a 也就是()*(void (*)())0括起来,后面还根了个(),说明这个代码不仅想将0转换成函数指针并解引用,还想调用地址在0处的不存在的某个void函数。

然后是代码2:

void (*signal(int, void(*)(int)))(int);12      3         4567   89ab   c

4到8以及void表示的是一个类型void(*)(int)

之后3和9将intvoid(*)(int)括起来,看起来很像函数声明时的形参列表,因此判断signal是函数名;

signal(int, void(*)(int))被外层的void(*)(int)也就是一个新的类型嵌套,因此可以判断这个代码是函数signal的声明,这个函数的返回值是void(*)(int)

发现返回类型和第2个形参都是void(*)(int),于是可以这样简化:

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

函数指针数组

既然确定了能用指针指向函数的地址,那这个函数指针也能用数组表示。

首先是函数指针:

void(*p)();

在指针变量名之后加[],使指针名p先和后面的[]结合表示它是一个数组,类型是void(*)()

因此函数指针数组可以这样表示:

void(*p[3])();

这个数组最大的用途是做转移表:

假如某个代码有很多个功能模块,每个功能模块都是函数,有自己的名字大概介绍特性,直接调用可能有不方便,于是可以用函数指针进行简化。

例如,简易计算器:

#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[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表int flag = 0;while (input){printf("*************************\n");printf(" 1:add           2:sub \n");printf(" 3:mul           4:div \n");printf(" other:quit\n");printf("*************************\n");printf("请选择:");flag = scanf("%d", &input);if ((input <= 4 && input >= 1) && flag != EOF){printf("输入操作数:");scanf("%d %d", &x, &y);ret = (*p[input])(x, y);}else {printf("退出\n");break;}printf("ret = %d\n", ret);}return 0;
}

函数指针数组同样有指向这个数组的指针。按照数组指针的方式去定义即可。

例如,这个指向函数指针数组的指针:

void(*(*a)[10])();

首先a先和*结合,表示它是一个指针,然后void(*[10])()是这个指针指向的数组的类型。

sizeof不能识别函数名,但可以识别指针。

指针作为函数参数

在c语言,函数的形参只有真实的变量和指针两种。这里做个简单的总结,指针作为函数形参时,实参可以上传的参数类型。

实参为一维数组

即一维数组传参。形参类型可以是:

  • 形参是没有指定元素数的一维数组。
    则实参可以是一维数组名,也可以是一维数组的某个元素的地址表示本身及索引大于自身的元素,还可以是单个变量(但只能访问索引为0的元素)。但要程序员指定能访问的元素个数。
    形参虽然是数组,但不会真的创建一个数组,本质还是指针。
    例如:void f(int a[]){}
  • 形参是指定了元素数的数组,例如void f(int a[10]){}
    虽然看上去指定了数组的成员数,但它和上一个一样,函数并不知道这个数组的元素个数,需要程序员指定。
  • 形参是整型一级指针,它能接受的实参有:
    1. 单个变量的地址,最好是同类型的,否则不保证精度。
    2. 数组名,最好是同类型、同维度的。
    3. 数组的某一个元素的地址,最好是同类型的。
  • 形参是指针类型的一级指针。例如void f(int* a[]){}
    实参可以是同样类型的一维数组名。
  • 形参是二级指针,例如void f(int** a){}
    实参可以是一维指针数组。

实参为二维数组

即二维数组传参。形参类型可以是:

  • 指定列数的二维数组。例如:void f(int a[][3]){}

    和一维数组一样,a的数据类型也是指针,需要程序员指定数组的每行、每列的个数。

  • 完整的二维数组。例如:void f(int a[3][3]){}

    a也是指针,需要程序员指定数组的每行、每列的个数。

  • 数组指针。例如:void f(int(*a)[3]){}

    a可以当二维数组使用,但需要程序员指定数组的每行、每列的个数。

形参为一级指针

形参为一级指针时,实参可以是

  • 某个变量的地址。最好是同类型的,否则不保证精度。
    例如: void f2(int* a){} void f1(){int a=0;f2(&a);}
  • 存了某个变量的地址的指针。同样地,最好是同类型的。
    例如: void f2(int* a){} void f1(){int a=0;int* p=&a;f2(p);}
  • 一维数组名,最好是同类型。
    例如: void f2(int* a){} void f1(){int a[3]={0};f2(a);}

注意:若形参是一级指针,无论传参时实参是什么(多级指针,存有具体数值的变量),都会被当成一级指针存储的地址,按照一级指针的格式使用,很容易时形参变成野指针。

形参为二级指针

形参为一级指针时,实参可以是

  • 二级指针。
  • 一级指针的地址。
  • 一维指针数组的数组名。

多级指针的情况可以通过二级指针推导,这里不再过多叙述。

回调函数

回调函数就是一个通过函数指针调用的函数

若把函数的指针(地址)作为实参传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

例如排序用的库函数qsort

void qsort (void* base, size_t num, size_t size,int (*compar)(const void*,const void*));

根据qsort - C++ Reference,使用它需要展开这个头文件stdlib.h。解释一下这个函数各个形参的功能:

base:一维指针,用于保存数组的首元素地址。

num:这个数组的成员数。

size:这个数组的成员占用的内存大小。

compar:回调函数,用于指定比较规则的函数。假设两个形参分别是ab,当返回值为b-a时,数组按降序排序;当返回值为a-b时,数组按升序排序。

这里就使用网站给的测试代码:

/* qsort example */
#include <stdio.h>      /* printf */
#include <stdlib.h>     /* qsort */int values[] = { 40, 10, 100, 90, 20, 25 };int compare(const void* a, const void* b)
{return (*(int*)a - *(int*)b);
}int main()
{int n;qsort(values, 6, sizeof(int), compare);for (n = 0; n < 6; n++)printf("%d ", values[n]);return 0;
}

输出:

10 20 25 40 90 100

qsort进行交换的本质是对两个数组成员进行逐字节交换。

指针复习题

因为指针的细节特别多,于是整理一部分指针的复习题进行复习。

数组和指针

尝试分析每个printf的运行结果。

案例1

#include <stdio.h>
#include <string.h>int main() {//一维数组int a[] = { 1,2,3,4 };printf("%d\n", sizeof(a));printf("%d\n", sizeof(a + 0));printf("%d\n", sizeof(*a));printf("%d\n", sizeof(a + 1));printf("%d\n", sizeof(a[1]));printf("%d\n", sizeof(&a));printf("%d\n", sizeof(*&a));printf("%d\n", sizeof(&a + 1));printf("%d\n", sizeof(&a[0]));printf("%d\n", sizeof(&a[0] + 1));return 0;
}

从第7行的printf开始。

  1. 输出16,因为是数组的大小。
  2. 首元素地址,指针。x86环境下是4x64环境下是8。
  3. 对数组名解引用,得到首元素的值,是int型变量,所以是4。
  4. 首元素地址加1还是地址。
  5. 索引为1的元素,是int型变量。
  6. 取整个数组的地址,还是地址。
  7. 优先级&大于*,所以先取地址&,再解引用*&抵消,或取地址后再解引用。所以是数组的大小。
  8. 取整个数组的地址再跳过1个单位,本质还是地址。
  9. 取首元素地址。
  10. 取变量地址再加1,本质还是地址。

案例2

#include <stdio.h>
#include <string.h>int main() {//字符数组char arr[] = { 'a','b','c','d','e','f' };printf("%d\n", sizeof(arr));printf("%d\n", sizeof(arr + 0));printf("%d\n", sizeof(*arr));printf("%d\n", sizeof(arr[1]));printf("%d\n", sizeof(&arr));printf("%d\n", sizeof(&arr + 1));printf("%d\n", sizeof(&arr[0] + 1));printf("%d\n", strlen(arr));printf("%d\n", strlen(arr + 0));printf("%d\n", strlen(*arr));printf("%d\n", strlen(arr[1]));printf("%d\n", strlen(&arr));printf("%d\n", strlen(&arr + 1));printf("%d\n", strlen(&arr[0] + 1));return 0;
}
  1. sizeof里放数组名,返回整个数组的大小。所以是6。
  2. 数组名加整数,相当于地址加整数。所以是指针。
  3. 对首元素地址解引用,是char型变量。所以是1字节。
  4. 索引为1的数组成员。
  5. 取整个数组的地址,所以是指针。
  6. 取整个数组的地址再加1,所以是指针。
  7. 取首元素地址再加1,所以是指针。
  8. 因为这个字符数组没有用\0结尾,所以凡是和strlen(读取指定地址开始的字符数,直到读到\0)有关系的都会越界访问,结果不可预测。
    其中printf("%d\n", strlen(*arr));printf("%d\n", strlen(arr[1]));更是直接将字符的ASCII码值当成字符串的首元素地址,
    printf("%d\n", strlen(&arr + 1));则是取整个数组的地址再加1,所以跳过了整个数组。

案例3

#include <stdio.h>
#include <string.h>int main() {char arr[] = "abcdef";printf("%d\n", sizeof(arr));printf("%d\n", sizeof(arr + 0));printf("%d\n", sizeof(*arr));printf("%d\n", sizeof(arr[1]));printf("%d\n", sizeof(&arr));printf("%d\n", sizeof(&arr + 1));printf("%d\n", sizeof(&arr[0] + 1));printf("%d\n", strlen(arr));printf("%d\n", strlen(arr + 0));printf("%d\n", strlen(*arr));printf("%d\n", strlen(arr[1]));printf("%d\n", strlen(&arr));printf("%d\n", strlen(&arr + 1));printf("%d\n", strlen(&arr[0] + 1));return 0;
}
  1. sizeof里放数组名,返回整个数组的大小。因为结尾有\0,所以是7字节。
  2. 数组名加整数,相当于地址加整数。所以是指针。
  3. 对首元素地址解引用,是char型变量。所以是1字节。
  4. 索引为1的数组成员,是char型变量。所以是1字节。
  5. 取整个数组的地址,所以是指针。
  6. 取整个数组的地址再加1,所以是指针。
  7. 取首元素地址再加1,所以是指针。
  8. 13、14、17和19均能返回字符串的长度。
    15、16是将字符的ASCII码当成字符串的首元素地址,18是跳过了整个数组,这些都是不合法的情况,结果无法预测。

案例4

#include <stdio.h>
#include <string.h>int main() {char* p = "abcdef";printf("%d\n", sizeof(p));printf("%d\n", sizeof(p + 1));printf("%d\n", sizeof(*p));printf("%d\n", sizeof(p[0]));printf("%d\n", sizeof(&p));printf("%d\n", sizeof(&p + 1));printf("%d\n", sizeof(&p[0] + 1));printf("%d\n", strlen(p));printf("%d\n", strlen(p + 1));printf("%d\n", strlen(*p));printf("%d\n", strlen(p[0]));printf("%d\n", strlen(&p));printf("%d\n", strlen(&p + 1));printf("%d\n", strlen(&p[0] + 1));return 0;
}
  1. p是指针,用sizeof输出,在32位环境下编译的程序运行结果是4,64位是8。
  2. 指针加整数还是地址。
  3. char*指针解引用,是char型变量。
  4. 对常量字符串的首元素地址解引用,是char型变量。
  5. 取指针的地址。
  6. 取指针的地址再加一个整数,还是地址。
  7. 取常量字符串的首元素地址,再加整数,还是地址。
  8. 15、16将字符的ASCII码值当地址,18是跳过整个常量字符串,这些都不合法,其余的strlen都是合法的。

案例5

#include <stdio.h>
#include <string.h>int main() {//二维数组int a[3][4] = { 0 };printf("%d\n", sizeof(a));printf("%d\n", sizeof(a[0][0]));printf("%d\n", sizeof(a[0]));printf("%d\n", sizeof(a[0] + 1));printf("%d\n", sizeof(*(a[0] + 1)));printf("%d\n", sizeof(a + 1));printf("%d\n", sizeof(*(a + 1)));printf("%d\n", sizeof(&a[0] + 1));printf("%d\n", sizeof(*(&a[0] + 1)));printf("%d\n", sizeof(*a));printf("%d\n", sizeof(a[3]));return 0;
}
  1. sizeof(数组名),所以是整个数组的大小,也就是 3 × 4 × 4 = 48 3\times 4\times 4=48 3×4×4=48字节。
  2. 单个int变量,所以是4。
  3. 二维数组中索引为0的一维数组的数组名,所以是整个一维数组的大小,也就是16。
  4. 地址加整数,还是地址。
  5. int*解引用,是int变量。
  6. 取首元素的地址再加1,还是地址。
  7. 首行一维数组的地址加1,变成第2行一维数组的数组名。
  8. 首行数组的地址加1,还是地址。
  9. 8的基础上解引用,索引为1的数组的数组名。
  10. 对数组名解引用,首行的数组名。
  11. 索引为3的数组的数组名。

对指针的理解

案例1

#include <stdio.h>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是取整个数组的地址再加1,或int(*)[5]型指针加1,跳过了整个一维数组。

所以*(a+1)首元素地址加1再解引用,所以是2;*(ptr-1)是按照int*类型指针的特性减去1个单位,所以是5。

请添加图片描述

案例2

#include <stdio.h>struct Test
{int Num;char* pcName;short sDate;char cha[2];short sBa[4];
}*p=(struct Test*)0x100000;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
int main()
{printf("%p\n", p + 0x1);printf("%p\n", (unsigned long)p + 0x1);printf("%p\n", (unsigned int*)p + 0x1);return 0;
}

根据结构体对齐的原理,Test类型占用的内存大小是20字节。

14行的p+0x1本质是跳过1个Test单位也就是20个字节,所以输出0x100020

15行先将地址强制转换为无符号长整型再加1,就只是数值上的加1,按照地址输出后还是这个数值,所以输出0x100001

16行先将地址强制转换为unsigned*指针再加1,跳过4个字节,所以输出0x100004

案例3

#include <stdio.h>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;
}

ptr1是取整个数组的地址再加1,相当于是跳过了整个数组。

ptr2则是将首元素地址强制转换为整型,再加1后,再强转为int*

ptr[-1]相当于*(ptr+(-1)),所以是4。

*ptr2取决于系统采用的存储方式,小端存储的话就是0x20000000,大端存储的话因为对齐的原因可能出现未定义行为。

请添加图片描述

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

二维数组用三个逗号表达式初始化,所以是int a[3][2]={1,3,5};

然后p保存第1行一维数组的首元素地址,最后p[0]相当于*(p+0),实际还是*p,所以是1。

案例5

#include <stdio.h>
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;
}

p指向列数为4的二维数组,a本身就是列数为5的二维数组。

所以若在一维数组的视角来看的话,&p[4][2]等价于(a+4*4+2)&a[4][2]等价于(a+4*5+2)

所以地址之差&p[4][2] - &a[4][2]-4,用%p输出其实也算是用十六进制输出,因此这个代码输出FFFFFFFC,-4

请添加图片描述

案例6

#include <stdio.h>
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[2][5]

因此&aa+1是跳过整个数组,ptr1int*指针,ptr1-1指向最后1个元素10所在的内存;

aa+1表示的是二维数组中索引为1的一维数组的首元素地址,因此ptr2int*指针,ptr2-1指向元素5所在的内存。

因此输出10,5

案例7

#include <stdio.h>
int main()
{char* a[] = { "work","at","alibaba" };char** pa = a;pa++;printf("%s\n", *pa);return 0;
}

因为pa的类型是存放char*的指针,因此pa++会跳过一个char*的内存空间,也就是说pa指向at的首元素地址。故输出at

案例8

#include <stdio.h>
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*数组,每个元素都保存有一段字符串常量。

然后是char** cp[] = { c + 3,c + 2,c + 1,c };,每个元素存有一个char*的地址,因此可以暂时这样理解:cp[]={&"FIRST",&"POINT",&"NEW",&"ENTER"};,即取字符串常量的地址。

再然后是cpp是三级指针,它存有二级指针数组的首元素的地址,对它解引用一次得到char**数组的内容,解引用两次得到char*的内容,也就是常量字符串的内容。

拆解到这里,再来看printf

  1. printf("%s\n", **++cpp);*++的优先级相同,因为结合性是从右往左,所以先执行++cpp,再经过二次解引用得到常量字符串POINT的内容。因此第1个print输出POINT

请添加图片描述

  1. printf("%s\n", *-- * ++cpp + 3);:进行的操作按结合性可拆解成这样:(*(-- (* (++cpp)))) + 3,因此在1的基础上首先进行++cpp,使cpp指向&"NEW",再解引用即*(++cpp)得到指向NEW的二级指针。
    然后进行--操作,移动一个char**的单位(-- (* (++cpp)))得到存放ENTER的首元素地址也的指针也就是char*指针的地址,再一次解引用
    *(-- (* (++cpp))))得到首元素的地址,此时再加3就得到字符串ER

请添加图片描述

  1. printf("%s\n", *cpp[-2] + 3);:同样在2的基础上进行**(cpp-2)运算,得到字符串FIRST的首元素地址,再移动3个单位得到字符串ST

请添加图片描述

  1. printf("%s\n", cpp[-1][-1] + 1);:同样在2的基础上,进行*(*(cpp-1)-1)的操作,得到NEW的首元素地址,再加1得到字符串EW

请添加图片描述

http://www.xdnf.cn/news/197875.html

相关文章:

  • js补环境工具使用技巧、补环境实例、重点环境检测点详解
  • Qt开发:XML文件的写入与读取
  • AI与机器人外科手术:如何用智能化技术提升手术精度与安全性?
  • 【android bluetooth 协议分析 06】【l2cap详解 10】【通过avdtp连接流程,感受l2cap通道的生命周期变化】
  • [JavaScript]对象关联风格与行为委托模式
  • WSL释放空间
  • ‌wangEditor 所有菜单项分类、说明及隐藏方法
  • Java项目场景题深度解析
  • Termux - Android终端应用与Linux环境
  • Java读Excel:解析阿里云easyExcel导入文件的行号
  • vmare pro安装报错用户在命令行上发出了EULAS_AGREED=1,表示不接受许可协议的错误解决方法
  • 高压开关柜局部放电信号分析系统
  • C/C++链表的常用操作实现
  • three.js后处理原理及源码分析
  • HTML5好看的水果蔬菜在线商城网站源码系列模板7
  • 文档在线协同工具ONLYOFFICE教程:如何使用宏突出显示具有特定提示文本的空文本字段
  • window 图形显示驱动-在 WDDM 1.2 中提供无缝状态转换(下)
  • 系统架构师2025年论文《论面向对象的软件设计——UML 在面向对象软件架构中的应用》
  • leetcode 876. 链表的中间结点
  • Python 实现的运筹优化系统数学建模详解(动态规划模型)
  • 第二阶段:基础加强阶段总体介绍
  • 网络安全怎么入门?快速了解
  • 基于大模型的公安预审办案笔录分析的挑战与应对策略-3
  • 2025汽车制造企业数字化转型路径参考
  • TypeScript之基础知识
  • vue报错:Loading chunk * failed,vue-router懒加载出错问题。
  • C++复习补充 类型转换和RTTI
  • 人工智能与机器学习:Python从零实现K-Means 算法
  • RAG工程-基于LangChain 实现 Advanced RAG(预检索优化)
  • 驱动开发硬核特训 │ Regulator 子系统全解