第九章:深入理解内存——指针、引用和动态内存¶
你好!欢迎来到第九章!在前面的章节,我们学习了变量、数组、函数和结构体。你有没有想过,变量在内存中到底存放在哪里?我们如何直接操作内存地址?这一章我们将学习C++中最强大也最需要小心的特性——指针。同时还会学习它的好兄弟引用,以及如何动态地分配内存。学完本章,你就能更深入地理解计算机内存的运作方式!
9.1 指针程序范例¶
先来看一个最简单的指针程序:定义一个整数变量,然后用指针指向它,通过指针修改它的值。
#include <iostream>
using namespace std;
int main() {
int a = 10; // 定义一个普通变量
int *p; // 定义一个指针变量,它可以存放整数的地址
p = &a; // 把a的地址赋值给p,现在p指向a
cout << "a的值 = " << a << endl;
cout << "a的地址 = " << &a << endl; // &是取地址运算符
cout << "p的值 = " << p << endl; // p存的就是a的地址
cout << "p指向的值 = " << *p << endl; // *是间接访问运算符,得到p指向的内容
*p = 20; // 通过指针修改a的值
cout << "修改后a的值 = " << a << endl;
return 0;
}
运行结果(地址会因运行环境不同而变化):
a的值 = 10
a的地址 = 0x61ff08
p的值 = 0x61ff08
p指向的值 = 10
修改后a的值 = 20
这个程序展示了指针的基本操作:&取地址,*解引用。就像你可以通过门牌号找到房子,指针就是内存地址,通过它可以访问变量。
9.2 指针的用法¶
9.2.1 指针的概念¶
指针是一个变量,它存储的是另一个变量的内存地址。简单说,指针就是地址的“容器”。每个变量在内存中都有一个地址,就像每间教室都有一个门牌号。
&:取地址运算符,获得变量的地址。*:间接访问运算符(解引用),通过地址访问该地址存储的值。
9.2.2 指针的定义和赋值¶
定义指针的格式:类型 *指针名;
- 例如:int *p; 表示p是一个指向整数的指针。
- double *dp; 表示指向double的指针。
- char *cp; 表示指向字符的指针。
指针必须初始化,否则会变成野指针(指向随机地址),非常危险。
int a = 5;
int *p = &a; // 定义时初始化
int *q; // 未初始化,危险!
q = &a; // 之后赋值也可以
9.2.3 指针的运算¶
指针可以像整数一样进行加减运算,但它的加减是以指向的类型大小为单位的。
int a[5] = {10, 20, 30, 40, 50};
int *p = &a[0]; // 指向第一个元素
cout << "*p = " << *p << endl; // 输出10
cout << "*(p+1) = " << *(p+1) << endl; // 输出20,p+1指向下一个整数
指针还可以做减法,得到两个指针之间的元素个数。
9.2.4 指针与数组¶
数组名在大多数情况下可以看作指向第一个元素的指针。
int a[5] = {1, 2, 3, 4, 5};
int *p = a; // 等价于 p = &a[0]
for (int i = 0; i < 5; i++) {
cout << *(p + i) << " "; // 用指针访问数组
}
但数组名不是真正的指针,它是常量,不能修改。
9.2.5 指针与函数¶
指针作为函数参数¶
通过传递指针,函数可以修改调用者的变量(因为知道了地址)。
#include <iostream>
using namespace std;
void swap(int *x, int *y) {
int temp = *x;
*x = *y;
*y = temp;
}
int main() {
int a = 3, b = 5;
swap(&a, &b); // 传递地址
cout << "a = " << a << ", b = " << b << endl; // a=5, b=3
return 0;
}
指针作为函数返回值¶
函数可以返回指针,但要小心不能返回局部变量的地址(因为函数结束局部变量就销毁了)。
int* findMax(int *arr, int size) {
int *max = arr;
for (int i = 1; i < size; i++) {
if (arr[i] > *max) {
max = &arr[i];
}
}
return max; // 返回指向最大元素的指针(安全,因为数组生命周期还在)
}
9.2.6 空指针和野指针¶
-
空指针:指向空地址的指针,用
nullptr(C++11)或NULL表示。解引用空指针会导致程序崩溃。
cpp int *p = nullptr; // 推荐使用nullptr if (p != nullptr) { *p = 10; // 安全 } -
野指针:未初始化或指向已释放内存的指针,使用它非常危险,可能导致程序崩溃或数据损坏。一定要避免!初始化指针是最好的预防。
9.3 引用¶
9.3.1 引用的概念¶
引用是给变量起的一个别名,它和原变量共享同一块内存。定义引用时,必须初始化,而且不能改变指向。
int a = 10;
int &b = a; // b是a的引用,b就是a的别名
b = 20; // 修改b相当于修改a
cout << a; // 输出20
9.3.2 引用的用法¶
引用常用于函数参数,避免拷贝,并且可以直接修改实参。
#include <iostream>
using namespace std;
void swap(int &x, int &y) { // 引用参数
int temp = x;
x = y;
y = temp;
}
int main() {
int a = 3, b = 5;
swap(a, b); // 直接传变量,不需要取地址
cout << "a = " << a << ", b = " << b << endl; // a=5, b=3
return 0;
}
引用也可以作为函数返回值,但同样不能返回局部变量的引用。
9.3.3 引用与指针的区别¶
| 特性 | 指针 | 引用 |
|---|---|---|
| 初始化 | 可以不初始化(但危险) | 必须初始化 |
| 可修改 | 可以改变指向其他变量 | 一旦绑定,不能改变 |
| 访问方式 | 用 * 解引用 |
直接像普通变量一样用 |
| 空值 | 可以为 nullptr |
不能为空 |
| 用途 | 动态内存、数组操作等 | 函数参数、操作符重载等 |
简单说:引用是更安全、更方便的指针,但功能稍弱。
9.4 动态内存分配¶
有时候我们不知道程序运行时会需要多少内存(比如需要根据用户输入决定数组大小),这时就需要动态内存分配。C++中用 new 和 delete 来申请和释放内存。
9.4.1 new和delete¶
new在堆上分配内存,返回指向该内存的指针。delete释放由new分配的内存。
int *p = new int; // 分配一个整数大小的内存
*p = 10;
cout << *p << endl;
delete p; // 释放内存,避免内存泄漏
p = nullptr; // 将指针置空,避免野指针
也可以初始化:
int *p = new int(20); // 分配并初始化为20
9.4.2 动态数组¶
用 new[] 分配数组,用 delete[] 释放。
int n;
cout << "请输入数组大小:";
cin >> n;
int *arr = new int[n]; // 动态分配数组
for (int i = 0; i < n; i++) {
arr[i] = i * 10;
}
for (int i = 0; i < n; i++) {
cout << arr[i] << " ";
}
cout << endl;
delete[] arr; // 释放数组内存
arr = nullptr;
9.4.3 内存泄漏¶
如果 new 分配了内存,但忘记 delete,这块内存在程序结束前都无法再使用,造成内存泄漏。程序运行时间越长,占用的内存越多,最终可能崩溃。所以一定要成对使用 new/delete 或 new[]/delete[]。
重要规则:
- new 对应 delete。
- new[] 对应 delete[]。
- 不要重复 delete。
- 释放后将指针置空。
9.5 编程实例讲解¶
实例1:用指针实现字符串复制(C风格)¶
#include <iostream>
using namespace std;
void stringCopy(char *dest, const char *src) {
while (*src != '\0') {
*dest = *src;
dest++;
src++;
}
*dest = '\0'; // 添加字符串结束符
}
int main() {
char s1[100] = {0};
char s2[] = "Hello, C++!";
stringCopy(s1, s2);
cout << "复制后的字符串:" << s1 << endl;
return 0;
}
实例2:动态创建结构体¶
#include <iostream>
#include <string>
using namespace std;
struct Student {
string name;
int age;
double score;
};
int main() {
Student *p = new Student; // 动态分配一个学生结构体
p->name = "小明"; // 用->访问成员,等价于 (*p).name
p->age = 12;
p->score = 98.5;
cout << "姓名:" << p->name << ",年龄:" << p->age << ",成绩:" << p->score << endl;
delete p; // 释放内存
p = nullptr;
return 0;
}
实例3:动态数组求平均值¶
#include <iostream>
using namespace std;
int main() {
int n;
cout << "请输入学生人数:";
cin >> n;
double *scores = new double[n];
double sum = 0;
for (int i = 0; i < n; i++) {
cout << "请输入第" << i+1 << "个学生的成绩:";
cin >> scores[i];
sum += scores[i];
}
double avg = sum / n;
cout << "平均分:" << avg << endl;
delete[] scores;
scores = nullptr;
return 0;
}
实例4:用引用交换两个数(对比指针)¶
#include <iostream>
using namespace std;
void swapByPointer(int *x, int *y) {
int temp = *x;
*x = *y;
*y = temp;
}
void swapByReference(int &x, int &y) {
int temp = x;
x = y;
y = temp;
}
int main() {
int a = 3, b = 5;
swapByPointer(&a, &b);
cout << "a=" << a << ", b=" << b << endl; // 5,3
int c = 10, d = 20;
swapByReference(c, d);
cout << "c=" << c << ", d=" << d << endl; // 20,10
return 0;
}
9.6 第9章编程作业¶
恭喜你学完了指针、引用和动态内存!现在来挑战几个综合题目。
作业1:动态数组排序¶
编写程序,让用户输入数组大小n,然后动态分配数组,输入n个整数,用冒泡排序(用指针操作数组)排序后输出,最后释放内存。
作业2:字符串统计(用指针)¶
编写函数 int countWords(const char *str),统计字符串中单词的个数(单词之间用空格分隔)。在 main 中测试。
作业3:动态学生信息管理¶
用结构体 Student 和动态内存实现:
- 输入学生人数n
- 动态创建学生数组
- 输入每个学生的姓名、年龄、成绩
- 按成绩从高到低排序(用指针或引用实现交换)
- 输出排序后的结果
- 释放内存
作业4:引用与指针对比¶
写三个函数,分别用传值、传指针、传引用的方式实现一个函数 increment,将参数增加1。在 main 中测试,并观察原变量的变化。
作业5:动态二维数组(选做)¶
动态创建一个 m×n 的二维数组(用指针的指针),输入矩阵并计算转置。注意释放内存。
好了,第九章的内容就到这里!指针是C++中最灵活也最复杂的部分,需要多加练习才能熟练掌握。记住:指针一定要初始化,new和delete要成对出现。下一章我们将学习更高级的内容——类和对象,进入面向对象编程的世界。加油!🚀