当前位置: 首页 > article >正文

【C语言】深入理解指针(一):从基础到高级应用

前言

在C语言中,指针是一个非常重要的概念,它不仅能够帮助我们高效地操作内存,还能实现许多复杂的功能。然而,指针也是一个容易让人感到困惑的话题,许多初学者在学习指针时会遇到各种问题。今天,就让我们一起深入探讨指针的奥秘,从基础概念到高级应用,逐步揭开指针的神秘面纱。

一、引言:为什么我们需要指针?

在C语言中,指针是一种特殊的变量,它存储的是内存地址。通过指针,我们可以直接访问和操作内存中的数据。这使得指针在许多场景下都非常有用,比如动态内存分配、数组和字符串操作、函数参数传递等。掌握指针的使用,能够让你的代码更加高效、灵活。

二、内存和地址:指针的基石

在计算机中,内存是用于存储数据的地方。为了能够快速访问内存中的数据,每个内存单元都有一个唯一的编号,这个编号就是地址。在C语言中,我们用指针来表示这些地址。

(一)内存单元与地址

计算机的内存被划分为一个个内存单元,每个内存单元的大小为1个字节。每个内存单元都有一个唯一的地址,就像宿舍房间的门牌号一样。CPU通过这些地址来访问内存中的数据。
在这里插入图片描述

(二)编址原理

计算机中的编址是通过硬件设计完成的。以32位机器为例,它有32根地址总线,每根线可以表示0或1。因此,32根地址线可以表示 2^32种不同的地址,每种地址对应一个内存单元。地址总线的作用就是将地址信息传递给内存,从而找到对应的内存单元。
在这里插入图片描述

三、指针变量和地址:如何操作指针

(一)取地址操作符(&)
在C语言中,我们可以通过取地址操作符&来获取变量的地址。例如:

int a = 10;
printf("%p\n", &a); // 打印变量a的地址

在这里插入图片描述
在这里插入图片描述

这段代码会打印出变量a在内存中的地址。并且&a取出的是a所占4个字节地址较小的字节地址。所以我们只要知道了第一个字节地址,就可以访问到4个字节的数据。

(二)指针变量

指针变量是用来存储地址的变量。例如:

int a = 10;
int* pa = &a; // 将变量a的地址存储到指针变量pa中

在这里,pa是一个指针变量,它存储了变量a的地址。
指针变量类型:int* /*表示pa是指针变量, int表示pa所指的是整型类型的对象
在这里插入图片描述

(三)解引用操作符(*)

解引用操作符*用于通过指针访问它指向的对象。例如:

int main
{
   int a = 100;
   int * pa = &a;
   *pa = 0; // 通过指针pa修改变量a的值
   return 0;
}

这段代码通过指针pa将变量a的值修改为0。

(四)指针变量的大小

指针变量的大小取决于地址的大小。在32位平台上,地址是32个bit位,指针变量的大小是4个字节;在64位平台上,地址是64个bit位,指针变量的大小是8个字节。
在这里插入图片描述
在这里插入图片描述

四、指针变量类型的意义
虽然指针变量的大小和类型无关,但指针类型仍然有其特殊的意义。

(一)指针的解引用

指针类型决定了对指针解引用时的权限,即一次能操作多少个字节。例如:

int n = 0x11223344;
int *pi = &n;
*pi = 0; // 将n的4个字节全部改为0

char *pc = (char *)&n;
*pc = 0; // 只将n的第一个字节改为0

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里,int类型的指针解引用时可以操作4个字节,而char类型的指针解引用时只能操作1个字节。

(二)指针±整数

指针类型还决定了指针向前或向后移动一步的距离。例如:

int n = 10;
char *pc = (char*)&n;
int *pi = &n;

printf("%p\n", pc);   // 打印pc的地址
printf("%p\n", pc+1); // 打印pc+1的地址
printf("%p\n", pi);   // 打印pi的地址
printf("%p\n", pi+1); // 打印pi+1的地址

在这里插入图片描述

char类型的指针变量+1会跳过1个字节,而int类型的指针变量+1会跳过4个字节。

(三)void*指针

void是一种特殊的指针类型,它可以用来接收任意类型数据的地址。但void类型的指针不能直接进行指针±整数和解引用的运算。它通常用于函数参数部分,实现泛型编程的效果。

int a = 10;
void* pa = &a;

五、const修饰指针:如何限制指针的修改

const关键字在C语言中用于修饰变量,使其值不能被修改。同样,const也可以修饰指针变量,但它修饰的位置不同,意义也会有所不同。

(一)const修饰变量

int m = 0;
m = 20; // m可以被修改

const int n = 0;
n = 20; // n不能被修改

虽然变量加上了const不能修改,但是我们可以通过访问变量的地址间接修改变量的值。代码如下:

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

在这里插入图片描述

(二)const修饰指针变量

int n = 10;
int m = 20;

const int* p1 = &n; // const在*的左边,修饰指针指向的内容
*p1 = 20; // 错误,不能通过p1修改n的值
p1 = &m;  // 正确,p1可以指向其他变量

int *const p2 = &n; // const在*的右边,修饰指针变量本身
*p2 = 20; // 正确,可以通过p2修改n的值
p2 = &m;  // 错误,p2不能指向其他变量

const int* const p3 = &n; // *的左右两边都有const
*p3 = 20; // 错误,不能通过p3修改n的值
p3 = &m;  // 错误,p3不能指向其他变量

在这里插入图片描述

六、指针运算:如何使用指针进行计算

指针的基本运算包括指针±整数、指针-指针和指针的关系运算。

(一)指针±整数

指针±整数的运算在数组操作中非常常见。通过指针+整数,我们可以快速访问数组中的元素。

int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
for(int i = 0; i < 10; i++)
{
    printf("%d ", *(p+i)); // 通过指针+整数访问数组元素
}

在这里插入图片描述
在这里插入图片描述

(二)指针-指针

指针-指针的运算可以用来计算两个指针之间的距离。例如,在字符串操作中,我们可以通过指针-指针来计算字符串的长度。(前提是两个指针指向的是同一个空间)

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

在这里插入图片描述

(三)指针的关系运算

指针的关系运算可以用来比较两个指针的大小。例如,在数组操作中,我们可以通过指针的关系运算来判断指针是否越界。

#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]);
	while (p < arr + sz) // 判断指针是否越界
	{
		printf("%d ", *p);
		p++;
	}
	return 0;
}

在这里插入图片描述

七、野指针:指针的危险地带

野指针是指指针指向的位置是不可知的、随机的或不正确的。野指针的成因主要有以下几种:

(一)指针未初始化

int *p; // 局部变量指针未初始化,默认为随机值
*p = 20; // 野指针操作,可能导致程序崩溃

在这里插入图片描述

(二)指针越界访问

int arr[10] = {0};
int *p = &arr[0];
for(int i = 0; i <= 11; i++) // 超出数组范围
{
    *(p++) = i; // p成为野指针
}

在这里插入图片描述

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

int* test()
{
    int n = 100;
    return &n; // 返回局部变量的地址,导致野指针
}

(四)如何规避野指针

指针初始化:如果明确指针指向哪里,就直接赋值地址;如果不知道,可以给指针赋值NULL。
小心指针越界:访问指针时,确保它指向的范围是合法的。
指针变量不再使用时,及时置NULL,使用之前检查有效性。
避免返回局部变量的地址。

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

八、assert断言:如何确保指针的安全

assert是一个非常有用的工具,它可以帮助我们在运行时检查程序是否符合指定条件。如果条件不满足,程序会报错终止运行。

#include <assert.h>
int main()
{
    int *p = NULL;
    assert(p != NULL); // 如果p为NULL,程序会报错终止
    return 0;
}

assert的优点是可以自动标识文件和出问题的行号,而且可以通过定义NDEBUG宏来启用或禁用assert。在Debug版本中,assert可以帮助我们排查问题;在Release版本中,可以禁用assert以提高程序效率。

九、指针的使用和传址调用:如何利用指针解决问题

(一)strlen的模拟实现

strlen函数用于计算字符串的长度。我们可以通过指针来实现这个功能。

int my_strlen(const char *str)
{
    int count = 0;
    assert(str); // 确保指针不为NULL
    while(*str)
    {
        count++;
        str++;
    }
    return count;
}

(二)传值调用和传址调用

传值调用和传址调用是函数调用的两种方式。传值调用是将变量的值传递给函数,函数内部对形参的修改不影响实参;传址调用是将变量的地址传递给函数,函数内部可以通过指针修改实参的值。

void Swap1(int x, int y) // 传值调用
{
    int tmp = x;
    x = y;
    y = tmp; // 修改形参,不影响实参
}

void Swap2(int* px, int* py) // 传址调用
{
    int tmp = 0;
    tmp = *px;
    *px = *py;
    *py = tmp; // 通过指针修改实参
}

十、总结

指针是C语言中一个非常强大且灵活的工具。通过深入理解内存和地址、指针变量和地址、指针变量类型的意义、const修饰指针、指针运算、野指针、assert断言以及指针的使用和传址调用,我们可以更好地掌握指针的使用方法,避免常见的错误,提高程序的效率和安全性。
希望这篇文章能帮助你更好地理解指针。如果你对指针还有其他疑问,欢迎在评论区留言,我们一起探讨!

如果你喜欢这篇文章,别忘了点赞和关注哦!更多编程知识和技术分享,尽在我的CSDN博客。


http://www.kler.cn/a/597538.html

相关文章:

  • 【第19节】windows sdk编程:文件I/O
  • 麒麟操作系统作为服务器,并且需要在浏览器上调试 MATLAB
  • Linux 内核源码阅读——ipv4
  • Linux系统管理与编程10:任务驱动综合应用
  • Github 2025-03-19 C开源项目日报 Top4
  • GPT-5 将免费向所有用户开放?
  • 雷池SafeLine-自定义URL规则拦截非法请求
  • 计算机网络——通信基础和传输介质
  • 深入理解 Collections.emptyList():优雅处理空列表的利器!!!
  • 【Nodejs】2024 汇总现状
  • SAP SD学习笔记33 - 预詑品(寄售物料),预詑品引渡(KB),预詑品出库(KE)
  • Nginx基于SSL的TCP代理
  • 数据结构-----哈希表和内核链表
  • Unity热更新方案HybridCLR+YooAsset,从零开始,保姆级教程,纯c#开发热更
  • 2024年十大开源SLAM算法整理
  • 点亮STM32最小系统板LED灯
  • 从零开始:使用 Cython + JNI 在 Android 上运行 Python 算法
  • 内网渗透(CSMSF) 构建内网代理的全面指南:Cobalt Strike 与 Metasploit Framework 深度解析
  • pfsense部署三(snort各版块使用)
  • 渗透测试工具推荐 | BurpSuite的常用功能——抓包