C语言指针入门讲解


入门

笔者对于这个可以算是C语言里较难的部分,其实也无法保证能解析的十分透彻,所以,若有不足之处,请各位师傅不吝赐教,若对于指针有更深理解的大佬,也欢迎讨论。

第一回合

1.指针的作用

指针是一种工具,我们可以通过指针间接访问一块内存,至于为什么要用这个神奇的东西?emmmm,笔者举个栗子,比如小明和小华是两个土豪,然后他们各自有一套房,有一天,他们想换房子住,但是又想走一个比较合理的程序,所以他们交换了房产证,这里的房产证我们就可以看作指针,房产证换了,房子也就换了。这样就可以节省很多麻烦,所以指针可以提高代码效率。

2.我们怎么定义指针?

在笔者最爱的C语言里,指针可以定义整型变量、浮点型变量、字符变量等,也可以定义这样一种特殊的变量,用它存放地址。
在编程语言里。每个变量都有自己的空间,就好比一个班里胖子和瘦子都有,两者占据的空间也是不一样的。在我们没有指针这种概念时,比如我们写一个 int a=10;那么这里的int类型就保存了整型的数据,同理像double或者char也是保存各自所具有的数据类型。
而当我们有了指针这种概念时,那么这就很有意思了,因为任何程序数据跑到内存后,在内存里都有他们的地址,这就是指针。而为了保存一个数据在内存中的地址,我们就需要指针变量。
我们可以这样理解:指针是程序数据在内存中的地址,而指针变量是用来保存这些地址的变量。

3.图解指针可好?

在这里插入图片描述
由上图可知,左边已经指入方框内的是指变量的内容,右边指向2002的是变量的地址

笔者用一个十以内的加减法来描写指针的奇妙之处

第一张图是不考虑指针时,第二张是考虑指针时,读者自行比较,就可以发现两者的差别。

4.指针变量的引用
在这里插入图片描述
由上图我们不难得出,p与*p,地址和值的关系。

我们来看一个有问题的程序

#include<stdio.h> 
int main()
{
    int n1,n2;
    int *p1,*p2;
    *p1=10;
    *p2=5;
    p1=&n1;
    p2=&n2;
    printf("n1=%d,n2=%d\n",n1,n2);
    printf("*p1=%d,*p2=%d\n",*p1,*p2);
    return 0;
}

运行结果
在这里插入图片描述
为什么什么都不出?
是因为指针变量为储存确定的地址时,不能使用,否则系统可能崩溃。

第二回合

有了第一次拜访指针的一定基础后,我们再深入一丢丢,就一小丢丢,放心,不会太难。

1.指针与函数

笔者举个栗子
我们交换两个数,用函数进行交换,代码如下

#include<stdio.h>
void swap1(int x,int y),swap2(int *px,int *py),swap3(int *px,int *py);
int main(void)
{
	int a=1,b=2;
	int *pa=&a,*pb=&b;
	
	swap1(a,b);
	printf("swap1 排序后 a=%d b=%d\n",a,b);
	
	a=1;b=2;
	swap2(pa,pb);
	printf("swap2 排序后 a=%d b=%d\n",a,b);
	
	a=1;b=2;
	swap3(pa,pb);
	printf("swap3 排序后 a=%d b=%d\n",a,b);
	return 0;
}
void swap1(int x,int y)
{
	int t;
	t=x;
	x=y;
	y=t;
}
void swap2(int *px,int *py)
{
	int t;
	t=*px;
	*px=*py;
	*py=t;
}
void swap3(int *px,int *py)
{
	int *pt;
	pt=px;
	px=py;
	py=pt;
}

运行结果
在这里插入图片描述
我们可以由这个黑黑的框框里得出,只有2成功了,为啥?
来笔者每个都分析一下
第一个swap1里面,是最常见的调用方式,也就是值调用,我们的实参a和b对应的传给了形参x和y,那个变量t实现了x和y的交换,当返回到主函数里面时,swap1()中的变量都被系统销毁了,所以主函数里面的值没有任何变化,所以a和b的结果没有改变。

第二个swap2里面,实参是指针变量pa和pb,其地址为变量a和b的地址,当我们用px和py作为形参接受pa和pb的值时,px和py里面就存放了a和b的地址,由第一回合 4.指针变量的引用的那张图可知,px和a代表同一个存储单元,只要我们改变了px的值,这块储存单元里面的内容也会发生改变,我们交换了px和py的值,主函数里面的数也就随机交换了,从而实现了目的。

第三个swap3里面,它在函数里直接交换了px和py,对于这种离谱的行为,我们可以用图来更好的说明,为什么不会影响实参pa和pb。
是时候展示博主的高超绘画功底了

综上所述,我们要是想用指针来改变主函数里的值,要将该变量的地址或者指向该变量的指针作为实参,在被调用的函数中,用指针类型形参接受该变量的地址,并改变形参所指向变量的值。

2.指针与数组

有人说数组就是指针,可以当成指针来看,其实有些道理,但是两者还是有着很大的区别,笔者曾在https://blog.csdn.net/weixin_52605156/article/details/117828483 中写出了关于指针指向数组的一道题目,我们这里看一下概念。

对于 int shuzu[10],先定义一个长度为10的数组,那么我们可以认为它有十个元素构成,我们对他们的命名是shuzu[0]、shuzu[1]…zhuzu[9]。shuzu[i]可 表示为该数组的第i个元素。(这里我们需要注意的便是shuzu[0]是第一个,而不是shuzu[1])。

在我们之前定义指针的时候,我们比方说int *ps;,那么我们就好比定义了一个整型的数据类型,可以把它看作是指向整型的指针。

当我们把指针和数组相结合的时候,比如我们用ps=&shuzu[0];我们就把它理解为ps这个东西指向数组shuzu的第0个元素,那么ps的值便是数组shuzu[0]的地址

当我们想调用我们数组里面的内容时,我们可以使用number=*ps;
这时候,我们便把数组shuzu[0]里面的内容给整到了变量number里面。

在实际敲代码的时候,会有指针的移动,也就是现在指向的比如说是第一个元素,然后经过了神奇的黑魔法,他就跑到了第二个元素。其实用大白话说,ps=&shuzu[i];,那么ps+1就是指指向了下一个元素,而ps-1就是指指向了它前一个元素。

所以,经过上述可知,如果ps指向的是shuzu[0],那么*(ps+i)就是指调用的是数组shuzu[i]的内容,ps+i储存的是数组元素shuzu[i]的地址。

当把数组名传递给一个函数时,根本上是传递的是该数组第一个元素的地址。在被调用函数中,该参数是一个局部变量,因此,数组名参数必须是一个指针,也就是一个可以用来储存地址值的变量。

3.指针与指针

这里可以说是一个无厘头的地方,因为这玩意似乎能无限的套下去,指针指向指针指向指针指向… …所以笔者就涉及到指针指向指针,在深层的希望大家自己深挖,也可以在评论区留言讨论。
笔者之前也写过指针指向指针的练习,详情参考博文:
https://blog.csdn.net/weixin_52605156/article/details/117855981
笔者这里直接手撕代码了,就不多讲概念了,因为我发现这个用文字描述太难了,所以直接上代码:

#include <iostream>//csdn-敲代码的xiaolang
using namespace std;
int main()
{
    int shuzu[5] = {1, 2, 3, 4, 5};
    int *p = shuzu;//我们让指针指向数组 
    int **zhizhen = &p;//我们再定义一个指针,指向上面的指针 
    cout << "shuzu = " << shuzu << endl;
    cout << "p = " << p << endl;
    cout << "&p = " << &p << endl;
	cout << "zhizhen = " << zhizhen << endl;
    cout << "&zhizhen= " << &zhizhen << endl;
    return 0;
}

通过运行可以得到如下的结果:
在这里插入图片描述
我们可以看到,shuzu和p的地址是一样的,因为我们用指针指向了数组,又由于zhizhen指向了指针p,所以指针的地址和指针zhizhen的地址是一样的,同理我们如果再用一个指针指向zhizhen,那么它的地址应当和&zhizhen是一样的。

我们再把数组里的元素地址打印出来
添加一段代码:

#include <iostream>//csdn-敲代码的xiaolang
using namespace std;
int main()
{
    int shuzu[5] = {1, 2, 3, 4, 5};
    int *p = shuzu;//我们让指针指向数组 
    int **zhizhen = &p;//我们再定义一个指针,指向上面的指针 
    cout << "shuzu = " << shuzu << endl;
    cout << "p = " << p << endl;
    cout << "&p = " << &p << endl;
	cout << "zhizhen = " << zhizhen << endl;
    cout << "&zhizhen= " << &zhizhen << endl;
    for (int i = 0; i < 5; i++)
    {
        cout << "&shuzu[" << i << "] = " << &shuzu[i] << endl;
    }
    return 0;
}

运行结果:
在这里插入图片描述
所以易知指针指向了数组的首地址,也就是shuzu[0]。

4.指针与字符串

字符串可以看作是一个特殊的数组,我们可以用字符串常量对指针进行初始化,或者把字符串存入到数组中,再用指针指向数组。
笔者用代码块来说明:

char *str = "I Love you.";

先对字符指针进行初始化。字符指针指向的是一个字符串常量的首地址,即指向字符串的首地址。

我们再用一个数组表示:

char string[ ]="I Love you.";

string是字符数组,它存放了一个字符串

我们用一个栗子表示:

#include <iostream>//csdn-xiaolang
using namespace std;
int main()
{
    char string[] = "csdn";
    char *p = string;
    cout<< "p = " << p << endl;
    cout<<"p = " << (void *) p << endl;//将p强制转换为void *时输出的是地址
    cout<< "*p = " << *p << endl;   
    for(int i = 0 ; i < 4; i++)
    {
        cout << "&string[" << i << "] = "<< (void *)&string[i] << endl;
    }
     return 0;
}

运行结果:
在这里插入图片描述
我们可以发现,指针p存入了字符串csdn,指针指向的地址和字符串存入的首个字符的地址相同,那么这里需要注意的是:
指针p中存放的是地址,只是当我们使用cout时,如果指针是字符型指针,那么会输出p中地址指向的内存中的内容(这里是c),直到遇到(\0)才结束。所以直接输出p时会输出csdn,而将p强制转换为【void *】输出的才是地址。

这里我们补充一下几个易混淆的写法:

void *p表示p为一个指针,其指向的类型不确定。

void *p()为一个指针函数,返回为指向void *类型的指针;

void (*p)();为函数指针,p指向一个无参数返回值为void函数的首地址;

5.指针与结构体

c语言对结构的涉及相对比与c++还是较少,但是也是十分重要的一点。一般数据量大,为了更好的调用,我们会选择使用指针和结构体结合,从而提高效率。

笔者举一个栗子

#include<stdio.h>//csdn-xiaolang
#include<string.h>
struct dalao//定义一个大佬类
{ 
    int money;
    int old;
    char name[80];
};
void hanshu(struct dalao *pd);//定义函数,为的是用指针改变对象的数值
int main(void)
{
    struct dalao d;//定义大佬类的一个对象
    struct dalao *pd = &d;//定义一个大佬结构体指针 
    strcpy(pd->name, "小佬");
    pd->old = 20;//赋值操作 
    pd->money = 10000;
    printf("%s %d岁 %d元\n", d.name, d.old, d.money);
    hanshu(&d);//我们调用函数,取那个大佬类的对象的地址
    return 0;
}
void hanshu(struct dalao *pd)
{
    strcpy(pd->name, "大佬");//用指针指向不同的部分,再通过(struct dalao *pd = &d;)达到传值的目的,这样就达到了指针与结构的结合
    pd->old = 18;
    pd->money = 20000;//这里改变了数据 
    printf("%s %d岁 %d元\n", pd->name, pd->old, pd->money);
}

运行结果:
在这里插入图片描述

第三回合

作为本部分最后一个小的回合,我们就把指针中的知识点还有一些易混淆的地方进行归纳,方便大家查漏补缺。

1.空指针

如果 p 是一个指针变量,则 p = 0; p = ‘\0’; p = 1- 1; p = 0 * 1;等等 中的任何一种赋值操作之后, p 都成为一个空指针,由系统保证空指针不指向任何实际的对象或者函数。所以,任何对象或者函数的地址都不可能是空指针。

在实际编程中不需要了解在我们的系统中空指针到底是一个神马存在,我们只需要了解一个指针是否是空指针就可以了——编译器会自动实现其中的转换,为我们屏蔽其中的实现的细节。更重要的一点是不要把空指针的内部表示等同于整数 0 的对象表示。

2.野指针

百度百科上指出:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)指针变量在定义时如果未初始化,其值是随机的,指针变量的值是别的变量的地址,意味着指针指向了一个地址是不确定的变量,此时去解引用就是去访问了一个不确定的地址,所以结果是不可知的。

也就是说野指针是未初始化或未清零的指针,他指向的内存地址不是程序员想要的,举几个栗子
eg1

char *p; //p此时便是野指针

改进措施:

char* p = NULL;

eg2

char *p=new char[10]; //指向堆中分配的内存首地址
cin>> p;
cout<<*(p+10); //后果未知

改进措施:

p=NULL;
free(p)

1.正确声明指针,让指针指向合法内存区或者NULL
2.释放指针内存时,让指针执行空,再释放内存

野指针参考资料:https://blog.csdn.net/dangercheng/article/details/12618161

3.指针数组

我们之前接触到的是数组指针,现在我们来看一下指针数组。
定义的方式:数据类型 *数组名[指针数组长度];

#include<stdio.h>//csdn-xiaolang
int main()
{
    int shuzu[5]={1, 2, 3, 4, 5};
    int i;
    int *p[5];//定义指针数组 
    int **pp;//定义一个二级数组 
    for(i=0;i<5;i++)//给指针数组赋值 
	{
	   p[i] = &shuzu[i];
    }
    pp=p;//将指针数组的首地址赋值给pp,数组存放的内容为普通变量,数组名我们看作变量的指针 
    for(i=0;i<5;i++)//利用二级指针输出数据 
	{
	printf("%d ", **pp);
	pp++;//移动指针至下一个单元 
}
    return 0;
}

运行结果:
在这里插入图片描述

4.指针作为返回值

#include <stdio.h>//csdn-xiaolang
#include <string.h>
char *hanshu(char *p1, char *p2)//定义一个函数,比较长短,指针传入
{
    if(strlen(p1)>=strlen(p2))
	{
        return p1;//把指针作为返回值
    }
	else
	{
        return p2;//同理
    }
}
int main()
{
    char p1[80], p2[80], *p;//定义两个指针数组
    gets(p1);//用gets()函数存入指针中的内容
    gets(p2);
    p=hanshu(p1,p2);//调用函数
    printf("较长的字符串是:%s\n", p);//输出 
    return 0;
}

运行结果:
在这里插入图片描述

关于指针的重点总结,像是如何分析****p之类的方法,书写指针的注意点,笔者引入知乎上的一篇文章,进行补充https://zhuanlan.zhihu.com/p/101934152

“人针合一”

本部分以习题分析解决为主,基础部分3题,中等部份3题,困难部分4题

基础

https://blog.csdn.net/weixin_52605156/article/details/117827909
https://blog.csdn.net/weixin_52605156/article/details/117828483
https://blog.csdn.net/weixin_52605156/article/details/117855981
基础部分就以笔者之前的三篇文章为主了,大家如果有问题可以在下面留言讨论。

中等

eg.1
在这里插入图片描述
代码:

#include<stdio.h>//csdn-xiaolang
void delete(int shuzu[], int k, int *p);//定于剔除人的函数delete 
int N;//定义一个变量,用来在主函数和定义的函数中使用 
int main()
{
	printf("请输入一共有多少个人: \n");//提示语句 
    int number;
	scanf("%d", &number);
	int shuzu_2[number];//再定义一个新的数组 
	N = number;//传入人数的数据 
    int i;//计数
	int j=1; 
	for(i = 1; i <= number; i++)
	{
	shuzu_2[i] = j;//把围成圈的人每个人给一个编号 
	j++; 
	}
	int k;
	while(number >= 1)//如果人数大于等于1个人 
	{ 
	for(i = 1; i <= N; i++)//从i=1到刚开始的人数 
	{
	if(shuzu_2[i] == 3)//当某个人的报数为3的时候 
	{
	k = i;//把这个人的位置记录到一个新的变量之中 
	break;//结束当前的循环,因为我们到3的时候,需要标注一下 
	} 
	} 
	if(number >= 3)//当人数输入的比3大或者相等,我们在while(number>=1)的前提下 
    {
	delete(shuzu_2, k, &number);//调用删除函数 
	}
	if(number == 2)//当人数刚好等于2的时候 
	{
	for(i = 1; i <= N; i++)//我们要考虑一个人可能要报两次数 ,这里的 N 与之前的number一样 
	{
    if(shuzu_2[i] != 0)//当我们的数组元素不为0时 ,我们把它设为0 
    {
    shuzu_2[i] = 0; 
    number--;//减一个人数 
    break;
    /*我们来看一下当number为2的时候,为什么这么做,首先我们当人数比3大时,就开始踢人, 
	 当我们的人数恰好为 2 的时候,比如甲和乙,被踢出去的一定是甲,也就是先报数的那个人,
	 所以,我们从 	for(i = 1; i <= N; i++)开始循环, 若if(shuzu_2[i] != 0),我们才开始踢人,
	 若if(shuzu_2[i] == 0),我们就跳过,继续排查下一个数 
    */
	}
    }
	}
	if(number == 1)
	{
	for(i = 1; i <= N; i++)
	{
	if(shuzu_2[i] != 0)
	{
		k = i; 
	    number--;
		break;
	}
	/*我们来看一下number==1的时候,如果只剩下一个人了,我们继续从开始的位置排查,
	如果if(shuzu_2[i] != 0),那么我们就找到了最后那个人的位置,我们把它的位置记录到新的变量 K 中 
	*/
	}
	}
	}
    printf("Last number is %d", k); //找到目标元素 
	return 0;
	}
void delete(int shuzu[], int k, int *p)//剔除人数的函数,只剔除,不返回值,所以是void类型 
{
 int i=1;
 int j=1;
 int *zhizhen = &shuzu[k]; //用指针去调用数组里面的元素 
 *zhizhen= 0;//首先初始化指针 
  zhizhen++; //指针加一,代表此时是第一个元素,也就是第一个人的位置 
  while(zhizhen <= &shuzu[N] && zhizhen >= &shuzu[k])
  {
   if(*zhizhen != 0)
  {
    *zhizhen = j;
	 j++;
  }
	zhizhen++;
  }
  /*
  我们来看一下这一小部分,因为当我们刚开始传数时,传入的是 3 ,
  所以,当我们的指针所指位置代表的数值在 3 到最大 你最大 N 之间时,
  如果指针不为空,那么我们把这个指针的值改为 1 ,因为你已经把 3 剔除了,
  所以,你要从 1 开始重新统计。 
  */
  if(zhizhen == &shuzu[*p] + 1)
  {
	zhizhen = &shuzu[1]; 
  }
  /*
  当指针指向的是shuzu[*p]+1的位置时,也就是从主函数我们可知,
  你比如你zhizhen指的是 4 ,也就是当number位置在 3 的时候,你把 3 剔除了,那么是不是该从 4 计数,
  那么我们就有了 zhizhen = &shuzu[1]; 
  请注意 while(zhizhen <= &shuzu[N] && zhizhen >= &shuzu[k]),也就是我们的范围 
  */
  while(zhizhen >= &shuzu[1] && zhizhen < &shuzu[k])
  {
	if(*zhizhen != 0)
	{
	  *zhizhen = j;
	   j++;
	}
	zhizhen++; 
  }
  /*
  这一部分,当我们的指针在 while(zhizhen >= &shuzu[1] && zhizhen < &shuzu[k]) 之间时,
  如果指针不为空,你比如你指针现在在 2 的位置,你的 1 的位置也就是被要剔除,
  那么你的 2 要被作为新的起点,也就是 *zhizhen = j;
  */
	*p = *p - 1;
	//当我们跑完这个函数之后,一定会被剔除一个数,所以也就有了  *p = *p - 1;
}

运行结果:
在这里插入图片描述

eg.2
在这里插入图片描述
代码:

#include<stdio.h>//csdn-xiaolang
void strmcpy (char *s,char *t,int m);//定义一个来回粘贴,存放字符串的函数 
int main()
{
	int m;//定义坐标 
	char s[30];//字符串类型数组,用来存放字符串 
	char t[30];
	printf("Enter t:");
	gets(t);//使用gets()来接收字符串 
	printf("Enter m:");
	scanf("%d",&m);
	strmcpy(s,t,m);//调用函数 
	puts(s);//把改变后的字符串输出 
}
void strmcpy (char *s,char *t,int m)
{
	t=t+m-1;//t+m是第几个数的位置,但是计数要从下一个数开始,所以要减去 1 
	while (*t!='\0')
	{
		*s=*t;//当字符不为'\0',我们就把 *t里面的内容拷贝给*s 
		s++;
		t++;//一位一位的拷贝 
	}
	*s='\0';//为了终止,输出字符串,所以*s='\0' 
}

运行结果:
在这里插入图片描述

eg.3
在这里插入图片描述
代码:

#include<stdio.h>//csdn-xiaolang
void hanshu(float number,int * intpart ,float * fracpart );
int main()
{  
    int intpart;
    float number,fracpart;
    printf("请输入一个数:"); 
    scanf("%f",&number);
    hanshu(number, &intpart , &fracpart);//使用指针调用整数和小数两个部分 
    printf("整数部分是 %d\n小数部分是 %f\n",intpart,fracpart);
    return 0;
}
void hanshu( float number, int *intpart, float *fracpart )//调用函数,只做运算,不返回结果 
{
    *intpart = (int)number;//强制转换 
    *fracpart = number- *intpart;//计算  
}

运行结果:
在这里插入图片描述

希望本博文能让C语言初学者更好的理解指针,文中参杂了少量C++内容,读者若有疑惑,可以在评论区留言或者私信博主,由于时间紧促,文中难免有疏漏或者错误之处,希望大家及时反馈。

  • 16
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 27
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 27
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

敲代码的xiaolang

你的鼓励是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值