精华内容
下载资源
问答
  • C++面试知识点总结

    2018-09-25 16:59:04
    个人总结C++知识点,涵盖C++很多方面,对于C++找工作很有帮助,当然对于C++学习者,也是一个巩固的方法
  • c++面试知识点总结

    2019-10-28 22:11:01
    C++面试基础中不可避免会问到STL容器相关知识点,STL容器包含两大类:序列式容器 & 关联容器 序列式容器:元素都可序(ordered),但是未必有序(sorted),通过元素的顺序来访问,包含vector、list、dequeue、stack...

    STL容器篇

    C++面试基础中不可避免会问到STL容器相关知识点,STL容器包含两大类:序列式容器 & 关联容器
    序列式容器:元素都可序(ordered),但是未必有序(sorted),通过元素的顺序来访问,包含vector、list、dequeue、stack、queue、priority-queue。
    关联容器:数据以key-value的形式组成,可通过key值访问元素,包含RB-tree、map、set、multiset、multimap、hashtable、hash_set、hash_map、hash_multiset、hash_multimap;
    

    萃取

    关于萃取可参考下面文章:
    https://yosef-gao.github.io/2016/10/05/cpp-traits/

    Vector

    vector可以看作式一种动态数组,普通的数组申明时需要固定大小但是vector不需要。向vector中添加元素时会存在“重新分配空间、元素移动、释放原空间”等步骤,
    1.在当前capacity不足以存放新元素时,需要将vector扩张至当前空间的2倍大小,若还不够则继续扩张;
    2.上述的扩张过程对于大对象在移动过程中会很损耗性能;
    

    List

    list vs vector
    1、list 相较于vector的特点是可以很方便的删除节点和插入节点,删除节点和插入节点的都不会造成原节点的失效,在某些情况下插入和删除节点的效率相较于vector要高;
    2、因为list的每个节点只保存前一节点(pre)或者后一节点(next)的指针,需要读取或者查找list节点时,需以其中一个节点为起始遍历比较,所以查找和读取节点不如vector方便。

    list迭代器的操作:递增时指向下一节点,递减时指向上一节点,取值时是节点的数据值,成员取用的是节点的成员。

    deque & stack & queue

    deque:双向开口的连续空间,可以在头尾两端分别做元素的插入和删除操作,
    没有容量的限制,随时可以新增一段新的空间链接起来,但是迭代器很复杂。每个节点至少保存三种数据结构:中控节点map、起始节点start、终止节点end,数据节点(数据节点的个数不少于8个或者是num+2个,num为已经存在的需要的数据节点,其中多余的两个是buffer)。dequeue允许以常数时间内对头端进行元素的插入和移除操作。
    stack:是一种先进后出的数据结构,底层以deque实现,或者以list实现也可;
    queue:是一种先进先出数据结构,可以dequeue或者list为底层容器;

    Priority Queue

    Priority Queue的底层实现是heap(堆)。
    heap:有最大堆(优先级最高的在堆顶)和最小堆(优先级最低的在堆顶),下面介绍的堆均默认为最大堆。
    性质:
    (1)heap是一颗完全二叉树,除叶子节点外,每一层均是满的;
    (2)叶子节点从左至右不得有空隙;
    (3)所有子树的根节点均大于叶子节点;
    heap有关的数据结构:
    (1)一颗完全二叉树
    (2)一个长度为n(树元素的个数)数组arr
    假设当前节点在数组arr的index为i,则左子节点arr的index为2i,右子节点的index为2i+1;
    heap中插入元素:将待插入的节点插入至树的最右子节点,然后一次上溯调整树,使其满足上述三点性质;
    pop-heap:首先删除最顶上元素,为了满足complete binary tree的条件,必须割舍最下层右边的叶子节点。并将其值重新安插至max-heap(因此有必要重新调整heap结构)

    仿函数

    仿函数:顾名思义,就是其行为像函数一样的对象;
    一般的应用场景:仿函数可以理解为针对一种算法思路或者是策略的封装,一般要将这类函数当做算法的参数使用,需要将封装好的函数作为函数指针传入调用函数,但是函数指针并不支持模板化;
    

    在面向对象程序设计过程中,将“操作”和数据结构解藕是比较高端的设计方式(反正我等菜鸟还在领悟中);例如将指定范围内的所有元素都相加有两种实现过程,一种是将所有元素从头至尾都加上,一种是设计仿函数实现“两两相加”;肯定第二种方式更高端塞。
    废话少说,上代码如下:编译环境g++ (GCC) 4.8.2,此代码可能在其他环境不行,因为没有加类型萃取

    #include <iostream>
    
    template<class T>
    struct plus {
        T operator()(const T& x, const T& y) { return x + y;}
    };
    
    template<class T>
    struct minus {
        T operator()(const T&x, const T&y) {return x -y;}
    };
    
    int main() {
        std::cout << plus<int>()(3, 4) << std::endl;  //不是plus<int>(3, 4)哈
        std::cout << minus<int>()(6,7) << std::endl;
        return 0;
    }
    

    输出:
    7
    -1

    STL中常见的仿函数使用方式如下

    algorithm(first, last, ..., functionObj)
    {
    	...
    	functionobj();
    	...
    }
    

    算法泛型化的过程

    算法泛型化的过程可以理解为抽象化的过程,具体方法论:
    (1)将操作对象游标化,即关注元素移动而不是操作对象的容器类型,相当于iterator的产生原因;
    (2)将具体的“操作”(算法的过程)抽象化,例如上述的仿函数;

    int* find(int* arrayHead, int arraySize, const int& value) {
        int i = 0;
        for (; i < arraySize; i++) {
            if (arrayHead[i] == value) {
                break;
            }
        }
        return &(arrayHead[i]);
    }
    
    

    以上代码对容器的依赖性比较强(例如,必须计算出容器的size),同时对于最后一个元素的判断不好处理,事实上stl的遵循“前闭后开”的原则,可以使用指针指向最后一个元素的next元素(end),但是无法针对end提取值,因为end指向的是最后一个元素的下一个节点,可能会为null,故改进find算法如下:

    int* find(int* start, int* end, const int& value) {
        while (start != end) {
            if (*start == value) {
                break;
            }
            ++start;
        }
        return start;
    }
    

    关联式容器

    在这里插入图片描述
    上图通过内缩表示各个容器的包含关系,例如vector底层实现是内建类型array,heap内部包含容器vector及树。。。。。
    本节重点剖析关联式容器,介绍关联容器之前势必需涉猎一点树的前世今生。

    1、二叉搜索树-set/map/multiset/multimap

        二叉树:即每个节点只有左右子节点;
         二叉搜索树:所有左子树节点的值小于根节点的值;所有右子树的值大于根节点的值;
         插入节点时,可从根节点开始,遇到键值大的就向左,遇到键值大的就向右边;
         删除节点时,若当前节点只有一个节点,则删除当前节点,并利用其子节点替代它原来的位置;若当前节点还有其他节点,则用右子树最小的节点替代当前节点;
      2、平衡二叉搜索树
       所谓的平衡是指没有任何一个节点过深。
       **AVL tree**:任何节点左右子树高度相差最多1,插入元素时,为了满足平横性,借助单旋(节点外侧插入元素)或者双旋(节点内侧插入元素)调整树的平衡性。
       **RB-tree**:不仅是一个二叉搜索树,还必须满足如下条件:
       (1)每个节点不是红色就是黑色;
       (2)根节点必须是黑色;
       (3)不能有两个连续的红色节点;
       (4)任一节点至NULL的任何路径,所含黑色节点数必须相同;
    RB-tree插入和删除元素的时间复杂度都是logN,且与vector不同,基于 RB-tree的容器——set/map/multiset/multimap等插入和删除之前的迭代器均不会失效,因为iterator是一个指向内存的指针,RB-tree插入和删除时原节点的存储位置并没有变化,变化的只是节点的right或者left指针,故iterator不会失效。
    
      
       set与multiset  map与multimap 的区别在于set和map插入元素时调用的接口是insert_unique() ,而multiset和multimap插入元素时调用的接口是insert_equal()。
    

    hashtable

    一种插入、删除、搜寻等操作具有“常数平均时间”的表现的容器。
    当有不同的元素经过hash函数hash后映射到相同的位置,即所谓的”碰撞“,解决碰撞的方法:线性探测、二次探测、开链等。其中开链是重点,所谓的开链是在每一个表格元素中维护一个list;hash function为我们分配一个list,然后我们在那个list身上执行元素的插入、搜寻、删除等操作。
    底层以hashtable数据结构实现的容器有hash_set/hash_map/hast_multimap/hash_multiset

    展开全文
  • C++ 面试知识点总结

    2018-08-02 13:21:37
    1. C++基础知识点 1.1 有符号类型和无符号类型 当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模之后的余数。当我们赋给带符号类型一个超出它表示范围的值时,结果是...

    1. C++基础知识点

    1.1 有符号类型和无符号类型

    • 当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模之后的余数。当我们赋给带符号类型一个超出它表示范围的值时,结果是未定义的;此时,程序可能继续工作、可能崩溃。也可能生成垃圾数据。
    • 如果表达式中既有带符号类型由于无符号类型那个,当带符号类型取值为负时会出现异常结果,这是因为带符号数会自动转换成无符号数。
    int a = 1;unsigned int b = -2;
    cout<<a+b<<endl;   // 输出4294967295
    int c = a + b;   //c=-1;
    a = 3;b = -2;
    cout<<a+b<<endl;   // 输出1
    

    1.2 引用与指针

    引用并非对象,它只是为一个已经存在的对象起的一个别名。在定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用。一旦初始化完成,应用将和它的初始值绑定在一起。以为无法令引用重新绑定到另外一个对象,因此引用必须初始化。

    指针是指向另外一种类型的符合类型。与引用类似,指针也实现了对其他对象的简介访问。然而指针与引用相比又有许多不同点:

    • 指针本身就是一个对象,允许对指针赋值和拷贝。而且在指针的生命周期内它可以先后指向几个不同的对象。引用不是对象,所以也不能定义指向引用的指针。
    • 指针无须在定义时赋值。

    void*是一种特殊的指针类型,可以存放任意对象的地址。但我们对该地址中存放的是什么类型的对象并不了解,所以也不能直接操作void*指针所指的对象。

    1.3 static关键字

    • 申明为static的局部变量,存储在静态存储区,其生存期不再局限于当前作用域,而是整个程序的生存期。
    • 对于全局变量而言, 普通的全局变量和函数,其作用域为整个程序或项目,外部文件(其它cpp文件)可以通过extern关键字访问该变量和函数;static全局变量和函数,其作用域为当前cpp文件,其它的cpp文件不能访问该变量和函数。
    • 当使用static修饰成员变量和成员函数时,表示该变量或函数属于一个类,而不是该类的某个实例化对象。

    1.4 const限定符

    const的作用

    1. 在定义常变量时必须同时对它初始化,此后它的值不能再改变。常变量不能出现在赋值号的左边(不为“左值”);
    2. 对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;
    3. 在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
    4. 对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量;
    5. 对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为"左值"。例如:
    //operator*的返回结果必须是一个const对象,否则下列代码编译出错
    const classA operator*(const classA& a1,const classA& a2);  
    classA a, b, c;
    (a*b) = c;  //对a*b的结果赋值。操作(a*b) = c显然不符合编程者的初衷,也没有任何意义
    

    用const修饰的符号常量的区别:const位于(*)的左边,表示被指物是常量;const位于(*)的右边,表示指针自身是常量(常量指针)。(口诀:左定值,右定向)

    const char *p;  //指向const对象的指针,指针可以被修改,但指向的对象不能被修改。
    char const *p;  //同上
    char * const p; //指向char类型的常量指针,指针不能被修改,但指向的对象可以被修改。
    const char * const p;  //指针及指向对象都不能修改。
    

    const与#define的区别

    1. const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应)。
    2. 有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试。
    3. 在C++程序中只使用const常量而不使用宏常量,即const常量完全取代宏常量。

    1.5 数组与指针的区别

    1. 数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。指针可以随时指向任意类型的内存块。
    2. 用运算符sizeof可以计算出数组的容量(字节数)。sizeof(p),p为指针得到的是一个指针变量的字节数,而不是p所指的内存容量。C/C++语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。
    3. C++编译系统将形参数组名一律作为指针变量来处理。实际上在函数调用时并不存在一个占有存储空间的形参数组,只有指针变量。

    实参数组名a代表一个固定的地址,或者说是指针型常量,因此要改变a的值是不可能的。例如:a++;是错误的。形参数组名array是指针变量,并不是一个固定的地址值。它的值是可以改变的。例如:array++;是合法的。

    为了节省内存,C/C++把常量字符串放到单独的一个内存区域。当几个指针赋值给相同的常量字符串时,它们实际上会指向相同的内存地址。但用常量内存初始化数组时,情况却有所不同。

    char str1[] = “Hello World”;
    char str2[] = “Hello World”;
    char *str3[] = “Hello World”;
    char *str4[] = “Hello World”;
    

    其中,str1和str2会为它们分配两个长度为12个字节的空间,并把“Hello World”的内容分别复制到数组中去,这是两个初始地址不同的数组。str3和str4是两个指针,我们无须为它们分配内存以存储字符串的内容,而只需要把它们指向“Hello World”在内存中的地址就可以了。由于“Hello World”是常量字符串,它在内存中只有一个拷贝,因此str3和str4指向的是同一个地址。

    1.6 sizeof运算符

    sizeof是C语言的一种单目操作符,它并不是函数。操作数可以是一个表达式或类型名。数据类型必须用括号括住,sizeof(int);变量名可以不用括号括住。

    int a[50];  //sizeof(a)=200
    int *a=new int[50];  //sizeof(a)=4;
    Class Test{int a; static double c};  //sizeof(Test)=4
    Test *s;  //sizeof(s)=4
    Class Test{ };  //sizeof(Test)=1
    int func(char s[5]);  //sizeof(s)=4;
    

    操作数不同时注意事项:

    1. 数组类型,其结果是数组的总字节数;指向数组的指针,其结果是该指针的字节数。
    2. 函数中的数组形参函数类型的形参,其结果是指针的字节数。
    3. 联合类型,其结果采用成员最大长度对齐。
    4. 结构类型或类类型,其结果是这种类型对象的总字节数,包括任何填充在内。
    • 类中的静态成员不对结果产生影响,因为静态变量的存储位置与结构或者类的实例地址无关;
    • 没有成员变量的类的大小为1,因为必须保证类的每一个实例在内存中都有唯一的地址;
    • 有虚函数的类都会建立一张虚函数表,表中存放的是虚函数的函数指针,这个表的地址存放在类中,所以不管有几个虚函数,都只占据一个指针大小。

    例题:

    1、下列联合体的sizeof(sampleUnion)的值为多少。

    union{
        char flag[3];
        short value;
    } sampleUnion;
    

    答案:4。联合体占用大小采用成员最大长度的对齐,最大长度是short的2字节。但char flag[3]需要3个字节,所以sizeof(sampleUnion) = 2*(2字节)= 4。注意对齐有两层含义,一个是按本身的字节大小数对齐,一个是整体按照最大的字节数对齐。

    2、在32位系统中:

    char arr[] = {4, 3, 9, 9, 2, 0, 1, 5};
    char *str = arr;
    sizeof(arr) = 8;
    sizeof(str) = 4;
    strlen(str) = 5;
    

    答案:8,4,5。注意strlen函数求取字符串长度以ASCII值为0为止。

    3、定义一个空的类型,里面没有任何成员变量和成员函数。
    问题:对该类型求sizeof,得到的结果是什么?
    答案:1。
    问题:为什么不是0?
    答案:当我们声明该类型的实例的时候,它必须在内存中占有一定的空间,否则无法使用这些实例。至于占用多少内存,由编译器决定。Visual Studio中每个空类型的实例占用1字节的空间。
    问题:如果在该类型中添加一个构造函数和析构函数,结果又是什么?
    答案:还是1。调用构造函数和析构函数只需要知道函数的地址即可,而这些函数的地址只与类型相关,而与类型的实例无关。
    问题:那如果把析构函数标记为虚函数呢?
    答案:C++的编译器一旦发现一个类型中有虚函数,就会为该类型生成虚函数表,并在该类型的每一个实例中添加一个指向虚函数表的指针。在32位的机器上,指针占用4字节,因此求sizeof得到4;如果是64位机器,将得到8。

    1.7 四个强制类型转换

    C++中有以下四种命名的强制类型转换:

    • static_cast:任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast。
    • const_cast:去const属性,只能改变运算对象的底层const。常用于有函数重载的上下文中。
    • reninterpret_cast:通常为运算对象的位模式提供较低层次的重新解释,本质依赖与机器。
    • dynamic_cast:主要用来执行“安全向下转型”,也就是用来决定某对象是否归属继承体系中的某个类型。主要用于多态类之间的转换

    一般来说,如果编译器发现一个较大的算术类型试图赋值给较小的类型,就会给出警告;但是当我们执行了显式的类型转换之后,警告信息就被关闭了。

    //进行强制类型转换以便执行浮点数除法
    int j = 1,i = 2;
    double slope = static_cast<double>(j)/i;
    
    //任何非常量对象的地址都能存入void*,通过static_cast可以将指针转换会初始的指针类型
    void* p = &slope;
    double *dp = static_cast<double*>(p);
    

    只有const_cast能够改变表达式的常量属性,其他形式的强制类型转换改变表达式的常量属性都将引发编译器错误。

    //利用const_cast去除底层const
    const char c = 'a';
    const char *pc = &c;
    char* cp = const_cast<char*>(pc);
    *cp = 'c';
    

    reinterpret_cast常用于函数指针类型之间进行转换。

    int doSomething(){return0;};
    typedef void(*FuncPtr)(); //FuncPtr是一个指向函数的指针,该函数没有参数,返回值类型为void
    FuncPtr funcPtrArray[10]; //假设你希望把一个指向下面函数的指针存入funcPtrArray数组:
    
    funcPtrArray[0] =&doSomething;// 编译错误!类型不匹配
    funcPtrArray[0] = reinterpret_cast<FuncPtr>(&doSomething); //不同函数指针类型之间进行转换
    

    dynamic_cast
    有条件转换,动态类型转换,运行时类型安全检查(转换失败返回NULL):

    1. 安全的基类和子类之间转换。
    2. 必须要有虚函数。
    3. 相同基类不同子类之间的交叉转换。但结果是NULL。
    class Base {
    public:
    int m_iNum;
    virtualvoid foo(){}; //基类必须有虚函数。保持多态特性才能使用dynamic_cast
    };
    
    class Derive: public Base {
    public:
    char*m_szName[100];
    void bar(){};
    };
    
    Base* pb =new Derive();
    Derive *pd1 = static_cast<Derive *>(pb); //子类->父类,静态类型转换,正确但不推荐
    Derive *pd2 = dynamic_cast<Derive *>(pb); //子类->父类,动态类型转换,正确
    
    Base* pb2 =new Base();
    Derive *pd21 = static_cast<Derive *>(pb2); //父类->子类,静态类型转换,危险!访问子类m_szName成员越界
    Derive *pd22 = dynamic_cast<Derive *>(pb2); //父类->子类,动态类型转换,安全的。结果是NULL
    

    1.8 结构体的内存对齐

    内存对齐规则

    • 每个成员相对于这个结构体变量地址的偏移量正好是该成员类型所占字节的整数倍。为了对齐数据,可能必须在上一个数据结束和下一个数据开始的地方插入一些没有用处字节。
    • 且最终占用字节数为成员类型中最大占用字节数的整数倍。
    struct AlignData1
    {
        char c;
        short b;
        int i;
        char d;
    }Node;
    

    这个结构体在编译以后,为了字节对齐,会被整理成这个样子:

    struct AlignData1
    {
        char c;
        char padding[1];
        short b;
        int i;
        char d;
        char padding[3];
    }Node;
    

    所以编译前总的结构体大小为:8个字节。编译以后字节大小变为:12个字节。
    但是,如果调整顺序:

    struct AlignData2
    {
        char c;
        char d;
        short b;
        int i;
    }Node;
    

    那么这个结构体在编译前后的大小都是8个字节。
    那么编译后不用填充字节就能保持所有的成员都按各自默认的地址对齐。这样可以节约不少内存!一般的结构体成员按照默认对齐字节数递增或是递减的顺序排放,会使总的填充字节数最少。

    1.9 malloc/free 与 new/delete的区别

    1. malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请和释放动态内存。
    2. 对于非内部数据类型的对象而言,用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free,因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,和一个能完成清理与释放内存工作的运算符delete。
    3. new可以认为是malloc加构造函数的执行。new出来的指针是直接带类型信息的。而malloc返回的都是void*指针。newdelete在实现上其实调用了malloc,free函数。
    4. new建立的是一个对象;malloc分配的是一块内存。

    2. 面对对象编程

    2.1 String类的实现

    class MyString  
    {  
    public:
        MyString();
        MyString(const MyString &);
        MyString(const char *);  
        MyString(const size_t,const char);  
        ~MyString();
      
        size_t length();// 字符串长度  
        bool isEmpty();// 返回字符串是否为空  
        const char* c_str();// 返回c风格的trr的指针 
        friend ostream& operator<< (ostream&, const MyString&);  
        friend istream& operator>> (istream&, MyString&);  
        //add operation  
        friend MyString operator+(const MyString&,const MyString&);  
        // compare operations  
        friend bool operator==(const MyString&,const MyString&);  
        friend bool operator!=(const MyString&,const MyString&); 
        friend bool operator<=(const MyString&,const MyString&); 
        friend bool operator>=(const MyString&,const MyString&);  
        // 成员函数实现运算符重载,其实一般需要返回自身对象的,成员函数运算符重载会好一些
        char& operator[](const size_t);  
        const char& operator[](const size_t)const; 
        MyString& operator=(const MyString&); 
        MyString& operator+=(const MyString&); 
        // 成员操作函数  
        MyString substr(size_t pos,const size_t n);  
        MyString& append(const MyString&);  
        MyString& insert(size_t,const MyString&);  
        MyString& erase(size_t,size_t);  
        int find(const char* str,size_t index=0);  
    
    private:  
        char *p_str;  
        size_t strLength;  
    };  
    

    2.2 派生类中构造函数与析构函数,调用顺序

    构造函数的调用顺序总是如下:

    1. 基类构造函数。如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序,而不是它们在成员初始化表中的顺序。
    2. 成员类对象构造函数。如果有多个成员类对象则构造函数的调用顺序是对象在类中被声明的顺序,而不是它们出现在成员初始化表中的顺序。如果有的成员不是类对象,而是基本类型,则初始化顺序按照声明的顺序来确定,而不是在初始化列表中的顺序。
    3. 派生类构造函数。

    析构函数正好和构造函数相反。

    2.3 虚函数的实现原理

    虚函数表:
    编译器会为每个有虚函数的类创建一个虚函数表,该虚函数表将被该类的所有对象共享。类的虚函数表是一块连续的内存,每个内存单元中记录一个JMP指令的地址。类的每个虚函数占据虚函数表中的一块,如果类中有N个虚函数,那么其虚函数表将有4N字节的大小。

    编译器在有虚函数的类的实例中创建了一个指向这个表的指针,该指针通常存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能)。这意味着可以通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。

    有虚函数或虚继承的类实例化后的对象大小至少为4字节(确切的说是一个指针的字节数;说至少是因为还要加上其他非静态数据成员,还要考虑对齐问题);没有虚函数和虚继承的类实例化后的对象大小至少为1字节(没有非静态数据成员的情况下也要有1个字节来记录它的地址)。

    哪些函数适合声明为虚函数,哪些不能?

    • 当存在类继承并且析构函数中有必须要进行的操作时(如需要释放某些资源,或执行特定的函数)析构函数需要是虚函数,否则若使用父类指针指向子类对象,在delete时只会调用父类的析构函数,而不能调用子类的析构函数,从而造成内存泄露或达不到预期结果;
    • 内联函数不能为虚函数:内联函数需要在编译阶段展开,而虚函数是运行时动态绑定的,编译时无法展开;
    • 构造函数不能为虚函数:构造函数在进行调用时还不存在父类和子类的概念,父类只会调用父类的构造函数,子类调用子类的,因此不存在动态绑定的概念;但是构造函数中可以调用虚函数,不过并没有动态效果,只会调用本类中的对应函数;
    • 静态成员函数不能为虚函数:静态成员函数是以类为单位的函数,与具体对象无关,虚函数是与对象动态绑定的。

    2.4 虚继承的实现原理

    为了解决从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题,将共同基类设置为虚基类。这时从不同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。这样不仅就解决了二义性问题,也节省了内存,避免了数据不一致的问题。

    构造函数和析构函数的顺序:虚基类总是先于非虚基类构造,与它们在集成体系中的次序和位置无关。如果有多个虚基类,则按它们在派生列表中出现的顺序从左到右依次构造。

    #include <iostream>
    using namespace std;
    
    class zooAnimal
    {
    public: zooAnimal(){cout<<"zooAnimal construct"<<endl;}
    };
    class bear : virtual public zooAnimal
    {
    public: bear(){cout<<"bear construct"<<endl;}
    };
    class toyAnimal
    {
    public: toyAnimal(){cout<<"toyAnimal construct"<<endl;}
    };
    class character
    {
    public: character(){cout<<"character construct"<<endl;}
    };
    class bookCharacter : public character
    {
    public: bookCharacter(){cout<<"bookCharacter construct"<<endl;}
    };
    class teddyBear : public bookCharacter, public bear, virtual public toyAnimal
    {
    public: teddyBear(){cout<<"teddyBear construct"<<endl;}
    };
    
    int main()
    {
        teddyBear();
    }
    

    编译器按照直接基类的声明顺序依次检查,以确定其中是否含有虚基类。如果有,则先构造虚基类,然后按照声明顺序依次构造其他非虚基类。构造函数的顺序是:zooAnimal, toyAnimal, character, bookCharacter, bear, teddyBear。析构过程与构造过程正好相反。

    3. 内存管理

    3.1 程序加载时的内存分布

    在多任务操作系统中,每个进程都运行在一个属于自己的虚拟内存中,而虚拟内存被分为许多页,并映射到物理内存中,被加载到物理内存中的文件才能够被执行。这里我们主要关注程序被装载后的内存布局,其可执行文件包含了代码段,数据段,BSS段,堆,栈等部分,其分布如下图所示。

     

    内存分布

    • 代码段(.text):用来存放可执行文件的机器指令。存放在只读区域,以防止被修改。
    • 只读数据段(.rodata):用来存放常量存放在只读区域,如字符串常量、全局const变量等。
    • 可读写数据段(.data):用来存放可执行文件中已初始化全局变量,即静态分配的变量和全局变量。
    • BSS段(.bss):未初始化的全局变量和局部静态变量一般放在.bss的段里,以节省内存空间。
    • 堆:用来容纳应用程序动态分配的内存区域。当程序使用malloc或new分配内存时,得到的内存来自堆。堆通常位于栈的下方。
    • 栈:用于维护函数调用的上下文。栈通常分配在用户空间的最高地址处分配。
    • 动态链接库映射区:如果程序调用了动态链接库,则会有这一部分。该区域是用于映射装载的动态链接库。
    • 保留区:内存中受到保护而禁止访问的内存区域。

    3.2 堆与栈的区别

    1. 申请管理方式

    (1)栈:由编译器自动管理,无需我们手工控制。
    (2)堆:堆的申请和释放工作由程序员控制,容易产生内存泄漏。

    2. 申请后系统的响应

    (1)栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
    (2)堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

    3、申请大小的限制

    (1)栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是1M(可修改),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
    (2)堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

    4、申请效率的比较

    (1)栈由系统自动分配,速度较快。但程序员是无法控制的。
    (2)堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一块内存,虽然用起来最不方便。但是速度快,也最灵活。

    5、栈和堆中的存储内容

    (1)栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
    (2)堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。

    总结:堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;并且可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,ebp和局部变 量都采用栈的方式存放。所以,推荐大家尽量用栈,而不是用堆。虽然栈有如此众多的好处,但是向堆申请内存更加灵活,有时候分配大量的内存空间,还是用堆好一些。

    3.3 常见的内存错误及其对策

    1. 内存分配未成功,却使用了它,因为没有意识到内存分配会不成功。
      解决办法:在使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行检查。如果是用malloc或new来申请内存,应该用if(p==NULL) 或if(p!=NULL)进行防错处理。

    2. 内存分配虽然成功,但是尚未初始化就引用它。犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。
      解决方法:不要忘记为数组和动态内存赋初值,即便是赋零值也不可省略。防止将未被初始化的内存作为右值使用。

    3. 内存分配成功并且已经初始化,但操作越过了内存的边界。例如在使用数组时经常发生下标“多1”或者“少1”的操作。特别是在for循环语句中,循环次数很容易搞错,导致数组操作越界。
      解决方法:避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。

    4. 忘记了释放内存,造成内存泄露。含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,你看不到错误。终有一次程序突然死掉,系统出现提示:内存耗尽。
      解决方法:动态内存的申请与释放必须配对,程序中malloc与free的使用次数一定要相同,否则肯定有错误(new/delete同理)。

    5. 释放了内存却继续使用它。有三种情况:(1)程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。(2)函数的return语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。(3)使用free或delete释放了内存后,没有将指针设置为NULL。导致产生“野指针”。
      解决方法:用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。

    3.4 智能指针

    智能指针是在 <memory> 标头文件中的std命名空间中定义的,该指针用于确保程序不存在内存和资源泄漏且是异常安全的。它们对RAII“获取资源即初始化”编程至关重要,RAII的主要原则是为将任何堆分配资源(如动态分配内存或系统对象句柄)的所有权提供给其析构函数包含用于删除或释放资源的代码以及任何相关清理代码的堆栈分配对象。大多数情况下,当初始化原始指针或资源句柄以指向实际资源时,会立即将指针传递给智能指针。在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释放智能指针对对象的所有权。

    智能指针的使用示例:

    #include <iostream>
    #include <string>
    #include <memory>
    using namespace std;
    
    class base
    {
    public:
        base(int _a): a(_a)    {cout<<"构造函数"<<endl;}
        ~base()    {cout<<"析构函数"<<endl;}
        int a;
    };
    
    int main()
    {
        unique_ptr<base> up1(new base(2));
        // unique_ptr<base> up2 = up1;   //编译器提示未定义
        unique_ptr<base> up2 = move(up1);  //转移对象的所有权 
        // cout<<up1->a<<endl; //运行时错误 
        cout<<up2->a<<endl; //通过解引用运算符获取封装的原始指针 
        up2.reset(); // 显式释放内存 
        
        shared_ptr<base> sp1(new base(3));
        shared_ptr<base> sp2 = sp1;  //增加引用计数 
        cout<<"共享智能指针的数量:"<<sp2.use_count()<<endl;  //2
        sp1.reset();  //
        cout<<"共享智能指针的数量:"<<sp2.use_count()<<endl;  //1
        cout<<sp2->a<<endl; 
        auto sp3 = make_shared<base>(4);//利用make_shared函数动态分配内存 
    }
    

    4. C++对象内存模型

    在C++中有两种类的数据成员:static和nonstatic,以及三种类的成员函数:static、nonstatic和virtual。在C++对象模型中,非静态数据成员被配置于每一个类的对象之中,静态数据成员则被存放在所有的类对象之外;静态及非静态成员函数也被放在类对象之外,虚函数则通过以下两个步骤支持:

    1. 每一个类产生出一堆指向虚函数的指针,放在表格之中,这个表格被称为虚函数表(virtual table, vtbl)。
    2. 每一个类对象被添加了一个指针,指向相关的虚函数表,通常这个指针被称为vptr。vptr的设定和重置都由每一个类的构造函数、析构函数和拷贝赋值运算符自动完成。另外,虚函数表地址的前面设置了一个指向type_info的指针,RTTI(Run Time Type Identification)运行时类型识别是由编译器在编译器生成的特殊类型信息,包括对象继承关系,对象本身的描述,RTTI是为多态而生成的信息,所以只有具有虚函数的对象在会生成。

    4.1 继承下的对象内存模型

    C++支持单一继承、多重继承和虚继承。在虚继承的情况下,虚基类不管在继承链中被派生多少次,永远只会存在一个实体。

    单一继承,继承关系为class Derived : public Base。其对象的内存布局为:虚函数表指针、Base类的非static成员变量、Derived类的非static成员变量。

    多重继承,继承关系为class Derived : public Base1, public Base2。其对象的内存布局为:基类Base1子对象和基类Base2子对象及Derived类的非static成员变量组成。基类子对象包括其虚函数表指针和其非static的成员变量。

    重复继承,继承关系如下。Derived类的对象的内存布局与多继承相似,但是可以看到基类Base的子对象在Derived类的对象的内存中存在一份拷贝。这样直接使用Derived中基类Base的相关成员时,就会引发歧义,可使用多重虚拟继承消除之。

    class Base1 : public Base
    class Base2: public Base
    class Derived : public Base1, public Base2
    

    虚继承,继承关系如下。其对象的内存布局与重复继承的类的对象的内存分布类似,但是基类Base的子对象没有拷贝一份,在对象的内存中仅存在在一个Base类的子对象。但是它的非static成员变量放置在对象的末尾处。

    class Base1 : virtual public Base
    class Base2: virtual public Base
    class Derived : public Base1, public Base2
    

    5. 常见的设计模式

    5.1 单例模式

    当仅允许类的一个实例在应用中被创建的时候,我们使用单例模式(Singleton Pattern)。它保护类的创建过程来确保只有一个实例被创建,它通过设置类的构造方法为私有(private)来实现。要获得类的实例,单例类可以提供一个方法,如GetInstance(),来返回类的实例。该方法是唯一可以访问类来创建实例的方法。

    优点:(1)由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。(2)减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。(3)避免对资源的多重占用。如避免对同一个资源文件的同时写操作。(4)单例模式可以在系统设置全局的访问点,优化和共享资源访问。

    缺点:单例模式一般没有接口,扩展困难。不利于测试。

    使用场景:(1)在整个项目中需要一个共享访问点或共享数据。(2)创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源。(3)需要定义大量的静态常量和静态方法的环境。

    实现:懒汉实现与饿汉实现
    懒汉实现,即实例化在对象首次被访问时进行。可以使用类的私有静态指针变量指向类的唯一实例,并用一个公有的静态方法获取该实例。同时需将默认构造函数声明为private,防止用户调用默认构造函数创建对象。

    //Singleton.h
    class Singleton
    {
    public:
        static Singleton* GetInstance();
    private:
        Singleton() {}
        static Singleton *m_pInstance;
    };
    //Singleton.cpp
    Singleton* Singleton::m_pInstance = NULL;
    Singleton* Singleton::GetInstance()
    {
        if (m_Instance == NULL)
        {
            Lock();
            if (m_Instance == NULL)
            {
                m_Instance = new Singleton();
            }
            UnLock(); 
        }
        return m_pInstance;
    }
    

    该类有以下特征:

    1. 它的构造函数是私有的,这样就不能从别处创建该类的实例。
    2. 它有一个唯一实例的静态指针m_pInstance,且是私有的。
    3. 它有一个公有的函数,可以获取这个唯一的实例,并在需要的时候创建该实例。

    此处进行了两次m_Instance == NULL的判断,是借鉴了Java的单例模式实现时,使用的所谓的“双检锁”机制。因为进行一次加锁和解锁是需要付出对应的代价的,而进行两次判断,就可以避免多次加锁与解锁操作,同时也保证了线程安全。

    上面的实现存在一个问题,就是没有提供删除对象的方法。一个妥善的方法是让这个类自己知道在合适的时候把自己删除。程序在结束的时候,系统会自动析构所有的全局变量。事实上,系统也会析构所有的类的静态成员变量,就像这些静态成员也是全局变量一样。利用这个特征,我们可以在单例类中定义一个这样的静态成员变量,而它的唯一工作就是在析构函数中删除单例类的实例。如下面的代码中的CGarbo类(Garbo意为垃圾工人):

    class Singleton
    {
    public:
        static Singleton* GetInstance() {}
    private:
        Singleton() {};
        static Singleton *m_pInstance;
        //CGarbo类的唯一工作就是在析构函数中删除CSingleton的实例
        class CGarbo
        {
        public:
            ~CGarbo()
            {
                if (Singleton::m_pInstance != NULL)
                    delete Singleton::m_pInstance;
            }
        };
        //定义一个静态成员,在程序结束时,系统会调用它的析构函数
        static CGarbo Garbo;
    };
    

    类CGarbo被定义为Singleton的私有内嵌类,以防该类被在其他地方滥用。程序运行结束时,系统会调用Singleton的静态成员Garbo的析构函数,该析构函数会删除单例的唯一实例。

    饿汉实现方法:在程序开始时就自行创建实例。如果说懒汉实现是“时间换空间”,那么饿汉实现就是“空间换时间”,因为一开始就创建了实例,所以每次用到的之后直接返回就好了。

    //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;
    }
    

    5.2 简单工厂模式

    简单工厂模式的主要特点是需要在工厂类中做判断,从而创造相应的产品。当增加新的产品时,就需要修改工厂类。

     

    例子:有一家生产处理器核的厂家,它只有一个工厂,能够生产两种型号的处理器核。客户需要什么样的处理器核,一定要显式地告诉生产工厂。

    enum CTYPE {COREA, COREB};
    class SingleCore
    {
    public:
        virtual void Show() = 0;
    };
    //单核A
    class SingleCoreA: public SingleCore
    {
    public:
        void Show() { cout<<"SingleCore A"<<endl; }
    };
    //单核B
    class SingleCoreB: public SingleCore
    {
    public:
        void Show() { cout<<"SingleCore B"<<endl; }
    };
    //唯一的工厂,可以生产两种型号的处理器核,在内部判断
    class Factory
    {
    public:
        SingleCore* CreateSingleCore(enum CTYPE ctype)
        {
            if (ctype == COREA) //工厂内部判断
                return new SingleCoreA();  //生产核A
            else if (ctype == COREB)
                return new SingleCoreB();  //生产核B
            else
                return NULL;
        }
    };
    

    这样设计的主要缺点之前也提到过,就是要增加新的核类型时,就需要修改工厂类。这就违反了开放封闭原则:软件实体(类、模块、函数)可以扩展,但是不可修改。于是,工厂方法模式出现了。

    5.3 工厂方法模式

    工厂方法模式是指定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法模式使一个类的实例化延迟到其子类。

     

    例子:这家生产处理器核的厂家赚了不少钱,于是决定再开设一个工厂专门用来生产B型号的单核,而原来的工厂专门用来生产A型号的单核。这时,客户要做的是找好工厂,比如要A型号的核,就找A工厂要;否则找B工厂要,不再需要告诉工厂具体要什么型号的处理器核了。

    class SingleCore
    {
    public:
        virtual void Show() = 0;
    };
    //单核A
    class SingleCoreA: public SingleCore
    {
    public:
        void Show() { cout<<"SingleCore A"<<endl; }
    };
    //单核B
    class SingleCoreB: public SingleCore
    {
    public:
        void Show() { cout<<"SingleCore B"<<endl; }
    };
    class Factory
    {
    public:
        virtual SingleCore* CreateSingleCore() = 0;
    };
    //生产A核的工厂
    class FactoryA: public Factory
    {
    public:
        SingleCoreA* CreateSingleCore() { return new SingleCoreA(); }
    };
    //生产B核的工厂
    class FactoryB: public Factory
    {
    public:
        SingleCoreB* CreateSingleCore() { return new SingleCoreB(); }
    };
    

    工厂方法模式也有缺点,每增加一种产品,就需要增加一个对象的工厂。如果这家公司发展迅速,推出了很多新的处理器核,那么就要开设相应的新工厂。在C++实现中,就是要定义一个个的工厂类。显然,相比简单工厂模式,工厂方法模式需要更多的类定义。

    5.4 抽象工厂模式

    抽象工厂模式的定义为提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。

     

    例子:这家公司的技术不断进步,不仅可以生产单核处理器,也能生产多核处理器。现在简单工厂模式和工厂方法模式都鞭长莫及。这家公司还是开设两个工厂,一个专门用来生产A型号的单核多核处理器,而另一个工厂专门用来生产B型号的单核多核处理器。

    //单核
    class SingleCore
    {
    public:
        virtual void Show() = 0;
    };
    class SingleCoreA: public SingleCore
    {
    public:
        void Show() { cout<<"Single Core A"<<endl; }
    };
    class SingleCoreB :public SingleCore
    {
    public:
        void Show() { cout<<"Single Core B"<<endl; }
    };
    //多核
    class MultiCore
    {
    public:
        virtual void Show() = 0;
    };
    class MultiCoreA : public MultiCore
    {
    public:
        void Show() { cout<<"Multi Core A"<<endl; }
    };
    class MultiCoreB : public MultiCore
    {
    public:
        void Show() { cout<<"Multi Core B"<<endl; }
    };
    //工厂
    class CoreFactory
    {
    public:
        virtual SingleCore* CreateSingleCore() = 0;
        virtual MultiCore* CreateMultiCore() = 0;
    };
    //工厂A,专门用来生产A型号的处理器
    class FactoryA :public CoreFactory
    {
    public:
        SingleCore* CreateSingleCore() { return new SingleCoreA(); }
        MultiCore* CreateMultiCore() { return new MultiCoreA(); }
    };
    //工厂B,专门用来生产B型号的处理器
    class FactoryB : public CoreFactory
    {
    public:
        SingleCore* CreateSingleCore() { return new SingleCoreB(); }
        MultiCore* CreateMultiCore() { return new MultiCoreB(); }
    };
    

    6. 深入理解C++11



    作者:Mr希灵
    链接:https://www.jianshu.com/p/cc1bdada166f
    來源:简书
    简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

    展开全文
  • C++面试知识点总结 宏定义=直接替换 函数指针:返回类型 (*指针)(参数列表),可以作为形参传递 引用和指针的区别 引用是变量的别名,初始化后不可修改,引用不是一个新的变量,指针则是一个变量,需要内存保留...

    C++面试知识点总结(腾讯TEG过经知识点)

    宏定义=直接替换
    函数指针:返回类型 (*指针)(参数列表),可以作为形参传递

    引用和指针的区别

    引用是变量的别名,初始化后不可修改,引用不是一个新的变量,指针则是一个变量,需要内存保留变量的地址

    malloc和new的区别

    都会从堆分配内存,都需要对应的回收函数free和delete来回收内存。malloc和free函数不会调用对象的构造和析构函数,new和delete则会调用这两个函数,因为他们是运算符而不是标准库函数

    堆和栈的区别

    数据结构上,堆是树型结构,有大根和小根两种,可以随意存取,栈则是后进先出,不能任意存取从操作系统角度,栈是由操作系统分配和维护的,不需要显式的内存分配机制就可以存取变量,从高地址向低地址生长;堆的内存空间需要程序员向OS申请,从低地址向高地址生长,需要显式的内存回收机制栈是连续的内存区域,堆则是不连续的内存区域,频繁使用会导致内存碎片

    const的用法

    • 修饰变量:声明变量内容不可更改
    • 修饰指针指向的变量:指针指向的变量不可更改
    • const char *p1,p1指向的变量不可修改
    • char* const p1,p1不可修改
    • 不可修饰引用,没有不可修改的引用,但有常量的引用
    • 修饰成员函数:函数中不可修改类成员属性

    C++ STL 不支持多线程,并发编程存在线程安全问题

    多态,重载和重写

    多态:分为编译时确定和运行时确定,编译时确定的是函数的重载,即同名函数,不同形参;运行时确定则是基类指针调用虚函数时,会根据指针的对象调用相应的函数重载:同名函数不同形参,编译器通过函数名修饰来实现重写:子类重新定义父类虚函数

    引用

    • 是变量的别名,不是变量,没有内存空间
    • 在声明的同时初始化,不能修改绑定的对象
    • 可以作为参数类型,与传指针一样,增加函数间数据通路,在传对象时可以避免调用拷贝函数
    • 可连续使用的运算符的返回类型是引用

    当类成员包含常量,引用时,使用初始化列表初始化

    内存的分配方式

    • 从静态存储区分配,如全局变量,静态变量
    • 在栈上创建,如局部变量
    • 在堆上动态分配

    C++源文件到可执行文件:预处理–编译–汇编–链接

    多线程的线程间资源共享方式和优势

    多线程独享寄存器和栈,共享静态变量,全局变量,堆,文件等资源,线程相对于进程开销小,上下文切换代价小,有方便的通信机制:共享内存,消息传递,有CPU硬件机制支持,超标量,多核;可以改善程序结构,易于代码维护

    STL库

    • STL库六大类型:Algorithm,Container,Adapter,Function object,iterator,allocator
    • 容器序列式容器list:双向链表,有last指向末尾空白节点,插入和删除快,随机访问慢,不需要过多的长距离跳转时较适用,只接受双向迭代器,有内置的sort
    • vector会预先分配足够的连续内存,往vector添加元素时,若空间不足,vector会重新申请新的内存空间,将所有元素移入新的内存空间中,插入和删除慢,为O(n)。vector动态数组只会增加内存,不会删除空间,当空间不够时会自动申请另一片更大的空间,然后把原有数据拷贝过去,并删除原来的空间的数据,但是存储空间不会释放,要等到vector调用析构函数的时候才会释放空间。VS2015 下配对源码 每次扩容50%,原来空间大小9,扩容之后9+9/2=13; Ubuntu 下源码是按每次增长两倍算;clear()只会清空数据,不会释放内存
    • deque(双向队列):是由一段一段的定量连续空间构成,可以向两端发展,因此不论在尾部或头部安插元素都十分迅速。 在中间部分安插元素则比较费时,因为必须移动其它元素。
    • 关联式容器set:一对一,自动排序,升序,不允许重复值multiset:一对多,自动排序,升序,允许重复
    • map(key,value):一对多,红黑树,不允许重复的key值
      *迭代器(iterator) 为所有容器提供一组很小的公共接口,本质是一种智能指针,重载了* -> ++ == != =,分为输入迭代器–只读输出迭代器–只写单向迭代器–双向迭代器
      list set multiset map multimap随机访问迭代器
      vector deque迭代器失效情况:vector元素被删除后指向后续元素的迭代器失效,关联式容器删除当前节点只会使指向当前节点的迭代器失效
    • 算法:sort,swap,search等
    • 适配器:在接口与复用环境不一致时,把需要适配的类的接口转换为用户需要的端口,从而实现复用(函数对象)
    • 仿函数:重载operator()
    • C++allocator类
      创建:allocator alloc;
      分配:alloc.allocate(n),分配n个用于类型type的空间,这些内存空间是未初始化的回收:alloc.deallocate(T,n) T为指向type的指针,回收从T开始的n块构造:alloc.construct(T,args) 在T指向的内存中构造对象析构:alloc.destory§ p为T*类型的指针,用于对p指向的对象执行析构函数
      好处:可以从操作系统预留的堆空间中获取连续内存,避免内存的破碎化,还可以快速分配小对象
    • 红黑树:特殊的查找二叉树,根节点值大于等于左节点值,小于等于右节点值
    • 红黑树的特性:每个节点或者是红色或者是黑色,根节点是黑色每个空的叶子节点是黑色若一个节点的颜色是红色,则子节点颜色为黑色从一个节点到该结点的子孙节点的所有路径包含相同的黑节点每进行一次插入和删除后会进行一次旋转,使其平衡,新插入的节点染红色相较于一般的二叉树,对空间利用更好
    展开全文
  • C/C++面试知识点总结(一)

    万次阅读 多人点赞 2017-08-10 23:44:34
    目录: 一、基础知识 1.C/C++基础 2.STL基础3.数据结构与算法基础 4.计算机网络基础5.操作系统基础6.数据库基础二、深入底层原理1.STL各种容器的底层实现2.各种经典树的原理以及实现 3.计算机网络深入4.操作系统深入5...

    ==> 学习汇总(持续更新)
    ==> 从零搭建后端基础设施系列(一)-- 背景介绍


    目录:

    一、基础知识

        1.C/C++

        2.STL

        3.数据结构与算法

        4.计算机网络

        5.操作系统

        6.数据库

    二、项目经历


        1.纯属个人YY










    一、基础知识

    1.C/C++

    (1).struct大小的确定

    由于内存对齐的原则,在32位机器上,内存是4字节对齐,也就是说,不够4个字节的按 4字节来算。同理,在32位机器上,内存是8字节对齐。

    例子:

    struct test
    {
    	int a;
    	char b;
    	int c;
    }TEST;
    

    其大小为4+4+4 = 12b

    struct test
    {
    	int a;
    	char b;
    	char d;
    	char e;
    	char f;
    	int c;
    }TEST;
    

    其大小为4+(1+1+1+1)+4 = 12b

    struct test
    {
    	int a;
    	char b;
    	int c;
    	char d;
    }TEST;
    

    其大小为4+4+4+4 = 16b

    struct test
    {
    	char a;
    }TEST;
    

    其大小为1b

    struct test
    {
    	char a;
    	char b;
    }TEST;
    

    其大小为2b,说明如果前面的字节+后面的字节不超过内存对齐所需要的字节,是不会用0填充的。实际多少个字节就是多少个字节。

    (2).strlen、strcpy的实现

    int _strlen(const char* str)
    {
    	if (!str || *str == 0) return 0;
    	int len = 0;
    	while (*str++)++len;
    	return len;
    }
    
    char* _strcpy(char* d, const char* s)
    {
    	if (!s || *s == 0) return d ? &(*d = 0) : NULL; //防止d未初始化乱码
    	char* td = d;
    	if (d) while (*d++ = *s++); //防止d为NULL
    	return td;
    }
    
    
    

    (3).memcpy的实现以及如何防止内存重叠造成的错误。

    我没用过未定义的memcpy,我在VC、VS2013、VS2015测试的时候,memcpy已经对内存重叠进行检测了。所以,这里就写一个未检测内存重叠的和检测内存重叠的memcpy。

    void* Memcpy(void* des, const void* src, size_t count)
    {
    	if (!src || count <= 0) return des;
    	char* td = (char*)des;
    	const char* ts = (const char*) src;
    	while (count--) *td++ = *ts++;
    	return des;
    }
    

    例子如下:

    	char* p = new char[6];
    	memcpy(p, "12345", 10);
    	printf("%s\n", p);
    	Memcpy(p + 1, p, 4);
    	printf("%s\n", p);
    

    void* Memmove(void* des,const void* src,size_t count)
    {
    	if (!src || count <= 0) return des;
    	char* td = (char*)des;
    	const char* ts = (char*)src;
    	//如果源地址 + count 小于目的地址,说明,内存无重叠,进行正向拷贝
    	if (ts + count < td)
    	{
    		while (count--) *td++ = *ts++;
    	}
    	//否则有内存重叠,进行逆向拷贝
    	else
    	{
    		char* ttd = td + count - 1;
    		const char* tts = ts + count - 1;
    		while (count--) *ttd-- = *tts--;
    	}
    	return des;
    }
    

    例子如下:

    	char* p = new char[6];
    	memcpy(p, "12345", 10);
    	printf("%s\n", p);
    	Memmove(p + 1, p, 4);
    	printf("%s\n", p);
    

    附上一张内存重叠示意图:

    (4).全局变量、局部变量、静态变量的作用域以及存放的内存区域。

    参考这篇文章http://www.cnblogs.com/bakari/archive/2012/08/05/2623637.html

    (5).static关键字的作用

    a.在函数体内部定义变量,该变量从程序开始到结束只会分配一次内存,当再次进入该函数的时候,其值不变,仍为上次退出时的值
    例子:

    int f()
    {
    	static int i = 0;
    	return ++i;
    }
    
    int main()
    {
    	cout << f() << f() << f() << endl;
    	return 0;
    }
    

    结果是321,为什么是321这也是一个点,下面会有解释。
    b.模块内的static定义的变量和函数不能被外部引用,唯一的办法是通过一个间接变量或者函数来引用才行。
    例子:

    A.cpp
    static char C = 'A';
    char c = C;
    static int F()
    {
    	return 5;
    }
    int ff()
    {
    	return F();
    }
    
    B.cpp
    int main()
    {
    	extern int ff();
    	extern char c;
    	cout << ff() << endl;
    	cout << c << endl;
    	return 0;
    }
    

    c.类中定义的static变量属于整个类,即类成员变量,与对象无关,只会在运行的时候创建一次。

    d.类中定义的static函数属于整个类,及类成员函数,与对象无关,所以不能在静态函数里面使用this指针,这个是对象独有的。

    例子:
    class TEST
    {
    public:
    	static int m_i;
    	static int f() { return m_i; } //静态成员函数只能引用静态成员变量
    };
    int TEST::m_i = 6;
    int main()
    {
    	cout << TEST::f() << endl;
    	return 0;
    }
    

    (6).const关键字的作用
    a.定义普通变量的时候,只能初始化一次,以后不可再修改其值。
    b.定义指针变量时,再类型前,则值不能改,再类型后,则其地址不能改,若两个都有,则两者都不能改。
    例子:

    int a = 2, b = 3;
    const int* p1 = &a;
    *p1 = b; //值不可以修改
    p1 = &b; //地址可以修改
    int* const p2 = &a;
    *p2 = b; //值可以修改
    p2 = &b; //地址不可以修改
    

    c.在函数形参声明中,使用const,可以防止传进来的参数被修改。
    d.在类中使用const定义的函数,在函数内部不能修改成员变量的值,但是可以修改传进来的形参值,但是一般不这么用。

    (7).sizeof是运算符而不是函数,计算变量或者结构体大小的时候可以不加括号,但是计算类型一定要加,所以计算什么都加就对了。

    (8).数组地址问题
    例子:

    int a[] = { 1,2,3,4,5 };
    cout << "a[0]: " << &a[0] << endl;
    cout << "a[4]: " << endl;
    cout << "(a[0] + 1): " <<&a[0] + 1 << endl;
    cout << "(a + 1): " << a + 1 << endl;
    cout << "&a + 1: " << &a + 1 << endl;
    

    可以看出来,a和&a的值一样,概念不一样,a表示数组的首地址,而&a表示的是整个对象的首地址,所以当它+1的时候,就会跳出数组的边界。

    (9).如何将浮点数分解为整数和小数部分

    a.强转为int即为整数部分,然后再减去整数部分即可。

    double Modf(double x, int* y)
    {
    	*y = (int)x;
    	return x - *y;
    }
    

    b.直接使用库函数,double modf(double x,double* y);

    (10).其它一样,只改变一样,不能使函数重载的是

    返回类型。

    (11).C的结构体和C++的结构体的区别

    a.C结构体内部不允许有函数存在,C++允许
    b.内部成员变量权限不同,C的只能是public,C++的可以有三种。
    c.C结构体不可以继承,C++可以继承结构体或者类

    (12).浅拷贝和深拷贝的原理

    其实这两个概念很简单,浅拷贝就是两个对象共享一块内存,其缺点就是当析构一个对象的时候,另一个对象也不存在了,如果再使用它就会发生错误。深拷贝就是完完全全的复制出一个对象,两者在内存上无任何关系。

    (13).不能重载的5个运算符

    .(成员访问运算符)
    ->(成员指针运算符)
    ::(作用域解析运算符)
    ?(条件运算符)
    sizeof运算符

    (14)常见的不能声明为虚函数的有哪些?

    普通函数(非成员函数)
    静态成员函数
    内联成员函数
    构造函数
    友元函数。

    (15)C++的静态多态和动态多态

    所谓静态多态就是运行前确定类型或者函数调用(也就是编译后确定),其采用的是函数重载和泛型(例如,模板。
    所谓动态多态就是运行后确定类型或者函数调用,其采用虚函数实现。

    (16)C++虚函数的原理

    虚函数由虚表管理,这张表存放着所有虚函数的地址,而类的实例对象维护着一个虚表指针,指向这个表。运行的时候,根据new的对象里面的虚表指针,确定调用的函数。如下例子所示:

    class A
    {
    public:
    	virtual void f1() { cout << "A...f1()" << endl; }
    };
    
    class B :public A
    {
    public:
    	void f1() { cout << "B...f1()" << endl; }
    };
    
    int main()
    {
    	
    	A* a = new A;   //因为不是抽象类,所以可以实例化
    	B* b = new B;   //直接用子类自己实例化
     	A* pB = new B;  //用基类指针指向子类
    	
    	return 0;
    }
    

    这就是为什么能在运行时确定调用哪个函数的原因了,当new B返回B的对象后,里面会有一张B类的虚表,然后根据B的虚表调用B的函数。

    (17)C++虚函数占用类的大小

    因为只需要维护一个指向虚表的指针,所以大小为4或8个字节
    静态成员和函数不计入sizeof中。

    (18)new与malloc的区别

    参考http://www.cnblogs.com/QG-whz/p/5140930.html这篇文章。

    (19)C++中有哪几种数据存储区?

    栈、堆、自由存储区、全局/静态存储区、常量存储区

    (20)什么是栈溢出?哪些情况下比较容易出现栈溢出?

    栈溢出泛指系统维护的栈溢出,因数据压不下去了,导致溢出,此时程序会崩溃。

    一般递归深度过大、创建普通数组过大(就是局部变量占用的空间大于栈了就会溢出,new的是堆,不算)。

    (21)“#include”后面跟引号与尖括号的区别?

    引号编译器会搜索当前工作目录下的头文件,尖括号会搜索安装目录下的头文件。

    (22)gcc和g++的区别

    参考这篇文章https://my.oschina.net/alphajay/blog/3989

    (23)类成员函数的重载、覆盖和重写区别

    重载和普通的函数重载一样。
    覆盖则基类的函数要加virtual (这就是多态的实现)
    如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏,子类就重写了这个函数
    如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏,子类就重写了这个函数

    (24)构造函数为什么不能是虚函数

    虚函数存在于虚表中,而虚表要靠虚表指针维护,而只有实例化后的对象才有虚表指针,而实例化对象就必须调用构造函数初始化对象,所以冲突了,结果就是构造函数不能是虚函数。

    (25)printf("%d,%d\n",i++,i++),若i=0,则结果输出什么。

    这里有一个点就是,printf和cout输出的时候都是从右至左读取值,所以结果应该是1,0。

    展开全文
  • C/C++面试知识点总结(二)

    千次阅读 2017-08-12 15:14:48
    目录: 一、基础知识 1.C/C++ 2.STL3.数据结构与算法 4.计算机网络5.操作系统6.数据库二、项目经历 1.纯属个人YY 2.STL基础(1).vector的实现原理vector和数组类似,不同的是vector能动态增长,数组则是静态的,一旦...
  • C-C++面试知识点总结(三)

    千次阅读 2017-08-14 22:53:20
    目录: 一、基础知识 1.C/C++ 2.STL3.数据结构与算法 4.计算机网络5.操作系统6.数据库二、项目经历 1.纯属个人YY 3.数据结构与算法基础 (1).基本的线性表的实现顺序表:用数组实现,因为空间连续,所以叫做顺序表。 ...
  • C/C++面试知识点总结

    2016-08-21 22:42:00
    1.中缀,后缀,前缀表达式: 后缀表达式是波兰逻辑学家卢卡西维奇(Lukasiewicz)发明的一种表示表达式的方法。这种表示方式把运算符写在运算对象的后面,例如,把a+b写成ab+,所以也称为后缀式。...
  • C/C++面试知识点总结之STL篇

    千次阅读 2018-07-26 12:49:55
    目录   一、智能指针的实现 1、auto_ptr 2、shared_ptr 3、unique_ptr 4、weak_ptr 二、vector的原理及实现 ... C++程序设计中堆内存是一个非常频繁的操作,堆内存的申请和释放都由程序员自己管理,虽然自...
  • C\C++面试知识点总结(超全)

    千次阅读 2018-11-08 10:25:15
    不同是: 指针是以地址作为值的变量,而数组名的值是一个特殊的固定地址(可看作是指针常量 ),不能改变其值 联合体union 当多个数据需要共享内存或者多个数据每次只取其一时,可以利用联合体union ...
  • C/C++常见面试知识点总结附面试真题----20210302更新

    万次阅读 多人点赞 2018-09-19 22:47:57
    以下内容部分整理自网络,部分为自己面试的真题。 第一部分:计算机基础 1. C/C++内存有哪几种类型? C中,内存分为5个区:堆(malloc)、栈(如局部变量、函数参数)、程序代码区(存放二进制代码)、全局/静态存储区...
  • 以下是面试中问到过的c++知识点一些总结,除了背诵基本知识点,更重要的多去深入理解里面的原理,不然面试官转个弯深究一下,可能就有点懵了。。。。。。。。祝大家都拿到心仪的offer。文章会持续更新(有些是自己的...
  • C++面试常用知识点总结——基础篇

    万次阅读 多人点赞 2019-07-15 18:13:04
    5、C++基础知识 5.1、C++的三大特性 5.1.1、面向对象特征:封装、继承、多态 5.1.2、析构函数可以为 virtual 型,构造函数则不能,为什么? 5.1.3、如果虚函数是非常有效的,我们是否可以把每个函数都声明为虚函数?...
  • 点击上方“五分钟学算法”,选择“星标”公众号重磅干货,第一时间送达这是一篇 C 语言与 C++面试知识点总结的文章,如果你觉得文章对你有帮助,文末右下角点个再看转发给更多的人。const...
  • 这是一篇 C 语言与 C++面试知识点总结的文章,如果你觉得文章对你有帮助,文末点赞转发给更多的人。来源:公众号:C语言与CPP编程 const 作用 修饰变量,说明该变量不可以被改变; 修饰指针,分为指向常量...
  • C++面试总结

    2019-11-05 23:29:47
    C++面试总结一、C-C++面试知识点总结二、C++面试总结三、C++面试总结之C++语言特性四、C++面试常问问题汇总五、网络总结:六、数据库总结七、C++面试总结之算法八、C++面试总结之操作系统 一、C-C++面试知识点总结 1...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 744
精华内容 297
关键字:

c++面试知识点总结

c++ 订阅