栈、堆、全局、代码区
栈区:实现函数的调用。存放函数参数值和局部变量。
堆区:由程序员分配和释放
全局/静态存储区:全局变量和静态变量以及常量,程序结束时自动释放
1.虚函数表是全局共享的元素,即全局仅有一个
2.虚函数表类似一个数组,类对象中存储vpt指针,指向虚函数表,即虚函数表不是函数,不是程序代码,不肯能存储在代码段
3.虚函数表存储虚函数的地址,即虛函数表的元素是指向类成员函数的指针,而类中虚函数的个数在编译时期可以确定,即虚函教表的大小可以确定,即大小是在编译时期确定的,不必动态分配内存空间存储虚函数表,所以不再堆中
根据以上特征,虚函数表类似于类中静态成员变量,静态成员变量也是全局共享,大小确定所以我推测虚函数表和静态成员变量一样,存放在全局数据区
多态
有继承关系 子类重写父类虚函数 当父类指针指向子类对象时会引发多态
STL容器了解哪些。用迭代器遍历map的过程中如果删东西会有问题吗。
一旦你erase了一个iterator指向的内容,这个iterator就无效了。这时候你再对这个iterator做任何操作其结果都是未定义的
指针常量和常量指针
常量指针: 指针指向是可以修改的 指针指向的值不能修改
引用,指针常量: 指针指向不可以修改的 指针指向的值可以修改
内存对齐
经过内存对⻬之后,CPU 的内存访问速度⼤⼤提升。因为 CPU 把内存当成是⼀块⼀块的,块的⼤⼩可以是 2,4,8,16 个字节,因此 CPU 在读取内存的时候是⼀块⼀块进⾏读取的,块的大小称为内存读取粒度。⽐如说 CPU 要读取⼀个 4 个字节的数据到寄存器中(假设内存读取粒度是 4),如果数据是从 0 字节开始的,那么直接将 0-3 四个字节完全读取到寄存器中进⾏处理即可。
如果数据是从 1 字节开始的,就⾸先要将前 4 个字节读取到寄存器,并再次读取 4-7 个字节数据进⼊寄存器,接着把 0 字节,5,6,7 字节的数据剔除,最后合并 1,2,3,4字节的数据进⼊寄存器,所以说,当内存没有对⻬时,寄存器进⾏了很多额外的操作,⼤⼤降低了 CPU 的性能。
简述一下C++的特点
面向对象:C++完全支持面向对象的程序设计,包括封装、继承、多态等面向对象开发的三大特性
C++更加安全,增加了const常量、引用、智能指针等
全局变量和局部变量的区别
全局变量和局部变量的主要区别在于它们的作用域和生命周期。
全局变量:
- 全局变量在程序的整个生命周期内都是有效的,可以在程序的任何地方被访问。
- 全局变量在程序开始时被创建,在程序结束时被销毁。
- 如果全局变量没有被初始化,它们会被自动初始化为零(对于数字类型)或者空(对于某些其他类型)。
局部变量:
- 局部变量只在定义它们的函数或代码块内部有效。
- 局部变量在进入函数或代码块时被创建,在离开函数或代码块时被销毁。
- 局部变量必须在使用前被明确地初始化。
全局变量和static变量的区别
static变量:
- static变量的生命周期是整个程序执行期间,但其作用域仅限于定义它的函数或代码块。
- static变量在程序开始时被创建,在程序结束时被销毁。
- 如果static变量没有被初始化,它们会被自动初始化为零(对于数字类型)或者空(对于某些其他类型)。
new和malloc的区别
new和malloc都是C++中用于动态分配内存的方法,但它们之间存在一些重要的区别:
- 函数与操作符:new是一个操作符,而malloc是一个函数。
- 构造函数的调用:new会调用构造函数,而malloc不会。实际上,原始数据类型(如char、int、float等)也可以使用new进行初始化。
- 返回类型安全性:new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
- 内存分配失败时的返回值:new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。
- 内存分配位置:new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。
C++中新增了string,与C语言中的 char* 有什么区别吗?它是如何实现的?
类型安全:string
是一个类,提供了许多方法(如append
,replace
,substr
等)来操作字符串。这使得字符串操作更加安全和方便。而char*
则需要使用字符串函数(如strcpy
,strcat
,strlen
等),这些函数在使用不当时可能会导致错误,如缓冲区溢出。
动态大小:string
对象可以动态地改变大小,你可以在运行时添加或删除字符,而不需要担心内存分配。而对于char*
,你需要手动管理内存,并确保有足够的空间来存储所有的字符。
易用性:使用string
可以更容易地进行一些操作,如字符串连接和比较。例如,你可以使用+运算符来连接两个字符串,或者使用==运算符来比较两个字符串是否相等。而对于char*
,你需要使用特定的函数(如strcat
和strcmp
)来进行这些操作。
至于C++中的 string
类是如何实现的,它通常是作为一个动态数组实现的,其中包含一个指向字符数组的指针、一个表示字符串长度的整数以及一个表示分配的内存大小的整数。当字符串增长并超出当前分配的内存大小时,会分配一块更大的内存区域,并将现有字符串复制到新内存中,然后释放旧内存。这种实现方式使得 string
类能够有效地处理动态大小变化的字符串。
虚函数是什么,和纯虚函数的区别
虚函数是一种特殊类型的函数,它在基类中被声明,并可以在任何派生类中被重写。虚函数允许我们通过基类指针来调用派生类的这个函数。这种机制被称为动态绑定或运行时多态。虚函数在基类中通常有一个默认的实现,但也可以没有。
而纯虚函数是在基类中声明的虚函数,它在基类中没有定义(即没有实现),但要求任何派生类都要定义自己的实现方法。定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。纯虚函数的声明方式是在函数原型后加=0
纯虚函数则必须在派生类中提供实现。
C++中哪些函数不能声明为虚函数
- 普通函数(非类成员函数):只有类的成员函数才有可能被声明为虚函数。因为普通函数(非成员函数)只能被重载,不能被重写。声明为虚函数也没有什么意义,因此编译器会在编译时绑定函数。
- 构造函数:构造函数是用来初始化对象的。虚函数的主要作用是实现多态,多态是依托于类的,多态的使用必须是在类创建以后,而构造函数是用来创建构造函数的,所以不行。
- 内联函数:内联函数必须有实体,是在编译时展开。内联函数就是为了在代码中直接展开,减少函数调用花费的代价。虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的。
- 静态成员函数:静态成员函数理论是可继承的。但是静态成员函数是编译时确定的,无法动态绑定,不支持多态,因此不能被重写,也就不能被声明为虚函数。
- 友元函数:友元函数不属于类的成员函数,不能被继承。对于没有继承特性的函数没有虚函数的说法。
- 不会被继承的基类的析构函数:析构函数可以是虚函数,而且通常声明为虚函数。但是对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。
虚函数表是针对类还是针对对象的
虚函数表是针对类的,而不是对象。每一个类都有一张虚函数表,存储中这个类所有虚函数的入口地址。同一个类的两个对象共享类的虚函数表。当一个对象被创建时,它会在内存中分配一块空间用于存储对象数据和指向虚函数表的指针。这个指针始终指向该类的虚函数表,不会因为对象的不同而改变。
如果派生类重写了基类的虚函数,那么派生类会在其自己的虚函数表中存储重写后的函数地址。如果派生类没有重写基类的虚函数,那么它会继承基类的虚函数地址,并将其复制到自己的虚函数表中。
如果存在多重继承的情况,则派生类会生成多个虚函数表的指针,分别维护来自不同基类的虚函数表,其规则和单继承相同。
总之,虚函数是针对类的,同一个类的两个对象共享同一张虚函数表,虚函数表的内容由类的层次结构和重载情况决定
C++11创造一个空类,会默认生成哪些函数
① 默认构造函数
② 默认的拷贝构造函数
③ 默认的析构函数
④ 默认的重载赋值运算符函数
⑤ 默认的重载取地址运算符函数
⑥ 默认的重载取地址运算符const函数
⑦ 默认移动构造函数(C++11)
⑧ 默认重载移动赋值操作符函数(C++11)
浅拷贝和深拷贝的区别
浅拷贝和深拷贝是面向对象编程中常用的两个概念,它们主要的区别在于复制对象时是否复制对象所引用的其他对象。
浅拷贝只复制对象本身,不复制对象所引用的其他对象。如果被复制对象中包含了引用类型的成员变量,那么复制出来的新对象和原对象将会共享这些成员变量,也就是说,这些成员变量在新对象和原对象中都指向同一个内存地址。简单来说,浅拷贝只是单纯地将原对象的指针指向新对象,而不复制它所指向的实体。
深拷贝则会将复制对象所引用的其他对象也一并复制。深拷贝是一个整个独立的对象拷贝,深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。简而言之,深拷贝把要复制的对象所引用的对象都复制了一遍。
总结一下,浅拷贝和深拷贝主要区别在于是否复制了原始数据类型以外的成员变量。
你了解哪些C++11新特性
- auto:C++ 11引入了类型推断能力,使用auto关键字,编译器可以自行推断变量的类型。
- nullptr:C++ 11的新特性之一是允许程序员使用nullptr代替NULL或0来指定一个指向无值的指针。这与不定义任何值是不同的。
- 无序容器:C++ 11引入了无序容器,如unordered_map和unordered_set。
智能指针说一下
- unique_ptr:这是一种独占所有权的智能指针,也就是说,同一时间只能有一个unique_ptr指向给定的对象。当unique_ptr离开作用域或被删除时,它所指向的对象也会被删除。
- shared_ptr:这是一种共享所有权的智能指针,可以有多个shared_ptr指向同一个对象。shared_ptr使用引用计数来跟踪有多少个智能指针共享同一个对象。当最后一个shared_ptr不再需要其共享的对象时,该对象就会被删除。环形引用时会导致sdptr不能释放,使用采用weak_ptr
- weak_ptr:这是一种弱引用的智能指针,它可以观察另一个智能指针(如shared_ptr)所拥有的对象,但不会增加该对象的引用计数。因此,weak_ptr不会阻止其观察的对象被删除。
如何解决循环引用问题
解决循环引用问题的一种方法是使用std::weak_ptr。std::weak_ptr是一种智能指针,它可以观察另一个智能指针(如std::shared_ptr)所拥有的对象,但不会增加该对象的引用计数。因此,如果我们将上述例子中的某一个std::shared_ptr替换为std::weak_ptr,那么当另一个std::shared_ptr被销毁时,由于引用计数器减到0,所以它所指向的对象就会被正确地删除。
move的作用
它的主要作用是将一个左值强制转化为右值引用。这个函数本身并不能移动任何数据,它的功能很简单,就是进行类型转换。
在这个例子中,std::move(str1)将str1转换为右值引用,然后str2的构造函数接受这个右值引用,并将str1的资源移动到str2。这样,我们就避免了在构造str2时复制str1的内容。
就相当于str2变成了str1,独占指针的转移
移动语义:所谓移动语义,指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。简单的理解,移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。换句话说,就是以浅拷贝的方式复制指针,然后将原指针置为空指针。移动构造函数就是通过移动语义的方式来初始化对象的。
STL
怎么在vector中删除一个元素后,然后继续往后遍历呢?
在C++的vector中删除元素后继续遍历,需要注意的是,当使用erase函数删除元素后,原有的迭代器可能会失效。因此,正确的做法是使用erase函数的返回值(即指向被删除元素的下一个元素的迭代器)来更新当前的迭代器。
it = vec.erase(it); // 使用erase的返回值更新迭代器
unordered_map和map的区别
std::map和std::unordered_map是C++中的两种关联容器,它们都可以存储键值对,但在内部实现和性能上有一些重要的区别:
- 内部实现:std::map内部使用平衡二叉树(红黑树)实现,因此它的元素会按照键的顺序进行排序。而std::unordered_map则使用哈希表实现,元素存储的顺序是任意的,不保证任何特定的顺序。
- 查找时间:对于std::map,查找操作的时间复杂度为O(log n),其中n是元素的数量。对于std::unordered_map,在平均情况下,查找操作的时间复杂度为O(1),但在最坏情况下(例如发生大量哈希冲突时),时间复杂度可能会退化为O(n)。
- 插入和删除时间:对于std::map,插入和删除操作的时间复杂度为O(log n),其中n是元素的数量。对于std::unordered_map,在平均情况下,插入和删除操作的时间复杂度为O(1),但在最坏情况下(例如发生大量哈希冲突时),时间复杂度可能会退化为O(n)。
- 排序:如果你需要按照键的顺序遍历元素,那么应该使用std::map。如果你不需要保持任何特定的顺序,并且希望最大限度地提高查找、插入和删除操作的速度,那么应该使用std::unordered_map。
总的来说,选择使用哪种容器取决于你的具体需求。如果需要排序或者频繁进行查找操作,那么std::map可能更合适。如果不需要排序,并且主要进行插入和删除操作,那么std::unordered_map可能会提供更好的性能。
常用的容器并分析底层实现数据结构
- array:std::array是一个固定大小的容器,其底层实现为静态数组。它支持快速随机访问,但大小固定,不能动态扩展。
- vector:std::vector是一个动态数组。它支持快速随机访问,并可以在尾部进行高效的插入和删除操作。但在非尾部进行插入和删除操作时效率较低。
- deque:std::deque(双端队列)的底层实现为多个分段的动态数组。它支持在首尾两端进行高效的插入和删除操作,也支持随机访问。
- list:std::list的底层实现为双向链表。它支持在任意位置进行高效的插入和删除操作,但不支持快速随机访问
- forward_list:std::forward_list的底层实现为单向链表。它支持在任意位置进行高效的插入和删除操作,但不支持快速随机访问。
- set/multiset:std::set和std::multiset的底层实现为红黑树。它们支持快速查找,但不支持快速随机访问。
- map/multimap:std::map和std::multimap的底层实现为红黑树。它们支持根据键值进行快速查找,但不支持快速随机访问。
- unordered_set/unordered_multiset:这些无序容器的底层实现为哈希表。它们支持快速查找,但不支持快速随机访问。
- unordered_map/unordered_multimap:这些无序容器的底层实现为哈希表。它们支持根据键值进行快速查找,但不支持快速随机访问。
- stack:std::stack是一个容器适配器,通常使用std::deque或std::list作为其底层容器。
- queue:std::queue是一个容器适配器,通常使用std::deque或std::list作为其底层容器。
- priority_queue:std::priority_queue是一个容器适配器,通常使用std::vector作为其底层容器,并使用堆(heap)来管理底层容器以提供优先级队列功能。
vector和数组区别
在C++中,vector和数组都是用来存储数据的容器,但它们有一些重要的区别:
- 内存分配:数组在声明时就需要确定大小,并且在编译时会分配固定的连续内存空间12。而vector是动态数组,它可以在运行时动态调整大小。v.reserve(100000)可以提前分配空间;
- 灵活性:数组的长度在声明时就已经确定,不能更改12。而vector可以根据需要动态调整大小,可以在末尾增加元素(使用push_back方法)。
- 访问方式:数组和vector都可以使用下标操作进行处理,也都可以用迭代器进行操作12。
- 内存管理:对于vector,当其生命周期结束后,它会自动释放所占用的内存4。而对于数组,如果是动态分配的,需要手动释放内存。
- 性能:如果数组的长度确定的话,效率上vector差一些1。因为vector需要管理动态内存,所以相比于数组会有额外的管理开销。
fork()
多线程相关
线程与进程的区别?
进程(Process)
进程是操作系统分配资源的基本单位,是程序执行的一个实例。每一个进程都有自己的独立地址空间和其他操作系统资源(如打开文件列表、系统调用上下文等)。进程之间相互隔离,拥有自己的虚拟内存空间,这意味着一个进程中的改变不会直接影响到其它进程。
线程(Thread)
线程是比进程更小的执行单元,它是进程中可并发执行的部分。一个进程中可以拥有多个线程,共享相同的内存空间和资源。因此,线程间通信更快速、更简单,同时创建和切换线程的开销也比进程要小得多。不过,这也意味着一个线程的错误行为可能会影响到同一进程中的其他线程。
区别
- 资源占用:每个进程都有独立的地址空间和资源,而线程共享所属进程的资源,包括内存、文件描述符等。
- 上下文切换开销:由于线程共享进程的地址空间,线程间的上下文切换比进程间切换要快得多。
- 通信:进程间通信(IPC)通常需要通过操作系统提供的机制如管道、套接字等来实现,而线程间可以直接访问共享内存来进行通信。
- 生命周期:一个进程中的所有线程都会随该进程的终止而结束,但一个线程的终止并不会直接影响到其他进程。
- 并行性:由于线程轻量级的特点,可以在一个进程内创建更多的线程来提高程序的并行性,而进程的数量通常受限于系统资源。
join() joinable() detach() ref()
join()可以让进程阻塞于该线程,执行完毕后再继续
joinable用于判断当前线程还能否join()
detach可以将线程分离出来
ref可以给线程传引用,因为线程本质上是值传递
互斥量
mutex
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int a = 0;
mutex mtx;
void func() {
mtx.lock();
for(int i=0;i<10000;i++)
{
a += 1;
}
mtx.unlock();
}
int main() {
thread t1(func);
thread t2(func);
t1.join();
t2.join();
cout << a << endl;
}
线程安全就是指多线程运行和单线程运行结果是一样的,可预测的
lock_guard
std::lock_guard
是 C++ 标准库中的一种互斥量封装类,用于保护共享数据,防止多个线程同时访问同一资源而导致的数据竞争问题。
std::lock_guard
的特点如下:
-
当构造函数被调用时,该互斥量会被自动锁定。
-
当析构函数被调用时,该互斥量会被自动解锁。
-
std::lock_guard
对象不能复制或移动,因此它只能在局部作用域中使用。
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int a = 0;
mutex mtx;
void func() {
lock_guard<mutex> lg(mtx);
for(int i=0;i<10000;i++)
{
a += 1;
}
}
int main() {
thread t1(func);
thread t2(func);
t1.join();
t2.join();
cout << a << endl;
}
std::unique_lock
std::unique_lock
是 C++ 标准库中提供的一个互斥量封装类,用于在多线程程序中对互斥量进行加锁和解锁操作。它的主要特点是可以对互斥量进行更加灵活的管理,包括延迟加锁、条件变量、超时等。
std::unique_lock
提供了以下几个成员函数:
-
lock()
:尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁。 -
try_lock()
:尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则函数立即返回false
,否则返回true
。 -
try_lock_for(const std::chrono::duration<Rep, Period>& rel_time)
:尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间。 -
try_lock_until(const std::chrono::time_point<Clock, Duration>& abs_time)
:尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间点。 -
unlock()
:对互斥量进行解锁操作
除了上述成员函数外,std::unique_lock
还提供了以下几个构造函数:
-
unique_lock() noexcept = default
:默认构造函数,创建一个未关联任何互斥量的std::unique_lock
对象。 -
explicit unique_lock(mutex_type& m)
:构造函数,使用给定的互斥量m
进行初始化,并对该互斥量进行加锁操作。 -
unique_lock(mutex_type& m, defer_lock_t) noexcept
:构造函数,使用给定的互斥量m
进行初始化,但不对该互斥量进行加锁操作。 -
unique_lock(mutex_type& m, try_to_lock_t) noexcept
:构造函数,使用给定的互斥量m
进行初始化,并尝试对该互斥量进行加锁操作。如果加锁失败,则创建的std::unique_lock
对象不与任何互斥量关联。 -
unique_lock(mutex_type& m, adopt_lock_t) noexcept
:构造函数,使用给定的互斥量m
进行初始化,并假设该互斥量已经被当前线程成功加锁。
阻塞/非阻塞 同步/异步
一次网络IO包含数据准备和数据读写
数据准备:
阻塞:如果sockfd中没有信息就一直阻塞,调用io方法的线程进入阻塞状态
int size = recv(sockfd,buf,1024,0)
非阻塞:不会改变线程的状态,通过返回值判断
size = -1可能是断开
size = -1&&errno =EAGAIN 非阻塞无数据
size = 0 网络对端关闭了socket
size > 0 接收到了size大小的数据
数据读写:
IO的同步和异步
同步:
sockfd中有数据后,程序将操作系统中的Tcp接收缓冲区的数据搬到程序定义的buf中,此时程序是暂停的 recv(同步的io接口)
异步:
请求内核sockfd上的数据,数据来了直接放在buf上,放置一个sigio信号通知应用程序 异步io接口 操作系统通知应用程序时,buf里的数据已经准备好了 回调
阻塞/非阻塞 同步/异步 描述的都是IO的一个状态,一个典型的网络IO包含数据准备和数据读写两个阶段。
Linux上五种IO模型
select/poll/epoll
LT模式/ET模式