C++面试问题

1. 什么是零三五原则?

2. C++可调用类型有哪些?

在C++中,可调用类型是指可以被调用的类型,包括以下几种:

  1. 函数指针类型:指向函数的指针。

  2. 成员函数指针类型:指向类的非静态成员函数的指针。

  3. 函数对象类型:实现了函数调用运算符()的类对象,也称为仿函数。

  4. Lambda表达式类型:一种匿名函数,可以用于创建临时的可调用对象。

  5. std::function类型:是一个通用的可调用对象包装器,可以用于存储任意可调用类型的对象,包括函数指针、成员函数指针、函数对象、Lambda表达式等。

  6. std::bind类型:是一个通用的函数对象适配器,可以用于将一个可调用对象适配成另一个可调用对象,支持绑定函数参数和成员函数指针。

  7. std::mem_fn类型:是一个通用的成员函数指针适配器,可以用于将一个成员函数指针适配成一个可调用对象。

总之,C++中的可调用类型包括函数指针、成员函数指针、函数对象、Lambda表达式、std::function、std::bind和std::mem_fn等。这些可调用类型可以用于实现各种功能,例如回调函数、事件处理、函数适配器等。

3. 构造函数、析构函数是需要定义成虚函数?为什么?

构造函数永远不能定义成虚的,
析构函数在有继承的时候一般定义成虚的。虚函数的特性就是调用的时候通过查虚表调用子类对象的实现。
构造函数系统保证先构造父类再构造子类。试想一下假设构造函数可以定义成虚函数,那么构造父类的时候就会通过虚表查找调用子类构造函数,父类是不是永远无法构造。而父类在被继承的情况下为什么经常被定义成虚析构函数?这个跟面向接口编程息息相关。

class A/* … */ };class Bpublic A { /* … */ };

假设有上面两个类,B继承了A。如果我们使用的时候都是通过B来的,那么A的析构函数是没有必要定义成虚的。

B nB;B *pB = new B();delete pB;pB = nullptr;

上面的用法里A的析构函数无论是不是虚的都不会有问题。但是看一下另外一种用法,这种情况下A的析构函数不是虚的就会出问题了。

class Base {
public:virtual void foo() {}virtual ~Base() {}
};class Derived : public Base {
public:void foo() override {}~Derived() {}
};int main() {Base* ptr = new Derived;delete ptr;  // 如果Base的析构函数不是虚函数,那么该语句只会调用Base的析构函数,导致Derived的析构函数没有被调用,从而可能导致内存泄漏或者程序崩溃等问题return 0;
}

在上述代码中,如果Base的析构函数不是虚函数,那么在使用delete释放ptr指向的内存时,只会调用Base的析构函数,而不会调用Derived的析构函数,从而可能导致内存泄漏或者程序崩溃等问题。因此,为了避免这种问题,通常需要将基类的析构函数定义为虚函数,从而能够确保在使用派生类对象时正确地调用析构函数,释放对象的内存空间。

4. c++断言是什么?断言和条件语句的优劣?

C++中的断言是一种用于在程序中检查错误的机制。它通常用于检查程序中的假设是否成立,如果不成立,则会在运行时终止程序并输出错误信息。断言的基本形式是assert(expression),其中expression是一个bool类型的表达式,如果expression的值为false,则会触发断言,程序会终止并输出错误信息。

断言和条件语句的优劣:

断言通常用于在程序开发和调试阶段中,对程序中的假设进行检查,以确保程序的正确性和可靠性。

条件语句通常用于在程序运行时,根据不同的条件来执行不同的代码,从而实现程序的控制流。

断言的优点在于能够在程序中快速地发现并定位错误,从而提高程序的调试效率。但是,由于断言通常用于开发和调试阶段,因此在发布版本中通常会关闭断言机制,从而避免影响程序的性能。

条件语句的优点在于能够根据不同的条件来执行不同的代码,从而实现程序的控制流。但是,由于条件语句需要在运行时进行判断,因此会对程序的性能产生一定的影响。

综上所述,断言和条件语句都是C++中常用的控制语句,它们各自有其适用的场景和优点。

5. c++11为什么引入枚举类?

传统的 C++ 枚举类型会将枚举值暴露在命名空间中,容易造成命名冲突,而枚举类则通过引入了作用域限定符来解决这个问题。其次,传统的 C++ 枚举类型是基于整数的,可以进行隐式的类型转换和比较操作,这可能会导致一些意想不到的错误,而枚举类则可以避免这个问题,因为它们只能进行显式的类型转换和比较操作。

6. extern & extern C

extern

extern是C/C++语言中的一个关键字,用于声明一个变量或者函数是在其他文件中定义的,告诉编译器在链接时要在其他文件中查找这个变量或者函数的定义。它的作用是将变量或函数的作用域扩展到其他文件中,方便在多个文件中共享变量或函数。

extern C

extern "C"是C++语言中的一个语法,用于指示编译器按照C语言的命名规则来编译函数名。C++语言支持函数重载,函数名可以相同但参数列表不同,而C语言不支持函数重载,函数名必须唯一。因此,当需要在C++中调用C语言编写的库函数时,需要使用extern "C"来告诉编译器按照C语言的命名规则来编译函数名,以便能够正确地链接C语言编写的库函数。

C语言编写的库函数:

// lib.c
#include <stdio.h>void print_hello() {printf("Hello, world!\n");
}

C++中调用C语言编写的库函数:

// main.cpp
#include <iostream>
extern "C" {void print_hello();
}int main() {std::cout << "Calling C function...\n";print_hello();return 0;
}

在C++中,使用extern "C"来声明print_hello函数,这样编译器就会按照C语言的命名规则来编译函数名,能够正确地链接C语言编写的库函数。

7.C++的编译流程?

先预处理,然后编译成目标文件,接着把目标文件链接成库文件或者可执行文件。

8. 动态库和静态库的区别?知道动态库延迟加载优化吗?

链接动态库和静态库的时候,静态库会被复制到可执行程序当中,而动态库不会。相比动态库,静态库的执行效率更高,但占用磁盘空间更多,不方便更新。动态库的延迟加载指的是,在运行时按需加载动态链接库中的函数和数据,而不是在启动的时候加载库函数和数据,从而降低启动时间,在linux系统下,延迟加载是通过PLT表和GOT表配合实现的。

9基本类型的长度?

这些长度可能会因编译器、操作系统和计算机体系结构的不同而有所变化。char长度是1字节;short长度至少2字节,大多情况下2字节;int长度至少2字节,大多数情况下4字节;long int长度大于等于int长度;float长度4字节;double长度8字节。所以为了移植性,一般不建议直接使用这些类型,建议使用int8_t,int16_t,int32_t等类型。

10. Static的作用是什么?

C语言中static的用法C 语言的 static 关键字有三种(具体来说是两种)用途:1. 静态局部变量用于函数体内部修饰变量,这种变量的生存期长于该函数。

int foo(){static int i = 1; // note:1//int i = 1;  // note:2i += 1;return i;
}

要明白这个用法,我们首先要了解 c/c++ 的内存分布,以及 static 所在的区间。对于一个完整的程序,在内存中的分布情况如下:

  1. 栈区: 由编译器自动分配释放,像局部变量,函数参数,都是在栈区。会随着作用于退出而释放空间。
  2. 堆区:程序员分配并释放的区域,像malloc©,new(c++)
  3. 全局数据区(静态区):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束释放。
  4. 代码区

所以上面 note:1 的 static 是在 全局数据区 分配的,那么它存在的意思是什么?又是什么时候初始化的呢?
首先回答第一个问题:它存在的意义就是随着第一次函数的调用而初始化,却不随着函数的调用结束而销毁(如果把以上的 note:1 换成 note:2,那么i就是在栈区分配了,会随着foo的调用结束而释放)。
那么第二个问题也就浮出水面了
它是在第一次调用进入 note:1 的时候初始化。且只初始化一次,也就是你第二次调用foo(),不会继续初始化,而会直接跳过。那么它跟定义一个全局变量有什么区别呢,同样是初始化一次,连续调用foo()的结果是一样的,但是,使用全局变量的话,变量就不属于函数本身了,不再仅受函数的控制,给程序的维护带来不便。   静态局部变量正好可以解决这个问题。静态局部变量保存在全局数据区,而不是保存在栈中,每次的值保持到下一次调用,直到下次赋新值。那么我们总结一下,静态局部变量的特点(括号内为 note:2,也就是局部变量的对比): (1)该变量在全局数据区分配内存(局部变量在栈区分配内存); (2)静态局部变量在程序执行到该对象的声明处时被首次初始化,即以后的函数调用不再进行初始化(局部变量每次函数调用都会被初始化); (3)静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为0(局部变量不会被初始化); (4)它始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,也就是不能在函数体外面使用它(局部变量在栈区,在函数结束后立即释放内存);2.静态全局变量定义在函数体外,用于修饰全局变量,表示该变量只在本文件可见。

static int i = 1;  //note:3
//int i = 1;  //note:4int foo()
{i += 1;return i;
}

note:3 和 note:4 有什么差异呢?你调用foo(),无论调用几次,他们的结果都是一样的。也就是说在本文件内调用他们是完全相同的。那么他们的区别是什么呢?文件隔离!假设我有一个文件a.c,我们再新建一个b.c,内容如下。

 //file a.c//static int n = 15;  //note:5
int n = 15;  //note:6
//file b.c
#include <stdio.h>extern int n;void fn()
{n++;printf("after: %d\n",n);
}void main()
{printf("before: %d\n",n);fn();
}

我们先使用 note:6,也就是 非静态全局变量,发现输出为:before: 15
after: 16
也就是我们的 b.c 通过 extern 使用了 a.c 定义的全局变量。 那么我们改成使用 note:5,也就是使用静态全局变量呢?gcc a.c b.c -o output.out会出现类似 undeference to “n” 的报错,它是找不到n的,因为 static 进行了文件隔离,你是没办法访问 a.c 定义的静态全局变量的,当然你用 #include “a.c” 那就不一样了。以上我们就可以得出静态全局变量的特点:静态全局变量不能被其它文件所用(全局变量可以);其它文件中可以定义相同名字的变量,不会发生冲突(自然了,因为static隔离了文件,其它文件使用相同的名字的变量,也跟它没关系了);3.静态函数准确的说,静态函数跟静态全局变量的作用类似:

//file a.c
#include <stdio.h>void fn()
{printf("this is non-static func in a");
}
//file b.c
#include <stdio.h>extern void fn();  //我们用extern声明其他文件的fn(),供本文件使用。void main()
{fn();
}

可以正常输出:this is non-static func in a。当给void fn()加上static的关键字之后呢? undefined reference to “fn”.所以,静态函数的好处跟静态全局变量的好处就类似了: 静态函数不能被其它文件所用; 其它文件中可以定义相同名字的函数,不会发生冲突;上面一共说了三种用法,为什么说准确来说是两种呢? 一种是修饰变量,一种是修饰函数,所以说是两种(这种解释不多)。静态全局变量和修饰静态函数的作用是一样的,一般合并为一种。(这是比较多的分法)。C++语言中static的用法C++ 语言的 static 关键字有二种用途 当然以上的几种,也可以用在c++中。还有额外的两种用法:1.静态数据成员用于修饰 class 的数据成员,即所谓“静态成员”。这种数据成员的生存期大于 class 的对象(实体 instance)。静态数据成员是每个 class 有一份,普通数据成员是每个 instance 有一份,因此静态数据成员也叫做类变量,而普通数据成员也叫做实例变量。

#include<iostream>using namespace std;class Rectangle
{
private:int m_w,m_h;static int s_sum;public:Rectangle(int w,int h){this->m_w = w;this->m_h = h;s_sum += (this->m_w * this->m_h);}void GetSum(){cout<<"sum = "<<s_sum<<endl;}
};int Rectangle::s_sum = 0;  //初始化int main()
{cout<<"sizeof(Rectangle)="<<sizeof(Rectangle)<<endl;Rectangle *rect1 = new Rectangle(3,4);rect1->GetSum();cout<<"sizeof(rect1)="<<sizeof(*rect1)<<endl;Rectangle rect2(2,3);rect2.GetSum();cout<<"sizeof(rect2)="<<sizeof(rect2)<<endl;system("pause");return 0;
}

结果如下:

sizeof(Rectangle)=8
sum = 12
sizeof(rect1)=8
sum = 12
sizeof(rect1)=8

由此可知:sizeof(Rectangle)=8bytes=sizeof(m_w)+sizeof(m_h)。也就是说 static 并不占用 Rectangle 的内存空间。 那么 static 在哪里分配内存的呢?是的,全局数据区(静态区)。 再看看 GetSum(),第一次12=34,第二次18=12+23。由此可得,static 只会被初始化一次,于实例无关。结论:对于非静态数据成员,每个类对象(实例)都有自己的拷贝。而静态数据成员被当作是类的成员,由该类型的所有对象共享访问,对该类的多个对象来说,静态数据成员只分配一次内存。 静态数据成员存储在全局数据区。静态数据成员定义时要分配空间,所以不能在类声明中定义。也就是说,你每 new 一个 Rectangle,并不会为 static int s_sum 的构建一份内存拷贝,它是不管你 new 了多少 Rectangle 的实例,因为它只与类Rectangle挂钩,而跟你每一个Rectangle的对象没关系。2.静态成员函数用于修饰 class 的成员函数。我们对上面的例子稍加改动:

#include<iostream>using namespace std;class Rectangle
{
private:int m_w,m_h;static int s_sum;public:Rectangle(int w,int h){this->m_w = w;this->m_h = h;s_sum += (this->m_w * this->m_h);}static void GetSum()  //这里加上static{cout<<"sum = "<<s_sum<<endl;}
};int Rectangle::s_sum = 0;  //初始化int main()
{cout<<"sizeof(Rectangle)="<<sizeof(Rectangle)<<endl;Rectangle *rect1 = new Rectangle(3,4);rect1->GetSum();cout<<"sizeof(rect1)="<<sizeof(*rect1)<<endl;Rectangle rect2(2,3);rect2.GetSum();  //可以用对象名.函数名访问cout<<"sizeof(rect2)="<<sizeof(rect2)<<endl;Rectangle::GetSum();  //也可以可以用类名::函数名访问system("pause");return 0;
}

上面注释可见: 对 GetSum() 加上 static,使它变成一个静态成员函数,可以用类名::函数名进行访问。那么静态成员函数有特点呢? 静态成员之间可以相互访问,包括静态成员函数访问静态数据成员和访问静态成员函数; 非静态成员函数可以任意地访问静态成员函数和静态数据成员; 静态成员函数不能访问非静态成员函数和非静态数据成员; 调用静态成员函数,可以用成员访问操作符(.)和(->)为一个类的对象或指向类对象的指针调用静态成员函数,也可以用类名::函数名调用(因为他本来就是属于类的,用类名调用很正常)前三点其实是一点:静态成员函数不能访问非静态(包括成员函数和数据成员),但是非静态可以访问静态。 有点晕吗?没关系,我来给你解释:因为静态是属于类的,它是不知道你创建了10个还是100个对象,所以它对你对象的函数或者数据是一无所知的,所以它没办法调用,而反过来,你创建的对象是对类一清二楚的(不然你怎么从它那里实例化呢),所以你是可以调用类函数和类成员的,就像不管 GetSum 是不是 static,都可以调用 static 的 s_sum 一样。

11. 如何避免内存泄露?用过哪些智能指针?智能指针实现原理是怎样的?

为了避免内存泄露,可以使用智能指针来管理动态分配的内存。智能指针是一种特殊类型的指针,它会自动管理所指向的对象的生命周期,当不再需要指向对象的指针时,智能指针会自动释放所指向的内存。

C++中常用的智能指针有:

shared_ptr:多个指针可以共享同一个对象,当所有指向该对象的指针都失效时,该对象才会被销毁。

unique_ptr:独占指针,只能有一个指针指向该对象,当该指针失效时,对象会被销毁。

weak_ptr:弱引用指针,不会增加对象的引用计数,可以用于解决shared_ptr循环引用的问题。
shared_ptr是最常用的智能指针,但是,第一,效率低,可以通过在特定场合使用unique_ptr弥补这点;第二,有循环引用的问题,故引入weak_ptr;第三,不能直接封装this并返回,否则会引起引用计数错误,故引入enable_shared_from_this。
智能指针的实现原理是通过重载指针操作符和析构函数来实现的。当智能指针被销毁时,它会自动调用析构函数,释放所指向的内存。同时,智能指针还会记录对象的引用计数,当引用计数为0时,智能指针会自动删除所指向的对象,从而避免内存泄露的发生。

12.有没有用过STL库?常见的STL容器有哪些?算法用过哪几个?

STL(Standard Template Library)是C++标准库的一部分,包含了许多数据结构和算法的模板实现,可以大大提高程序的开发效率。

STL库中常见的容器有以下几种:

vector:动态数组,支持随机访问和在尾部插入和删除元素。

list:双向链表,支持在任意位置插入和删除元素。

deque:双端队列,支持在队列的两端插入和删除元素。

stack:栈,只能在栈顶插入和删除元素。

queue:队列,只能在队尾插入,在队头删除元素。

priority_queue:优先队列,支持快速查找最大或最小元素。

set:集合,自动排序,不允许重复元素。

map:映射,自动排序,不允许重复的键值。

STL库中常见的算法有以下几种:

sort:快速排序算法。

find:查找算法,用于在容器中查找指定元素。

transform:转换算法,用于对容器中的元素进行转换。

accumulate:累加算法,用于对容器中的元素进行累加操作。

count:计数算法,用于计算容器中指定元素的个数。

unique:去重算法,用于去除容器中的重复元素。

reverse:反转算法,用于将容器中的元素反转。

copy:复制算法,用于将一个容器中的元素复制到另一个容器中。

13.C++的空类有哪些成员函数?

C++的空类(即没有成员变量的类)默认具有以下几个成员函数:

  1. 默认构造函数:空类默认具有一个无参的默认构造函数,用于创建对象。

  2. 默认析构函数:空类默认具有一个默认析构函数,用于销毁对象。

  3. 拷贝构造函数:空类默认具有一个拷贝构造函数,用于复制对象。

  4. 拷贝赋值运算符:空类默认具有一个拷贝赋值运算符,用于将一个对象赋值给另一个对象。

  5. 拷贝取址运算符

  6. 拷贝取址运算符const。

这些成员函数都是编译器自动生成的默认实现,它们的访问权限都是public。

14. 四种指针类型转换的区别?

reinterpret_cast

reinterpret_cast用于任意指针(引用)类型之间的转换。原理是直接将二进制数据进行转换,不进行任何类型检查。

static_cast

static_cast用于基类和子类指针(引用)之间的转换,编译期进行类型检查。
static_cast可以用来执行基本数据类型的转换、类层次结构中的向下转换、以及void指针和任何其他指针类型之间的转换。static_cast的原理是在编译时进行类型检查,如果类型不匹配,则会导致编译错误。

使用static_cast时需要注意的是,它不能用来执行动态类型转换,也就是说,无法将一个基类指针转换为派生类指针。此外,static_cast还不能用来执行const和volatile修饰符的转换。
dynamic_cast用于基类和子类指针(引用)之间的转换,运行期进行类型检查。
const_cast用于指针(引用)类型,用于删除限定符,不进行类型检查。
const_cast是C++中的一个类型转换运算符,用于去除指针或引用类型的常量限定符。它的语法如下:

const_cast<type>(expression)

其中,type是要转换的类型,expression是要转换的表达式。const_cast将expression转换为type类型,并去除其中的常量限定符。

#include <iostream>void func(const int* p) {int* q = const_cast<int*>(p);  // 去除p的常量限定符*q = 20;  // 修改*q的值,会导致未定义行为
}int main() {int x = 10;const int* p = &x;std::cout << *p << std::endl;  // 输出10func(p);std::cout << *p << std::endl;  // 输出20,未定义行为return 0;
}

在这个示例中,我们定义了一个名为func的函数,其参数是一个指向常量整型的指针const int* p。在函数中,我们使用const_cast去除了p的常量限定符,并将其转换为一个指向整型的指针int* q。然后,我们修改了*q的值,并返回到main函数中,输出了*p的值。

由于p指向的是一个常量整型,而我们在func函数中使用const_cast去除了其常量限定符,并修改了其值,因此会导致未定义行为。因此,使用const_cast进行类型转换时,必须确保转换后的对象不会被修改。

15. C++如何实现只在栈上实例化对象?

C++中可以通过将构造函数声明为私有的方式来实现只在栈上实例化对象。这样,只有在类内部或友元函数中才能调用构造函数,从而防止在堆上实例化对象。

示例代码如下:


Copy
class StackOnly {
public:static StackOnly create() {return StackOnly();}private:StackOnly() {}  // 构造函数为私有~StackOnly() {}  // 防止在堆上删除对象
};int main() {// 只能在栈上实例化对象StackOnly obj = StackOnly::create();// 以下代码会编译错误// StackOnly* ptr = new StackOnly();// delete ptr;return 0;
}

构造函数并不会默认在堆上创建对象。C++中,创建对象有两种方式:在栈上创建和在堆上创建。

在栈上创建对象,就是在函数内部或者main函数中直接使用类名创建对象,例如:

MyClass obj;

在堆上创建对象,就是使用new关键字动态地分配内存来创建对象,例如:

MyClass* ptr = new MyClass();

如果将类的构造函数声明为私有,则外部无法直接调用该构造函数创建对象,只有在类内部或友元函数中才能调用构造函数创建对象。这样就可以防止在堆上实例化对象,从而实现只在栈上实例化对象的目的。

需要注意的是,即使将构造函数声明为私有的,如果在类内部或友元函数中使用new关键字来创建对象,仍然可以在堆上创建对象。因此,如果要完全禁止在堆上创建对象,还需要禁止使用new关键字或者重载new运算符。

16. Vector具体是怎么分配函数的?

C++中的vector是一种动态数组,它可以自动增长和缩小,以适应数据的大小变化。vector的扩容实现是通过重新分配内存空间来实现的。当vector的容量不足时,会重新分配一块更大的内存空间,并将原有的数据复制到新的内存空间中。

以下是vector扩容的实现代码:

Copy
template <class T, class Allocator>
void vector<T, Allocator>::reserve(size_type new_capacity)
{if (new_capacity > capacity()){T* new_data = allocator.allocate(new_capacity);std::uninitialized_copy(std::make_move_iterator(begin()), std::make_move_iterator(end()), new_data);destroy_elements();deallocate();data_ = new_data;capacity_ = new_capacity;}
}

在实现中,reserve函数首先检查新的容量是否大于当前容量,如果是,则重新分配内存空间。使用allocator.allocate函数分配新的内存空间,并使用std::uninitialized_copy函数将原有数据复制到新的内存空间中。接着调用destroy_elements函数销毁原有数据,并调用deallocate函数释放原有内存空间。最后将data_指针指向新的内存空间,capacity_更新为新的容量。

JAVA 的 ArrayList

ArrayList类底层使用一个数组来存储数据,并且在需要扩容时会创建一个新的数组,并将原有的数据复制到新的数组中。

以下是ArrayList扩容的实现代码:

private void grow(int minCapacity) {int oldCapacity = elementData.length;int newCapacity = oldCapacity + (oldCapacity >> 1);if (newCapacity - minCapacity < 0)newCapacity = minCapacity;if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);elementData = Arrays.copyOf(elementData, newCapacity);
}

在实现中,grow方法首先计算新的容量newCapacity,它是原有容量的1.5倍(即oldCapacity + (oldCapacity >> 1))。然后,grow方法检查新的容量是否大于MAX_ARRAY_SIZE,如果是,则将新的容量设置为MAX_ARRAY_SIZE。接着,grow方法调用Arrays.copyOf方法创建一个新的数组,并将原有的数据复制到新的数组中。最后,将elementData指向新的数组。

需要注意的是,Java的动态数组ArrayList类是一种引用类型,它存储的是对象的引用,而不是对象本身。因此,在扩容时,ArrayList类只需要复制对象的引用,而不需要复制对象本身。

17. C++如何新建线程池

C++中可以使用标准库和第三方库来实现线程池。以下是一种基于C++11标准库的线程池实现方式:

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <vector>
#include <queue>
#include <functional>class ThreadPool
{
public:ThreadPool(size_t num_threads){for (size_t i = 0; i < num_threads; ++i){threads_.emplace_back([this](){while (true){std::function<void()> task;{std::unique_lock<std::mutex> lock(mtx_);cv_.wait(lock, [this]() { return !tasks_.empty() || stop_; });if (stop_ && tasks_.empty()) return;task = std::move(tasks_.front());tasks_.pop();}task();}});}}~ThreadPool(){{std::unique_lock<std::mutex> lock(mtx_);stop_ = true;}cv_.notify_all();for (auto& thread : threads_){thread.join();}}template <class Func, class... Args>void enqueue(Func&& func, Args&&... args){std::function<void()> task = std::bind(std::forward<Func>(func), std::forward<Args>(args)...);{std::unique_lock<std::mutex> lock(mtx_);tasks_.emplace(std::move(task));}cv_.notify_one();}private:std::vector<std::thread> threads_;std::queue<std::function<void()>> tasks_;std::mutex mtx_;std::condition_variable cv_;bool stop_ = false;
};

这段代码中的花括号是用于控制std::unique_lockstd::mutex lock对象的作用域的。花括号内部的代码块被称为锁定区域(lock_guard region),在这个区域中,lock对象会被自动加锁,从而保护了任务队列的访问。当代码块执行完毕后,lock对象会被自动解锁,从而允许其他线程访问任务队列。
而条件则是通过cv_.wait函数的第二个参数来传递的,这个参数是一个lambda表达式,用于检查等待条件是否满足。在这个lambda表达式中,我们使用了成员变量tasks_和stop_来检查任务队列是否为空,或线程池是否被标记为停止。如果这个lambda表达式返回false,则cv_.wait函数会将当前线程阻塞,等待条件满足后再继续执行。

这个线程池实现了以下功能:

构造函数创建指定数量的线程,并在每个线程中执行一个无限循环,从任务队列中获取任务,并执行任务。

析构函数标记线程池停止,并通知所有线程退出循环,并等待所有线程退出。

enqueue函数将任务添加到任务队列中,并通知一个线程去执行任务。

使用时,可以通过创建ThreadPool对象来创建线程池,并使用enqueue函数将任务添加到任务队列中。例如:

#include <iostream>
#include <chrono>void func(int i)
{std::cout << "Task " << i << " started" << std::endl;std::this_thread::sleep_for(std::chrono::seconds(1));std::cout << "Task " << i << " finished" << std::endl;
}int main()
{ThreadPool pool(4);for (int i = 0; i < 8; ++i){pool.enqueue(func, i);}return 0;
}

这个例子创建了一个包含4个线程的线程池,并添加了8个任务到任务队列中。每个任务执行1秒钟的sleep操作,并输出任务的开始和结束信息。可以看到,线程池会自动分配线程去执行任务,并且任务执行的顺序是不确定的。

18. 描述char*、const char*、char* const、const char* const的区别?

这四种类型的定义都是指向 char 类型的指针,但它们的含义和用法有所不同。

char*:表示指向 char 类型的指针,可以通过该指针来修改所指向的字符。

const char*:表示指向 char 类型常量的指针,指针本身可以修改,但不能通过该指针来修改所指向的字符。

char* const:表示指向 char 类型的常指针,指针本身不能修改,但可以通过该指针来修改所指向的字符。

const char* const:表示指向 char 类型常量的常指针,指针本身和所指向的字符都不能修改。

下面分别对这四种类型进行详细说明:

char*:表示指向 char 类型的指针,可以通过该指针来修改所指向的字符,例如:

char* str = "hello";
str[0] = 'H'; // 可以修改所指向的字符

const char*:表示指向 char 类型常量的指针,指针本身可以修改,但不能通过该指针来修改所指向的字符,例如:

const char* str = "hello";
str = "world"; // 可以修改指针本身,指向另一个字符串常量
str[0] = 'H'; // 不能修改所指向的字符

char* const:表示指向 char 类型的常指针,指针本身不能修改,但可以通过该指针来修改所指向的字符,例如:

char* const str = "hello";
str[0] = 'H'; // 可以修改所指向的字符
str = "world"; // 不能修改指针本身

const char* const:表示指向 char 类型常量的常指针,指针本身和所指向的字符都不能修改,例如:

const char* const str = "hello";
str[0] = 'H'; // 不能修改所指向的字符
str = "world"; // 不能修改指针本身

需要注意的是,对于字符串常量,应该使用 const char* 类型的指针来进行引用,而不是 char* 类型的指针,因为字符串常量是只读的,不能通过指针修改其中的内容。如果需要修改字符串内容,应该使用 char 数组或动态分配的内存。

19. 虚函数和普通函数的区别?

虚函数需要在运行时动态绑定,而普通函数在编译期就已经确定了调用的函数。因此,虚函数的调用需要额外的指针操作,会带来一定的性能开销。

虚函数可以被派生类重写,而普通函数不能被派生类重写。

虚函数可以实现多态,即同一个函数名可以在不同的派生类中有不同的实现。而普通函数只能有一个实现。

总之,在需要多态性和动态绑定的情况下,虚函数是非常有用的。但是,在不需要多态性和动态绑定的情况下,使用普通函数会更加高效。

虚函数的调用

#include <iostream>
using namespace std;class Shape {
public:virtual void draw() {cout << "Drawing shape..." << endl;}
};class Circle : public Shape {
public:void draw() override {cout << "Drawing circle..." << endl;}
};int main() {Circle c;Shape* p = &c;p->draw();   // 调用 Circle 类的 draw 函数return 0;
}

20.new[]和delete[]一定要配对使用吗?为什么?

new[]

当需要在程序运行期间动态创建一个数组时,可以使用new[]操作符来分配数组内存。下面是一个new[]的例子:

int* arr = new int[10]; // 在堆上分配一个包含10个整数的数组

这行代码将在堆上分配一个包含10个整数的数组,并返回指向该数组第一个元素的指针。这意味着arr指向一个可以通过下标访问的、大小为10的整数数组。

delete[]

在使用完这个数组后,应该使用delete[]操作符来释放这个数组的内存:

delete[] arr; // 释放arr指向的数组内存

这行代码将释放arr指向的整数数组的内存,以便在程序运行期间可以再次使用该内存。

new[]和delete[]必须要配对使用。new[]用于在堆上动态分配数组内存,而delete[]用于释放这些内存。如果在使用new[]动态分配数组内存后,使用delete释放内存,会导致未定义的行为,因为delete不能正确地释放动态分配的数组内存。同样地,如果使用new分配单个对象的内存,却使用delete[]释放内存,也会导致未定义的行为。

这是因为new[]和delete[]在实现上是不同的操作,new[]分配的内存包含了数组长度信息,而delete[]需要利用这个信息来正确释放内存。如果使用了不匹配的释放操作,就会导致内存泄漏或者内存访问错误等问题。

因此,为了保证程序的正确性和健壮性,new[]和delete[]必须要配对使用。

21.inline内联函数的特点有哪些?它的优缺点是什么?

内联函数

内联函数是一种特殊的函数,它的定义通常出现在头文件中。当程序调用内联函数时,编译器会将函数的代码插入到调用点处,而不是像普通函数那样通过跳转到函数体来执行。

内联函数的目的是为了优化程序的性能。由于内联函数的代码在调用点处被插入,可以减少函数调用的开销,提高程序的运行速度。

内联函数通常比普通函数短小,避免了函数调用和返回时的额外开销。内联函数的代码通常比较简单,并且不包含循环、递归等复杂的结构。

Q:你的意思是 普通的函数是一块一块编译的。然后通过跳转来访问,而内联函数是直接在调用处编译,附加在原函数中?

是的,普通函数的代码是编译成一块独立的代码块,在调用时通过跳转来访问。而内联函数的代码是直接插入到调用点处,不需要通过跳转来访问。内联函数的代码被编译器嵌入到调用点处,可以减少函数调用的开销,提高程序的运行速度。但是也会增加代码的体积,因为内联函数的代码会被插入到调用点处,可能会导致代码的重复。

优点

  1. 内联函数可以减少函数调用的开销,提高程序的运行速度。
  2. 内联函数通常比较简单,可以避免函数调用和返回时的额外开销,从而提高程序的效率。
  3. 内联函数的定义通常出现在头文件中,可以使代码更加简洁、清晰。

缺点

1.内联函数的代码被插入到调用点处,会增加代码的体积,可能会导致可执行文件的大小增加。
2.内联函数的定义通常出现在头文件中,如果头文件被多个源文件包含,可能会导致代码的重复,增加编译时间和可执行文件的大小。
3. 编译器对内联函数的支持不一致,可能会导致代码的可移植性问题。

22. 如何计算结构体长度?

在C++中,结构体的长度可以使用sizeof操作符来获取。sizeof操作符返回结构体的大小,以字节为单位。

例如,对于以下的结构体:

struct Person {char name[20];int age;float height;
};

可以使用sizeof操作符来计算它的长度,如下所示:

size_t size = sizeof(Person);

这会返回Person结构体的大小,以字节为单位。在计算结构体长度时,需要注意结构体成员的对齐方式。不同的编译器可能会有不同的对齐方式,导致结构体大小不同。因此,在编写代码时,应该遵循编译器的对齐方式,以确保结构体大小正确。

23.std::vector最大的特点是什么?它的内部是怎么实现的?resize和reserve的区别是什么?clear是怎么实现的?

std::vector最大的特点是它是一个动态数组,可以根据需要自动扩展或收缩容量,提供了类似于数组的访问方式,同时还提供了很多方便的方法来操作容器。

std::vector的内部是通过连续的内存块来实现的,即在内存中分配一段连续的空间,用来存储元素。当需要添加元素时,如果容量不足,就会自动扩展容器的大小,重新分配一段更大的内存空间,将原有元素复制到新的内存空间中,并释放原来的内存空间。当需要删除元素时,如果容量太大,就会自动收缩容器的大小,重新分配一段更小的内存空间,将原有元素复制到新的内存空间中,并释放原来的内存空间。

resize和reserve的区别是:resize用于改变容器的大小,并将新元素初始化为默认值,如果新容量小于当前大小,则会删除多余的元素。reserve仅仅是改变容器的容量,不会改变容器的大小或元素个数,也不会初始化新元素。

clear用于清空容器中的元素,将容器的大小设置为0。clear的实现方式是调用容器中每个元素的析构函数来销毁元素,然后将容器的大小设置为0。注意,clear只会销毁元素,不会释放内存,因此容器的容量不会改变。如果需要释放内存,可以使用shrink_to_fit方法。

假设你的vector的名称是vec,它的容量为10,大小为5,现在你想访问vec[10]-vec[14]这些超出了vector范围的元素,你可以这样做:

int* ptr = &vec[10];  // 获取指向vec[10]的指针
int value = *(ptr + 2);  // 访问vec[12]的值

这里我们定义了一个指向vec[10]的指针ptr,并通过指针算术运算访问vec[12]的值。需要注意的是,这种方式非常不安全,容易导致程序崩溃或者产生未定义的行为,应该尽量避免使用。

24谈一谈你对左值和右值的了解,了解左值引用和右值引用吗?

左值&右值

左值和右值是C++中的两个概念,左值是指可以取地址的表达式,右值是指不能取地址的表达式。左值通常指代变量,右值通常指代常量或者表达式的计算结果。

左值引用&右值引用

左值引用和右值引用是C++11中的新特性,它们是对左值和右值的引用。左值引用是指对左值进行的引用,右值引用是指对右值进行的引用。

  1. 左值引用使用&符号来声明,例如int& a = b;表示将a引用到变量b上。左值引用可以修改所引用的变量的值。
  2. 右值引用使用&&符号来声明,例如int&& a = 10;表示将a引用到常量10上。右值引用通常用于移动语义(move semantics),即将一个对象的资源所有权转移给另一个对象,从而避免不必要的复制操作,提高程序的性能。

左值和右值是C++中的基本概念,左值引用和右值引用是C++11中新增的特性,它们可以提高程序的效率和灵活性。

25. c++的内存结构是什么?

C++ 的内存结构主要分为以下几个部分:

栈(Stack):栈是一种后进先出(LIFO)的数据结构,用于存储函数的局部变量、函数的参数、函数调用的上下文等。在函数调用时,系统会为每个函数分配一段栈空间,函数执行完毕后,这段栈空间会被自动回收。

堆(Heap):堆是一种动态分配的内存空间,用于存储动态分配的对象。堆的大小没有固定限制,可以根据需要动态增长或缩小。程序员需要手动管理堆中的内存,包括分配、释放等操作。

全局变量区(Data Segment):全局变量区用于存储全局变量和静态变量,它的大小在程序运行前就已经确定,程序结束时才会被释放。

常量区(Const Segment):常量区用于存储常量字符串和其他常量数据,它的大小在程序运行前就已经确定,程序结束时才会被释放。

代码区(Code Segment):代码区用于存储程序的指令代码,它的大小在程序运行前就已经确定,程序结束时才会被释放。

C++ 的内存结构在不同的操作系统和编译器上可能会有所不同,但以上几个部分是比较常见的。程序员需要了解这些内存结构,才能更好地编写高效、安全的 C++ 代码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/143437.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

一键生成,轻松制作个性化瓜分红包活动二维码

在如今竞争激烈的市场中&#xff0c;营销活动成为各个品牌推广的重要手段。而在朋友圈这个信息交流的平台上&#xff0c;如何引起用户的关注和参与&#xff0c;成为了每个营销人员的关注焦点。而打造一个引爆朋友圈的瓜分红包活动&#xff0c;无疑是一种非常有效的方法。接下来…

【C++杂货店】类和对象(上)

【C杂货店】类和对象&#xff08;上&#xff09; 一、面向过程和面向对象初步认识二、类的引入三、类的定义四、类的访问限定符及封装4.1 访问限定符4.2 封装 五、类的作用域六、类的实例化七、类对象模型7.1 类对象的存储规则7.2 例题7.3结构体内存对齐规则 八、this指针8.2 t…

版本控制系统:Perforce Helix Core -2023

Perforce Helix Core是领先的版本控制系统&#xff0c;适用于需要加速大规模创新的团队。存储并跟踪您所有数字资产的更改&#xff0c;从源代码到二进制再到IP。连接您的团队&#xff0c;让他们更快地行动&#xff0c;更好地构建。 通过 Perforce 版本控制加速创新 Perforce H…

Zabbix介绍与安装

目录 一、概述 二、zabbix的主要功能 三、zabbix监控原理 四、Zabbix 监控模式 五、zabbix的架构 server-client server-proxy-client master-node-client 六、zabbix的安装 安装zabbix服务端 安装zabbix客户端 测试zabbix 1、在 Web 页面中添加 agent 主机点击左…

opencv英文识别tesseract-orc安装

文章目录 一、安装并保存所在路径二、配置环境变量1、打开高级设置2、配置环境变量三、修改tesseract.py文件中的路径,否则运行报错1、进入python所在的文件夹,找到Lib,site-packages2、搜索pytesseract3、打开py文件修改路径一、安装并保存所在路径 特别注意路径名中不能有…

软件工程第三周

可行性研究 续 表达工作量的方式 LOC估算&#xff1a;Line of Code 估算公式S(Sopt4SmSpess)/6 FP&#xff1a;功能点 1. LOC (Line of Code) 估算 定义&#xff1a;LOC是指一个软件项目中的代码行数。 2. FP (Function Points) 估算 定义&#xff1a;FP是基于软件的功能性和…

【操作】国标GB28181视频监控EasyGBS平台更新设备信息时间间隔

国标GB28181协议视频平台EasyGBS是基于GB28181协议的视频监控云服务平台&#xff0c;可支持多路设备同时接入&#xff0c;并对多平台、多终端分发出RTSP、RTMP、FLV、HLS、WebRTC等格式的视频流。平台可提供视频监控直播、云端录像、云存储、检索回放、智能告警、语音对讲、平台…

k8s-2 集群升级

首先导入镜像到本地 然后上传镜像到仓库 在所有集群节点 部署cri-docker k8s从1.24版本开始移除了dockershim&#xff0c;所以需要安装cri-docker插件才能使用docker 配置cri-docker 升级master 节点 升级kubeadm 执行升级计划 修改节点套接字 腾空节点 升级kubelet 配置k…

2009-2018年各省涉农贷款数据(wind)

2009-2018年各省涉农贷款数据&#xff08;wind&#xff09; 1、时间&#xff1a;:209-2018年 2、范围&#xff1a;31省 3、来源&#xff1a;wind 4、指标&#xff1a;涉农贷款 指标解释 &#xff1a;在涉农贷款的分类上&#xff0c;按照城乡地域将涉农贷款分为农村贷款和城…

Django之视图

一&#xff09;文件与文件夹 当我们设定好一个Djiango项目时&#xff0c;里面会有着view.py等文件&#xff0c;也就是文件的方式&#xff1a; 那么我们在后续增加app等时&#xff0c;view.py等文件会显得较为臃肿&#xff0c;当然也根据个人习惯&#xff0c;这时我们可以使用…

华为云云耀云服务器L实例评测 | 实例使用教学之简单使用:通过 Docker 容器化技术在华为云云耀云服务器快速构建网站

华为云云耀云服务器L实例评测 &#xff5c; 实例使用教学之简单使用&#xff1a;通过 Docker 容器化技术在华为云云耀云服务器快速构建网站 介绍华为云云耀云服务器 华为云云耀云服务器 &#xff08;目前已经全新升级为 华为云云耀云服务器L实例&#xff09; 华为云云耀云服务器…

如何去开展软件测试工作

1. 软件测试 在一般的项目中&#xff0c;一开始均为手动测试&#xff0c;由于自动化测试前期投入较大&#xff0c;一般要软件项目达到一定的规模&#xff0c;更新频次和质量均有一定要求时才会上自动化测试或软件测试。 1.1. 项目中每个成员的测试职责 软件测试从来不是某一…

每天学习3个小时能不能考上浙大MBA项目?

不少考生经常会问到上岸浙大MBA项目想要复习多长时间&#xff0c;这个问题其实没有固定答案。在行业十余年的经验总结来看&#xff0c;杭州达立易考教育认为基于每一位考生的个人复习时间、个人学习能力以及原有基础情况等不同&#xff0c;复习上岸的预期分数目标也会有差异&am…

112. 路径总和

力扣题目链接(opens new window) 给定一个二叉树和一个目标和&#xff0c;判断该树中是否存在根节点到叶子节点的路径&#xff0c;这条路径上所有节点值相加等于目标和。 说明: 叶子节点是指没有子节点的节点。 示例: 给定如下二叉树&#xff0c;以及目标和 sum 22&#xf…

Unity中Shader用到的向量的乘积

文章目录 前言一、向量的乘法1、点积2、差积 二、点积&#xff08;结果是一个标量&#xff09;1、数学表示法2、几何表示法 三、叉积1、向量叉积的结果 与 两个相乘的向量互相垂直2、判断结果正负方向的方法&#xff1a;右手法则 前言 Unity中Shader用到的向量的点积 一、向量…

手机能搜到某个wifi,电脑搜不到解决方法(也许有用)

方法一&#xff1a;更新驱动 下载驱动大师、驱动精灵等等驱动软件&#xff0c;更新网卡驱动 方法二 按 win 键&#xff0c;打开菜单 搜索 查看网络连接&#xff08;win11版本是搜这个名字&#xff09; 点击打开是这样式的 然后对 WLAN右击->属性->配置->高级 这…

unittest单元测试框架使用

什么是unittest 这里我们将要用的unittest是python的单元测试框架&#xff0c;它的官网是 25.3. unittest — Unit testing framework — Python 2.7.18 documentation&#xff0c;在这里我们可以得到全面的信息。 当我们写的用例越来越多时&#xff0c;我们就需要考虑用例编写…

如何使用 API 接口获取商品数据,从申请 API 接口、使用 API 接口到实际应用,一一讲解

在当今的数字化时代&#xff0c;应用程序接口&#xff08;API&#xff09;已经成为数据获取的重要通道。API 接口使得不同的应用程序能够方便地进行数据交换&#xff0c;从而促进了信息的广泛传播和利用。在众多的数据源中&#xff0c;商品数据是一个非常重要的领域&#xff0c…

Win/Mac版Scitools Understand教育版申请

这里写目录标题 前言教育版申请流程教育账号申请 前言 上篇文章为大家介绍了Scitools Understand软件&#xff0c;通过领取的反馈来看有很多朋友都想用这个软件&#xff0c;但是我的网盘里只存了windows的pojie版&#xff0c;没有mac版的&#xff0c;我没有去网上找相关的资源…

【Java 进阶篇】数据定义语言(DDL)详解

数据定义语言&#xff08;DDL&#xff09;是SQL&#xff08;结构化查询语言&#xff09;的一部分&#xff0c;它用于定义、管理和控制数据库的结构和元素。DDL允许数据库管理员、开发人员和其他用户创建、修改和删除数据库对象&#xff0c;如表、索引、视图等。在本文中&#x…