- 编译原理和内存
- 基础知识
- 面向对象与类
- C++11新特性
- 面试题
- 虚函数可以被声明为static吗?
- 构造函数不为虚函数?
- Sleep和yield
- 析构函数中调用delete this会如何
- 只在堆创建或只在栈创建类?
- 设计一个类计算对象的个数
- 通过指针或引用实现多态,而不可以通过对象呢?
- 全局数组和局部数组初始化变量?
- 如何阻止类被继承呢?
- 请你说说C语言参数压栈顺序?
- 内存池的实现
- operator new和new?
- 请介绍线程池,简述构造原理?可以手写一个简单的线程池么?
- explicit关键词有什么作用?
- 连等?||和&&?strlen和sizeof?
- 知道断言和异常机制么?
- 类中成员默认内联?如果类成员是引用(reference)?const和constexpr?
- array、vector、数组的区别?sizeof不一样?
- auto?
- delete []
- 大端小端顺序?
- null和nullptr?
- 与零值的比较(float、double等)
- 模板编程
思维导图:复习的时候看一遍就好了!
编译原理和内存
编译链接
1.预处理器
C/C++的预处理器其实就是一个词法(而不是语法)预处理器,其主要完成文本替换、宏展开以及删除注释等,完成这些操作之后,将会获得真正地“源代码”。常见的include语句即是一个预处理器命名,在预处理器中它将所有的头文件包含进来。
2.编译器
在这一步骤得到汇编程序语言,值得注意的是所有的编译器输出的汇编语言都是同一种语法。
注:内联函数就是在这一环节“膨胀”进源码的,它的作用即在于:不是在调用时发生控制转移,而是在编译时将函数体嵌入在每一个调用处,适用于功能简单,规模较小又使用频繁的函数。递归函数无法内联处理,内联函数不能有循环体,switch语句,不能进行异常接口声明。仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
3.汇编器
将.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件.o中(把汇编语言翻译成机器语言的过程)。
4.链接器
链接过程主要包括了地址空间分配、符号决议和重定向,三个步骤。
链接的主要内容就是将各个模块之间相互引用的部分正确的衔接起来。它的工作就是把一些指令对其他符号地址的引用加以修正。
符号决议:有时候也被叫做符号绑定、名称绑定、名称决议、或者地址绑定,其实就是指用符号来去标识一个地址。比如说 int a = 6;这样一句代码,用a来标识一个4字节大小的空间,空间里边存放的内容就是6.
重定位:重新计算各个目标的地址过程叫做重定位。
进一步了解可以看:https://www.cnblogs.com/pengyingh/articles/5369061.html
头/源文件、声明、定义
先看一个问题:C++编译模式是怎样的?简单来说就是“事先声明”、“分别编译”、“事后链接”的编译模式。
程序源代码无非就是变量和函数的总和,多个分开的文件分别编译自己的部分,在最后阶段进行互相链接,从而得到了一个可执行文件。
这是如何实现的呢?这就得提到声明和定义的区别。
简单地说,“定义”就是将一个符号(函数和变量都是符号)完整描述:类型、参数、实现细节等;
而“声明”则简单很多:它只是“说明”有这样一个变量或者函数,但其内部是什么情况,请等链接的时候我们再去找找。
从这就可以看出:一个函数或者变量可以被声明很多次,但是只能被定义一次!(这其实也就是头文件的好处,你想用这个函数,那就把我这个头文件给包含吧,最后链接的时候从其他目标文件去找就行了)
到这又不得不提一点了,头文件到底是个什么东西?头文件其实跟源文件没什么区别,都是C++的源代码。
因为在编译的时候头文件中的内容会被直接copy进cpp文件,但是有的时候头文件会互相包含,这可能就会造成在一份源码中copy两次同样的头文件,这也是为什么需要ifndef endif 或者#pragma once的用处。
因为预处理时include是将包含的文件中的代码插入到当前代码里,文件是不能包含自己的,如果相互包含编译器只能取舍一下,否则是不可能正常通过的。如果遇到这种情况就需要对头文件进行重构,修改其包含关系。所以头文件里最好只放变量和函数的声明,而不能放它们的定义(如果多个函数都include定义,那么就会出错了)。
其实.cpp和.h文件名称没有任何直接关系,很多编译器都可以接受其他扩展名。在link的时候,需要在makefile里面说明需要连接哪个.o或.obj文件(在这里是b.cpp生成的.o或.obj文件),此时,连接器会去这个.o或.obj文件中找在b.cpp中实现的函数,再把他们build到makefile中指定的那个可以执行文件中。
动态/静态链接
1、静态链接库与动态链接库都是共享代码的方式。
如果采用静态链接库,lib 中的指令代码都全部被直接包含在最终生成的 EXE 文件中了,所以程序运行的时候不再需要其它的库文件。
动态链接(Dynamic Linking),把链接这个过程推迟到了运行时再进行,在可执行文件装载时或运行时,由操作系统的装载程序加载库。但是若使用 DLL,该 DLL 不必被包含在最终 EXE 文件中,EXE 文件执行时可以“动态”地引用和卸载这个与 EXE 独立的 DLL 文件。
静态链接库和动态链接库的另外一个区别在于静态链接库中不能再包含其他的动态链接库或者静态库,而在动态链接库中还可以再包含其他的动态或静态链接库。
2、动态库就是在运行时需要调用其中的函数时,根据函数映射表找到该函数的入口然后调入堆栈执行。
如果在当前工程中有多处对dll文件中同一个函数的调用,那么执行时,这个函数只会留下一份拷贝。
但是如果有多处对lib文件中同一个函数的调用,那么执行时,该函数将在当前程序的执行空间里留下多份拷贝,而且是一处调用就产生一份拷贝。
静态链接的优缺点
静态链接的缺点很明显,一是浪费空间,因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,如多个程序中都调用了printf()函数,则这多个程序中都含有printf.o,所以同一个目标文件都在内存存在多个副本;另一方面就是更新比较困难,因为每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。
为什么会出现动态链接
动态链接出现的原因就是为了解决静态链接中提到的两个问题,一方面是空间浪费,另外一方面是更新困难。下面介绍一下如何解决这两个问题。
动态链接的优缺点
动态链接的优点显而易见,就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多分,副本,而是这多个程序在执行时共享同一份副本;另一个优点是,更新也比较方便,更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。但是动态链接也是有缺点的,因为把链接推迟到了程序运行时,所以每次执行程序时都需要进行链接,所以性能会有一定损失。
内存分段
.txt(代码段) .GVAR(全局已初始化变量,data) .bss(全局未初始化变量) heap(堆) stack(栈)。内存分段见下图:
其中,data用于存放初始化过的全局变量。bss段被用来存放那些没有初始化或者初始化为0的全局变量。若全局变量值为0,为了优化编译器会将它放在.bss段中。bss段只占运行时的内存空间而不占文件空间。在程序运行的整个周期内,.bss段的数据一直存在。
内存布局
1 类如何布局?
兼容C的struct:按照声明顺序对齐;
C++类的实例大小完全取决于其自身及基类的成员变量,成员函数不影响。
具体解释:数据成员每一个类对象不同空间,但是函数是公用一份
2 成员变量如何访问?
每一个对象其实都有一个this指针,通过this来切换不同的对象;
3 成员函数如何访问?
函数地址是所有类共享的。
4 所谓的“调整块”(adjuster thunk)是怎么回事?
为了进行内存的对齐。
5 使用如下机制时,开销如何:
单继承、多重继承、虚继承
虚函数调用
强制转换到基类,或者强制转换到虚基类
异常处理
在计算大小(sizeof)的时候:
除了虚函数表指针、成员变量(非静态和常量),其他的常量,静态,成员函数都不占类内存大小的。
参考以下博客,重要性依次降低: 这里有一道关于内存布局的题,贼妙! https://blog.csdn.net/laozhong110/article/details/6402574 之所以拷贝到&pca,是将该指针自身的地址更改为CA的对象的地址; https://blog.twofei.com/496/ https://blog.csdn.net/fairyroad/article/details/6376620 https://www.cnblogs.com/noryes/p/6434245.html https://www.cnblogs.com/QG-whz/p/4909359.html https://www.cnblogs.com/mysky007/p/11042294.html
基础知识
关键字
Extern有什么作用?
主要有两个功能:
一是,调用其他文件中外部定义的变量或函数(防止重定义),告诉编译器需要去外部寻找对应的变量和函数定义;
二是,在C++中调用C语言代码,则编译器在编译fun这个函数名时按C的规则去翻译相应的函数名而不是C++的;
Extern “c” {} 说明C++编译跟C编译有什么区别呢?作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。
函数被C++编译后在符号库中的名字与C语言的不同。例如,假设某个函数的原型为:void foo( int x, int y );
该函数被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字。
不同的编译器可能生成的名字不同,但是都采用了相同的机制,生成的新名字称为“mangled name”
_foo_int_int这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机制来实现函数重载的。
例如,在C++中,函数void foo( int x, int y )与void foo( int x, float y )编译生成的符号是不相同的,后者为_foo_int_float。同样地,C++中的变量除支持局部变量外,还支持类成员变量和全局变量。
用户所编写程序的类成员变量可能与全局变量同名,我们以”.”来区分。而本质上,编译器在进行编译时,与函数的处理相似,也为类中的变量取了一个独一无二的名字,这个名字与用户程序中同名的全局变量名字不同。
如果在多个源文件包含同一个名字的全局变量的定义,就会引起重定义。 因此要想在多个文件共用一个全局变量,我们只需在一个头文件里 声明(注意是声明)这个变量:extern int i; 然后在其中一个源文件(.c,只能是一个)里面定义这个变量 int i =1; 注意不能用 i =1;要用 int i =1; 最后在要使用这个变量的源文件(即其他源文件)里#include 头文件即可。
Static的特性
static修饰的变量有一个重要特点那就是:该变量限制在该源文件内;
如果将static定义在头文件中,多个程序同时包含该头文件,那么在编译的时候就会产生多个同名变量,且互不影响,所可以是可以但不建议。如果想实现其他文件访问该变量,可以使用extern的方式。
变量的定义一般不放在头文件里,但可以把声明放在头文件里,供其他文件引用这个变量。
比如:在test.c文件中定义变量static int global = 0;
可以在头文件test.h中声明这个变量为:extern int global;
要使用这个变量的其他文件,只要包含test.h就可以了。
参考链接: https://bbs.csdn.net/topics/80055779 https://www.cnblogs.com/lulululu/p/3693865.html
全局/局部静态变量
静态变量都存放于全局数据区,都在程序退出时才销毁,两者唯一的区别就在于作用域不同,全局变量全局可见,而局部静态变量仅在局部区域可见。
作用域和生命周期是从两个不同的角度:时间和空间对变量进行描述。 作用域,即是该变量可被引用的范围;生命周期即是该变量从初始化到销毁的时间。 一个程序的内存分为代码区、全局数据区、堆区、栈区,不同的内存区域,对应不同的生命周期。
数组、指针与引用
指针与数组
int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
int (*p)[4] = a;//定义一个指向a的指针变量p
括号中的*表明p是一个指针,它指向一个数组,数组的类型为int [4],这正是a所包含的每个一维数组的类型。
[]的优先级高于,( )是必须要加的,如果赤裸裸地写作int p[4],那么应该理解为int *(p[4]),p 就成了一个指针数组,而不是二维数组指针。
p指向数组 a 的开头,也即第 0 行;p+1前进一行,指向第 1 行。
PS:如何获取函数多个返回值? 法一、需要的变量在函数外定义,利用引用传值在函数内修改; 法二、将需要的值打包为数组(相同数据类型)、或结构体(不同的类型),返回指针;
函数指针与指针函数
函数指针是指向函数的指针,确切的说,是指向特定类型函数的指针(函数与函数指针类型要匹配)。
函数指针用来保存函数首地址,即可以通过该指针访问函数。函数指针相当于取别名。函数指针可以指向一类函数,而不是一个函数,即可以重新赋值。
简单使用:
进阶使用:利用typedef来声明变量
typedef 返回类型(*新类型)(参数表)
指针函数是返回值为指针的函数,所以我们在main()中调用它时可以用一个同类型的指针来接收。
指针函数可以用来解决众多问题,如返回多个值的问题。(见”函数返回多个值的方法”那篇文章)(见后续)
指针函数比函数指针更经常用到 这博客的例三很经典,涉及到了指针,数组指针,指针函数,二维数组的赋值,函数返回多个值,数组指针的自增与指针自增的区别。
引用与指针的区别
从概念上来讲:
指针,其实就是一个存储变量地址的变量;
引用,变量的别名,内存块的别名,编译器一般将其实现为const指针;即是常量指针
从实现特点来讲:
指针可以更改,引用不能更改;指针可以指向数组,而引用无法绑定数组;(存疑)
引用必须声明初始化;
又一个问题,指针传递和引用传递?
指针传递的本质是值传递,将指针存储的地址作为形参,若改变其中指针的地址,将不会影响外部;
而引用传递传入的虽然也有局部变量,但其中存储的是实参的地址,即对该局部变量的操作都间接寻址到了原本的实参;
又一个问题,指针的引用和指向引用的指针?
不存在指向引用的指针,对引用(引用不是对象)取地址,其实就是对引用的对象取地址;
下面这些语法是错误的:
int &p=v;
int&*q=&p;
int &rnum = &num1;//这是不允许的无法从“int ”转换为“int *&”
而指针的引用就简单了:
int v=0;
int p=&v;
**int &q=p;**
悬空(迷途)指针和野指针?
野指针:指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针。野指针? 好办!那就是养成初始化的习惯;
悬空指针:悬空指针是指针最初指向的内存已经被释放了的一种指针。避免悬空指针的方法:使用智能指针。
几种指针
指向函数的指针(函数指针) :
int( p )(int ,int);//p是指向函数的指针变量。指向的函数返回值类型为int型,参数类型为(int ,int)
使用形式为 :
p=function;
result = (p)(x, y);
返回指针值的函数(指针函数):类型名函数名(参数表列)如int a(int x,int y);
指向指针的指针,如char (p)或char**p ,注:指向指针的指针一般用它来指向二维数组的;
智能指针
参考:https://blog.csdn.net/xu1105775448/article/details/80627371
有哪些智能指针?
C++11版本之后提供,包含在头文件
为什么需要智能指针?
便于资源的管理,进行堆内存的分配和回收,防止造成内存泄露(如忘记delete或者catch异常之后忘记释放内存)。
简单来看,智能指针使用基于RAII(资源获取即初始化)思想对普通指针进行了封装,使其实际是一个对象,然表现地像一个指针;
shared_ptr
多个shared_ptr可以指向同一个指针,采用引用计数来实现指针的释放:使用一次计数+1,析构一次计数-1;
每个shared_ptr对象在内部指向两个内存位置:
1、指向对象的指针。
2、用于控制引用计数数据的指针。
//创建
std::shared_ptr<int> p1(new int());
//创建空的
std::shared_ptr<int> p1 = std::make_shared<int>();
//检查计数
p1.use_count();
//减小计数1
p1.reset();
//重定位ptr
p1.reset(new int(34));
//shared_ptr充当普通指针,我们可以将*和->与 shared_ptr 对象一起使用,也可以像其他 shared_ptr 对象一样进行比较;
//但缺少 ++, – – 和 [] 运算符
//创建多个同一对象的指针
int *num = new int(23);
std::shared_ptr<int> p1(num);
std::shared_ptr<int> p2(p1); // 正确使用方法
std::shared_ptr<int> p3(num); // 不推荐
//假如使用原始指针num创建了p1,又同样方法创建了p3,当p1超出作用域时会调用delete释放num内存,此时num成了悬空指针,当p3超出作用域再次delete的时候就可能会出错。
shared_ptr 默认的构造函数中使用的是delete来删除关联的指针,所以构造的时候也必须使用new出来的堆空间的指针。
因此,不要使用栈指针,如下:
int x = 12;
std::shared_ptr<int> ptr(&x);
return 0;
智能指针里的计数器维护的是一个指针,指向的实际内存在堆上,不是栈上的
如何实现共享所有权的?
1、当新的 shared_ptr 对象与指针关联时,则在其构造函数中,将与此指针关联的引用计数增加1。
2、当任何 shared_ptr 对象超出作用域时,则在其析构函数中,它将关联指针的引用计数减1。如果引用计数变为0,则表示没有其他 shared_ptr 对象与此内存关联,在这种情况下,它使用delete函数删除该内存。
另外值得注意的是,shared_ptr关联的对象必须是new出来的堆空间的指针,不能够是栈空间的指针,否则会报错。
注2:https://blog.csdn.net/shaosunrise/article/details/85228823 注3:https://blog.csdn.net/qq_31904421/article/details/107708025
weak_ptr
https://www.jb51.net/article/188294.htm
weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象,将一个weak_ptr绑定到一个shared_ptr不会改变引用计数,一旦最后一个指向对象的shared_ptr被销毁(它是一个若引用),对象就会被释放,即使有weak_ptr指向对象,对象还是会被释放。
循环引用解决方法:
弱指针用于专门解决shared_ptr循环引用的问题,weak_ptr不会修改引用计数,即其存在与否并不影响对象的引用计数器。
int main() {
shared_ptr<int> sp(new int(5));
cout << "创建前sp的引用计数:" << sp.use_count() << endl; // use_count = 1
weak_ptr<int> wp(sp);
cout << "创建后sp的引用计数:" << sp.use_count() << endl; // use_count = 1
}
循环引用就是:两个对象互相使用一个shared_ptr成员变量指向对方,这样当离开各自作用域后内存也得不到释放从而引起内存泄露。
如果对象存在,lock()函数返回一个指向共享对象的shared_ptr,否则返回一个空shared_ptr。
如何解决呢?
在其中一个类里使用weak_ptr,这是弱引用。
弱引用并不对对象的内存进行管理,在功能上类似于普通指针,然而一个比较大的区别是,弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存。
其内部实现原理?
不过就是指向了shareedptr而已,但不会增加sharedptr的引用次数。
实现代码:https://www.jb51.net/article/188294.htm
unique_ptr
https://blog.csdn.net/shaosunrise/article/details/85158249
独享被管理对象指针所有权的智能指针。其实现是通过包装一个原始指针,来负责其生命周期,在该unique_ptr对象被销毁时,调用析构函数释放关联指针的内存。
//创建
std::unique_ptr<Task> taskPtr(new Task(22));
//获取对象
Task *p1 = taskPtr.get();
// 释放关联指针的所有权
Task * ptr = taskPtr5.release();
// 编译错误 : unique_ptr 不能复制
std::unique_ptr<Task> taskPtr3 = taskPtr2; // Compile error
// 编译错误 : unique_ptr 不能复制
taskPtr = taskPtr2; //compile error
// 通过原始指针创建 taskPtr2
std::unique_ptr<Task> taskPtr2(new Task(55));
// 把taskPtr2中关联指针的所有权转移给taskPtr4
std::unique_ptr<Task> taskPtr4 = std::move(taskPtr2);
// 现在taskPtr2关联的指针为空
if(taskPtr2 == nullptr)
std::cout<<"taskPtr2 is empty"<<std::endl;
// taskPtr2关联指针的所有权现在转移到了taskPtr4中
if(taskPtr4 != nullptr)
std::cout<<"taskPtr4 is not empty"<<std::endl;
// 会输出55
std::cout<< taskPtr4->mId << std::endl;
简单来说就是对普通指针做了一层封装,使其具有自动析构的功能。unique_ptr无法复制,只能移动move(所有权),因为独享所有权,如果能复制就违背了这一前提(将拷贝赋值构造函数给禁用了)。
auto_ptr
已经被C++11废弃的智能指针,其简单的封装普通指针,使其可以在析构时自动释放资源;
跟unique_ptr有何不同?
前者支持拷贝和赋值,且完成操作之后所有权就被转移了。
前者不能作为容器对象,后者可以利用move()函数来实现作为容器对象。
参见:https://blog.csdn.net/weixin_40081916/article/details/79377564
auto_ptr< string> p1 (new string (“I reigned lonely as a cloud.”));
auto_ptr
p2 = p1; //auto_ptr不会报错.
此时不会报错,p2剥夺了p1的所有权(p1=NULL了!),但是当程序运行时访问p1将会报错。所(问题)以auto_ptr的缺点是:存在潜在的内存崩溃问题!
手写智能指针实现 https://www.cnblogs.com/xiehongfeng100/p/4645555.html
++it,it++的源码
1) 前置返回一个引用,后置返回一个对象
// ++i实现代码为:
int& operator++() {
*this += 1;
return *this;
}
2) 前置不会产生临时对象,后置必须产生临时对象,临时对象会导致效率降低
//i++实现代码为:
int operator++(int) {
int temp = *this;
++*this;
return temp;
}
运算符重载
- 注意运算符重载的形式和一般函数的形式非常类似,唯一区别就是将函数名换成operator 及 运算符两部分,如:
CMyString& operator = (const CMyString& str);
……
2、那程序什么时候执行拷贝构造,什么时候执行的是运算符重载里的内容呢?
CMyString str2=str1; //执行的拷贝构造
CMyString str2(str1); //执行的拷贝构造
str2 = str1; //执行的运算符重载
写时拷贝
是指在写的时候(即改变字符串的时候)才会真正的开辟空间拷贝(深拷贝),如果只是对数据的读时,只会对数据进行浅拷贝。
写时拷贝:引用计数器的浅拷贝,又称延时拷贝:写时拷贝技术是通过”引用计数”实现的,在分配空间的时候多分配4个字节,用来记录有多少个指针指向块空间,当有新的指针指向这块空间时,引用计数加一,当要释放这块空间时,引用计数减一(假装释放),直到引用计数减为0时才真的释放掉这块空间。当有的指针要改变这块空间的值时,再为这个指针分配自己的空间(注意这时引用计数的变化,旧的空间的引用计数减一,新分配的空间引用计数加一)。
其实我们对写时拷贝并不陌生,Linux fork和STL string是比较典型的写时拷贝应用,本文只讨论STL string的写时拷贝。
string类的实现必然有个char成员变量,用以存放string的内容,写时拷贝针对的对象就是这个char成员变量。通过赋值或拷贝构造类操作,不管派生多少份string“副本”,每个“副本”的char成员都是指向相同的地址,也就是共享同一块内存,直到某个“副本”执行string写操作时,才会触发写时拷贝,拷贝一份新的内存空间出来,然后在新空间上执行写操作。显然,那些只读的“副本”节省了内存分配的时间和空间。
参考博客:https://blog.csdn.net/qiansg123/article/details/80128063
移动拷贝
class A {
public:
int x;
A(int x) : x(x)//构造函数
{
cout << "Constructor" << endl;
}
A(A& a) : x(a.x)//拷贝构造
{
cout << "Copy Constructor" << endl;
}
A& operator=(A& a)//拷贝赋值
{
x = a.x;
cout << "Copy Assignment operator" << endl;
return *this;
}
A(A&& a) : x(a.x)//移动构造,右值引用
{
cout << "Move Constructor" << endl;
}
A& operator=(A&& a)//移动赋值,右值引用
{
x = a.x;
cout << "Move Assignment operator" << endl;
return *this;
}
};
1) 我们用对象a初始化对象b,后对象a我们就不在使用了,但是对象a的空间还在呀(在析构之前),既然拷贝构造函数,实际上就是把a对象的内容复制一份到b中,那么为什么我们不能直接使用a的空间呢?这样就避免了新的空间的分配,大大降低了构造的成本。这就是移动构造函数设计的初衷;
2) 拷贝构造函数中,对于指针,我们一定要采用深层复制,而移动构造函数中,对于指针,我们采用浅层复制。浅层复制之所以危险,是因为两个指针共同指向一片内存空间,若第一个指针将其释放,另一个指针的指向就不合法了。所以我们只要避免第一个指针释放空间就可以了。避免的方法就是将第一个指针(比如a->value)置为NULL,这样在调用析构函数的时候,由于有判断是否为NULL的语句,所以析构a的时候并不会回收a->value指向的空间;
3) 移动构造函数的参数和拷贝构造函数不同,拷贝构造函数的参数是一个左值引用,但是移动构造函数的初值是一个右值引用。意味着,移动构造函数的参数是一个右值或者将亡值的引用。也就是说,只用用一个右值,或者将亡值初始化另一个对象的时候,才会调用移动构造函数。而那个move语句,就是将一个左值变成一个将亡值。
浅拷贝和深拷贝
可能会出现的问题的情况主要是由于:存在指针和内存分配。
#include <iostream>
using namespace std;
class Student {
private:
int num;
char *name;
public:
Student();
~Student();
};
Student::Student() {
name = new char(20);
cout << "Student" << endl;
}
Student::~Student()
{
cout << "~Student " << (int)name << endl;
delete name;
name = NULL;
}
int main() {
{// 花括号让s1和s2变成局部对象,方便测试
Student s1;
Student s2(s1);// 复制对象
}
system("pause");
return 0;
}
执行结果:调用一次构造函数,调用两次析构函数,两个对象的指针成员所指内存相同,这会导致什么问题呢?name指针被分配一次内存,但是程序结束时该内存却被释放了两次,会导致崩溃!因此需要添加拷贝构造:链接
Student::Student(const Student &s) {
name = new char(20);
memcpy(name, s.name, strlen(s.name));
cout << "copy Student" << endl;
}
回调函数
先来看看来自维基百科的对回调(Callback)的解析:**In computer programming, a callback is any executable code that is passed as an argument to other code, which is expected to call back (execute) the argument at a given time. This execution may be immediate as in a synchronous callback, or it might happen at a later time as in an asynchronous callback**
.
也就是说,把一段可执行的代码像参数传递那样传给其他代码,而这段代码会在某个时刻被调用执行,这就叫做回调。如果代码立即被执行就称为同步回调,如果在之后晚点的某个时间再执行,则称之为异步回调。
再来看看来自Stack Overflow某位大神简洁明了的表述:**A "callback" is any function that is called by another function which takes the first function as a parameter。 **
也就是说,函数 F1 调用函数 F2 的时候,函数 F1 通过参数给 函数 F2 传递了另外一个函数 F3 的指针,在函数 F2 执行的过程中,函数F2 调用了函数 F3,这个动作就叫做回调(Callback),而先被当做指针传入、后面又被回调的函数 F3 就是回调函数。到此应该明白回调函数的定义了吧?
为什么不像普通函数调用那样,在回调的地方直接写函数的名字呢?那就是解耦。
#include<stdio.h>
#include<softwareLib.h> // 包含Library Function所在读得Software library库的头文件
int Callback() // Callback Function
{
// TODO
return 0;
}
int main() // Main program
{
// TODO
Library(Callback);
// TODO
return 0;
}
乍一看,回调似乎只是函数间的调用,和普通函数调用没啥区别,但两者之间的一个关键不同:在回调中,主程序把回调函数像参数一样传入库函数。这样一来,只要我们改变传进库函数的参数,就可以实现不同的功能,并且丝毫不需要修改库函数的实现,这就是解耦。
怎么使用回调函数?用函数指针!
#include<stdio.h>
int Callback_1() // Callback Function 1
{
printf("Hello, this is Callback_1 ");
return 0;
}
int Callback_2() // Callback Function 2
{
printf("Hello, this is Callback_2 ");
return 0;
}
int Callback_3() // Callback Function 3
{
printf("Hello, this is Callback_3 ");
return 0;
}
int Handle(int (*Callback)())
{
printf("Entering Handle Function. ");
Callback();
printf("Leaving Handle Function. ");
}
int main()
{
printf("Entering Main Function. ");
Handle(Callback_1);
Handle(Callback_2);
Handle(Callback_3);
printf("Leaving Main Function. ");
return 0;
}
new、delete、malloc、free
new、delete则为C++的操作运算符,它调用的分别为赋值运算符重载operator new()和operator delete();从上面几个图可以看出,new以及delete其实是基于malloc和free,其中分配内存的过程大致分为:
分配内存构造函数析构函数销毁内存(且都需要手动释放<或者等待main函数结束全部销毁>):
值得注意的是开辟数组空间的时候,有如下过程: 即delete T[N]时,需要从空间的首地址前推四个字节开始释放(因为多分配了四个字节记录析构函数的次数)
注:malloc单纯申请内存而不会调用类的构造函数(因此需要显示地指明其指针类型),free单纯释放内存,不会调用类的析构函数;
sizeof,strlen以及内存对齐
sizeof总结:
它的功能是:返回一个对象或类型所占的内存字节数。
具体而言,当参数分别如下时,sizeof返回的值表示的含义如下:
数组——编译时分配的数组空间大小;
指针——存储该指针所用的空间大小(存储该指针的地址的长度,是长整型,应该为4);
类型——该类型所占的空间大小;
对象——对象的实际占用空间大小;
函数——函数的返回类型所占的空间大小。函数的返回类型不能是void。
注意:要特别留意sizeof一个指针与sizeof一个数组的区别。
见下例:
string find = "de";
char find1[] ="de";
int len = sizeof(find);//就是固定的28(因为string是一个对象,类似于vector)
int len1 = sizeof(find1);//3,因为后面还有一个终止符
strlen总结
strlen(…)是函数,要在运行时才能计算。参数必须是字符型指针(char)。当数组名作为参数传入时,实际上数组就退化成指针了。
它的功能是:返回字符串的长度。该函数实际完成的功能是从代表该字符串的第一个地址开始遍历,直到遇到结束符’\0’。返回的长度大小不包括’\0’*。
Sizeof、Strlen区别与联系
1、sizeof是运算符,strlen是函数。 sizeof功能是返回一个对象或类型所占的内存字节数。strlen的功能是返回字符串的长度。
2、sizeof操作符的返回值类型是size_t,strlen返回值类型是int。
PS:size_t和int?
size_t在32位架构上是4字节,在64位架构上是8字节,在不同架构上进行编译时需要注意这个问题。而int在不同架构下都是4字节,与size_t不同;且int为带符号数,size_t为无符号数。
3、sizeof可以用类型、函数做参数,strlen只能用char*做参数,且必须是以’’\0’’结尾。
注意:
1.sizeof后如果是类型必须加括弧,如果是变量名可以不加括弧。
2.sizeof用函数做参数,比如:
short f();
printf(“%d\n”, sizeof(f()));
输出的结果是sizeof(short),即2。
3.当适用于一个结构类型时或变量, sizeof 返回实际的大小, 当适用一静态地空间数组, sizeof 归还全部数组的尺寸。 sizeof 操作符不能返回动态地被分派了的数组或外部的数组的尺寸 。
4、数组做sizeof的参数不退化,传递给strlen就退化为指针。
5、大部分编译程序在编译的时候就把sizeof计算过了是类型或是变量的长度。
char str[20]=”0123456789”;
int a=strlen(str); //a=10;
int b=sizeof(str); //而b=20;
注意:数组作为参数传给函数时传的是指针而不是数组,传递的是数组的首地址。
sizeof与内存对齐
1.类的大小为类的非静态成员数据的类型大小之和,也就是说静态成员数据不计算在内。
2.普通成员函数与sizeof无关。
3.虚函数由于要维护在虚函数表,所以要占据一个指针大小,也就是4字节。
4.类的总大小也遵守类似class字节对齐的,调整规则。
5.空类的对象占据一个字节。
6.有关内存对齐的知识,见下;
注意:
1、只定义类但未实例化系统是不会为其分配存储空间的,sizeof(类类型)只不过是计算其所占内存大小。
2、一个类的实例所对应的内存空间中存放的第一个元素的位置是该类的第一个数据成员的存放位置。
class、struct的内存对齐
在默认对齐方式下,结构体成员的内存分配满足下面三个条件:
·结构体第一个成员的地址和结构体的首地址相同
·结构体每个成员地址相对于结构体首地址的偏移量(offset)是该成员大小的整数倍,如果不是则编译器会在成员之间添加填充字节(internal adding)。
·结构体总的大小要是其成员中最大size的整数倍,如果不是编译器会在其末尾添加填充字节(trailing padding)。
为什么需要这么做来完成内存对齐?
主要是为了实现快速寻址,提高效率,如int为4位,在32位CPU下,一次寻址是4个字节,如果int对象在地址6,就需要在0和4取两次,如果地址是4,那就只需要取一次。
strcpy、strncpy、memset
strcpy和memcpy的区别
- 复制的内容不同。
strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。
注意:用memcpy给字符串赋值时应为memset(number,’0’, n);
不能是memset(number,0,n);否则后面strlen(number)时的结果为1。
2、复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符”\0”才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。
3、用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy
strcpy函数和strncpy函数的区别?哪个函数更安全?
1、函数功能和区别说明
strcpy():char strcpy(char dest,const char src),返回值为char ,便于链式访问,参数列表中dest为目标字符串,src为源字符串。
功能:将源字符串整体拷贝到目标字符串,包括字符串结束符“\0”,注意在使用时应该注意dest的空间应该足够放下src。
strncpy():char _strncpy(char _dest,const char src,int count),与strcpy()不同的地方就是多了参数count,count为字符串src拷贝到字符串dest的字符个数,如果count给的数值大于src的长度,会在标字符串相应位置补上“\0”。如果count给的数值小于src的长度,那么只有len个字符被复制到dst中,注意!此时它的结果将不会以‘\0’字节结尾。
使用这个函数,尤其需要注意,不要出现count>strlen(dst)的情况,如果count>strlen(dst),那么会破坏dst后面的内存(即缓冲区溢出):strncpy是不负责检测count是否大于dst长度的。
*2、代码实现
// strcpy()
char my_strcpy(char *dest, const char * src) {
assert(dest&&src);
char * temp = dest;//这句不能少,否则找不到字符首部!!
while ((dest++ = src++)!=’\0’);
return temp;
}
//strncpy()
char my_strncpy(char dest, const char * src,int count) {
assert(dest&&src);
char * temp = dest;
while (count--&&(dest++ = src++))
{
;
}
if(count>0)
{
while (count--)
{
dest++ = '\0';
}
}
return temp;
}
3、安全性
在安全性方面,显然strncpy要比strcpy安全得多,strcpy无法控制拷贝的长度,可能出现越界的问题,程序就会崩溃。而strncpy就控制了拷贝的字符数避免了这类问题,但是要注意的是dest依然要注意要有足够的空间存放src,而且src 和 dest 所指的内存区域不能重叠。
考虑内存重叠的strncpy
面试中经常会遇到让你写一个能够处理内存重叠的strncpy,标准库中的strncpy是不考虑内存重叠的,如果出现内存重叠,结果将是未定义的。如果内存重叠和src的长度小于len这两种情况同时出现,又如何处理?
代码博客:https://blog.csdn.net/sinat_30071459/article/details/72771137
成员函数里memset(this,0,sizeof(*this))会发生什么
有时候类里面定义了很多int,char,struct等c语言里的那些类型的变量,我习惯在构造函数中将它们初始化为0,但是一句句的写太麻烦,所以直接就memset(this, 0, sizeof this);*将整个对象的内存全部置为0。对于这种情形可以很好的工作,但是下面几种情形是不可以这么使用的;
- 类含有虚函数表指针:这么做会破坏虚函数表,后续对虚函数的调用都将出现异常;
- 类中含有C++类型的对象:例如,类中定义了一个list的对象,由于在构造函数体的代码执行之前就对list对象完成了初始化,假设list在它的构造函数里分配了内存,那么我们这么一做就破坏了list对象的内存。
类型转换符static_cast等
C++中强制类型转换操作符有static_cast、dynamic_cast、const_cast、reinterpert_cast四个。
1.static_cast:
用法:static_cast < type-id > ( expression )
说明:该运算符把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性。
用途:
情况1:void指针->其他类型指针
情况2:const转非const
情况3:多态向上转化,可以向下但不建议;
PS:参考链接
编译器隐式执行的任何类型转换都可以由static_cast来完成,比如int与float、double与char、enum与int之间的转换等。
当编译器隐式执行类型转换时,大多数的编译器都会给出一个警告(即常见的warning)。
有何作用?使用static_cast可以明确告诉编译器,这种损失精度的转换是在知情的情况下进行的,也可以让阅读程序的其他程序员明确你转换的目的而不是由于疏忽。把精度大的类型转换为精度小的类型,static_cast使用位截断进行处理。
使用static_cast可以找回存放在void指针中的值。如:
static_cast也可以用在于基类与派生类指针或引用类型之间的转换。然而它不做运行时的检查,不如dynamic_cast安全。static_cast仅仅是依靠类型转换语句中提供的信息来进行转换,而dynamic_cast则会遍历整个类继承体系进行类型检查,因此dynamic_cast*在执行效率上比static_cast要差一些。
2.dynamic_cast
用法:dynamic_cast < type-id > ( expression )
说明:该运算符把expression转换成type-id类型的对象。Type-id必须是类的指针、类的引用或者void *;如果type-id是类指针类型,那么expression也必须是一个指针,如果type-id是一个引用,那么expression也必须是一个引用。
来源:为什么需要dynamic_cast强制转换?
简单的说,当无法使用virtual函数的时候。
PS:代码详见第二篇博客,很大的应用价值;
Base要有虚函数,否则会编译出错;static_cast则没有这个限制。这是由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表(关于虚函数表的概念,详细可见
3.reinpreter_cast
用法:reinpreter_cast
说明:type-id必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原先的指针值)。
PS:基本什么都能转,但是可能出错,少用;
4.const_cast
用法:const_cast
说明:该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外, type_id和expression的类型是一样的。
常量指针被转化成非常量指针,并且仍然指向原来的对象;常量引用被转换成非常量引用,并且仍然指向原来的对象;常量对象被转换成非常量对象。
参考博客:https://www.cnblogs.com/xiangtingshen/p/10851349.html
更为详细:https://blog.csdn.net/windgs_yf/article/details/88396056
补充:volitate
一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去优化这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。
而const值放在编译器符号表中,而没有访问内存,所以用volatile修饰const,就可以对其进行修改了。
http://www.zzvips.com/article/127340.html
面向对象与类
空类会添加哪些东西
拷贝(缺省、拷贝、赋值)、析构一共四个东西;
Empty(); // 缺省构造函数//
Empty( const Empty& ); // 拷贝构造函数//
~Empty(); // 析构函数//
Empty& operator=( const Empty& ); // 赋值运算符//
即便是空类,也有一个字节的大小。
成员列表初始化效率高?
对于在函数体中初始化,是在所有的数据成员被分配内存空间(并初始化)后才进行的。
列表初始化是给数据成员分配内存空间时就进行初始化。
前者是初始化、赋值;后者是直接初始化时赋值。少了一个步骤,当然更快;
类成员变量初始化时按照类中声明的顺序初始化的,而不是按照初始化列表的排序方式。(如果颠倒顺序,后面的值可能有不可预估的值)
面向对象的概念
面向对象就是通过将需求要素转化为对象进行问题处理的一种思想。
C++面向对象的特性可以总结为:封装、继承和多态。
封装:
封装就是将程序模块化,对象化,把具体事物的特性属性和通过这些属性来实现一些动作的具体方法放在一个类中。对象是封装的最基本单位。
继承:
继承是子类自动共享父类数据和方法的机制。父类的相关属性,可以被子类重复使用,而对于子类中需要用到的新的属性和方法,子类可以自己扩展。
多态:
多态包含了重载和重写。
重载是静态多态,指对函数进行重载,一般是参数类型、返回类型等等(必须重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表,只修改返回类型不行)。
/交换 int 变量的值
void Swap(int *a, int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
//交换 float 变量的值
void Swap(float *a, float *b)
{
float temp = *a;
*a = *b;
*b = temp;
}
//交换 char 变量的值
void Swap(char *a, char *b)
{
char temp = *a;
*a = *b;
*b = temp;
}
//交换 bool 变量的值
void Swap(bool *a, bool *b)
{
char temp = *a;
*a = *b;
*b = temp;
}
注意,仅仅只是返回类型不同,不能进行重载!
接口调用原则—就近调用
1、就近调用原则:派生类的某一个对象调用某个接口,若本派生类有该接口的话就调用自己的,如果没有该接口,就会存在一个就近调用原则,如果父辈存在相关接口则优先调用父辈接口,如果父辈也不存在相关接口则调用祖辈接口。
2、调用过程中若不是直接(通过类域名)调用某一虚函数则会调用派生类对该接口的重写,若是直接调用某虚函数(通过类域名),则调用的就是该虚函数。
3、基类对象调用虚函数,若派生类对该虚类有重写则会优先调用该接口的重写。这句话是错误的!!!
基类对象(或指向基类对象的指针)调用虚函数是只会调用自己的,指针把基类指针指向派生类对象时才会调用派生类的。
4、注意派生类对象地址(指针)可以直接赋给基类指针,而基类赋给派生类必须显示转化。(看这个类引用他的成员会不会出错来区分)
5、构造函数是从最初的基类开始构造的,各个类的同名变量没有形成覆盖,都是单独的变量。
6、在C++的类继承中,建立对象时,首先调用基类的构造函数(不管基类构造函数是否带参数),然后在调用下一个派生类的构造函数,依次类推;析构对象时,其顺序正好与构造相反;
7、注意区别虚函数继承与虚继承(多重继承中特有的概念,虚拟基类是为解决多重继承而出现的)。
静态成员变量在类外初始化?
是的,因为静态变量在main之前就已经在全局数据段产生的,它不应该去依赖类对象的生命周期。若是在类内初始化,说明需要等待该类实例化才初始化。因此在类外初始化,程序编译时就已完成。
https://www.cnblogs.com/sggggr/p/13570280.html
友元类/函数有什么用?
让类的函数或者其他类能够访问该类的内部成员变量、函数();
右元在类内进行声明(无关public和private,它不是成员函数),在类外进行定义;
在内类声明的原因就在于,为了声明这个函数可以访问该类的私有成员。比如下面这个例子,使用友元函数来计算两点间的距离,不用使用GetX和GetY函数:
#include <iostream>
#include <cmath>
using namespace std;
//使用友元函数计算两点之间的距离
class Point{
public:
Point(int xx = 0, int yy = 0) { X = xx; Y = yy;}
int GetX() {return X;}
int GetY() {return Y;}
friend float fDist( Point &a, Point &b );
private:
int X, Y;
};
float fDist(Point &p1, Point &p2){
double x = double(p1.X - p2.X);//通过对象访问私有数据成员,而不是必须使用Getx()函数
double y = double(p1.Y - p2.Y);
return float(sqrt(x*x + y*y));
}
int main(){
Point p1(1, 1), p2(4, 5);
cout << "the distance is:";
cout << fDist(p1, p2) << endl;//计算两点之间的距离
return 0;
}
下面是一个友元类的例子:
class A{
public:
int GetX() { return x; }
friend class B;//B类是A类的友元类
//其它成员略
private:
int x;
};
class B{
public:
void set(int i);
//其他成员略
private:
A a;
};
void B :: set(int i){
a.x = i;//由于B类是A类的友元类,所以在B的成员函数中可以访问A类对象的私有成员
}
关于友元,需要注意的点: 友元关系不能传递 友元关系是单向的 友元关系不能被继承 https://blog.csdn.net/yiwanyuan2756/article/details/80437536
重载、重写和覆盖?
重载(Overload):同名函数参数(包括参数类型,个数与顺序)或返回值不同,注意返回值不能作为重载的标志。
覆盖(Override):派生类覆盖基类函数(虚函数)。
重写:派生类重写基类函数,但不覆盖(基类函数是虚函数的话就变成覆盖了)。
Override是覆盖的意思,也就是重写。
重写Override表示子类中的方法可以与父类中的某个方法的名称和参数完全相同,通过子类创建的实例对象调用这个方法时,将调用子类中的定义方法,这相当于把父类中定义的那个完全相同的方法给覆盖了,这也是面向对象编程的多态性的一种表现。
重载和覆盖有何不同?
虚函数总是在派生类被改写,这种改写被称为“override”(覆盖)。
重载:
发生在同一个类中
相同的方法名
参数列表不同
不看返回值,如果出现了只有返回值不同的“重载”,是错的。
重写:
发生在子类与父类中
相同的方法名
相同的参数列表
返回值相同 或者 子类方法的返回值是父类方法返回值类型的子类
访问修饰符相同 或者 子类方法的修饰符范围 大于 父类
抛出的异常相同 或者 子类方法抛出的异常 小于父类
静态成员函数的特点?
1、被类的所有的对象共有,不属于某一个对象。通过类名::就可以直接调用。
2、跟普通的成员函数比,没有隐藏的this指针作为参数。这一点可用于封装线程类。
3、静态成员函数只可以访问静态成员变量。
拷贝(复制)构造函数
1、当类的数据成员中有指针类型,或存在动态内存分配时,默认的拷贝构造函数实现的只能是浅拷贝,浅拷贝会带来数据安全方面的隐患(如同一块内存的多次析构,任何一方变动都会影响到另一方)。此时要实现正确的拷贝也就是深拷贝,必须自行编写拷贝构造函数。如下:
class Complex{
public:
double real, imag;
Complex(double r,double i){
real = r; imag = i;
}
Complex(const Complex & c){
real = c.real; imag = c.imag;
cout<<"Copy Constructor called"<<endl ;
}
};
2、拷贝构造函数必须形式必须是:类名(const 类名&对象名),缺少&编译不通过(堆栈溢出)(因为重复套娃)
3、拷贝构造在三种情况下会被调用:
①用类的一个对象去初始化类的另一个新创建的对象;
②函数的形参是类对象,调用函数时(所以拷贝构造形参必须是引用类型);
③函数的返回值是类的对象,函数执行完返回调用时(所以赋值函数用于连续赋值场合时返回值必须是引用类型);
4、A a = 10;
注意当A只有一个成员变量的时候是允许这么定义类的实例的。
多态的实现原理?
子类中对父类的虚函数进行了重写,那么利用基类指针就可以实现子类的动态调用。
1,如果以一个基类指针指向一个衍生类对象(派生类对象),那么经由该指针只能访问基础类定义的函数(静态连接),如果是存在虚函数,那就可以动态连接。
2,如果以一个衍生类指针指向一个基础类对象,必须先做强制转型动作(explicit cast),给程序员带来困扰。(一般不会这么去定义)
3,如果基础类和衍生类定义了相同名称的成员函数,那么通过对象指针调用成员函数时,到底调用那个函数要根据指针的原型来确定,而不是根据指针实际指向的对象类型确定。(基类和派生类之间的同名函数会被后者覆盖,而不存在重载,但可以显式指定调用)
虚拟函数就是为了对“如果你以一个基础类指针指向一个衍生类对象,那么通过该指针,你只能访问基础类定义的成员函数”这条规则反其道而行之的设计。
静态多态和动态多态?
动态多态其实就是上述虚函数表中所提到的基类指针运行期间选择函数;
静态多态则是编译期间实现的,一是通过函数重载,二是通过函数模板;
虚函数表?
虚函数表有什么内容?
任何有虚函数的类及其派生类的对象都包含虚函数表(准确来说是虚函数表的地址,64位机即8个字节,且虚函数表位于对象指针的前八个字节)。
多态(动态绑定)即是通过基类指针所指向的对象的虚函数表来实现的:如果该基类指针指向的是基类对象,那就调用基类的虚函数表;如果该基类指针指向的是派生类,则调用派生类的虚函数表;
https://blog.csdn.net/primeprime/article/details/80776625 虚表是属于类的,而不是属于某个具体的对象,对象中仅有虚表的指针;
定义实例时会在构造函数中进行虚表的创建和虚表指针的初始化,每个对象调用的虚函数都是通过虚表指针来索引的。
虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。
如果基类有3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。如果派生类有自己的虚函数,那么虚表中就会添加该项。派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。
做一道题?
请问一下代码块的输出是啥?
#include <iostream>
using namespace std;
class A {
public:
virtual void foo()
{
cout << "A's foo()" << endl;
bar();
}
virtual void bar()
{
cout << "A's bar()" << endl;
}
};
class B: public A {
public:
void foo()
{
cout << "B's foo()" << endl;
A::foo();
}
void bar()
{
cout << "B's bar()" << endl;
} };
int main() {
B bobj;
A *aptr = &bobj;
aptr->foo();
A aobj = *aptr; //转化为A类对象 ()
aobj.foo();
}
aptr->foo()输出结果是:
B's foo()//这个明白,多态性
A's foo()//这个也明白,执行A::foo();
B's bar()//虽然调用的是这个函数:A::foo(); 但隐式传入的还是bobj 的地址,所以再次调用bar();调用时还是会调用B的函数, 与虚函数指针有关
aobj.foo()//输出结果是:
A's foo() //这个不是指针,aobj完全是一个A的对象,与多态没有关系
A's bar()
先写下来再看答案:https://blog.csdn.net/cxycj123/article/details/81700621
虚函数表是在运行/还是编译时创建的?
答:虚函数表在编译的时候就确定了,而类对象的虚函数指针vptr是在运行阶段确定的,这是实现多态的关键。
虚函数表示类所共有的,有点类似于static变量,存在全局数据区/常量区
析构函数定义为虚函数?
直接的讲,C++中基类采用virtual虚析构函数是为了防止内存泄漏。
具体地说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。
假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定(基类指针被撤销时,会先调用派生类的析构函数,再调用基类的析构函数。),因而只会调用基类的析构函数,而不会调用派生类的析构函数。
那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。所以,为了防止这种情况的发生,C++中基类的析构函数应采用virtual虚析构函数。
菱形继承问题
菱形继承即两个子类继承同一个父类,而另一个类同时继承这两个子类。这回出现二义性,D调用接口时应该调用哪一个?
#include<bits/stdc++.h>
using namespace std;
class Base{
public:
void fun(){
cout<<"Base()"<<endl;
}
};
class A:public Base{
};
class C:public Base{
};
class D:public A,public C{
};
int main(){
D d;
//d.fun(); 出错,返回request for member 'fun' is ambiguous
d.A::fun();
d.C::fun();
return 0;
}
可能出现模糊调用的问题,这时需要虚继承(virtual public),具体实现是:A和C中不再保存Base的具体内容,而是保存了一份偏移地址,所以在D调用fun()时,调用的就是Base的fun(),但对于A、C相同的变量名,D在调用时还是要利用域限定来处理。
#include<bits/stdc++.h>
using namespace std;
class Base{
public:
int _base=1;
void fun(){
cout<<"Base()"<<endl;
}
};
class A:virtual public Base{
public:
int _base=2;
};
class C:virtual public Base{
public:
int _base=3;
};
class D:public A,public C{
};
int main(){
D d;
d.fun();//Base()
d.A::fun();//Base()
d.C::fun();//Base()
cout<<d.Base::_base<<endl;//1
cout<<d.A::_base<<endl;//2
cout<<d.C::_base<<endl;//3
return 0;
}
纯虚函数是什么?
纯虚拟函数(pure virtual):virtual void myfunc ( ) =0;
纯虚拟函数不许定义其具体动作,它的存在只是为了在衍生类中被重新定义。只要是拥有纯虚拟函数的类,就是抽象类,它们是不能够被实例化的(只能被继承)。
定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。(强制子类重写该函数)
如果一个继承类没有改写父类中的纯虚函数,那么他也是抽象类,也不能被实例化。抽象类不能被实例化,不过我们可以拥有指向抽象类的指针,以便于操纵各个衍生类。纯虚拟函数衍生下去仍然是纯虚拟函数,而且还可以省略掉关键字“virtual”。
protected与private继承
protected成员可以被派生类对象访问,不能被类外访问。
private继承有什么用?(只负责实现is-a,只想要父类的某些函数实现,见下例代码:)
class Timer{
public:
virtual void timeout(){
cout << __FUNCTION__ << endl;
} //用于计算超时功能
};
class Widget: private Timer
{
//private 继承
private:
//这里也改private 或许比较好,如果是public接口,有可能不太好哦.客户误意味widget居然有超时!
virtual void timeout()
{
Timer::timeout();
//调用父类的超时功能
cout << __FUNCTION__ << endl;
//干自己的事
}
};
C++11新特性
1.long long ,nullptr(解除二义性,见最后的面试问题),auto(编译时确定类型)
2.decltype():编译时取出变量类型
3.constexptr等价于const
5.封装了多线程库如:mutex
6.for(auto d:v)
7.begin(v),end(v)
8.智能指针
9.可变参数模板:它对参数进行了高度泛化,它能表示0到任意个数、任意类型的参数。
10. 可变模版参数类:template< class… Types >class tuple;
11.右值引用:int &&i=10;将10这个值存到临时存储位置,再将临时存储位置的地址存给i
12.lambda:匿名函数:
auto func = [] () { cout << “hello,world”; };
func();
其中:[]函数开始,(填写函数参数),{函数主体};
左值引用和右值引用
在C++中可以取地址的、有名字的就是左值(内存中),反之,不能取地址的、没有名字的就是右值(寄存器中),见简单来说就是临时变量。左值引用是具名变量的别名即常规的引用,右值引用则是不具名变量的别名。
右值引用解决了什么问题?
- 临时对象非必要的拷贝操作
- 应用于普通函数
- 应用于类的移动构造函数(移动语义)
- 模板函数中如何按照参数的实际类型进行转发
这就是完美转发:主要用作模板函数的参数类型推导
std::move():可以将左值强制转换为右值;右值转左值?将右值赋值给一个新的变量即可
那么为什么需要右值引用这个东西呢?可以从寄存器直接取值,降低性能损耗。
T&&就是C++11之后的右值引用,一般用作转移构造函数,如:int &&a = i+j;
T&是普通的左值引用;
array与数组
std::array 保存在栈内存中,相比堆内存(vector对象在栈内存,其中有指向堆内存的指针)中的std::vector。
内置数组如下:
int a[5];
int b = new int[5];
*前者时建立在栈内存,后者是建立在堆内存。
既然有了内置的数组,为什么还要引入array呢?
内置的数组有很多麻烦的地方,比如无法直接对象赋值,无法直接拷贝(比如两个数组无法直接赋值)等等,同时内置的数组又有很多比较难理解的地方,比如数组名是数组的起始地址等等。
简单来说,std::array除了有内置数组支持随机访问、效率高、存储大小固定等特点外,还支持迭代器访问、获取容量、获得原始指针等高级功能。而且它还不会退化成指针给开发人员造成困惑。
memory_order和atomic?
std::memory_order(可译为内存序,访存顺序)。
C++11 中规定了 6 中访存次序(Memory Order):只是针对原子变量的原子操作来说的!
链接
enum memory_order {
memory_order_relaxed,
memory_order_consume,
memory_order_acquire,
memory_order_release,
memory_order_acq_rel,
memory_order_seq_cst
};
上述内存序分为3类,
顺序一致性模型(std::memory_order_seq_cst),
Acquire-Release 模型(std::memory_order_consume, std::memory_order_acquire, std::memory_order_release, std::memory_order_acq_rel,)(获取/释放语义模型)
和 Relax 模型(std::memory_order_relaxed)(宽松的内存序列化模型)。
memory_order_relaxed: 只保证当前操作的原子性,不考虑线程间的同步,其他线程可能读到新值,也可能读到旧值。
memory_order_release:(可以理解为 mutex 的 unlock 操作)
memory_order_acquire: (可以理解为 mutex 的 lock 操作)
memory_order_acq_rel:
对读取和写入施加 acquire-release 语义,无法被重排
可以看见其他线程施加 release 语义的所有写入,同时自己的 release 结束后所有写入对其他施加 acquire 语义的线程可见
memory_order_seq_cst:(顺序一致性)
如果是读取就是 acquire 语义,如果是写入就是 release 语义,如果是读取+写入就是 acquire-release 语义
同时会对所有使用此 memory order 的原子操作进行同步,所有线程看到的内存操作的顺序都是一样的,就像单个线程在执行所有线程的指令一样
通常情况下,默认使用 memory_order_seq_cst,所以你如果不确定怎么这些 memory order,就用这个。
std::atomic_flag是一个原子的布尔类型,可支持两种原子操作:
test_and_set, 如果atomic_flag对象被设置,则返回true; 如果atomic_flag对象未被设置,则设置之,返回false
clear.清除atomic_flag对象
使用atomic_flag可实现mutex。
std::atomic对int, char, bool等数据结构进行原子性封装,在多线程环境中,对std::atomic对象的访问不会造成竞争-冒险。利用std::atomic可实现数据结构的无锁设计。
https://www.cnblogs.com/taiyang-li/p/5914331.html
Allocator
考虑小型区块造成的内存破碎问题,SGI设计了双层级配置器:
第一级直接使用allocate()调用malloc()、deallocate()调用free(),使用类似new_handler机制解决内存不足(抛出异常),配置无法满足的问题(如果在申请动态内存时找不到足够大的内存块,malloc 和new 将返回NULL 指针,宣告内存申请失败)。
第二级视情况使用不同的策略,当配置区块大于128bytes时,调用第一级配置器,当配置区块小于128bytes时,采用内存池的整理方式:配置器维护16个(128/8)自由链表,负责16种小型区块的此配置能力。内存池以malloc配置而得,如果内存不足转第一级配置器处理。
Operator new(PS:这是new的底层)和malloc:
Operator new其中调用的就是malloc,值得注意的是,malloc实际分配的内存比你申请的更大,这是为了内存管理而设计的(分配内存不连续需要用header指针链接)。
基本思路是利用链表来管理。
容器介绍
**顺序容器**:**数组(Array)、Vector、Deque(念dei ke)**:双向队列,两端皆可进可出(**其内部其实是分块的内存buffer(线性空间),逻辑相邻的buffer利用指针相连(中控器map来管理)**);**List(链表)**:标准库是双向(环状)链表,前后向指针;**Forward-List:**单向链表;<br /> **关联容器**:(associative)<br /> **Set/Multiset:**集合,大小排序,独一无二的数据(Multi可以重复),基本都是**红黑树实现;**<br /> **Map/MultiMap:**映射表,也是红黑树实现;<br /> **UnorderedMap/Multimap:**hash表,支持多对一,一般是Separate Chaining(分离链接法);<br />**set或者map底层解析?**<br />[https://blog.csdn.net/chongzi_daima/article/details/107849493](https://blog.csdn.net/chongzi_daima/article/details/107849493) <br />**容器分析**<br />vector的size增长倍数**是2或者1.5倍**,根据编译器的不同倍数不同;<br />**vector内存成长方式**可归结以下**三步曲**:<br />(1)另觅更大空间;<br />(2)将原数据复制过去;<br />(3)释放原空间三部曲。<br />
从上图可以看出,string的大小默认是28,且不会因为赋值而改变,这是为什么呢? string的实现在各库中可能有所不同,但是在同一库中相同一点是,无论你的string里放多长的字符串,它的sizeof()都是固定的,字符串所占的空间是从堆中动态分配的,与sizeof()无关。 有sizeof()为12、32字节的库实现。通常,我们所用到的 string 类型一般都会是这样实现:
双向队列deque,在内存中是分段连续(即在多个内存块中存放,利用指针连接这几个内存块),如果已分配的不够,那么就会在前或者后增加buffer。(戴补充)
详见:https://blog.csdn.net/u010710458/article/details/79540505
map/Multimap,key-value;其实multiset跟哈希表其实本质一样,只不过set的key和value是合一的,即value就是key!另外,两者底层都是红黑树结构;
详见:https://blog.csdn.net/zhang_guyuan/article/details/62237971
Unordered set/multiset,采用分散链接法处理冲突的情况(即multi),如果分配内存不够,则扩大一倍,重新规划散列;
Unordered map/multimap,跟前者本质无区别;
Priority_queue(优先队列),在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。优先队列具有最高级先出 (first in, largest out)的行为特征。本质是一个堆实现的。可以实现升序或者降序。
Vector的简单代码实现
https://blog.csdn.net/u011408355/article/details/47957481
vector底层是一个动态数组,包含三个迭代器,start和finish之间是已经被使用的空间范围,end_of_storage是整块连续空间包括备用空间的尾部。
当空间不够装下数据(vec.push_back(val))时,会自动申请另一片更大的空间(1.5倍或者2倍),然后把原来的数据拷贝到新的内存空间,接着释放原来的那片空间【vector内存增长机制】。
当释放或者删除(vec.clear())里面的数据时,其存储空间不释放,仅仅是清空了里面的数据。
因此,对vector的任何操作一旦引起了空间的重新配置,指向原vector的所有迭代器会都失效了。
为什么vector中不能放引用?
看这个回答:https://bbs.csdn.net/topics/360185953
可以这样理解: 引用需要初始化,且不能改变引用指向新的对象。而vector执行的时候是需要执行copy的,把以前的对象放在vector开辟的内存中,这样破坏了引用不能指向新的对象。
扩容为什么要以1.5倍或者2倍扩容?
之所以不使用常量的增加操作,是考虑均摊时间。
举个例子,一共要vector中插入n个数,倍增因子为m,即每次变为以前的m倍,假定这n个数最后都需要内存扩容才装的下去,那么需要分配内存的次数为logm(n)(比如n=8,m=2,那么需要分配三次才能装下所有的东西)。
另外,要注意的是,每次内存分配之后,需要将之前的数据拷贝至新内存(这个移动操作也看做是push_back),第i次分配内存就需要拷贝m^i个元素,所以n个元素全部插入,涉及的元素插入次数为:
将m-1看成一个常量,那就是n的时间消耗,所以均摊下来,每个插入操作的时间复杂度为O(1)。
补充:等比数列的求和公式
那如果是增加指定值呢?比如增加m?
假定有n个元素,每次增加k个。第i次增加复制的数量为为:100i 。
所以最终的耗时为:
n 次 push_back 操作所花费的时间复杂度为O(n^2)。
那么均摊下来,每一个push_back的时间复杂度为O(n)。
为什么倍增因子不建议大于2呢?
这个问题似乎在于之前内存空间的重用。
如果倍增因子大于2,那么新分配的空间必然大于之前内存空间的总和,也就无法重新使用之前的那一段内存,这对缓存极不友好,可能会造成内存碎片。
从上图可见如果选用k=1.5,那么之前的内存空间就可以被使用了。
map插入方式有几种?
1、用insert函数插入pair数据,
mapStudent.insert(pair
2、用insert函数插入value_type数据
mapStudent.insert(map
3、在insert函数中使用make_pair()函数
mapStudent.insert(make_pair(1, “student_one”));
4、用数组方式插入数据
mapStudent[1] = “student_one”;
注意:关联容器也是可以通过迭代器从头到尾遍历的。
unorder_map底层结构?
1、unordered_map和map类似,都是存储的key-value的值,可以通过key快速索引到value。不同的是unordered_map不会根据key的大小进行排序,unordered_map的底层实现是哈希表,hash_table使用的开链法进行冲突避免。
2、存储时是根据key的hash值(并不是元素值value)判断元素是否相同,即unordered_map内部元素是无序的,而map中的元素是按照二叉搜索树存储,进行中序遍历会得到有序遍历。
3、注意无序容器unordered_map在存储上组织为一个桶,每个桶保存0个或多个元素(value),哈希函数是用来计算桶号。
4、对于自定义类型,使用时map的key需要重载运算符 <。而unordered_map需要定义hash_value哈希函数并且重载operator== (用来比较键值对相应的哈希值)。(这个在leetcode题中经常遇到)
map、set是为什么使用红黑树?
1、他们的底层都是以红黑树的结构实现,因此插入删除等操作都在O(logn)时间内完成;(相比于平衡二叉树,红黑树删除操作时旋转次数更少)
2、实现map的红黑树的节点数据类型是key+value,而实现set的节点数据类型是value;
3、因为map和set要求是自动排序的,红黑树能够实现这一功能,而且时间复杂度比较低;
4、四类容器仅仅只是在RBTree上进行了一层封装,map的键和值不同,每个键都有自己的值,键不能重复,但是值可以重复。multimap和multiset就在map和set的基础上,使他们的键可以重复,除此之外基本等同。
解决hash冲突的方法?
常用的解决冲突方法有以下四种:
链地址法\开链法
这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存放在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。
插入和查找以及删除操作消耗的时间会达到O(n)。
开放定址法:
这种方法也称再散列法,其基本思想是:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。
他不需要更多的空间,但是在最坏的情况下(例如所有输入数据都被map到了一个index上)的时间复杂度也会达到O(n)。
再哈希法:
这种方法是同时构造多个不同的哈希函数:
Hi=RH1(key) i=1,2,…,k
当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。
建立公共溢出区:
这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。
什么是一致性哈希?
一致性哈希目前主要应用于分布式缓存当中(或者是负载均衡)。
一致性哈希可以有效地解决分布式存储结构下动态增加和删除节点所带来的问题。我们简单举例说明一下:
首先,我们把全量的缓存空间当做一个环形存储结构。环形空间总共分成2^32个缓存区
如何让key和节点对应起来呢?很简单,每一个key的顺时针方向最近节点,就是key所归属的存储节点。
https://www.jianshu.com/p/49e3fbf41b9b
容器迭代器为何失效呢?
当使用一个容器的insert或者erase函数通过迭代器插入或删除元素“可能”会导致迭代器失效,因此我们为了避免危险,应该重新获取的新的有效的迭代器进行正确的操作。
迭代器失效的类型:
1.由于插入元素,使得容器元素整体“迁移”导致存放原容器元素的空间不再有效,从而使得指向原空间的迭代器失效。
2.由于删除元素使得某些元素次序发生变化使得原本指向某元素的迭代器不再指向希望指向的元素。
面试题
虚函数可以被声明为static吗?
不可以,因为静态成员函数/变量没有this指针。
参考链接:https://blog.csdn.net/leiming32/article/details/8619893
构造函数不为虚函数?
虚函数相应一个指向vtable虚函数表的指针,但是这个指向vtable的指针事实上是存储在对象的内存空间的。
假设构造函数是虚的,就须要通过 vtable来调用,但是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。(先有鸡还是先有蛋的问题)
Sleep和yield
线程执行sleep()方法后会转入阻塞状态,所以执行sleep()方法的线程在指定的时间内肯定不会被执行,而yield()方法只是使当前线程重新回到可执行状态,所以执行yield()方法的线程有可能在进入到可执行状态后马上又被执行。sleep()方法比yield()方法具有更好的可移植性。
C++11 标准库提供了yield()和sleep_for()两个方法。
(1)std::this_thread::yield(): 线程调用该方法时,主动让出CPU,并且不参与CPU的本次调度,从而让其他线程有机会运行。在后续的调度周期里再参与CPU调度。这是主动放弃CPU的方法接口。
(2)std::sleep_for():线程调用该方法时,同样会让出CPU,并且休眠一段时间,从而让其他线程有机会运行。等到休眠结束时,才参与CPU调度。这也是主动放弃CPU的方法。
两者的不同很明显,yield()方法让出CPU的时间是不确定的,并且以CPU调度时间片为单位。而sleep_for()让出CPU的时间是固定的。
yield()的实现依赖于操作系统CPU调度策略,在不同的操作系统或者同一个操作系统的不同调度策略下,表现也可能是不同的。
析构函数中调用delete this会如何
会导致堆栈溢出。
原因很简单,delete的本质是“为将被释放的内存调用一个或多个析构函数,然后,释放内存”。
显然,delete this会去调用本对象的析构函数,而析构函数中又调用delete this,形成无限递归,造成堆栈溢出,系统崩溃。
只在堆创建或只在栈创建类?
只在堆创建的基本思路:将构造或析构改成私有,建议将析构改成私有;
只在栈:将operator new/delete重载私有;
设计一个类计算对象的个数
声明静态成员变量count,类内声明,类外初始化;
构造函数、拷贝、赋值,变量+1;析构变量-1;
通过指针或引用实现多态,而不可以通过对象呢?
对指针的赋值,仅仅是让基类指针_pB指向的子类对象的地址。
而上面代码中无论赋值操作还是赋值构造时,只会处理成员变量,一个类对象里面的vptr永远不会变,永远都会指向所属类型的虚函数表。
全局数组和局部数组初始化变量?
全局数组、变量在定义时默认初始化为0;<br /> 局部数组、变量在定义时**默认初始化为随机值**;
如何阻止类被继承呢?
[https://www.cnblogs.com/moonz-wu/archive/2008/05/07/1186065.html](https://www.cnblogs.com/moonz-wu/archive/2008/05/07/1186065.html)<br /> 这个回答更好:<br /> https://www.cnblogs.com/wangpei0522/p/4460425.html
请你说说C语言参数压栈顺序?
主要是为了确定参数的个数方便:
https://blog.csdn.net/jiange_zh/article/details/47381597
https://blog.csdn.net/weiyayunerfendou/article/details/72805766
内存池的实现
利用自建的结构实现对内存的分配(大小相符)和释放进行优化。
https://www.cnblogs.com/bangerlee/archive/2011/09/01/2161437.html
operator new和new?
new是不可重载,完成内存分配和构造函数调用;
operator new可重载,只完成内存分配;
https://www.cnblogs.com/raichen/p/5808766.html
请介绍线程池,简述构造原理?可以手写一个简单的线程池么?
线程池简单来说就是提前申请一堆线程(资源),后续任务需要即分配,用完即放回线程池,基本步骤如下:
线程池有两个核心的概念,一个是任务队列,一个是工作线程队列。任务队列负责存放主线程需要处理的任务,工作线程队列其实是一个死循环,负责从任务队列中取出和运行任务,可以看成是一个生产者和多个消费者的模型。
任务队列相当于生产者,工作线程相当于消费者,当任务来临时,只有一个线程可以抢到。
参考代码:https://zhuanlan.zhihu.com/p/64739638
模板类代码:https://zhuanlan.zhihu.com/p/61464921
explicit关键词有什么作用?
主要作用是禁止隐式转换。如将类的构造函数设置为explicit,那么编译器无法对其进行隐式转换,让类的构造函数被显式调用。
强烈建议观看链接:https://zhuanlan.zhihu.com/p/137947734
或https://www.cnblogs.com/winnersun/archive/2011/07/16/2108440.html
连等?||和&&?strlen和sizeof?
x=y=5,最后x、y都是5;
||具有惰性,前面完成了就不会完成后面;
strlen计算字符串的长度,不包括末尾的’\0’,C语言字符串默认加上;
sizeof变量或者类型的大小(以字节为单位);
知道断言和异常机制么?
https://www.cnblogs.com/starrys/p/12664953.html
类中成员默认内联?如果类成员是引用(reference)?const和constexpr?
是的,但是否内联还要看编译器(C++primier);
如果是引用,那么就必须用初始化参数列表,并且不能有缺省构造;
const是运行期常量;constpexr主要是编译期常量:
https://blog.csdn.net/u012516419/article/details/105792962
array、vector、数组的区别?sizeof不一样?
https://www.cnblogs.com/Kernel001/p/7853441.html
相同点:
都和数组类似,利用下标访问;存储空间连续,可随机访问;
不同点:
array和vector可以赋值给另一个array、vector,但数组不行;
由于vector是动态变化的,因此在插入和删除时,需要考虑迭代器是否失效。
array和vector是自动释放的,而数组如果是new/malloc的需要先手动进行内存释放。
sizeof 计算array那就是个数乘以元素字节;
计算vector那就是vector的成员变量的大小(它是一个对象),跟编译器有关!
auto?
delete []
对于内存空间的清理,由于申请时记录了其大小,因此无论使用delete还是delete[ ]都能将这片空间完整释放,而问题就出在析构函数的调用上,当使用delete时,仅仅调用了对象数组中第一个对象的析构函数,而使用delete [ ]的话,将会逐个调用析构函数。
大端小端顺序?
“大端”和”小端”表示多字节值的哪一端存储在该值的起始地址处;小端存储在起始地址处,即是小端字节序;大端存储在起始地址处,即是大端字节序。
UDP/TCP/IP协议规定:把接收到的第一个字节当作高位字节看待,这就要求发送端发送的第一个字节是高位字节;而在发送端发送数据时,发送的第一个字节是该数值在内存中的起始地址处对应的那个字节,也就是说,该数值在内存中的起始地址处对应的那个字节就是要发送的第一个高位字节(即:高位字节存放在低地址处);由此可见,多字节数值在发送之前,在内存中因该是以大端法存放的;
不同CPU有不同的字节序类型,典型的使用小端存储的CPU有:Intel x86和ARM 典型的使用大端存储CPU有:Power PC、MIPS UNIX和HP-PA UNIX。
C/C++中有如下四个常用的转换函数,这四个函数在小端系统中生效,大端系统由于和网络字节序相同,所以无需转换。
htons —— 把unsigned short类型从主机序转成网络字节序
ntohs —— 把unsigned short类型从网络字节序转成主机序
htonl —— 把unsigned long类型从主机序转成网络字节序
ntohl —— 把unsigned long类型从网络字节序转成主机序
大端形同阅读顺序,高子节存在低地址,低字节存在高地址;
小端表明了权重,高子节在高地址,低字节在低地址;
如何利用代码判断呢?
法一、int型强转char型
int main(){
int a=1;
int *p =&a;
char *q=(char*)p;
if(*q==1)
//小端
else
//大端
}
法二、联合体Union判断
联合体与结构体不同的是,联合体是所有成员共享内存(联合体大小是其中成员最大那个),且会相互覆盖,其内存首地址是一致的。
void checkCPU() {
union MyUnion{
int a;
char c;
}test;
test.a = 1;
if (test.c == 1)
cout << "little endian" <<endl;
else
cout << "big endian" <<endl;
}
null和nullptr?
nullptr是C++11版本中新加入的,它的出现是为了解决NULL表示空指针在C++中具有二义性的问题。
NULL在C++中就是0,这是因为在C++中void* 类型是不允许隐式转换成其他类型的,所以之前C++中用0来代表空指针,但是在重载整形的情况下,会出现上述的问题。
所以,C++11加入了nullptr,可以保证在任何情况下都代表空指针,而不会出现上述的情况,因此,建议以后还是都用nullptr替代NULL吧,而NULL就当做0使用。
https://blog.csdn.net/qq_18108083/article/details/84346655
与零值的比较(float、double等)
整型与0的比较不用赘述,易错点在于布尔变量、浮点、指针变量与零值的比较。<br /><br /><br />PS:这个误差可以是1e-6等;<br /> 
有时候为了防止将 if (p == NULL)误写成 if (p = NULL),而有意把p和NULL颠倒。写成 if (NULL == p)。(这是一个很好的规避风险的习惯)
模板编程
模板T是指代任何类型(template
PS:有关模板的特化(specoalization),偏特化(partial specialization)
为什么需要模板?
为了减小重复编写相似代码的工作量,如函数功能一致,差别仅在于其中参数或返回值的类型不同。
C++中的模板分为:函数模板、类模板。
通常而言,并不是把模板编译成一个可以处理任何类型的单一实体;而是对于实例化模板参数的每种类型,都从模板产生一个不同的实体。这种用具体类型代替模板参数的过程叫做实例化,它产生了一个模板的实例。(即是说在编写代码的时候可能并不知道有些函数调用对不对,因此程序易读性较差)
于是,模板被编译了两次,分别发生在:
(1)实例化之前,先检查模板代码本身,查看语法是否正确;在这里会发现错误的语法,如遗漏分号等。
(2)在实例化期间,检查模板代码,查看是否所有的调用都有效。在这里会发现无效的调用,如该实例化类型不支持某些函数调用(该类型没有提供模板所需要使用到的操作)等。
函数模板
模板可以让我们生成通用参数类型的函数,这个通用的函数能够接受任意类型参数,同样可以返回任意类型的值,这样就避免了对所有类型的函数进行重载。<br /> 如:
template <class G_Type>
G_Type Plus(G_Type a, G_Type b)
{
G_Type Sum;
Sum = a + b;
return Sum;
}
上述只是声明了一种通用类型,可以增加任意个:
template <class G_Type,class U_Type>
G_Type Plus(G_Type a, U_Type b)
{
G_Type Sum;
Sum = a + b;
return Sum;
}
类模板
类模板的实现,可以使类有通用类型的成员,不必在定义类的时候定义成员的类型,即类的实现不关注数据元素的具体类型,而只关注类所需要实现的功能。如:
//类模板实现
template<class Name1,class Name2>
class Student
{
public:
Name1 m_Member1;
Name2 m_Member2;//成员函数定义
public:
void m_Add(Name1 a, Name2 b);//成员函数声明
};
//成员函数的外部实现
template <class Name1,class Name2>
void Student<Name1, Name2>::m_Add(Name1 a, Name2 b)
{
return a + b;
}
模板的特化
3.1 函数模板特化
函数模板在某种特定类型下的特定实现称为函数模板的特化。如下:
template <typename T>
T add(T a, T b)
{
printf("T add(T a, T b)\n");
return a + b;
}
//上述函数不支持int*类型,所以要对传入指针类型参数进行特化。
template <>
int* add<int*>(int* pa, int* pb)
{
printf("T add(const int* pa, const int* pb)\n");
*pa += *pb;
return pa;
}
int main(void)
{
int a = 8, b = 6;
add<>(&a, &b); //<>,编译器会自动推导数据类型为int*
return 0;
}
3.2 类模板特化
类模板特化分为部分特化与完全特化。模板的特殊化是当模板中的pattern有确定的类型时,模板有一个具体的实现。简单来说,部分特化是修改了参数的类型,而完全特化则是定死了参数的类型(int、char还是其他)。
a.部分特化
//原版的模板
template <typename T1, typename T2>
class TestCls
{
public:
void add(T1 a, T2 b);
};
template <typename T1, typename T2>
void TestCls <T1, T2>::add(T1 a, T2 b)
{
printf("estCls <T1, T2>::add(T1 a, T2 b) = %d\n", a + b);
}
//部分特化
template <typename T>
class TestCls <T, T>
{
public:
void add(T a, T b);
};
template <typename T>
void TestCls <T, T>::add(T a, T b)
{
printf("TestCls <T, T>::add(T a, T b) = %d\n", a + b);
}
int main(void)
{
TestCls<int, int> t; //编译器会选择特化后的类。
//注意<>内不能为空,编译器对类模板不能进行类型推导。
t.add(5, 5);
return 0;
}
原先的设定是T1、T2两种类型,二者既可以是同一种类型,也可以是不同一种,特化后T1、T2只能是相同的一种类型T。当我们定义的对象的T1、T2类型是一样的时候,编译器会选择特化后的类模板。
b.完全特化
//完全特化,完全特化后"<>"内为空
template <>
class TestCls<int, char> //直接指定了T1、T2的类型
{
public:
void add(int a, char b)
{
printf("TestCls<int, char>::add(int a, char b)\n");
}
};
int main(void)
{
TestCls<int, char> t;//也需要指明参数类型
t.add(5, 5);
return 0;
}
模板这种类似宏(macro-like) 的功能,对多文件工程有一定的限制:函数或类模板的实现 (定义) 必须与原型声明在同一个文件中。也就是说我们不能再将接口(interface)存储在单独的头文件中,而必须将接口和实现放在使用模板的同一个文件中。
模板的萃取trait
来干嘛?实现在函数模板中获取迭代器的特性!
https://blog.csdn.net/Jiangtagong/article/details/108896943