ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C++指针从入门到崩溃?一篇文章彻底讲透

C++指针从入门到崩溃?一篇文章彻底讲透 1. 为什么指针是C的“劝退之王”很多初学者在学完基础语法后一碰到指针就开始怀疑人生指针到底是什么它和变量有什么区别为什么用不好就会段错误、内存泄漏、悬空指针其实指针并没有想象中那么可怕。它本质上就是一个保存内存地址的变量。你之所以觉得难是因为它把“值”和“地址”两个概念叠加在了一起再加上内存管理、引用、智能指针等一堆衍生概念很容易让人绕晕。这篇文章会从最底层的“内存地址”讲起一步步带你理解指针的本质、常见用法、经典陷阱以及现代C中如何用智能指针优雅地替代裸指针。读完你会发现指针不是玄学而是C最强大的工具之一。2. 先搞懂内存地址指针的地基在理解指针之前必须先理解一个核心概念程序运行时变量是存放在内存中的而内存中的每一个字节都有一个编号这个编号就是内存地址。你可以把内存想象成一排带编号的储物柜。每个柜子能放一定大小的数据柜子的编号就是地址。当你声明一个变量时编译器会为你分配一个或多个柜子并把变量名和这些柜子的编号关联起来。2.1 取地址运算符 与解引用运算符 *在C中有两个运算符是理解指针的关键取地址运算符获取变量在内存中的起始地址。*解引用运算符根据地址访问该地址上存储的数据。2.2 第一个指针程序#include iostream int main() { int age 25; // 声明一个普通变量 int* ptr age; // 声明一个指针保存 age 的地址 std::cout age 的值 age std::endl; std::cout age 的地址 age std::endl; std::cout ptr 保存的地址 ptr std::endl; std::cout 通过 ptr 解引用得到 *ptr std::endl; *ptr 30; // 通过指针修改 age 的值 std::cout 修改后 age 的值 age std::endl; return 0; }运行这段代码你会发现ptr保存的地址和age完全一致而*ptr取出的值就是age的值。这就是指针最核心的机制通过地址间接访问变量。2.3 指针的声明语法指针的声明语法是类型* 变量名其中类型表示“这个指针指向什么类型的数据”。例如int* p指向int的指针double* p指向double的指针char* p指向char的指针void* p无类型指针可以指向任意类型但不能直接解引用3. 指针的核心操作赋值、解引用与算术掌握了基础语法后接下来要理解指针的三种核心操作赋值、解引用和指针算术。3.1 指针的赋值指针只能保存“同类型变量的地址”否则编译器会报错除非使用void*或强制类型转换。例如int a 10; double b 3.14; int* p1 a; // 正确p1 指向 int // int* p2 b; // 错误b 是 double*不能赋给 int* void* p3 b; // 正确void* 可以保存任意地址 // *p3 1.0; // 错误void* 不能直接解引用3.2 解引用与空指针解引用一个指针前必须确保它指向有效的内存。如果指针是nullptr空指针解引用会导致程序崩溃段错误。int* p nullptr; // *p 42; // 危险解引用空指针会导致崩溃 if (p ! nullptr) { *p 42; } else { std::cout 指针为空不能解引用 std::endl; }3.3 指针算术数组与指针的亲密关系指针支持加减运算但加减的单位是“指向类型的大小”而不是1个字节。这一点在遍历数组时非常有用#include iostream int main() { int arr[] {10, 20, 30, 40, 50}; int* p arr; // 数组名会退化为指向首元素的指针 for (int i 0; i 5; i) { std::cout arr[ i ] *(p i) std::endl; } // 指针也可以自增遍历 int* q arr; while (q ! arr 5) { std::cout *q ; q; } std::cout std::endl; return 0; }注意p 1并不是地址加1而是加sizeof(int)个字节通常是4字节这样*(p 1)才能正确访问下一个int元素。4. 指针与函数传值、传址与引用指针最常见的用途之一就是作为函数参数实现“在函数内部修改外部变量”的效果。4.1 传值 vs 传址#include iostream void swap_by_value(int a, int b) { int tmp a; a b; b tmp; // 这里修改的是副本不影响外部变量 } void swap_by_pointer(int* a, int* b) { int tmp *a; *a *b; *b tmp; // 通过地址修改外部变量 } void swap_by_reference(int a, int b) { int tmp a; a b; b tmp; // 引用是别名直接修改外部变量 } int main() { int x 1, y 2; swap_by_value(x, y); std::cout 传值后x x , y y std::endl; // 1, 2 swap_by_pointer(x, y); std::cout 传址后x x , y y std::endl; // 2, 1 swap_by_reference(x, y); std::cout 传引用后x x , y y std::endl; // 1, 2 return 0; }4.2 指针与引用的区别对比项指针引用是否可以为空可以nullptr不可以必须初始化是否可以重新指向可以随时改变指向不可以绑定后不能改变是否需要解引用需要*不需要直接使用内存占用占用一个指针大小的空间通常不额外占用空间编译器优化适用场景动态内存、数组遍历、可选参数函数参数、运算符重载、避免拷贝4.3 函数指针把函数当作参数传递函数指针是指向函数的指针可以用来实现回调机制#include iostream int add(int a, int b) { return a b; } int multiply(int a, int b) { return a * b; } // 函数指针作为参数 int compute(int x, int y, int (*func)(int, int)) { return func(x, y); } int main() { int (*fp)(int, int) add; // 声明并初始化函数指针 std::cout add(3, 4) fp(3, 4) std::endl; std::cout compute(3, 4, add) compute(3, 4, add) std::endl; std::cout compute(3, 4, multiply) compute(3, 4, multiply) std::endl; return 0; }5. 动态内存new 与 delete 的正确姿势指针最强大的能力是配合new和delete在堆上动态分配内存。堆内存的生命周期由程序员控制这也是内存泄漏和悬空指针的“重灾区”。5.1 基本用法#include iostream int main() { // 分配单个变量 int* p new int(42); std::cout *p *p std::endl; delete p; // 释放内存 p nullptr; // 置空避免悬空指针 // 分配数组 int* arr new int[5]; for (int i 0; i 5; i) { arr[i] i * i; } for (int i 0; i 5; i) { std::cout arr[i] ; } std::cout std::endl; delete[] arr; // 释放数组要用 delete[] return 0; }5.2 经典陷阱内存泄漏与悬空指针内存泄漏new了却没有delete导致内存无法回收。程序长期运行会耗尽内存。悬空指针delete之后没有把指针置空指针仍然保存着已释放的地址。再次解引用是未定义行为。重复释放对同一个指针delete两次会导致程序崩溃。int* p new int(10); delete p; // p 现在是悬空指针 // delete p; // 错误重复释放 p nullptr; // 正确做法置空 delete p; // 对 nullptr 执行 delete 是安全的5.3 用智能指针告别手动管理现代C强烈推荐使用智能指针替代裸指针让内存管理自动化#include iostream #include memory int main() { // unique_ptr独占所有权不能拷贝 std::unique_ptrint uptr std::make_uniqueint(100); std::cout *uptr *uptr std::endl; // shared_ptr共享所有权引用计数 std::shared_ptrint sptr1 std::make_sharedint(200); std::shared_ptrint sptr2 sptr1; std::cout use_count sptr1.use_count() std::endl; // weak_ptr弱引用不增加引用计数用于打破循环引用 std::weak_ptrint wptr sptr1; if (auto sp wptr.lock()) { std::cout weak_ptr 有效*sp *sp std::endl; } return 0; // 离开作用域时智能指针自动释放内存 }6. 指针进阶多级指针、const 与 void*掌握了基础之后再来看几个容易让人“崩溃”的进阶概念。6.1 多级指针指向指针的指针二级指针保存的是“一级指针的地址”常用于在函数中修改指针本身#include iostream void allocate(int** pp) { *pp new int(99); // 修改外部指针的指向 } int main() { int* p nullptr; allocate(p); // 传入指针的地址 std::cout *p *p std::endl; delete p; return 0; }6.2 const 与指针的三种组合写法含义能否修改指向能否修改值const int* p指向 const int 的指针可以不可以int* const pconst 指针指向 int不可以可以const int* const pconst 指针指向 const int不可以不可以int a 10, b 20; const int* p1 a; // 指向常量的指针 // *p1 30; // 错误不能通过 p1 修改值 p1 b; // 正确可以改变指向 int* const p2 a; // 常量指针 *p2 30; // 正确可以修改值 // p2 b; // 错误不能改变指向 const int* const p3 a; // 双重 const // *p3 30; // 错误 // p3 b; // 错误6.3 void* 与类型转换void*可以保存任意类型的地址但使用前必须转换回具体类型int a 42; void* vp a; // 必须转换回 int* 才能解引用 int* ip
返回列表