C语言指针(下):一篇文章让你秒懂基础!(5000字)

🎈个人主页:JAMES别扣了

💕在校大学生一枚。对IT有着极其浓厚的兴趣
✨系列专栏目前为C语言初阶、后续会更新c语言的学习方法以及c题目分享.
😍希望我的文章对大家有着不一样的帮助,欢迎大家关注我,我也会回关,大家一起交流一起互动,感谢大家的多多支持哈!

🎉欢迎 👍点赞✍评论⭐收藏

目录

前言

 1.void* 指针

2.const修饰指针

2.1const修饰指针变量

3.野指针

3.1指针未初始化

3.2 指针越界访问

3.3指针指向的空间释放

4.如何规避野指针

4.1指针初始化

4.2⼩⼼指针越界

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

5.assert断言

6.指针的使⽤和传址调⽤

6.1strlen的模拟实现

6.2传值调⽤和传址调⽤


我会持续更新C语言系列知识,如果你觉得对你有帮助,🎉欢迎 👍点赞✍评论⭐收藏。我会一一拜访,回关。

 

前言

 经过上篇文章的指针基础学习,我们明白指针的基础相关概念,同时也明白了如何基本的使用指针,以及一些常见的操作符的使用和指针的运算!接下来请跟进步伐,让我们更加深入了解指针的魅力!

往期文章:C语言指针(上):一篇文章让你秒懂基础! 

CSDN 

 1.void* 指针

在指针类型中有⼀种特殊的类型是 void* 类型的,可以理解为⽆具体类型的指针(或者叫泛型指 针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, ⾏指针的+-整数和解引⽤的运算。 举例:

#include <stdio.h>
 int main()
 {
 int a = 10;
 int* pa = &a;
 char* pc = &a;
 return 0;
 }

在上⾯的代码中,将⼀个int类型的变量的地址赋值给⼀个char*类型的指针变量。编译器给出了⼀个警 告(如下图),是因为类型不兼容。⽽使⽤void*类型就不会有这样的问题。

#include <stdio.h>
 int main()
 {
 int a = 10;
 void* pa = &a;
 void* pc = &a;
 *pa = 10;
 *pc = 0;
 return 0;
 }

 

这⾥我们可以看到, 那么 v void* 类型的指针可以接收不同类型的地址,但是⽆法直接进⾏指针运算。

2.const修饰指针

变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量的也可以修改这个变量。 但是如果我们希望⼀个变量加上⼀些限制,不能被修改,怎么做呢?这就是const的作⽤。

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

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

#include <stdio.h>
 int main()
 {
 const int n = 0;
 printf("n = %d\n", n);
 int*p = &n;
 *p = 20;
 printf("n = %d\n", n);
return 0;
 }

我们可以看到这⾥⼀个确实修改了,但是我们还是要思考⼀下,为什么n要被const修饰呢?就是为了 不能被修改,如果p拿到n的地址就能修改n,这样就打破了const的限制,这是不合理的,所以应该让 p拿到n的地址也不能修改n,那接下来怎么做呢?

2.1const修饰指针变量

#include <stdio.h>
 //
代码
1 
void test1()
 {
 int n = 10;
 int m = 20;
 int *p = &n;
 *p = 20;//ok?
 p = &m; //ok?
 }
 void test2()
 {//代码2
int n = 10;
 int m = 20;
 const int* p = &n;
 *p = 20;//ok?
 p = &m; //ok?
 }
 void test3()
 {
 int n = 10;
 int m = 20;
 int *const p = &n;
*p = 20; //ok?
p = &m;  //ok?
 }

 void test4()
 {
 int n = 10;
 int m = 20;
 int const * const p = &n;
 *p = 20; //ok?
 p = &m;  //ok?
 }
 int main()
 { //测试⽆const修饰的情况
 
test1(); //测试const放在*的左边情况
 
test2(); //测试const放在*的右边情况 
test3();//测试*的左右两边都有const 
test4();
 return 0;
 }

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

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

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

3.野指针

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

3.1指针未初始化

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

3.2 指针越界访问

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

3.3指针指向的空间释放

#include <stdio.h>
 int* test()
 {
 int n = 100;
 return &n;
 }
 int main()
 {
 int*p = test();
 printf("%d\n", *p);
 return 0;
 }

4.如何规避野指针

4.1指针初始化

如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL. NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址 会报错。

#include <stdio.h>
 int main()
 {
 int num = 10;
 int*p1 = &num;
 int*p2 = NULL;
 return 0;
 }

4.2⼩⼼指针越界

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

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

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

我们可以把野指针想象成野狗,野狗放任不管是⾮常危险的,所以我们可以找⼀棵树把野狗拴起来, 就相对安全了,给指针变量及时赋值为NULL,其实就类似把野狗栓前来,就是把野指针暂时管理起 来。

不过野狗即使拴起来我们也要绕着⾛,不能去挑逗野狗,有点危险;对于指针也是,在使⽤之前,我 们也要判断是否为NULL,看看是不是被拴起来起来的野狗,如果是不能直接使⽤,如果不是我们再去 使⽤。

int main()
 {
 int arr[10] = {1,2,3,4,5,6,7,8,9,10};
 int *p = &arr[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;
 }

5.assert断言

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

assert(p != NULL);

上⾯代码在程序运⾏到这⼀⾏语句时,验证变量 继续运⾏,否则就会终⽌运⾏,并且给出报错信息提⽰。

p 是否等于 NULL 。如果确实不等于 NULL ,程序 assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零), 任何作⽤,程序继续运⾏。如果该表达式为假(返回值为零), 流 st assert() 不会产⽣ assert() 就会报错,在标准错误 derr 中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号。

assert() 的使⽤对程序员是⾮常友好的,使⽤ assert() 有⼏个好处:它不仅能⾃动标识⽂件和 出问题的⾏号,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问 题,不需要再做断⾔,就在 #include 语句的前⾯,定义⼀个宏 


 #define NDEBUG
 #include <assert.h>

然后,重新编译程序,编译器就会禁⽤⽂件中所有的 除这条 assert() 语句。如果程序⼜出现问题,可以移 #define NDBUG 指令(或者把它注释掉),再次编译,这样就重新启⽤了 比特就业课 assert() 语 句。 

assert() 的缺点是 :因为引⼊了额外的检查,增加了程序的运⾏时间。

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

6.指针的使⽤和传址调⽤

6.1strlen的模拟实现

库函数strlen的功能是求字符串⻓度,统计的是字符串中\0 之前的字符的个数

函数原型如下:


 size_t strlen ( const char * str );

参数str接收⼀个字符串的起始地址,然后开始统计字符串中 \0 之前的字符个数,最终返回⻓度。 如果要模拟实现只要从起始地址开始向后逐个字符的遍历,只要不是 到 \0 就停⽌。 参考代码如下:

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

6.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);
 Swap1(a, b);
 printf("交换前:a=%d b=%d\n", a, b);
 printf("交换后:a=%d b=%d\n", a, b);
 return 0;
 }

我们发现其实没产⽣交换的效果,这是为什么呢? 调试⼀下,试试呢?

 我们发现在main函数内部,创建了a和b,a的地址是0x00cffdd0,b的地址是0x00cffdc4,在调⽤ Swap1函数时,将a和b传递给了Swap1函数,在Swap1函数内部创建了形参x和y接收a和b的值,但是 x的地址是0x00cffcec,y的地址是0x00cffcf0,x和y确实接收到了a和b的值,不过x的地址和a的地址不 ⼀样,y的地址和b的地址不⼀样,相当于x和y是独⽴的空间,那么在Swap1函数内部交换x和y的值, ⾃然不会影响a和b,当Swap1函数调⽤结束后回到main函数,a和b的没法交换。Swap1函数在使⽤ 的时候,是把变量本⾝直接传递给了函数,这种调⽤函数的⽅式我们之前在函数的时候就知道了,这 种叫传值调⽤。

结论:实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实 参。 所以Swap是失败的了。

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

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

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

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

总结

本文深入探讨了C语言中的几个重要概念:void指针、const修饰符、野指针、传值和传址,以及assert断言。文章首先解释了void指针的用途和限制,然后详细阐述了const修饰符如何用于保护变量的值不被修改。接着,文章探讨了野指针的成因和如何避免野指针的出现。在传值和传址部分,文章对比了两种参数传递方式的差异和适用场景。最后,文章介绍了assert断言在调试和错误处理中的应用。

评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值