【C语言】深入理解指针(上)

请添加图片描述
主页链接: LSR的主页
专栏链接: 《C语言》


前言

以下是对指针的见解,希望可以帮助到你。


一、指针变量和地址

1.取地址操作符(&)

顾名思义,&就是用来取地址的,示例代码如下:

#include <stdio.h>
int main()
{
 int a = 10;//在内存中开辟一块空间
 &a;//取出a的地址
 printf("%p\n", &a);
 return 0;
}
}

说明:a在内存中开辟一块空间,因为是int类型,所以占用4个字节,而这四个字节在内存中一定是连续的,而&a所取出来的地址就是最小的(首地址)地址。


示例:

//假设上述a的地址是:
0x006FFD70
0x006FFD71
0x006FFD72
0x006FFD73
//printf打印结果就为0x006FFD70

提醒:地址用%p打印。


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

2.1 指针变量

指针变量的作用和变量相似,本质就是用来储存地址的变量,示例代码如下:

#include <stdio.h>
int main()
{
 int a = 10;
 int* pa = &a;//取出a的地址并存储到指针变量pa中
 
 return 0;

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

先看代码:

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

pa的意思就是通过pa中存放的地址找到指向的空间,即a,所以pa = 0,就是将a改为了0。
可以把*操作符看作一把万能钥匙,只要你能给我地址,我就能根据地址打开你想要的那个变量的门。


2.3指针变量的大小

指针变量是用来储存地址的,所以指针变量的大小本质上就是地址的大小,这里引入一个概念,32位机器有32根地址总线,那我们把32根地址线产生的2进制序列当做一个地址,那么一个地址就是32个bit位,根据1byte = 8bit可知需要4个字节才能储存
同理,64位机器,假设有64根地址线,那就需要8个字节。
结论:
32位平台下地址为32gebit位,指针变量大小是4个字节。
64位平台下地址为64gebit位,指针变量大小是8个字节。
值得注意的是,指针变量大小和类型无关,相同平台下大小都相同。


二、指针变量类型的意义

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

1.指针±整数

来看一段代码:

#include <stdio.h>
int main()
{
 int n = 10;
 char *pc = (char*)&n;//强制类型转换
 int *pi = &n;
 
 printf("%p\n", &n);
 printf("%p\n", pc);
 printf("%p\n", pc+1);
 printf("%p\n", pi);
 printf("%p\n", pi+1);
 return 0;
}
//运行结果如下:
&n     = 00AFF974
pc     = 00AFF974
pc + 1 = 00AFF975
pi     = 00AFF974
pi + 1 = 00AFF978

可以看到,char类型的指针+1跳过1个字节,int类型的指针变量+1跳过了4个字节。
结论:指针类型决定了指针向前或向后操作时一步的距离。


2.void* 指针

这种类型的指针被称作泛指型指针(无具体类型),可以用来接收任意类型的地址。
局限性:不能直接进行指针的±整数和解引用操作

#include<stdio.h>

int main()
{
     int a = 10;
     int* pa = &a;
     char* pb = &a;
     void* pc = &a;
     *pa = 100;
     *pc = 0;
     return 0;
}

说明:char接收时会报错而void 不会,但是void*赋值时也仍会报错。


三、const修饰指针

1.const修饰变量

我们知道,无论是变量还是指针变量,都是可以通过赋值修改的,而const的作用就是给变量加上限制,使其不能被修改。示例如下:

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

2.const修饰指针变量

#include<stdio.h>
void test1()
{   
      int n = 10;
      int m = 20;
      const int* p = &n;
      *p = 20;
      p = &m;
}
void test2()
{   
      int n = 10;
      int m = 20;
      int* const p = &n;
      *p = 20;
      p = &m;
}
void test3()
{   
      int n = 10;
      int m = 20;
      const int* const p = &n;
      *p = 20;
      p = &m;
}    
int main()
{
      //测试const在*左边的情况
      test1();
      //测试const在*右边的情况
      test2();
      //测试const在两边的情况
      test3();
      return 0;
}

结论:
const修饰p,那么p指向的变量的内容就无法修改,p本身所存地址可以修改;
const修饰p, 那么p所存的地址就无法修改,而*p指向的变量的内容可以修改。


四、指针运算

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

1.指针±整数

指针打印数组每个值示例代码:

incldue<stdio.h>
int main()
{
      itn arr[] = {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));//指针+-整数
      }
      return 0;
}

数组在内存中是连续存放的,所以只要知道首元素的地址,存入指针变量,再通过指针±整数就可以找到所有的元素。


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

说明:指针-指针,就是两个地址之间的计算,而计算结果就是两个指针之间的字节大小。


3.指针的关系运算

同样以打印数组为例:

incldue<stdio.h>
int main()
{
      itn arr[] = {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;
}

五、野指针

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

1.野指针的成因

1.1指针未初始化

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

1.2指针越界访问

#include<stdio.h>
int main()
{
     int arr[] = {0};
     int* p = &arr[0];
     int i = 0;
     for(i = 0;i<=11;i++)
     {
            //指针指向的范围最终会超出数组arr的范围,p就为野指针
            *(++p) = i;
     }
     return 0;
}

1.3指针指向的空间释放

#include<stdio.h>
int* test()
{
     int n = 100;
     return &n;
}
     
int main()
{
     int* p = test();
     printf("%d", *p);//因为调用结束后,函数空间释放,那么*p就无权限访问n的空间。
     return 0;
}

2.如何避免野指针的出现

2.1指针初始化

如果明确知道指针指向的地址,就直接以地址进行初始化,如果暂时不知道,可以给指针赋值NULL(值为0),但是NULL不能直接读写,会报错。

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

2.2避免指针越界

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


2.3指针使用前检查其有效性

如果指针已经越界或者不再使用时,我们可以直接置NULL,而我们使用指针前,也要确保指针不为NULL,因为NULL不能被读写访问。


2.3不要返回无效地址

如造成野指针的第三个例子,返回一些局部变量的地址也会造成野指针的产生。


六、assert断言

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

assert(p != NULL);

代码在运行到这一行语句的时候,就会验证指针变量p是否为NULL,如果不等于,程序就会继续运行,否则就会终止运行,并标出出现问题的行号。
原理:assert()宏接受一个表达式作为参数,表达式为真,程序继续进行;表达式为假,assert()就会报错。


使用assert():
如果确认程序没有问题,不需要再做断言,那么可以在#incldue<stdio.h>前定义一个宏NDEBUG

#define NDEBUG
#include<assert.h>

如此一来,编译器就会禁用文件中所有的assert()语句。而当程序又出现问题,可以移除(注释)#define NDEBUG指令,就可以重新启用assert()语句。

assert的缺点也很明显:
因为引入了额外的检查,所以会增加程序的运行时间。
所以一般我们可以在Debug中使用,而在Release版本中选择禁用assert,这样在VS这样的集成开发环境中,在Release版本下可以直接优化。
这样在Debug版本有利于我们排查问题,而在Release版本下不影响用户使用时程序的效率。


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

1.strlen的模拟实现

库函数strlen()的功能是求字符串的长度,统计的是字符串中‘\0’之前的字符的个数。
函数原型:

size_t strlen(const char* str);

模拟实现strlen:

int my_strlen(const char * str)
{
 int count = 0;
 assert(str != NULL);//判断传入的指针是否为空,为空就中断程序
 while(*str != '\0')
 {
 count++;
 str++;
 }
 return count;
}
int main()
{
 int len = my_strlen("abcdef");
 printf("%d\n", len);
 return 0;
}

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

但是遗憾的是,输出结果为:

10 20 
交换前: a = 10 b = 20
交换后: a = 10 b = 20

可以发现,这样并未产生交换的效果。调试之后发现,a和b的地址并不会随着实参的传递传递给形参,而形参x和y会重新创建一份临时空间来储存a和b的值,而地址和a和b是截然不同的,所以本质上只有x和y发生了交换,而我们的a和b并未发生交换,这个就叫传值调用

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


解决办法:
我们可以通过指针的形式将a和b的地址传入Swap函数内部,Swap函数里面通过地址间接的操作main函数中的a和b,并达到交换的目的。

#include <stdio.h>
void Swap1(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;
}

输出结果为:

10 20
交换前: a = 10 b = 20
交换后: a = 20 b = 10

这里我们是将变量的地址传递给了函数,这种函数调用方式就叫做:传址调用


以上就是初入指针的相关内容,希望对你有所帮助。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值