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

简介: 深入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;//指向一个双精度浮点数的指针


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

1. *p1 = 4;
2. *p2 = 'a';
3. *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修饰的变量,可以当做一个常量,而常量是不能改变的

1.  int a = 1;//a可修改的
2.  const int b = 2;
3.  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的作用后,就可以看看下面几段代码。

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

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

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

1.  int a = 10;
2. int* const p = &a;
3.  *p = 20;//是否可以
4.  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常常用于检查空指针问题,以防止程序因为空指针的问题而出错。

1. int *p=NULL;
2. 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,地址分别如下所示:

名称 类型
&a 0x0000003c41d9fb34 {3} int *
&b 0x0000003c41d9fb54 {4} int *
&x 0x0000003c41d9fb10 {3} int *
&y 0x0000003c41d9fb18 {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 不访问临时变量的地址

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

相关文章
|
14天前
|
存储 NoSQL 编译器
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
指针是一个变量,它存储另一个变量的内存地址。换句话说,指针“指向”存储在内存中的某个数据。
66 3
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
|
14天前
|
存储 编译器 C语言
【C语言】指针大小知多少 ?一场探寻C语言深处的冒险 !
在C语言中,指针的大小(即指针变量占用的内存大小)是由计算机的体系结构(例如32位还是64位)和编译器决定的。
41 9
|
14天前
|
安全 程序员 C语言
【C语言】指针的爱恨纠葛:常量指针vs指向常量的指针
在C语言中,“常量指针”和“指向常量的指针”是两个重要的指针概念。它们在控制指针的行为和数据的可修改性方面发挥着关键作用。理解这两个概念有助于编写更安全、有效的代码。本文将深入探讨这两个概念,包括定义、语法、实际应用、复杂示例、最佳实践以及常见问题。
38 7
|
15天前
|
存储 算法 程序员
C 语言递归算法:以简洁代码驾驭复杂逻辑
C语言递归算法简介:通过简洁的代码实现复杂的逻辑处理,递归函数自我调用解决分层问题,高效而优雅。适用于树形结构遍历、数学计算等领域。
|
17天前
|
存储 程序员 编译器
C 语言数组与指针的深度剖析与应用
在C语言中,数组与指针是核心概念,二者既独立又紧密相连。数组是在连续内存中存储相同类型数据的结构,而指针则存储内存地址,二者结合可在数据处理、函数传参等方面发挥巨大作用。掌握它们的特性和关系,对于优化程序性能、灵活处理数据结构至关重要。
|
17天前
|
算法 C语言
C语言中的文件操作技巧,涵盖文件的打开与关闭、读取与写入、文件指针移动及注意事项
本文深入讲解了C语言中的文件操作技巧,涵盖文件的打开与关闭、读取与写入、文件指针移动及注意事项,通过实例演示了文件操作的基本流程,帮助读者掌握这一重要技能,提升程序开发能力。
51 3
|
16天前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
42 1
|
18天前
|
存储 算法 程序员
C 语言指针详解 —— 内存操控的魔法棒
《C 语言指针详解》深入浅出地讲解了指针的概念、使用方法及其在内存操作中的重要作用,被誉为程序员手中的“内存操控魔法棒”。本书适合C语言初学者及希望深化理解指针机制的开发者阅读。
|
17天前
|
程序员 C语言
C语言中的指针既强大又具挑战性,它像一把钥匙,开启程序世界的隐秘之门
C语言中的指针既强大又具挑战性,它像一把钥匙,开启程序世界的隐秘之门。本文深入探讨了指针的基本概念、声明方式、动态内存分配、函数参数传递、指针运算及与数组和函数的关系,强调了正确使用指针的重要性,并鼓励读者通过实践掌握这一关键技能。
32 1
|
21天前
|
存储 C语言 计算机视觉
在C语言中指针数组和数组指针在动态内存分配中的应用
在C语言中,指针数组和数组指针均可用于动态内存分配。指针数组是数组的每个元素都是指针,可用于指向多个动态分配的内存块;数组指针则指向一个数组,可动态分配和管理大型数据结构。两者结合使用,灵活高效地管理内存。