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

2023-12-14 13:42:29

接前面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 <stdio.h>

#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 <stdio.h>
#include <stdbool.h>


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)。
????????方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。

具体对比分析
具体对比分析

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<char> 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<int>::iterator支持“+”,“+=”,“<”等操作符。

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

????????vector<int>::iterator和list<int>::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 析构函数最好是虚函数吗?构造函数可以为虚函数吗?

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

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

文章来源:https://blog.csdn.net/m0_47324800/article/details/134905967
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。