什么是指针
在计算机程序存储数据时必须要跟踪的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; // 通过指针直接写入硬件寄存器
- 钥匙(指针)的作用:直接访问固定物理地址。
- 直接开门的问题:这些地址没有变量名,只能通过指针访问。
为什么不能“直接开门”?
- 内存的动态性:堆内存、硬件地址、动态数据结构等没有固定的“门”(变量名),必须通过指针间接访问。
- 间接性的必要性:程序运行时才能确定操作的目标(如用户输入、网络数据),指针提供动态绑定的能力。
- 资源共享与高效性:传递指针避免数据拷贝,提升性能。
总结:钥匙(指针)的意义
| 场景 | 钥匙(指针)的作用 | 直接开门的局限性 |
|---|---|---|
| 动态内存分配 | 操作没有变量名的堆内存 | 无法通过变量名访问动态内存 |
| 运行时决策 | 动态绑定到不同内存地址 | 编译时无法确定目标 |
| 数据共享 | 多个函数操作同一内存 | 函数参数默认传递副本 |
| 复杂数据结构 | 连接动态创建的节点 | 变量名无法表示无限节点 |
| 硬件/系统编程 | 访问固定物理地址 | 物理地址没有变量名 |
指针的“钥匙”本质是解决内存访问的间接性需求。它不是多余的步骤,而是程序灵活性与动态性的关键工具。就像现实世界中,如果所有房间的门都永远敞开且位置固定,钥匙确实多余;但在程序的世界里,内存的分配、共享和动态性让“钥匙”变得不可或缺。
声明和初始化指针:
这样就声明了一个指向整型的指针,当然现在它只是声明但是没有指向具体的某个地址
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是安全的
904

被折叠的 条评论
为什么被折叠?



