知识屋:更实用的电脑技术知识网站
所在位置:首页 > 科技

C语言指针-初学者也能看懂

发表时间:2022-03-26来源:网络

1.指针是什么(可能有点难理解)

指针的是啥?


指针实际上就是地址,地址就是系统给定的编号,编号就是一个个内存单元。


在某种情况来说指针=地址=编号=内存单元。


指针就是地址,顾名思义,就是可以用来寻找目标的。


所以指针变量就是存放地址的变量。


当然我们口头上常说的指针就是指针变量~


那指针是怎么产生的呢,也就是说内存是怎样产生的呢?


我们知道我们的计算机就是32位或64位系统组成,这32与64在物理上就是32根物理电线或64根物理电线组成。这物理电线通电时,就会产生高电频,从而产生电信号,再由电信号转变为数字信号,在我们计算机上就显示数字信号,我们知道计算机只能识别二进制系列数字,所以这就最终变为1与0组成的数字信号。最终就有二的三十二或六十四次方的存储空间,经过换算,也就是4或8个GB,也就是内存,当然这些都是有硬件决定的。


而指针的大小也就是4个字节或八个字节组成(与指针的类型无关)


原理:指针的大小由系统决定,比如32位系统,就由32给比特位组成,


比如00000000000000000000000000000001


也就是4个字节~


也就是说无论是char类型还是int类型指针大小都是4或8(在64位系统上)个字节。

2. 指针和指针的类型

int a=10;


int* pa=&a; //此时int*就是指针pa的类型,而pa就是指针变量,来储存地址的~


指针类型分很多种如int,float,double等;


既然大小都是四个字节,那为什么要区分不同类型的指针呢?


这就要说到指针类型的特点:


1.指针类型决定解引用时访问几个字节;一个int类型指针就直接访问4给字节的空间,一个char类型就只能访问一个空间的内存等等


2.指针类型决定了指针向前一步或向后一步能走多远的距离。就比如说int类型的指针加一个单位就相当于走了四个字节的空间。

从图上就可以看到,当指针变量的类型是int型时,指针变量加一,地址就变了四个字节的,当指针是char类型的时候,指针变量加一就变成跳过一个字节的单位了。


可以连续定义多个指针如:


int*pa,*pb;(并不是int*pa,pb);


3.野指针

所谓野指针就是没有地址的指针,系统就会出现错误,都是造成野指针的情况下,系统往往都不会报错,都不能说明,野指针就是正确的,就比如说,一个小偷,没被进警察抓到就能说明他的行为是对的吗?回答是,当然不能。

虽然最终程序依然可以运行,但是最终的结果并不会是预期的那样。这时候就占用了一块未申请的空间了。这块空间的内容是随机的。


那野指针是怎样造成的呢?怎样避免野指针的发生呢?


造成原因:1.由于指针未定义,就开始使用。


比如:int* p;


*p=20;


2.指针的越界访问,这常常体现在数组里。



这就出现溢出的情况了~


3.指针指向的空间释放,在函数中很常见。

既然出现了野指针,那有什么办法可以避免野指针的出现呢?


解决方案


1.给指针初始话


2.小心指针越界


3.指针指向的空间释放后,及时定位NULL,防止被再次使用。


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


5.指针使用前先检测有效性。


4.指针的运算

4.1 指针的加减整数的运算


指针加减一个整数,地址就会有相应的变化;



4.2 指针-指针


这里的指针-指针,最终的结果是中间的元素个数(而不是中间有几个字节)


几个字节由指针的类型决定;


当然,既然由减,肯定有人会问为什么没加呢(问得好,下次别问了)


两指针相减,得到的是两个地址中间的元素个数,那相加就没有什么特别的含义了,所以不存在相加。


举个例子


我们平时计算字符串的长度通常有三种方法(1.计数器 2.递归 3.就是指针-指针)


前两种方法就不讲了~ (前提是两个指针是指向同一块空间)



5.指针与数组

指针是地址,口头上也就是指针变量~


数组是什么:数组就是相同类型数据的集合~


这似乎两者没什么关系,但是实际上我们可以通过指针来访问数组。


事实证明:*(p+i) *(arr+i) arr[i] p[i] 这四种情况的结果是相同的,都可以访问元素


他们代表的意义是相同的!!!


6.二级指针

你可能会担心很难(我指针都不懂,还二级指针,不看了)


别~~~~


其实二级与多级指针都很容易


二级指针指向的就是存放一级指针的地址(指针)


int main() { int a = 10; int* pa = &a; int** ppa = &pa;//ppa就是一个二级指针 **ppa = 20; printf("%d\n", a); //int*** pppa = &ppa;//pppa就是三级指针 return 0; }


7.指针数组

指针数组是数组还是指针呢?


答案是:数组,是用来存放指针的数组。


除了指针数组外,其实我们还学习了整形数组和浮点型数组;


比如:


int* arr[4]={&a,&b,&c,&d}


&a

&b

&c

&d

int arr[4]={a,b,c,d}


a

b

c

d

对比一下,是不是很容易理解了,数组就是装有相同类型的数据,而指针数组也一样,只是里面装的都是一个个指针,不要想的很难哦~

这是指针的进阶


坚持看完,一定会有很大的收获~~


那接下来—起航


1.字符指针

我们目前知道整形指针,浮点型指针,字符指针跟他俩类型


字符指针—顾名思义就是指针,一个char*类型的指针


在讲解字符指针前,我先提一下怎么连续创建多个指针


连续创建多个指针的方法:


你可能会想到用:


int a ,b;

int* a,b;

或者


#define PINT int*

int main(){

int a ,b;

p a,b;

}

但是实际上这样创建的结果是:


创建一个整形指针int*a与整形变量int b.


创建指针的正确打开方式:


//方案一,直接定义两次

int a,b;

int*a; int*b;

//方案二,采用typedef重定义

typedef int* pint

{

int a,b;

pint pa, pb;//此时就是定义int *pa与int *pb都是指针变量

return 0;

}

下面给出一个简单的代码:


char ch='w';

char* p=ch;

char* p="abcde";

定义一个char类型的变量ch,将ch地址放在指针变量p中


此时p存放的就是字符w的地址


这个很容易理解,那么char* p="abcde"是什么意思呢


实际上这次的p存放的是字符串abcde的首元素的地址,也就是a的地址


有了首地址,就很容易找到后续元素的地址


例题 1

判断下面代码是否相等

char arr1[] = "abcdef";

char arr2[] = "abcdef";

const char* str1 = "abcdef";

const char* str2 = "abcdef";

#include//证明相等关系 int main() { char arr1[] = "abcdef"; char arr2[] = "abcdef"; const char* str1 = "abcdef"; const char* str2 = "abcdef"; if (arr1 == arr2) printf("arr1==arr2\n"); else printf("arr1!=arr2\n"); if (str1 == str2) printf("str1==str2\n"); else printf("str1!=str2\n"); return 0; }

为什么arr1!=arr2; str1==str2


首先创建数组,arr1与arr2,先向系统申请两个不同的空间,然后将abcdef放入两个不同的空间里,所以这两个空间的地址当然就不相同


其次是str1与str2的abcdef只存储在只读存储区(这跟const无关,const起到强调的作用,实际上有无const的意思是相同的),就是不能更改其中的元素;


这时候两字符串的内容相同,系统就不会在浪费多余的空间去储存两个不同的内容~


所以就形成str1==str2,实际上这两个变量储存的都是a的地址


2.指针数组

在这之前我们知道整形数组,浮点型数组等


意思就是储存整形与浮点型数子的数组


指针数组就是存放指针的数组,实际上还是数组,里面存放着不同类型的指针

int*arr[5];//整形指针数组

char*arr[5];//一级字符指针数组

char**arr[5];//二级字符指针数组

知道定义,那如何使用呢


例题 2

多组打印字符串


#include int main()//指针数组 { char* arr[] = { "abcdef","ghi" ,"jklmn" }; //打印 int i = 0; int sz = sizeof (arr) / sizeof (arr[0]); for (i = 0; i < sz; i++) { printf("%s\n",arr[i]); } return 0; }

其中的 char* arr[] = { "abcdef","ghi" ,"jklmn" }就是指针数组,存放的就是char类型的指针,


他的作用就相当于:


char arr1[] = "abcdef";

char arr2[] = "ghi";

char arr3[] = "jklmn";

打印的结果就是:

✨✨打印整形的数组


#include 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 }; int i = 0; int j = 0; for (i = 0; i < 3; i++) { for (j = 0; j < 5; j++) { printf("%d", arr[i][j]); } printf("\n"); } return 0; }

3.数组指针

3.1数组指针的定义

有了指针数组的概念,相信很多人就知道数组指针的概念


跟你们想的一样


数组指针—就是指针,什么指针呢,存放数组的指针


int *pint;//能够指向整形数据的指针

char *p;//能够指向字符数据的指针

char (*p)[10];能够指向数组的指针

它的类型包括int(*)[], char(*)[]


解读一下,其中的(*)就代表是一个指针[]就代表是一个数组的指针,char或者int就代表数组中的数据是啥类型的元素


那么int * p1[10]与 int (*) p2[10]


其中的p1 p2是什么意思呢


前者是一个数组(指针数组)变量


后者是一个指针(数组指针)变量


3.2 &数组名与数组名

我们知道数组名就是首元素的地址(数组名直接与sizeof相连与&数组名除外)


那么&数组名是什么意思呢


实际上&数组名就是整个数组的地址


#include int main() { int arr[20] = { 0 }; printf("%p\n", arr); printf("%p\n", arr+1); printf("%p\n", &arr); printf("%p\n", &arr+1); return 0; }


有打印的结果中很容易看出


arr 与 arr+1隔四个字节,也就是一个整形的大小


而&arr与&arr+1之间隔八十个字节,就是二十个整形的大小


接下来在看一个代码


int *p[10];

int *(*pp)[10];

此时的int *p[10],p就是指针数组变量


int *(*pp)[10]这句的意思就是定义一个指针变量,什么指针?数组指针,而这个数组里的元素都是int*,有十个int*类型的数据。这就是数组指针~~


故arr在一般情况下都是这个数组首元素的地址,&arr就是整个数组的地址


3.3 数组指针的使用

说完数组指针的定义和使用规则,下面讲解数组指针的使用




给定三个数组,分别打印出他们的值


#include print(int (*p)[5],int r,int c)//此时的int (*p)[5]就是一个数组指针 { int i = 0; int j = 0; for(i=0;i

这道题运用了一个二维数组


在这里我将二维数组看作是一维数组,也就是把一行当作一个元素


所以此数字有三个元素


就算是说二维数组的数组名就是第一行的的地址


当然一维数组里也可以用到数组指针的思想,但实际上也不需要,这样反而变得麻烦,反而在二维数组中可以很好的利用。


那下面来总结一下:


int *arr;

int *parr1[10];

int (*parr2)[10];

int(*parr3[10])[10]

int *arr


是一个指针,类型是int*的指针;


int *parr1[10]


是一个数组,数组的类型是int *[],且是一个指针数组


int (*parr2)[10]


是一个指针,指针的类型是int(*)[],且是一个指针数组,数组中有十个整形元素


int(*parr3[10])[10]


是一个数组,此时把parr3去掉就得到nt(*)[],这里就是一个数组指针,所以这是一个存有十个数组指针的数组。


4.数组与指针在函数里的传参

函数传参的前提就是形参与实参是对应的关系,也就是说实参是什么类型,那么形参也是相应的类型,比如说实参是数组,那形参就是数组,参数是指针,那么形参也是指针。


4.1 一维数组的传参

#include test(arr1[]) {} test(arr1[10])//上面两种方法,实参是数组,用数组来接收,因为在函数中本身不创建空间,所以无论【】{} //中的值为多少,都能达到目的~ test(*arr1) {} //前面我提到数组名就是首元素的地址,此时就把实参中的数组名当作首元素的地址,此时用指 //针来接收 test(*arr2[10]) {} //这里也跟第一二两个相同,也是数组传参,数组来接收 test(**arr2) {} //这里采用二级指针来接收,实参是一级指针,那么一级指针的指针就可以用二级指针来接收 int main() { int arr1[10]={0}; int* arr2[10]={0}; test(arr1); test(arr2); return 0; }

4.2 二维数组的传参

#include //通过数组 test(int arr[10][10]) //二级指针传参时以数组的形式,【】中的行可以省略,但是列不能省略 {} //列可以让系统知道一行有多少的元素,从而分配多少的空间,便于知道每一 //个元素的地址,故test(int arr[10][10])与test(int arr[][10])是可以 //进行传参的,但是test(int arr[][])显然是不可以的。 test(int arr[][]) {} test(int arr[][10]) {} //通过指针 test(int *arr) {} test(int (*arr)[10]) //我们知道实参传的是首元素的地址,这里的首元素的地址就是第一行元素的 {} //地址,也就是数组的地址,所以需要数组指针来接收 //故需要test(int (*arr)[10])来接收,而一级指针与二级指针显然是不可的 test(int** arr) {} int main()//二级指针传参 { int arr[10][10] = { 0 }; test(arr); return 0; }

4.3 一级指针的传参

#include test(int *p) {} int main() { int a = 10; int* par = &a; int arr[10]; test(par); test(arr); test(*a); //当一级指针传参时,形参是指针,那么实参就可以用这三种方式传参 return 0; }

4.4 二级指针的传参

#include //当二级指针传参时,形参是二级指针,那实参有哪些方式呢 test(int** p) {} int main() { //二级指针传参 char a = 'w'; char* p = &a; char** pp = &p; char* arr[10] = { 0 }; test(&p); //通过一级指针p的取地址 test(pp); //通过二级指针传参,用二级指针来接收 test(arr); //通过char*()类型指针数组名来传递 return 0; }

5.函数指针

数组指针就是数组的地址


那么函数指针也是存放函数地址


那问题是函数有指针吗


#include int Add(int x,int y) { int sum = 0; sum = x + y; } int main() { //证明函数有地址 int a = 0; int b = 0; printf("%p", &Add); printf("%p\n", Add); //定义一个函数指针变量 int (*pf)(int,int)=&Add; //此时的pf就是函数的指针变量,用pf就可以调用函数 return 0; //括号中是参数的类型 }


这段代码证明,函数也有地址,而且取地址加函数名与函数名的作用是相同的,既然函数有地址就可以通过指针调用这个函数


调用这个函数:


#include int Add(int x,int y) { int sum = 0; sum = x + y; return sum; } int main() { int (*pf)(int,int) = &Add; int ret1=(*pf)(2, 3); //括号别忘了 int ret2 = (pf)(2, 3);//这里的括号可以省略 printf("%d\n", ret1); printf("%d\n", ret2); return 0; }

打印的结果:

可以看到,主函数中的pf与*pf的使用效果是一样的,打印的结果也是一样的,所以是否有*都可以达到相同的目的,但是*代表着解引用,容易理解~


下面给出了一个有趣的代码:


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


这段代码的意思是什么呢,是不是看着有点晕


实际上把signal (int,void (*) (int))提出来剩下的就是void(*)(int),其中的signal是函数声明。


首先这是一个函数指针,有两个参数,一个参数是int,还有一个是函数指针,其返回值就是void(*)(int),也就是一个函数指针。


这个函数可以简化一下,也就是把void(*)(int)重新定义为一个新的变量


你可能认为是typedef void(*)(int) pfun_t


这样的确好理解一些,毕竟跟我们所学习的结构体是一样的


但是真正的结果是typedef void(*pfun_t)(int) ,其中的pfun_t放在中间,我这样学也起到了强调的作用。


那void(* signal (int,void (*) (int)) )(int)简化的结果是pfun-t signal(int,void (*) (int))


pfun-t是类型名,并不是类型名


6. 函数指针数组

前面我们学了整形指针数组,字符指针数组,和他们相同,函数指针数组也是一个数组,只不过数组里的元素是整形指针。


函数指针数组可以一次性实现多个函数的调用


#include 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 main() { //int (*pf1)(int, int)=&Add; //int (*pf2)(int, int)=⋐ //int (*pf3)(int, int)=&Mul; //int (*pf4)(int, int)=&Div; int (*pfAdd[4])(int, int) = { &Add ,&Sub,&Mul,&Div };//数组指针可以很好的实现多次的定义 int i = 0; for (i = 0; i < 4; i++) { printf("%d\n", *(pfAdd[i])(8, 4)); } return 0; }


通过简单的数组的调用,就可以实现加减乘除的运算,那函数指针数组有什么用呢


既然函数指针数组可以同时实现加减乘除,那当然可以实现一个计算器


用函数指针数组实现一个计算器

#include 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; } menu() { printf("*****************************************\n"); printf("******** 1.Add 2.Sub ***********\n"); printf("******** 3.Mul 4.Div ***********\n"); printf("******** 0.exit ***********\n"); printf("*****************************************\n"); } int main()//用函数指针来实现一个计算器 { int x = 0; int y = 0; int input = 0; int ret = 0; int (*pfArr[5])(int, int) = { 0, &Add ,& Sub,& Mul,& Div }; do { menu(); printf("请选择:>"); scanf_s("%d", &input); if (input == 0) { printf("计算机以关闭"); } else if (input >= 1 && input

7.回调函数

回调函数就是通过函数指针调用的函数,如果把一个函数的指针(地址)当作参数,传给另一个函数,当这个函数调用所指的函数时,我们就说这是回调函数。


下面来用回调函数来实现上一次用函数指针数组实现的计算器


#include 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; } menu() { printf("*****************************************\n"); printf("******** 1.Add 2.Sub ***********\n"); printf("******** 3.Mul 4.Div ***********\n"); printf("******** 0.exit ***********\n"); printf("*****************************************\n"); } void calc(int (*pf)(int, int)) { int x = 0; int y = 0; printf("请输入两个数:>"); scanf_s("%d %d", &x, &y); int ret = pf( x, y); printf("%d\n", ret); } int main() { int input = 0; do { printf("请选择:\n"); menu(); scanf_s("%d", &input); switch(input) { case 1: calc(Add); break; case 2: calc(Sub); break; case 3: calc(Mul); break; case 4: calc(Div); break; case 0: printf("退出计算器"); break; default: printf("选择错误"); break; } } while (input); return 0; }

如果想更好的了解,欢迎关注我的博客

指针初阶

指针进阶

收藏
  • 人气文章
  • 最新文章
  • 下载排行榜
  • 热门排行榜