C++基础
文章目录
-
- C++基础
-
- [一、++在前 vs ++在后](#一、++在前 vs ++在后)
-
- [1. 前置++ vs 后置++](#1. 前置++ vs 后置++)
- [2. C++ 中简单赋值/自增操作是否线程安全](#2. C++ 中简单赋值/自增操作是否线程安全)
- [二、函数指针 vs 指针函数](#二、函数指针 vs 指针函数)
-
- [1. 函数指针](#1. 函数指针)
- [2. 指针函数](#2. 指针函数)
- [三、`struct` 和 `Class`区别](#三、
struct
和Class
区别) -
- [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 参数只是一个区分用途,根本不会真的被用到。你可以把它看作是一个"标签"或"标志",用来告诉编译器这是"后置"。
- 二者区别:
- 前置 ++:直接对对象自身做自增,返回引用。
- 后置 ++:先复制当前对象作为临时变量,执行前置 ++,然后返回临时副本。
- 为什么后置返回对象,而不是引用?
- 因为后置 ++ 需要返回的是旧值,而旧值是通过拷贝临时对象来实现的。如果返回引用,这个临时对象会在函数结束时被销毁,那么引用将变成悬垂引用,造成错误。
- 为什么后置的前面也要加 const?
- 后置 ++ 返回的是一个临时旧值副本,你本来就不应该改它。
- 加上 const,是为了防止你误操作一个即将销毁的对象。
- 处理用户自定义类型时建议用前置 ++:
- 因为后置 ++ 会产生临时对象,涉及一次构造和析构,性能较差。
- 对于自定义类型推荐使用前置 ++,避免不必要的性能开销。
区别:
运算符类型 | 返回类型 | 是否加 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;
}
使用场景:
- 回调函数机制:把函数地址传入其他函数,让它在适当的时候调用。
- 函数指针数组:可以用函数指针数组实现类似状态机设计,类似状态切换,根据输入决定调用哪个函数。
- 动态链接库调用:动态加载
.so
或.dll
库时通过函数指针调用函数。 - 多态或虚函数模拟:虚函数+函数指针结合使用,可实现类似多态的效果。
- 参数函数化(高阶函数):把函数作为参数传递实现策略模式、插件模式等,实现可插拔的函数行为。
2. 指针函数
定义:
- 指针函数就是返回指针的函数,用于返回指向某种类型数据的指针。
使用:
cpp
int* getPointer() {
int x = 10;
return &x; // ❌ 非常危险,返回了局部变量地址,不建议这样使用
}
三、struct
和 Class
区别
1. 二者本质
在 C++ 中,struct
和 class
的底层实现是一样的,两者都可以:
- 定义成员变量、成员函数
- 使用构造函数、析构函数
- 支持继承、多态、虚函数等面向对象特性
它们的唯一差异是:
项目 | struct 默认 | class 默认 |
---|---|---|
成员访问权限 | public | private |
基类继承权限 | public | private |
2. 访问权限的默认区别
struct
默认是public
:
cpp
struct A {
int x; // 默认 public
void f(); // 默认 public
};
// 等价于
struct A {
public:
int x;
void f();
};
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 继承
这影响到了派生类是否可以访问基类的 public
或 protected
成员。
4. 使用场景
- 使用 struct 的常见场景:(表达数据)
- 表示纯数据结构(POD,Plain Old Data):
- 点、向量、颜色、配置项等
- 无需封装逻辑、继承、保护数据
- 只需要数据公开访问,不考虑封装
- 表示纯数据结构(POD,Plain Old Data):
- 使用 class 的常见场景:(表达对象)
- 封装行为(封装数据 + 操作)
- 面向对象设计:继承、多态、抽象类
- 希望控制访问权限(private/protected)
5. 编译器行为
对于 struct
和 Class
如果你没有显式写构造函数、析构函数,编译器会自动生成默认构造函数,包括:
- 默认构造函数
- 拷贝构造函数
- 拷贝赋值运算符
- 析构函数
四、静态局部变量
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:静态转换(编译期检查)
- 特点:
- 在编译期 执行类型检查,没有运行时开销。
- 用于安全的向下或向上转换
- 不进行动态类型检查(所以不适合带有虚函数的多态类进行 downcast)
- 安全性:
- 向上转换(派生类 → 基类):安全
- 向下转换(基类 → 派生类):不安全,除非你能确保类型确实正确
- 使用示例:
cpp
int x = 42;
char c = static_cast<char>(x); // 基本类型转换
Base* b = new Derived();
Derived* d = static_cast<Derived*>(b); // 向下转换,编译能过,但风险大
2. dynamic_cast:动态转换(运行期检查)
- 特点:
- 用于多态类(有虚函数的类)之间的安全转换
- 会在运行时进行类型检查(RTTI)
- 适合基类指针/引用 → 派生类指针/引用的转换
- 安全性:
- 向下转换时:dynamic_cast具有类型检查(信息在虚函数中)的功能,比static_cast更安全。
- 行为:
- 指针转换失败:返回 nullptr
- 引用转换失败:抛出 std::bad_cast 异常
- 使用示例:
cpp
Base* b = new Derived();
Derived* d = dynamic_cast<Derived*>(b); // 安全转换
if (d) {
// 转换成功
}
3. reinterpret_cast:重新解释类型(极端用法)
- 特点:
- 直接重解释内存内容,不做任何类型安全检查
- 可以将指针强转为其他类型的指针,或整数、数组等
- 具有极强的平台相关性和不安全性
- 警告:
除非你完全清楚数据布局和用途,否则不推荐使用,属于"非常规操作"。
4. const_cast:去除 const/volatile 限定符
- 特点:
- 用于去掉 const 或 volatile 修饰的属性(常量指针转换为非常量指针,并且仍然指向原来的对象。常量引用被转换为非常量引用,并且仍然指向原来的对象。)
- 只能在转换时移除常量性,不能添加