编程语言

半小时以上阅读 9164 字 + 5764 词
面向对象的编程思想解决什么问题
答:
  1. 面向对象主要是可以将复杂的系统拆解为各个小的独立的对象,每个对象负责特定的功能。这样做降低复杂性,使得系统更容易维护。
  2. 面向对象提供了抽象封装继承多态等机制,可以实现代码的复用,以及对扩展开放对修改关闭的原则保证了系统的稳定以及易维护。
左值右值引用,以及作用,什么是将亡值
答:
  1. 首先说明左值和右值的区别,左值是能够取地址的值,而右值是不能够取地址的值。在c++中可以用取地址符号来判断。而左值引用是单个取地址符号,用来绑定一个左值的,来作为变量的别名,可以直接在引用上操作修改变量,以及避免在参数传递中发生拷贝。const左值引用是一个万能引用,也可以绑定右值。
  2. 而右值引用只能用来绑定右值,是两个取地址符号来表示的,右值引用最重要的作用是实现移动语义,通过移动而不是拷贝来避免不必要的资源消耗。而在模版编程中,T && t表示万能引用,可以绑定左值也可以绑定右值,可以搭配std::forward来实现完美转发。
  3. 将亡值是指C++11新增的和右值引用相关的表达式,通常指 将要被移动的对象 T&&函数的返回值 std::move函数的返回值 、转换为T&& 类型转换函数 的返回值,将亡值可以理解为即将要销毁的值,通过“盗取”其它变量内存空间方式获取的值,在确保其它变量不再被使用或者即将被销毁时,可以避免内存空间的释放和分配,延长变量值的生命周期, 常用来完成移动构造或者移动赋值 的特殊任务。
什么错误是编译不报,运行时报错,举例说明
答:
  1. 空指针解引用,数组越界,除以0,多次释放同一个内存地址,访问已经释放过的内存, dynamic_cast失败的时候
迭代器失效原因,怎么编程能够避免迭代器失效
答:
  1. vector,deque,string等容器添加元素或者删除元素,会导致迭代器失效,因为添加元素的时候,可能会发生扩容现象,这个时候需要重新分配内存,所以迭代器指向的空间还是原来的老空间上。
  2. 可以使用一些比较稳定的容器比如list,map,set,当删除元素的时候,其他迭代器并不会失效。第二点是当插入或删除元素后,保持重置迭代器的习惯,比如利用erase返回的迭代器进行迭代。
可重入函数和不可重入函数
答:
可重入函数指的是在 任何时刻都可以被中断 ,然后在 中断后可以安全地再次被调用 。这种函数在被多个线程同时调用时 不会引起竞争条件或数据不一致等问题 。可重入函数不依赖于不受保护的全局变量或静态数据
不可重入函数就是指执行过程中被中断后再次进入或调用时,会导致未定义行为或者数据不一致问题。比如malloc函数就是不可重入函数,malloc的内部实现维护了全局的内存分布表,在标准C实现中,malloc内部并没有同步机制来管理对这个全局对象的并发访问。
可重入和线程安全的区别
值得注意的是,可重入和线程安全(Thread-safe)是相关但不同的概念:
  • 可重入函数 :可以 安全地被中断并在中断后重新进入
  • 线程安全函数 :可以 安全地被多个线程同时调用
一个函数可以是可重入的但不是线程安全的,也可以是线程安全的但不是可重入的。 线程安全函数 通常 通过锁(mutex)等同步机制 来实现,而可重入函数则通常 避免使用任何需要同步的共享资源
内存分区讲一下
答:
C++的内存分区分为 内核空间,栈,映射段,堆,全局静态存储区(数据段,BSS段),代码段。
  • Stack ): 用于 存储局部变量和函数调用的上下文 。栈的内存分配是自动的,由编译器管理。
  • Heap ): 用于 动态内存分配 。程序员可以使用 new malloc 等操作符或函数从堆上分配内存,并使用 delete free 释放内存。
  • 映射段 :内存映射区包含文件映射和匿名内存映射,比如mmap系统调用进行映射就是映射这块内存。
  • 全局 /静态 存储区 Global/Static Storage ): 存储全局变量和静态变量 ,包括:
    • 数据段:存储 初始化的 全局变量和静态变量。
    • BSS 段 :存储 未初始化的 全局变量和静态变量。
  • 常量存储区 Constant Data ): 存储程序中的常量数据 ,如 字符串字面量 。此外,如果有定义了虚函数, 常量区还会存储虚表
  • 代码段 Code Segment 或 Text Segment ): 存储 程序的可执行代码和函数的二进制指令
c语言怎么实现多态
答:
利用函数指针和结构体,定义一个通用的结构体,然后这个结构体里有一个函数指针的数据成员,创造对象的时候将这个指针指向需要的函数即可。
静态成员函数访问非静态数据成员的方法
答:
传一个对象指针,调用这个对象的成员方法,或者使用一个静态成员指针,再利用这个指针调用对象的成员方法。
内存泄漏和内存溢出的概念,怎么防止
答:
内存泄漏 是指程序中 己动态分配的堆内存 未释放或无法释放,造成 系统内存的浪费 ,导致程序运行速度减慢甚至系统崩溃等严重后果。
内存溢出 通俗理解就是内存不够 ,指 程序要求的内存超出了系统所能分配的范围 ,通常在运行大型软件或游戏时,软件或游戏所需要的内存远远超出了你主机内安装的内存所承受大小,就叫内存溢出。
内存泄露可能会导致内存溢出
防止内存泄漏的方法:保持良好的习惯来进行delete或者free,采用RAII的思想和智能指针来管理对象,利用valgrind工具来分析内存问题。
new和delete的底层原理
  • new 是C++的 操作符 ,由编译器实现,可以直接用来分配对象或数组,并且在分配失败会抛出 std::bad_alloc 异常。
  • new表达式工作步骤有三步
    1.调用operator new库函数,分配未类型化的空间,用来保存指定类型的一个对象 ,而在operator new中,会调用malloc函数
    2.运行该类型的构造函数初始化对象
    3.返回指向对象的指针
  • new 是类型安全的,它会根据分配的对象类型进行 正确的内存分配和构造函数调用
  • 使用 new 分配的对象在对象生命周期结束时需要使用 delete 来释放, delete 会自动调用对象的析构函数。
总结来说, new malloc 都是动态内存分配的手段,但 new 提供了类型安全和构造/析构的自动化
placement_new 允许在已经分配的内存地址上构造对象
delete表达式工作步骤有两步
1.调用析构函数,回收对象中数据成员申请资源
2.调用operator delete的标准库函数释放该对象所用的内存
malloc和free的底层原理
malloc 是一个库函数,通常需要包含头文件 并且只分配原始内存,分配失败时会返回NULL
malloc 不是类型安全的,它 只分配原始内存,不调用构造函数 。返回类型是 void* ,需要强制类型转换为具体的指针类型。
使用 malloc 分配的内存需要使用 free 来释放, free 不会自动调用析构函数,因此如果分配的是对象数组,需要手动调用析构函数。
malloc 则提供了更底层的内存分配方式,需要 手动管理构造和析构
  1. malloc的底层实现是怎样的?free是怎么回收内存的? 答: 1、 当开辟的空间A小于128K的时候,malloc底层调用brk()函数 ,brk是将数据段的 最高地址指针_edata 往高地址推**,_edata+30K只是完成虚拟地址的分配,A这块内存现在还是没有物理页与之对应的,等到进程第一次读写A这块内存的时候,发送缺页中断, 这个时候,内核才会分配给A这块内存对应的物理页。也就是说, 如果用malloc分配了A这块内容,然后从来不访问它,那么A对应 的物理页是不会被分配的。 当开辟的空间 大于128K 的时候,利用 mmap系统调用****,从堆和栈的中间也就是 文件映射区 分配一块 虚拟内存 。这样做主要是因为: brk分配的内存需要等到高地址内存释放以后才能释放 (比如先申请A再申请B,所以B相对于A来说是高地址空间,所以在B释放 之前,A是不可能释放的, 因为只有一个_edata指针 ,这就是 内存碎片产生的原因 。而 mmap分配的内存可以单独释放
    2、free回收内存时首先要知道这块内存的地址多大,所以在malloc返回的地址的前一小段的内存块控制块,存储这一块有多大的信息。对使用mmap机制分配空间的malloc返回的地址free时,直接将其对应的虚拟内存和物理内存一起释放。而对brk机制分配空间的malloc返回的地址free时,并不会立刻释放,默认情况下, 当最高地址空间的空闲内存超过128K时,执行内存紧缩操作(trim)
new/delete与malloc/free的区别与联系是什么?(面试常考)
答: 1、malloc开辟空间类型大小需手动计算,new是由编译器自己计算。 2、malloc返回类型为void*,必须强转成对应类型指针,而new直接返回对应类型指针。 3、malloc开辟内存时返回内存地址要检查判空,因为若它可能跟 开辟失败会返回NULL ;new则不用判断,因为内存分配失败时,它会抛出异常bac_alloc,可以使用异常机制。 4、无论释放几个空间大小,free只传递指针,而多个对象时,delete需加[],(分配的大小是数组空间大小). 5、malloc/free函数 只是开辟空间并释放 ,new/delete则 不仅会开辟空间 ,并 调用构造函数和析构函数进行初始化和清理 。 6、new/delete底层是基于malloc/free来实现的。 7、因为new/delete是操作符,它调用operator new/operator delete,它们可以被重载,在标准库里它有8个重载版本;而malloc/free不可以重载。 8、对于malloc分配内存后,若在使用过程中内存分配不够或太多,这时可以使用realloc函数对其进行扩充或者缩小,但是new分配好的内存不能这样被直观简单的改变。 9、对于malloc申请内存的位置是在堆上分配内存的;但是new申请内存有一个抽象概念,它为自由存储区,它可以在堆上,也可以在静态存储区上分配,这主要取决于operator new实现细节,取决于它在哪里为对象分配空间。
this指针的原理 ,如果把this delete,还能用吗,什么场景下还能用?
答:
当一个对象被创建后,它的每一个成员函数都含有一个系统自动生成的隐含指针this,用以保存这个对象的地址
保证这个对象是普通new分配的,并且要保证delete this后,不会有其他调用this的地方,用来进行对象的自杀。并且delete this不能再析构函数中使用,否则会无限递归。比如智能指针中当引用计数变为0的时候,手动进行delete this.
push_back()左值和右值的区别是什么?
答:
push_back左值会执行拷贝构造函数,而右值会执行移动拷贝,因为push_back的实现
template<typename Ty> // 函数模板的类型推演 + 引用折叠
	void push_back(Ty &&val) //Ty CMyString& + && = CMyString&
	{
		if (full())
			expand();
		// move(左值):移动语义,得到右值类型   (int&&)a
		// forward:类型完美转发,能够识别左值和右值类型
		_allocator.construct(_last, std::forward<Ty>(val));
		_last++;
	}
move底层是怎么实现的?
答:move底层就是用一个static_cast将对象转化成右值
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
	return static_cast<typename remove_reference<T>::type&&>(t);
完美转发
答:
完美转发是为了在模版编程中 解决 传递参数时的 临时对象(右值)被强制转换为左值 的问题。C++11中是用std::forward来实现的,它的作用是 根据传入的参数 ,== 决定将参数以左值引用还是右值引用的方式进行转发。 ==
forward的内部实现只有一行代码,即static_cast(t), 然后还有一个概念和这个实现有关,也就是模版推导中的引用折叠,引用折叠的规则如下: 如果两个引用中至少其中一个引用是左值引用,那么折叠结果就是左值引用 ;否则折叠结果就是右值引用。所以说T如果是右值,那么转换之后还是右值,如果是左值,转换之后是左值。这样就实现了完美转发
怎么利用C++新特性实现一个无锁的并发
atomic 加内存顺序模型实现,具体看atomic文档
原子性和可见性是什么, 为什么需要保证可见性,是什么问题导致的
答:
  • 原子性:原子性指的是一个操作是不可分割、不可中断的,要么全部执行并且执行的过程不会被任何因素打断,要么就全不执行。
  • 可见性: 可见性指的是一个线程修改了某一个共享变量的值时,其它线程能够立即知道这个修改
  • 有序性:有序性指的是对于一个线程的执行代码,从前往后依次执行,单线程下可以认为程序是有序的,但是并发时有可能会发生指令重排。
单核CPU下不存在可见性问题 ,在多核情况下,CPU有时候会直接读cache而不去读内存,而这个时候这个变量的值可能已经被修改了,但是并没有同步到这个cache中,而各个CPU之间的高速缓存是不可见的。
CPU的缓存一致性是什么
答:
  1. 什么是CPU缓存一致性?
CPU缓存是处理器内部用于快速访问数据的临时 存储 区域。由于CPU速度远高于主内存,缓存能够显著提高数据访问速度。 但当多个处理器核心或线程同时访问同一数据时,就可能出现数据不一致的问题。这就是CPU缓存一致性的重要性所在。
2. 为什么需要缓存一致性?
想象一下,如果两个处理器核心分别读取了同一内存地址的数据到各自的缓存中,并且其中一个核心修改了该数据,但没有及时通知其他核心,那么其他核心仍然使用着旧的数据,导致数据不一致。缓存一致性协议就是为了解决这个问题而设计的。
其中最著名的是MESI协议(Modified, Exclusive, Shared, Invalid)。
3.1 MESI协议状态
  • Modified (M) : 数据已被修改,与主内存不同。
  • Exclusive (E) : 数据仅存在于当前缓存中,与主内存相同。
  • Shared (S) : 数据存在于多个缓存中,与主内存相同。
  • Invalid (I) : 缓存中的数据无效,需要从主内存或其他缓存中获取。
设计模式,使用单例要注意什么,双检锁是绝对安全的吗?
答:
单例模式需要设置 一个static的变量,通过一个static的getinstance方法返回,并且设置构造函数为私有,拷贝构造函数和赋值函数删除
自动释放的话可以设置atexit绑定destroy方法,用饿汉模式保证线程安全。
也可以用call_once保证只初始化一次 当使用懒汉模式的时候,就需要用双检锁,防止两个线程同时进入,造成new两次。
但是dcl可能会失效,在多处理器多线程状态下,会出现CPU指令重排
new操作的步骤一般为:1.调用malloc开辟未类型化的空间,用来存储对象。2.调用构造函数创建对象。3.返回这个空间的指针
而指令重排可能导致第二个步骤和第三个步骤顺序颠倒。导致有一个线程拿到的指针指向的空间是一个未初始化的空间。
需要利用到原子变量和内存屏障解决。
cpp 继承时析构函数为什么为虚?
答:
当定义了虚函数的时候,都要将基类的析构函数设置为虚函数,因为如果不设置为虚函数,在利用基类的指针进行delete时,只会delete掉基类对象,而派生类对象的成员和派生类对象并不会被析构。
当基类的析构函数设置为虚函数时,编译器做了优化,将析构函数看成destructor,只要派生类写了析构函数,就相当于对虚函数进行了重写(进而满足动态多态的五个条件),体现多态性。之所以编译器可以这么做,是因为析构函数特点:不能被重载,具有唯一性
cpp 是否可以在构造函数中调用虚函数
答:
构造函数和析构函数是特殊的成员函数,在其中访问虚函数时,**C++采用静态联编, 即在构造函数或析构函数内,即使是使用 “this->虚函数名” 的形式来调用,编译器 仍将其解释为静态联编的 “本类名::虚函数名” 。**即它们所调用的虚函数是自己类中定义的函数,如果在自己的类中没有实现该函数,则调用的是基类中的虚函数。但绝不会调用任何在派生类中重定义的虚函数。
shared_ptr是否线程安全
答:
1)同一个shared_ptr被多个线程“读”是安全的;
2) 同一个shared_ptr被多个线程“写”是不安全的;
3)共享引用计数的不同的shared_ptr被多个线程”写“ 是安全的;
4)但同时访问指针管理的对象是不安全的
并且 引用计数是线程安全 的,而智能指针托管的对象不是线程安全的,在多线程情况需要加锁再操作。
shared_ptr发生拷贝的流程:
1)拷贝智能指针指向的资源(非原子操作)
2)增减引用计数(原子操作)
在多线程情况下,可能会有步骤一步骤二分开执行,导致智能指针指向的是一个已经被销毁的对象。
shared_ptr 的引用计数机制通过原子操作保证了线程安全,但对象的访问和 shared_ptr 实例的直接修改仍需用户同步。理解其线程安全边界是避免并发问题的关键。
c++11总结15——shared_ptr在多线程下的安全性问题_sharedptr线程安全-CSDN博客
stl容器线程安全
stl容器并不是线程安全的,所以有些时候需要用到加锁,也可以使用原子变量以及cas的机制
C++ STL容器如何解决线程安全的问题?-腾讯云开发者社区-腾讯云 (tencent.com)
vec的pushback和clear的时间复杂度
正常来说push_back没有发生扩容的情况下是O(1),发生扩容的时候是O(n),但是均摊下来是O(1)
clear需要遍历vector中的所有元素,并调用析构函数进行销毁,因此时间复杂度是O(n)
幂等性概念?如何实现接口的幂等?
答:
原意是 指一个操作如果连续执行多次所产生的结果与仅执行一次的效果相同 ,那么我们就 称这个操作是幂等的
腾讯二面:如何保证接口幂等性?高并发下的接口幂等性如何实现? - 码农Academy - 博客园 (cnblogs.com)
linux上open打开一个文件的过程,通过open打开一个文件的返回值是什么
答:利用 FILE *fopen(const char * pathname ,const char *mode);也可以用open int open(const char * path,int flag,......);//第三个参数可以是权限,返回值是文件描述符,-1为失败 int open(const char * path,int flag,mode_t mode);//文件名 打开方式 权限
vector和list之间的区别
答:
vector底层是通过三个指针和一段连续的空间实现的。start指针指向了空间的开始位置,finish指针指向了最后元素的后一个位置而end of storage是空间的后一个位置。vector实现了push_back,pop_back,以及insert等操作。
list底层是用一个双向链表来实现的,可实现任何一个位置插入节点,任何一个位置删除节点。
空类中有什么函数?
答:
默认构造函数、析构函数、复制构造函数和赋值运算符
explicit用在哪里?有什么作用?
答:
explicit可以防止内置类型隐式转换,所以在类的构造函数中,最好尽可能多用explicit关键字,防止不必要的隐式转换.
成员变量初始化的顺序是什么?
答:
根据变量定义的顺序进行初始化
指针占用的大小是多少?
答:
根据64位机器和32位机器分别是8字节和4字节。
野指针和内存泄漏是什么?如何避免?
答:
野指针指向的位置是不可知的
多线程会发生什么问题?线程同步有哪些手段?
答:多线程之间的执行顺序并不确定,并且共享的资源访问可能会出现数据错乱。需要用到互斥和同步等方法。
同步可以使用信号量,条件变量,互斥锁,也可以使用内核socket.
什么是STL?
C++ 标准模板库(Standard Template Library,STL)是一套功能强大的 C++ 模板类和函数的集合,它提供了一系列通用的、可复用的算法和数据结构。
对比迭代器和指针的区别
迭代器是一种模板类的对象,封装了指针并提供了类似指针的功能,用来模拟指针的行为。对不同的对象提供了统一的接口。
线程有哪些状态,线程锁有哪些?
答:
新建状态 :新建了一个线程对象,该对象就处于新建状态。
就绪状态 :线程对象创建后,其他线程调用了该对象的start方法。该状态的线程位于可运行线程池中,变得可以运行,只等待获取cpu的使用权。 运行状态 :就绪状态的线程获取了CPU,执行程序代码。 阻塞状态 :阻塞状态时线程因为某种原因放弃CPU使用权,暂时停止运行,直到线程进入就绪状态,才有机会转为运行状态。
阻塞的情况分三种: 等待阻塞:运行的线程执行wait方法,该线程会释放占用的所有资源,JVM会把该线程放入等待池中。可以通过notify或notifyAll方法唤醒。同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。其他阻塞:运行的线程执行sleep或join方法或者发出I/O请求时,JVM会把该线程置为阻塞状态。当sleep超时,join等待中止或超时,或I/O完毕,重新进入就绪状态。 死亡状态 :线程实行完了,或者因为异常退出了run方法,该线程结束生命周期。
如何实现线程安全,除了加锁还有没有其他的方式?
答:原子操作,条件变量,以及线程局部存储变量
C++中空类的大小是多少?
答:C++的空类大小是1,因为不同的对象必须要有它自己的地址
weak_ptr是怎么实现的?
答:
weak_ptr拥有指向托管对象的指针以及一个指向智能指针也指向的控制块,但是没有实现解引用运算符和箭头运算符的方法。所以不能直接操作对象,这个控制块存储了use_count,weak_count以及原生指针还有目标对象的删除器。
C++ 智能指针(shared_ptr/weak_ptr)原理分析 - CNHK19 - 博客园 (cnblogs.com)
一个函数f(int a,int b),其中a和b的地址关系是什么?
答:
在函数 f(int a, int b) 中, a b 是函数参数,以传值方式传递。这意味着它们在函数内部是局部变量,存储在栈上。它们的地址关系取决于编译器和具体的调用约定,通常情况下, a b 的地址是连续的,但具体顺序和间隔可能会有所不同。
关键点
  1. 传值机制 a b 是传值参数,函数接收的是实参的拷贝。
  2. 内存位置 :它们通常存储在栈上,地址关系由编译器决定,可能连续也可能不连续。
  3. 不可预测性 :由于栈帧的布局由编译器实现,不同编译器或编译选项可能导致不同的内存布局,因此不应依赖于它们的具体地址关系。
如果关心参数的地址关系,通常是出于调试或学习目的。在实际应用中,不应依赖于这些地址关系来实现功能。
移动构造和拷贝构造的区别是什么?
答:
通常拷贝构造函数会执行一个深拷贝的方法,而移动构造我们只需要将指针指向另一块空间即可。
lamda表达式捕获列表捕获的方式有哪些?如果是引用捕获要注意什么?
答:
捕获的方式分为两种:“ 值捕获”和“引用捕获”;
lambda表达式的“参数”是在被调用时拷贝 ,而**“被捕获的变量的值”是在lambda表达式创建时拷贝**。
理解这一点至关重要,因为lambda表达式捕获的值在创建时被拷贝,因此如果采用“值捕获”的方式,后续对于该变量的修改并不会影响到lambda表达式的计算;但是如果采用“引用捕获”的方式,在lambda表达式被创建后被调用前的这个阶段,如果该值发生了改变,就会影响到lambda表达式的计算结果。
因此,在使用lambda表达式的捕获之前,必须要考虑好这个值是否需要变化,如果需要变化,那么就采用“引用捕获”的方式,如果不需要变化,就可以直接采用“值捕获”的方式。
哈希碰撞的处理方法
hash碰撞指的是,两个不同的值(比如张三、李四的学号)经过hash计算后,得到的 hash值相同 ,后来的李四要放到原来的张三的位置,但是数组的位置已经被张三占了, 导致冲突
增加哈希值的位数 :通过增加哈希值的位数,可以显著降低碰撞的概率。例如,将哈希值从 32 位增加到 64 位,可以将碰撞的概率降低到几乎可以忽略不计的程度。
使用抗碰撞的哈希函数 :一些哈希函数,如 SHA-256 和 SHA-3,被设计为具有很强的抗碰撞性。使用这些哈希函数可以显著降低碰撞的风险。
链地址法 :在哈希表中,可以使用链地址法来处理碰撞。具体来说,每个哈希桶维护一个链表,所有哈希值相同的元素都存储在这个链表中。查找时,需要遍历链表来找到目标元素。
开放地址法 :另一种处理哈希碰撞的方法是开放地址法。在这种方法中,当发生碰撞时,会尝试在哈希表中寻找另一个空闲的位置来存储元素。常见的开放地址法包括线性探测、二次探测和双重哈希。
再哈希法 :当发生碰撞时,可以使用另一个哈希函数来计算新的哈希值,直到找到一个空闲的位置。
unordered_map的扩容过程
unordered_map底层是使用一个装着list的vector,使用的是链地址法,当size大小到达了bucket的个数时就进行扩容,也就是负载因子到1的时候扩容,可以修改这个阈值。bucket的扩容是 8 64 512 1024 2048 4096 8192 ....
扩容之后进行rehash, 可以使用max_load_factor函数改变负载因子
类中static函数是否能声明为虚函数?
在有虚函数的类实例中,this指针调用vptr指针,指向的是vtable(虚函数列表),通过虚函数列表找到需要调用的虚函数的地址。总体来说虚函数的调用关系是:this指针->vptr(4字节)->vtable ->virtual虚函数。
所以说, static静态函数没有this指针,也就无法找到虚函数了 。所以静态成员函数不能是虚函数。他们的关键区别就是this指针。
哪些函数不能被声明为虚函数?
  1. 普通函数(非成员函数) :定义虚函数的主要目的是为了重写达到多态,所以 普通函数声明为虚函数没有意义,因此编译器在编译时就绑定了它。
  2. 静态成员函数 :静态成员函数对于每个类都只有一份代码,所有对象都可以共 享这份代码,他不归某一个对象所有,所以它也没有动态绑定的必要。
  3. 内联成员函数 :内联函数本就是为了减少函数调用的代价,所以在代码中直接 展开。但虚函数一定要创建虚函数表,这两者不可能统一。另外,内联函数在 编译时被展开,而虚函数在运行时才动态绑定。
  4. 构造函数 :这个原因很简单,主要从语义上考虑。因为构造函数本来是为了初 始化对象成员才产生的,然而虚函数的目的是为了在完全不了解细节的情况下 也能正确处理对象,两者根本不能“ 好好相处 ”。因为虚函数要对不同类型的对 象产生不同的动作,如果将构造函数定义成虚函数,那么对象都没有产生,怎 么完成想要的动作呢
  5. 友元函数 :当我们把一个函数声明为一个类的友元函数时,它只是一个可以访 问类内成员的普通函数,并不是这个类的成员函数,自然也不能在自己的类内 将它声明为虚函数。
如何保证类的对象只能被开辟在堆或者栈上?
将析构函数设置为私有,这样不能生成栈对象
将operator new/operator delete设置为私有,不能生成堆对象
讲讲你理解的虚基类
虚基类有以下作用
    • 主要用来解决菱形继承时可能发生的对同一基类继承多次而产生的 二义性 问题
    • 为最远的派生类提供唯一的基类成员,而不重复产生多次复制
  • 这时候虚基指针会指向同一个虚基类
C++哪些运算符不能被重载?
  1. . :成员访问运算符
  2. .* :成员指针访问运算符
  3. :: :作用域解析运算符
  4. ?: :条件运算符
  5. sizeof :求字节大小运算符
  6. typeid :类型信息运算符
动态链接和静态链接的区别,动态链接的原理是什么?
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在 程序运行时才将它们链接在一起形成一个完整的程序 ,而不是像 静态链接一样把所有程序模块都链接成一个单独的可执行文件
静态库 静态链接的时候,把库文件打包到程序里面 , 容易部署 难以升级体积大
bash
1. gcc -c add.c 得到add.o
2. ar crsv libadd.a add.o 打包
3. sudo cp libadd.a /usr/lib 将库文件放在库目录
4. gcc -o main main.c -ladd 编译时加入库名
动态库 链接的时候,得库文件的位置,在 运行时再加载到内存(映射区) ,难以部署,容易升级,体积小,dll
bash
1. gcc -c add.c -fpic 得到add.o
2. gcc -shared -o libadd.so add.o
3. sudo cp libadd.so /usr/lib
4. gcc -o main main.c -ladd
ldd 查看依赖的动态库
版本更新 libadd.so 符号链接(软链接)
  1. 删除原软链接
  2. 重新建立链接 ln -s
sudo ln -s libadd.so.0.0.0 libadd.so
C++中怎么编译C语言代码?
extern "C"
内联函数
内联函数分为隐式内联和显示内联,C++中在类内定义的所有函数都自动称为内联函数,类的成员函数的定义直接写在类的声明中时,不需要inline关键字。显示内联是使用Inline关键字
内联函数将调用函数的地方直接展开,.内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。
除了new和malloc还有什么方法申请内存
void * realloc (void * ptr,size_t size);
ptr:初始空间的地址
size:将空间开辟到多大
用于对开辟的空间进行初始化,calloc会将开辟的空间中每个 元素初始化为0
void * calloc (size_t num,size_t size);
num:分配的元素数量
size:每个元素的大小
互斥量怎么实现的
1. 原子操作
pthread_mutex 使用了硬件提供的原子操作来实现锁的基本功能,如 compare-and-swap (CAS)等。这些操作确保在多处理器环境中可以安全地执行检查和设置操作。
2. Futex(快速用户空间互斥)
在 Linux 中, pthread_mutex 常常使用 futex(fast userspace mutex)机制。futex 允许在用户空间执行快速的加锁和解锁操作,而只有在竞争的情况下才会陷入内核。
  • 无竞争路径 :如果锁是可用的, 线程使用原子操作直接在用户空间获取锁,不需要陷入内核
  • 竞争路径 :如果锁不可用,线程会调用 futex 系统调用进入内核,线程会被挂起,直到锁可用。
3. 内存屏障
为了保证对内存的访问顺序, pthread_mutex 使用内存屏障来防止编译器和 CPU 进行不当的指令重排序。
快速用户区互斥量futex
随着并行的增加,有效的同步和锁机制对性能而言非常重要。如果等待时间短的话,完全可以用自旋锁忙等待,因为如果阻塞进程,陷入内核的开销远远比其大。但如果等待时间长,则会浪费CPU周期。如果有很多竞争,那么阻塞该进程,并仅当锁被释放的时候让内核接触阻塞会更加有效。然而,这却带来了相反的问题:竞争不激烈时,那么不断地内核切换将花销太大。
因此引入了futex,他是linux系统的一个特性,结合了以上两者之所长。它实现了基本的锁,但避免陷入内核,除非它是真的不得不这样做。因为来回切换到内核花销很大,所以这样做可以十分可观的改善性能。一个futex包含两个部分:一个内核服务和一个用户库。内核服务提供一个等待队列,它允许多个进程在一个锁上等待,直到内核对他们解除阻塞。将一个进程放到内核等待队列需要系统调用,而系统调用将陷入内核,这会非常花费时间,我们应该尽量避免他们。因此,在没有竞争时,futex完全在用户空间工作,进程共享一个锁变量,假设锁初始态是释放状态,初始值为1.线程要进入临界区前,通过执行原子操作"减少并检验"来获得锁,接下来,这个线程检查结果,看锁是否被释放,如果为处于被锁状态,线程成功获取锁。如果该锁被其他线程持有,则进行系统调用将该线程投入内核等待队列。当一个线程将要退出临界区时,进行原子操作"增加并检验"来释放锁,并检查结果,看是否扔有进程阻塞在内核等待队列上。如果有,通知内核可以对等待队列里的一个或个线程解除阻塞。

目录