连续熬夜爆肝C指针,你想知道的这里都有

Posted aaaaaaaWoLan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了连续熬夜爆肝C指针,你想知道的这里都有相关的知识,希望对你有一定的参考价值。

以下内容知识量巨大,建议大家分次反复琢磨。

指针是什么?

在计算机科学中,指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向(points to)存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址的内存单元。
我们用图来理解:
地址是按照十六进制形式来表示的
每个内存单元都对应着一个地址,指针存放的就是地址,所以指针本质上也就是地址,指针指向该内存单元。

指针

指针是个变量,存放内存单元的地址(编号)。
对应到代码:

#include <stdio.h>
int main()

{

 int a = 10;//在内存中开辟一块空间

 int *p = &a;//这里我们对变量a,取出它的地址,可以使用&操作符。

   //将a的地址存放在p变量中,p就是一个之指针变量。

 return 0; 
}

总结:指针就是变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。
那这里的问题是:

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

如何编址?

经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。

对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的是产生一个电信号正电/负电(1或 者0)

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

00000000 00000000 00000000 00000000

00000000 00000000 00000000 00000001

11111111 11111111 11111111 11111111

这里就有2的32次方个地址。

每个地址标识一个字节,那我们就可以给 (2^32Byte == 2^32/1024KB ==

2^32 / 1024 / 1024MB==2^32 / 1024 / 1024 / 1024GB == 4GB) 4G的空间进行编址。

同样的方法,那64位机器,如果给64根地址线,那能编址多大空间,大家自行计算。

这里我们就明白:

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

那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。

总结:

指针是用来存放地址的,地址是唯一标示一块地址空间的。

指针的大小在32位平台是4个字节,在64位平台是8个字节。

指针和指针类型

指针也是分类型的,不同类型的指针指向不同类型的元素

int num = 10; 
p = &num;

//要将&num(num的地址)保存到p中,我们知道p就是一个指针变量,那它的类型是怎样的呢? 我们

给指针变量相应的类型。
char  *pc = NULL;

int   *pi = NULL;

short *ps = NULL;

long  *pl = NULL;

float *pf = NULL;

double *pd = NULL;

这里可以看到,指针的定义方式是: type + * 。 其实: char* 类型的指针是为了存放 char 类型变量的地址。 short* 类型的指针是为了存放 short 类型变量的地址。 int* 类型的指针是为了存放int 类型变量的地址。

那指针类型的意义是什么?

指针±整数

#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);

 return  0; 
}

我们先看结果:
在这里插入图片描述
我们可以看到pc的值与&n的值相同,所以可以进一步确信指针是用来存放地址的,再看pc+1的结果,地址大小增加了1,而pi+1的结果地址大小增加了4。所以,不同类型的指针决定了向前或向后移动的距离有多大。
就比如不同身高的人迈一步的距离是不一样的,char* 的“身高”就比较矮,所以它跨过的距离就很小,而int* 的“身高”就比较高,跨过的距离就比char* 大得多。

指针的解引用
*是解引用操作符,可以通过它来通过指针存放的地址找的该内存单元,也可对其进行更改。
看下面代码:

int main()
{
	int a = 10;
	printf("%d\\n", a);
	int* p = &a;
	*p = 20;
	printf("%d\\n", a);
	return 0;
}

先看结果:
在这里插入图片描述
p是一个指针变量名,int* 代表的是它的类型,说明他是一个整形指针变量,就像int a = 10 ; a是一个变量名,int是a这个变量的类型,而*p则是对它存放的地址进行解引用来找到该内存单元。

演示实例

#include <stdio.h>

int main()

{

 int n = 0x11223344;

 char *pc = (char *)&n;//赋值要把n的地址进行强制类型转换,否则一个整型的地址与char*的指针不匹配

 int *pi = &n;

 *pc = 0;   //重点在调试的过程中观察内存的变化。

 *pi = 0;   //重点在调试的过程中观察内存的变化。

 return 0; 
}

这里把n的值赋成了一个十六进制数字,这样是为了我们更好观察它内存的存放,因为我们同样可以把内存的存放形式也设置成十六进制形式
看下图,我们先看未修改n的值时n在内存中的存放,可以看到是“倒着放的”,这是因为我们的机器采取的是小端存储,详情可以看作者的另外一条博客
大小端介绍
在这里插入图片描述
在内存中的存储情况如下图:
在这里插入图片描述

先看* pc = 0,pc是一个char类型的指针,只能访问一个字节的内存,顺着地址由低到高访问,44在最低的一个字节位,所以44被改成了0,而其他位仍然不变
如下图
在这里插入图片描述
我们通过调试也可以发现结果和上图相同
在这里插入图片描述

再看*pi = 0,pi是int型的指针,所以可以访问4个字节的内存,故n就全都被修改为0了
如下图
在这里插入图片描述

我们再看调试时n在内存中的改变:
在这里插入图片描述

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

野指针

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的
野指针成因

  1. 指针未初始化
#include <stdio.h>

int main()

{ 

int *p;//局部变量指针未初始化,默认为随机值

*p = 20;

return 0; 
}
  1. 指针越界访问
#include <stdio.h>

int main()

{

    int arr[10] = {0};

    int *p = arr;

    int i = 0;

    for(i=0; i<=11; i++)

   {

        //当指针指向的范围超出数组arr的范围时,p就是野指针

        *(p++) = i;

   }

    return 0; 
}
  1. 指针指向的空间释放

如何规避野指针

  1. 指针初始化

  2. 小心指针越界

  3. 指针指向空间释放及时置NULL

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

例:

#include <stdio.h>

int main()

{

    int *p = NULL;

    //....

    int a = 10;

    p = &a;

    if(p != NULL)//检查指针有效性

   {

        *p = 20;

   }

    return 0; 
}

指针运算

指针±整数

#define N_VALUES 5

float values[N_VALUES];

float *vp;

//指针+-整数;指针的关系运算

for (vp = &values[0]; vp < &values[N_VALUES];)

{

     *vp++ = 0;   先使用再++
}

指针加减一个整数,就是向前或向后移动,同理,指针加减整数本质上就是地址加减整数,所以地址加减整数的效果与此相同而移动的距离取决于指针(或地址)本身的类型,如果是char * (或char类型元素的地址) 加减一,则只能向前或向后访问一个字节,而如果是int * (或int类型元素的地址)加减一,则只能向前或向后访问四个字节,这一点在前面的指针类型我们也提到了。这一运算常用于访问数组元素,如上面代码所示。

指针(或地址)+ 1,这里的1的大小取决于指针(或地址)的类型,如果是个整型指针(或地址),1就表示一个整型的大小,如果是个数组指针(或地址),1就表示一个数组的大小。

请大家记住,指针即地址

指针-指针

指针减指针得到的是两个指针之间的元素个数(语法规定)

即arr[0]之后的元素(包括arr[0])一直到arr[end](不包括arr[end])

如下面的代码所示,不仅使用了指针加整数的运算,还进行了指针减指针的运算

int my_strlen(char *s) //实现的是计算字符串的长度
{

       char *p = s;//s表示数组首元素的地址

       while(*p != '\\0' )//当p指向/0时,找到了数组的结尾,跳出循环

              p++;

       return p-s; 
}

我们再来看一个例子
在这里插入图片描述
解释:
在这里插入图片描述

指针加指针意义不大

指针的关系运算

指针比较的前提:两个指针指向同一块空间(数组中)

for(vp = &values[N_VALUES]; vp > &values[0];
//vp跳出循环时指向的数组的第一个元素
{

    *--vp = 0; )//将values数组中的元素全部赋值为0
}
代码简化, 这里将代码修改如下:

for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--) //vp跳出循环时指向的是位于数组之前的那个元素
{

    *vp = 0; 
}

因为在数组中,数组元素是按照地址从低到高连续存放的,所以地址可进行比较,也就是指针可以进行比较。

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

标准规定:

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

我们用图来解释:
在这里插入图片描述

二级指针

指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里? 这就是二级指针
如:
在这里插入图片描述
对于二级指针的运算有:

*ppa 通过对ppa中的地址进行解引用,这样找到的是 pa , *ppa 其实访问的就是 pa

int a = 20; 

int *pa = &a;

int **ppa = &pa;

*ppa = &b;//等价于 pa = &a;

//**ppa 先通过 *ppa 找到 pa ,然后对 pa 进行解引用操作: *pa ,那找到的是 a
**ppa = 30;

//等价于*pa = 30;

//等价于a = 30;

二级指针点到即止,之后还有讲解到它的地方。

指针和数组

在了解指针与数组的联系之前,我们先了解一下数组名的意义

数组名的意义

先看一个例子:

#include <stdio.h>

int main()

{

 int arr[10] = {1,2,3,4,5,6,7,8,9,0};

    printf("%p\\n", arr);
	printf("%p\\n", &arr);
    printf("%p\\n", &arr[0]);

    return 0; 
}

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

可见数组首元素的地址、数组的地址和数组名是一样的,我们先来区分数组首元素地址和数组的地址:
可以利用指针的加减运算来进行证明。
在这里插入图片描述
我们看到,当数组地址加1时,跳过大小的是40个字节即10个数组元素的大小(地址是十六进制的,两个地址相减得到40),而当数组首元素地址加1时,跳过的是4个字节即1个数组元素的大小,所以我们发现了他们的区别,两者虽然数值上一样,但表示的意义并不一样。

再来比较arr和&arr:
在这里插入图片描述

我们发现arr+1与arr相减也是4个字节即一个数组元素的大小,并且与&arr[0],&arr[0] + 1的结果相同,此时我们可以得出结论:数组名代表数组首元素的地址。

所以下面代码也就成立了:

int arr[10] = {1,2,3,4,5,6,7,8,9,0};

int *p = arr;//p存放的是数组首元素的地址

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

例如:

#include <stdio.h>

int main()

{

    int arr[] = {1,2,3,4,5,6,7,8,9,0};

    int *p = arr; //指针存放数组首元素的地址

    int sz = sizeof(arr)/sizeof(arr[0]);//计算数组元素个数

    for(int i=0; i<sz; i++)

   {

        printf("&arr[%d] = %p   <====> p+%d = %p\\n", i, &arr[i], i, p+i);

   }

    return 0; 
}

结果为:
在这里插入图片描述
&arr[i] 与 p+i 的结果时是相同的

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

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

如下:

int main()

{

 int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };

 int *p = arr; //指针存放数组首元素的地址

 int sz = sizeof(arr) / sizeof(arr[0]);

 int i = 0;

 for (i = 0; i<sz; i++)

 {

 printf("%d ", *(p + i));

 }

 return 0; 
}

结果为:
在这里插入图片描述
所以可通过指针来访问数组元素。

我们再进行拓展,既然* (p + i) = arr[i],而p表示首元素的地址,而arr也表示首元素的地址,那* (p+i)也可以写成* (arr + i),同样,是不是也把arr[i]可以写成p[i]呢?还有,* (arr + i) 是不是又等同于 * (i+arr)=arr[i],那写成 i [arr],是不是也可以呢?
答案是:都可以。
我们来进行验证:

int main()

{

	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };

	int* p = arr; //指针存放数组首元素的地址

	int sz = sizeof(arr) / sizeof(arr[0]);

	int i = 0;

	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\\n");
	for (i = 0; i < sz; i++)
	{
		printf("%d ", p[i]);
	}
	printf("\\n");
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(i + arr));
	}
	printf("\\n");
	for (i = 0; i < sz; i++)
	{
		printf("%d ", i[arr]);
	}
	printf("\\n");
	return 0;
}

在这里插入图片描述
虽然事实摆在这里,大家可能也还是很难想象,为啥是这样呢?
其实,[ ]是一个地址操作符,arr[i]其实就等于*(arr+i),这样大家是不是就更好理解了,所以[ ]的前面不一定要是数组名,只要是地址都能被使用,当然,一般都是用于数组中,比如p代表数组首元素地址,也可以被结合,p[i]就等于* (p+i),且[ ]的里外也没有要求,因为都要被转化为*(arr+i)的形式进行编译。
使用p[i]这种写法时,应该注意:p当前指向的是数组首元素地址,否则当p指向arr[2]时,再使用p[i],实际上指向的就是arr[2+i]了。

以上提到数组名代表首元素的地址,但有两种情况不一样:
1.当用sizeof(arr)计算大小时,arr单独放在sizeof内部,表示的是整个数组,所以计算的也就是整个数组的大小
2.当使用&arr时,取出的是整个数组的地址,虽然数值上和数组首元素地址相同,但意义是不相同的。

学会利用指针引用数组元素及运算后,我们来看下面这些表达式:

	int a[10];
	int* p = a;

	//1、p++;
	// *p;  //p++使p指向下一元素a[1],再执行*p,则得到a[1]的值

	//2. *p++;  //++的优先级高于*,因此它等价于*(p++);先取p的值,再解引用p,最后使p自增1  
	//假设p指向a[i],
	//所以 *p++就相当于a[i++]
	//因此下面两个代码的效果就相同
	/*1.
	for (int i = 0; i < 10; i++, p++)
	{
		printf("%d ,*p");
	}
	
	2.
	for (int i = 0; i < 10; i++)
	{
		printf("%d ,*p++");
	}*/

	//3. *(p++)与*(++p)
	//前者先取*p的值,再使p自增1
	//后者是先使p自增1,再取*p的值
	//比如此时p指向a[0],若输出*(p++),则结果是a[0]的值,若输出*(++p),则结果是a[1]的值
	// *(p++)就相当于p[i++]
	// *(++p)就相当于a[++i]


	//4. ++(*p);
	//括号的优先级最高,p先与*结合得到a[0]的值,再使a[0]自增一
	//注意,这里a[0]自增1是指a[0]的值自增一,比如a[0]的值如果是1,自增后a[0] = 2
	//所以++(*p)的效果就相当于 ++a[i]

字符指针

在指针的类型中我们知道有一种指针类型为字符指针 char* ;

一般使用:

int main()

{

    char ch = 'w';

    char *pc = &ch;

    *pc = 'w';

    return 0; 
}

还有一种使用方式如下:

int main()

{

    char* pstr = "hello bit.";//这里是把一个字符串放到pstr指针变量里了吗?

    printf("%s\\n", pstr);

    return 0; 
}

代码 char* pstr = “hello bit.”; 特别容易让同学以为是把字符串 hello bit 放到字符指针 pstr 里了,但是本质是把字符串 hello bit. 首字符的地址放到了pstr中。即把 ‘h’ 的地址放在了指针变量pstr中。

准确来说,这类指针应该叫做字符串指针。

我们来看一道题:

#include <stdio.h>

int main()

{

    char str1[] = "hello bit.";

    char str2[] = "hello bit.";

    char *str3 = "hello bit.";

    char *str4 = "hello bit.";

    if(str1 ==str2)

 printf("str1 and str2 are same\\n");

    else

 printf("str1 and str2 are not same\\n"); 

    if(str3 ==str4)

 printf("str3 and str4 are same\\n");

    else

 printf("str3 and str4 are not same\\n");
 
    return 0; 
    }

我们可能会认为输出结果为:
str1 and str2 are not same
str3 and str4 are not same

但事实真的如此吗?

来看结果:
在这里插入图片描述
这里str3和str4指向的是同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2相同,str3和str4不同。
因为常量字符串是不能被修改的,它本质上是个常量,所以为了节省空间,只开辟一块内存空间存放它即可,而数组中的字符串是可以进行修改的,因此数组str1和str2存在于内存的两块不同区域,故str1(首元素地址)和str2(首元素地址)不相等。

所以使用字符串指针变量时,是不能修改字符串内容的,只可对其引用。

指针数组

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

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

数组我们已经知道整形数组,字符数组。

如:

int arr1[5];

char arr2[6];
三百多个资源网站,熬夜爆肝整理,各行各业都有全网最全

熬夜爆肝!C++实现圣域之战!(修过码)

Python学习目录规划大全,爆肝熬夜整理,看完老奶奶都知道怎么学了

关于指针数组字符串的恩怨,这里有你想知道的一切

熬夜爆肝两万字,建议收藏scrapy学习路线及其爬虫框架详解

熬夜爆肝,docker常用命令集合!