C++面试自己总结
1.C和C++的区别?C++的特性?面向对象编程的好处?
答:c++在c的基础上增添类,C是一个结构化语言,它的重点在于算法和数据结构。C程序的设计首要考虑的是如何通过一个过程,对输入(或环境条件)进行运算处理得到输出(或实现过程(事务)控制),而对于C++,首要考虑的是如何构造一个对象模型,让这个模型能够契合与之对应的问题域,这样就可以通过获取对象的状态信息得到输出或实现过程(事务)控制。
好处:
1、维护简单:模块化是面向对象编程中的一个特征。实体被表示为类和同一名字空间中具有相同功能的类,我们可以在名字空间中添加一个类而不会影响该名字空间的其他成员。
2、可扩充性:面向对象编程从本质上支持扩充性。如果有一个具有某种功能的类,就可以很快地扩充这个类,创建一个具有扩充的功能的类。
3、代码重用:由于功能是被封装在类中的,并且类是作为一个独立实体而存在的,提供一个类库就非常简单。
C++特性:抽象,封装,继承,多态
2. const 有什么用途
1:定义只读变量,即常量
2:修饰函数的参数和函数的返回值
3: const修饰函数的定义体,这里的函数为类的成员函数,承诺在本函数内部不会修改类内的数据成员,不会调用其它非 const 成员函数(也就是被const修饰函数定义体的函数)。( 类体外定义的 const 成员函数,在定义和声明处都需要 const 修饰符。);一个函数名字后有const,这个函数必定是类的成员函数,也就是说普通函数后面不能有const修饰;
const修饰函数的定义体--------类体外定义的 const 成员函数,在定义和声明处都需要 const 修饰符4. const 修饰类的成员变量,表示成员常量,不能被修改。
5. 如果 const 构成函数重载,const 对象只能调用 const 函数,非 const 对象优先调用非 const 函数。
6. const 函数只能调用 const 函数。非 const 函数可以调用 const 函数。
7. const 类成员变量只可以初始化列表中初始化
3. 指针和引用的区别
1. 指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用仅是个别名;
2. 用只能在初始化时被赋值,其他时候值不能被改变,指针的值可以在任何时候被改变
3. 引用不能为NULL,指针可以为NULL
4. 引用变量内存单元保存的是被引用变量的地址,从内存分配上看:程序为指针变量分配内存区域,而引用不需要分配内存区域(因为变量名本身是不占内存的,确切来说变量名在 运行期间 是不占内存的,在 编译期间 是占内存的,而我们在这里指的是运行期)
5. “sizeof 引用" = 指向变量的大小 , "sizeof 指针"= 指针本身的大小
6. 引用可以取地址操作,返回的是被引用变量本身所在的内存单元地址
7. 引用使用在源代码级相当于普通的变量一样使用,做函数参数时,内部传递的实际是变量地址
8. 引用没有 const,指针有 const;
9. 指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的)
(1)引用的底层实现:
虽然从底层来说,引用的实质是指针(确切来说是常量指针),但是从高层语言级别来看,我们不能说引用就是指针,他们是两个完全不同的概念。因为从语言级别上,指针和引用没有关系,引用就是另一个变量的别名。对引用的任何操作等价于对被引用变量的操作。
(1)引用在内存中也会分配空间(引用变量的确存放的是被引用对象的地址,只不过,对于高级程序员来说是透明的,编译器屏蔽了引用和指针的差别),空间中存放的是被引用变量的地址,因此可以将引用看作为一个常量指针ptr;
(2) 对引用取地址操作,其实是对被引用变量取地址,编译器将对引用取地址解释为&(*ptr)取地址
(2)函数传值、传引用、传指针区别
传值:传值无非就是实参拷贝传递给形参,单向传递(实参->形参),赋值完毕后实参就和形参没有任何联系,对形参的修改就不会影响到实参。
传指针:传指针是把实参地址的拷贝传递给形参。还是一句话,传指针就是把实参的地址复制给形参。复制完毕后实参的地址和形参的地址没有任何联系,对实参形参地址的修改不会影响到实参, 但是对形参地址所指向对象的修改却直接反应在实参中,因为形参指向的对象就是形参的对象。
传引用:传引用本质没有任何实参的拷贝,一句话,就是让另外一个变量也执行该实参。就是两个变量指向同一个对象。这是对形参的修改,必然反映到实参上。
4. C++中有了malloc / free , 为什么还需要 new / delete
1,malloc与free是C++/C语言的标准库函数( void *malloc(size_t size); void free(void *ptr); ),new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
2,对于非内部数据类型的对象而言,光用maloc/free 无法完成对动态对象的内存管理(因为内部数据类型对象没有构造与析构的过程)
对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
3,因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。
5. 编写类String 的构造函数,析构函数,拷贝构造函数和赋值函数 (即自己实现一个string类(重点))
是考验C++基础知识的好题。至少要能实现以下:构造函数,析构函数,拷贝构造函数(copy constructor),重载赋值操作符(copy assignment operator)。
重载 = 运算符重载 + 运算符
6. 多态的实现
多态用虚函数结合动态绑定来实现
C++中有两种多态,称为动多态(运行期多态)和静多态(编译期多态),而静多态主要通过模板来实现,宏也是实现静多态的一种途径。动多态在C++中是通过虚函数实现的,即在基类中存在一些接口(一般为纯虚函数),子类必须重载这些接口。这样通过使用基类的指针或者引用指向子类的对象,就可以实现调用子类对应的函数的功能。动多态的函数调用机制是执行期才能进行确定,所以它是动态的。
包含虚函数的类才会有虚函数表, 同属于一个类的对象共享虚函数表, 但是有各自的_vptr. 虚函数表实质是一个指针数组,里面存的是虚函数的函数指针。(这样的基类和子类都有自己的虚函数表)
多继承7. 单链表的逆置
看我自己的 单链表反转 - 简书
8. 堆和栈的区别
(1)堆 栈空间分配
1. 管理方式不同
栈,由编译器自动管理,无需程序员手工控制;堆:产生和释放由程序员控制。
2. 空间大小不同
栈的空间有限;堆内存可以达到4G,。
3. 能否产生碎片不同
栈不会产生碎片,因为栈是种先进后出的队列。堆则容易产生碎片,多次的new/delete
会造成内存的不连续,从而造成大量的碎片。
4. 生长方向不同
堆的生长方式是向上的,也就是向着内存地址增加的方向,栈是向下的,也就是向着内存地址减小的方向。
5. 分配方式不同
堆是动态分配的。栈可以是静态分配和动态分配两种,静态分配是编译器完成的,比如局部变量的分配。动态分配由函数 alloca 进行分配( int *p = (int *)alloca(sizeof(int)*10); )。不过栈的动态分配和堆不同,他的动态分配是由编译器进行释放,无需我们手工实现
6. 分配效率不同
栈是机器系统提供的数据结构,计算机底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令。堆则是由C/C++函数库提供,库函数会按照一定的算法在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
l 堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家 尽量用栈,而不是用堆。
l 栈和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。
l 无论是堆还是栈,都要防止越界现象的发生。
9. 不调用C/C++ 的字符串库函数,编写strcpy
char * strcpy(char * strDest,const char * strSrc) {
if ((strDest==NULL)||strSrc==NULL))
return NULL;
char * strDestCopy=strDest;
while ((*strDest++=*strSrc++)!='\0'); //先赋值给*strDest, 然后比较*strDest与'\0'的是否相等,
return strDestCopy; //赋值表达式返回左 操作数,所以在赋值'\0'后,循环停止。
}
额外提一下:strcpy和memcpy主要有以下3方面的区别。
1)、复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。
2)、复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。
3)、用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy
10. 关键字static的作用
1. 函数体内 static 变量的作用范围为该函数体,不同于 auto 变量, static 变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值
2. 在模块内的 static 全局变量可以被模块内所有函数访问,但不能被模块外其他函数访问(模块由头文件和实现文件组成)
3. 在类的static 成员变量属于整个类所拥有,对类的所有对象只有一份拷贝
4. 在类中的 static 成员函数属于整个类所拥有,这个函数不接收 this 指针,因而只能访问类的 static 成员变量,
也就是说,static成员函数只能访问类中的static 成员变量,其他的成员变量不能访问。
5. static成员变量需要在类外进行初始化与定义,声明可以在类内部
介绍它很重要的一条:隐藏(static函数,static变量均可)
当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。
举例来说明。同时编译两个源文件,一个是a.c,另一个是main.c。
为什么在a.c中定义的全局变量a和函数msg能在main.c中使用?
前面说过,所有未加static前缀的全局变量和函数都具有全局可见性,其它的源文件也能访问。此例中,a是全局变量,msg是函数,并且都没有加static前缀,
因此对于另外的源文件main.c是可见的。
如果加了static,就会对其它源文件隐藏。例如在a和msg的定义前加上static,main.c就看不到它们了。
利用这一特性可以在不同的文件中定义同名函数和同名变量,而不必担心命名冲突。static可以用作函数和变量的前缀,对于函数来讲,static的作用仅限于隐藏
11. 在c++程序中调用被C编译器编译后的函数,为什么要加extern“C”
C++语言支持函数重载,C语言不支持函数重载,函数被C++编译器编译后在库中的名字与C语言的不同,
假设某个函数原型为:
void foo(int x, inty);
该函数被C编译器编译后在库中的名字为: _foo
而C++编译器则会产生像: _foo_int_int 之类的名字。
为了解决此类名字匹配的问题,C++提供了C链接交换指定符号 extern "C"。
12. 头文件中的ifndef/define/endif 是干什么用的
防止头文件被重复包含
13. 什么时候要用虚析构函数?什么时候要用拷贝构造函数?
一. 什么时候用虚析构函数
通过基类的指针来删除派生类的对象时,基类的析构函数应该是虚的。否则其删除效果将无法实现。
一般情况下,这样的删除只能够删除基类对象,而不能删除子类对象,形成了删除一半形象,从而造成内存泄漏。
原因:在公有继承中,基类对派生类及其对象的操作,只能影响到那些从基类继承下来的成员。
如果想要用基类对非继承成员进行操作,则要把基类的这个操作(函数)定义为虚函数。
那么,析构函数自然也应该如此:如果它想析构子类中的重新定义或新的成员及对象,当然也应该声明为虚的。 调用析构函数后,该对象的虚函数指针,会指向基类的虚函数表
注意: 如果不需要基类对派生类及对象进行操作,则不能定义虚函数(包括虚析构函数),因为这样会增加内存开销。
二. 什么时候要用拷贝构造函数
拷贝构造函数也是一种构造函数,它的功能是使用传入对象的值生成一个新的对象的实例。调用的是拷贝构造函数还是赋值运算符,主要是看是否有新的对象实例产生
(1)将新对象初始化为一个同类对象时 (2)将对象以值传递的方式传给函数时 (3)对象作为函数的返回值,以值的方式从函数返回时 (4)编译器生成临时对象时
14. c++怎样让返回对象的函数不调用拷贝构造函数
拷贝构造函数前加 “explicit” 关键字;这样拷贝构造函数就不会进行自动类型转换,也就不会实现拷贝构造函数的调用了。
explicit主要是用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换,只能以显示的方式进行类型转换。类构造函数默认情况下声明为隐式的即implicit。
explicit关键字只能用于类内部的构造函数声明上,而不能用在类外部的函数定义(函数实现)上,它的作用是不能进行隐式转换;explicit关键字作用于单个参数的构造函数,如果构造函数有多个参数,但是从第二个参数开始,如果各参数均有默认赋值,也可以应用explicit关键字。
可以由单个实参来调用的构造函数定义了一个从形参类型到该类类型的隐式转换。编译器在试图编译某一条语句时,如果某一函数的参数类型不匹配,编译器就会尝试进行隐式转换,如果隐式转换后能正确编译,编译器就会继续执行编译过程,否则报错。
15.请用简单的语言告诉我C++ 是什么?
答:C++是在C语言的基础上开发的一种面向对象编程语言,应用广泛。C++支持多种编程范式 --面向对象编程、泛型编程和过程化编程。 其编程领域众广,常用于系统开发,引擎开发等应用领域,是最受广大程序员受用的最强大编程语言之一,支持类:类、封装、重载等特性!
16.什么是面向对象(OOP)?
答:面向对象是一种对现实世界理解和抽象的方法、是通过将需求要素转化为对象进行问题处理的一种思想。面向对象的三个基本特征是:封装、继承、多态。
(1)封装:封装是实现面向对象程序设计的第一步,封装就是将数据或函数等集合在一个个的单元中(我们称之为类)。封装的意义在于保护或者防止代码(数据)被我们无意中破坏。
(2)继承:继承主要实现重用代码,节省开发时间。子类可以继承父类的一些东西。
17.什么是多态?
答:有了虚函数,基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向派生类对象时就使用派生类的成员。换句话说,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,我们将这种现象称为多态(Polymorphism)。
18.设计模式举个例子(单例模式,工厂模式) 设计模式 - CSDN博客
答:设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。
比如 单例模式,是一种常用的软件设计模式,在它的核心结构中值包含一个被称为单例的特殊类。一个类只有一个实例,即一个类只有一个对象实例,并提供一个访问它的全局访问点。
单例模式可以分为 懒汉式 和 饿汉式:
(1)懒汉式单例模式:在类加载时不初始化。
(2)饿汉式单例模式:在类加载时就完成了初始化,所以类加载比较慢,但获取对象的速度快。
适用于:当类只能有一个实例而且客户可以从一个众所周知的访问点访问它时;当这个唯一实例应该是通过子类化可扩展的,并且客户应该无需更改代码就能使用一个扩展的实例时。
比如工厂模式,定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method 使一个类的实例化延迟到其子类。
适用于:当一个类不知道它所必须创建的对象的类的时候;当一个类希望由它的子类来指定它所创建的对象的时候;当类将创建对象的职责委托给多个帮助子类中的某一个,并且你希望将哪一个帮助子类是代理者这一信息局部化的时候。
19.STL库用过吗?常见的STL容器有哪些?
答:STL包括6部分内容:C++ STL 一般总结 - as_ - 博客园
(1)容器(containers):是一种数据结构容器,使用类模板的方式提供,我们可以方便的进行数据的存储操作。
(2)适配器(adapters):以序列式容器为基础,提供的栈,队列和优先级队列的这种容器。
(3)迭代器(iterators):类似于指针,用来操作容器的对象。
(4)算法(algorithm):包含一系列的常见算法。
(5)空间配置器(allocator):其中主要工作包括两部分:1,对象的创建与销毁。2,内存的创建与释放。
(6)仿函数(functor):仿函数又称为函数对象,其实就是重载了()操作符的struct,没有什么特别的地方。
容器,即存放数据的地方。比如array等。
在STL中,容器分为两类:顺序容器和关联式容器。
顺序容器,其中的元素不一定有序,但都可以被排序。如:vector、list、deque、stack、queue、heap、priority_queue、slist;
关联式容器,内部结构基本上是一颗平衡二叉树。所谓关联,指每个元素都有一个键值和一个实值,元素按照一定的规则存放。如:RB-tree、set、map、multiset、multimap、hashtable、hash_set、hash_map、hash_multiset、hash_multimap。
vector:它是一个动态分配存储空间的容器。区别于c++中的array,array分配的空间是静态的,分配之后不能被改变,而vector会自动重分配(扩展)空间。
set:其内部元素会根据元素的键值自动被排序。区别于map,它的键值就是实值,而map可以同时拥有不同的键值和实值。
算法,如排序,复制……以及个容器特定的算法。这点不用过多介绍,主要看下面迭代器的内容。
迭代器是STL的精髓,我们这样描述它:迭代器提供了一种方法,使它能够按照顺序访问某个容器所含的各个元素,但无需暴露该容器的内部结构。它将容器和算法分开,好让这二者独立设计。
20. 类的static变量在什么时候初始化?函数的static变量在什么时候初始化?
答:类的静态成员变量在类实例化之前就已经存在了,并且分配了内存。函数的static变量在执行此函数时进行初始化。
21.什么是内存泄漏?面对内存泄漏和指针越界,你有哪些方法?你通常采用哪些方法来避免和减少这类错误?
答:用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元即为内存泄露。
使用的时候要记得指针的长度。
malloc的时候得确定在那里free. new完之后一定要delete ,如果是数组的话还得要 delete[ ]
对指针赋值的时候应该注意被赋值指针需要不需要释放.
动态分配内存的指针最好不要再次赋值.
可以使用智能指针来避免内存泄漏的问题
22. new和malloc的区别?
答:1、malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
2、对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。
3、由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。
4、C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。
5、new可以认为是malloc加构造函数的执行。new出来的指针是直接带类型信息的。而malloc返回的都是void指针。
备注:new的时候,其实做了两件事,一是:new调用了operator new(size_t size)分配内存,相当于malloc分配所需内存的功能; 二是:调用构造函数。
delete的时候,也是做了两件事,一是:调用析构函数,二是:调用 operator delete(void * mem)释放内存。
23. 解释C++中静态函数和静态变量?
答:(1)类静态数据成员在编译时创建并初始化:在该类的任何对象建立之前就存在,不属于任何对象,而非静态类成员变量则是属于对象所有的。类静态数据成员只有一个拷贝,为所有此类的对象所共享。
(2)类静态成员函数属于整个类,不属于某个对象,由该类所有对象共享。
1、static 成员变量实现了同类对象间信息共享。
2、static 成员类外存储,求类大小,并不包含在内。
3、static 成员是命名空间属于类的全局变量,存储在 data 区的rw段。
4、static 成员只能类外初始化,但是可以在类内部声明。
5、可以通过类名访问(无对象生成时亦可),也可以通过对象访问。
静态成员函数
1、静态成员函数不能使用this指针,只能访问静态成员变量,静态成员函数中是不能调用非静态成员的,包括 非静态成员函数 和 非静态成员变量。 (如果要访问非静态成员变量的话,只能访问某一个对象的非静态成员变量和静态成员函数,可以传一个对象的指针,引用等参数给这个静态成员函数。)
2.在非静态成员函数中是可以调用静态成员函数的,因为静态成员函数属于类本身,在类的对象产生之前就已经存在了
原因:非静态成员函数,在调用时 this指针时被当作参数传进。而静态成员函数属于类,而不属于对象,不能使用 this 指针。
3、静态成员函数的意义,不在于信息共享,数据沟通,而在于管理静态数据成员,完成对静态数据成员的封装。
24. 说下你对内存的了解?
1. 栈 - 由编译器自动分配释放
2. 堆 - 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收
3. 全局区(静态区),全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,(C的话未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,C++是不区分的)- 程序结束释放
4.常量存储区:存储常量,内容不允许更改, 程序结束释放。
5 自由存储区: C++中由new创建,由deletel释放的动态内存单元。
在函数体中定义的变量通常是在栈上,用malloc, calloc, realloc等分配内存的函数分配得到的就是在堆上。在所有函数体外定义的是全局量,加了static修饰符后不管在哪里都存放在全局区(静态区),在所有函数体外定义的static变量表示在该文件中有效,不能extern到别的文件用,在函数体内定义的static表示只在该函数体内有效。另外,函数中的"adgfdf"这样的字符串存放在常量区。
25. C++ 字符串操作函数(数组和字符串的函数是最常问的,非常多,一定不要记混了)
c/c++中sizeof()、strlen()、length()、size()详解和区别 - CSDN博客
string str;
(1)c++中的size()和length()没有区别; 字符串长度 len = str.length() 和 len = str.size() 都不计算字符串末尾的 '/0' ;
sizeof()是运算符,只能用char*做参数, 计算数组末尾的 '/0' ;strlen是函数,不计算字符数组末尾的 '/0' ;
strlen()只能用char*做参数,且必须是以''\0''结尾的strlen(char *); strlen()只能用char*做参数,且必须是以''\0''结尾的。
(2)字符串比较 可以直接比较, 也可以: str1.compare(str2);
str1.compare(str2);(3).附加
str1 += str2; 或 str1.append(str2); str1.append(str2.pos2,len2);
(4). 字符串提取
str2 = str1.substr(); str2 = str1.substr(pos1); str2 = str1.substr(pos1,len1);
假设:string s = "0123456789";
string sub1 =s.substr(5);//只有一个数字5表示从下标为5开始一直到结尾:sub1 = "56789"
string sub2 =s.substr(5, 3);//从下标为5开始截取长度为3位:sub2 = "567"
(5). 字符串搜索
int where = str1.find(str2);
int where = str1.find(str2,pos1); pos1是从str1的第几位开始,即数组下标
int where = str1.rfind(str2); 从后往前搜。
(6). 插入字符串
不是赋值语句。
str1.insert(pos1,str2);
str1.insert(pos1,str2,pos2,len2);
str1.insert(pos1,numchar,char); numchar是插入次数,char是要插入的字符。
(7). 替换字符串
str1.replace(pos1,str2);
str1.replace(pos1,str2,pos2,len2);
(8). 删除字符串
str.erase(pos,len); str.clear();
(9). 交换字符串
swap(str1,str2);
(10). C<==> C++
char *cstr = "Hello"; string str1; cstr = cstr; string str2(cstr); string.c_str(); to_string(int(a))
(11)字符串复制:
strcpy:将由source指针指示的C 字符串(包括结尾字符)复制到destination指针指示的区域中。该函数不允许source和destination的区域有重叠,当src指针指向为‘\0’时将会停止字符串的复制,同时,为了避免溢出,destination区域应该至少和source区域一样大。 strcpy只用于字符串复制,并且它不仅复制字符串内容之外,还会复制字符串的结束符;(是个深复制)
最终答案输出是 "ABC\0";而不是"ABC\0ef"strncpy:复制source的前num字符到destination。如果遇到null字符(’\0’),且还没有到num个字符时,就用(num - n)(n是遇到null字符前已经有的非null字符个数)个null字符附加到destination。注意:并不是添加到destination的最后,而是紧跟着由source中复制而来的字符后面。下面举例说明:
memcpy:将source区域的前num个字符复制到destination中。该函数不检查null字符(即将null字符当作普通字符处理),意味着将复制num个字符才结束。该函数不会额外地引入null字符,即如果num个字符中没有null字符,那么destination中相应字符序列中也没有null字符。同strcpy的区别:允许将source中null字符后面的字符也复制到destination中,而strcpy和strncpy则不可以。
26. 重写和重载区别
重载(Overloading):函数名相同,函数的参数个数、参数类型或参数顺序三者中必须至少有一种不同。函数返回值的类型可以相同,也可以不相同。发生在一个类内部。
重写(override):也叫做覆盖,一般发生在子类和父类继承关系之间。子类重新定义父类中有相同名称和参数的 虚函数。
重定义:也叫做隐藏,子类重新定义父类中有相同名称的非虚函数 (参数列表可以不同 ) ,指派生类的函数屏蔽了与其同名的基类函数。发生在继承中。
重写需要注意:
1、 被重写的函数不能是static的。必须是virtual的
2 、重写函数必须与被重写函数有相同的类型,名称和参数列表,返回的类型,否则不能称其为重写而是重载。
3 、重写函数的访问修饰符可以不同。尽管virtual是private的,派生类中重写改写为public,protected也是可以的
访问修饰符的限制一定要大于被重写方法的访问修饰符(public>protected>default>private)
重载的规则:
1)、必须具有不同的参数列表;
2)、可以有不同的返回类型,只要参数列表不同就可以了;
3)、可以有不同的访问修饰符;
4)、可以抛出不同的异常;
重定义规则如下:
a 、如果派生类的函数和基类的函数同名,但是参数不同,此时,不管有无virtual,基类的函数被隐藏。
b 、如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有vitual关键字,此时,基类的函数被隐藏(如果相同有Virtual就是重写覆盖了)。
27. C++类中成员变量的初始化总结
(1)普通的变量: 一般不考虑啥效率的情况下 可以在构造函数中进行赋值。考虑一下效率的可以再构造函数的初始化列表中进行。
(2)static 静态变量: 在类内部声明,但是必须 在类的外部进行定义和初始化。 static变量属于类所有,而不属于类的对象,因此不管类被实例化了多少个对象,该变量都只有一个。
(3)const常量:常量在类内部声明,但是定义和初始化只能在构造函数的初始化列表进行。 (const常量需要在声明的时候即初始化)
static 静态变量,const常量的初始化(4)Reference 引用型变量(也就是&): 引用型变量和const变量类似,需要在创建的时候即进行初始化。也是在初始化列表中进行。但需要注意用Reference类型。但是可以在类内部初始化的。
(5)const static integral 变量: 对于既是const又是static 而且还是整形变量,C++是给予特权的(但是不同的编译器可能有会有不同的支持,VC 6好像就不支持)。可以直接在类的定义中初始化。short可以,但float的不可以哦。
总结起来,可以初始化的情况有如下四个地方:
1、在类的定义中进行的,只有const 且 static 且(int, short) 的变量。
2、在类的构造函数初始化列表中, 包括const对象和Reference对象。
3、在类的定义之外初始化的,包括static变量。因为它是属于类的唯一变量。
4、普通的变量可以在构造函数的内部,通过赋值方式进行。当然这样效率不高。
28.(1)为什么不能在构造函数中调用虚函数(2)构造函数为什么不能是虚函数
(1) 基类部分在派生类部分之前被构造,当基类构造函数执行时派生类中的数据成员还没被初始化。用一个基类指针指向一个子类对象,子类对象会先调用基类的构造函数,如果此时基类构造函数中的虚函数调用被解析成调用派生类的虚函数,而派生类的虚函数中又访问到未初始化的派生类数据,将导致程序出现一些未定义行为和bug。(同理析构函数中也是不能调用虚函数的,例如基类的析构函数中调用虚函数,派生类的析构函数在函数退出时先被调用,此时派生类已经没有内存资源了,再去调用基类的析构函数,此时如果析构函数中的虚函数被解析成派生类中的函数,也是不存在的。)
(2)虚函数对应一个指向vtable虚函数表的指针,这大家都知道,可是这个指向vtable的指针其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。
总结起来就是: 如果构造函数是虚函数,那么: 先要有虚函数表----->才能有虚构造函数; 但是问题在于???? 对象必须靠构造函数才能实现实例化------>实例化后才能有内存去存虚函数表; 这样实现的顺序完全相反了!所以不行!!!
构造函数为什么不能是虚函数 - lizezheng - 博客园
另外个原因是:虚函数的作用在于通过父类的指针调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针去调用,因此就规定构造函数不能是虚函数。
29. 指针函数和函数指针
指针函数:本质是一个函数。函数返回类型是某一类型的指针
格式: 类型标识符*函数名(参数表) int * f (x,y);
函数指针:是指向函数的指针变量,即本质是一个指针变量。
格式:类型说明符(*函数名)(参数) int (*f) (int x);
30.C++函数模板底层实现原理
函数模板:实际上是建立一个通用函数,其函数类型和形参类型不具体指定,用一个虚拟的类型来代表。这个通用函数就称为函数模板。 凡是函数体相同的函数都可以用这个模板来代替,不必定义多个函数,只需在模板中定义一次即可。在调用函数时系统会根据实参的类型来取代模板中的虚拟类型,从而实现了不同函数的功能。
使用函数模板能减少不必要的重复不使用的话会重复定义函数
函数模板和普通函数的区别:函数模板是不允许自动类型转换的,而普通函数允许自动类型转换
当函数模板和普通函数在一起时,调用规则如下:
(1)函数模板可以像普通函数一样被重载
(2)c++编译器优先考虑普通函数
(3)如果函数模板可以产生一个更好的匹配,那么选择模板
(4)可以通过空模板实参列表的语法,限定编译器只通过模板匹配(max<>(a,b); //显示使用函数模板方法,则使用<>空的类型参数化列表)
(5)函数模板不提供隐式的类型转换,必须是严格的匹配。
函数模板底层的实现模式:
编译器并不是把函数模板处理成能够处理任意类的函数;编译器从函数模板通过具体类型产生不同的函数;编译器会对函数模板进行两次编译,在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。
31. 智能指针:智能指针详解 - CSDN博客 (推荐)
C++11中智能指针的原理、使用、实现 - wxquare - 博客园
如果 func 函数能顺利执行到 delete 处当然是最理想的。如果由于某种原因导致函数在中途返回了,或者在还没执行到 delete 语句时发生了异常,那么就悲剧了,为指针pt分配的内存得不到释放,产生了一个经典的内存泄漏。如果 func 函数是一个执行频率较高的函数,那么就尴尬了…为了消除传统的内存分配方式上存在的隐患,C++提供了一些强大的智能指针模版类,
智能指针的核心思想是:将堆对象的生存期用栈对象(这个栈对象就是智能指针)来管理,当new一个堆对象的时候,立刻用智能指针来接管;具体做法是:在构造函数中对资源初始化(用一个指针指向堆对象),在析构函数中调用delete对资源进行释放。由于智能指针本身就是一个栈对象,它的作用域结束的时候,自动调用析构函数,从而调用了delete释放了堆对象。-------->这样一个堆对象的生存期就由栈对象来管理了。
智能指针:就是智能/自动化的管理指针所指向的动态资源的释放。它是一个类,有类似指针的功能。
从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。智能指针的作用是防止忘记调用delete释放内存和程序异常的进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决
智能指针典型的有着这四种: auto_ptr、 scoped_ptr、unique_ptr、share_ptr、 weak_ptr
1. auto_ptr类 (独占所有权,转移所有权 ,C++98引入的,带有很大缺陷不建议使用)
(1)auto_ptr没有使用引用计数,如果多个auto_ptr指向同一个对象,就会造成对象被删除一次以上的错误。因此一个对象只能由一个auto_ptr所拥有,在给其他auto_ptr赋值的时候,会转移这种拥有关系。所以,在赋值、参数传递的时候会转移所有权,因此不要轻易进行此类操作。
(2)auto_ptr的析构函数内部释放资源时调用的是delete而不是delete[],因此不要让auto_ptr托管数组
2. scoped_ptr (独占所有权,防拷贝)
scoped_ptr的实现原理是防止对象间的拷贝与赋值。具体实现是将拷贝构造函数和赋值运算符重载函数设置为保护或私有,并且只声明不实现,并将标志设置为保护或私有,防止他人在类外拷贝,简单粗暴,但是也提高了代码的安全性。
3.unique_ptr (独占所有权)
unique_ptr “唯一 ” 拥有其所指对象,同一时刻只能有一个unique_ptr实例指向给定对象(通过禁止拷贝、只有移动所有权move()函数来实现)。在出现异常的情况下,动态资源能得到释放。
unique_ptr本身的生命周期:从unique_ptr实例对象创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。
转移所有权创建unque_ptr和转移所有权
4.shared_ptr(共享所有权,引用计数)
shared_ptr允许多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。
shared_ptr 的实现原理是通过引用计数来实现,智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象指针指向同一对象,拷贝或赋值时将引用计数加1,析构时只有当引用计数减到0才释放空间,否则只需将引用计数减1即可.
shared_ptr的通用实现技术是使用引用计数(reference count)。智能指针类将一个计数器与类实例指向的对象相关联,引用计数跟踪该类有多少个对象的指针指向同一对象。每次创建类的新对象时,初始化指针就将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,析构函数减少引用计数(如果引用计数减至0,则删除基础对象)。
5. weak_ptr 关于shared_ptr与weak_ptr的使用 - CSDN博客
weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个引用的 shared_ptr智能指针对象, weak_ptr只是提供了对管理对象的一个访问手段.
weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 其实它的出现是伴随shared_ptr而来,尤其是解决了一个引用计数导致的问题:存在循环引用的时候会出现内存泄漏。
存在循环引用的时候会出现内存泄漏。它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少.
智能指针应用在多线程的时候要注意事项:
在多线程环境下,引用计数的更新存在安全隐患-------我们可以在改变引用计数的时候加上一把互斥锁,防止多线程带来的隐患
32. 保护继承和私有继承,虚继承
A继承B虚继承:是指一个指定的基类,在继承体系结构中,将其成员数据实例共享给也从这个基类型直接或间接派生的其它类。
虚拟继承与普通继承的区别:
假设derived 继承自base类,那么derived与base是一种“is a”的关系,即derived类是base类,而反之错误;
假设derived 虚继承自base类,那么derivd与base是一种“has a”的关系,即derived类有一个指向base类的vptr。
因此虚继承可以认为不是一种继承关系,而可以认为是一种组合的关系。
virtual base class的原始模型是在class object中为每一个有关联的virtual base class加上一个指针vptr,该指针指向virtual基类表。有的编译器是在继承类已存在的virtual table直接扩充导入一个virtual base class table。不管怎么样由于虚继承已完全破坏了继承体系,不能按照平常的继承体系来进行类型转换。
33. 可继承的类的实现需要注意什么问题(构造函数、析构函数)
(1) 当一个类作为基类时,它的析构函数应该为虚析构函数。防止基类指针无法调用子类析构函数,造成内存泄漏
(2)在子类调用父类构造函数的时候,如果父类没有默认的构造函数,则子类的构造函数应当显式地调用父类的自定义构造函数;换句话说:子类在构造时,如果没有显式调用父类的构造函数,会默认先调用父类的默认构造函数,如果此时父类没有默认的构造函数,就会报错了。
子类调用父类带参数的构造方法,需要用初始化父类成员对象的方式;例如图中Base( a )对基类构造函数初始化了在C++中,构造函数不会自动继承,只是如果子类没有写构造函数,那么系统会这个类自动添加一个默认构造函数,是一个空函数体,所以什么也没有做,接着就会调用父类的构造函数,再调用这个系统添加的子类默认构造函数。(但是很特殊的一点是,C++11里面 通过提供一条注明了直接基类的using声明语句来让派生类“继承”基类的构造函数的方法,这种方法要和常规的继承而来的方法区别开来。
class Son : public Base{
public:
using Base::Base; //继承了父类Base的构造函数了 ,using声明语句将令编译器生成代码,对于每个基类函数,编译器都 生成与之对应的派生类构造函数,形如 : derived(参数2) : base(参数1) { }
}
)
34. 什么是RAII 技术?
C++ 资源管理之 RAII - 静悟生慧 - 博客园 (推荐)
C++中的RAII介绍 - binbinneu - 博客园
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
实现原理:利用stack上的临时对象生命期是程序自动管理的这一特点,将我们的资源释放操作封装在一个临时对象中。
RAII 的一般做法是这样的:在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
(1)不需要显式地释放资源。
(2)采用这种方式,对象所需的资源在其生命期内始终保持有效。
资源管理技术的关键在于:要保证资源的释放顺序与获取顺序严格相反。
35. 深拷贝和浅拷贝
深拷贝(值拷贝):拷贝的是内容,主要是看复制过程的时候,如果资源重新分配,这个过程就是深拷贝
浅拷贝(位拷贝):拷贝的是地址,没有重新分配资源,就是浅拷贝
36. 拷贝构造函数什么时候需要重写
(1)当构造函数涉及到动态存储分配空间时,要自己写拷贝构造函数,并且要深拷贝。
(2)如果你需要定义一个非空的析构函数,那么,通常情况下你也需要定义一个拷贝构造函数。
37. placement new 操作符
C++中placement new操作符(经典) - CSDN博客
placement new :placement new允许在一个已经分配好的内存中构造一个新的对象。 在使用时需要我们传入一个指针,此时会在该指针指向的内存空间构造该对象,该指针指向的地址可以是堆空间,栈空间,也可以是静态存储区。
使用new操作符分配内存需要在堆中查找足够大的剩余空间,这个操作速度是很慢的,而且有可能出现无法分配内存的异常(空间不够)。placement new就可以解决这个问题,我们构造对象都是在一个预先准备好了的内存缓冲区中进行,不需要查找内存,内存分配的时间是常数;而且不会出现在程序运行中途出现内存不足的异常。所以,placement new非常适合那些对时间要求比较高,长时间运行不希望被打断的应用程序。
使用方法:
(1)缓冲区提前分配
可以使用堆的空间,也可以使用栈的空间,所以分配方式有如下两种:
class MyClass { … } ;
char *buf=new char [ N* sizeof( MyClass )+ sizeof( int ) ];
或者char buf [ N*sizeof(MyClass) + sizeof(int) ];
注意: 在C++标准中,对于placement operator new [] , 必须申请比原始对象大小多出sizeof(int)个字节空间来存放对象的个数,或者说数组的大小。
(2)对象的构造
MyClass * pClass=new(buf) MyClass;
这里的 new才是placement new,其实是没有申请内存的,只是调用了构造函数,返回一个指向已经分配好的内存的一个指针,所以对象销毁的时候不需要调用delete释放空间,但必须调用析构函数销毁对象。
(3) 对象的销毁
一旦这个对象使用完毕,你必须显式的调用类的析构函数进行销毁对象。但此时内存空间不会被释放,以便其他的对象的构造。
pClass -> ~MyClass();
(4)内存的释放
如果缓冲区在堆中,那么调用delete[] buf;进行内存的释放;如果在栈中,那么在其作用域内有效,跳出作用域,内存自动释放。
38. 对象池
深度剖析C++对象池自动回收技术实现 - 贺大卫 - 博客园
对象池------通过对象复用的方式来避免重复创建对象,它会事先创建一定数量的对象放到池中,当用户需要创建对象的时候,直接从对象池中获取即可,用完对象之后再放回到对象池中,以便复用。这种方式避免了重复创建耗时或耗资源的大对象,大幅提高了程序性能
39. 函数模板与类模板区别,函数模板与模板函数,类模板与模板类
(1)函数模板的实例化是由编译程序在处理函数调用时自动完成的,而类模板的实例化 必须由程序员在程序中显式地指定。 即函数模板的实例化允许隐式调用和显式调用 ,而类模板只能显示调用;
函数模板的隐式调用和显示调用,MaxValue是个函数函数模板
函数模板,实际上是建立一个通用函数,它所用到的数据的类型(包括返回值类型、形参类型、局部变量类型)可以不具体指定,而是用一个虚拟的类型来代替(实际上是用一个标识符来占位),等发生函数调用时再根据传入的实参来逆推出真正的类型。这个通用函数就称为函数模板(Function Template)。
在函数模板中,数据的值和类型都被参数化了,发生函数调用时编译器会根据传入的实参来推演形参的值和类型。换个角度说,函数模板除了支持值的参数化,还支持类型的参数化。
一但定义了函数模板,就可以将类型参数用于函数定义和函数声明了。说得直白一点,原来使用 int、float、char 等内置类型的地方,都可以用类型参数来代替。
类模板在实例化时必须显式地指明数据类型,编译器不能根据给定的数据推演出数据类型类模板在实例化时必须显式地指明数据类型,编译器不能根据给定的数据推演出数据类型
注意:模板头和类头是一个整体,可以换行,但是中间不能有分号。
一但声明了类模板,就可以将类型参数用于类的成员函数和成员变量了。换句话说,原来使用 int、float、char 等内置类型的地方,都可以用类型参数来代替。
(2)函数模板的重点是模板。表示的是一个模板,专门用来生产函数。(函数模拟板的实例化允许隐式调用和显式调用 )
模板函数的重点是函数。表示的是由一个模板生成而来的函数。
(3)类模板的重点是模板。表示的是一个模板,专门用于产生类的模子。(类模板的实例化 必须由程序员在程序中显式地指定)
模板类的重点是类。表示的是由一个模板生成而来的类。
40. 定义全局变量需要注意哪些
(1)在程序中定义了全局变量,但是需要在定义之前使用的话:这时在定义之前用extern关键字对其进行声明
(2)在一个cpp文件中定义了全局变量,需要在其他文件中使用:这时需要在其他文件中需要使用的地方之前用extern声明下
(1)(2)(3)在一个cpp文件中定义了全局变量,但是仅仅需要在本文件中使用该变量:这是需要在定义的时候加上static关键字
41. 静态库和动态库及其区别, C++部署动态库、静态库,程序如何去使用静态、动态链接库
(1)静态库和动态库
库(library),库是写好的现有的,成熟的,可以复用的代码。
静态库:链接时会完整地拷贝到可执行文件中去,被多次使用就有多份冗余拷贝。在链接的时候,随着目标文件(.obj)一起被链接器打包到最后生成的可执行文件中的库叫静态库。Windows上的静态库是.lib文件(但和dll文件的.lib文件是不同的)。
动态库:链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序可以共用,节省内存。在可执行程序被加载到内存中执行的时候,才会去加载的库叫做动态库。Widows上的动态库是dll文件(Dynamic Linked Library)。
静态库与动态库的区别:
1)静态库在程序的链接阶段被复制到了程序中,和程序运行的时候没有关系;动态库在链接阶段没有被复制到程序中,而是程序在运行时由系统动态加载到内存中供程序调用。
2)使用动态库系统只需载入一次动态库,不同的程序可以共享内存中相同的动态库的副本,相比于使用静态库节省了很多内存;而使用了静态库的程序会有多个副本在内存中时,它们所使用的库所占的内存也是多份,因此浪费空间。
3)在库需要升级的时候,使用动态库的程序只需要升级动态库就好(假设接口不变),而使用了静态库的程序则需要升级整个程序。
(2) C++部署动态库、静态库,
关于VS2013下制作和使用静态库和动态库 - 20145320周岐浩 - 博客园
在Windows操作系统中,Visual Studio使用lib.exe作为库的管理工具,负责创建静态库和动态库。
在静态库情况下,函数和数据被编译进一个二进制文件(通常扩展名为*.LIB), Visual C++的编译器在处理程序代码时将从静态库中恢复这些函数和数据并把他们和应用程序中的其他模块组合在一起生成可执行文件。这个过程称为"静态链接",此时因为应用程序所需的全部内容都是从库中复制了出来,所以静态库本身并不需要与可执行文件一起发行。
在动态库的情况下,有两个文件,一个是引入库(.LIB)文件,一个是DLL文件,引入库文件包含被DLL导出的函数的名称和位置,DLL包含实际的函数和数据,应用程序使用LIB文件链接到所需要使用的DLL文件,库中的函数和数据并不复制到可执行文件中,因此在应用程序的可执行文件中,存放的不是被调用的函数代码,而是DLL中所要调用的函数的内存地址,这样当一个或多个应用程序运行是再把程序代码和被调用的函数代码链接起来,从而节省了内存资源。从上面的说明可以看出,DLL和.LIB文件必须随应用程序一起发行,否则应用程序将会产生错误。
Window与Linux执行文件格式不同,在创建动态库的时候有一些差异:
在Windows系统下的执行文件格式是PE格式,动态库需要一个DllMain函数作为初始化的入口,通常在导出函数的声明时需要有_declspec(dllexport)关键字。
Linux下gcc编译的执行文件默认是ELF格式,不需要初始化入口,亦不需要函数做特别的声明,编写比较方便。
(3)程序如何去使用静态、动态链接库
静态链接库的使用 (利用 #pragma comment( ) )
动态链接库的使用
动态链接库的使用和静态链接库类似,唯一不同的是需要将myStaticLibTest.dll文件和项目编译生成的执行文件:.exe文件放在一起,一般位于解决方案的debug或release文件夹中。
42.debug和release的区别
vs中的程序有debug和release两个版本,Debug通常称为调试版本,通过一系列编译选项的配合,编译的结果通常包含调试信息,而且不做任何优化,它为开发人员提供强大的应用程序调试能力。而Release通常称为发布版本,是为用户使用的,一般客户不允许在发布版本上进行调试。所以不保存调试信息,同时,它往往进行了各种优化,以期达到代码最小和速度最优。为用户的使用提供便利。
debug程序通常比release程序要慢,尤其是处理视频图像方面release要比debug快很多。在release模式对程序进行调试的时候经常会遇到变量虽然初始化了,但是在查看其值的时候却发现是一个随机的数并不是初始化的值,有时候在对变量进行监视的时候了,会出现找不到变量的情况,原因大致如下:
debug跟release在初始化变量时所做的操作是不同的,debug是将每个字节位都赋成0xcc, 而release的赋值近似于随机。如果你的程序中的某个变量没被初始化就被引用,就很有可能出现异常:用作控制变量将导致流程导向不一致;用作数组下标将会使程序崩溃;更加可能是造成其他变量的不准确而引起其他的错误。所以在声明变量后马上对其初始化一个默认的值是最简单有效的办法,否则项目大了你找都没地方找。代码存在错误在debug方式下可能会忽略而不被察觉到。debug方式下数组越界也大多不会出错,在release中就暴露出来了,这个找起来就比较难了。
只有DEBUG版的程序才能设置断点、单步执行、使用 TRACE/ASSERT等调试输出语句。REALEASE不包含任何调试信息,所以体积小、运行速度快。
43. (1) main函数有没有返回值,分别针对什么情况?(2) main函数中的参数argc和argv含义及用法?(3) 如果出现异常,怎么捕获?(4)main函数一定是最先执行的吗?
(1) 一般C++要求函数返回值为int,如果函数内没有出现return语句的时候,编译器会在main()函数的末尾自动添加 return 0; 语句。
VS环境下mian函数的返回值可以是数值类型,如char,float,double或者long,但是绝不能是string这类不能强制转换成int的类型(VS环境下对main函数的返回值没有太严格的要求,只要可以强制转换为int的类型都可以作为返回值 )
(2)main函数的参数形式就三种: int main(); int main( int argc, char* argv[] );
int main(int argc, char** argv);
argc 是 argument count的缩写,表示传入main函数的参数个数;
argv 是 argument vector的缩写,表示传入main函数的参数序列或指针,并且第一个参数argv[0]一定是程序的名称,并且包含了程序所在的完整路径,所以确切的说需要我们输入的main函数的参数个数应该是argc-1个;
(3)C++异常捕获和处理 - CSDN博客 C++ 异常处理 | 菜鸟教程
1)异常的抛出和处理 C++ 异常处理涉及到三个关键字:try、catch、throw
第一点: try { //执行的代码,其中可能有异常。一旦发现异常,则立即跳到catch执行。否则不会执行catch里面的内容 }
第二点: catch(...)或者 catch(exception e) { //catch中的参数可以指定想要捕捉的异常类型,可以是exception, ExceptionName等,也可以使用...符号进行省略(省略的情况下就是能够处理 try 块抛出的任何类型的异常)除非try里面执行代码发生了异常,否则这里的代码不会执行 }
finally { //不管什么情况都会执行,包括try catch 里面用了return ,可以理解为只要执行了try或者catch,就一定会执行 finally }
第三点: throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。可以使用 throw 语句在代码块中的任何地方抛出异常。throw 语句的操作数可以是任意的表达式,表达式的结果的类型决定了抛出的异常的类型。
(4)实际上,所有的外部对象的构造函数都是先于main()函数执行的。如果要对类中的成员对象进行初始化,那么这些对象的构造函数也是在main()函数之前执行的。如果在这些构造函数中还调用了其他函数的话,就可以是更多的函数先于main()函数之前运行。因此main()函数不一定是C++程序的第一个被执行的函数。
44. C++写的动态链接库能不能直接给C用,为什么?
动态链接库(Dynamic Link Library, dll):其实dll文件就是一堆函数的集合,我们只不过是给普通函数的定义加点东西罢了
最前面的 extern "C" 是为了防止不同编译器间的差异的,它的意思是以C语言方式命名,作用是让编译器知道要以C语言的方式编译和连接封装函数。 其中_declspec为什么要加这个条件编译呢? 因为这种技术也可能会用在由C头文件产生出的C++文件中,这样使用是为了建立起公共的C和C++文件,也就是保证当这个文件被用做C文件编译时,可以去掉C++结构,也就是说,extern "C"语法在C编译环境下是不允许的。(1)用C++去调用C动态链接库:(不能直接去调用)
用C写的动态链接库需要在声明外部函数的时候加上 extern "C" 就可以让C++程序在进行调用的时候进行正确的编译--------在CDLL(用C写的动态链接库)中函数当然是使用C编译器的方式进行编译的,所以在调用程序中,在声明外部函数的时候,必须加上”C”,以使的这个C++程序,在编译的时候使用C编译的方法对这个外部函数声明进行编译,即直接使用函数名而不是一个经过处理的函数名,否则在编译运行的时候就会报链接错误。比如一个函数void fun(double d),C语言会把它编译成类似_fun这样的符号,C链接器只要找到该函数符号就可以链接成功,它假设参数类型信息是正确的。而C++会把这个函数编译成类似_fun_double或_xxx_funDxxx这样的符号,在符号上增加了类型信息,这也是C++可以实现重载的原因。
(2)用C去调用C++动态链接库:(不能直接去调用)
主要的思想就是将C++的动态库再封装一层,做一个中间接口库,对C++库进行二次封装,也就是说在封装的这一层编写C语言的函数接口,而接口中使用C++动态库提供的类;还是用到 extern "C"
45. 设计一个模板函数对常数类型输入返回1(深入------函数和类模板特例化,偏特化)
这里再进一步介绍 函数模板特例化 和 类模板的特例化与偏特化
特例化版本时,函数参数类型必须与先前声明的模板中对应的类型匹配,其中T为const char*。(1)函数模板特例化:必须为原函数模板的每个模板参数都提供实参,且使用关键字template后跟一个空尖括号对<>,表明将原模板的所有模板参数提供实参。
本质:特例化的本质是实例化一个模板,而非重载它。特例化不影响参数匹配。参数匹配都以最佳匹配为原则。例如,此处如果是compare(3,5),则调用普通的模板,若为compare(“hi”,”haha”)则调用特例化版本(因为这个cosnt char*相对于T,更匹配实参类型),注意,二者函数体的语句不一样了,实现不同功能。
注意:普通作用于规则使用于特例化,即,模板及其特例化版本应该声明在同一个头文件中,且所有同名模板的声明应该放在前面,后面放特例化版本。
(2)类模板特例化:原理类似函数模板,不过在类中,我们可以对模板进行特例化,也可以对类进行部分特例化。对类进行特例化时,仍然用template<>表示是一个特例化版本
按照最佳匹配原则,若T != sales_data,就用普通类模板,否则,就使用含有特定功能的特例化版本。类模板的部分特例化:不必为所有模板参数提供实参,可以指定一部分而非所有模板参数,一个类模板的部分特例化本身仍是一个模板,使用它时还必须为其特例化版本中未指定的模板参数提供实参。此功能就用于STL源码剖析中的traits编程。详见C++primer 628页的例子。(特例化时类名一定要和原来的模板相同,只是参数类型不同,按最佳匹配原则,那个最匹配,就用相应的模板)
特例化类中的部分成员:可以特例化类中的部分成员函数而不是整个类。
46. 左值和右值
左值:指的是既能够出现在等号左边也能出现在等号右边的变量(或表达式),指的是如果一个表达式可以引用到某一个对象,并且这个对象是一块内存空间且可以被检查和存储,那么这个表达式就可以作为一个左值。
右值:指的则是只能出现在等号右边的变量(或表达式),指的是引用了一个存储在某个内存地址里的数据。
a,b为左值,3,4为右值47. cpp大规模对象分配,生命周期控制的难点(并发情况下)?
智能指针控制生命周期,小对象可以参考java的Spring直接搞成只读的,配合Linux写时拷贝技术copy-on-write (只有进程空间的各段的内容要发生变化时,才将父进程的内容复制一份给子进程)和原子操作尽量把临界区弄小;大规模内存分配的解决方案,go语言的tcmalloc参考,给每个线程弄自己的内存缓存区。
48. 如何用让cpp程序输出他本身的代码
方法一:
利用system("type filename.cpp")输出程序本身的代码、
或者方法二:
fstream提供了三个类,用来实现c++对文件的操作。(文件的创建、读、写)fstream用法总结 C++ - CSDN博客
(1)ifstream -- 从已有的文件读 ; (2)ofstream -- 向文件写内容; (3)fstream -- 打开文件供读写 ;常用的错误判断方法:
读取数据过程中:
(1)>>操作符会忽略前面的空白符和换行符,但不会越过后面的换行符和空白符
(2)get()方法不会略过任何符号
(3)peek()方法预读取下一个字符(不管是何符号);
文件文件打开模式:
ios::in 读 ;ios::out 写;ios::app 从文件末尾开始写;ios::binary 二进制模式
ios::nocreate 打开一个文件时,如果文件不存在,不创建文件。
ios::noreplace 打开一个文件时,如果文件不存在,创建该文件
ios::trunc 打开一个文件,然后清空内容
ios::ate 打开一个文件时,将位置移动到文件尾 ;
常用的错误判断方法:
(1)good() 如果文件打开成功;(2)bad() 打开文件时发生错误;(3)eof() 到达文件尾
49.源代码到最后的可执行文件经历的过程 C++源文件到可执行文件的过程 - CSDN博客
1)预处理主要包含下面的内容:
a.对所有的“#define”进行宏替换;
b.处理所有的条件编译指令,比如“#if”,“#ifdef”,“#elif”,“#else”,“#endif”
c.处理“#include”指令,这个过程是递归的,也就是说被包含的文件可能还包含其他文件
d.删除所有的注释“//”和“/**/”
e.添加行号和文件标识
f.保留所有的“#pragma”编译器指令
经过预处理后的 .i 文件不包含任何宏定义,因为所有的宏已经被替换,并且包含的文件也已经被插入到 .i 文件中。
2)编译
编译的过程就是将预处理完的文件进行一系列词法分析,语法分析,语义分析及优化后生成相应的汇编代码文件(.s文件)
3)汇编
汇编器是将汇编代码转变成机器可以执行的代码,每一个汇编语句几乎都对应一条机器指令。最终产生 目标 文件 (.o文件或.obj文件)。
4)链接
链接的过程主要包括了地址和空间分配(Address and Storage Allocation)、符号决议(Symbol Resolution)和重定位(Relocation)
静态链接和动态连接区别 静态链接和动态链接的区别 - CSDN博客
静态链接把要调用的库函数直接链接到目标程序,成为可执行文件的一部分。换句话说,要调用的库函数在程序的exe文件中,该文件包含了运行时所需的全部代码。静态链接的缺点是当多个程序都调用相同的函数时,内存中会有多个这个函数的拷贝,所以浪费了内存资源。
动态链接所调用的库函数代码并没有拷贝到程序的可执行文件中,它仅仅在exe文件中加入了调用的函数所在文件模块(DLL)和调用函数在文件中的位置等信息链接进目标程序,仅当应用程序被装入内存开始运行时,才从DLL中寻找相应函数代码,因此需要相应DLL文件的支持
50.sizeof与strlen的区别
sizeof与strlen的区别 - luori - 博客园
51.虚函数表会随着对象生成而复制吗?
不会的,包含虚函数的类才会有虚函数表, 同属于一个类的对象共享虚函数表, 但是有各自的_vptr.
虚函数表实质是一个指针数组,里面存的是虚函数的函数指针。
52. C 如何模拟C++的多态特性
我们知道在C语言中是没有class类这个概念的,但是有struct结构体,我们可以考虑使用struct来模拟;但是在C语言的结构体内部是没有成员函数的,如果实现这个父结构体和子结构体共有的函数呢?我们可以考虑使用函数指针来模拟。但是这样处理存在一个缺陷就是:父子各自的函数指针之间指向的不是类似C++中维护的虚函数表而是一块物理内存,如果模拟的函数过多的话就会不容易维护了。C语言模式实现C++继承和多态 - CSDN博客
53.memset函数的作用,有哪些参数
void *memset(void *s, int c, size_t n)
总的作用:将已开辟内存空间 s 的首 n 个字节的值设为值 c。
如果要把一个char a[20]清零,一定是 memset(a,0,20*sizeof(char));
memset:作用是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法
54. C/C++中volatile关键字详解
volatile 和 const 是相对应的,const代表所修饰的变量是不变的,volatile代表所修饰的变量是随时都可能会变化的。
volatile 指出 i 是随时可能发生变化的,每次使用它的时候必须从 i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在 b 中。而优化做法是,由于编译器发现两次从 i读数据的代码之间的代码没有对 i 进行过操作,它会自动把上次读的数据放在 b 中。而不是重新从 i 里面读。这样以来,如果 i是一个寄存器变量或者表示一个端口数据就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问。注意,在 VC 6 中,一般调试模式没有进行代码优化,所以这个关键字的作用看不出来。55.如何让C++类不能被继承
用C++实现一个不能被继承的类 - kaizen - 博客园
(1)利用static 属性定义静态方法,在方法内部实现一个对象,然后返回它的指针
(2)将构造函数和析构函数设置成私有或者保护属性