学习C语言(十)——深入理解指针(1)

目录

1.内存和地址

1.1 内存

1.2 如何理解编址

2.指针变量和地址

2.1 取地址操作符(&)

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

2.2.1 指针变量

2.2.2 拆解指针类型

2.2.3 解引用操作符(*)

2.3 指针变量的大小

3.指针变量类型的意义

3.1 指针的解引用

3.2 指针 + - 整数

4.const修饰指针

4.1 const修饰变量

4.2 const 修饰指针变量

5.指针运算

5.1 指针 + - 整数

5.2 指针 + - 指针

5.3 指针的关系运算

6.野指针

6.1 三个类型的野指针

6.1.1 指针未初始化

6.1.2 指针越界访问

6.1.3 指针指向的空间释放

 6.2 如何规避野指针

6.2.1 指针初始化

6.2.2 小心指针越界

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

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

7.assert 断言

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

8.1 传址调用和 传值调用

8.2 strlen的模拟实现


1.内存和地址

1.1 内存

        CPU(中心处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数值也会放回到内存中。内存划分为一个个的内存单元,每个内存单元的大小是一个字节,即8个比特。一个比特位可以存储一个二进制的 0 或 1 。

        每个内存单元都有一个编号(相当于门牌号),有了这个编号,CPU就可以快速找到一个内存空间。学生八人间宿舍中的八个学生,就相当于每个内存单元中的8个比特。

        我们把内存单元的编号称为地址,C语言中的地址有一个新的名字叫指针(pointer)。可以理解为:内存单元的编号 == 地址 == 指针。

1.2 如何理解编址

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

         计算机有很多相互独立的硬件单元,这些单元为了进行数据互通,是通过“线”连接起来的。

CPU和内存之间也是用“线”连接起来进行数据交互的。

        我们主要了解一下地址总线。在32位机器中有32根地址总线,每根线用 0、1表示两种态【电脉冲的有无】,一根线表示两种含义,那么32根线就表示2^32中含义,每一种含义都代表一种地址。

        地址信息被下达给内存,在内存上,就可以找到改地址对应的数据,将数据在通过数据总线传入CPU内寄存器。

2.指针变量和地址

2.1 取地址操作符(&)

        C语言中的创建变量就是想内存申请空间,示例如下:

        整型变量a,内存中申请了4个字节,用于存放整数10,其中每个字节都有地址(上图黄框内的)。 

        我们可以使用去地址操作符(&)来获取 a 的地址。示例如下:

         但是每次取到的 a 的地址都不一样,这是因为 a 是局部变量,在main函数执行结束后就会销毁,所以每次打印的地址都会不一样,但是设置成全局变量,地址就变成固定的了。示例如下:

 

         整型变量占用了4个字节,我们知道第一个字节地址后,就可以之后四个字节的地址了。通过地址访问的4个字节的数据也是可行的,请看后面的解引用操作符(*)。

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

2.2.1 指针变量

        我们通过去地址操作符(&)拿到的地址是一个数值,这个数值有时候也需要存储起来,方面后期继续使用,我们把这样的地址值存放在指针变量中。示例如下:

int main()
{
	int a = 10;
	int* p = &a;	//去除 a 的地址并存储到指针变量 p 中
	return 0;
}

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

2.2.2 拆解指针类型

int main()
{
	int a = 10;
	int* p = &a;	//去除 a 的地址并存储到指针变量 p 中
	return 0;
}

        在上面的代码中,p 的左边是 int* ,* 说明 p 是指针变量,而前面的 int 说明 p 指向的是整型(int) 类型的对象。

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

int main()
{
	char ch = "123";
	char* pc = &ch;	//	放在char* 类型的指针变量中 
	return 0;
}

2.2.3 解引用操作符(*)

        在C语言中,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)所指向的对象,这里需要使用解引用操作符(*)。

int main()
{
	int a = 10;
	int* p = &a;
	*p = 0;
	return 0;
}

        *p 就是通过 p 中存放的地址,找到指向的内存空间,*p 就相当于变量 a 了,所以 *p=0,相当于是 a=0。

2.3 指针变量的大小

        我们把32根地址线产生的二进制序列当做一个地址,一个地址就是32个bit位,也就是4个字节。如果指针变量用来存放地址,那么指针变量的大小就需要4个字节空间才可以。

        同理,64位机器,有64根地址线,一个地址的二进制序列就是64个bit位,存储起来就需要8个字节的空间。指针变量的大小就是8个字节。

         结论:

  • 32位平台下的地址是32个bit位,指针变量大小是4个字节
  • 64位平台下的地址是64个bit位,指针变量大小是8个字节
  • 指针变量的大小和类型无关,在相同平台下,指针变量的大小是相同的

3.指针变量类型的意义

3.1 指针的解引用

        对比下面的段代码,通过调试观察内存的变化。

        指针变量类型为 int* ,4个字节全部改为0。变化如下:

 

        指针类型为 char* ,知识将 n 的地址个字节改为0。变化如下:

 

         结论:指针的类型决定了,对指针解引用的时候有多大的权限(一次能操作几个字节)。

        比如:char* 的指针解引用就能访问一个字节,而 int* 指针的解引用就能访问四个字节。

3.2 指针 + - 整数

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

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

4.const修饰指针

4.1 const修饰变量

        变量是可以修改的,如果把变量的地址交给一个指针变量,通过指针变量也可以修改这个变量。但是如果我们希望一个变量加上一些限制,不能被修改,就需要用到 const 。

int main()
{
	int m = 0;
	m = 20;			//m可以修改
	const int n = 0;
	n = 20;			//n不能被修改
	return 0;
}

         在上面的代码中,n是不能被修改的。变量n被counst修饰之后,在语法上加了限制,修改n时就不符合语法规则,就会报错,导致没有办法修改n。

        但是如果我们绕过n,使用n的地址去修改n,这样就可以修改了。但是是在破坏语法规则。

         使用 const 修饰 n,是为了 n 不被修改,但是 p 拿到了 n 的地址,这样就打破了 counst 的限制,这样就失去的 const 的作用了。

        那么如何让 p 拿到了 n 的地址,也不能修改 n 的值。

 4.2 const 修饰指针变量

        使用下面这段代码来进行测试,放入VS2022中就能够看到哪里运行错误。

void test1()
{
	int n = 10;
	int m = 20;
	int* p = &n;
	*p = 20;	//能否运行?
	p = &m;	//能否运行?
}
void test2()
{
	int n = 10;
	int m = 20;
	const int* p = &n;
	*p = 20;	//能否运行?
	p = &m;		//能否运行?
}
void test3()
{
	int n = 10;
	int m = 20;
	int* const p = &n;
	*p = 20;	//能否运行?
	p = &m;		//能否运行?
}
void test4()
{
	int n = 10;
	int m = 20;
	int const * const p = &n;
	*p = 20;	//能否运行?
	p = &m;		//能否运行?
}
int main()
{
	test1();	//测试没有 counst 修饰的情况
	test2();	//测试 counst 放在 *左边的情况
	test3();	//测试 counst 放在 *右边的情况
	test4();	//测试 * 左右都有 const 的情况
}

        结论:const 修饰指针变量的时候

  • const 如果放在 * 的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本身的内容可变。
  • const 如果放在 * 的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指向的内容可以通过指针改变。

5.指针运算

        指针的基本运算有三种,分别是:

  1. 指针 + - 整数
  2. 指针 + - 指针
  3. 指针的关系运算

5.1 指针 + - 整数

        数组在内存中是连续存放的,只要知道第一个元素的地址,就能找到后面的所有元素。

 

5.2 指针 + - 指针

        在这段代码中,p 和 s 指向的是同一个字符串的起始位置,指针变量 p 的三次自增,然后再减去指针变量  s ,就能够得到字符的长度。

5.3 指针的关系运算

        在这段代码中,p 指向了arr数组中第一个元素的位置 ,这里的数组名 arr 就是arr数组中的第一个元素。arr+sz是指结尾位置之后的下一个位置的指针,用于在循环条件中判断是否达到数组的结尾。

6.野指针

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

6.1 三个类型的野指针

6.1.1 指针未初始化

6.1.2 指针越界访问

6.1.3 指针指向的空间释放

 6.2 如何规避野指针

6.2.1 指针初始化

        如果明确知道指针指向哪里就直接赋值,如果不知道指针指向哪里,就可以给指针赋值NULL。NUll 是C语言中定义的一个标识符常量,值是0,0也是地址。

6.2.2 小心指针越界

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

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

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

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

	}
	return 0;
}

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

        如造成野指针的第三个例子 

7.assert 断言

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

assert(p != NULL);

        在上面的示例中,验证变量 p 是否等于 NULL 。如果不等于 NULL 程序继续运行,否则就会终止,而且会给出报错信息提示。

        使用 assert()  的好处:它不仅能自动标识文件和出问题的行号,还有一种能无需改代码就能开启或者关闭 assert() 的功能。如果已经确认程序没有问题,不许要再做断言。就在 #include <assert.h> 语句的前面,定义一个宏 NDBUG。

#define NDBUG
#include <assert.h>

        然后重新编译程序,编译器就会禁用文件中的所有 asser() 语句。如果程序又出现问题,可以移除或者注释掉 #include NDBUG。再次编译,就能够重新启用 assert() 语句。

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

8.1 传址调用和 传值调用

        有什么问题是非要使用到指针呢?

        例如:写一个函数,交换两个整型变量的值。

         在运行程序知乎,我们会发现程序没有产生交换的效果,这是因为相较于 a和b ,x和y是独立的空间, a和b  x和y地址也不一样。x和y 是函数内的变量,并没有改变a和b的值,在函数运行结束之后,x和y的地址就被销毁了。

        swap函数在使用的时候,把变量本身直接传递给了函数,这种调用函数的方式叫做传值调用。在之前的学习中,我们就了解了。

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

        知道了问题所在,现在我们来解决它!!

        我们可以使用指针,直接在 swap 函数内部将 a和b 的值进行交换。方法就是在 main 函数中将 a和b 的地址传递个swap函数。swap函数里边通过地址间接操作 main 函数里的 a和 b 。

         通过这种方式成功将 a和b 的值进行了置换。这种将变量的地址传递给了函数,这种函数调用的方式叫:传址调用

8.2 strlen的模拟实现

int my_strlen(const char* str)
{
	int count = 0;
	assert(str);
	while (*str != '\0')
	{
		count++;
		str++;
	}
	return count;
}
int main()
{
	int len = my_strlen("abcdef");
	printf("%d\n",len);
	return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值