欢迎访问 生活随笔!

生活随笔

当前位置: 首页 > 编程资源 > 编程问答 >内容正文

编程问答

万字长文搞定C语言指针

发布时间:2024/10/14 编程问答 63 豆豆
生活随笔 收集整理的这篇文章主要介绍了 万字长文搞定C语言指针 小编觉得挺不错的,现在分享给大家,帮大家做个参考.

目录:
1.指针是什么?
2.定义和使用指针变量
        定义指针变量
        指针的初始化、赋值、取值
        指针变量的交换
3.指针变量作为函数参数
4.通过指针引用数组
        指针引用一维数组
        指向一维数组的指针+1
        指针变量作为函数的参数
        指针指向多维数组
        指向一维数组的指针
5.通过指针引用字符串
        字符形指针变量
        字符串指针变量和字符数组的比较
6.指向函数的指针
        函数指针
        返回指针值的函数
7.指针数组和多重指针
        指针数组
        指向指针变量的指针
8.动态内存分配与指向它的指针变量
        动态内存分配与C语言内存模型
        动态内存分配与释放函数

前言:指针是C语言最重要的一块知识,也是我们必须要掌握的内容,对于初学,可能很难,但是迎难而上才是我们学习必须有的态度。由于博主水平有限,如果博客中出现错误,还忘指正,博主会在第一时间修改

1.指针是什么?

在我们学习C语言的过程中难免会定义变量,如:

int n=1;

对程序进行编译的时候会根据n的数据类型为n分配内存,我们通过前面的学习知道,int类型的数据在内存中占据4个字节,内存区的每一个字节都有一个编号,这个编号叫地址(这就像人的名字一样,一个名字对应一个人)。地址指向变量单元。实际上,计算机是通过变量名找到存储单元的地址,对变量值的存取都是通过地址进行的,比如上边定义的变量n

这里我们区分几个概念:

地址就相当于一个旅馆房间的门牌号,变量单元就相当于房间,存放的数据就相当于房间里边的人。

接下来我给出指针的概念:

一个变量的地址称为该变量的指针,如果有一个变量专门存储另一变量的地址,则称这个变量为指针变量

讲到这里不知道你有没有一个疑问,既然int型的变量有4个字节,每个字节有一个地址,那么这个变量的指针是四个字节中的哪一个字节的地址?

答案是第一个字节。口说无凭,我们做一个实验,我们知道一维数组的内存空间是连续分配的,而且数组中的每一个下标都相当于一个变量(如arr[[0],arr[1]…),那么我们打印数组中的连续两个下标就可以得出结论了

2.定义和使用指针变量

2.1定义指针变量

类型名 *指针变量名

int *p;

类型名代表指针指向的数据的数据类型(也称基类型),比如上面定义的指针变量p 只能用来指向int类型的数据,不能指向浮点型数据。*指的是定义的变量是指针类型。

2.2指针的初始化、赋值、取值

指针可以定义时初始化如:

int a=1; int *p=&a;

&a就是把a的地址传递给整形指针p

注意:注意不要把变量赋给指针,要把变量的地址赋给指针

同样的也可以定义后赋值如:

int a=1; int *p; p=&a;

注意:再定义后初始化时p已经是一个指针类型的变量不要写成*p=&a;

当我们想要取得指针所指向对象的值时,我们需要*运算符

int a=1; int *p1=&a; printf("%d",*p); //以上程序打印结果:1 //如果你使用printf("%d",p);是以整形的形式打印p所代表内存中地址的编号

2.3指针变量的交换

举个例子,请想一想下面的程序应该输出什么:

#include<stdio.h> int main() {int a=1;int b=2;int *p1=&a;int *p2=&b;printf("%d %d\n",*p1,*p2);int *p;p=p1;p1=p2;p2=p;printf("%d %d\n",*p1,*p2);printf("%d %d\n",a,b); }

在p1和p2没交换之前在内存中的指向:

交换之后p1和p2在内存中的指向:

交换的只是指向,并没有交换变量单元里边的内容,如果把a和b的值交换了,就交换的是变量单元里边的内容,如下:

3.指针变量作为函数参数

看一个程序:

#include<stdio.h> void swap(int *a1,int *a2) {int temp=*a1;*a1=*a2;*a2=temp; } int main() {int a=1;int b=2;int *p1=&a;int *p2=&b;printf("*p1=%d *p2=%d\n",*p1,*p2);swap(p1,p2);printf("*p1=%d *p2=%d\n",*p1,*p2); }

打印结果:

emm,没错就是这样的,是变量单元里内容的交换,好像自己又行了


再看一个程序:

#include<stdio.h> void swap(int *a1,int *a2) {int *temp=a1;a1=a2;a2=temp; } int main() {int a=1;int b=2;int *p1=&a;int *p2=&b;printf("%d %d\n",*p1,*p2);swap(p1,p2);printf("%d %d",*p1,*p2); }

嗯,是这样,地址交换,看一眼答案:


怎么会这样??????

地址交换,值应该变了啊,别急,首先我们要知道:

C语言里边的实参变量和形参变量的数据传递是值传递

什么意思呢?就是形参其实是实参的一个副本,这怎么讲呢,对于上面的那一段代码下面一张图:

p1把自己的值(表示地址)给了a1,p2把自己的值给了a2,也就是说就相当于新定义了两个指针变量和p1,p2指向一样。也就是说p1与p2指向始终未变,看下图,我们输出指针的地址:


如上所示,那么我们就可以知道,交换的只是a1和a2的指向,与p1和p2无关。如果你第一个程序不是这么分析,那么可以再试着分析一遍

4.通过指针引用数组

4.1指针引用一维数组

我们明确两个概念:

1.一维数组的内存分配是连续的
2.一维数组的数组名代表首元素的地址

所以我们可以得到如下:

int a[10]; int *p1=&a[0]; //int *p1=a;本行与上一行等价

4.2指向一维数组的指针+1

我们知道,指针是一个字节的地址编号,那么当一个指针指向数组元素,指针+1是不是内存中下一个字节的地址,来看一个程序:

很明显,不是简单的地址+1,而是加上一个数组元素所占字节,其实不只是一维数组,任意类型的指针+1加上的都是指针基类型的字节数,我们看下面一个程序:


通过指针这个性质,我们可以可以使用指针找到指针任意位置的元素,所以可以用下面的方式遍历数组(注意:请不要随意访问你未申请的内存):

4.3指针变量作为函数的参数

我们知道当我们使用数组数组作为函数参数有:

void fun(int arr[],int len);

其实程序在编译的时候就把arr[]当成指针变量处理,所以上面一行代码就等价于

void fun(int *arr,int len);

那么以下两个函数(fun1与fun2)也是等价的:

这里有一个注意点:

实参数组名代表一个固定的地址,或者说是指针常量,但是形参数组名并不是一个固定的地址,而是可以看做一个指针变量

举个例子(程序编译运行会报错):

这是因为我们定义的一位数组首地址是一个指针常量,我们知道常量的值是无法修改的,所以我们使用arr=arr+3;这行代码就有问题。但是当我们把这个指针常量传给函数参数时,函数参数这个时候就是一个指针变量,这个时候就可以进行类似于arr=arr+3;的操作

4.4指针指向多维数组

int a[10][10];

这里我们主要弄清几个概念:

表示形式含义备注
a二维数组名,指向一维数组a[0],即0行首地址行首地址
a[0], *(a+0), *a, &a[0][0]0行0列元素地址元素地址
a+1, &a[1]1行首地址行首地址
a[1], *(a+1)1行0列元素的地址,即a[1][0]的地址元素地址
*(a[1]+2), *(*(a+1)+2), a[1][2]1行2列元素的值,即a[1][2]的值

备注的行首地址元素地址什么意思呢?我们看一个程序:

我们知道a是代表行首地址,*a代表元素地址,使用整形的形式打印出他们的地址发现是一样的,那么你可能会问他们有什么区别,再看一个程序:

二维数组可以看做是多个一维数组为元素组成的数组,a映射到第一行(也就是第一个一维数组的首地址),*a则是映射到第一行第一列(也就是第一行第一个元素)的首地址:

第一行的首地址和第一个元素的首地址当然是一样的,因为地址是元素第一个字节的编号,只是两者映射的范围不同,映射的范围不同+1所产生的结果也不同(第二张图),比如上面两张图我们自己也可以算算,比如a是第一行数组的首地址6683776,那么a+1代表第二行数组的首地址,他俩中间隔着是10个整形元素,一个整形元素是4个字节,那么一共隔了4*10=40个字节,所以根据数组中的元素在内存中是连续分配的a+1的地址是a的地址+40,结合上面两张图a+1的地址是6683816印证了我们的想法。*a映射到的是元素,所以 * a+1映射到的就是下一个元素,一个整形元素四个字节,所以按照正常逻辑 他俩相差四个字节,上面的两张图,*a地址是6683776,*a+1的地址是6683780,这就印证了上面的讲解。补充一点:a和a[0]都是指向第一行,只是不同的表现形式而已

4.5指向整个一维数组的指针

int (*p)[4];

以上定义p表示为一个指针变量,它指向包含四个整形元素的一维数组,注意它指向的是整个一维数组,而不是一个整形元素

int a[4];

p和a是不同的,a是映射到数组第一个元素,p是映射到整个一维数组,看下面程序应该就知道我在说什么了:

用指针数组的指针作为函数的参数时:

#include<stdio.h> void func(int (*p)[8]) {} int main() {int arr[10][8]={0};func(arr); }

这个程序注意两个细节:

1.由于p映射整个一维数组,所以传参的时候传递的是二维数组名
2.由于函数参数中p是指向含8个整形元素的一维数组,所以传入的二维数组的每一个一维数组长度也是8

5.通过指针引用字符串

5.1字符形指针变量

char *str="www.baidu.com";

C语言通过字符串指针变量(str)来引用字符串常量,同时字符串常量(www.baidu.com)按照字符数组处理;str映射到的是第一个字符的地址(str+1就映射到第二个字符的地址)

注意:通过字符数组或字符指针变量可以输出一整个字符串,而对于一个数值型数组(int a[10]),是不能企图利用数组名输出它全部的数据的。

5.2字符串指针变量和字符数组的比较

char str1[20]="www.bilibili.com"; //字符数组只能定义的时候赋值,不能定义后使用str1[20]="www.bilibili.com"; char *str2="www.bilibili.com"; //对于指针变量来说可以定义后再赋值如:str2="123123";

注意:

1.字符数组里边的每一个元素存放字符串的一个字符,字符串指针变量存放的是字符串首个字符的地址
2.str1是指针常量,str2指针变量
3.编译时为字符数组分配若干个存储单元,而对字符串指针变量只分配一个存储单元
4.字符数组中的各个元素是可以改变的,字符串指针变量指向的字符串常量是不可被改变的,但是字符串指针变量的指向是可以改变的

6.函数与指针

6.1函数指针

如果我们在程序中定义了一个函数,那么编译系统会为函数分配一段存储空间,这段存储空间的其实地址称为函数的指针

#include<stdio.h> int max(int a,int b) {if(a>b)return a;return b; } int main() {int (*p)(int,int);//p只能指向返回值为int类型,参数也是两个int类型的函数p=max;int a=1;int b=2;printf("%d",p(a,b)); }

同一个函数指针可以先后指向同类型的不同函数

6.2返回指针值的函数

顾名思义,返回指针类型其实就是返回地址类型得函数,一般得定义形式为:

类型名 *函数名(参数列表)

例子:

#include<stdio.h> int *max(int *a,int *b) {if(*a>*b)return a;return b; } int main() {int a=1;int b=2;int *p=max(&a,&b);printf("%d",*p); }

7.指针数组和多重指针

7.1指针数组

定义格式:

类型名 * 数组名[数组长度]

例子:

再看一个程序:

str[0]存放字符串”aaaio"中第一个字符的地址,str[1]存放字符串"bbb"中第一个字符的地址。对于一个指针数组来说,注意,这里和二维数组不同的是:str不是指向第一行的地址,str里边存放的是str[0]的地址,相当于一个二级指针,如下(str与str[0]的地址):


str,str[0]与str[0][0]的关系就相当于,str是门牌号1,打开门牌号1对应的门,里边有一个门牌号2,这个门牌号2就是str[0],再打开门牌号2的门就可以找到元素str[0][0]

一定要注意指针数组存放的是内容是地址

这里还有一个知识点,你可以发现指针str,str+1,str+2(也就是二级指针)相差八个字节,那么也就是一个二级的char类型的指针占据八个字节,如下:


那么是不是不同类型的指针所占的内存空间大小不同,做一个实验(sizeof函数返回所占字节数):

结论:

在64位计算机中,不管什么类型的指针,都占据8个字节

注意:这里只是64位计算机,32位计算机指针占据四个字节

7.2指向指针变量的指针

当一个指针指向一个普通数据的时候,把这个指针称为一级指针,指针变量既然是变量,那么肯定也是占据内存中的,所以我们还可以用指针指向这个一级指针的的存储空间,这时候指向一级指针的指针就叫做二级指针,同理,指向二级指针的指针称为三级指针。

举个例子:

a[0]相当于存放数据元素1的地址,a[1]相当于数据元素2的地址,a存放a[0]这个整形指针的地址,a+1相当于存放a[1]这个整形指针的地址。a与p就相当于两个二级指针。

8.动态内存分配与指向它的指针变量

8.1动态内存分配与C语言内存模型

我们复习几个概念:

1.局部变量是按照动态存储方式分配内存(分配在动态存储区)
2.全局变量是按照静态存储方式分配内存(分配在静态存储区)

动态存储区分为堆和栈,动态内存分配就是分配堆中的内存空间,因为堆中的内存空间是程序员自行分配和释放的

C语言的内存模型分为5个区:栈区、堆区、静态区、常量区、代码区。每个区存储的内容如下:

内存区域内容
栈区存放函数的参数值、局部变量等,由编译器自动分配和释放,通常在函数执行完后就释放了,其操作方式类似于数据结构中的栈
堆区就是通过new、malloc、realloc分配的内存块,编译器不会负责它们的释放工作,需要用程序区释放。分配方式类似于数据结构中的链表。“内存泄漏”通常说的就是堆区。
静态区全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后,由系统释放。
常量区常量存储在这里,不允许修改。
代码区顾名思义,存放代码

8.2动态内存分配与释放函数

1.void *malloc(unsigned int size):作用是在动态存储区中分配一个长度为size的连续空间,unsigned代表没有符号位的整形数据(非负整数),返回所分配内存区域第一个字节的地址.分配失败返回NULL指针
2.void *calloc(unsigned n,unsigned size):作用是在动态内存空间中分配n个长度为size的连续空间,分配失败返回NULL指针
3.void free(void *p):释放指针变量p所指向的动态空间
4.void *realloc(void *p,unsigned int size):对已经通过malloc函数calloc函数获得了动态空间,想改变其大小,用此函数重新分配

注意:void*类型的指针表示指向空类型或者不指向确定的类型的数据

以上函数得使用#include<stdlib.h>

使用举例:

#include<stdio.h> #include<stdlib.h> int main() {int i=0;int *p=(int*)malloc(4);//(函数前的int*代表把分配的内存转换成int*类型)*p=3;printf("%d\n",*p);free(p); }

总结

以上是生活随笔为你收集整理的万字长文搞定C语言指针的全部内容,希望文章能够帮你解决所遇到的问题。

如果觉得生活随笔网站内容还不错,欢迎将生活随笔推荐给好友。