一、基础知识
面向对象的三大特性,详细介绍
√
封装、继承、多态
如何解决多重继承
√
例1
#include<iostream>
using namespace std;
class A{
public:
void f();
};
class B{
public:
void f();
void g();
};
class C : public A,public B{
public:
void g();
void h();
};
int main(){
C c1;
c1.f();//具有二义性
c1.g();//无二义性(同名覆盖)
return 0;
}
具有二义性的错误显示(看下图的error部分)
解决方案
1、类名限定
c1.A::f();
c1.B::f();
2、同名覆盖
在class C中声明一个同名函数,该函数根据需要内部调用A的f()或者B的f()
例2
#include<iostream>
using namespace std;
class B{
public:
int b;
};
class B1:public B{
private:
int b1;
};
class B2:public B{
private:
int b2;
};
class C : public B1,public B2{
public:
int f();
private:
int d;
};
int main(){
C c1;
c1.B1::b;//可以
c1.B::b;//不行基类存在二义性(报错见下图)
return 0;
}
例2这种情况的解决方案(虚基类)
虚基类:用于有共同基类的场合
声明
virtual修饰说明基类
主要用来解决多继承时,可能对同一基类继承,继承多次从而产生的二义性
为最远的派生类提供唯一的基类成员,而不重复产生多次拷贝
注意:需要再第一次继承的时候就要讲共同的基类设计为虚基类
具体改进代码见下面
#include<iostream>
using namespace std;
class B{
public:
int b;
};
class B1: virtual public B{//改进处
private:
int b1;
};
class B2: virtual public B{//改进处
private:
int b2;
};
class C : public B1,public B2{
public:
int f();
private:
int d;
};
int main(){
C c1;
c1.B::b;//正确
return 0;
}
补充
虚基类及其派生类构造函数
建立对象时所指定的类称为最(远)派生类
1、虚基类的成员是由派生类的构造函数 通过 调用虚基类的构造函数进行初始化的
2、在整个继承结构中,直接或者间接继承虚基类的所有派生类,都必须在构造函数的成员初始化表中给出对虚基类的构造函数的调用。
如果未列出,则表示调用该虚基类的缺省构造函数(default constructor就是默认构造函数)
3、在建立对象时,只有最派生类的构造函数调用虚基类的构造函数,该派生类的其他基类对虚基类的构造函数的调用被忽略
上诉描述过程见下面代码
#include<iostream>
using namespace std;
class B{
public:
B(int n){
nv = n;
cout<<"I am B,my num is "<<nv<<endl;
}
void fun(){
cout<<"Member of B"<<endl;
}
private:
int nv;
};
class B1: virtual public B{
public:
B1(int x,int y):B(y){
nv1 = x;
cout<<"I am B1,my num is"<<nv1<<endl;
}
private:
int nv1;
};
class B2: virtual public B{
public:
B2(int x,int y):B(y){
nv2 = x;
cout<<"I am B2,my num is"<<nv2<<endl;
}
private:
int nv2;
};
class C : public B1,public B2{
public:
C(int x,int y,int z,int k):B(x),B1(y,y),B2(z,y){
nvc = k;
}
private:
int nvc;
};
int main(){
C c1(1,2,3,4);
c1.fun();
return 0;
}
断点位置
经过调试
单步调试
然后就不能调试了(暂时还不知道原因????)
经过后面测试,Linux下的clion编译器可以,但是Windows下的clion编译器不行
若是采用单步跳过
直接跳到了55行代码,然后【单步调试】又调用基类中的函数
然后就不能调试了(暂时还不知道原因????)
经过后面测试,Linux下的clion编译器可以,但是Windows下的clion编译器不行
结论,写C++代码还是应该在Linux下比较好
volatile关键字
√
背景【使用原因】
编译器优化常用的方法有:将内存变量缓存到寄存器。
由于访问寄存器要比访问内存单元快的多,编译器在存取变量时,为提高存取速度,编译器优化有时会先把变量读取到一个寄存器中;
以后再取变量值时就直接从寄存器中取值。【但在很多情况下会读取到脏数据,严重影响程序的运行效果。】
volatile意思是“易变的”,可理解为【“直接存取原始内存地址”】。
volatile提醒【编译器】它后面所【定义的变量随时都有可能改变】,因此编译后的程序每次需要存储或读取这个变量的时候,
告诉编译器对该变量不做优化,都会直接从变量内存地址中读取数据,从而可以提供对特殊地址的稳定访问
如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,
将出现不一致的现象。
使用场合
1、中断服务程序中修改的供其它程序检测的变量,需要加volatile
2、多任务环境下各任务间共享的标志,应该加volatile
3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;
一个参数可以即是const又是volatile的吗?
可以,一个例子是只读状态寄存器,是volatile是因为它可能被意想不到的被改变,是const告诉程序不应该试图去修改他
一个指针可以是volatile 吗?
可以,当一个中服务子程序修改一个指向buffer的指针时。
【注意】频繁地使用volatile很可能会增加代码尺寸和降低性能,因此要合理的使用volatile。
static关键字的作用
√修饰类的成员变量
1、隐藏。(static函数,static变量均可)
2、保持变量内容的持久
3、默认初始化为0(static变量)
4、C++中的类成员声明static
修饰全局变量:表明一个全局变量只对定义在同一文件中的函数可见。
修饰局部变量:表明该变量的值不会因为函数终止而丢失。
修饰全局/局部函数:表明该函数只在同一文件中调用
修饰类的成员变量、成员函数
extern关键字的作用
√
1) extern修饰变量的声明
如果文件a.c需要引用b.c中变量int v,就可以在a.c中声明extern int v,然后就可以引用变量v。
2) extern修饰函数的声明
如果文件a.c需要引用b.c中的函数,比如在b.c中原型是int fun(int mu),那么就可以在a.c中声明extern int fun(int mu),
然后就能使用fun来做任何事情。就像变量的声明一样,extern int fun(int mu)可以放在a.c中任何地方,而不一定非要放在a.c的文件作用域的范围中。
3) extern修饰符可用于指示C或者C++函数的调用规范
暂时先用两段代码来说明问题
int max(int x,int y);
int main()
{
int result;
/*外部变量声明*/
extern int g_X;/
extern int g_Y;
result = max(g_X,g_Y);
printf("the max value is %d\n",result);
return 0;
}
/*定义两个全局变量*/
int g_X = 10;
int g_Y = 20;
int max(int x, int y)
{
return (x>y ? x : y);
}
qwb-test.cpp中
#include<iostream>
using namespace std;
/***main.c****/
#include <stdio.h>
/*定义两个全局变量*/
int g_X=10;
int g_Y=20;
int max();
int main()
{
int result;
result = max();
cout<<"----1----"<<endl;
printf("the max value is %d\n",result);
return 0;
}
main.cpp中
extern int g_X ;
extern int g_Y ;
int max()
{
return (g_X > g_Y ? g_X : g_Y);
}
全局变量和局部变量的区别
√
1. 作用域不同:全局变量的作用域为整个程序,而局部变量的作用域为当前函数或循环等
【全局变量的作用域是从当前行开始往下】
2. 内存存储方式不同:全局变量存储在全局数据区中,局部变量存储在栈区
3. 生命期不同:全局变量的生命期和主程序一样,随程序的销毁而销毁,局部变量在函数内部或循环内部,随函数的退出或循环退出就不存在了
4. 使用方式不同:全局变量在声明后程序的各个部分都可以用到,但是局部变量只能在局部使用。
【函数内部会优先使用局部变量再使用全局变量】
重写、重载与隐藏(重定义)的区别
√
类如何实现只能动态分配和静态分配
引用
1、引用和指针的区别
√
1、【指针是一个实体】、需要分配内存空间。
引用知识变量的别名,不需要分配内存空间
2、指针在定义的时候不一定要初始化,并且指向的空间可变
【引用在定义的时候必须进行初始化】,并且不能改变
注:引用的值不能为NULL
3、【有多级指针】,但是没有多级引用,只能有一级引用
4、指针和引用的自增运算结果不一样。
指针指向下一个空间,引用是引用的变量值加1
5、sizeof引用得到的是所指向的变量(对象)的大小
sizeof指针所得到的是指针本身的大小
注:32位中,指针的大小都为4
6、引用访问一个变量是直接访问,
而指针访问一个变量是间接访问
7、使用指针前最好做【类型检查】,防止野指针的出现
8、【引用底层】是通过指针实现的
9、参数传递时不同
传指针的实质是传值,传递的值是指针的地址;
传引用的实质是传地址,传递的是变量的地址
2、从汇编层去解释一下引用
√
3、C++中的指针参数传递和引用参数传递
√(精简点)
1、指针参数传递本质上是【值传递】,他所传递的是一个【值地址】。
值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,会在【栈中开辟内存空间】来存放由主调函数传递进来的【实参值】,
从而形成实参的一个副本。
值传递的特点是,被调函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值(形参指针变了,实参指针不会变)
2、引用参数传递的过程中,被调函数的形式参数也作为局部变量在栈中开辟了空间,但是这时存放的是由主调函数放进来的【实参变量的地址】。
被调函数对形参的任何操作都被处理成【间接寻址】,即通过【栈中存放的地址访问主调函数中的实参变量】(根据别名找到主调函数中的本体)。
因此被调函数对形参的任何操作都会影响主调函数中的实参变量
3、引用传递和指针传递是不同的,虽然他们都是在【被调函数栈空间】上的一个【局部变量】,但是任何对于引用参数的处理都会通过一个间接
寻址的方式操作到主调函数中的相关变量。而对于指针传递的参数,若改变被调函数中的指针地址,它将应用不到主调函数的相关变量。
若想通过指针参数传递来改变主调函数中的相关变量(地址),那就得使用指向指针的指针或者指针引用。
4、从编译的角度,程序在编译时分别将指针和引用添加到符号表上,符号表中记录的是变量名及变量所对应地址。指针变量在符号表上对应的
地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值(与实参名字不同,地址相同)。符号表生成之后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能改
多态
1、介绍一下C++里面的多态
√
多态:同一种操作作用于不同的对象,有不同的解释,产生不同的结果。
(1)静态多态(重载,模板)
是在【编译的时候】就【确定调用函数的类型】
(2)动态多态(覆盖,虚函数的实现)
在【运行的时候】,才确定调用的是哪个函数,动态绑定。(运行基类指针指向派生类的对象,并调用派生类的函数)
基类中和子类中必须有同一个方法,且基类中的方法是抽象。这个在子类对象指针赋值给基类对象指针的时候,
基类对象指针调用方法的时候才调用的是子类中的方法
虚函数实现原理:虚函数表和虚函数指针
纯虚函数:virtual int fun()=0;
函数的运行版本由实参决定,在运行时选择函数的版本,所以动态绑定又称为运行时绑定。当编译器遇到一个模板定义时,它并不生成代码。
只有当实例化出模板的一个特定版本时,编译器才会生成代码
2、用C语言实现C++的继承和多态(腾讯20)
√
#include<iostream>
#include<vector>
using namespace std;
//C++中的继承与多态
struct A
{
virtual void fun() //C++中的多态:通过虚函数实现
{
cout<<"A:fun()"<<endl;
}
int a;
};
struct B:public A //C++中的继承:B类公有继承A类
{
virtual void fun() //C++中的多态:通过虚函数实现(子类的关键字virtual可加可不加)
{
cout<<"B:fun()"<<endl;
}
int b;
};
//C语言模拟C++的继承与多态
typedef void (*FUN)(); //定义一个函数指针来实现对成员函数的继承
struct _A //父类
{
FUN _fun; //由于C语言中结构体不能包含函数,故只能用函数指针在外面实现
int _a;
};
struct _B //子类
{
_A _a_; //在子类中定义一个基类的对象即可实现对父类的继承(这里实现继承)
int _b;
};
void _fA() //父类的同名函数
{
printf("_A:_fun()\n");
}
void _fB() //子类的同名函数
{
printf("_B:_fun()\n");
}
int main()
{
//测试C++中的继承与多态
A a; //定义一个父类对象a
B b; //定义一个子类对象b
A* p1 = &a; //定义一个父类指针指向父类的对象
p1->fun(); //调用父类的同名函数
p1 = &b; //让父类指针指向子类的对象
p1->fun(); //调用子类的同名函数
//C语言模拟继承与多态的测试
_A _a; //定义一个父类对象_a
_B _b; //定义一个子类对象_b
_a._fun = _fA; //父类的对象调用父类的同名函数(这里不是赋值操作嘛)
cout<<"----"<<endl;
_b._a_._fun = _fB; //子类的对象调用子类的同名函数
cout<<"----"<<endl;
_A* p2 = &_a; //定义一个父类指针指向父类的对象
p2->_fun(); //调用父类的同名函数
p2 = (_A*)&_b; //让父类指针指向子类的对象,由于类型不匹配所以要进行强转
p2->_fun(); //调用子类的同名函数
return 0;
}
3、C++的多态是如何实现的
√
C++的多态性:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。
如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数
虚函数
1、说一下虚析构函数
√
构造函数
1、作用:赋初值,初始化对象的数据成员,由编译器帮我们调用。
2、特点:①函数名和类名一样。②没有返回值和声明类型。③支持有参/无参。④可以重载。
3、调用时机:在类的对象创建时刻,编译器帮我们调用构造函数。
析构函数:
1、作用:用于释放资源。
2、特点:①和类名一样,不过得在前面加上~。
②无参数(和构造函数的区别),无返回值和声明类型。
③因为无参数,无返回值,所以不可以重载。
④尽量不要自己调用析构函数,但是在某些需要的时候再调用。
3、调用时机:快退出函数的时候,编译器帮我们调用。
注:析构函数不被调用的话就会造成内存泄漏
虚函数:在类的成员函数前面加virtual关键字的函数;
一般把虚函数定义在public区,方便在主函数中调用,
如果一个类有一个虚函数,则该类就有一个虚函数列表,所有该类的对象都共享这个虚函数表;(QT调试过程中显示的是vptr)
如果一个类有一个或者一个以上的虚函数,则该类有且只有一张虚函数表,每个类都只有一个虚函数表,该类的所有对象都共享这张虚函数表。
子类的虚函数表中子类的虚函数覆盖父类的虚函数的情况,当子类将父类的虚函数override时,就覆盖了父类的虚函数;
满足override的条件:函数名相同,函数的返回值相同,形参列表相同;
纯虚函数:形式为virtual void fun1() = 0;
纯虚函数不需要实现,原因是不会被调用到;
抽象基类:至少有一个纯虚函数的类;
抽象基类不能产生该类的对象,但可以有该类的指针或引用;在子类中必须将父类的纯虚函数实现,不然该子类也是抽象基类;
虚析构函数:只有当一个类被用来作为基类的时候,才把析构函数写成虚函数
当一个类有子类时,该类的析构函数必须是虚函数,原因:会有资源释放不完全的情况;
2、【虚函数表】的作用
观点1
1、实现多态,【父类对象指针】指向父类对象调用的是父类的虚函数,指向子类调用的是子类的虚函数
2、同一个类的多个对象的虚函数表是同一个,所以这样就可以节省空间,一个类自己的虚函数和继承的虚函数还有重写父类的虚函数
都会存在自己的虚函数表
3、基类A有虚函数,子类B重写虚函数,有几个虚函数表
4、虚继承
5、C++的虚函数底层是如何实现的
纯虚析构函数
6、纯虚函数的作用
√
虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来【访问】基类和派生类中的同名函数。
纯虚函数是一种特殊的虚函数,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。
这就是纯虚函数的作用。
7、那些函数不能被定义虚函数
√
1、友元函数,它不是类的成员函数
2、全局函数
3、静态成员函数,它没有this指针
4、构造函数,拷贝构造函数以及赋值运算符重载(可以但是一般不建议作为虚函数)
8、为什么调用普通函数比调用虚函数的效率高
√
因为普通函数是静态联编的,而调用虚函数是动态联编的
联编的作用:程序调用函数,编译器决定使用哪个可执行代码块
静态联编:在编译的时候就确定了函数的地址,然后call就调用了
动态联编:首先需要取到对象的首地址,然后再解引用取到虚函数表的首地址,再加上偏移量才能知道要调的虚函数,然后call调用
明显动态联编要比静态联编做的操作多,所以浪费时间
9、为什么要把基类的析构函数定义为虚函数
√
在用基类操作派生类的时候,为了防止执行基类的析构函数,不执行派生类的析构函数。
(这样删除只能删除基类对象,而不能删除子类对象,形成了删除一半形象,会造成内存泄露)详情见下面代码
#include<iostream>
using namespace std;
class Base
{
public:
Base() {};
~Base()
{
cout << "delete Base" << endl;
};
};
class Derived : public Base
{
public:
Derived() {};
~Derived()
{
cout << "delete Derived" << endl;
};
};
int main()
{
//操作1
Base* p1 = new Derived;
delete p1;
system("pause");
}
因为这里子类的析构函数重写了父类的析构函数,虽然子类和父类的析构函数名不一样,
但是编译器对析构函数做了特殊的处理,在内部子类和父类的析构函数名是一样的
。
所以如果不把父类的析构函数定义成虚函数,就不构成多态,由于父类的析构函数隐藏了子类 的析构函数,所以只能调到父类的析构函数。
但是若把父类的析构函数定义成虚函数,那么调用时就会直接调用子类的析构函数,
由于子类析构先要去析构父类,在析构子类,这样就把子类和继承的父类都析构了
#include<iostream>
using namespace std;
class Base
{
public:
Base() {};
virtual ~Base()
{
cout << "delete Base" << endl;
};
};
class Derived : public Base
{
public:
Derived() {};
virtual ~Derived()
{
cout << "delete Derived" << endl;
};
};
int main()
{
//操作1
Base* p1 = new Derived;
delete p1;
system("pause");
}
构造函数与析构函数
1、一个类既有拷贝构造函数,也有移动构造函数,什么时候调用拷贝,什么时候调用移动
2、C++有哪些构造函数
3、拷贝构造函数
4、构造函数可以是虚函数吗
√面经
5、析构函数可以是虚函数吗,如果不是虚函数,会有什么问题
√面经
6、析构函数为什么需要虚函数
√面经
7、C++构造函数能抛异常吗?析构呢?
√面经
8、构造函数和析构函数可以调用虚函数嘛
√
const
1、define和const的区别
√
1、从【定义常量】的角度:
define定义的只是个【常数】,【不带类型】,Const定义的常量是变量,带类型
2、从【起作用】的角度:
define是在【编译的预处理阶段】起作用,而const是在【编译,运行】的时候起作用
3、从【起作用的方式】的角度:
define只是简单的字符串替换,没有类型检查,而const有对应的数据类型,是要进行判断的,可以避免一些低级错误
正因为define只是简单的字符串替换会导致边界效应,具体举例可以参考下面代码
4、从【空间占用】的角度:
#define PI 3.14 //预处理后,占用代码段空间
const float PI = 3.14 //本质上还是一个float,占用数据段空间
5、从【代码调试的方便程度】的角度:
const常量可以进行调试,define是不能进行调试的,因为在预编译阶段就已经替换掉了
6、从【是否可以用再定义】的角度:
const不足的地方,是与生俱来的,const不能重定义,而#define可以通过#undef 取消某个符号的定义,再重新定义。
7、从【某些特殊功能】而言:
define可以用来防止头文件重复引用,而const不能,可以参看下面代码
8、从某些复杂功能的实现的实现角度:
使用define会使得代码看起来非常简单,而const无法实现该功能
例如,MFC在实现六大核心机制中,大量使用了define
1、MFC程序的初始化
2、运行时类型识别(RTTI)
3、动态创建
4、永久保存
5、消息映射
6、消息传递
define\const\inline的区别与联系
√
本质:define只是字符串替换,inline由编译器控制,具体的:
内联函数在编译时展开,而宏(define)是由预处理器对宏进行展开
内联函数会检查参数类型,宏定义不检查函数参数 ,所以内联函数更安全。
宏不是函数,而inline函数是函数
宏在定义时要小心处理宏参数,(一般情况是把参数用括弧括起来)。
2、两个名字一样的函数,一个参数是int &, 一个是const int &. 现在实参传递一个常量const int i ,调用哪个函数。
3、const和static的区别
√
区别:
1、初始化
2、一个是类(static),一个是对象(const)
参数
4、形参和实参的区别
√
1、形参变量只有在【被调用时才分配内存单元】,在调用结束时,立刻释放所分配的内存单元。因此,【形参只有在函数内部有效】。
函数调用结束返回主调函数后则不能再使用该形参变量
2、实参可以是常量、变量、表达式、函数等,无论实参是何种类型的量,在进行函数调用时,它们都必须是确定的值,以便把这些值传递给
形参。因此,【应预先用赋值,输入等方法使实参获得确定值,会产生一个临时变量】
3、【实参和形参在数量上、类型上和顺序上应严格一致】,否则会发生“类型不匹配”的错误
4、【函数调用中发生的数据传送是单向的】。(只能把实参的值传送给形参,而不能把形参的值反向地传送实参。因此,在函数调用过程中,
形参的值发生改变,而实参中的值不会变化)
5、当形参和实参不是指针类型时,在该函数运行时,形参和实参是不同的变量,他们在内存中位于不同的位置,形参将实参的内容复制一份,
在该函数运行结束的时候,形参被释放,而实参内容不会改变
1)值传递:有一个形参向函数所属的栈拷贝数据的过程,如果值【传递的对象是类对象】或是大的结构体对象,将耗费一定的时间和空间。(传值)
2)指针传递:同样有一个形参向函数所属的栈拷贝数据的过程,但拷贝的数据是一个【固定为4字节的地址】。(传值,传递的是地址值)
3)引用传递:同样有上述的数据拷贝过程,但其是针对地址的,相当于【为该数据所在的地址起了一个别名】。(传地址)
4)效率上讲,【指针传递和引用传递比值传递效率高】。一般主张使用引用传递,代码逻辑上更加紧凑、清晰。
智能指针
智能指针的概念,有哪几种智能指针
√
什么是智能指针:
从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,
这使得智能指针实质是一个对象,行为表现的却像一个指针。
智能指针的种类: auto_ptr【C++11之后就不再用了】, shared_ptr, weak_ptr, unique_ptr
为什么要用智能指针?
c++内存管理,当写一个new语句时,一般就会立即把delete语句直接也写了,
但是不能避免程序还未执行到delete时就跳转了或者在函数中没有执行到最后的delete语句就返回了,
如果我们【不在每一个可能跳转】或者返回的语句前释放资源,就会造成【内存泄露】。使用智能指针可以很大程度上的避免这个问题,
因为【智能指针就是一个类】,当超出了类的作用域时,类会自动调用析构函数,析构函数会自动释放资源。
手写一个智能指针
√
字节对齐的原则及作用
√
原则:
从0位置开始存储;
变量存储的起始位置是该变量大小的整数倍;
结构体总的大小是其最大元素的整数倍,不足的后面要补齐;
结构体中包含结构体,从结构体中最大元素的整数倍开始存;
如果加入pragma pack(n) ,取n和变量自身大小较小的一个。
计算某个结构体有多少字节
√和字节对齐挂钩
侯老师的基础课程里面有
----------------------------------------------------
二、STL
容器
(准备几个容器的底层源码)
vector为什么要使用2倍扩容
Map和unordered_map的区别和应用场景
Map 的迭代器,什么情况下会失效
√
20亿个整数,中间只有一个数字出现了两次,如何快速找到它?如果用map,需要多少字节空间,map<int,bool> 20亿*5=100亿字节 多少kb、mb、gb、tb
map插入元素后,迭代器会失效吗? vector呢,vector两种情况都要考虑。
√
迭代器
iterator什么时候失效
√
使用iterator遍历时,进行插入或者删除操作,怎么保证iterator不失效
----------------------------------------------------
三、C++ 11
1、C++ 11新特性有哪些(腾讯20+字节20)
√
包括auto和decltype
2、初始化√
3、基于范围的for和静态断言√
4、noexcept修饰符、nullptr、原生字符串字面值√
5、强类型枚举√
6、常量表达式√(感觉不是重点)
7、自定义后缀√(感觉不是重点)
8、类的改进√
包括继承构造、委托构造、继承控制:final和override
9、defaulted 和 deleted 函数√
10、模板的改进√
1、右尖括号>改进
2、模板的别名
3、函数模板的默认模板参数
1、可变参数模板
2、参数包的展开
3、可变参数模板类
12、右值引用
13、移动语义
14、std::move和std::forward
15、智能指针
16、闭包
17、std::function
18、std::bind
19、lambda表达式
20、线程(可以扩展并发)
21、互斥量、原子操作
右值引用
√见1
移动语义
√见1
std::forward的作用和应用场景(引用折叠)
√见1
介绍C++ 11的移动
√见1
move的底层是如何实现的
√见1
c++11 的线程库
√
----------------------------------------------------
四、内存管理
malloc和new的底层源码要看
程序内存分为几个区
√
内存,指的是计算机的随机存储器(RAM),程序都是在这里运行的
栈区(stack):由编译器自动分配释放,存储函数的参数值,局部变量值等,其操作方法类似于数据结构中的栈
堆区(heap):一般由程序员申请(并指明大小)和释放,与数据结构中的堆没有任何关系,分配方式类似于链表
全局/静态区(static):全局变量和静态变量是存储在一起的,在程序编译时分配
文字常量区:存储常量字符串
程序代码区:存储函数体(类的成员函数、全局函数)的二进制代码【C++构造类的时候在这里】
C++内存管理方式(RAII、智能指针)
√
RAII:Resource Acquisition Is Initialization
是C++语言的一种管理资源、避免泄漏的惯用方法。利用的就是C++构造的对象最终会被销毁的原则。RAII的做法是使用一个对象,
在其构造时获取对应的资源,在对象生命期内控制对资源的访问,使之始终保持有效,最后在对象析构的时候,释放构造时获取的资源。
为什么要用RALL????
RAII是用来管理资源、避免资源泄漏的方法。
那么资源是如何定义的?在计算机系统中,资源是数量有限且对系统正常运行具有一定作用的元素。比如:网络套接字、互斥锁、文件句柄和内存等等,
它们属于系统资源。由于系统的资源是有限的,所以,我们在编程使用系统资源时,都必须遵循一个步骤:
1.申请资源;
2.使用资源;
3.释放资源。
第一步和第二步缺一不可,因为资源必须要申请才能使用的,使用完成以后,必须要释放,如果不释放的话,就会造成资源泄漏。
【如果总是申请资源而不释放资源,最终会导致资源全部被占用而没有资源可用的场景】
如何使用RALL????
当我们在一个函数内部使用局部变量,当退出了这个局部变量的作用域时,这个变量也就被销毁了;当这个变量是类对象时,这个时候,就会自动调用这个类的析构函数,而这一切都是自动发生的,不要程序员显示的去调用完成。
RAII就是这样去完成的。由于系统的资源不具有自动释放的功能,而C++中的类具有自动调用析构函数的功能。如果把资源用类进行封装起来,对资源操作都封装在类的内部,在析构函数中进行释放资源。当定义的局部变量的生命结束时,它的析构函数就会自动的被调用,如此,就不用程序员显式的去调用释放资源的操作了。
手写实现RALL
RALL总结:
RAII的本质内容是用【对象代表资源】,把管理资源的任务转化为管理对象的任务,将资源的获取和释放与对象的构造和析构对应起来,
从而确保在对象的生存期内资源始终有效,对象销毁时资源一定会被释放。说白了,就是拥有了对象,就拥有了资源,对象在,资源则在。
总讲
一、内存对齐
为什么要内存对齐?
1、减少使用的内存;2、提升数据读取的效率
二、OS和C内存管理机制
三、malloc的实现
四、new和delete的实现原理
C++/C内存分配方式,堆与栈的区别
√
1.分配和管理方式不同
堆是动态分配的,其空间的【分配】和【释放】都由程序员控制。
栈由编译器自动管理。栈有两种分配方式:静态分配和动态分配。静态分配由编译器完成,比如局部变量的分配。动态分配由_alloca()函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放,无须手工控制。
2.产生碎片不同
对堆来说,频繁的new/delete或者malloc/free可能会造成内存空间的不连续,造成大量的碎片,使程序效率降低。
对栈而言,则不存在碎片问题,因为栈是先进后出的队列,永远不可能有一个内存块从栈中间弹出。
3.增长方向不同
堆由低地址向高地址增长。
栈由高地址向低地址增长。
堆是怎么存内存的????
有哪些内存泄漏?如何判断内存泄漏?如何定位内存泄漏?
√用VS试一下定位内存泄漏
有哪些内存泄露????
1. 在类的构造函数和析构函数中没有匹配的调用new和delete函数
2. 在释放对象数组时在delete中没有使用方括号
3. 指向对象的指针数组不等同于对象数组
4. 缺少拷贝构造函数
5. 缺少重载赋值运算符
6. 没有将基类的析构函数定义为虚函数【没有实现动态绑定】
如何判断内存泄漏???
1、使用工具
如果是Windows,考虑下Visual Leak Detector,这个对泄露点信息定位相当不错;【得下载VS,真麻烦】
如果是Linux,考虑下Valgrind,检测内存使用以及线程方面的Bug功能非常强大。
2、对于因为错误调用系统API造成的内存泄漏还是只能通过查log和review代码来定位,
C++动态内存
√
new/delete与malloc/free的区别
√
1、new/delete是C++的操作运算符,它们调用的分别为赋值运算符重载operator new()和operator delete();
malloc/free是【C语言标准库函数】,函数原型为:
void* malloc(size_t size)//参数代表字节个数
void free(void* pointer)//参数代表内存地址
2、new可以重载,它调用operator new / operator delete,它们可以被重载,在标准库里它有8个重载版本,
malloc不能重载
3、下面用代码来说明二者的一部分区别
用malloc分别开辟了1个和4个整型大小的空间和并free释放它们;
new同上
void func()
{
//开辟一个空间
int* p1=(int*)malloc(sizeof(int));
if(p1==NULL)//判断是否为空
{
exit(1);
}
free(p1);
//开辟多个空间
int*p2=(int*)malloc(sizeof(int)*4);
if(p2==NULL)
{
exit(1);
}
free(p2);
}
void func()
{
//开辟一个空间
int* p1=new int(1);
delete p1;
//开辟多个空间
int*p2=new int[4];
delete []p2;
}
由上诉两个例子对比可知
new的变量是数据类型,malloc的变量是字节大小
①malloc开辟【空间类型大小】需手动计算,new是由编译器自己计算;
②malloc返回类型为void*,必须强制类型转换对应类型指针,new则直接返回对应类型指针(指定对象的指针)(malloc的函数原型见1)
③malloc开辟内存时返回内存地址要检查判空,因为它可能开辟失败会返回NULL;
new则不用判断,因为内存分配失败时,它会抛出异常bac_alloc,可以使用异常机制(注意这个异常bac_alloc,不会就不说)
④无论释放几个空间大小,free只传递指针,多个对象时delete需加[]
4、malloc/free为函数只是开辟空间并释放,
new/delete则不仅会开辟空间,并调用构造函数和析构函数进行初始化和清理,
如下为new/delete、new[]/delete[]实现机制:
而new[]/delete[]则为:
在上诉过程中,在开辟大小会【多开辟四个字节】,用于【存放对象的个数】,在返回地址时则会向后偏移4个字节,
而在delete时则会查看内存上对象个数,从而根据个数count确定调用几次析构函数,从而完全清理所有对象占用内存。
【注意】对于内置类型若new[]但用delete释放时,没有影响,
但若是自定义类型如类时,若释放使用 delete时,这时则会只调用一次析构函数,只析构了一个对象,剩下的对象都没有被清理。
5、由前面可知:new/delete底层是基于malloc/free来实现的,而malloc/free不能基于new/delete实现;
6、对于malloc分配内存后,若在使用过程中内存分配不够或太多,这时可以使用realloc函数对其进行扩充或缩小,
但是new分配好的内存不能这样被直观简单的改变;
7、对于new/delete若内存分配失败,用户可以指定处理函数或重新制定分配器(new_handler(可以在此处进行扩展)),
malloc/free用户是不可以处理的。
8、对于new/delete与malloc/free申请内存位置说明,
malloc我们知道它是在堆上分配内存的,
但new其实不能说是在堆上,C++中,对new申请内存位置有一个抽象概念,它为自由存储区,它可以在堆上,也可以在静态存储区上分配,
这主要取决于operator new实现细节,取决与它在哪里为对象分配空间。
----------------------------------------------------
五、其他
多线程、原子都是C++11里面的内容
模板
原子类
√
原子性
voliate的作用,在多线程中,跟原子类的区别
√
思路:答一下voliate的语义——可见性,讲一下为啥不能保证原子性(特别是多核场景下),再说一下voliate的使用场景
volatile关键字的作用:可见性
volatile关键字的使用场景
volatile为什么不能保证原子性???
多线程的同步方式和场景
√
线程同步的常见方法:互斥锁,条件变量,读写锁,信号量
无锁编程(同步方式中提到,原理是cas)
√
CAS:Compare And Swap即比较并交换。执行函数:CAS(V,E,N)
手写C++单例模式
(20字节)√
C++怎么编译的
√
IO多路复用
-
常见的IO模型
- 阻塞IO模型:老李去火车站买票,排队三天买到了票。耗时:在火车等待三天
- 非阻塞IO模型:老李买票,没有在火车站等,而是每隔12个小时去问一次,直到买到票。耗时:往返车站6次,路上6次。
- IO复用模型:
- select/poll:老李买票,委托黄牛,然后每个6个小时打电话询问黄牛,黄牛三天买到票,然后老李去火车站交钱领票。耗时:打电话
- epoll:老李去火车站买票,委托黄牛。黄牛买到了通知老李去领。耗时:无需打电话
- 信号驱动IO模型:老李去买票,给售票员留下电话,有票了,售票员电话通知老李去取,耗时:无需打电话
- 异步IO模型:老李去买票,给售票员留下电话,有票后,售票员送货到家。
-
IO多路复用
- 一个线程,通过记录I/O流的状态来同时管理多个I/O,可以提高服务器的吞吐能力。
- I/O多路复用的实现:用户将想要监视的文件描述符添加到select/poll/epoll函数中,由内核监视,函数阻塞,一旦描述符就绪(读就绪或写就绪),或者超时(设置timeout),函数就会返回,然后该进程可以进行相应的读写操作。
- 这里的“复用”指的是复用同一个线程
-
select poll epoll 三者的区别
- selelct:将文件描述符放入一个集合中,调用select时,将这个集合从用户空间拷贝到内核空间中(缺点1:每次都要复制,开销大),由内核根据就绪状态修改该集合的内容,(缺点2)集合大小有限制,32位机默认时1024;采用水平触发机制,select函数返回后,需要通过,遍历这个集合,找到就绪的文件描述符(缺点3:轮询的方式效率太低),当文件描述符的数量增加时,效率会线性下降。 时间复杂度 O(n)
- poll:和select几乎没什么区别,区别在于文件描述符的储存方式不同,poll采用链表的方式储存,没有最大储存数量的限制 时间复杂度O(n)
- epoll:通过内核和用户空间共享内存,避免了不断复制的问题,支持的同时连接上限很高;文件描述符就绪时,采用了回调机制,避免了轮询(回调函数将就绪的描述符添加到一个链表中,执行epoll_wait,返回这个链表),支持水平触发和边缘触发,采用了边缘触发机制,只有活跃的描述符才会触发回调函数。时间复杂度 O(1)
区别主要在
- 一个线程/进程所能打开的最大连接数
- 文件描述符传递方式(是否复制)
- 水平触发or边缘触发
- 查询就绪的描述符时的效率(是否轮询)
-
什么时候用epoll,什么时间用select/poll
当连接数较多并且有很多的不活跃连接时,epoll的效率比其他二者高很多,但是当连接数比较少并且都十分不活跃的情况下,由于epoll需要很多回调,因此性能可能低于其他二者。
-
大端 小端
大端小端讲的是数据在内存中的存放顺序。
- 大端:大端存储格式就是数据的高字节存放在低地址,低字节数据存放在高地址
- 小端:小端存储格式就是数据的高字节放在高地址,低字节数据存放在低地址