这里是自己搜罗准备的C++面试八股文常用题目,持续更新。


Static关键字作用

基本概念

  1. 使用目的:

    在 C++ 中,需要一个数据对象为整个类而非某个对象服务,同时又力求不破坏类的封装性,即要求此成员隐藏在类的内部,对外不可见时,可将其定义为静态数据。

  1. 存放位置

    DATA 段(全局初始化区)存放初始化的全局变量和静态变量;BSS 段(全局未初始化区)存放未初始化的全局变量和静态变量。其中BBS段在程序执行之前会被系统自动清0,所以未初始化的全局变量和静态变量在程序执行之前已经为0。存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化

  2. 使用static的优势:

    • 可以节省内存,因为它是所有对象所公有的,因此,对多个对象来说,静态数据成员只存储一处,供所有对象共用。静态数据成员的值对每个对象都是一样,但它的值是可以更新的。只要对静态数据成员的值更新一次,保证所有对象存取更新后的相同的值,这样可以提高时间效率。使用静态成员变量实现多个对象之间的数据共享不会破坏隐藏的原则,保证了安全性还可以节省内存。
    • 静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的。即静态全局变量不能被其它文件所用;其它文件中可以定义相同名字的变量,不会发生冲突。
  3. 动态数据vs静态数据:

    一般程序把新产生的动态数据存放在堆区,函数内部的自动变量存放在栈区。自动变量一般会随着函数的退出而释放空间,静态数据(即使是函数内部的静态局部变量)也存放在全局数据区。全局数据区的数据并不会因为函数的退出而释放空间。

静态数据成员

静态数据成员的生存期大于 class 的对象,静态数据成员是每个 class 有一份,普通数据成员是每个 instance 有一份,因此静态数据成员也叫做类变量,而普通数据成员也叫做实例变量。

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>

using namespace std;

class Test
{
public:
int n = 0;
// 类内的声明只是声明了静态成员变量的存在,类外的定义才真正分配内存并初始化它。
static int i;
};

// 静态成员变量属于类本身,而不是类的某个实例,因此它们的内存分配和初始化必须在类外进行。
// 在类外初始化时,必须再次指定变量的类型,以确保编译器知道该变量的类型和所属类。
int Test::i = 0;

int main()
{
Test test1;
Test test2;
test1.i += 1;
test1.n += 1;
cout << test2.i << endl; // 输出为1
cout << test2.n << endl; // 输出为0
return 0;
}

静态成员函数

静态成员函数不能访问非静态(包括成员函数和数据成员),但是非静态可以访问静态。因静态成员函数不属于类实例,它获取不了实例的信息,而非静态成员能获取全局静态数据,所以它可以调用静态数据。

代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>

using namespace std;

class Test
{
public:
int n = 0;
static int i;
static void printi()
{
cout << i << endl;
}
void printn()
{
cout << n << endl;
}
};

int Test::i = 0;

int main()
{
Test test1;
Test test2;
test1.i += 1;
test1.n += 1;
cout << test2.i << endl;
cout << test2.n << endl;
test1.printi();
test2.i += 1;
Test::printi(); // 输出为2,因test2也给i加了1
// Test::printn(); // 报错,因printn得由实例来call
test1.printn(); // 实例call就okay
return 0;
}

Extern关键字作用

  1. 允许多个文件访问同一个全局变量/函数
  2. 在当前文件表明变量/函数来自于其他文件
代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// file1
#include <iostream>

using namespace std;

int i = 5;

// file2
#include <iostream>
#include "file1.cpp"
using namespace std;
extern int i; // 声明取的extern的全局变量
extern int i = 0; // 会报错,说重复定义
int main()
{
cout << i << endl; // 输出为5
return 0;
}

指针和引用

指针vs引用

动态库和静态库的区别

特征 动态库 静态库
链接时间 在程序运行时才会被加载,执行文件较小 在编译链接阶段就已经被加载,所以编译后的可执行文件中就包含了静态库的代码。这样可能导致可执行文件体积过大。
加载时间 程序在运行时,动态库需要由操作系统动态加载到内存中,所以时间较长 静态库在编译中已经存在于可执行文件中,不需要重新加载,时间较快
更新和维护 因在运行时才需要加载动态库,所以其他代码文件无需重新编译。比较灵活,但是要保证接口上的兼容性。 当更新静态库时,因为要重写执行文件,所以需要全部重新编译和链接,较为复杂
共享性 多个程序可共享,因为在被动态运行加载时,动态库会被加载到内存中的共享位置,多进程可以同时使用这个区域,从而减少内存使用 对于静态库,每个使用它的程序都得单独将静态库加入到自己的执行文件中,所以每个程序的副本都是独立的,互不共享。
常用场景 - 需要频繁更新迭代的库
- 需要共享库文件
- 不需要频繁更新的库文件
- 希望将所有的依赖库都放在一个可执行文件中

虚函数和纯虚函数的区别

虚函数

虚函数的作用是在运行时决定调用哪个类的函数,可以调用派生类中的重写版本,被称为“动态多态”或“运行时多态”。

虚函数在基类中定义时使用 virtual 关键字,但它有一个函数体(可以是空的),派生类可以选择重写这个函数,也可以不重写而直接使用基类的实现。:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

class Base {
public:
virtual void show() {
std::cout << "Base class show function" << std::endl;
}
};

class child : public Base {
public:
void show() {
std::cout << "Child class show function" << std::endl;
}
};

int main()
{
Base *obj = new child();
obj->show(); // 输出为:Child class show function
return 0;
}

纯虚函数

纯虚函数是一种特殊的虚函数,它在基类中没有具体的实现,只提供一个接口,需要在派生类中实现。

纯虚函数的定义使用 virtual 关键字,并且在函数声明后使用 = 0 表示它是纯虚函数,没有函数体。如果一个类包含一个或多个纯虚函数,那么这个类就是一个抽象类,不能直接实例化对象。派生类必须实现所有纯虚函数才能被实例化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>

// 抽象基类
class Base {
public:
virtual void show() = 0; // 纯虚函数
virtual void info() { // 虚函数
std::cout << "Base class info function" << std::endl;
}
};

class Derived : public Base {
public:
void show() override {
std::cout << "Derived class show function" << std::endl;
}
void info() override {
std::cout << "Derived class info function" << std::endl;
}
};

int main() {
Base* b = new Derived();
b->show(); // 调用的是派生类的实现
b->info(); // 调用的是派生类的重写版本
delete b;
return 0;
}

友元的应用

友元函数

它可以访问类中所有的成员,包括私有成员和受保护成员。尽管友元函数不是该类的成员函数,但由于被声明为友元(使用friend关键字),因此获得了访问该类内部数据的权限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

class MyClass {
private:
int secret;
public:
MyClass(int val) : secret(val) {}
friend void showSecret(const MyClass& obj); // 友元函数声明
};

void showSecret(const MyClass& obj) {
std::cout << "The secret is: " << obj.secret << std::endl; // 友元函数访问私有成员
};

int main()
{
MyClass obj(10);
showSecret(obj);
return 0;
}

常用场景

  • 需要直接访问类的内部成员的情况下,尤其是当多个类之间需要紧密合作时。例如,两个类之间的操作可能需要访问彼此的私有数据。
  • 操作符重载(如 << 和 >> 运算符),因为这些运算符通常不能作为成员函数实现。

友元关系是单向的,即如果类 A 将函数 B 声明为友元函数,那么 B 可以访问 A 的私有成员,但反过来 A 并不能访问 B 的私有成员,除非 B 也将 A 声明为友元。

友元类

友元类中的所有成员函数都可以访问另一个类的所有成员(包括私有成员和受保护成员)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
class B; // 前向声明

class A {
private:
int secretA;
public:
A(int val) : secretA(val) {}
friend class B; // 声明B为友元类
};

class B {
public:
void showSecretA(A& a) {
std::cout << "Secret of A is: " << a.secretA << std::endl; // 访问A的私有成员
}
};

int main()
{
A objA(10);
B objB;
objB.showSecretA(objA); // 输出为:Secret of A is: 10
return 0;
}

常用场景:

  • 友元类通常用于两个类之间需要频繁且深度地访问彼此的私有数据的场合
    • 复杂数据结构的实现(如链表、树等),其中节点类和容器类可能需要相互访问。
    • 封装类与管理类之间的紧密协作,例如封装数据的类和处理这些数据的算法类。
  • 友元类可以减少访问器(getter)和设置器(setter)的使用,因为友元类可以直接访问数据。

友元类的关系是单向的,即如果类 A 声明类 B 为友元类,则 B 可以访问 A 的私有成员,但反之 A 不能访问 B 的私有成员,除非 B 也声明 A 为友元类。

BFS与DFS的实现

BFS Bread-First Search 广度优先搜索

DFS

STL常用容器和算法

List和Set

红黑树

二叉搜索数

HashMap的原理

HashMap通过散列算法使查找、插入、删除接近O(1)的时间复杂度。

散列函数(Hash Function)负责将拿到的Key映射到不同的哈希值上,而哈希值决定了传入的Key在储存中的位置。一个哈希值便是哈希table的key。

为了避免发生冲突,即映射到的哈希值已经被分配了变量,当节点到达8时,HashMap会自动变为红黑树。

最坏的可能性是当N个Key都映射到一个哈希值时,那样映射的值就会一直往后排,所以时间复杂度增加为O(N)。

堆和栈的区别

特征
内存分配方式 堆由手动分配和释放(newdelete),其内存分配也是动态不连续的,管理和操作更加复杂,速度相对较慢 栈内存由编译器自动分配和释放,内存分配是连续的,一般以先进后出的方式管理
储存内容 需要用于动态分配的大块数据,比如动态数组,复杂数据结构等 局部变量,函数调用信息,返回地址等系统自动分配的内存
生命周期 由程序员控制,生存周期可以跨过函数调用和作用域的限制,如果一直不手动删除,便会一直存在,可能导致内存泄漏 由函数或代码块的作用域决定,一旦离开作用域,便会自动销毁,内存由系统自动回收
优缺点 - 需要动态分配,大小不确定的对象
- 需要手动管理,可能导致内存泄漏
- 相对较慢
- 生命周期短,且大小固定的变量
- 系统自动管理
- 处理速度快

如何解决死锁