C++面试

目录

1.智能指针相互引用的冲突问题怎么解决

2.介绍unique_ptr

3.数据库三大范式

4.如何加快数据检索的效率

5.ET和LT的区别、IO多路复用

6.基类构造函数能调用子类构造函数吗

7.线程同步共享怎么实现


1.智能指针相互引用的冲突问题怎么解决

智能指针相互引用会导致循环引用,从而造成内存泄漏。为了解决这个问题,可以使用弱引用或者将其中一个指针设置为裸指针。

弱引用是一种不增加引用计数的智能指针。它只能访问对象,不能拥有对象。当对象被销毁时,弱引用自动失效。使用弱引用可以避免循环引用,因为它不会增加对象的引用计数。

将其中一个指针设置为裸指针也可以解决循环引用问题。我们可以使用 shared_ptr 来管理一个指针,然后将另一个指针设置为裸指针来引用这个对象。这种方法需要手动管理内存,需要注意生命周期的管理,避免引发内存泄漏。

总之,在使用智能指针相互引用时,需要注意循环引用的问题,并考虑使用弱引用或裸指针来解决这个问题。

2.介绍unique_ptr

unique_ptr是C++11标准引入的智能指针,其主要特点是拥有独有的所有权,即只有一个指针可以指向该对象。当unique_ptr指针被销毁时,它自动释放所指向的对象,无需手动释放。unique_ptr的主要优点有:

  1. 消除了内存泄漏的风险:unique_ptr使用RAII技术,会在超出作用域时自动释放资源。

  2. 避免了重复释放问题:unique_ptr的一个重要特点是独占所有权,因此它可以避免多个指针同时释放同一个对象的情况。

  3. 更好的性能:unique_ptr比较轻量级,因此在运行时开销相对较小。

  4. 支持自定义删除器:可以指定unique_ptr何时释放资源,可以使用自定义的删除器,对资源进行特定的释放操作。

  5. 避免复制和移动过程中资源的拷贝:unique_ptr禁止复制和移动操作,从而可以防止复制和移动过程中资源的拷贝,确保了没有资源被复制或移动。

3.数据库三大范式

数据库三大范式指的是第一范式、第二范式和第三范式,是规范化数据库设计的基本原则。

  1. 第一范式(1NF):要求表中的每个属性不可再分。也就是说每个属性都是原子性的。表中不存在重复的字段或数组字段。满足第一范式的表被称作"第一范式表"。第一范式是任何关系数据库设计的基础。

  2. 第二范式(2NF):要求表中的非主键属性完全依赖于主键属性。也就是说所有非主键属性只与主键有关,而与主键以外的任何字段都没有关系。满足第二范式的表被称作"第二范式表"。

  3. 第三范式(3NF):要求表中的非主键属性直接依赖于主键属性,而不是依赖于其他非主键属性。也就是说每个非主键属性都和主键以及与这个属性无关的其他属性没有关系。满足第三范式的表被称作"第三范式表"。

通过这三个范式的规范化设计,可以避免数据冗余、保证数据一致性和减少数据更新异常的机会,提高数据库的完整性和可靠性。

4.如何加快数据检索的效率

有几种方法可以加快数据检索的效率:

  1. 索引:在数据库中,创建索引是一种常用的优化技术,可以大大提高数据检索的速度。索引可以是单列索引或多列组合索引,具体应该根据表的结构和查询要求进行选择。

  2. 分区:如果一个表比较大,可以将其分成若干个小分区,这样就可以在查询时只搜索必要的分区,提高查询速度。

  3. 优化查询语句:写出高效的查询语句也是提高数据检索效率的重要因素。例如,可以使用JOIN语句代替嵌套查询,尽量避免使用SELECT *等不必要的语句。

  4. 数据库缓存:将频繁查询的数据缓存到内存中,可以显著提高查询速度。

  5. 硬件优化:可以通过升级硬件,如增加内存或使用更快的硬盘,来加快数据检索效率。

总的来说,要加快数据检索效率需要从多个方面入手,包括索引、分区、优化查询语句、数据库缓存和硬件优化等。

5.ET和LT的区别、IO多路复用

ET和LT是两种不同的IO触发模式。ET触发模式指的是只有数据从没有可读到可读状态时才会触发一次读事件,而LT触发模式,则是只要有数据可读就会触发读事件。

IO多路复用是一种在单线程或者少量线程下同时监听多个IO的机制。它允许一个进程同时等待多个IO操作,当某个IO操作准备就绪时,就会通知进程进行相关的处理。这样,可以在程序中实现非阻塞IO,充分发挥出系统资源的利用效率。

IO多路复用的优点在于在单线程下同时处理多个IO事件,而不必为每个IO操作分配一个单独的线程或进程,并且因为单线程处理IO事件的关系,避免了多线程同步的问题。此外,IO多路复用的实现可靠性高,且跨平台性较好。

总体而言,ET和LT是2种触发模式,而IO多路复用是一种通过单线程或少量线程同时监听多个IO,并在事件触发时进行响应的机制。

下面是一个简单的使用C++实现的ET和LT的代码,假设是通过Linux的epoll机制实现的:

#include 
#include 
#include 
#include 
#include 

using namespace std;

const int MAX_EVENTS = 20; // 最大的事件数量
const int BUF_SIZE = 1024; // 读取缓冲区大小

int set_non_block(int fd) // 设置非阻塞状态
{
    int flags = fcntl(fd, F_GETFL, 0);
    return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

void do_something(int fd, epoll_event &event, int epoll_fd) // 处理事件的函数
{
    char buf[BUF_SIZE];
    memset(buf, 0, BUF_SIZE);
    int nread = read(fd, buf, BUF_SIZE);

    if (nread == -1)
    {
        close(fd);
        event.data.fd = -1;
    }
    else if (nread == 0)
    {
        close(fd);
        event.data.fd = -1;
    }
    else
    {
        cout << "Read content: " << buf << endl;
        //...

        if (epoll_ctl(epoll_fd, EPOLL_CTL_MOD, fd, &event) < 0) // 更新事件
        {
            cerr << "Failed to modify epoll event." << endl;
        }
    }
}

int main()
{
    int epoll_fd = epoll_create1(0); // 创建epoll文件描述符

    if (epoll_fd == -1)
    {
        cerr << "Failed to create epoll file descriptor." << endl;
        return -1;
    }

    int server_fd = socket(AF_INET, SOCK_STREAM, 0); // 创建socket

    // 设置socket选项和绑定端口等

    if (set_non_block(server_fd) < 0)
    {
        cerr << "Failed to set server socket to non-block mode." << endl;
        return -1;
    }

    epoll_event event;
    event.data.fd = server_fd;
    event.events = EPOLLIN | EPOLLET; // 使用ET触发模式

    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) < 0) // 开始监听server_fd
    {
        cerr << "Failed to add server socket to epoll." << endl;
        return -1;
    }

    epoll_event events[MAX_EVENTS];

    while (true)
    {
        int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); // 等待有事件发生

        for (int i = 0; i < n; i++)
        {
            if (events[i].data.fd == server_fd) // 有新的连接
            {
                int client_fd = accept(server_fd, nullptr, nullptr);

                if (set_non_block(client_fd) < 0)
                {
                    cerr << "Failed to set client socket to non-block mode." << endl;
                    return -1;
                }

                event.data.fd = client_fd;
                event.events = EPOLLIN | EPOLLET; // 使用ET触发模式

                if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) < 0) // 开始监听client_fd
                {
                    cerr << "Failed to add client socket to epoll." << endl;
                    return -1;
                }
            }
            else // 已连接的socket有读事件
            {
                if (events[i].events & EPOLLIN) // 有数据可读
                {
                    bool done = false;

                    while (true) // 读取所有可读的数据
                    {
                        char buf[BUF_SIZE];
                        memset(buf, 0, BUF_SIZE);
                        int nread = read(events[i].data.fd, buf, BUF_SIZE);

                        if (nread < 0) // 读取失败
                        {
                            if (errno == EAGAIN || errno == EWOULDBLOCK) // 非阻塞状态下读取完毕
                            {
                                done = true;
                                break;
                            }

                            cerr << "Failed to read data from client." << endl;
                            done = true;
                        }
                        else if (nread == 0) // 读取结束
                        {
                            done = true;
                            break;
                        }
                        else // 读取成功
                        {
                            cout << "Read content: " << buf << endl;
                            //...
                        }
                    }

                    if (done) // 读取完毕,更新事件
                    {
                        event = events[i];
                        event.events = EPOLLOUT | EPOLLET; // 修改为写事件

                        if (epoll_ctl(epoll_fd, EPOLL_CTL_MOD, events[i].data.fd, &event) < 0)
                        {
                            cerr << "Failed to modify epoll event." << endl;
                        }
                    }
                }
                else if (events[i].events & EPOLLOUT) // 可以写数据
                {
                    do_something(events[i].data.fd, events[i], epoll_fd);
                }
            }
        }
    }

    close(server_fd);
    close(epoll_fd);

    return 0;
}

需要注意的是,ET和LT的实现需要调整epoll_event的events字段。使用EPOLLET表示ET触发模式,使用EPOLLIN表示对可读事件进行监听。相应地,使用EPOLLOUT表示对可写事件进行监听。

6.基类构造函数能调用子类构造函数吗

基类构造函数无法直接调用子类构造函数,因为在调用子类构造函数之前,子类对象还没有被创建。子类构造函数仅能被子类对象调用。

在C++中,如果需要在基类构造函数中初始化子类的成员变量,可以在基类中定义一个虚函数,在子类中重写该虚函数,在构造函数中调用该虚函数来进行子类成员变量的初始化。例如:

class Base {
public:
    Base() {
        //调用子类的虚函数
        init();
    }
    virtual void init() {
        //基类的实现
    }
};

class Subclass : public Base {
public:
    Subclass() : Base() {
        //子类的构造函数
    }
    virtual void init() override {
        //子类的实现
    }
};

在子类调用基类构造函数时,会先调用基类构造函数中的虚函数,然后再调用子类的构造函数。这样就保证了子类成员变量的初始化顺序。

7.线程同步共享怎么实现

在C++中,实现线程同步共享的经典方法是使用互斥锁(mutex)和条件变量(condition variable)。

  1. 互斥锁:可以确保在同一时刻只有一个线程能够访问共享资源。在C++11中,可以使用 std::mutex 类实现互斥锁。其用法如下:

    #include 
    std::mutex myMutex;
    myMutex.lock();    // 加锁
    // 访问共享资源
    myMutex.unlock();  // 解锁
    
  2. 条件变量:可以让线程在某个条件满足时等待,直到条件满足时再继续执行。在C++11中,可以使用 std::condition_variable 类实现条件变量。其用法如下:

    #include 
    std::condition_variable myCV;
    std::unique_lock myLock(myMutex);
    myCV.wait(myLock); // 等待条件变量满足
    // 条件变量满足后继续执行
    

    在调用wait()方法前需要先获得互斥锁myMutex,即先调用std::unique_lock myLock(myMutex),然后调用myCV.wait(myLock)实现等待。如果其它线程调用了该条件变量的notify_one()或者notify_all()方法,那么该线程便会被唤醒,继续执行。

通过互斥锁和条件变量的组合可以实现多线程同步共享。例如,在多个线程访问一个输出队列的情况下,可以用互斥锁保证同一时刻只有一个线程访问队列,用条件变量实现线程等待。例如:

#include 
#include 
std::mutex myMutex;
std::queue myQueue;
std::condition_variable myCV;

// 生产者线程创建数据并且添加到myQueue中
void producer() {
    int data = 0;
    while (true) {
        // 创建一些数据
        data = someData();

        std::lock_guard myLock(myMutex);
        myQueue.push(data);
        myCV.notify_one();
    }
}

// 消费者线程在队列中取数据并进行处理
void consumer() {
    while (true) {
        std::unique_lock myLock(myMutex);
        myCV.wait(myLock, [] {return !myQueue.empty(); });

        int data = myQueue.front();
        myQueue.pop();
        myLock.unlock();

        processData(data); // 处理数据
    }
}

其中,在生产者线程中,通过std::lock_guard myLock(myMutex)加锁,然后将数据添加到队列,最后通过myCV.notify_one()唤醒任意一个等待线程。在消费者线程中,通过myCV.wait(myLock, [] {return !myQueue.empty(); })进入等待状态等待数据,并通过myLock.unlock()解锁。如果数据已经添加到队列中,myCV.wait()方法将返回,并执行processData(data)进行处理。

你可能感兴趣的:(面经,c++,面试,开发语言,数据库,leetcode)