深入C语言指针,使代码更加灵活(一)

前言:愿我们都有自己的目标并正在为其不懈努力

一、内存和地址

1.1 概念

我们都知道计算机的数据必须存储在内存里,为了正确地访问这些数据,必须为每个数据都编上号码,就像门牌号、身份证号一样,每个编号是唯一的,根据编号可以准确地找到某个数据。

生活中我们把门牌号叫地址,而在计算机中我们把内存单元的编号也称为地址。但是在C语⾔中给地址起了一个新的名字:指针。
所以我们可以理解为:
内存单元的编号 == 地址 == 指针

1.2 取地址操作符(&)

理解了内存和地址的关系,我们再回到C语⾔,在C语⾔中创建变量其实就是向内存申请空间!

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
	
int main()
{
	int a = 10;//变量创建的本质:在内存上开辟空间
	//要向内存申请个字节的空间,存放数据10
	//10 ---> a
	//0000 0000 0000 0000 0000 0000 0000 1010
	//0x   00   00   00   0a
	//
	//%a  &---取地址操作符
	printf("%p\n", &a);
	
	return 0;
}

整型变量a,在内存中申请4个字节,用于存放整数10,每个字节都有地址

&a取出的是a所占4个字节中第一个字节的地址(地址较小的那个字节的地址)来打印,例如:0x006FFC0C 。
虽然整型变量占用4个字节,但我们只要知道了第⼀个字节地址,顺藤摸瓜访问到4个字节的数据也是可行的。

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

数据在内存中的地址称为指针,如果一个变量存储了一份数据的指针(地址),我们就称它为指针变量

那我们如何使用指针变量呢?

在酒店中,我们可以通过门牌号准确找到每个客户。同理,我们也可以通过每个地址准确找到每个变量。C语言中也是⼀样的,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,而在这之前我们必须要学习⼀个操作符叫解引用操作符(*)。

例如:

int* p1;//指向一个整型的指针
char* p2;//指向一个字符的指针
float* p3;//指向一个单精度浮点数的指针
double* p4;//指向一个双精度浮点数的指针

并且我们可以通过指针变量进行赋值。

*p1 = 4;
*p2 = 'a';
*p3 = 5.0;

1.4 void指针和空指针

(1)void*是一种特殊的指针类型,它可以指向任意类型的数据,就是说可以用任意类型的指针对 void 指针赋值。

void*p1;
int*p2;
p1=p2;

但是却不能把void*指针赋值给任意指针类型,也不能直接对其解引用

例如:

void*p1;
int *p2;
//这是错误的赋值方式与解引用方式
p2=p1;
*p1

(2)NULL 是C语⾔中定义的⼀个标识符常量,值是0,地址也是0,这个地址是⽆法使⽤的。

int*p=NULL;//初始化指针

1.5 指针变量的大小

我们先运行以下代码

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{

	int num = 10;
	int* p = &num;

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

	printf("%u\n", sizeof(p));
	printf("%u\n", sizeof(pc));
	return 0;
}

结果为如下所示:

我们发现两种类型的指针变量大小都是8个字节,这是为什么呢?
首先我们要明白指针是用来干什么的?指针是为了存放地址,而地址的大小取决于存储一个地址需要多大的空间。
我们知道,现在常见的计算机分为32位机器64位机器。地址是由地址总线产生的,32位的机器有32根地址线,地址上传输过来的电信号转换成数字信号后,得到的32个0/1组成的序列就是地址。地址都是32个0/1组成的二进制序列的话,那么存放这个地址所需要的空间大小是4个字节。所以指针变量的大小都是4个字节。
同理64位机器,假设有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要8个字节的空间,指针变的大小就是8个字节。

输出结果:

32位机器:4 4

64位机器:8 8

二、指针变量的运算

2.1 指针+/-整数

我们先观察一下如下代码的地址变化

#include <stdio.h>
int main()
{
	int n = 10;
	char* p1 = (char*)&n;//将int*强转为char*
	int* p2 = &n;
	printf("%p\n", &n);
	printf("%p\n", p1);
	printf("%p\n", p1 + 1);//p1向后移动一位
	printf("%p\n", p2);
	printf("%p\n", p2 + 1);//p2向后移动一位
	return 0;
}

输出结果如下:

我们可以看出, char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。由此我们得出结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)。

因为每次代码运行时,系统都会重新分配内存,所以输出结果每次都不会一样,但是规律是一样的。

我们知道数组在内存中是连续存储的(地址由低到高),所以我们只需要首元素的地址就能顺藤摸瓜就能找到后面的所有元素。

代码如下:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

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

编译结果如下:

2.2 指针-指针

前提条件:两个指针指向同一块空间。
指针 - 指针得到的是两个指针之间元素的个数。

例如:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int my_strlen(char* str)
{
	char* start = str;
	while (*str != '\0')
	{
		str++;
	}
	return str - start;
}

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

输出结果为6

2.3 指针的关系运算

我们知道了指针变量本质是存放的地址,而地址本质就是十六进制的整数,所以指针变量也是可以比较大小的

前面我们通过循环的方式实现对数组的访问,而通过比较指针的大小,我们也可以实现对数组的访问,例如

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	//int* p=&arr[0];
	int* p = arr;//数组名就是首元素的地址
	while (p < arr + sz)//指针大小的比较
	{
		printf("%d ", *p);
		p++;
	}
	return 0;
}

编译结果如下:

三、const修饰指针

我们知道变量是可以改变的,但是在有些场景下,我们不希望变量改变,那我们该怎么办呢?这就是我们接下来要讲的const的作用啦。

3.1 指针修饰变量

简单来说,经过const修饰的变量,可以当做一个常量,而常量是不能改变的

	int a = 1;//a可修改的
	const int b = 2;
	b=3;//b不可修改的

但我们可以使用指针来修改:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

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

运行结果为10

通过代码我们发现,虽然const修饰变量n后,我们无法直接修改,但是当我们把变量的地址取出来,通过解引用操作*p=10,我们依然能够修改n的值。

这就好比我们发现门关了,就从窗户爬进去,这种爬窗户的行为显然是我们所不能接受的。

这显然也不合理,我们应该限制p也不能修改。要做到这种效果,需要我们用const来修饰指针。

3.2 指针修饰指针

我们知道const的作用后,就可以看看下面几段代码。

	int a = 10;
	const int* p = &a;
	*p = 20;//是否可以
	p = p + 1;//是否可以

通过测试我们发现,*p无法改变成20,但是p可以改变成p+1.

那如果把const调换一下位置,又会出现什么情况呢~

	int a = 10;
    int* const p = &a;
	*p = 20;//是否可以
	p = p + 1;//是否可以

再次测试之后我们发现,*p可以被赋值为20,但是p不能赋值为p+1了

通过上述测试,我们大致可以总结出两个结论。

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

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

四、assert断言

assert是一个宏,它的头文件为<assert.h>,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报错终⽌运⾏。这个宏常常被称为“断⾔”。

举一个简单的例子:

assert(a>0);
  1. 如果a的确大于0,assert判断为真,就会通过。

  2. 如果a不大于0,assert判断为假,就会报错。

所以assert常常用于检查空指针问题,以防止程序因为空指针的问题而出错。

int *p=NULL;
assert(p);//空指针是0,0为假,就会报错

但是assert() 也是有缺点的,因为引入了额外的检查,增加了程序的运行时间。

⼀般我们可以在debug中使用,在release版本中选择禁用assert就行,在VS这样的集成开发环境中,在release版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题,在release版本不影响用户使用时程序的效率。

五、传值调用与传址调用

5.1 传值调用

学习指针的目的是使用指针解决问题,那什么问题,非指针不可呢?
例如:写⼀个函数,交换两个整型变量的值
⼀番思考后,我们可能写出这样的代码:

#define _CRT_SECURE_NO_WARNINGS
#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函数内部,创建了a和b,地址分别如下所示:

名称类型
&a0x0000003c41d9fb34 {3}int *
&b0x0000003c41d9fb54 {4}int *
&x0x0000003c41d9fb10 {3}int *
&y0x0000003c41d9fb18 {4}int *

在调用Swap1函数时,将a和b传递给了Swap1函数,在Swap1函数内部创建了形参x和y接收a和b的值,x和y确实接收到了a和b的值,不过x的地址和a的地址不⼀样,y的地址和b的地址不⼀样,相当于x和y是独立的空间,那么在Swap1函数内部交换x和y的值,自然不会影响a和b。

当Swap1函数用结束后回到main函数,a和b的没法交换。Swap1函数在使用的时候,是把变量本身直接传递给了函数,这种调用函数的方式我们之前在函数的时候就知道了,这种叫传值调用。

总结:
因为形参只是实参的一份临时拷贝,对形参改变,根本不会改变实参

5.2 传址调用

那怎么办呢?
我们现在要解决的就是当调用Swap1函数的时候,Swap函数内部操作的就是main函数中的a和b,直接将a和b的值交换了。那么就可以使用指针,在main函数中将a和b的地址传递给Swap1函数,Swap函数里边通过地址间接的操作main函数中的a和b就好了。

修改后的代码:

#define _CRT_SECURE_NO_WARNINGS
#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);
	Swap2(&a, &b);
	printf("交换后:a=%d b=%d\n", a, b);
	return 0;
}

运行结果如下:

我们可以看到实现成Swap2的方式,顺利完成了任务,这⾥调用Swap2函数的时候是将变量的地址传递给了函数,这种函数调用方式叫:传址调用。 

六、野指针

6.1 野指针的成因

那野指针是怎么产生的呢?
一般来说,产生野指针原因有3种:1. 指针未初始化;2. 指针越界访问;3. 指针指向的空间释放

6.1.1 指针未初始化

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

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

编译器会发生报错:

6.1.2 指针越界访问

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

运行结果如下:、

 

6.1.3 指针指向的空间释放

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int* test()
{
	int n = 100;
	return &n;
}

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

运行结果如下:

 

est函数返回之后,确实把n的地址带回来放到p里边去了。但是有一个现象需要我们注意,这个n是个局部变量,进入函数创建,出函数就销毁了。也即是进入函数后存放n的4个字节拿到了,出函数就还给操作系统了,但是p中还存放着这个地址。如果通过解引用操作给它赋值20,将20这个值放到n里面去,这样就非常危险。

6.2 如何规避野指针的出现

6.2.1 初始化

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

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

初始化如下:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
	int num = 10;
	int*p1 = &num;
	int*p2 = NULL;

	return 0;
}

6.2.2 小心越界访问

⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。要解决越界访问,只能小伙伴们自己去解决,多敲代码。

6.2.3 不访问临时变量的地址

临时变量出了作用域就会销毁,系统会回收该空间,所以我们要尽量避免指针指向已经销毁的空间,尤其在函数中,不能返回临时变量的地址。

  • 70
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: "C语言指针详解.pdf" 是一份详细介绍C语言指针概念和使用的PDF文档。C语言中,指针是一种特殊的变量类型,用于存储其他变量的内存地址。 该PDF文档首先详细介绍了指针的定义和声明。指针的声明需要指定指针变量的类型和名称,并使用星号(*)来表示该变量是一个指针指针变量名的前面加上一个星号,可以获取所指向的变量的值,这被称为"解引用"。 文档还介绍了指针的运算。指针可以进行自增和自减运算,指针之间可以进行相减操作,返回的结果表示它们之间的距离或者偏移量。此外,还可以将指针赋值给另一个指针,或者将指针赋值给一个变量,反之亦然。 除了基本的指针概念,文档还详细介绍了指针的常见应用场景。这包括指针作为函数参数,用于在函数内部对传入的变量进行修改。还有通过指针来实现动态内存分配和释放,以及使用指针实现数据结构(如链表和树)等。 此外,该文档还包含一些常见的指针错误和问题的解决方案。这些错误包括空指针引用、野指针引用以及内存泄漏等。文档指出了这些错误的影响以及如何避免它们。 总的来说,"C语言指针详解.pdf" 是一份详细介绍C语言指针概念、使用和常见问题解决方案的文档,对于学习和理解C语言指针的人们是一份宝贵的资料。 ### 回答2: 《C语言指针详解.pdf》是一本关于C语言指针的详细解析的电子书。在这本书中,作者详细介绍了C语言指针的概念、用途和基本语法。 首先,指针C语言中非常重要的概念,它是一种数据类型,用于存储和操作内存地址。指针可以指向各种数据类型,如整数、字符、数组和结构体等。 在《C语言指针详解.pdf》中,作者详细讲解了指针的声明和初始化,以及如何通过指针来访问和修改变量的值。作者还介绍了指针与数组的关系,以及指针和函数之间的关联。 此外,书中还涵盖了指针的高级应用,如指针的算术运算、指向指针指针指针数组等。作者通过丰富的例子和代码来帮助读者理解这些概念和技巧。 《C语言指针详解.pdf》不仅适合C语言初学者,也适合有一定编程基础的读者。通过阅读此书,读者将能够更深入地理解C语言指针的功能和用法,掌握指针在编程中的灵活运用。 总之,《C语言指针详解.pdf》是一本内容详尽且易于理解的C语言指针教程。读者通过阅读此书,可以提高自己在C语言编程中的指针应用能力,从而更好地实现程序的设计和开发。 ### 回答3: 《C语言指针详解.pdf》是一本介绍C语言指针概念和使用方法的详细手册。C语言中的指针是一种非常重要和特殊的数据类型,它提供了直接访问内存地址的能力,使得C语言具有了更高的灵活性和效率。 这本手册首先会介绍指针的基本概念,包括指针变量的定义和声明、指针的初始化和赋值。它会详细讲解指针和变量之间的关系,以及指针的运算规则和使用方法。读者可以学习到如何通过指针操作变量的值和地址,以及如何利用指针实现函数的参数传递和返回值。 接下来,手册会介绍指针和数组之间的关系。C语言中,数组名本质上是一个指向数组首元素的常量指针,因此可以通过指针来操作数组。手册将详细讲解指针和数组的指针算术运算,以及指针和多维数组的关系。 此外,手册还会介绍指针和字符串之间的关系。C语言中,字符串本质上是以空字符结尾的字符数组,可以通过指针来操作字符串。手册将详细讲解指针和字符串的操作,包括字符串的输入输出、字符串的比较和拷贝。 最后,手册还会介绍指针和结构体之间的关系。C语言中,结构体是用户自定义的复合数据类型,可以通过指针来操作结构体。手册将详细讲解指针和结构体的操作,包括结构体指针的定义和使用,以及结构体指针作为函数参数的传递方式。 总之,《C语言指针详解.pdf》是一本深入浅出的指针教程,对于想更深入理解C语言指针的读者来说,是一本非常实用的参考书。无论是初学者还是有一定基础的读者,都可以从中获得很多宝贵的知识和技巧。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值