C++方向高频面试题(C++语法相关)

什么是左值引用和右值引用

  • 可取地址的是左值,不可取地址的是右值。
  • 左值引用是借用,右值引用是接管。

C++ move 的作用和原理?

  • 核心作用:将对象的资源所有权从一个对象“转移”到另一个对象,避免不必要的深拷贝,提高程序性能。
  • 原理std::move 只是类型转换工具,不会实际移动数据,真正的移动逻辑由类的移动构造函数或移动赋值运算符决定。

static 关键字的作用?什么场景下用?

static 用于控制变量或函数的作用域和生命周期。

  1. 修饰局部变量(函数内部)

    • 在函数作用域内可见,但只会初始化一次,生命周期贯穿整个程序。
    • 使用场景:记录函数调用次数,实现懒加载。
    #include <iostream>
    using namespace std;
    
    void func() {
        static int count = 0; // 只在第一次调用 func 时初始化
        cout << "Count is: " << count << endl;
        count++;
    }
    
    int main() {
        for (int i = 0; i < 5; i++) {
            func(); // 每次调用都会显示增加的 count 值
        }
        return 0;
    }
    
  2. 修饰全局变量或函数(文件作用域)

    • 限制变量/函数只在当前源文件中可见,避免命名冲突。
    • 使用场景:模块化开发中隐藏实现细节。
    // file1.cpp
    static int count = 10;      // 仅在 file1.cpp 中可见
    static void func() {        // 仅在 file1.cpp 中可见
        cout << "Function in file1" << endl;
    }
    
    // file2.cpp
    extern int count;           // 错误:count 为 static,无法访问
    void anotherFunc() {
        func();                 // 错误:func 为 static,无法访问
    }
    
  3. 修饰类的成员变量

    • 静态成员变量属于类而非实例,所有对象共享一份数据。
    • 使用场景:计数器、全局配置、缓存等。
  4. 修饰类的成员函数

    • 静态成员函数不依赖实例即可调用。
    • 使用场景:访问静态变量、工具函数、工厂函数等。
    #include <iostream>
    using namespace std;
    
    class MyClass {
    public:
        static int staticValue;
        static void staticFunction() {
            cout << "Static function called" << endl;
        }
    };
    
    int MyClass::staticValue = 10;
    
    int main() {
        MyClass::staticFunction();       // 调用静态成员函数
        cout << MyClass::staticValue;    // 访问静态成员变量
        return 0;
    }
    

const 关键字的作用?谈谈你是怎么理解 const 的?

  1. 定义普通常量

    const int MAX_SIZE = 100; // MAX_SIZE 是一个常量,初始化后不能修改
    
  2. 修饰指针

    • const int* ptr:指向常量,不能通过 ptr 修改数据。
    • int* const ptr:指针常量,地址不可变,但可修改指向的数据。
    • const int* const ptr:指针常量且指向常量。
  3. 修饰引用(函数参数)

    void func(const int& a) { /* 不能通过 a 修改值 */ }
    
  4. 修饰成员函数

    class MyClass {
    public:
        void myFunc() const { /* 不可修改成员变量,除非标记 mutable */ }
    };
    
  5. 修饰成员变量

    class MyClass {
    public:
        const int a = 5;
    };
    

new和malloc的区别?delete和free的区别?


#defineconst 的区别?

  • 作用机制

    • #define X 10:预处理阶段文本替换,无类型检查。
    • const int x = 10:编译阶段定义常量,保留类型信息。
  • 类型安全const 有类型检查,#define 无。

  • 作用域

    • #define 在整个源文件中有效。
    • const 遵循 C++ 作用域规则。

inline 关键字的作用?有什么优缺点?

  • 作用:建议编译器将函数调用展开为函数体,减少调用开销。
  • 优点:降低栈操作开销;类型安全;易调试。
  • 缺点:函数体过大或递归不适合;可能增大可执行文件体积。
inline int add(int a, int b) {
    return a + b;
}

sizeofstrlen 的区别?

  • sizeof:编译时运算符,获取类型或对象的字节大小。
  • strlen:运行时函数,计算 C 风格字符串长度(不含终止符 \0)。
int arr[10];
std::cout << sizeof(arr); // 若 int 为 4 字节,则输出 40

char s[] = "hello";
std::cout << strlen(s);  // 输出 5

extern 有什么作用?extern "C" 有什么作用?

  • extern:声明外部链接符号,告诉编译器符号定义在其他翻译单元。
  • extern "C":关闭 C++ 名称改编(name mangling),与 C 代码兼容。
// example.h (C)
void c_function();

// example.cpp (C++)
extern "C" {
#include "example.h"
}

explicit 关键字的作用?

  • 防止构造函数或转换函数的隐式类型转换。
class Foo {
public:
    explicit Foo(int x) : value(x) {}
private:
    int value;
};

Foo f1 = 10;      // 错误,需显式 Foo f1(10);
Foo f2 = Foo(10); // 正确

final 关键字的作用?

  • 防继承class A final {},禁止派生。
  • 防重写virtual void f() final;,禁止派生类重写。
class Base final {};
// class Derived : public Base {}; // 错误

class B {
public:
    virtual void f() final {}
};

class D : public B {
public:
    void f() override; // 错误
};

volatile 关键字的作用?

  • 核心作用:告诉编译器这个变量随时可能被外部因素修改,不要对它做任何依赖于其值不变的优化。可以理解为禁用编译器的优化(缓存),确保每次读写都是从内存中获取的数据而非寄存器。
volatile int flag = 0;
void irq_handler() {
  flag = 1;           // 硬件中断或信号处理程序修改
}
int main() {
  while (flag == 0) { // 每次都从内存读 flag,不会被优化成 while(true)
    /* 等待中断 */
  }
}

️:volatile不提供原子性,仍要使用std::atomic或互斥锁来保障多线程安全。

char *const char *char * constconst char * const 的区别?

  • char *:可变指针,可修改指针和指向的数据。
  • const char *:可变指针,不可修改指向的数据。
  • char * const:指针常量,不可修改指针,可修改指向的数据。
  • const char * const:指针常量且指向常量。

delete 关键字和 default 关键字的作用?

  1. 作用
  • delete用于禁用某些默认的成员函数。主要的作用就是禁用拷贝构造函数和拷贝移动赋值运算符。
class MyClass {
public:
    MyClass() = default;                // 使用默认构造函数
    MyClass(const MyClass&) = delete;   // 禁用拷贝构造函数
    MyClass& operator=(const MyClass&) = delete; // 禁用拷贝赋值运算符
};
  • default关键字用于显示地指示编译器为某个成员函数生成默认的实现。经常用在构造函数、析构函数,以及拷贝构造函数上。
class MyClass {
public:
    MyClass() = default;                // 使用默认构造函数
    ~MyClass() = default;               // 使用默认析构函数
    MyClass(const MyClass&) = default;  // 使用默认拷贝构造函数
    MyClass& operator=(const MyClass&) = default; // 使用默认拷贝赋值运算符
};
  1. 扩展知识
  • delete还可以用来禁用某些传统的函数重载。
class MyClass {
public:
    MyClass(int value) = delete;   // 禁用带一个整数参数的构造函数
};
  • 可以结合deletedefalut构造更加安全的类,更好的控制类的行为和接口。
class NonCopyable {
public:
    NonCopyable() = default;  // 默认构造函数
    NonCopyable(const NonCopyable&) = delete;  // 禁用拷贝构造函数
    NonCopyable& operator=(const NonCopyable&) = delete;  // 禁用拷贝赋值运算符
};

智能指针中的unique_ptr的原理就是使用delete进行了拷贝构造函数和拷贝赋值运算符。


this 指针的作用?可以使用 delete this 吗?

  1. 作用
    this指针是隐含在每个非静态成员函数中的指针。他指向的是调用成员函数的那个对象的地址,主要作用包括:
  • 访问类成员变量和成员函数,可用this指针区分当局变量与成员变量。
class Box {
private:
    double length;

public:
    Box(double length) {
        this->length = length;  // 用this指针区分成员变量和构造函数参数
    }

    void setLength(double length) {
        this->length = length;  // 同样在成员函数中区分成员变量和参数
    }

    double getLength() {
        return this->length;  // 使用this指针访问成员变量
    }
};
  • 链式调用,通过*this可以实现链式调用。
class Box {
private:
    double length;

public:
    Box& setLength(double length) {
        this->length = length;
        return *this;  // 返回对象本身的引用
    }

    void display() {
        std::cout << "Length: " << length << std::endl;
    }
};

int main() {
    Box box;
    box.setLength(5.0).display();  // 链式调用
    return 0;
}
  • 动态绑定,可以在基类函数中使用this指针调用派生类对象。
class Base {
public:
    virtual void show() {
        std::cout << "Base show" << std::endl;
    }

    void display() {
        this->show();  // 调用的是实际对象的show()方法
    }
};

class Derived : public Base {
public:
    void show() override {
        std::cout << "Derived show" << std::endl;
    }
};

int main() {
    Derived d;
    Base *b = &d;
    b->display();  // 尽管指针是Base类的,但调用的是Derived的show()方法
    return 0;
}
  1. 可以delete this指针,但是必须非常谨慎,否则会出现未定义行为。delete this的主要作用是允许对象在其成员函数中自行销毁。(普通程序员不建议使用)
  • 使用场景:使用new申请的空间+需要成员函数销毁内存+后续不会访问这个类的实例的成员函数和变量。

什么是深拷贝和浅拷贝?写一个标准的拷贝构造函数?

  • 深拷贝(copy):复制内容,资源独立。
  • 浅拷贝(move):源所有权,源对象不再使用,但必须保持有效状态。
  • 拷贝构造函数
#include <cstring> // for std::strlen and std::strcpy

class MyClass {
private:
    char* data;
public:
    MyClass(const char* inputData) {
        data = new char[std::strlen(inputData) + 1];
        std::strcpy(data, inputData);
    }

    // 深拷贝构造函数
    MyClass(const MyClass& other) {
        data = new char[std::strlen(other.data) + 1];
        std::strcpy(data, other.data);
    }

    // Destructor
    ~MyClass() {
        delete[] data;
    }

    // 打印数据
    void printData() {
        std::cout << data << std::endl;
    }
};

int main() {
    MyClass obj1("Hello");
    MyClass obj2 = obj1;
    obj1.printData(); // Output: Hello
    obj2.printData(); // Output: Hello

    return 0;
}

MyClass类有一个指向字符数组的指针data,拷贝构造函数对另一个对象进行深拷贝,即为data分配新的内存并复制字符串,两个对象都有自己独立的空间和资源。

C++ 中什么是移动语义和完美转发?

三种智能指针的使用场景

std::unique_ptr

  • 独占所有权,同一时间只有一个所有者,销毁时释放资源。
  • 场景:单一所有者的资源管理,如文件句柄、互斥锁等。

std::shared_ptr

  • 共享所有权,引用计数管理生命周期,最后一个析构时释放资源。
  • 场景:多个所有者共享同一资源。

std::weak_ptr

  • 不增引用计数,观察 shared_ptr 管理的对象,解决循环引用。
  • 场景:缓存、观察者模式中需要弱引用。

C++11 中的新特性

auto 类型推导

auto a = 10;              // 推导为 int
auto b = vec.begin();     // 推导为迭代器类型

RAII 锁

  1. std::lock_guard<std::mutex>:简单轻量,构造时加锁,析构时解锁。
std::mutex mtx;
void func() {
    std::lock_guard<std::mutex> lock(mtx);
    // 临界区
}
  1. std::unique_lock<std::mutex>:灵活,可延迟加锁、手动解锁、与条件变量配合。
std::unique_lock<std::mutex> lock(mtx);
cond_var.wait(lock);

std::thread

  • 创建并管理线程,使用 join()detach()
  • 注意:线程对象需 joindetach,否则程序终止时异常。
std::thread t1(func);
std::thread t2 = std::move(t1);

左值和右值

  • 左值:有名称或可取地址的表达式。
  • 右值:临时值或不可取地址的表达式。

std::function

  • 通用可调用包装,可存储函数指针、lambda、函数对象等。
std::function<void(int)> f = [](int x) { std::cout << x; };
f(42);

Lambda 表达式

auto func = [capture_list](params) -> ret_type {
    body;
};
  • capture_list:捕获外部变量方式,如 [=][&]
  • ret_type:返回类型可省略,由编译器推导。
std::function<int(int)> add_x = [x](int a) { return x + a; };

野指针和悬挂指针的区别?

两者都可能导致程序出现不可预测的行为,但是他们有明显的区别:

  • 野指针:一种未被初始化的指针,通常会指向一个随机的内存地址。(这个地址不可空)
int *p;
std::cout<< *p << std::endl;
  • 悬挂指针:一个原本合法的指针,原但是指向的内存已被释放或重新分配,当访问此指针指向的内存时,会导致未定义行为,因为那块内存数据可能已经不是期望的数据了。
int main(void) {
  int * p = nullptr;
  int* p2 = new int;

  p = p2;
  delete p2;
}

如何避免野指针?

  • 初始化指针:在声明一个指针时,立即赋予它一个值。(可以是nullptr)
int *ptr = nullptr; // 初始化
  • 使用智能指针(unique_ptr和shared_ptr)
std::unique_ptr<int> ptr(new int(10));

如何避免悬挂指针?

  • 删除对象后,将指针设置为nullptr,确保指针不再指向已经释放的内存。
delete ptr;
ptr = nullptr;
  • 同样是使用智能指针

内存对齐及其意义?

  • 概念:内存对齐是变量或结构体成员在内存中的存放地址按照一定的规则排列。
  • 意义:性能提升,提高可移植性。
  • 规则:
    • 每个成员的起始地址必须是该成员类型大小的整数倍。
    • 结构体的大小必须是其最大成员类型大小的整数倍。(不够的话就进行填充)

四种类型转换及应用场景?

  1. 四种常用的类型转换
  • static_cast:用于有明确定义的类型之间转换。
int a = 10;
float b = static_cast<float>(a);  // 将 int 转换为 float
  • dynamic_cast:用于多态类型的指针或引用转换。️只有在类包含虚函数的时候才能使用,而如果转换失败,指针会返回nullptr,引用转换失败则会抛出std::bad_cast异常。
Base* basePtr = new Derived();
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);  // 运行时检查并转换
  • const_cast:用于移除或添加对象的const属性。
const int a = 10;
int* b = const_cast<int*>(&a);  // 移除 const 属性
*b = 20;  // 修改原本为 const 的值
  • reinterpret_cast:底层强制转换。
long p = 0x12345678;
int* i = reinterpret_cast<int*>(p);  // 将 long 转换为 int 指针

多态和虚函数?

简单来讲,就是同一个函数或方法调用,可以根据上下文的不同执行执行不同的功能。(同一接口,不同实现,根据传入参数决定调用哪个功能)多态主要通过基类的指针或引用,来调用子类的重写函数实现。C++中的多态主要通过虚函数来实现。

#include <iostream>
using namespace std;

class Base {
public:
    virtual void show() {
        cout << "Base class show function" << endl;
    }
};

class Derived : public Base {
public:
    void show() override {
        cout << "Derived class show function" << endl;
    }
};

int main() {
    Base* basePtr;
    Derived derivedObj;
    basePtr = &derivedObj;

    basePtr->show();  // 输出:Derived class show function

    return 0;
}

上述例子中,通过基类指针basePtr调用派生类Derivedshow方法,这就是多态。

多态还分为静态多态动态多态,静态多态通过函数重载和函数模板实现,动态多态通过虚函数实现。

  1. 静态多态
    函数在编译时就确定了调用哪个函数,效率高但是灵活性差。(静态多态可以理解为一个人有不同的身份,根据传入参数类型的不同,扮演不同的角色)
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;
}
  1. 动态多态
    本质和静态多态一样,不同的是在运行时才确定调用的函数效率低但是灵活性高。动态多态通过虚函数来实现。
  2. 虚函数:虚函数允许在基类中用virtual声明一个函数,然后在派生类中对其进行重新定义。每个含有虚函数的类都有一张虚函数表,表中存放该类的虚函数的地址。每个该类的对象都有一个虚指针指向这张虚函数表。
    • 虚函数表:编译器为每个有虚函数的类生成一张虚函数指针表,表中存储该类各虚函数的地址。
    • 虚指针:每个对象实例在其内存布局开头,有一个指向所属类的虚函数表vtable的指针。
    • 纯虚函数抽象类:如果一个类中有纯虚函数,这个类就是抽象类,不能直接实例化。这种类只提供接口,不提供具体实现。

️静态绑定的成员变量不能是虚函数(因为静态函数既没有 vptr,也不在 vtable 中);虚函数调用比普通函数调用多了一个vtable查找的过程,增加一定开销

class Shape {
public:
    virtual void draw() = 0;  // 纯虚函数
};

class Circle : public Shape {
public:
    void draw() override {
        cout << "Drawing a circle" << endl;
    }
};

通过基类指针或引用调用虚函数时,编译器生成的机器码会先读取对象的虚指针vptr,再在虚函数表vtable上查到正确的函数地址,最后跳转执行。
️:使用动态多态的时候基类析构函数要virtual,避免通过基类指针删除子类对象时只调用基类析构。

有了多态,就可以实现接口分离和常用的设计模式。


构造函数和析构函数的虚函数规则?

构造函数可以是虚函数

构造函数不可以是虚函数,因为构造函数是用来初始化资源的,而虚函数机制依赖虚函数表,虚函数表的建立是在调用构造函数之后才能完成的。(只有调用了构造函数,才会生成虚函数表)

析构函数应该是虚函数

确保在删除一个调用派生类对象的基类指针时,能正确调用派生类的析构函数。如果析构函数不是虚函数,那么通过基类指针删除子类对象时,只会调用基类的析构函数,不会调用子类的析构函数,会造成内存泄漏。

静态成员函数和友元函数不能是虚函数

  • 静态成员函数与类关联,不与某个对象关联。
  • 友元函数不属于类的成员函数。

移动构造函数和移动赋值运算符使用场景?

  1. 使用场景
  • 当函数返回一个对象时,用移动构造函数可以避免返回值拷贝。
  • 当函数传递参数时,使用右值+移动构造函数可以避免参数拷贝。
  • 当需要将一个大对象从一个容器(如vector)移动到另外一个容器中,用移动赋值运算可以避免重复的资源分配。
  1. 核心原理
  • 移动构造函数:接收同类型的右值引用,T(T&& other) noexcept,用于初始化新对象并“窃取”旧对象的资源。
  • 移动赋值运算:接收右值引用,T& operator=(T&& other) noexcept,用于将右值对象的资源转移到已存在对象,并清理旧资源。
class Buffer {
    char* data_;
    size_t size_;
public:
    // 移动构造
    Buffer(Buffer&& other) noexcept
      : data_(other.data_), size_(other.size_)
    {
        other.data_ = nullptr;
        other.size_ = 0;
    }

    // 移动赋值
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data_;          // 1. 释放旧资源
            data_ = other.data_;      // 2. 窃取资源
            size_ = other.size_;
            other.data_ = nullptr;    // 3. 重置源
            other.size_ = 0;
        }
        return *this;                // 4. 返回自身
    }

    ~Buffer() { delete[] data_; }
};
  1. 扩展知识
  • 右值引用:右值引用使用两个连续的&&表示。在函数声明中,如果参数是右值引用,它只接受可被销毁的对象。
std::string makeStr() { return "hello"; }
std::string s = makeStr();            // 直接调用移动构造(若有)
std::string&& tmp = makeStr();        // tmp 绑定到临时字符串
  • std::move:本质是一个强制转换,将任何左值x转换成对应类型的右值引用T&&
std::string a = "data";
std::string b = std::move(a);  // a 的内部缓冲区“窃取”到 b,a 变为空

什么是虚继承?

当一个类通过多个派生类继承同一个基类时,虚继承确保基类的成员只有一个实例,而不是生成多余冗余实例。用于解决菱形继承问题。

  1. 语法
    在派生类声明的时候,使用virtual表示虚继承。
class A { /* 基类 */ };
class B : virtual public A { /* 派生自A */ };
class C : virtual public A { /* 派生自A */ };
class D : public B, public C { /* 派生自B和C */ };
  1. 原理
    编译器在派生类对象中不直接嵌入A的成员,而是存放一个指向唯一 A子对象的指针(虚基指针,vptr)。在构造时,保证虚基指针只初始化一次,共享同一份基类实例。

函数重载和重写的区别?

  • 重载:同一个作用域内允许多个同名函数,他们的参数类型或者个数不同。注意返回值类型不同不能算是重载。
  • 重写:继承时,子类重写父类的成员函数,参数列表必须与父类一致。
  • 区别
    • 重载在编译时就决定调用哪个函数(静态绑定),重写在运行时才决定。(动态绑定)
    • 重载可以发生在同一个类中,重写发生在继承关系的子类中。
    • 重载要求参数列表必须不同,重写要求参数列表和返回类型必须一致。
#include <iostream>  
#include <string>  
  
// 重载print函数以打印整数  
void print(int i) {  
    std::cout << "Printing int: " << i << std::endl;  
}  
  
// 重载print函数以打印浮点数  
void print(double f) {  
    std::cout << "Printing float: " << f << std::endl;  
}  
  
// 重载print函数以打印字符串  
void print(const std::string& s) {  
    std::cout << "Printing string: " << s << std::endl;  
}  
  
// 重载print函数以打印字符  
void print(char c) {  
    std::cout << "Printing char: " << c << std::endl;  
}  
  
int main() {  
    print(5);          // 调用打印整数的print  
    print(500.263);    // 调用打印浮点数的print  
    print("Hello");    // 调用打印字符串的print  
    print('A');        // 调用打印字符的print  
  
    return 0;  
}


运算符重载?


vector 原理,resize vs reservesizeVScapacity


deque 原理?

双端队列,

  • 分段式存储结构:分配若干个小块的连续内存,并用一个映射表(类似指针数组)来管理这些小内存。
  • 中央控制块:map(或者指针数组),这个数组的每个元素都是指针,分别指向一块子内存,这些内存块成为缓冲区。
  • 双端操作:插入和删除仅在小块内存上进行操作,不需要想vector一样在整个数组上进行。
  • 内存增长机制:需要增多内存时,分配新的缓冲区并更新map,不需要移动现有的元素。
  • 缓存局限性:频繁访问大量元素的时候,性能比较差,但在插入和删除方面会更加灵活。

map vs unordered_map

两者都是关联容器,都以键值对存储数据,但是在底层实现和使用场景上有明显的区别。

  1. 底层实现:
  • map基于有序的红黑树(自平衡二叉查找树),所有元素按键值有序存储。
  • unordered_map基于哈希表,元素按照哈希桶存储,不保证顺序。
  1. 时间复杂度
  • map:查找插入删除都为O (logn)
  • unordered_map:平均情况下查找插入删除为O(1),最坏的时候(大量哈希冲突)会退化到O(n)。
  1. 内存开销
  • map:存储指针和键值对,内存开销比较低。
  • unordered_map:需要维护哈希表的桶数组,并为每个元素额外存储桶链接或开放地址信息,需要占用更多的内存。
  1. 总结
  • map:需要有序遍历,内存要求较小
  • unordered_map:只关心快速查找/插入,不要求顺序。
  1. 扩展知识
  • 如果是复杂的数据类型,在map中需要自定义比较函数。
struct MyKey {
 int id;
 std::string name;
 bool operator<(const MyKey& other) const {
     return id < other.id; // 按id排序
 }
};
std::map<MyKey, int> m;
  • 在unordered_map中如果键类型是用户自定义的类型,需要自行提供哈希函数和比较器。
struct MyKey {
 int id;
 std::string name;
};
struct HashFunction {
 std::size_t operator()(const MyKey& k) const {
     return std::hash<int>()(k.id) ^ std::hash<std::string>()(k.name);
 }
};
struct KeyEqual {
 bool operator()(const MyKey& lhs, const MyKey& rhs) const {
     return lhs.id == rhs.id && lhs.name == rhs.name;
 }
};
std::unordered_map<MyKey, int, HashFunction, KeyEqual> um;

struct/class/union 区别与优化?

  1. 三种类型
  • struct默认成员为public:用于数据结构封装。
  • class默认成员为private:用于面向对象编程,支持封装、继承和多态。
  • union默认成员为public:所有成员共用一块内存,节省空间但是只保存一个成员变量。
  1. 区别
  • structclass的区别
    struct在C++中除了默认成员访问级别为public外,其他与class基本一致。
  • srtuctunion的区别
    • 存储方式:struct中所有成员变量格局占有独立的内存空间,union共用一块内存空间,且大小是最大成员的大小
    • 访问方式:前者都可访问,后者每次只能有一个成员变量有效,如果在一个成员值改变后访问另一个成员,结果不可预知。
    • 用途:struct用于存储逻辑上关联的不同数据,而union通常用于节省内存空间,作为一个优化项使用。例如在嵌入式系统或者资源有限的场景中,让变量共享内存,可以减少内存消耗。很多底层库为了性能极致,也会使用union,业务层一般不用。

举个例子:

//struct
struct MyStruct {
    int intVal;
    float floatVal;
    char charVal;
};

MyStruct s;
s.intVal = 10;
s.floatVal = 3.14f;
s.charVal = 'A';

//union
union MyUnion {
    int intVal;
    float floatVal;
    char charVal;
};

MyUnion u;
u.intVal = 10;
std::cout << "intVal: " << u.intVal << std::endl;

u.floatVal = 3.14f;
std::cout << "floatVal: " << u.floatVal << std::endl;

u.charVal = 'A';
std::cout << "charVal: " << u.charVal << std::endl;
//每次只能一个变量有效,其他成员的值会被覆盖
  1. 使用union进行优化
    利用union来节省空间(如解析网络数据包或者内存有限的嵌入式场景),例子如下:
struct DataPacket {
    int type;
    union {
        int intData;
        float floatData;
        char charData[4];
    } data;
};

DataPacket packet;

// 用于指示数据类型
packet.type = 0;  // 0 表示整数,1 表示浮点数,2表示字符数组
packet.data.intData = 10;
std::cout << "intData: " << packet.data.intData << std::endl;

// 再更改为浮点数数据
packet.type = 1;  
packet.data.floatData = 5.5;
std::cout << "floatData: " << packet.data.floatData << std::endl;

上述例子中通过使用结构体中的union来表示多种类型的数据,可以动态的切换数据类型,同时节省开销。
️在真实的项目中最好用enum(枚举类型)来标记当前有效的类型避免错误。


usingtypedef 区别?

建议用using,因为两个都可以用来为已有的类型定义一个新的名称,但是using可以用来定义模板别名,typedef不能。

using ulong = unsigned long;
using FuncPtr = int (*)(double);
  • using定义模板别名
template<typename T>
using Vec = std::vector<T>;
Vec<int> vecInt; // 相当于 std::vector<int> vecInt;
  • using还可以定义命名空间
namespace LongNamespaceName {
    int value;
}

using LNN = LongNamespaceName;
LNN::value = 42; // 相当于 LongNamespaceName::value

enum vs enum class

表示一组相关的、有限的常量值,使代码更具可读性、可维护性。

enum Color { Red, Green, Blue };
Color c = Red; // 比直接用 0、1、2 更直观

enum是传统枚举类型,enum class是强类型枚举(C++11后引进的新特性)推荐使用enum class(用enum class可获得更好的类型安全和命名隔离)

//示例
//enum
enum Color { Red, Green };
int x = Red;

//enum class
enum class Color : unsigned { Red, Green };
Color c = Color::Red;

list 使用场景?

list是双向链表,适合频繁插入和删除的场景,时间复杂度都是O(1)。
扩展知识

  • 与其他容器的比较
    vector适合频繁访问和修改元素,但中间插入和删除时效率较低。
    deque特点是双端可以快速插入和删除,同时支持快速访问。
    setmap之类的关联容器可以进行快速查找(基于平衡二叉树),但不适合频繁修改。

  • list::sortstd::sort
    list有自己实现排序功能的成员函数,在list中实现排序不要用std::sort。两者在底层上有本质不同:

    • std::sort采用快速排序+堆排序+插入排序三合一算法(平均和最坏复杂度都是O(nlogn))。
    • std::list::sort采用归并排序,利用链表可以直接修改节点链接而无需移送数据或swap,属于稳定的排序,时间复杂度也是O(nlogn)。

之所以用list::sort不用std::sort是因为std::sort它的使用要求随机访问迭代器,而list只提供双向访问迭代器。而且list::sort的排序稳定,迭代器也比较安全。


RAII 概念及场景?

  1. 概念
    RAII,资源获取即初始化。核心思想是将资源的获取与对象的生命周期绑定,通过构造函数获取资源,通过析构函数释放资源。
  2. 使用场景
  • 内存管理:两种智能指针。
  • 文件操作:std::fstream类在打开文件的时候获取资源,在析构函数中关闭文件。
  • 互斥锁:lock_guardunique_lock

lock_guard vs unique_lock

  • lock_guard很简洁,它唯一的作用就是确保做在作用域结束时自动释放互斥锁。
std::mutex mtx;
void example() {
 std::lock_guard<std::mutex> lock(mtx);
 // 互斥锁已经锁定,可以安全地访问共享资源
}  // 作用域结束,mtx 自动解锁
  • unique_lock提供了更加灵活的锁管理方式,适用于需要延迟锁定、显示解锁和锁所有权转移的场景。
  1. 延迟锁定:可以在构造unique_lock时选择不锁定互斥锁,而是在后续调用lock()方法进行显示锁定。
std::mutex mtx;
void example() {
    std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
    // 在需要时显式锁定
    lock.lock();
    // 互斥锁已经锁定,可以安全地访问共享资源
}  // 作用域结束,mtx 自动解锁
  1. 显示解锁:可以在中间需要时显示解锁互斥锁,然后再次锁定。
std::mutex mtx;
void example() {
    std::unique_lock<std::mutex> lock(mtx);
    // 访问共享资源
    lock.unlock();
    // 互斥锁已解锁
    // 其他不能并发访问的操作
    lock.lock();
    // 再次锁定共享资源
}  // 作用域结束,mtx 自动解锁
  1. 锁所有权转移:unique_lock的所有权可以在不同作用域之间转移,在一些需要精准控制锁的生命周期的情景特别有用。
std::mutex mtx;
void example() {
    std::unique_lock<std::mutex> lock1(mtx);
    // 访问共享资源
    std::unique_lock<std::mutex> lock2 = std::move(lock1);
    // lock1 不再拥有互斥锁
    // lock2 拥有互斥锁
}  // 作用域结束,mtx 自动解锁(如果 lock2 尚未解锁)

strcpy vs memcpy

两者都用于复制数据,strcpy用于复制字符串(会复制到遇到终止字符’\0’),memcpy可以复制任意类型的数据。(复制指定字长)

//strcpy
#include <cstring>

char src[] = "Hello, World!";
char dest[20];

strcpy(dest, src);

//memcpy
#include <cstring>

int src[] = {1, 2, 3, 4, 5};
int dest[5];

memcpy(dest, src, 5 * sizeof(int));

待补充

  • 运算符重载?
  • 拷贝构造函数和移动复制运算符?
  • thread join vs detach?
  • jthread vs thread
  • memcpy vs memmove
  • function/bind/lambda 使用情景?
  • C++ 模板优缺点?
  • 函数模板 vs 类模板?
  • SFINAE 原则?
  • std::array 优点?
  • 堆内存 vs 栈内存?
  • 栈溢出?
  • 回调函数用途?
  • nullptr vs NULL
  • 大端序 vs 小端序?
  • #include <...> vs #include "..."
  • 是否可 include 源文件?
  • 命名空间作用?
  • 友元类和友元函数?
  • 设计线程安全类?
  • 调用 C 语言库?
  • 指针与引用区别?

来源链接:https://www.cnblogs.com/frandermann/p/18869802

© 版权声明
THE END
支持一下吧
点赞13 分享
评论 抢沙发
头像
请文明发言!
提交
头像

昵称

取消
昵称表情代码快捷回复

    暂无评论内容