微信搜索“编程笔记本”,获取更多信息
------------- codingbook2020 -------------
今天继续分享 CVTE 视源股份的 C++ 软件开发的二面面试题,题目依旧很多,哈撒给!
TCP 通过序列号、检验和、确认应答信号、重发控制、连接管理、窗口控制、流量控制、拥塞控制实现可靠性。
关于这些机制的具体描述参见往期笔记:TCP/IP 协议栈。
停止等待协议是最简单但也是最基础的数据链路层协议。具体来说就是,每发送完一个分组就停止发送,等待对方的确认,在收到确认消息后再发送下一个分组。
线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,其他线程处于等待状态。
同步方式
常见的线程同步主要有如下几种方式:
有关线程及线程间同步的更多介绍参见往期笔记:操作系统基础知识:程序、进程与线程。
线程的信号量与进程的信号量类似,但是使用线程的信号量可以高效地完成基于线程地资源计数。信号量实际上是一个非负的整数计数器,用来实现对公共资源的控制。在公共资源增加的时候,信号量的值增加;公共资源消耗的时候信号量减少;只有当信号量的值大于 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 竞争的结果。
有关线程及线程间同步的更多介绍参见往期笔记:操作系统基础知识:程序、进程与线程。
可以通过 raise()
和 signal()
生成和处理信号。
使用 raise()
函数生成信号
函数原型为:
int raise (signal sig);
其中,参数 sig 是要生成的信号,函数调用成功返回 0 ,失败返回非零。常用的信号如下所示:
使用 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
*/
关于内存泄漏相关内容参见往期笔记:使用 STL 智能指针就一定不会出现内存泄漏了吗?
这不算内存泄漏。内存泄漏的定义是程序员开辟了空间却忘记释放空间导致的,题目的情境下 new 的操作和 delete 的操作是成对出现的,并且在 delete 之前程序被杀死,那么 new 出来的空间也被系统释放了,所以不算内存泄漏。
当我们在某个目录使用 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--
。
第一部分:文件类型
第二部分:文件所有者的权限
第三部分:用户所在组的其他用户的权限
第四部分:其他组用户的权限
由不同的权限组成不同的数字,其中 755 就表示 rwxr-xr-x
,含义为:当前用户对该文件有读取、写入、执行的权限,该组其他用户对该文件有读取、执行的权限,组外用户对该文件有读取、执行的权限。
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 软件。”
系统调用
所谓系统调用是指用户在程序中调用操作系统所提供的一个子功能,也就是系统 API,系统调用可以被看做特殊的公共子程序。系统中的各种共享资源都由操作系统统一掌管,因此在用户程序中,凡是与资源有关的操作(如存储分配、进行 I/O 传输及管理文件等),都必须通过系统调用方式向操作系统提出服务请求,并由操作系统代为完成。通常,一个操作系统提供的系统调用命令有几十个乃至上百个之多。
函数调用
所谓函数调用就是对编写好的函数进行使用,完成函数语句的高级功能,函数调用又叫过程调用。
区别
程序中执行系统调用或过程(函数)调用,虽然都是对某种功能或服务的需求,但两者从调用形式到具体实现都有很大区别:
DLL 是动态链接库英文 (Dynamic Link Library) 的缩写。DLL 是一个包含可由多个程序同时使用的代码和数据的库。
通过使用 DLL 有如下优点:
什么情况下会发生 dll 缺失?
当我们使用一些工具的时候,可能会提示“缺失 xxx.dll 文件”,这是因为,我们所使用的工具会用到这个动态链接库,而我们并没有安装这样的库,所以就会缺失文件,这体现的是一种依赖关系。
举个不太恰当的例子:我们买来一辆小轿车,当我们上车时发现打不着火,中控提示缺少燃油。你就纳闷了,我用的是车,跟油有什么关系呢?原来,你用的是车不假,车要用油,你不给车提供油,它就无法为你提供服务。
现在我们在安装大部分工具时,它都会自动检测并安装依赖项。
动态链接使用动态链接库,动态链接允许可执行文件 (.dll 或 .exe ) 在运行时调用动态链接库中的某个函数。(程序运行阶段)
静态链接使用静态链接库,链接器从静态链接库获取所有被引用函数,并将这些函数加入到可执行文件中。(程序编译链接阶段)
解决内存泄漏的一个可行的办法是尽量使用智能指针去管理内存。
定位内存泄漏工具的可能实现原理:在进行内存申请时,将申请的地址假如到一个链表当中,释放时在链表中找到相应节点并删除。链表的信息就可以反馈出内存的问题。
C++ 的可执行文件后缀名是 .exe ,在 Linux 中,一个文件是否可执行,是看文件是否有可执行属性的,与后缀名无关。
如果我们知道具体是在哪一次循环中出错,那我们可以为循环变量设置条件断点;如果我们不知道具体循环到什么时候会出错,那我们可以用二分查找的思想,将循环分成两部分,并不断缩小查找区间,直至找到错误的循环。
结合实际情况作答,要体现出问题的价值,基础语法的问题不应该放到这里来说。最适合在这个问题中表达的有:针对目标应用,选用的编程语言缺少有效的特性支持,阐述自己如何解决的;结合项目背景,针对选用语言的语言性能进行评估,若性能不足,阐述改进方式。
常见的单例的实现形式有两种:懒汉模式与饿汉模式。
单例模式的懒汉实现:构造函数声明为私有的或保护的,防止被外部函数实例化,内部保存一个私有的静态类指针保存唯一的实例,实例的动作由一个公有的类方法代劳,该方法返回单例类唯一的实例。
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;
svn 是集中式版本控制系统
集中式版本控制系统,版本库是集中存放在中央服务器的,而大家工作的时候用的都是自己的电脑,所以要先从中央服务器取得最新的版本后再开始工作,工作完成后再把自己的修订推送给中央服务器。
git 是分布式版本控制系统
分布式版本控制系统根本没有“中央服务器”,每个人的电脑上都是一个完整的版本库。这样,在工作的时候就不需要联网了,因为版本库就在自己的电脑上。
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
map 的大部分操作都能在 O(logn) 时间内完成,unordered_map 可以在 O(1) 时间完成查找。