面题汇总

手撕常见类或函数

strcpy()

VS2015环境下,显示strcpy()函数出错的解决办法: This function or variable may be unsafe. Consider using strcpy_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details. 即使前面加了头函数文件cstring 也无济于事。

原因:vs准备弃用strcpy的,安全性较低,所以微软提供了strcpy_s来代替.如果继续使用strcpy的,那么只需要在函数体前面加上#pragma warning(disable:4996)就可以了。

strcpy_s(dest, bufsize, src);
bufsize为dest缓冲区最大长度。它与 strcpy 的不同在于,在它取得额外参数来决定目的缓冲区大小时,会因为发生溢位而出现错误,如此一来,就可以预防缓冲区溢位。

写出线程安全的strcpy()

线程安全:当多个线程访问某个方法时,不管你通过怎样的调用方式、或者说这些线程如何交替地执行,我们在主程序中不需要去做任何的同步,这个方法的结果行为都是我们设想的正确行为,那么我们就可以说这个方法是线程安全的。

参考:https://www.cnblogs.com/chenyg32/p/3739564.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <assert.h>
#include <string.h>

// const 修饰 src,防止被修改
char * my_strcpy(char * dest, const char * src) {
// 空指针的检查
assert(src != nullptr && dest != nullptr);

char * ret = dest;
//一路赋值,直至终止符。
while ( (*dest++ = *src++) != '\0');

// 返回原始dest的值
return ret;
}
//返回dst的原始值,使得函数能够支持链式表达式
//如: char * strA = strcpy(new char[10],strB);
//即: char * strA = new char[10];


//解决dest与src内存重叠, cnt = 复制个数
char * my_memcpy(char * dest, const char * src, int cnt) {
// 空指针的检查
//宏 : 计算表达式 expression ,如果其值为假(即为0),那么它先向stderr打印一条出错信息,然后通过调用 abort 来终止程序运行。
assert(src != nullptr && dest != nullptr);

char * ret = dest;

//内存覆盖:src<=dst<=src+strlen(src)
if (dest >= src && dest <= src + cnt - 1) {
//从高地址开始复制
dest = dest + cnt - 1;
src = src + cnt - 1;
while (cnt--)
*dest-- = *src--;
}
else {
//内存无覆盖,从低地址开始复制
while(cnt--)
*dest++ = *src++;
}

// 返回原始dest的值
return ret;
}

char * my_strcpy2(char * dest, const char * src) {
// 空指针的检查
assert(src != nullptr && dest != nullptr);

char * ret = dest;

//strlen仅仅计算字符个数,不包括最后的\0。
my_memcpy(dest, src, strlen(src) + 1);

// 返回原始dest的值
return ret;
}

内存覆盖时为什么要从高地址开始复制,往低地址走:

eg:

1
2
3
*s = "abc";
my_strcpy2(s+1, s);
//防止dst与src重叠,把'\0'覆盖了;

String类的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
#include <iostream>
#include <cctype>
#include <cstring>

//#pragma warning(disable:4996)

class string
{
public:

string(const char* src = NULL)
{
//TODO1:构造函数
//开始添加代码
if (src == NULL) {
m_data = new char[1];
*m_data = '\0';
m_length = 0;
}
else {
m_length = strlen(src);
m_data = new char[m_length + 1];
strcpy(m_data, src);
}
//结束添加代码
}

~string()
{
//TODO2:析构函数
//开始添加代码
m_length = 0;
if (m_data != nullptr) {
delete[]m_data;
}
//结束添加代码
}

int size() const
{
return m_length;
}

const char *c_str() const
{
return m_data;
}

string(const string &src)
{
//TODO3:拷贝构造函数
//开始添加代码
m_length = strlen(src.m_data);
m_data = new char[m_length + 1];
strcpy(m_data, src.m_data);
//结束添加代码
}

string operator+(const string &src) const
{
string ret;
//TODO4:运算符+
//开始添加代码
ret.m_length = this->m_length + src.m_length;
ret = new char[ret.m_length + 1];
strcpy(ret.m_data, this->m_data);
strcat(ret.m_data, src.m_data);
//结束添加代码
return ret;
}

string& operator+=(const string &src)
{
//TODO5:运算符+=
//开始添加代码
char *temp = new char[this->m_length + 1];
strcpy(temp, this->m_data);
delete[] this->m_data;

this->m_length = src.m_length;
this->m_data = new char[m_length + 1];
strcpy(this->m_data, temp);
strcat(this->m_data, src.m_data);

//结束添加代码
return *this;
}

bool operator==(const string &str) const
{
//TODO5:运算符==
//开始添加代码
if (this->m_length != str.m_length) {
return false;
}
else {
if (strcmp(this->m_data, str.m_data)) {
return true;
}
else {
return false;
}
}

//结束添加代码
}

void Reverse()
{
//TODO5:字符串反转
//开始添加代码
strrev(this->m_data);
//结束添加代码
}

void Sort()
{
//TODO6:字符串从小到大排序
//开始添加代码

//结束添加代码
}
private:
char *m_data;
int m_length;
};

int main()
{
//构造函数
string str1 = "abcde";
string str2("12345");

//拷贝构造函数
string str3 = str1;
std::cout << (str3 == str1) << std::endl;

//拷贝构造函数
string str4(str2);
std::cout << (str2 == str4) << std::endl;

//+ 运算符
string str5 = str1 + str2;
std::cout << (str5 == "abcde12345") << std::endl;
std::cout << (str5.size() == str1.size() + str2.size()) << std::endl;

//+= 运算符
str5 += str1;
std::cout << (str5 == "abcde12345abcde") << std::endl;

//反转
string str6 = "12345678";
str6.Reverse();
std::cout << (str6 == "87654321") << std::endl;

//排序
string str7 = "202008131617";
str7.Sort();
std::cout << (str7 == "000111223678") << std::endl;
return 0;
}

C++特性

可重入函数与不可重入函数

一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入 OS 调度下去执行另外一段代码,而返回控制时不会出现什么错误。

不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题。

  • 函数体内使用了静态(static)的数据结构;
  • 函数体内调用了 malloc() 或者 free() 函数;
  • 函数体内调用了标准 I/O 函数;

原因在于:

中断时保存上下文仅限于返回地址,cpu 寄存器等之类的少量上下文,而函数内部使用的诸如全局或静态变量,buffer 等并不在保护之列,所以如果这些值在函数被中断期间发生了改变,那么当函数回到断点继续执行时,其结果就不可预料了。

场景:

(1)在中断处理函数中调用有互斥锁保护的全局变量,如果恰好该变量正在被另一个线程调用,会导致中断处理函数不能及时返回,导致中断丢失等严重问题。

(2)并且在多线程环境中使用,在没有加锁的情况下,对同一段内存块进行并发读写,就会造成 segmentfault/coredump 之类的问题。

如何写出可重入的函数?
(1)在函数体内不访问那些全局变量;
如果必须访问全局变量,记住利用互斥信号量来保护全局变量。或者调用该函数前关中断,调用后再开中断;
(2)不使用静态局部变量;
(3)坚持只使用缺省态(auto)局部变量;
(4)在和硬件发生交互的时候,切记关闭硬件中断。完成交互记得打开中断,在有些系列上,这叫做“进入/退出核心”或者用 OS_ENTER_KERNAL/OS_EXIT_KERNAL 来描述;
(5)不能调用任何不可重入的函数;
(6)谨慎使用堆栈。最好先在使用前先 OS_ENTER_KERNAL;

可执行文件的装载与进程](https://www.cnblogs.com/fr-ruiyang/p/11196858.html))

进程的建立

从操作系统的角度来看,一个进程最关键的特征是它拥有独立的虚拟地址空间,这使得它有别于其他进程。很多时候一个程序被执行同时都伴随着一个新的进程的创建,那么我们就来看看这种最通常的情形:创建一个进程,然后装载相应的可执行文件并且执行。在有虚拟存储的情况下,上述过程最开始只需要做三件事情:

  • 创建一个独立的虚拟地址空间。
    • 一个虚拟空间由一组页映射函数将虚拟空间的各个页映射至相应的物理空间,那么创建一个虚拟空间实际上并不是创建空间而是创建映射函数所需要的相应的数据结构。
  • 读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系。
    • 当程序执行发生页错误时,操作系统将从物理内存中分配一个物理页,然后将该“缺页”从磁盘中读取到内存中,再设置缺页的虚拟页和物理页的映射关系,这样程序才得以正常运行。当操作系统捕获到缺页错误时,它应知道程序当前所需要的页在可执行文件中的哪一个位置。
    • 这种映射关系只是保存在操作系统内部的一个数据结构。Linux中将进程虚拟空间中的一个段叫做虚拟内存区域(VMA,Virtual Memory Area);在Windows中将这个叫做虚拟段(Virtual Section)
  • 将CPU的指令寄存器设置成可执行文件的入口地址,启动运行。
    • 操作系统通过设置CPU的指令寄存器将控制权转交给进程,由此进程开始执行。这一步看似简单,实际上在操作系统层面上比较复杂,它涉及内核堆栈和用户堆栈的切换、CPU运行权限的切换。不过从进程的角度来看这一步可以简单地认为操作系统执行了一条跳转指令,直接跳转到可执行文件的入口地址。

C++中垃圾回收机制中几种经典的垃圾回收算法

有向可达图 :当存在一个根节点可到达某个堆节点时,我们称该堆节点是可达的,反之称为不可达(需要回收的垃圾)。

根节点–对应于不在堆中的位置,这些位置可以是寄存器、栈中的变量,或者是虚拟存储器中读写数据区域的全局变量;

堆节点–对应于堆中一个分配块

(1)引用计数算法(share_ptr有所运用,会有循环引用计数问题)

(2)标记清除算法

动态申请内存时,先按需分配内存,当内存不足以分配时,从寄存器或者程序栈上的引用出发,遍历上述的有向可达图并作标记(标记阶段),然后再遍历一次内存空间,把所有没有标记的对象释放(清除阶段)

程序涉及内存大、对象多的时候过程可能有点长(中断正常执行程序)。

可以作为一个独立线程不断地定时更新可达图和回收垃圾来解决

(3)节点复制算法

从根节点开始,被引用的对象都会被复制到一个新的存储区域中,而剩下的对象则是不再被引用的,即为垃圾,留在原来的存储区域。释放内存时,直接把原来的存储区域释放掉,继续维护新的存储区域即可。

当需要回收的对象越多时,它的开销很小,而当大部分对象都不需要回收时,其开销反而很大。

综合:

首先从根开始进行一次常规扫描,扫描过程中如果遇到老生代对象则不进行递归扫描,这样可大大减少扫描次数。然后,把扫描后残留下来的对象划分到老生代。

节点复制算法:把新的存储区域内对象设置为老生代。

若是采用标记清除算法,则应该在对象上设置某个标志位标志其年龄;

C++ 模板,特化,与偏特化

模板特化:有时为了需要,针对特定的类型,需要对模板进行特化,也就是所谓的特殊处理。

eg : 模板类里面就包括一个Equal方法,用来比较两个参数是否相等。 但是对于float类型或者double的参数,绝对不能直接使用“==”符号进行判断。所以,对于float或者double类型,我们需要进行特殊处理。

模板的偏特化:提供另一份template定义式,而其本身仍为templatized;也就是说,针对template参数更进一步的条件限制所设计出来的一个特化版本。

eg : 针对const指针的偏特化设计

补充:特化与偏特化的调用顺序

先照顾最特殊的,然后才是次特殊的,最后才是最普通的。

vector中resize()和reserve()区别

resize()函数和容器的size息息相关。调用resize(n)后,容器的size即为n。至于是否影响capacity,取决于调整后的容器的size是否大于capacity。所有的空间都已经初始化了,所以可以直接访问。

reserve()函数和容器的capacity息息相关。调用reserve(n)后,若容器的capacity<n,则重新分配内存空间,从而使得capacity等于n。如果capacity>=n呢?capacity无变化。预分配出的空间没有被初始化,所以不可访问。

vector插入元素时迭代器失效问题

  1. 当size()==capacity(),插入元素时vector会被重新分配内存,指向元素的迭代器都会失效。
  2. 当size()<capacity(),插入元素时vector不会被重新分配内存。此时,若迭代器指向插入位置之前的元素,它仍有效;若迭代器指向插入位置之后的元素,它将会失效。

shared_ptr 与 make_share 的区别

在执行std::shared_ptr<A> p2(new A)的时候,首先会申请数据的内存,然后申请内控制块(计数器),因此是两次内存申请,而std::make_shared<A>()则是只执行一次内存申请,将数据和控制块的申请放到一起。

注意:make_shared -》虽然计数器为0,但weak_ptr仍需要得到指向数据的shared_ptr数目,这使得使得控制块一直在使用,不会释放。

shared_ptr是线程安全的吗?

shared_ptr线程安全性分析

不是,它只提供了类似内置类型同级别的线程安全性.

线程安全:

  1. 同一个shared_ptr对象可以被多线程同时读取。

  2. 不同的shared_ptr对象可以被多线程同时修改(即使这些shared_ptr对象管理着同一个对象的指针)。

  3. 任何其他并发访问的结果都是无定义的。

线程不安全:

当出现同一个shared_ptr对象可以对多线程同时修改,会产生错误结果【因所有的同步都是针对引用计数类sp_counted_base进行的,shared_ptr本身并没有任何同步保护。】

使用auto_ptr类的限制

https://blog.csdn.net/superbfly/article/details/9134635

1)不要使用auto_ptr对象保存指向静态分配对象的指针,否则,当auto_ptr对象本身被撤销的时候,它将试图删除指向非动态分配对象的指针,导致未定义的行为。

2)永远不要使用两个 auto_ptrs 对象指向同一对象,导致这个错误的一种明显方式是,使用同一指针来初始化或者 reset 两个不同的 auto_ptr对象。另一种导致这个错误的微妙方式可能是,使用一个 auto_ptr 对象的 get 函数的结果来初始化或者 reset另一个 auto_ptr 对象。

3)不要使用 auto_ptr 对象保存指向动态分配数组的指针。当auto_ptr 对象被删除的时候,它只释放一个对象—它使用普通delete 操作符,而不用数组的 delete [] 操作符。

4)不要将 auto_ptr 对象存储在容器中。容器要求所保存的类型定义复制和赋值操作符,使它们表现得类似于内置类型的操作符:在复制(或者赋值)之后,两个对象必须具有相同值,auto_ptr 类不满足这个要求。

C++ unordered_map 实现原理

C/C++中CRT表示什么意思?

STL排序总结与std::sort()底层原理

其源码分析:https://feihu.me/blog/2014/sgi-std-sort/

为什么快速排序比堆排序要快?

综合:

(1)充分利用位置的局部性,它访问的是靠近前面和后面的连续元素。不像堆排序,访问是比较分散。

(2)就地操作,不需要创建任何辅助数组来保存临时值。与归并排序相比,这是一个巨大的优势,因为分配和删除辅助数组所需的时间可能是很明显的。

为什么快速排序比堆排序要快?

快排为什么比归并快?

C++ 最小公倍数、最大公约数

让类只能用new来创建实例

C++语言中规定函数的返回值的类型是由函数定义时决定的

可以使用memset,memcpy直接进行初始化和拷贝的有:
a 结构
b枚举
c类实例
d指针
正确答案:A B D

C++11–匿名函数(Lambda函数)

https://blog.csdn.net/just_kong/article/details/102497406

形参的默认值

1,在函数没有声明时,在定义中可以指定形参的默认值,而当函数即有声明又有定义时,在声明时定义后,定义就不能再指定形参的默认值

2,形参的默认值 赋值必须遵守右到左的顺序

3,函数在调用时实参与形参按从左到右的顺序进行匹配

重载参数如果形参带有默认值时,可能产生二义性(会有都匹配的问题).

c++函数重载机制实现原理
https://blog.csdn.net/gogogo_sky/article/details/72807123
重载规则
https://blog.csdn.net/ASJBFJSB/article/details/80213725

https://blog.csdn.net/hong2511/article/details/80999282

事务和锁(共享锁,排他锁,乐观,悲观,死锁)
https://blog.csdn.net/qq_35023382/article/details/76155122

设计模式

常用设计模式以及使用场景

C++设计模式

手撕单例模式

1
2


手撕工厂模式

1
2


linux

free命令详解

https://www.cnblogs.com/machangwei-8/p/10352537.html
通过free命令查看机器空闲内存时,会发现free的值很小。这主要是因为,在Linux系统中有这么一种思想,内存不用白不用,因此它尽可能的cache和buffer一些数据,以方便下次使用。但实际上这些内存也是可以立刻拿来使用的。

非阻塞IO在linux上的运用。

1
2


计网

线程间到底共享了哪些进程资源?

c++11中thread join和detach的区别

http2.0多路复用

排查网络故障

Linux服务器开机启动流程

场景题

海量数据找出现次数最多或,不重复的。

像这种题最常考,最主要的是

有海量数据查询,其中随机查询与按key访问均存在50%的概率,请设计一个数据结构

1
2


假设有多核处理器(比如4个相同的核),从1到10亿中找素数,素数的判断的方法已知,怎么办?

大概是这么个意思,有什么好的解决思路吗?

实现一个 生成高n位1,其他位为0的掩码(360二面面试题)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
eg: n=4
mask = 1111 0000... (32位);

int getMask(int n){
//参数校验
if(n > 32){
return 0xFFFF;
}
else if( n < 0){
return 0;
}
else{
int count = 32 - n;
int value = 0xFFFF;
//在编译器自动补零的情况下。
return value << count;
// 在编译器循环的情况下
while(n--){
value == value << 1;
value &= 0xfff1(与上 当前位为0的值)
}
//或者 参考这个https://blog.csdn.net/qq_32832803/article/details/82286073
// 先生成 生成低count -1 位全为1,其余位全为0的掩码 temp = (1<<count-1)-1
// 然后 value - temp
}
}

QT

Qt 信号和槽机制 优点 效率的详解:https://blog.csdn.net/qq_21334991/article/details/78073269

信号槽,实际就是观察者模式。 作者:亿光年の距离 https://www.bilibili.com/read/cv6237068/ 出处:bilibili

C++面试题之 QT信号和槽实现机制

常用容器

image-20210909105022850

CGI

sql LIMIT 0,20
https://www.cnblogs.com/datang6777/p/10650212.html
where 1=1 where 1 <> 1;
https://blog.csdn.net/qq_23994787/article/details/79045768

为什么参数化SQL查询可以防止SQL注入?
https://www.zhihu.com/question/52869762

---------------- 本文结束 ----------------

本文标题:面题汇总

文章作者:Pabebe

发布时间:2021年09月10日 - 17:53:37

最后更新:2022年04月14日 - 16:34:05

原始链接:https://pabebezz.github.io/article/e4767f57/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%