指针不理解,看这篇就够了

关于指针的概念用一句解释就是:指针就是地址,指向某块空间的位置

1、内存和地址

1.1 内存
我们知道计算上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中。

所以可以理解内存就是计算机中存储数据的一块空间,我们在编写程序时产生的数据也都存放在内存当中。

1.2 地址

为了高效地管理这些内存空间,把内存划分成一个个内存单元,每个内存单元大小都为一个字节(=8bit),都每个单元进行编号,这个编号就是这块空间的地址。把每个单元看作是一个个房间,那么指针就是房间的门牌号。而内存单元的大小可以看作房间的容纳量,8个bit位相当于每个房间可以住8个人。

比喻成酒店的房间号,

通过房间编号可以快速找到对应的房间位置,通过指针可以快速找到对应的内存空间

 房间的地址我们称为门牌号,那么内存空间的地址我们也取了一个名字那就是——指针。

总结的说:内存单元编号==地址==指针

1.21 理解内存编址

CPU进入某个内存空间,要快速准确地找到该空间,便要通过内存单元编号才能找到。这就需要对内存空间进行编址,计算机的编址不是把每一块空间的地址记录下来,而是通过计算机硬件来完成的,简单地说就是用“线”将CPU和内存连接在一起,CPU和内存在运行时会进行大量的数据交互,而数据就是通过“线”来交互,这里我们只关注CPU和内存中有一组专门用来传输地址的总线,称为地址总线。

简单理解地址总线,32位机器有32根地址总线,每根线两种状态即 0 和 1 【电脉冲有无】,那么一根线两种含义,两根线四种含义,以此类推。32根地址总线就能表示2^32种含义,每种含义都代表一个地址。地址信息就这么传达给内存,在内存上就可以通过地址找到对应空间的数据信息,再通过数据总线传回CPU。

2、 指针变量和地址

2.1 取地址操作(&)符

回到C语言中,我们已经知道内存和地址的关系,那么当我们在创建变量时,其实就是在向内存申请空间,比如下列创建的变量a,

通过编译器调试可以看到,整型变量a在内存中申请了4个字节,上图四个字节的地址分别是

1 0x00B3FA1C  0a  
2 0x00B3FA1D  00  
3 0x00B3FA1E  00  
4 0x00B3FA1F  00 

最后面的两位数用于表示每个字节内存放的内容,因为10只需要4个比特位就可以表示,小于一个内存单元的大小(8个比特位),所以只在第一个字节中有数据0a(也就是十六进制表示的10)

那么如何直接取出变量a存放的地址呢,此时就需要用到取地址操作符&

#include<stdio.h>
int main()
{
	int a = 10;
	printf("%p\n", &a);

	return 0;
}

按照这个代码就可以打印出第一个地址0x00B3FA1C,&a取出的是a所占的四个字节中地址最小的字节的地址

虽然整型变量a整体占用了四个字节,但是获取了第一个字节地址就可以找到剩下的字节地址

2.2 指针变量和解引用操作符(*)
2.2.1 指针变量

前面说过指针就是地址,&变量名就可以取出变量所在的内存地址,那么取出变量的地址后该放在哪里呢,答案是放在指针变量里,指针变量也是一种变量,只不过它存放的内容是指针(即地址),类比整数放在整型变量中,指针就放在指针变量里,如何创建指针类型的变量呢?

指针类型 * 变量名 :  int * p

上面的p就是一个指针变量,对它的指针类型(int *)进行拆解,

首先 * 表示p是一个指针变量,int 表示这个指针所指向的空间中存放的数据是int型的。

或者简单说,

(int)类型变量的地址就用(int*)类型的指针接收,

(char)类型变量地址就用(char*)类型的指针接收

##include<stdio.h>
int main()
{
    int a=10;
    int *pa=&a;

    char b='w';
    char*pb=&b;

    return 0;
}
 2.2.2 解引用操作符(*)

 我们把一个变量的地址保存起来肯定后续是要用的,那么如何获取到指针变量里的内容呢?

此时就需要用到解引用操作符-(*),来看下列代码

这边我们看到指针变量pa存的是变量a的地址,但是打印a和打印*pa得到的结果是一样的

原因就是解引用操作符(*),*pa就是通过pa找到pa指向的空间的内容,所以可以获得a中存放的值

如果说&a是获得a的地址,解引用*pa就是通过地址找到地址指向的对象,即*pa=a

解引用操作符就是用来获取所存地址指向空间中的内容

如果指针p存的是a的地址,则*p则表示a的值,*pa=a

再来看一个代码

可以发现a的值也可以通过*pa来改变,也就是当我们获得一个变量的地址,不仅仅可以通过地址获得其指向空间的内容,同时可以通过指针改变其存放的内容

2.3 指针变量的大小

前面的内容有提到,32位的机器假设有32根地址总线那我们把32根地址线产⽣的2进制序列当做⼀个地址,也就是32个bit位=4个字节的空间。

即如果指针要表示地址,则指针大小必须要有4个字节的空间。

同理64位机器,假设有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要 8个字节的空间大小,就是8个字节
#include <stdio.h>
//指针变量的⼤⼩取决于地址的⼤⼩
//32位平台下地址是32个bit位(即4个字节)
//64位平台下地址是64个bit位(即8个字节)
int main()
{
 printf("%zd\n", sizeof(char *));
 printf("%zd\n", sizeof(short *));
 printf("%zd\n", sizeof(int *));
 printf("%zd\n", sizeof(double *));
 return 0;
}

结论:

·在32位平台下,指针大小都为4个字节

·在64位平台下,指针大小都为8个字节

·注意指针变量的大小与指针类型无关,只要是指针类型的变量,在相同的平台下,大小都是相同的。

3. 指针变量类型的意义 

如果在相同平台下指针的大小与类型无关,那为什么还要有不同类型的指针呢?

其实是因为指针类型是有特殊意义的。

3.1 指针的解引用

来看下列两个代码

   

 调试可以看到n在内存中的存储都是0x11223344(倒着显示是小端存储的原因),不知道小端存储的可以看我的这篇文章整数和浮点数在内存中的存储(C语言)-CSDN博客icon-default.png?t=O83Ahttps://blog.csdn.net/zn0wul/article/details/136964081?spm=1001.2014.3001.5501

对代码1进行调试运行发现 n 的4个字节都变为了0

对代码2进行调试运行却发现 n 的只有1个字节变为了0

两个代码唯一的区别就是接收 n 的地址的指针p的类型不同,代码1是int*类型的指针,代码2是char*类型的指针

结论:

指针的类型决定了,对指针解引用时一次可以操作几个字节

比如上方的 int* 的指针可以访问4个字节,而 char* 只能访问1个字节 

3.2 指针+-整数

先通过一段代码来感受一下指针+-整数时的变化

可以看出, char* 类型的指针pc +1 跳过1个字节,从0058F810到0058F811,

 int* 类型的指针pi + 1 跳过4个字节,从0058F810到0058F814,

这就是指针变量类型差异带来的变化。

结论:

指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)。
3.3  void* 指针

在指针类型中有一特殊的类型是 void* 类型的,可以理解为无具体类型的指针(或叫泛型指针),这种类型的指针可以用来接受任意类型的地址。但是具有局限性, void* 类型指针不能直接进行指针的+-整数和解引用运算

void*类型的指针一般是使用在函数参数部分,用来接收不同类型数据的地址,使得一个函数处理多种类型的数据。比如一个函数既可以排序整型数据又可以排序字符型数据,那么它可以接受的参数既有整型又有字符型,此时就可以用void*来接收参数

4. const修饰指针

4.1 const修饰变量

变量可以通过直接赋值修改变量的值,也可以通过获得变量的地址,由指针来修改变量的值

但是如果我们想一个变量不能被修改,也就是限制变量的值,怎么做呢?这就是const的作用

这个代码的报错提示n是不可修改的值,其实n本质上是一个变量,但是因为被const修饰后,增加了语法上的限制,此时n只要被修改就不符合语法规则,则会冒红即报错

但是如果我们不用直接赋值的方式修改n的值,而是通过地址,也就是指针来对n的值进行修改,却发现可以了

 可以看到n的值确实被修改了,但是显然这样做是在打破语法规则,我们用const修饰变量的目的就是希望n的值不能被修改,如果拿到n的地址就可以修改 n 的值,那const还有什么意义呢,所以应该让 p 拿到变量 n 的地址也不能修改 n 的值

4.2 const修饰指针变量

对下列代码调试运行

#include <stdio.h>
//代码1
void test1()
{
	int n = 10;
	int m = 20;
	int* p = &n;
	*p = 20;//ok?
	p = &m; //ok?
}
void test2()
{
	//代码2
	int n = 10;
	int m = 20;
	const int* p = &n;
	*p = 20;//ok?
	p = &m; //ok?
}
void test3()
{
	int n = 10;
	int m = 20;
	int* const p = &n;
	*p = 20; //ok?
	p = &m; //ok?
}
void test4()
{
	int n = 10;
	int m = 20;
	int const* const p = &n;
	*p = 20; //ok?
	p = &m; //ok?
}
int main()
{
	//测试无const修饰的情况
	test1();
	//测试const放在*的左边情况
	test2();
	//测试const放在*的右边情况
	test3();
	//测试*的左右两边都有const
	test4();
	return 0;
}

可以发现对于指针const在不同的位置具有不同的修饰作用:

const int * p:当const在 * 的左边时, *p 的内容不能被修改

int * const p:当const在 * 的右边时,p的内容不能被修改,即p所存的地址不能更改

const  int * const p:当 * 左右两边均有const时,则同时具有上述两种作用

5. 指针运算

指针有三种基本运算:

· 指针+-整数

· 指针+-指针

· 指针的关系运算

5.1 指针+-整数

在内存当中数组是连续存放的,所以只要知道第一个元素的地址,就可以顺藤摸瓜往下找到剩下元素的位置,就像10个连续的房间

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

 

所以就可以用指针遍历数组

#include <stdio.h>
//指针+- 整数
int main()
{
 //数组
 int arr[10] = {1,2,3,4,5,6,7,8,9,10};
 int *p = &arr[0]; //存数组的第一个元素的地址
 int i = 0;
 int sz = sizeof(arr)/sizeof(arr[0]);//获得数组元素个数
 for(i=0; i<sz; i++)
 {
     printf("%d ", *(p+i));//p+i 这⾥就是指针+整数
 }
 return 0;
}

这里p一开始指向数组的首元素的地址(第一个房间的门牌号),也就是 p=&arr[0] ,前面说过指针会根据类型决定移动的字节大小,这里数组每个元素都是 int 类型,即每个元素都为4个字节,而 p 作为一个 int* 类型的指针,每次+ - 1都会跳过4个字节,所以每+1次会获得数组中的一个元素的地址,再对其解引用就可以获得该元素。

p+0就是arr[0]的地址,p+1就跳过一个元素(int型4个字节),也就是&arr[1]的地址

5.2 指针-指针

这里用指针-指针实现strlen函数(一个计算字符串长度的函数)

//指针-指针
#include <stdio.h>
int my_strlen(char *s)
{
  char *p = s;
  while(*p != '\0' )
  p++;
  return p-s;//不包括'\0'在内的最后一个指针减去首指针
}

int main()
{
  printf("%d\n", my_strlen("abc"));
  return 0;
}

这里用字符串的尾地址(不是'\0'的地址)减去首地址,可以获得两个地址之间相差的字节数

指针-指针得到两个指针相差的字节数

因为char类型变量的大小刚好是1个字节,所以字符串中每一个字节代表一个字符,所以计算字符串首尾相差的字节数可以统计出字符串的字符个数,即字节数==字符串长度

5.3 指针的关系运算
//指针的关系运算
#include <stdio.h>
int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9,10};
 int *p = &arr[0];
 int i = 0;
 int sz = sizeof(arr)/sizeof(arr[0]);
 while(p<arr+sz) //指针的⼤⼩⽐较
 {
 printf("%d ", *p);
 p++;
 }
 return 0;
}

arr作为数组名表示数组第一个元素的地址,即数组首元素地址

arr + sz(数组的长度)就相当于p+sz,表示arr[sz]元素的地址,也就是末尾元素的地址

因为数组在内存中是连续存放的,且是从低地址往高地址一次存放的

p作为arr[0]元素的地址即数组首地址自然是小于arr[sz]元素的地址即尾地址的

6. 野指针

概念:该指针指向的空间位置是不可知的、随机的、不正确的

6.1 野指针的成因

1. 指针未初始化

#include <stdio.h>
int main()
{ 
 int *p;//局部变量指针未初始化,默认为随机值
 *p = 20;
 return 0;
}

解决办法: 如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL.

NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址会报错。

2. 指针越界访问

⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。
#include <stdio.h>
int main()
{
 int arr[10] = {0};
 int *p = &arr[0];
 int i = 0;
 for(i=0; i<=11; i++)
 {
  //当指针指向的范围超出数组arr的范围时,p就是野指针
  *(p++) = i;
 }
 return 0;
}

解决办法:注意指针可以访问的范围以及当指针变量不再使⽤时,及时置NULL,防止后面遗忘,再次使用该指针访问内存,指针使⽤之前检查有效性,即是否是空指针

当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问,同时使⽤指针之前可以判断指针是否为NULL

3. 指针指向的空间释放

#include <stdio.h>
int* test()
{
  int n = 100;
  return &n;
}
int main()
{
  int*p = test();//函数在调用完后会释放其在运行时申请的空间
  printf("%d\n", *p);
  return 0;
}

 解决办法:使用之前检查指针的合法性

7. assert断言

assert.h的头文件里定义了宏assert(),用于在运行确保程序符合某指定条件,如果不符合就报错终止运行。这个宏常常被称为“断言”,可以用来在指针使用之前检查其合法性

assert(p!=NULL)

上⾯代码在程序运⾏到这⼀⾏语句时,验证变量 p 是否等于 NULL 。如果确实不等于 NULL ,程序继续运行,否则就会终止运行,并且给出报错信息提示。
assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零), assert() 不会产⽣
任何作⽤,程序继续运⾏。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误
stderr 中写⼊⼀条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号。
assert() 的使⽤对程序员是⾮常友好的,使⽤ assert() 有⼏个好处:它不仅能⾃动标识⽂件和
出问题的⾏号,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问
题,不需要再做断⾔,就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG
# define NDEBUG
# include <assert.h>
然后,重新编译程序,编译器就会禁⽤⽂件中所有的 assert() 语句。如果程序⼜出现问题,可以移
除这条 #define NDBUG 指令(或者把它注释掉),再次编译,这样就重新启⽤了 assert()
句。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值