【面试题】360 C++ 软件开发

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

今天继续分享面经:360 软件开发,提前批。

面试题

1. 实现 strcpy() ?

string.h中的 strcpy()的函数原型如下:

char *strcpy(char* dest, const char *src);

这个函数的功能是:把从 src 地址开始且含有 ‘\0’ 结束符的字符串复制到以 dest 开始的地址空间。

我们的实现:

char *strcpy(char* dest, const char *src)
{
    assert(dest != NULL && src != NULL);

    char *ret = dest;

    do {
        *dest = *src;
    } while (*src++ != '\0') {
        
    return ret;
}

实现的时候有几个地方需要注意一下:

  • 源串的参数类型为 const ,保证函数不会修改源串
  • 使用 assert 进行指针有效性检查
  • 保存目的串的起始地址并返回

2. 如何判断大、小端字节序,写出代码?

字节序的问题是由于 CPU 对整数在内存中存放方式的不同而造成的。多于一个字节的数据类型在内存中的存放顺序主机字节序。最常见的字节序有两种,小端字节序大端字节序

  • 小端字节序
    即 Little Endian ,简称 LE ,将数据的最低字节放在内存的起始位置。小端字节序的特点是内存地址较低的位存放数据的低位,内存地址高的位存放数据的高位,与思维习惯一致。采用小端字节序的 CPU 有 X86 架构的 Intel 系列产品
  • 大端字节序
    即 Big Endian ,简称 BE ,将数据的高字节放在内存的起始位置。大端字节序的特点是内存中低字节位置存放数据的高位字节,内存中的高位字节存放数据的较低字节数据,与思维习惯不一致。但是与实际数据的表达方式是一致的。如果将内存中的数据直接存放在文件中,打开文件查看会发现和原来的数据的高低位一致。采用大端字节序的典型代表有 PowerPC 的 UNIX 系统

下面我们通过一个简单的例子来获取笔者所使用主机的字节序:

#include <stdio.h>

union Test {
    short numb;
    char c[2];
};

int main()
{
    printf("sizeof(short) = %d\n", sizeof(short));
    printf("sizeof(char)  = %d\n", sizeof(char));

    union Test t;
    t.numb = 0x1234;

    printf("numb = %x\n", t.numb);
    printf("c[0] = %x\n", t.c[0]);
    printf("c[1] = %x\n", t.c[1]);

    return 0;
}

/*
运行结果:
sizeof(short) = 2
sizeof(char)  = 1
numb = 1234
c[0] = 34
c[1] = 12
*/

从运行结果中,我们可以清晰地看出,低位地址存放的是低字节数据,高位地址存放的是高字节数据,所以笔者主机使用的是小端字节序(CPU: X86 Intel Core i5)。

关于字节序的详细解读见往期笔记:主机字节序与网络字节序。

3. 讲一讲 TCP 的拥塞控制?

什么是拥塞?

拥塞:在某段时间,如果对网络中的某一资源的需求超过了该资源所能提供的可用部分,网络的性能就会发生变化,这种情况叫拥塞。

拥塞控制:防止过多的数据注入到网络当中,这样可以使网络中的路由器或链路不致过载。

TCP拥塞控制是传输控制协议(TCP)避免网络拥塞的算法,是互联网上主要的一个拥塞控制措施。它使用一套基于线增积减模式的多样化网络拥塞控制方法来控制拥塞。包括:滑动窗口机制慢启动机制拥塞避免机制快速重传与恢复

  • 滑动窗口机制
    首先明确滑动窗口的范畴:TCP 是全双工的协议,会话的双方都可以同时接收和发送数据。TCP 会话的双方都各自维护一个发送窗口和一个接收窗口。各自的接收窗口大小取决于应用、系统、硬件的限制(TCP传输速率不能大于应用的数据处理速率)。各自的发送窗口则要求取决于对端通告的接收窗口,要求相同。滑动窗口机制包括发送窗口(SWND)、接受窗口(RWND)和拥塞窗口(CWND)。其中max(SWND) = min(CWND,RWND)
    其主要过程是:维护一个指定长度的窗口,发送多个数据,并准备接下来的一些数据等待发送。等待收到对方的关于特定数据的应答消息,一旦受到应答,立刻讲准备发送的数据进行发送。若等待超时,则将等待应答的数据进行重新发送。
  • 慢启动机制
    当主机开始发送数据时,如果立即将大量数据字节注入到网络,那么就有可能因为不清楚当前网络的负荷情况而引起网络阻塞。所以,最好的方法是先探测一下,即由小到大逐渐增大发送窗口,也就是说,由小到大逐渐增大拥塞窗口数值。通常在刚刚发送报文段时,先把拥塞窗口 CWND 设置为一个最大报文段 MSS 的数值。而在每收到一个新的报文段的确认后,把拥塞窗口增加至多一个 MSS 的数值(相当于加倍)。用这样的方法逐步增大发送方的拥塞窗口 CWND ,可以使分组注入到网络的速率更加合理。
    当 RWND 足够大的时候,为了防止拥塞窗口 CWND 的增长引起网络阻塞,还需要另外一个变量——慢开始门限(SSTHRESH):当 CWND<SSTHRESH 时,使用慢启动算法;当 CWND > SSTHRESH 时,停止使用慢启动算法,改用拥塞避免算法。
    慢启动中的“慢”并不是指 CWND 的增长速率慢,而是在 TCP 开始发送报文段时先设置 CWND=1 ,使得发送方在开始时只发送一个报文段)。
  • 拥塞避免机制
    让拥塞窗口 CWND 缓慢的增大,即每经过一个往返时间 RTT 就把发送方的拥塞窗口 CWND 加 1 ,而不是加倍。这样拥塞窗口 CWND 按线性规律缓慢的增长,比慢开始算法的拥塞窗口增长速率缓慢的多。
    无论是慢启动算法还是拥塞避免算法,只要判断网络出现拥塞,就要把慢启动开始门限设置为发送窗口的一半,CWND 设置为 1 ,然后再使用慢启动算法,这样做的目的能迅速的减少网络当中的数据传输,使发生拥塞的路由器能够把队列中堆积的分组处理完毕。
  • 快速重传与恢复
    快速重传算法要求首先接收方收到一个失序的报文段后立刻发出重复确认,而不要等待自己发送数据时才进行捎带确认。假如接收方成功地接受了发送方发来的 data1 和 data2 并且分别发送了 ACK ,现在接收方没有收到 data3 ,而收到了 data4 ,显然接收方不能确认 data4 ,因为 data4 是失序的报文段。如果根据可靠性传输原理接收方什么都不做,但是按照快速重传算法,在收到 data4、data5 等报文段的时候,不断重复的向发送方发送 data2 的 ACK ,如果发送方一连收到三个重复的 ACK ,则会立即重传确认所期待的下一个报文。
    关于快速恢复,当发送方连续收到三个重复确认时,执行“乘法减小”算法,慢启动门限减半,为了预防网络发生阻塞;由于发送方现在认为网络很可能没有发生阻塞,因此现在不执行慢启动算法,而是把 CWND 值设置为慢启动门限减半后的值,然后开始执行拥塞避免算法,拥塞窗口 CWND 值线性增大。

4. 四种 cast 强制类型转换的区别和使用?

见往期笔记:C++ 中的 cast 类型转换。

5. 讲一下智能指针?

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

6. 做了哪些面向对象编程的工作?

视自己具体情况作答,这里仅介绍面向对象编程的概念。

面向对象程序设计:作为一种新方法,其本质是以建立模型体现出来的抽象思维过程和面向对象的方法。模型是用来反映现实世界中事物特征的。任何一个模型都不可能反映客观事物的一切具体特征,只能对事物特征和变化规律的一种抽象,且在它所涉及的范围内更普遍、更集中、更深刻地描述客体的特征。通过建立模型而达到的抽象是人们对客体认识的深化。

7. 了解完美转发吗?

先来看一个例子:

#include <iostream>

using namespace std;

template<typename T>
void f(T &param) {
    cout << "左值参数" << endl;
}

template<typename T>
void f(T &&param) {
    cout << "右值参数" << endl;
}

template<typename T>
void func(T &&param) {
    f(param);
}

int main()
{
    int num = 2020;
    func(num);
    func(2020);

    return 0;
}

/*
运行结果:
左值参数
左值参数
*/

是不是跟我们期望的不太一样?难道第二个不是“右值参数”吗?下面我们来分析一下:

func() 函数本身的形参是一个万能引用,即可以接受左值又可以接受右值;第一个func()函数调用实参是左值,所以,func() 函数中调用 f()中传入的参数也应该是左值;第二个 func() 函数调用实参是右值,warp() 函数接收的参数类型是右值引用,那么为什么却调用了 f() 的左值版本了呢?这是因为在 func() 函数内部,由于参数有了名称,右值引用类型变为了左值。

那么问题来了,怎么保持函数调用过程中,变量类型的不变呢?这就是我们所谓的完美转发技术,在 C++11 中通过 std::forward() 函数来实现。我们修改我们的 func() 函数如下:

template<typename T>
void func(T &&param) {
    f(std::forward<T>(param));
}

再运行一遍,结果如下:

左值参数
右值参数

8. future 和 thread 了解吗?

future

简单地说,std::future 可以用来获取异步任务的结果,因此可以把它当成一种简单的线程间同步的手段。
std::future 通常由某个 Provider 创建,你可以把 Provider 想象成一个异步任务的提供者,Provider 在某个线程中设置共享状态的值,与该共享状态相关联的 std::future 对象调用 get(通常在另外一个线程中) 获取该值,如果共享状态的标志不为 ready,则调用std::future::get 会阻塞当前的调用者,直到 Provider 设置了共享状态的值(此时共享状态的标志变为 ready),std::future::get 返回异步任务的值或异常(如果发生了异常)。

thread

C++11 中引入了一个用于多线程操作的 thread 类。

下面看一个简单的示例:

#include <iostream>
#include <thread>
#include <unistd.h>

using namespace std;

void thread01()
{
	for (int i = 0; i < 5; i++)
	{
		cout << "Thread 01 is working !" << endl;
		sleep(1);
	}
}
void thread02()
{
	for (int i = 0; i < 5; i++)
	{
		cout << "Thread 02 is working !" << endl;
		sleep(1);
	}
}

int main()
{
	thread task01(thread01);
	thread task02(thread02);
	task01.join();
	task02.join();

	for (int i = 0; i < 5; i++)
	{
		cout << "Main thread is working !" << endl;
		sleep(1);
	}

	sleep(100);
}

/*
运行结果:
Thread 02 is working !
Thread 01 is working !
Thread 02 is working !
Thread 01 is working !
Thread 01 is working !
Thread 02 is working !
Thread 02 is working !
Thread 01 is working !
Thread 02 is working !Thread 01 is working !

Main thread is working !
Main thread is working !
Main thread is working !
Main thread is working !
Main thread is working !

*/

可以看到,主线程最后执行,这是因为 join()会阻塞主线程detach()将子线程从主流程中分离,独立运行,不会阻塞主线程。

int main()
{
	thread task01(thread01);
	thread task02(thread02);
	task01.detach();
	task02.detach();

	for (int i = 0; i < 5; i++)
	{
		cout << "Main thread is working !" << endl;
		sleep(1);
	}

	sleep(100);
}

/*
运行结果:
Main thread is working !Thread 02 is working !

Thread 01 is working !
Thread 01 is working !
Thread 02 is working !
Main thread is working !
Thread 02 is working !
Thread 01 is working !
Main thread is working !
Thread 01 is working !Main thread is working !

Thread 02 is working !
Thread 01 is working !
Main thread is working !
Thread 02 is working !

*/

我们还可以向线程函数传递参数:

#include <iostream>
#include <thread>
#include <unistd.h>

using namespace std;

void thread01(int num)
{
	for (int i = 0; i < num; i++)
	{
		cout << "Thread 01 is working !" << endl;
		sleep(1);
	}
}
void thread02(int num)
{
	for (int i = 0; i < num; i++)
	{
		cout << "Thread 02 is working !" << endl;
		sleep(1);
	}
}

int main()
{
	thread task01(thread01, 5);
	thread task02(thread02, 5);
	task01.detach();
	task02.detach();

	for (int i = 0; i < 5; i++)
	{
		cout << "Main thread is working !" << endl;
		sleep(1);
	}

	sleep(100);
}

/*
运行结果:
Main thread is working !Thread 01 is working !
Thread 02 is working !

Main thread is working !
Thread 01 is working !
Thread 02 is working !
Thread 02 is working !
Thread 01 is working !
Main thread is working !
Thread 01 is working !
Thread 02 is working !
Main thread is working !
Thread 02 is working !
Main thread is working !
Thread 01 is working !

*/

9. TCP/IP 四层模型和 OSI 七层模型具体是哪些?

见往期笔记:网络结构的标准模型:ISO/OSI 开放互联模型和 TCP/IP 协议栈。

10. 进程和线程的区别是什么?进程间通信有哪些方式?

见往期笔记:操作系统基础知识:程序、进程与线程。

点击下方图片关注我,或微信搜索**“编程笔记本”**,获取更多信息。
在这里插入图片描述

发布了19 篇原创文章 · 获赞 0 · 访问量 201
展开阅读全文
评论将由博主筛选后显示,对所有人可见 | 还能输入1000个字符

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览