C/C++的一些面经以及必备知识分享

1、如何计算结构体大小?

  一,结构体变量的首地址,必须是结构体 "最宽基本类型成员" 大小的整数倍。

     二,结构体每个成员相对于结构体首地址的偏移量,都是该成员的整数倍。

     三,结构体的总大小,为结构体 “最宽基本类型成员” (将嵌套结构体里的基本类型也算上,得出的最宽基本类型) 大小的整数倍。

参考:(24条消息) 结构体的大小如何计算?_littlezls的博客-CSDN博客

2、c ++类的对象大小由什么决定

在C++中,一个类的对象的大小主要由以下几个因素决定:

  1. 类中成员的大小:
    类中的每个成员都会占用一定的内存空间。如果类中有一些大型成员(如大数组、结构体或者类),那么对象的大小就会相应地增大。
  2. 对齐要求(Alignment):
    C++的编译器和运行时系统可能会对类中的成员进行内存对齐,以提高访问效率。这种对齐可能会导致对象的大小大于其所有成员大小的总和。
  3. 内存对齐填充(Padding):
    为了满足对齐要求,编译器可能会在类的成员之间或者类的末尾添加一些填充字节,以确保对象在内存中的地址是对齐的。这也会增加对象的大小。
  4. 内嵌(Nested)类和虚函数(Virtual Functions):
    如果类中包含内嵌类或者虚函数,那么对象的大小也会相应增大。
  5. 编译器和平台:
    不同的编译器和平台可能会有不同的内存管理策略,这可能会导致同一个类的对象在不同环境下大小不同。

需要注意的是,C++标准并没有规定类的对象的大小必须是其成员大小的精确总和。实际上,编译器可能会添加额外的内存来满足特定的内存对齐要求或者其他优化需求。因此,如果你需要精确控制对象的大小,可能需要手动进行内存管理和布局。

3、STL 中的优先队列的实现?

STL(Standard Template Library)中的优先队列(priority queue)是一种抽象数据类型,它类似于队列,但元素不是先进先出,而是按照优先级来排列。优先队列确保优先级最高的元素总是在队列的顶部。当元素数量为 n 的时候,获取最高优先级元素的时间复杂度为 O(log n)。

STL 的 priority_queue 是一个典型的堆实现。默认情况下,它是一个最大堆,即优先级最高的元素是最大的元素。你可以通过提供自定义的比较函数来改变这个行为,使得优先级最高的元素是最小的元素。

以下是一个使用 STL priority_queue 的例子:

#include  
#include  
using namespace std; 


int main() { 
// 创建一个最大堆 
priority_queue pq; 


// 向堆中添加元素 
pq.push(3); 
pq.push(5); 
pq.push(1); 
pq.push(8); 


// 输出优先级最高的元素(最大元素) 
cout << "最大元素: " << pq.top() << endl; 


// 弹出优先级最高的元素 
pq.pop(); 


// 再次输出优先级最高的元素(下一个最大元素) 
cout << "新的最大元素: " << pq.top() << endl; 


return 0; 
}

在这个例子中,我们创建了一个最大堆,然后向堆中添加了一些整数。top() 函数返回优先级最高的元素(即最大的元素)。然后我们弹出这个元素,然后再次输出优先级最高的元素(此时是剩余元素中的最大元素)

4、构造函数里面可以调用虚函数吗?

在C++中,构造函数可以调用虚函数,但结果可能并不是你所期望的。当在构造函数中调用虚函数时,该函数总是执行基类的版本,而不是派生类的版本。这是因为,在对象完全构造之前,它的虚函数表指针(vptr)还没有被正确地设置。

这个行为的原因可以理解为:在构造过程中,对象还没有完全创建,所以在这个时候,调用虚函数可能还没有定义具体的实现(因为子类的构造函数还没有执行)。因此,为了保证对象在构造过程中能够正常工作,所以在构造函数中调用虚函数时,会直接使用基类的实现。

但是需要注意的是,如果在构造函数中调用虚函数,并且在子类中重写了这个函数,那么在子类的构造函数执行的时候,还是会执行子类中的虚函数实现。只不过在构造函数调用期间,如果再次调用虚函数,还是会执行基类的实现。

这是一个简单的例子:

class Base { 
public: 
Base() { 
print(); // 输出 "Base" 
} 


virtual void print() { 
cout << "Base" << endl; 
} 
}; 


class Derived : public Base { 
public: 
Derived() { 
print(); // 输出 "Base",而不是 "Derived" 
} 


void print() override { 
cout << "Derived" << endl; 
} 
};

在这个例子中,当在派生类的构造函数中调用 print() 函数时,输出的始终是 "Base",而不是 "Derived"。这是因为在派生类的构造函数执行之前,基类的构造函数已经执行,此时虚函数表指针还没有被设置,因此会调用基类的 print() 函数。

所以,虽然在构造函数中可以调用虚函数,但通常这不是一个好的做法,因为它可能会导致意外的结果。如果你需要在构造函数中使用派生类的方法,可以考虑使用初始化列表(initializer list)或者在构造完成后调用该方法。

总结:基类部分在派生类部分之前被构造,当基类执行构造函数时,派生类中的数据成员还未被初始化。如果在基类构造函数中调用虚函数被解析成调用派生类的虚函数,而派生类的虚函数中又访问到未初始化的派生类数据,这是危险的,将会导致程序出现未知行为及bug。

参考:

构造函数中是否可以调用虚函数_构造函数中可以调用虚函数吗_sumup的博客-CSDN博客

C++的构造函数可以调用其他虚函数吗? - 知乎 (zhihu.com)

构造函数和析构函数中能否调用虚函数? - 知乎 (zhihu.com)

5、说说什么是移动构造函数?

移动构造函数是C++11引入的一个概念,它是一种特殊的拷贝构造函数,用于在将对象的所有权从一个对象转移到另一个对象时,保证源对象不被拷贝,从而优化性能。移动构造函数的基本工作方式是,在用原对象初始化新对象后,将原对象资源(如内存、文件句柄等)转移给新对象,同时将原对象资源置空,以防止在析构时产生内存泄漏

6、虚函数是什么,纯虚函数是什么?

虚函数(virtual function)和纯虚函数(pure virtual function)是面向对象编程中用于实现动态多态性的重要概念。

虚函数(virtual function):
虚函数是在基类中声明的成员函数,它在基类中没有定义,但可以被派生类重写。当调用一个指向派生类对象的基类指针或引用来调用该函数时,会根据实际类型调用相应派生类的函数。虚函数的作用是实现动态多态性,也就是在运行时根据对象的实际类型调用相应的函数。虚函数可以在基类中定义,也可以在派生类中被重写。

纯虚函数(pure virtual function)
纯虚函数是在基类中声明的没有实现的成员函数,通常用 "= 0" 来表示。派生类必须重写纯虚函数才能成为完全具体的类。纯虚函数不能在基类中定义,只能在声明为纯虚函数的函数在基类中提供一个默认实现的情况下才可以在基类中定义。引入纯虚函数的目的是为了使派生类具有多态性,但在某些情况下,基类不能为虚函数提供合适的实现,这时就需要使用纯虚函数。纯虚函数没有实现,不能被直接调用,只能通过指向派生类对象的基类指针或引用来调用。

总结:

虚函数的作用就是实现运行时多态性,也就是运行时根据实际对象类型调用相应函数,

1、虚函数可以在基类中定义,也可以在派生类重写。

2、纯虚函数在基类声明但没有实现,派生类必须重写才能成为具体的类,否则为抽象类。

7、数组和链表的区别是什么?

数组和链表都是数据结构,它们有以下区别:

  1. 存储方式:
    数组的存储是连续的,每个数组元素都是线性排列的。
    而链表的存储是不连续的,每个节点都包含两个部分,即数据部分和指针部分。
  2. 查找效率:
    在数组中,根据下标访问元素的时间复杂度为O(1)。
    而在链表中,即使是通过节点中的索引来查找元素,其时间复杂度是O(n),因为需要从链表头开始逐个遍历每个节点。
  3. 插入和删除效率:
    在数组中,如果要插入或删除一个元素,需要移动数组中的其他元素来填补空位或覆盖被删除的元素,因此其时间复杂度为O(n)。
    而在链表中,插入和删除节点的操作可以很快完成,时间复杂度为O(1)。
  4. 内存空间:
    数组在声明时需要预先分配固定大小的内存空间,因此可能会浪费一些内存空间。
    而链表则不需要预先分配内存空间,可以动态地进行扩展和收缩。

综上所述,数组和链表各有其优缺点,需要根据实际情况选择适合的数据结构。

8、unordered_map,unordered_set,set,map的底层实现各是什么

unordered_mapunordered_setsetmap的底层实现依赖于具体的C++标准库实现,不同的实现可能会有所不同。但是,一般来说,这些数据结构通常使用平衡二叉搜索树(如红黑树)或哈希表来实现。

  1. unordered_mapunordered_set

unordered_mapunordered_set通常使用哈希表来实现。它们内部维护一个哈希表,每个键值对都会被哈希到一个桶(bucket)中。哈希表通常使用开放寻址法或链地址法来解决哈希冲突。

哈希表的主要优点是查找速度快,时间复杂度为O(1)。但是,由于哈希表不保持元素的顺序,所以无法保证元素的顺序。此外,哈希表的内存开销也相对较大。

  1. setmap

setmap通常使用平衡二叉搜索树(如红黑树)来实现。它们内部维护一棵二叉搜索树,每个节点都包含一个键和一个值。通过二叉搜索树的性质,可以快速查找、插入和删除元素。

二叉搜索树的主要优点是可以保证元素的顺序,并且内存开销较小。但是,由于二叉搜索树的时间复杂度为O(log n),所以在处理大量数据时,性能可能不如哈希表。

9、 深拷贝、浅拷贝、double free是什么?

  1. 深拷贝和浅拷贝是两种不同的对象复制方式,主要应用于计算机科学和编程领域。

浅拷贝(Shallow Copy):当一个对象复制另一个对象时,如果新对象只包含原始对象的非指针成员,那么可以使用浅拷贝。在这种情况下,新对象将复制原始对象的值,但不会复制指向动态分配内存的指针。这意味着,如果原始对象更改了其指针所指向的内存中的值,那么浅拷贝创建的副本中的相应指针也将指向同一内存地址,因此两个对象将共享同一内存

深拷贝(Deep Copy):当一个对象复制另一个对象时,如果新对象包含原始对象的指针成员,那么需要使用深拷贝。在这种情况下,新对象将复制原始对象的值,并且会复制指针所指向的内存。这意味着,如果原始对象更改了其指针所指向的内存中的值,那么深拷贝创建的副本中的相应指针将不会受到影响,因为它们指向不同的内存地址。

  1. Double Free是指当程序尝试释放已经释放的内存块时出现的问题。这种情况通常发生在以下情况:
  • 程序使用动态内存分配(如malloc或calloc)分配内存块,然后尝试释放该内存块多次。
  • 程序使用动态内存分配分配内存块,然后尝试释放错误的指针,导致意外释放了另一个已经分配的内存块。

Double Free可能会导致各种问题,例如程序崩溃、数据损坏或未定义行为。因此,程序员应该谨慎处理动态内存分配和释放操作,以避免此类问题。

总结:

复制另一个对象时,如果新对象(类)不包含原始对象的指针成员,那么可以使用浅拷贝,否则使用深拷贝,浅拷贝的两个对象共享同一内存,如果原始对象更改指针指向内存的值,那么浅拷贝对象也会改变,而深拷贝创建的副本相应指针不会受到影响,因为它们指向不同的内存地址。

10、结构体的类型名和变量名用法

在C语言中,结构体是一种可以将多个不同类型的数据组合成一个单独的数据结构的方式。结构体可以包含不同类型的成员变量,例如整数、浮点数、字符、指针等。

结构体的类型名是指定义结构体时使用的名称,类似于一个类型别名,可以用来声明结构体类型的变量。

下面是一个简单的例子:


	struct Student { 

	char name[50]; 

	int age; 

	float gpa; 

	};

在这个例子中,我们定义了一个名为"Student"的结构体,它包含了三个成员变量:一个字符数组"name",一个整数"age",和一个浮点数"gpa"。

结构体的类型名可以用来声明结构体类型的变量,变量名是用来标识具体的结构体实例。例如:


	struct Student student1;

在这个例子中,我们使用"Student"作为类型名来声明了一个名为"student1"的结构体变量。

我们也可以在定义结构体的同时声明结构体变量,例如:


	struct Student { 

	char name[50]; 

	int age; 

	float gpa; 

	} student1;

在这个例子中,我们在定义"Student"结构体的同时声明了一个名为"student1"的结构体变量。

需要注意的是,结构体变量只能存储结构体的成员变量,不能直接存储其他结构体变量。如果需要存储多个结构体变量,可以使用结构体数组或结构体指针等方式。

在C语言中,预处理器指令 # 和 ## 在宏定义中起着非常重要的作用。

  1. # 称为“字符串化”操作符。当宏定义中使用 #,编译器会将宏参数转换为字符串。例如:
     

    
    	#define TO_STRING(x) #x

在上述例子中,如果你调用 TO_STRING(Hello),那么 #x 会被替换为字符串 "Hello"

  1. ## 称为“连接”操作符。它可以在宏中合并两个表达式,从而创建一个新的表达式。例如:


	#define CONCAT(x, y) x ## y

在上述例子中,如果你调用 CONCAT(a, b),那么 x ## y 会被替换为 ab。注意,## 不能用于连接字符串,只能用于连接标识符或宏。

这样是可以的

这些预处理器操作符可以使宏更加强大和灵活,但是也需要注意它们的用法和限制,以避免产生不期望的副作用或错误。


11、如果基类是虚函数,子类的同名函数不是虚函数会发生什么?

如果基类中的虚函数被声明为虚函数,那么在子类中重写该函数时,默认也会被声明为虚函数。如果子类中的同名函数不是虚函数,那么它将不会覆盖基类中的虚函数,而是会成为一个新的非虚函数。

在这种情况下,如果你通过子类的对象调用该函数,将会调用子类中的非虚函数,而不是基类中的虚函数。因此,如果你希望在子类中重写基类中的虚函数并保留其虚函数特性,你需要在子类中明确地将该函数声明为虚函数。

总之,如果基类中的虚函数被声明为虚函数,子类中的同名函数如果不是虚函数,那么它们将不会相互覆盖,而是会成为两个独立的函数。

12、软件开发需要经历哪些阶段?内容是什么?

  1. 需求分析阶段:
    在这个阶段,开发团队与业务人员或客户进行密切的交流和协商,以明确软件系统的需求。这个阶段的任务包括对软件系统的功能需求、性能需求、用户界面需求等进行详细的分析和定义,以确保开发团队对软件系统的目标和要求有清晰的认识。
  2. 设计阶段:
    根据需求分析的结果,开发团队进行软件系统的设计。这个阶段包括系统架构设计、数据库设计、模块设计等。在这个阶段,开发团队需要考虑到系统的可扩展性、可维护性和可重用性等因素,以确保软件系统的质量和效率。
    设计阶段还可分为  概要设计   详细设计
  3. 编码阶段:
    在设计完成后,开发团队开始进行编码工作。这个阶段是将设计结果转化为实际可运行的程序代码的过程。在编码过程中,开发团队需要遵循编码规范和标准,确保代码的清晰性和可读性,并进行代码的测试和调试。
  4. 测试阶段:
    在编码完成后,开发团队进行软件系统的测试。这个阶段的任务包括功能测试、性能测试、安全测试等,以确保软件系统的稳定性和正确性。在测试阶段中,开发团队需要发现并纠正软件中的缺陷和问题,以确保软件系统的质量。
  5. 维护阶段:
    在软件系统上线后,开发团队进行软件的维护。这个阶段包括对软件的修改、升级和保障系统的稳定性和安全性,同时对用户进行培训和技术支持。开发团队需要定期对软件系统进行维护和更新,以确保软件系统的正常运行。

软件开发的过程是一个迭代的过程,每个阶段都有其特定的任务和目标,以确保最终的软件系统能够满足用户的需求并具有高质量。

13、同一个进程的多个线程堆栈共享状况哪个描述正确

对于堆,多个线程可以同时访问和修改同一个堆区域。这意味着,如果一个线程修改了堆中的某个数据,其他线程可以立即看到这个修改。为了避免多个线程同时修改同一个数据导致数据不一致的问题,需要使用适当的同步机制,如互斥锁或信号量等。

对于栈,每个线程都有自己的栈,相互之间不共享。这意味着,一个线程不能直接访问或修改另一个线程的栈。但是,如果一个线程通过指针访问了另一个线程的栈,那么它可以读取或修改该指针指向的内存区域。同样,为了避免数据不一致的问题,需要使用适当的同步机制。

线程之间的堆空间是共享的吗:

是的,线程之间的堆空间是共享的。同一个进程的多个线程可以访问和修改同一个堆空间。这意味着,如果一个线程修改了堆中的某个数据,其他线程可以立即看到这个修改。但是,每个线程在堆中分配对象时,分配到的内存空间是互相独立的。也就是说,一个线程分配的对象不会影响到其他线程分配的对象。同时,不同线程之间对同一个对象的访问也需要进行同步,以保证数据的一致性和线程安全性。

线程之间的栈空间是共享的吗  

不同的线程有独立的栈空间,它们不共享栈空间。这是为了保证线程之间的函数调用关系不受影响,以及每个线程的独立性和安全性。虽然同一进程的多个线程共享内存地址空间,但是每个线程的栈空间是独立的,位于其独立的内存地址处。即使两个线程使用相同的内存地址,它们也不会互相影响,因为它们各自使用独立的栈空间。同时,线程切换时,其栈空间也会随之切换,以保证线程的独立性和安全性。

总结:同一进程中的堆空间共享,栈空间不共享

14、哪些排序算法是稳定的?哪些不稳定

稳定的排序算法是指,如果两个元素相等,那么在排序后它们的相对位置不会改变。反之,如果两个元素不相等,那么在排序后它们的相对位置可能会改变。

以下是一些常见的稳定排序算法:

  1. 插入排序(Insertion Sort):在每一次比较中,如果当前元素比目标位置的元素小,就将当前元素插入到目标位置,并把目标位置的元素向后移动。
  2. 冒泡排序(Bubble Sort):每次比较两个相邻元素,如果它们顺序不对就交换它们,直到序列有序为止。
  3. 归并排序(Merge Sort):将序列不断地分为更小的子序列,然后对每个子序列进行排序,最后将子序列合并成完整的序列。
  4. 基数排序(Radix Sort):按从最低位开始的顺序,对每一位进行排序,直到最高位。

以下是一些不稳定排序算法:

  1. 选择排序(Selection Sort):在每一次比较中,选择当前序列中最小的元素,然后将其放到序列的起始位置。
  2. 快速排序(Quick Sort):通过选择一个基准元素,将序列分为小于等于基准元素和大于基准元素两个子序列,然后对这两个子序列进行递归排序。
  3. 希尔排序(Shell Sort):通过定义一个递减的增量序列,对序列进行分组,然后对每个分组进行插入排序。
  4. 堆排序(Heap Sort):通过构建最大堆或最小堆,然后将堆顶元素与最后一个元素交换并删除堆顶元素,重复这个过程直到序列有序。

16、关于C++中struct和class说法?

关于C++中的struct和class,以下说法是正确的:

  • struct可以继承,class也可以继承。在C++中,struct是可以继承的,而class也是可以的。这是它们之间的重要区别之一。
  • struct可以有无参构造函数。实际上,C++中的struct和class都可以有无参构造函数。
  • struct的成员变量默认是public。struct的成员变量的默认访问级别是public,这也是它们之间的重要区别之一。
  • class的成员变量默认是private,而struct的成员变量默认是public。

17、C++面向对象编程语言中,接口是什么

在C++中,接口是通过纯虚函数来实现的。纯虚函数是一种在基类中声明但不定义,且需要在派生类中重写的函数。如果一个类包含至少一个纯虚函数,那么它就被认为是一个接口。

以下是一个简单的示例代码,展示了一个接口的定义和使用:

// 定义一个接口 
class IAnimal { 
public: 
virtual ~IAnimal() {} // 虚析构函数 
virtual void speak() = 0; // 纯虚函数 
}; 


// 定义一个实现了IAnimal接口的类 
class Dog : public IAnimal { 
public: 
void speak() override { 
cout << "Woof!" << endl; 
} 
}; 


// 使用接口 
int main() { 
IAnimal* animal = new Dog(); 
animal->speak(); // 输出 "Woof!" 
delete animal; 
return 0; 
}

在上面的代码中,IAnimal 是一个接口,它定义了一个纯虚函数 speak()Dog 类通过继承 IAnimal 来实现这个接口,并重写了 speak() 函数。在 main() 函数中,我们使用 IAnimal 指针来指向 Dog 对象,并调用 speak() 函数。

18、C或C++中可用做变量名的是

在C和C++中,可用于变量名的规则基本相同。以下是一些你可以使用的规则:

  1. 变量名可以包含字母,数字和下划线。
  2. 变量名必须以字母或下划线开头,不能以数字开头。
  3. 变量名可以是一个或多个字符。
  4. 变量名是区分大小写的,所以 "myVar" 和 "myvar" 是两个不同的变量名。
  5. 关键字(如 int, float, if, while 等)不能用作变量名。
  6. 变量名不能是一个C或C++保留的关键字。

这些规则允许你创建有意义的变量名,以描述变量的用途和/或内容。例如,你可以使用变量名 "age" 来表示年龄,或者 "totalScore" 来表示总分。

题目:

1.以下关于C++术语叙述错误的是()

A.常量引用是指向常量的引用,与别名声明的作用相同

B.常量表达式是能在编译时计算并获取结果的表达式

C.auto是一个类型说明符,通过变量的初始值来推断变量的类型

D.复合类型的定义以其他类型为基础

A. 错误,常量引用是指向常量的引用,它不能修改所引用的变量的值,但是可以使用常量引用来传递参数或返回值,以避免复制操作。别名声明是C++11引入的一种特性,它允许给类型定义一个别名,而不是重新定义它。常量引用和别名声明的作用不同。

B. 对,常量表达式是指在编译时就能计算出结果的表达式,不能在运行时动态改变。

C. 对,auto是一个C++11引入的自动类型推导关键字,它可以根据变量的初始值自动推断变量的类型。

D.对, 复合类型的定义以其他类型为基础,例如指针、数组、引用等都是基于基本类型的复合类型。

所以答案是A. 常量引用和别名声明的作用不同。

2、设栈S和队列Q的初始状态均为空,元素a,b,c,d,e,f,g依次进入栈S。如果每个元素出栈后立即进入队列Q,且7个元素出队的顺序为b,d,e,f,c,a,g,则栈S的容量至少是( )。

A.1

B.4

C.3

D.2

正确答案:C解析: 模拟一遍出栈结果,发现栈中最多时候存储的是3个元素

3、单链表中已知某一节点的指针为p,其后继节点的指针为p->next,寻找结点的后继节点和前驱的时间复杂度为

A.O(1)、O(n)

B.O(1)、O(1)

C.O(n)、O(n)

D.O(n)、O(1)

在单链表中,找到某一节点的后继节点的时间复杂度为O(1),因为我们可以直接通过该节点的next指针找到下一个节点。然而,找到该节点的前驱节点的时间复杂度为O(n),因为我们需要从头节点开始遍历整个链表,直到找到该节点的前一个节点。因此,答案为A.O(1)、O(n)。

故选A

需要注意的是,如果我们要在单链表中查找特定的节点,那么时间复杂度就是O(n),因为需要遍历整个链表。

4.关于类模板下面说法错误的是:

A.可用来创建动态增长和减小的数据结构

B.可用于基本数据类型

C.在运行时检查数据类型,保证了类型安全

D.它是类型无关的,因此具有很高的可复用性

关于类模板,下面说法错误的是

C.在运行时检查数据类型,保证了类型安全。

类模板是一种用于创建不同数据类型的可重用代码的工具。它允许你在类中定义一个或多个类型参数,然后使用这些参数来定义类中的其他部分。类模板可以用来创建动态增长和减小的数据结构,并且可以用于基本数据类型。

然而,类模板在编译时进行类型检查,而不是在运行时。这意味着如果你尝试使用错误的类型实例化类模板,你将在编译时收到错误消息。这种机制被称为静态类型检查,它有助于防止许多常见的错误,但并不保证类型安全。

因此,选项C是错误的,因为类模板不是在运行时检查数据类型。

 5、已知串S = 'abcdabcd',其next 数组值为()。

A.0.1.2,3.4.5.6,7

B.0.1,1,1,2,3,4,5

C.0.0.1,2,3.4,5,6

D.0.1,2,3,4,5,6,0

正确答案是B.0.1,1,1,2,3,4,5。
根据next数组的定义,对于串S中任意一个字符'c',其next数组值表示在串S中,以'c'为前缀的子串(长度为2的子串)出现的第1次的位置。因此,我们需要找到S中以每个字符为前缀的子串出现的第1次的位置。

对于选项A,C,D,它们的结果都不符合next数组的定义。

对于选项B,它表示S中以每个字符为前缀的子串出现的第1次的位置:

  • 对于'a',它本身是串S的第一个字符,因此其next值为0。
  • 对于'b',它出现在串S的第2个位置,因此其next值为1。
  • 对于'c',它出现在串S的第3个位置和第6个位置(注意:由于next数组定义的是以当前字符为前缀的子串的第1次出现的位置,所以对于串S中连续出现的字符,我们只记录它们后面的一个位置),因此其next值为1。
  • 对于'd',它出现在串S的第4个位置和第7个位置(同上),因此其next值为2。
  • 对于'a',它出现在串S的第1个位置和第5个位置(同上),因此其next值为3。
  • 对于'b',它出现在串S的第2个位置(同上),因此其next值为4。
  • 对于'c',它出现在串S的第3个位置(同上),因此其next值为5。
  • 对于'd',它出现在串S的第4个位置(同上),因此其next值为6。
  • 对于'a',它出现在串S的第1个位置(同上),因此其next值为7。

所以,选项B是正确的。

同类题:

已知串S='abaabcabc',其next数组值为()

  • 011221234
  • 012312124
  • 011223123
  • 011223223

链接:已知串S='abaabcabc',其next数组值为()__牛客网
来源:牛客网
 

]这道题采用手工求next数组的方法。

先求串S='abaabcabc'的部分匹配值:

'a'的前后缀都为空,最长相等前后缀长度为0。

'ab'的最长相等前后缀长度为0

'aba'的最长相等前后缀长度为1

PM是部分匹配值(Partial Match)

编号 1 2 3 4 5 6 7 8 9
S a b a a b c a b c
PM 0 0 1 1 2 0 1 2 0
next -1 0 0 1 1 2 0 1 2

选项中next[1]等于0,故将next数组整体加1,答案选C

6.已知串S='abaabcabc',采用KMP算法进行模式匹配,则得到的next数组值为(()

A.012312124

B.011223123

C.011221234

D.011223223

正确答案是:B.011223123。

KMP算法是一种改进的暴力匹配算法,通过next数组优化匹配过程。next[i]表示当模式串从i-1处匹配失败后,应该跳转到模式串中的哪个位置开始继续匹配。计算next数组的方法是:从模式串的第二个字符开始,如果当前字符与前一个字符匹配成功,则next[i]=next[i-1]+1;否则,就需要在前面的next数组中查找一个位置p,使得从p+1开始的子串与从i-1开始的子串相同或者包含相同的前缀,且长度不小于i-p的长度,那么next[i]=p。

对于本题,根据上述规则计算next数组:

next[0][0]=0

next[1][0]=0

next[2][0]=1

next[3][0]=2

next[4][0]=3

next[5][0]=1

next[6][0]=2

next[7][0]=3

所以选B。

7、假设某颗二叉树共有8个结点,且其中只有1个叶子结点,那个该二叉树的深度为(假设根结点的深度为1)()

A4 B8 C6 D3

这道题要求我们计算一个特定二叉树的深度。二叉树的深度是其所有子树深度的最大值。

给定的二叉树有8个结点,且只有一个叶子结点。根据二叉树的性质,对于任何一颗二叉树,叶子结点的数量总是比度2的结点多一个。因此,我们可以推断出,在这颗二叉树中,有一个叶子结点,剩下的7个结点都是度2的结点。

对于一颗完全二叉树(即除了最后一层以外,其它层的结点数都达到最大),其深度(最大层数)为 log2(结点数 + 1)。我们可以将这个公式用于计算给定二叉树的深度。

然而,给定的二叉树并不是完全二叉树,因为它只有7个度2的结点。因此,我们需要通过一种更复杂的方法来计算其深度。

我们可以将给定的二叉树视为一颗满二叉树(即除了最后一层以外,每一层的结点数都达到最大),然后减去最后一层的结点数。满二叉树的深度为 log2(结点数) - 1。对于给定的二叉树,其深度为 log2(8) - 1 = 3。

由于根节电深度为1,该二叉树的深度为3,答案为4选A。

8、现有1G数据需要排序,计算资源只有1G内存可用,下列排序方法中最可能出现性能问题的是____。

A.堆排序

B.插入排序

C.归并排序

D.快速排序

E.选择排序

F.冒泡排序

参考答案:C.

排序法 平均时间 最差情形 稳定度 额外空间
冒泡 O(n2)     O(n2) 稳定 O(1)
交换     O(n2)     O(n2) 不稳定 O(1)
选择 O(n2) O(n2) 不稳定 O(1)
插入 O(n2) O(n2) 稳定 O(1)
基数 O(logRB) O(logRB) 稳定 O(n)
Shell O(nlogn) O(ns) 1 不稳定 O(1)
快速 O(nlogn) O(n2) 不稳定 O(logn)
归并 O(nlogn) O(nlogn) 稳定 O(n)

9、设一个有序的单链表中有n个结点,现要求插入一个新结点后使得单链表仍然保持有序,则该操作的时间复杂度为() 。

A. O(log2n) B. O(1) c. O(n2) D. O(n)

对于有序的单链表,插入一个新结点并保持有序的操作的时间复杂度为O(n),因为插入一个新结点需要遍历整个链表找到合适的位置,遍历的时间复杂度为O(n)。因此,选项D是正确的。选项A是基于二分查找的时间复杂度,对于有序的链表也可以使用二分查找来插入新结点,但是时间复杂度仍然是O(n),因为二分查找的时间复杂度是O(log2n),但是插入新结点仍然需要遍历链表,所以总的时间复杂度是O(n)。选项B和C的时间复杂度比O(n)更差。

10、定义一个函数指针,指向的函数有两个int形参并且返回一个函数指针,返回的指针指向一个有一个int形参且返回int的函数是?()

A. int(*(*F)(int,int))(int) B. int (*F)(int, int) c. int(*(*E)(int,int)) D. *(*F)(int,int)(int)

正确答案是A. int(*(*F)(int,int))(int)。

这个函数指针的定义按照从左到右的顺序解析如下:

*F:这是一个指向函数的指针,该函数接收两个int参数,返回一个指针。

(*F)(int,int):这是对函数指针F的解引用,调用F指针指向的函数,接收两个int参数,并得到一个返回的指针。

int(*(*F)(int,int))(int):这是对F指向的函数的再次解引用,调用这个函数,该函数返回一个指向函数的指针,该函数接收一个int参数,返回一个int。

所以这个函数指针F的定义是指向一个接收两个int参数、返回一个接收一个int参数且返回一个int的函数的指针。

选A

11、下列哪些说法是正确的:

A二分查找法在一个长度为1000的有序整数数组查找一个整数,比较的次数不超过100次

B.在二叉树中查找元素的时间复杂度为O(log2n);

C.对单向链表,可以使用冒泡排序;

D.对双向链表,可以使用快速排序;

正确答案是A、B。
对于A,二分查找法在一个有序数组中查找特定元素,每次比较可以排除一半的元素,因此在一个长度为1000的有序整数数组中查找一个整数,比较的次数最多为1000-1=999次,所以A错误。
对于B,二叉搜索树是一种特殊的二叉树,它的特点是任何节点的值都大于其左子树中任何一个节点的值,而小于其右子树中任何一个节点的值。在二叉搜索树中查找元素的时间复杂度为O(log2n),因此B正确。
对于C,冒泡排序是一种简单的排序算法,它需要一个线性表,而单向链表是一个链式结构,无法像数组一样进行顺序访问和修改,因此C错误。
对于D,快速排序是一种高效的排序算法,它需要随机访问数组元素。对于双向链表这种链式结构,不能进行随机访问,因此无法使用快速排序,D错误。

12.一下C++ STL标准库容器中,哪个插入和删除元素代价最大()

A.vector

B.List

C.map

D.set

在C++ STL标准库中,各种容器的插入和删除元素的代价是不同的,具体如下:

  1. vector:向量容器,它的插入和删除操作的时间复杂度为O(n),其中n是向量中的元素数量。这是因为向量需要移动插入或删除位置之后的所有元素。
  2. list:双向链表容器,它的插入和删除操作的时间复杂度为O(1),这是由于链表中的元素可以直接通过它们的链接进行移动。
  3. mapset:这两种容器是基于红黑树实现的,红黑树是一种自平衡的二叉搜索树。在mapset中插入和删除元素的时间复杂度为O(log n),其中n是容器中的元素数量。这是因为红黑树的插入和删除操作需要重新平衡树结构,而平衡操作的时间复杂度是O(log n)。

因此,从时间复杂度来看,vector的插入和删除操作代价最大,为O(n),而mapset的插入和删除操作代价最小,为O(log n)。list的插入和删除操作代价为O(1)。

12、下面哪个程序优化手段是错误的()

消除不必要的数据引用

选择适当的算法和数据结构

展开循环,增加数据相关性

手动编写汇编代码

程序优化手段是错误的:

  • 消除不必要的数据引用:这个优化手段是正确的,删除那些不再需要的或者重复的数据引用可以减少内存消耗,提高程序效率。
  • 选择适当的算法和数据结构:这是正确的优化方法。选择适合问题的算法和数据结构可以显著提高程序的效率和性能。
  • 展开循环,增加数据相关性:这个优化手段可能是错误的。在某些情况下,展开循环可能会增加程序的运行时间,因为这将导致更多的循环迭代,而不是更少的。这可能会降低程序的效率,而不是提高。
  • 手动编写汇编代码:这个优化手段可以在某些情况下提高程序的性能,但是这需要程序员有深入的汇编语言知识,并且能够手动优化代码。如果程序员没有足够的经验,手动编写汇编代码可能会导致程序变得更加复杂,难以维护,并且可能引入新的错误。

因此,根据上述解释,"展开循环,增加数据相关性"是错误的程序优化手段。

13、下列说法错误的是()

被const修饰的局部变量不可以被其他指针变量进行修改

const修饰函数返回值表示返回值不可改变

const修饰的局部变量会在内存中占用栈空间

const和volatile可以同时修饰一个变量

这些说法中错误的是:

C.const修饰的局部变量会在内存中占用栈空间。

原因如下:

对于局部变量,不论是const修饰与否,都不会在内存中占用栈空间,因为它们都是在编译时分配的,而不是运行时。局部变量存储在堆栈(stack)上,而const只是告诉编译器这个变量是只读的,不能在运行时被修改。

1和2的说法都是正确的:

  1. 被const修饰的局部变量不可以被其他指针变量进行修改。这是对的,const关键字就是告诉编译器这个变量是只读的,任何试图改变它的操作都是不允许的。
  2. const修饰函数返回值表示返回值不可改变。这也是对的,const关键字可以用来修饰函数的返回值类型,表示这个返回值是只读的,调用者不能修改它。

至于const和volatile可以同时修饰一个变量的问题,实际上这是可以的,但这个变量的读写语义会变得复杂,需要具体分析。

14.顺序表包含127个元素,向其中插入一个新元素并保持原来顺序不变,平均要移动几个元素() 8个 7个 63.5个 32个


平均要移动63.5次; 如果插在第一个位置那就要移动127个元素(即127次); 如果插在第二个位置那就要移动126个元素(即126次);

                    .……

                    .……

                    .……

                    .……

 如果插在最后一个位置那不用移动移动次数为0;

就是从0~127的一个递增数列(想倒过来递减也行);

所以平均要移动的次数N=(0+127)/2=63.5;

选C

13.以下哪种类型的函数不能被声明为虚函数()

A.类成员函数 B.构造函数 C.析构函数 D.以上全部

选B、构造函数

1:只有类的成员函数才能说明为虚函数; 
2:静态成员函数不能是虚函数; 
3:内联函数不能为虚函数; 
4:构造函数不能是虚函数; 
5:析构函数可以是虚函数,而且通常声明为虚函数。

14.如果只想得到1000个元素组成的序列中第5个最小元素之前的部分排序的序列,用一下哪个排序方法最快()

A.冒泡排序           B.Shell排序                  C. 堆排序                D.快速排列

如果只想得到1000个元素组成的序列中第5个最小元素之前的部分排序的序列,使用堆排序(Heap Sort)是最快的。

堆排序是一种高效的排序算法,其时间复杂度为O(nlogn)。它利用了堆这种数据结构的一些特性,通过构建最大堆或最小堆,然后不断地将堆顶元素与最后一个元素交换,最终得到排序后的序列。

对于这个问题,我们可以先构建一个最小堆,然后从堆顶开始取出元素,直到取到第5个最小元素为止。这样就可以得到第5个最小元素之前的部分排序的序列。

相比之下,冒泡排序和快速排序的时间复杂度都是O(n^2),而Shell排序的时间复杂度在均摊情况下为O(nlogn),但是其算法实现相对复杂,不如堆排序简单直观。

因此,答案是(C)堆排序(Heap Sort)。

15.若数组名作实参而指针变量作形参,函数调用实参传给形参的是(()

A.数组的长度 B.有元素的值 C.第一个元素的地址 D.数组第一个元素的值

若数组名作实参而指针变量作形参,函数调用实参传给形参的是第一个元素的地址。

在 C/C++ 中,数组名本质上是一个指向数组第一个元素的指针。因此,当数组名作为实参传递给接受指针形参的函数时,实际上传递的是数组的第一个元素的地址。

所以,正确答案是(C)第一个元素的地址。

16.线程以下不能被共享的是()

A.文件句柄            B.堆            C.栈                  D.全局变量

B

进程的堆内存是各线程共享的,但每个线程有自己的私有栈,

17.下面是对宏定义的描述,不正确的是()

A.宏替换只不过是字符替代而已

B.宏替换不占用运行时间

C.宏不存在类型问题,宏名无类型,它的参数也无类型

D.宏替换时先求出实参表达式的值,然后代入形参运算求值

这是关于C语言中宏定义(macro)的描述,我的回答是基于这个语境的。

关于你的问题,以下是对三个选项的详细解释:

  1. 宏替换只不过是字符替代而已:

    • 这是正确的。宏替换就是在预处理阶段,将宏的形参用实参进行替换。
  2. 宏替换不占用运行时间:

    • 这是正确的。宏替换发生在编译阶段,不占用程序运行时间。
  3. 宏不存在类型问题,宏名无类型,它的参数也无类型:

    • 这是不正确的。虽然宏没有像变量那样的具体类型,但是宏名和参数在定义时需要遵守一定的语法规则,比如不能有空的宏名和参数,宏名必须全部大写等。

所以,你的问题的答案是第三个选项C。

18.Linux中如何修改一个文件的权限设置()

A.chmod     B.file     C.attrib     D.change

在Linux中,你可以使用chmod命令来修改文件的权限设置。chmod命令用于改变文件或目录的权限。

要修改文件的权限设置,可以使用以下语法:

chmod [选项] [模式] 文件名

其中,[选项]是可选的,可以指定特定的选项,比如-R表示递归地修改目录及其子目录中的文件权限。[模式]定义了权限的规则,可以是指定具体的权限设置,也可以是使用不同的表示方式。

下面是一些常见的权限设置方式:

  • u(用户):文件所有者的权限
  • g(组):文件所在组的权限
  • o(其他):除文件所有者和所在组之外的其他用户的权限
  • a(所有):包括所有用户的权限

每个权限有三种状态:

  • r(读取)
  • w(写入)
  • x(执行)

你可以使用数字来表示权限,其中:

  • r = 4
  • w = 2
  • x = 1

例如,要修改文件为所有用户都具有读取和执行权限,可以使用以下命令:

chmod a+rx file.txt

这将为文件添加读取和执行权限给所有用户。

19.下列关于构造函数的论述中,不正确的是(()

A.构造函数的函数名与类名相同

B.构造函数可以重载

C.构造函数的返回类型缺省为int

D.构造函数可以设置默认参数

下列关于构造函数的论述中,不正确的是构造函数可以设置默认参数。

实际上,C++ 中的构造函数是不能设置默认参数的。构造函数主要用于创建对象,其参数需要与类名相同。虽然构造函数可以重载,但是其返回类型不能缺省为 int,因为构造函数的返回类型应该是该类的类型。

所以,答案是(CD)。

20.C++中—个指针可以使用volatile 修饰

正确

错误

正确。在C++中,volatile关键字可以用于指针,以表示该指针可能会被意想不到地修改,或者它指向的数据可能会被意想不到地修改。这可以确保编译器不会对涉及该指针的代码进行优化,从而确保程序的正确性。

21、C++移动构造函数和移动赋值操作符需要在堆上分配新内存

错误

正确

错误。移动构造函数和移动赋值操作符的目的是优化性能,避免不必要的内存分配和释放。它们的目标是将资源从一个对象转移到另一个对象,而不是在堆上分配新的内存。这两个操作符都应该在源对象(要移动的对象)上执行必要的操作,以准备资源的转移,并在目标对象上执行必要的操作,以接受这些资源。

22、C++中常量指针是一个指针,它指向的地址是不可修改的

正确

错误

正确。在C++中,常量指针(const pointer)是一个指针,它指向一个常量,即它指向的地址不可修改。这意味着你不能通过常量指针来修改它指向的数据,但是你可以改变指针本身的值,让它指向另一个地址。

23、C++中定义const指针,指向的对象不能被修改

错误

正确

错误。在C++中,const指针是指向const对象的指针。这意味着你不能通过const指针来修改它指向的数据,但是指向的对象本身可以是非const对象,可以被其他非const指针修改。

24、C++中类(class)与结构体(struct)成员变量和成员函数的默认访问级别相同

正确

错误

错误,在C++中,类的成员变量和成员函数的默认访问级别是private,而结构体的默认访问级别是public。因此,题目的说法是错误的。

25.sizeof计算字节是在什么时候:

在C语言中,sizeof是一个编译时运算符,它用于计算其操作数所占的字节大小。这个操作数可以是一个类型,也可以是一个变量。

sizeof运算符在编译时进行计算,而不是在运行时。这意味着,无论在实际运行时该变量或类型实际占用多少字节,sizeof都会返回编译时该类型或变量所占的字节大小。这通常是与目标机器的硬件和操作系统相关的。

例如:

int x; 
printf("Size of int: %zu\n", sizeof(int)); 
printf("Size of x: %zu\n", sizeof(x));

这段代码在编译时就会计算出int类型和变量x所占的字节大小,并在运行时打印出来。注意,sizeof运算符返回的是一个size_t类型的值,所以在这里使用zu作为格式说明符来打印大小。

26.Linux环境下编译地址无关代码的编译器参数是:

在Linux环境下,可以使用GCC编译器的一些参数来编译地址无关的代码。以下是一些常用的参数:

  1. -fPIC(位置无关代码):此参数告诉编译器生成位置无关代码,使得在不同的内存地址上链接和执行代码成为可能。
  2. -pie(位置无关的执行):此参数告诉编译器生成位置无关的可执行文件,使得在加载时可以将其加载到任何内存地址上执行。
  3. -Wl,--build-id:此参数告诉链接器生成一个唯一的标识符,用于链接器跟踪可执行文件。这对于地址无关的代码非常重要,因为它允许链接器在加载时找到正确的可执行文件。

以下是一个示例命令,用于在Linux环境下使用GCC编译器编译地址无关的代码:

gcc -fPIC -pie -Wl,--build-id your_source_file.c -o your_executable

请注意,这些参数仅适用于GCC编译器。其他编译器可能有不同的参数和选项来编译地址无关的代码。

C++

该节内容转自:

字节跳动C++/Qt PC客户端面试题精选 - 知乎 (zhihu.com)

特别详细的C++虚函数表讲解

C++中的虚函数表实现机制以及用C语言对其进行的模拟实现 - 陪她去流浪 (twofei.com)

1. C++的虚函数实现机制

虚函数是实现多态(动态绑定)/接口函数的基础。利用虚表实现。

C++对象的内存布局,对象的前8位(64位系统)为虚表指针(vtpr),指向对象所对应的虚表。虚表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针。

同一个类的不同实例共用同一份虚函数表,他们都通过一个虚函数表指针指向该虚函数表

虚函数表本质上是静态类型数组。在编译时,每个包含虚函数的类(或继承自包含虚函数的类)都会确定自己的虚函数表,这个表是一个静态数组,里面存储着每个成员函数的地址。

题目:

  • 讲一下C++的多态?

 —— 静态多态(函数重载),动态多态(虚函数)

  • 讲一下C++的虚函数的实现机制?
  • 多继承情况下,基类Base1与Base2都有虚函数,继承自Base1和Base2的子类Derived1有几个虚表?
    —— 1个。Derived1的虚函数表仍然是保存到第1个拥有虚函数表的那个基类的后面的。(详情可见于上文博文链接)

2. C++的智能指针相关实现

C++11为C++标准库带来了三个智能指针,分别是shared_ptrunique_ptrweak_ptr

C++智能指针的实现原理为引用计数。引用计数无法处理循环引用的情况。

shared_ptr实现原理是同一个内存空间每多一个指针指向就计数加1,如果计数变为0就释放内存空间。当用普通指针初始化的时候,只能使用一次普通指针。它还可以自定义释放函数。

unique_ptr是计数只能为1,没有拷贝构造函数。

weak_ptr只能指向该内存空间而没有所有权。主要用于辅助第一个指针shared_ptr,防止出现互锁(循环引用)。借助weak_ptr类型指针, 我们可以获取shared_ptr指针的一些状态信息,比如有多少指向相同的shared_ptr指针、shared_ptr指针指向的堆内存是否已经被释放等等。在构建weak_ptr指针对象时,可经常利用已有的shared_ptr指针为其初始化。

std::shared_ptr的典型实现

典型实现中,std::shared_ptr保留两个指针

  • get()所返回的指针
  • 指向控制块的指针

控制块是一个动态分配的对象,其中包括:

  • 指向被管理对象的指针或者被管理对象本身
  • 删除器
  • 分配器
  • 占有被管理对象的shared_ptr数量
  • 涉及被管理对象的weak_ptr数量

指向同一被管理对象内存的shared_ptr共享控制块。

强弱引用分别计数,shared_ptr计数器减至零,控制块调用被管理对象的析构函数。但控制块本身知道weak_ptr计数器同样归零时才会释放。

类对象在使用std::shared_ptr时有什么需要注意的事情?

实际开发中,有时候需要在类中返回包裹当前对象(this)的一个 std::shared_ptr对象给外部使用,C++ 新标准也为我们考虑到了这一点,有如此需求的类只要继承自 std::enable_shared_from_this模板对象即可。用法如下:

#include 

#include 

​

class A : public std::enable_shared_from_this

{

public:

    A()

    {

        std::cout << "A constructor" << std::endl;

    }

​

    ~A()

    {

        std::cout << "A destructor" << std::endl;

    }

​

    std::shared_ptr getSelf()

    {

        return shared_from_this();

    }

};

​

int main()

{

    std::shared_ptr sp1(new A());

​

    std::shared_ptr sp2 = sp1->getSelf();

​

    std::cout << "use count: " << sp1.use_count() << std::endl;

​

    return 0;

}

上述代码中,类 A 的继承std::enable_shared_from_this并提供一个 getSelf()方法返回自身的std::shared_ptr对象,在getSelf()中调用 shared_from_this()即可。

使用std::enable_shared_from_this时,应注意不应该共享栈对象的this给智能指针:

题目

  • C++有哪些智能指针,分别介绍一下?
  • 为什么需要智能指针?
    —— 为了防止内存泄漏,设置的自动回收机制
  • 举一个循环引用的例子?
  • 为什么尽量不要用裸指针创建shared_ptr
    —— 使用裸指针会出现分组不同导致错误:
    int* p = new int;
    shared_ptr ptr1( p); // count 1
    shared_ptr ptr2( p ); // count 1
  • 类对象在使用std::shared_ptr时有什么需要注意的事情?

C++协程相关

C++协程和线程是什么?有什么区别?

C++协程和线程是两种不同的并发执行方式,它们有以下区别:

  1. 执行方式:协程是一种用户态的轻量级线程,由程序自行管理,因此启动和销毁非常轻便,不需要系统级的支持,也不占用系统资源。而线程是操作系统级别的执行单位,由操作系统管理和调度,因此启动和销毁需要消耗更多的系统资源。
  2. 执行效率:由于协程是用户态的轻量级线程,因此它的切换开销比线程小,可以更高效地利用系统资源。而线程的切换开销相对较大,因为涉及到操作系统级别的调度。
  3. 同步方式:协程通常采用基于通信的同步方式,即通过发送和接收消息来实现协程之间的同步。而线程通常采用基于锁的同步方式,即通过互斥锁、读写锁等来实现线程之间的同步。
  4. 错误处理:协程通常采用异常处理机制来处理错误,而线程通常采用返回值或者特殊的错误码来处理错误。

总之,协程和线程都是实现并发执行的机制,但是它们在执行方式、执行效率、同步方式和错误处理等方面存在一些区别。在实际应用中,需要根据具体的场景和需求选择合适的并发执行方式。

线程分为内核态线程、用户态线程两种。

协程的本质就是处理自身挂起和恢复的用户态线程

协程的切换比线程的切换速度更快,在IO密集型任务情境下更适合。IO密集型任务的特点是CPU消耗少,其大部分时间都是在等待IO操作完成,对于这样的场景,一个线程足矣,因此适合采用协程。

挂起/恢复

相比于函数,协程最大的特点就是支持挂起/恢复

协程分类

按照是否开辟相应的调用栈,我们可以将协程分为两类:

  • 有栈协程(Stackful Coroutine):每个协程都有自己的调用栈,类似于线程的调用栈。
  • 无栈协程(Stackless Coroutine):协程没有自己的调用栈,挂起点的状态通过状态机或闭包等语法来实现。C++20提供的协程支持即无栈协程。

有栈协程

有栈协程会改变函数调用栈

有栈协程:在内存中给每个协程开辟一个栈内存(存在堆中),当协程挂起时会将它的运行时上下文(即栈空间)从系统栈中保存至所分配的栈内存中,当协程恢复时会将其运行时上下文从栈内存中恢复至系统栈中。

它可以在任意函数调用层级的位置进行挂起,并转移调度权。

无栈协程

无栈协程不会为各个协程开辟相应的调用栈,无栈协程通常是基于状态机或闭包来实现。

基于状态机的解决方案一般是通过状态机,记录上次协程挂起时的位置,并基于此决定协程恢复时开始执行的位置。这个状态必须存储在栈以外的地方,从而避免状态与栈一同销毁。

相比于有栈协程,无栈协程不需要修改调用栈,也无需额外的内存来保存调用栈,因此它的开小会更小。但是相比于保存运行时上下文这种实现方式,无栈协程的实现还是存在比较多的限制,最大的缺点就是,它无法实现在任意函数调用层级的位置进行挂起。

以上内容参考自:初识协程 | 楚权的世界 (chuquan.me)

题目

  • 了解协程吗?讲一下协程的实现?什么时候适合使用协程?
  • 无栈协程、有栈协程了解吗?

10、 boost库是什么,有什么用

Boost是为C++语言标准库提供扩展的一些C++程序库的总称,由Boost社区提供和维护。Boost库包含众多经过严格测试的高质量、高效能的C++程序库,被广泛用于各种应用中,包括但不限于数学计算、字符串操作、文件操作、数据结构和图形处理等。

Boost库提供了一系列的工具和功能,以帮助开发人员更高效地编写和测试C++程序。其中一些重要的库包括:

  1. Boost.Asio:提供了高效的I/O操作和跨平台的网络编程功能。
  2. Boost.Graph:提供了用于图算法的通用数据结构和相关算法。
  3. Boost.Date_Time:提供了日期和时间的处理功能。
  4. Boost.Serialization:提供了对象的序列化和反序列化功能。
  5. Boost.Thread:提供了跨平台的线程管理和同步功能。

Boost库还包含许多其他有用的库,可以满足各种需求。由于Boost库是由社区提供和维护的,因此其质量和可靠性得到了广泛认可。使用Boost库可以提高开发效率、提高代码质量和减少开发成本。
 

你可能感兴趣的:(c++,面试,学习)