【面试】 CVTE 视源股份 C++ 软件开发 二面

微信搜索“编程笔记本”,获取更多信息
------------- codingbook2020 -------------

今天继续分享 CVTE 视源股份C++ 软件开发二面面试题,题目依旧很多,哈撒给!

面试题

文章目录

      • 面试题
      • 2.1 TCP 怎么保证可靠传输?
      • 2.2 TCP 的停止等待协议是什么?
      • 2.3 线程同步的方式?
      • 2.4 线程之间怎么传递信号?
      • 2.5 C++ 怎么传递信号?
      • 2.6 什么是内存泄漏?
      • 2.7 如果我 new 了一块内存,然后在 delete 之前这个进程被系统杀死了。这样算内存泄漏吗?
      • 2.8 Linux 755 指的是什么权限?
      • 2.9 wine 使 windows 程序能够在 Linux 下使用,你觉得它是怎么实现的?
      • 2.10 系统调用和函数调用的区别是什么?
      • 2.11 dll 是什么文件?什么情况下会发生 dll 缺失?
      • 2.12 静态库和动态库的区别是什么?
      • 2.13 怎么解决内存泄漏问题?你觉得哪些定位内存泄漏的开源工具是怎么实现的?
      • 2.14 C++ 下的可执行文件和 Linux 下的可执行文件分别是什么后缀名?
      • 2.15 windows 下 C++ 文件有哪些后缀,分别是什么用途?
      • 2.16 如果在调试过程中,出现了需要循环很多次才出现的错误应该怎么调试?
      • 2.17 最近在编程上遇到什么问题?
      • 2.18 写出单例模式线程安全的代码?若多个线程同时判断到当前对象未创建,应该怎么解决?C++11 中可以用什么特性替换单例模式中的 static 写法?
      • 2.19 git 和 svn 的区别是什么?
      • 2.20 讲一下 STL 中的 map ?map 和 unordered map 的区别是什么?

2.1 TCP 怎么保证可靠传输?

TCP 通过序列号检验和确认应答信号重发控制连接管理窗口控制流量控制拥塞控制实现可靠性。

关于这些机制的具体描述参见往期笔记:TCP/IP 协议栈。

2.2 TCP 的停止等待协议是什么?

停止等待协议是最简单但也是最基础的数据链路层协议。具体来说就是,每发送完一个分组就停止发送,等待对方的确认,在收到确认消息后再发送下一个分组。

2.3 线程同步的方式?

线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,其他线程处于等待状态。

同步方式
常见的线程同步主要有如下几种方式:

  • 临界区
    临界区的作用:线程在执行代码时将代码锁定,不允许其他线程执行,只有该线程离开后,其他线程才能使用这些代码。常用的操作有:初始化临界区临界区加锁临界区解锁释放临界区
  • 信号量
    一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。
  • 事件
    事件对象也可以通过通知操作的方式来保持线程的同步。常用的操作有:创建一个事件对象打开一个事件设置事件复位事件等待一个事件等待多个事件。使用临界区只能同步同一进程中的线程,而使用事件内核对象则可以对进程外的线程进行同步,其前提是得到对此事件对象的访问权。
  • 互斥量
    一个线程占用了某一个资源,就对该资源加锁,别的线程无法访问,直到这个线程解锁,其他的线程才开始可以利用这个资源。

有关线程及线程间同步的更多介绍参见往期笔记:操作系统基础知识:程序、进程与线程。

2.4 线程之间怎么传递信号?

线程的信号量与进程的信号量类似,但是使用线程的信号量可以高效地完成基于线程地资源计数。信号量实际上是一个非负的整数计数器,用来实现对公共资源的控制。在公共资源增加的时候,信号量的值增加;公共资源消耗的时候信号量减少;只有当信号量的值大于 0 时,才能访问信号量代表的公共资源。

常用的操作有:

  • 线程信号量初始化函数 sem_init()
  • 线程信号量增加函数 sem_post()
  • 线程信号量等待函数 sem_wait()
  • 线程信号量销毁函数 sem_destroy()

下面我们来看一个线程间使用信号量的例子:使用一个全局变量来计数,一个线程通过增加信号量来模拟生产者,另一个线程通过获取信号量类模拟消费者。

#include 
#include 
#include 
#include 

// 定义信号量
sem_t sem;

int running = 1;

// 生产者线程函数
void producer () {
    int semVal = 0;

    while (running) {
        usleep(100000);

        // 信号量增加并获取信号量的值
        sem_post(&sem);
        sem_getvalue(&sem, &semVal);

        printf("生产一个,剩余总数:%d\n", semVal);
    }
}

// 消费者线程函数
void consumer() {
    int semVal = 0;

    while (running) {
        usleep(100000);

        // 等待信号量并获取信号量的值
        sem_wait(&sem);
        sem_getvalue(&sem, &semVal);

        printf("消费一个,剩余总数:%d\n", semVal);
    }
}

int main()
{
    pthread_t producer_t, consumer_t;

    // 信号量只在统一进程的多个线程间共享,并且信号量的初值为16
    sem_init(&sem, 0, 16);

    // 创建线程
    pthread_create(&producer_t, NULL, (void*)producer, NULL);
    pthread_create(&consumer_t, NULL, (void*)consumer, NULL);

    // 运行时间
    sleep(1);
    // 设置线程退出
    running = 0;

    // 等待线程退出
    pthread_join(producer_t, NULL);
    pthread_join(consumer_t, NULL);

    // 销毁信号量
    sem_destroy(&sem);

    return 0;
}

/*
运行结果:
jincheng@jincheng-PC:~/Desktop$ gcc -lpthread -o test test.c
jincheng@jincheng-PC:~/Desktop$ ./test
消费一个,剩余总数:15
生产一个,剩余总数:16
生产一个,剩余总数:17
消费一个,剩余总数:16
生产一个,剩余总数:17
消费一个,剩余总数:16
生产一个,剩余总数:17
消费一个,剩余总数:16
生产一个,剩余总数:17
消费一个,剩余总数:16
消费一个,剩余总数:15
生产一个,剩余总数:16
生产一个,剩余总数:17
消费一个,剩余总数:16
消费一个,剩余总数:15
生产一个,剩余总数:16
生产一个,剩余总数:17
消费一个,剩余总数:16
消费一个,剩余总数:15
生产一个,剩余总数:16
*/

从执行的结果可以看出,上述示例建立的各个线程间存在竞争的关系。数值并未按照生产一个消费一个的顺序进行,有时候生产多个消费多个,造成这种现象的原因是信号量的产生和消耗是线程对 CPU 竞争的结果。

有关线程及线程间同步的更多介绍参见往期笔记:操作系统基础知识:程序、进程与线程。

2.5 C++ 怎么传递信号?

可以通过 raise()signal()生成和处理信号。

使用 raise()函数生成信号

函数原型为:

int raise (signal sig);

其中,参数 sig 是要生成的信号,函数调用成功返回 0 ,失败返回非零。常用的信号如下所示:

  • SIGABRT:(Signal Abort) 程序异常终止
  • SIGFPE:(Signal Floating-Point Exception) 算术运算出错,如除数为 0 或溢出(不一定是浮点运算)
  • SIGILL:(Signal Illegal Instruction) 非法函数映象,如非法指令,通常是由于代码中的某个变体或者尝试执行数据导致的
  • SIGINT:(Signal Interrupt) 中断信号,如 ctrl-C,通常由用户生成
  • SIGSEGV:(Signal Segmentation Violation) 非法访问存储器,如访问不存在的内存单元。
  • SIGTERM:(Signal Terminate) 发送给本程序的终止请求信号

使用 signal()注册信号

函数原型:

void (*signal (int sig, void (*func)(int)))(int);

其中,参数 sig 是信号编号,func 是信号处理函数的指针。

下面是一个简单的示例:

#include 
#include 
#include 

using namespace std;

// 信号处理函数
void handler(int sig) {
    cout << "Capture a signal of " << sig << " and exit" << endl;
    exit(sig);
}

int main() {
    cout << "main start" << endl;
    signal(SIGINT, handler);    // 注册信号

    raise(SIGINT);              // 生成信号

    cout << "main over" << endl;

    return 0;
}

/*
运行结果:
jincheng@jincheng-PC:~/Desktop$ g++ test.cpp -o test
jincheng@jincheng-PC:~/Desktop$ ./test
main start
Capture a signal of 2 and exit
*/

2.6 什么是内存泄漏?

关于内存泄漏相关内容参见往期笔记:使用 STL 智能指针就一定不会出现内存泄漏了吗?

2.7 如果我 new 了一块内存,然后在 delete 之前这个进程被系统杀死了。这样算内存泄漏吗?

这不算内存泄漏。内存泄漏的定义是程序员开辟了空间却忘记释放空间导致的,题目的情境下 new 的操作和 delete 的操作是成对出现的,并且在 delete 之前程序被杀死,那么 new 出来的空间也被系统释放了,所以不算内存泄漏。

2.8 Linux 755 指的是什么权限?

当我们在某个目录使用 ls -l指令查看文件时:会显示如下信息:

 jincheng@jincheng-PC:~/Desktop/newfolder$ ls -l
 总用量 28
 -rw-r--r-- 1 jincheng jincheng 1252  2月   6 15:15 ParallelAccelerator.jl
 -rw-r--r-- 1 jincheng jincheng 1208  2月   6 15:15 PlainJulia.jl
 -rwxr-xr-x 1 jincheng jincheng 13608 3月  21 15:11 test
 -rw-r--r-- 1 jincheng jincheng   80  2月   6 10:09 test.jl

这些信息中包含了文件的权限信息所有者以及所在组,还有该文件的大小,该文件最后修改的日期时间文件名称等信息。

现在我们来详细谈一谈权限信息,也即显式的 -rw-r--r--部分。这一部分又分为 4 个字部分 -rw-r--r--

第一部分:文件类型

  • -:普通文件
  • d:目录
  • l:软连接
  • c:字符设备
  • b:块文件

第二部分:文件所有者的权限

  • r:读取(第一位,表示 4 )
  • w:写入(第二位,表示 2 )
  • x:执行(第三位,表示 1 )

第三部分:用户所在组的其他用户的权限

第四部分:其他组用户的权限

由不同的权限组成不同的数字,其中 755 就表示 rwxr-xr-x,含义为:当前用户对该文件有读取、写入、执行的权限,该组其他用户对该文件有读取、执行的权限,组外用户对该文件有读取、执行的权限。

2.9 wine 使 windows 程序能够在 Linux 下使用,你觉得它是怎么实现的?

wine 是一个开放源代码项目,它尝试去解决在 Linux 上运行 Windows 可执行文件的复杂问题。使用过 Linux 操作系统的都会有一种感慨,那就是应用程序太少了,尤其是 QQ 。但是有了 wine 以后,我们就可以在 Linux 上运行 windows 应用程序了。那它是怎么做到的呢?

据 WineHQ 的说法,“WINE 代表 Wine Is Not an Emulator(即,wine 不是一个仿真器)。更确切地说,wine 是 X 和 UNIX 之上对 windows API 的一个开放源代码实现。您可以认为它是一个 window 兼容层。wine 不需要 Microsoft Windows,因为它是由 100% 非 Microsoft 代码构成的另一个实现。但是它可以使用本机系统 DLL,只要这些 DLL 可用。而且它可以让您在 Linux 或者其他类 UNIX 操作系统之上运行大部分 windows 软件。”

2.10 系统调用和函数调用的区别是什么?

系统调用
所谓系统调用是指用户在程序中调用操作系统所提供的一个子功能,也就是系统 API,系统调用可以被看做特殊的公共子程序。系统中的各种共享资源都由操作系统统一掌管,因此在用户程序中,凡是与资源有关的操作(如存储分配、进行 I/O 传输及管理文件等),都必须通过系统调用方式向操作系统提出服务请求,并由操作系统代为完成。通常,一个操作系统提供的系统调用命令有几十个乃至上百个之多。

函数调用
所谓函数调用就是对编写好的函数进行使用,完成函数语句的高级功能,函数调用又叫过程调用。

区别
程序中执行系统调用或过程(函数)调用,虽然都是对某种功能或服务的需求,但两者从调用形式到具体实现都有很大区别:

  • 调用形式不同
    过程(函数)使用一般调用指令,其转向地址是固定不变的,包含在跳转语句中;但系统调用中不包含处理程序入口,而仅仅提供功能号,按功能号调用。
  • 被调用代码的位置不同
    过程(函数)调用是一种静态调用,调用者和被调用代码在同一程序内,经过连接编辑后作为目标代码的一部份。当过程(函数)升级或修改时,必须重新编译连结。而系统调用是一种动态调用,系统调用的处理代码在调用程序之外(在操作系统中),这样一来,系统调用处理代码升级或修改时,与调用程序无关。而且,调用程序的长度也大大缩短,减少了调用程序占用的存储空间。
  • 提供方式不同
    过程(函数)往往由编译系统提供,不同编译系统提供的过程(函数)可以不同;系统调用由操作系统提供,一旦操作系统设计好,系统调用的功能、种类与数量便固定不变了。
  • 调用的实现不同
    程序使用一般机器指令(跳转指令)来调用过程(函数),是在用户态运行的;程序执行系统调用,是通过中断机构来实现,需要从用户态转变到核心态,在管理状态执行,因此,安全性好。

2.11 dll 是什么文件?什么情况下会发生 dll 缺失?

DLL 是动态链接库英文 (Dynamic Link Library) 的缩写。DLL 是一个包含可由多个程序同时使用的代码和数据的

通过使用 DLL 有如下优点:

  • 程序可以实现模块化,由相对独立的组件组成。
  • 使用较少的资源,当多个程序使用同一个函数库时,DLL 可以减少在磁盘和物理内存中加载的代码的重复量。
  • 简化部署和安装,当 DLL 中的函数需要更新或修复时,部署和安装 DLL 不要求重新建立程序与该 DLL 的链接。

什么情况下会发生 dll 缺失?

当我们使用一些工具的时候,可能会提示“缺失 xxx.dll 文件”,这是因为,我们所使用的工具会用到这个动态链接库,而我们并没有安装这样的库,所以就会缺失文件,这体现的是一种依赖关系。

举个不太恰当的例子:我们买来一辆小轿车,当我们上车时发现打不着火,中控提示缺少燃油。你就纳闷了,我用的是车,跟油有什么关系呢?原来,你用的是车不假,车要用油,你不给车提供油,它就无法为你提供服务。

现在我们在安装大部分工具时,它都会自动检测并安装依赖项。

2.12 静态库和动态库的区别是什么?

动态链接使用动态链接库,动态链接允许可执行文件 (.dll 或 .exe ) 在运行时调用动态链接库中的某个函数。(程序运行阶段
静态链接使用静态链接库,链接器从静态链接库获取所有被引用函数,并将这些函数加入到可执行文件中。(程序编译链接阶段

2.13 怎么解决内存泄漏问题?你觉得哪些定位内存泄漏的开源工具是怎么实现的?

解决内存泄漏的一个可行的办法是尽量使用智能指针去管理内存。
定位内存泄漏工具的可能实现原理:在进行内存申请时,将申请的地址假如到一个链表当中,释放时在链表中找到相应节点并删除。链表的信息就可以反馈出内存的问题。

2.14 C++ 下的可执行文件和 Linux 下的可执行文件分别是什么后缀名?

C++ 的可执行文件后缀名是 .exe ,在 Linux 中,一个文件是否可执行,是看文件是否有可执行属性的,与后缀名无关。

2.15 windows 下 C++ 文件有哪些后缀,分别是什么用途?

  • **.cpp :**源代码文件
  • **.h :**头文件
  • **.a:**由目标文件构成的档案库文件
  • **.ii:**预处理后的源代码文件
  • **.o:**编译后的目标文件
  • **.s:**汇编语言源代码文件

2.16 如果在调试过程中,出现了需要循环很多次才出现的错误应该怎么调试?

如果我们知道具体是在哪一次循环中出错,那我们可以为循环变量设置条件断点;如果我们不知道具体循环到什么时候会出错,那我们可以用二分查找的思想,将循环分成两部分,并不断缩小查找区间,直至找到错误的循环。

2.17 最近在编程上遇到什么问题?

结合实际情况作答,要体现出问题的价值,基础语法的问题不应该放到这里来说。最适合在这个问题中表达的有:针对目标应用,选用的编程语言缺少有效的特性支持,阐述自己如何解决的;结合项目背景,针对选用语言的语言性能进行评估,若性能不足,阐述改进方式。

2.18 写出单例模式线程安全的代码?若多个线程同时判断到当前对象未创建,应该怎么解决?C++11 中可以用什么特性替换单例模式中的 static 写法?

常见的单例的实现形式有两种:懒汉模式饿汉模式

  • 懒汉模式:故名思义,不到万不得已就不会去实例化类,也就是说在第一次用到类实例的时候才会去实例化
  • 饿汉模式:饿了肯定要饥不择食,所以在单例类定义的时候就进行实例化

单例模式的懒汉实现:构造函数声明为私有的或保护的,防止被外部函数实例化,内部保存一个私有的静态类指针保存唯一的实例,实例的动作由一个公有的类方法代劳,该方法返回单例类唯一的实例。

class singleton
{
protected:
    singleton(){}
private:
    static singleton* p;
public:
    static singleton* instance() {
        if (p == nullptr) {
            p = new singleton();
        }
        return p;
    }
};

singleton* singleton::p = nullptr;

上面这种简单的方式就是懒汉模式的单例模式,但是这种方法是线程不安全的,考虑两个线程同时首次调用 instance 方法且同时检测到 p 是 nullptr 值,则两个线程会同时构造一个实例给 p ,这是严重错误的。

单纯的懒汉模式是线程不安全,一个简单的方法是:加锁

class singleton
{
protected:
    singleton()
    {
        pthread_mutex_init(&mutex);          // 互斥量初始化
    }
private:
    static singleton* p;
public:
    static pthread_mutex_t mutex;            // 互斥量,即锁
    static singleton* initance() {
        if (p == nullptr) {
            pthread_mutex_lock(&mutex);      // 上锁
            if (p == nullptr) {
                p = new singleton();
            }
            pthread_mutex_unlock(&mutex);    // 解锁
        }
        return p;
    }
};

singleton* singleton::p = nullptr;

另一种实现方式使用内部静态变量:该方法很容易实现,在 instance 函数里定义一个静态的实例,也可以保证拥有唯一实例,在返回时只需要返回其指针就可以了。

class singleton
{
protected:
    singleton()
    {
        pthread_mutex_init(&mutex);
    }
public:
    static pthread_mutex_t mutex;

    static singleton* initance() {
        pthread_mutex_lock(&mutex);
        static singleton obj;
        pthread_mutex_unlock(&mutex);
        return &obj;
    }
};

单例模式的饿汉实现:简单的懒汉模式是线程不安全的,因此需要加锁。但饿汉模式本来就是线程安全的,所以不用加锁。原因很简单,因为的类实例创建(对象创建)放在类外了,因此只有唯一的一个实例。

class singleton
{
protected:
    singleton() {}
private:
    static singleton* p;
public:
    static singleton* initance() {
        return p;
    }
};

singleton* singleton::p = new singleton;

2.19 git 和 svn 的区别是什么?

svn 是集中式版本控制系统

集中式版本控制系统,版本库是集中存放在中央服务器的,而大家工作的时候用的都是自己的电脑,所以要先从中央服务器取得最新的版本后再开始工作,工作完成后再把自己的修订推送给中央服务器。

git 是分布式版本控制系统

分布式版本控制系统根本没有“中央服务器”,每个人的电脑上都是一个完整的版本库。这样,在工作的时候就不需要联网了,因为版本库就在自己的电脑上。

2.20 讲一下 STL 中的 map ?map 和 unordered map 的区别是什么?

map 是从键(key)到值(value)的映射,其内部实现是一棵以 key 为关键码的红黑树

基本用法为:

map mapName;
mapName[key] = value;

常用成员函数:

// 返回 map 中的键值对数量
mapName.size();

// 检查 map 是否为空
mapName.empty();

// 清空 map
mapName.clean();

// 返回 map 中值位 x 的元素数量
mapName.count(x);

// 插入操作,参数是 pair ,返回插入地址的迭代器和是否插入成功的 bool 并成的 pair
map.insert(pair<..., ...>)

// 删除操作,参数可以是 pair 或者迭代器,返回下一个元素的迭代器
map.erase(param)

map:(#include) map 内部是一颗红黑树(非严格平衡二叉树),红黑树有自动排序的功能,所以 map 内部的所有元素都是有序的,红黑树的每一个节点都代表这 map 的一个元素。因此对 map 进行插入、删除、查找等操作都是相当于对红黑树进行操作。最后根据树的中序遍历可以将键值按照大小顺序遍历出来。

unordered_map:(#include) unordered_map 内部是一个哈希表,元素的排列顺序是无序的。

map 的大部分操作都能在 O(logn) 时间内完成,unordered_map 可以在 O(1) 时间完成查找。

点击下方图片关注我,或微信搜索**“编程笔记本”**,获取更多信息。
【面试】 CVTE 视源股份 C++ 软件开发 二面_第1张图片

你可能感兴趣的:(【面试】 CVTE 视源股份 C++ 软件开发 二面)