深入理解指针1

1.内存和地址

首先我们先来了解内存和地址的相关概念:

*内存(Memory)是计算机用于临时存储数据和指令的硬件设备,属于随机存取存储器(RAM)。其特点是读写速度快,但断电后数据会丢失。内存直接与CPU交互,存储当前运行的程序和所需数据,是计算机性能的关键部件之一。

*地址(Address)是内存中每个存储单元的编号,用于唯一标识数据的位置。通常以十六进制表示(如0x0000FFFF)。CPU通过地址访问内存中的数据,类似于通过门牌号找到具体房屋。

对于初学者或许会比较难以理解,我们可以把内存想象成一个个小房间,在管理内存空间时会将内存划分成内存单元,而这一个内存单元就相当于一个学生宿舍,一个字节空间里面能放八个比特位,就好比一个宿舍住八个,每个人都是一个比特位。

每个内存单元都有一个编号(想象成宿舍门牌号),这个编号就是地址。而在C语言中我们给这个地址取了一个新名字叫:指针

所以我们可以理解为:内存单元的编号=地址=指针

字节及其衍生单位

计算机内存容量通常以字节及其衍生单位表示,常见换算关系如下:

1 Byte = 8 bits
1 Kilobyte (KB) = 1024 Bytes
1 Megabyte (MB) = 1024 KB
1 Gigabyte (GB) = 1024 MB
1 Terabyte (TB) = 1024 GB

2.指针变量和地址

2.1取地址操作符(&)

在C语言中创建变量的过程其实就在在向内存申请空间,比如:

// An highlighted block
int main()
{
int a=10;
return 0;
}

创建整形变量a时,向内存申请了4个字节,用于存放整数10,其中每个字节都有地址。
为了得到a的地址,我们可以使用取地址操作符:&

// An highlighted block
#include <stdio.h>
int main()
{
 int a = 10;
 &a;//取出a的地址 
 printf("%p\n", &a);
 return 0;
}

虽然整形变量占四个字节,但我们只要知道了第一个字节的地址,就可以顺藤摸瓜访问到这四个字节的数据。

2.2指针变量

那我们通过取地址操作符(&)拿到的地址是一个数值,比如:0x006FFD70,这个数值有时候也是需要
存储起来,方便后期再使用的,我们把这样的地址值存放在指针变量中。
比如

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

指针变量是一种用于存放地址的辨别,存放在指针变量中的值都会理解为地址。

如何拆解指针类型呢
我们看到pa的类型是int*,*是在说明pa是指针变量,而前面的int是在说明pa指向的是整型(int)类型的对象。
那如果我们有一个char型的数据,需要存放它的地址该怎么做呢?
如下:

// An highlighted block
char c='t';
char *pc=&c;

2.3解引用操作符(*)

我们用指针变量存放了数据的地址,那该如何使用呢?这时候就要用到解引用操作符(*)

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

*pa的意思就是通过pa中存放的地址,找到指向的空间,pa其实就是a变量了;所以pa=0,这个操作符是把a改成了0。

有的同学可以会想,要改a的地址完全可以直接写a=0;但并不是所有情况都可以使用a=0,有时候我们会遇到只能用指针帮助的情况,在后面我们会讲到传值调用和传址调用。

2.4指针变量的大小

// An highlighted block
include <stdio.h>
//指针变量的⼤⼩取决于地址的⼤⼩ 
//32位平台下地址是32个bit位(即4个字节) 
//64位平台下地址是64个bit位(即8个字节) 
int main()
{
 printf("%zd\n", sizeof(char *));
 printf("%zd\n", sizeof(short *));
 printf("%zd\n", sizeof(int *));
 printf("%zd\n", sizeof(double *));
 return 0;
}

在X86环境的输出结果是:
4
4
4
4
在X64环境的输出结果是:
8
8
8
8
结论:
• 32位平台下地址是32个bit位,指针变量大小是4个字节
• 64位平台下地址是64个bit位,指针变量大小是8个字节
• 注意指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的。

3.指针变量类型的意义

3.1指针的解引用

对比下面俩段代码

//代码1
#include <stdio.h>
int main()
{
 int n = 0x11223344;
 int *pi = &n; 
 *pi = 0; 
 return 0;
}
// 代码2
#include <stdio.h>
int main()
{
 int n = 0x11223344;
 char *pc = (char *)&n;
 *pc = 0;
 return 0;
}

调试我们可以看到,代码1会将n的4个字节全部改为0,但是代码2只是将n的第⼀个字节改为0。
结论:指针的类型决定了,对指针解引用的时候有多大的权限(⼀次能操作几个字节)。
比如: char* 的指针解引用就只能访问⼀个字节,而 int* 的指针的解引⽤就能访问四个字节。

3.2指针±整数

// An highlighted block
#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;
}

运行结果如下:
在这里插入图片描述
我们可以看出, char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。
这就是指针变量的类型差异带来的变化。指针+1,其实跳过1个指针指向的元素。指针可以+1,那也可以-1。
结论:指针的类型决定了指针向前或者向后走一步有多大(距离)。

3.3void*指针

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

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

在上面的代码中,将⼀个int类型的变量的地址赋值给⼀个char类型的指针变量。编译器给出了⼀个警告(如下图),是因为类型不兼容。而使用void类型就不会有这样的问题。
在这里插入图片描述
⼀般 void* 类型的指针是使⽤在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果。使得⼀个函数来处理多种类型的数据。

4.const修饰指针

4.1const修饰变量

当我们想要一个变量不能被修改时,就可以使用const来修饰这个变量。

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

上述代码中n是不能被修改的,其实n本质是变量,只不过被const修饰后,在语法上加了限制,只要我们在代码中对n就⾏修改,就不符合语法规则,就报错,致使没法直接修改n。

但是如果我们绕过n,使⽤n的地址,去修改n就能做到了。

// An highlighted block
#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;
}

4.2const修饰指针变量

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

5.指针运算

5.1指针±整数

因为数组在内存中是连续存放的,只要知道第⼀个元素的地址,就能找到后⾯的所有元素。

// An highlighted block
#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;
 }

5.2指针-指针

// An highlighted block
#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;
}

5.3指针的关系运算

// An highlighted block
#include <stdio.h>
int main()
{
 int arr[10] = {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;
}

6.野指针

6.1野指针的三种情况

6.1.1指针未初始化

// An highlighted block
int *p;//局部指针未初始化默认为随机值
*p=20;

6.1.2指针越界访问

// An highlighted block
int arr[10]={0};
int *p = &arr[0];
 int i = 0;
 for(i=0; i<=11; i++)
 {
 //当指针指向的范围超出数组arr的范围时,p就是野指针 
 *(p++) = i;
 }

6.1.3指针指向的空间释放

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

当跳出test函数时,系统分配给n的内存已经返回给了系统,而指针p也就变成了野指针。

6.2如何规避野指针

初始化指针
声明指针时立即初始化为NULL或有效的内存地址。未初始化的指针可能指向任意内存地址,使用这样的指针会导致未定义行为

// An highlighted block
int *ptr=NULL;

释放后置空

// An highlighted block
free(p);
p=NULL;

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

7.assert断言

7.1assert断言的基本概念

assert是C语言标准库中的一个宏,用于在程序运行时检查某个条件是否满足。若条件为真(非零),程序继续执行;若条件为假(零),assert会输出错误信息并终止程序。assert常用于调试阶段,帮助开发者快速定位逻辑错误。

7.2assert的语法

#include <assert.h>
void assert(int expression);
  • expression:需要检查的条件表达式。若为假,触发断言失败。

7.3 assert的使用场景

  1. 检查函数参数合法性:确保传入函数的参数符合预期范围。
  2. 验证中间结果:在复杂计算中检查中间步骤的正确性。
  3. 调试辅助:快速捕捉程序运行时的逻辑错误。

7.4assert的示例代码

#include <stdio.h>
#include <assert.h>

int divide(int a, int b) {
    // 检查除数是否为0
    assert(b != 0);
    return a / b;
}

int main() {
    int x = 10, y = 2;
    printf("10 / 2 = %d\n", divide(x, y));

    // 触发断言失败
    y = 0;
    printf("10 / 0 = %d\n", divide(x, y));
    return 0;
}
  1. divide函数:通过assert(b != 0)确保除数不为零。若除数为零,程序终止并输出错误信息(包含文件名、行号及断言条件)。
  2. 正常调用divide(10, 2)不会触发断言,输出结果5
  3. 异常调用divide(10, 0)触发断言失败,程序终止。

7.5禁用assert的方法

在发布版本中,可通过定义NDEBUG宏禁用assert:

#define NDEBUG
#include <assert.h>

此时,所有assert语句会被预处理器忽略,不会影响程序性能。

7.6 注意事项

  1. 仅用于调试:assert不应替代正常的错误处理逻辑(如输入验证)。
  2. 副作用问题:断言条件中的表达式应避免副作用(如修改变量值),否则禁用assert时可能引发问题。
  3. 错误信息:断言失败会输出标准错误流(stderr),便于日志记录。

通过合理使用assert,可以显著提升代码的健壮性和可维护性。### assert断言的基本概念

assert是C语言标准库中的一个宏,用于在程序运行时检查某个条件是否满足。若条件为真(非零),程序继续执行;若条件为假(零),assert会输出错误信息并终止程序。assert常用于调试阶段,帮助开发者快速定位逻辑错误。

8.指针使用和传址调用

8.1传值调用和传址调用

8.1.1传值调用(Call by Value)

传值调用是一种参数传递方式,函数调用时会将实际参数的值复制一份传递给形式参数。在函数内部对形式参数的修改不会影响实际参数的值。传值调用的特点是简单、安全,但可能因复制大量数据而影响性能。

特点:

  • 实际参数和形式参数占用不同的内存空间。
  • 函数内部对参数的修改不会影响调用者的变量。
  • 适用于基本数据类型(如整数、浮点数)或小型结构体。

示例:

void swap(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 1, y = 2;
    swap(x, y); // x和y的值不会被交换
    return 0;
}

8.1.2传址调用(Call by Reference)

传址调用是一种参数传递方式,函数调用时会将实际参数的地址(指针)传递给形式参数。通过地址可以直接操作实际参数的值。传址调用的特点是高效,但可能因直接修改数据而带来风险。

特点:

  • 实际参数和形式参数共享同一内存地址。
  • 函数内部对参数的修改会影响调用者的变量。
  • 适用于需要修改实际参数或传递大型数据的场景。

示例:

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int x = 1, y = 2;
    swap(&x, &y); // x和y的值会被交换
    return 0;
}

8.1.3传值与传址的对比

特性传值调用传址调用
参数传递方式复制值传递地址(指针)
内存占用额外内存存储副本直接操作原数据
安全性高(不影响原数据)低(可能误修改原数据)
性能可能较低(复制开销)较高(无复制开销)
典型应用场景基本数据类型、小型结构体大型数据、需要修改原数据时

8.2传址调用与指针的联系

8.2.1 指针的作用

指针是存储内存地址的变量,通过指针可以间接访问或修改内存中的数据。指针的核心功能是提供对内存的直接操作能力,常用于动态内存分配、数组操作和函数参数传递等场景。

8.2.2传址调用与指针的联系

传址调用通常通过指针实现。在C/C++等语言中,将指针作为参数传递给函数,函数通过解引用指针(*操作)修改原始变量的值。例如:

void modifyValue(int *ptr) {
    *ptr = 10; // 通过指针修改原始变量的值
}
int main() {
    int a = 5;
    modifyValue(&a); // 传递变量a的地址
    return 0;
}

通过指针实现的传址调用是底层编程的核心机制,理解其原理有助于优化性能并避免内存错误。

本期有关指针的讲解,到此结束,下期再见👋

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值