推荐几款实用的C++ 在线工具
C++、Qt基础面试题

C++

unordered_map桶增长策略

swap的实现

C++中的swap函数使用move移动语义实现交换,节省了临时拷贝的开销

template<class T> inline
void swap(T& a, T& b)
{
	T temp = move(a);
	a = move(b);
	b = move(temp);
}

如何归还vector容器中的内存给系统

如果有一个vector a;的对象,一直往其中push_back元素,达到1G之多,现在有没有一个方法可以把a这个vector容器中的元素所占用的内存交还给操作系统。

在 C++ 中,对象的 swap 操作通常会交换它们的内部状态,包括指向堆内存的指针、资源句柄等等。在 std::vector 的情况下,调用 swap 函数会将两个 std::vector 对象的元素以及其内存空间进行交换,而不是进行拷贝或移动操作。因此,如果将一个非空的 std::vector 和一个空的 std::vector 进行 swap 操作,就可以让原先非空的 std::vector 占用的内存空间被空的 std::vector 所接管,从而实现内存的释放。具体来说,swap 函数会将两个 std::vector 对象内部的指针进行交换,而不会进行内存的分配和释放操作。

需要注意的是,如果一个 std::vector 对象中的元素是指针或引用等类型,而这些指针或引用指向的对象仍然存在于程序中,那么在 swap 操作之后,虽然 std::vector 对象所占用的内存已经被释放,但是指针或引用指向的对象仍然会存在于程序中。因此,在进行 swap 操作之前,需要确保 std::vector 对象中的元素不再被程序所使用。

template <typename T>
void swap(std::vector<T>& a, std::vector<T>& b)
{
    // 交换 vector 的元素
    a.swap(b);

    // 交换 vector 内部的大小、容量等信息
    std::swap(a._M_impl, b._M_impl);
}

定义一个空的vector,占多大的内存

定义一个空的 vector 不占用任何元素的内存空间,但是在内存中仍然需要为 vector 对象本身分配一些空间来存储其内部的元素指针、元素数量、容量等信息。

具体来说,一个空的 vector 对象在内存中通常需要占用以下空间:

在 32 位系统上,vector 对象通常需要占用 12 字节的空间;
在 64 位系统上,vector 对象通常需要占用 24 字节的空间。
需要注意的是,这只是一个大致的估计,不同的实现可能会有所不同。另外,vector 对象的大小还会受到内存对齐等因素的影响,因此具体的空间占用大小可能会有所差异。

多线程

使用多线程及其同步的方法,写出一个多线程打印0,1,0,1序列的功能。或者写出一个生产者消费者的功能。
条件变量的wait为何有第二个参数?

多线程的通信
  • 共享全局变量
  • 消息队列(也是一个全局共享的队列)
  • 创建线程时,传参
多线程的同步
控制多线程的顺序执行

生产者、消费者
关键:

  • 一个互斥量,用于保护生产者和消费者共用的队列
  • 一个条件变量,用于及时通知
    while { cv.wati() },防止虚假唤醒。
#include <iostream>
#include <thread>//多线程的头文件
#include <mutex>//互斥锁的头文件
#include <condition_variable>//条件变量的头文件
#include <queue>//C++ STL所有的容器都不是线程安全
using namespace std;

std::mutex mtx;//定义互斥锁,做线程间的互斥操作
std::condition_variable cv;//定义条件变量,做线程间的同步通信操作

//生产者生产一个物品,通知消费者消费一个;消费完了,消费者再通知生产者继续生产物品
class Queue
{
public:
    void put(int val)//生产物品
    {
        unique_lock<std::mutex> lck(mtx);
        while (!que.empty())
        {
            //que不为空,生产者应该通知消费者去消费,消费者消费完了,生产者再继续生产
            //生产者线程进入#1等待状态,并且#2把mtx互斥锁释放掉
            cv.wait(lck);//传入一个互斥锁,当前线程挂起,处于等待状态,并且释放当前锁 lck.lock()  lck.unlock
        }
        que.push(val);
        cout<<"produce "<<val<<endl;
        /*
        notify_one:通知唤醒另外的一个线程
        notify_all:通知唤醒其它所有线程
        通知其它所有的线程,我生产了一个物品,你们赶紧消费吧
        其它线程得到该通知,就会从等待状态 =》 到就绪状态 =》 但是要获取互斥锁才能继续向下执行
        */
        cv.notify_all();
    }

    int get()//消费物品
    {
//        lock_guard<std::mutex> guard(mtx);
        unique_lock<std::mutex> lck(mtx);
        while (que.empty())
        {
            //消费者线程发现que是空的,通知生产者线程先生产物品
            //#1 挂起,进入等待状态 #2 把互斥锁mutex释放
            cv.wait(lck);
        }//如果其他线程执行notify了,当前线程就会从等待状态 =》到就绪状态 =》但是要获取互斥锁才能继续向下执行

        int val = que.front();
        cout<<"consume: "<<val<<endl;
        que.pop();

        cv.notify_all();//通知其它线程我消费完了,赶紧生产吧
        return val;
    }
private:
    queue<int> que;
};

//这里模拟生产者生产10个物品,消费者消费10个物品
void producer(Queue* que)//生产者线程
{
    for (int i = 1; i <= 10; ++i)
    {
        que->put(i);
        std::this_thread::sleep_for(std::chrono::milliseconds(100));//睡眠100毫秒
    }
}

void consumer(Queue* que)//消费者线程
{
    for (int i = 1; i <= 10; ++i)
    {
        que->get();
        std::this_thread::sleep_for(std::chrono::milliseconds(100));//睡眠100毫秒
    }
}

int main()
{
    Queue que;	//两个线程共享的队列
    std::thread t1(producer, &que);//开启生产者线程
    std::thread t2(consumer, &que);//开启消费者线程
    //主线程等待两个子线程都执行完再结束。
    t1.join();
    t2.join();
    return 0;
}

什么是野指针、悬空指针

拷贝构造、移动构造、赋值运算符、移动赋值运算符

  • 拷贝构造、赋值运算符:注意深拷贝资源的指针。
    通常在类中自定义移动构造函数的同时,会再为其自定义一个适当的拷贝构造函数,由此当用户利用右值初始化类对象时,会调用移动构造函数;使用左值(非右值)初始化类对象时,会调用拷贝构造函数。
构造函数内调用虚函数
class A
{
public:
    A() {
        cout<<"A sturct"<<endl;
        fun();
    }
    virtual void fun() {
        cout<<"A fun"<<endl;
    }
};

class B : public A
{
public:
    B() {
        cout<<"B sturct"<<endl;
        fun();
    }
    virtual void fun() {
        cout<<"B fun"<<endl;
    }
};
    A a;	// A sturct  A fun
    B b; // A sturct  A fun B sturct B fun
    A* a1 = new B(); // A sturct  A fun B sturct B fun

返回值

Test拥有默认构造函数、析构函数、拷贝构造、移动构造、移动赋值、拷贝赋值运算符。看fun函数对Test这些函数的调用时机。

Test fun() {
	Test t;
	return t;
}

有移动构造和没有移动构造,这里的不一样:

  • 没有移动构造:Test默认构造 -》用t去构造一个临时变量,Test的拷贝构造 -》t变量析构,析构函数 -》返回临时变量
  • 有移动构造:Test默认构造 -》用t去构造一个临时变量,Test的移动构造 -》t变量析构,析构函数 -》返回临时变量
class Demo {
public:
    Demo() : num(new int(0)) {
        cout << "construct!" << endl;
    }

    Demo(const Demo &d) : num(new int(*d.num)) {
        cout << "copy construct!" << endl;
    }

    Demo(Demo &&d) : num(d.num) {
        d.num = nullptr;
        cout << "move construct!" << endl;
    }

    ~Demo() {
        cout << "class destruct!" << endl;
    }

private:
    int *num;
};

Demo getDemo() {
    Demo a;
    return a;
}

int main()
{
    auto b = getDemo();
    return 0;
}

i++/++i

    int i = 1;
    int a = i++;
    i = 1;
    int b = ++i;

    i = 1;
    int c = ++i + ++i;

a = 1 b = 2 c = 6

操作符重载

面试题:实现一个++i,i++的重载。
参考:C++操作符重载

emplace

  • 解释vector.push_back("xyzzy");的过程
    1、"xyzzy"调用string构造函数,得到一个临时右值变量temp
    2、调用push_back的右值重载函数,把temp实参传入给其形参x,然后内部调用vector的移动构造函数,得到一个副本,插入到vector容器中。
    3、temp临时变量析构
    而采用emplace_back则没有第一步,只有第二步,那么当然的第三步也没有。
    参考:https://cntransgroup.github.io/EffectiveModernCppChinese/8.Tweaks/item42.html

右值引用

  • std::move的实现
template<typename T>                            //在std命名空间
typename remove_reference<T>::type&&
move(T&& param)
{
    using ReturnType =                          //别名声明,见条款9
        typename remove_reference<T>::type&&;

    return static_cast<ReturnType>(param);
}
  • 解释下面的代码value(std::move(text)) 是否调用了string的移动构造函数?
class Annotation {
public:
    explicit Annotation(const std::string text)
    :value(std::move(text)) 
    { … }                       
    …
private:
    std::string value;
};

参考:https://cntransgroup.github.io/EffectiveModernCppChinese/5.RRefMovSemPerfForw/item23.html
记住:
第一,不要在你希望能移动对象的时候,声明他们为const。对const对象的移动请求会悄无声息的被转化为拷贝操作。第二点,std::move不仅不移动任何东西,而且它也不保证它执行转换的对象可以被移动。

  • std::forward是怎么知道它的实参是否是被一个右值初始化的?
    std::forward只对实参是右值时才做转换,把形参转为右值(所有的函数参数,都是左值,当实参是左值时,没必要转换)。

智能指针

  • shared_ptr是否线程安全?
    多线程操作同一个shared_ptr对象时,不是线程安全的。多线程操作多个shared_ptr对象(指向一块内存地址),是线程安全的,不需要去管。踏马的,这个不是正常吗,都不是一个对象了,多线程当然是安全的。
    另一个层面就是:shared_ptr对象所管理的那块内存是不是线程安全的。答,不是。多个shared_ptr对象,在多线程中,共同所指的那块内存地址的操作是不安全的。但也不一定都无脑的加锁去判断。有一种解决方式就是,用一个原子变量代表是否要对这块内存做处理,而在回到顺序处理的流程中,通过判断这个原子变量是否是true,来做最终的处理。
    另一个层面就是:shared_ptr初始化时的线程安全性。make_shared是线程安全的。
processWidget(std::shared_ptr<Widget>(new Widget),  //潜在的资源泄漏!
              computePriority());
// 解决方法1
processWidget(std::make_shared<Widget>(),   //没有潜在的资源泄漏
              computePriority());
// 解决方法2
std::shared_ptr<Widget> spw(new Widget);
processWidget(spw, computePriority());  // 正确,但是没优化,因为这里spw是一个左值,方法1中是一个右值参数
// 解决方法3
processWidget(std::move(spw), computePriority());   //高效且异常安全

因为std::shared_ptr<Widget>(new Widget),是两个步骤:1、new Widget 2、shared_ptr的构造函数,而std::make_shared<Widget>()是一步操作。参考:条款二十一:优先考虑使用std::make_unique和std::make_shared,而非直接使用new
参考:https://blog.csdn.net/bureau123/article/details/121300979
https://zhuanlan.zhihu.com/p/416289479

  • make_shared
// 两次分配内存,一次new Widget,一次shared_ptr构造函数中包含引用计数的控制块
std::shared_ptr<Widget> spw(new Widget);
// 一次分配,分配的内存同时容纳了Widget对象和控制块
auto spw = std::make_shared<Widget>();
  • make函数的缺点
    make函数都不允许指定自定义删除器。
    和直接使用new相比,make函数消除了代码重复,提高了异常安全性。对于std::make_shared和std::allocate_shared,生成的代码更小更快。
    不适合使用make函数的情况包括需要指定自定义删除器和希望用花括号初始化。
    对于std::shared_ptrs,其他不建议使用make函数的情况包括(1)有自定义内存管理的类;(2)特别关注内存的系统,非常大的对象,以及std::weak_ptrs比对应的std::shared_ptrs活得更久。

Lambda表达式

Lambda实现原理:参考https://www.zhihu.com/question/57241113/answer/2440288161
Lambda捕获原理,在定义时捕获还是运行时捕获? 运行时,如果是定义时,那么某个捕获的变量如果变化了,那这个变化的值就得不到了。
下面这段代码有什么问题:

    int i = 10;
    auto f = [=]() {
        i = 9;
    };
Lambda使用问题
  • 引用捕获外部的局部变量,此时可能在执行Lambda表达式时,该捕获的局部变量早已脱离作用域而析构了,导致悬空引用。此时需要改用值捕获来解决。
  • 值捕获一个指针变量。需要注意这个指针变量所指的内存是否已经被外面释放了。此时最好值捕获一个shared_ptr,或者使用深拷贝(即重写赋值运算符,里进行深拷贝)。
  • 捕获this问题。如果使用默认捕获,或者值捕获类对象的成员变量,都会存在潜在的问题。即类对象可能会析构了,而lambda表达式中的对成员变量的使用还在继续,这会导致崩溃。
    解决办法:1、捕获一个对这个成员变量的临时赋值的变量,而不是这个成员变量本身。2、使用C++14的广义Lambda 3、C++17增加了新特性可以捕获*this,不持有this指针,而是持有对象的拷贝,这样生命周期就与对象的生命周期不相关,使用上就安全一些。
    参考:Effective Modern C++ 条款31
    参考:c++的lambda使用注意事项,可能导致的崩溃问题分析

智能指针的实现

// 定义一个引用计数类,封装接口
class SharedCount
{
public:
    SharedCount() : m_count(1) {}

    void Add() { m_count++; }
    auto Reduce() { return --m_count; }
    auto GetCount() { return m_count; }

private:
    unsigned long m_count;
};


template <typename T>
class SharedPtr
{
public:
    // 默认构造
    SharedPtr() : m_ptr(nullptr), m_count(nullptr) {}
    // 普通指针初始化
    SharedPtr(T* t = nullptr) : m_ptr(t) {
        if (t != nullptr) {
            m_count = new SharedCount;
        }
    }
    ~SharedPtr() {
        if (m_ptr && m_count->Reduce() == 0) {
            delete m_ptr;
            m_ptr = nullptr;
            delete m_count;
            m_count = nullptr;
        }
    }

    // 拷贝构造
    SharedPtr(const SharedPtr<T> &other) :
        m_count(other.m_count), m_ptr(other.m_ptr) {
        if (other.m_count) {
            other.m_count->Add();
        }
    }

    // 移动构造
    SharedPtr(SharedPtr<T> &&other)
//        : m_count(std::move(other.m_count)),
//        m_ptr(std::move(other.m_ptr))
    {
        this->m_count = other.m_count;
        this->m_ptr = other.m_ptr;
        other.m_ptr = nullptr;
    }

    // 赋值运算
    SharedPtr<T>& operator= (const SharedPtr<T>& other) {
        if (m_ptr == other.m_ptr) {
            return *this;
        }
        // 目的对象不空
        if (m_ptr != nullptr) {
            if (m_count->Reduce() == 0) {
                delete m_ptr;
            }
        }
        // 赋值
        this->m_ptr = other.m_ptr;
        other.m_count->Add();
        this->m_count = other.m_count;
        return *this;
    }

    // 移动赋值运算符
    SharedPtr<T>& operator=(const SharedPtr<T> &&other) {
        if (m_ptr == other.m_ptr) {
            return *this;
        }

        if (m_ptr != nullptr) {
            if (m_count->Reduce() == 0) {
                delete m_ptr;
            }
        }

        m_ptr = other.m_ptr;
        m_count = other.m_count;
        other.m_ptr = nullptr;
        return *this;
    }

    // 解引用
    T* operator-> () {
        return m_ptr;
    }

    T& operator* () {
        return *m_ptr;
    }

    SharedPtr<T>& swap(SharedPtr<T> &other) {

    }

    auto Get() { return m_ptr; }
    auto GetCount() { return m_count ? m_count->GetCount() : 0; }
private:
    T* m_ptr = nullptr;
    SharedCount* m_count = nullptr;
};

编译和内存知识

这部分需要完整看完【程序员的自我修养】

QT

使用qml有哪些问题

  • 避免定时轮询,采用信号槽机制通知。
  • 避免频繁的属性访问,如果必要,可以先用一个变量存储这个属性,再去频繁范围这个变量即可。尽量用临时变量替代对属性的访问。
  • 属性绑定时,避免复杂的计算。简单的计算,QML可以直接得出结果,效率比较快。
  • 尽量少用属性绑定(因为被绑定的值一旦变化,属性表达式将重新计算)。用锚布局。
  • 属性设置为异步加载,asynchronous异步属性设置为true,在组件实例化时可以提高流畅性。
  • 避免var的声明,声明为具体的类型,局部变量声明未let
    https://www.jianshu.com/p/e6fcb575f916

Linux

fork进程

在这里插入图片描述
fork会拷贝当前进程的内存,并创建一个新的进程。如上图,fork函数会将整个进程的内存镜像拷贝到新的内存地址,包括代码段、数据段、堆栈以及寄存器内容。之后,我们就有了两个拥有完全一样内存的进程。
fork系统调用在两个进程中都会返回,在父进程中,fork系统调用会返回子进程的pid。而在新创建的进程中,fork系统调用会返回0。所以即使两个进程的内存是完全一样的,我们还是可以通过fork的返回值区分旧进程和新进程。
某种程度上来说这里的拷贝操作浪费了,因为所有拷贝的内存都被丢弃并被exec替换。在大型程序中这里的影响会比较明显。实际上操作系统会对其进行优化。(比如使用COW(copy on write)技术)
fork创建的新进程从fork语句后开始执行,因为新进程也继承了父进程的PC程序计数器。
参考:fork函数详解(附代码)

Windows

Windows如何定位卡死问题?
如何定位崩溃问题?
Windows下如何监听其他应用的一些系统调用?(安全客户端)

Windows内存泄漏定位

1、crt提供的接口:_CrtDumpMemoryLeaks
2、在两段代码直接建立内存快照(_CrtMemCheckpoint),然后比较一前一后两个内存快照的信息(_CrtMemDifference
3、使用WinDbg,通过heap命令查看代码前后的堆的信息,通过WinDbg的输出信息,查看那个堆地址的内存增长过多。然后通过!heap -p -a 000001e134546ce0,来输出一下它的调用堆栈

WINDBG定位内存泄漏

查看Dump文件,帮助我们定位问题。这个过程主要分为以下几步:

准备工具:需要安装一个Windows调试工具,比如WinDbg。

加载Dump文件:使用WinDbg打开Dump文件,在命令行输入“.loadby sos clr”,以加载托管代码的调试信息。

分析堆栈:使用命令“!clrstack”,以获取当前线程的堆栈,进而找出内存泄漏的代码。

检查对象:使用命令“!dumpheap -type [类型名称]”,检查该类型的对象,以找出是否有对象未被正确回收。

检查引用:使用命令“!gcroot [对象地址]”,检查该对象是否被其他对象引用,如果被引用,则需要递归检查引用链。

通过以上步骤,我们可以找到内存泄漏的原因,并通过修改代码来解决问题。

win32窗口从点击到相应的过程

参考:https://leetcode.cn/circle/discuss/g4YxE2/

Windows抓dump的原理

Windows 的 dump 抓取是一个捕捉程序异常信息的过程。当程序发生异常时,Windows 会将程序的当前状态保存到一个文件中,这个文件就是 dump 文件。

Windows 在抓取 dump 文件的过程中,会捕捉到程序的内存状态、系统状态、线程状态等信息。用户可以使用调试工具(例如 WinDBG)来分析 dump 文件,从而找出程序异常的原因。

Windows 抓取 dump 文件的方法有多种,如程序崩溃时系统会自动生成一个 mini dump,用户也可以通过命令行工具(例如 Procdump)手动生成一个 full dump。

总体来说,Windows 抓取 dump 的原理是通过捕捉程序的运行状态,来帮助用户分析程序的异常情况。

dump生产的原因有哪些

Windows dump 文件可以由多种原因生成。一些常见的原因包括:

程序崩溃:当程序发生异常时,Windows 会自动生成一个 mini dump。

内存泄漏:当程序在运行时不断分配内存,但没有释放内存,导致内存泄漏。用户可以通过 dump 文件找出内存泄漏的原因。

假死:当程序没有响应时,用户可以生成一个 dump 文件,以便分析程序的假死原因。

手动生成:用户可以通过命令行工具(例如 Procdump)手动生成一个 dump 文件,以便分析程序的异常情况。

这些只是一些常见的原因,实际原因还有很多。使用 dump 文件分析程序的异常情况是一项非常有用的工具,可以帮助用户更快地定位和解决程序的问题。

dll的入口函数

参考:https://blog.csdn.net/qq_33757398/article/details/82230360

设计模式

观察者-监听者模式

观察者:接口类提供一个接口handle,每个具体的观察者实现这个接口,并拥有自己独特的处理方法。
监听者:提供一个注册接口,接收观察者实例,并记录到内部的数据结构中。
另外提供一个处理的接口,从外部接收不同类型的数据,从内部匹配具体的观察者,然后调用对应的观察者的handle接口完成。

两个观察者,实现handle接口:

class Observer {
public:
	virtual void handle(int msg_id) = 0;
};

class Observer1 : public Observer {
public:
	void handle(int msg_id) {
		switch (msg_id) {
		case 1:
			cout << "Observer1 recv msg 1" << endl;
			break;
		case 2:
			cout << "Observer1 recv msg 2" << endl;
			break;
		default:
			break;
		}
	}
};

class Observer2 : public Observer {
public:
	void handle(int msg_id) {
		switch (msg_id) {
		case 2:
			cout << "Observer2 recv msg 2" << endl;
			break;
		default:
			break;
		}
	}
};

监听者:

class Subject {
public:
	void add_observer(int msg_id, Observer* observer) {
		_sub_map[msg_id].push_back(observer);
	}

	// 发布消息,通知对msg_id感兴趣的观察者处理该事件
	void publish(int msg_id) {
		auto iter = _sub_map.find(msg_id);
		if (iter != _sub_map.end()) {
			for (Observer* obser : iter->second) {
				obser->handle(msg_id);
			}
		}
	}
private:
	// 存放对事件感兴趣的观察者们
	unordered_map<int, list<Observer*>> _sub_map;
};

共享内存

共享内存上创建C++对象的问题

参考:https://blog.csdn.net/dickyjyang/article/details/21403451
https://www.cnblogs.com/yangru/p/3805192.html

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐