C++面试题集锦
前言
11、extern用法?
- extern修饰变量的声明
如果文件a.c需要引用b.c中变量int v,就可以在a.c中声明extern int v,然后就可以引用变量v。
//b.c
#include<stdio.h>
int v = 10;
//a.c
#include<stdio.h>
extern int v;
int main()
{
int a = 20;
int b = 0;
b = a + v;
return 0;
}
- extern修饰函数的声明
如果文件a.c需要引用b.c中的函数,比如在b.c中原型是 int fun(int mu),那么就可以在a.c中声明extern int fun(int mu),然后就能使用fun来做任何事情。就像变量的声明一样,extern int fun(int mu) 可以放在a.c的任何地方,而不一定非要放在a.c的文件作用域的范围中。
- extern修饰符可用于指示C或者C++函数的调用规范
比如在C++中调用C库函数,就需要在C++程序中用 extern "C"声明要引用的函数。这是给编译器用的,告诉编译器在链接时候用C函数规范来链接。主要原因是C++和C程序编译完成后再目标代码中命名规则不同。
12、int与字符串互转换
C++11标准增加了全局函数std::to_string
可用使用std::stoi/stol/stoll等待函数
strcpy拥有返回值,有时候函数原本不需要返回值,但为了增加灵活性如支持链式表达。
13、深拷贝与浅拷贝
-
浅赋值 ——只是拷贝了基本类型的数据,而引用类型数据,复制后也会发生引用,我们把这种拷贝叫做“(浅复制)浅拷贝”,换句话说,浅复制仅仅是指向被复制的内存地址,如果原地址中对象被改变了,那么浅复制出来的对象也会相应改变。
-
深拷贝 ——在计算机中开辟了一块新的内存地址用于存放复制的对象。
-
在某些情况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如果 A= B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。
-
类和对象——对象特性:深浅拷贝
浅拷贝: 简单的赋值拷贝操作。深拷贝: 在堆区重新申请空间,进行拷贝操作。
浅拷贝带来的问题是堆区内存重复释放。
class Person {
public:
Person() {
cout << "Person的默认构造函数调用" << endl;
}
Person(int age, int height) :m_Age(age) {
m_Height = new int(height);
}
//自己实现拷贝构造函数,解决浅拷贝带来的问题
Person(const Person& p) {
m_Age = p.m_Age;
//m_Height = p.m_Height; 编译器默认实现就是这行代码
//深拷贝操作
m_Height = new int(*p.m_Height);
cout << "Person 的拷贝构造函数调用" << endl;
}
~Person() {
//析构代码,将堆区开辟数据做释放操作
if (m_Height != nullptr) {
delete m_Height;
m_Height = nullptr;
}
cout << "Person的析构函数调用" << endl;
}
int m_Age;
int *m_Height;
};
static void test() {
Person p1(18, 176);
cout << "p1的年龄为:" << p1.m_Age << "身高:" << *p1.m_Height << endl;
Person p2(p1);
cout << "p2的年龄为:" << p2.m_Age << "身高:" << *p2.m_Height << endl;
}
int main()
{
test();
system("pause");
return 0;
}
14、C++模板是什么,底层怎么实现的?
- 编译器并不是把函数模板处理成能够处理任意类的函数:编译器从函数模板通过具体类型产生不同的函数;编译器会对函数模板进行两次编译:在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。
- 这是因为函数模板要被实例化后才能成为真正的函数,在使用函数模板的源文件中包含函数的头文件,如果该头文件中只有声明,没有定义,那么编译器无法实例化该模板,最终导致链接错误。
15、C语言struct和C++ struct区别
- C语言中:struct是用户自定义数据类型(UDT):C++中struct是抽象数据类型(ADT),支持成员函数的定义,(C++中的struct能继承,能实现多态)。
- C中struct是没有权限的设置的,且struct中只能是一些变量的集合体,可以封装数据却不可以隐藏数据,而且成员不可以是函数。
- C++中,struct的成员默认访问说明符为public(为了与C兼容),class中默认访问限定符为private,struct增加了访问权限,且可以和类一样有成员函数。
- struct作为类的一种特例是用来自定义数据结构的。一个结构标记声明后,在C中必须在结构标记前加上struct,才能做结构类型名。
16、虚函数可以声明为inline吗?
- 虚函数用于实现运行时的多态,或者称为晚绑定或动态绑定。而内联函数用于提高效率。内联函数的原理是,在编译期间,对调用内联函数的地方的代码替换成函数代码。内联函数对于程序中需要频繁使用和调用的小函数非常有用。
- 虚函数要求在运行时进行类型确定,而内联函数要求在编译期完成相关的函数替换;
17、类成员初始化方式?构造函数的执行顺序?为什么用成员初始化列表会快一些?
-
赋值初始化,通过在函数体内进行赋值初始化;列表初始化,在冒号后使用初始化列表进行初始化
两种方式的主要区别在于:
对于在函数体中初始化,是在所有的数据成员被分配内存空间后才进行的。
列表初始化是给数据成员分配内存空间时就进行初始化,就是说分配一个数据成员只要冒号后有此数据成员的赋值表达式(此表达式必须是括号赋值表达式),那么分配了内存空间后在进入函数体之前给数据成员赋值,就是说初始化这个数据成员此时函数体还未执行。 -
一个派生类构造函数的执行顺序如下:
① 虚拟基类的构造函数(多个虚拟基类则按照继承的顺序执行构造函数)
②基类的构造函数(多个普通基类也按照继承的顺序执行构造函数)
③类类型的成员对象的构造函数(按照初始化顺序)
④派生类自己的构造函数
18、成员列表初始化?
- 必须使用成员初始化的四种情况
①初始化一个引用成员时;
②当初始化一个常量成员时;
③当调用一个基类的构造函数,而它拥有一组参数时;
④当调用一个成员类的构造函数,而它拥有一组参数时;
- 成员初始化列表做了什么
① 编译器会一一操作初始化列表,以适当的顺序在构造函数之内安插初始化操作,并且在任何显示用户代码之前;
② list中的项目顺序是由类中的成员声明顺序决定的,不是由初始化列表的顺序决定的。
19、构造函数为什么不能为虚函数?析构函数为什么要虚函数?
- 从存储空间角度,虚函数相对应一个指向vtable虚函数表指针,但这个指向vtable的指针事实上是存储在对象的空间的。问题出来了,假设构造函数是虚的,就必须要通过vtable来调用,但是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。
- 从使用角度,虚函数主要用于在信息不全的情况下,使重载的函数得到相应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义。所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候可以变成调用子类的那个成员函数。而构造函数时在创建对象时自己主动调用的,不可能通过父类的指针或者引用去调用,一次也就规定构造函数不能是虚函数。
- 构造函数不需要是虚函数,也不同意是虚函数,由于创建一个对象时我们总是要明白指定对象的类型,虽然我们可能 通过实验室的基类的指针或引用去访问它但析构却不一定,我们往往通过基类的指针来销毁对象。这时候假设析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。
- 从实现上看,vbtl在构造函数调用后才建立,因而构造函数不可能称为虚函数从实际含义上看,在调用构造函数时还不能确定对象的真实类型(由于子类会调用父类的构造函数);并且构造函数的作用是提供初始化,在对象生命期仅仅运行一次,不是对象的动态行为,也没有必要成为虚函数。
- 当一个构造函数被调用时,它做的首要的事情之中的一个是初始化它的VPTR。因此,它仅仅能知道它是“当前”类的,而全然忽视这个对象后面是否还有继承者。当编译器为这个构造函数产生代码时,它是为了这个类的构造函数产生代码——即不是为基类,也不是为它的派生类(由于类不知道谁继承它)。
直接的讲,C++中基类采用virtual虚析构函数是为了防止内存泄漏。具体地说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。所以,为了防止这种情况的发生,C++中基类的析构函数应采用virtual虚析构函数。
20、析构函数的作用,如何起作用?
- 构造函数只是其初始化值的作用,但实例化一个对象的时候,可以通过实例去传递参数,从主函数传递到其他的函数里面,这样就使其他的函数里面有值了。规则:只要你一实例化对象,系统自动回调用一个构造函数,就是你不写,编译器也自动调用一次。
- 析构函数与构造函数的作用相反,用于撤销对象的一些特殊任务处理,可以是释放对象分配的内存空间;特点:析构函数与构造函数同名,但该函数前面加~。析构函数没有参数,也没有返回值,而且不能重载,在一个类中只能有一个析构函数。当撤销对象时,编译器也会自动调用析构函数。每一个类必须有一个析构函数,用户可以自己定义析构函数,也可以时编译器自动生成默认的析构函数。一般析构函数定义为类的公有成员。