一.多态性都有哪些?(静态和动态,然后分别叙述了一下虚函数和函数重载)
多态性可以简单地概括为“一个接口,多种方法”,程序在运行时才决定调用的函数
子类重新定义父类的做法称为覆盖(override),或者称为重写
重载则是允许有多个同名的函数,而这些函数的参数列表不同,允许参数个数不同,参数类型不同
多态与非多态的实质区别就是函数地址是早绑定还是晚绑定
封装可以使得代码模块化,继承可以扩展已存在的代码,多态的目的则是为了接口重用
最常见的用法就是声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而实现不同的方法
编译时多态性:通过重载函数实现
运行时多态性:通过虚函数实现。
虚函数是在基类中被声明为virtual,并在派生类中重新定义的成员函数,可实现成员函数的动态覆盖(Override)
二.动态绑定怎么实现?(就是问了一下基类与派生类指针和引用的转换问题)
1,直接用基类指针引用基类对象
2,直接用派生类指针引用派生类对象
对于指针和引用,分为两种情况:
(a)没有虚函数:此时,调用基类还是派生类完全取决于函数形参的静态类型。如果形参与实参类型不符,需要类型转化:派生类指针转化为基类指针,由编译器自动完成;而基类指针转化为派生类,则必须强制完成。其中后者可能是不安全的。比如我们程序中的那个打印结果为随机数。因为这里试图访问一个基类中并不存在的内容。通常,如果使用C++的类型转化操作:dynamic_cast可以解决这个问题,如果从基类向派生类转化,则会转化失败,dynamic_cast会返回一个空指针;而对应的引用操作则会抛出异常。
(2)有虚函数:此时,函数的调用是通过虚函数表来完成的。每次通过指针或者引用调用它时,都调用的是它实际类型的函数。如果形参与实参类型不符,可以通过强制类型转化来完成匹配,但即使强制类型转化后,并没有改变虚函数表里面的内容,所以不管你如何转化,都调用的它实际指向对象的那个函数。
三. 类型转换有哪些?(四种类型转换,分别举例说明):
去const属性用const_cast。
基本类型转换用static_cast。
static_cast保证了非 类型的向下转换以外的 转换的安全性转换。与c比
多态类之间的类型转换用daynamic_cast。
不同类型的指针类型转换用reinterpreter_cast。
1.static_cast
最常用的类型转换符,在正常状况下的类型转换,如把int转换为float,如:int i;float f; f=(float)i;或者f=static_cast<float>(i);
2.const_cast
用于取出const属性,把const类型的指针变为非const类型的指针,如:const int *fun(int x,int y){} int *ptr=const_cast<int *>(fun(2.3))
3.dynamic_cast
该操作符用于运行时检查该转换是否类型安全,但只在多态类型时合法,即该类至少具有一个虚拟方法。dynamic_cast与static_cast具有相同的基本语法,dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全
4.reinterpret_cast
interpret是解释的意思,reinterpret即为重新解释,此标识符的意思即为数据的二进制形式重新解释,但是不改变其值。如:int i; char *ptr="hello freind!"; i=reinterpret_cast<int>(ptr);这个转换方式很少使用。
四. 操作符重载(+操作符),具体如何去定义,?(让把操作符重载函数原型说一遍)
希望操作我们自己定义的数据类型能像操作int和double这些内置数据类型一样方便
重载实际上可以分为函数重载和操作符重载。运算符重载和函数重载的不同之处在于操作符重载重载的一定是操作符。
实现操作符重载的两种方式
操作符重载的实现方式有两种,即通过“友元函数”或者“类成员函数”。
1.友元函数重载操作符的格式:
复制代码
1 class 类名
2 {
3 friend 返回类型 operator 操作符(形参表);
4 };
5 //类外定义格式:
6 返回类型 operator操作符(参数表)
7 {
8 //函数体
9 }
复制代码
2.类成员函数实现操作符重载的格式:
复制代码
1 class 类名
2 {
3 public:
4 返回类型 operator 操作符(形参表);
5 };
6 //类外定义格式
7 返回类型 类名::operator 操作符(形参表)
8 {
9 //函数体
10 }
五.内存对齐的原则:原则叙述了一下并举例说明
1结构体变量的首地址能够被其最宽基本类型成员大小与对齐基数中的较小者所整除;
2结构体每个成员相对于结构体首地址的偏移量(offset)都是该成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);
3结构体的总大小为结构体最宽基本类型成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。
内存对齐的主要作用是:
1、 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2、 性能原因:经过内存对齐后,CPU的内存访问速度大大提升。具体原因稍后解释。
六.模版怎么实现?
模板是C++支持参数化多态的工具
通常有两种形式:函数模板和类模板;
函数模板针对仅参数类型不同的函数;
类模板针对仅数据成员和成员函数类型不同的类。
使用模板的目的就是能够让程序员编写与类型无关的代码
函数模板的格式:
template <class 形参名,class 形参名,......> 返回类型 函数名(参数列表)
{
函数体
}
template <class T> void swap(T& a, T& b){},
类模板的格式为:
template<class 形参名,class 形参名,…> class 类名
{ ... };
template<class T> class A{public: T a; T b; T hy(T c, T &d);};
七. 指针和const的用法?(就是四种情况说了一下)
const最常用的就是定义常量,除此之外,它还可以修饰函数的参数、返回值和函数的定义体
如果输入参数采用“指针传递”,那么加const 修饰可以防止意外地改动该指针,起到保护作用。
如果给以“指针传递”方式的函数返回值加const 修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const 修饰的同类型指针。
const修饰成员函数const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数.
b. const对象的成员是不可修改的,然而const对象通过指针维护的对象却是可以修改的.
c. const成员函数不可以修改对象的数据,不管对象是否具有const性质.
八.虚函数、纯虚函数、虚函数与析构函数?(纯虚函数如何定义,为什么析构函数要定义成虚函数)
只有用virtual声明类的成员函数,使之成为虚函数,不能将类外的普通函数声明为虚函数。因为虚函数的作用是允许在派生类中对基类的虚函数重新定义。所以虚函数只能用于类的继承层次结构中。
有时候,基类中的虚函数是为了派生类中的使用而声明定义的,其在基类中没有任何意义。此类函数我们叫做纯虚函数,不需要写成空函数的形式,只需要声明成:
virtual 函数类型 函数名(形参表列)=0;
注意:纯虚函数没有函数体;
最后面的“=0“并不代表函数返回值为0,只是形式上的作用,告诉编译系统”这是纯虚函数”;
析构函数的作用是在对象撤销之前把类的对象从内存中撤销。通常系统只会执行基类的析构函数,不执行派生类的析构函数。
只需要把基类的析构函数声明为虚函数,即虚析构函数,这样当撤销基类对象的同时也撤销派生类的对象,这个过程是动态关联完成的。.
构造函数不能声明为虚函数。
九.内联函数(讲了一下内联函数的优点以及和宏定义的区别)
内联函数是使用inline关键字声明的函数,
引入内联函数的主要目的是:解决程序中函数调用的效率问题宏的定义很容易产生二意性内联函数和宏的区别在于,宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。你可以象调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。内联函数与带参数的宏定义进行下比较,它们的代码效率是一样,但是内联欢函数要优于宏定义,因为内联函数遵循的类型和作用域规则,它与一般函数更相近,在一些编译器中,一旦关上内联扩展,将与一般函数一样进行调用,比较方便。 对于私有或者保护成员的读写就必须使用成员接口函数来进行。如果我们把这些读写成
员函数定义成内联函数的话,将会获得比较好的效率。
1.内联函数在运行时可调试,而宏定义不可以;
2.编译器会对内联函数的参数类型做安全检查或自动类型转换(同普通函数),而宏定义则不会;
3.内联函数可以访问类的成员变量,宏定义则不能;
4.在类中声明同时定义的成员函数,自动转化为内联函数。
十.const和typedef(主要讲了const的用处,有那些优点)
#define 只是用来做文本替换的
#define Pi 3.1415926
const与#define最大的差别在于:前者在堆栈分配了空间,而后者只是把具体数值直接传递到目标变量罢了。或者说,const的常量是一个Run-Time的概念,他在程序中确确实实的存在并可以被调用、传递。而#define常量则是一个Compile-Time概念,它的生命周期止于编译期:在实际程序中他只是一个常数、一个命令中的参数,没有实际的存在。
中,typedef常用来定义一个标识符及关键字的别名,它是语言编译过程的一部分,但它并不实际分配内存空间,实例像:
typedef int INT;
十一.extern "C" 是c++语言,包含双重含义,从字面上即可得到:首先,被它修饰的目标是"extern"的;其次,被它修饰的目标是"C"的。
十二.strcpy实现
char *my_strcpy(char *dst,const char *src)
{
assert(dst != NULL);
assert(src != NULL);
char *ret = dst;
while((* dst++ = * src++) != '\0')
;
return ret;
}
char *strcpy(char *strDest,const char *strSrc);
strcpy返回类型是干嘛用
同时函数的返回值又是strDest。
这样做并非多此一举,可以获得如下灵活性:增加灵活性如支持链式表达
char str[20];
int length = strlen( strcpy(str, “Hello World”) );
十三.引用与指针:
(1)引用是C++的特性,C语言中没有;(
2)引用是个别名,不是数据类型,不分配存储空间,只是一种机制;
(3)引用作为目标的别名使用,对引用的改动实际上是对目标的改动;(
4)在C++中,定义含有分配存储空间的意义,那么引用是只有声明,没有定义;
(5)要求在声明一个引用前,它所维系的目标已经声明或者定义;例如:inta;int&b=a;//a应该是已经声明或者定义过的变量;
(6)引用在声明时,必须被初始化,否则产生编译错误;
(7)C++没有提供访问应用本身地址的方法,如果程序寻找引用的地址,只能找到它所引用的目标的地址;引用一旦初始化,就维系在一定的目标上,再也不分开;
(8)引用与指针有很大的区别,指针是个变量,可以把它再次赋值成指向别处的地址,但是,建立引用时必须初始化,并且决不会再关联其他的任何变量;
(9)引用本身不是一种数据类型,所以没有引用的引用,也没有引用的指针;
(10)有空指针,无空引用;由此可知道,指向引用的指针是不存在的,因为它没有空间,也就没有地址,及时使用&,取出来的也只是它引用对象的地址。而指针的引用和其他的引用一样,可以这么理解:typedefint*type;typepi;type&pr=pi;
指针与引用转化:
void print_info(Student& stu)
{
cout << "Student's age:" << stu.age << endl;
}
int main(int argc, char *argv[])
{
Student *Jack = new Student(24);
print_info(*Jack); // 这样子就可以了
十四.虚函数,虚函数表里面内存如何分配
所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术
一般继承(无虚函数覆盖)
1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。
一般继承(有虚函数覆盖)
覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧。
多重继承(无虚函数覆盖)
每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的
多重继承(有虚函数覆盖)
十五.
STL的容器可以分为以下几个大类:
一:序列容器, 有vector, list, deque, string.
二 : 关联容器, 有set, multiset, map, mulmap, hash_set, hash_map, hash_multiset, hash_multimap
三: 其他的杂项: stack, queue, valarray, bitset
STL各个容器的实现:
(1) vector
内部数据结构:数组。
随机访问每个元素,所需要的时间为常量。
在末尾增加或删除元素所需时间与元素数目无关,在中间或开头增加或删除元素所需时间随元素数目呈线性变化。
可动态增加或减少元素,内存管理自动完成,但程序员可以使用reserve()成员函数来管理内存。
vector的迭代器在内存重新分配时将失效(它所指向的元素在该操作的前后不再相同)。当把超过capacity()-size()个元素插入vector中时,内存会重新分配,所有的迭代器都将失效;否则,指向当前元素以后的任何元素的迭代器都将失效。当删除元素时,指向被删除元素以后的任何元素的迭代器都将失效。
(2)deque
内部数据结构:数组。
随机访问每个元素,所需要的时间为常量。
在开头和末尾增加元素所需时间与元素数目无关,在中间增加或删除元素所需时间随元素数目呈线性变化。
可动态增加或减少元素,内存管理自动完成,不提供用于内存管理的成员函数。
增加任何元素都将使deque的迭代器失效。在deque的中间删除元素将使迭代器失效。在deque的头或尾删除元素时,只有指向该元素的迭代器失效。
(3)list
内部数据结构:双向环状链表。
不能随机访问一个元素。
可双向遍历。
在开头、末尾和中间任何地方增加或删除元素所需时间都为常量。
可动态增加或减少元素,内存管理自动完成。
增加任何元素都不会使迭代器失效。删除元素时,除了指向当前被删除元素的迭代器外,其它迭代器都不会失效。
(4)slist
内部数据结构:单向链表。
不可双向遍历,只能从前到后地遍历。
其它的特性同list相似。
(5)stack
适配器,它可以将任意类型的序列容器转换为一个堆栈,一般使用deque作为支持的序列容器。
元素只能后进先出(LIFO)。
不能遍历整个stack。
(6)queue
适配器,它可以将任意类型的序列容器转换为一个队列,一般使用deque作为支持的序列容器。
元素只能先进先出(FIFO)。
不能遍历整个queue。
(7)priority_queue
适配器,它可以将任意类型的序列容器转换为一个优先级队列,一般使用vector作为底层存储方式。
只能访问第一个元素,不能遍历整个priority_queue。
第一个元素始终是优先级最高的一个元素。
(8)set
键和值相等。
键唯一。
元素默认按升序排列。
如果迭代器所指向的元素被删除,则该迭代器失效。其它任何增加、删除元素的操作都不会使迭代器失效。
(9)multiset
键可以不唯一。
其它特点与set相同。
(10)hash_set
与set相比较,它里面的元素不一定是经过排序的,而是按照所用的hash函数分派的,它能提供更快的搜索速度(当然跟hash函数有关)。
其它特点与set相同。
(11)hash_multiset
键可以不唯一。
其它特点与hash_set相同。
(12)map
键唯一。
元素默认按键的升序排列。
如果迭代器所指向的元素被删除,则该迭代器失效。其它任何增加、删除元素的操作都不会使迭代器失效。
(13)multimap
键可以不唯一。
其它特点与map相同。
(14)hash_map
与map相比较,它里面的元素不一定是按键值排序的,而是按照所用的hash函数分派的,它能提供更快的搜索速度(当然也跟hash函数有关)。
其它特点与map相同。
(15)hash_multimap
键可以不唯一。
其它特点与hash_map相同。
十六:泛型特化
函数模板的特化:当函数模板需要对某些类型进行特别处理,称为函数模板的特化
explicit 关键字的主要作用是用来避免自定义类型隐式转化为类类型
十七.new与malloc的区别,delet和free的区别
malloc与free是C++/C 语言的标准库函数,new/delete 是C++的运算符。
对象在创建的同时要自动执行构造函数, 对象消亡之前要自动执行析构函数。由于malloc/free 是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加malloc/free。
1、new自动计算需要分配的空间,而malloc需要手工计算字节数
2、new是类型安全的,而malloc不是,比如:
int* p = new float[2]; // 编译时指出错误
int* p = malloc(2*sizeof(float)); // 编译时无法指出错误
new operator 由两步构成,分别是 operator new 和 construct
3、operator new对应于malloc,但operator new可以重载,可以自定义内存分配策略,甚至不做内存分配,甚至分配到非内存设备上。而malloc无能为力
4、new将调用constructor,而malloc不能;delete将调用destructor,而free不能。
5、malloc/free要库文件支持,new/delete则不要。
十八.迭代器删除元素的会发生什么
序列容器的erase返回的是被删除元素后的有效迭代器
关联容器的erase方法没有返回值,被删除的迭代器失效.
十九.c++中什么类型的成员变量只能在构造函数的初始化列表中进行
常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面
引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面
没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化
二十.auto_ptr 类可以用于管理由 new 分配的单个对象,但是无法管理动态分配的数组(我们通常不会使用数组,而是使用 vector 代替数组)
二十一.可重入函数:
可重入(reentrant)函数可以由多于一个任务并发使用,而不必担心数据错误。可重入函数可以在任意时刻被中断,稍后再继续运行,不会丢失数据。可重入函数要么使用本地变量,要么在使用全局变量时保护自己的数据。
可重入函数:
函数中使用了静态变量,无论是全局静态变量还是局部静态变量。
函数返回静态变量。
函数中调用了不可重入函数。
函数体内使用了静态的数据结构;
函数体内调用了malloc()或者free()函数;
函数体内调用了其他标准I/O函数。
函数是singleton中的成员函数而且使用了不使用线程独立存储的成员变量 。
总的来说,如果一个函数在重入条件下使用了未受保护的共享的资源,那么它是不可重入的。
所谓的函数是可重入的(也可以说是可预测的),即只要输入数据相同就应产生相同的输出。这个函数之所以是不可预测的,就是因为函数中使用了static变量,因为static变量的特征,这样的函数被称为:带“内部存储器”功能的的函数。因此如果需要一个可重入的函数,一定要避免函数中使用static变量,这种函数中的static变量,使用原则是,能不用尽量不用。