管你有没有看懂,写就完事儿了。。。一些网络搜罗的亿点点不一样的有意思的面试问题;
C++语言基础
为什么静态成员函数不能访问类的非静态成员,只能访问静态成员?
因为静态成员函数不依赖于类的实例,它属于类本身,而不是类的某个具体实例。当调用一个对象的非静态成员函数时,系统会把该对象的地址给this指针,而静态成员函数不属于任何一个对象,C++规定其没有this指针,所以就无法对一个对象的非静态成员访问。
下面代码中指针变量为空的p是否能够成功输出?
class a{
public:
void sleep(){ cout << "animal sleep" << endl; }
};
int main(){
a *p = nullptr;
p->sleep();
return 0;
}
可以,因为在编译时对象就绑定了函数地址,和指针空不空没关系。pAn->breathe();编译的时候,函数的地址就和指针pAn绑定了;调用breath(*this), this就等于pAn。由于函数中没有需要解引用this的地方,所以函数运行不会出错,但是若用到this,因为this=nullptr,运行出错。
什么是野指针?如何产生的,如何避免?
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
产生原因:释放内存后指针不及时置空(野指针),依然指向了该内存,那么可能出现非法访问的错误。这些我们都要注意避免。
避免办法:
(1)初始化置NULL
(2)申请内存后判空
(3)指针释放后置NULL
(4)使用智能指针
内联函数和宏函数的区别
宏函数是在预编译的时候把所有的宏名用宏体来替换,简单的说就是字符串替换 ;而内联函数则是在编译的时候进行代码插入,编译器会在每处调用内联函数的地方直接把内联函数的内容展开,这样可以省去函数的调用的开销,提高效率
宏定义是没有类型检查的,无论对还是错都是直接替换;而内联函数在编译的时候会进行类型的检查,内联函数满足函数的性质,比如有返回值、参数列表等
++i 和 i++ 有什么不同?
赋值顺序不同,效率不同,i++ 不能作为左值,而++i 可以:
int i = 0;
int *p1 = &(++i);//正确
int *p2 = &(i++);//错误
++i = 1;//正确
i++ = 1;//错误
new和malloc的底层实现
new底层实现
1、创建一个新的对象并将构造函数的作用域赋值给这个新的对象(因此this指向了这个新的对象)
2、执行构造函数中的代码(为这个新对象添加属性)
3、返回新对象
malloc底层实现
当开辟的空间小于 128K 时,调用 brk()函数;当开辟的空间大于 128K 时,调用mmap()。malloc采用的是内存池的管理方式,以减少内存碎片。先申请大块内存作为堆区,然后将堆区分为多个内存块。当用户申请内存时,直接从堆区分配一块合适的空闲快。采用隐式链表将所有空闲块,每一个空闲块记录了一个未分配的、连续的内存地址。
C++中函数指针和指针函数的区别
定义不同
指针函数本质是一个函数,其返回值为指针。
函数指针本质是一个指针,其指向一个函数。
写法不同
指针函数:int *fun(int x,int y);
函数指针:int (*fun)(int x,int y);
用法不同
用法xx
以下分别什么?
const int a; //指的是a是一个常量,不允许修改。
const int *a; //a指针所指向的内存里的值不变,即(*a)不变
int const *a; //同const int *a;
int *const a; //a指针所指向的内存地址不变,即a不变
const int *const a; //都不变,即(*a)不变,a也不变
使用指针需要注意什么?
(1)初始化置NULL
(2)申请内存后判空
(3)指针释放后置NULL
const * 和 * const的区别
int const * a ; //常量指针, a指针所指的内存里的值不变,即 *a 不变
int * const a; //指针常量,a指针指向的内存地址不变, 即 a 不变
C++以下选项中那种变量类型没有布尔值?
A 、int B、char C、void D、double
答:void 是一种空类型,不可存储具体的值,因此没有布尔值
在C++中,还可以使用 const 关键字来限制类的对象被复制。将类的拷贝构造函数和赋值运算符声明为 const 可以防止对象被复制,但允许对象进行移动语义的操作。此外,也可以将类的拷贝构造函数和赋值运算符声明为删除的,从而完全禁止对象的复制操作。
在C++中,异常规格说明已被弃用,取而代之的是 noexcept 关键字。
noexcept 是一个类型修饰符,用于指定函数是否会抛出异常。如果一个函数被声明为 noexcept,则它保证不会抛出任何类型的异常,包括其内部可能抛出的异常。如果函数确实抛出了异常,程序将调用 std::terminate() 终止程序执行。这有助于提高代码的可读性和可维护性,并允许编译器进行更好的优化。
C++内存管理
常见的内存错误及其对策:
(1)内存分配未成功,却使用了它。
(2)内存分配虽然成功,但是尚未初始化就引用它。
(3)内存分配成功并且已经初始化,但操作越过了内存的边界。
(4)忘记了释放内存,造成内存泄露。
(5)释放了内存却继续使用它。
对策:
(1)定义指针时,先初始化为NULL。
(2)用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。
(3)不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。
(4)避免数字或指针的下标越界,特别要当心发生“多1”或者“少1”操作
(5)动态内存的申请与释放必须配对,防止内存泄漏
(6)用free或delete释放了内存之后,立即将指针设置为NULL,防止“野指针”
(7)使用智能指针。
内存泄露及解决办法
简单地说就是申请了一块内存空间,使用完毕后没有释放掉。(1)new和malloc申请资源使用后,没有用delete和free释放;(2)子类继承父类时,父类析构函数不是虚函数。(3)Windows句柄资源使用后没有释放。
怎么检测?
第一:良好的编码习惯,使用了内存分配的函数,一旦使用完毕,要记得使用其相应的函数释放掉。
第二:将分配的内存的指针以链表的形式自行管理,使用完毕之后从链表中删除,程序结束时可检查改链表。
第三:使用智能指针。
第四:一些常见的工具插件,如ccmalloc、Dmalloc、Leaky、Valgrind等等。
内存模型和堆栈
如上图,从低地址到高地址,一个程序由代码段、数据段、 BSS 段组成。
数据段:存放程序中已初始化的全局变量和静态变量的一块内存区域。
代码段:存放程序执行代码的一块内存区域。只读,代码段的头部还会包含一些只读的常数变量。
BSS 段:存放程序中未初始化的全局变量和静态变量的一块内存区域。
可执行程序在运行时又会多出两个区域:堆区和栈区。
堆区:动态申请内存用。堆从低地址向高地址增长。
栈区:存储局部变量、函数参数值。栈从高地址向低地址增长。是一块连续的空间。
最后还有一个文件映射区,位于堆和栈之间。
堆 heap :由new分配的内存块,其释放由程序员控制(一个new对应一个delete)
栈 stack :是那些编译器在需要时分配,在不需要时自动清除的存储区。存放局部变量、函数参数。
常量存储区 :存放常量,不允许修改。
内存对齐
为了CPU快速访问,如果一个变量的内存地址正好位于它长度的整数倍被称做自然对齐。
①基本类型:自身对齐
②数组:
③联合:成员最大对齐值,公倍数;
④结构体:其成员中自身对齐值最大的那个值。
⑤指定对齐方式:
#pragma pack (n)和pragma pack ();
attribute((aligned(n)))和__attribute__((packed))。
⑥数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中较小者,即有效对齐值=min{自身对齐值,当前指定的pack值};
union example {
int a[5];
char b;
double c;
};
//最大对齐值为20,但是4,1,8的最大公倍数,所以返回24
struct example {
int a[5];
char b;
double c;
}
//向8对齐,16(a的前4个元素)+8(a最后一个元素和b扩展的4个字节)+8(c的8个字节)= 32
struct example {
char b;
double c;
int a;
}
//向8对齐,8 + 8 + 8 = 24
C++面向对象
C++STL
说说stl基本组成
容器(Container)
是一种数据结构, 如list, vector, 和deques,以模板类的方法提供。为了访问容器中的数据,可以使用由容器类输出的迭代器。
算法(Algorithm)
是用来操作容器中的数据的模板函数。例如,STL用sort()来对一 个vector中的数据进行排序,用find()来搜索一个list中的对象, 函数本身与他们操作的数据的结构和类型无关,因此他们可以用于从简单数组到高度复杂容器的任何数据结构上。
迭代器(Iterator)
提供了访问容器中对象的方法。例如,可以使用一对迭代器指定list或vector中的一定范围的对象。 迭代器就如同一个指针。事实上,C++ 的指针也是一种迭代器。 但是,迭代器也可以是那些定义了operator*()以及其他类似于指针的操作符方法的类对象;
仿函数(Function object)
仿函数又称之为函数对象, 其实就是重载了操作符的struct,没有什么特别的地方。
适配器(Adaptor)
简单的说就是一种接口类,专门用来修改现有类的接口,提供一中新的接口;或调用现有的函数来实现所需要的功能。主要包括3中适配器Container Adaptor、Iterator Adaptor、Function Adaptor。
空间配制器(Allocator)
为STL提供空间配置的系统。其中主要工作包括两部分:
(1)对象的创建与销毁;
(2)内存的获取与释放。
stl常见容器实现原理
序列式容器,其迭代器随机访问
vector:动态数组
deque:双向队列
list:双向链表
关联式容器:元素是排序的,通常以平衡二叉树实现,其迭代器双向访问
set/multiset:集合
map:first、second,红黑树
容器适配器:封装deque,不支持迭代器
stack
queue
priority_queue
stl常见容器查找时间复杂度
vector、deque查找O(1),插入删除O(n)
list查找O(n),插入删除O(1)
map、multimap、set、multiset,查删插都O(log n)
unordered_map、unordered_set、unordered_multimap、 unordered_multiset:插查删: O(1),最坏情况O(N)
介绍一下 STL 的空间配置器(allocator)
空间配置器为容器分配内存空间,内存分配有通常静态存储区、栈、堆三种,通过构造函数或直接定义分配在栈空间,通过new分配在堆空间,首先要分配内存,然后调用构造函数构造对象内容,而allocator是把空间配置和对象构造分开为
内存配置操作: 通过alloc::allocate()实现
内存释放操作: 通过alloc::deallocate()实现
对象构造操作: 通过::construct()实现
对象释放操作: 通过::destroy()实现
关于内存空间的配置与释放,SGI STL采用了两级配置器:一级配置器主要是考虑大块内存空间,利用malloc和free实现;二级配置器主要是考虑小块内存空间而设计的(为了最大化解决内存碎片问题,进而提升效率),采用链表free_list来维护内存池(memory pool),free_list通过union结构实现,空闲的内存块互相挂接在一块,内存块一旦被使用,则被从链表中剔除,易于维护。
迭代器用过吗?什么时候会失效?
对于序列容器vector,deque来说,使用erase后,后边的每个元素的迭代器都会失效,后边每个元素都往前移动一位,erase返回下一个有效的迭代器。
对于关联容器map,set来说,使用了erase后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素,不会影响下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。
迭代器是类模板,封装了指针,重载了->,++,–等操作符,可以不用暴露集合内部的结构而达到循环遍历集合;
迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用取值后的值而不能直接输出其自身。
reserve是容器分配空间,对应capacity(最大容纳元素个数)。resize是即分配空间又创建对象,对应capacity和size(容器中实际元素个数)
如果n大于当前的vector的容量(是容量,并非vector的size),将会引起自动内存分配。所以现有的pointer,references,iterators将会失效。而内存的重新配置会很耗时间。
STL 容器动态链接可能产生的问题?
容器是一种动态分配内存空间的一个变量集合类型变量。在一般的程序函数里,局部容器,参数传递容器,参数传递容器的引用,参数传递容器指针都是可以正常运行的,而在动态链接库函数内部使用容器也是没有问题的,但是给动态库函数传递容器的对象本身,则会出现内存堆栈破坏的问题。
产生问题的原因
容器和动态链接库相互支持不够好,动态链接库函数中使用容器时,参数中只能传递容器的引用,并且要保证容器的大小不能超出初始大小,否则导致容器自动重新分配,就会出现内存堆栈破坏问题。
hashtable 扩容和如何解决冲突
默认容量为16,填装因子(loaderFactor) > 0.75进行扩容,每次扩容容量都为原来的2倍,需求超过2倍按具体大小扩容
解决哈希冲突通常有开放地址法和链地址法两种方法
1、address(key)=key%11,12和23时,线性探测空地址插入。
2、采用数组和链表相结合的办法,将Hash地址相同的记录存储在一张线性表中,而每张表的表头的序号即为计算得到的Hash地址。
C++新特性
C++新特性主要包括包含语法改进和标准库扩充两个方面
语法的改进
(1)统一的初始化方法
C++98/03 可以使用初始化列表(initializer list)进行初始化,在 C++11 中可以用于任何类型对象的初始化
(2)成员变量默认初始化
(3)auto关键字 用于定义变量,编译器可以自动判断的类型(前提:定义一个变量时对其进行初始化)
(4)decltype 求表达式的类型
(5)智能指针 shared_ptr
(6)空指针 nullptr(原来NULL)
(7)基于范围的for循环
(8)右值引用和move语义 让程序员有意识减少进行深拷贝操作
标准库扩充(往STL里新加进一些模板类,比较好用)
(9)无序容器(哈希表) 用法和功能同map一模一样,区别在于哈希表的效率更高
(10)正则表达式 可以认为正则表达式实质上是一个字符串,该字符串描述了一种特定模式的字符串
(11)Lambda表达式