0%

C/C++

一个程序在内存中分为五个段:

  • text: 保存源代码
  • data: 初始化的全局变量和静态变量
  • bss: 未初始化的全局变量和静态变量 或 初始化为0的
  • stack: 程序运行时栈,保存局部变量、可能存在的argument以及return的地址
  • heap: 用来保存手动分配出来的内存,由用户管理。内存块由一条链表来维护

volatile的作用

  • 不可优化性:当一个变量即使是不用,也不让编译器去优化它。
  • 下一条语句不会直接使用上一条语句对应的volatile变量的寄存器内容,而是重新从内存中读取。即不让编译器去随意把它给优化掉。

全局变量和局部变量的区别:

  • 作用范围不同
  • 在存储器中存储位置不同,全局是在数据段中,局部变量一般在堆/栈段
  • 操作系统通过内存分配的位置来知道全局变量在全局数据段,在程序运行的时候加载
  • 编译器通过词法分析来知道它是全局变量还是局部

static关键字:

  • static成员函数:只能调用static成员变量
  • static成员:所有instance共享,不能在类的内部初始号,在类的实现文件中初始化

const关键字:

  • const成员函数:不改变数据成员的成员函数都要在后面加 const,而对于改变数据成员的成员函数不能加 const。
  • const成员:不能在类的内部初始化,只能通过构造函数初始化

多态性:

  • 静多态:template和函数重载
    template在编译期决定,模板的实例化就在编译期对其进行替换。
    重载是参数表,C++编译时会将函数的参数类型加在函数symbol的后面,因此参数不一样的函数也不一样
  • 动多态
    虚函数,是动态绑定,运行时决定

虚函数:

  • 纯虚函数:virtual xxx = 0;在派生类中重写成员函数的实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class A{
    public:
    virtual void func1() = 0; // pure virtual function
    virtual void func2(); // normal virtual function
    }

    class B: public A {...};
    class C: public A {...};

    A* a = new B(); a->func1(); // B::func1
    A* b = new C(); b->func1(); // C::func1
    a->A::func1(); // A::func1
  • 普通虚函数: 在基类中定义一个缺省实现,表示继承的是基类成员函数接口和缺省实现,由派生类选择是否重写该函数。
    但是这种方式是很危险的!因为可能忘记重写
    解决方案:最好是基类中实现缺省行为,但只有在派生类要求时才提供该缺省行为。
    使用C++11的关键字override,可以显式的在派生类中声明,哪些成员函数需要被重写,如果没被重写,则编译器会报错。

    1
    2
    3
    4
    class Base{
    public:
    virtual void func1() override;
    }
  • 原理:
    virtual table里存的成员函数地址,在继承重写的时候,会替换其相应位置重写后的函数地址,但是因为是同样的offset,所以在运行时会找到正确的函数地址。
    pic1

  • 使用:
    构造函数不能为虚函数,而析构函数可以且常常是虚函数。 C++对象在三个地方构建:(1)函数堆栈 stack;(2)自由存储区,或称之为heap;(3)静态存储区。
    无论在那里构建,其过程都是两步:首先,分配一块内存;其次,调用构造函数。好,问题来了,如果构造函数是虚函数,那么就需要通过vtable 来调用,但此时面对一块 raw memeory,到哪里去找 vtable 呢?毕竟,vtable 是在构造函数中才初始化的啊,而不是在其之前。因此构造函数不能为虚函数。
    析构函数可以是虚函数,且常常如此。因为此时 vtable 已经初始化了;况且我们通常通过基类的指针来销毁对象,如果析构函数不为虚的话,就不能正确识别对象类型,从而不能正确销毁对象。
    每个析构函数(不加 virtual)只负责清除自己的成员。但可能有基类指针,指向的确是派生类成员的情况。(这是很正常的)。那么当析构一个指向派生类成员的基类指针时,程序就不知道怎么办了。所以要保证运行适当的析构函数,基类中的析构函数必须为虚析构。

继承:

  • 虚继承:
    虚继承
    在多重继承中,如果发生了如:类B继承类A,类C继承类A,类D同时继承了类B和类C。最终在类D中就有了两份类A的成员,这在程序中是不能容忍的。当然解决这个问题的方法就是利用虚继承
    在派生时将关键字virtual加在相应相应继承方式前,就可防止在D类中同时出现两份A类成员。只会将虚基类的构造函数调用一次,忽略虚基类的其他派生类(class B,class C)对虚继承的构造函数的调用,从而保证了虚基类的数据成员不会被多次初始化
  • 内存分布:
    内存分布

    指针&&引用:

  • 指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元,即指针是一个实体;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名
  • 两者都是地址的概念;指针指向一块内存,它的内容是所指内存的地址;引用是某块内存的别名。
  • 可以有const指针,但是没有const引用;
  • 指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的)
  • 指针的值可以为空,但是引用的值不能为NULL,并且引用在定义的时候必须初始化;
  • 指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了,从一而终。
  • ”sizeof引用”得到的是所指向的变量(对象)的大小,而”sizeof指针”得到的是指针本身的大小;
  • 指针和引用的自增(++)运算意义不一样

函数返回局部变量的引用

  • 当函数返回引用类型时,没有复制返回值,相反,返回的是对象本身。
  • 千万不要返回局部对象的引用!千万不要返回指向局部对象的指针!当函数执行完毕时,将释放分配给局部对象的存储空间。此时对局部对象的引用会指向不确定的内存!返回指向局部对象的指针也是一样的,当函数结束时,局部对象被释放,返回的指针就变成了不再存在对象的悬垂指针。
  • 返回引用时,要求在函数的参数中,包含有以引用方式或指针方式存在的,需要被返回的参数。

内存泄漏

  • 不再会被使用的对象的内存不能被回收,就是内存泄露
  • 在C++中,所有被分配了内存的对象,不再使用后,都必须程序员手动释放他们。所以,每个类,都会含有一个析构函数,作用就是完成清理工作,如果我们忘记了某些对象的释放,就会造成内存泄露。

构造/析构函数的执行顺序

构造的时候先构造基类后子类,析构的时候相反先析构子类后析构基类

内联成员函数(编译展开),静态成员函数(编译确定,不能重写)

重写(override)和重载(overload):

  • 重写是覆盖,是派生类重写基类的相应方法。要求函数名、参数相同
  • 重载是同样的函数名,但是不同的参数,有参数表的存在。比如类的多个构造函数

重载的过程

  • C++为了支持函数重载,是编译器的函数符号改名机制,符号名是在对应的函数名上改编的。
  • 简单来说就是函数的symbol是根据函数名、函数参数表(类型、数量)相关的
  • 例如 func(…)参数为一个int,就加一个i,为float就加一个f,char就加一个c,int&则加Ri,依此类推
  • 所以可以容易理解,用C++去找由C编译的函数,则会找不到定义。所以应该加一个extern C