C++学习笔记——指针与const的奇妙之旅
C++ 指针与 const
的奇妙之旅:从基础到高级应用
引言:为什么指针和 const
这么重要?
在C++的学习路上,指针和 const
绝对是两个绕不开的“拦路虎”,但它们也是C++强大和灵活的基石。刚开始学的时候,我常常被它们搞得晕头转向,尤其是当它们俩“手拉手”一起出现的时候,简直是“劝退神器”!
不过别担心,经过一番摸索和老师的耐心讲解,我发现它们其实并没有那么神秘。指针就像是内存的“地址簿”,能让我们直接找到数据;而 const
就像是数据的“守护神”,能防止数据被意外修改。当它们结合起来,就能实现更精细、更安全、更高效的代码控制。
这篇笔记就是我学习过程中的一些心得体会,希望能用大白话和生动的例子,帮助大家一起攻克这两个难点!
1. 指针:内存的“地址簿”
想象一下,你的电脑内存就像一个巨大的图书馆,里面有很多书架(内存地址),每本书(数据)都放在一个特定的书架上。指针,就是记录这些书架位置的“地址簿”。它不存储书的内容,只存储书的“位置信息”。
- 是什么? 指针是一个变量,它存储的是另一个变量的内存地址。
- 怎么声明?
数据类型 *指针变量名;
int *p;
:声明一个指向int
类型数据的指针p
。
- 怎么获取地址? 使用
&
(取地址运算符)。int num = 10;
int *p = #
:把num
的地址存到p
里。
- 怎么访问数据? 使用
*
(解引用运算符)。*p = 20;
:通过p
找到num
的位置,然后把num
的值改成20
。
小例子:
1 |
|
2. const
:数据的“守护神”
const
关键字就像一个“封印”,一旦给某个变量加上 const
,就意味着它的值不能再被修改了。这能让我们的程序更健壮,避免一些不小心造成的错误。
- 基本含义: 常类型,其值不能被更新。
- 主要作用:
- 定义常量:
const int MAX_VALUE = 100;
- 类型检查: 编译器会帮你检查,如果你尝试修改
const
变量,它会报错。 - 防止修改: 保护数据,增加程序稳定性。
- 节省空间:
const
常量在内存中通常只有一份拷贝,而宏定义可能会有多份。
- 定义常量:
小例子:
1 |
|
3. 指针与 const
的“组合拳”:谁是老大?
当指针和 const
结合在一起时,就变得有点复杂了。关键在于 const
到底修饰的是指针本身,还是指针指向的数据。
核心秘诀:
const
在*
的左边:const
修饰的是指针指向的数据,数据不能改。const
在*
的右边:const
修饰的是指针本身,指针不能改(不能指向别的地址)。
让我们看看这三种主要组合:
(1) 指向常量的指针 (Pointer to const
)
- 语法:
const 类型 *指针变量名;
或类型 const *指针变量名;
- 含义: 指针指向的数据是常量,你不能通过这个指针去修改它。但是,指针本身可以改变,它可以指向其他地方。
- 比喻: 就像你有一个“只读的地址簿”,你可以查阅地址上的书,但不能修改书的内容。不过,你可以把这个地址簿指向另一本书。
- 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14const int *ptr_to_const_data; // 指向 const int 的指针
int val = 10;
const int c_val = 20;
ptr_to_const_data = &val; // OK,可以指向非 const 数据
// *ptr_to_const_data = 15; // 错误!不能通过这个指针修改 val 的值
ptr_to_const_data = &c_val; // OK,可以指向 const 数据
// *ptr_to_const_data = 25; // 错误!不能通过这个指针修改 c_val 的值
// 注意:虽然不能通过 ptr_to_const_data 修改 val,但可以通过 val 本身修改:
// int *another_ptr = &val;
// *another_ptr = 30; // OK,val 的值变成了 30
// std::cout << *ptr_to_const_data << std::endl; // 输出 30
(2) 常量指针 (const
Pointer)
- 语法:
类型 *const 指针变量名;
- 含义: 指针本身是常量,一旦初始化,它就不能再指向其他地址了。但是,它所指向的数据是可以修改的。
- 比喻: 就像你有一个“固定指向的地址簿”,它永远只能指向那本书,不能换别的书。但你可以随意修改那本书的内容。
- 示例:
1
2
3
4
5
6int num = 0;
int num1 = 1;
int *const const_ptr = # // 常量指针,必须初始化!
// const_ptr = &num1; // 错误!常量指针不能修改指向!
*const_ptr = 100; // OK,可以通过指针修改 num 的值
(3) 指向常量的常量指针 (Constant Pointer to const
)
- 语法:
const 类型 *const 指针变量名;
- 含义: 指针本身是常量(不能修改指向),同时它所指向的数据也是常量(不能通过它修改数据)。
- 比喻: 就像你有一个“固定指向的只读地址簿”,它永远只能指向那本书,而且你也不能修改那本书的内容。
- 示例:
1
2
3
4
5const int p = 3;
const int *const const_ptr_to_const_data = &p; // 指向 const int 的常量指针
// const_ptr_to_const_data = &some_other_var; // 错误!不能修改指向
// *const_ptr_to_const_data = 5; // 错误!不能通过指针修改 p 的值
4. 函数参数传递的“艺术”:如何高效又安全地“送东西”?
在函数调用时,我们如何把数据“送”给函数呢?C++提供了几种方式,每种方式都有自己的特点和适用场景。
4.1 值传递:老实巴交的“复制粘贴”
- 特点: 函数接收的是参数的副本。
- 工作原理: 当你把一个变量(尤其是类对象)通过值传递给函数时,编译器会为函数参数在内存中创建一个一模一样的副本。函数内部对这个副本的任何操作,都不会影响到原始变量。
- 效率问题:
- 对于
int
、char
等基本类型,复制很快,开销可以忽略。 - 但对于像
std::string
或自定义的MyClass
这样的大对象,复制一个完整的对象需要调用复制构造函数,这可能涉及到大量的内存分配和数据拷贝。函数结束后,这个副本还会调用析构函数被销毁。这些操作会消耗大量时间和资源,导致效率低下。
- 对于
示例:
1 |
|
输出:
1 | MyClass 构造函数 |
可以看到,func_by_value
调用前后,复制构造函数和析构函数都被调用了,这就是额外的开销。
4.2 引用传递:高效的“别名”魔法
- 特点: 函数接收的是参数的别名。
- 工作原理: 当你把一个变量通过引用传递给函数时(例如
MyClass &obj
),函数参数obj
不会创建新的对象副本,它直接成为原始变量(例如original_obj
)的另一个名字。在底层,编译器通常会把引用实现为一个常量指针,存储着原始变量的地址。 - 优点:
- 避免复制: 不会调用复制构造函数和析构函数,大大提高了效率,尤其对于大对象。
- 可修改原始数据: 函数内部对引用的操作,就是直接对原始变量的操作。
const
引用:效率与安全的完美结合- 如果你希望函数能高效地访问大对象,但又不希望函数修改原始对象,那么
const
引用 (const MyClass &obj
) 是最佳选择。 - 它既享受了引用传递的效率(不复制),又通过
const
保证了数据的安全性(不能修改)。
- 如果你希望函数能高效地访问大对象,但又不希望函数修改原始对象,那么
示例:
1 |
|
输出:
1 | MyClass 构造函数 |
可以看到,使用引用传递时,没有额外的复制构造函数和析构函数调用,效率更高。
4.3 按地址传递:传统的“地址簿”传递
- 特点: 函数接收的是参数的地址(指针)。
- 工作原理: 你把变量的内存地址(一个指针值)复制一份,传递给函数。函数内部通过这个地址来找到并操作原始变量。
- 优点:
- 避免复制: 和引用传递一样,只复制一个地址(通常4或8字节),避免了大对象的复制开销。
- 可修改原始数据: 通过指针可以修改原始数据。
- 可为空: 指针可以为
nullptr
,表示不指向任何对象,这在某些场景下很有用(例如可选参数)。
- 缺点:
- 需要解引用: 在函数内部访问数据时,需要使用
*
或->
运算符,语法相对繁琐。 - 空指针风险: 必须小心处理
nullptr
,否则解引用空指针会导致程序崩溃。
- 需要解引用: 在函数内部访问数据时,需要使用
与引用传递的对比:
特性 | 引用传递 (MyClass &obj ) |
按地址传递 (MyClass *obj_ptr ) |
---|---|---|
语法 | 更简洁,直接使用变量名,无需 * 或 & |
需要 * 解引用或 -> 访问成员,调用时需要 & 取地址符 |
空值 | 引用不能为 nullptr ,必须引用一个有效对象 |
指针可以为 nullptr ,需要空指针检查 |
重新绑定 | 引用一旦初始化,不能再引用其他对象(是别名) | 指针可以重新指向其他对象 |
安全性 | 相对更安全,因为不能为 nullptr ,且不能重新绑定 |
相对不安全,有空指针风险,且可以重新指向,需要更多注意 |
底层实现 | 通常被编译器实现为常量指针 | 就是指针 |
选择建议:
- 优先使用
const
引用传递 (const MyClass &obj
): 当函数只需要读取大对象的数据,而不需要修改它时,这是最推荐的方式,兼顾了效率和安全性。 - 使用引用传递 (
MyClass &obj
): 当函数需要修改大对象的数据,且确保参数不会为空时。 - 使用按地址传递 (
MyClass *obj_ptr
): 当函数需要修改数据,且参数可能为空,或者需要处理动态数据结构(如链表、树)时。
5. const
在函数和类中的“身影”
const
不仅能修饰变量和指针,在函数和类中也扮演着重要角色。
const
修饰函数参数:void func(const int var);
:对于基本类型,值传递加const
意义不大。void func(const char *str);
:指针指向的内容是常量,函数内不能修改str
指向的数据。void func(char *const str);
:指针本身是常量,函数内不能改变str
的指向。void func(const MyClass &obj);
:最常用! 引用传递大对象,且保证函数内不修改对象。
const
修饰成员函数(常成员函数):- 语法: 在成员函数声明的末尾加上
const
,例如:void print() const;
- 作用: 保证该成员函数不会修改类的任何非静态数据成员。
- 限制: 常成员函数只能调用其他常成员函数,不能调用非常成员函数。
- 常对象:
const
声明的对象(例如const MyClass obj;
)只能调用其常成员函数。
- 语法: 在成员函数声明的末尾加上
const
成员变量:- 类中的
const
成员变量必须通过初始化列表进行初始化,因为它们在对象构造后就不能再被修改了。1
2
3
4
5class Apple {
public:
const int apple_number; // const 成员变量
Apple(int i) : apple_number(i) {} // 必须通过初始化列表初始化
};
- 类中的
我的学习总结
- 指针是地址,**
const
** 是限制。 const
在*
左边,数据不能改;const
在*
右边,指针不能改。- 值传递:复制,开销大,不改原值。
- 引用传递:别名,高效,可改原值(加
const
则不可改)。 - 按地址传递:传地址,高效,可改原值,但有空指针风险。
- 大对象参数:首选
const
引用,其次引用,最后考虑指针。 const
成员函数:不改对象状态,const
对象只能调它。