C++ 基础入门 - 第四章
前面我们学了变量、类型、表达式、函数、条件判断、循环、数组和字符串。那些内容像是在学习怎么和程序说话。而这一章开始,我们要慢慢接触 C++ 很有代表性的能力:它不仅让你使用数据,还允许你更直接地“接近数据所在的位置”。说得更形象一点。
普通变量像你手里拿着一杯奶茶。
引用像这杯奶茶多了一个昵称,你喊哪个名字,拿到的都是同一杯。
指针则更像一张写着地址的小纸条,上面告诉你:奶茶放在桌子的哪个位置。
提问
假设你写了一个交换两个数字的函数。
#include <iostream>
using namespace std;
void swapValue(int a, int b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int x = 10;
int y = 20;
swapValue(x, y);
cout << x << " " << y << endl;
return 0;
}
你可能以为输出会变成 20 10,但实际还是:
10 20
Why ? 因为这个函数拿到的是 x 和 y 的副本。你在函数里改的是副本,不是原来的变量。
那如果我们希望函数内部真的改到原变量,该怎么办。
答案有两条路。
- 一条是引用。
- 一条是指针。
什么是引用
引用可以理解成一个已经存在变量的别名。最基本的写法是:
int x = 10;
int& r = x;
这里的 r 不是一个新的独立整数变量,而是 x 的另一个名字。
也就是说:
cout << x << endl; // 10
cout << r << endl; // 10
r = 99;
cout << x << endl; // 99
你改 r,本质上就是在改 x。
引用的语法规则
引用有几个非常重要的规则。
- 引用在定义时必须初始化
int x = 10;
int& r = x;
下面这样不行:
int& r; // 错误,引用必须立刻绑定对象
- 引用一旦绑定到某个变量,通常就不能再改绑到别的变量
int a = 10;
int b = 20;
int& r = a;
r = b; // 这不是让 r 改绑到 b,而是把 b 的值赋给 a
执行后:
a == 20
b == 20
- 引用本身一般没有“空引用”这种正常用法。它应该始终代表一个
合法对象
通过引用修改原变量
我们把前面的交换函数改成引用版。
#include <iostream>
using namespace std;
void swapRef(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int x = 10;
int y = 20;
swapRef(x, y);
cout << x << " " << y << endl;
return 0;
}
这次输出就是:
20 10
因为 a 是 x 的别名,b 是 y 的别名。
函数里面改 a 和 b,就是真正在改 x 和 y。
为什么函数参数经常用引用
你以后会经常看到这样的函数:
void printString(const string& s) {
cout << s << endl;
}
这里使用引用通常有两个原因。
- 避免复制,提高效率。如果字符串很长,直接按值传参会复制一份,比较浪费。
- 配合
const,表示函数只读,不修改原对象。
const string& s
意思是: 我不想复制这份字符串,但我也保证不改它。
什么是指针
如果说引用是“别名”,那么指针就是“地址记录员”。指针变量里存放的不是普通值,而是另一个对象在内存中的地址。
先看最基本的例子:
int x = 42;
int* p = &x;
这段代码可以拆成两部分理解。
&x 表示取出 x 的地址。
int* p 表示 p 是一个“指向 int 的指针”。
合起来就是:把 x 的地址保存到指针 p 里。
& 和 * 到底是什么意思
先看 &。
当它出现在变量前面时,通常表示“取地址”。
int x = 10;
cout << &x << endl;
这会输出 x 的地址。
当它出现在类型后面时,表示“引用”。
int& r = x;
再看 *。
当它出现在类型后面时,表示“指针类型”。
int* p;
当它出现在指针变量前面时,表示“解引用”,也就是顺着地址找到那个对象。
int x = 42;
int* p = &x;
cout << *p << endl; // 输出 42
这里的 *p 意思是:
去 p 里存的那个地址,找到对应的整数值。
通过指针修改变量
既然 p 保存了 x 的地址,那么我们也可以通过 p 改 x。
#include <iostream>
using namespace std;
int main() {
int x = 42;
int* p = &x;
cout << x << endl; // 42
cout << *p << endl; // 42
*p = 100;
cout << x << endl; // 100
cout << *p << endl; // 100
return 0;
}
这里的 *p = 100; 不是在改指针本身,而是在改 p 指向的那个整数。
通过指针实现交换
和引用一样,指针也可以让函数修改外部变量。
#include <iostream>
using namespace std;
void swapPointer(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int x = 10;
int y = 20;
swapPointer(&x, &y);
cout << x << " " << y << endl;
return 0;
}
这里要特别注意两层关系。
- 调用时传的是地址:
swapPointer(&x, &y);
- 函数内部通过解引用拿到原变量:
*a
*b
所以指针的思维方式经常是: 先找到地址,再顺着地址找到值。
引用和指针有什么区别
- 引用更像“已经绑定好的别名”,写起来更自然。
- 指针更像“手里拿着地址”,能力更灵活,但心智负担更重。
你可以先这样理解。
int x = 10;
int& r = x; // r 像 x 的另一个名字
int* p = &x; // p 里装的是 x 的地址
使用时的感觉也不一样。
r = 20; // 很像普通变量
*p = 30; // 先通过指针找到对象,再修改
再总结几条常见区别。
- 引用定义时必须绑定对象,指针可以先不指向任何对象
- 引用使用时像普通变量,指针要经常配合
*和& - 指针可以为空,引用通常应始终有效
- 指针可以改为指向别处,引用一般不能重新绑定
什么是空指针
指针可以不指向任何对象,这时常常写成空指针。
int* p = nullptr;
nullptr 是现代 C++ 里专门表示空指针的关键字。它的意思大致是:这里现在没有合法地址。
为什么空指针有用。
因为有时候我们要明确表达:“现在还没有指向任何对象。”
例如:
int* p = nullptr;
if (p != nullptr) {
cout << *p << endl;
}
野指针为什么危险
野指针通常指的是:指针里有一个无效地址,但你还把它当成有效对象去访问。
比如下面这种情况就非常危险:
int* p;
cout << *p << endl; // 危险,p 没初始化
因为 p 没有初始化,它里面可能是乱七八糟的地址。你去解引用它,就相当于闭着眼冲进一栋根本不属于你的房子。
再比如:
int* p = nullptr;
cout << *p << endl; // 也危险,空指针不能解引用
初识内存:变量到底住在哪里
我们经常说变量存放在内存里,但“内存”不是一个单一抽象盒子。初学阶段你可以先粗略地区分两个区域。
- 一个叫栈 stack。
- 一个叫堆 heap。
什么是栈 (stack)
你可以先把栈理解成:函数执行时临时工作的地方。
例如:
int main() {
int x = 10;
int y = 20;
return 0;
}
像这种普通局部变量,通常都可以先理解为在栈上。
它们的特点是:函数开始时创建,函数结束时自动销毁。
所以栈上的对象,通常不需要你手动释放。
什么是堆 (heap)
堆可以先理解成:由程序员主动申请的一块动态内存区域。
例如:
int* p = new int(99);
这句代码的意思是:在堆上创建一个 int,初值为 99,然后把它的地址交给指针 p。
访问它的方法:
cout << *p << endl;
释放它的方法:
delete p;
p = nullptr;
完整示例:
#include <iostream>
using namespace std;
int main() {
int* p = new int(99);
cout << *p << endl;
delete p;
p = nullptr;
return 0;
}
new 和 delete
只要你手动管理内存,就有可能忘记释放、重复释放,或者释放之后还继续使用。
例如下面这些情况都很危险。
int* p = new int(99);
// 忘记 delete,可能造成内存泄漏
int* p = new int(99);
delete p;
delete p; // 重复释放,危险
int* p = new int(99);
delete p;
cout << *p << endl; // 释放后继续使用,危险
这就是为什么现代 C++ 更推荐你优先使用:
stringvectorarray- 智能指针
而不是一上来就沉迷手写 new 和 delete。
一个坑:不要返回局部变量的地址
看这段代码:
int* badFunction() {
int x = 10;
return &x;
}
这段代码很危险。
因为 x 是局部变量,函数结束后它就被销毁了。但你却把它的地址返回出去了。于是外面拿到的是一个已经失效的地址。
这就是经典错误之一。
正确思路通常是:
- 返回值本身
- 让调用者传引用或指针进来
- 用更安全的标准库类型
另一个坑:cin、数组、指针不是一回事
初学者在前几章接触数组和字符串之后,容易产生一种误解:“凡是和内存有关的东西,是不是都得靠指针硬扛。”
不是。
C++ 里很多高频任务你都不需要手写裸指针。
例如:
- 读入一行文本,用
string - 保存一组元素,用
vector - 交换两个整数参数,很多时候直接用引用
你学习指针,不是为了以后所有代码都写成指针风暴,而是为了真正理解数据、地址、对象和内存之间的关系。
案例练习
例子一:用引用让函数返回两个结果
有时候一个函数不只想返回一个值。我们可以用引用参数把结果带出去。
#include <iostream>
using namespace std;
void divide(int a, int b, int& quotient, int& remainder) {
quotient = a / b;
remainder = a % b;
}
int main() {
int q, r;
divide(17, 5, q, r);
cout << "商: " << q << endl;
cout << "余数: " << r << endl;
return 0;
}
输出:
商: 3
余数: 2
这个例子展示了引用参数的一个典型用途:不仅能修改原变量,还能让函数“带回多个结果”。
例子二:用指针遍历数组
前面我们学过数组,这里可以用指针重新看一遍数组。
#include <iostream>
using namespace std;
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int* p = arr;
for (int i = 0; i < 5; i++) {
cout << *(p + i) << " ";
}
cout << endl;
return 0;
}
这里的 p 指向数组首元素。
p == &arr[0]
所以:
*(p + 0) // arr[0]
*(p + 1) // arr[1]
*(p + 2) // arr[2]
数组和指针关系密切,但它们不是同一个东西。
例子三:观察值传递、引用传递、指针传递
#include <iostream>
using namespace std;
void addValue(int x) {
x++;
}
void addRef(int& x) {
x++;
}
void addPointer(int* x) {
(*x)++;
}
int main() {
int a = 5;
int b = 5;
int c = 5;
addValue(a);
addRef(b);
addPointer(&c);
cout << a << endl;
cout << b << endl;
cout << c << endl;
return 0;
}
输出:
5
6
6
这个例子很适合反复看几遍。
它会帮你彻底理解三件事。
- 按值传递改的是副本。
- 按引用传递改的是原对象。
- 按指针传递本质上也是通过地址改原对象。
初学者最容易犯的错误
- 引用没有初始化
int& r; // 错误
- 忘了给指针合法地址
int* p;
*p = 10; // 危险
- 把普通值误当地址
int x = 10;
int* p = x; // 错误,x 是值,不是地址
- 忘记解引用
int x = 10;
int* p = &x;
cout << p << endl; // 输出地址,不是 10
cout << *p << endl; // 才是 10
- 把
=和==混淆之后,又在指针判断里踩坑
if (p = nullptr) {
// 这是赋值,不是比较
}
正确写法:
if (p == nullptr) {
}
- 释放后不置空
delete p;
p = nullptr;
这不是万能保险,但至少能让你更容易避免后续误用。
小练习:你能猜出输出吗
#include <iostream>
using namespace std;
void change1(int x) {
x = 100;
}
void change2(int& x) {
x = 200;
}
void change3(int* x) {
*x = 300;
}
int main() {
int a = 10;
int b = 10;
int c = 10;
change1(a);
change2(b);
change3(&c);
cout << a << " " << b << " " << c << endl;
return 0;
}
答案是:
10 200 300
小练习:下面哪一行危险
int x = 10;
int* p1 = &x;
int* p2 = nullptr;
int* p3;
答案是:
p1安全,指向合法对象p2可以,空指针本身不危险,乱解引用才危险p3最危险,因为它未初始化
练习题
1. 写一个函数,使用引用参数交换两个 double 类型变量
2. 写一个函数,接收 int* 指针,如果指针不为空,就把它指向的值乘以 2
3. 写一个程序,定义一个整型数组,分别用下标和指针两种方式输出数组内容
4. 写一个函数,接收一个 string 的 const 引用,输出它的长度
5. 尝试写出一个会产生野指针风险的例子,并说明为什么危险
本章小结
- 引用像别名,使用自然,常用于函数参数
- 指针保存地址,解引用后才能访问对象
nullptr表示空指针- 未初始化指针和非法解引用非常危险
- 栈上对象通常自动管理,堆上对象需要更小心
- 现代 C++ 中,优先使用标准库和更安全的写法