<>-1. C++中通过基类指针分别指向基类对象以及派生类对象时,调用虚函数以及同名的普通函数的情况。
只有虚函数(非缺省参数)才使用的是动态绑定,其他的全部是静态绑定。
eg: BaseClass *p=new DerivedClass(); 1.
静态类型:指编译时静态绑定的类型,比如上述指针,静态类型就是左侧BaseClass。 2.
动态类型:是运行阶段动态绑定的,比如上述指针的动态类型就是右侧的类型DerivedClass。
在这里,涉及的知识点是绑定问题,静态绑定和动态绑定。
1)静态绑定:非virtual成员的绑定是静态的,外加缺省成员函数的绑定(该缺省成员函数可以为virtual类型的)。此时它调用的成员都是跟变量(指针)的静态类型一致的。即根据对象的静态类型选择函数。
2)动态绑定:即非缺省函数的virtual的成员函数是动态绑定的,调用它的指变量的动态类型决定了所调用的成员函数。
<>0. 基础知识点:
C++中整个编译的过程分为:
1)预处理阶段:将头文件拷贝过来,将替换所有的宏。
2)编译阶段:词法分析/语法分析/语义分析/中间代码的生成/代码的优化—>汇编程序
3)汇编阶段:汇编程序–>机器语言
4)链接:目标代码块连接成可执行文件(静态/动态)
5)执行
<>@C++中的死循环:
1)for循环中判断语句(S2)的缺失或者while循环中判断语句永远为true,造成死循环问题。
2)逻辑错误:比如在while循环体或者for循环体内部出现循环变量赋值的问题。
for(int i=0;i<10;i++){ if(i=5){ //逻辑等==写成了运算赋值符= cout<<i<<endl; } }
3)无穷递归造成的循环:递归一定要有递归终止条件,如果没有递归终止条件只有递归体,那么就会无穷的递归,最终甚至会造成栈溢出。
4)假死循环:看着是死循环,但是由于某些性质,经过长时间之后会结束循环。
eg:i=65535是最大值。
for(unsigned short int i=1;i!=0;i++){ cout<<i<<<endl; }
<>0. 关于运行时错误和编译错误的问题:
1)编译错误:一般就是词法错误、语法错误、语义错误。比如关键字错误、类型不匹配(int数据赋string类型的值),没有逗号结尾等等。
2)运行时错误:可能是由于不够仔细代码编写或者输入数据出问题,比如数组下标访问越界,除0错,栈溢出。还有可能是由于程序复杂出现逻辑错误,使得程序执行结果出现问题。
<>1. 关于const和static以及普通的成员变量初始化的问题:
1)static是属于类级别的,不是对象级别的变量,所以在类内是声明,初始化只能放到类外进行(并且在类外不能加static关键字,类内声明的时候加即可。)
2)const类型的成员变量只能直接初始化,不可以先声明之后再赋值!所以可以在类内直接初始化或者通过构造函数的初始化列表(加快效率)进行初始化。绝对不可以在类外(它属于对象级别的变量),或者在构造函数体内部进行二次赋值!
3)普通的成员变量可以在类内初始化或者在构造函数内部或者初始化列表中进行初始化,但是一般为了效率可以选择在初始化列表进行,比在构造函数内部快。
<>2. 初始化列表涉及的问题以及多继承实例化子类时构造函数的执行顺序问题:
1)对于初始化列表:如其名,初始化成员变量的列表,初始化!!!(就是声明时立即开辟空间赋予初值)。
在这里初始化成员变量的顺序跟初始化列表中赋值的顺序完全无关,只跟类内部变量声明的顺序有关!!!!!!
2)多继承实例化子类时构造函数的执行顺序问题:跟上面的问题一模一样,
跟你构造函数初始化列表中给初始化父类成员的顺序完全无关(父类构造函数有参时,必须在子类构造函数初始化列表内显示传参),跟子类继承父类时声明所继承的父类的顺序一样!!!!!!
一般为了在时间和空间上同时节省资源,我们调用函数时使用引用传递,为了安全性考虑,还可以使用const类型的引用传递,方式引用修改变量值。
<>3. 会调用复制构造函数(拷贝构造函数)的情况
一定是用该类型的对象去初始化一个本类型的对象的时候(即用一个已存在的对象去初始化一个对象!)
1)A a(b);或者在初始化列表中这样去初始化其他class类型的成员变量。
2)function test(A b); 作为实参会初始化形参(但是如果形参是引用类型的,就不会调用,此时形参只是别名而已!)。
3)在函数内return
A,此时A作为该函数内的局部变量,会在函数运行结束后自动调用析构函数,所以在返回值的时候会调用复制构造函数用A初始化相同的一个对象返回。
A Reuse(A a, A b) { A A1; A1.b= a.b + b.b; return A1; } int main() { int a = 10
; int b = 100; A A1; A A2; A A3; A3=Reuse(A1, A2); return 0; } class A { public:
int b = 10; //类内可以赋初值的有普通变量和常量! public: A(){ //注意,这是初始化列表,一个成员初始化只有一次!!! cout <<
"Construct A!" << endl; } A(A& p){ b = p.b; cout << "Copy A!" << endl; } ~A() {
cout<< "Destruct A!" << endl; } A& operator=(const A& p1) { cout << "operate =!"
<< endl; this->b = p1.b; return *this; } };
<>3. 关于this指针的问题:
1)每个非静态的成员函数中都有一个this指针,指向调用它们的对象。当一个对象调用它的非静态成员函数时,会自动的将该对象的地址赋值给该成员函数的this指针,之后隐式的通过this指针去操作该对象的成员数据。
2)非静态非const类型的成员函数:this指针式 ClassName* const this,即this指针是常量类型的,不可以更改this指针的值。
3)非静态但是为const类型的成员函数:const ClassName* const
this,即既是常量类型的指针又是指针类型的常量,既不能修改指针的值,又不能修改指针指向的对象(即const成员函数不可以修改成员函数的值,也不可以访问非const类型的成员函数,因为非const类型的成员函数是有权限修改成员变量的)。
<>4. 如何定义一个只能在堆上(栈上)生成对象的类?:
1)仅在堆上生成,那么就以为着系统不能进行自主的构造和析构在栈上实例化的对象,可以将类的析构函数private化,每次系统想在栈上实例化一个对象前都会检查该类的析构函数是否可访问,不可访问就不行。
2)仅在栈上生成,那么就是不能通过new去产生一个对象。即不能够直接调用该类的构造函数,将构造函数私有化。然后用单例模式的思想产生对象即可。
<>1. C++ 的STL中的vector/map/set中增删元素对迭代器的影响?
<>(1)对于底层是连续存储实现的vector来说,删除元素和插入元素都会使得当前迭代器以及之后的都失效,可以通过 erase(删除位置的指针)
返回下一个数据的一个新的迭代器来继续遍历数据。插入元素失效的原因可以使因为push_back()操作使得vector容量不够进行扩容,寻找了新的地址,也可能是因为insert操作使得当前以及之后的迭代器均无效。依照C++标准,插入和删除位置之后的迭代器是应该失效的。
#include<stdlib.h> #include<iostream> #include<vector> using namespace std; int
main() { vector<int> test(10, -1); for (int i = 0; i < test.size(); i++) { cout
<< "test[" << i << "]: " << &test[i] << endl; } return 0; }
<>(2)对于set和map这种底层是非连续的实现的(即非连续存储结构),删除以及插入均只会使得当前迭代器失效,不会影响到后续的迭代器。
#include<stdlib.h> #include<iostream> #include<set> using namespace std; int
main() { set<int> test; for (int i = 0; i < 10; i++) { test.insert(i); } set<int
>::iterator iter= test.begin(); for (; iter != test.end();) { cout << *iter <<
endl; test.erase(iter++); //再删除之前必须使得其自增指向下一个元素的迭代器。 } cout << "test.length=" <<
test.size() << endl; return 0; } <>
1)map底层是用红黑树或者哈希表实现的。unordered_map是用哈希表实现的,当碰撞不多的时候查询速度非常的,但是碰撞比较多的时候到O(n)了。将其换成红黑树存储,虽然插入和删除的时间比较多,但是相比于冲突很多的HushMap而言,查询的速度可以优化到O(logn)。(频繁插入和删除更适合unorder_map,频繁检索更适合map)
<>2)set的底层实现是红黑树(链式存储)。
<>2. New/Delete和malloc/free的区别?
<>3. const和define的区别
<>(1)const
1. 编译器预处理的方式不同: define是在预处理阶段展开的,const常量是在编译运行的时候使用的。 2. 类型和安全性检查不同:
define宏没有类型,直接展开。const常量有具体的类型,在编译的时期需要进行类型检查。 3. 存储的方式不同:
define宏仅仅之展开,有多少地方使用就展开多少次,不分配内存。 const常量会在内存的常量存储区中为其分配内存。 4.
不能在类声明中初始化const数据成员。const数据成员的初始化只能在类构造函数的初始化表中进行。 5. const
可以节省空间,避免不必要的内存分配。const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝。 6.
提高了效率。编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。
提问:既然C++中有更好的const为什么还要使用宏?
答: const无法代替宏作为卫哨来防止文件的重复包含。
<>4. override重写和overload重载的区别
<>(1)override重写和overload重载
1.
override重写:在虚函数+继承的基础上实现的,子类对父类虚函数的重写(函数名、返回值类型、参数数目、类型、顺序完全一样,只是对函数体内操作进行修改),实现了多态。基类函数必须是虚函数,具有virtual关键字。
2.
overload重载:同一个类中的同名但具有不同数目的参数/参数类型不同/参数顺序不同的成员函数,或者不是类的成员函数而是普通的具有不同数目参数、不同类型参数、不同顺序参数的同名函数。函数重载不靠返回值区分,C++在编译阶段就可以根据同名函数参数的不同为他们重新分配函数名。
<>(2)重写可以改变访问修饰符
新知识点:重写可以改变访问修饰符,也就是说基类虚函数是public类型的,子类重写他的虚函数可以是private类型的,依旧可以实现多态,基类虚函数private类型,子类是public也行,没有那么必须从高到低或者从低到高点的限制,但是自己测试多态性的时候,肯定是
father*p=new
son(),所以如果基类的虚函数是private类型的,该指针将无法直接访问到,但是可以通过基类的其他public的成员函数去调用,依旧可以实现多态。
class A { private: int x; virtual void print() { cout << "Father_print()" <<
endl; } public: A(int x); A(A& p); ~A(); int getX(); A operator+(A temp); void
test() { print(); } }; class B :public A{ public: B():A(10){ } void print() {
cout<< "Son_print()" << endl; } }; int main() { A* a = new B(); a->test();
return 0; }
<>5. 堆和栈的区别?
<>(1)管理的方式
1. 堆上存放的数据是用户动态申请的数据,需要用户手动去释放空间,否则容易造成内训泄漏,比如new/malloc. 2.
栈上存放的是局部变量以及形参等数据,编译器自动管理,不需要用户操作。
<>(2)空间大小(栈要比堆小很多,所以算法题数据量很大的时候要注意)
1. 堆:对于32位的系统来说,堆存储空间可以接近4G,从这个角度来看堆内存几乎没有什么限制。 2.
栈:一般都有一定的空间大小,例如在VC6下面,默认的栈空间的大小约是1M。
<>(3)碎片问题
1. 堆由于长期进行new/delete以及malloc/free操作势必会造成内存空间的不连续,会造成大量的碎片,使得程序效率低下。 2.
对于栈而言,是编译器进行空间的管理(分配与回收),而且栈是一后进先出的线性表,永远都不会有一个内存块从中间弹出,所以碎片少。
<>(4)生长方向
1. 对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向。 2. 对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
<>(5)分配方式
1. 堆都是动态分配的,没有静态分配的堆。 2. 栈有2 种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配,动态分配由alloca
函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放,不需要我们手工实现。
<>(6)分配效率
1. 栈是机器系统提供的数据结构,计算机会在底层分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。 2.
堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,然后进行返回。显然,堆的效率比栈要低得多。
<>6. C++中的Static用法
<>(1)全局的static变量
1. 存储在全局/静态数据区,在编译阶段初始化,初始化的时候可以赋初值,如果没赋初值,系统会自动为它赋予一个初值,比如int型的就是0。 2.
程序运行结束后系统自动进行释放。 3. 在整个文件都是可见的,但在文件之外是不可见的。
<>(2)局部的static变量(普通函数内部定义的)
1.
存储在全局/静态数据区,但是拥有的是局部的作用域,出了函数就不可见了。在程序的运行过程中,它始终存储在全局/静态数据区中,直到程序结束,系统进行释放位置。
2.
在第一次运行拥有它的函数时进行初始化,此时如果没有赋予初值,系统会自动赋予其一个初值。在之后再次调用该程序将不会再次初始化,直接操作的是全局/静态存储区中该值。
<>(3)static函数
定义静态函数的好处: • 静态函数不能被其它文件所用;。 • 其它文件中可以定义相同名字的函数,不会发生冲突。
<>(4)static成员变量
1. 存储在全局的数据区。类中每个实例化的对象都共享这一个数据。即它只分配一次内存供该类所有对象使用。 2. 可以通过 类名::静态数据成员 或者 通过
实例化的对象名.静态数据成员 访问。 3. 在定义的时候分配内存空间,所以不能再类声明中定义。 4. 在没有产生类实例化对象的时候就可以通过类名进行访问。
5. • 同全局变量相比,使用静态数据成员有两个优势: 1) 静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其它全局名字冲突的可能性; 2)
可以实现信息隐藏。静态数据成员可以是private成员,而全局变量不能;
<>(5)static成员函数
1. 在类外定义时,不能够使用static关键字。 2. 可以访问静态成员(变量、函数),但是不能访问其他的成员变量和成员函数。 3.
由于没有this指针的额外开销,因此静态成员函数与类的全局函数相比速度上会有少许的增长。 4. 调用静态成员函数可以通过 类名::静态成员函数 或者 通过
实例化的对象名.静态成员函数 。
<>7. 子类、父类中的名称遮掩,如何避免?
1. 子类可以调用父类的同名函数,通过 父类名::父类成员函数(前提是可以访问到)。 2.
通过子类的实例化对象也可以调用。子类实例化对象.父类名::父类成员函数
<>8. 深拷贝,浅拷贝?
涉及到指针的时候。浅拷贝就是直接指针赋值,两个指针会指向同一个地址,那么一旦一个对象对指针指向的地址进行更改,对另一个对象而言,也会产生变动。深拷贝就是新生成一个指针,将copy对象指针指向的位置内的数据赋值给新生成的指针指向的对象。
浅拷贝和深拷贝主要区别就是复制指针时是否重新创建内存空间。 1)如果没有没有创建内存只赋值地址为浅拷贝,创建新内存把值全部拷贝一份就是深拷贝。
2)浅拷贝在类里面有指针成员的情况下只会复制指针的地址,会导致两个成员指针指向同一块内存,这样在要是分别delete释放时就会出现问题,因此需要用深拷贝。
<>9.新知识点:C++中的struct和class
<>C++中的struct和class最大的区别就是默认的访问访问权限不同。class默认private,但是struct默认public。
1. 他们两个都支持继承、多态、都能包含数据和方法。 2. 但是 “class”这个关键字还用于定义模板参
数,就像“typename”。但关键字“struct”不用于定义模板参数。 3.
C++中的struct和class最大的区别就是默认的访问访问权限不同。class默认private,但是struct默认public。 4.
说实话,除了上述两个不同之外,其他的几乎一模一样,但是还是想说,struct中的数据还是不会称之为成员,他本质还是一个复杂的数据结构,而我们的类就是一个封装的思想。
5.
甚至class可以继承struct,struct也可以继承class,默认继承的权限取决于子类,子类是class,那么就是private默认继承。子类是struct,那么就是public默认继承。
class test { public: int m, n; public: test(int m,int n) { this->m = m; this->n
= n; } test(test& p) { m = p.m; n = p.n; } ~test() { } virtual void print() {
cout<< "class_test" << endl; } }; struct student: test { int x; int y; public:
student(int x, int y):test(x,y) { this->x = x; this->y = y; } student(student& p
):test(p.m,p.n) { x = p.x; y = p.y; } ~student() { } private: int max(); void
print() { cout << "struct_student" << endl; } }; int student::max() { return x >
y? x : y; } int main() { test* a = new struct student(10, 20); a->print();
return 0; }
<>10.指针和引用的区别?
1. 本质: •指针:它是一个变量,保存地址。可以为NULL,非const类型的指针的值可以进行改变。
•引用:是一个变量的别名,必须在声明的时候初始化,绑定对象,且后续不能改变这个绑定。 2. sizeof()值的区别:
•sizeof(指针)是这个指针本身的大小,32位机就是4,64位机就是8。 •sizeof(引用)是绑定的原来的数据类型的大小。 3.
const常指针和常引用 指针有指针常量和常量指针:const int* p为
常量指针,指向常量的指针,指针的指向可以变化,但是不可以通过指针更改所指向的数据的值。 4. 作为函数参数进行传递时的区别:
•指针作为函数参数传递时,函数内部的指针形参是指针实参的一个副本,改变指针形参并不能改变指针实参的值,通过解引用*运算符来更改指针所指向的内存单元里的数据。
•引用在作为函数参数进行传递时,实质上传递的是实参本身,即传递进来的不是实参的一个拷贝,因此对形参的修改其实是对实参的修改,所以在用引用进行参数传递时,不仅节约时间,而且可以节约空间。
int main() { int b = 10; int* a =& b; int& c = b; cout << sizeof(a) << " 、" <<
sizeof(c) << endl; double m = 10; double* n = &m; cout << sizeof(n) << endl;
cout<< sizeof(m) << endl; return 0; }
<>11. C++中的强制类型转换
C++不是类型安全型的,两个不同类型的指针之间可以进行强制类型转换(reinterfret_cast)
<>(1)static_cast:可以在不同的基本类型之间、具有继承关系的两个类之间、不同类型的类指针之间进行转化。(但是基本类型的指针之间不可以!!!)
1)进行上行转换(把子类的指针或引用转换成基类表示)是安全的;
进行下行转换(把基类指针或引用转换成子类表示)时,由于没有动态类型检查,所以是不安全的。
2)在编译期强制转换。
#include<stdlib.h> #include<iostream> #include<string> using namespace std;
class A { private: int a; public: A(int a) { this->a = a; } virtual void print()
{ cout << "father_A_print()" << endl; } void test() { cout << "father_A: a= " <<
a<< endl; } }; class B:public A { private: int b; public: B(int a,int b):A(a) {
this->b = b; } void print() { cout << "son_B_print()" << endl; } void test() {
cout<< "son_B: b= " << b << endl; } }; int main(){
//static_cast:用于基本类型之间、有继承关系的类之间、类指针之间的转换。 //不能用于基本类型指针之间的转换。 int a = 10; int*
pa= &a; double b = 100.98; double* pb = &b; //pa = static_cast<int*>(pb); error
cout<< "before b= " << b << endl; a = static_cast<int> (b); cout << "after b= "
<< a << endl; A a1(10); B b1(-10, 20); A* pa1 = new A(1); B* pb1 = new B(-1,-2);
cout<< "****************************" << endl; a1.test(); a1.print();
//输出的a的值是10. a1 = static_cast<A>(b1); //可以从子类到父类,但是不可以从父类到子类! a1.test(); a1.
print(); //输出的a的值是-10. cout << "****************************" << endl; pb1->test
(); pb1->print(); pb1 = static_cast<B*>(pa1); //指针的话父到子类、子类到父类都可以! pb1->test();
pb1->print(); cout << "****************************" << endl; pa1->test();
//是满足多态性的 pa1->print(); pa1 = static_cast<A*>(pb1); pa1->test(); pa1->print();
//是满足多态性的! return 0; }
<>(2)const_cast:用于去除变量的只读特性,用于指针之间以及引用之间类型的转换。但是注意,不能更改基础类型,只能调节类型限定符。
不能更改基础类型,只能调节类型限定符。
cout <<"before: b0=" << b0 << endl; //a0 = const_cast<int&>(b0);
error,不能更改基础类型,只能调节类型限定符。 b0 = const_cast<int&>(a0); cout << "after: b0=" << b0
<< endl; //const_cast: 用于去除变量的只读特性,用于指针之间以及引用之间类型的转换。 const int a = 10; int* pa
= const_cast<int*>(&a); int& a0 = const_cast<int&>(a); cout <<
"*******************************" << endl; cout << "a= " << a ; cout<<"、*pa= "
<< *pa; cout << "、&a0= " << a0 << endl; cout <<
"*******************************" << endl; *pa = 100; cout << "a= " << a; cout
<< "、*pa= " << *pa; cout << "、&a0= " << a0 << endl; cout <<
"*******************************" << endl; a0 = 1000; cout << "a= " << a; cout
<< "、*pa= " << *pa; cout << "、&a0= " << a0 << endl; cout <<
"*******************************" << endl;
<>(3)reinterpret_cast:用于指针类型之间,整数和指针类型之间的转换。(基本类型之间不可以!)
<>(4)dynamic_cast:用于有继承关系的类指针之间、有交叉关系的类指针之间的转换、具有类型检查的功能、需要虚函数的支持
作用:用于将一个父类的指针或引用转化为子类的指针或引用(安全的向下转型)
1. 基类指针所指对象是派生类对象,这种转换是安全的; 2. 基类指针所指对象为基类对象,转换失败,返回结果为0。 class Base { public:
Base() {}; virtual void show() { cout << "Base\n"; } }; class Derive :public
Base { public: Derive() {}; virtual void show() { cout << "Derive\n"; } }; int
main() { Base* base = new Derive; if (Derive * ptr1 = dynamic_cast<Derive*>(base
)) { cout << "转换成功" << endl; //执行 ptr1->show(); cout << endl; } base = new Base;
if (Derive * ptr2 = dynamic_cast<Derive*>(base)) { cout << "转换成功" << endl; ptr2
->show(); cout << endl; } else { cout << "转换失败" << endl; //执行 } delete(base);
return 0; }
<>12. 当一个类A 中没有声命任何成员变量与成员函数这时sizeof(A)的值是多少,如果不是零,请解释一下编译器为什么没有让它为零。
肯定不是零。举个反例,如果是零的话,声明一个class
A[10]对象数组,而每一个对象占用的空间是零,这时就没办法区分A[0],A[1]…了。所以,通常是1,用作占位的。为了确保不同对象有不同的地址。
class test { }; int main(){ cout << sizeof(test) << endl; return 0; }
<>13. 为什么内联函数,构造函数,静态成员函数不能为virtual函数?
<>(1)内联函数inline
因为inline函数是在编译时期展开的,但是虚函数是在运行时期动态编译绑定的。所以两者定义矛盾,不能将inline内联函数定义为虚函数。
<>(2)构造函数
1.
构造函数是用来创建一个对象,而虚函数的运行是在一个对象之上,当对象还不存在的时候是没法运行虚函数的,所以如果构造函数是虚函数的话,那么构造函数将无法运行。
2. 当实例化一个对象执行构造函数时,会为该对象初始化一个指向虚函数表的虚表指针vptr,当构造函数未执行时,虚表指针尚未初始化,那么是无法执行构造函数的。
<>(3)静态成员函数
1.
静态成员函数是在编译时期就绑定的,属于这个类,可以直接通过类去调用静态成员函数而不必实例化对象去调用。但是虚函数是运行时期动态编译的,是通过实例化对象中生成的vptr指针指向的虚函数表调用的。但是静态成员函数中是没有this指针的,所以没法调用。
2. 对于静态成员函数,它没有this指针,所以无法访问vptr. 这就是为何static函数不能为virtual。
<>14. 构造函数和析构函数为什么没有返回值?
1.
安全性方面:如果构造函数和析构函数带有返回值的话,那么编译器必须制定如何处理返回值,或者由程序员去显示的调用构造函数和析构函数,这样一来,安全性就被人破坏了。
2. 在程序中创建和消除一个对象的行为非常特殊,就像出生和死亡,而且总是由编译器来调用这些函数以确保它们被执行。
<>15. 函数模板与类模板的区别?
函数模板的实例化可以由编译阶段自处理函数时自动完成。 但是类模板必须由程序员在程序中显示的指示。 template<class T> T Max(T a,
T b) { return a > b ? a : b; } //可以隐式调用,编译程序在处理函数调用时自动完成的。 cout << Max(a1, b1)
<< endl; //也可以显示的调用,自己在程序中指出类型。 cout << Max<double>(10.0,20.0) << endl; template
<class T> class test_template { private: T a; T b; public: test_template(T a,T b
) { this->a = a; this->b = b; } T add() { return a + b; } T max() { return a > b
? a : b; } }; int main(){ test_template<int> p(10, 20); }
<>16. 基类的析构函数不是虚函数,会带来什么问题?
如果存在基类的指针指向new出来的子类的对象,那么会使得后期调用delete去析构该指针所指向的空间时,只会调用基类的析构函数,而不会调用子类的虚构函数(因为基类的析构函数不是virtual类型的,所以不会有多态性),使得子类开辟的空间不会被回收,可能产生内存泄露问题。
<>17. 构造函数可以调用虚函数吗?语法上通过吗?语义上可以通过吗?
构造函数可以调用虚函数,语法上通过,语义上也通过。但是该虚函数不会具有多态性,而是会被当成一个普通成员函数调用,所以调用的只能是基类的。
调用当然是没有问题的但多态这个功能被屏蔽了。
首先,构造函数的执行是通过实例化对象中的虚表指针vptr执行的,当我们声明一个基类的指针指向new出的父类的对象时: father *p=new
son();
首先会调用父类的构造函数,此时该对象的vptr指向的是父类的虚函数表,并且此时并未调用子类的构造函数。C++为了防止vptr访问未初始化的数据,所以限定在构造函数中调用的虚函数不具备多态性,此时调用的就是父类的虚函数。
<>18. 析构函数可以抛出异常吗?为什么不能抛出异常?会造成什么问题?
析构函数不可以抛出异常,如果析构函数执行过程中在某点抛出异常没那么在该点之后如果存在资源的释放将不会执行,会造成资源的泄漏问你。
通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。
析构函数中抛出异常导致程序不明原因的崩溃是许多系统的致命内伤。
解决办法:那就是把异常完全封装在析构函数内部,决不让异常抛出函数之外。如使用Try{
}Catch{这里可以什么都不做,只是保证catch块的程序抛出的异常不会被扔出析构函数之外。}
<>19. 虚函数与纯虚函数区别?
1. 含有纯虚函数的类是抽象类,不可以实例化对象。 2. 虚函数必须给出实现,但是纯虚函数没有实现体,声明之后就是=0。 3.
定义纯虚函数没有实现,表示的是一个规范的接口,继承抽象类的子类中必须实现抽象类中的所有纯虚函数。 4. 虚函数和纯虚函数都可以在子类(sub
class)中被重载,以多态的形式被调用
<>20. 如何减少频繁分配内存(malloc或者new)造成的内存碎片?
内存池(Memory Pool)是一种内存分配方式。
通常我们习惯直接使用new、malloc等API申请分配内存,这样做的缺点在于:由于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能。内存池则是在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。
当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的一个显著优点是尽量避免了内存碎片,使得内存分配效率得到提升。