C语言基础之【指针】(上)

发布于:2025-02-28 ⋅ 阅读:(18) ⋅ 点赞:(0)

往期《C语言基础系列》回顾:
链接:
C语言基础之【C语言概述】
C语言基础之【数据类型】(上)
C语言基础之【数据类型】(下)
C语言基础之【运算符与表达式】
C语言基础之【程序流程结构】
C语言基础之【数组和字符串】(上)
C语言基础之【数组和字符串】(下)
C语言基础之【函数】


概述

内存

内存(Memory):是计算机系统中用于存储数据指令的关键硬件组件。

内存是程序运行的基石,所有正在执行的程序和数据都需要加载到内存中才能被CPU处理。


内存的特点:

  • 速度快:相较于外部存储设备如硬盘、光盘等,内存的读写速度极快。
    • 内存作为CPU与硬盘之间的桥梁,提供快速数据访问。
  • 临时存储:内存用于暂时存储正在运行的程序和数据。
    • 当计算机断电后,内存中存储的数据会立即丢失,这是与硬盘等外部存储设备最显著的区别之一。
  • 与 CPU 直接交互:内存是计算机中唯一能与 CPU 直接进行数据交互的部件。
    • CPU 可以直接从内存中读取指令和数据,进行运算处理后再将结果写回内存。
  • 可扩展性:计算机的内存具有一定的可扩展性。
    • 用户可以通过添加内存模块来增加内存容量,从而提升计算机的性能。

内存的分类:

  • RAM(随机存取存储器):可读写,用于临时存储数据。
    • 如:程序运行时变量
  • ROM(只读存储器):只读,用于存储固件。
    • 如:BIOS(数据不可修改)

物理存储器和存储地址空间

物理存储器:是指计算机系统中实际存在的存储硬件,数据和程序的物理存储介质。

  • 如:内存条(RAM)、硬盘、固态硬盘(SSD)等。

物理存储器的分类:

  1. 主存储器(内存):用于存储正在运行的程序和数据。
    • 直接与CPU交互
    • 速度快,容量有限
  2. 辅助存储器(外存):用于存储需要长期存储的数据。
    • 速度较慢,容量大
  3. 高速缓存(Cache):用于加快数据访问的速度。
    • 位于CPU和主存之间
    • 分为L1、L2、L3缓存,速度依次递减,容量依次递增

存储地址空间:是指计算机系统中用于存储数据的地址范围。


存储地址空间可以分为 物理地址空间逻辑地址空间

  • 物理地址空间:是对物理存储器中每个存储单元进行编号的地址范围,与物理存储器的实际地址相对应。

  • 逻辑地址空间:是程序和进程所使用的地址空间,也称为虚拟地址空间。

内存地址

内存单元:是计算机内存系统中的基本存储单位,用于存储数据的最小物理单位。

它就像一个小格子,每个格子都有自己的编号(地址),可以存放特定数量的数据。

在计算机内存里,众多这样的内存单元组合在一起,形成了能够存储大量数据的内存空间。

内存地址:是计算机系统中用于标识访问内存中特定存储单元的唯一编号。

  • 将内存抽象成一个很大的一维字符数组
  • 编码就是对内存的每一个字节分配一个32位或64位的编号(与32位或者64位处理器相关)
  • 这个内存编号我们称之为内存地址

内存地址的作用:

  • 程序:通过内存地址访问数据或指令。
  • 操作系统和硬件:通过内存地址管理内存资源。

内存地址的表示:

  • 内存地址的表示通常是十六进制

    • 如:0x1000
  • 内存地址的范围由系统的位数决定

    • 如:32位系统的地址范围为0x000000000xFFFFFFFF

指针和指针变量

如果在程序中定义了一个变量,在对程序进行编译或运行时,系统就会给这个变量分配内存单元,并确定它的内存地址(编号)

指针的实质就是内存“地址”。指针就是地址,地址就是指针

在这里插入图片描述


指针的作用:

  • 直接访问内存:允许程序直接操作内存中的数据,能够更灵活地对数据进行处理。

    • 如:在需要高效地处理大量数据或对内存进行精细管理的场景中,指针可以让程序员直接定位到特定的内存区域,进行数据的读取、写入或修改等操作。
  • 实现数据结构:是构建各种复杂数据结构的基础。

    • 如:链表、树、图等。通过指针,这些数据结构中的节点可以相互链接,形成特定的逻辑结构,方便对数据进行组织和操作。
  • 函数间数据传递:在函数之间传递数据时,指针可以起到高效传递数据的作用。

    • 如:对于大型数据结构或对象,传递指针比传递数据本身更加高效,因为只需要传递一个地址值,而不需要复制大量的数据,从而节省了内存空间和数据传输时间。

指针基础知识

指针变量的定义和使用

指针变量定义的语法数据类型 *指针变量名;

  • 数据类型:指针所指向的变量的类型 (如:intfloatchar等)

    • 决定了从指针存储的地址开始向后读取的字节数。
    • 决定了指针进行+1操作向后加过的字节数。
  • *:表示这是一个指针变量。

  • 指针变量名:指针变量的名称。

int *p;      // 定义一个指向整型的指针变量p
float *q;    // 定义一个指向浮点型的指针变量q
char *r;     // 定义一个指向字符型的指针变量r

注意:由于指针变量定义的语法中数据类型指针变量名之间的*该怎么放?C语言中并没有明确规定。

所以

//指针变量定义的语法:(以下的都可以)
数据类型* 指针变量名; ----->Windows平台常用
数据类型 *指针变量名; ----->Linux平台常用
数据类型 * 指针变量名; ----->几乎没人这么写
数据类型*指针变量名; ----->几乎没人这么写

指针变量初始化的语法指针变量名 = &变量名;

指针变量在定义后,通常需要初始化为某个变量的地址,可以使用&运算符获取变量的地址。

int a = 10;
int *p = &a; // 将指针变量p初始化为变量a的地址

指针变量的使用:

  1. 取地址操作(&):使用&运算符获取变量的地址

    int a = 10;
    int *p = &a; // p存储变量a的地址
    

    注意

    • &可以取得一个变量在内存中的地址,但是不能取寄存器变量。

    • 因为寄存器变量不在内存里,而在CPU里面,所以是没有地址的。

  2. 解引用操作(*):使用*运算符访问指针所指向的内存中的数据

    int a = 10;
    int *p = &a;
    
    printf("%d", *p); // 输出10
    

代码示例:通过指针间接修改变量的值

#include <stdio.h>

int main() 
{
    int a = 10;      // 定义一个整型变量a
    int *p = &a;     // 定义一个指针变量p,并将其初始化为变量a的地址
    printf("修改前,a的值: %d\n", a); // 输出a的初始值


    //通过指针间接修改变量的值
    *p = 20;         // 通过指针p修改变量a的值
    printf("修改后,a的值: %d\n", a); // 输出修改后的a的值
    return 0;
}

输出:

修改前,a的值: 10
修改后,a的值: 20

指针的类型与解引用

代码示例

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	int a = 0x12345678;
	int* p = &a;

	printf("*p = %p\n", *p);

	printf("*p = %x\n", *p);  // 打印 a 的值
	printf("p = %p\n", (void*)p);  // 打印 p 的地址

	system("pause");
	return EXIT_SUCCESS;
}

输出

*p = 0000000012345678
*p = 12345678
p = 00000022016FF5A4

代码分析

  1. 变量声明与初始化

    • int a = 0x12345678;
      • 声明了一个整型变量 a 并将其初始化为十六进制值 0x12345678
    • int *p = &a;
      • 声明了一个指向整型的指针 p,并将其初始化为变量 a 的地址
  2. 打印指针指向的值

    printf("*p = %p\n", *p);  //err
    
    • %p:是用于**打印指针地址**的格式说明符。
    • *p:是解引用指针,得到的是 a 的值,而不是地址。

正确的用法应该是 :

  • printf("*p = %x\n", *p); 来打印 a 的十六进制值。

    • printf("p = %p\n", (void*)p); 来打印指针 p 的地址。
      • %p 期望的参数类型是 void*,即指向 void 的指针,所以int* 类型的指针 p 需要强制转换为 void*类型,以便与 %p 格式说明符匹配。
      • 这是因为 %p 被设计为可以打印任何类型的指针地址,而 void* 是一种通用指针类型,可以指向任何数据类型。

总结:强制转换为 void* 是为了确保类型的匹配代码的可移植性


代码示例

short a = 0x12345678;
short* p = &a;

printf("*p = %p\n", *p);
printf("*p = %x\n", *p);  // 打印 a 的值
printf("p = %p\n", (void*)p);  // 打印 p 的地址

代码片段分析:

short a = 0x12345678;
  • short 类型通常占用 2 字节(16 位)
  • 0x12345678 是一个 4 字节的十六进制值,赋值给 short 类型时会发生 截断,只有最低的 2 字节(0x5678)会被存储到 a 中。

所以short* p 解引用时读取 2 字节数据

输出

*p = 0000000000005678
*p = 5678
p = 0000001988EFFB04

代码示例

char a = 0x12345678;
char* p = &a;

printf("*p = %p\n", *p);
printf("*p = %x\n", *p);  // 打印 a 的值
printf("p = %p\n", (void*)p);  // 打印 p 的地址

代码片段分析:

char a = 0x12345678;
  • char 类型通常占用 1 字节(8 位)
  • 0x12345678 是一个 4 字节的十六进制值,赋值给 char 类型时会发生 截断,只有最低的 1 字节(0x78)会被存储到 a 中。

所以char* p 解引用时读取 1 字节数据

输出

*p = 0000000000000078
*p = 78
p = 0000009FC08FF574

指针大小

指针的大小:指针变量本身占用的内存字节数。

指针的大小与它所指向的数据类型无关,而是取决于系统的架构和编译器的实现

  • 在32位系统中,指针的大小通常为4字节
  • 在64位系统中,指针的大小通常为8字节

指针大小与系统架构的关系:

  • 32位系统

    • 地址总线宽度为32位,可以寻址的内存空间为 ( 2 32 ) (2^{32}) (232) 字节(即:4GB)
    • 指针的大小通常为4字节。
  • 64位系统

    • 地址总线宽度为64位,可以寻址的内存空间为 ( 2 64 ) (2^{64}) (264)字节(即:16EB)
    • 指针的大小通常为8字节。

获取指针大小的语法sizeof(指针变量);

#include <stdio.h>

int main() 
{
	int a = 10;
	char b = 'A';
	double c = 3.14;

	int* p = &a;
	char* q = &b;
	double* r = &c;

	printf("int指针的大小: %zu字节\n", sizeof(p));
	printf("char指针的大小: %zu字节\n", sizeof(q));
	printf("double指针的大小: %zu字节\n", sizeof(r));

	return 0;
}

输出结果(32位系统)

int指针的大小: 4字节
char指针的大小: 4字节
double指针的大小: 4字节

输出结果(64位系统)

int指针的大小: 8字节
char指针的大小: 8字节
double指针的大小: 8字节

总结:无论指针指向的是intchardouble还是其他类型,指针的大小都是相同的。


#include <stdio.h>

int main()
{
	int a = 10;
	int* p = &a;
	int** pp = &p;

	printf("int指针的大小: %zu字节\n", sizeof(p));
	printf("int指针的指针的大小: %zu字节\n", sizeof(pp));

	return 0;
}

输出结果(32位系统)

int指针的大小: 4字节
int指针的指针的大小: 4字节

输出结果(64位系统)

int指针的大小: 8字节
int指针的指针的大小: 8字节

总结:多级指针(指向指针的指针)的大小与普通指针相同。

空指针

空指针(NULL Pointer):不指向任何有效的内存地址的指针。

  • 在C语言中,空指针通常用宏NULL表示,其值为0

空指针的用途:

  • 初始化指针在定义指针时,如果暂时不知道指针应该指向哪里,可以先将其初始化为NULL,以表明它不指向任何有效数据。

    • 例如:int *p = NULL;
  • 判断指针是否有效在使用指针之前,可以通过判断指针是否为NULL来确定它是否指向了一个有效的内存地址,从而避免对空指针进行操作导致的错误。

#include <stdio.h>

int main() 
{
    int *p = NULL;   //初始化指针

    if (p == NULL)  //判断指针是否有效
    {
        printf("p是一个空指针\n");
    }

    return 0;
}

输出:

p是一个空指针


注意事项解引用空指针会导致程序崩溃

  • 因为空指针不指向任何有效内存,所以对空指针进行解引用操作是非法的,会导致程序出现错误。
int *p = NULL;

*p = 10; // 错误:解引用空指针
//这样的代码是错误的,因为它试图向一个不存在的内存地址写入数据。

野指针

野指针(Dangling Pointer):指向无效内存地址的指针。


野指针产生原因:

  1. 定义指针时未初始化

    • 指针变量定义后未赋值,其值是随机的。

      int *p;   // 未初始化
      *p = 10;  // 错误:p是野指针
      
      //或者有一部分人会这样作:
      int p2 = 1000;  // 错误:p2是野指针
      //这样手动为指针赋内存地址的行为,绝大多数情况下会导致该指针为野指针
      
  2. 释放内存后未置空指针

    • 动态分配的内存释放后,指针仍然指向该内存地址。

      int *p = (int *)malloc(sizeof(int));
      free(p);  // 释放了指针指向的内存,但是未将指针置空
      *p = 10;  // 错误:p是野指针
      
  3. 返回局部变量的指针

    • 局部变量的内存在被调函数执行结束后就被释放掉了

    • 所以禁止返回局部变量的地址

      int *getPointer() 
      {
          int a = 10;
      
          return &a;
          //返回局部变量的地址,但是局部变量a的内存在函数执行结束后就被释放掉了
      }
      
      int main() 
      {
          int *p = getPointer();
      
          *p = 20; // 错误:p是野指针
          return 0;
      }
      

野指针的危害:

  • 访问野指针指向的内存可能导致程序崩溃(段错误)
  • 修改野指针指向的内存可能导致数据损坏安全漏洞

如何避免野指针:

  1. 初始化指针

    • 定义指针时初始化为NULL

      int *p = NULL;
      
  2. 释放内存后置空指针

    • 释放动态分配的内存后,将指针置为NULL

      int *p = (int *)malloc(sizeof(int));
      free(p);
      
      p = NULL; // 置空指针
      
  3. 避免返回局部变量的地址

    • 不要返回函数内局部变量的地址。

空指针与野指针的对比:

特性 空指针(NULL Pointer) 野指针(Dangling Pointer)
定义 指针不指向任何内存地址 指针指向无效的内存地址
产生原因 显式初始化为NULL 定义指针时未初始化
释放内存后未置空指针
返回局部变量的地址
解引用后果 程序崩溃 程序崩溃或数据损坏
如何避免 初始化指针为NULL 定义指针时初始化指针
释放内存后置空指针
避免返回局部变量地址

代码示例:空指针

#include <stdio.h>

int main()
{
	int* p = NULL; // 定义一个空指针

	if (p == NULL)
	{
		printf("p是一个空指针\n");
	}

	// *p = 10; // 错误:解引用空指针会导致程序崩溃
	return 0;
}

代码示例:野指针

#include <stdio.h>
#include <stdlib.h>

int main()
{
	int* p = (int*)malloc(sizeof(int)); // 动态分配内存
	*p = 10;
	printf("p指向的值: %d\n", *p);

	free(p); // 释放内存
	// p现在是野指针

	// *p = 20; // 错误:解引用野指针会导致未定义行为

	p = NULL; // 置空指针,避免成为野指针
	return 0;
}

万能指针

void * :是一种通用指针类型,可以指向任意数据类型的内存地址,因此也称为 万能指针泛型指针


万能指针定义的语法void *指针变量名;

void *p; // 定义一个万能指针p

万能指针的特点:

  1. 可以指向任意类型的数据

    • void * 可以指向 intfloatchar、结构体等任意类型的数据。

      int a = 10;
      float b = 3.14;
      void *p;
      
      p = &a; // 指向int类型
      p = &b; // 指向float类型
      
  2. 不能直接解引用

    • 由于 void * 没有类型信息,编译器无法确定它指向的数据类型,因此不能直接解引用。

      int a = 10;
      void *p = &a;
      // *p = 20; // 错误:不能直接解引用void指针
      
      int *q = (int *)p; // 将void指针转换为int指针
      *q = 20;           // 解引用并修改值
      
  3. 不能直接指针算术运算

    • void * 不能直接进行指针算术运算,因为编译器无法确定指针的步长。

      int arr[] = {1, 2, 3};
      void *p = arr;
      // p++; // 错误:void指针不能进行算术运算
      
      int *q = (int *)p;
      q++; // 正确:int指针可以进行算术运算
      

总结在使用 void * 时,需要将其转换为具体类型的指针,才能解引用或进行指针算术运算

代码示例:展示 void * 的使用

#include <stdio.h>
#include <stdlib.h>

// 打印任意类型的数据
void printValue(void* data, char type)
{
    switch (type)
    {
    case 'i':
        printf("int: %d\n", *(int*)data);
        break;
    case 'f':
        printf("float: %.2f\n", *(float*)data);
        break;
    case 'c':
        printf("char: %c\n", *(char*)data);
        break;
    default:
        printf("Unknown type\n");
    }
}

int main()
{
    int a = 10;
    float b = 3.14;
    char c = 'A';

    printValue(&a, 'i'); // 打印int类型
    printValue(&b, 'f'); // 打印float类型
    printValue(&c, 'c'); // 打印char类型

    return 0;
}

输出:

int: 10
float: 3.14
char: A

const修饰的指针变量

const关键字:用于定义常量限制变量的修改。


const 修饰指针的基本形式:

指向常量的指针:指针指向的数据是常量,不能通过指针修改数据。

  • 语法const 数据类型 *指针变量名;

  • 语法数据类型 const *指针变量名;

    const int *p; // p是一个指向常量的指针,不能通过p修改数据
    int const *p; // p是一个指向常量的指针,不能通过p修改数据
    

常量指针:指针本身是常量,不能修改指针的指向。

  • 语法数据类型 *const 指针变量名;

    int *const p; // p是一个常量指针,不能修改p的指向
    

指向常量的常量指针:指针指向的数据是常量,且指针本身也是常量。

  • 语法const 数据类型 *const 指针变量名;

    const int *const p; // p是一个指向常量的常量指针
    

指向常量的指针的特点:

  • 指针指向的数据是常量,不能通过指针修改数据。
  • 指针本身可以修改,指向其他地址。
#include <stdio.h>

int main() 
{
    int a = 10;
    const int *p = &a; // p是一个指向常量的指针
    printf("a的值: %d\n", *p);

    // *p = 20; // 错误:不能通过p修改a的值
    a = 20;     // 正确:可以直接修改a的值
    printf("修改后a的值: %d\n", *p);

    int b = 30;
    p = &b;     // 正确:可以修改p的指向
    printf("b的值: %d\n", *p);

    return 0;
}

输出:

a的值: 10
修改后a的值: 20
b的值: 30

常量指针的特点:

  • 指针本身是常量,不能修改指针的指向。
  • 指针指向的数据可以修改。
#include <stdio.h>

int main() 
{
    int a = 10;
    int *const p = &a; // p是一个常量指针
    printf("a的值: %d\n", *p);

    *p = 20;    // 正确:可以通过p修改a的值
    printf("修改后a的值: %d\n", a);

    int b = 30;
    // p = &b; // 错误:不能修改p的指向

    return 0;
}

输出:

a的值: 10
修改后a的值: 20

指向常量的常量指针的特点:

  • 指针指向的数据是常量,不能通过指针修改数据。
  • 指针本身也是常量,不能修改指针的指向。
#include <stdio.h>

int main() 
{
    int a = 10;
    const int *const p = &a; // p是一个指向常量的常量指针
    printf("a的值: %d\n", *p);

    // *p = 20; // 错误:不能通过p修改a的值
    a = 20;    // 正确:可以直接修改a的值
    printf("修改后a的值: %d\n", *p);

    int b = 30;
    // p = &b; // 错误:不能修改p的指向

    return 0;
}

输出:

a的值: 10
修改后a的值: 20

const 修饰指针的常见用法:

1.保护函数参数

  • 使用 const 修饰指针参数,防止函数内部修改数据。

    void printArray(const int *arr, int n) 
    {
        for (int i = 0; i < n; i++) 
        {
            printf("%d ", arr[i]);
        }
        printf("\n");
    }
    

2.定义常量字符串

  • 使用 const 修饰指针,定义常量字符串。

    const char *str = "Hello, World!";
    // str[0] = 'h'; // 错误:不能修改常量字符串
    

关于 const 修饰指针的三种形式的对比表格:

形式 语法 特点 示例
指向常量的指针 const 数据类型 *指针变量名; 不能通过指针修改数据
可以修改指针的指向
const int *p;
p = &a;
// *p = 20; // 错误
常量指针 数据类型 *const 指针变量名; 可以通过指针修改数据
不能修改指针的指向
int *const p = &a;
*p = 20;
// p = &b; // 错误
指向常量的常量指针 const 数据类型 *const 指针变量名; 不能通过指针修改数据
不能修改指针的指向
const int *const p = &a;
// *p = 20; // 错误
// p = &b; // 错误

👨‍💻 博主正在持续更新C语言基础系列中。
❤️ 如果你觉得内容还不错,请多多点赞。

⭐️ 如果你觉得对你有帮助,请多多收藏。(防止以后找不到了)

👨‍👩‍👧‍👦 C语言基础系列 持续更新中~,后续分享内容主要涉及 C++全栈开发 的知识,如果你感兴趣请多多关注博主。


网站公告

今日签到

点亮在社区的每一天
去签到