C++八股 | Day2 | atom/函数指针/指针函数/struct、Class/静态局部变量、局部变量、全局变量/强制类型转换

C++基础

文章目录

    • C++基础
      • [一、++在前 vs ++在后](#一、++在前 vs ++在后)
        • [1. 前置++ vs 后置++](#1. 前置++ vs 后置++)
        • [2. C++ 中简单赋值/自增操作是否线程安全](#2. C++ 中简单赋值/自增操作是否线程安全)
      • [二、函数指针 vs 指针函数](#二、函数指针 vs 指针函数)
        • [1. 函数指针](#1. 函数指针)
        • [2. 指针函数](#2. 指针函数)
      • [三、`struct` 和 `Class`区别](#三、structClass区别)
        • [1. 二者本质](#1. 二者本质)
        • [2. 访问权限的默认区别](#2. 访问权限的默认区别)
        • [3. 继承权限默认不同](#3. 继承权限默认不同)
        • [4. 使用场景](#4. 使用场景)
        • [5. 编译器行为](#5. 编译器行为)
      • [四、`静态局部变量` vs `全局变量` vs `局部变量`的区别](#四、静态局部变量 vs 全局变量 vs 局部变量的区别)
        • [1. 静态局部变量(static)](#1. 静态局部变量(static))
        • [2. 全局变量](#2. 全局变量)
        • [3. 局部变量](#3. 局部变量)
        • [4. 总结](#4. 总结)
      • 五、强制类型转换
        • [1. static_cast:静态转换(编译期检查)](#1. static_cast:静态转换(编译期检查))
        • [2. dynamic_cast:动态转换(运行期检查)](#2. dynamic_cast:动态转换(运行期检查))
        • [3. reinterpret_cast:重新解释类型(极端用法)](#3. reinterpret_cast:重新解释类型(极端用法))
        • [4. const_cast:去除 const/volatile 限定符](#4. const_cast:去除 const/volatile 限定符)

一、++在前 vs ++在后

1. 前置++ vs 后置++

二者实现代码

cpp 复制代码
// 前置++
self &operator++() {
    node = (linktype)((node).next);
    return *this;
}

// 后置++
const self operator++(int) {
    self tmp = *this;
    ++*this;
    return tmp;
}

区分前置和后置的方法

cpp 复制代码
MyIter it;
++it;      // 调用 operator++()        -> 前置 ++
it++;      // 调用 operator++(int)    -> 后置 ++

后置版本必须接受一个 int 类型的参数(通常不使用它)。这个 int 参数只是一个区分用途,根本不会真的被用到。你可以把它看作是一个"标签"或"标志",用来告诉编译器这是"后置"。

  1. 二者区别:
    • 前置 ++:直接对对象自身做自增,返回引用。
    • 后置 ++:先复制当前对象作为临时变量,执行前置 ++,然后返回临时副本。
  2. 为什么后置返回对象,而不是引用?
    • 因为后置 ++ 需要返回的是旧值,而旧值是通过拷贝临时对象来实现的。如果返回引用,这个临时对象会在函数结束时被销毁,那么引用将变成悬垂引用,造成错误。
  3. 为什么后置的前面也要加 const?
    • 后置 ++ 返回的是一个临时旧值副本,你本来就不应该改它。
    • 加上 const,是为了防止你误操作一个即将销毁的对象。
  4. 处理用户自定义类型时建议用前置 ++:
    • 因为后置 ++ 会产生临时对象,涉及一次构造和析构,性能较差。
    • 对于自定义类型推荐使用前置 ++,避免不必要的性能开销。

区别:

运算符类型 返回类型 是否加 const 原因
前置 ++ self& ❌ 不加 返回的是本体,允许继续操作
后置 ++ const self ✅ 一定加 返回的是副本,禁止误操作
2. C++ 中简单赋值/自增操作是否线程安全

答案:不是线程安全的。

在多线程程序中,很多人以为像 a++a = b 这样的简单语句是原子的、线程安全的,其实不是。虽然从 C/C++ 语法上看是一条语句,但编译器生成的底层汇编代码却不是一条指令,而是分为多个步骤,因此在多个线程同时操作时,容易出现竞态条件(race condition)。

❖ 例子1:a++

  • a++ 实际会被编译成三条汇编指令:
asm 复制代码
mov eax, dword ptr [a]  ; 从内存中读 a 的值到寄存器
inc eax                 ; 给寄存器的值加 1
mov dword ptr [a], eax  ; 把结果写回内存
  • 假设 a = 0,如果有两个线程同时执行 a++,看起来应该会变成 2,结果有可能是 1!为什么?
    • 假设初始 a = 0
    • 线程1执行第一条指令 a = 0 → eax = 0
    • 被中断,线程2也执行:a = 0 → eax = 0 → eax++ → 写回1
    • 回到线程1继续执行:eax++(0+1)→ 写回1
    • 最终两次自增后仍然是1,丢失一次操作

❖ 例子2:a = b

  • 表面上是赋值语句,底层汇编其实也不是原子的:
asm 复制代码
mov eax, dword ptr [b]  ; 把 b 的值读到寄存器
mov dword ptr [a], eax  ; 写入 a
  • 如果两个线程同时执行这句,线程切换的时机不同,也可能导致 a 的值不是预期的,出现数据不一致。

❖ 解决方案:使用 std::atomic

C++11 提供了 std::atomic<T>,用于确保对变量的原子操作,即线程安全的读写和更新。

使用方法:

  • 定义方法:
cpp 复制代码
std::atomic<int> value;
value = 99;
  • 注意:
    • std::atomic 禁止拷贝构造(拷贝构造是 =delete 的),因此不能这样写:
cpp 复制代码
std::atomic<int> value = 99;  // 编译错误

二、函数指针 vs 指针函数

1. 函数指针

定义:

  • 函数指针就是指向函数的指针变量,它可以保存函数的地址,从而允许我们在运行时动态选择要调用的函数。

语法定义:

cpp 复制代码
int (*operationPtr)(int, int);

含义:operationPtr 是一个指向参数为两个 int,返回值为 int 的函数的指针变量。

使用:

cpp 复制代码
int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

int main() {
    int (*operationPtr)(int, int);

    operationPtr = &add;
    int result = operationPtr(10, 5);  // 调用 add(10, 5)
    cout << result << endl;  // 输出 15

    operationPtr = &subtract;
    result = operationPtr(10, 5);  // 调用 subtract(10, 5)
    cout << result << endl;  // 输出 5
}

两种写法:

cpp 复制代码
int add(int a, int b) {
    return a + b;
}
int main() {
    int (*ptr)(int, int) = &add;

    int result1 = ptr(3, 4);        // 推荐写法
    int result2 = (*ptr)(3, 4);     // 也可以

    cout << result1 << " " << result2 << endl;  // 输出:7 7
    return 0;
}

使用场景:

  1. 回调函数机制:把函数地址传入其他函数,让它在适当的时候调用。
  2. 函数指针数组:可以用函数指针数组实现类似状态机设计,类似状态切换,根据输入决定调用哪个函数。
  3. 动态链接库调用:动态加载 .so.dll 库时通过函数指针调用函数。
  4. 多态或虚函数模拟:虚函数+函数指针结合使用,可实现类似多态的效果。
  5. 参数函数化(高阶函数):把函数作为参数传递实现策略模式、插件模式等,实现可插拔的函数行为。
2. 指针函数

定义:

  • 指针函数就是返回指针的函数,用于返回指向某种类型数据的指针。
    使用:
cpp 复制代码
int* getPointer() {
    int x = 10;
    return &x;  // ❌ 非常危险,返回了局部变量地址,不建议这样使用
}

三、structClass区别

1. 二者本质

在 C++ 中,structclass的底层实现是一样的,两者都可以:

  • 定义成员变量、成员函数
  • 使用构造函数、析构函数
  • 支持继承、多态、虚函数等面向对象特性

它们的唯一差异是:

项目 struct 默认 class 默认
成员访问权限 public private
基类继承权限 public private
2. 访问权限的默认区别
  1. struct 默认是 public
cpp 复制代码
struct A {
    int x;             // 默认 public
    void f();          // 默认 public
};

// 等价于
struct A {
public:
    int x;
    void f();
};
  1. class 默认是 private
cpp 复制代码
class B {
    int x;             // 默认 private
    void f();          // 默认 private
};

// 等价于
class B {
private:
    int x;
    void f();
};
3. 继承权限默认不同
cpp 复制代码
struct A {};
class B {};

// 默认继承权限
struct Derived1 : A {};      // 默认是 public 继承
class Derived2 : A {};       // 默认是 private 继承

这影响到了派生类是否可以访问基类的 publicprotected 成员。

4. 使用场景
  1. 使用 struct 的常见场景:(表达数据)
    • 表示纯数据结构(POD,Plain Old Data):
      • 点、向量、颜色、配置项等
    • 无需封装逻辑、继承、保护数据
    • 只需要数据公开访问,不考虑封装
  2. 使用 class 的常见场景:(表达对象)
    • 封装行为(封装数据 + 操作)
    • 面向对象设计:继承、多态、抽象类
    • 希望控制访问权限(private/protected)
5. 编译器行为

对于 structClass 如果你没有显式写构造函数、析构函数,编译器会自动生成默认构造函数,包括:

  • 默认构造函数
  • 拷贝构造函数
  • 拷贝赋值运算符
  • 析构函数

四、静态局部变量 vs 全局变量 vs 局部变量的区别

1. 静态局部变量(static)
cpp 复制代码
void exampleFunction() {
    static int count = 0;
    count++;
    cout << "Count: " << count << endl;
}

特点:

  • 作用域:只在定义它的函数(或块)内有效。
  • 生命周期:整个程序运行期间存在,不会因为函数结束而销毁。
  • 初始化:只初始化一次,下次再进入函数时变量保留上一次的值。
  • 访问限制:只能在定义它的函数内部访问,外部访问不到。

使用场景:当你希望函数之间调用保留变量的值(如计数器、缓存)但不暴露给全局使用。

2. 全局变量
cpp 复制代码
int globalVar = 10;  // 全局变量

void function1() {
    globalVar++;
}

void function2() {
    globalVar--;
}

特点:

  • 作用域:整个程序所有函数都可以访问。
  • 生命周期:从程序开始到程序结束都存在。
  • 关键字:定义在函数外,不需要关键字。
  • 访问权限:全局共享,所有函数都能访问和修改。

使用场景:当多个函数需要共享同一份数据时使用。(全局变量太多会让程序耦合性高,调试和维护变困难,,因此需要谨慎使用)

3. 局部变量
cpp 复制代码
void myFunction() {
    int localVar = 5;
}

特点:

  • 作用域:仅在它所属的代码块(函数、if、for 等)中有效。
  • 生命周期:进入代码块时创建,离开时销毁。
  • 关键字:无特别关键字(在定义的代码块内,普通变量声明)。
  • 访问权限:只能在定义的作用域中使用,出了作用域就无效。

使用场景:当变量只在某一段代码中使用,且不需保留状态或共享时,使用局部变量最合适。

4. 总结
类型 作用域 生命周期 特点/用途
局部变量 代码块/函数 每次进入创建,退出销毁 最小作用域,不占资源
静态局部变量 函数/块内 程序整个生命周期 记住状态,但只能内部访问
全局变量 所有文件/函数 程序整个生命周期 所有函数共享数据,但耦合度高

五、强制类型转换

1. static_cast:静态转换(编译期检查)
  1. 特点:
    • 编译期 执行类型检查,没有运行时开销
    • 用于安全的向下或向上转换
    • 不进行动态类型检查(所以不适合带有虚函数的多态类进行 downcast)
  2. 安全性:
    • 向上转换(派生类 → 基类):安全
    • 向下转换(基类 → 派生类):不安全,除非你能确保类型确实正确
  3. 使用示例:
cpp 复制代码
int x = 42;
char c = static_cast<char>(x);  // 基本类型转换

Base* b = new Derived();
Derived* d = static_cast<Derived*>(b);  // 向下转换,编译能过,但风险大
2. dynamic_cast:动态转换(运行期检查)
  1. 特点:
    • 用于多态类(有虚函数的类)之间的安全转换
    • 会在运行时进行类型检查(RTTI)
    • 适合基类指针/引用 → 派生类指针/引用的转换
  2. 安全性:
    • 向下转换时:dynamic_cast具有类型检查(信息在虚函数中)的功能,比static_cast更安全。
  3. 行为:
    • 指针转换失败:返回 nullptr
    • 引用转换失败:抛出 std::bad_cast 异常
  4. 使用示例:
cpp 复制代码
Base* b = new Derived();
Derived* d = dynamic_cast<Derived*>(b);  // 安全转换
if (d) {
    // 转换成功
}
3. reinterpret_cast:重新解释类型(极端用法)
  1. 特点:
    • 直接重解释内存内容,不做任何类型安全检查
    • 可以将指针强转为其他类型的指针,或整数、数组等
    • 具有极强的平台相关性和不安全性
  2. 警告:
    除非你完全清楚数据布局和用途,否则不推荐使用,属于"非常规操作"。
4. const_cast:去除 const/volatile 限定符
  1. 特点:
    • 用于去掉 const 或 volatile 修饰的属性(常量指针转换为非常量指针,并且仍然指向原来的对象。常量引用被转换为非常量引用,并且仍然指向原来的对象。)
    • 只能在转换时移除常量性,不能添加
相关推荐
Dovis(誓平步青云)12 分钟前
探索C++标准模板库(STL):String接口的底层实现(下篇)
开发语言·c++·stl·string
KyollBM1 小时前
【CF】Day75——CF (Div. 2) B (数学 + 贪心) + CF 882 (Div. 2) C (01Trie | 区间最大异或和)
c语言·c++·算法
feiyangqingyun1 小时前
Qt/C++开发监控GB28181系统/取流协议/同时支持udp/tcp被动/tcp主动
c++·qt·udp·gb28181
CV点灯大师1 小时前
C++算法训练营 Day10 栈与队列(1)
c++·redis·算法
成工小白2 小时前
【C++ 】智能指针:内存管理的 “自动导航仪”
开发语言·c++·智能指针
sc写算法2 小时前
基于nlohmann/json 实现 从C++对象转换成JSON数据格式
开发语言·c++·json
SunkingYang2 小时前
C++中如何遍历map?
c++·stl·map·遍历·方法
Andrew_Xzw2 小时前
数据结构与算法(快速基础C++版)
开发语言·数据结构·c++·python·深度学习·算法
库库的里昂2 小时前
【C++从练气到飞升】03---构造函数和析构函数
开发语言·c++
momo卡2 小时前
MinGW-w64的安装详细步骤(c_c++的编译器gcc、g++的windows版,win10、win11真实可用)
c语言·c++·windows