本章目标是数据结构课的地基:把指针的 const 组合分清、搞懂数组退化、会用 new/delete、理解 RAII 和智能指针——这是写链表、树、图时不担心内存泄漏的关键。
4.1 指针与 const
心智模型:指针是“门牌号”,告诉你东西在哪。问题在于——这个门牌号本身能不能改?门牌号指向的东西能不能改?
const 的四种组合
const int* p:指向常量的指针,不能通过 p 改值,但能改指向;int* const p:常量指针,能改值,但不能改指向;const int* const p:两者都不能;int* p:都能。记不住就往回读:const 默认修饰它左边最近的东西,左边没东西就修饰右边的词。
int a = 1, b = 2;
const int* p = &a; // 不能 *p = 20(改值),但 p = &b 合法
int* const q = &a; // *q = 30 合法,但 q = &b 编译错误
空指针用 nullptr
nullptr 是类型安全的空指针(C++11)。NULL 在 C++ 里通常是整数 0,在重载解析时会挑错函数。既然写 C++,就用 nullptr。
✍️ 本节练习
写一个函数 void swapPtr(int* a, int* b) 交换两个 int(用指针解引用),main 里读两个数,传入地址调用,输出交换结果。
输入 一行两个整数 a b。
输出 交换后的 a b。
3 55 3💡 提示 调用时传 &x、&y;函数里用 *a 解引用。先判空 if (!a || !b) return;
✅ 查看参考答案与解析
#include <iostream>
void swapPtr(int* a, int* b) {
if (!a || !b) { return; }
int t = *a;
*a = *b;
*b = t;
}
int main() {
int x, y;
std::cin >> x >> y;
swapPtr(&x, &y);
std::cout << x << ' ' << y << '\n';
return 0;
}
解析 指针可能为空,使用前先判空;&x 取地址,*a 解引用。
写程序验证:const int* p 不能通过 p 修改值但能改指向;int* const q 能改值但不能改指向。让程序在“非法操作”的地方用注释标出,只保留合法操作,最后输出 a 和 b 的值。
输入 无
输出 两行,a 的值、b 的值。
(本题无输入)30
2💡 提示 下面参考代码里被注释的行就是非法操作,手敲时试着打开注释看看编译器报什么错。
✅ 查看参考答案与解析
#include <iostream>
int main() {
int a = 1, b = 2;
const int* p = &a; // 指向常量的指针:不能改 *p
// *p = 20; // 非法:编译错误
p = &b; // 合法:可以改指向
std::cout << *p << '\n'; // 2:读一下,避免“变量未使用”警告
int* const q = &a; // 常量指针:不能改指向
*q = 30; // 合法:可以改值
// q = &b; // 非法:编译错误
std::cout << a << '\n'; // 30
std::cout << b << '\n'; // 2
return 0;
}
解析 const int* 修饰的是 *p(值);int* const 修饰的是 p 本身(指向)。
4.2 数组、字符串与 vector
数组是 C++ 里最容易“静悄悄出错”的东西。核心教训:能用 vector 就别用原生数组。
数组会退化为指针
数组不是指针,但传参、赋值给指针时退化为指向首元素的指针。最经典的陷阱:在定义数组的作用域里 sizeof(a) 是数组总大小;一旦退化成指针,sizeof 就变成指针大小(8 字节)。
#include <iostream>
void f(int* arr) { // 写成 int arr[] 也等价
std::cout << sizeof(arr); // 8:指针大小,不是数组大小
}
int main() {
int a[5]{1, 2, 3, 4, 5};
std::cout << sizeof(a) / sizeof(a[0]); // 5:元素个数
}
选择标准:能用容器就别用数组
长度在编译期就确定、且不需要在函数之间传递,才考虑原生数组;其他情况一律用 std::vector。数据结构课的实现里几乎只会用到 vector 和 std::array。
字符串:用 std::string,别用 char*
字符串字面量是 const char[N],存只读区,绝不能通过指针修改;写 char* p = "abc"; 在 C++11 起是编译错误。std::string 值语义、自动管理内存、可以拼接修改。传参用 const std::string&(Java 传引用副本没有这个开销,C++ 里 string 传值会拷贝)。
std::string s = "hello";
s += ", world";
std::cout << s.substr(0, 5) << ' ' << s.find("world"); // hello 7
int n = std::stoi("42"); // 字符串转数字
std::string t = std::to_string(3.14); // 数字转字符串
✍️ 本节练习
读入 n 个整数存入 std::vector,输出其中的最大值。
输入 第一行 n;第二行 n 个整数。
输出 最大值。
5
3 7 2 9 19💡 提示 遍历用 for (int x : v)。
✅ 查看参考答案与解析
#include <iostream>
#include <vector>
int main() {
int n;
std::cin >> n;
std::vector<int> v(n);
for (int i = 0; i < n; ++i) { std::cin >> v[i]; }
int mx = v[0];
for (int x : v) { if (x > mx) { mx = x; } }
std::cout << mx << '\n';
return 0;
}
解析 动态长度优先 vector;范围 for 只读遍历最简洁。
读入一行字符串(不含空格),输出反转后的字符串。要求先用 std::reverse 做一遍,再手写循环做一遍(两行输出)。
输入 一行字符串(不含空格,长度 ≤ 100)。
输出 两行,都是反转后的字符串。
helloolleh
olleh💡 提示 std::reverse(s.begin(), s.end()) 需要 <algorithm>。
✅ 查看参考答案与解析
#include <algorithm>
#include <iostream>
#include <string>
int main() {
std::string s;
std::cin >> s;
std::string t = s;
std::reverse(t.begin(), t.end());
std::cout << t << '\n';
for (int i = 0, j = (int)s.size() - 1; i < j; ++i, --j) {
std::swap(s[i], s[j]);
}
std::cout << s << '\n';
return 0;
}
解析 双指针首尾交换是手写反转的标准写法;std::swap 在 <utility>(algorithm 也会带进来)。
4.3 动态内存:new 与 delete
你在 C 里学过 malloc/free,在 Java 里完全不用管内存。C++ 在中间:用 new/delete 手动管理,但有一条铁律必须守住。
new / delete 的配对规则
new 分配单个对象、delete 释放;new[] 分配数组、delete[] 释放(必须配对)。new Node{1, nullptr} 分配结构体并列表初始化。两套内存函数绝对不能混用:new 出来的不能 free,malloc 出来的不能 delete,混用=未定义行为,程序经常在毫不相关的地方崩溃。
int* p = new int(42); delete p;
int* arr = new int[10](); delete[] arr;
Node* head = new Node{1, nullptr}; delete head;
内存分区:对象放在哪、什么时候销毁
程序运行起来,内存分成几个区:栈(局部变量,离开作用域自动销毁)、堆(new 出来的,手动管理)、静态区(全局/静态变量,程序结束时销毁)、代码区(机器码)、字面量区(只读)。理解它们的分工,才能回答“这个对象什么时候被销毁”。
✍️ 本节练习
用 new 分配一个 5 个 int 的动态数组并初始化为 {1,2,3,4,5},求和输出,最后用正确方式释放。
输入 无
输出 15
(本题无输入)15💡 提示 new int[5]{1,2,3,4,5};释放用 delete[] arr;
✅ 查看参考答案与解析
#include <iostream>
int main() {
int* arr = new int[5]{1, 2, 3, 4, 5};
int sum = 0;
for (int i = 0; i < 5; ++i) { sum += arr[i]; }
std::cout << sum << '\n';
delete[] arr;
return 0;
}
解析 new[] 和 delete[] 必须配对;混用 free/delete 是未定义行为。
读入 n,用 new 分配 n 个 int 的动态数组,读入 n 个数,排序后输出,最后释放。排序用 std::sort。
输入 第一行 n;第二行 n 个整数。
输出 升序排列,空格分隔。
5
5 3 8 1 21 2 3 5 8💡 提示 std::sort(arr, arr + n)。
✅ 查看参考答案与解析
#include <algorithm>
#include <iostream>
int main() {
int n;
std::cin >> n;
int* arr = new int[n];
for (int i = 0; i < n; ++i) { std::cin >> arr[i]; }
std::sort(arr, arr + n);
for (int i = 0; i < n; ++i) {
std::cout << arr[i] << (i + 1 == n ? '\n' : ' ');
}
delete[] arr;
return 0;
}
解析 指针算术 arr + n 是合法的(同一数组内);删除后不要再碰 arr。
4.4 RAII 与智能指针
RAII 是 C++ 里最重要的编程思想,没有之一。心智模型:把资源的生命周期绑定到对象的生命周期上——构造时获取资源,析构时释放资源。因为栈对象的析构是编译器保证的,所以资源释放也就被保证了。
RAII:资源跟着对象走
不管你是正常返回、提前 return、break 跳出循环,还是抛了异常,栈对象的析构一定会被调用。把“释放内存”“关闭文件”“解锁互斥量”写进析构函数,它们就必然发生。标准库的容器、智能指针、lock_guard 全是 RAII。
unique_ptr:独占所有权,第一选择
std::unique_ptr 独占所有权:不能拷贝只能移动,没有额外开销。链表、树、图里的“拥有”关系用它最合适——head 析构时递归释放 next,整条链自动回收,不需要任何 delete。
struct Node {
int value = 0;
std::unique_ptr<Node> next;
};
auto head = std::make_unique<Node>();
head->value = 1;
head->next = std::make_unique<Node>();
// 离开作用域时整条链自动释放
shared_ptr / weak_ptr:共享与观察
shared_ptr 用引用计数,最后一个引用消失时释放;拷贝使计数 +1,use_count() 查看。只在确实需要共享所有权时才用(有原子操作开销)。weak_ptr 不增加计数,用来观察或打破循环引用。
std::shared_ptr<Node> p = std::make_shared<Node>();
std::shared_ptr<Node> q = p; // 计数 2
std::cout << p.use_count(); // 2
std::weak_ptr<Node> w = p; // 不增加计数
✍️ 本节练习
用 std::unique_ptr 建两个链表节点(Node 含 int value 和 std::unique_ptr<Node> next),输出两个节点的值。整个程序不允许出现 delete。
输入 无
输出 1 2
(本题无输入)1 2💡 提示 std::make_unique<Node>();head->next = std::make_unique<Node>();
✅ 查看参考答案与解析
#include <iostream>
#include <memory>
struct Node {
int value = 0;
std::unique_ptr<Node> next;
};
int main() {
auto head = std::make_unique<Node>();
head->value = 1;
head->next = std::make_unique<Node>();
head->next->value = 2;
std::cout << head->value << ' ' << head->next->value << '\n';
return 0; // 离开作用域自动释放整条链
}
解析 unique_ptr 析构时递归释放 next,整条链自动回收,这就是 RAII。
用 shared_ptr 创建对象,拷贝两次后输出 use_count(),再让一个引用离开作用域,再输出一次 use_count(),观察引用计数变化。
输入 无
输出 两行:3、2。
(本题无输入)3
2💡 提示 std::make_shared<Node>();拷贝使计数 +1。
✅ 查看参考答案与解析
#include <iostream>
#include <memory>
struct Node { int value = 0; };
int main() {
std::shared_ptr<Node> a = std::make_shared<Node>();
std::shared_ptr<Node> b = a; // 计数 2
{
std::shared_ptr<Node> c = a; // 计数 3
std::cout << a.use_count() << '\n'; // 3
} // c 离开作用域,计数 2
std::cout << a.use_count() << '\n'; // 2
return 0;
}
解析 shared_ptr 引用计数归零才释放;要共享所有权就从同一个指针拷贝,绝不用裸指针 new 两次。
本章小结
- const int* 修饰值、int* const 修饰指向;int *pa, *pb 才两个都是指针。
- 原生数组不检查越界、传参会退化;能用 vector 就别用原生数组。
- new 配 delete、new[] 配 delete[],两套内存函数绝不混用。
- RAII:资源绑定生命周期;优先 unique_ptr,需要共享才用 shared_ptr。
🧩 本章综合练习
这几道题把本章多个知识点串起来,建议合上资料独立完成,再展开答案对照。
用 std::unique_ptr<int[]> 管理动态数组(全程不写 delete);写 void reverse(int* first, int* last) 按左闭右开区间就地反转;再写 long long sum(const int* first, const int* last) 求和。输出反转后的数组与总和。
输入 第一行整数 n;第二行 n 个整数。
输出 第一行反转后的数组(空格分隔);第二行 sum=总和。
5
1 2 3 4 55 4 3 2 1
sum=15💡 提示 区间用左闭右开 [first,last):last 指向最后一个元素的下一位;unique_ptr<int[]> 用 get() 取裸指针传给函数。
✅ 查看参考答案与解析
#include <algorithm>
#include <iostream>
#include <memory>
void reverse(int* first, int* last) { // [first, last)
if (first == last) { return; }
--last; // 变成闭区间再交换
while (first < last) {
std::swap(*first, *last);
++first;
--last;
}
}
long long sum(const int* first, const int* last) {
long long s = 0;
for (const int* p = first; p != last; ++p) { s += *p; }
return s;
}
int main() {
int n{};
std::cin >> n;
std::unique_ptr<int[]> a(new int[n]);
for (int i = 0; i < n; ++i) { std::cin >> a[i]; }
reverse(a.get(), a.get() + n);
for (int i = 0; i < n; ++i) { std::cout << a[i] << (i + 1 == n ? '\n' : ' '); }
std::cout << "sum=" << sum(a.get(), a.get() + n) << '\n';
return 0;
}
解析 裸指针只在函数内部流转,所有权始终在 unique_ptr 手里;这样即使中间抛异常也不会泄漏。
指针 · const 与 const 正确性 · 栈与堆 · RAII · 智能指针