Jaime0117-恒生 C/C++开发 一面——解答

从今天起,豆芽有空也尽己所能,帮助一下大家。

面经来源:https://www.nowcoder.com/discuss/740250?source_id=discuss_experience_nctrack&channel=-1


1. C++11新特性了解多少

  1. 右值引用

  2. lambda

  3. 智能指针


2. lambda表达式用过吗,怎么用

在我们编程的过程中,我们常定义一些只会调用一次的函数,这样我们就还得老老实实写函数名、写参数,其实还挺麻烦的.

但是C++ 11 引入Lambda 表达式用于定义并创建匿名的函数对象,就可以简化我们的编程工作了。

Lambda 的语法形式如下:

[函数对象参数] (操作符重载函数参数) mutable 或 exception 声明 -> 返回值类型 {函数体}  
  1. [函数对象参数]

    函数对象参数标识一个 Lambda 表达式的开始,这部分不能省略。方括号 [] 内的参数集合叫做一个闭包,允许Lambda 函数可以引用在它之外声明的变量,这个机制允许这些变量被按值或按引用捕获

    函数对象参数有以下形式:

    函数对象参数 形式 含义
    [] 没有任何函数对象参数
    = [=] 表示按值捕获外部参数
    & [&] 表示按引用捕获外部参数
    this - 函数体内可以使用 Lambda 所在类中的成员变量
    a [a] 将参数a按值进行捕获。按值进行捕获时,函数体内不能修改传递进来的 a 的拷贝,因为默认情况下函数是 const 的,要修改传递进来的拷贝,可以添加 mutable 修饰符
    &a [&a] 表示将 a 按引用进行捕获
    a,&b [a, &b] 表示将 a 按值捕获,b 按引用进行捕获
    =,&a,&b [=, &a, &b] 表示除 a 和 b 按引用进行捕获外,其他参数都按值进行捕获
    &,a,b [&, a, b] 表示除 a 和 b 按值进行捕获外,其他参数都按引用进行捕获
  2. (操作符重载函数参数)

    标识重载的 () 操作符的参数,没有参数时,这部分可以省略。参数可以通过按值(如: (a, b))和按引用 (如: (&a, &b)) 两种方式进行传递。

  3. mutable 或 exception 声明

    这部分可以省略。按值传递函数对象参数时,加上 mutable 修饰符后,可以修改传递进来的拷贝(注意是能修改拷贝,而不是值本身)。exception 声明用于指定函数抛出的异常,如抛出整数类型的异常,可以使用 throw(int)。

  4. 返回值类型

    标识函数返回值的类型。

  5. {函数体}

    标识函数的实现,这部分不能省略,但函数体可以为空。

好了我们给出实例:

[] (int x, int y) { return x + y; } // 隐式返回类型  
[] (int& x) { ++x; } //没有return语句,Lambda函数的返回类型是void
[] () { ++global_x; } //没有参数,仅访问某个全局变量  
[] { ++global_x; } //与上一个相同,省略了(操作符重载函数参数)  
[] (int x, int y)->int{ int z = x+y; return z; }//可以指定返回类型  
aotu cmp = [](int x, int y){ return x > y; } //匿名函数也可以有名称 

3. 移动和拷贝的区别

移动构造函数与拷贝构造不同,它并不是重新分配一块新的空间同时将要拷贝的对象复制过来,而是"拿"了过来,将自己的指针指向别人的资源,然后将别人的指针修改为nullptr,这一步很重要,如果不将别人的指针修改为空,那么临时对象析构的时候就会释放掉这个资源,那么就没有“拿”过来。下面这张图可以解释copy和move的区别:

img

4. socket编程了解多少

socket是一种特殊的文件。在Linux中,“一切皆文件”,都可以用“打开(open)——读写(write/read)——关闭(close)”的模式来操作。socket就是改模式的一个实现,并提供了一系列对应的函数接口。

我们以TCP为例,下图展示了其交互的过程:

img

图中展示的交互流程,具体如下所述 :

(1)服务器根据地址类型( ipv4, ipv6 )、 socket 类型、协议创建 socket。

(2)服务器为 socket 绑定 IP 地址和端口号。

(3)服务器 socket 监听端口号请求,随时准备接收客户端发来的连接,这时候服务器的socket 并没有被打开 。

(4)客户端创建 socket。

(5)客户端打开 socket,根据服务器 IP 地址和端口号试图连接服务器 socket。

(6)服务器 socket 接收到客户端 socket 请求,被动打开,开始接收客户端请求,直到客户端返回连接信息 。这时候 socket 进入阻塞状态,所谓阻塞即accept()方法一直到客户端返回连接信息后才返回,开始接收下一个客户端连接请求 。

(7)客户端连接成功,向服务器发送连接状态信息 。

(8)服务器 accept 方法返回,连接成功 。

(9)客户端向 socket 写入信息 。

(10)服务器读取信息 。

(11)客户端关闭 。

(12)服务器端关闭 。

涉及到的接口函数如图


5. IO多路复用了解多少

IO多路复用:Linux用select/poll函数实现IO复用模型,这两个函数也会使进程阻塞,但是和阻塞IO所不同的是这两个函数可以同时阻塞多个IO操作。而且可以同时对多个读操作、写操作的IO函数进行检查。select/poll会监听所有的IO,直到有数据可读或可写时,才真正调用IO操作函数。

select,poll,epoll都是IO多路复用的机制,I/O多路复用就是通过一种机制,可以监视多个文件描述符(Linux中,一切皆文件,通过文件描述符来标识),一旦某个文件描述符就绪(一般是读就绪或者写就绪),能够通知应用程序进行相应的读写操作。

但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需用户自己进行读写,异步I/O的实现是内核负责把数据从内核拷贝到用户空间,用户直接使用数据,这个过程是非阻塞的

三者的原型如下所示:

int select(int nfds, fd_set *readfds,   
           fd_set *writefds, fd_set *exceptfds,   
           struct timeval *timeout  
          );  

int poll(struct pollfd *fds, nfds_t nfds,   
         int timeout  
        );  

int epoll_wait(int epfd, struct epoll_event *events,   
               int maxevents, int timeout  
              );  

6. 多线程常见的锁有哪些,悲观锁乐观锁的区别,自旋锁介绍一下

(1)互斥锁:mutex,保证在任何时刻,都只有一个线程访问该资源,当获取锁操作失败时,线程进入阻塞,等待锁释放。

(2)读写锁:rwlock,分为读锁和写锁,处于读操作时,可以运行多个线程同时读。但写时同一时刻只能有一个线程获得写锁。

互斥锁和读写锁的区别:

(a)读写锁区分读锁和写锁,而互斥锁不区分

(b)互斥锁同一时间只允许一个线程访问,无论读写;读写锁同一时间只允许一个线程写,但可以多个线程同时读。

(3)自旋锁:spinlock,在任何时刻只能有一个线程访问资源。但获取锁操作失败时,不会进入睡眠,而是原地自旋,直到锁被释放。这样节省了线程从睡眠到被唤醒的时间消耗,提高效率。

(4)条件锁:就是所谓的条件变量,某一个线程因为某个条件未满足时可以使用条件变量使该程序处于阻塞状态。一旦条件满足了,即可唤醒该线程(常和互斥锁配合使用)

(5)信号量

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。

要明确一下:无论是悲观锁还是乐观锁,都是人们定义出来的概念,可以认为是一种思想。其实不仅仅是关系型数据库系统中有乐观锁和悲观锁的概念,像memcache、hibernate、tair等都有类似的概念。所以,不应该拿乐观锁、悲观锁和其他的数据库锁等进行对比。

实现方式:

悲观锁的实现可以依靠数据库里的锁机制,如排它锁。
乐观锁的实现Compare and Swap(CAS):当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加不会减少。除了version以外,还可以使用时间戳,因为时间戳天然具有顺序递增性。

ABA:如果另一个线程修改V值假设原来是A,先修改成B,再修改回成A。当前线程的CAS操作无法分辨当前V值是否发生过变化。关于ABA问题举一个例子:在你非常渴的情况下你发现一个盛满水的杯子,你一饮而尽。之后再给杯子里重新倒满水。然后你离开,当杯子的真正主人回来时看到杯子还是盛满水,他当然不知道是否被人喝完重新倒满。解决这个问题的方案的一个策略是每一次倒水假设有一个自动记录仪记录下,这样主人回来就可以分辨在她离开后是否发生过重新倒满的情况。这也是解决ABA问题目前采用的策略。用悲观锁就可以解决这个问题。



以上所有题的答案其实都来源于我的博客面经,欢迎大家围观:https://blog.nowcoder.net/jiangwenbo



#百度实习阿里实习##面经##恒生公司##C++工程师##嵌入式工程师#
全部评论

相关推荐

14 78 评论
分享
牛客网
牛客企业服务