C++八股
约 2162 字大约 7 分钟
2025-03-19
左值与右值
什么是左值和右值?
左值和右值的区分:
- 左值特点
- 有明确的内存地址
- 可以取地址
- 可以出现在赋值语句左边
int a = 10; // a是左值
int& ref = a; // 左值引用
- 右值特点
- 临时的值或对象
- 不能取地址
- 只能出现在赋值语句右边
int b = a + 1; // a+1是右值
int&& rref = std::move(a); // 右值引用
- 作用
主要用于:- 移动语义
- 完美转发
- 资源管理优化
什么是左值引用和右值引用?
- 左值引用
理解为左值的引用,可以修改引用对象的值且不产生额外的空间开销
// 基本使用
int a = 1;
int& ref = a; // 左值引用
ref = 2; // 修改原变量值
// 用于函数参数
void func(int& x) {
x += 1; // 直接修改原参数
}
// const左值引用
const int& cref = 10; // 可以绑定右值,此时常量数字会存储在内存中,可以取地址
const int& cref2 = a; // 不能通过引用修改值
- 右值引用
可以理解为对右值的引用,即对一个临时对象或者即将销毁的对象的引用
注
右值引用是一个左值
// 基本使用
int&& rref = 10; // 绑定右值
int&& rref2 = std::move(a); // 将左值转为右值
// 移动构造函数
class MyClass {
public:
MyClass(MyClass&& other) noexcept {
// 转移资源所有权
data = other.data;
other.data = nullptr;
}
private:
int* data;
};
// 移动赋值运算符
MyClass& operator=(MyClass&& other) noexcept {
if(this != &other) {
delete data;
data = other.data;
other.data = nullptr;
}
return *this;
}
// 实例
MyClass obj1;
MyClass obj2 = std::move(obj1); // 移动构造
obj2 = std::move(obj1); // 移动赋值
- 完美转发
指可以写一个接受任意实参的函数模板,并转发到其它函数,目标函数会收到与转发函数完全相同的实参,转发函数实参是左值那么目标函数实参也是左值,相应的右值转发也是右值
使用std::forward
实现完美转发
template<typename T>
void wrapper(T&& arg) {
func(std::forward<T>(arg)); // 保持参数的值类别
}
- 应用场景
// 避免拷贝
void processVector(const vector<int>& vec); // 传大对象用const引用
// 资源转移
vector<int> createVector() {
vector<int> temp = {1,2,3};
return temp; // 自动触发移动语义
}
// 实现移动语义
unique_ptr<int> ptr1(new int(10));
unique_ptr<int> ptr2 = std::move(ptr1); // ptr1变为空
数组和链表
数组和链表的主要区别
- 内存分配
// 数组:连续内存
int arr[5] = {1,2,3,4,5};
// 链表:分散内存
struct Node {
int val;
Node* next;
};
- 基本操作时间复杂度
数据结构 | 随机访问 | 头部插入 | 尾部插入 | 中间插入 |
---|---|---|---|---|
数组 | O(1) | O(n) | O(1)未满 | O(n) |
链表 | O(n) | O(1) | O(1)保存尾指针 | O(1)找到位置后 |
- 优缺点
数据结构 | 优点 | 缺点 |
---|---|---|
数组 | 随机访问快 内存利用率高 | 大小固定 插入删除慢 |
链表 | 动态大小 插入删除快 | 随机访问慢 额外空间开销 |
重载
重载分为函数重载
和运算符重载
函数重载
函数重载指同一作用域内允许存在多个同名函数
,它们的参数个数
不同或参数类型
不同
要注意函数的返回类型
不同时不能算作重载
// 当函数名相同但参数类型或数量不同时,构成函数重载
class Example {
public:
void func(int x) { }
void func(double x) { }
void func(int x, int y) { }
// 函数返回值返回值不同不构成重载
// int func(int x) { } // 错误
};
优点: 增强代码可读性,通过使用函数重载定义一个通用接口,改善程序可维护性
运算符重载
通过运算符重载,可以定义如何使用运算符
(如+-*/==等)来操作自定义类型
class Complex {
public:
// 成员函数重载
Complex operator+(const Complex& c) {
return Complex(real + c.real);
}
// 友元函数重载
friend Complex operator-(const Complex& a, const Complex& b);
private:
double real;
};
注意事项
- 不能改变运算符优先级
- 不能创建新运算符
- 某些运算符不能重载(如::, .)
- 保持语义一致性
函数重载和函数重写(覆盖)
函数重载和重写的区别:
- 函数重载(Overload)
class Example {
void func(int x) { } // 同一个类中
void func(int x, int y) { } // 参数不同
void func(double x) { } // 构成重载
};
- 函数重写(Override)
class Base {
virtual void show() { } // 基类虚函数
};
class Derived : public Base {
void show() override { } // 派生类重写
// 函数名、参数、返回值必须完全相同
};
- 主要区别
- 重载:同一作用域,参数不同
- 重写:派生类中覆盖基类虚函数
- 重载是编译时多态
- 重写是运行时多态
友元
友元函数和友元类
友元关系是一种单向的访问权限,不会破坏封装性,也不会牵涉到类之间的继承关系
- 友元函数:允许某个类
外
的函数访问该类的私有成员
和保护成员
class MyClass {
private:
int privateMember;
public:
MyClass() : privateMember(0) {}
//声明友元函数
friend void friendFunction(MyClass &obj);
};
void friendFunction(MyClass &obj) {
//访问 privateMember
obj.privateMember = 10;
}
- 友元类:允许
另一个类
访问某个类的私有成员
和保护成员
class B; //前向声明
class A {
private:
int privateMember;
public:
A() : privateMember(0) {}
//声明B为友元类
friend class B;
};
class B {
public:
void accessA(A &obj) {
//访问 A 的 privateMember
obj.privateMember = 20;
}
};
多态
多态作为面向对象
的三大特征之一,指的是同一个接口
可以有多个不同的实现
,即同一个函数或方法调用会根据上下文的不同而执行不同的功能
多态又分为静态多态
和动态多态
,静态多态通过函数重载
和模板
实现,动态多态通过虚函数
实现
静态多态
静态多态借助函数重载
和模板
完成,函数在编译时
确定调用哪个函数,该方式可以提高效率,但使用灵活性较差
void print(int i) {
cout << "Integer: " << i << endl;
}
void print(double f) {
cout << "Float: " << f << endl;
}
template <typename T>
void print(T t) {
cout << "Template: " << t << endl;
}
动态多态
函数在运行时
确定调用哪个函数,这种方式在使用时更加灵活,但效率较低
为了实现动态多态,C++编译器
会为每个包含虚函数的类生成一个虚函数表
,虚函数表中存储了指向该类虚函数的指针
,每个对象实例包含一个指向虚函数表
的指针,通过这个指针来调用实际的函数实现
在如下示例中,通过基类指针basePtr
调用了派生类Derived
的show
方法,就是动态多态的实现
#include <iostream>
using namespace std;
class Base {
public:
virtual void show() {
cout << "Base class show function" << endl; // 基类虚函数
}
virtual ~Base() {} // 虚析构函数
};
class Derived : public Base {
public:
void show() override {
cout << "Derived class show function" << endl;
}
};
int main() {
Base* basePtr;
Derived derivedObj;
// or: Base* basePtr = new Derived();
basePtr = &derivedObj;
basePtr->show(); // 输出:Derived class show function
return 0;
}
关键点
- 需要虚函数
- 需要通过基类指针/引用调用
- 派生类重写虚函数
- 基类需要虚析构函数
进程与线程
进程与线程的区别:
- 基本概念
进程:
- 独立的执行单位
- 资源分配的基本单位
- 拥有独立的地址空间
线程:
- 进程内的执行单位
- CPU调度的基本单位
- 共享进程的地址空间
- 资源共享
进程间资源:
- 独立的内存空间
- 独立的文件描述符
- 独立的PID
线程间共享:
- 代码段
- 数据段
- 堆空间
- 文件描述符
线程独有:
- 栈空间
- 寄存器
- 线程ID(TID)
- 通信方式
进程间通信(IPC):
- 管道(pipe)
- 消息队列
- 共享内存
- 信号量
- 套接字(socket)
线程间通信:
- 直接共享变量
- 互斥锁(mutex)
- 条件变量
- 信号量
- 开销比较
进程开销大:
- 创建新的地址空间
- 分配资源
- 建立页表
线程开销小:
- 共享进程资源
- 仅创建线程独有资源
- 切换速度快
- 安全性
进程:
- 地址空间隔离
- 一个进程崩溃不影响其他进程
- 安全性高
线程:
- 共享地址空间
- 一个线程崩溃导致整个进程崩溃
- 需要注意同步问题