一个程序在内存中分为五个段:
- 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
12class 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
4class Base{
public:
virtual void func1() override;
}原理:
virtual table里存的成员函数地址,在继承重写的时候,会替换其相应位置重写后的函数地址,但是因为是同样的offset,所以在运行时会找到正确的函数地址。- 使用:
构造函数不能为虚函数,而析构函数可以且常常是虚函数。 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