C++ 面试
- 知识点
- 1、C++ 申请十个 char 的堆空间,并且释放
- 2、vector 和 list 有什么区别
- 3、std::remove_if
- 4、std::remove
- 5、Lamda表达式
- 6、内存划分的类型
- 7、栈和堆
- 8、虚表
- 9、new、malloc区别
- 10、__stdcall和__cdcel 调用方式区别、参数入栈区别
- 11、智能指针
- 12、TCP/IP协议
- 14、static_cast及dynamic_cast的底层原理
- 15、不用虚函数如何实现多态
- 16、构造函数
- 17、如何禁止一个类被继承
- 18、线程的通信方式,同步方式,为什么要进行同步?系统是如何给线程分配一个栈的?
- 19、const用法,define用法 区别
- 20、sizeof和strlen的区别
- 21、伪函数
- 22、如何设计线程池
- 23、connect的原理
- 24、STL原理你看过哪些
- 25、static 类的构造和析构是什么时候
- 26、XML如何自己实现解析
- 27、不看代码,CPU100%如何找出原因
- 28、
- 算法题
知识点
1、C++ 申请十个 char 的堆空间,并且释放
char* heapArray = new char[10];
delete[] heapArray; //注意释放的是数组
2、vector 和 list 有什么区别
Vector | List | |
---|---|---|
存储方式 | 连续内存空间 | 链式数据结构 |
访问速度 | 快(常数时间) | 慢(线性时间) |
插入/删除操作 | 相对较慢(线性时间) | 相对较快(常数时间) |
内存管理 | 自动调整容量 | 动态分配节点 |
迭代器效率 | 高效 | 低效 |
内存占用 | 相对小 | 相对大 |
-
存储方式:vector 使用数组实现,连续存储元素,而 list 使用链表实现,非连续存储元素。这意味着在 vector 中,元素在内存中是连续存储的,可以通过指针进行快速访问;而在 list 中,元素在内存中不是连续存储的,只能通过迭代器进行访问。
-
插入和删除操作效率:由于存储方式的不同,list 在任意位置插入和删除元素的效率要比 vector 高。在 list 中,插入和删除操作只需要对相应节点进行指针链接的修改,时间复杂度为 O(1);而在 vector 中,插入和删除操作可能需要移动其他元素,时间复杂度为 O(n),其中 n 是元素个数。
-
随机访问效率:由于 vector 的元素在内存中是连续存储的,所以支持通过下标直接访问元素,时间复杂度为 O(1);而 list 不支持通过下标访问元素,只能通过迭代器进行顺序访问,时间复杂度为 O(n),其中 n 是访问元素的位置。
-
内存占用:由于 vector 连续存储元素,所以每个元素只需要固定大小的内存空间,并且没有额外的指针开销。而 list 中的每个元素需要额外的指针来链接前后节点,对于大量小对象的情况下,vector 的内存占用通常比 list 更低。
综上所述,如果需要频繁地进行元素的插入和删除操作,并且不需要通过下标随机访问元素,可以优先选择使用 list。而如果需要频繁地通过下标随机访问元素,或者元素数量较少且需要较少的内存占用,可以选择使用 vector。
2.1 vector存放类有什么要求
在C++中,std::vector
是一个动态数组,它可以存放各种类型的对象,包括自定义的类对象。但是,为了使类与std::vector
兼容并确保其正常工作,类应满足以下要求:
-
可复制或可移动:为了在vector中存放对象,类应该提供拷贝构造函数、拷贝赋值运算符、移动构造函数和移动赋值运算符。如果类中定义了移动操作,那么在需要的时候,
std::vector
可以更高效地重新分配其内部存储。 -
默认构造函数:在某些情况下,如
resize()
方法,std::vector
可能需要默认构造函数来创建对象。 -
析构函数:当
std::vector
的大小减小或其被销毁时,它会销毁其内部的对象,所以类应该有一个析构函数来正确地清理资源。 -
连续内存存储:
std::vector
保证其元素在内存中是连续存储的。因此,存储在vector中的类不应该依赖于非连续的内存分配策略。 -
深拷贝 vs. 浅拷贝:如果类内部有指向动态分配内存的指针,确保正确实现拷贝构造函数和拷贝赋值运算符,以避免浅拷贝导致的问题。
-
异常安全:当进行操作(如
push_back()
)时,如果类的方法抛出异常,应确保类的状态保持一致,并且不泄露资源。 -
赋值:类应该支持赋值操作,因为
std::vector
的某些操作(如std::fill()
)可能需要它。
注意:对于大多数简单的类,编译器会为你自动生成默认构造函数、拷贝构造函数、移动构造函数、拷贝赋值运算符、移动赋值运算符和析构函数。但是,如果你为类定义了任何这些函数,编译器可能不会自动生成其他的。因此,确保你为类提供了所有必要的函数,以使其与std::vector
兼容。
3、std::remove_if
可以用于删除容器中满足指定条件的元素。该算法会将符合条件的元素移到容器的末尾,并返回指向新的逻辑末尾的迭代器,不会实际删除元素,返回的是迭代器!
- 函数原型
template<class ForwardIt, class UnaryPredicate>
ForwardIt remove_if(ForwardIt first, ForwardIt last, UnaryPredicate p)
{
first = std::find_if(first, last, p);
if (first != last)
for(ForwardIt i = first; ++i != last; )
if (!p(*i))
*first++ = std::move(*i);
return first;
}
- 例子
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
bool isSpace(char x) { return x == ' '; }
int main()
{
string s2("Text with spaces");
cout << "删除之前"<<s2 << endl;
s2.erase(remove_if(s2.begin(), s2.end(), isSpace), s2.end());
cout <<"删除之后"<< s2 << endl;
return 0;
}
4、std::remove
std::remove
是 C++ 标准库中的一个算法,用于移除序列中与给定值相等的所有元素。但它的行为可能与一些开发者的直觉不符,因为它并不真正地从容器中删除元素。
用法:
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3, 2, 4, 2, 5};
auto new_end = std::remove(v.begin(), v.end(), 2);
v.erase(new_end, v.end());
for (int i : v) {
std::cout << i << " ";
}
return 0;
}
在上面的例子中,我们首先使用 std::remove
移除所有值为2的元素。然后,我们使用 erase
方法真正地从 std::vector
中删除这些元素。
行为:
std::remove
通过重排容器中的元素来工作。它将所有不等于给定值的元素移到容器的前面,并返回一个迭代器,指向序列中的新“结束”。
这意味着在 std::remove
之后,序列的“有效”元素都在前面,而后面的元素是未定义的。为了真正删除这些元素,你需要使用容器的 erase
方法。
注意:
std::remove
不会改变容器的大小。为了真正删除元素并缩小容器,你需要调用容器的erase
方法。- 由于
std::remove
只是重排元素,所以它可以在普通数组上工作,而不仅仅是容器。 - 存在一个常见的误区,即认为
std::remove
会缩小容器。这是不正确的,你总是需要调用erase
来完成这个操作。
为了简化这个过程,一些容器(如 std::vector
)提供了一个 erase-remove
惯用法,如上面的例子所示。
5、Lamda表达式
本质 | 特点 | |
---|---|---|
定义 | 匿名函数对象 | 可以在需要函数的地方直接定义和使用,无需命名和定义独立函数 |
形式 | [捕获列表] (参数列表) -> 返回类型 { 函数体 } | 捕获列表:用于捕获外部变量;参数列表:传递给Lambda的参数;返回类型:指定Lambda表达式的返回类型;函数体:Lambda表达式的具体实现部分 |
作用域 | 局部作用域 | Lambda函数可访问其所在作用域的局部变量、全局变量、静态变量等 |
捕获外部变量 | 可以按值捕获(=)或按引用捕获(&),也可以混合使用 | 按值捕获:复制外部变量的值到Lambda表达式的闭包中;按引用捕获:通过引用在Lambda表达式中使用外部变量,可以修改外部变量的值 |
类型推导 | 自动推导参数和返回类型 | 可以省略参数和返回类型的显式声明,由编译器根据Lambda表达式的实现自动推导 |
简洁性 | 简洁的语法形式 | 使用Lambda表达式可以简化代码,特别是处理函数对象的场景 |
5.1 lamda表达式和仿函数有什么关联
Lambda表达式和仿函数(函数对象)之间有密切的关联。实际上,Lambda表达式本质上就是一个匿名的仿函数。
Lambda表达式可以被看作是一种便捷的编写和使用函数对象的方式。它提供了一种简洁的语法形式,使得我们可以在需要函数对象的地方直接定义和使用匿名的函数。Lambda表达式可以像函数一样被调用,并且可以访问其所在作用域的局部变量、全局变量等。
与仿函数类似,Lambda表达式也可以传递给算法函数或作为函数对象的参数使用。它可以完成类似于仿函数的功能,如执行某种操作、进行元素的筛选、排序、转换等。通过捕获外部变量,Lambda表达式还可以在函数体内部使用和修改这些变量的值。
因此,Lambda表达式可以看作是一种特殊的仿函数,它提供了更加便捷的语法形式以及直接在需要的地方进行定义和使用的能力。同时,Lambda表达式的出现也使得编写和使用函数对象更加灵活和简便。
6、内存划分的类型
存储区 | 特点 | 段名 |
---|---|---|
堆 | 由new 分配的内存块,程序员自己控制释放 | 无 |
栈 | 编译器在需要时分配,在不需要时自动清除 | 无 |
全局/静态存储区 | 存放全局和静态变量 | .bss段/.data段 |
常量存储区 | 存放常量,不允许修改 | .rodata段 |
代码区 | 存放可执行代码 | .text段 |
7、栈和堆
7.0 一个进程里头有几个heap,几个stack
一个进程一个堆,一个线程一个栈。也就是说栈是跟线程相关的,堆是跟进程相关。
数量 | |
---|---|
堆 | 1个 |
栈 | 取决于线程数量 |
请注意,一个进程只有一个堆,而栈的数量取决于线程的创建和销毁。每个线程都有自己的栈用于保存局部变量、函数调用和返回地址等信息。
7.1 什么是栈
- 栈是为了线程的运行而服务的,主要用于存储函数调用过程中的局部变量和程序数据。当一个函数被调用时,栈会为该函数分配一个块(block),并将其置于栈顶。这个块用于存储函数的参数、局部变量和其他相关数据。
- 函数返回时,这个block被释放以便下一次函数调用时使用。栈采用LIFO的顺序来存储,最经常被保留的block往往是下一个被释放的block。这使得跟踪一个block的使用变得很简单。
7.2 什么是堆
- 堆则类似于链表(linked list),它是一个无序的内存区域,可以灵活地分配和释放内存
- 堆的大小没有固定限制,可以根据需要动态增长或缩减。
- 堆中的数据存储方式没有特定的顺序,每个数据项的访问和操作都必须通过指针来实现。
7.3 什么情况下栈会溢出
-
递归调用层数过深:当一个函数递归调用自身或其他函数多次时,每一次函数调用都会在栈上分配一块内存来存储函数的返回地址、局部变量等信息。如果递归层数过深,栈空间会被耗尽,从而导致栈溢出。
-
局部变量过多:每个函数调用都会在栈上为局部变量分配内存空间。如果函数内部定义了大量的局部变量,或者这些局部变量占用的内存空间很大,就可能导致栈空间被耗尽。
-
无限循环:如果程序中存在没有明确终止条件的无限循环,每次循环迭代都会产生一个新的函数调用,从而逐渐耗尽栈空间。
-
函数调用嵌套过多:如果在短时间内频繁地进行函数调用,每次调用都会在栈上分配一段内存,累积起来可能导致栈空间耗尽。
7.4 什么情况下堆会溢出
- 动态申请空间使用之后没有释放
- 数组访问越界
- 指针非法访问。指针保存了一个非法的地址,通过这样的指针访问所指向的地址时会产生内存访问错误
- 无限new
8、虚表
8.1 多态的本质是什么
- 通过统一的接口来操作不同类型的对象,实现代码的灵活性和可扩展性
- 继承和方法重写:子类可以继承父类的属性和方法,并且可以重写(覆盖)父类的方法,以便根据自身的特点进行定制化的实现。
- 动态绑定(动态分派):通过父类引用指向子类对象时,编译器会根据引用的实际类型(而非声明类型)来决定调用哪个子类的方法,使程序在运行时根据实际对象的类型来确定具体执行的方法。
8.2 虚函数表在类的哪里?如何起作用
-
虚函数表中的指针顺序,按照虚函数声明的顺序。
-
基类的虚函数指针在派生类的前面。
-
多个基类之间的虚函数,按照继承的顺序,存放虚函数指针。
-
基类内部的虚函数,按照虚函数内部声明的顺序存放。
-
虚函数表中,派生类重写的虚函数替换了基类虚函数指针,并指向了派生类的函数实现
-
同一个类的不同实例共用同一份虚函数表, 她们都通过一个所谓的虚函数表指针__vfptr(定义为void**类型)指向该虚函数表
8.3 构造函数和析构函数可以写为virtual吗?为什么
- 构造函数不可以
- 构造函数不能被声明为virtual。这是因为构造函数是在创建类的对象时被调用的,这时候虚表(vtable)可能还没有被设置,因此我们不能有一个指向虚构造函数的虚表入口。
- 当创建一个派生类对象时,它的构造函数是由基类的构造函数开始,然后再到派生类的构造函数。这个过程是静态的,编译器知道哪个构造函数将被调用,所以不需要虚构造函数。
- 析构函数可以为virtual,多态的时候必须设置为virtual。这样才能在vTable中增加自己的析构函数,否则只会调用基类的析构。
8.4 虚函数表最多只有一个吗?
每个含有虚函数的基类都会有一个虚表。因此,一个多重继承的对象可能会有多个vptr,每个vptr指向一个不同的虚表。但在单一继承的情况下,对象只会有一个vptr
8.5 指向虚表的指针是什么完成初始化的
vptr的初始化是在对象的构造过程中完成的。具体来说,当你创建一个多态对象时,其构造函数会设置vptr以指向该类的vtable。如果有多层继承,每个类的构造函数都会更新vptr,使其指向其自己的vtable。
class Base {
public:
Base() { /* Base的构造函数 */ }
virtual void func() { /* ... */ }
};
class Derived : public Base {
public:
Derived() : Base() { /* Derived的构造函数 */ }
virtual void func() override { /* ... */ }
};
当你创建一个Derived
对象时,以下事情会发生:
Base
的构造函数首先被调用。- 在
Base
的构造函数中,vptr被设置为指向Base
的vtable。 - 接下来,
Derived
的构造函数被调用。 - 在
Derived
的构造函数中,vptr被更新,现在它指向Derived
的vtable。
8.6 虚函数成员函数和普通成员函数,野指针调用是否会crash
9、new、malloc区别
特点 | new | malloc |
---|---|---|
关键字/库函数 | 关键字 | 库函数 |
内存大小 | 由编译器根据类型自行计算 | 需要显式指定字节数 |
调用构造函数 | 是 | 否 |
内存初始化 | 是 | 否 |
释放函数 | delete | free |
返回类型 | 对象类型指针 | void* |
异常处理 | 抛出bad_alloc异常 | 返回NULL |
构造/析构函数 | 可调用自定义构造和析构函数 | 只进行动态内存分配和释放,不涉及构造析构 |
重载 | 可以 | 不可 |
内存泄漏检测 | 可以明确指出是哪个文件的哪一行 | 不支持明确指出是哪个文件的哪一行 |
效率 | 高于malloc | 低于new |
10、__stdcall和__cdcel 调用方式区别、参数入栈区别
__stdcall
和 __cdecl
是两种常见的调用约定,主要在Windows平台上使用。它们定义了函数参数如何传递、如何从栈上取回,以及谁负责清理栈。以下是它们的主要特点和区别:
__stdcall
-
编译后函数名:在32位Windows平台上,使用
__stdcall
调用约定的函数名会被修饰,通常是在函数名前加上一个下划线,并在函数名后加上一个表示参数字节大小的十进制数。例如,FunctionName
可能会变成_FunctionName@8
。 -
调用方式:参数从右到左的顺序被推入栈中。
-
参数入栈:参数由调用者推入栈。
-
清理栈:被调用的函数(callee)负责清理栈。这意味着函数执行完毕后,会有一个加到ESP寄存器的指令来清理栈。
__cdecl
-
编译后函数名:使用
__cdecl
调用约定的函数名通常只在前面加上一个下划线。例如,FunctionName
可能会变成_FunctionName
。 -
调用方式:参数也是从右到左的顺序被推入栈中。
-
参数入栈:参数由调用者推入栈。
-
清理栈:调用者(caller)负责清理栈。这意味着在函数调用后,会有一个加到ESP寄存器的指令来清理栈。
主要区别:
- 名称修饰:如上所述,两者在编译后的函数名修饰上有所不同。
- 栈清理:在
__stdcall
中,被调用的函数负责清理栈,而在__cdecl
中,调用者负责清理栈。 - 可变参数:
__cdecl
支持可变数量的参数,这就是为什么C语言的函数(如printf
)使用__cdecl
。因为__stdcall
需要被调用的函数来清理栈,所以它不能确定要清理的参数数量。
在实际应用中,Windows API函数大多使用__stdcall
,而大多数C运行时函数使用__cdecl
。如果你在链接时遇到与调用约定相关的错误,确保你的函数声明和定义都使用了正确的调用约定。
11、智能指针
左值引用和右值引用
std::move
std::forward
share_ptr、unique_ptr、weak_ptr
weak_ptr内部实现和shapre_ptr内部实现的差异
自己实现一遍share_ptr、unique_ptr、weak_ptr
12、TCP/IP协议
TCP/IP协议族按照层次由上到下,层层包装。最上面的是应用层,这里面有http,ftp,等等我们熟悉的协议。而第二层则是传输层,著名的TCP和UDP协议就在这个层次。第三层是网络层,IP协议就在这里,它负责对数据加上IP地址和其他的数据以确定传输的目标。第四层是数据链路层,这个层次为待传送的数据加入一个以太网协议头,并进行CRC编码,为最后的数据传输做准备。
- TCP三次握手/四次挥手
14、static_cast及dynamic_cast的底层原理
static_cast
-
用途:
static_cast
是最通用的转换机制,用于进行各种标准转换,如整数到浮点数、浮点数到整数、指针到整数等。它也可以用于类层次结构中的向上和向下转换,但不进行运行时类型检查。 -
底层原理:
static_cast
的实现是编译时的,它不需要运行时的支持。转换的有效性完全取决于程序员。编译器生成必要的指令来完成所请求的转换。例如,将整数转换为浮点数可能需要生成转换指令;将基类指针转换为派生类指针可能只需要简单地调整指针值。
dynamic_cast
-
用途:
dynamic_cast
主要用于类层次结构中的指针和引用的安全向下转换。它确保所请求的转换在运行时是有效和安全的。 -
底层原理:
-
dynamic_cast
需要运行时类型信息(RTTI)来检查转换的有效性。当你尝试将基类指针(或引用)转换为派生类指针(或引用)时,dynamic_cast
会检查该指针所指向的对象的实际类型是否与目标类型匹配或兼容。 -
这通常是通过查看对象的虚函数表(vtable)来实现的,其中通常会有一个指向类型信息的指针。
dynamic_cast
使用这些信息来确定对象的实际类型,并根据这些信息决定是否允许转换。 -
如果转换是有效的,
dynamic_cast
返回一个指向目标类型的指针。如果转换是无效的(例如,当使用指针时),它返回nullptr
。
-
总之,static_cast
是一个编译时的转换,完全依赖于程序员确保其安全性,而 dynamic_cast
是一个运行时的转换,它使用运行时类型信息来确保转换的安全性。
15、不用虚函数如何实现多态
-
函数指针:
使用函数指针,你可以在运行时选择要调用的函数。这可以模拟多态行为。class Base { public: typedef void (*FuncType)(); FuncType func; Base(FuncType f) : func(f) {} void call() { func(); } }; void funcA() { std::cout << "Function A\n"; } void funcB() { std::cout << "Function B\n"; } int main() { Base b1(funcA); Base b2(funcB); b1.call(); // Outputs: Function A b2.call(); // Outputs: Function B }
-
自己实现vtble
#include<iostream>
using namespace std;
class Base;
typedef int(Base::*p)();//定义成员函数指针类型
class Base
{
public:
p virtual_p;//成员函数指针
Base()
{
//初始化成员函数指针指向基类test
virtual_p = (int(Base::*)())&Base::test;
}
~Base(){}
int test()//判断virtual_p的指向,如果指向派生类test就返回派生类的test调用
{
if (virtual_p == &Base::test)
{
cout << "Base" << endl;
return 1;
}
else
return (this->*virtual_p)();//返回派生类test调用,对virtual_p解引用后是test函数
}
};
class Derived :public Base
{
public:
Derived()
{
//初始化成员函数指针指向派生类test
virtual_p = (int(Base::*)())&Derived::test;
}
~Derived(){}
int test()
{
cout << "Derived" << endl;
return 2;
}
};
int main()
{
Base *b;//基类指针
Derived d;//派生类对象
Base bb;//基类对象
//指向基类对象时调用基类test,指向派生类对象时调用派生类test,模拟了动态关联
b = &d;
cout << b->test() << endl;
b = &bb;
cout << b->test() << endl;
return 0;
}
- 模板:CRTP
这些方法都有其优点和缺点。虚函数提供了一种简单、直接的方式来实现多态性,但在某些情况下,上述方法可能更合适。
16、构造函数
好的,我将为你补充常见的函数原型到上述表格中:
类型 | 描述 | 用途 | 原型示例 |
---|---|---|---|
默认构造函数 | 不接受任何参数(或所有参数都有默认值)的构造函数。 | 创建对象时没有提供任何初始值时调用。 | T(); |
拷贝构造函数 | 接受同类型对象的引用作为参数的构造函数。 | 1. 通过另一个同类型的对象初始化新对象。 2. 函数参数传递。 3. 从函数返回对象。 | T(const T& other); |
移动构造函数 | 接受同类型对象的右值引用作为参数的构造函数。 | 从临时对象或使用 std::move 转换的对象初始化新对象。 | T(T&& other); |
拷贝赋值运算符 | 接受同类型对象的常量引用作为参数的赋值运算符。 | 将一个对象的值赋给另一个已存在的对象。 | T& operator=(const T& other); |
移动赋值运算符 | 接受同类型对象的右值引用作为参数的赋值运算符。 | 使用临时对象或使用 std::move 转换的对象的值赋给另一个已存在的对象。 | T& operator=(T&& other); |
参数化构造函数 | 接受一个或多个参数(但不是同类型对象的引用)的构造函数。 | 创建对象时需要提供一个或多个初始值时调用。 | T(Type1 arg1, Type2 arg2, ...); |
委托构造函数 | 在同一个类中,一个构造函数调用另一个构造函数。 | 为了避免在多个构造函数中重复相同的代码,可以让一个构造函数委托给另一个构造函数来完成初始化。 | T() : T(arg1, arg2, ...) {} |
默认赋值运算符 | 如果没有定义任何赋值运算符,编译器会提供的赋值运算符。 | 将一个对象的成员变量赋值给另一个同类型对象的成员变量。 | T& operator=(const T& other) = default; |
析构函数 | 在对象销毁时自动调用的特殊成员函数。 | 释放对象占用的资源,如动态分配的内存、文件句柄等。 | ~T(); |
这个表格提供了各种构造函数和赋值运算符的常见原型。请注意,T
是类的名称,Type1
、Type2
等是参数的类型。
16.1 构造函数类型和用途
在C++11中,构造函数的种类和用途如下:
构造函数种类 | 用途描述 | 示例 |
---|---|---|
默认构造函数 | 当对象被默认初始化时调用。 | class A { public: A() {} }; |
拷贝构造函数 | 当一个对象以另一个同类对象为参数进行初始化时调用。 | class A { public: A(const A& other) {} }; |
移动构造函数 | 当一个对象以另一个同类对象的右值为参数进行初始化时调用。 | class A { public: A(A&& other) {} }; |
参数化构造函数 | 当对象在初始化时需要参数时调用。 | class A { public: A(int x) {} }; |
委托构造函数 | 当一个构造函数调用类中的另一个构造函数时。 | class A { public: A() : A(42) {} A(int x) {} }; |
列表初始化构造函数 | 当使用花括号列表初始化对象时调用。 | class A { public: A(std::initializer_list<int> l) {} }; |
- 默认构造函数:用于没有给出任何初始化参数的对象创建。
- 拷贝构造函数:用于基于另一个同类型对象的值创建新对象,如函数参数传递或返回值。
- 移动构造函数:用于从另一个同类型对象“移动”资源,而不是复制,通常在涉及临时对象或
std::move
的情况下使用。 - 参数化构造函数:当你需要提供参数来初始化对象时使用。
- 委托构造函数:允许一个构造函数重用类中的其他构造函数的代码,从而避免代码重复。
- 列表初始化构造函数:当你想使用花括号列表来初始化对象时使用,这在C++11中引入的
std::initializer_list
类型非常有用。
16.2 拷贝构造函数和移动构造函数如何选择
拷贝构造函数和移动构造函数都是C++中用于对象初始化的特殊成员函数。它们的选择和使用取决于特定的场景和需求。以下是关于它们的基本概念和如何选择它们的指南:
拷贝构造函数:
- 用途:当你需要基于现有对象创建一个新对象的完整副本时,使用拷贝构造函数。
- 定义:拷贝构造函数接受一个同类型对象的常量引用作为参数。
- 场景:
- 通过值传递对象。
- 从函数返回对象(尽管编译器优化可能会省略这种拷贝)。
- 初始化一个对象,使用另一个同类型的对象。
移动构造函数:
- 用途:当你不需要保留源对象的状态,而只想“移动”其资源到新对象时,使用移动构造函数。
- 定义:移动构造函数接受一个同类型对象的右值引用作为参数。
- 场景:
- 使用
std::move
显式地请求移动语义。 - 从函数返回局部对象。
- 初始化一个对象,使用临时对象或即将被销毁的对象。
- 使用
如何选择:
-
资源管理:如果你的类管理资源(如动态内存、文件句柄等),考虑实现移动构造函数。这可以提高性能,因为你可以直接转移资源,而不是复制它。
-
性能关键代码:在性能关键的代码中,移动语义可以大大提高性能,特别是当涉及到大量数据或昂贵资源的对象时。
-
兼容性:如果你的代码需要与不支持C++11或更高版本的编译器兼容,你可能无法使用移动构造函数。
-
默认行为:如果你没有为类定义拷贝构造函数或移动构造函数,编译器会为你提供默认的实现。但是,如果你定义了移动构造函数或移动赋值运算符,编译器不会为你提供默认的拷贝构造函数或拷贝赋值运算符,反之亦然。
-
移动后的状态:当实现移动构造函数时,确保移动后的源对象处于有效的、可析构的状态。
-
使用
= default
:如果你想使用编译器生成的默认版本,可以使用= default
来明确地请求它,例如:MyClass(MyClass&&) = default;
总的来说,选择是否实现拷贝构造函数或移动构造函数取决于你的类的需求和你想要的语义。在许多情况下,实现移动构造函数可以提供更好的性能和更灵活的资源管理。
17、如何禁止一个类被继承
在C++中,如果你想禁止一个类被继承,你可以将该类声明为final
。这样,任何尝试继承这个类的操作都会导致编译错误。
以下是如何使用final
关键字来禁止类被继承的示例:
class Base final {
// ... class definition ...
};
class Derived : public Base { // This will cause a compile-time error
// ... class definition ...
};
在上述代码中,由于Base
类被声明为final
,所以尝试从Base
派生出Derived
类会导致编译错误。
注意:final
关键字是C++11及更高版本中的特性,所以确保你的编译器支持C++11或更高版本。
18、线程的通信方式,同步方式,为什么要进行同步?系统是如何给线程分配一个栈的?
线程的通信方式:
- 共享内存:线程之间可以通过读写共享的数据结构或变量来通信。
- 消息传递:线程之间发送和接收消息来通信,这通常通过消息队列实现。
- 条件变量:允许线程等待某个条件成为真。
- 信号量:是一个同步对象,可以使多个线程等待,直到它被释放。
- 事件:某些系统提供了事件机制,允许一个线程通知其他线程某个事件已经发生。
- 管道和套接字:虽然它们通常用于进程间通信,但也可以用于线程间通信。
线程的同步方式:
- 互斥量(Mutex):提供了一种方式来保证在任何时候只有一个线程可以执行特定的代码段。
- 读写锁:允许多个线程同时读共享数据,但在写入时只允许一个线程。
- 信号量:允许多个线程访问一个有限数量的资源。
- 条件变量:允许线程等待一个特定的条件成为真。
- 屏障:允许多个线程同步它们的执行点,确保它们都达到一个执行点后再继续。
为什么要进行同步?
- 数据一致性:多个线程可能会同时读写共享数据,如果不同步,可能会导致数据不一致或损坏。
- 避免竞态条件:在多线程环境中,线程的执行顺序可能会导致不可预测的结果。
- 资源限制:某些资源(如数据库连接、文件句柄等)可能有数量限制,需要同步以确保不超过这些限制。
- 协作:线程可能需要在某些点上同步它们的执行,以确保它们都完成了必要的工作。
系统是如何给线程分配一个栈的?
当操作系统创建一个新线程时,它会为该线程分配一个新的栈。这个栈的大小和位置取决于操作系统和系统配置。通常,线程的栈大小会预先定义,但在某些系统中,它可能是动态增长的。
- 固定大小:大多数操作系统为每个线程分配一个固定大小的栈。这个大小可以在创建线程时指定,或者使用系统的默认值。
- 动态增长:某些系统支持动态增长的栈。当线程需要更多的栈空间时,操作系统会自动增加它。但这种方式可能会增加系统的复杂性和开销。
- 栈溢出检查:为了检测和防止栈溢出,操作系统通常在栈的底部放置一个“守卫页”。如果线程尝试访问这个页,操作系统会产生一个异常,通常导致程序崩溃。
总之,线程的栈管理是操作系统的责任,它确保每个线程都有足够的栈空间来执行其任务。
19、const用法,define用法 区别
const
和 #define
都可以用于定义常量,但它们在C++中的工作方式和用途有所不同。以下是它们的定义、用法和主要区别:
const
-
定义:
const
是一个关键字,用于声明一个变量为常量,这意味着一旦给它赋值,就不能再改变它。 -
用法:
const int x = 10;
-
特点:
const
变量有明确的类型。- 它们在编译时或运行时进行初始化。
- 可以用于各种类型,如基本数据类型、指针、引用、类对象等。
- 可以与其他C++特性一起使用,如类、函数、模板等。
#define
-
定义:
#define
是一个预处理指令,用于定义宏。 -
用法:
#define PI 3.14159
-
特点:
#define
是一个文本替换工具。在编译前,预处理器会将所有的PI
替换为3.14159
。- 它不是一个变量,没有数据类型。
- 可以用于定义常量、条件编译、宏函数等。
- 由于它是预处理指令,所以它不受C++作用域规则的约束。
区别:
- 类型安全:
const
是类型安全的,因为它是一个真正的变量或对象,而#define
只是一个文本替换,没有类型信息。 - 作用域:
const
变量遵循C++的作用域规则,而#define
宏在定义后直到文件结束或被#undef
取消定义都是有效的。 - 调试:
const
变量在调试时更容易追踪,因为它们是真正的变量。而#define
宏在预处理后就不存在了,所以在调试器中看不到它们。 - 内存使用:
const
变量通常会占用内存(尽管编译器可能会进行优化),而#define
宏不会占用内存,因为它只是文本替换。 - 灵活性:
#define
可以用于定义宏函数和条件编译,这是const
不能做的。
总的来说,const
和 #define
都有其用途和优点。在现代C++编程中,推荐使用 const
或 constexpr
来定义常量,因为它们提供了类型安全和其他C++特性。但在某些情况下,如条件编译,#define
仍然是必要的。
20、sizeof和strlen的区别
sizeof
和 strlen
是两个完全不同的操作,它们在C和C++中有不同的用途和行为。以下是它们的主要区别:
sizeof
-
定义:
sizeof
是一个编译时运算符,用于返回一个类型或对象在内存中的大小(以字节为单位)。 -
用法:
- 获取类型的大小:
sizeof(int)
- 获取对象的大小:
int arr[10]; sizeof(arr);
- 获取类型的大小:
-
特点:
sizeof
返回的是数据类型或对象的完整大小。- 对于数组,
sizeof
返回整个数组的大小,而不是数组中的元素数量。 sizeof
在编译时计算大小,不是运行时。
strlen
-
定义:
strlen
是一个函数,定义在C的<cstring>
或<string.h>
中,用于返回C风格字符串(以null终止)的长度。 -
用法:
char str[] = "hello"; size_t len = strlen(str);
-
特点:
strlen
只适用于C风格字符串。- 它返回的是字符串中字符的数量,不包括终止的null字符。
strlen
在运行时计算字符串的长度。
区别:
- 用途:
sizeof
用于获取类型或对象的内存大小,而strlen
用于获取C风格字符串的长度。 - 计算时间:
sizeof
在编译时计算,而strlen
在运行时计算。 - 适用性:
sizeof
可以应用于任何数据类型或对象,而strlen
只适用于C风格字符串。 - 返回值:
sizeof
返回的是字节大小,而strlen
返回的是字符数量。
示例:
char str[] = "hello";
printf("%zu\n", sizeof(str)); // 输出:6 (5个字符 + 1个null终止字符)
printf("%zu\n", strlen(str)); // 输出:5 (只有5个字符,不计null终止字符)
总的来说,sizeof
和 strlen
有不同的用途,它们不能互换使用。
21、伪函数
在C++中,所谓的“伪函数”并不是一个正式的术语。但在某些上下文中,你可能听到“伪函数”这个词,它可能指的是以下几种情况:
-
运算符重载:C++允许你为自定义类型重载大多数内置运算符。这些重载的运算符看起来像是函数调用,但实际上使用的是运算符的语法。
class Complex { public: Complex operator+(const Complex& other) const; };
-
函数对象(Functors):这些是重载了
operator()
的类或结构体的对象。它们可以像函数一样被调用,但实际上是对象。class Adder { private: int value; public: Adder(int v) : value(v) {} int operator()(int x) const { return x + value; } };
-
宏:预处理器宏可以定义为看起来像函数的形式,但它们在编译前就被展开了。
#define SQUARE(x) ((x) * (x))
-
类型转换运算符:这些是特殊的成员函数,允许一个类类型转换为另一个类型。
class MyClass { public: operator int() const { return 42; } };
-
Lambda表达式:从C++11开始,你可以定义匿名函数或lambda表达式。它们在某种程度上也可以被视为“伪函数”,因为它们允许你在没有命名函数的情况下定义函数行为。
auto lambda = [](int x) { return x * x; };
总之,当人们提到C++的“伪函数”时,他们可能指的是上述的某种情况,或者是其他使得某些代码看起来像函数但实际上不是真正的函数的机制。
22、如何设计线程池
23、connect的原理
24、STL原理你看过哪些
25、static 类的构造和析构是什么时候
26、XML如何自己实现解析
27、不看代码,CPU100%如何找出原因
28、
算法题
0、写一个单例模式
class hxLog
{
private:
hxLog(); // 私有构造函数
~hxLog(); // 私有析构函数
public:
hxLog(const hxLog&) = delete; // 删除拷贝构造函数,确保不能通过拷贝来创建新实例
hxLog(hxLog&&) = delete; // 删除移动构造函数,确保不能通过移动来创建新实例
hxLog& operator=(const hxLog&) = delete; // 删除拷贝赋值运算符,确保不能通过拷贝赋值来复制实例
hxLog& operator=(hxLog&&) = delete; // 删除移动赋值运算符,确保不能通过移动赋值来移动实例
static hxLog& GetInstance() // 静态成员函数,用于获取类的唯一实例
{
static hxLog Instance; // 局部静态变量,它在第一次调用时初始化,并且只有一个实例,并且是线程安全的
return Instance; // 返回这个唯一的实例的引用
}
};
1、有一个类 students,名字和学号,生成 50 个实例,保存在一个类里面。排序怎么排列?排序,重载。年龄20岁的删除
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
class students {
public:
std::string name;
int id;
};
bool compareStudents(const students& s1, const students& s2) {
return s1.id < s2.id;
}
int main() {
std::vector<students> studentList;
// 添加 50 个学生实例到容器
for (int i = 0; i < 30; i++) {
students student;
student.name = "Student " + std::to_string(i);
student.id = i + 1;
studentList.push_back(student);
}
// 按姓名排序学生列表
std::sort(studentList.begin(), studentList.end(), compareStudents);
// 删除年龄为 20 的学生实例
studentList.erase(std::remove_if(studentList.begin(), studentList.end(), [](const students& s) {
// 根据年龄是否为 20 进行筛选
// ...
if (s.id == 20) return true;
return false; // 返回 true 表示需要删除该学生实例
}), studentList.end());
// 打印排序后的学生列表
for (const auto& student : studentList) {
std::cout << "Name: " << student.name << ", ID: " << student.id << std::endl;
}
return 0;
}
3、实现一个字符串类
实现一个简单的字符串类是一个大任务,因为标准的 std::string
类有很多功能和优化。但是,我可以为你提供一个简化版的字符串类,以展示基本的思路和C++11的一些特性。
以下是一个简单的 String
类的实现:
#include <iostream>
#include <cstring>
class String {
private:
char* data;
size_t length;
public:
// 默认构造函数
String() : data(nullptr), length(0) {}
// 构造函数,从C风格字符串初始化
String(const char* str) {
length = std::strlen(str);
data = new char[length + 1];
std::strcpy(data, str);
}
// 拷贝构造函数
String(const String& other) {
length = other.length;
data = new char[length + 1];
std::strcpy(data, other.data);
}
// 移动构造函数
String(String&& other) noexcept : data(other.data), length(other.length) {
other.data = nullptr;
other.length = 0;
}
// 拷贝赋值运算符
String& operator=(const String& other) {
if (this != &other) {
delete[] data;
length = other.length;
data = new char[length + 1];
std::strcpy(data, other.data);
}
return *this;
}
// 移动赋值运算符
String& operator=(String&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
length = other.length;
other.data = nullptr;
other.length = 0;
}
return *this;
}
// 析构函数
~String() {
delete[] data;
}
// 获取字符串长度
size_t size() const {
return length;
}
// 输出字符串
void print() const {
if (data) {
std::cout << data << std::endl;
}
}
};
int main() {
String s1("Hello");
String s2 = s1; // 调用拷贝构造函数
String s3 = std::move(s2); // 调用移动构造函数
s1.print();
s2.print(); // s2 现在为空
s3.print();
return 0;
}
这个简化版的 String
类展示了如何使用C++11的移动语义来优化资源的管理。但请注意,这只是一个基础版本,真实的 std::string
类有更多的功能和优化。如果你想进一步扩展这个类,你可能需要考虑添加更多的成员函数、操作符重载、异常处理等。
4、删除vector中重复的元素(双指针)
#include <iostream>
#include <vector>
#include <algorithm>
void removeDuplicates(std::vector<int>& nums) {
//排序 从小到大
std::sort(nums.begin(), nums.end());
//双指针技巧
int i = 0;
for (auto& v : nums)
{
if (v != nums[i])
{
i++;
nums[i] = v;
}
}
//删除剩下的
nums.erase(nums.begin() + i + 1, nums.end());
}
int main() {
std::vector<int> nums = { 4, 2, 2, 8, 3, 3, 1 };
removeDuplicates(nums);
for (int num : nums) {
std::cout << num << " ";
}
return 0;
}
5、将一个vector中的所有奇数放前面,所有偶数放后面
要将一个 std::vector
中的所有奇数放前面,所有偶数放后面,我们可以使用双指针技巧。具体步骤如下:
- 使用两个指针,一个从前向后(
left
),一个从后向前(right
)。 - 当
left
指向的元素是奇数时,移动left
。 - 当
right
指向的元素是偶数时,移动right
。 - 当
left
指向偶数且right
指向奇数时,交换这两个元素。 - 重复上述步骤,直到
left
和right
相遇。
以下是实现这一技巧的代码:
#include <iostream>
#include <vector>
#include <algorithm>
void segregateEvenOdd(std::vector<int>& nums) {
int left = 0, right = nums.size() - 1;
while (left < right) {
// 增加 left,直到找到一个偶数
while (left < right && nums[left] % 2 == 1) {
left++;
}
// 减少 right,直到找到一个奇数
while (left < right && nums[right] % 2 == 0) {
right--;
}
// 交换 nums[left] 和 nums[right]
if (left < right) {
std::swap(nums[left], nums[right]);
left++;
right--;
}
}
}
int main() {
std::vector<int> nums = {12, 34, 45, 9, 8, 90, 3};
segregateEvenOdd(nums);
for (int num : nums) {
std::cout << num << " ";
}
return 0;
}
这个方法的时间复杂度是 O(n),其中 n 是 vector
的大小。