文章

c++高频总结七

完美转发介绍一下 去掉std::forward会怎样?

完美转发(Perfect Forwarding)是C++中的一个技术,用于将函数模板参数无损地传递给另一个函数。这主要通过 右值引用std::forward 实现。它的目标是保持参数的值类别(左值或右值)不变,从而避免多余的拷贝或移动。


完美转发的核心要点

  1. 右值引用 (T&&) 作为模板参数的特殊性:
    • T&& 是一个转发引用(Forwarding Reference)或万能引用(Universal Reference)。
    • 当结合模板类型推导时,T&&可以匹配左值或右值:
      • 左值传参时,T 推导为左值引用类型(T&)。
      • 右值传参时,T 推导为原类型(非引用类型)。
  2. std::forward 的作用
    • 根据参数的值类别,选择性地将参数转为左值或右值:
      • 如果 T 是左值引用类型,则 std::forward<T>(arg) 保持为左值。
      • 如果 T 是非引用类型,则 std::forward<T>(arg) 转为右值。
    • 它本质上是一个条件转换,保证参数的值类别与初始调用一致。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <utility> // std::forward

void func(int& x) {
    std::cout << "Lvalue reference: " << x << '\n';
}

void func(int&& x) {
    std::cout << "Rvalue reference: " << x << '\n';
}

template <typename T>
void wrapper(T&& arg) {
    // 完美转发 arg 到 func
    func(std::forward<T>(arg));
}

int main() {
    int a = 42;
    wrapper(a);       // 输出:Lvalue reference: 42
    wrapper(42);      // 输出:Rvalue reference: 42
}

去掉 std::forward 会怎样?

如果去掉 std::forward,代码会退化为普通传参,导致参数的值类别被破坏。

修改后的代码:

1
2
3
4
5
template <typename T>
void wrapper(T&& arg) {
    // 直接传递 arg 而不是使用 std::forward
    func(arg);
}

运行结果:

1
2
3
4
5
int main() {
    int a = 42;
    wrapper(a);       // 输出:Lvalue reference: 42
    wrapper(42);      // 输出:Lvalue reference: 42
}
  • wrapper(42) 的情况下,本来应当调用右值引用版本的 func(int&&),但因为 argwrapper 内是一个左值(函数参数是命名的),最终调用了左值引用版本 func(int&)
  • 右值信息丢失,可能导致非预期的拷贝或资源管理问题。

总结

  • std::forward 是完美转发的关键,可以在模板中根据参数的值类别正确地传递参数。
  • 去掉 std::forward会破坏参数的值类别:
    • 左值仍然是左值。
    • 右值会被降级为左值。
  • 如果你的代码需要根据参数值类别区分逻辑(如重载函数处理左值和右值),不使用 std::forward 会导致错误或非最佳的性能表现。

在实际开发中,如果涉及模板函数和转发引用,始终使用 std::forward 来确保完美转发。

介绍一下unique_lock和lock_guard区别

std::unique_lockstd::lock_guard 是 C++ 标准库中两种用于管理互斥锁(std::mutex)的工具,它们都提供了 RAII(资源获取即初始化)机制来管理锁的生命周期,但它们的使用场景和功能存在一定差异。


1. std::lock_guard

特点

  • 简单轻量:主要用于简单的互斥锁场景。
  • 作用域锁:在对象生命周期内锁住互斥量,生命周期结束时自动解锁。
  • 不可控锁行为lock_guard 不支持手动解锁或重新加锁操作。

适用场景

适用于锁操作简单且生命周期明确的场景,例如函数内需要短时间保护临界区。

代码示例

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <mutex>

std::mutex mtx;

void simple_lock_guard_example() {
    std::lock_guard<std::mutex> lock(mtx); // 构造时锁定互斥量
    std::cout << "Lock_guard is holding the lock.\n";
    // 临界区代码
} // lock 离开作用域时自动解锁

优点

  • 易于使用,简单安全。
  • 性能开销低,不支持复杂操作,但在大多数简单场景中已经足够。

2. std::unique_lock

特点

  • 灵活性更高:支持延迟锁定、显式解锁和重新加锁。
  • 可移动:允许将锁的所有权从一个 unique_lock 对象转移到另一个。
  • 条件变量兼容:通常用于需要与条件变量(std::condition_variable)配合的场景。

适用场景

适用于复杂的锁管理需求,例如需要显式解锁、重新加锁、延迟锁定或者与条件变量配合使用的场景。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <mutex>
#include <thread>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void worker() {
    std::unique_lock<std::mutex> lock(mtx); // 构造时锁定互斥量
    cv.wait(lock, [] { return ready; });   // 等待条件变量,自动释放并重新锁定互斥量
    std::cout << "Worker is proceeding.\n";
}

void notify() {
    std::unique_lock<std::mutex> lock(mtx);
    ready = true;
    cv.notify_one(); // 唤醒等待中的线程
}

int main() {
    std::thread t(worker);
    notify();
    t.join();
}

优点

  • 提供更高级的锁管理功能:
    • 延迟锁定unique_lock 的构造可以选择不立即锁定互斥量。
    • 显式解锁/加锁:支持 unlock()lock() 方法。
    • 可移动性:允许将锁的管理权转移。
  • 适合与条件变量配合使用。

延迟锁定示例

1
2
3
4
5
std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 构造时不锁定
// 需要时手动锁定
lock.lock();
std::cout << "Unique_lock has locked the mutex.\n";
lock.unlock(); // 显式解锁

3. 区别对比

特性std::lock_guardstd::unique_lock
锁的灵活性不支持延迟锁定、显式解锁或重新加锁支持延迟锁定、显式解锁、重新加锁
条件变量兼容性不适合与条件变量配合使用适合与条件变量配合使用
性能开销较低(轻量级)较高(灵活性带来额外开销)
可移动性不支持支持
复杂性简单较复杂
使用场景简单的作用域锁定场景复杂锁管理或条件变量场景

总结

  • std::lock_guard:适合简单场景,轻量、高效、不需要显式管理锁状态。
  • std::unique_lock:适合复杂场景,提供灵活的锁管理,尤其适合需要条件变量或动态锁控制的情况。

选择哪个取决于你的需求。如果仅需要基本的作用域锁,std::lock_guard 是首选;如果需要更复杂的锁管理功能,则使用 std::unique_lock

静态多态有什么?虚函数原理 虚表是什么时候建立的 为什么要把析构函数设置成虚函数?

1. 静态多态

静态多态(Static Polymorphism)是指在 编译时 决定的多态行为,通常通过模板、函数重载和运算符重载实现。

静态多态的特点

  • 编译时确定函数调用:函数的具体实现由编译器在编译期决定。
  • 性能较高:因为函数调用不涉及运行时查找,直接展开或内联。
  • 与类型相关:依赖模板或类型推导。

静态多态的实现方式

  1. 函数重载

    1
    2
    
    void print(int x) { std::cout << "Integer: " << x << '\n'; }
    void print(double x) { std::cout << "Double: " << x << '\n'; }
    

    调用 print(42) 时,编译器选择 print(int)

  2. 模板

    1
    2
    
    template <typename T>
    void print(const T& x) { std::cout << "Value: " << x << '\n'; }
    

    调用 print(42) 会实例化为 void print(const int&)

  3. 运算符重载

    1
    2
    3
    4
    5
    6
    7
    
    class Complex {
    public:
        double real, imag;
        Complex operator+(const Complex& other) const {
            return {real + other.real, imag + other.imag};
        }
    };
    

2. 虚函数的原理

虚函数用于实现 动态多态(Dynamic Polymorphism),即在 运行时 决定调用哪个函数。

虚函数的特点

  • 使用 virtual 关键字声明。
  • 函数的调用在运行时通过 虚表(V-Table) 查找。
  • 通常需要通过 基类的指针或引用 调用。

3. 虚表(Virtual Table)

虚表的原理

  1. 虚表 是一个存储类的虚函数地址的表。
    • 每个含有虚函数的类在编译时会生成一张虚表。
    • 每个对象都有一个指向其所属类虚表的指针(虚表指针,vptr)。
  2. 虚表的工作流程
    • 如果类定义了虚函数,编译器为类生成虚表,将虚函数的地址存入虚表。
    • 在构造对象时,初始化 vptr 指针,使其指向对应的虚表。
    • 调用虚函数时,通过 vptr 指针找到虚表,再根据偏移量找到具体函数地址。

虚表的建立时机

  • 虚表是在 编译期 创建的。
  • 每个具体类有自己的虚表。
  • 对象的 vptr 指针在对象 构造函数中初始化,确保指向正确的虚表。

4. 为什么要把析构函数设置成虚函数?

如果基类的析构函数不是虚函数,可能导致 内存泄漏或资源未正确释放 的问题。

问题背景

当使用基类指针或引用删除派生类对象时:

  • 如果基类析构函数非虚,则只会调用基类的析构函数。
  • 派生类的资源不会被释放。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Base {
public:
    ~Base() { std::cout << "Base destructor\n"; }
};

class Derived : public Base {
public:
    ~Derived() { std::cout << "Derived destructor\n"; }
};

int main() {
    Base* obj = new Derived();
    delete obj; // 只调用 Base 的析构函数
}

输出:

1
Base destructor

结果:

  • Derived 的析构函数未被调用,可能导致资源泄漏。

解决方法

将基类的析构函数设为虚函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Base {
public:
    virtual ~Base() { std::cout << "Base destructor\n"; }
};

class Derived : public Base {
public:
    ~Derived() { std::cout << "Derived destructor\n"; }
};

int main() {
    Base* obj = new Derived();
    delete obj; // 调用 Base 和 Derived 的析构函数
}

输出:

1
2
Derived destructor
Base destructor

总结

  • 将析构函数声明为虚函数可以确保通过基类指针正确释放派生类的资源。
  • 规则:凡是设计为多态的基类,通常需要将析构函数声明为虚函数。

总结对比

特性静态多态动态多态
实现方式函数重载、模板虚函数
调用时机编译时确定运行时通过虚表查找
性能开销无额外开销有一定的运行时开销
灵活性静态类型决定行为运行时动态决定行为
适用场景类型和行为在编译期确定行为需在运行时确定

选择静态或动态多态取决于需求:

  • 若性能为主且行为固定,选 静态多态
  • 若需要运行时灵活性,选 动态多态(虚函数)

C代码中引用C++代码有时候会报错为什么?

在C代码中引用C++代码时,可能会出现报错的情况,这通常是由于C和C++之间的语言差异导致的。以下是一些常见原因和解决方法:


1. 符号名称修饰(Name Mangling)

  • 原因: C++支持函数重载,因此编译器会对函数名称进行修饰以区分不同的函数。这种修饰方式与C语言的符号名称不兼容。
  • 报错: 通常会看到“未定义的引用”错误。

解决方法: 在C++代码中,使用extern "C"来关闭名称修饰:

1
2
3
4
// C++代码
extern "C" {
    void my_function();
}

然后在C代码中直接声明并调用:

1
2
// C代码
void my_function();

2. C++语言特性

  • 原因: C++支持的特性(如类、模板、异常等)在C语言中是不可用的。如果C代码尝试使用C++特性,会导致语法或链接错误。
  • 报错: “syntax error”或“不支持的功能”。

解决方法: 需要在接口层将复杂的C++特性封装起来,只暴露兼容C的函数接口。例如:

1
2
3
4
5
6
7
8
9
// C++代码
extern "C" {
    void wrapper_function();
}

void wrapper_function() {
    MyClass obj;
    obj.method();
}

3. 头文件兼容性

  • 原因: C++头文件可能包含不兼容C的内容(如命名空间、模板)。
  • 报错: “unexpected identifier”或“syntax error”。

解决方法: 用#ifdef __cplusplus来检查语言环境,并对代码进行区分:

1
2
3
4
5
6
7
8
9
#ifdef __cplusplus
extern "C" {
#endif

void my_function();

#ifdef __cplusplus
}
#endif

4. 链接器问题

  • 原因: 编译C代码和C++代码时可能使用了不同的编译器或链接器选项,例如C++需要链接标准库(libstdc++)。
  • 报错: 链接器错误,例如“undefined reference to std::cout”。

解决方法: 确保链接时包含C++标准库,通常是通过C++编译器进行链接:

1
g++ main.c my_cpp_code.cpp -o output

5. 内存布局和ABI不兼容

  • 原因: C++中的类、虚函数、继承等特性会影响对象的内存布局,而C语言无法理解这些布局。
  • 报错: 运行时崩溃或不可预期的行为。

解决方法: 避免直接在C中操作C++对象。可以通过C++封装函数提供简化接口。


6. 异常处理

  • 原因: C不支持异常机制,而C++抛出的异常无法在C代码中捕获。
  • 报错: 程序异常终止。

解决方法: 禁止C++异常传播到C代码中,或者在C++中捕获异常后以错误码的形式返回:

1
2
3
4
5
6
7
8
extern "C" int wrapper_function() {
    try {
        // C++逻辑
        return 0; // 成功
    } catch (...) {
        return -1; // 错误
    }
}

7. 数据类型不匹配

  • 原因: C++中可能使用了STL容器、std::string等,而C语言无法理解这些类型。
  • 报错: 数据类型不兼容。

解决方法: 在接口层转换数据类型。例如,将std::string转换为C字符串:

1
2
3
4
5
6
extern "C" void print_string(const char* str);

void print_string(const char* str) {
    std::string cpp_str = str;
    std::cout << cpp_str << std::endl;
}

总结

为了确保C代码与C++代码兼容:

  1. 使用extern "C"解决名称修饰问题。
  2. 在接口层封装C++特性,确保只暴露C兼容的接口。
  3. 确保编译和链接时的工具链一致,且链接时包含C++标准库。
  4. 在头文件中通过条件编译保证跨语言兼容。
本文由作者按照 CC BY 4.0 进行授权