C语言——指针(1)

1. 内存和地址

1.1 内存

在了解内存之前,我们先举生活中的一个例子。假如我们要在一栋公寓中找一位朋友,如果我们一个一个房间的找,这样会很浪费时间,但如果我们知道房号,那我们便能快速精准的找到我们的朋友。

如果把上面的例子对照到计算机中,又是怎么样呢?

我们知道计算机CPU在处理数据时,需要的数据要从内存中读取获得,处理后的数据也会放回内存中。那么,计算机是如何从内存中获取数据的呢?我们平常买电脑的时候,电脑内存是8GB/16GB/32GB等,这些内存空间是如何进行有效的管理呢?

其实也是把内存分为一个个内存单元,每个内存单元的大小取1个字节,每个内存单元都有独属于自己的内存编号。

计算机常见单位补充:

一个比特位可以存储一个二进制位0或者1。

1. bit---比特位                                   1 byte = 8 bit                     

                                                         

2. byte--字节                                      1 KB = 1024 byte

3. KB                                                  1 MB =1024 KB

4.MB                                                   1 GB = 1024 MB                                             

5.GB                                                   1 TB = 1024 GB

6.TB                                                    1 PB ==1024 TB 

7.PB

其中一个内存单元的大小为一个字节,可以存8个比特位,就好比一个八人间宿舍,一间宿舍可以住8个人。

每个内存单元都有自己的编号,有了这个内存编号,CPU就可以迅速找到一个内存空间。

在生活中,我们把门牌号叫做地址,但在C语言中,给其起了一个特殊的名字--指针

我们可以特殊理解为:

门牌号==地址==指针

图示:             0XFFFFFFF是内存编号。

1.2 如何理解编址

CPU要访问内存中的某个内存单元,就必须要知道这个内存单元在内存的哪一个位置,由于内存中的内存单元有很多个,为了方便寻找,我们要给内存单元编址。

计算机中的编址,并不是把每个内存单元的地址记录下来的,而是通过硬件完成的。

以生活中的钢琴为例,钢琴上没有“嘟擂咪发嗦啦”这样的信息,但未何演奏者就能准确的弹琴呢?

这是因为制造商已经在乐器硬件层面上设计好了,并且所有的演奏者都知道。本质上是一种约定出来的事实。

硬件编址也是如此。硬件编址是是以CPU所知道的位置对内存的空间进行编址的。

首先,我们必须了解计算机中是有很多硬件的,而硬件之间是要相互协助的,所谓协助,至少相互之间要进行相互之间的数据传递。

但是硬件与硬件之间是相互独立的,那它们是如何进行联系的呢?答案很简单,就是通过"线”来连接。

而CPU与内存之间也是有大量的数据进行交互的。CPU与内存之间也是通过“线”来连接的。

不过我们今天关注一组线,叫做地址总线。

图:

解释:32位机器有32根地址总线,每根线只有两种状态,1或者0(电脉冲有无),那么1根线就能表示1中含义,两根线就能表示4种含义,32根线就能表示2^32种含义,每一种含义都能代表一个地址 。

首先,我们通过控制总线对计算机下读数据或者写数据的命令,将命令传给CPU,CPU在通过数据总线寻找要读取的数据或者要写的数据在内存中的地址,将地址信息传给内存,在内存上就可以找到该地址对应的数据,将数据通过数据总线传入到CPU内寄存器中。

2. 指针变量和指针

2.1 取地址符号操作符(&)

理解了内存和地址的关系,我们再回到C语言,在C语言中创建变量本质上就是向内存申请空间,比如:

比如,上述的代码就是创建了整形变量a,其向内存申请了4个字节的空间,用于存放10,其中每一个字节都有属于自己的地址,上图中4个字节的地址分别是:

1.  0x006FFD70

2.  0x006FFD71

3.  0x006FFD72

4.  0x006FFD73

其中0x006FFD70就是变量a的地址。

那我们要如何获取变量a的地址呢?这里我们就要用到取地址操作符---&

#include <stdio.h>
int main()
{
 int a = 10;
 &a;//取出a的地址
 printf("%p\n", &a);
 return 0;
}

按照我画图的例子,会打印:006FFD70

&a 取出的是变量a所占的4个字节中地址较小的字节的地址。 

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

2.2.1 指针变量

我们通过取址操作符(&)拿的的地址是一串数值,有时候我们也要将其存储起来方便后期使用,因为它是一个地址,不能用创建普通类型来存储,所以在C语言中规定,将地址值存储到指针变量中。

#include <stdio.h>
int main()
{

 int a = 10;
 int* pa = &a;//取出a的地址并存储到指针变量pa中
 
 return 0;

}

指针变量也是一种变量,这种变量就是用来存放地址的,存放在指针变量中的值都会被理解为地址。

2.2.1 如何拆解指针变量

我们看到pa的类型是 int* ,那我们该如何理解指针的类型呢?

int* pa 中的 * 是在说明pa是一个指针变量,int 则在说明pa是一个指向整形的指针。

图解

2.2.3 解引用操作符——*

我们将地址保存起来,未来是需要使用的,但我们要如何使用呢?

在C语言中我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,这里我们必须学习一个操作符------ 解引用操作符--* 。

#include <stdio.h>
int main()
{
	int a = 10;
	printf("a未改变前的值为%d\n", a);
	int* pa = &a;
	*pa = 0;
	printf("a改变后的值:%d", a);
	return 0;
}

上述代码第7行就使用了解引用操作符,*pa的意思就是通过pa存放的地址,找到指向的空间,*pa本质上就是变量a了,*pa=0也就等于a=0 。 

如下图所示

这时候我们就很疑惑,如果想把a变为10,那直接写a=10不就好了吗? 为什么偏偏要用一个指针变量呢?

其实这里a的值的修改是交给pa来操作的,这样对a的修改就多了一种方法,写代码就更加灵活了,存在即合理(除了蚊子),后期我们慢慢就能理解了。

2.3 指针变量的大小

前面的内用我们了解到,32为机器就有32根地址总线,每根地址地址线出来的电信号转换成数字信号后是0或1,那我们把32根地址线产生的2进制序列当作一个地址,那么一个地址就是32个bit,需要4个字节才能存储。

所以如果在32位机器中,用指针变量来存储地址,那么这个指针变量的大小必须是4个字节大小才可以。

同理,在64位机器中,一个地址就是64根地址线产生的二进制序列组成的,存储起来就要8个字节,所以指针变量的大小就应该是8个字节。

int main()
{
	printf("%zd\n", sizeof(char*));
	printf("%zd\n", sizeof(short*));
	printf("%zd\n", sizeof(int*));
	printf("%zd\n", sizeof(double*));
}

结论:

1.32位平台下地址是32个bit位,指针变量的大小为4个字节。

2.64位平台下地址是64个bit位,指针变量的大小为8个字节。

3.指针变量的大小是与指针类型无关的,只与平台类型有关,在相同的平台下,指针类型的大小都是相同的。 

3.指针变量类型的意义

指针变量的大小与类型无关,只要在相同平台,指针变量的大小都是一样的,为什么还要各种各样的指针类型呢?

其实指针类型也是有特殊意义的。接下来让我们来学习一下。

3.1 指针的解引用

对比下面两段代码,主要在调试时观察内存的变化。

#include <stdio.h>
int main()
{
 int n = 0x11223344;
 int *pi = &n; 
 *pi = 0; 
 return 0;
}
#include <stdio.h>
int main()
{
 int n = 0x11223344;
 char *pc = (char *)&n;
 *pc = 0;
 return 0;
}

通过调试我们发现,代码1会将n的4个字节全部改为0,但代码2只会将n的1个字节改为0.

结论:通过以上例子可知,指针类型决定了,对指针进行解引用时的访问权限有多大,也就是一次可以操作多少个字节。 

比如:int* 类型的指针一次能访问4个字节,而char* 类型的指针一次只能访问1个字节。

3.2 指针+-整数

观察以下代码,调试观察地址的变化

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

代码运行结果如下: 

我们可以看出,char* 类型的指针变量+1只跳过了1个字节,int* 类型的指针变量+1跳过了4个字节。这就是指针变量类型带来的变化。

结论:指针的类型决定了指针向前或向后走一步的距离有多大。 

3.3 void*指针

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

#include <stdio.h>
int main()
{
 int a = 10;
 int* pa = &a;
 char* pc = &a;
 return 0;
}

在上面的代码中,将一个int类型变量的地址赋值给一个char*类型的指针变量。编译器就会爆出一个警告如下图:

这就是因为类型不兼容导致的,而使用void*类型就不会有这样的问题。

使用void*类型接收地址:

#include <stdio.h>
int main()
{
 int a = 10;
 void* pa = &a;
 void* pc = &a;
 
 *pa = 10;
 *pc = 0;
 return 0;
}

运行代码:

这里可以看出,void* 类型的指针可以接收不同类型的指针,但是无法进行指针运算。

那么void*类型的指针有什么作用呢?

一般void*类型的指针是使用在函数参数的部分,用来接收不同类型数据的指针,这样的设计可以实现泛型编程的效果。

4.const修饰指针变量

4.1 const修饰变量

变量是可以修改的,如果把这个变量的地址交给一个指针变量,通过指针变量就可以改变这个变量的值。

如:

#include <stdio.h>
int main()
{
 int m = 0;
 m = 20;//m是可以修改的
 const int n = 0;
 n = 20;//n是不能被修改的
 return 0;
}

不通过指针变量,用const修饰的n是无法改变的。

但是如:

#include <stdio.h>
int main()
{
 const int n = 0;
 printf("n = %d\n", n);
 int*p = &n;
 *p = 20;
 printf("n = %d\n", n);
return 0;
}

如果我们绕过n,通过n的地址,使用指针是可以成功改变n的值的。

需要注意的是:虽然n被const修饰,但n还是个变量。

4.2 const修饰指针变量

const修饰指针变量的时候有两种情况,分别是*在const的左边或者在const的右边。

4.2.1 const在*右边

当const在*右边时,如 int *const a,const修饰的时指针变量本身,保证了指针变量本身的内容无法被改变,但是a指向的内容是可以被改变的。

4.2.2 const在*左边

当const在*左边时,如int const * a,这是可以将*a看成一个整体,这时const修饰的时指针a指向的内容,而不是指针本身,所以这时候,指针本身的内容是可以改变的,但是这时候,指针所指向的内容是无法被改变的。

4.2.3 const在*两边都有

当*两边都有const修饰时,这种情况下,指针本身的内容也无法被改变,指针指向的内容也无法被改变。

5.指针运算

指针的基本运算有三种:

1. 指针 + - 整数

2. 指针 - 指针

3. 指针的关系运算

5.1 指针+ - 整数

指针加减整数我们在前面已经提到过指针的加减,下面我们再通过数组来了解一下指针+-整数。

我们知道数组在内存中是连续存放的,所以只要我们知道第一个元素的地址,我们便能顺藤摸瓜找到其他元素。

#include <stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* p = &arr[0];
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));  //这里就是指针加减整数
	}
	return 0;
}

 

一开始我们把指针赋值在arr[0]的位置,随着指针进行加上i ,指针p也会随着i的增加去访问后面的元素。从而会打印出数组的各个元素。

 

5.2 指针 - 指针

下面我们以一段代码为例

#include <stdio.h>
int my_strlen(char* s)
{
	char* p = s;
	while (*p != '\0')
		p++;
	return p - s;
}
int main()
{
	printf("%d\n", my_strlen("abc"));
	return 0;
}

运行如下图

 

结论:由此我们可以得知指针 - 指针得到的是两个指针之间元素的个数。 

5.3 指针的关系运算

指针的关系运算也就是指针之间大小的比较运算。下面我们还是以代码为例

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

运行代码

 

6.野指针

我们知道指针是有指向内容的,但一有疏忽,就可能导致指针指向的内容不明确,这样就导致了野指针的出现。

那野指针如何定义呢?

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

6.1 野指针成因

1. 指针未初始化

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

以上代码就是指针未初始化导致了野指针的例子

解释:通过函数栈帧可知,在调用main()函数之后,内存会给main()函数开辟一块函数栈帧,也就是一块空间,在创建完空间之后,编译器就会先给main()函数里面随机给上初始值,就如上面的代码,由于没给指针初始化,所以导致该指针随机指向一个内容,因此导致了野指针的出现。

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;
}

有i=11的时候,指针p就会指向arr[11]的位置,但由于arr数组大小只有10,当指针p指向arr[11]的时候,就没有对其访问的权限,这也变向导致指针没有明确指向的内容,就导致了野指针。

3. 指针指向的空间释放

首先我们得知道我们在调用一个函数,内存会为这个函数开辟一块空间,但在调用完函数之后,这块内存就会被回收到内存中去。

如以下代码

#include <stdio.h>
int* test()
{
 int n = 100;
 return &n;
}
int main()
{
 int*p = test();
 printf("%d\n", *p);
 return 0;
}

当我们调用test()这个函数的时候,内存就会为它开辟一块空间,这时候指针是有权限来访问这块空间的,但是当函数调用完之后,为这个函数开辟的空间就会被内存回收,这个时候指针就没有权限访问这块空间了,这样就导致了野指针。

6.2 如何规避野指针

6.2.1 指针初始化

我们在设计指针的时候要记得给指针初始化,也就是给指针赋值,如果明确知道指针指向哪里就直接赋值地址,如果不知道指针指向哪里,可以给指针赋初值NULL。

NIULL是C语言中定义的一个标识符常量,值为0,0也是地址,但这个地址是无法使用的,读写该地址时会报错。

6.2.2 小心指针越界

一个程序向内存申请了多少空间,指针就能访问多少空间,不能超出访问范围,超出了就是越界访问。

6.2.3 指针变量不再使用时,及时设置NULL,指针使用前检查有效性

当指针变量指向一块区时,我们可以通过指针访问该区域,后期不在使用指针的时候,我们要及时将其设置为NULL。

 因为有一个约定熟成的规则:

只要是NULL指针就不去访问,同时使用前可以判断指针是否为NULL。

例子:

int main()
{
 int arr[10] = {1,2,3,4,5,67,7,8,9,10};
 int *p = &arr[0];
 for(i=0; i<10; i++)
 {
 *(p++) = i;
 }
 //此时p已经越界了,可以把p置为NULL
 p = NULL;
 //下次使⽤的时候,判断p不为NULL的时候再使⽤
 //...
 p = &arr[0];//重新让p获得地址
 if(p != NULL) //判断
 {
 //...
 }
 return 0;
}

6.2.4 避免返回局部变量的地址

如造成野指针的第3个例子,不要返回局部变量的地址。

7. 指针的使用和传址调用

7.1 strlen的模拟实现

库函数strlen的功能是求字符串的长度,统计的时 \0 之前的字符个数。

函数原型如下:

 

参数str接受一个字符串的起始地址,然后开始统计字符串中\0之前字符个数,最终返回长度。

如果要模拟实现只要从起始地址开始向后逐个字符的遍历,只要不是 \0 字符,计数器就+1,这样直到 \0 就停⽌。
代码如下:
int my_strlen(const char * str)
{
 int count = 0;
 assert(str);
 while(*str)
 {
 count++;
 str++;
 }
 return count;
}
int main()
{
 int len = my_strlen("abcdef");
 printf("%d\n", len);
 return 0;
}

7.2 传值调用和传址调用

学习指针的目的是使用指针解决问题,那什么问题非指针不可呢?

例如:写一个函数实现交换两个变量的值

也许我们会这样写

#include <stdio.h>
void Swap1(int x, int y)
{
 int tmp = x;
 x = y;
 y = tmp;
}
int main()
{
 int a = 0;
 int b = 0;
 scanf("%d %d", &a, &b);
 printf("交换前:a=%d b=%d\n", a, b);
 Swap1(a, b);
 printf("交换后:a=%d b=%d\n", a, b);
 return 0;
}

但运行代码我们会发现失败了,这是为什么呢?

让我们调试下代码

 

当main()函数进行传参时,swap()函数会开辟空间来存储形参,由上图可知,a和b的值确实传给了x和y,但a和b的地址分别于x和y不同,这样我们就得知x和y是一块独立的空间 ,所以当我们在swap()函数里面交换它们的值时,是不会影响a和b的。

结论:实参传递给形参时,形参会单独创建一份临时空间来接收实参,对实参的修改不影响实参。

则这种时候我们就可以使用指针了,我们可以通过指针来间接交换a和b的值

如:

#include <stdio.h>
void Swap2(int*px, int*py)
{
 int tmp = 0;
 tmp = *px;
 *px = *py;
 *py = tmp;
}
int main()
{
 int a = 0;
 int b = 0;
 scanf("%d %d", &a, &b);
 printf("交换前:a=%d b=%d\n", a, b);
 Swap1(&a, &b);
 printf("交换后:a=%d b=%d\n", a, b);
 return 0;
}

运行代码

这时候发现就成功了。

这种将变量的地址传递给函数,这种函数调用就叫:传址调用。 

传址调用,可以让函数于主函数之间建立真正的联系。如果函数内部改变主函数的变量的值,就可以使用传址调用。

 

 

 

 

 

 

  • 45
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 11
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值