面试会问到的知识点


1.结构体对齐

默认按照最大字节数据对齐。例如:


    struct Info {
        short a;     // 2字节
        int b;       // 4字节
        long c;      //(32位编译器4字节,64位编译器8字节)
        long long d; // 8字节
    }
    //对齐方式按照最长8字节,故而sizeof(Info) == 4*8 == 32

alignas可以指定对齐方式


    struct alignas(4) Info {
        short a;    // 2字节
        char c;     // 1字节
    }
    //对齐方式按照4字节,故而sizeof(Info) == 2*4 == 8

alignas失效:


    struct alignas(2) Info {
        int a;      // 4字节
        char c;     // 1字节
    }
    //指定对齐方式小于int的4字节,对齐方式依然为4字节,故而sizeof(Info) == 2*4 == 8

单字节对齐方式:


    #if defined(__GNUC__) || defined(__GNUG__)
    #define ONEBYTE_ALIGN __attribute__((packed))
    #elif defined(_MSC_VER)
    #define ONEBYTE_ALIGN
    #pragma pack(push,1)    //指定单字节对齐
    #endif

    struct Info {
    uint8_t a;
    uint32_t b;
    uint8_t c;
    } ONEBYTE_ALIGN;

    #if defined(__GNUC__) || defined(__GNUG__)
    #undef ONEBYTE_ALIGN
    #elif defined(_MSC_VER)
    #pragma pack(pop)
    #undef ONEBYTE_ALIGN
    #endif

    std::cout << sizeof(Info) << std::endl;   // 6 1 + 4 + 1
    std::cout << alignof(Info) << std::endl;  // 6

2.指针和引用

  • 指针是一个变量,存储的是一个地址, 引用跟原来的变量实质上是同一个东西,是原变量的别名
  • 指针可以为空(NULL(c++11后用nullptr代替)),引用不能为空
  • 指针可变,引用不可变
  • sizeof得到指针本身大小(32位编译器指针4字节,64位编译器8字节),而得到引用指向的变量大小。
  • 指针参数传递是实参的拷贝形参,修改后不会影响实参指向, 而引用参数传递是实参的地址,修改指向内容的值会影响原来的值。

3.堆和栈的区别

区别
申请方式 程序员代码分配和释放,容易产生内存泄漏 由系统自动分配
申请大小 大小可以灵活调整(默认1G-4G) 大小固定(默认4M)
申请效率 堆由程序员分配,速度慢,且会有碎片 栈由系统分配,速度快,不会有碎片
扩展方向 堆向高地址扩展,是不连续的内存区域 栈顶和栈底是之前预设好的,栈是向栈底扩展
分配方式 动态分配 有静态分配和动态分配,静态分配由编译器完成(如局部变量分配),动态分配由alloca函数分配,但栈的动态分配的资源由编译器进行释放,无需程序员实现。
使用方法 堆由C/C++函数库提供,机制很复杂 栈是其系统提供的数据结构,计算机在底层对栈提供支持,分配专门 寄存器存放栈地址,栈操作有专门指令

就像我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。

就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。

4.new delete和malloc free

  • malloc和free是标准库函数,支持覆盖;new和delete是运算符,支持重载。

  • new在c++里封装了malloc,分配足够的内存空间并调用构造函数,返回对象指针,delete封装了free,不仅释放内存,而且还会调用析构函数。
  • free回收的内存会首先被ptmalloc使用双链表保存起来,当用户下一次申请内存的时候,会尝试从这些内存中寻找合适的返回。避免了频繁的系统调用,ptmalloc也会尝试对小块内存进行合并,避免过多的内存碎片。

5.常量指针和指针常量

  • 指针常量是一个指针,读成常量的指针,指向一个只读变量的指针

    int const *p;   // 指针常量
    const int *q;   // 指针常量
    
  • 常量指针是一个不能改变指向的指针

    int *const p;   // 常量指针

6.struct和class的区别

相同:

  • 两者都拥有成员函数、公有和私有部分
  • struct可以实现class所有工作

不同:

  • 不指定权限时,成员函数和成员变量在class中默认是privatestruct中默认是public
  • class默认是private继承, 而struct默认是public继承

c和c++的struct

区别 c c++
类型 用户自定义数据类型(UDT) 抽象数据类型(ADT),支持成员函数的定义
权限 没有权限的设置 有权限设置
声明对象 必须在结构标记前加上struct,才能做结构类型名来声明定义对象(除:typedef struct class{}; 可以直接作为结构体类型名使用

7.const,static

(1)与类无关时

区别 const static
初始化 定义时必须初始化且无法再改变 定义时不指定值,默认为0
可见性 和其他变量一样 全局变量和函数用了static只能在该文件所在的编译模块使用,否则全局可见
函数内 不可改变 只初始化一次,函数执行完毕也不回收内存

其中static的可见性解释也就是:使用include包含文件后也不能使用被包含文件中static声明的变量和函数

(2)与类有关时

区别 const static
变量关联 和一般成员变量相同 只与类关联,不与类的对象关联
变量初始化 不能在类定义外部初始化,只能通过构造函数初始化列表进行初始化 不能在类声明中初始化,必须在类定义体外部初始化
成员函数 const对象不可以调用非const成员函数;非const对象都可以调用;不可以改变非mutable(用该关键字声明的变量可以在const成员函数中被修改)数据的值 不具有this指针,无法访问类对象的非static成员变量和非static成员函数;不能被声明为const、虚函数和volatile;可以被非static成员函数任意访问

const修饰变量是也与static有一样的隐藏作用。只能在该文件中使用,其他文件不可以引用声明使用。 因此在头文件中声明const变量是没问题的,因为即使被多个文件包含,链接性都是内部的,不会出现符号冲突

8.顶层const和底层const

  • 顶层constconst本身是一个常量,无法修改。(例如常量指针)
  • 底层constconst修饰的变量指向的对象是常量,所指的对象。(例如指针常量)

9.final,override,virtual


    class Father{
    public:
        Father(){  //构造函数一般不能为虚函数,无法用virtual修饰
            cout<<"Construct Father!"<<endl;
        }
        virtual void func1(){
            cout<<"func1 of Father!"<<endl;
        }

        void func2(){
            cout<<"func2 of Father!"<<endl;
        }

        virtual void func3() = 0;     //纯虚函数,父类就变成了虚基类,子类必须重写这个函数,如果不重写就会报错

        virtual ~Father(){    //析构函数,一般都使用virtual修饰
            cout<<"Destruct Father!"<<endl;
        }
    };

    class Child : public Father{
    public:
        Child(){  //构造函数一般不能为虚函数,无法用virtual修饰
            cout<<"Construct Child!"<<endl;
        }
        void func1() override {     //继承自父类,不报错
            cout<<"func1 of Child!"<<endl;
        }

        //void func0() override;      //父类没有,会报错

        void func2(){
            cout<<"func2 of Child!"<<endl;
        }

        void func3() override {}              //重写父类的纯虚函数

        ~Child(){
            cout<<"Destruct Child!"<<endl;
        }
    };

    int main(){
        Father* f = new Child();
        f->func1();
        f->func2();
        delete f;
        return 0;
    }

其中,虚基类无法定义对象,但可以定义为子类的指针。

上述main函数中的输出如下:


    Construct Father!   //在构造子类对象时,先调用父类构造函数
    Construct Child!    //再构造子类构造函数
    func1 of Child!     //父类的func1因为用了virtual被隐藏
    func2 of Father!    //父类的func2不是虚函数,未被隐藏
    Destruct Child!     //在析构指针时,先调用子类析构函数
    Destruct Father!    //再调用父类析构函数

注意,如果父类析构函数不是虚函数,析构父类指针时无法调用子类的析构函数。

10.拷贝初始化和直接初始化


    class A{
    public:
        int num1;
        int num2;
    public:
        A(int a=0, int b=0):num1(a),num2(b){};      //-直接构造函数
        A(const A& a){};    //拷贝初始化函数
        //重载 = 号操作符函数
        A& operator=(const A& a){
            num1 = a.num1 + 1;
            num2 = a.num2 + 1;
            return *this;
        };
    };
    int main(){
        A a(1,1);   //直接初始化,调用构造函数
        A a1 = a; //拷贝初始化操作,调用拷贝构造函数
        A b;
        b = a;//赋值操作,对象a中,num1 = 1,num2 = 1;对象b中,num1 = 2,num2 = 2
        return 0;
    }