C/C++常见面试题(二)

接前面C/C++常见面试题(一),继续巩固

目录

1 sizeof和strlen的区别

2 宏定义的陷阱

3 不使用sizeof计算出类型或者变量所占的内存的字节数

4 给定一个数判断是否其是2的N次幂

 5 C/C++打印所在文件、行号、函数、日期,时间、遵循的标准   

6 简单说一下重载和重写的区别

7 简单讲一下虚函数和纯虚函数

8 C++的链接属性

9 你了解内联函数吗

10 this 指针

11 头文件中的ifndef/define/endif和program once

12 std::string可以被继承吗(虚析构)

13 内存泄露 

14 栈溢出

15 malloc/free 和 new/delete, new [] / delete [] 区别

16 构造函数和析构函数的执行顺序

17 final 

18 C++11的新特性

19 智能指针

20 struct和class的本质区别

21 C++中的vector和list的区别

22 进程和线程的区别

23 C++引用,左值引用和右值引用,完美转发,循环引用的解决

24 sprintf, strcpy, memcpy函数的区别

25 虚函数可以声明为static吗

26 C++空类,编译器会自动生成哪些函数

27 析构函数最好是虚函数吗?构造函数可以为虚函数吗?


1 sizeof和strlen的区别

        回答这个问题从以下三个方面:

        a. sizeof是关键字、运算符,而strlen是函数

        b. strlen 只能使用char * 做参数,必须以“\0”结尾,一般用于计算字符串的长度,不包括"\0"的所占的空间;sizeof可以用类型或者变量做参数,一般用于计算类型或者变量所占的内存的字节数,因此在计算字符串长度的时候,会包含"\0"所占的内存空间大小。

        c. sizeof 是关键字在编译的时候已经计算好了,strlen是在运行程序的时候进行计算的

2 宏定义的陷阱

#include

#define VHU(x)  x* x*x

int main()

{

        int a = 4;

        printf("%d \n",VHU(a+1));

        return 0;

}        

如上的一个宏定义:

VHU(a+1) = a + 1 * a + 1 * a + 1 = 3*a + 1 = 13

切记宏定义的本质只是简单的替换,因此对于宏定义的操作,一般要求加括号,比如上述的宏定义,要计算x的三次方的话,应该写成 VHU(x) (x)*(x)*(x)

3 不使用sizeof计算出类型或者变量所占的内存的字节数

        利用0地址的转换

        #define SIZEOF(T) ((size_t)((typeof(T)*)0 + 1))

4 给定一个数判断是否其是2的N次幂

        利用2^{n}2^{n}-1相与得到值为0,可以判断出

        仅仅只需要判断 n & (n-1) 是否为0,为0就是2的N次幂,特殊处理一下n <= 0的情况即可

 代码如下:

#include 
#include 


bool Is2_n(int n)
{   
    if (n < 0)
    {
        printf("输入的值是: %d 不在范围内,应该大于0 Fail Exit\n",n);
        exit(0);
    }

    if (n == 0)
    {

        return false;
    }
    
    if ((n & (n-1)) == 0)
    {
        return true;
    }
    else
    {
        return false;
    }

}

int main(int argc,char *argv[])
{

    if (Is2_n(8))
    {
        printf("是2的N次幂\n");
    }
    else
    {
        printf("不是2的N次幂\n");
    }
    
    return 0;
}

 5 C/C++打印所在文件、行号、函数、日期,时间、遵循的标准   

        ANSIC标准定义了可供C语言使用的预定义宏:
        __LINE__ : 在源代码中插入当前源代码行号
        __FILE__ : 在源代码中插入当前源代码文件名                                                            __FUNCTION__:在源代码中插入当前源代码所在函数,ISO C 标准 C89引入,C99为

                                __func__
        __DATE__ : 在源代码中插入当前编译日期
        __TIME__ : 在源代码中插入当前编译时间                                                                                       __STDC__:在函数中查看C/C++遵循的标准,ANSI C标准 是一个非0值,一般的C/C++都遵循

                        ANSI 标准

6 简单说一下重载和重写的区别

        回答一:

        a. 重载和重写是实现C++多态的两种方式,都可以满足对于不同的对象收到相同的消息产生

                不同的行为这一多态特性。重写的实现是利用虚函数实现的。重载是利用相同作用域的

                同名函数的不同的参数列表(参数个数、参数类型)实现的。

        b. 虚函数是基类希望派生类重新定义的函数,派生类重新定义基类虚函数的做法叫做覆盖

        (重写);重载就在允许在相同作用域中存在多个同名的函数,这些函数的参数表不同。

        重载的概念不属于面向对象编程,编译器根据函数不同的形参表对同名函数的名称做修

        饰,然后这些同名函数就成了不同的函数。

        c. 重载的确定是在编译时确定,是静态的,属于编译时多态;虚函数则是在运行时动态确定,

                属于运行时多态。

        回答二:

        重载:

                (1)发生在同一个类中;
                (2)相同的方法名;
                (3)参数列表不同;
                (4)不看返回值,如果出现了只有返回值不同的“重载”,是错的;
        重写:

                (1)发生在子类与父类中;
                (2)相同的方法名;
                (3)相同的参数列表;
                (4)返回值相同 或者 子类方法的返回值是父类方法返回值类型的子类;
                (5)访问修饰符相同 或者 子类方法的修饰符范围 大于 父类;
                (6)抛出的异常相同 或者 子类方法抛出的异常 小于父类;

        
        方法的重写(Overriding)和重载(Overloading)是多态性的不同表现,重写是父类与子类之间

        多 态性的一种表现,重载可以理解成多态的具体表现形式。

        方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次

        序不同,则称为方法的重载(Overloading)。
        方法重写是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一

        样的方法,就称为重写(Overriding)。
        方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。

C/C++常见面试题(二)_第1张图片 具体对比分析

7 简单讲一下虚函数和纯虚函数

        虚函数:

        在类的成员函数前加virtual关键字。

        虚函数是实现多态的基础。一旦基类定义了虚函数,该基类的派生类中的同名函数也自动称

        为虚函数。

        虚函数的重写:派生类中有一个跟基类的完全相同的虚函数,我们就称子类的虚函数重写了

        基类的虚函数。

        “完全相同”是指:函数名、参数、返回值都相同。另外,虚函数的重写也叫做虚函数的覆盖。

        纯虚函数

        在虚函数的后面写上 = 0,则这个函数为纯虚函数。(纯虚函数没有函数体)

        包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也

        不能实例化出对象

        只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚

        函数更体现了接口继承。        

        子类和父类的实例化关系:

        子类可以实例化成父类,父类不能实例化成子类,因为子类可能重写了父类,子类的范畴是

        大于父类的,大到小是可以的,小到大是不行的。

8 C++的链接属性

        链接属性一定程度范围决定着符号的作用域,C++中链接属性有三种:none(无)、external(外部)和 internal(内部)。

        内部链接——如果一个名称对编译单元来说是局部的,在链接的时候其他编译单元无法链接

        到它且不会与其他编译单元中的同样名称相冲突。(例如被关键字static,inline标识)

        外部链接——如果一个名称对编译单元来说不是局部的,而在链接的时候其他的编译单元可

        以访问它,也就是说它可以和别的编译单元交互。

        external,外部链接属性。非常量全局变量和自由函数(除成员函数以外的函数)均默认为外

        部链接的,它们具有全局可见性,在全局范围不允许重名。
        internal,内部链接属性。具有该属性的类型有,const对象,constexpr对象,命令空间内的

        静态对象(static objects in namespace scope),局部变量等
        none,在类中、函数体和代码块中声明的变量默认是具有none链接属性。它和internal一样只

        在当前作用域可见。

        特殊说明:

        (1)inline修饰的变量是内部链接属性,即它在编译时会被直接嵌入到调用它的代码中,而不

        是通过外部符号表进行访问。

        (2) inline修饰的函数主要是为了优化程序,它会让函数在编译时被直接嵌入到调用它的代

        码中,从而减少函数调用的开销。对于链接属性来说,无论是C还是C++,inline函数都具有

        外部链接属性,也就是说,它在编译后会生成对应的标签符号,这些符号可以在其他文件中

        通过外部链接访问到。需要注意的是,虽然inline函数具有外部链接属性,但并不意味着它将

        在整个程序中可见


9 你了解内联函数吗

        定义:

        内联函数就是以inline修饰的函数叫做内联函数,编译时会在调用内联函数的地方展开,没有函数调用占用建立栈帧的开销,它将函数的定义和声明放在一起,是一种编译器优化技术。

        特性:

        1.内联函数会在编译阶段用函数体替换函数调用。缺陷是可能会使可执行程序(或者目标文

        件)变大,优势就是减少了函数调用的开销,提高了运行效率。
        2. inline是一个弱符号,内联函数只是编译器给我们的一个建议,最终函数是否会成为内联函数取决于编译器自己。

        如果我们把递归函数、比较长的函数加上inline的话就会被编译器否决掉。我们可以这样理

        解,内联函数只是我们向编译器发出的一个请求,然而编译器可以接收这个请求,当然也完

        全可以忽略这个请求。
        3.内联函数不建议声明和定义分离,如果分离的话就会导致链接错误,因为inline被展开,此

        时就没有函数地址了,链接就会找不到。

        使用要求:

        1. 函数体比较小(递归一般不行,复杂的代码也不行),频繁调用,性能要求高的场景

        2. 一般放在头文件中

        3. 定义个声明在一起,不然会有链接错误

        4. 可以通过编译器优化来打开,比如 -O2 ,-O0 不会优化,内联的展开是编译器决定的

        优势:

        1. 内联和宏相对于普通函数都可以提高程序的效率,可以进行编译器优化,避免函数调用消

            耗栈空间

        2. 内联相对于宏在编译阶段,而宏在预处理阶段

        3. 内联是函数,在编译阶段插入,宏仅仅是替换

        4. 内联有类型和错误的检查,宏没有,内联的操作不容易出错

        5. 对于C++,内联可以操作类的私有成员,但是宏不能操作累的私有数据成员

10 this 指针

        (1)this指针指向调用它的类或者对象实例,this的值就是类或者对象实例的地址,是一个指

                针常量

        (2)this只能在成员函数的内部使用,本质是成员函数的第一个隐含的指针形参,对象调用

                成员函数时,将对象的地址作为实参传递给this形参,所以对象中国不存储this指针,隐

                含存在,也可以显式的表示出来

        (3)非静态成员函数可以直接用this替代指向对象的指针,静态成员函数不能使用this指针

                ,因为静态成员函数不具体作用于某个对象,因此静态成员函数真实的参数的个数,就

                是实际的参数的个数。

11 头文件中的ifndef/define/endif和program once

        相同:都是用来防止头文件的重复包含。

        区别:

                (1) ifndef 作用于包含语句的之前的所有的代码,program once作用于包含此标识的

                        整个文件,因此program once 速度更快

                (2)ifndef是由语言本身支持的,program once是编译器支持的,因此对于有些老的编

                        译器可能不支持

                (3)ifndef 可以保证互相包含的文件中的内容不会出现宏定义的重复情况,但

                        program once  作用的是整个文件,是从物理上保证一个文件是否被重复包含的,

                        不是从内容上,因此无法保证头文件不被重复包含

12 std::string可以被继承吗(虚析构)

        typedef std::basic_string string; 

        std::string是 std::basic_string模板类的一个特化,由于std::string的析构函数不是virtual 虚函数,这样容易引起内存泄露。对于不是虚析构的Base *A = Derived A1; delete A; 只会调用基类的析构,不会调用派生类的析构。

       C++的类被继承时,基类的析构函数要为虚函数,即虚析构。虚析构可以防止内存泄漏,正确析构指向派生类实例的基类指针。

TIPS:如果不是虚析构函数,基类指针被释放不会调用派生类的析构函数。

13 内存泄露 

定义:

狭义上,内存泄漏是指动态分配的内存未正确的释放导致的,如new之后未delete。

广义上,不再使用的内存未能回收都属于内存泄漏,如已失效的全局map缓存、socket句柄、文件句柄等。

对于长时间运行的服务器后台程序,内存泄漏可能造成十分严重的后果,如性能下降、程序崩溃、系统崩溃等问题。

内存泄漏的产生方式
(1)常发性内存泄漏
产生泄漏的代码被多次执行,每次都会产生内存泄漏。

(2)偶发性内存泄漏
偶发性内存泄漏只在特定场景下会触发,并产生内存泄漏。

当然,偶发性内存泄漏也是相对的,可能原来不常用的业务变为常用的业务,假设不常用业务存在内存泄漏,那此时的内存泄漏就是常发性内存泄漏。

(3)一次性内存泄漏
产生泄漏的代码只会执行一次。

(4)隐式内存泄漏
        隐式内存泄漏是指由于释放内存时效引起的内存泄漏。

这里主要指的是不及时释放内存会引发的其他问题,如内存碎片导致无内存可分配引起的程序或系统崩溃等问题。

        如:

        频繁的new/delete
        free/delete执行后不立即回收内存
        STL中 vector.clear() 不会释放空间
        全局缓存未设置失效机制导致缓存越来越大

内存泄露分类:

(1)未释放

        new 和 malloc 使用后,没有delete 和 free

(2)未匹配

        申请与释放没有正确的匹配, 比如 new a[]; delete a; 应该是delete []a;

  • malloc/free : 只申请/释放空间
  • new/delete : 申请空间,调用构造函数/调用析构函数,释放空间
  • new[]/delete[] : 申请空间,调用多次构造函数/调用多次析构,释放空间

  (3)虚析构

        父类析构函数不是虚函数,当父类指针释放子类对象时,不会调用子类的析构函数,产生内存泄露。

  (4)循环引用

        智能指针shared_ptr ,两个类中互相有对方的智能指针时,会出现这种情况,造成内存泄露,使用weak_ptr来解决循环引用的问题

内存泄露的防范

(1)减少使用堆内存,使用栈内存

(2)不使用裸指针,使用智能指针

(4)使用RALL机制

内存泄露排查思路:

(1)代码检测(静态代码检测工具、动态内存检测工具),工具valgrind 内存泄露定位

(2)代码Review(未释放、未匹配、虚析构、循环引用)

(3)查看日志,输出内存信息

(4)最小化场景复现

14 栈溢出

        栈溢出检测办法:

                已知栈顶,可以给栈顶赋一特定值,查看此值是否改变

        最大栈使用计算

                先分配较大的栈空间,将栈空间赋为一特定值,运行程序,最终计算栈中被改变的值占分配的比率,得到此程序运行需要的最大栈空间

15 malloc/free 和 new/delete, new [] / delete [] 区别

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

(3)new 在申请内存的时候就可以初始化, 而malloc是不允许的,但是calloc可以初始化。另外,由于malloc是库函数,需要相应的库支持,因此某些简易的平台可能不支持,但是new就没有这个问题了,因为new是C++语言所自带的运算符。

16 构造函数和析构函数的执行顺序

构造函数:

        (1)首先调用父类的构造函数;

        (2)调用父类成员变量的构造函数;

        (3)调用类自身的构造函数。

析构函数:

       (1)调用子类的自身的析构函数

       (2)调用父类成员变量的析构函数

        (3)调用父类的析构函数

17 final 

        final关键字是在C++11标准中引入的。它用于限制类、成员函数和虚函数的继承和重写。

        (1)对于类:在类声明中,使用final关键字可以防止该类被继承。

        (2)对于成员函数:在成员函数声明中,使用final关键字可以防止该函数在派生类中被重写(override,覆盖)。
        (3)对于虚函数:在虚函数声明中,使用final关键字可以防止派生类中的进一步重写。例如:

18 C++11的新特性

19 智能指针

20 struct和class的本质区别

本质区别是默认的继承访问权限:class是private, struct是public

21 C++中的vector和list的区别

vector数据结构
        vector和数组类似,拥有一段连续的内存空间,并且起始地址不变。因此能高效的进行随机存取,时间复杂度为o(1);但因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为o(n)。另外,当数组中内存空间不够时,会重新申请一块内存空间并进行内存拷贝。

list数据结构
        list是由双向链表实现的,因此内存空间是不连续的。只能通过指针访问数据,所以list的随机存取非常没有效率,时间复杂度为o(n);但由于链表的特点,能高效地进行插入和删除。

vector和list的区别

        vector拥有一段连续的内存空间,能很好的支持随机存取,因此vector::iterator支持“+”,“+=”,“<”等操作符。

        list的内存空间可以是不连续,它不支持随机访问,因此list::iterator则不支持“+”、“+=”、“<”等

        vector::iterator和list::iterator都重载了“++”运算符。

        总之,如果需要高效的随机存取,而不在乎插入和删除的效率,使用vector;如果需要大量的插入和删除,而不关心随机存取,则应使用list。

22 进程和线程的区别

  • 进程是资源分配最小单位,线程是CPU最小调度单元;
  • 线程运行在进程中;
  • 进程之间无法共享存储空间;而同一进程所产生的线程共享内存空间
  • 同一进程中的两段代码不能同时执行,除非引入多线程。
  • 进程和线程的区别

23 C++引用,左值引用和右值引用,完美转发,循环引用的解决

24 sprintf, strcpy, memcpy函数的区别

  这些函数的区别在于 实现功能以及操作对象不同。

(1)strcpy 函数操作的对象是字符串,完成从源字符串到目的字符串的拷贝功能。

(2)sprintf 函数操作的对象不限于字符串:虽然目的对象是字符串,但是源对象可以是字符串、也可以是任意基本类型的数据。这个函数主要用来实现(字符串或基本数据类型)向字符串的转换功能。如果源对象是字符串,并且指定 %s 格式符,也可实现字符串拷贝功能。

(3)memcpy函数顾名思义就是内存拷贝,实现将一个内存块的内容复制到另一个内存块这一功能。内存块由其首地址以及长度确定。程序中出现的实体对象,不论是什么类型,其最终表现就是在内存中占据一席之地(一个内存区间或块)。因此,memcpy的操作对象不局限于某一类数据类型,或者说可适用于任意数据类型,只要能给出对象的起始地址和内存长度信息、并且对象具有可操作性即可。鉴于memcpy函数等长拷贝的特点以及数据类型代表的物理意义,memcpy函数通常限于同种类型数据或对象之间的拷贝,其中当然也包括字符串拷贝以及基本数据类型的拷贝。

        对于字符串拷贝来说,用上述三个函数都可以实现,但是其实现的效率和使用的方便程度不同:

strcpy 无疑是最合适的选择:效率高且调用方便。
sprintf 要额外指定格式符并且进行格式转化,麻烦且效率不高。
memcpy 虽然高效,但是需要额外提供拷贝的内存长度这一参数,易错且使用不便;并且如果长度指定过大的话(最优长度是源字符串长度 +1),还会带来性能的下降。其实 strcpy 函数一般是在内部调用 memcpy 函数或者用汇编直接实现的,以达到高效的目的。因此,使用memcpy 和 strcpy 拷贝字符串在性能上应该没有什么大的差别。

25 虚函数可以声明为static吗

不可以,因为静态成员函数/变量没有this指针

虚函数是为了实现多态,它允许父类指针访问子类的函数。这种动态绑定是在运行时确定的,即函数的具体实现是在运行时才知道的。而静态成员函数与任何实例无关,它是类的一个属性,可以直接通过类名调用,而不需要通过对象来调用。因此,C++中不允许将静态成员函数声明为虚函数。如果尝试这样做,编译器会报错。

静态函数和静态变量可以直接通过类名调用。

26 C++空类,编译器会自动生成哪些函数

对于空类,声明时,编译器会生成1个字节的占位符。

会生成默认构造函数,析构函数,拷贝构造函数,赋值运算符,, 取址运算符

class Empty
{
  public:
    Empty();                            //缺省构造函数
    Empty(const Empty &rhs);            //拷贝构造函数
    ~Empty();                           //析构函数 
    Empty& operator=(const Empty &rhs); //赋值运算符
    Empty* operator&();                 //取址运算符
    const Empty* operator&() const;     //取址运算符(const版本)
};

27 析构函数最好是虚函数吗?构造函数可以为虚函数吗?

        析构函数是否需要是虚函数,取决于具体的使用情境。如果一个类存在可能的继承关系,并且你预计可能会通过基类的指针来释放子类的对象,那么将父类的析构函数声明为虚函数就是必要的。这是因为,如果父类的析构函数不是虚函数,当通过基类指针删除子类对象时,子类的析构函数可能不会被调用,从而导致内存泄漏。反之,如果你的类不可能被继承,或者你确定不会使用基类指针来删除派生类对象,那么将析构函数声明为虚函数就没有必要,甚至还可能浪费内存。

        构造函数则决不能是虚函数。这是因为构造函数在执行过程中需要访问对象的内存空间,而虚函数则需要通过虚函数表来调用。但是在这个时候,对象都还没有被完全构造,内存空间还未分配好,因此无法找到虚函数表,从而使得构造函数无法被执行。

你可能感兴趣的:(C/C++精进之路,c语言,c++,面试,数据结构)