1.虚拟内存的作用?
虚拟内存提供了三个重要的能力: 缓存,内存管理,内存保护
-
将主存视为一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据
-
为每个进程提供了一致的地址空间,简化内存管理
-
保护了每个进程的地址空间不被其他进程破坏
https://www.jianshu.com/p/baf3a13c47db
2.锁有哪些?锁的实现原理?
C++11线程中的几种锁:互斥锁、条件锁、自旋锁、读写锁、递归锁。
互斥锁
互斥锁用于控制多个线程对他们之间共享资源互斥访问的一个信号量。也就是说是为了避免多个线程在某一时刻同时操作一个共享资源。例如线程池中的有多个空闲线程和一个任务队列。任何是一个线程都要使用互斥锁互斥访问任务队列,以避免多个线程同时访问任务队列以发生错乱。
在某一时刻,只有一个线程可以获取互斥锁,在释放互斥锁之前其他线程都不能获取该互斥锁。如果其他线程想要获取这个互斥锁,那么这个线程只能以阻塞方式进行等待。
条件锁
条件锁就是所谓的条件变量,某一个线程因为某个条件为满足时可以使用条件变量使改程序处于阻塞状态。一旦条件满足以“信号量”的方式唤醒一个因为该条件而被阻塞的线程。最为常见就是在线程池中,起初没有任务时任务队列为空,此时线程池中的线程因为“任务队列为空”这个条件处于阻塞状态。一旦有任务进来,就会以信号量的方式唤醒一个线程来处理这个任务。
自旋锁
自旋锁是一种busy-waiting的锁。也就是说,如果T1正在使用自旋锁,而T2也去申请这个自旋锁,此时T2肯定得不到这个自旋锁。与互斥锁相反的是,此时运行T2的处理器core2会一直不断地循环检查锁是否可用(自旋锁请求),直到获取到这个自旋锁为止。
从“自旋锁”的名字也可以看出来,如果一个线程想要获取一个被使用的自旋锁,那么它会一致占用CPU请求这个自旋锁使得CPU不能去做其他的事情,直到获取这个锁为止,这就是“自旋”的含义。
当发生阻塞时,互斥锁可以让CPU去处理其他的任务;而自旋锁让CPU一直不断循环请求获取这个锁。
读写锁
说到读写锁我们可以借助于“读者-写者”问题进行理解。首先我们简单说下“读者-写者”问题。
计算机中某些数据被多个进程共享,对数据库的操作有两种:一种是读操作,就是从数据库中读取数据不会修改数据库中内容;另一种就是写操作,写操作会修改数据库中存放的数据。因此可以得到我们允许在数据库上同时执行多个“读”操作,但是某一时刻只能在数据库上有一个“写”操作来更新数据。这就是一个简单的读者-写者模型。
3.同步除了锁,还有哪些技术?
进程间通信主要包括管道, 系统IPC(包括消息队列,信号量,共享存储), SOCKET.
管道包括三种:
- 普通管道PIPE, 通常有种限制,一是半双工,只能单向传输;二是只能在父子进程间使用.
- 流管道s_pipe: 去除了第一种限制,可以双向传输.
- 命名管道:name_pipe, 去除了第二种限制,可以在许多并不相关的进程之间进行通讯.
系统IPC的三种方式相近,都是使用了内核里的标识符来识别.
-
管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
-
有名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
-
信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
-
消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
-
信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
-
共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
-
套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
4.C++宏运算?
1.1 #define的概念
#define命令是C语言中的一个宏定义命令,它用来将一个标识符定义为一个字符串,该标识符被称为宏名,被定义的字符串称为替换文本。该命令有两种格式:一种是简单的宏定义,另一种是带参数的宏定义。
(1)简单的宏定义:
#define <宏名> <字符串>
例: #define PI 3.1415926
(2) 带参数的宏定义
#define <宏名> (<参数表>) <宏体>
例: #define A(x) x
一个标识符被宏定义后,该标识符便是一个宏名。这时,在程序中出现的是宏名,在该程序被编译前,先将宏名用被定义的字符串替换,这称为宏替换,替换后才进行编译,宏替换是简单的替换。
1.2 宏替换发生的时机
为了能够真正理解#define的作用,让我们来了解一下对C语言源程序的处理过程。当我们在一个集成的开发环境如Turbo C中将编写好的源程序进行编译时,实际经过了预处理、编译、汇编和连接几个过程。其中预处理器产生编译器的输出,它实现以下的功能:
(1)文件包含
可以把源程序中的#include 扩展为文件正文,即把包含的.h文件找到并展开到#include 所在处。
(2)条件编译
预处理器根据#if和#ifdef等编译命令及其后的条件,将源程序中的某部分包含进来或排除在外,通常把排除在外的语句转换成空行。
(3)宏展开
预处理器将源程序文件中出现的对宏的引用展开成相应的宏定义,即本文所说的#define的功能,由预处理器来完成。
经过预处理器处理的源程序与之前的源程序有所有不同,在这个阶段所进行的工作只是纯粹的替换与展开,没有任何计算功能,所以在学习#define命令时只要能真正理解这一点,这样才不会对此命令引起误解并误用。
5.C++函数指针?定义一个指向函数指针数组的的指针
举例:定义函数指针DetectCallback,返回值为int,输入参数为(char*, char*)
typedef int (*DetectCallback)(
char* json_data, char* request_id
);
函数指针的定义
格式:返回类型 (*函数指针名)(输入参数)
注1:将*和函数名的连起的括号不可少,否则就会成为指向函数的指针(即返回一个指针类型的函数)
注2:函数类型必须匹配要调用的函数类型,函数形参表要匹配调用的函数形参表。如:
int max(int a,int b);
int min(int a,int b);
int (*fun)(int a,int b);
函数指针的典型用途——实现函数回调
函数指针数组的定义
const double * f1(const double ar[], int n);
const double * f2(const double [], int);
const double * f3(const double *, int);
按指针函数定义,如果定义一个指针表示函数f1,则为:
const double * (*p1)(const double *, int);
指向f1,*p1等同于f1:
const double * (*p1)(const double *, int) = f1;
p1虽然是f1的地址,但是在函数调用的时候,两者等同,所以输出与f1相同,要想输出函数f1()的地址,需要&p1。
函数指针数组
const double * (*pa[3])(const double *, int) = {f1, f2, f3};
首先看优先级,[]最高,所以可以变为:
const double * (* (pa[3]))(const double *, int) = {f1, f2, f3};
这样子就好看了,类似于数组的定义,当pa[0]=f1, pa[2]=f2, pa[3]=f3的时候,定义的时候就是double pa[3]={f1, f2, f3}. 再把前面那些修饰符加上,就是函数指针数组的定义加初始化。再解释就方便了,pa是个数组,数组有三个元素,三个元素分别指向f1, f2, f3,拆开看跟之前的单个定义一样(比如pa[0]等价于p1)。
http://www.classnotes.cn/2095.html
6.C++程序编译过程?符号名在哪个过程处理
代码–>可执行程序的过程:预处理->编译->汇编->链接
7. 32位系统和64位系统下,char、short、int、long、long long、指针各占据多少字节?
测试代码:
#include <iostream>
using namespace std;
int main(){
int c = sizeof(char);
int s = sizeof(short);
int si = sizeof(short int);
int i = sizeof(int);
int li = sizeof(long int);
int l = sizeof(long);
int ll = sizeof(long long);
int f = sizeof(float);
int d = sizeof(double);
int p = sizeof(char*);
cout << "char = " << c << endl <<
"short=" << s << endl <<
"short int = " << si << endl <<
"int = " << i << endl <<
"long int = " << li << endl <<
"long = " << l << endl <<
"long long = " << ll << endl <<
"float = " << f << endl <<
"double = " << d << endl <<
"address = " << p << endl;
return 0;
}
32位系统:
64位系统:
区别:
- char、short、long、int、long long、float、double基本数据类型的字节长度不变,分别是1、2、4、4、8、4、8字节
- 存在不同的只有地址长度,32位系统下为4字节,64位系统下是8字节
8.算法题一:找出两个字符串中的最长公共子序列
思路——动态规划
-
确定dp数组(dp table)以及下标的含义
dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j] -
确定递推公式
主要就是两大情况: text1[i - 1] 与 text2[j - 1]相同,text1[i - 1] 与 text2[j - 1]不相同:
- 如果text1[i - 1] 与 text2[j - 1]相同,那么找到了一个公共元素,所以dp[i][j] = dp[i - 1][j - 1] + 1;
- 如果text1[i - 1] 与 text2[j - 1]不相同,那就看看text1[0, i - 2]与text2[0, j - 1]的最长公共子序列 和 text1[0, i - 1]与text2[0, j - 2]的最长公共子序列,取最大的。即:dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
vector<vector<int>> dp(text1.size() + 1, vector<int>(text2.size() + 1, 0));
for (int i = 1; i <= text1.size(); i++) {
for (int j = 1; j <= text2.size(); j++) {
if (text1[i - 1] == text2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[text1.size()][text2.size()];
}
};
9.算法题二:合并K个有序链表
class Solution {
public:
// 合并两个有序链表
ListNode* merge(ListNode* p1, ListNode* p2){
if(!p1) return p2;
if(!p2) return p1;
if(p1->val <= p2->val){
p1->next = merge(p1->next, p2);
return p1;
}else{
p2->next = merge(p1, p2->next);
return p2;
}
}
ListNode* mergeKLists(vector<ListNode*>& lists) {
if(lists.size() == 0) return nullptr;
ListNode* head = lists[0];
for(int i = 1; i<lists.size(); ++i){
if(lists[i]) head = merge(head, lists[i]); //进行两两归并
}
return head;
}
};