C语言之指针那些事(1)

 

目录

前言

一、内存和编址

1.0 内存

1.1 编址 

二、指针变量和地址

2.0 取地址操作符

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

2.1.0 指针变量

2.1.1 解引用操作符

2.2 指针变量的大小

三、指针变量类型的意义

3.0 指针的解引用

3.1 指针+- 整数

3.2 void* 指针

四、const 修饰指针

4.0  const修饰变量

4.1 const修饰指针变量

五、指针运算

5.0 指针的基本运算

5.0.0 指针+-整数

5.0.1 指针-指针

5.0.2 指针的关系运算

六、野指针

6.0 野指针成因

 6.1 如何避免使用到野指针

七、assert 断言

7.0 概念

7.1 assert()使用的好处和缺点

八、指针的使用和传址调用

8.0 传值调用和传址调用


前言

谈到指针,我们第一印象就是难,但是在C语言中指针却是很重要的一部分,所以我们得打好基础。在这部分我将会详细讲解指针,大家认真做好小笔记,学习起来简直易如反掌(bushi

好啦,话不多说,搬好小板凳开始学习吧!

一、内存和编址

1.0 内存

首先,了解指针前,我们先简单知道一下计算机中的内存分配

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

内存又叫主存,从名称可以看出,它是计算机主要存储位置。那有人就会问,那磁盘的存储空间不是更大嘛。磁盘的存储空间虽然很大,但是计算机的CPU没办法直接跟磁盘交互,它只跟内存打交道。CPU通过总线读写内存的数据,如果数据不在内存中,就会去磁盘中检索对应的数据到内存中,然后返回给CPU。所以内存在计算机中核心的作用就是用来做主缓存,配合CPU读写数据。

那我们买电脑的时候,电脑上内存是8GB/16GB/32GB等,其实也是把内存划分为一个个的内存单元,每个内存单元的大小取1个字节

我们可以把内存比作学校,内存单元就是教室,1个字节空间放8个比特,如同一个教室只存8个人(每个人是一个比特位),内存单位有编号(相当于教室有门牌号),所以CPU可以快速找到内存空间(教室)。

门牌号相当于地址,在计算机中内存单元的编号也称地址。C语言中则称其为指针

也就是说内存单元编号==地址==指针

8bb08e7fb81645ada28fc2a93f518838.png


 

1.1 编址 

内存编址的目的:

程序运行时,计算机CPU只认识内存地址,而不关心这个地址所代表的空间在哪里,怎么分配等问题。CPU访问内存中的某个字节空间,必须知道这个字节空间在内存的什么位置,而因为内存中字节很多,所以需要给内存进行编址。注意:内存的编址以字节为单位。

CPU和内存之间是有大量的数据交互的,两者必须也用线连起来。不过我们今天关心一组线就行,就是地址总线

地址总线:

(1)CPU是通过地址总线来指定存储单元的。

(2)地址总线决定了CPU所能访问的最大内存空间的大小。eg: 10根地址线能访问的最大的内存为1024位二进制数据(1024个内存单元)(1B)

(3)地址总线是地址线数量之和。

32位机器有32根地址总线, 每根线只有两态,表示0,1【电脉冲有无】,那么一根线,就能表示2种含义,2根线就能表示4种含义,依次类推。32根地址线,就能表示2^32种含义,每一种含义都代表一个地址。 地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据在通过数据总线传入CPU 内寄存器。

3a7fb8947d2d4d57b3b4f619e3790269.png

二、指针变量和地址

2.0 取地址操作符

有了前面的理解,我们就知道了在C语言中创建变量其实就是向内存申请空间

如代码示例

#include<stdio.h>

int main()
{
	int a = 10; // 变量创建的本质其实就是在内存中申请空间
	            // 向内存申请了4个字节的空间,用来存放10这个数值
	return 0;
}

在x86环境下我们通过调试可以看出内存申请了4个字节用来存放10,每个字节也有地址。

7a75a5fac8194a19bfc6eb81ffc5af82.png

为了得到a的地址,我们可以使用&--取地址操作符,可以拿到变量a的地址。

#include<stdio.h>

int main()
{
	int a = 10; // 变量创建的本质其实就是在内存中申请空间
	           // 向内存申请了4个字节的空间,用来存放20这个数值
	printf("%p\n", &a);// &--取地址操作符,取出地址a
	return 0;
}

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

2.1.0 指针变量

为了存放我们用操作符(&)得到的值,我们定义了一个指针变量,把它存到指针变量中,方便我们以后使用。

int main()
{
	int a = 10; 
	           
	printf("%p\n", &a);

	int* pa = &a;// 变量 pa 就是用来存放地址(指针)的,pa叫指针变量
	return 0;
}

解析:

fa51add85a3b4388adf0a937d16f58fb.png   b34ba4130ca34024b36ff02f76a8b388.png

例如我们要通过pa找到a的地址

256bb7216ef04c0bb2516634c094df69.png

2.1.1 解引用操作符

我们既然将地址保存起来了,怎么使用呢?

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

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

	*pa = 100;  //  * 就是解引用操作符,此时*pa就是变量a了
	            //  我们将a改成了100

	printf("%d\n", a); // 100

	return 0;
}

为什么不直接写成 a=100呢,其实这里是把a的修改交给了pa来操作,这样对a的修改,就多了一种的途径,代码会更灵活。如果自己不方便,借它人之手干事嘛,很好理解的。之后学习会更加清晰,且看下去吧。

其他类型操作也这样,如:

int main()
{
	char ch = 'f';
	char* pc = &ch;
	printf("%c\n", *pc);  // f
	return 0;
}

2.2 指针变量的大小

前面的内容我们了解到,32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产生的2进制序列当做一个地址,那么一个地址就是32个bit位,需要4个字节才能存储。如果指针变量是用来存放地址的,那么指针变的大小就得是4个字节的空间才可以。 同理64位机器,假设有64根地址线,一个地址就是64个二进制位组成的二进制序列,存储起来就需要 8个字节的空间,指针变量的大小就是8个字节。

4e61444b438b442bb8f3034a415d44af.png

结论:

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

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

● 指针变量的大小和类型是无关的只要指针类型的变量,在相同的平台下,大小都是相同的。

三、指针变量类型的意义

疑问:

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

3.0 指针的解引用

下面我们通过两段代码来解析。

// 代码一
#include <stdio.h>
int main()
{
	int a = 0x11223344;
	int* pa = &a;
	*pa = 0;
	return 0;
}
//代码二
#include <stdio.h>
int main()
{
	int a = 0x11223344;
	char* pc = (char*)&a;
	*pc = 0;
	return 0;
}

通过调试我们可以观察到代码一会将a的4个字节全部改为0,但是代码二只是将a的第一个字节改为0。

980a8cac3b3b4045a5864d81726ca439.png

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

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

3.1 指针+- 整数

先看一段代码

#include <stdio.h>
int main()
{
	int a = 0x11223344;
	int* pa = &a;
	char* pc = (char*)&a;
	printf("&a = %p\n", &a);
	printf("pa = %p\n", pa);
	printf("pc = %p\n", pc);

	printf("&a+1 = %p\n", &a+1);
	printf("pa+1 = %p\n", pa + 1);
	printf("pc+1 = %p\n", pc + 1);

	return  0;
}

运行结果:

 8c3b060e9251427292e5c452ec249d2f.png

b5a6375247294797b93c940f6ae8a701.png

 由此可见int* 类型的指针变量+1跳过了4个字节。char* 类型的指针变量+1跳过1个字节,这就是指针变量的类型差异带来的变化。指针+1,其实跳过1个指针指向的元素。指针可以+1,那也可以-1。

敲黑板啦:指针的类型决定了指针向前或者向后走一步有多大(距离)。

3.2 void* 指针

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

1afcfadda9a543238a365efcd06cddac.png

154932d151be4d0bbab7236d46fc30d0.png

我们从void谈起,void即类型不确定,从而所占内存也不确定,所以不能写 void pa=10; 之类的声明。

在C语言中,void通常有两种作用

1. 对函数返回类型限定,利用void对象的大小来限定函数没有任何返回值。

2. 对参数类型的限定,当函数不允许接参时,必须用void来限定函数的参数。 

但是对void*来说,编译器允许写如 int* someInt = 10; void* pa = &someInt;之类的操作,因为无论指向什么类型指针,指针本身所占空间是一定的 。根据前面所学的知识也知道。

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

其实我们可以理解它为一个通用指针。一般 void* 类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果。使得一个函数来处理多种类型的数据。这里就不过多赘述了,后面会慢慢理解的。

四、const 修饰指针

4.0  const修饰变量

const是一个C语言(ANSI C)的关键字,它限定一个变量不允许被改变,产生静态作用。

当我们把变量的地址交给一个指针变量,通过指针变量的也可以修改这个变量。 但我们希望这个变量不能被修改,就可以用的const。

#include<stdio.h>

int main()
{
	int a = 0;
	a = 20;//a是可以修改的

	const int b = 0;
	b = 20;//  err, b是不能被修改的

	return 0;
}

因此,有了const 限定,就不能修改代码中b的值了,否则会报警告:

但是如果我们绕过b,使用b的地址,去修改b就能做到了,这样做是在打破语法规则。但是我们就是为了不让b值被修改才加const限制的,没必要打破语法规则。

4.1 const修饰指针变量

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

我们来段代码分析一下

//代码1 - 测试无const修饰的情况 
void test1()
{
	int n = 10;
	int m = 20;
	int* p = &n;
	*p = 20;//ok
	p = &m; //ok

}
//代码2 - 测试const放在*的左边情况 
void test2()
{
	int n = 10;
	int m = 20;
	const int* p = &n;
	*p = 20;//err
	p = &m; //ok
}
//代码3 - 测试const放在*的右边情况 
void test3()
{
	int n = 10;
	int m = 20;
	int* const p = &n;
	*p = 20; //ok
	p = &m; //err
}
//代码4 - 测试*的左右两边都有const 
void test4()
{
	int n = 10;
	int m = 20;
	int const* const p = &n;
	*p = 20; //err
	p = &m; //err
}

int main()
{
	//测试⽆const修饰的情况 
	test1();
	//测试const放在*的左边情况 
	test2();
	//测试const放在*的右边情况 
	test3();
	//测试*的左右两边都有const 
	test4();
	return 0;
}

总结一下:

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

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

五、指针运算

5.0 指针的基本运算

指针的基本运算有三种

● 指针+-整数

● 指针-指针

● 指针的关系运算

5.0.0 指针+-整数

首先,我们明确两点:

(1)数组在内存中是连续存放的。

(2)指针的类型决定了指针向前或者向后走一步有多大,也就是解引用的权限多大。

#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];  // &arr[0] 就是取出数组首元素的地址
	for (int i = 0; i < sz; i++)
	{
		
		printf("%d ", *p); // 解引用,取出数组中的元素
		                   // 1 2 3 4 5 6 7 8 9 10
		p++;  // 指针++
	}

	return 0;
}

5.0.1 指针-指针

指针-指针的绝对值是指针和指针之间元素的个数
指针-指针计算的前提条件是两个指针指向的是同一个空间

#include<stdio.h>
size_t my_strlen(char* p)
{
	char* start = p;
	char* end = p;
	while (*end)
	{
		end++;
	}
	return end - start; // 指针-指针
}
int main()
{
	char arr[] = "abcdef";
	size_t len = my_strlen(arr);//数组名其实是数组首元素的地址 arr == &arr[0]
	printf("%zd\n", len); // 6

	return 0;
}

5.0.2 指针的关系运算

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

	{
	 printf("%d ", *p);
	 p++;
	}
	return 0;
}

六、野指针

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

6.0 野指针成因

指针未初始化

指针越界访问

指针指向的空间释放

代码示例如下:

#include <stdio.h>
 // 代码一(指针未初始化)
int main()
{
	int* p;//局部变量指针未初始化,默认为随机值
    * p = 20;
	return 0;
}
  // 代码二(指针越界访问)
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;
}
  // 代码三( 指针指向的空间释放)
int* test()
{
	int n = 10;
	return &n;
}
int main()
{
	int* p = test();
	printf("%d\n", *p);
	return 0;
}

 6.1 如何避免使用到野指针

● 指针初始化

如果明确知道指针指向哪里就直接赋值地址,如果不知道指针应该指向哪里,可以给指针赋值NULL。

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

// 指针初始化 
#include <stdio.h>
 int main()
 {
 int num = 100;
 int*p1 = &num;
 int*p2 = NULL;
 return 0;
 }

● 小心指针越界

指针访问已经申请的空间,不能超出范围访问。

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

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

● 避免返回局部变量的地址。

七、assert 断言

7.0 概念

C/C++ 中的 assert是一个,assert.h 头文件定义了宏 assert() ,用于在运行时检查一个条件是否为真,如果条件不满足,则运行时将终止程序的执行并输出一条错误信息。

语法:

● assert(condition);

● condition 就是要判断的条件

assert() 宏接受一个表达式作为参数。如果该表达式为真(返回值非零),assert() 不会起任何作用,程序继续运行。如果该表达式为假(返回值为零),assert() 就会报错

例如;

 assert(p != NULL);

上面代码在程序运行到这条语句时,验证变量 p 是否等于 NULL 。如果确实不等于继续运行,否则就会终止运行,并且给出报错信息提示

7.1 assert()使用的好处和缺点

好处:

● 能自动标识文件和出问题的行号,有一种无需更改代码就能开启或关闭 assert() 的机制

● 如果已经确认程序没有问题,不需要再做断言,就在 #include <assert.h> 语句的前面,定义一个宏NDEBUG

 #define NDEBUG
 #include <assert.h>

缺点:引入了额外的检查,增加了程序的运行时间

注意:

程序一般分为Debug 版本和Release 版本,Debug 版本用于内部调试,Release 版本发行给用户使用。断言assert 是仅在Debug 版本起作用的宏。在Release 版本中优化掉了。

八、指针的使用和传址调用

8.0 传值调用和传址调用

如果我们要写一个函数,交换两个整型变量的值,很多友友可能一开始会这样写:

但是它并没有产生效果,没有交换数值,为什么呢?

其实我们值的确传过去了,但是x的地址和a的地址不 一样,y的地址和b的地址不一样,相当于x和y是独立的空间,你在Swap1函数内部对x,y哐哐操作自然不会影响a,b的值。

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

那如何解决呢?当然有办法!

我们可以使用指针,在main函数中将a和b的地址传递给Swap2函数,Swap2函数里边通过地址间接的操作main函数中的a和b,并达到交换的效果就好了。

#include <stdio.h>
void Swap2(int* pa, int* pb) // pa里存的是a的地址,pb里存的是b的地址
{
	int tmp = 0;
	tmp = *pa;
	*pa = *pb;
	*pb = 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;
}

输入 2 3 ,运行结果为:

交换成功了!这里调用Swap2函数的时候是将变量的地址传递给了函数,这种函数调用方式叫:传址调用。

小结一下:

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

好了,第一期的指针就这样啦,有没有对指针没那么恐惧了呢!

多多点赞支持收藏,如有不足欢迎评论区指出

你们的支持是我更新最大的动力!!!ヾ(≧O≦)〃嗷~

Respect!

  • 38
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值