C/C++面试/笔试题2022

  1. 多态的实现//超高频,每个面试官都问
    在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。
    如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数,此为多态的表现;

  2. Cpp四种强制类型转换
    const_cast:从字面意思上就可以理解,去除变量的const属性。
    static_cast:静态类型转换,一般用于基本类型间的转换,如char->int
    dynamic_cast:动态转换,同于多态之间的类型转换
    reinterpret_cast:用于不同类型的指针类型的转换。

  3. 类的static成员的特点
    static成员只有一份拷贝,被该类的所有对象所共享;
    static成员只能在类外初始化,并存放在全局(静态)存储区,不计入类的大小中;
    static可以通过类名直接访问,也可以通过对象访问;
    static成员函数只能访问static成员变量,因为其他的数据成员与生成的对象是绑定的,static成员函数不属于任何对象,没有this指针;

  4. 指针和引用的区别//引用sizeof大小和引用本身大小,问了好几次
    引用是被引用对象的一个别名,其只能在定义的时候初始化,并且其值不能改变不能为空
    指针可以在任何时候给其赋值,并且其可以为nullptr
    sizeof引用为其引用对象的大小,sizeof指针为指针本身的大小
    对引用取地址为其引用对象的地址

  5. 谈谈对Cpp内存的理解//栈和堆的区别
    1、栈区(stack)― 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
    2、堆区(heap)― 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
    3、全局区(静态区)(static)― 全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放
    4、文字常量区 ― 常量字符串就是放在这里的。 程序结束后由系统释放
    5、程序代码区 ― 存放函数体的二进制代码。

  6. 谈谈new、delete、malloc、free
    malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
    对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
    因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。

  7. const关键字
    1.const 修饰类的成员变量,表示成员常量,不能被修改。
    2.const修饰函数承诺在本函数内部不会修改类内的数据成员,不会调用其它非 const 成员函数。
    3.如果 const 构成函数重载,const 对象只能调用 const 函数,非 const 对象优先调用非 const 函数。
    4.const 函数只能调用 const 函数。非 const 函数可以调用 const 函数。
    5.类体外定义的 const 成员函数,在定义和声明处都需要 const 修饰符。。
    int const *p / const int *p; //value是常数
    int * const p; //常指针
    int *const p const; //常指针、value值也是常数

  8. static关键字//修饰局部变量和修饰成员变量的区别很重要

    1.static修饰全局变量

    当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性,其它的源文件也能访问。

    未加static的全局变量,在符号表中是global符号,其他目标文件可见,这样的符号是要参与符号解析的。加了static之后,是local符号,其他目标文件不可见,只在当前文件中可见,不参与符号解析过程。所以多个源文件可以定义同名的static全局变量,不会产生重定义错误。

    修饰全局变量是改变变量的作用域,让它只能在本文件中使用。

    2. 修饰局部变量时,使它放在.data 或者.bss段,默认初始化为0,初始化不为0放在.data段,没有初始化或初始化为0放在.bss段。程序一运行起来就给他分配内存,并进行初始化,也是唯一一次初始化。它的生存期为整个源程序,程序结束,它的内存才释放。但是其作用域仍与自动变量相同,只能在定义该变量的函数内使用该变量。退出该函数后, 尽管该变量还继续存在,但不能使用它。

    修饰局部变量是改变它的生存期,变为和整个程序的生命周期一样。

    3.修饰普通函数时,和修饰全局变量一样。函数经过编译产生一个函数符号,被static修饰后,就变为local符号,不参与符号解析,只在本文件中可见。

    4.修饰类的成员变量时。就变成静态成员变量,不属于对象,而属于类。不能在类的内部初始化,类中只能声明,定义需要在类外。类外定义时,不用加static关键字,只需要表明类的作用域。

    5、修饰类的成员函数时。变成静态成员函数,也不属于对象,属于类。形参不会生成this指针,仅能访问类的静态数据和静态成员函数。调用不依赖对象,所以不能作为虚函数。用类的作用域调用。

    6、如果你希望在一个函数中对一个变量只执行一次初始化,以后不再初始化,使用上一次结果,就应该使用静态局部变量。

  9. 构造函数为什么不能是虚函数

    1.调用虚函数需要访问虚表,访问虚表需要虚指针,构造函数在调用前根本不存在虚指针,这带来了先有鸡还是先有蛋的问题;
    2.多态是根据指针指向的具体对象类型调用对应的方法,前提当然是这个对象是已经构建出来了;
    3.构造函数是定义一个对象时自动调用的,用户不能自己调用构造函数,所以没必要是虚函数;

    总结:使用虚函数的前提是对象已经构造出来了,对象还没构造,使用什么虚函数?

  10. select、poll、epoll

    select的调用过程如下所示:

    (1)使用copy_from_user从用户空间拷贝fd_set到内核空间

    (2)注册回调函数__pollwait

    (3)遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll)

    (4)poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。

    (5)如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd。

    (6)把fd_set从内核空间拷贝到用户空间。

    select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:

    1、 单个进程可监视的fd数量被限制,即能监听端口的大小有限。

          一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.

    2、 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:

           当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。

    (1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

    (2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大

    3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。

    poll:

    poll的实现和select非常相似,只是描述fd集合的方式不同,poll使用pollfd结构而不是select的fd_set结构,其他的都差不多,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

    poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

    它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:

    1、大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。                   

    2、poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

    epoll:

    epoll有EPOLL LT和EPOLL ET两种触发模式,LT是默认的模式,ET是“高速”模式。LT模式下,只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作,而在ET(边缘触发)模式中,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无 论fd中是否还有数据可读。所以在ET模式下,read一个fd的时候一定要把它的buffer读光,也就是说一直读到read的返回值小于请求值,或者 遇到EAGAIN错误。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

    epoll为什么要有EPOLL ET触发模式?

    如果采用EPOLL LT模式的话,系统中一旦有大量你不需要读写的就绪文件描述符,它们每次调用epoll_wait都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率.。而采用EPOLL ET这种边沿触发模式的话,当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符。

    epoll的优点:

    1、没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);
    2、效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;
    即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。

    3、 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。

    select、poll、epoll之间的区别:

    \ select poll epoll
    操作方式 遍历 遍历 回调
    底层实现 数组 链表 哈希表
    IO效率 每次调用都进行线性遍历,时间复杂度为O(n) 每次调用都进行线性遍历,时间复杂度为O(n) 事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到rdllist里面。时间复杂度O(1)
    最大连接数 1024(x86)或 2048(x64) 无上限 无上限
    fd拷贝 每次调用select,都需要把fd集合从用户态拷贝到内核态 每次调用poll,都需要把fd集合从用户态拷贝到内核态 调用epoll_ctl时拷贝进内核并保存,之后每次epoll_wait不拷贝

  11. sql执行顺序 

    from—join—on—where—group by—avg,sum....—having—select—distinct—order by—limit

  12. 字符串的操作(C和C++都说一说)

    C语言中有很多字符串操作函数,常见的有:

    字符串复制:char *strcpy(char *s1, const char *s2);
    求字符串长度:size_t strlen(const char *s);
    字符串拼接:char *strcat(char *s1, const char *s2);
    字符串比较:int strcmp(const char *s1, const char *s2);

    C++中字符串保存在string类型的变量(对象)中,而与上述字符串操作对应的操作:

    字符串复制:string& assign (const string& str);
    求字符串长度:size_t length() const;
    字符串拼接:string& append (const string& str);
    字符串比较:int compare (const string& str) const;

  13. 知道STL吗,挑两个你最常用的容器说一说

    vector:动态扩容数组

    map:key-value数据,自动排序去重。有以下几种不同的map(map、multimap、unordered_map、unordered_multimap),其中map用的是红黑树,unordered_map用的是hash表。
  14. 怎么确定一个程序是C编译的还是C++编译的
    如果编译器在编译cpp文件,那么__cplusplus就会被定义,如果是一个C文件被编译,那么 _STDC_就会被定义,_STDC_是预定义宏,当它被定义后,编译器将按照ANSIC标准来编译C语言程序。

  15. 说一下什么是内存泄漏,如何避免//2.28刚刚遇到一个问的
     内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。最终的结果就是导致OOM。
      内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。
    确保没有在访问空指针。
    每个内存分配函数都应该有一个 free 函数与之对应,alloca 函数除外。
    每次分配内存之后都应该及时进行初始化,可以结合 memset 函数进行初始化,calloc 函数除外。
    每当向指针写入值时,都要确保对可用字节数和所写入的字节数进行交叉核对。
    在对指针赋值前,一定要确保没有内存位置会变为孤立的。
    每当释放结构化的元素(而该元素又包含指向动态分配的内存位置的指针)时,都应先遍历子内存位置并从那里开始释放,然后再遍历回父节点。
    始终正确处理返回动态分配的内存引用的函数返回值。

  16. 一个文件从源码到可执行文件所经历的过程
    1.预处理,产生.ii文件
    2.编译,产生汇编文件(.s文件)
    3.汇编,产生目标文件(.o或.obj文件)
    4.链接,产生可执行文件(.out或.exe文件)

  17. 了解C++新特性吗//如果你说你的教材是谭浩强,他就不会问你了
    1.关键字及新语法:auto、nullptr、for
    2.STL容器:std::array、std::forward_list、std::unordered_map、std::unordered_set
    3.多线程:std::thread、std::atomic、std::condition_variable
    4.智能指针内存管理:std::shared_ptr、std::weak_ptr
    5.其他:std::function、std::bind和lamda表达式

  18. C++构造函数和析构函数在父子类之间的调用顺序

    建立对象时,首先调用基类的构造函数,然后在调用下一个派生类的构造函数,依次类推;

    析构对象时,其顺序正好与构造相反;

  19. 什么是纯虚函数
    相当于一个函数接口,只声明不定义。在其派生类里会重写。有纯虚函数的类为抽象类,不能实例化出对象。

  20. 构造函数和析构函数可以为虚函数吗//虚析构函数天天问
    构造函数不可以,析构函数可以甚至有时候必须声明为虚函数。用基类类型指针绑定派生类实例,析构的时候,如果基类析构函数不是虚函数,则只会析构基类,不会析构派生类对象,从而造成内存泄漏。

  21. 栈和堆的区别,什么时候必须使用堆//冲
    栈是由程序分配的,而堆是由程序员手动去分配释放的。当需要的空间特别大的时候,就必须使用堆,因为栈的大小是有限制的,一般为5MB左右,所以当需要一个大块空间是,必须在堆上开辟空间。

  22. 如何不用sizeof判断一个机器是16位还是32位//不用sizeof(), 判断系统是32位还是16位...
    #include
    #include
    #include
    using namespace std;
    int main()
    {
        unsigned int a = ~0;
        if( a>65536 )
        {
            cout<<"32 bit"<     }
        else
        {
            cout<<"16 bit"<     }
        system("PAUSE");
        return 0;
    }

  23. 用宏定义实现swap
    #define F(a, b) (a = a ^ b);(b = a ^ b);(a = a ^ b);

  24. 头文件<>和""的区别
    遇到#include时,系统先从系统默认的头文件目录中查找头文件
    遇到#include"math.h"时,系统先从当前的目录中搜索,若没有找到,再从系统默认的头文件中找
    故包含系统提供的库函数用#include更快
    当包含用户自定义的.h文件时,使用#include"math.h"更快

  25. 编写string的构造函数、拷贝构造函数、赋值操作符重载和析构函数

    #include
    #include
    
    using namespace std;
    
    class MyString {
    public:
        MyString(const char* pcData = nullptr) {
            if(pcData == nullptr) {
                m_pdata = new char[1];
                *m_pdata = '\0';
            }
            else {
                int len = strlen(pcData);
                m_pdata = new char[len+1];
                strcpy(m_pdata, pcData);
            }
        }
    
        MyString(const MyString& other) {
            int len = strlen(other.m_pdata);
            m_pdata = new char[len+1];
            strcpy(m_pdata, other.m_pdata);
        }
    	
        MyString& operator =(const MyString &str) {
    		if(this == &str)
    			return *this;
    		delete [] m_pdata;
    		m_pdata = nullptr;
    		m_pdata = new char[strlen(str.m_pdata)+1];
    		strcpy(m_pdata, str.m_pdata);
    		return *this;
        }
    
        void Print() {
            cout << this->m_pdata << endl;
        }
    
        ~MyString() {
            delete [] m_pdata;
        }
    
    private:
        char* m_pdata;
    };
    
    int main() {
        MyString mstr;
    	MyString mstr2("hello world!");
    	mstr = mstr2;
    	mstr.Print();
    	mstr2.Print();
    
    	return 0;
    }
    

26. 计算机内部如何存储负数和浮点数?

负数比较容易,就是通过一个标志位和补码来表示。

对于浮点类型的数据采用单精度类型(float)和双精度类型(double)来存储,float数据占用32bit,double数据占用64bit,我们在声明一个变量float f= 2.25f的时候,是如何分配内存的呢?如果胡乱分配,那世界岂不是乱套了么,其实不论是float还是double在存储方式上都是遵从IEEE的规范的,float遵从的是IEEE R32.24 ,而double 遵从的是R64.53。更多可以参考浮点数表示。
无论是单精度还是双精度在存储中都分为三个部分:

  • 1). 符号位(Sign) : 0代表正,1代表为负
  • 2). 指数位(Exponent):用于存储科学计数法中的指数数据,并且采用移位存储
  • 3). 尾数部分(Mantissa):尾数部分
    其中float的存储方式如下图所示:

而双精度的存储方式如下图:
在这里插入图片描述

27.函数调用的过程?

 如下结构的代码:

int main(void)
{
  ...
  d = fun(a, b, c);
  cout<

调用fun()的过程大致如下:

  • main()========
  • 参数拷贝(压栈),注意顺序是从右到左,即c-b-a;
  • 保存d = fun(a, b, c)的下一条指令,即cout<
  • 跳转到fun()函数,注意,到目前为止,这些都是在main()中进行的;
  • fun()=====
  • 移动ebp、esp形成新的栈帧结构;
  • 压栈(push)形成临时变量并执行相关操作;
  • return一个值;
  • 出栈(pop);
  • 恢复main函数的栈帧结构;
  • 返回main函数;
  • main()=======

28.左值和右值

  • 可以取地址的,有名字的,非临时的就是左值
  • 不能取地址的,没有名字的,临时的,通常生命周期就在某个表达式之内的就是右值

29.C和C++的区别?

1). C++是C的超集;
2). C是一个结构化语言,它的重点在于算法和数据结构。C程序的设计首要考虑的是如何通过一个过程,对输入(或环境条件)进行运算处理得到输出(或实现过程(事务)控制),而对于C++,首要考虑的是如何构造一个对象模型,让这个模型能够契合与之对应的问题域,这样就可以通过获取对象的状态信息得到输出或实现过程(事务)控制。

30.int fun() 和 int fun(void)的区别?
这里考察的是c 中的默认类型机制。

  • 在c中,int fun() 会解读为返回值为int(即使前面没有int,也是如此,但是在c++中如果没有返回类型将报错),输入类型和个数没有限制, 而int fun(void)则限制输入类型为一个void。
  • 在c++下,这两种情况都会解读为返回int类型,输入void类型。

31.在C中用const 能定义真正意义上的常量吗?C++中的const呢?
不能。c中的const仅仅是从编译层来限定,不允许对const 变量进行赋值操作,在运行期是无效的,所以并非是真正的常量(比如通过指针对const变量是可以修改值的),但是c++中是有区别的,c++在编译时会把const常量加入符号表,以后(仍然在编译期)遇到这个变量会从符号表中查找,所以在C++中是不可能修改到const变量的。

补充:

  • c中的局部const常量存储在栈空间,全局const常量存在只读存储区,所以全局const常量也是无法修改的,它是一个只读变量。
  • 这里需要说明的是,常量并非仅仅是不可修改,而是相对于变量,它的值在编译期已经决定,而不是在运行时决定。
  • c++中的const 和宏定义是有区别的,宏是在预编译期直接进行文本替换,而const发生在编译期,是可以进行类型检查和作用域检查的。
  • c语言中只有enum可以实现真正的常量。
  • c++中只有用字面量初始化的const常量会被加入符号表,而变量初始化的const常量依然只是只读变量。
  • c++中const成员为只读变量,可以通过指针修改const成员的值,另外const成员变量只能在初始化列表中进行初始化。

32.宏和内联(inline)函数的比较?

  • 首先宏是C中引入的一种预处理功能;
  • 内联(inline)函数是C++中引入的一个新的关键字;C++中推荐使用内联函数来替代宏代码片段;
  • 内联函数将函数体直接扩展到调用内联函数的地方,这样减少了参数压栈,跳转,返回等过程;
  • 由于内联发生在编译阶段,所以内联相较宏,是有参数检查和返回值检查的,因此使用起来更为安全;
  • 需要注意的是, inline会向编译期提出内联请求,但是是否内联由编译器决定(当然可以通过设置编译器,强制使用内联);
  • 由于内联是一种优化方式,在某些情况下,即使没有显示的声明内联,比如定义在class内部的方法,编译器也可能将其作为内联函数。
  • 内联函数不能过于复杂,最初C++限定不能有任何形式的循环,不能有过多的条件判断,不能对函数进行取地址操作等,但是现在的编译器几乎没有什么限制,基本都可以实现内联。
  • 在类中,使用inline定义内联函数时,必须将类的声明和内联成员函数的定义都放在同一个文件(或同一个头文件)中,否则编译时无法进行代码置换。

33.求下列结果(static_cast,dynamic_cast区别)

#include 

using namespace std;
class B
{
public:
    int m_iNum;
    virtual void foo(){cout<<"B"<(pb);
    D *pd2 = dynamic_cast(pb);
    pd1->foo();
    pd2->foo();
}
int main()
{
    B *b=new B();
    func(b);
    return 0;
}

B

pd1为D*类型,pd2为空指针

34.在C++程序中调用被C编译器编译后的函数,为什么要加extern“C”?

被extern "C"修饰的变量和函数是按照C语言方式编译和连接的。

C++语言支持函数重载,C语言不支持函数重载,函数被C++编译器编译后在库中的名字与C语言的不同,假设某个函数原型为:

          void foo(int x, int y);

该函数被C编译器编译后在库中的名字为 _foo, 而C++编译器则会产生像: _foo_int_int 之类的名字。为了解决此类名字匹配的问题,C++提供了C链接交换指定符号 extern “C”。

在C中引用C++语言中的函数和变量时,C++的头文件需添加extern "C",但是在C语言中不能直接引用声明了extern "C"的该头文件,应该仅将C文件中将C++中定义的extern"C"函数声明为extern类型。

35. 头文件中的 ifndef/define/endif 是干什么用的? 该用法和 program once 的区别?
相同点:

  • 它们的作用是防止头文件被重复包含。

不同点

  • ifndef 由语言本身提供支持,但是 program once 一般由编译器提供支持,也就是说,有可能出现编译器不支持的情况(主要是比较老的编译器)。
  • 通常运行速度上 ifndef 一般慢于 program once,特别是在大型项目上, 区别会比较明显,所以越来越多的编译器开始支持 program once。
  • ifndef 作用于某一段被包含(define 和 endif 之间)的代码, 而 program once 则是针对包含该语句的文件, 这也是为什么 program once 速度更快的原因。
  • 如果用 ifndef 包含某一段宏定义,当这个宏名字出现“撞车”时,可能会出现这个宏在程序中提示宏未定义的情况(在编写大型程序时特别需要注意,因为有很多程序员在同时写代码)。相反由于program once 针对整个文件, 因此它不存在宏名字“撞车”的情况, 但是如果某个头文件被多次拷贝,program once 无法保证不被多次包含,因为program once 是从物理上判断是不是同一个头文件,而不是从内容上。

36.指针和引用的区别?//冲冲冲
相同点:

  • 1). 都是地址的概念;
  • 2). 都是“指向”一块内存。指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名;
  • 3). 引用在内部实现其实是借助指针来实现的,一些场合下引用可以替代指针,比如作为函数形参。

不同点:

  • 指针是一个实体,而引用(看起来,这点很重要)仅是个别名;
  • 引用只能在定义时被初始化一次,之后不可变;指针可变;引用“从一而终”,指针可以“见异思迁”;
  • 引用不能为空,指针可以为空;
  • “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小;
  • 指针和引用的自增(++)运算意义不一样;
  • 引用是类型安全的,而指针不是 (引用比指针多了类型检查)
  • 引用具有更好的可读性和实用性。

37.引用占用内存空间吗?//注意hr想问的是sizeof还是引用本身

对引用取地址,其实是取的引用所对应的内存空间的地址。这个现象让人觉得引用好像并非一个实体。但是引用是占用内存空间的,而且其占用的内存和指针一样,因为引用的内部实现就是通过指针来完成的。

38.三目运算符

在C中三目运算符(? :)的结果仅仅可以作为右值,比如如下的做法在C编译器下是会报错的,但是C++中却是可以是通过的。这个进步就是通过引用来实现的,因为下面的三目运算符的返回结果是一个引用,然后对引用进行赋值是允许的。

int main(void)
{
        int a = 8;
        int b = 6;
        (a>b ? a : b) = 88;
        cout<

39.指针数组和数组指针的区别

数组指针,是指向数组的指针,而指针数组则是指该数组的元素均为指针。

数组指针,是指向数组的指针,其本质为指针,形式如下。如 int (*p)[n],p即为指向数组的指针,()优先级高,首先说明p是一个指针,指向一个整型的一维数组,这个一维数组的长度是n,也可以说是p的步长。也就是说执行p+1时,p要跨过n个整型数据的长度。数组指针是指向数组首元素的地址的指针,其本质为指针,可以看成是二级指针。

类型名 (*数组标识符)[数组长度]

指针数组,在C语言和C++中,数组元素全为指针的数组称为指针数组,其中一维指针数组的定义形式如下。指针数组中每一个元素均为指针,其本质为数组。如 int *p[n], []优先级高,先与p结合成为一个数组,再由int*说明这是一个整型指针数组,它有n个指针类型的数组元素。这里执行p+1时,则p指向下一个数组元素,这样赋值是错误的:p=a;因为p是个不可知的表示,只存在p[0]、p[1]、p[2]…p[n-1],而且它们分别是指针变量可以用来存放变量地址。但可以这样 *p=a; 这里*p表示指针数组第一个元素的值,a的首地址的值。

类型名 *数组标识符[数组长度]

40.左值引用与右值引用

左值引用就是我们通常所说的引用,如下所示。左值引用通常可以看作是变量的别名。

type-id & cast-expression 

// demo
int a = 10
int &b = a

int &c = 10	// 错误,无法对一个立即数做引用

const int &d = 10	// 正确, 常引用引用常数量是ok的,其等价于 const int temp = 10; const int &d = temp	

右值引用是 C++11 新增的特性,其形式如下所示。右值引用用来绑定到右值,绑定到右值以后本来会被销毁的右值的生存期会延长至与绑定到它的右值引用的生存期。

type-id && cast-expression  

// demo
int &&var = 10;	// ok

int a = 10
int &&b = a	// 错误, a 为左值

int &&c = var	// 错误,var 为左值

int &&d = move(a)	// ok, 通过move得到左值的右值引用

在汇编层面右值引用做的事情和常引用是相同的,即产生临时量来存储常量。但是,唯一 一点的区别是,右值引用可以进行读写操作,而常引用只能进行读操作。

41.右值引用的意义

  • 右值引用支持移动语义的实现,可以减少拷贝,提升程序的执行效率

    // move example
    #include       // std::move
    #include      // std::cout
    #include        // std::vector
    #include        // std::string
     
    int main () {
      std::string foo = "foo-string";
      std::string bar = "bar-string";
      std::vector myvector;
     
      myvector.push_back (foo);                    // copies
      myvector.push_back (std::move(bar));         // moves
     
      std::cout << "myvector contains:";
      for (std::string& x:myvector) std::cout << ' ' << x;
      std::cout << '\n';
      
      std::cout << "foo:"<

    输出结果如下:

    myvector contains: foo-string bar-string
    foo:foo-string
    bar:

  • 右值引用可以使重载函数变得更加简洁。右值引用可以适用 const T& 和 T& 形式的参数。

42.什么是面向对象(OOP)?面向对象的意义?

Object Oriented Programming, 面向对象是一种对现实世界理解和抽象的方法、思想,通过将需求要素转化为对象进行问题处理的一种思想。其核心思想是数据抽象、继承和动态绑定(多态)。
面向对象的意义在于:将日常生活中习惯的思维方式引入程序设计中;将需求中的概念直观的映射到解决方案中;以模块为中心构建可复用的软件系统;提高软件产品的可维护性和可扩展性。

43.解释下封装、继承和多态?//天天问,面试官会问如果你给C++知识点分类,你怎么分,我答了这个和面向对象还有数据结构

1). 封装
封装是实现面向对象程序设计的第一步,封装就是将数据或函数等集合在一个个的单元中(我们称之为类)。
封装的意义在于保护或者防止代码(数据)被我们无意中破坏。
从封装的角度看,public, private 和 protected 属性的特点如下。

不管哪种属性,内类都是可以访问的
public 是一种暴露的手段,比如暴露接口,类的对象可以访问
private 是一种隐藏的手段,类的对象不能访问
protected 成员:
和 public 一样可以被子类继承
和 private 一样不能在类外被直接调用
特例:在衍生类中可以通过衍生类对象访问

2). 继承
继承主要实现重用代码,节省开发时间。
子类可以继承父类的一些东西。
a.公有继承(public) 公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态(基类的私有成员仍然是私有的,不能被这个派生类的子类所访问)。
b.私有继承(private) 私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员(并且不能被这个派生类的子类所访问)。
c.保护继承(protected) 保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员(并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的)。
这里特别提一下虚继承。虚继承是解决C++多重继承问题(其一,浪费存储空间;第二,存在二义性问题)的一种手段。比如菱形继承,典型的应用就是 iostream, 其继承于 istream 和 ostream,而 istream 和 ostream 又继承于 ios。

3).多态
多态是指通过基类的指针或者引用,在运行时动态调用实际绑定对象函数的行为。与之相对应的编译时绑定函数称为静态绑定。多态是设计模式的基础,多态是框架的基础。

44.什么时候生成默认构造函数(无参构造函数)?什么时候生成默认拷贝构造函数?什么是深拷贝?什么是浅拷贝?默认拷贝构造函数是哪种拷贝?什么时候用深拷贝?//面试官一般问你用过什么构造函数,或者问const引用经常用来干嘛的
1). 没有任何构造函数时,编译器会自动生成默认构造函数,也就是无参构造函数;当类没有拷贝构造函数时,会生成默认拷贝构造函数。
2). 深拷贝是指拷贝后对象的逻辑状态相同,而浅拷贝是指拷贝后对象的物理状态相同;默认拷贝构造函数属于浅拷贝。
3). 当系统中有成员指代了系统中的资源时,需要深拷贝。比如指向了动态内存空间,打开了外存中的文件或者使用了系统中的网络接口等。如果不进行深拷贝,比如动态内存空间,可能会出现多次被释放的问题。是否需要定义拷贝构造函数的原则是,类是否有成员调用了系统资源,如果定义拷贝构造函数,一定是定义深拷贝,否则没有意义。

45.虚析构函数的作用?//重要重要

基类采用虚析构函数可以防止内存泄漏。比如下面的代码中,如果基类 A 中不是虚析构函数,则 B 的析构函数不会被调用,因此会造成内存泄漏。

class A{
public:
  A(){}
  //~A(){}
  virtual ~A(){cout << "A disconstruct" << endl;}  // 虚析构
//   ~A(){cout << "A disconstruct" << endl;}  // 析构

};

class B : public A{
public:
  B(){
    // new memory
    // ...
    cout << "B construct" << endl;
  }
  ~B(){
    // delete memory
    // ...
    cout << "B disconstruct" << endl;
  }
};

int main(int argc, char **argv)
{
  A *p = new B;
  
  // some operations
  // ...
  
  delete p;  // 由于基类中是虚析构,这里会先调用B的析构函数,然后调用A的析构函数
  
  return 0;
}

但并不是要把所有类的析构函数都写成虚函数。因为当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里面来存放虚函数指针,这样就会增加类的存储空间。所以,只有当一个类被用来作为基类的时候,才把析构函数写成虚函数。 

46.细看拷贝构造函数

对于 class A,它的拷贝构造函数如下:

 A::A(const A &a){}

1) 为什么必须是当前类的引用呢?
循环调用。如果拷贝构造函数的参数不是当前类的引用,而是当前类的对象,那么在调用拷贝构造函数时,会将另外一个对象直接传递给形参,这本身就是一次拷贝,会再次调用拷贝构造函数,然后又将一个对象直接传递给了形参,将继续调用拷贝构造函数……这个过程会一直持续下去,没有尽头,陷入死循环。

只有当参数是当前类的引用时,才不会导致再次调用拷贝构造函数,这不仅是逻辑上的要求,也是 C++ 语法的要求。

2) 为什么是 const 引用呢?
拷贝构造函数的目的是用其它对象的数据来初始化当前对象,并没有期望更改其它对象的数据,添加 const 限制后,这个含义更加明确了。

另外一个原因是,添加 const 限制后,可以将 const 对象和非 const 对象传递给形参了,因为非 const 类型可以转换为 const 类型。如果没有 const 限制,就不能将 const 对象传递给形参,因为 const 类型不能直接转换为非 const 类型,这就意味着,不能使用 const 对象来初始化当前对象了。

47.C++的编译环境
如下图所示,C++的编译环境由如下几部分构成:C++标准库、C语言兼容库、编译器扩展库及编译模块。

在这里插入图片描述

#include  //C++标准库,不带".h"
#include  //C语言兼容库,由编译器厂商提供

值得注意的是,C语言兼容库功能上跟C++标准库中的C语言子库相同,它的存中主要为了兼容C语言编译器,也就是说如果一个文件只包含C语言兼容库(不包含C++标准库),那么它在C语言编译器中依然可以编译通过。

48.Most vexing parse

自己编写一个类

假设自己写了这么一个类,我们想调用 copy 构造:

class String {
public:
    String() {
        cout << "dctor" << endl;
    }

    String(const string &name) {
        cout << name << endl;
    }
};

调用实现:

int main() {
    char *t = "helloworld";
    String s(string(t));        // no result
    return 0;
}

这种没有任何结果输出,理想情况下,我们会认为它会调用所谓的拷贝构造,可事实呢,这行被编译器认为是函数声明!

上述传递的是一个匿名对象,被解析成了函数名为 s,带了一个参数(函数指针指向参数 t 并返回 string 对象的函数),返回一个 String 对象的函数声明。

像这种问题被称为:"Most Vexing Parse"

解决方案:

Scott Meyers 在 Effective C++中提到有如下解决方案:

String s((string(t)));        // ok

在外部再次添加一个括号!

另外在 C++11 中也可以使用 Uniform initialization(统一初始化)来处理这种歧义:

String ss{string(t)};        // ok

49.c++有哪些技术可以代替宏

  • 常量定义 换用const
  • 函数定义 换用内联函数

50.auto使用细则

1).auto与指针和引用结合起来使用

 用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&

#include 
using namespace std;
int main()
{
	int a = 10;
	auto b = &a;   //自动推导出b的类型为int*
	auto* c = &a;  //自动推导出c的类型为int*
	auto& d = a;   //自动推导出d的类型为int
	//打印变量b,c,d的类型
	cout << typeid(b).name() << endl;//打印结果为int*
	cout << typeid(c).name() << endl;//打印结果为int*
	cout << typeid(d).name() << endl;//打印结果为int
	return 0;
}

注意:用auto声明引用时必须加&,否则创建的只是与实体类型相同的普通变量,只不过将其换了个姓名而已。

2).在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。

void TestAuto()
{
 auto a = 1, b = 2; 
 auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}

3).auto不能推导的场景

(1).auto做为函数的参数

// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}

(2).auto不能直接用来声明数组

void TestAuto()
{
 int a[] = {1,2,3};
 auto b[] = {4,5,6};
}

为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等
进行配合使用。

51.const

在C语言中,习惯使用#define来定义常量,例如#define PI 3.14,C++提供了一种更灵活、更安全的方式来定义常量,即使用const修饰符来定义常量。例如const float PI = 3.14;

const可以与指针一起使用,它们的组合情况复杂,可归纳为3种:指向常量的指针、常指针和指向常量的常指针。

指向常量的指针:一个指向常量的指针变量。

const char* pc = "abcd";
//该方法不允许改变指针所指的变量,即
    pc[3] = ‘x';   是错误的,
//但是,由于pc是一个指向常量的普通指针变量,不是常指针,因此可以改变pc所指的地址,例如
    pc = "ervfs";
//该语句付给了指针另一个字符串的地址,改变了pc的值。


常指针:将指针变量所指的地址声明为常量

char* const pc = "abcd";
//创建一个常指针,一个不能移动的固定指针,可更改内容,如
    pc[3] = 'x';
//但不能改变地址,如
    pc = 'dsff';  不合法


指向常量的常指针:这个指针所指的地址不能改变,它所指向的地址中的内容也不能改变。

const char* const pc = "abcd";
//内容和地址均不能改变

52.函数重载

在C++中,用户可以重载函数。这意味着,在同一作用域内,只要函数参数的类型不同,或者参数的个数不同,或者二者兼而有之,两个或者两个以上的函数可以使用相同的函数名。

说明:

调用重载函数时,函数返回值类型不在参数匹配检查之列。因此,若两个函数的参数个数和类型都相同,而只有返回值类型不同,则不允许重载。

int mul(int x, int y);
double mul(int x, int y);

函数的重载与带默认值的函数一起使用时,有可能引起二义性。

void Drawcircle(int r = 0, int x = 0, int y = 0);
void Drawcircle(int r);
Drawcircle(20);

在调用函数时,如果给出的实参和形参类型不相符,C++的编译器会自动地做类型转换工作。如果转换成功,则程序继续执行,在这种情况下,有可能产生不可识别的错误。

void f_a(int x){cout<<"int"<

53.友元
类的主要特点之一是数据隐藏和封装,即类的私有成员(或保护成员)只能在类定义的范围内使用,也就是说私有成员只能通过它的成员函数来访问。但是,有时为了访问类的私有成员而需要在程序中多次调用成员函数,这样会因为频繁调用带来较大的时间和空间开销,从而降低程序的运行效率。为此,C++提供了友元来对私有或保护成员进行访问。友元包括友元函数和友元类。

友元函数
友元函数既可以是不属于任何类的非成员函数,也可以是另一个类的成员函数。友元函数不是当前类的成员函数,但它可以访问该类的所有成员,包括私有成员、保护成员和公有成员。

在类中声明友元函数时,需要在其函数名前加上关键字friend。此声明可以放在公有部分,也可以放在保护部分和私有部分。友元函数可以定义在类内部,也可以定义在类外部。

1).将非成员函数声明为友元函数

#include 
using namespace std;
class Score{
private:
    int mid_exam;
    int fin_exam;
public:
    Score(int m, int f);
    void showScore();
    friend int getScore(Score &ob);
};

Score::Score(int m, int f)
{
    mid_exam = m;
    fin_exam = f;
}

int getScore(Score &ob)
{
    return (int)(0.3 * ob.mid_exam + 0.7 * ob.fin_exam);
}

int main()
{
    Score score(98, 78);
    cout << "成绩为: " << getScore(score) << endl;

    return 0;
}

说明:

友元函数虽然可以访问类对象的私有成员,但他毕竟不是成员函数。因此,在类的外部定义友元函数时,不必像成员函数那样,在函数名前加上“类名::”。
因为友元函数不是类的成员,所以它不能直接访问对象的数据成员,也不能通过this指针访问对象的数据成员,它必须通过作为入口参数传递进来的对象名(或对象指针、对象引用)来访问该对象的数据成员。
友元函数提供了不同类的成员函数之间、类的成员函数与一般函数之间进行数据共享的机制。尤其当一个函数需要访问多个类时,友元函数非常有用,普通的成员函数只能访问其所属的类,但是多个类的友元函数能够访问相关的所有类的数据。
例子:一个函数同时定义为两个类的友元函数

#include 
#include 
using namespace std;

class Score;    //对Score类的提前引用说明
class Student{
private:
    string name;
    int number;
public:
    Student(string na, int nu) {
        name = na;
        number = nu;
    }
    friend void show(Score &sc, Student &st);
};

class Score{
private:
    int mid_exam;
    int fin_exam;
public:
    Score(int m, int f) {
        mid_exam = m;
        fin_exam = f;
    }
    friend void show(Score &sc, Student &st);
};

void show(Score &sc, Student &st) {
    cout << "姓名:" << st.name << "  学号:" << st.number << endl;
    cout << "期中成绩:" << sc.mid_exam << "  期末成绩:" << sc.fin_exam << endl;
}

int main() {
    Score sc(89, 99);
    Student st("白", 12467);
    show(sc, st);

    return 0;
}


2).将成员函数声明为友元函数

一个类的成员函数可以作为另一个类的友元,它是友元函数中的一种,称为友元成员函数。友元成员函数不仅可以访问自己所在类对象中的私有成员和公有成员,还可以访问friend声明语句所在类对象中的所有成员,这样能使两个类相互合作、协调工作,完成某一任务。

#include 
#include 
using namespace std;

class Score;    //对Score类的提前引用说明
class Student{
private:
    string name;
    int number;
public:
    Student(string na, int nu) {
        name = na;
        number = nu;
    }
    void show(Score &sc);
};

class Score{
private:
    int mid_exam;
    int fin_exam;
public:
    Score(int m, int f) {
        mid_exam = m;
        fin_exam = f;
    }
    friend void Student::show(Score &sc);
};

void Student::show(Score &sc) {
    cout << "姓名:" << name << "  学号:" << number << endl;
    cout << "期中成绩:" << sc.mid_exam << "  期末成绩:" << sc.fin_exam << endl;
}

int main() {
    Score sc(89, 99);
    Student st("白", 12467);
    st.show(sc);

    return 0;
}

说明:

一个类的成员函数作为另一个类的友元函数时,必须先定义这个类。并且在声明友元函数时,需要加上成员函数所在类的类名;
友元类
可以将一个类声明为另一个类的友元

class Y{
    ···
};
class X{
    friend Y;    //声明类Y为类X的友元类
};

当一个类被说明为另一个类的友元类时,它所有的成员函数都成为另一个类的友元函数,这就意味着作为友元类中的所有成员函数都可以访问另一个类中的所有成员。

友元关系不具有交换性和传递性。

54.输出一个函数所在文件名,行数和函数//笔试考的

#include 

using namespace std;


int main(int argc, char **argv)
{
        printf("File    Fame: %s\n", __FILE__);      //文件名
        printf("Present Line: %d\n", __LINE__);      //所在行
        printf("Present Function: %s\n", __func__);  //函数名

        return 0;
}

输出: 

File    Fame: D:资料\新建文件夹\输出代码所在文件,行和函数\main.cpp
Present Line: 9
Present Function: main

55.什么时候推荐使用protected?

protected在派生类中的访问属性依然是protected,但在派生类的派生类中为private属性,因此当希望某些成员和方法只希望直接继承的派生类使用时,推荐使用protected。

56.一道算法思想问题:一个人想找到最高的山,但他只能爬到山顶才能发现其他山比当前山高还是低,那么他应该怎样找到最高的山或者近似最高的山呢?

我答了BFS相关的...但是面试官说了这样人会累死(●—●)

面试官给了个大致方向是梯度上升。

57.梯度下降(上升)

梯度下降(上升)法基于的思想是:

要找到某函数的 最小(大)值,最好的方法是沿着该函数的梯度(反)方向探寻。如果梯度记为这里写图片描述,则函数f(x,y)的梯度由 下式表示:这里写图片描述,这是机器学习中最易造成混淆的一个地方,但在数学上并不难,需要做的只是牢记这些符号的意义。这个梯度意味着要沿x的方向移动 这里写图片描述,沿y的方向移动这里写图片描述,其中,函数’f(x,y)必须要在待计算的点上有定义并且可微。

梯度下降(上升)算法每次沿梯度(反)方向移动一步。梯度算子总是指向函数值减少(增加)最快的方向。这里所说的是移动方向,而未提到移动量的大小。该量值称为步长,记作这里写图片描述。用向量来表示的话,梯度算法的迭代公式如下:这里写图片描述。该公式将一直被迭代执行,直至达到某个停止条件为止,比如迭代次数达到某个指定值或算法达到某个可以允许的误差范闱。
参考 https://blog.csdn.net/haoronge9921/article/details/80804587

例子:

关于梯度上升法和梯度下降法的原理,大多数都是纯理论的解释和公式的推导,没有一种直观的表达方式。

在这我分别举出两个简单而又直观的例子,大家就明白了,为什么梯度下降法一定是减梯度,而梯度上升法一定是加梯度。

 

例子:

梯度下降:

如用梯度下降法求此函数的极小值, 

在x1,x2点分别可导,

在x1处导数为负数,在此函数中,(-∞,0)区间,对任意一点xi,导数都为负数;

在x2处导数为正数,在此函数中,(0,+∞)区间,对任意一点xi,导数都为正数;

这样,在梯度下降中,公式 就容易解释了,在(-∞,0)区间,导数为负数,更新的在增大(比如原来的xi处于点x=-4,在向最小值y逼近的时候,x一直在增大),一直向最低点逼近;同样在(0,+∞)区间,导数为正数,更新的在减小,一直向最低点逼近。

在梯度上升法是在逻辑回归中求概率最大值,即求最大似然函数的最大值用到的方法,

的极大值为例; 

在此函数中,(-∞,0)区间,对任意一点xi,导数都为正数;

                      (0,+∞)区间,对任意一点xi,导数都为负数;

在梯度上升中,公式 在(-∞,0)区间,导数为正数,更新的在增大,一直逼向最高点;同样在(0,+∞)区间,导数为负数,在减小,一直向最高点逼近。

反向推理,就能明白梯度上升为什么是加梯度,而梯度下降是减梯度。

原文链接:https://blog.csdn.net/weixin_39631030/article/details/81260960

58.虚析构函数(我又写了一遍,我答错了555)

总的来说虚析构函数是为了避免内存泄露,而且是当子类中会有指针成员变量时才会使用得到的。也就说虚析构函数使得在删除指向子类对象的基类指针时可以调用子类的析构函数达到释放子类中堆内存的目的,而防止内存泄露的.

我们知道,用C++开发的时候,用来做基类的类的析构函数一般都是虚函数。可是,为什么要这样做呢?下面用一个小例子来说明:

#include
using namespace std;

class ClxBase
{
    public:
        ClxBase() {};
        virtual ~ClxBase() { cout<<"delete ClxBase"<DoSomething();
     delete pTest;
    return 0;
}
//输出
Do something in class ClxDerived!
delete ClxDerived
delete ClxBase

但是,如果把类ClxBase析构函数前的virtual去掉,那输出结果就是下面的样子了:

//输出
Do something in class ClxDerived!
delete ClxBase

没有调动子类的析构函数

59.你知道的设计模式

我只答了工厂模式和单例模式,讲了一下MVC

(1条消息) 23 种设计模式详解(全23种)_人生智慧的博客-CSDN博客_设计模式

60.生成器模式

问完我知道的设计模式后特意问了这个!

定义:封装一个复杂对象构造过程,并允许按步骤构造。

定义解释: 我们可以将生成器模式理解为,假设我们有一个对象需要建立,这个对象是由多个组件(Component)组合而成,每个组件的建立都比较复杂,但运用组件来建立所需的对象非常简单,所以我们就可以将构建复杂组件的步骤与运用组件构建对象分离,使用builder模式可以建立。

生成器模式结构中包括四种角色:

(1)产品(Product):具体生产器要构造的复杂对象;

(2)抽象生成器(Bulider):抽象生成器是一个接口,该接口除了为创建一个Product对象的各个组件定义了若干个方法之外,还要定义返回Product对象的方法(定义构造步骤);

(3)具体生产器(ConcreteBuilder):实现Builder接口的类,具体生成器将实现Builder接口所定义的方法(生产各个组件);

(4)指挥者(Director):指挥者是一个类,该类需要含有Builder接口声明的变量。指挥者的职责是负责向用户提供具体生成器,即指挥者将请求具体生成器类来构造用户所需要的Product对象,如果所请求的具体生成器成功地构造出Product对象,指挥者就可以让该具体生产器返回所构造的Product对象。(按照步骤组装部件,并返回Product)

生成器模式的优缺点

优点

  • 将一个对象分解为各个组件

  • 将对象组件的构造封装起来

  • 可以控制整个对象的生成过程

缺点

  • 对不同类型的对象需要实现不同的具体构造器的类,这可能会大大增加类的数量

61.函数指针

#include 

using namespace std;
int add(int a,int b){return a+b;}
int (*funcp)(int,int)=add;
int main()
{
    cout<

62.知道反射机制吗?C++里没有反射机制,你觉得应该怎样实现

我答了用用map,把每一个创建的实例和它对应的函数指针对应起来,面试官没有说对不对也没有补充,回来参考了一下下面这个_(:ι」∠)_

(1条消息) C++ 实现反射机制_YzlCoder的记事本-CSDN博客_c++ 反射机制

63.面对大量重复代码时,你如何处理?

使用C++泛型编程,模板类。

64.Socket编程知道吗

65.TCP,UDP区别

  • TCP 是面向连接的,UDP 是面向无连接的
  • UDP程序结构较简单
  • TCP 是面向字节流的,UDP 是基于数据报的
  • TCP 保证数据正确性,UDP 可能丢包
  • TCP 保证数据顺序,UDP 不保证

66.红黑树最大子树之间的高度是多少

红黑树是黑色平衡的树,左子树与右子树高度差不会超过2倍

67.vector怎么扩容//vector必问

  • 采用采用成倍方式扩容,可以保证常数的时间复杂度,而增加指定大小的容量只能达到O(n)的时间复杂度,因此,使用成倍的方式扩容。//面试官问如果只pushback一个,那时间复杂度多大?--O(1)
  • 根据查阅的资料显示,考虑可能产生的堆空间浪费,成倍增长倍数不能太大,使用较为广泛的扩容方式有两种,以2二倍的方式扩容,或者以1.5倍的方式扩容。
  • 以2倍的方式扩容,导致下一次申请的内存必然大于之前分配内存的总和,导致之前分配的内存不能再被使用,所以最好倍增长因子设置为(1,2)之间
  • 为了防止申请内存的浪费,现在使用较多的有2倍与1.5倍的增长方式,而1.5倍的增长方式可以更好的实现对内存的重复利用,因为更好。

68.在vector中插入200个元素,怎样提高效率

resize()

69.判断一个链表是否是循环链表

用两个指针,一个在前,一个在后,前面的如果能再次追上后面的,就是循环

不要说什么head==p这种,面试官不喜欢遍历

70.讲一下你熟悉的求最短路径算法

Dijkstra和A*

71.二叉查找树(往平衡树贴)

72.红黑树

  • 每一个节点要么红色要么黑色.

  • 根节点是黑色.

  • 所有叶子节点NIL是黑色.

  • 红色节点的左右孩子必定是黑色节点.

  • 从任何一个节点出发,并达到这个节点下面的所有叶子节点的所有路径.这些路径中,含有的黑色节点个数相同.

73.线程锁

  • 互斥量(锁):用于保护关键的代码段,以确保其独占式的访问。
  • 自旋锁:  应用在实时性要求较高的场合(缺点:CPU浪费较大)
  • 读写锁(共享-独占锁):应用场景---大量的读操作  较少的写操作,注意:读读共享, 读写互斥,写优先级高(同时到达)

74.进程和线程的区别//经常问

进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位

75.Mysql优化方式

  • 选取最适用的字段属性
  • 使用连接(JOIN)来代替子查询(Sub-Queries)
  • 使用联合(UNION)来代替手动创建的临时表。MySQL从4.0的版本开始支持union查询,它可以把需要使用临时表的两条或更多的select查询合并的一个查询中。
  • 慢查询优化 常见Mysql的慢查询优化方式_菜鸟不会飞-CSDN博客_慢查询

76.虚拟地址//面试官想知道怎样用比内存更大的程序

  • 虚拟内存是内存管理的一种方式, 它在磁盘上划分出一块空间由操作系统管理,当物理内存耗尽是充当物理内存来使用。它将多个物理内存碎片和部分磁盘空间重定义为连续的地址空间,以此让程序认为自己拥有连续可用的内存。当物理内存不足时,操作系统会将处于不活动状态的程序以及它们的数据全部交换到磁盘上来释放物理内存,以供其它程序使用。
  • 虚拟地址空间:在多任务操作系统中,每个进程都运行在属于自己的内存沙盘中,这个沙盘就是虚拟地址空间(virtual address space)。虚拟地址空间由内核空间(kernel space)和用户模式空间(user mode space)两部分组成。 
  • 虚拟地址会通过页表(page table)映射到物理内存,页表由操作系统维护并被处理器引用,每个进程都有自己的页表。内核空间在页表中拥有较高特权级,因此用户态程序试图访问这些页是会导致一个页错误(page fault)。其中内核空间是持续存在的,并且在所有进程中都映射到同样的物理内存。与此相反,用户模式空间的映射随进程切换的发生而不断变化。

77.从操作系统读入一张图片,经过几个硬件?

我答了硬盘,内存,cache,页块,面试官让我补充ヽ(ー_ー)ノ

78.为什么数组从0开始

从内存模型来看,“下标”也称为“偏移”。

我们知道在C语言中数组名代表首地址(第一个元素的地址),a[0]就是偏移为 0 的位置。a[k]就表示偏移 k 个元素类型大小的位置。得出计算公式:

a[k]_address = base_address + k * type_size

但是钥匙从 1 开始计数,那这个公式就会变为:

a[k]_address = base_address + (k-1) * type_size

对比两个公式,如果从 1 开始编号,每次随机访问数组元素就多了一次减法运算,对于CPU来说就是多了一次减法指令。

数组作为非常基础的数据结构,通过下标访问数组元素又是数组上的基础操作,效率优化应做的很好,所以为了减少一次减法操作,数组选择了从 0 开始编号。

79.虚函数怎么实现的?//注意是实现

C++的虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖(override)的问题,保证其能真实的反应实际的函数。这样,在有虚函数的类的实例中这张表被分配在了这个实例的内存中,所以当我们用父类的指针操作一个子类的时候,这张虚函数表就显得尤为重要了,他就像一个地图一样,指明了实际所应该调用的函数。

虚函数表中只存有一个虚函数的指针地址,不存放普通函数或是构造函数的指针地址。只要有虚函数,C++类都会存在这样的一张虚函数表,不管是普通虚函数  亦或 是 纯虚函数,亦或是 派生类中隐式声明的这些虚函数都会 生成这张虚函数表。

虚函数表创建的时间:在一个类构造的时候,创建这张虚函数表,而这个虚函数表是供整个类所共有的。虚函数表存储在对象最开始的位置。

参考:C++虚函数表详解_amoscykl的博客-CSDN博客_c++虚函数表

80.CPU的时间片是分配给进程还是线程?

线程 

你可能感兴趣的:(C/C++,c++,c语言,面试)