C/C++面试相关问题

本人之前从事的是嵌入式相关行业,主要是数字座舱相关操作系统底层以及中间层的开发,主要是基于Android、qnx、以及linux,语言的话用的比较多的是c/c++、java、python这些。最近面试的一些总结如下。

  1. 遇到的多线程的问题,出现数据不同步的问题怎么处理?
    一般都是对变量读写之前都进行加锁处理,但是需要注意的就是加锁之后的函数如果调用另一个函数,需要注意他是不是会加锁,并且加的是不是同一把锁,如果是同一把锁的话容易造成死锁。
    为了防止多个线程竞争一把锁的时候,其他线程会多次执行同一个操作,比如一个线程连接需要一定的时间,发现connect失败,但是其他的线程竞争到锁之后会继续进行connect,这样的话,就会造成多次调用connect。因此应该使用pthread_mutex_trylock。
    pthread_mutex_trylock是lock的非阻塞版本,常用的mutex如果没有当前线程访问某个数据时没有获取到锁的话会阻塞,而try_lock会调用返回,返回失败的话表示没有获取到互斥锁,返回0表示加锁成功。

  2. Android或者linux中的某个service挂掉这种问题是否有遇到,如何定位?
    首先取出tomestone,查看挂掉的线程,然后根据trace查看代码的逻辑;
    如果没有复现的现场,可以查看一下机器中记载的DiagLog,从中推断之前发生的动作以及涉及到的逻辑;
    可以利用gdb在自己推测的附近打上断点,复现现象。如果还没有问题的话就需要再现,等再现出来之后看问题发生的log定位问题。

  3. 内存泄漏怎样定位问题?

    1. 先用top/htop/dumpsys meminfo查看一下线程的占用。
    2. 使用内存池分配内存,可以解决内存泄漏的问题。在内存池统一malloc的时候,把log打上,这时候就能看到分配时内存泄漏的定位。
    3. 运行时判断是内存池的泄漏还是非内存池的泄漏
  4. 项目中的service如何启动
    正常的Java service时调用startService和bindService启动,项目中是有sysctrl有统一的接口管控系统的service的生命周期,我们这边需要事先在sysctrl那边注册,在xml的文件中将service注册进去。

  5. socket通信是否有一些安全措施?
    我了解到的可以对一些敏感的数据进行加密,使用MD5的方式,或者直接调用openssl这个库进行数据的通信也可以实现加密

  6. socket通信的模式有哪几种?
    同步,异步
    可以通过fcntl函数阻塞和非阻塞,设置为非阻塞之后connect就会自动返回,根据返回值去判断这个通信是否建立。

  7. 网络IO的模型有哪几种?
    同步,异步,信号驱动,多路复用,非阻塞IO

  8. pipe管道的用法?
    半双工,一边读,一边写;只能在父子进程之间使用;不能多次读取同一份数据。
    有名管道FIFO可以在任意进程之间使用

  9. 锁的应用?
    读写锁:用于读操作多于写操作的场景,读锁共享的线程可以拿到,写锁只有一个线程占用
    自旋锁:没有拿到锁的话就会一直处于睡眠的状态
    mutex锁:同一时刻只能有一个线程能拿到这把锁,其他线程阻塞等待

  10. 找链表的倒数第三个节点?
    先要问清楚是否是单链表,如果是单链表的话可以使用双指针法,然后一个指针先走3步,后面的指针再一起走,这样的话第一个指针走到结尾的时候,前一个指针就正好在倒数第三个位置。(后续贴出代码)

  11. 一个集合1,2,3,4,5,输出所有的子集?
    这道题有两种方法:一种是回溯法,一种是使用位操作。第一种方法的主要思想就是将整个子集的输出作为一颗二叉树,因为每个元素只有存在和不存在两种可能的情况,因此对应二叉树的两个分支,这样每次到底层再往上回溯,然后再走向另一个分支,就能完整的输出整个子集。递归的主要出口就是递归的层次达到元素集合的长度,输出此次包含的元素集合,然后递归方法就是如果当前是不包含的话就走不包含的分支,包含的话就走包含的分支。第二种方法是利用为操作的方法,这种方法比较简单,因为所有的子集就是有5!中,也就是1左移5位。那么外层循环就是0-1<<5,内存循环就是外层的i&(1<<j),j是内层循环,实际就是0-5,每一位为1的时候和i进行取&得出的结果,这样也能遍历出所有的集合。

  12. mysql引擎?
    主要是将两个存储引擎:InnoDB和MyIsam;InnoDB是使用的行级锁,支持事务,不支持全文检索(内部B+树,但是节点里面直接存数据);MYIsam不支持事务和外键,但是支持全文检索(B+树,但是节点存储的是指针)

  13. delete与delete[]的区别?
    new一个指针的时候使用delete,new多个比如数组的时候,使用delete[]释放多次

  14. 动态库和静态库的区别?
    二者的不同点在于代码被载入的时刻不同。
    静态函数库的扩展名一般为(.a或.lib),静态库在程序编译时会被连接到目标代码中,因此体积较大。静态库的优点是移植比较方便,程序不用再依赖其他的库加载;
    静态函数库的扩展名一般为(.a或.lib),动态函数库的扩展名一般为(.so或.dll)动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入,因此代码体积较小。动态库的好处是更加共享,需要修改时不需要编译全部的代码。

  15. c++ 11 新特性,常用的?
    见C++11NewFeature.cpp

  16. 从操作系统内存的角度讲一下进程和线程的区别?
    进程是操作系统进行内存分配的最小单位,线程是CPU进行分配调度的最小单位。
    进程是程序的一次执行过程,有初始态、执行态、等待态、就绪态、终止态。
    线程是CPU调度和分派的基本单位,它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
    线程是进程的一部分,一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
    每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
    系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。

    写时复制技术:内核只为新生成的子进程创建虚拟空间结构,它们来复制于父进程的虚拟结构,但是不为这些段分配物理内存,它们共享父进程的物理空间,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。fork()的实际开销就是复制父进程的页表以及给子进程创建一个进程描述符。在这里插入图片描述

    1. 栈 --有编译器自动分配释放
    2. 堆 -- 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收
    3. 全局区静态区—— 全局变量和静态变量的存储是放在一块的(.data),初始化的全局变量和静态变
      量在一 块区域,未初始化的全局变量(.bss)
    4. 常量区——另外还有一个专门放常量的地方(常量区)“asfdfdfdf\0”。程序结束释放。
    5. 程序代码区 —— .text代码段
  17. c++ 定义对象和new的区别?
    定义对象的话不需要手动释放内存,析构函数自动释放;new的话需要和delete搭配使用,需要手动释放。指针没有new的时候不会占用空间,比如CTest* test=NULL;定义对象CTest test;系统会自动分配空内存空间。

  18. sizeof和getmemory的考量?(具体百度可以看到出题)
    sizeof:结构体时char a:3,表示a占用三个bit,一个字节最多8bit,不能将一个数据的空间拆分到两个字节;
    注意字节对齐
    string根据操作系统的不同会固定分配不同的内存空间
    指针占4字节,数组占数组的size*数据类型,字符串的话需要加上结尾\0
    数组作为函数参数的话会转化为指针,因此size也是4
    对于一个类来说,如果有虚函数的话,内部就会有一个虚函数表,占用四个字节,如果继承父类的话,需要加上父类的大小

  19. getmemory相关?(百度可以看到出题)

  20. select模型的瓶颈?
    select中的fd_set集合容量的限制(FD_SETSIZE) ,这需要重新编译内核。
    ulinit -n来查询最大文件描述符 得到1024
    连接少,通信频繁时,select可能比epoll高效

  21. 为何选用select模型进行socket的通信?
    传统的阻塞IO读数据之后实时性比较差,需要阻塞等待每个消息;非阻塞IO的话循环调用recv去读消息,如果消息通信比较频繁的话会占用CPU;select的优势在于可以在一个线程里面监听多个socket的消息,在一个线程内监听消息的话对于消息也比较好处理,相对占用的资源也会比较少。异步IO的话使用的是回调的方式。

  22. 线程池和内存池简单说说?
    都是用于线程或者内存需要频繁的创建和销毁的场景。
    线程池旨在减少创建和销毁线程的频率,使其维持在一定的数量,并让空闲的线程执行新的任务。
    线程池包括一个线程池的管理者,用于调度线程执行任务;一个任务队列,需要各个执行的各个任务抽象成一个个结构体放在队列中等待执行;还有一个就是执行任务的线程,内部是一个永真的循环在执行任务。
    内存中一页是4k,因此一般小于4k的话就属于小块内存,大于4k的话属于大块内存。如果当前的内存是小块内存的话,比较难办的一个问题实际就是将小块内存合并成一个大块的内存,防止小块过多而引起碎片化的现象。这个解决方案就是每次都分配4k的大小,小块的内存通过指针的偏移计算分配的大小,返回分配的指针,如果小块内存累加达到4k,下次再分配4k的内存。两块4k的内存之间使用链表连接。

  23. epoll的应用及原理?
    1、epoll_create函数
    函数声明:int epoll_create(int size)
    该函数生成一个epoll专用的文件描述符,其中的参数是指定生成描述符的最大范围。在linux-2.4.32内核中根据size大小初始化哈希表的大小,在linux2.6.10内核中该参数无用,使用红黑树管理所有的文件描述符,而不是hash。
    2、epoll_ctl函数
    函数声明:int epoll_ctl(int epfd, int op, int fd, struct epoll_event event)
    该函数用于控制某个文件描述符上的事件,可以注册事件,修改事件,删除事件。
    参数:epfd:由 epoll_create 生成的epoll专用的文件描述符;
    op:要进行的操作例如注册事件,可能的取值
    EPOLL_CTL_ADD 注册、
    EPOLL_CTL_MOD 修改、
    EPOLL_CTL_DEL 删除
    fd:关联的文件描述符;
    event:指向epoll_event的指针;
    如果调用成功返回0,不成功返回-1
    3、epoll_wait函数
    函数声明:int epoll_wait(int epfd,struct epoll_event events,int maxevents,int timeout)
    该函数用于轮询I/O事件的发生;
    参数:
    epfd:由epoll_create 生成的epoll专用的文件描述符;
    epoll_event:用于回传代处理事件的数组;
    maxevents:每次能处理的事件数;
    timeout:等待I/O事件发生的超时值(ms);-1永不超时,直到有事件产生才触发,0立即返回。
    返回发生事件数。-1有错误。

  24. 水平触发和边沿触发的区别?
    在这里插入图片描述
    Epoll的ET模式与LT模式
    ET(Edge Triggered)与LT(Level Triggered)的主要区别可以从下面的例子看出
    eg:
    1. 标示管道读者的文件句柄注册到epoll中;
    2. 管道写者向管道中写入2KB的数据;
    3. 调用epoll_wait可以获得管道读者为已就绪的文件句柄;
    4. 管道读者读取1KB的数据
    5. 一次epoll_wait调用完成
    如果是ET模式,管道中剩余的1KB被挂起,再次调用epoll_wait,得不到管道读者的文件句柄,除非有新的数据写入管道。如果是LT模式,只要管道中有数据可读,每次调用epoll_wait都会触发。

  25. 单例模式为何双指针判空?
    单锁的话每次都需要加锁,比较耗资源;
    举个例子:假如现在没有第二次验校,线程A执行到第一次验校那里,它判断到single ==null。此时它的资源被线程B抢占了,B执行程序,进入同步代码块创建对象,然后释放同步锁,此时线程A又拿到了资源也拿到了同步锁,然后执行同步代码块,因为之前线程A它判断到single ==null,因此它会直接创建新的对象。所以就违反了我们设计的最终目的。

  26. 变量为什么要加volatile关键字
    在上面例子中volatile保证代码指令不会被重排序,首先我们得先了解什么是volatile关键字与及它的特性。

  27. 单例模式?

// 单例模式,线程安全
#include <mutex>
#include <thread>
#include <memory>
template<typename T>
class Singleton {
 public:
    static T* GetInstance() {
        if (nullptr == m_instance_) {
            std::call_once(m_flag_, []{m_instance_=std::make_shared<T>();});
        }
        return m_instance_.get();
    }


 private:
    static std::once_flag m_flag_;
    static std::shared_ptr<T> m_instance_;
};

template<typename T>
std::shared_ptr<T> Singleton<T>::m_instance_ = nullptr;

template<typename T>
std::once_flag Singleton<T>::m_flag_;
  1. handlethread的原理,以及如何使用handle绑定子线程执行?
    正常的Handle创建之后都是在主线程中进行操作。
    //消息处理者,创建一个Handler的子类对象,目的是重写Handler的处理消息的方法(handleMessage())
    private Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what){
                case UPDATE:
                    tv.setText(String.valueOf(msg.arg1));
                    break;
            }
        }
    };

使用HandleThread可以创建一个与子线程绑定的Handle。HandlerThread是一个内部拥有Handler和Looper的特殊Thread,可以方便地在子线程中处理消息。HandlerThread对象start后可以获得其Looper对象,并且使用这个Looper对象实例Handler,之后Handler就可以运行在其他线程中了。
代码实例:

Handle mThreadHandler;
mHandlerThread = new HandlerThread(THREAD_NAME);
mHandlerThread.start();

mThreadLooper = mHandlerThread.getLooper();

mThreadHandler = new Handler(mThreadLooper, new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_THREAD_UPDATE:
                    //在子线程中执行耗时任务
                    SystemClock.sleep(3000);
                    mMainHandler.sendEmptyMessage(MSG_MAIN_UPDATE);
                    break;
                default:
                    break;
            }
            return false;
        }
});
  1. Android中进程间通信的手段有哪些?
    Activity
    Activity的跨进程访问与进程内访问略有不同。虽然它们都需要Intent对象,但跨进程访问并不需要指定Context对象和Activity的 Class对象,而需要指定的是要访问的Activity所对应的Action(一个字符串)。有些Activity还需要指定一个Uri(通过 Intent构造方法的第2个参数指定)。
    在android系统中有很多应用程序提供了可以跨进程访问的Activity,例如,下面的代码可以直接调用拨打电话的Activity。
    Intent callIntent = new Intent(Intent.ACTION_CALL, Uri.parse(“tel:12345678” );
    startActivity(callIntent);
    Content Provider
    Android应用程序可以使用文件或SqlLite数据库来存储数据。Content Provider提供了一种在多个应用程序之间数据共享的方式(跨进程共享数据)。应用程序可以利用Content Provider完成下面的工作
    一、查询数据
    二、修改数据
    三、添加数据
    四、删除数据
    虽然Content Provider也可以在同一个应用程序中被访问,但这么做并没有什么意义。Content Provider存在的目的向其他应用程序共享数据和允许其他应用程序对数据进行增、删、改操作。

广播(Broadcast)
广播是一种被动跨进程通讯的方式。当某个程序向系统发送广播时,其他的应用程序只能被动地接收广播数据。这就象电台进行广播一样,听众只能被动地收听,而不能主动与电台进行沟通。
在应用程序中发送广播比较简单。只需要调用sendBroadcast方法即可。该方法需要一个Intent对象。通过Intent对象可以发送需要广播的数据。

Service
1.利用AIDL Service实现跨进程通信
这是我个人比较推崇的方式,因为它相比Broadcast而言,虽然实现上稍微麻烦了一点,但是它的优势就是不会像广播那样在手机中的广播较多时会有明显的时延,甚至有广播发送不成功的情况出现。
注意普通的Service并不能实现跨进程操作,实际上普通的Service和它所在的应用处于同一个进程中,而且它也不会专门开一条新的线程,因此如果在普通的Service中实现在耗时的任务,需要新开线程。
要实现跨进程通信,需要借助AIDL(Android Interface Definition Language)。Android中的跨进程服务其实是采用C/S的架构,因而AIDL的目的就是实现通信接口。

  1. service权限的管理
    Service 的重启机制:
    onStartCommand(Intent,int,int)方法,这个方法return一个int值,return 的值有四种:
    START_STICKY:如果service进程被kill掉,保留service的状态为开始状态,但不保留递送的intent对象。随后系统会尝试重新创建service,由于服务状态为开始状态,所以创建服务后一定会调用onStartCommand(Intent,int,int)方法。如果在此期间没有任何启动命令被传递到service,那么参数Intent将为null。
    START_NOT_STICKY:“非粘性的”。使用这个返回值时,如果在执行完onStartCommand后,服务被异常kill掉,系统不会自动重启该服务。
    START_REDELIVER_INTENT:重传Intent。使用这个返回值时,如果在执行完onStartCommand后,服务被异常kill掉,系统会自动重启该服务,并将Intent的值传入。
    START_STICKY_COMPATIBILITY:START_STICKY的兼容版本,但不保证服务被kill后一定能重启。

  2. Android里面已经有ActivityManagerService去管理生命周期,为何还需要封装一个SysCtrl管理生命周期?
    ActivityManager管理service的声明周期,但是在真正的项目中,可能不同的模块拉起的顺序会需要变化,因此猜测是需要自定义的方式控制每个service拉起的顺序,达到正常启动的效果。

  3. Android binder权限控制以及安全机制
    SEAndroid安全机制保证Binder读取数据和访问资源的安全性;当Client通过Binder驱动请求与Server发送通信时,Binder驱动会检查Client是否具有与Server通信的SEAndroid安全权限;假设Client与Server的通信数据带有Binder对象或者文件描写叙述符,那么Binder驱动还会进一步检查源进程是否具有向目标进程传输Binder对象或者文件描写叙述符的SEAndroid权限。
    函数selinux_binder_transaction调用LSM模块提供的通用函数avc_has_perm检查Client进程对Server进程是否具有类别为SECCLASS_BINDER的BINDER__CALL的权限,也就是检查Client进程是否有权限向Server进程发送Binder IPC请求,而且将结果返回给调用者。
    从前面的分析能够知道,在Android系统中,全部类型的应用程序进程,以及系统服务进程,比如Service Manager进程、Zygote进程和System Server进程,它们能不能传递文件描写叙述符给对方,不是取决于它们的domain有没有unconfined_domain属性,而是取决于目标进程是否具有使用传递的文件的描写叙述符以及訪问传递的文件的内容的权限。

  4. Android内部的权限控制?
    SEAndroid是一种MAC模式。通过每个进程或文件都会关联一个安全上下文,这个安全上下文由用户、角色、类型、安全级别四个部分组成。这样系统可以制定策略设置每个进程的访问权限。

  5. share_ptr和weak_ptr的联系,weak_ptr如何转化为shared_ptr?
    弱引用使用时需要先调用expired判断当前的弱引用指向的对象是否有效,如果有效的话,需要调用lock获取对应的shareptr,然后调用对应的方法。

   shared_ptr<CA> ptr_a(new CA());     // 输出:CA() called!
    shared_ptr<CB> ptr_b(new CB());     // 输出:CB() called!

    cout << "ptr_a use count : " << ptr_a.use_count() << endl; // 输出:ptr_a use count : 1
    cout << "ptr_b use count : " << ptr_b.use_count() << endl; // 输出:ptr_b use count : 1
    
    weak_ptr<CA> wk_ptr_a = ptr_a;
    weak_ptr<CB> wk_ptr_b = ptr_b;

    if (!wk_ptr_a.expired())
    {
        wk_ptr_a.lock()->show();        // 输出:this is class CA!
    }

    if (!wk_ptr_b.expired())
    {
        wk_ptr_b.lock()->show();        // 输出:this is class CB!
    }

其实weak_ptr本身设计的很简单,就是为了辅助shared_ptr的,它本身不能直接定义指向原始指针的对象,只能指向shared_ptr对象,同时也不能将weak_ptr对象直接赋值给shared_ptr类型的变量,最重要的一点是赋值给它不会增加引用计数
关于lock()成员简单说明一下,lock成员获取到的shared_ptr p指针创建一个临时对象(我们weak_ptr弱引用的体现),这个临时对象同样指向p,即使p执行了reset这样的delete引用的操作,弱引用对象仍然持有智能指针的地址,直到r指针的生命周期结束才会释放。
1.shared_ptr对象能够初始化实际指向一个地址内容而weak_ptr对象没办法直接初始化一个具体地址,它的对象需要由shared_ptr去初始化
2.weak_ptr不会影响shared_ptr的引用计数,因为它是一个弱引用,只是一个临时引用指向shared_ptr。即使用shared_ptr对象初始化weak_ptr不会导致shared_ptr引用计数增加。依此特性可以解决shared_ptr的循环引用问题。
3.weak_ptr没有解引用*和获取指针->运算符,它只能通过lock成员函数去获取对应的shared_ptr智能指针对象,从而获取对应的地址和内容。

  1. new和malloc的区别?
    首先new和delete是c++的关键字而malloc/free是c的库函数;其次后者申请内存时需要指明申请内存块的大小,对于类型的对象后者不会调用构造和析构函数,也就是说不会进行初始化。

  2. move语义的实现原理
    将资源从一个对象转移到另一个对象
    作用:减少不必要的临时对象的创建、拷贝以及销毁

    1. 原对象不再被使用,如果对其使用会造成不可预知的后果。
    2. 所有权转移,资源的所有权被转移给新的对象。
      // remove_reference表示的是被remove的数据的类型
      template
      typename remove_reference::type&& move(T&& t)
      {
      return static_cast<typename remove_reference::type &&>(t);
      }
      Move的实例见:test_move.cpp
#include <deque>
#include <iostream>
#include <algorithm>
#include <vector>
#include <memory>
#include <sstream>
#include <string>
#include <queue>
using namespace std;

//std::move进行右值引用,可以将左值和右值转为右值引用, 这种操作意味着被引用的值将不再被使用,否则会引起“不可预期的结果”。

class Base
{
public:
    Base(int k)
    {
        p=new int(1);
        q=*p=k;
    }

    ~Base()
    {
        delete p;
    }
    void show()
    {
        cout<<"q address: "<<&q<<endl;
        cout<<"p address: "<<p<<endl;
    }
private:
    int q;
    int *p;
};

int main()
{
    cout << endl << "常规变量-----------------------------------------" << endl;
    int k = 6, s = 7;
    cout << k << " " << s << endl;
    k=std::move(s);
    cout << k << " " << s << endl;
    k=8;
    cout<<k<<endl;

    cout << endl << "常规数组(自动清空)-----------------------------------------" << endl;
    vector<int> data1 = {1, 2};
    vector<int> data2 = {1, 3, 4, 5, 4, 3, 5, 2};
    data1 = std::move(data2);
//     data1 = static_cast<vector<int> &&>(data2);
    cout<< "after move:" << endl<< "data1:";
    for (int foo : data1)  cout << foo << " ";

    cout << endl<< "data2:";
    for (int foo : data2)  cout << foo << " ";

    cout<< endl << endl << "指针变量-----------------------------------------" << endl;
    int m = 3, n = 5;
    int *p = &m, *q = &n;
    p = std::move(q);
    cout<<p<<"       "<<*p<<endl;
    cout<<q<<"       " << *q << endl;

    cout << endl << "class 对象-----------------------------------------" << endl;
    Base ba(5);
    Base bb(2);
    bb=std::move(ba);
    ba.show();
    bb.show();

    cout << endl << "string(自动清空) -----------------------------------------" << endl;
    string str=std::move("deng wen");
    string str1("luo chao");
    str1=std::move(str);
    cout<<&str<<"   "<<str<<endl;
    cout<<&str1<<"   "<<str1<<endl;

    cout << endl << "vector(自动清空) -----------------------------------------" << endl;
    vector<int> vec1={1,2,3,4,5};
    vector<int> vec2={9,0,9};
    vec1=std::move(vec2);
    for (int foo : vec1)  cout << foo << "  ";cout<<endl;
    for (int i=0;i<vec1.size();i++)  cout << &vec1[i] << "   ";
    cout<<endl;

    cout << endl << "&&的真正含义--------------------------------------" << endl;
    int ta = 3; // int &&tb=2;//临时对象的引用,即tb存的是临时对象2的地址
    int tb = 2; //生成对象tb,并将2赋值给tb所指的地址中。 感觉两种的结果一样,但是含义不一样
    cout << &ta << "  " << ta << endl;
    cout << &tb << "  " << tb << endl;
    cout << endl;
    int tc = 1;
    ta = tc;
    tb = tc;
    cout << &ta << "  " << ta << endl;
    cout << &tb << "  " << tb << endl;

    return 0;
}
  1. 区分左值和右值?
    左值:能取地址并且有名字的值为左值;
    右值:不能取地址的、没有名字的就是右值
    在这里插入图片描述
    在这里插入图片描述
  2. auto内部的实现原理?
    auto实现的基石就是模版类型推导;在编译期间,编译器使用传入的参数来推断出auto的类型以及对于模板来说就是推导出type以及函数的参数类型。
    分三种情况:
    1.ParamType是一个指针或者引用,但是不是一个universal引用(universal引用会在Item 24中介绍。目前你只需要知道它们是存在的并且和左值引用以及右值引用是不同的)
    如果expr是一个引用,那么忽略引用部分,然后使用expr的类型和ParamType进行模式匹配,从而得出T
    2.ParamType是一个universal引用
    如果expr是一个左值,那么T和ParamType都会被推断成左值引用。如果expr是一个右值,那么“正常规则”(比如Case1里面的)就可以适用了
    3.ParamType既不是一个指针也不是一个引用
    和之前一样,如果expr的类型是引用,那么忽略该引用部分
    在忽略了引用部分后,如果expr是一个常量,那么也忽略掉。如果它是volatile,也忽略掉。
    4.在模板类型推断时,数组或者函数类型的参数会退化成指针,除非他们用了引用形式
    对于括号初始化的区别对待是auto类型推断和模板类型推断唯一不一样的地方。当一个auto声明的变量被用括号初始化后,其推断类型是一个std::initilializer_list的实例。但是如果将相同的初始化值传递给对应的模板,类型推断就出错,代码也通不过编译:
auto x = {11,23,9}; // x's type is std::initializer_list<int>

template<typename T> //template with parameter declaration equivalent to x's declaration
void f(T param);

f({11,23,9}); //error!can't deduce type for T

然而,如果指定模板的param是针对某位置类型T的std::initializer_list类型,那么模板类型推断会推断出T的类型:

tmeplate<typename T>
void f(std::initializer_list<T> initList);

f({11,23,9}); //T deduced as int, and initList's type is std::initializer_list<int>

记忆要点
1.auto类型推断一般和模板类型推断一样,但是auto类型推断假设括起来的初始化值表示std::initializer_list类型,但模板类型推断就不会。
2.对函数的返回类型或者lambda表达式的参数使用auto时,其应用的是模板类型推断规则,而不是auto的类型推断规则。

  1. 虚函数指针指向的一个分配,父类何时创建虚函数表?
    在类设计的时候,虚函数表直接可以从基类继承,如果覆盖了某个虚函数,那么虚函数的指针也会被替换,因此可以根据指针找到对应类的虚函数。
    虚函数表属于类,类的所有对象共享这个类的虚函数表。
    虚函数表由编译器在编译时生成,保存在exe的(常量区).rdata只读数据段。
    虚函数表指针随对象走,它发生在对象运行期,当对象创建的时候,虚函数表表指针位于该对象所在内存的最前面。 使用虚函数时,虚函数表指针指向虚函数表中的函数地址即可实现多态。
    虚函数表是在编译期间就已经确定,且虚函数表存放虚函数的地址也是在创建时被确定。
    C++中的虚函数的作用主要是实现了多态机制,即父类类别的指针(或者引用)指向其子类的实例,然后通过父类的指针(或者引用)调用实际子类的成员函数。多态机制可以简单地概括为“一个接口,多种方法”。
      虚函数是通过一张虚函数表(Virtual Table)来实现的,简称为V-Table。在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得极为重要了,它就像一个地图一样,指明了实际所应该调用的函数。

  2. 万能引用?
    universal引用出现在两种上下文中。最通用的情况是在函数模板参数中,就像来自于上面示例代码的这个例子一样:
    template
    void f(T&& param); // param是一个universal引用
    第二个情况是auto声明,包括上面示例代码中的这一行代码:
    auto&& var2 = var1; // var2是一个universal引用
    存在类型推倒才是万能引用
    调用时被初始化为右值就为右值引用,初始化为左值,就为左值引用。

  3. c++11 锁的管理?
    lock_guard
    创建lock_guard对象时,它将尝试获取提供给它的互斥锁的所有权。当控制流离开lock_guard对象的作用域时,lock_guard析构并释放互斥量。
    它的特点如下:
    1.创建即加锁,作用域结束自动析构并解锁,无需手工解锁
    2.不能中途解锁,必须等作用域结束才解锁
    3.不能复制
    const std::lock_guardstd::mutex lock(g_i_mutex);
    我们要尽可能的减小锁定的区域,也就是使用细粒度锁,使用unique_lock
    unique_lock有单独的lock和unlock的接口,可以实现锁的细粒度,可以实现移动和复制,
    可以使用std::unique_lockstd::mutex guard(_mu, std::defer_lock);初始化是不默认上锁;
    还可以调用try_lock尝试上锁,使用比lock_guard更加灵活。
    std::unique_lockstd::mutex guard(_mu);

  4. 函数后面加const的作用?
    修饰常量:const修饰的类型为TYPE的变量value是不可变的
    修饰指针:const char* const pContent; 如果const位于的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量; 如果const位于的右侧,const就是修饰指针本身,即指针本身是常量。
    函数中使用const
    a.传递过来的参数在函数内不可以改变(无意义,因为Var本身就是形参)
    void function(const int Var);
    b.参数指针所指内容为常量不可变
    void function(const char* Var);
    c.参数指针本身为常量不可变(也无意义,因为char* Var也是形参)
    void function(char* const Var);
    d.参数为引用,为了增加效率同时防止修改。修饰引用参数时:
    void function(const Class& Var); //引用参数在函数内不可以改变
    void function(const TYPE& Var); //引用参数在函数内为常量不可变
    防止拷贝
    函数返回值具有const属性,则返回实例只能访问类A中的公有(保护)数据成员和const成员函数,并且不允许对其进行赋值操作
    Const修饰成员变量:const修饰类的成员变量,表示成员常量,不能被修改,同时它只能在初始化列表中赋值
    Const修饰成员函数:const修饰类的成员函数,则该成员函数不能修改类中任何非const成员函数。一般写在函数的最后来修饰。对于const类对象/指针/引用,只能调用类的const成员函数,因此,const修饰成员函数的最重要作用就是限制对于const对象的使用。
    a. const成员函数不被允许修改它所在对象的任何一个数据成员。
    b. const成员函数能够访问对象的const成员,而其他成员函数不可以。
    Const修饰类对象/对象指针/对象引用
    1.const修饰类对象表示该对象为常量对象,其中的任何成员都不能被修改。对于对象指针和对象引用也是一样。
    2.const修饰的对象,该对象的任何非const成员函数都不能被调用,因为任何非const成员函数会有修改成员变量的企图。

  5. static的作用?
    从外部角度,从类的成员变量角度,从类的成员函数的角度分别展开

  6. vector怎样减少拷贝和内存的浪费?
    1.如果当前已经知道长度的话,使用固定长度的vector;
    2.使用引用或者是move的方式减少拷贝的发生

  7. 公司封装的lock和c++的lock有什么区别?
    主要还是公司内部为了统一各个模块的使用去做了这样的一个集成,方便不同的模块调查问题,内部使用的是pthread的mutex互斥锁的接口进行了封装。

  8. 为什么c++在局部静态变量使用的是递归锁?
    递归互斥锁基本上可以被一个线程锁定多次,但不能被另一个线程锁定,除非锁定线程将它们解锁相同的次数。这意味着递归/可重入不会导致死锁,但是我们仍然可以一次访问一个线程。
    因此为了防止在递归初始化时,local static变量初始化时发生死锁,使用递归锁来抛出异常。
    如果发生需要一个线程需要进行递归并且加锁的话,可以使用可重入的锁,递归锁。

  9. 避免死锁的手段?
    1.一次性分配的原则,将线程所用到的资源全都初始化好给线程;
    2.要求每个线程申请新的资源之前需要先释放自己已经拥有的资源
    3.将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出

48.protobuf使用时,怎样保证协议增减的解析?
protobuf内部使用时可以使用每个生成的cc文件的set接口进行初始化,然后变为io流写给手机;接受手机侧的消息时,可以使用包头判断出消息的type,根据type来调用不同cc文件中的input接收数据,然后回调到内部的线程。
增减的时候注意规则,不能破坏tag,不能删除required的字段,可以增减optional的字段。

  1. 大小段字节序的问题怎么判断?
    大小端需要统一转换为网络字节序,网络字节序转换为大端。在这里插入图片描述
/*
Funcion-name:IsLitte_Endian
Funcion-In:none
Funcion-Out: true-litte, false-bigger
*/
bool IsLitte_Endian() {
    int iTestValue = 0x12345678;
    short *sTestValue = (short*)&iTestValue;
    return !(0x1234 == sTestValue[0]);
}
int main() {
    cout<<IsLitte_Endian()<<endl;
    return 0;
}
// 此时在main函数中打印出的应该就是1,也就是当前系统采用的是小端存储
  1. socket建立接连的client和server的创建步骤?
    client:先创建一个socket套接字,然后填充它的sockaddr_in,主要是填入协议族AF_INET,填入端口,端口需要调用htos表示主机字节序转换为网络字节序,ip地址也需要用inet_addr将点分十进制转为网络字节序地址,然后调用connect进行socket连接。

  2. 32位系统和64位系统的区别
    运动能力不同,64位系统可以一次处理八个字节的数据量,32位系统一次只能处理4字节数据量;内存寻址不同,64位理论可以16TB内存,32位最多只有4GB;支持软件不同,32位只能运行32位软件,64位系统一般也能运行32位软件。

  3. socket建立的基本流程用到的函数?

首先创建一个socket套接字,然后将sockaddr_in内部的参数设置好,然后调用connect进行通信;

int socket (int __domain, int __type, int __protocol);
参数:__domain:套接字使用的协议族的信息,AF_INET、AF_INET6、AF_LOCAL
	   __type:套接字类型,SOCKET_RAW、SOCK_STREAM(数据流)、SOCK_DGRAM(数据报)
返回值:sockfd

设置sockaddr_in:
memset(&servaddr, 0, sizeof(servaddr));  
servaddr.sin_family = AF_INET;  // 设置消息的协议族
servaddr.sin_port = htons(8000);  // 主机字节序转换为网络字节序
 if( inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0){  // IP地址的点分十进制转换为整数
        printf("inet_pton error for %s\n",argv[1]);  
	exit(0);  
}  

int connect (int socket, struct sockaddr* servaddr, socklen_t addrlen);
参数:
	int socket:sockfd
	struct sockaddr* servaddr:将sockaddr_in强制转换为sockaddr
	socklen_t addrlen:上面的结构体的长度

设置套接字选项以及获取套接字的选项
经常用于设置套接字的端口重用,SO_REUSEADDR
extern int setsockopt (int sock, int __level, int __optname,
		       const void *__optval, socklen_t __optlen) __THROW;

extern int getsockopt (int sock, int __level, int __optname,
		       void *__optval, socklen_t *optlen) __THROW;

参数:
int sock:sockfd
int __level:所属的协议层,SOL_SOCKET(套接字相关的选项)、IPPROTO_IP(IP层相关的选项)、IPPROTO_TCP(TCP设置套接字相关的属性)
int __optname:具体的选项名,SO_REUSEADDR、TCP_NODELAY(禁用Nagle算法)比较常用
void *__optval:保存和更改的结果
socklen_t *optlen:上面的参数的大小

Server:首先也是需要创建socket套接字,然后需要填充sockaddr_in的值,然后调用bind、listen、accept进行socket的通信;
数据之间的通信可以使用send和recv。

sockaddr_in结构体:
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET; // 协议族
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 主机字节序转换为网络字节序
servaddr.sin_port = htons(6666); // 主机字节序转换为网络字节序

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

extern int listen (int __fd, int __n);
int __n:连接请求的队列长度,如果为6,表示队列中最多同时有6个连接请求

extern int accept (int __fd, struct sockaddr *addr, socklen_t *addr_len);
函数成功执行时返回socket文件描述符,失败时返回-1

  1. 什么是线程安全?
    线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。

  2. map自定义key类型的时候是怎样判断当前的key是不能重复的?
    通过重载operator<()操作符,在我们插入<key, value>时,map会先通过比较函数地函数对象来比对key的大小,然后根据比对结果进行有序存储。c++标准库中,map比较函数的函数对象不可避免地会用到’<'运算,因此一种方法就是直接在自定义类里重载operator<()操作符。

  3. 当map插入数据时,如果key值相同,value值是覆盖么?
    答:如果key在map里面有的话,不会覆盖之前的value,一般先判断之前有没有数据(见4.),有的话,先删除,再去添加。还有一种方法,可以通过value = map[key],利用[]=来实现覆盖,进行数据的更新。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值