接前面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 析构函数最好是虚函数吗?构造函数可以为虚函数吗?
回答这个问题从以下三个方面:
a. sizeof是关键字、运算符,而strlen是函数
b. strlen 只能使用char * 做参数,必须以“\0”结尾,一般用于计算字符串的长度,不包括"\0"的所占的空间;sizeof可以用类型或者变量做参数,一般用于计算类型或者变量所占的内存的字节数,因此在计算字符串长度的时候,会包含"\0"所占的内存空间大小。
c. sizeof 是关键字在编译的时候已经计算好了,strlen是在运行程序的时候进行计算的
#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)
利用0地址的转换
#define SIZEOF(T) ((size_t)((typeof(T)*)0 + 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;
}
ANSIC标准定义了可供C语言使用的预定义宏:
__LINE__ : 在源代码中插入当前源代码行号
__FILE__ : 在源代码中插入当前源代码文件名 __FUNCTION__:在源代码中插入当前源代码所在函数,ISO C 标准 C89引入,C99为
__func__
__DATE__ : 在源代码中插入当前编译日期
__TIME__ : 在源代码中插入当前编译时间 __STDC__:在函数中查看C/C++遵循的标准,ANSI C标准 是一个非0值,一般的C/C++都遵循
ANSI 标准
回答一:
a. 重载和重写是实现C++多态的两种方式,都可以满足对于不同的对象收到相同的消息产生
不同的行为这一多态特性。重写的实现是利用虚函数实现的。重载是利用相同作用域的
同名函数的不同的参数列表(参数个数、参数类型)实现的。
b. 虚函数是基类希望派生类重新定义的函数,派生类重新定义基类虚函数的做法叫做覆盖
(重写);重载就在允许在相同作用域中存在多个同名的函数,这些函数的参数表不同。
重载的概念不属于面向对象编程,编译器根据函数不同的形参表对同名函数的名称做修
饰,然后这些同名函数就成了不同的函数。
c. 重载的确定是在编译时确定,是静态的,属于编译时多态;虚函数则是在运行时动态确定,
属于运行时多态。
回答二:
重载:
(1)发生在同一个类中;
(2)相同的方法名;
(3)参数列表不同;
(4)不看返回值,如果出现了只有返回值不同的“重载”,是错的;
重写:
(1)发生在子类与父类中;
(2)相同的方法名;
(3)相同的参数列表;
(4)返回值相同 或者 子类方法的返回值是父类方法返回值类型的子类;
(5)访问修饰符相同 或者 子类方法的修饰符范围 大于 父类;
(6)抛出的异常相同 或者 子类方法抛出的异常 小于父类;
方法的重写(Overriding)和重载(Overloading)是多态性的不同表现,重写是父类与子类之间
多 态性的一种表现,重载可以理解成多态的具体表现形式。
方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次
序不同,则称为方法的重载(Overloading)。
方法重写是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一
样的方法,就称为重写(Overriding)。
方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。
虚函数:
在类的成员函数前加virtual关键字。
虚函数是实现多态的基础。一旦基类定义了虚函数,该基类的派生类中的同名函数也自动称
为虚函数。
虚函数的重写:派生类中有一个跟基类的完全相同的虚函数,我们就称子类的虚函数重写了
基类的虚函数。
“完全相同”是指:函数名、参数、返回值都相同。另外,虚函数的重写也叫做虚函数的覆盖。
纯虚函数:
在虚函数的后面写上 = 0,则这个函数为纯虚函数。(纯虚函数没有函数体)
包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也
不能实例化出对象。
只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚
函数更体现了接口继承。
子类和父类的实例化关系:
子类可以实例化成父类,父类不能实例化成子类,因为子类可能重写了父类,子类的范畴是
大于父类的,大到小是可以的,小到大是不行的。
链接属性一定程度范围决定着符号的作用域,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函数具有外部链接属性,但并不意味着它将
在整个程序中可见
定义:
内联函数就是以inline修饰的函数叫做内联函数,编译时会在调用内联函数的地方展开,没有函数调用占用建立栈帧的开销,它将函数的定义和声明放在一起,是一种编译器优化技术。
特性:
1.内联函数会在编译阶段用函数体替换函数调用。缺陷是可能会使可执行程序(或者目标文
件)变大,优势就是减少了函数调用的开销,提高了运行效率。
2. inline是一个弱符号,内联函数只是编译器给我们的一个建议,最终函数是否会成为内联函数取决于编译器自己。
如果我们把递归函数、比较长的函数加上inline的话就会被编译器否决掉。我们可以这样理
解,内联函数只是我们向编译器发出的一个请求,然而编译器可以接收这个请求,当然也完
全可以忽略这个请求。
3.内联函数不建议声明和定义分离,如果分离的话就会导致链接错误,因为inline被展开,此
时就没有函数地址了,链接就会找不到。
使用要求:
1. 函数体比较小(递归一般不行,复杂的代码也不行),频繁调用,性能要求高的场景
2. 一般放在头文件中
3. 定义个声明在一起,不然会有链接错误
4. 可以通过编译器优化来打开,比如 -O2 ,-O0 不会优化,内联的展开是编译器决定的
优势:
1. 内联和宏相对于普通函数都可以提高程序的效率,可以进行编译器优化,避免函数调用消
耗栈空间
2. 内联相对于宏在编译阶段,而宏在预处理阶段
3. 内联是函数,在编译阶段插入,宏仅仅是替换
4. 内联有类型和错误的检查,宏没有,内联的操作不容易出错
5. 对于C++,内联可以操作类的私有成员,但是宏不能操作累的私有数据成员
(1)this指针指向调用它的类或者对象实例,this的值就是类或者对象实例的地址,是一个指
针常量
(2)this只能在成员函数的内部使用,本质是成员函数的第一个隐含的指针形参,对象调用
成员函数时,将对象的地址作为实参传递给this形参,所以对象中国不存储this指针,隐
含存在,也可以显式的表示出来
(3)非静态成员函数可以直接用this替代指向对象的指针,静态成员函数不能使用this指针
,因为静态成员函数不具体作用于某个对象,因此静态成员函数真实的参数的个数,就
是实际的参数的个数。
相同:都是用来防止头文件的重复包含。
区别:
(1) ifndef 作用于包含语句的之前的所有的代码,program once作用于包含此标识的
整个文件,因此program once 速度更快
(2)ifndef是由语言本身支持的,program once是编译器支持的,因此对于有些老的编
译器可能不支持
(3)ifndef 可以保证互相包含的文件中的内容不会出现宏定义的重复情况,但
program once 作用的是整个文件,是从物理上保证一个文件是否被重复包含的,
不是从内容上,因此无法保证头文件不被重复包含
typedef std::basic_string
std::string是 std::basic_string模板类的一个特化,由于std::string的析构函数不是virtual 虚函数,这样容易引起内存泄露。对于不是虚析构的Base *A = Derived A1; delete A; 只会调用基类的析构,不会调用派生类的析构。
C++的类被继承时,基类的析构函数要为虚函数,即虚析构。虚析构可以防止内存泄漏,正确析构指向派生类实例的基类指针。
TIPS:如果不是虚析构函数,基类指针被释放不会调用派生类的析构函数。
定义:
狭义上,内存泄漏是指动态分配的内存未正确的释放导致的,如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;
(3)虚析构
父类析构函数不是虚函数,当父类指针释放子类对象时,不会调用子类的析构函数,产生内存泄露。
(4)循环引用
智能指针shared_ptr ,两个类中互相有对方的智能指针时,会出现这种情况,造成内存泄露,使用weak_ptr来解决循环引用的问题
内存泄露的防范:
(1)减少使用堆内存,使用栈内存
(2)不使用裸指针,使用智能指针
(4)使用RALL机制
内存泄露排查思路:
(1)代码检测(静态代码检测工具、动态内存检测工具),工具valgrind 内存泄露定位
(2)代码Review(未释放、未匹配、虚析构、循环引用)
(3)查看日志,输出内存信息
(4)最小化场景复现
栈溢出检测办法:
已知栈顶,可以给栈顶赋一特定值,查看此值是否改变
最大栈使用计算:
先分配较大的栈空间,将栈空间赋为一特定值,运行程序,最终计算栈中被改变的值占分配的比率,得到此程序运行需要的最大栈空间
(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++语言所自带的运算符。
构造函数:
(1)首先调用父类的构造函数;
(2)调用父类成员变量的构造函数;
(3)调用类自身的构造函数。
析构函数:
(1)调用子类的自身的析构函数
(2)调用父类成员变量的析构函数
(3)调用父类的析构函数
final关键字是在C++11标准中引入的。它用于限制类、成员函数和虚函数的继承和重写。
(1)对于类:在类声明中,使用final关键字可以防止该类被继承。
(2)对于成员函数:在成员函数声明中,使用final关键字可以防止该函数在派生类中被重写(override,覆盖)。
(3)对于虚函数:在虚函数声明中,使用final关键字可以防止派生类中的进一步重写。例如:
本质区别是默认的继承访问权限:class是private, struct是public
vector数据结构
vector和数组类似,拥有一段连续的内存空间,并且起始地址不变。因此能高效的进行随机存取,时间复杂度为o(1);但因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为o(n)。另外,当数组中内存空间不够时,会重新申请一块内存空间并进行内存拷贝。
list数据结构
list是由双向链表实现的,因此内存空间是不连续的。只能通过指针访问数据,所以list的随机存取非常没有效率,时间复杂度为o(n);但由于链表的特点,能高效地进行插入和删除。
vector和list的区别
vector拥有一段连续的内存空间,能很好的支持随机存取,因此vector
list的内存空间可以是不连续,它不支持随机访问,因此list
vector
总之,如果需要高效的随机存取,而不在乎插入和删除的效率,使用vector;如果需要大量的插入和删除,而不关心随机存取,则应使用list。
这些函数的区别在于 实现功能以及操作对象不同。
(1)strcpy 函数操作的对象是字符串,完成从源字符串到目的字符串的拷贝功能。
(2)sprintf 函数操作的对象不限于字符串:虽然目的对象是字符串,但是源对象可以是字符串、也可以是任意基本类型的数据。这个函数主要用来实现(字符串或基本数据类型)向字符串的转换功能。如果源对象是字符串,并且指定 %s 格式符,也可实现字符串拷贝功能。
(3)memcpy函数顾名思义就是内存拷贝,实现将一个内存块的内容复制到另一个内存块这一功能。内存块由其首地址以及长度确定。程序中出现的实体对象,不论是什么类型,其最终表现就是在内存中占据一席之地(一个内存区间或块)。因此,memcpy的操作对象不局限于某一类数据类型,或者说可适用于任意数据类型,只要能给出对象的起始地址和内存长度信息、并且对象具有可操作性即可。鉴于memcpy函数等长拷贝的特点以及数据类型代表的物理意义,memcpy函数通常限于同种类型数据或对象之间的拷贝,其中当然也包括字符串拷贝以及基本数据类型的拷贝。
对于字符串拷贝来说,用上述三个函数都可以实现,但是其实现的效率和使用的方便程度不同:
strcpy 无疑是最合适的选择:效率高且调用方便。
sprintf 要额外指定格式符并且进行格式转化,麻烦且效率不高。
memcpy 虽然高效,但是需要额外提供拷贝的内存长度这一参数,易错且使用不便;并且如果长度指定过大的话(最优长度是源字符串长度 +1),还会带来性能的下降。其实 strcpy 函数一般是在内部调用 memcpy 函数或者用汇编直接实现的,以达到高效的目的。因此,使用memcpy 和 strcpy 拷贝字符串在性能上应该没有什么大的差别。
不可以,因为静态成员函数/变量没有this指针
虚函数是为了实现多态,它允许父类指针访问子类的函数。这种动态绑定是在运行时确定的,即函数的具体实现是在运行时才知道的。而静态成员函数与任何实例无关,它是类的一个属性,可以直接通过类名调用,而不需要通过对象来调用。因此,C++中不允许将静态成员函数声明为虚函数。如果尝试这样做,编译器会报错。
静态函数和静态变量可以直接通过类名调用。
对于空类,声明时,编译器会生成1个字节的占位符。
会生成默认构造函数,析构函数,拷贝构造函数,赋值运算符,, 取址运算符
class Empty
{
public:
Empty(); //缺省构造函数
Empty(const Empty &rhs); //拷贝构造函数
~Empty(); //析构函数
Empty& operator=(const Empty &rhs); //赋值运算符
Empty* operator&(); //取址运算符
const Empty* operator&() const; //取址运算符(const版本)
};
析构函数是否需要是虚函数,取决于具体的使用情境。如果一个类存在可能的继承关系,并且你预计可能会通过基类的指针来释放子类的对象,那么将父类的析构函数声明为虚函数就是必要的。这是因为,如果父类的析构函数不是虚函数,当通过基类指针删除子类对象时,子类的析构函数可能不会被调用,从而导致内存泄漏。反之,如果你的类不可能被继承,或者你确定不会使用基类指针来删除派生类对象,那么将析构函数声明为虚函数就没有必要,甚至还可能浪费内存。
构造函数则决不能是虚函数。这是因为构造函数在执行过程中需要访问对象的内存空间,而虚函数则需要通过虚函数表来调用。但是在这个时候,对象都还没有被完全构造,内存空间还未分配好,因此无法找到虚函数表,从而使得构造函数无法被执行。