C++指针和自由存储空间

什么是指针

在计算机程序存储数据时必须要跟踪的3种基本属性:1、信息存储在何处 2、存储的信息是什么类型 3、存储的值是多少

我们最常用的策略就是定义一个简单变量,例如int a=1,在声明语句的时候就指出了值的类型、符号名和值,还让程序为这个变量分配内存。

还有另外一种策略,这种策略以指针为基础,指针是一个变量,其存储的是值的地址,而不是值本身。(这里涉及到内存地址这一概念,是计算机组成原理、操作系统特别重要的一个知识,这里就不多赘述) 

我们先说说如何获得一个变量的地址,只需要通过&(取地址)这一运算符,例如home是一个变量,那么&home就是它的地址,这个地址输出是16进制的。

#include<iostream>
using namespace std;
int main(){
    int home=10;
    cout<<&home<<endl;//0x16b68efac
}

所以指针究竟是什么呢:

​指针(Pointer)​​ 是一种特殊的变量,它的核心功能是​​直接操作内存地址​​。你可以将指针理解为一种“导航工具”,它能告诉你数据存放在内存中的具体位置,并允许你通过这个位置访问或修改数据。

我们在初学指针的时候,可能很多老师都告诉我们指针就相当于钥匙,可以通过这个钥匙去打开房间门来获取我们需要的值,这样说很形象。但是我们可能都会有一个疑问:为什么一定要通过钥匙去开门呢,我们不能直接让门开着然后直接去拿到里面的值吗,这个指针不是多此一举吗???

理解指针的关键在于明白:​​指针的“间接性”是为了解决编程中无法直接访问某些内存的问题​​。下面通过几个具体场景来解释为什么“直接开门”并不总是可行:


​1. 动态内存分配:房间的门可能不存在​

当程序在​​堆内存​​(Heap)中动态分配空间时,这个“房间”(内存块)在编译时是未知的,只有在运行时才会被创建。此时,你无法直接通过变量名访问它,因为变量名是编译时确定的。
​示例​​:

int* p = new int(10);  // 在堆上动态分配一个int,初始化为10
  • ​钥匙(指针)的作用​​:p 保存了动态分配的地址,此时没有“门”(变量名)可以直接访问这个值,必须通过指针(钥匙)操作。
  • ​直接开门的问题​​:动态分配的内存没有变量名,你无法直接通过类似 int x = ... 的语法访问它。

​2. 间接访问:房间的门在运行时才能确定​

某些情况下,程序需要在运行时才能决定具体操作哪个内存地址。
​示例​​:根据用户输入选择操作不同的变量。

int a = 10, b = 20;
int* p = nullptr;

// 用户输入1选择a,输入2选择b
int choice;
std::cin >> choice; 

if (choice == 1) p = &a;
else p = &b;

// 通过指针间接访问用户选择的变量
std::cout << *p; 
  • ​钥匙(指针)的作用​​:指针允许在运行时动态绑定到不同的内存地址。
  • ​直接开门的问题​​:编译时无法预知用户会选哪个变量,无法直接写死 a 或 b

​3. 共享数据:多个钥匙对应同一扇门​

当多个函数或对象需要操作同一块内存时,指针可以高效地传递内存地址,避免数据拷贝。
​示例​​:函数修改外部变量。


void increment(int* ptr) {
    (*ptr)++;  // 通过指针修改外部变量
}

int main() {
    int x = 5;
    increment(&x);  // 传递x的地址
    std::cout << x; // 输出6
}
  • ​钥匙(指针)的作用​​:通过指针共享同一块内存,避免传递值的副本。
  • ​直接开门的问题​​:函数无法直接修改外部变量的值(除非使用全局变量或引用)。

​4. 复杂数据结构:门与门之间的连接​

指针是构建链表、树、图等动态数据结构的基石。
​示例​​:单链表节点。

struct Node {
    int data;
    Node* next;  // 指向下一个节点的指针
};

int main() {
    Node* head = new Node{1, nullptr};
    head->next = new Node{2, nullptr};  // 通过指针连接节点
}
  • ​钥匙(指针)的作用​​:指针将多个独立的内存块(节点)连接成逻辑上的整体。
  • ​直接开门的问题​​:无法直接用变量名表示无限多个动态创建的节点。

​5. 底层操作:门的位置由硬件决定​

在系统编程或嵌入式开发中,某些内存地址由硬件设备映射(如寄存器、外设缓冲区),必须通过指针直接访问。
​示例​​:操作硬件寄存器。

volatile uint32_t* const USB_STATUS_REG = reinterpret_cast<uint32_t*>(0x40000000);
*USB_STATUS_REG = 0x1;  // 通过指针直接写入硬件寄存器
  • ​钥匙(指针)的作用​​:直接访问固定物理地址。
  • ​直接开门的问题​​:这些地址没有变量名,只能通过指针访问。

​为什么不能“直接开门”?​

  1. ​内存的动态性​​:堆内存、硬件地址、动态数据结构等没有固定的“门”(变量名),必须通过指针间接访问。
  2. ​间接性的必要性​​:程序运行时才能确定操作的目标(如用户输入、网络数据),指针提供动态绑定的能力。
  3. ​资源共享与高效性​​:传递指针避免数据拷贝,提升性能。

​总结:钥匙(指针)的意义​

​场景​​钥匙(指针)的作用​​直接开门的局限性​
动态内存分配操作没有变量名的堆内存无法通过变量名访问动态内存
运行时决策动态绑定到不同内存地址编译时无法确定目标
数据共享多个函数操作同一内存函数参数默认传递副本
复杂数据结构连接动态创建的节点变量名无法表示无限节点
硬件/系统编程访问固定物理地址物理地址没有变量名

指针的“钥匙”本质是解决​​内存访问的间接性需求​​。它不是多余的步骤,而是程序灵活性与动态性的关键工具。就像现实世界中,如果所有房间的门都永远敞开且位置固定,钥匙确实多余;但在程序的世界里,内存的分配、共享和动态性让“钥匙”变得不可或缺。

声明和初始化指针:

这样就声明了一个指向整型的指针,当然现在它只是声明但是没有指向具体的某个地址

int *ptr

初始化:

int a=10;
int *ptr=&a;

这样就对ptr进行了一个初始化,此时ptr中存储的值就是变量a的地址,那我如何通过ptr来获取a的值呢?此时,cout<<*ptr就会输出10.

注意:int *ptr 和*ptr中的两个*的含义和作用是完全不一样的,这里初学的时候很容易混淆

1. int *ptr 中的 *:指针声明​

​作用​​:
  • ​声明一个指针变量​​:表示 ptr 是一个指向 int 类型数据的指针变量。
  • ​类型说明符​​:int* 是一个整体,表示指针类型(指向整数的指针)。
​语法规则​​:
int* ptr;   // 清晰写法:强调ptr的类型是int*
int *ptr;   // 传统写法:强调*是修饰ptr的
int *a, *b; // 正确:声明多个指针时,每个变量前都要加*
int* c, d;  // 错误:c是指针,d是普通int(不要混淆!)
​示例​​:
int x = 10;
int *ptr = &x;  // ptr存储x的地址(如0x7FFF1234)

​2. *ptr 中的 *:解引用操作符​

​作用​​:
  • ​访问指针指向的值​​:通过指针变量 ptr 找到它指向的内存地址,并操作该地址的值。
  • ​解引用(Dereference)​​:将指针转换为它指向的实际数据。
​语法规则​​:
int value = *ptr; // 获取ptr指向的值
*ptr = 20;        // 修改ptr指向的值
​示例​​:
int x = 10;
int *ptr = &x;    // ptr指向x的地址
cout << *ptr;     // 输出10(访问x的值)
*ptr = 30;        // 等价于 x = 30

​3. 对比总结​

​上下文​int *ptr 中的 **ptr 中的 *
​作用​声明指针变量类型解引用,访问指针指向的值
​出现位置​变量声明语句中表达式(赋值、运算等)中
​操作对象​变量名(如 ptr指针变量(如 ptr
​语义​类型修饰符(定义指针类型)操作符(解引用指针)

​4. 常见误区与注意事项​

​(1) 混淆声明中的 * 和解引用操作符​
int* ptr1, ptr2; // ptr1是指针,ptr2是普通int(不是指针!)
int *p1, *p2;    // 正确:p1和p2都是指针
​(2) 未初始化的指针解引用​
int *ptr;        // ptr未初始化,指向随机地址
*ptr = 10;       // 未定义行为(程序可能崩溃)
(3) 空指针解引用​
int *ptr = nullptr; // ptr为空指针
*ptr = 10;          // 运行时错误(程序崩溃)

注意:一定要在对指针应用解除引用运算符(*)之前,将指针初始化为一个确定的、适当的地址

使用new来分配内存

在c语言中,使用malloc()这一库函数来分配内存,在c++中还可以使用new这一关键字来分配内存,例如:

int *ptr=new int

为一个数据对象(可以是结构,也可以是基本数据类型)获得并分配内存的通用格式:

typename * pointer_name=new typename

对于指针,new分配的内存块通常与常规变量声明分配的内存快不同,变量的值都存储在栈stack中,而new从堆heap的内存区域分配内存。

使用new来创建动态数组

在C++中,​​动态数组​​是指在程序运行时(而非编译时)确定大小,且可以灵活调整容量的数组结构。与静态数组(如 int arr[10])相比,动态数组能更好地适应未知或变化的数据量需求。以下是实现动态数组的两种主要方式及其详细说明:


​一、手动管理动态数组(new/delete)​

通过 new[] 和 delete[] 操作符手动管理堆内存,适合需要精细控制内存的场景。

​1. 创建动态数组​
int size = 5;
int* arr = new int[size]; // 分配大小为5的int数组
​2. 初始化与使用​
// 初始化数组
for (int i = 0; i < size; i++) {
    arr[i] = i * 10; // 赋值:0, 10, 20, 30, 40
}

// 访问元素
cout << arr[2]; // 输出20
​3. 扩容与缩容​

需要手动重新分配内存并复制数据:

// 扩容到10个元素
int newSize = 10;
int* newArr = new int[newSize];

// 复制旧数据
for (int i = 0; i < size; i++) {
    newArr[i] = arr[i];
}

// 释放旧内存,指向新数组
delete[] arr;
arr = newArr;
size = newSize;
​4. 释放内存​

delete[] arr; // 必须显式释放内存,否则内存泄漏
arr = nullptr; // 避免悬垂指针
​5. 优缺点​
  • ​优点​​:完全控制内存分配过程。
  • ​缺点​​:
    • 易出错(内存泄漏、越界访问)。
    • 扩容/缩容需手动处理,代码复杂。

使用new和delete时,应该遵守以下规则:

1、不要使用delete来释放不是new分配的内存

2、不要使用delete来释放同一个内存块两次

3、如果使用new[]为数组分配内存,则应该使用delete[]来释放内存

4、如果使用new为一个实体分配内存,则应该使用delete(没有方括号)来释放

5、对空指针应用delete是安全的
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值