深入了解指针(一)

1. 内存和地址

计算机上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中。
把内存划分为一个个的内存单元,每个内存单元的大小取1个字节。1个字节空间存放8个比特位。
每个内存单元都有一个编号,在计算机中 把内存单元的编号也称为地址
C语言中给地址起名为指针
可以理解为:内存单元的编号 == 地址 == 指针
在这里插入图片描述

2. 指针变量和地址

2.1 取地址操作符 &

在C语言中创建变量其实就是向内存申请空间
在这里插入图片描述
创建了变量n,向内存申请了4个字节大小的空间,用来存放1。

要取出n的地址,需要使用取地址操作符 &
请添加图片描述
实际上只会打印一个地址,可是int类型有4个字节,应该有4个地址。
这是因为,我们只要知道了第一个字节地址,顺藤摸瓜访问到4个字节的数据也是可以的。

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

2.2.1 指针变量

通过取地址操作符拿到一个地址,比如:0x006FFD70。这个值要存放在指针变量中。
指针变量也是一种变量,专门来存放地址的,存放在指针变量中的值都会理解为地址。
在这里插入图片描述
两个打印结果是相同的,说明 pn 和 &n 里存放的值是相等的,存放的都是n的地址。

2.2.2 拆解指针类型

pn是变量名,pn的类型是 int* ,要怎么理解指针变量?
请添加图片描述
int* 的 * 说明pn是指针变量,int* 的int 说明pn指向的是int类型的对象。

再比如:

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

2.2.3 解引用操作符(间接访问操作符) *

只要拿到了地址(指针),就可以通过指针找到指针指向的对象
在这里插入图片描述
n = 1,没有去修改n的值,但是最后打印出来的是100,说明有个操作访问了n,并将n的修改了。
*pn的意思是 通过地址pn找到地址指向的对象n, *pn就是n变量。 *pn = 100就是n = 100,所以最后打印的是100。

可能会有人觉得直接 n = 100就好了,但是这样写代码会更加灵活,多了一种途径。

2.3 指针变量的大小

32位机器(64位机器)假设有32(64)根地址总线,每根地址总线出来的电信号转换成数字信号后是1或0。把32(64)根地址总线产生的2进制序列当作一个地址,那么一个地址就是32(64)个比特位,需要4(8)个字节才能存储。

指针变量的大小取决于地址大小
x64——64位环境
x86——32位环境
请添加图片描述
请添加图片描述
32位平台下地址是32个bit位,指针变量的大小是4个字节
64位平台下地址是64个bit位,指针变量的大小是8个字节
注:
注意指针变量的大小和类型无关,只要指针类型的变量,在相同的平台下,大小都是相同的。

3. 指针变量类型的意义

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

3.1 指针的解引用

先观察以下代码
请添加图片描述
可以看到代码一将n的4个字节全部该为0,但是代码二只是将m的一个字节改为0。

代码一
请添加图片描述

请添加图片描述

代码二
请添加图片描述
请添加图片描述
结论
指针的类型决定了,对指针解引用的时候有多大的权限(一次能操作几个字节)。
比如:char* 的指针解引用就只能访问1个字节,而int* 的指针解引用就能访问4个字节。

3.2 指针 ± 指针

在这里插入图片描述
可以看出,char类型的指针变量+1跳过一个字节,int 类型的指针变量+1跳过四个字节。
结论:指针的类型决定了指针向前或者向后走一步有多大(距离)。

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修饰变量

变量是可以修改的,通过指针变量的也可以修改这个变量。
但const可以加上一些限制,不能被修改。

#include <stdio.h>

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

上述代码中n是不能被修改的,其实n的本质是变量,只不过被const修饰后(常属性),在语法上加了限制。导致无法直接修改n。

但是如果绕过n,使用n的地址,去修改n就可以做到了,虽然这样做是在打破语法规则。

#include <stdio.h>

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

这样就打破了语法规则,这是不合理的。所以应该让p拿到的n的地址也不能修改n,这就要 const修饰指针变量

4.2 const修饰指针变量

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

int* p;  

int const * p;  //const放在*的左边做修饰
const int * p;  //const在*的左边就行

int * const p;  //const放在*的右边做修饰

下面看几段代码,分析一下

#include <stdio.h>

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

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

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

5. 指针运算

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

5.1 指针 ± 整数

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

#include <stdio.h>

int main()
{
	//    数组      1  2  3  4  5  6  7  8  9  10
	//    下标      0  1  2  3  4  5  6  7  8   9
	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));
		//printf("%d ", p[i]);
		//可以写*(p+i)或p[i],是一样的
	return 0;
}

在这里插入图片描述

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* p = &arr[0];
	int sz = sizeof(arr) / sizeof(arr[0]);

	//指针的大小比较
	while (p < arr + sz)   //arr + sz 相当于 &arr[sz]
	{
		printf("%d ", *p);
		p++;
	}
	return 0;
}

在这里插入图片描述

6. 野指针

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

6.1 野指针成因

  1. 指针未初始化
#include <stdio.h>

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

在这里插入图片描述

  1. 指针越界访问
#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;
	for (i = 0; i <= 11; i++)
		printf("%d ", arr[i]);
	return 0;
}

请添加图片描述

  1. 指针指向的空间释放
#include <stdio.h>

int* test()
{
	int n = 10;
	return &n;
}
int main()
{
	int* p = test();   //非法访问
	printf("%d\n", *p);
	return 0;
}

n在 test() 函数结束后就销毁了,非法访问

6.2 如何规避野指针

6.2.1 指针初始化

如果不知道指针应该指向哪里,可以给指针赋值NULL
NULL是C语言中定义的一个标识符常量,值是00也是地址,这个地址无法直接使用,读写该地址会报错。

#ifdef __cplusplus
	 #define NULL 0
#else
	#define NULL ((void *) 0 )
#endif

初始化如下:

#include <stdio.h>

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

6.2.2 小心数组越界

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

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

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

#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;
	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. assert断言

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

assert(p != NULL);

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

assert()宏接受一个表达式作为参数。如果该表达式为假(返回值为零),assert()就会报错,在标准错误流stderr中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号。

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

#define NDEBUG
#include <assert.h>

然后,重新编译程序,编译器就会禁用文件中所有的assert()语句。

assert()的缺点是,因为引入了额外的检查,增加了程序的运行时间
一般可以在 Debug中使用,在 Release版本中选择禁用 assert就行,在VS这样的集成开发环境中,在 Release版本中,直接优化掉了。
这样在debug版本写有利于程序员排查问题,在release版本中不影响用户使用时程序的效率。

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

8.1 strlen的模拟实现

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

size_t strlen(const char* str);
//const可以修饰指针本身或指针所指向的变量
//分别表示指针的指向不可变或
//指针指向的变量值不可变

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

如果要模拟实现,只要从起始地址开始向后逐个字符的遍历,只要不是 \0字符,计数器就+1,直到 \0就停止。

#include <stdio.h>
#include <assert.h>

int my_strlen(const char* str)
{
	int count = 0;   //计数器
	assert(str);    //assert() 断言宏
	while (*str)    //判断是不是 \0
	{
		count++;   //没遇到 \0,计数器+1
		str++;    //指针向后走一位
	}
	return count;  //返回计数器
}
int main()
{
	int len = my_strlen("abcdef");
	printf("%d\n", len);
	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 = 1;
	int b = 2;
	printf("交换前: a = %d b = %d\n", a, b);
	Swap1(a, b);   //传值调用
	printf("交换后: a = %d b = %d\n", a, b);
	return 0;
}

运行后
在这里插入图片描述

并没有产生交换的效果,为什么呢?
调试一下

请添加图片描述
请添加图片描述
可以发现a、b和x、y的地址各不相同,a和b的值没有交换,x和y的值交换了。

我们在 main()函数内部创建了a和b,a的地址为0x000000512beff694,b的地址为0x000000512beff6b4,
将a和b的 传递给Swap1函数,在 Swap1函数内部创建了 形参x和y接收a和b的值,
但x的地址为0x000000512beff670,y的地址为0x000000512beff678,
x和y确实接收了a和b的值,不过a和x、b和y的地址不相同, 相当于x和y是独立的空间,在Swap1函数内部交换x和y的值自然不会影响a和b。

把变量本身直接传给函数,这种叫 传值调用

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

8.2.2 传址调用

可以将a和b的地址传递给Swap1函数,Swap1函数通过地址间接的操作main()函数中a和b,并达到交换的效果就好了。

#include <stdio.h>


void Swap1(int* px, int* py)
{
	int tmp = *px;
	*px = *py;
	*py = tmp;
}
int main()
{
	int a = 1;
	int b = 2;
	printf("交换前: a = %d b = %d\n", a, b);
	Swap1(&a, &b);   //传址调用
	printf("交换后: a = %d b = %d\n", a, b);
	return 0;
}

输出结果:
在这里插入图片描述

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值