常量指针和指针常量的区别是什么?
常量指针:常量指针本质上是个指针,只不过这个指针指向的对象是常量。也就是说,不可以通过对指针解引用修改指针指向的内容,而可以修改指针的指向。
指针常量的本质上是个常量,只不过这个常量的值是一个指针,和上面那个恰恰相反,不可以修改指针的指向,但可以通过对指针解引用修改指针指向的内容。
| 特性 | 常量指针 (const int *p) |
指针常量 (int *const p) |
|---|---|---|
const 位置 |
在 * 左边 |
在 * 右边 |
| 修饰对象 | 修饰指针指向的值 | 修饰指针本身 |
| 能否修改指向的值 | 不能 (*p = ... 非法) |
能 (*p = ... 合法) |
| 能否修改指针指向 | 能 (p = &... 合法) |
不能 (p = &... 非法) |
| 初始化要求 | 不强制在定义时初始化 | 必须在定义时初始化 |
函数指针和指针函数的区别是什么?
指针函数本质是一个函数,只不过该函数的返回值是一个指针。相对于普通函数而言,只是返回值是指针。
cpp
#include <stdlib.h>
// 一个返回 int* 的指针函数
int* create_array(int size) {
// 动态分配内存
int *arr = (int*)malloc(size * sizeof(int));
return arr; // 返回堆内存的地址
}
int *my_array = create_array(10); // 调用函数,获取返回的地址
// ... 使用 my_array ...
// 注意:使用完毕后需要手动释放内存 free(my_array);
函数指针,顾名思义,是"指向函数的指针"。它的核心特点是:它是一个指针变量,存储了某个函数的入口地址,通过这个指针可以间接调用该函数。
- 语法形式 :
*和指针名被括号()包裹。int (*p)(int, int);
cpp
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int (*func_ptr)(int, int); // 声明一个函数指针
func_ptr = add; // 指向 add 函数
int result1 = func_ptr(5, 3); // 调用 add,result1 = 8
func_ptr = sub; // 改变指向,指向 sub 函数
int result2 = (*func_ptr)(5, 3); // 调用 sub,result2 = 2
| 特性 | 函数指针 (Pointer to Function) | 指针函数 (Function Returning Pointer) |
|---|---|---|
| 本质 | 指针变量 | 函数 |
| 语法关键 | (*指针名) |
返回类型* 函数名 |
| 作用 | 存储函数地址,实现间接调用 | 执行逻辑,并返回一个内存地址 |
| 典型用途 | 回调函数、策略模式 | 动态内存分配、返回数组/结构体地址 |
指针和引用值传递的区别是什么?
本质区别:别名 vs. 地址副本
| 特性 | 引用传递 (T&) |
指针传递 (T*) |
|---|---|---|
| 语法操作 | 直接使用,像普通变量一样 (x = 10) |
需要解引用 (*x = 10) |
| 空值处理 | 不能为空 (必须绑定有效对象) | 可以为空 (nullptr) |
| 可变性 | 一旦初始化,不可重绑定到其他变量 | 指针可以重新赋值指向其他地址 |
| 安全性 | 高 (无空指针风险,无需检查) | 较低 (需防范空指针、野指针) |
| 可读性 | 高 (代码简洁,意图明确) | 较低 (需显式取地址和解引用) |
引用 vs. 指针:为什么引用更受欢迎?
虽然指针也能实现类似的功能,但引用在大多数情况下是更优的选择,因为它更安全、更清晰。
| 特性 | 引用传递 (T&) |
指针传递 (T*) |
|---|---|---|
| 安全性 | 高。引用不能为空,必须在初始化时绑定到一个有效对象,避免了空指针解引用的风险source_group_web_11。 | 较低。指针可以为 nullptr,使用前必须进行判空检查,否则可能导致程序崩溃source_group_web_12。 |
| 可读性 | 高。调用时直接传递变量,函数内部直接操作,语法简洁自然source_group_web_13。 | 较低。调用时需要取地址符 &,函数内部需要解引用符 * 或 ->,代码稍显繁琐source_group_web_14。 |
| 可变性 | 不可重绑定。引用一旦绑定到某个变量,就不能再指向其他变量source_group_web_15。 | 可重定向。指针可以随时被赋予新的地址,指向不同的对象source_group_web_16。 |
参数传递时,值传递、引用传递、指针传递的区别?
简单来说:
- 值传递:创建副本,安全但可能低效。
- 引用传递:创建别名,高效且安全。
- 指针传递:传递地址副本,高效但需谨慎。
| 特性 | 值传递 (Pass by Value) | 引用传递 (Pass by Reference) | 指针传递 (Pass by Pointer) |
|---|---|---|---|
| 本质 | 创建实参的完整副本 | 实参的别名 | 传递实参地址的副本 |
| 修改影响 | 不影响原始实参 | 直接影响原始实参 | 解引用后直接影响原始实参 |
| 空值处理 | 不适用(总是有效对象) | 不能为空 (必须绑定有效对象) | 可以为空 (nullptr) |
| 性能开销 | 高(尤其对大型对象) | 极低(无拷贝) | 低(仅拷贝地址) |
| 语法 | void func(Type param) |
void func(Type& param) |
void func(Type* param) |
C++全局变量、局部变量、静态全局变量、静态局部变量的区别?
C++ 变量根据定义的位置的不同的生命周期,具有不同的作用域,作用域可分为 6 种:全局作用域,局部作用域,语句作用域,类作用域,命名空间作用域和文件作用域。
全局变量:具有全局作用域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件。当然其他不包含全局变量定义的源文件需要用extern 关键字再次声明这个全局变量。
静态全局变量:具有文件作用域。它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被static 关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。
局部变量:具有局部作用域。它是自动对象(auto),在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用执行结束后,变量被撤销,其所占用的内存也被收回。
静态局部变量:具有局部作用域。它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。
简单来说:
- 局部变量:临时工,用完即走。
- 全局变量:公共财产,全程序共享。
- 静态局部变量:有记忆的临时工,只在函数内可见。
- 静态全局变量:私有财产,只在当前文件内共享。
| 变量类型 | 作用域 (在哪能用) | 生命周期 (活多久) | 链接性 (能否跨文件) | 存储位置 | 初始化行为 |
|---|---|---|---|---|---|
| 局部变量 | 函数或代码块内部 | 函数执行期间 | 无链接 | 栈区 | 不初始化则为随机值 |
| 全局变量 | 定义点之后的所有文件 | 整个程序运行期 | 外部链接 | 静态存储区 | 自动初始化为0 |
| 静态全局变量 | 定义点之后的当前文件 | 整个程序运行期 | 内部链接 | 静态存储区 | 自动初始化为0 |
| 静态局部变量 | 函数或代码块内部 | 整个程序运行期 | 无链接 | 静态存储区 | 只初始化一次,默认为0 |
6 种作用域分类
C++ 变量根据定义的位置的不同的生命周期,具有不同的作用域,作用域可分为 6 种:全局作用域,局部作用域,语句作用域,类作用域,命名空间作用域和文件作用域。
- 文件作用域 (File Scope)
这通常就是指全局作用域。在所有函数和类之外定义的变量就属于这个作用域。
- 可见性 :从定义点开始,直到当前源文件(
.cpp)的末尾。 - 生命周期:贯穿整个程序的运行期。
- 链接性 :默认具有外部链接 ,意味着其他文件可以通过
extern关键字来访问它。
- 局部作用域 (Local Scope)
这是在函数内部定义的变量,也就是我们常说的局部变量。
- 可见性 :仅在定义它的函数或代码块
{}内部有效。 - 生命周期:当函数被调用时创建,函数返回时销毁。
- 存储位置 :通常在**栈(Stack)**上。
- 语句作用域 (Statement Scope)
这通常被称为块作用域(Block Scope) ,是局部作用域的一种更精细的形式。它指的是在任何一个代码块(由 {} 包围)内部定义的变量。
- 可见性 :仅在它所在的那个特定代码块内有效。例如,
for循环或if语句中定义的变量。 - 生命周期:程序执行进入该块时创建,离开该块时销毁。
- 典型场景 :
for(int i = 0; ...)中的循环变量i。
- 类作用域 (Class Scope)
这是在类(class 或 struct)内部定义的变量,即类的成员变量。
- 可见性 :在类的所有成员函数内部有效。外部需要通过对象(
obj.member)或指针(ptr->member)来访问。 - 生命周期:与类的实例(对象)的生命周期相同。对象创建时成员变量诞生,对象销毁时成员变量消亡。
- 命名空间作用域 (Namespace Scope)
这是在命名空间(namespace)内部定义的变量。
- 可见性 :在命名空间内部有效。外部需要通过作用域解析运算符
::来访问,例如std::cout。 - 生命周期:如果命名空间内定义的是全局变量,则生命周期贯穿整个程序运行期。
| 变量类型 | 典型作用域 | 生命周期 | 链接性 |
|---|---|---|---|
| 局部变量 | 局部/块作用域 | 自动(函数/块结束) | 无链接 |
| 静态局部变量 | 局部/块作用域 | 静态(程序结束) | 无链接 |
| 全局变量 | 文件/全局作用域 | 静态(程序结束) | 外部链接 |
| 静态全局变量 | 文件作用域 | 静态(程序结束) | 内部链接 |
| 类成员变量 | 类作用域 | 与对象共存亡 | 无链接 |
全局变量定义在头文件中有什么问题?
如果在头文件中定义全局变量,当该头文件被多个文件 include 时,该头文件中的全局变量就会被定义多次,导致重复定义,因此不能再头文件中定义全局变量。
extern C 的作用是什么?
extern "C" 的核心作用非常明确:实现 C++ 代码与 C 代码的混合编程。
简单来说,它是一条编译指令,告诉 C++ 编译器:"请按照 C 语言的规则来处理这部分代码(主要是函数名和变量名)"。
当 C++ 程序 需要调用 C 语言编写的函数,C++ 使用链接指示,即 extern "C" 指出任意非 C++ 函数所用的语言。 举例:
cpp
// 可能出现在 C++ 头文件<cstring>中的链接指示
extern "C"{
int strcmp(const char*, const char*);
}
C 和 C++ struct 的区别?
的 struct 只是一个纯粹的数据容器,而 C++ 的 struct 是一个功能完备的面向对象类型。
- 在 C 语言中 struct 是用户自定义数据类型;在 C++ 中 struct 是抽象数据类型,支持成员函数的定义。
- C 语言中 struct 没有访问权限的设置,是一些变量的集合体,不能定义成员函数;C++ 中 struct 可以和类一样,有访问权限,并可以定义成员函数。
- C 语言中 struct 定义的自定义数据类型,在定义该类型的变量时,需要加上 struct 关键字,例如:struct A var;定义 A 类型的变量;而 C++ 中,不用加该关键字,例如:A var;
| 特性 | C 语言中的 struct |
C++ 中的 struct |
|---|---|---|
| 成员函数 | 不支持 | 支持(包括构造函数、析构函数) |
| 访问控制 | 无(所有成员默认且只能是 public) |
支持(public/private/protected) |
| 继承 | 不支持 | 支持(可继承其他 struct 或 class) |
| 变量声明 | 必须带 struct 关键字 |
可以省略 struct 关键字 |
| 初始化 | 仅支持聚合初始化 | 支持聚合初始化、构造函数、C++11 成员初始化 |
| 默认继承权限 | 不适用 | public |
与 class 的关系 |
无 class 概念 |
几乎等同于 class,仅默认权限不同 |
C++ 中 struct和Class区别是什么?
简单来说:struct 更偏向于作为纯粹的数据容器,而 class 更偏向于作为具有封装和行为的对象。
默认访问权限不同
struct: 成员默认是public的。class: 成员默认是private的。
cpp
struct MyStruct {
int x; // 默认为 public,外部可以直接访问
};
class MyClass {
int x; // 默认为 private,外部无法直接访问
};
默认继承方式不同
struct: 继承时默认是public继承。class: 继承时默认是private继承。
cpp
struct Base {};
struct DerivedStruct : Base {}; // 默认为 public 继承
class DerivedClass : Base {}; // 默认为 private 继承
为什么有了 class 还保留 struct?
这主要是为了向后兼容 和语义表达 。虽然 C++ 把
struct升级得几乎和class一样强大,但保留它绝对不是为了制造重复,而是因为它在表达"纯数据结构"时更直观。
struct暗示"纯数据" :当你看到一个struct时,你会本能地认为这是一个简单的数据集合(Data Carrier),它是公开的、透明的,没有复杂的逻辑封装。比如坐标点Point、矩形尺寸Rect、数据库的一行记录等。class暗示"对象" :当你看到一个class时,你会认为这是一个封装了数据和行为的对象,它有私有成员,有复杂的内部逻辑,对外提供接口。保留
struct让代码的意图更清晰 。如果你写了一个只有公有成员变量的类型,用struct比用class更符合直觉。
struct 和 union 的区别是什么?
核心区别:内存分配方式。这是两者最本质的区别。
struct(结构体): 每个成员都有自己独立的内存空间。
- 结构体的总大小 = 所有成员大小之和 + 内存对齐填充的字节。
- 修改一个成员,不会影响其他成员。
- 所有成员可以同时有效。
union(共用体/联合体): 所有成员共享同一块内存空间。
- 共用体的总大小 = 最大的那个成员的大小(同样需要考虑对齐)。
- 修改一个成员,会覆盖其他成员的数据(因为大家用的是同一块地)。
- 在同一时刻,只能有一个成员有效。
特性 struct(结构体)union(共用体)内存模式 每个成员独立分配内存 所有成员共享首地址,重叠存放 总大小 所有成员大小之和 (含对齐) = 最大成员的大小 (含对齐) 数据存取 各成员互不干扰,可同时使用 写入一个会覆盖另一个,一次只能用其一 主要用途 组合相关的数据(如:学生信息) 节省内存、数据类型转换、寄存器操作 struct 示例
cppstruct MyStruct { int a; // 4字节 char b; // 1字节 }; // 总大小通常是 8字节 (因为内存对齐) MyStruct s; s.a = 65; // 内存前4字节存入 65 s.b = 'B'; // 内存第5字节存入 'B' // s.a 依然是 65,s.b 是 'B',互不影响。
cppunion MyUnion { int a; // 4字节 char b; // 1字节 }; // 总大小是 4字节 (取决于最大的 int) MyUnion u; u.a = 65; // 4字节内存被写入 65 (十六进制 0x00000041) u.b = 'B'; // 第1字节被覆盖为 'B' (ASCII 66, 0x42) // 此时内存变为 0x00000042 // 结果: // u.b 是 'B' (66) // u.a 变成了 66 (不再是 65!因为低字节被 b 覆盖了)
C 和 C++ static 的区别是什么?
| 特性 | C 语言中的 static |
C++ 中的 static |
|---|---|---|
| 核心用途 | 控制变量/函数的作用域和生命周期 | 包含 C 的所有用途,并增加类成员的归属与共享 |
| 面向对象 | 无关 | 紧密相关,是类概念的核心部分 |
| 数据共享 | 在同一函数或同一文件内共享 | 可在同一类的所有对象间共享数据 |
| 函数调用 | 静态函数只能在本文件内调用 | 静态成员函数可通过类名直接调用,无需对象 |
与 this 指针 |
不适用 | 静态成员函数没有 this 指针 |
C++ static作用是什么?
在 C++ 中,
static关键字的作用非常丰富,它的核心语义根据使用场景的不同而变化,但总体上可以归纳为两个核心目的:控制生命周期 和控制作用域/可见性。
- 修饰函数内的局部变量:延长生命周期
当
static用于函数内部的局部变量时,它改变了变量的存储位置和生命周期。
- 作用:变量不再存储在栈上,而是存储在静态存储区。它的生命周期从"函数调用结束即销毁"延长至"整个程序运行结束"。
- 效果 :
- 只初始化一次:该变量在程序第一次执行到其定义语句时进行初始化,后续的函数调用会跳过初始化步骤。
- 值被保留:函数调用结束后,变量的值不会丢失,下次调用时会保留上次调用结束时的值。
- 典型应用:统计函数调用次数、实现懒加载(Lazy Initialization)等。
- 修饰全局变量或普通函数:限制作用域(文件作用域)
当
static用于函数外部(全局作用域)的变量或函数时,它改变了符号的链接性(linkage)。
- 作用:将变量或函数的链接性从"外部链接"(可被其他源文件访问)变为"内部链接"。
- 效果 :该变量或函数的作用域被限制在定义它的源文件(
.cpp文件)内部,对其他文件不可见。- 典型应用:避免不同源文件间的命名冲突,实现"文件私有"的全局变量或辅助函数。
cpp// file1.cpp static int globalVal = 10; // 仅在 file1.cpp 中可见 static void helperFunc() { /* ... */ } // 仅在 file1.cpp 中可见 // file2.cpp static int globalVal = 20; // 与 file1.cpp 中的 globalVal 是两个不同的变量,不会冲突3. 修饰类的成员变量:实现类级别的数据共享
这是 C++ 面向对象特性中
static的重要应用。
- 作用:声明一个属于类本身,而非类的某个具体对象的变量。
- 效果 :
- 所有对象共享:该类的所有实例共享同一份静态成员变量。
- 不占用对象内存 :它不计算在
sizeof(对象)的大小内。- 访问方式 :可以通过
类名::变量名直接访问,无需创建对象。- 初始化 :静态成员变量必须在类外进行定义和初始化(除非是
static const的整型或 C++17 的inline static)。
- 修饰类的成员函数:实现类级别的工具方法
与静态成员变量类似,静态成员函数也属于类本身。
- 作用:声明一个属于类的函数。
- 效果 :
- 无
this指针 :静态成员函数不与任何对象绑定,因此没有this指针。- 访问限制:它只能访问类的静态成员变量和其他静态成员函数,不能直接访问非静态成员。
- 访问方式 :可以通过
类名::函数名()直接调用,无需创建对象。- 典型应用:工厂方法、工具函数、访问静态数据的接口等。
使用位置 核心作用 效果 函数内部 延长生命周期 变量"记住"上次的值,只初始化一次 文件作用域 限制作用域 变量/函数"文件私有",避免命名冲突 类内部 (变量) 实现数据共享 所有对象共享一份数据,属于类本身 类内部 (函数) 实现类级方法 无需对象即可调用,没有 this指针
static 在类中使用的注意事项有哪些?
static 静态成员变量:
- 静态成员变量是在类内进行声明,在类外进行定义和初始化,在类外进行定义和初始化的时候不要出现 static关键字和private、public、protected 访问规则。
- 静态成员变量相当于类域中的全局变量,被类的所有对象所共享,包括派生类的对象。
- 静态成员变量可以作为成员函数的参数,而普通成员变量不可以。
static 静态成员函数:
- 静态成员函数不能调用非静态成员变量或者非静态成员函数,因为静态成员函数没有 this 指针。静态成员函数做为类作用域的全局函数。
- 静态成员函数不能声明成虚函数(virtual)、const 函数和 volatile 函数。
static 全局变量和普通全局变量的异同是什么?
| 特性 | 普通全局变量 | Static 全局变量 |
|---|---|---|
| 作用域 | 全局(跨文件可见) | 文件内(仅当前文件可见) |
| 链接属性 | 外部链接 (External Linkage) | 内部链接 (Internal Linkage) |
| 外部访问 | 可以通过 extern 访问 |
无法被其他文件访问 |
| 存储位置 | 静态存储区 (.data/.bss) | 静态存储区 (.data/.bss) |
| 生命周期 | 程序运行全程 | 程序运行全程 |
| 默认初始化 | 0 | 0 |
| 主要用途 | 跨模块共享数据 | 模块内部私有状态,避免冲突 |
cpp
// file1.c
int globalVar = 10; // 普通全局变量
static int staticVar = 20; // static全局变量
void func1() {
globalVar++; // 可以访问
staticVar++; // 可以访问
}
// file2.c
extern int globalVar; // 正确:声明外部变量,链接到 file1.c 的 globalVar
// extern int staticVar; // 错误!链接器找不到 staticVar,因为它被限制在 file1.c 中
void func2() {
globalVar = 100; // 正确:修改了 file1.c 中的变量
// staticVar = 200; // 错误:编译或链接错误,不可见
}
C++ 静态变量的使用场景是什么?未初始化的全局静态变量呢?
静态变量(包括全局静态、局部静态、类静态成员)的核心特点是生命周期贯穿程序运行始终,且作用域受限定,常见使用场景如下:
全局静态变量(
static修饰的全局变量)
- 作用:限制变量仅在当前文件内可见(避免不同文件中同名变量冲突),但生命周期是整个程序运行期间。
- 场景 :当多个文件需要独立使用同名变量(如统计各模块的内部计数),但不希望被其他文件访问或修改时。例:
static int count = 0;(仅当前.cpp文件可访问,其他文件即使声明extern int count也无法使用)。局部静态变量(函数内的
static变量)
- 作用:变量在函数第一次调用时初始化,后续调用不再重新初始化,值会被保留(生命周期全局,作用域仅限函数内)。
- 场景 :记录函数被调用的次数(如
static int call_count = 0; call_count++;);单例模式中,确保全局只存在一个实例(如函数内返回静态对象的指针);避免频繁创建销毁临时对象(如工具函数中复用的缓冲区)。类静态成员变量(
static修饰的类成员)
- 作用:属于整个类而非某个对象,所有对象共享该变量,生命周期全局,需在类外单独初始化。
- 场景 :统计类的实例数量(如
static int total;,在构造函数中total++,析构函数中total--);存储类级别的常量或共享配置(如static const int MAX_SIZE = 100;)。未初始化的全局静态变量
未初始化的全局静态变量(如
static int a;)有两个关键特性:
- 自动初始化 :编译器会将其默认初始化为
0(包括数值类型为0,指针类型为nullptr等)。这是因为全局静态变量存放在内存的 BSS 段(未初始化数据段),程序启动时系统会自动将该段所有数据清零。- 作用域限制:和初始化的全局静态变量一样,仅在当前文件内可见,不影响其他文件的同名变量。
cpp// file1.cpp static int uninit; // 未初始化,默认值为0,仅file1可见 // file2.cpp static int uninit; // 与file1的uninit无关,各自为0
介绍const 作用及用法?
作用:
const修饰成员变量,定义成const常量,相较于宏常量,可进行类型检查,节省内存空间,提高了效率。const修饰函数参数,使得传递过来的函数参数的值不能改变。const修饰成员函数,使得成员函数不能修改任何类型的成员变量(mutable修饰的变量除外),也不能调用非const成员函数,因为非const成员函数可能会修改成员变量。在类中的用法:
const 成员变量:
const成员变量只能在类内声明、定义,在构造函数初始化列表中初始化。const成员变量只在某个对象的生存周期内是常量,对于整个类而言却是可变的,因为类可以创建多个对象,不同类的const成员变量的值是不同的。因此不能在类的声明中初始化const成员变量,类的对象还没有创建,编译器不知道他的值。const 成员函数:
不能修改成员变量的值,除非有
mutable修饰;只能访问成员变量。不能调用非常量成员函数,以防修改成员变量的值。
修饰变量:定义常量:这是const最基本的用法,用于声明一个在初始化后其值不能被改变的变量。作用 :防止变量被意外修改,编译器会在编译期进行检查,任何修改
const变量的尝试都会导致编译错误。特点 :
- 必须初始化 :
const变量在定义时必须被赋予一个初始值。- 类型安全 :
const变量有明确的数据类型,编译器会进行类型检查。- 内部链接 :在 C++ 中,
const全局变量默认具有内部链接属性,这意味着它们只在定义它们的源文件(.cpp)内可见,避免了不同文件间的命名冲突。
cppconst double PI = 3.14159; // PI = 3.14; // 编译错误:不能修改常量修饰指针和引用
const与指针和引用结合使用时,可以提供更细粒度的控制。一个简单有效的记忆方法是:const在*左边,修饰的是指针指向的内容;const在*右边,修饰的是指针本身。指向常量的指针:指针指向的内容是常量,不能通过该指针来修改内容,但指针本身可以指向其他地址。
cppint a = 10, b = 20; const int* ptr = &a; // 或者 int const* ptr = &a; // *ptr = 30; // 编译错误:不能通过 ptr 修改 a 的值 ptr = &b; // 正确:ptr 可以指向别的地址常量指针:指针本身是常量,一旦初始化就不能再指向其他地址,但可以通过该指针修改它所指向的内容。
cppint a = 10, b = 20; int* const ptr = &a; *ptr = 30; // 正确:可以修改 a 的值 // ptr = &b; // 编译错误:ptr 不能指向别的地址指向常量的常量指针:指针本身和它指向的内容都是常量,两者都不能被修改。
cppint a = 10; const int* const ptr = &a; // *ptr = 30; // 编译错误 // ptr = &b; // 编译错误常量引用:引用本身在创建时就必须绑定到一个对象,因此它天生就是"常量指针"。
const引用 的主要用途是避免拷贝大对象,同时保证函数不会修改传入的参数。
cppvoid printValue(const std::string& str) { // str += "hello"; // 编译错误:不能修改 str std::cout << str << std::endl; }修饰函数参数和返回值
- 修饰函数参数:通常与引用或指针结合使用,目的是防止函数内部修改传入的参数,同时避免不必要的拷贝,提高效率。
cpp// 保证函数不会修改传入的 data 对象 void processData(const std::vector<int>& data);
- 修饰函数返回值:当函数返回一个指针或引用时,使用
const可以防止调用者通过返回值修改函数内部的私有数据。
cppclass DataHolder { int secret_data; public: // 返回一个常量引用,调用者不能修改 secret_data const int& getSecret() const { return secret_data; } };修饰类成员函数
这是
const在面向对象编程中的关键应用。将const放在成员函数的参数列表之后,表示这是一个"常成员函数"。
- 作用 :承诺该函数不会修改调用它的对象的任何非静态成员变量。
- 重要性 :
const对象只能调用const成员函数。这是一个重要的设计原则,它清晰地区分了"读取操作"(const函数)和"修改操作"(非const函数)。
cppclass Point { private: int x, y; public: Point(int x_val, int y_val) : x(x_val), y(y_val) {} // 常成员函数:只读取数据,不修改 int getX() const { return x; } // 非常量成员函数:会修改数据 void setX(int new_x) { x = new_x; } }; int main() { const Point p(1, 2); // 定义一个常量对象 p.getX(); // 正确:const 对象可以调用 const 成员函数 // p.setX(10); // 编译错误:const 对象不能调用非 const 成员函数 return 0; }
define 和 const 的区别是什么?
-
#define(预处理指令):- 阶段 :在预处理阶段进行简单的文本替换(宏替换)。
- 机制:编译器在编译代码之前,会将代码中所有的宏名直接替换为定义的值。它没有类型概念,也不分配内存(除非替换后的代码导致分配)。
- 例子 :
#define PI 3.14,编译器看到的是3.14,不知道PI是什么。
-
const(关键字):- 阶段 :在编译和运行阶段起作用。
- 机制:它定义了一个具有明确类型的变量,由编译器进行管理。它有数据类型,占用内存(通常在只读数据段或符号表中),并且受作用域规则限制。
- 例子 :
const double PI = 3.14;,编译器知道PI是一个double类型的常量。
| 特性 | #define |
const |
|---|---|---|
| 类型检查 | 无。仅仅是文本替换,不进行类型检查,容易引发隐患。 | 有。编译器会严格检查类型,类型不匹配会报错。 |
| 内存分配 | 不分配(通常)。只是替换文本,不占用内存空间(除非多次使用导致多次产生立即数)。 | 分配。作为变量存储在内存中(通常是只读段),调试时可以查看其地址。 |
| 作用域 | 无作用域限制。一旦定义,直到被 #undef 取消或文件结束都有效。 |
有作用域。遵循块作用域、命名空间作用域等规则(如局部 const、类内 const)。 |
| 调试 | 困难。预处理后宏名消失,调试器看不到宏名,只能看到具体的值。 | 容易。调试器可以识别 const 变量名,方便调试。 |
| 安全性 | 低。容易发生"边际效应"错误(见下文例子)。 | 高。避免了宏替换带来的逻辑错误。 |
define 和 typedef 的区别是什么?
本质的区别:#define 是预处理阶段的文本替换,而 typedef 是编译阶段对类型进行真正的别名定义。
处理阶段不同
-
#define:
- 在预处理阶段进行处理。编译器在正式编译之前,预处理器会将代码中所有的宏名简单地替换为定义的文本。
- 它不理解 C++ 的语法,只是纯粹的字符串操作。
-
typedef:
- 在编译阶段进行处理。由编译器解析,它理解 C++ 的语法和类型系统。
- 它为现有的类型创建一个新的别名,这个别名会被编译器记录在符号表中。
🧩 功能与原理不同
-
#define:
- 文本替换。它不仅可以定义类型别名,还可以定义常量、宏函数等。
- 例如:
#define INT int,在预处理后,代码中所有的INT都会变成int。
-
typedef:
- 类型别名。它专门用于给类型起别名,不能用于定义常量。
- 例如:
typedef int INT;,编译器知道INT是int类型的别名。
指针声明时的陷阱(关键区别)
这是两者最显著的区别,特别是在处理指针时。
- #define:由于是简单的文本替换,容易导致意想不到的结果。
cpp
#define PTR_INT int*
PTR_INT a, b;
// 预处理后变成:int* a, b;
// 结果:a 是 int* 类型(指针),但 b 是 int 类型(整数)!
typedef:遵循 C++ 的类型声明规则,行为符合直觉。
cpp
typedef int* PTR_INT;
PTR_INT a, b;
// 编译器理解为:a 和 b 都是 PTR_INT 类型(即 int*)
// 结果:a 和 b 都是 int* 类型(指针)。
作用域不同
-
#define :无作用域限制 。一旦定义,直到文件结束或被
#undef取消,它在任何地方都有效。这可能导致命名冲突。 -
typedef :有作用域 。遵循块作用域、命名空间作用域等规则。例如,在函数内定义的
typedef只在该函数内有效。
🛡️ 类型检查
-
#define :无类型检查。因为它只是文本替换,编译器在编译时看不到宏名,无法进行类型安全检查。
-
typedef :有类型检查。编译器知道别名的真实类型,可以进行严格的类型检查。
| 特性 | #define |
typedef |
|---|---|---|
| 处理阶段 | 预处理阶段 | 编译阶段 |
| 本质 | 文本替换 | 类型别名 |
| 指针声明 | 容易出错(#define PTR int*) |
安全正确(typedef int* PTR) |
| 作用域 | 全局(无作用域) | 有作用域(块、命名空间等) |
| 类型检查 | 无 | 有 |
| 功能 | 定义常量、宏函数、类型别名等 | 仅定义类型别名 |
cpp
#include <iostream>
#define INTPTR1 int *
typedef int * INTPTR2;
using namespace std;
int main()
{
INTPTR1 p1, p2; // p1: int *; p2: int
INTPTR2 p3, p4; // p3: int *; p4: int *
int var = 1;
// 相当于 const int * p5; 常量指针,即不可以通过 p5 去修改 p5 指向的内容
const INTPTR1 p5 = &var;
// 相当于 int * const p6; 指针常量,不可使 p6 再指向其他内容
const INTPTR2 p6 = &var;
return 0;
}
volatile 的作用?是否具有原子性,对编译器有什么影响?
volatile 的作用:当对象的值可能在程序的控制或检测之外被改变时,应该将该对象声明为 volatile,告知编译器不应对这样的对象进行优化。
volatile不具有原子性。
volatile 对编译器的影响:使用该关键字后,编译器不会对相应的对象进行优化,即不会将变量从内存缓存到寄存器中,防止多个线程有可能使用内存中的变量,有可能使用寄存器中的变量,从而导致程序错误。
什么情况下一定要用 volatile, 能否和 const 一起使用?
使用 volatile 关键字的场景:
- 当多个线程都会用到某一变量,并且该变量的值有可能发生改变时,需要用 volatile 关键字对该变量进行修饰;
- 中断服务程序中访问的变量或并行设备的硬件寄存器的变量,最好用 volatile 关键字修饰。
volatile 关键字和 const 关键字可以同时使用,某种类型可以既是 volatile 又是 const,同时具有二者的属性。