指针其实也没有那么难

指针其实也没有那么难 — 初级指针

本章内容是指针的内容,有哪些地方写的不好还请多多指点。????

站在用户的角度思考问题,与客户深入沟通,找到天峨网站设计与天峨网站推广的解决方案,凭借多年的经验,让设计与互联网技术结合,创造个性化、用户体验好的作品,建站类型包括:成都做网站、网站设计、外贸营销网站建设、企业官网、英文网站、手机端网站、网站推广、域名注册雅安服务器托管、企业邮箱。业务覆盖天峨地区。

首先说一下指针的初级知识点什么是指针。


指针是什么?

按传统的方式来讲:

在计算机科学中,指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向
(points to)存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以
说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址
内存单元

内存单元是什么呢?就好比现实生活中我们的房间,不就是哪个单元哪个房间号嘛。

我们用画图的形式来展示。

指针

指针是个变量,存放内存单元的地址(编号)。

用代码方式表示:

#include 
int main()
{
    int i = 9;//向内存申请一块空间
    int* p = &i;//&i 取出i的地址 放到p变量中  而p就是一个指针变量 它指向的是i变量所在的地址
    return 0;
}

简单的来说: 指针就是个变量,这个变量是用来存放地址的。(存放到指针变量中的值都将被当作地址处理)。

比如说: int* p = 12;

数据在内存中都是地址的形式存放的,而在内存中地址是以4位16进制和8位16进制表示的,而12的16进制不就是字母C吗。

上面一行代码解释为:定义一个整型指针变量,把12这个值当作地址赋值给变量p。%p以地址的形式打印所以是 --> 0000000C。

  • 一个小的单元到底是多大?(1个字节)

对于32位机器来说,我们假设有32根地址线,每根地址线在寻找地址的时候产生一个电信号正电或者负电(1/0)

那么32根地址线产生的地址是:

00000000 00000000 00000000 00000000

00000000 00000000 00000000 00000001

00000000 00000000 00000000 00000010

......

2的32次方个地址。

每个地址标识一个字节,那么就可以给(2^32Byte == 2^32/1024KB == 2^32/1024/1024MB == 2^32/1024/1024/1024GB == 4GB)

4GB的空闲编址。同样的方法放到64位机器上,编址有TB,是这么多吗

这里我们就明白了:

  • 在32位机器上,地址是32个0或者1组成的二进制序列,那地址就应该用4个字节的空间来存储,所以一个指针变量大小就是4个字节
  • 换做64位机器上,那地址就应该用8个字节来存储,指针变量大小就是8个字节。

由此我们得出结论:

  • 指针是用来存放地址的,地址是唯一标示一块地址空间的。
  • 指针的大小在32位平台上是4个字节,在64位平台上是8个字节。

指针和指针类型

在此之前我们学过基本类型有:整型、字符型、浮点型等。既然指针也是变量,那指针是不是也会有不同的类型呢?答案是肯定的!

指针也是变量,所以也会有该对应的指针类型。下面举几个例子:

#include 
int main()
{
    int a = 10;//整型
    char b = 'w';//字符型
    float c = 3.14;//单精度浮点型
    double d = 123.789;//双精度浮点型
    
    int* pa = &a;//整形指针
    char* pb = &b;//字符指针
    float* pc = &c;//浮点型指针
    double* pd = &d;//浮点型指针
    return 0;
}

以上代码就列举了部分指针类型。

下面我们来看一段代码

sizeof()是计算数据大小的操作符,单位是字节,可以看到不管是什么类型的指针都是4个字节。诶~既然大小都是4个字节,那我们定义不同类型的指针又有什么意义呢?那我们何不造一个通用类型的指针呢?当然不行呢。你看我们每个人每天都要吃饭,虽然说有不同的方式可以让自己吃饱,但是为什么要这么多不同的食品让我们填饱肚子呢?直接造一种可以让人吃饱的食品不就好了。这个时候大家又知道了不同的食品有不同的作用,有些要补维生素A呀或者是维生素B呀等等。同样的指针类型也会有它不同的意义呀!

下面我们继续看段代码

我们看到内存上,a的地址,后面是变量a的数据存储方式 -->小端存储方式。当我们把a的地址存放到变量pa中,我们通过地址去改变a的值,请看下面

右下角我们看到pa变量的值变成了a的地址,而且a的值也被改成了20。好这个看不出什么,下面我们看另外一段代码

跟上面一样,我只是改变了指针的类型,改变指针类型后我们看到*pa=20执行之后,为什么右上角的值变成了 14 46 13 00呢?

通过观察,指针类型不同,操作的字节数就不同,可以看到int* 指针改变的是4个字节,而char* 指针改变的是1个字节。

由此我们得出结论:

指针类型决定了,对指针解引用的时候有多大权限(能操作几个字符)。比如说:char* 的指针解引用就只能访问一个字节,而int*的指针解引用能访问4个字节。

指针类型意义1:

指针类型决定了指针解引用时,能访问的空间大小。

我们继续看下面一段代码的运行结果:

可以看到int类型指针p的地址与p+1的地址之间差了4 而char类型指针pc和pc+1的地址之间差了1。虽然说是以数组来举例子,不过就算不用数组举例也是一样的。

指针类型意义2:

指针类型决定了指针+1或者-1有多大距离(指针的步长)。也就是字节

这有啥用呢?

可以知道我们创建了一个整型数组,这个数组有10个元素全部初始化成0了。数组名代表数组首元素的地址,把数组首元素的地址传给变量p通过循环给数组赋值。

结果很明显,通过指针+整数访问数组的元素,然后再给数组元素赋值。那么我们把指针类型改一下呢会出现什么效果?

它改变的是什么?搜嘎斯内,它只改变了整型数据中一个字节的内容。这是一个字节一个字节的访问,没改变指针类型之前呢,没改变之前是4个字节4个字节的访问所以啊。

  1. 希望一个字节一个字节访问 可以用char类型的指针
  2. 一个整型一个整型访问 可以用int类型的指针

以此类推,想怎样访问就用什么样类型的指针。

总结一下指针类型的作用

  1. 指针类型决定了指针解引用(访问字节)的权限有多大。
  2. 指针类型决定了指针向前走或者向后走有多大(距离,步长)。

野指针

野指针,大家看到会想起啥?野狗?野猫?它们都是没得家的没得主人,到处流浪。那么野指针是啥?

先按传统的说法:

野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

那为什么会产生野指针这玩意儿呢?

接下来我们看看野指针形成的原因。

野指针成因

1.指针未初始化

 #include 
 int main()
 {
     int* p ;//定义一个局部的指针变量,局部变量不初始化的话,默认是随机值
     *p = 10;//*p是对指针的解引用操作,这里非法访问内存
 	//使用了未初始化的局部变量p
 	//使用未初始化的内存p
     return 0;
 }

2.指针越界访问

 int main()
 {
 	int arr[10] = { 0 };
 	int* p = arr;
 	int i = 0;
 	for (i = 0; i <= 10; i++)
 	{
 		//当指针指向的范围超出数组arr的范围时,p就是野指针
 		*p = i;
 		p++;
 	}
 	return 0;
 }

3.指针指向的空间释放

int* work()
{
	int a = 20;//函数调用后变量a销毁了
	return &a;//所以此时a的地址虽然说还是那地址,但是指向的那块空间已经还给了操作系统
}
int main()
{
	int* p = work();
    //对p指针的解引用 去访问一个已经还给操作系统的空间 那片空间说不定操作系统已经存放其他内容 而且这也是属于非法访问内存
	*p = 30;
	return 0;
}

说了这么多的野指针成因,那我们应该怎样有效的避免野指针的产生呢?

  1. 初始化指针
  2. 小心指针越界
  3. 指针指向空间释放即使置NULL
  4. 指针使用之前检查有效性 可用assert断言
#include 
int main()
{
    //当不知道p应该初始化为什么地址的时候,可以初始化NULL
    int* p = NULL;//初始化指针
    return 0;
}

指针运算

指针运算有3种形式:

  • 指针±整数
  • 指针-指针
  • 指针的关系运算

指针±整数

int main()
{
	int arr[10] = { 20,21,22,23,24,25,26,27,28,29 };
	int* p = arr;
	int* end = arr + 9;
	while (p <= end)
	{
		printf("%d\n", *p);
		p++;//指针+整数    p+=1;
	}
	return 0;
}

可以知晓,指针+整数 在整型数组中访问下一个元素,既然可以+那肯定也能-

指针-整数

指针-指针

指针-指针,联系之前学过的内容,指针不就是地址嘛,那指针-指针 不就是 地址 - 地址。指针+指针没啥意义,就和日期一样,日期+日期有啥用嘛?日期-日期可以,日期+天数也可以。但是日期加日期就没啥意义了。

#include 
int main()
{
    int arr[10]={1,2,3,4,5,6,7,8,9,10};
    int* p = arr;
    int* end = arr + 9;
    printf("%d\n", end - p);
    return 0;
}

代码运行如下:

运行结果是9,我们发现从arr[0]到arr[9]之间刚好就是9个元素。

试试arr - (arr +9)这不是-9嘛看看代码运行效果:

结果也是-9,虽然数字是相同了,但是符号却不相同,而数组中是随着数组下标的增长地址是由低到高的。内存中地址也是由低到高。

因此我们如要需要得到元素的个数,应该用高地址- 低地址。

注意事项:

指针-指针的前提是:两个指针指向的是通一块空间

给大家来个实际点的:

#include 
//函数递归方法
int me_strlen(char*str)
{
    if(*str != '\0')
        return 1 + me_strlen(str + 1);
    else
        return 0;
}
int me_strlen(char* str)
{
    char* p = str;//p指针指向 str 变量   str 指向的是 字符串首字符的地址
    while(p != '\0')//字符串长度是找'\0'
    {
        p++;
    }
    return p - str;//指针-指针 就是中间元素个数 
}
int main()
{
    char arr[] = "hello world";
    int ret = me_strlen(arr);//传字符串首字符的地址
    return 0;
}

指针关系的运算

指针关系运算,我们知道关系运算是啥,关系运算不就是 > < >= <= != ==

用代码举个例子吧:

#include 
int main()
{
    int arr[5];
    int *p;
    for( p = &arr[5]; p > arr[0])
    {
        *--p = 0;
    }
    return 0;
}

这个代码不太标准,看着有点难受。修改一下

#include 
int main()
{
    int arr[5];
    int *p;
    for( p = &arr[4]; p > arr[0]; p--)
    {
        *p = 0;
    }
    return 0;
}

这个代码是什么意思呢?咱们画图理解一下

该代码的意思是:取出arr数组后面一个元素的地址,判断该地址是否大于数组首元素的地址,如果大于那就把p变量所指向的内容改为0,然后在自减,也就是往arr[0]那边走,再以同样的方式赋值,直到p的地址要小于或者是等于arr[0]循环就结束。

但是请注意:

允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。

如果还不明白请自己画图理解一下。

指针和数组

指针和数组的关系:指针是地址嘛,而数组名是数组首元素地址。 我们举个例子

有图有真相(doge),可以看到arr和&arr[0]的地址是一样的,所以说,数组名就是数组首元素地址。

#include 
int main()
{
    int arr[10]={1,2,3,4,5,6,7,8,9,10};
    int* p = arr;//p存放的就是数组首元素地址
    return 0;
}

但是数组名就一定是数组首元素地址吗?NONONO。也有例外啦!请记住下面2点

  1. sizeof(数组名) --> 只有数组名没有其他操作符 数组名表示整个数组,计算的是整个数组的大小,单位是字节。
  2. &数组名 -- > 取出的是整个数组的地址 数组名表示整个数组

其他情况下,数组名就是首元素地址。

首元素的地址和数组的地址有啥区别呢?

靠,这不一样嘛(手动狗头)?雀食哈,这打印出来都一样,但是意义却截然不同呢,不信再举个例子:

看到区别没,arr是数组名,代表首元素地址,+1 因为数组类型是int 而int类型指针+1 不就是跳过4个字节 刚好差4。

而&arr,arr代表整个数组,+1 跳过了一个数组,两个地址之间差 40个字节。

既然可以把数组名当成地址存放到一个指针中,我们可以使用指针来一个个的访问。

#include 
int main()
{
	int arr[10] = { 0 };
	int* p = arr;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%p <==> %p\n", &arr[i], p + i);
		*(p + i) = i;
	}
	return 0;
}

效果:

所以 p+i 其实计算的是数组 arr 下标为i的地址 。

那我们就可以直接通过指针来访问数组 。

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;//数组名
    int sz = sizeof(arr)/sizeof(arr[0]);
    int i = 0;
    for(i=0;i *(p+2)
	//arr[2] --> *(arr+2) --> *(2+arr) --> 2[arr]
	//arr[2]<==> *(arr+2) <==> *(p+2) <==> *(2+p) <==> *(2+arr) == 2[arr]
	//2[arr] <==> *(2+arr)
	return 0;
} 

二级指针

二级指针,有没有觉得好高级。之前了解到指针本质上还是变量嘛,只是用来存放地址的变量而已。既然是变量那它是不是也会有地址?

那当然,那么存放指针变量的地址就叫二级指针咯。画图举个例子就知道了:

这样是不是好理解一些。

#include 
int main()
{
    int a= 1;
    int* p = &a;//p是指针变量,一级指针
    int** pa = &p;//pa也是指针变量,二级指针
    return 0;
}

说完了什么是二级指针,那二级指针如何使用呢?其实和一级指针的使用是类似的。

  • *pa 通过对pa的地址解引用,找到p, *pa其实访问的就是p。

    int b = 1;
    *pa = &a;//等价于 p = &a;
    
  • **pa先通过 *pa找到p,然后对p进行解引用操作: *p,找到a

    **pa = 30;
    //等价于*pa = 30;
    //等价于a = 30;
    

指针数组

我们学过整型数组、字符型数组。那么指针有没有数组呢?

答案肯定是有的。还记得数组的定义是什么吗?数组是一组相同类型元素的集合。那么一组相同类型的指针放在一起这算不算是数组呢?

这肯定是数组,因为它满足数组的定义。那么指针数组又是怎么样的呢?

首先指针数组的定义:一组相同类型的指针的集合

int arr[10];//这是整型数组,数组有10个元素,数组中每个元素都是整型。

char arry[5];//这是字符型数组,数组有5个元素,数组中每个元素都是字符型。

#include 
int main()
{
    int a = 1;
    int b = 2;
    int c = 3;
    int* pa = &a;
    int* pb = &b;
    int* pc = &c;
    return 0;
}

这段代码中,a、b、c均为整型变量,pa、pb、pc均为整形指针。3个整型变量用3个整型指针来接收岂不是浪费空间了?那我们干脆把这3个整型指针放在一起组成一个整型指针数组不就好了嘛,诶指针数组这不就出来了嘛。

首先定义一个数组 int arr[10]; 既然每个类型都是指针,那么把数组的类型改一下不就好了嘛。 int* arr[10]; 这时我们就说这个数组是指针数组嘛,这是一个整型指针数组,该数组有10个元素且每一个元素都是整型指针。那么我们画图展示一下:

用代码的形式:

int main()
{
	int arr[10];//整形数组 - 存放整形的数组就是整形数组
	char ch[5];//字符数组 - 存放的是字符
	//指针数组 - 存放指针的数组
	int* parr[5];//整形指针的数组
	char* pch[5];//字符型指针数组
	return 0;
}

想必大家看了这几幅图和代码,对指针数组应该有大概的了解了吧。


感谢大家的收看,以上都是鄙人学习的个人理解如果有哪些地方说错了或者是没有讲明白,还请大家多多指点指点。谢谢大家!!!

这部分是鄙人对指针的初步了解后面进阶的会在【指针其实也没有那么难—进阶指针】这篇中讲解。


名称栏目:指针其实也没有那么难
URL网址:http://pwwzsj.com/article/dsoipcc.html