目录
[2.1 指针作为函数参数](#2.1 指针作为函数参数)
[2.2 引用作为函数参数](#2.2 引用作为函数参数)
[2.3 指针传参和引用传参的区别](#2.3 指针传参和引用传参的区别)
[三、const 修饰指针和引用时到底限制谁](#三、const 修饰指针和引用时到底限制谁)
[3.1 const int* x:不能修改指向的数据,但可以修改指针指向](#3.1 const int* x:不能修改指向的数据,但可以修改指针指向)
[3.2 int* const x:不能修改指针保存的地址,但可以修改指向的数据](#3.2 int* const x:不能修改指针保存的地址,但可以修改指向的数据)
[3.3 const int* const x:既不能修改值,也不能修改地址](#3.3 const int* const x:既不能修改值,也不能修改地址)
[3.4 三种写法对比总结](#3.4 三种写法对比总结)
[3.5 const int& x:不能修改引用绑定的变量](#3.5 const int& x:不能修改引用绑定的变量)
[3.6 指针和引用中 const 的最终理解](#3.6 指针和引用中 const 的最终理解)
[4.1 普通值传参会产生复制](#4.1 普通值传参会产生复制)
[4.2 使用 const string& 避免复制](#4.2 使用 const string& 避免复制)
[4.3 const char* 接收 C 风格字符串](#4.3 const char* 接收 C 风格字符串)
[4.4 右值引用 string&&](#4.4 右值引用 string&&)
[5.1 指针作为参数返回多个值](#5.1 指针作为参数返回多个值)
[5.2 引用作为参数返回多个值](#5.2 引用作为参数返回多个值)
[5.3 指针作为函数返回值](#5.3 指针作为函数返回值)
[5.4 引用作为函数返回值](#5.4 引用作为函数返回值)
[5.5 不能返回普通局部变量的地址或引用](#5.5 不能返回普通局部变量的地址或引用)
[6.1 完整代码](#6.1 完整代码)
[6.2 本节核心总结](#6.2 本节核心总结)
目录
一、本节学习内容概要
二、指针和引用作为函数参数
三、const 修饰指针和引用时到底限制谁
四、使用指针和引用减少对象复制
五、指针和引用作为参数返回多个值,以及作为函数返回值
六、完整代码与本章小结

一、本节学习内容概要
在前面已经学习过指针、引用、栈区变量以及函数参数。本节继续围绕函数展开,重点理解:
cpp
void TestParaPtr(const int* x, const int* y);
void TestParaRef(const int& x, const int& y);
这类写法的意义。
本节主要解决三个问题:
(1)为什么函数参数可以使用指针和引用?
因为指针和引用可以直接访问外部变量,避免把数据重新复制一份。对于 int 这类小数据影响不大,但对于 string、vector、结构体、类对象等较大的数据,可以明显减少复制成本。
(2)函数参数什么时候需要加 const?
如果函数内部只是读取数据,不需要修改外部变量,就应该加 const。
例如:
cpp
void TestParaPtr(const int* x, const int* y);
void TestParaRef(const int& x, const int& y);
这样可以防止函数内部误修改原来的数据。
(3)指针和引用能不能作为返回值?
可以,但是必须注意生命周期问题。不能返回普通局部变量的地址和普通局部变量的引用。


二、指针和引用作为函数参数
2.1 指针作为函数参数
代码如下:
cpp
void TestParaPtr(const int* x, const int* y)
{
cout << "TestParaPtr " << *x << " " << *y << endl;
cout << x << ":" << y << endl;
}
调用时:
cpp
int x{ 99 };
int y{ 88 };
TestParaPtr(&x, &y);
这里要注意,函数形参是:
cpp
const int* x
const int* y
所以调用时传入的必须是地址:
cpp
TestParaPtr(&x, &y);
可以理解为:
- 函数里的 x 保存的是外面变量 x 的地址
- 函数里的 y 保存的是外面变量 y 的地址
所以在函数内部,如果想访问外面变量的值,就要使用解引用:
cpp
*x
*y
完整理解如下:
cout << *x << endl;
表示根据 x 保存的地址,找到对应变量的值并输出。
2.2 引用作为函数参数
代码如下:
cpp
void TestParaRef(const int& x, const int& y)
{
// x++;
cout << "TestParaRef " << x << " " << y << endl;
cout << &x << ":" << &y << endl;
}
调用时:
cpp
TestParaRef(x, y);
引用和指针不同。
指针调用时需要传地址:
TestParaPtr(&x, &y);
引用调用时直接传变量:
TestParaRef(x, y);
因为引用可以理解为变量的别名。
也就是说,函数中的引用参数 x 和外面的变量 x 指向的是同一个数据。
所以函数内部可以直接使用:
x
y
不需要写成:
*x
*y
2.3 指针传参和引用传参的区别
(1)指针传参:
cpp
void TestParaPtr(const int* x, const int* y);
TestParaPtr(&x, &y);
特点是:
- 调用时传地址;
- 函数内部用 *x 访问值;
- 可以传 nullptr;
- 从调用形式上能明显看出传的是地址。
(2)引用传参:
void TestParaRef(const int& x, const int& y);
TestParaRef(x, y);
特点是:
- 调用时直接传变量;
- 函数内部直接用 x 访问值;
- 写法更像普通变量;
- 一般不能表示空引用。
简单来说:
指针更强调地址;
引用更强调别名。
三、const 修饰指针和引用时到底限制谁
在函数传参时,经常会看到下面这种写法:
cpp
void TestParaPtr(const int* x, const int* y);
void TestParaRef(const int& x, const int& y);
很多初学者看到**const 会直接理解成"这个变量不能改",但在指针中,const 的位置不同,限制的对象也不同。**
对于指针来说,主要有三种常见情况:
cpp
const int* p; // 不能通过 p 修改它指向的值,但是 p 可以改指向
int* const p; // p 保存的地址不能改,但是可以通过 p 修改它指向的值
const int* const p; // p 指向的值不能改,p 保存的地址也不能改
也就是说,const 修饰指针时,不能只看有没有 const,还要看 const 放在什么位置。
3.1 const int* x:不能修改指向的数据,但可以修改指针指向
代码中写的是:
cpp
void TestParaPtr(const int* x, const int* y)
{
cout << *x << endl;
cout << *y << endl;
}
其中:
cpp
const int* x
表示:
x 指向的是一个不能被修改的 int 数据
所以函数内部不能写:
*x = 100; // 错误,不能通过 x 修改它指向的数据
例如:
cpp
int a = 10;
int b = 20;
TestParaPtr(&a, &b);
调用时:
cpp
TestParaPtr(&a, &b);
本质上是把 a 的地址传给形参 x。
如果函数内部允许:
*x = 100;
就相当于把外面的 a 改成了 100。
但是由于形参写成了:
const int* x
所以编译器不允许通过 x 修改它指向的数据。
也就是说:
const int* x;
保护的是:
*x
不是:
x
所以下面这种写法是可以的:
cpp
void TestParaPtr(const int* x, const int* y)
{
int temp = 999;
x = &temp; // 正确,可以修改 x 自己保存的地址
}
为什么 x = &temp; 可以?
因为函数参数 x 本身也是一个局部变量。
调用时:
TestParaPtr(&a, &b);
可以理解为:
const int* x = &a;
const int* y = &b;
- 函数只是把
a的地址复制一份给形参x。- 所以在函数内部修改
x的指向,只是修改形参指针变量自己保存的地址,不会改变外面的a
因此:
const int* p = &a;
*p = 100; // 错误,不能修改 p 指向的数据
p = &b; // 正确,可以让 p 指向 b
总结一句话:
const int* p表示不能通过指针p修改它指向的数据,但是指针变量p自己保存的地址可以改变。
3.2 int* const x:不能修改指针保存的地址,但可以修改指向的数据
如果想让指针变量本身保存的地址不能改变,可以把 const 放在指针变量名前面:
int* const x
这种写法表示:
x 是一个固定指向的指针
也就是说,x 一旦指向某个变量,就不能再指向其他变量。
例如:
cpp
void TestParaPtr(int* const x, int* const y)
{
*x = 100; // 正确,可以修改 x 指向的数据
int temp = 999;
// x = &temp; // 错误,不能修改 x 自己保存的地址
}
假设调用:
cpp
int a = 10;
int b = 20;
TestParaPtr(&a, &b);
那么进入函数后,可以理解为:
cpp
int* const x = &a;
int* const y = &b;
这里的 x 始终保存 a 的地址,不能再改成其他地址。
所以这句代码错误:
x = &temp; // 错误
因为这相当于想让 x 从指向 a 改成指向 temp。
但是这句代码是正确的:
*x = 100;
因为 int* const x 限制的是 x 这个指针变量不能改地址,并没有限制 x 指向的数据不能改。
所以:
cpp
int* const p = &a;
*p = 100; // 正确,可以修改 a 的值
p = &b; // 错误,不能修改 p 保存的地址
总结一句话:
int* const p表示指针变量p自己保存的地址不能改变,但是可以通过p修改它指向的数据。
3.3 const int* const x:既不能修改值,也不能修改地址
如果希望:
不能通过指针修改外部变量的值;
也不能在函数内部改变指针自己的指向;
可以写成:
const int* const x
例如:
cpp
void TestParaPtr(const int* const x, const int* const y)
{
// *x = 100; // 错误,不能修改 x 指向的数据
int temp = 999;
// x = &temp; // 错误,不能修改 x 自己保存的地址
cout << *x << endl;
cout << *y << endl;
}
这里:
const int* const x
可以拆开看:
const int*
表示:
x 指向的数据不能被修改
后面的:
const x
表示:
x 自己保存的地址不能被修改
不过在普通函数参数中,最常用的还是:
const int* p
因为大多数情况下,我们主要关心的是:
这个函数不能修改外部传进来的数据。
至于形参指针变量自己在函数内部能不能改指向,一般影响不大,因为形参本身只是地址的一份拷贝。
3.4 三种写法对比总结
可以用一张表总结:
| 写法 | 能否修改 *p 指向的值 |
能否修改 p 保存的地址 |
含义 |
|---|---|---|---|
const int* p |
不能 | 能 | 指向的数据不能改 |
int* const p |
能 | 不能 | 指针自己的地址不能改 |
const int* const p |
不能 | 不能 | 值和地址都不能改 |
用代码再看一遍:
cpp
int a = 10;
int b = 20;
// 1. 指向的数据不能改,指针可以改指向
const int* p1 = &a;
// *p1 = 100; // 错误
p1 = &b; // 正确
// 2. 指针不能改指向,指向的数据可以改
int* const p2 = &a;
*p2 = 100; // 正确
// p2 = &b; // 错误
// 3. 指向的数据不能改,指针也不能改指向
const int* const p3 = &a;
// *p3 = 100; // 错误
// p3 = &b; // 错误
可以简单记忆为:
cpp
const int* p; // const 在 * 左边,限制的是 *p
int* const p; // const 在 p 左边,限制的是 p
const int* const p; // 两边都有 const,*p 和 p 都限制
3.5 const int& x:不能修改引用绑定的变量
引用参数中也可以加 const:
cpp
void TestParaRef(const int& x, const int& y)
{
// x++;
cout << x << " " << y << endl;
}
这里的:
const int& x
表示:
x 是外部变量的引用,但是不能通过 x 修改外部变量。
例如:
int a = 10;
const int& r = a;
此时:
r = 100; // 错误,不能通过 r 修改 a
但是要注意,引用和指针有一个区别:
- 指针可以改变指向;
- 引用一旦绑定某个变量后,就不能再改成引用其他变量。
例如:
int a = 10;
int b = 20;
int& r = a;
此时 r 是 a 的别名。
如果写:
r = b;
这不是让 r 改成引用 b,而是把 b 的值赋给 a。
也就是说:
r = b;
等价于:
a = b;
引用本身没有"重新指向"的说法。
所以对于引用来说:
int& r = a; // 可以通过 r 修改 a
const int& r = a; // 不能通过 r 修改 a
引用不像指针那样分成:
能不能改地址
能不能改值
因为引用一旦绑定后,本来就不能再改绑定对象。
3.6 指针和引用中 const 的最终理解
这一部分可以这样总结:
对于指针来说,要区分两个东西:
p :指针变量自己保存的地址
*p :指针指向的数据
所以:
const int* p;
而:
int* const p;
限制的是:
p
如果写成:
const int* const p;
则表示:
p 不能改;
*p 也不能改。
对于引用来说:
const int& r;
表示不能通过引用修改它绑定的变量。
最终记住这几句话即可:
cpp
const int* p; // 不能改值,可以改地址
int* const p; // 可以改值,不能改地址
const int* const p; // 值和地址都不能改
const int& r; // 不能通过引用修改绑定的变量
放到函数参数中,可以这样选择:
cpp
void Func(int* p); // 可以修改外部变量,也可以改形参指针指向
void Func(const int* p); // 不能修改外部变量,但可以改形参指针指向
void Func(int* const p); // 可以修改外部变量,但不能改形参指针指向
void Func(const int* const p); // 不能修改外部变量,也不能改形参指针指向
void Func(int& x); // 可以修改外部变量
void Func(const int& x); // 不能修改外部变量
实际开发中最常见的是:
cpp
void Func(const int* p);
void Func(const int& x);
void Func(const string& str);
因为它们表达的意思很清楚:
函数只读取传入的数据,不会修改原始变量,同时还能减少复制。
四、使用指针和引用减少对象复制
4.1 普通值传参会产生复制
代码中有这个函数:
cpp
void TestString(string str)
{
cout << "TestString " << str << endl;
cout << &str << endl;
}
调用:
cpp
string str = "test string";
TestString(str);
这里是普通值传参。
也就是说,函数会重新创建一个形参变量 str,然后把外面的字符串内容复制一份进来。
可以理解为:
外面的 string str
↓ 复制一份
函数里面的 string str
所以函数内部打印的:
&str
是函数内部形参的地址,不是外面那个 str 的地址。
对于 int、double 这种小数据,复制成本很低。
但是对于:
string
vector
结构体
类对象
如果数据比较大,复制成本就会增加。
4.2 使用 const string& 避免复制
代码如下:
cpp
void TestStringRef(const string& str)
{
cout << "TestStringRef " << str << endl;
cout << &str << endl;
}
调用:
cpp
TestStringRef("test");
这里使用的是:
const string& str
它的作用有两个:
- & :使用引用接收,避免复制
- const :只读取,不修改原始数据
所以对于较大的对象,如果函数内部只是读取,不修改,推荐写法是:
cpp
void Func(const string& str);
void Func(const vector<int>& v);
void Func(const Student& stu);
这样既能减少复制,又能防止函数内部误修改数据。
4.3 const char* 接收 C 风格字符串
代码如下:
cpp
void TestStringPara(const char* str)
{
cout << "TestStringPara " << str << endl;
cout << static_cast<const void*>(str) << endl;
}
调用:
cpp
TestStringPara("test");
这里的 "test" 是字符串常量。
形参:
cpp
const char* str
表示:
- str 保存字符串首字符的地址;
- 不能通过 str 修改字符串内容。
注意这句:
cpp
cout << static_cast<const void*>(str) << endl;
为什么要转成**const void*?**
因为如果直接输出:
cpp
cout << str << endl;
cout 会把 char* 当成字符串输出,而不是输出地址。
如果想打印字符串的地址,需要写成:
cpp
cout << static_cast<const void*>(str) << endl;
4.4 右值引用 string&&
代码如下:
cpp
void TestStringRight(string&& str)
{
cout << "TestStringRight " << str << endl;
cout << &str << endl;
}
调用:
cpp
TestStringRight("test string");
TestStringRight(string("test"));
这里的:
string&&
叫右值引用,主要用于接收临时对象。
例如:
string("test")
就是临时创建出来的 string 对象。
本节先简单理解为:
string&& 可以接收临时对象,为后续移动语义做准备。
后面学习移动构造、移动赋值、完美转发时,还会继续用到右值引用。
五、指针和引用作为参数返回多个值,以及作为函数返回值
5.1 指针作为参数返回多个值
函数正常情况下只能直接返回一个值。
但是如果希望一个函数返回多个结果,可以通过指针参数修改外部变量。
代码如下:
cpp
void GetPosPtr(int* x, int* y)
{
*x = 800;
*y = 600;
}
调用:
cpp
int x{ 99 };
int y{ 88 };
GetPosPtr(&x, &y);
cout << "x:" << x << " y:" << y << endl;
输出结果:
x:800 y:600
原因是:
GetPosPtr(&x, &y);
把外面变量的地址传进函数。
函数内部:
*x = 800;
*y = 600;
通过地址找到外面的变量,并修改外面的变量。
所以指针参数不仅可以减少复制,还可以作为输出参数,让函数修改外部数据。
5.2 引用作为参数返回多个值
代码如下:
cpp
void GetPosRef(int& x, int& y)
{
x = 640;
y = 480;
}
调用:
cpp
GetPosRef(x, y);
cout << "x:" << x << " y:" << y << endl;
输出结果:
x:640 y:480
引用方式看起来更简单。
指针写法:
GetPosPtr(&x, &y);
引用写法:
GetPosRef(x, y);
指针函数内部要写:
*x = 800;
*y = 600;
引用函数内部直接写:
x = 640;
y = 480;
所以引用作为输出参数时,写法更接近普通变量。
但是引用传参有一个问题:
GetPosRef(x, y);
从调用处不一定能直接看出 x 和 y 会被修改。
而指针传参:
GetPosPtr(&x, &y);
因为传入了地址,所以修改意图更加明显。
5.3 指针作为函数返回值
代码如下:
cpp
int* GetPosPtr()
{
static int pos{ 0 };
cout << "GetPosPtr pos:" << pos << endl;
return &pos;
}
调用:
cpp
auto res = GetPosPtr();
(*res) += 10;
GetPosPtr();
第一次调用:
auto res = GetPosPtr();
函数返回的是:
&pos
也就是 pos 的地址。
所以:
res
保存的是 pos 的地址。
然后:
(*res) += 10;
表示通过指针找到 pos,并让 pos 加 10。
再次调用:
GetPosPtr();
会发现 pos 的值已经变成了 10。
这里为什么可以返回 pos 的地址?
因为**pos 是静态局部变量:**
static int pos{ 0 };
静态局部变量不会在函数结束后销毁,它的生命周期会持续到程序结束。
所以可以返回它的地址。
5.4 引用作为函数返回值
代码如下:
cpp
int& GetPosRef()
{
static int pos{ 0 };
cout << "GetPosRef pos:" << pos << endl;
return pos;
}
调用:
cpp
auto& r2 = GetPosRef();
r2 += 20;
GetPosRef();
这里:
int& GetPosRef()
表示函数返回一个 int 类型的引用。
函数内部:
static int pos{ 0 };
return pos;
返回的是静态局部变量 pos 的引用。
调用时:
auto& r2 = GetPosRef();
这里一定要写成:
auto& r2表示
r2是返回值的引用。也就是说:
r2 是 pos 的别名
所以:
r2 += 20;
就相当于:
pos += 20;
再次调用:
GetPosRef();
就能看到 pos 已经变成了 20。
5.5 不能返回普通局部变量的地址或引用
虽然指针和引用可以作为返回值,但不能随便返回。
错误写法一:
int* ErrorPtr()
{
int pos = 0;
return &pos; // 错误
}
错误写法二:
int& ErrorRef()
{
int pos = 0;
return pos; // 错误
}
原因是:
int pos = 0;
- 这是普通局部变量,存放在栈区。
- 函数结束后,
pos的生命周期结束,这块空间就不再属于pos。- 此时返回出去的地址或引用已经失效。
这种问题叫:
悬空指针
悬空引用
所以要记住:
不能返回普通局部变量的地址;
不能返回普通局部变量的引用。
如果要返回指针或引用,必须保证返回的数据在函数结束后仍然存在。
例如:
static 局部变量
全局变量
堆区对象
类成员变量
六、完整代码与本节总结
6.1 完整代码
cpp
#include <iostream>
#include <string>
#include <vector>
using namespace std;
// 1. 指针和引用作为参数,减少复制,不修改
void TestParaPtr(const int* x, const int* y)
{
cout << "TestParaPtr " << *x << " " << *y << endl;
cout << x << ":" << y << endl;
}
void TestParaRef(const int& x, const int& y)
{
// x++;
cout << "TestParaRef " << x << " " << y << endl;
cout << &x << ":" << &y << endl;
}
void TestStringPara(const char* str)
{
cout << "TestStringPara " << str << endl;
cout << static_cast<const void*>(str) << endl;
}
void TestStringRef(const string& str)
{
cout << "TestStringRef " << str << endl;
cout << &str << endl;
}
void TestString(string str)
{
cout << "TestString " << str << endl;
cout << &str << endl;
}
void TestStringRight(string&& str)
{
cout << "TestStringRight " << str << endl;
cout << &str << endl;
}
// 2. 指针和引用作为参数,返回多个值
void GetPosPtr(int* x, int* y)
{
*x = 800;
*y = 600;
}
void GetPosRef(int& x, int& y)
{
x = 640;
y = 480;
}
// 3. 指针和引用在返回值中的作用
int* GetPosPtr()
{
static int pos{ 0 };
cout << "GetPosPtr pos:" << pos << endl;
return &pos;
}
int& GetPosRef()
{
static int pos{ 0 };
cout << "GetPosRef pos:" << pos << endl;
return pos;
}
int main()
{
int x{ 99 };
int y{ 88 };
TestParaPtr(&x, &y);
TestParaRef(x, y);
GetPosPtr(&x, &y);
cout << "x:" << x << " y:" << y << endl;
GetPosRef(x, y);
cout << "x:" << x << " y:" << y << endl;
string str = "test string";
TestStringPara("test");
TestStringRef("test");
TestString(str);
TestStringRight("test string");
TestStringRight(string("test"));
auto res = GetPosPtr();
(*res) += 10;
GetPosPtr();
auto& r2 = GetPosRef();
r2 += 20;
GetPosRef();
return 0;
}
6.2 本节核心总结
本节可以总结为以下几点。
(1)指针作为函数参数时,调用需要传地址:
cpp
TestParaPtr(&x, &y);
函数内部通过解引用访问或修改数据:
*x
*y
(2)引用作为函数参数时,调用直接传变量:
cpp
TestParaRef(x, y);
函数内部直接使用变量名:
x
y
(3)如果函数内部只读取数据,不修改外部变量,建议加 const:
cpp
void Func(const int* p);
void Func(const int& x);
void Func(const string& str);
(4)const int* p 限制的是 *p,不是 p。
也就是说:
*p = 100; // 错误,不能修改指向的数据
p = &b; // 正确,可以改变指针自己的指向
如果想让值和地址都不能改,需要写成:
cpp
const int* const p = &a;
**(5)对于 string、vector、结构体、类对象等较大的数据,推荐使用:**引用
cpp
const string& str
const vector<int>& v
const Student& stu
这样可以减少复制,提高效率。
(6)指针和引用可以作为返回值,但要注意生命周期。
正确写法:
cpp
int& Func()
{
static int x = 10;
return x;
}
错误写法:
cpp
int& Func()
{
int x = 10;
return x; // 错误,不能返回局部变量引用
}
最后用一句话总结:
- 指针和引用作为函数参数,可以减少对象复制,也可以让函数修改外部变量;
- 如果只读取不修改,应加
const。- 指针和引用作为返回值时,必须保证返回的数据在函数结束后仍然有效,不能返回栈区局部变量的地址或引用。