[指针]从浅到深-初识指针-第一课

一、内存和地址

在先了解指针之前,我们先来了解一下内存和地址的概念等知识。

1.内存

说白了,内存就是一片空间,用来存放东西的地方,那么对与计算机来讲,肯定是存放各种数据和程序的地方。
学习过机组知识后就知道,计算机上CPU中央处器)在处理数据的时候,需要的数据是通过数据总线在内存中读取的,处理后的数据也会通过数据总线放回内存中(所以数据总线是双向的)。如图
在这里插入图片描述

但是CPU是怎么知道要进行数据的存入和读出,并且准确无误地找到数据要存放和读出的地方,这里就要引出地址的概念了。

2.地址

内存是一片很大的空间,把内存划分为⼀个个的内存单元,每个内存单元的大小取1个字节,这样可以更好地管理数据。然后给每一个内存单元都分配一个编号,可以通过这个编号找到对应的内存单元,也就是地址。而C语言中,我们可以这样理解,内存单元的编号 = 地址= 指针

所以通过地址(编号)我们就可以准确无误地找到数据要存放和读出的地方在哪里了,比如在内存那点的图片里,我们可以看到有很多地址线组成了地址总线,CPU会通过译码找到相应的地址编号,从而通过数据总线和控制总线来执行数据的存入还是数据的读出

计算机中常⻅的单位(补充):
⼀个⽐特位可以存储⼀个2进制的位1或者0
1Byte = 8bit(比特位)
1KB = 1024Byte(字节)
1MB = 1024KB
1GB = 1024MB
1TB = 1024GB
1PB = 1024TB

在这里插入图片描述


二、指针变量

首先我们应该知道在C语⾔中创建变量其实就是向内存申请空间,如下:

在这里插入图片描述
我们可以看到,当我们创建了整型变量a,就向内存中申请了4个字节的空间,一行代表一个字节,因为是以十六进制显示,10的十六进制为a,存入到内存中去了。

补充:创建的变量其实是给我们程序员看的,比如a它并没有存入到内存中去,没有什么实际意义,只是让我们自己知道,a代表着10,这样好进行其他操作(加减乘除等),所以以后大家在取变量名时要尽可能取有意义的名字,好让我们一眼就知道是关于什么的。

从图片中看到,我们是怎么拿出a的地址的?是用了&这个符号,接下来让我们来简单了解这个符号吧。

1.操作符(&)-取地址操作符

&a取出的是a所占4个字节中地址较小的字节的地址。虽然整型变量占用4个字节,我们只要知道了第⼀个字节地址,顺藤摸瓜访问到4个字节的数据也是可行的。其中%p是用来打印地址的
在这里插入图片描述

2.指针变量

那我们通过取地址操作符(&)拿到的地址是⼀个数值,这个数值有时候也是需要存储起来,方便后期再使用的,那我们把这样的地址值存放在哪里呢?答案是:指针变量中

int a = 10;
int * pa = &a;//其中a的地址就存放在了pa这个变量中,pa也就是存放了地址的变量,
//pa也叫做指针变量 

3.指针类型

我们该如何理解指针的类型呢?其实很简单。
这⾥pa左边写的是 int * (int * 就是指针变量的类型), * 是在说明pa是指针变量,⽽前⾯的 int 是在说明pa指向是整型(int)类型的对象。

那如果有⼀个char类型的变量ch,ch的地址,要放在什么类型的指针变量中呢?

char ch = 'w';
pc = &ch;//pc 的类型怎么写呢?

相信你们也会了

char ch = 'w';
char * pc = &ch;

4.解引用操作符(*)

我们将地址保存起来,未来是要使用的,那怎么使用呢?
在现实生活中,我们使用地址要找到⼀个房间,在房间里可以拿去或者存放物品。
C语言中其实也是⼀样的,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,这里必须学习⼀个操作符叫解引用操作符(*)。

在这里插入图片描述
像这里,我通过指针变量pa间接来改变a的值,只是很有用的一步,现在你们可能觉得这是个多余的步骤,不过以后你们就不觉得了。

5.指针变量的大小

在前面我们已经介绍了地址有关的知识,我们应该知道了,CPU是通过地址总线到主存中读取数据或者存入数据的,平常所说的32位机器是指有32根地址线构成地址总线,即一个地址编号有32位比特,为4个字节;64位机器是指有64根地址线构成地址总线,即一个地址编号有64位比特,为8个字节。

  1. 32位机器:一个地址编号有4个字节,因为指针变量是用来存放地址的,所以指针变量的大小只需要4个字节即可存储地址。
  2. 64位机器:一个地址编号有8个字节,因为指针变量是用来存放地址的,所以指针变量的大小只需要8个字节即可存储地址。

上图是在32位机器下运行的结果,可以看到,无论指针变量的类型是什么,指针变量的大小都是相等的,而且只与机器的位数有关。看图片左上角X86(其实就是X32)

在这里插入图片描述

上图是在64位机器下运行的结果,看图片左上角X64。

结论:
• 32位平台下地址是32个bit位,指针变量大小是4个字节
• 64位平台下地址是64个bit位,指针变量大小是8个字节
• 注意指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的。

提问:不知道大家会不会对此指针变量类型产生疑惑?觉得指针变量类型它没有意义,因为指针变量的大小与指针变量类型无关呢?下面让我们进一步了解!!!


三、指针变量类型的意义

1.指针的解引用

例一:
在这里插入图片描述
在这里插入图片描述

通过F11逐条执行语句,当还未执行*pa=0之前,我们可以看到有一个4字节空间已经存入数据,然后pa指针变量指向了a,再解引用指针变量pa后,那片空间中的数据会被改变,即全部被改为0。

例二:
在这里插入图片描述
在这里插入图片描述

此时我们改变了pa指针变量的类型从int * 到char * ,通过F11逐条执行语句,当还未执行*pa=0之前,我们可以看到有一个4字节空间已经存入数据,然后pa指针变量指向了a,再解引用指针变量pa后,那片空间中的数据会被改变,但是我们发现只有其中的一个字节被改变为了0,并不是4个字节全部被改变。

注:&a的值可以放到char * 类型的指针变量中,因为不管是那种指针类型,大小都是与地址编号一样相等。

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

2.指针的运算(指针±整数)

在这里插入图片描述
我们可以看出, char * 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。这就是指针变量的类型差异带来的变化。指针+1,其实跳过1个指针指向的元素。指针可以+1,那也可以-1。

结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)。也可以说指针变量类型决定了指针的步长(提示:理解这一点在后面理解起数组与指针结合的代码有大用处)
给一个更详细地对比:
int *pa;//pa+1类似等于pa+1*sizeof(int) 即指针偏移了4个字节; pa+n等于pa+n*sizeof(int)即偏移了4n个字节
char *pa;//pa+1类似等于pa+1*sizeof(char) 即指针偏移了1个字节; pa+n等于pa+n*sizeof(char)即偏移n个字节

3.void *指针

void * —>无具体类型的指针

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

int a = 10;
int *ptr1 = &a;//&a的类型是int * 与指针ptr1的类型int *一样,不会出现警告
char *ptr2 = &a;//&a的类型是int * 与指针ptr2的类型char *不一致,会出现警告,但是程序还是能够运行

//那么我们应该怎么才可以消除这个警告呢?   答:这就要用到void *无具体指针类型来接受各种指针类型

int a = 10;
void *ptr1 = &a;//此时就不会出现因为指针类型不符合而导致警告。
void *ptr2 = &a;

//但是会出现新的问题,相信有些同学已经想到了,你们还知不知道上面所讲的指针的步长
//步长就是指针+-整数时会偏移多少个字节。指针类型会导致解引用指针和指针+-整数时导致不同

int a =10;
void *ptr1 = &a;
void *ptr2 = &a;
*ptr1 = 10;//此时会报错,因为指针ptr1的类型是无具体的,根本不知道一次要访问多少个字节
*ptr2 = 0;//此时会报错,因为指针ptr2的类型是无具体的,根本不知道一次要访问多少个字节
ptr1++;//不知道指针类型,导致不知道+1应该偏移多少个字节
ptr2--;//不知道指针类型,导致不知道-1应该偏移多少个字节

那么 void* 类型的指针到底有什么⽤呢? ⼀般 void* 类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果。使得⼀个函数来处理多种类型的数据,


四、const修饰指针

1.const修饰变量

在这里插入图片描述

const修饰变量的时候,此时变量叫常变量
这个被修饰的变量本质上还是变量,只是不能被修改,如上图中,想要修改num的值会出现报错。

那么我们怎么证明它本质还是变量?

在这里插入图片描述

如上图,变量n被const修饰,具有常属性,但是也不能在定义数组时来指定数组的大小,所以本质还是变量。

通过分析,我们明白了被const修饰的变量不能被改变,但是以我们现有的知识,我们应该是可以改变那个被修饰的变量,即通过取变量的地址(&),让指针间接地改变其值。如下图:
在这里插入图片描述
虽然这样确实可以更改其值,但是我们发现逻辑上就错误了,说不过去。我们来想想,我们用const来修饰变量,本意就是让变量不被改变,但是可以用指针来打破const的限制,这不是多此一举吗?那么我们也应该对指针做出一些限制,使其也不能改变const修饰的变量。这就引出了const修饰指针变量!!!

2.const修饰指针变量

⼀般来讲const修饰指针变量,可以放在的左边,也可以放在的右边,意义是不⼀样的。

int * p;//没有const修饰?
int const * p;//const 放在*的左边做修饰
int * const p;//const 放在*的右边做修饰

例子一:无const修饰
在这里插入图片描述

例子一中,p是可以改变指向的,从一开始指向n到指向m,而且也可以修改指向的对象的值,n的值从10更改为了20,即指向和指向对象的值都可以改变

例子二:const放在 * 的左边
形式有二种,但意义相同:

  • const int * p
  • int const * p
    在这里插入图片描述

const放在 * 的左边,限制的是指针指向的内容,也就是不能通过指针变量来修改它所指向的内容,但是指针变量本身是可以改变的

例子三:const放在 * 的右边
在这里插入图片描述

例子四: * 两边都放const

代表即不可以修改指向的内容,也不可以修改指向的方向


五、指针运算

指针的基本运算有三种,分别是:
指针± 整数
指针-指针
指针的关系运算

1.指针±整数

在上面,我们已经讲了这点,此时该告诉你它有什么用。在指针和数组的结合代码中,我们可以通过指针来遍历整个数组,如下图所示:
下标方式访问数组
指针访问数组

为什么可以用指针来访问数组?关键的一点在于,定义数组时,内存中会开辟一片连续的空间给数组,即连续存放的在这里插入图片描述


根据这节内容,指针±整数,让我们加深对指针类型的理解

如下图,我们也是用指针来遍历数组,但是这次我将int * p改为了 char * p,一样是把数组遍历出来了,那么下面的代码有没有错误?错在哪里了?

在这里插入图片描述

还是让我来直接告诉你们吧,看下图,内存中此时数据是这样存放的,p指针最开始是指向开头的(存放数据1) ,记得指针类型是会影响解引用时一次访问多少个字节,此时p指针的类型是char *,所以一次访问1个字节。导致访问了4个字节中的1个字节,遗落了其他3个字节,当数据够大时,会导致数据丢失。在这里插入图片描述

让我们改变一下数组内数据的大小
在这里插入图片描述
此时打印出来的数据发生了错误

是不是觉得对指针又有一步更近地理解了

2.指针-指针

指针-指针的绝对值是指针和指针之间元素的个数
但是前提条件:二个指针指向同一个空间

在这里插入图片描述

答案是9

应用:
例子一:我们通过调用strlen()函数来计算字符数组中字符的个数
在这里插入图片描述
例子二:我们通过自己写一个函数来计算字符数组的个数(运用指针±整数)

#include<stdio.h>
#include<string.h>
size_t my_strlen(char* p)//传的数据类型是什么,则接受的数据类型就是什么
{
	size_t count = 0;
	while (*p != '\0')//注意:计算机中的斜杆是'\',而不是'/',它代表的是运算的除法
	{
		count++;
		p++;
	}
	return count;
}

int main()
{
	char arr[] = "abcdef";
	size_t len = my_strlen(arr);//数组名是首元素地址,即arr == &arr[0],arr的指针类型是char *
	printf("%zd\n", len);  //结果是6
	return 0;
}

例子三:我们通过自己写一个函数来计算字符数组的个数(运用指针-指针)

size_t my_strlen(char* p)
{
	char* start = p;
	char* end = p;
	while (*end != '\0')
	{
		end++;
		
	}
	return end - start;
}

int main()
{
	char arr[] = "abcdef";
	size_t len = my_strlen(arr);//数组名是首元素地址,即arr == &arr[0],arr的指针类型是char *
	printf("%zd\n", len);//结果也是6
	return 0;
}

3.指针的关系运算

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	size_t sz = sizeof(arr) / sizeof(arr[0]);
	int* p = &arr[0];
	while (p <= &arr[sz - 1])//数组中的元素从低地址到高地址存放,当p小于arr[sz-1]时,代表还未遍历完数组元素
	{
		printf("%d ", *p);  //结果为1 2 3 4 5 6 7 8 9 10
		p++;
	}
	return 0;
}

6.野指针

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

6.1野指针的成因

1. 指针未初始化

#include <stdio.h>
int main()
{ 
 int *p;//局部变量指针未初始化,默认为随机值,即p的指向不确定,会改变
 *p = 20;//解引用操作符就会形成非法访问,此时p就是野指针
 return 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;
}

当p指向下标为10的元素时,这时进行解引用操作,会导致非法越界访问,此时p就是野指针
如图

3. 指针指向的空间释放

#include <stdio.h>
int* test()
{
 int n = 100;
 return &n;
}

int main()
{
 int*p = test();
 printf("%d\n", *p);
 return 0;
}

重点要知道,局部变量n出test()函数会被销毁,内存空间会返回操作系统管理,此时再通过p指向的空间进行访问(解引用),会导致非法访问
在这里插入图片描述

6.2规避野指针

6.2.1 指针初始化

如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL.
NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址
会报错。

#include <stdio.h>
int main()
{
 int num = 10;
 int*p1 = &num;
 int*p2 = NULL;  //不知道p2指向哪里,就先给p2指向NULL
 
 return 0;
}

6.2.2 小心指针越界

⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是
越界访问。

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

int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9,10};
 int *p = &arr[0];
 int i = 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 避免返回局部变量的地址

不要返回局部变量的地址
在这里插入图片描述


7.assert 断言

assert.h 头文件定义了宏 assert() ,用于在运行时确保程序符合指定条件,如果不符合,就报
错终止运行。这个宏常常被称为“断言”。

assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值非零), assert() 不会产生
任何作用,程序继续运行。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误
流 stderr 中写入⼀条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号。

//代码一
#include<stdio.h>
#include<assert.h>

int main()
{
	int a = 0;
	int* p = &a;
	assert(p != NULL);

	return 0;  //程序不会报错
}
//代码二
#include<stdio.h>
#include<assert.h>

int main()
{
	int a = 0;
	int* p = NULL;
	assert(p != NULL);

	return 0;
}

代码二中,不满足条件会报错,并给出在哪一行错误
在这里插入图片描述

assert() 的使⽤对程序员是⾮常友好的,使⽤ assert() 有⼏个好处:它不仅能⾃动标识⽂件和
出问题的⾏号,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问
题,不需要再做断⾔,就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG 。

#define NDEBUG
#include <assert.h>

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

8.1 strlen的模拟实现

size_t strlen ( const char * str );//strlen函数原型
#include<stdio.h>
#include<assert.h>
size_t my_strlen(const char* s)//const限制*s对其字符串数组进行修改
{
	size_t count = 0;
	assert(s != NULL);//检测指针s是否有效

	while (*s)
	{
		count++;
		s++;
	}
	return count;
}

int main()
{
	char arr[] = "abcdef";
	printf("%zd", my_strlen(arr));
	return 0;
}

8.2传值调用和传址调用

8.2.1传值调用

#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);//输入 3  5
 Swap1(a, b);
 printf("交换后:a=%d b=%d\n", a, b);//结果 3  5
 return 0;
}

其中x,y的地址与a,b不一样,x,y是形参,a,b是实参;形参是实参的一份临时拷贝,即对形参修改值也不会对实参有影响

8.2.2传址调用

#include <stdio.h>
void Swap2(int*px, int*py)//用指针接收a,b的地址,则可以通过px,py指针分别找到a,b的空间,再通过解引用操作修改a,b的值
{
 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);
 Swap2(&a, &b);
 printf("交换后:a=%d b=%d\n", a, b);
 return 0;
}

传址调用,可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量;所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采用传值调用。如果函数内部要修改主调函数中的变量的值,就需要传址调用。

谢谢观看!希望以上内容帮助到了你,对你起到作用的话,可以一键三连!你们的支持是我更新地动力。
因作者水平有限,有错误还请指出,多多包涵,谢谢!

  • 29
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 16
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值