C++面试题-----Qt篇

C++面试题-----Qt基础知识

  • Qt信号与槽
    • Qt信号与槽的优势和不足
    • 自定义信号槽注意事项:
    • 信号槽的多种用法:
  • TCP/UDP
    • 描述Qt下Tcp通信的整个流程
    • 描述QT下udp通信的整个流程
  • 多线程
    • 描述QT下多线程的两种使用方法, 以及注意事项
    • 多线程下,信号槽分别在什么线程中执行,如何控制
    • Qt多线程同步的几种实现方式
  • 智能指针
  • Qt的d指针和p指针
  • QVariant使用
  • 实例问题
  • 继承与派生
    • 继承与派生的区别
    • 单继承与多继承
    • 三种继承方式 公有继承,私有继承和保护继承
  • Qt4和Qt5的区别
  • Qwidget、Qobejct实现了哪些功能
    • QObject
    • QWidget
  • 参数传值、指针、引用有什么区别,在什么场景常用哪种传递方式?
    • 传值
    • 传址
    • 传引用
    • 哪一种更高效?

Qt信号与槽

Qt信号与槽的优势和不足

优势:

  1. 类型安全。信号的参数类型和参数个数同接收该信号的槽的参数类型和参数个数相同。不过,一个槽的参数个数是可以少于信号的参数个数的,但缺少的参数必须是信号参数的最后一个或几个参数。
  2. 松散耦合。信号和槽机制减弱了Qt对象的耦合度。激发信号的Qt对象无需知道是哪个对象的哪个槽需要接收它发出的信号,它只需在适当的时间发送适当的信号就可以了,而不需要知道也不关心它的信号有没有被接收到,更不需要知道是哪个对象的哪个槽收到了信号。同样的,对象的槽也不知道是哪些信号关联了自己,而一旦关联信号和槽,Qt就保证了适合的槽得到了调用。即使关联的对象在运行时被删除,应用程序也不会崩溃。
  3. 信号和槽机制增强了对象间通信的灵活性。一个信号可以关联多个槽,也可以多个信号关联一个槽。

不足:

同回调函数相比,信号和槽机制运行速度有些慢。通过传递一个信号来调用槽函数将会比直接调用非虚函数运行速度慢10倍。原因如下:

  1. 需要定位接收信号的对象;
  2. 安全地遍历所有的关联(如一个信号关联多个槽的情况);
  3. 编组/解组传递的参数;
  4. 多线程的时候,信号可能需要排队等待。

然而,与创建对象的new操作及删除对象的delete操作相比,信号和槽的运行代价只是他们很少的一部分。信号和槽机制导致的这点性能损耗,对实时应用程序是可以忽略的。

本质 :回调函数
回调函数的本质是“你想让别人的代码执行你的代码,而别人的代码你又不能动”这种需求下产生的。

自定义信号槽注意事项:

  1. 发送者和接收者都需要是QObject的子类(当然,槽函数是全局函数、Lambda 表达式等无需接收者的时候除外);

  2. 使用 signals 标记信号函数,信号是一个函数声明,返回 void,不需要实现函数代码;

  3. 槽函数是普通的成员函数,作为成员函数,会受到 public、private、protected 的影响;

  4. 使用 emit 在恰当的位置发送信号;

  5. 使用QObject::connect()函数连接信号和槽;

  6. 任何成员函数、static 函数、全局函数和 Lambda 表达式都可以作为槽函数。

信号槽的多种用法:

  1. 一个信号可以和多个槽相连
    如果是这种情况,这些槽会一个接一个的被调用,但是它们的调用顺序是不确定的。
  2. 多个信号可以连接到一个槽
    只要任意一个信号发出,这个槽就会被调用。
  3. 一个信号可以连接到另外的一个信号
    当第一个信号发出时,第二个信号被发出。除此之外,这种信号-信号的形式和信号-槽的形式没有什么区别。
  4. 槽可以被取消链接
    这种情况并不经常出现,因为当一个对象delete之后,Qt自动取消所有连接到这个对象上面的槽。
  5. 使用Lambda 表达式
    在使用 Qt 5 的时候,能够支持 Qt 5 的编译器都是支持 Lambda 表达式的。

TCP/UDP

描述Qt下Tcp通信的整个流程

服务器端:

  1. 创建用于监听的套接字 (Socket)【socket可以看成在两个程序进行通讯连接中的一个端点,一个程序将一段信息写入Socket中,该Socket将这段信息发送给另外一个Socket中,使这段信息能传送到其他程序中】
  2. 给套接字设置监听
  3. 如果有连接到来, 监听的套接字会发出信号newConnected
  4. 接收连接, 通过nextPendingConnection()函数, 返回一个QTcpSocket类型的套接字对象(用于通信)
  5. 使用用于通信的套接字对象通信
    1>. 发送数据: write
    2>. 接收数据: readAll/read

客户端:

  1. 创建用于通信的套接字
  2. 连接服务器: connectToHost
  3. 连接成功与服务器通信
    1>. 发送数据: write
    2>. 接收数据: readAll/read

描述QT下udp通信的整个流程

QT下udp通信服务器端和客户端的关系是对等的, 做的处理也是一样的.

  1. 创建套接字对象
  2. 如果需要接收数据, 必须绑定端口
  3. 发送数据: writeDatagram
  4. 接收数据: readDatagram

多线程

描述QT下多线程的两种使用方法, 以及注意事项

方法一:

  1. 创建一个类从QThread类派生
  2. 在子线程类中重写 run 函数, 将处理操作写入该函数中
  3. 在主线程中创建子线程对象, 启动子线程, 调用start()函数

方法二:

  1. 将业务处理抽象成一个业务类, 在该类中创建一个业务处理函数
  2. 在主线程中创建一QThread类对象
  3. 在主线程中创建一个业务类对象
  4. 将业务类对象移动到子线程中
  5. 在主线程中启动子线程
  6. 通过信号槽的方式, 执行业务类中的业务处理函数

方法三:

QFuture< void> fut1 = QtConcurrent::run(processFun, command);

processFun为线程回调函数

多线程使用注意事项:

  1. 业务对象, 构造的时候不能指定父对象
  2. 子线程中不能处理ui窗口(ui相关的类)
  3. 子线程中只能处理一些数据相关的操作, 不能涉及窗口

多线程下,信号槽分别在什么线程中执行,如何控制

可以通过connect的第五个参数connectType进行控制信号槽执行时所在的线程

connect有几种连接方式,直接连接和队列连接、自动连接

直接连接:Qt::AutoConnection: 默认值,使用这个值则连接类型会在信号发送时决定。如果接收者和发送者在同一个线程,则自动使用
   Qt::DirectConnection:槽函数会在信号发送的时候直接被调用,槽函数运行于信号发送者所在线程。效果看上去就像是直接在信号发送位置调用了槽函数。这个在多线程环境下比较危险,可能会造成奔溃。

队列连接:Qt::QueuedConnection:信号在信号发出者所在的线程中执行,槽函数在信号接收者所在的线程中执行
  Qt::BlockingQueuedConnection:槽函数的调用时机与Qt::QueuedConnection一致,不过发送完信号后发送者所在线程会阻塞,直到槽函数运行完。接收者和发送者绝对不能在一个线程,否则程序会死锁。在多线程间需要同步的场合可能需要这个。

自动连接:多线程时为队列连接函数,单线程时为直接连接函数。

Qt多线程同步的几种实现方式

(1)互斥量:QMutex
QMutex类提供的是线程之间的访问顺序化。QMutex的目的是保护一个对象/数据结构或者代码段在同一时间只有一个线程可以访问。基本使用方法如下:

QMutex mutex;
int var;
 
void function()
{
    mutex.lock();
    // 访问var
    var * var;
    mutex.unlock();
}

如果使用mutex加锁,却没有使用unlock解锁,那么就会造成死锁,其他线程永远也得不到访问变量的机会,所以为了解决这个问题,Qt引入了QMutexLocker类,二者直接可以配合使用更加方便简洁,示例如下:

QMutex mutex;
int var;
 
void function()
{
    QMutextLocker locker(&mutex); 
    // 访问var
    var * var;
}

(2)QReadWriteLock
QMutex只允许某个时刻有一个线程对共享资源进行访问,如果需要多个线程对共享资源进行读访问,同时只有一个线程进行写访问,这种情况下就可以使用QReadWriteLock。QReadWriteLock主要实现多个线程读资源,一个线程写。写线程执行的时候会阻塞所有的读线程,而读线程之间的运行不需要进行同步。使用示例如下:

int var;
QReadWriteLock lock;
 
void function()
{
    lock.lockForRead();
    int x = var;
    lock.unlock();
}
 
void function2()
{
    lock.lockForWrite();
    var = 100;
    lock.unlock();
}

和QMutexLocker一样,Qt同样提供了QReadLocker和QWriteLocker。

int var;
QReadWriteLock lock;
 
void fun()
{
    QReadLocker(&lock);
    int x = var;
}
 
void fun2()
{
    QWriteLocker(&lock);
    var = 1000;
}

(3)QSemaphore
QSemaphore是QMutex的一般化,它可以保护一定数量的相同资源,而QMutex只能保护一个资源。信号量比互斥量具有更好的并发性,我们可以利用信号量实现生产者-消费者模式,如下所示:

const int dataSize = 100000;
const int bufferSize = 1024;
char buffer[bufferSize];
QSemaphore freeBytes(bufferSize);
QSemaphore usedButes;
 
void Producer::run()
{
    for (int i = 0; i < dataSize; ++i)
    {
        freeBytes.acquire();
        buffer[i % bufferSize] = i;
        usedBytes.release();
    }
}
 
void Consumer::run()
{
    for (int i = 0; i < dataSize; ++i)
    {
        usedBytes.acquire();
        qDebug() << buffer[i % bufferSize];
        freeBytes.release();
    }
}

智能指针

1.QPointer

  • 特点:当其指向的对象(T必须是QObject及其派生类)被销毁时,它会被自动置NULL.
  • 注意:它本身析构时不会自动销毁所guarded的对象
  • 用途:当你需要保存其他人所拥有的QObject对象的指针时,这点非常有用

2.QScopedPointer QScopedArraytPointer与 std::unique_ptr/scoped_ptr

  • 这是一个很类似auto_ptr的智能指针,它包装了new操作符在堆上分配的动态对象,能够保证动态创建的对象在任何时候都可以被正确地删除。但它的所有权更加严格,不能转让,一旦获取了对象的管理权,你就无法再从它那里取回来。
  • 无论是QScopedPointer 还是 std::unique_ptr 都拥有一个很好的名字,它向代码的阅读者传递了明确的信息:这个智能指针只能在本作用域里使用,不希望被转让。因为它的拷贝构造和赋值操作都是私有的,这点我们可以对比QObject及其派生类的对象哈。
  • unique_ptr 是 C++11 才开始提供的类型,是一种在异常时可以帮助避免资源泄漏的智能指针。采用独占式拥有,意味着可以确保一个对象和其相应的资源同一时间只被一个 pointer 拥有。一旦拥有者被销毁或编程 empty,或开始拥有另一个对象,先前拥有的那个对象就会被销毁,其任何相应资源亦会被释放。(interview)

3.QSharedPointer QSharedArrayPointer 与 std::shared_ptr

  • QSharedPointer 与 std::shared_ptr 行为最接近原始指针,是最像指针的"智能指针",应用范围比前面的提到的更广。
  • QSharedPointer 与 QScopedPointer 一样包装了new操作符在堆上分配的动态对象,但它实现的是引用计数型的智能指针 ,可以被自由地拷贝和赋值,在任意的地方共享它,当没有代码使用(引用计数为0)它时才删除被包装的动态分配的对象。shared_ptr也可以安全地放到标准容器中,并弥补了std::auto_ptr 和 QScopedPointer 因为转移语义而不能把指针作为容器元素的缺陷。
  • 多个智能指针可以共享同一个对象,对象的最末一个拥有着有责任销毁对象,并清理与该对象相关的所有资源。支持定制型删除器(custom deleter),可防范 Cross-DLL 问题(对象在动态链接库(DLL)中被 new 创建,却在另一个 DLL 内被 delete 销毁)、自动解除互斥锁 (interview)

4.QWeakPointer 与 std::weak_ptr

  • 强引用类型的QSharedPointer已经非常好用,为什么还要有弱引用的 QWeakPointer? QWeakPointer 是为配合 QSharedPointer 而引入的一种智能指针,它更像是 QSharedPointer 的一个助手(因为它不具有普通指针的行为,没有重载operator*和->)。它的最大作用在于协助 QSharedPointer 工作,像一个旁观者一样来观测资源的使用情况。
  • weak_ptr 主要是为了避免强引用形成环状。摘自msdn中一段话:
    A cycle occurs when two or more resources controlled by shared_ptr objects hold mutually referencing shared_ptr objects. For example, a circular linked list with three elements has a head node N0; that node holds a shared_ptr object that owns the next node, N1; that node holds a shared_ptr object that owns the next node, N2; that node, in turn, holds a shared_ptr object that owns the head node, N0, closing the cycle. In this situation, none of the reference counts will ever become zero, and the nodes in the cycle will not be freed. To eliminate the cycle, the last node N2 should hold a weak_ptr object pointing to N0 instead of a shared_ptr object. Since the weak_ptr object does not own N0 it doesn’t affect N0’s reference count, and when the program’s last reference to the head node is destroyed the nodes in the list will also be destroyed.
  • 在Qt中,对于QObject及其派生类对象,QWeakPointer有特殊处理。它可以作为QPointer的替代品
    这种情况下,不需要QSharedPointer的存在
  • weak_ptr 允许你共享但不拥有某对象,一旦最末一个拥有该对象的智能指针失去了所有权,任何 weak_ptr 都会自动成空(empty)。因此,在 default 和 copy 构造函数之外,weak_ptr 只提供 “接受一个 shared_ptr” 的构造函数。可打破环状引用(cycles of references,两个其实已经没有被使用的对象彼此互指,使之看似还在 “被使用” 的状态)的问题 (interview)

5.QSharedDataPointer

  • 这是为配合 QSharedData 实现隐式共享(写时复制 copy-on-write))而提供的便利工具。
  • Qt中众多的类都使用了隐式共享技术,比如QPixmap、QByteArray、QString、…。而我们为自己的类实现隐式共享也很简单,比如要实现一个 Employee类:定义一个只含有一个数据成员(QSharedDataPointer) 的 Employee 类.我们需要的所有数据成员放置于 派生自QSharedData的 EmployeeData类中。

6.QExplicitlySharedDataPointe

  • 这是为配合 QSharedData 实现显式共享而提供的便利工具。
  • QExplicitlySharedDataPointer 和 QSharedDataPointer 非常类似,但是它禁用了写时复制功能。这使得我们创建的对象更像一个指针。

Qt的d指针和p指针

保持一个库中的所有公有类的大小恒定的问题可以通过单独的私有指针给予解决。这个指针指向一个包含所有数据的私有数据结构体。这个结构体的大小可以随意改变而不会产生副作用,应用程序只使用相关的公有类,所使用的对象大小永远不会改变,它就是该指针的大小。这个指针就被称作D指针。

D指针的其他好处

  1. 隐藏实现细节——我们可以不提供widget.cpp文件而只提供WidgetLib和相应的头文件和二进制文件。
  2. 头文件中没有任何实现细节,可以作为API使用。
  3. 由于原本在头文件的实现部分转移到了源文件,所以编译速度有所提高。
  4. 二进制兼容

其实以上的点都很细微,自己跟过源代码的人都会了解,qt是隐藏了d指针的管理和核心源的实现。像是在_p.h中部分函数的声明,qt也宣布在以后版本中将会删除。

d_ptr指针指向父类,使用如下宏定义辅助函数和声明友元类

#ifndef D_PTR_H
#define D_PTR_H
 
#include 
 
template <typename T> static inline T *GetPtrHelper(T *ptr) { return ptr; }
 
#define DECLARE_PRIVATE(Class) \
    inline Class##Private* d_func() { return reinterpret_cast<Class##Private*>(GetPtrHelper(d_ptr)); } \
    inline const Class##Private* d_func() const { return reinterpret_cast<const Class##Private*>(GetPtrHelper(d_ptr)); }\
    friend class Class##Private;
 
#define DPTR(Class) Class##Private * const d  = d_func()
 
class MyClassPrivate;
 
class MyClass : public QObject {
    Q_OBJECT
public:
    explicit MyClass(QObject *parent = 0);
    virtual ~MyClass();
    void testFunc();
    protected:
         MyClass(MyClassPrivate &d);
 
private:
        MyClassPrivate * const d_ptr;
    DECLARE_PRIVATE(MyClass);
    MyClass(const MyClass&);
    MyClass& operator= (const MyClass&);
};
 
#endif 

#ifndef Q_PTR_H
#define Q_PTR_H
 
 
#include 
#include "d_ptr.h"
 
#define DECLARE_PUBLIC(Class) \
    inline Class* q_func() { return static_cast<Class *>(q_ptr); } \
    inline const Class* q_func() const { return static_cast<const Class *>(q_ptr); } \
    friend class Class;
 
#define QPTR(Class) Class * const q = q_func()
 
class MyClassPrivate : public QObject
{
Q_OBJECT
 
public:
    MyClassPrivate(MyClass *q, QObject *parent = 0);
    virtual ~MyClassPrivate() {}
 
signals:
    void testSgnl();
 
private slots:
    void testSlt();
 
public:
    void fool();
 
private:
    MyClass * const q_ptr;
    DECLARE_PUBLIC(MyClass);
};
 
#endif 

QVariant使用

  1. 用户自定义需要先注册一个类型,即使用qRegisterMetaType,注册到QT的一个Vector中
  2. QVariant里面会new一个用户自定义类型的内存,并调用拷贝构造函数,QVariant自身的赋值会使用共享内存管理
    所以用户可以传入一个临时变量地址,如果用户传入的是一个指针,这个指针需要用户自己析构,改变这个指针的值,并不会改变QVariant,因为是两个不同的空间了

而如果QVariant a1=b1(b1是QVariant),改变b1的值会改变a1的。因为这样用的是shared指针

初看以为是对的,验证发现不准确,改变b1并没有改变a1的值,细看发现这里面有QT使用了个小技巧,要取b1的值然后改变时,会调用data函数

CVariantHelp* pBTemp =  reinterpret_cast<CVariantHelp*>(b1.data());
pBTemp->j_ = 99;

而data的实现会调用detach将shared分离

void* QVariant::data()
{
    detach();
    return const_cast<void *>(constData());
}

void QVariant::detach()
{
    if (!d.is_shared || d.data.shared->ref == 1)
        return;
    Private dd;
    dd.type = d.type;
    handler->construct(&dd, constData());
    if (!d.data.shared->ref.deref())
        handler->clear(&d);
    d.data.shared = dd.data.shared;
}

实例问题

1. 描述过程, 如何实现一个自定义按钮, 使其在光标进入,按下,离开三种状态下显示不同的图片.

创建一个类, 让其从QPushButton类派生, 重写该类中的事件处理器函数

方法1:
1>. enterEvent() – 光标进入
2>. leaveEvent() – 光标离开
3>. mousePressEvent() – 鼠标按下
4>. paintEvent() – 刷新背景图

方法二:

通过setstylesheet设置

2. QMainForm是从哪里派生的?
QMainWindow::QWidget::QObject

继承与派生

继承与派生的区别

1. 角度不同
继承是从子类的角度讲的,派生是从基类的角度讲的。

2. 定义不同
继承 是面向对象软件技术当中的一个概念,与多态、抽象共为面向对象的三个基本特征。 继承可以使得子类具有父类的属性和方法或者重新定义、追加属性和方法等。派生指江河的源头产生出支流。引申为从一个主要事物的发展中分化出来。

单继承与多继承

1. 单继承(派生类只从一个直接基类继承)时派生类的定义:

class 派生类名:继承方式 基类名
{
新增成员声明;
}

2. 多继承时派生类的定义:

class 派生类名:继承方式1 基类名1,继承方式2 基类名2,…
{
成员声明;
}
注意:每一个“继承方式”,只用于限制对紧随其后之基类的继承。

三种继承方式 公有继承,私有继承和保护继承

1. 继承的访问控制

基类的public和protected成员:访问属性在派生类中保持不变;
基类的private成员:不可直接访问。

2. 访问权限

派生类中的成员函数:可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员;
通过派生类的对象:只能访问public成员。

3. 公有派生类对象可以被当作基类的对象使用,反之则不可。

派生类的对象可以隐含转换为基类对象;
派生类的对象可以初始化基类的引用;
派生类的指针可以隐含转换为基类的指针。
通过基类对象名、指针只能使用从基类继承的成员,派生类新增的成员就不能使用了

Qt4和Qt5的区别

1. 新增widgets模块
在Qt4中,Qt提供的全部图形界面相关类都包含在Qt Gui模块中,但QT5将一些图形界面类移到了QT widgets模块中。所以在Pro文件中,需要增加一句话:

 greaterThan(QT_MAJOR_VERSION, 4):QT += widgets

意思是如果Qt版本大于Qt4,则需要增加widgets模块。

2. 信号与槽的语法
Qt4中关联信号与槽一般这样写:

connect(sender, SINGAL(valueChanged(QString, QString)), receiver, SLOT(showValue(QString)));

但这样写,没有编译器检查,有时编译器通过但应该调用的槽函数没有执行。这是编译器不能给出错误信息,只能在运行时看是否有警告。

Qt5中关联信号与槽是这样写:

connect(sender, &Sender::valueChanged, receiver, &Receiver::showValue);

这种写法支持编译器检查,能够在编译时就发现错误;并支持类型的隐式转换。

3. 对c++11的支持
Qt5支持C++11,但有些编译器默认不开启。所以需要在Pro文件中增加一行:

CONFIG += c++11

Qwidget、Qobejct实现了哪些功能

QObject

  1. 信号和槽的非常强大的机制,使用connect()把信号和槽连接起来并且可以用disconnect()来破坏这种连接。为了避免从不结束的通知循环,你可以调用blockSignals()临时地阻塞信号。保护函数connectNotify()和disconnectNotify()使跟踪连接成为可能。

  2. QObject可以通过event()接收事件并且过滤其它对象的事件。详细情况请参考installEventFilter()和eventFilter()。一个方便的处理者,childEvent(),能够被重新实现来捕获子对象事件。

  3. 最后但不是最不重要的一点,QObject提供了Qt中最基本的定时器,关于定时器的高级支持请参考QTimer。

  4. 注意Q_OBJECT宏对于任何实现信号、槽和属性的对象都是强制的。

  5. 所有的Qt窗口部件继承了QObject。方便的函数isWidgetType()返回这个对象实际上是不是一个窗口部件。它比inherits(“QWidget” )快得多。

QWidget

  1. QWidget类是所有用户界面对象的基类。
  2. Widget是用户界面的基本单元:它从窗口系统接收鼠标,键盘和其他事件,并在屏幕上绘制自己。每个Widget都是矩形的,它们按照Z-order进行排序。
    C++面试题-----Qt篇_第1张图片

参数传值、指针、引用有什么区别,在什么场景常用哪种传递方式?

传值

这种传递方式中,实参和形参是两个不同的地址空间,参数传递的实质是将原函数中变量的值,复制到被调用函数形参所在的存储空间中,这个形参的地址空间在函数执行完毕后,会被回收掉。整个被调用函数对形参的操作,只影响形参对应的地址空间,不影响原来函数中的变量的值,因为这两个不是同一个存储空间。

即使形参的值在函数中发生了变化,实参的值也完全不会受到影响,仍为调用前的值。

传址

这种参数传递方式中,实参是变量的地址,形参是指针类型的变量,在函数中对指针变量的操作,就是对实参(变量地址)所对应的变量的操作,函数调用结束后,原函数中的变量的值将会发生改变。

被调用函数中对形参指针所指向的地址中内容的任何改变都会影响到实参。

传引用

这种参数传递方式中,形参是引用类型变量,其实就是实参的一个别名,在被调用函数中,对引用变量的所有操作等价于对实参的操作,这样,整个函数执行完毕后,原先的实参的值将会发生改变。

被调函数对形参做的任何操作都影响了主调函数中的实参变量。

哪一种更高效?

在内置类型当中三种传递方式的效率上都差不多;
在自定义类型当中,传引用的更高效一些,因为它没有对形参进行一次拷贝

你可能感兴趣的:(面试题,qt,c++,开发语言)