C++无锁设计

#include <atomic>
#include <iostream>

template <typename T>
class LockFreeStack {
private:
    struct Node {
        T data;
        Node* next;
        Node(T value) : data(value), next(nullptr) {}
    };

    std::atomic<Node*> head;

public:
    LockFreeStack() : head(nullptr) {}

    void push(T value) {
        Node* newNode = new Node(value);
        newNode->next = head.load();
        // CAS操作:比较并交换
        //head先比较它的值是否等于newNode->next,如果是则将head改为newNode,否则一直等待
        while (!head.compare_exchange_weak(newNode->next, newNode)); 
    }

    bool pop(T& result) {
        Node* topNode = head.load();
        while (topNode && !head.compare_exchange_weak(topNode, topNode->next)); // CAS操作
        if (topNode) {
            result = topNode->data;
            delete topNode;
            return true;
        }
        return false;
    }
};

C++为什么比其他高级语言运行的快

  1. C++编译器最终生成的是机器码,直接与底层硬件交互,而其他语言可能需要依赖如虚拟机(java),无需而外面的解释或者翻译
  2. 手动内存管理,不依赖语言层面的垃圾回收机制,虽说有垃圾回收机制能简化开发,但回收机制不可能是虽说想回收就回收的,c++能更加细粒度的控制分配和释放。
  3. 更少的运行检查,其他语言可能会依赖于语言机制的检查,而C++全由程序员检查
  4. 编译器优化,比如GCC和Clang的-o2和-o3优化选项等。

编译器优化

编译器优化主要是最终生成的机器码的优化

  1. 常量传播:
    指的是编译器中的常量值会被替换到所有它们用到的地方,减少内存访问和计算开销
  2. 内联展开:
    将函数调用展开成为函数体代码,从而消除调用开销,这种优化非常适合短小、频繁调用的函数。
  3. 循环优化
    循环展开:将循环体复制多分,减少循环控制变量的增减和跳转开销
    循环合并:将两个变量相同的数组循环合并成一个,较少不必要的循环控制开销
    循环分裂:复杂循环分成小循环,优化内存访问的局部性
  4. 死代码消除

内存可见性

内存可见性主要是说并发编程中,一个线程修改了变量后,其他线程在其寄存器或者缓存中保留了内存位置的副本,导致其他线程无法及时对修改可见。

产生此问题的原因主要是多处理器的cpu缓存导致、或者说是编译器优化(为了提高性能可能会重拍指令执行顺序,使变量的复制和读写顺序变化)、内存屏障
解决方案是用std::atomic原子操作、互斥锁

区分()和{}不同初始化

结论:

  1. 圆括号不会检查变窄的转换,花括号不允许,无法通过编译
  2. C++规定凡是能解析成声明的东西必须被解析为声明。因此:
    Widget w2();  //本来是要调用默认构造的,但是被解析成函数声明
    Widget w2{};  //此方式能够避免这个问题

3.存在的问题:

  • 构造函数调用,只要不包含std::initializer_list形参,花括号和圆括号初始化结果一样
  • 如果有std::initializer_list形参,使用花括号就会更倾向于此,甚至普通构造函数和移动构造函数也会被劫持,总之只有没办法转为参数列表,编译器才会选其他构造函数。如:
class Widget { 
public:  
    Widget(int i, bool b);      //同上
    Widget(int i, double d);    //同上
    Widget(std::initializer_list<long double> il);      //新添加的
}; 


Widget w1(10, true);    //使用圆括号初始化,同之前一样
                        //调用第一个构造函数

Widget w2{10, true};    //使用花括号初始化,但是现在
                        //调用带std::initializer_list的构造函数
                        //(10 和 true 转化为long double)

Widget w3(10, 5.0);     //使用圆括号初始化,同之前一样
                        //调用第二个构造函数 

Widget w4{10, 5.0};     //使用花括号初始化,但是现在
                        //调用带std::initializer_list的构造函数
                        //(10 和 5.0 转化为long double)

什么是静态多态

就是在编译期就能够进行类型绑定,核心思想是通过CRTP(奇异递归模版模式)来实现。
CRTP的思想:将派生类自身作为模版参数传递给基类,从而使积累的方法可以在编译器访问到派生类的实现。
代码示例:

template<typename Derived>
class Base{
public:
    void interface(){
        static_cast<Derived*>(this)->doSomething();
    }

    void doSomething()
    {
        cout<<"默认实现";    
    }
};

class Derived:public Base<Derived>
{
public:
    void doSomething()
    {
        cout<<"继承类实现";    
    }
};

class Derived2 : public Base<Derived2> {
    // 使用基类的默认 implementation 实现
};

int main()
{
    Derived d;
    d.interface();    //调用的是集成类的实现

    Derived2 d2;
    d2.interface(); // 输出 "Base implementation"
    return 0;
}

C++的发展方向的看法

  1. 简化语法和提高易用性:auto、lambda、智能指针等
  2. 性能优化和零开销抽象(使用抽象时在编译、运行时不会带来任何额外性能损耗,及和低级别的等价代码没有性能显著差异)

对MVC的理解,工作中哪个用了MVC。

作中的技术突破,什么功能做了比较有成就感?(不是业务逻辑的)

从图片挂靠功能问有没有用碰撞检测。

从统计节点工具延申出树的遍历方式(深度和广度)。

有正整数,数字范围是0~,用struct或者class,设计一种数据结构,方便查找跟插入。

浏览器渲染过程是怎么样的

http://www.jb51.net/softjc/67746.html

知道碰撞检测吗?对AABB和OBB了解多少?

知道texture2D跟texture3D吗?

知道哪些寻路算法?

图集优化,drawcall的理解,一张图什么时候不能打入一张图集?

有一大堆重复int值,如何获知不同int的数量

内存消耗太大,怎么改进

用位图需要提前分配足量内存,假如这些值很稀疏,那就会浪费大量内存,怎么改进?(*)

Qt 多线程

线程池怎么封装,一个线程给另一个线程抛消息怎么实现?

#include <vector>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <future>
#include <iostream>

class ThreadPool {
public:
    ThreadPool(size_t numThreads);
    ~ThreadPool();

    // 提交任务到线程池
    // 返回值类型:std::future<typename std::result_of<F(Args...)>::type>
    // 表示根据执行F(Args...)结果推导出返回值的类型
    template <class F, class... Args>
    auto enqueue(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type>;

private:
    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;

    std::mutex queueMutex;
    std::condition_variable condition;
    bool stop;
};

// 构造函数,创建指定数量的线程
ThreadPool::ThreadPool(size_t numThreads) : stop(false) {
    for (size_t i = 0; i < numThreads; ++i) {
        workers.emplace_back([this] {
            for (;;) {
                std::function<void()> task;
                {
                    std::unique_lock<std::mutex> lock(this->queueMutex);
                    this->condition.wait(lock, [this] { return this->stop || !this->tasks.empty(); });
                    if (this->stop && this->tasks.empty())
                        return;
                    task = std::move(this->tasks.front());
                    this->tasks.pop();
                }
                task();
            }
        });
    }
}

// 向线程池中提交任务
template <class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type> {
    using return_type = typename std::result_of<F(Args...)>::type;

    auto task = std::make_shared<std::packaged_task<return_type()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...));

    std::future<return_type> res = task->get_future();
    {
        std::unique_lock<std::mutex> lock(queueMutex);
        if (stop)
            throw std::runtime_error("enqueue on stopped ThreadPool");
        tasks.emplace([task]() { (*task)(); });
    }
    condition.notify_one();
    return res;
}

// 析构函数,等待所有线程完成
ThreadPool::~ThreadPool() {
    {
        std::unique_lock<std::mutex> lock(queueMutex);
        stop = true;
    }
    condition.notify_all();
    for (std::thread& worker : workers)
        worker.join();
}

快排优化,时间复杂度、空间复杂度

模板里可以定义虚函数吗

全部静态变量,局部静态变量,类内静态成员变量的生命周期,作用域,以及构造和析构函数的调用时间

在头文件中声明一个静态变量,在多个cpp文件进行调用,会生成多少个实例

sort()函数的实现,会用到什么排序,为什么

说一下C++11的新特性有哪些

auto、range for循环、lambda函数式编程、nullptr解决了C99中NULL的二义性、智能指针

python RAII机制

python的内存管理机制

用过哪些linux命令?比如网络方面的

查看cpu的命令呢

进程的状态有那几个

编程遇到过CLOSE_WAIT状态吗

文字排版引擎、表格计算引擎、演示动画引擎(WPS)

OCR 模型以及库文件大小不超过 9MB,可轻量化部署,该模型在文本检测、文本分类和文本识别上都表现出了较好的性能

TIME_WAIT对端口有什么影响

基本认识
是TCP主动断开后进入的一个短暂状态,目的是确保数据完整传输以及确保连接是可靠关闭的

对端口影响
会导致端口占用、同时有一定的系统占用、可能导致连接数限制

如何缓解

  1. TCP端口重用(SO_REUSEADDR/SO_REUSEPORT),服务端套接字开启后此状态下的端口依然可用
  2. 缩短持续时间(需要在系统上配置)
  3. 使用长连接,减少频繁的连接与关闭

注意事项
只有主动发起关闭请求的一方会进入此状态

遇到过程序崩溃的情况吗?怎么调试?gdb咋用的

g++ -g myprogram.cpp -o myprogram  # -g 生成调试信息

子窗口和父窗口的事件传递,叠加窗口的事件穿透

基本常识:

  1. 非gui事件由QCoreApplication负责将QEvent分发到QObject的子类
  2. GUI则由QApplication负责
  3. 需要接受处理事件,重写QObject::event()函数
  4. Qt中事件是一个QEvent对象,一个事件一个对象

event()函数并不直接处理事件,而是分发用,如果事件被识别并处理,需要返回true,Qt会认为事件已经处理完毕。

GUI事件处理流程:

  1. 接受鼠标事件
  2. QApplication调用QObject::event,进行事件分配
  3. 调用Button的时间处理函数
  4. 调用button的click函数
  5. 触发信号

事件传递流程

  1. 事件传递给获得焦点窗口的部件
    系统事件 -> QCoreApplication事件 -> (根据坐标、界面结构)从顶层窗口部件向下传递找到具体组件(childAt()函数)
  2. 如果该部件忽略该事件,则传递给父组件
    沿父子对象的层次结构向上冒泡传递,直到找到一个能够处理事件的对象
  3. 处理事件
    找打目标QCpreApplication的notify调用目标对象的event()

事件底层结构
每个线程都有一个事件队列,基于链表存储,每个事件都作为一个链表节点
QCoreApplication::postEvent将时间插入到时间队列中,QEventLoop不断去除事件处理。

linux基本命令

ip addr  ifconfig     #查看IP;
mv oldname newname    #给文件改名;
ls -al file           #查看文件权限
chmod 754  file       #修改权限
chmod +x file         #添加权限
lsb_release -a        #怎么查看当前系统的版本
df -h                 #怎么查看当前系统硬盘空间的总量与使用情况
free -h               #怎么查看系统内存多少
ldd /bin/ls           #怎么查看某个命令执行的时候需要链接哪些系统库;
ln -s file  linkname  #怎么给一个文件做一个软链接

说一下gdb

GUN中的调试器,用于调试C++程序

gdb ./my_program  # 启动gdb
run arg1          # 带参启动
break main        # 设置断点
break file.cpp:25 # 特定文件和行号设置断点
break file.cpp:25 if x==5 # 设置条件断点
continue          # 继续执行
next              # 单步执行
step              # 单步进入函数内部
print varname     # 打印变量
print *ptr        # 查看指针指向内容
backtrace         # 查看当前调用栈
set varname = newvalue # 修改变量值
info threads      # 查看线程列表
thread threadid   # 切换到特定线程
quit              # 退出调试器

怎么把一个test.cpp编译成一个test.so;

g++ -fPIC -shared -o test.so test.cpp -I/includePath -lotherlib

// -fPIC 生成动态库必须得配置,表示生成位置无关代码(PIC),共享库可以在任何位置加载
// -fPIC 生成共享库
// -I 依赖头文件位置
// -l 依赖库

ls -l test.so

红黑树的原理

1. 基本规则

a. 根节点是黑色的
b. 所有为null的叶子结点都是黑色的
c. 如果一个节点是红色,则它的子节点都是黑色的
d. 一个节点到它的子孙节点的所有黑色节点的路径都是相同的。
e. 时间复杂度是O(lgn)

2. 插入和删除

左旋:交换当前节点和它的右子节点,将右子节点作为当前节点的父亲,根据当前节点是其原父亲的左右节点,以相同的方式挂载新的父亲节点,新服务的左子结点作为当前节点的右子节点(取代新父亲之前的位置)。

LEFT-ROTATE(T, x)  
01  y ← right[x]            // 前提:这里假设x的右孩子为y。下面开始正式操作
02  right[x] ← left[y]      // 将 “y的左孩子” 设为 “x的右孩子”,即 将β设为x的右孩子
03  p[left[y]] ← x          // 将 “x” 设为 “y的左孩子的父亲”,即 将β的父亲设为x
04  p[y] ← p[x]             // 将 “x的父亲” 设为 “y的父亲”
05  if p[x] = nil[T]       
06  then root[T] ← y                 // 情况1:如果 “x的父亲” 是空节点,则将y设为根节点
07  else if x = left[p[x]]  
08            then left[p[x]] ← y    // 情况2:如果 x是它父节点的左孩子,则将y设为“x的父节点的左孩子”
09            else right[p[x]] ← y   // 情况3:(x是它父节点的右孩子) 将y设为“x的父节点的右孩子”
10  left[y] ← x             // 将 “x” 设为 “y的左孩子”
11  p[x] ← y                // 将 “x的父节点” 设为 “y”

右旋:将当前节点旋转成左子节点的右子节点,取代左子节点上的右子节点后,将这个右子节点当做自己的左子节点。

RIGHT-ROTATE(T, y)  
01  x ← left[y]             // 前提:这里假设y的左孩子为x。下面开始正式操作
02  left[y] ← right[x]      // 将 “x的右孩子” 设为 “y的左孩子”,即 将β设为y的左孩子
03  p[right[x]] ← y         // 将 “y” 设为 “x的右孩子的父亲”,即 将β的父亲设为y
04  p[x] ← p[y]             // 将 “y的父亲” 设为 “x的父亲”
05  if p[y] = nil[T]       
06  then root[T] ← x                 // 情况1:如果 “y的父亲” 是空节点,则将x设为根节点
07  else if y = right[p[y]]  
08            then right[p[y]] ← x   // 情况2:如果 y是它父节点的右孩子,则将x设为“y的父节点的左孩子”
09            else left[p[y]] ← x    // 情况3:(y是它父节点的左孩子) 将x设为“y的父节点的左孩子”
10  right[x] ← y            // 将 “y” 设为 “x的右孩子”
11  p[y] ← x                // 将 “y的父节点” 设为 “x”

map怎么插入数据,有几种方式

void test(){
    std::map<int,int> maps;
    maps.insert({1,100});
    maps.insert({1,200}); //如果存在相同的key则不会插入成功,返回当前已经存在的迭代器
    maps.emplace(2,333);  //更高效,直接构造键值对,避免额外拷贝
    maps[3] = 444;        //直接使用operator []
    maps.insert_or_assign(3,555) //C++17之后,如果存在则更新
}

进程怎么同步,线程怎么同步

1. 定位

同步是指在对共享资源访问时,尽可能避免数据不一致问题

2. 进程同步

信号量

#include <iostream>
#include <thread>
#include <semaphore>  // C++20

std::counting_semaphore<1> semaphore(1);  // 1 表示信号量初始值

void task(int id) {
    semaphore.acquire();  // 请求信号量
    std::cout << "Task " << id << " is working." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));  // 模拟工作
    std::cout << "Task " << id << " is done." << std::endl;
    semaphore.release();  // 释放信号量
}

int main() {
    std::thread t1(task, 1);
    std::thread t2(task, 2);

    t1.join();
    t2.join();

    return 0;
}

共享内存

#include <iostream>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>

int main() {
    const char* name = "/my_shared_memory";
    const size_t SIZE = 4096;

    // 创建共享内存
    int shm_fd = shm_open(name, O_CREAT | O_RDWR, 0666);
    ftruncate(shm_fd, SIZE);

    // 映射到进程的虚拟内存空间
    void* ptr = mmap(0, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);

    // 写入数据
    const char* message = "Hello from shared memory!";
    std::memcpy(ptr, message, std::strlen(message) + 1);

    std::cout << "Message written to shared memory: " << (char*)ptr << std::endl;

    // 清理
    munmap(ptr, SIZE);
    close(shm_fd);
    shm_unlink(name);

    return 0;
}

管道

#include <iostream>
#include <unistd.h>
#include <cstring>

int main() {
    int pipe_fd[2];
    char buffer[128];

    if (pipe(pipe_fd) == -1) {
        std::cerr << "Pipe failed!" << std::endl;
        return 1;
    }

    // 写入数据
    const char* message = "Hello from pipe!";
    write(pipe_fd[1], message, std::strlen(message) + 1);

    // 读取数据
    read(pipe_fd[0], buffer, sizeof(buffer));
    std::cout << "Received message: " << buffer << std::endl;

    close(pipe_fd[0]);
    close(pipe_fd[1]);

    return 0;
}

消息队列

#include <iostream>
#include <mqueue.h>
#include <cstring>

int main() {
    mqd_t mq;
    const char* queue_name = "/my_message_queue";

    // 创建消息队列
    mq = mq_open(queue_name, O_CREAT | O_RDWR, 0644, NULL);
    if (mq == -1) {
        std::cerr << "Message Queue failed!" << std::endl;
        return 1;
    }

    const char* message = "Hello from message queue!";
    mq_send(mq, message, std::strlen(message) + 1, 0);  // 发送消息

    char buffer[128];
    mq_receive(mq, buffer, sizeof(buffer), NULL);  // 接收消息
    std::cout << "Received message: " << buffer << std::endl;

    mq_close(mq);
    mq_unlink(queue_name);  // 删除消息队列

    return 0;
}

互斥锁

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;  // 互斥锁

void task(int id) {
    std::lock_guard<std::mutex> lock(mtx);  // 自动加锁和解锁
    std::cout << "Thread " << id << " is working." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));  // 模拟工作
    std::cout << "Thread " << id << " is done." << std::endl;
}

int main() {
    std::thread t1(task, 1);
    std::thread t2(task, 2);

    t1.join();
    t2.join();

    return 0;
}
3. 线程同步
  • 互斥锁
  • 读写锁
  • 条件变量
  • 原子操作
  • 自旋锁

tcp与udp的区别,udp怎么实现多对多通信,问的很细,包括tcp的各种机制。

1. 区别

前:TCP 后:UDP

  • TCP是面向连接的,先建立连接再通信;udp是非连接的
  • tcp可靠,udp不可靠
  • tcp支持流量控制,udp不支持
  • tcp流传输协议、数据包传输
  • 检验和重传机制、无重传
2. TCP机制
  • 连接断开:三次握手、四次挥手
  • 可靠性保证:数据包确认、当接受方收到数据包时会返回ack包确认,超时未收到,重传
  • 流量控制:滑动窗口
3. udp实现多对多通信
  • 广播,通过广播地址客户端可以向局域网的所有其他设备发送数据包,属于一对多
  • 组播类似,属于多对多,设备需要加入特定组播组
  • 通过心跳包机制保证长连接

tcp粘包问题

1. 定义,tcp是面向字节流协议并不保证数据包的边界,在发送过程中可能会导致多个小包合并成一个包的现象,导致数据边界丢失
2. 解决
  • 固定包长度
  • 数据包头部指定长度,支持可变长度消息
  • 使用分隔符,适用于文本协议如html,smtp
  • TCP_NODELAY项目,关闭Nagle算法(小包合并大包算法)

QT的信号槽是什么原理

  1. 基本的认识:
    Qt的信号声明实际上是 一个空函数,目的只是为了通知消息
    底层用的是生产者消费者模式
    信号与槽是基于qt的元对象系统,支持不同线程的正确传递
1. Q_OBJECT宏声明

依赖于元对象系统,启用信号槽功能,通过moc生成额外代码,包括:

  • 元信息:(类信号、槽、属性信息)
  • 信号槽链接、断开代码实现(connect、disconnect)
  • qt_metacall函数,管理信号槽调用参数传递
2. connect连接会将信号、槽信息存储到元数据中:
  • 通过qmetaobject查找信号、槽索引值
  • 每个object存有connectList,存储链接信息(接受对象、调用方式)
  • 如果是队列方式,qt会调用事件系统,封装信号发送到接受线程队列中
3. 信号发射实现

通过内部的connectList找到对应的槽函数,调用槽函数

4. 信号槽自动断开

object析构会调用connectList,删除相关连接信息

5. 信号槽性能优化
  • 快速路径:同一个线程用直连,不进人事件系统
  • 稀疏数组:qmetaobject信号槽索引用的是稀疏数组,加快查找速度
  • 无锁设计:同一个线程中不涉及锁,减少多线程锁开销
作者:admin  创建时间:2024-11-11 17:44
最后编辑:admin  更新时间:2024-11-14 09:20