Bootstrap

轻松拿下指针(2)

文章目录



提示:以下是本篇文章正文内容,下面案例可供参考

一、const修饰指针

1.1 const修饰变量

  首先先让大家认识一下什么是const。

(1)我们知道,变量是可以修改的,如果把变量的地址交给一个指针变量,通过指针变量也可以修改这个变量,但是如果我们想给这个变量一些限制,使其不能被修改,我们就可以加上 const :

#include <stdio.h>
int main()
{
 int m = 0;
 m = 100;//m是可以修改的
 const int n = 0;
 n = 200;//n是不能被修改的
 return 0;
}

(注意,一旦const修饰了变量 m 使其具有了常量的属性并且不能改变,这就是const的作用。但是被修饰的变量本质上还是变量,只是不能被修改。)这里再给大家举个例子:

  (2)但是如果我们绕过n,使⽤n的地址,去修改n就能做到了,虽然这样做是在打破语法规则。

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

好比我们有一个房间,现在把房间的门锁上了,我们还可以通过翻窗户进去,而上面的代码通过使用 n 的地址去修改 n 就是从窗户翻进房间里。但是往往这种行为会被发现或者说是违法行为,我把房间锁上本来就是不想让别人进去,而有人非要翻窗户进去,意思就是既然 const 修饰了 n ,就是不想修改 n 的 值,如果 p 拿到 n 的地址就能修改 n ,这样就打破了 const 的限制,这是很不合理的,所以我们应该让 p 的地址也不能修改 n (把窗户也锁上):

int main()
{
	int n = 10;
	int m = 100;

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


    int * const p = &n;
	//const 修饰指针变量
	//放在*的右边,限制的是指针变量本身,指针不能改变它的指向
	//但是可以通过指针变量修改它所指向的内容
	
    *p = 20;//err
	p = &m;//ok

	return 0;
}

二、指针运算

指针的基本运算有三种:

指针+- 整数
指针-指针
指针的关系运算
2.1 指针+-整数

  在数组学习中我们知道元素是通过下标访问的,所以要打印数组中的元素是可以通过下标访问的方法打印出一组数组里面的元素:

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	//              0 1 2 3 4 5 6 7 8 9
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);

	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}

	return 0;
}

 在指针(1)的学习中提出到,数组在内存中是连续存放的,只要知道第一个元素的地址,顺藤摸瓜就能找到后面的所有元素,而现在我们就可以用这种方法来打印数组的元素:

#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;

#1.指针类型决定了指针+1的步长,决定了指针解引用的权限(p+i 是跳过 i * sizeof(int)个字节);

#2.数组在内存中是连续存放的。

2.2 指针-指针
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	printf("%d\n", &arr[9] - &arr[0]);
	printf("%d\n", &arr[0] - &arr[9]);

	return 0;
}

#1.指针-指针的绝对值是指针和指针之间元素的个数;

#2.指针-指针,计算的的前提条件是两个指针指向的是同一个空间

这里我们举个例来展示指针-指针的作用:

//写一个函数,求字符串的长度//

#include<stdio.h>
#include<string.h>

//size_t 是一种无符号整型
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);

	return 0;
}

(注意,strlen其实统计的是字符串中\0之前的字符个数;\0 == 0)

2.3 指针的关系运算

关系运算其实就是比较大小!

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];
	while (p<&arr[sz])
	{
		printf("%d ", *p);
		p++;
	}
	return 0;
}

根据代码就可以很直观的理解成,只要指针变量 p 小于 &arr[9] 就一直++直到跳出循环。

三、野指针

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

3.1野指针的成因

(1)指针未初始化

(#一个局部变量不初始化的话,它的值是随机值,如果在vs编译器上编写的话会报错

   #int*p,p是局部变量,但是没有初始化,其值是随机值,如果将p存放的地址当作地址,解引用操作符就会形成非法访问!)

(2)指针越界访问

(这里的异常:表示存在缓冲区溢出问题。这意味着您正在向数组(如'arr')中写入的数据超过了其容量,导致内存损坏。

(3)指针指向的空间释放

(看似这段代码没有错误,注意这里有一个警告:“返回局部变量或者临时变量的地址:n”。意思是 n 是在 test()中创建的一个局部变量,当 n 传出 test () 后就不能使用了,p 就会形成非法访问,所以 p 就是一个野指针!!)

3.2 如何规避野指针
如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL. NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址会报错。初始化如下:
#include <stdio.h>
int main()
{
 int num = 10;
 int*p1 = &num;
 int*p2 = NULL;
 
 return 0;
}

(#⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是 越界访问。指针变量不再使用时,及时置NULL,指针使用之前检查有效性)

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

四、assert断言

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

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

(如果已经确定程序没有问题,不需要在做断言,就在#include <assert.h> 语句的前面,定义一个宏NDDEBUG。然后重新编译程序,编译器就会禁用文件中所有的assert()语句。如果程序又出现问题,就可以注释掉#define NDEBUG 指令,再次编译。)

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

5.1 strlen的模拟实现

在上述2.2中我们提到写一个求字符串的函数,在了解了上述的知识后我们可以对代码进行一个完善和改进:

#include <assert.h>

//求字符串长度
//参数s指向的字符串不期望被修改
size_t my_strlen(const char* s)
{
	size_t count = 0;
	assert(s != NULL);//检测指针s是否有效
	while (*s)
	{
		count++;
		s++;
	}
	return count;
}

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

其中利用了assert()来检测指针s是否为空值或者是否有效,以及使这个代码更具有健壮性(鲁棒性https://baike.baidu.com/item/%E9%B2%81%E6%A3%92%E6%80%A7/832302 ),使参数s所指向的字符串不期望被修改。

5.2 传值调用和传址调用

首先我们先写⼀个函数,交换两个整型变量的值。

看到这个题目我们立马就能写出一个这样的代码:

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

大致的思路就是创建一个新的变量,然后对两个整型变量进行交换,但是我们运行一下看看:

  结果发现其实并没有产生交换的结果,所以我们就要思考既然我们学习了指针,就要使用指针解决问题,有什么问题是非指针解决不可的呢?

此时我们可以对上面的代码调试监控一下,看看是那里出现了差错:

  我们发现形参 x ,y 的地址和实参 a,b 的地址竟然是不一样的,

(1)说明了在代码执行的时候,在main函数创建了int a ,int b,他们都有各自的地址。

(2)接下来要执行Swap函数的时候,x的地址和a的地址不一样,y的地址和b的地址不一样,当

所以在传参的时候把a的值传给了x,把b的值传给了y,此时又创建了一个变量tmp,然后进行交换,交换的只是x和y的值,不会影响a和b的值的变化,a ,b只是把值传给了 x 和 y。所以当Swap函数执行完毕返回main函数时,a 和 b依旧是20 ,30。

  #所以可以得到一个结论:

“当实参传递给形参的时候,形参是实参的一份临时拷贝,对形参的修改不会影响实参!”

  所以我们就可以利用指针变量了,在main函数中将a和b的地址传递给Swap函数,Swap函数里边通过地址间接的操作main函数中的a和b,达到交换的效果:

void Swap2(int* pa, int* pb)
{
    int 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;
}

  (1)我们可以看到实现成Swap2的方式,顺利完成了任务,这⾥调⽤Swap2函数的时候是将变量的地址传递给了函数,这种函数调用方式叫:传址调用。
  (2)传址调用,可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量;
 
  (3)所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采⽤传值调用。如果函数内部要修改主调函数中的变量的值,就需要传址调用。

总结

  本章内容又是对指针的一部分内容进行了详细的解读和运用,往后的学习会更加精彩丰富,希望对大家有帮助,谢谢大家!!!

;