c++面试常问1

常量指针和指针常量的区别是什么?

常量指针:常量指针本质上是个指针,只不过这个指针指向的对象是常量。也就是说,不可以通过对指针解引用修改指针指向的内容,而可以修改指针的指向。

指针常量的本质上是个常量,只不过这个常量的值是一个指针,和上面那个恰恰相反,不可以修改指针的指向,但可以通过对指针解引用修改指针指向的内容。

特性 常量指针 (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 种:全局作用域,局部作用域,语句作用域,类作用域,命名空间作用域和文件作用域。

  1. 文件作用域 (File Scope)

这通常就是指全局作用域。在所有函数和类之外定义的变量就属于这个作用域。

  • 可见性 :从定义点开始,直到当前源文件(.cpp)的末尾。
  • 生命周期:贯穿整个程序的运行期。
  • 链接性 :默认具有外部链接 ,意味着其他文件可以通过 extern 关键字来访问它。
  1. 局部作用域 (Local Scope)

这是在函数内部定义的变量,也就是我们常说的局部变量

  • 可见性 :仅在定义它的函数或代码块 {} 内部有效。
  • 生命周期:当函数被调用时创建,函数返回时销毁。
  • 存储位置 :通常在**栈(Stack)**上。
  1. 语句作用域 (Statement Scope)

这通常被称为块作用域(Block Scope) ,是局部作用域的一种更精细的形式。它指的是在任何一个代码块(由 {} 包围)内部定义的变量。

  • 可见性 :仅在它所在的那个特定代码块内有效。例如,for 循环或 if 语句中定义的变量。
  • 生命周期:程序执行进入该块时创建,离开该块时销毁。
  • 典型场景for(int i = 0; ...) 中的循环变量 i
  1. 类作用域 (Class Scope)

这是在类(classstruct)内部定义的变量,即类的成员变量

  • 可见性 :在类的所有成员函数内部有效。外部需要通过对象(obj.member)或指针(ptr->member)来访问。
  • 生命周期:与类的实例(对象)的生命周期相同。对象创建时成员变量诞生,对象销毁时成员变量消亡。
  1. 命名空间作用域 (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
继承 不支持 支持(可继承其他 structclass
变量声明 必须带 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 示例

cpp 复制代码
struct 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',互不影响。
cpp 复制代码
union 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 关键字的作用非常丰富,它的核心语义根据使用场景的不同而变化,但总体上可以归纳为两个核心目的:控制生命周期控制作用域/可见性

  1. 修饰函数内的局部变量:延长生命周期

static 用于函数内部的局部变量时,它改变了变量的存储位置和生命周期。

  • 作用:变量不再存储在栈上,而是存储在静态存储区。它的生命周期从"函数调用结束即销毁"延长至"整个程序运行结束"。
  • 效果
    1. 只初始化一次:该变量在程序第一次执行到其定义语句时进行初始化,后续的函数调用会跳过初始化步骤。
    2. 值被保留:函数调用结束后,变量的值不会丢失,下次调用时会保留上次调用结束时的值。
  • 典型应用:统计函数调用次数、实现懒加载(Lazy Initialization)等。
  1. 修饰全局变量或普通函数:限制作用域(文件作用域)

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 的重要应用。

  • 作用:声明一个属于类本身,而非类的某个具体对象的变量。
  • 效果
    1. 所有对象共享:该类的所有实例共享同一份静态成员变量。
    2. 不占用对象内存 :它不计算在 sizeof(对象) 的大小内。
    3. 访问方式 :可以通过 类名::变量名 直接访问,无需创建对象。
  • 初始化 :静态成员变量必须在类外进行定义和初始化(除非是 static const 的整型或 C++17 的 inline static)。
  1. 修饰类的成员函数:实现类级别的工具方法

与静态成员变量类似,静态成员函数也属于类本身。

  • 作用:声明一个属于类的函数。
  • 效果
    1. this 指针 :静态成员函数不与任何对象绑定,因此没有 this 指针。
    2. 访问限制:它只能访问类的静态成员变量和其他静态成员函数,不能直接访问非静态成员。
    3. 访问方式 :可以通过 类名::函数名() 直接调用,无需创建对象。
  • 典型应用:工厂方法、工具函数、访问静态数据的接口等。
使用位置 核心作用 效果
函数内部 延长生命周期 变量"记住"上次的值,只初始化一次
文件作用域 限制作用域 变量/函数"文件私有",避免命名冲突
类内部 (变量) 实现数据共享 所有对象共享一份数据,属于类本身
类内部 (函数) 实现类级方法 无需对象即可调用,没有 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;)有两个关键特性:

  1. 自动初始化 :编译器会将其默认初始化为 0(包括数值类型为 0,指针类型为 nullptr 等)。这是因为全局静态变量存放在内存的 BSS 段(未初始化数据段),程序启动时系统会自动将该段所有数据清零。
  2. 作用域限制:和初始化的全局静态变量一样,仅在当前文件内可见,不影响其他文件的同名变量。
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 变量的尝试都会导致编译错误。

  • 特点

    1. 必须初始化const 变量在定义时必须被赋予一个初始值。
    2. 类型安全const 变量有明确的数据类型,编译器会进行类型检查。
    3. 内部链接 :在 C++ 中,const 全局变量默认具有内部链接属性,这意味着它们只在定义它们的源文件(.cpp)内可见,避免了不同文件间的命名冲突。
cpp 复制代码
const double PI = 3.14159;
// PI = 3.14; // 编译错误:不能修改常量

修饰指针和引用

const 与指针和引用结合使用时,可以提供更细粒度的控制。一个简单有效的记忆方法是:const* 左边,修饰的是指针指向的内容;const* 右边,修饰的是指针本身。

指向常量的指针:指针指向的内容是常量,不能通过该指针来修改内容,但指针本身可以指向其他地址。

cpp 复制代码
int a = 10, b = 20;
const int* ptr = &a; // 或者 int const* ptr = &a;
// *ptr = 30;       // 编译错误:不能通过 ptr 修改 a 的值
ptr = &b;           // 正确:ptr 可以指向别的地址

常量指针:指针本身是常量,一旦初始化就不能再指向其他地址,但可以通过该指针修改它所指向的内容。

cpp 复制代码
int a = 10, b = 20;
int* const ptr = &a;
*ptr = 30;          // 正确:可以修改 a 的值
// ptr = &b;        // 编译错误:ptr 不能指向别的地址

指向常量的常量指针:指针本身和它指向的内容都是常量,两者都不能被修改。

cpp 复制代码
int a = 10;
const int* const ptr = &a;
// *ptr = 30;       // 编译错误
// ptr = &b;        // 编译错误

常量引用:引用本身在创建时就必须绑定到一个对象,因此它天生就是"常量指针"。const 引用 的主要用途是避免拷贝大对象,同时保证函数不会修改传入的参数。

cpp 复制代码
void printValue(const std::string& str) {
    // str += "hello"; // 编译错误:不能修改 str
    std::cout << str << std::endl;
}

修饰函数参数和返回值

  1. 修饰函数参数:通常与引用或指针结合使用,目的是防止函数内部修改传入的参数,同时避免不必要的拷贝,提高效率。
cpp 复制代码
// 保证函数不会修改传入的 data 对象
void processData(const std::vector<int>& data); 
  1. 修饰函数返回值:当函数返回一个指针或引用时,使用 const 可以防止调用者通过返回值修改函数内部的私有数据。
cpp 复制代码
class DataHolder {
    int secret_data;
public:
    // 返回一个常量引用,调用者不能修改 secret_data
    const int& getSecret() const { return secret_data; }
};

修饰类成员函数

这是 const 在面向对象编程中的关键应用。将 const 放在成员函数的参数列表之后,表示这是一个"常成员函数"。

  • 作用 :承诺该函数不会修改调用它的对象的任何非静态成员变量。
  • 重要性const 对象只能调用 const 成员函数。这是一个重要的设计原则,它清晰地区分了"读取操作"(const 函数)和"修改操作"(非 const 函数)。
cpp 复制代码
class 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;,编译器知道 INTint 类型的别名。

指针声明时的陷阱(关键区别)

这是两者最显著的区别,特别是在处理指针时。

  • #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,同时具有二者的属性。

相关推荐
weixin_568996062 小时前
如何用 IndexedDB 存储从 API 获取的超大列表并实现二级索引
jvm·数据库·python
2301_775148152 小时前
如何授权AWR报告生成_GRANT SELECT ANY DICTIONARY诊断权限
jvm·数据库·python
点云侠2 小时前
隧道中线提取的优化方法
c++·算法·最小二乘法
汉克老师2 小时前
GESP2023年6月认证C++三级( 第二部分判断题(1-10))
c++·数组·位运算·进制·gesp三级·gesp3级
minji...2 小时前
Linux 线程同步与互斥(五) 日志,线程池
linux·运维·服务器·开发语言·c++·算法
6Hzlia2 小时前
【Hot 100 刷题计划】 LeetCode 108. 将有序数组转换为二叉搜索树 | C++ 分治法详解
c++·算法·leetcode
兩尛2 小时前
c++面试常问2
开发语言·c++·面试
re林檎3 小时前
八大排序算法(C++实现)
c++·算法·排序算法
此生只爱蛋3 小时前
【vscode环境配置心得】C++版
c++·ide·vscode