深入理解指针(一)

       大家好这里是LoveHutao,指针是c语言的精髓,较为复杂,因此一篇博客肯定讲不完,所以我会分多次来讲述何为指针。(温馨提示:由于本文不是一天写好的,可能会出现一个东西前后称呼不同甚至错误的情况,我自己又找不出来,也懒得找,只要能看懂就行,请见谅~,理解万岁!!!)

一.内存和地址

       什么是内存和地址?

1.1 内存

       内存是计算机中的数据存储的地方,它就像一个大仓库存放着数不清的数据,我们在编译的过程中,所有的元素都会被存放在一个叫做内存仓库里面的一个货架上,而为了找到这么多货架中我们存放了我们需要的元素的货架,因此内存仓库里的每个货架都有一个标号,叫做地址。而一个货架的宽度就是一个字节,由于货架大小有限,因此有些大的货物需要两个货架甚至八个货架,即不同种类的货物要用不同数量的货架来装,因此不同类型的元素所占用的字节数量不一样,例如int类型的元素就占用了四个字节。图例如下:

36be60ac5c6a48908fa6dfbd2cd1a07c.png

1.2 如何理解编址

       前面讲了计算机中会给内存相应的地址,相当于给货架上编号,储存起来,那计算机是如何给这么多内存编号的呢?其实计算机中内存的编址不是由软件完成的,而是通过硬件实现的,我们这里只关系一组线,名叫地址总线,其中每根线都可以根据电脉冲的有无来表示10两个数字,这也是为什么我们计算机中以二进制来存储信息的原因,假设我们有32根地址线,我们就能表示2^32个地址。而这些操作需要不同硬件的组合,如下:(只需了解即可)

6c35bc6bf6e043aeaa7a531158fafa44.png

二.指针变量和地址

       指针指向地址(或者说指针中存着地址),我们一般认为内存单元的编号==指针==地址。

2.1 取地址操作符(&)

       通过取地址操作符(&)我们可以取出元素的地址,如下:

int main()
{
 int a = 10;
 printf("%p",&a);
 return 0;
}

4164f4ebda9141a7b3e8fc2959dee37e.png

       当然,如果觉得a的地址还是不够清楚,这时候我们就可以通过打开调试中的内存来查看(这里由于不小心重新执行了一次导致地址不同了)

6e68e74bc4ae4ab99029a6661722c260.png

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

1.指针变量

       当我们取出元素的地址时,总是想使用它的,因此我们就希望把地址保存起来留着以后使用。那么地址保存在哪里呢?答案是保存在指针变量里。指针变量既然被称为指针变量,当然其本身也是一种变量,我们可以如下创建一个指针变量:(int*,char*,double*,......的形式创建)

特别注意!int,char,double......才是指针变量的类型,而*代表的是变量是一个指针!!!

int main()
{
 int a = 10;
 int *p = &a; //这里只是拿int*举例,实际上char*,double*都是可以的
 return 0;   //int 是指针变量的类型,而*p则表示变量是指针变量
}

       当然指针变量既然是变量也是有地址的,我们可以打印出来看看它的地址。

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

        这里就可以看到pa&pa打印出来的地址是不同的,那么指针和地址到底是什么关系呢?我们可以画一张简单的示意图,如下:(提醒:不同元素在计算机中的存储是从高地址到低地址存储的)

2.解引用操作符(*)

       通过指针,我们可以将一个元素的地址保存起来,同样我们也可以通过指针找到这个元素的值,为了完成这步操作,我们就需要用到解引用操作符(*),使用方法如下:

int main()
{
 int a = 10;
 int* p = &a; // p = &a
 *p = 5;  //这里*p解引用后等效于a //*p=a
 printf("%d",a); //这里a的值就被我们修改了
 return 0;
}

       我们知道,通过指针能够找出a的地址,即p=&a,当我们对p进行解引用时,我们就有*p = a。当我们对*p进行修改时,就相当于找到了&a,并将里面的a的值改变,这样看似多次一举,但是我们通过之后的学习,自然能够理解其妙用。

3.指针变量的大小

        我们知道int类型占四个字节,char类型占一个字节,那么指针类型的大小是多少呢?我们有int*,char*......,它们究竟占几个字节呢?我们之前也说了,指针中存放的是地址,不管是哪一种类型的地址,它们终究是地址,因此int*,char*,......它们的大小应该是一样的,那么地址的大小又是多少呢?这就取决于我们的环境了,为什么这么说呢?       

       前⾯的内容我们了解到,32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后1或者0,那我们把32根地址线产⽣的二进制序列当做⼀个地址,那么⼀个地址就是32个bit位,需要4个字节才能存储。

       同理64位机器,假设有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要8个字节的空间。
       
       当然,我们也可以打印出地址来判断它们的大小,如下:
int main()
{
	int a = 10;
	printf("%p", &a);
	return 0;
}

       因此,在32位环境下,所有类型的指针变量大小都为4个字节,而在64位环境下,所有类型的指针变量大小都为8个字节

三.指针变量类型的意义

        既然指针变量的大小于类型无关,那么给指针变量设置类型有何意义呢?c语言之所以这么规定,一定是有其意义的,因此让我们继续往下看。

3.1 指针变量的解引用

       我们前面讲解引用操作符时,只是对变量的类型指针变量的类型相同的指针变量解引用,那么如果变量类型指针变量类型不同呢?如下所示:

int main()
{
 int a = 0x11223344;   //此处的0x表示的是a为16进制数
 char *pa = &a;
 printf("%x",*pa);     //%x表示的是以16进制的形式打印
 return 0;
}        

       我们不妨猜一下最后输出的结果是多少。是11223344吗?我们来看看下面的结果:

       诶?为什么只打印出来了最后两位数字44呢?因为指针变量的类型决定了指针变量可以一次访问多少个字节,而不同类型的指针变量一次能访问的字节也很好记,你这个类型的变量占多少个字节,这个类型的指针变量一次就能访问多少个字节,不用多想,这样子无疑是有利于指针对不同类型的值进行访问的,如果对此还有疑问可以继续往下看。

3.2 指针的加减运算

       我们都知道变量是可以进行加减运算的,(例如:int a=10,那么执行a = a+1后,a就变成11了。)同理,指针变量也是可以进行加减运算的,而指针变量加减后改变的是指针指向的地址,但是指针变量的加减和普通变量的加减也不尽相同,我们之前说过,指针允许一次访问的字节和变量的类型有关,因此指针+1/-1一次改变的地址的大小是和指针变量的类型有关的。证明如下:

int main()
{
 int a[2] = {0x11223344,0x22334455}; //数组中的元素是从地址值向高地址存储的!
 int *pai = a;    //由于a是数组,因此这里的a表示的是数组首元素地址
 char*pac = a;    //即0x11223344的地址
 pai += 1;
 pac += 1;
 printf("%p\n", a);  //这里%p以地址的方式打印出结果
 printf("%p\n",pai);
 printf("%p\n",pac);
 return 0;
}

       我们可以看到两个指针同样加一后 ,得到的地址却并不相同,因此我们可以得出一个结论,指针变量进行一次加减运算后增加的地址等于 加减的数 x 类型的大小。(pa+n=pa+n*类型大小)

       当然,我们也可以将pai和pac解引用后的值打印出来比较,如下所示:

int main()
{
	int a[2] = { 0x11223344,0x22334455 }; //数组中的元素是从地址值向高地址存储的!
	int* pai = a;    //由于a是数组,因此这里的a表示的是数组首元素地址
	char* pac = a;    //即0x11223344的地址
	pai += 1;
	pac += 1;
	printf("%x\n", *pai);
	printf("%x\n", *pac);
	return 0;
}

         (  提醒:数组中的元素是连续存储的,因此最后输出的结果并不会导致越界访问!)

3.3 void*指针

       前面我们知道指针有int*,char*......不同的指针,但是有一种指针比较特殊,就是void*指针,那么void*指针究竟特殊在哪里呢?

1.void*指针可以接收任意类型的变量

        我们前面强行将int类型变量的地址赋给char*指针时,会报出这样的警告:

        如果我们将上面的char*改成void*指针,那么给于任意指针都它都不会报出这个警告,如下:

int main()
{
	int a[2] = { 0x11223344,0x22334455 }; 
	void *pa = a;   //数组名就是数组首元素地址
    char b = 'e';
    pa = &b;
    float c = 3.14;
    pa = &c;       //这里只是拿三个类型举例
	return 0;      //void*类型的指针可以接受任意类型的地址
}

        (void*类型的指针可以接受所有类型的指针,不仅仅限于上述三种类型的指针。)

2.void*指针不能进行解引用操作

       void*指针不能进行解引用操作,这其实很好理解,我们知道指针变量的类型决定了解引用后指针进行一次加减运算地址的改变了,而void*指针类型值为空,能接受所有类型指针,但是我们无法判断void*指针的类型,因此也无法判断void*解引用的操作了。

3.如何将void*指针里的内容取出

        虽然我们没法直接从void*指针中取出它指向的值,但是我们可以通过强制类型转换“赋予”void*类型,什么意思呢?我们前面说过void*之所以没法解引用是因为它没有类型,当我们强制类型转换后,void*就相当于变成了我们想要的类型,此时我们再解引用就不再是对void*解引用了,如下所示:

int main()
{
	int a[2] = { 0x11223344,0x22334455 }; 
	void *pa = a;
    *(int*)pa = 0x66666666;
    printf("%x",a[0]);
    return 0;
}

四.const修饰指针

4.1 const修饰一般变量

       我们知道const这个函数可以修饰一个变量使其变成无法修改变量,如下:

int main()
{
 int const a = 0;
 const char b = 'c';  //对于一般变量来说const放在类型前和类型后没有区别
 a = 10;
 b = 'a';
 return 0;
}

       

4.2 const修饰指针

       那么const是如何修饰指针的呢?和一般的变量是一样的吗?答案是不尽相同!

       当我们使用const修饰指针的时候,const的位置会影响到其修饰的东西,如下三种情况:

int main()
{
 int a = 0;
 const int *pa = &a;
 int const *pb = &a; 
 int *const pc = &a;
 pa +=1, *pa = 1;
 pb +=1, *pb = 1;
 pc +=1, *pc = 1; 
 return 0;
}

       

       那么我们该如何去理解const究竟修饰的是哪一个值呢?

       我们可以理解为由于const修饰的是右边的变量,因此const在 * 之前时修饰的是*pa和*pb,而const在 * 之后时修饰的是pc。所以当我们使用时被const修饰的依次是*pa,*pb,pc。、

       不知不觉间已经讲了有一会了,那么谢谢大家的观看!我是LoveHutao我们下次再见!

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值