以下总结了我近期找工作准备的一些题目,很多在实际面试中确实遇到了。当然还不完善,后续会增加。
this指针
- **指针何时创建:**this指针在成员函数开始执行前构造,在执行结束后清除。
- **this指针存放在何处:**this指针会因编译器不同,而放置的位置不同。可能是栈,也可能是寄存器,甚至全局变量。
- **this指针如何传递给类中函数的:**this是通过函数参数的首参数来传递的。this指针是在调用之前生成的。每一个对象都能通过this 指针来访问自己的地址。this指针是所有成员函数的隐含参数
- this指针如何访问类中变量的:类似结构指针
访问控制
- private,public,protected方法的访问范围.(public继承下)
- private: 只能由该类中的函数、其友元函数访问,不能被任何其他访问,该类的对象也不能访问.
- protected: 可以被该类中的函数、子类的函数、以及其友元函数访问,但不能被该类的对象访问
- public: 可以被该类中的函数、子类的函数、其友元函数访问,也可以由该类的对象访问
- 类的继承后方法属性变化
- private继承,父类的所有方法在子类中变为private;
- protected继承,父类的protected和public方法在子类中变为protected,private方法不变;
- public继承,父类中的方法属性不发生改变;
public | protected | private |
---|---|---|
public继承 | public | protected |
protected继承 | protected | protected |
private继承 | private | private |
引用和指针的区别
引用必须被初始化,指针不必。
引用初始化以后不能被改变,指针可以改变所指的对象,引用接近与常量指针。
不存在指向空值的引用,但是存在指向空值的指针。
不能建立引用的引用、引用的指针,可以建立指针的引用。
int *&q=p;//true int &&r=n;//false but rvalue refrence is allowed int &*p=n;//false
各类指针
指针与整形是不同的概念
int *pt; pt = 0xB8000000;//type mismatch pt = (int *)0xB8000000//type now match
常量指针:
int const * p
指针常量:
int * const p
数组指针:
int (*p)[10]
指针数组:
int *p[10]
函数指针:函数返回值为指针
int *f()
指针函数:指向函数的指针,方便调用,可以使用typedef化简
int max(int iv1, int iv2) { return (iv1 > iv2 ? iv1 : iv2); } typedef int (*p_fun)(int, int); int main(int argc, char* argv[]) { int (*pMaxFun)(int, int); //定义一个函数指针pMaxFun,可以带形参 pMaxFun = max; 或者pMAxFun = &max //让pMaxFun指向max函数的入口地址 p_fun t = max; //use typedef cout << (*pMaxFun)(2, 3) << endl; 或者 pMaxFun(2,3)//输出3 return 0; }
内联函数和宏
尽量使用内联函数代替宏
- 使用宏,在复制代码时,容易出现一想不到的边际效应
- 使用宏,无法进行调试
- 使用宏,无法访问类的私有成员
STL
包括algorithm(算法)、container(容器)和iterator(迭代器)
算法
算法部分主要由头文件algorithm,numeric和functional组成。
- algorithm是所有STL头文件中最大的一个(尽管它很好理解),它是由一大堆模版函数组成的,可以认为每个函数在很大程度上都是独立的,其中常用到的功能范围涉及到比较、交换、查找、遍历操作、复制、修改、移除、反转、排序、合并等等。
- numeric体积很小,只包括几个在序列上面进行简单数学运算的模板函数,包括加法和乘法在序列上的一些操作。
- functional中则定义了一些模板类,用以声明函数对象。
容器
<vector>,<list>,<deque>,<set>,<map>,<stack>,<queue>
向量(vector)----连续存储的元素<vector>
列表(list)----由节点组成的双向链表,每个结点包含着一个元素<list>
双队列(deque)----连续存储的指向不同元素的指针所组成的数组<deque>
集合(set)----由节点组成的红黑树,每个节点都包含着一个元素,节点之间以某种作用于元素对的谓词排列,没有两个不同的元素能够拥有相同的次序 <set>
多重集合(multiset)----允许存在两个次序相等的元素的集合 <set>
栈(stack)----后进先出的值的排列 <stack>
队列(queue)----先进先出的执的排列 <queue>
优先队列(priority_queue)----元素的次序是由作用于所存储的值对上的某种谓词决定的的一种队列 <queue>
映射(map)----由{键,值}对组成的集合,以某种作用于键对上的谓词排列 <map>
多重映射(multimap)----允许键对有相等的次序的映射 <map>
const
const成员函数
- const对象只能访问const成员函数,而非const对象可以访问任意成员函数
- const对象的成员是不可修改的,然而const对象通过指针维护的对象却是可以修改的
- const成员函数不可以修改对象的数据,不管对象是否具有const性质,也不可以调用非const函数
- mutable修饰的数据成员,对于任何情况下通过任何手段都可修改,const也不例外
const 和 static 同时修饰成员函数
C++编译器在实现const的成员函数的时候为了确保该函数不能修改类的实例的状态,会在函数中添加一个隐式的参数const this*。但当一个成员为static的时候,该函数是没有this指针的。也就是说此时const的用法和static是冲突的。
const和define的选择
- define在预处理阶段展开,对源代码执行文本替换;const在编译运行阶段使用
- const声明显性指明了类型,而且可以更方便的用于复合类型。define必须在数字后面加各种后缀指出char int double之外的各种类型
- const遵循变量作用域规则,可以创建全局名称空间或数据块的常量
static
- 全局静态变量/函数:限定变量或函数为静态存储,只能在本源文件使用,不能使用extern调用。
- 局部静态变量:作用于局部,不销毁,全局变量区。
静态成员变量:所有类共用,不占用类的空间。必须在类实例化前初始化,初始化时不需要加static,以免与一般静态变量混淆
静态成员函数:只能调用静态成员
C与C++区别
- 面向对象:封装,继承,多态。
- 引入引用代替指针。
- const/inline/template替代宏常量。
- namespace解决重名的问题。
- STL提供高效的数据结构和算法
new/delete malloc/free
new 是关键词,malloc是函数
new operator/operator new/placement new
delete 后会变成悬停指针(陷阱)
特征 | new/delete | malloc/free |
---|---|---|
分配内存的位置 | 自由存储区(存疑) | 堆 |
内存分配失败返值 | 完整类型指针 | void* |
内存分配失败返值 | 默认抛出异常 | 返回NULL |
分配内存的大小 | 由编译器根据类型计算得出 | 必须显式指定字节数 |
处理数组 | 有处理数组的new版本new[] | 需要用户计算数组的大小后进行内存配 |
已分配内存的扩充 | 无法直观地处理 | 使用realloc简单完成 |
是否相互调用 | 可以,看具体的operator new/delete实现 | 不可调用new |
分配内存时内存不足 | 客户能够指定处理函数或重新制定分配器 | 无法通过用户代码进行处理 |
函数重载 | 允许 | 不允许 |
构造函数与析构函数 | 调用 | 不调用 |
构造函数和析构函数调用顺序
构造函数
析构函数
调用顺序
首先,如果存在基类,那么先调用基类的构造函数,如果基类的构造函数中仍然存在基类,那么程序会继续进行向上查找,直到找到它最早的基类进行初始化;
其次,如果所调用的类中定义的时候存在着对象被声明,那么在基类的构造函数调用完成以后,再调用对象的构造函数
最后,将调用派生类的构造函数
析构函数正好相反
析构函数为什么要是虚函数的原因是什么
#include "stdafx.h" #include "iostream" using namespace std; class Base { public: Base(){ std::cout<<"Base::Base()"<<std::endl; } ~Base(){ std::cout<<"Base::~Base()"<<std::endl; } }; class Base1:public Base { public: Base1(){ std::cout<<"Base1::Base1()"<<std::endl; } ~Base1(){ std::cout<<"Base1::~Base1()"<<std::endl; } }; class Derive { public: Derive(){ std::cout<<"Derive::Derive()"<<std::endl; } ~Derive(){ std::cout<<"Derive::~Derive()"<<std::endl; } }; class Derive1:public Base1 { private: Derive m_derive; public: Derive1(){ std::cout<<"Derive1::Derive1()"<<std::endl; } ~Derive1(){ std::cout<<"Derive1::~Derive1()"<<std::endl; } }; int _tmain(int argc, _TCHAR* argv[]) { Derive1 derive; return 0; } Base::Base() Base1::Base1() Derive::Derive() Derive1::Derive1() Derive1::~Derive1() Derive::~Derive() Base1::~Base1() Base::~Base()
虚函数
-
编译器会为每个有虚函数的类创建一个虚函数表,该虚函数表将被该类的所有对象共享。类的虚函数表是一块连续的内存,每个内存单元中记录一个JMP指令的地址。类的每个虚函数占据虚函数表中的一块,如果类中有N个虚函数,那么其虚函数表将有4N字节的大小。
编译器在有虚函数的类的实例中创建了一个指向这个表的指针,该指针通常存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能)。这意味着可以通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
有虚函数或虚继承的类实例化后的对象大小至少为4字节(确切的说是一个指针的字节数;说至少是因为还要加上其他非静态数据成员,还要考虑对齐问题);没有虚函数和虚继承的类实例化后的对象大小至少为1字节(没有非静态数据成员的情况下也要有1个字节来记录它的地址)。
单一的一般继承
虚函数表在最前面的位置。
- 成员变量根据其继承和声明顺序依次放在后面。
在单一的继承中,被overwrite的虚函数在虚函数表中得到了更新。
多重继承
每个父类都有自己的虚表。
- 子类的虚成员函数被放到了第一个父类的表中。
- 内存布局中,其父类布局依次按声明顺序排列。
每个父类的虚表中被overwrite的虚函数都会得到更新。
虚继承
在最后保留虚基类实例
保留一个指针指向虚基类实例(vs有,gcc没有)
哪些不能声明为虚函数
- 当存在类继承并且析构函数中有必须要进行的操作时(如需要释放某些资源,或执行特定的函数)析构函数需要是虚函数,否则若使用父类指针指向子类对象,在delete时只会调用父类的析构函数,而不能调用子类的析构函数,从而造成内存泄露或达不到预期结果;
- 内联函数不能为虚函数:内联函数需要在编译阶段展开,而虚函数是运行时动态绑定的,编译时无法展开;
- 构造函数不能为虚函数:构造函数在进行调用时还不存在父类和子类的概念,父类只会调用父类的构造函数,子类调用子类的,因此不存在动态绑定的概念;但是构造函数中可以调用虚函数,不过并没有动态效果,只会调用本类中的对应函数;
- 静态成员函数不能为虚函数:静态成员函数是以类为单位的函数,与具体对象无关,虚函数是与对象动态绑定的。
虚基类
用处:为了解决从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题,将共同基类设置为虚基类。这时从不同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。这样不仅就解决了二义性问题,也节省了内存,避免了数据不一致的问题。
构造函数:构造函数和析构函数的顺序:虚基类总是先于非虚基类构造,与它们在集成体系中的次序和位置无关。如果有多个虚基类,则按它们在派生列表中出现的顺序从左到右依次构造。
编译器按照直接基类的声明顺序依次检查,以确定其中是否含有虚基类。如果有,则先构造虚基类,然后按照声明顺序依次构造其他非虚基类。
class a { virtual void func(); }; class b:public virtual a { virtual void foo(); }; //size = 12(vs) gcc(4) class b:public a { virtual void foo(); }; //size = 4(vs) gcc(4) <hr /> class a { virtual void func(); char x; }; class b:public virtual a { virtual void foo(); }; //size = 16(vs) 12(gcc) class b:public a { virtual void foo(); }; //size = 8(vs) 8(gcc)
运算符重载
友元
C++内存管理
linux 内存分布
在多任务操作系统中,每个进程都运行在一个属于自己的虚拟内存中,而虚拟内存被分为许多页,并映射到物理内存中,被加载到物理内存中的文件才能够被执行。这里我们主要关注程序被装载后的内存布局,其可执行文件包含了代码段,数据段,BSS段,堆,栈等部分,其分布如下图所示。
- 代码段(.text):用来存放可执行文件的机器指令。存放在只读区域,以防止被修改。
- 只读数据段(.rodata):用来存放常量存放在只读区域,如字符串常量、全局const变量等。
- 可读写数据段(.data):用来存放可执行文件中已初始化全局变量,即静态分配的变量和全局变量。
- BSS段(.bss):未初始化的全局变量和局部静态变量一般放在.bss的段里,以节省内存空间。
- 堆:用来容纳应用程序动态分配的内存区域。当程序使用malloc或new分配内存时,得到的内存来自堆。堆通常位于栈的下方。
- 栈:用于维护函数调用的上下文。栈通常分配在用户空间的最高地址处分配。
- 动态链接库映射区:如果程序调用了动态链接库,则会有这一部分。该区域是用于映射装载的动态链接库。
- 保留区:内存中受到保护而禁止访问的内存区域。
C++内存分布
**栈区**stack:由编译器自动分配释放 ,存放为运行函数而分配的局部变量、函数参数、返回数据、返回地址等。其操作方式类似于数据结构中的栈。
**堆区**heap:一般由程序员分配释放, 若程序员不释放,程序结束时可能由 OS 回收 。分配方式类似于链表。
- 全局/静态存储区 static :存放全局变量、静态数据、常量。程序结束后有系统释放
- 常量存储区:存放常量,如字符串常量、全局const变量(非全局const存放在栈中)。 程序结束后由系统释放。
- 程序代码区:存放函数体(类成员函数和全局函数)的二进制代码
堆栈区别
- 申请管理方式
- 栈:编译器自动管理,无需我们手工控制
- 堆:申请释放程序员控制,容易发生内存泄漏
- 申请后系统响应
- 栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
- 堆:操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
- 大小限制
- 栈:向低地址扩展的数据结构,栈顶地址和最大容量系统预定好,windows下1MB(可以修改)。
- 堆:向高地址扩展,不连续,链表,受限于系统中有限的内存。
- 申请效率
- 栈:系统自动分配,速度快,但程序员无法控制
- 堆:new分配,速度慢,但方便
- 存储内容
- 栈:在函数调用时,第一个进栈的函数调用的下一条可执行语句的地址。然后是该函数的所有参数 ,参数由右往左入栈,然后是函数中的局部变量(非静态)。调用结束后局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
- 堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。
总结
堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;并且可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,ebp和局部变 量都采用栈的方式存放。所以,推荐大家尽量用栈,而不是用堆。虽然栈有如此众多的好处,但是向堆申请内存更加灵活,有时候分配大量的内存空间,还是用堆好一些。
智能指针
C++11中,定义了3种智能指针(unique_ptr、shared_ptr、weak_ptr),并删除了C++98中的auto_ptr。
智能指针的设计思想:将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间。
unique_ptr 只允许基础指针的一个所有者。unique_ptr小巧高效;大小等同于一个指针且支持rvalue引用,从而可实现快速插入和对STL集合的检索。
shared_ptr采用引用计数的智能指针,主要用于要将一个原始指针分配给多个所有者(例如,从容器返回了指针副本又想保留原始指针时)的情况。当所有的shared_ptr所有者超出了范围或放弃所有权,才会删除原始指针。大小为两个指针;一个用于对象,另一个用于包含引用计数的共享控制块。最安全的分配和使用动态内存的方法是调用make_shared标准库函数,此函数在动态分配内存中分配一个对象并初始化它,返回对象的shared_ptr。
智能指针支持的操作
- 使用重载的
->
和*
运算符访问对象。 - 使用get成员函数获取原始指针,提供对原始指针的直接访问。你可以使用智能指针管理你自己的代码中的内存,还能将原始指针传递给不支持智能指针的代码。
- 使用删除器定义自己的释放操作。
- 使用release成员函数的作用是放弃智能指针对指针的控制权,将智能指针置空,并返回原始指针。(只支持unique_ptr)
- 使用reset释放智能指针对对象的所有权。
设计模式
当仅允许类的一个实例在应用中被创建的时候,我们使用单例模式(Singleton Pattern)。它保护类的创建过程来确保只有一个实例被创建,它通过设置类的构造方法为私有(private)来实现。要获得类的实例,单例类可以提供一个方法,如GetInstance(),来返回类的实例。该方法是唯一可以访问类来创建实例的方法。
优点
- 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。
- 减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。
- 避免对资源的多重占用。如避免对同一个资源文件的同时写操作。
- 单例模式可以在系统设置全局的访问点,优化和共享资源访问。
缺点
- 单例模式一般没有接口,扩展困难。不利于测试。
使用场景
- 在整个项目中需要一个共享访问点或共享数据。
- 创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源。
- 需要定义大量的静态常量和静态方法的环境。
实现代码
- 懒汉模式
//Singleton.h class Singleton { public: static Singleton* GetInstance(); private: Singleton() {pthread_mutex_init(&mutex);} static Singleton *m_pInstance; static pthread_mutex_t mutex; //CGarbo类的唯一工作就是在析构函数中删除CSingleton的实例 class CGarbo { public: ~CGarbo() { if (Singleton::m_pInstance != NULL) delete Singleton::m_pInstance; } }; //定义一个静态成员,在程序结束时,系统会调用它的析构函数 static CGarbo Garbo; }; //Singleton.cpp Singleton* Singleton::m_pInstance = NULL; Singleton* Singleton::GetInstance() { if (m_Instance == NULL) { pthread_mutex_lock(&mutex); if (m_Instance == NULL) { m_Instance = new Singleton(); } pthread_mutex_unlock(&mutex); } return m_pInstance; }
- 饿汉模式
//Singleton.h class Singleton { public: static Singleton* GetInstance(); private: Singleton() {} static Singleton *m_pInstance; class CGarbo { public: ~CGarbo() { if (Singleton::m_pInstance != NULL) delete Singleton::m_pInstance; } }; static CGarbo garbo; }; //Singleton.cpp Singleton* Singleton::m_pInstance = new Singleton(); Singleton* Singleton::GetInstance() { return m_pInstance; }