C++11

UTF-8、UTF-16、UTF32的区别?

  1. UFT-8:变长编码(1~4字节),兼容 ASCII,是互联网(HTML、JSON等)和类 Unix 系统的默认编码。
  2. UTF-16:固定2字节,适合基本多语言平面字符,不兼容 ASCII,英文文本空间效率低于 UTF-8。
  3. UTF-32:固定4字节,覆盖全字符集,空间浪费严重。

什么是wchar_t、char16_t、char32_t?

  1. wchar_t(宽字符):早期用于支持非ASCII字符(如中文、日文),但编码依赖平台。Windows下为2字节,Linux为4字节,跨平台不一致,无法直接兼容现代 Unicode 标准。
  2. char16_t(UTF-16字符):C++11引入,表示 UTF-16 编码的 Unicode 字符(适合基本多语言平面字符),固定 2 字节,跨平台一致。
  3. char32_t(UTF-32字符):C++11引入,表示 UTF-32 编码的 Unicode 字符(覆盖全字符集),固定 4 字节,跨平台一致。

说说C++的左值和右值?

  1. 左值:可以出现在赋值表达式的左右侧,有名称(可以通过变量名访问)、可以取地址(使用 &操作符)、生命周期通常超出当前表达式、可以多次使用(除非被移动)。
  2. 右值:只能出现在赋值表达式的右侧,匿名(没有持久的内存地址)、不能取地址(如临时对象、字面量)、生命周期通常仅限于当前表达式、可以安全地被“移动”(C++11后)。
    1. 纯右值(prvalue):纯粹的临时值(如字面量、临时对象)
    2. 将亡值(xvalue):可以被“移动”的右值(如 std::move的结果)
  3. 左值引用(T&):只能绑定到左值,左值引用属于左值,可以取址
  4. 右值引用(T&&):只能绑定到右值,右值引用属于左值,可以取址。它的本质是一个指针,当右值引用绑定到右值时,编译器会为临时对象分配内存(通常在栈上),并将右值引用指向该内存。
  5. 左值常引用(const T&):因为const的修饰使该类型不会修改底层对象,因此它既可以绑定左值也可以绑定右值
  6. 右值常引用(const T&&):只能绑定右值,不能绑定左值,需要与左值常引用区分开来。

什么是移动语义?

  1. std::move 是 C++11 引入的一个关键工具,用于 显式标记对象为可移动(右值),从而启用移动语义(Move Semantics),避免不必要的深拷贝,提升性能。
  2. 它可以将任何类型的参数(左值或右值)强制转换为右值引用(T&&),表示该对象可以被“移动”(资源可以被“窃取”)
  3. 它仅是类型转换工具,真正的“移动”逻辑由移动构造函数/移动赋值运算符实现。
  4. 被移动后的对象如果是个左值,那么在原来的作用域依然存在,但是其中的值会由移动操作函数所定义,一般为nullptr或是空。所以,如果要将左值通过移动语义传输给一个函数,那么就要保证其在后面的作用域上不再使用该对象。

说说移动构造函数?

  1. 移动构造是C++11标准中提供的一种新的构造方法,用来给予程序员新的构造选择,用以替换拷贝构造。
  2. 拷贝构造函数会将传入的参数对象进行一次深拷贝,这就会有一次拷贝对象的开销,拷贝的内存越大越耗费时间,并且进行了深拷贝,就需要给新对象分配地址空间。
  3. 移动构造函数可以直接对传入的对象进行浅拷贝,以此直接接管源对象空间,既不会产生额外的拷贝开销,也不会给新对象分配内存空间。提高程序的执行效率,节省内存消耗。
  4. 移动构造函数的参数必须是自身类型的右值引用,也就是说能调用移动构造函数的参数必然是个右值(纯右值和将亡值)。

auto、decltype的用法?

  1. auto:根据初始化表达式推导变量类型,计算表达式的最终类型,默认忽略顶层 const 和引用,主要用于简化变量声明。如果要添加 const和引用,只能在auto关键字上手动添加,如 const auto&
    int x = 10;
    const int& rx = x;
    
    // auto: 忽略引用和顶层 const
    auto a1 = x;    // int
    auto a2 = rx;   // int(忽略了引用,此时类型为const int,然后又因为忽略了顶层const,因此又转为int)
    
  2. decltype: 根据给定表达式推导其声明类型,计算表达式的最终类型,保留所有修饰符(包括引用和 const),获取表达式的精确类型。
    1. 如果 e 是一个变量名或类的成员变量名(如 ex.m),则返回该实体的声明类型(不追加 &)。
      int x = 10;
      decltype(x) a = x;  // int(x 是标识符)
      
    2. 如果 e 是一个表达式(如 *p(x)),它会先计算表达式的值,如果原本的值有引用则去除,获取其基本类型,然后进行判断。若 e 是左值 → 返回 T&,若 e 是纯右值 → 返回 T,若 e 是将亡值 → 返回 T&&
      int y = 20;
      decltype(*&y) b = y;  // int&(*&y 是左值)
      decltype(y + 1) c = 0; // int(y + 1 是纯右值)
      decltype(std::move(y)) d = 0; // int&&(std::move(y))是一个将亡值
      
    3. decltype对表达式只在编译时推导类型而计算,并不会真正影响到原来的变量。
    4. decltype 对于表达式的计算是故意为之的,因为这样一来,如果其中的表达式是一个函数,那么decltype就可以精准推导出其函数的返回类型了。

说下auto定义基本类型时的规则?

  1. 整型:

    1. 默认情况下,整数字面量(如 42)会被推导为 int

    2. 如果数值超出 int 范围,则可能推导为 longlong long

    3. 可以使用后缀显式指定类型:

      • uUunsigned int

      • lLlong

      • llLLlong long

      • ulULunsigned long

        auto a = 42;      // int
        auto b = 42U;     // unsigned int
        auto c = 42L;     // long
        auto d = 42LL;    // long long
        auto e = 42UL;    // unsigned long
        
  2. 浮点型:

    1. 默认情况下,浮点数字面量(如 3.14)会被推导为 double
    2. 可以使用后缀显式指定类型:
      1. fFfloat

      2. lLlong double

        auto x = 3.14;    // double
        auto y = 3.14f;   // float
        auto z = 3.14L;   // long double
        
  3. 字符型:

    1. 单引号 ' ' 默认推导为 char

    2. 可以使用前缀显式指定类型:

      • Lwchar_t(宽字符,Unicode编码,大小为2个字节)

      • uchar16_t(UTF-16)

      • Uchar32_t(UTF-32)

        auto ch = 'A';    // char
        auto wch = L'中'; // wchar_t
        auto u16ch = u'字'; // char16_t
        auto u32ch = U'符'; // char32_t
        
  4. 布尔型:truefalse 会被推导为 bool

  5. 字符串:双引号 " " 默认推导为 const char*(C 风格字符串)。如果使用 std::string 初始化,则推导为 std::string

  6. 也可以直接对初始值进行强制类型转换来限制auto的类型推导。

什么是万能引用和完美转发?

  1. 万能引用:一种特殊的引用类型,能同时绑定到左值和右值,无需重载多个版本,形式为 T&&,但仅在模板推导时成立,也就是说T不能是具体类型。
    template <typename T>
    void foo(T&& arg) {}  // arg 是万能引用
    
    int x = 10;
    foo(x);   // T 推导为 int&,arg 类型为 int&(绑定左值)
    foo(20);  // T 推导为 int,arg 类型为 int&&(绑定右值)
    
  2. 完美转发:其定义为 std::forward<T>,它在函数模板中,可以保持参数的原始值类别(左值/右值),避免转发时丢失引用属性。如果不使用 forwardarg 在函数内部始终是左值(即使传入的是右值),导致无法调用右值版本。
    #include <utility>
    
    template <typename T>
    void wrapper(T&& arg) {
        target(std::forward<T>(arg));  // 完美转发
    }
    
    void target(int& x)  { std::cout << "左值引用\n"; }
    void target(int&& x) { std::cout << "右值引用\n"; }
    
    int main() {
        int x = 10;
        wrapper(x);       // 调用 target(int&)
        wrapper(20);      // 调用 target(int&&)
    }
    

C++中NULL和nullptr的区别?

  1. NULL是一个宏定义,C中NULL为(void*)0,C++中NULL为整数0。
  2. 将NULL定义为0带来的一个问题是无法与整数的0区分,因为C++中允许有函数重载,若是有个a、b两个重载函数,参数分别为整数和指针,那么在传入NULL参数时,会把NULL当做整数0来看,导致错误调用了参数为整数的函数。
  3. nullptr可以解决这一问题,nullptr可以明确区分整型和指针类型,能够根据环境自动转换成相应的指针类型,但不会被转换为任何整型,所以不会造成参数传递错误。

说说final和override关键字?

  1. Override指定了子类的这个虚函数是对父类虚函数的重写,如果函数名不小心打错了的话,编译器会进行报错。
  2. 当不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字,添加final关键字后如果被继承或重写,编译器会报错。

C++中的智能指针?

  1. 智能指针会管理程序员申请的内存,在使用结束后会自动释放,防止堆内存泄漏。

  2. auto_ptr:最原始的智能指针。auto_ptr采用的是独享所有权语义,一个非空的auto_ptr总是拥有它所指向的资源,转移一个auto_ptr将会把所有权全部从源指针转移给目标指针,源指针被置空。由于支持拷贝语义,拷贝后源对象变得无效,如果程序员忽视了这点,这可能引发很严重的问题。在C++11中该指针已被弃用。

  3. unique_ptr:与auto_ptr类似,采用独享所有权语义。unique_ptr提供移动语义,这在很大程度上避免了auto_ptr的错误,因为很明显必须使用std::move()进行转移,提醒程序员在这个地方发生了移动。

  4. shared_ptr:采用引用计数器的方法,允许多个智能指针指向同一个对象,每当多一个指针指向该对象时,指向该对象的所有智能指针内部的引用计数加1,每当减少一个智能指针指向对象时,引用计数会减1,当计数为0的时候会自动的释放动态分配的资源。引用计数器的变化依据如下所示。

    1. 每次创建类的新对象时,初始化指针并将引用计数置为1
    2. 当对象作为另一对象的副本而创建时,拷贝构造函数会拷贝指针并增加与之相应的引用计数
    3. 对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数
    4. 调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)
  5. weak_ptr: 由于shared_ptr引用计数存在的问题,即互相引用形成环(环形引用),使得两个指针指向的内存都无法释放,如下所示。

    class BB;
    
    class AA
    {
    public:
        AA() { cout << "AA::AA() called" << endl; }
        ~AA() { cout << "AA::~AA() called" << endl; }
        shared_ptr<BB> m_bb_ptr; //!
    };
    
    class BB
    {
    public:
        BB() { cout << "BB::BB() called" << endl; }
        ~BB() { cout << "BB::~BB() called" << endl; }
        shared_ptr<AA> m_aa_ptr; //!
    };
    
    int main()
    {
        shared_ptr<AA> ptr_a(new AA);
        shared_ptr<BB> ptr_b(new BB);
        cout << "ptr_a use_count: " << ptr_a.use_count() << endl;
        cout << "ptr_b use_count: " << ptr_b.use_count() << endl;
        // 下面两句导致了AA与BB的循环引用,结果就是AA和BB对象都不会析构
        ptr_a->m_bb_ptr = ptr_b;
        ptr_b->m_aa_ptr = ptr_a;
        cout << "ptr_a use_count: " << ptr_a.use_count() << endl;
        cout << "ptr_b use_count: " << ptr_b.use_count() << endl;
        return 0;
    }
    
    1. 为了解决这个问题,C++引入了weak_ptr(弱引用),它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,也就是说,它只引用,不计数。
    2. 如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。
    3. weak_ptr不保证它指向的内存一定是有效的,在使用之前应先使用weak_ptr的成员函数lock来检查weak_ptr是否为空指针,避免访问非法内存。如果非空,lock会返回一个新的shared_ptr,使用者可以以此来访问对象。如果为空,则返回nullptr。正因如此,weak_ptr并不能直接访问对象,他只能通过转化为shared_ptr来使用对象。

如何使用shared_ptr和weak_ptr?

#include <iostream>
#include <memory>

class Person {
public:
    std::string name;

    Person(const std::string& name) : name(name) {
        std::cout << "Person " << name << " constructed.\n";
    }

    ~Person() {
        std::cout << "Person " << name << " destroyed.\n";
    }

    void sayHello() {
        std::cout << "Hello, I'm " << name << "!\n";
    }
};

int main() {
    std::weak_ptr<Person> weakPerson; // 声明一个 weak_ptr

    {
        std::shared_ptr<Person> sharedPerson = std::make_shared<Person>("Alice");
        weakPerson = sharedPerson; // weak_ptr 观察 shared_ptr,但不增加引用计数

        std::cout << "Use count (inside block): " << sharedPerson.use_count() << "\n";

        if (auto sp = weakPerson.lock()) { // lock 尝试获取 shared_ptr
            sp->sayHello();
        } else {
            std::cout << "Person has expired.\n";
        }
    }

    // 此处 shared_ptr 已析构,资源已释放
    std::cout << "Use count (after block): " << weakPerson.use_count() << "\n";

    if (auto sp = weakPerson.lock()) {
        sp->sayHello();
    } else {
        std::cout << "Person has expired.\n";
    }

    return 0;
}

如何在成员函数中返回自身的shared_ptr?

  1. 不能直接在普通成员函数中使用 std::make_shared返回智能指针,如下所示。std::make_shared本身接受一组构造参数,然后间接调用了类相对应的构造函数。如果在成员函数中用this创建的智能指针,实际上是触发了类的拷贝构造函数,它所返回的是一个新对象的智能指针,而原来的调用者对象本身仍然还是由系统管理生命周期,因此下面代码会返回两次析构函数调用的结果。
    #include <iostream>
    #include <memory>
    
    class Person {
    public:
        std::string name;
    
        Person(const std::string& name) : name(name) {
            std::cout << "构造 Person: " << name << "\n";
        }
    
        ~Person() {
            std::cout << "析构 Person: " << name << "\n";
        }
    
        std::shared_ptr<Person> getPtr() {
            return std::make_shared<Person>(*this);  // 看起来像是“返回自己”
        }
    };
    
    int main() {
        Person p("Alice");
        auto ptr = p.getPtr();  // 你以为返回的是 p 自己的指针
    }
    
    
  2. 可以使用工厂函数创建,注意工厂函数必须是静态成员函数。
    #include <memory>
    #include <iostream>
    
    class Person {
    public:
        std::string name;
    
        Person(const std::string& n) : name(n) {}
    
        void greet() {
            std::cout << "Hi, I'm " << name << "!\n";
        }
    
        // 工厂方法,返回 shared_ptr
        static std::shared_ptr<Person> create(const std::string& name) {
            return std::make_shared<Person>(name);
        }
    };
    
    int main() {
        std::shared_ptr<Person> p = Person::create("Alice");
        p->greet();
        return 0;
    }
    
    
  3. 使用 shared_from_this()即可以在普通成员函数中返回自身的智能指针,但同时类也需要声明继承 std::enable_shared_from_this类才可以使用该函数。
    #include <memory>
    #include <iostream>
    
    class Person : public std::enable_shared_from_this<Person> {
    public:
        std::string name;
    
        Person(const std::string& n) : name(n) {}
    
        std::shared_ptr<Person> getPtr() {
            return shared_from_this(); // 从 this 获取 shared_ptr
        }
    
        void greet() {
            std::cout << "Hi, I'm " << name << "!\n";
        }
    };
    
    int main() {
        auto p = std::make_shared<Person>("Bob");
        auto p2 = p->getPtr(); // p2 和 p 共享同一个对象
        p2->greet();
        std::cout << "Use count: " << p.use_count() << "\n"; // 输出 2
        return 0;
    }
    
    

说说STL容器中的智能指针?

具备独占所有权语义的智能指针不能在STL的容器中使用,如auto_ptr和unique_ptr,因为STL容器中的元素经常要支持拷贝、赋值操作,在这过程中auto_ptr会传递所有权,容易导致错误,而unique_ptr又不支持普通的拷贝和赋值操作,也不能用在STL标准容器中。

说说lambda函数?

利用lambda表达式可以编写内嵌的匿名函数,用以替换独立函数或者函数对象。每当你定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类当然重载了()运算符),我们称为闭包类型(closure type)。那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,该实例是一个右值。lambda的优点有以下几点:

  1. 距离:很多人认为,让定义位于使用的地方附近很有用。这样,就无需翻阅很多页的源代码,以了解函数。另外,如果需要修改代码,设计的内容就在附近,就很好修改。
  2. 简洁:函数符代码要比lambda代码更加繁琐,函数和lambda的简洁程度相当。
  3. 功能:lambda可以访问作用域内的任何动态变量,可以采用取值、引用的形式进行捕获。

什么是声明时初始化?

C++11新增了类成员初始化新机制——声明时初始化,可以直接在类中声明数据成员时就进行初始化操作,而不用借助构造函数或者初始化列表。

C++11添加哪几种构造函数关键字?

  1. default关键字可以显式要求编译器生成默认构造函数,防止在调用时相关构造函数没有定义而报错。
  2. delete关键字可以删除构造函数、赋值运算符函数等,在使用时编译器会报错

什么是列表初始化?

列表初始化是C++ 11新引进的初始化方式,它采用一对花括号(即{})进行初始化操作。能用直接初始化和拷贝初始化的地方都能用列表初始化,而且列表初始化能对容器进行方便的初始化,因此在新的C++标准中,推荐使用列表初始化的方式进行初始化。

// 列表初始化,C++11新特性
int a{12};
string s{"123"};
vector<int> vec{1, 2, 3};

初始化列表和列表初始化的区别?

  1. 初始化列表是在创建类对象时,对类对象内部的数据成员进行的一种初始化方式,具体用在类的构造函数中。
    class date
    {
    public:
        date(int hour = 0): _hour(hour), _t(hour)
        {}
    private:
        int _hour;
        Time _t;
    };
    
  2. 列表初始化是C++11引入的一种新的对象初始化方式,它采用一对花括号(即{})进行初始化操作。主要用在类对象定义时,为它指定初始值。
    // 列表初始化,C++11新特性
    int a{12};
    string s{"123"};
    vector<int> vec{1, 2, 3};
    

说说constexpr?

  1. constexpr 是 C++11 引入的关键字,用于指示编译时常量可在编译时求值的表达式/函数。它的核心目的是让编译器在编译阶段完成计算,从而提高运行时效率或支持某些只能在编译时使用的场景
  2. 编译时常量:替代传统的 const 或宏定义,明确要求编译器在编译时计算值(宏定义在预编译时期替换值,而const是一个运行期常量,是否在编译期优化取决于编译器行为,没有强制要求)
    constexpr int size = 10;  // 编译时常量
    int arr[size];            // 合法,size是编译期确定的
    
  3. 编译时函数:函数可以在编译时执行(若参数是编译期常量)
    constexpr int square(int x) {
        return x * x;
    }
    constexpr int val = square(5);  // 编译时计算,结果25
    

说说noexpect?

  1. noexcept 是 C++11 引入的关键字,用于显式声明函数不会抛出异常。它既是编译时说明符,也是运行时运算符,对代码的安全性、性能优化和移动语义有重要影响。

  2. 编译时说明符:作为函数修饰符,声明函数不抛出异常

    void foo() noexcept;  // 声明 foo() 不抛出异常
    void bar() noexcept(true);  // 等价于 noexcept
    void baz() noexcept(false); // 可能抛出异常
    
  3. 作为运行时运算符:检查表达式是否可能抛出异常

    bool mayThrow = noexcept(someFunction()); // 返回 true/false
    
  4. 编译器会为 noexcept 函数生成更高效的代码(无需准备异常处理栈帧),标准库(如 std::vector)在扩容时,如果元素的移动构造函数是 noexcept,会优先使用移动而非拷贝。

  5. 如果函数可能抛出异常,不要强行加 noexcept,否则一旦抛出异常程序立即终止。虚函数可以声明为noexcept,派生类的重写版本可以更严格(即 noexcept 可以覆盖非 noexcept),但不能反过来。

说一下C++11的函数封装?

  1. std::function 是 C++11 引入的一个通用函数包装器,属于 <functional> 头文件。它可以存储、复制和调用任何可调用对象(如普通函数、Lambda 表达式、函数对象、成员函数等),通过类型擦除技术隐藏具体类型,统一处理不同可调用对象。
  2. std::function在存储类成员时有点特殊,它的第一个参数必须设置为该成员函数所属类的引用类型,并且在实际调用时也需要传入具体的类对象,同时该特性也导致它可以存储类成员变量。
    struct Linear{
    Linear(float k,float b):k_(k),b_(b){}
    float f(float x){return k_*x+b_;}
    float k_, b_;
    };
    
    int main(){
    std::function<float(Linear&,float)> mf = &Linear::f
    Linear 1(1.2,2.3);
    float res = mf(l,5);
    std::cout<<res<<std::endl;
    std::function<float(Linear&)> k = &Linear::k_;
    std::cout<<k(l)<<std::endl;
    }
    

说一下C++11的bind?

  1. std::bind 是 C++11 引入的一个函数适配器(位于 <functional> 头文件),用于部分绑定函数参数调整参数顺序,生成一个新的可调用对象。它常与 std::function 配合使用,实现更灵活的回调机制。
    #include <functional>
    #include <iostream>
    
    int add(int a, int b) { return a + b; }
    
    int main() {
        // 绑定第一个参数为 10,调用时只需传递第二个参数
        auto bound_add = std::bind(add, 10, std::placeholders::_1);
        std::cout << bound_add(5); // 等价于 add(10, 5),输出 15
    }
    
  2. 在将变量绑定为函数参数时,默认按值绑定,若需绑定引用,使用 std::refstd::cref
    void update(int& x) { x++; }
    int val = 0;
    auto bound_update = std::bind(update, std::ref(val));
    bound_update(); // val 变为 1
    

什么是 emplace_back?

  1. emplace_back 是标准库容器(如 std::vectorstd::dequestd::list 等)提供的一个成员函数,用于在容器末尾直接构造一个元素,而不是先创建临时对象再拷贝或移动到容器中。
  2. 对于非平凡类型(如需要动态分配资源的类),emplace_back 通常比 push_back 更高效,尤其是当对象的拷贝或移动成本较高时。

什么是原子类型和原子操作?

  1. 原子类型是由标准库提供的一组类型,用于支持原子操作。这些类型确保对它们的操作是不可中断的原子操作
  2. 原子操作总共有三种:读(load)、写(store)、Read Modify Write(RMW,先读取后修改)
  3. STL提供了 atomic_flagatomic模板, atomic_flag是无锁的原子类型,STL保证了其操作是无锁的,而其他类型都有可能在底层使用了锁操作。
  4. 要查看 atomic类型在底层是否使用了锁,可以使用其 is_lock_free函数来查看。一般来说,基本类型和指针都是无锁的,而自定义的类型则与类型大小和内存对齐有关。因此,不推荐使用 atomic来包装复杂类型。

说说atomic_flag?

  1. atomic_flag 是一个布尔值类型的原子变量,但它不提供普通的 load()store() 接口
  2. 它支持两个操作:
    1. test_and_set:返回原值并设置为true
    2. clear:设置为false
  3. 它在初始化时需要使用 ATOMIC_FLAG_INIT宏来定义为 false状态。
  4. 它的操作是原子的,且非常适合用于构建低级别的同步原语(比如自旋锁),如下是使用 atomic_flag实现的自旋锁。
    #include <atomic>
    
    class SpinLock {
        public:
            void lock(){
                while (flag.test_and_set(std::memory_order_acquire)) {}
            }
            void unlock(){
                flag.clear(std::memory_order_release);
            }
        private:
            std::atomic_flag flag{ATOMIC_FLAG_INIT};
    };
    

什么是CAS

  1. CAS(Compare-And-Swap,比较交换)是多线程并发编程中非常重要的原子操作,通常用来实现无锁算法。
  2. 在C++中,CAS可以使用 atmoic类型的 compare_exchage成员函数来调用,它接收两个参数,一个期望值和一个新值。它的操作逻辑是:
    1. 如果当前值等于期望值,则将当前值修改为新值,然后返回true。
    2. 如果当前值不等于期望值,则将期望值修改为当前值的最新值,然后返回false。
  3. compare_exchange函数有两个版本,分别为strong版本和weak版本。weak版本可能会报出假false,但性能更高,而strong不会报出假fasle
  4. 这个操作的用法一般是在循环的判断中,如下所示。可以简单的理解为,如果当前值被其他线程修改了,则读取最新值,执行相应的操作后再进行判断当前值是否被其他线程修改,如果没用被修改,则将当前值赋予新值。
    #include <atomic>
    
    int fetch_multiply(std::atomic<int> &value,int multiplier) {
        int oldvalue = value.load();
        int desired;
        do
        {
            desired = oldvalue * multiplier;
        } while(!value.compare_exchange_strong(oldvalue, desired));
        return oldvalue;
    }
    
    

什么是线程局部变量?

  1. thread_localC++11 引入的关键字,用于定义线程局部存储变量,常用于高并发场景中。
  2. 编译器在编译时会为每个 thread_local 变量分配一个偏移量,而每个线程的TCB(线程控制块)中会保存自身的TLS(线程局部存储区)的基地址。
  3. 运行时系统会根据当前线程的TLS基地址,加上线程局部变量所对应的偏移量来访问具体变量,每个线程对同一个 thread_local 变量访问的是属于自己的那一份,这样就保证了线程之间数据互不干扰,也不需要加锁。
  4. 这个机制主要依赖于编译器、线程库(如 pthread)和操作系统三者的协同。

什么是lock_guard和unique_lock?

  1. 在 C++ 中,unique_locklock_guard 都是用来管理 互斥锁(mutex) 的 RAII(资源获取即初始化)机制的工具,用于防止多线程环境下的数据竞争和死锁,但它们各有特点和适用场景。
  2. lock_guard:简单轻量,构造时自动加锁,析构时自动解锁。不能显式 unlock,也不能转移所有权(不可移动)。一般用于临界区非常简单的情况。
  3. unique_lock:支持延迟加锁(defer_lock)、提前解锁(unlock)、重新加锁(lock)、条件变量(std::condition_variable),可以转移所有权(可移动,不可复制),比lock_guard性能要低。

其他问题

怎么使用vim?

  1. vim是linux下的文本编辑器,具有三种模式:命令模式、底行模式、插入模式
  2. 在命令模式下,可以进行光标移动、文本复制(yy)、文本粘贴(p)、文本删除(dd)等操作。
  3. 在底行模式下,可以对文本文件进行设置,如保存(:w)、退出(:q)等操作。
  4. 在插入模式下,可以对文本进行编辑操作。
  5. 一般情况下,在进入底行模式和插入模式之前必须经过命令模式,不能直接切换
    1. 底行模式->插入模式, 按esc到命令模式,按i/a/o到插入模式
    2. 插入模式->底行模式, 按esc到命令模式,按:到底行模式
  6. 无论当前处于什么模式,按esc一定可以回到命令模式

怎么使用gcc/g++?

  1. gcc/g++是linux下常用的C/C++编译工具,gcc是C语言编译器,g++是C++编译器。通过该工具,可以完成对C/C++代码文件的预处理、编译、汇编、链接等操作。
  2. 安装了gcc/g++之后,可直接在命令行下调用此工具。若不加入任何参数,默认对代码文件完成所有编译工作,生成名为“a.out”的可执行文件。
  3. 下面介绍gcc/g++命令的常用参数:-E只执行预处理工作并生成.i文件、-S只执行编译工作并生成.s文件、-c(小写)只执行汇编工作并生成.o文件、-o指定经过gcc/g++处理后的文件名。
  4. 对于几个常用参数的文件后缀的记忆方法:“gcc-E,-S,-c”刚好对应键盘左上角的esc键,而对应生成的文件后缀分别为“i、s、o”,组合起来刚好是一个镜像文件的后缀。
  5. 链接操作没有单独的参数,在执行gcc/g++时,只要不加“E、S、c”等参数,默认会进行链接。

怎么使用gdb?

  1. gdb是linux下的C/C++程序调试器。要使用gdb调试,其程序必须是debug模式。gcc/g++默认生成的程序是release模式,必须在源代码生成程序时加上“-g”选项,以debug模式输出程序,才能使用gdb。
  2. gdb的常用操作有:list/l 行号(显示指定行号开始的10行代码)、list/l 函数名(显示指定函数的代码)、r/run(运行程序)、n /next(逐语句执行,不进入函数)、s/step(逐过程执行,会进入函数)、break/b行号(在某一行设置断点)、print/p表达式(打印表达式的值)、continue/c(继续执行程序,直到下个断点)、display 变量名(跟踪查看变量)、set变量名(设置变量值)、breaktrace/bt(查看调用堆栈)、quit(退出)

怎么使用make/Makefile?

  1. make是一个命令,Makefile是一个文件。两者搭配使用,可以完成项目的自动化构建并生成可执行程序,无需手动调用gcc/g++。
  2. Makefile文件属于一种特殊的代码文件,其代码描述了各种中间文件(比如.o文件)之间的先后依赖关系,语法较为复杂,这里不展开细讲。
  3. make命令会自动调用gcc/g++,让他们依照Makefile文件中的依赖关系逐一生成文件,并最后输出可执行文件。
  4. make clean命令用于清除已生成的可执行文件。

怎么制作并使用静态库?

  1. 在linux下制作静态库需要借助gcc/g++,以及归档工具ar(相当于tar、zip等解压缩工具)。
  2. 首先需要将所有的代码源文件编译成.o文件,再使用ar命令将所有.o文件打包在一起,就生成了一个静态库。
  3. 在linux下,静态库的后缀为.a,且库名称前必须加上“lib”,否则链接静态库会出错。
  4. 要想使用静态库,只需要在对使用静态库的源文件进行编译链接时,加上几个参数即可,如下图所示

怎么制作并使用动态库?

  1. 使用gcc/g++生成动态库的.o文件时,需要加上-fPIC选项,如“g++ -fPIC -c test.c -o test.o”。
  2. 在生成库文件时,无需使用ar,只需要使用gcc/g++加上-shared选项,如“gcc -shared test.o -o test.so”,即可生成动态库。
  3. 在linux下,动态库的后缀为.so,且库名称前必须加上“lib”,否则链接动态库会出错。
  4. 动态库使用方式与静态库相同,在对使用动态库的源文件进行编译链接时,需要加上同样的参数,以指明库文件和头文件的搜索路径以及库名称。
  5. 与静态库不同的是在运行程序时,还必须告诉操作系统动态库路径,指明方法有三个:
    1. 修改环境变量LD_LIBRARY_PATH,添加库文件路径,但下次重启系统会失效。
    2. 将库文件和头文件放到系统的默认目录下。
    3. 修改系统配置文件的动态库搜索路径。

如何使用Valgrind?

  1. Valgrind用于在Linux下检测内存泄露。
  2. 用 Valgrind 检测内存泄漏,需在编译时添加 -g 生成调试信息,然后运行下面代码。其中 --leak-check=full表示显示泄漏的详细位置(在哪个文件的哪一行)。
    g++ -g your_program.cpp -o your_program  # 编译时加 -g
    valgrind --leak-check=full ./your_program
    
  3. 泄露信息关键字段:
    1. definitely lost:明确泄露,未释放内存,必须修复
    2. indirectly lost:由于其他内存块(definitely lost)的泄漏,导致依赖它的内存块也无法被访问,通常出现在嵌套数据结构中(如链表、树的结构体未完全释放)。
    3. possibly lost:指针指向已分配内存的中间地址(而非起始地址),导致 Valgrind 无法确定是否故意为之,需要人工检查是否真实泄漏。
    4. still reachable:程序结束时,某些内存块未被 free()delete,但仍有全局变量、静态变量或某些数据结构(如主线程的堆栈)持有它们的指针。例如:全局变量 char* ptr = malloc(100); 未释放,但程序退出时 ptr 仍指向该内存,这可能是有意保留的,需要人工检查。
    5. suppressed:被用户标记为忽略的泄漏(通过 suppression 文件)

如何使用Dr.Memory?

  1. Dr.Memory可以在Windows或Linux中检查内存泄漏,在命令行中运行:

    drmemory [选项] -- <被测程序名> [被测程序参数]
    
  2. 运行后,Dr. Memory 会生成报告,包含以下常见错误:

    1. UNINITIALIZED READ:读取未初始化内存
    2. INVALID READ/WRITE:越界访问
    3. LEAK:内存泄漏
    4. POSSIBLE LEAK:潜在内存泄漏

pstack是什么?

  1. pstack是Linux下用于快速查看某个进程里每个线程正在执行什么函数,适用于程序卡住、死循环、线程阻塞等问题的定位。
  2. 使用步骤:
    1. 找到目标进程的 PID,可以使用ps或top命令查找。
    2. 执行 pstack <pid> 可以显示某个进程所有线程的当前函数调用栈
  3. pstack 是查看“当前状态”,它不是采样工具,可以把输出保存到文件方便分析,如 pstack 12345 > stack.log

perf是什么?

  1. perf 是 Linux 下非常强大的性能分析工具,尤其擅长CPU 性能采样,用来找程序的性能瓶颈。
  2. 实时查看系统或某个程序的热点函数(类似 top,但显示的是 CPU 占用最高的函数):perf top
  3. 采样某个进程运行情况,生成报告(-p指定采样进程,-g表示采集调用栈信息,sleep表示10秒后停止):perf record -p 12345 -g -- sleep 10,然后执行 perf report会看到耗费 CPU 时间最多的函数和调用关系
  4. 采样运行某个程序(从运行开始):
    perf record -g ./my_program
    perf report
    

cpu占用率过高怎么定位?

  1. 当一个项目 CPU 占用高而请求不多时,大概率是程序内部出现死循环、锁竞争或资源泄漏等问题,可通过 pstack/perf等工具锁定高占用线程和函数栈,定位异常代码进行优化。
  2. 如果是线上问题,优先考虑使用 kill -STOP 命令暂停进程,检查进程状态和资源占用。如果要恢复进程运行,可以执行 kill -CONT命令。

系统调用和库函数的区别

  1. 系统调用是操作系统提供的操作接口,执行系统调用需要从用户态切换到内核态,以此直接请求操作系统服务(如文件读写、进程管理)
  2. 库函数是函数库提供的函数,运行在用户态,它的底层是对系统调用的封装和功能扩展。
  3. 以文件读写为例,read(fd, buf, size)系统调用(直接向 OS 请求读取文件原始字节),fgets(buf, size, fp)库函数(封装了 read,加入了缓存处理,可以按行读取文本,自动加'\0')。再比如,write是系统调用,printf是库函数,底层使用了 write

所有系统调用都会阻塞吗?

并非所有系统调用都会阻塞。是否阻塞取决于资源状态和调用方式,比如 I/O 操作如果资源不可用时会阻塞,但像 getpid()time() 等不会阻塞;同时,许多 I/O 系统调用也可以通过设置非阻塞标志(如 O_NONBLOCK)避免阻塞。

读取文件时,文件内容存储在哪个区?

  1. 读取文件时,文件内容并不是自动属于C++程序内存结构中的某个固定“区”,而是取决于你如何读取文件、读入的数据存到哪里。
  2. 通常会先申请一块内存(比如用 newmalloc、定义数组等),然后把文件内容读入这块内存。

有没有什么方法可以不用read操作也可以读取文件?

  1. mmap(内存映射 I/O)可以把文件“映射”到一段内存地址上,像访问内存一样访问文件内容,无需显式 read(),也不用自己开 buffer,效率高。
  2. mmap的底层原理是:操作系统通过虚拟内存机制,将文件的内容映射到进程的虚拟地址空间中,进程可以像访问普通内存一样访问文件内容。映射完成后,操作系统并不会立刻将整个文件读入内存,而是在程序第一次访问某个地址时触发缺页异常(page fault),再按需从磁盘将对应的文件页加载到物理内存中,并建立虚拟页到物理页的映射关系。由于省去了显式的读写操作和数据拷贝,mmap 在处理大文件或需要高效随机访问时性能更优。