1.面向对象程序设计(object-orientedprogramming)的核心思想?
核心思想是数据抽象、继承和动态绑定。通过使用数据抽象,将类的接口与实现分离(c++primer第7章);使用继承,可以定义相似的类型并对其相似关系进行建模;使用动态绑定,可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象。
2.c++多态性?
多态性简单概括为“一个接口,多种方法”,程序在运行时才决定调用的函数。
C++多态性是通过虚函数来实现的,虚函数允许子类重新定义成员函数,而子类重新定义父类的做法称为覆盖(override),或者称为重写。(这里我觉得要补充,重写的话可以有两种,直接重写成员函数和重写虚函数,只有重写了虚函数的才能算作是体现了C++多态性)而重载则是允许有多个同名的函数,而这些函数的参数列表不同,允许参数个数不同,参数类型不同,或者两者都不同。编译器会根据这些函数的不同列表,将同名的函数的名称做修饰,从而生成一些不同名称的预处理函数,来实现同名函数调用时的重载问题。但这并没有体现多态性。
多态与非多态的实质区别就是函数地址是早绑定还是晚绑定。如果函数的调用,在编译器编译期间就可以确定函数的调用地址,并生产代码,是静态的,就是说地址是早绑定的。而如果函数调用的地址不能在编译器期间确定,需要在运行时才确定,这就属于晚绑定。
3.动态绑定怎么实现?
基类通过在其成员函数的声明语句之前加上关键字virtual使得该函数执行动态绑定。
4.静态类型与动态类型
表达式的静态类型在编译时总是已知的,它是变量声明时的类型或表达式生成的类型;动态类型则是变量或表达式表示的内存中的对象的类型.动态类型直到运行时才可知。
如果表达式即不是引用也不是指针,则它的动态类型永远与静态类型一致。
5.基类和派生类转换问题
存在派生类向基类的类型转换,因为每个派生类对象都包含一个基类部分,而基类的引用或指针可以绑定到该基类部分。
不存在从基类向派生类的隐式类型转换。
从派生类向基类的类型转换只对指针或引用类型有效。
尽管自动类型转换只对指针或引用类型有效,但是继承体系中的大多数类仍然定义了拷贝控制成员。因此,通常能够将一个派生类对象拷贝、移动或赋值给一个基类对象。不过需要注意,这种操作只处理派生类对象的基类部分。
6.类型转换有哪些?
隐式:数组转换成指针:intia[10]; int *ip = ia //ia转换成指向数组首元素的指针
指针的转换:常量整数值0或字面值nullptr能转换成任意指针类型;
指向任意非常量的指针能转换成void*
指向任何对象的指针能转换成constvoid*
显示:强制类型转换 cast-name<type>(expression)
static_cast
任何有明确定义的类型转换,不包含底层const,都可以用static_cast
把较大的算术类型赋值给较小的类型时,避免编译器发现精度损失给出的警告
对编译器无法自动执行的类型转换有用,找回存在于void*指针中的值:
void* p = &; double *dp = static_cast<double*>(p)
const_cast
const_cast只能改变运算对象的底层const(指针所指的对象是一个常量)
只有const_cast能改变表达式的常量属性
reinterpret_cast
为运算对象的位模式提供较低层次上的重新解释
它主要用于将一种数据类型从一种类型转换为另一种类型。它可以将一个指针转换成一个整数,也可以将一个整数转换成一个指针,在实际开发中,先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原来的指针值;特别是开辟了系统全局的内存空间,需要在多个应用程序之间使用时,需要彼此共享,传递这个内存空间的指针时,就可以将指针转换成整数值,得到以后,再将整数值转换成指针,进行对应的操作。
dynamic_cast
用于将基类的指针或引用安全地转换成派生类的指针或引用,例:
假定Base类至少含有一个虚函数,Derived是Base的公有派生类,如果有一个指向Base的指针bp,则可以在运行时将它转换成指向Derived的指针:
If( Derived *dp = dynamic_cast<Derived*>(bp))
{
//使用dp指向的Derived对象
}else{ //bp指向一个Base对象
//使用bp指向的Base对象
}
如果bp指向Derived对象,则上述的类型转换初始化dp并令其指向bp所指的Derived对象。此时,if语句内部使用Derived操作的代码是安全的。否则,类型转换的结果为0,dp为0意味着if语句的条件失败,此时else执行Base操作。
引用类型的dynamic_cast不会产生空引用,会抛出异常。
7.操作符重载,具体如何定义?
操作符重载有两种实现方式,即通过”友元函数”或者”类成员函数”
除了类属关系运算符”.“、成员指针运算符”.*“、作用域运算符”::“、sizeof运算符和三目运算符”?:“以外,C++中的所有运算符都可以重载
在实际开发过程中,单目运算符建议重载为成员函数,而双目运算符建议重载为友元函数。
前缀++obj重载时没有虚参,通过引用返回*this或自身引用,也就是返回变化之后的数值.
后缀obj++重载的时候有一个int类型的虚参,返回原状态的拷贝。
友元函数重载格式:
class 类名
{
friend 返回类型 operator 操作符(形参表);
};
//类外定义格式:
返回类型 operator操作符(参数表)
{
//函数体
}
类成员函数重载格式:
class 类名
{
public:
返回类型 operator操作符(形参表);
};
//类外定义格式
返回类型类名::operator 操作符(形参表)
{
//函数体
}
8.内存对齐
对齐规则:1.对于类(结构或联合)的各个成员,第一个成员位于偏移为0的位置,以后每个数据成员的偏移量必须是min(#pragma pack(n)指定的数,这个数据成员的自身长度)的倍数。
2.数据成员完成各自对齐后,类(结构或联合)本身也要进行对齐,对齐将按照#pragma pack制定的数值和类(结构或联合)最大数据成员长度中,比较小的那个进行。
c++空类的内存大小为1字节,为了保证其对象拥有彼此独立的内存地址。非空类的大小与类中非静态成员变量和虚函数表的多少有关,而类中非静态成员变量的大小与编译器内存对齐的设置有关。
内存对齐的主要作用:
1、 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2、 性能原因:经过内存对齐后,CPU的内存访问速度大大提升。(CPU读取内存时是按块进行读取的)
9.模板怎么实现?
模板是C++支持参数化多态的工具,使用模板可以使用户为类或者函数声明一种一般模式,使得类中的某些数据成员或者成员函数的参数、返回值取得任意类型。
通常有两种形式:函数模板和类模板;
使用模板的目的就是能够让程序员编写与类型无关的代码。
函数模板格式:
class可以用typename 关键字代替,在这里typename和class没区别
template <class 形参名,class 形参名,......> 返回类型函数名(参数列表)
{
函数体
}
除了定义类型参数,还可以在模板中定义非类型参数,一个非类型参数表示一个值而非一个类型.通过特定类型名而非关键字class或typename来指定。
类模板格式:template<class形参名,class 形参名,…> class 类名{…};
类模板对象的创建:比如一个模板类A(vector),创建对象的方法A<int>m;
10.const限定符
只能在const对象上执行不改变其内容的操作。
如果利用一个对象去初始化另一个对象,则它们是不是const都无关紧要。
默认状态下const对象仅在文件内有效,如果想在多个文件之间共享const对象,必须在变量的定义和声明之前添加关键字extern。
常量引用是对const的引用,严格意义上不存在常量引用,因为引用不是一个对象。
初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。允许为一个常量引用绑定非常量的对象,字面值,甚至是表达式。
常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未作限定,因为对象也可能是个非常量,所以允许通过其他途径改变它的值。
指向常量的指针不能用于改变其所指对象的值。
指针本身是一个常量并不意味着不能通过指针修改其所指对象的值,能否这样做完全依赖于所指对象的类型。例:*curE = 0;(curE是一个常量指针)
顶层const表示指针本身是个常量,底层const表示指针所指的对象是一个常量。
11.为什么析构函数要定义成虚函数?什么情况下需要虚析构函数?纯虚函数如何定义?
如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为。析构是调用基类的析构函数,派生类的资源没有办法释放。如果定义了虚析构函数,这时候就先释放子类,然后释放基类的资源。
涉及到继承时使用虚析构函数。
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。纯虚函数的方法是在函数原型后加“=0”
引入纯虚函数原因:1.为了方便使用多态特性,我们常常需要在基类中定义虚函数2.很多情况下,基类本身生成对象是不合理的。例如:动物作为基类可以派生出老虎、大象等,但本身生成对象是不合理的。
含有纯虚函数的类称为抽象类。
12.内联函数的优点和宏定义的区别
内联取代宏:1.内联函数在运行时可调试,而宏定义不可以;2.编译器会对内联函数的参数类型做安全检查或自动类型转换,而宏定义不会;3.内联函数可以访问类的成员变量,宏定义则不能;4.在类中声明同时定义的成员函数,自动转化为内联函数。
C++语言支持函数内联,其目的是为了提高函数的执行效率。在c中,可以用宏代码提高执行效率。宏代码本身不是函数。预处理器用复制宏代码的方式代替函数调用,省去了参数压栈、生成汇编语言的CALL调用、返回参数、执行return等过程,从而提高速度。使用宏代码最大的缺点是容易出错,预处理器在复制宏代码时常常产生意想不到的边际效应。例如:
#define MAX(a,b) (a)>(b)?(a):(b)
语句
Result = MAX(i,j)+2将被预处理器解释为Result=(a)>(b)?(a):(b)+2
由于运算符‘+’比‘:’优先级高,则上述语句得不到期望的结果。
13.const、typedef和#define
有时候我们希望定义一种变量,它的值不能被改变。为了防止程序一不小心改变了类似缓冲区的值,可以用关键字const对变量的类型加以限定。const意味着“只读”
关键字const给读你代码的人传达有用的信息,实际上,声明一个参数为常量是为了告诉用户这个参数的应用目的。
使用const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。即减少bug的出现。
const关键字的作用:1.阻止一个变量被改变,定义该const变量时,需要对它进行初始化。2.对指针来说,可以指定指针本身为const,也可以指定指针指向的数据为const,或者同时。3.在函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值4.对于类的成员函数,若指定其为const类型,表明其是一个常函数(类的成员函数后面加const),不能修改类的成员变量5.对类的成员函数,有时候必须指定其返回值为const类型,使得返回值不为“左值”
typedef常用来定义一个标识符及关键字的别名,它是语言编译过程的一部分,但并不实际分配内存空间;typedef可以增强程序的可读性,以及标识符的灵活性。
C++中#define可以用来给标识符关键字其别名,不过只是简单的字符串代换(原地扩展),而typedef则不是原地扩展,它的新名字具有一定封装性。例如:
Typedef (int *) pINT;
#define pINT2 int*
实践:pINT a,b 效果同int *a;int *b;而pINT2 a,b;效果同int *a,b;
14.链接指示符extern“C”的作用
为了支持原来的C代码和已经写好的C库,需要在C++中尽可能的支持C
为何需要使用extern“C”:作为一种面向对象的语言,C++支持函数重载,而过程式C不支持。函数被C++编译后在符号库中的名字与C语言的不同。C++的名字包含了函数名、函数参数数量及类型信息。因此C++通过提供extern“C”来解决名字匹配的问题。
链接指示符有两种形式:单一语句形式和复合语句形式
15.C与C++
C是面向过程的,C++面向对象
C++有类的概念,包括成员变量和成员函数
C函数传参只能传值,后者可以传引用
C不支持异常处理,C++有try-catch-finally支持
16. 不调用C语言库函数,编程分别实现strcpy、memcpy、memmove
(1)strcpy C语言标准库函数,把从src地址开始且含有‘\0’结束符的字符串复制到以dest开始的地址空间。src和dest所指内存区域不可以重叠且dest必须有足够空间来容纳src
//实现从strSource到strDestination的复制
char* strcpy(char* strDestination,constchar* strSource)
{
//判断strDestination和strSource的有效性
assert((strDestination == NULL) || (strSource == NULL));
char* strDestCopy = strDestination; //保存目标字符串的首地址
while ((*strDestination++ = *strSource++) != '\0') //把strSource字符串的内容复制到
strDestination中
{
;
}
return strDestCopy;
}
(2)memcpy从原地址拷贝n字节到目标地址
void *memcpy( void *memDest, const void*memSrc, size_t count )
{
assert((NULL != memDest) && (NULL != memSrc)); //memDest和memSrc必须有效
char* tempDest = (char*)memDest; //保存memDest的首地址
char *tempSrc = (char*)memSrc; //保存memSrc的首地址
while (count-- > 0) //循环count次,复制memSrc的值到memDest中
{
*tempDest++ = *tempSrc++;
}
return memDest;
}
strcpy和memcpy的区别:1.strcpy只能复制字符串,而memcpy可以复制任意内容2.复制方法不同。Strcpy不需要指定长度,遇到‘\0’结束。
(3)memmove用于从src拷贝count个字节到dest,如果目标区域和源区域有重叠的话,memmove能够保证源串在被覆盖之前将重叠区域的字节拷贝到目标区域。但复制后src内容会被更改,但当没有重叠的话和memcpy函数功能相同。
17.排序算法有哪些?快速排序怎么实现?
排序算法:1.插入排序:直接插入排序、折半插入排序、希尔排序2.交换排序:冒泡排序、快速排序3.选择排序:简单选择排序、堆排序4.归并排序5.基数排序
18.数据结构中二叉树的非递归遍历?
二叉树的链式存储: typedef struct BiTNode{
ElemType data;
struct BiTNode*lchild,*rchild;
}BiTNode,*BiTree;
二叉树中序遍历的递归实现:void InOrder(BiTree *T){
If(T == null)return;
InOrder(T->lchild);
Visit(T);
InOrder(T->rchild);
}
二叉树中序遍历的非递归实现:借助栈
void InOrder(BiTree *T){
initStack(S);
BiTree p= T;
while(p|| !IsEmpty(s)){
if(p){
Push(S,p);
p = p->lchild;
}
else{
Pop(S,p);
Visit(p);
p = p->rchild;
}
}
}
19.c++中各种数据类型占据字节长度?
int占据操作系统一个内存单元的大小。long和int相同,32位系统的一个内存单元是32位,所以4字节。char通常占据一个字节。布尔型占据一个字节。float占据4字节。指针的位数和操作系统的位数相同,32位系统占4字节。
20.虚函数工作原理、虚函数表内存分配?
虚函数的实现要求对象携带额外的信息,这些信息用于在运行时确定该对象应该调用哪一个虚函数。这一信息被称为vptr(虚函数表指针),即虚函数表入口,占4个字节。
每一个具有虚函数的类都有一个虚函数表vtable,里面按在类中声明的虚函数的顺序存放着虚函数的地址,这个虚函数表vtable是这个类的所有对象所共有的,也就是说无论用户声明了多少个类对象,但是这个vtable虚函数表只有一个。
在每个具有虚函数的类的对象里面都有一个vptr虚函数指针,这个指针指向vtable的首地址,每个类的对象都有这么一种指针。
虚继承:对于虚继承,若派生类有自己的虚函数,则它本身需要有一个虚指针,指向自己的虚表。另外,派生类虚继承父类时,首先通过加入一个虚指针来指向父类,因此有两个虚指针。
(虚)继承类的内存占用大小:
首先,平时所声明的类只是一种类型定义,它本身是没有大小可言的。因此,如果用sizeof运算符对一个类型名操作,那得到的是具有该类型实体的大小。
计算一个类对象大小时的规律:1.空类、单一继承的空类、多重继承的空类所占空间大小为:1字节;2.一个类中,虚函数本身、成员函数和静态数据成员都是不占类对象的存储空间的;3.当类中声明了虚函数,那么在实例化对象时,编译器会自动在对象里安插一个指针vptr指向虚函数表vtable;4.虚继承的情况:由于涉及到虚函数表和虚基表,会同时增加一个(多重继承下对应多个)vfptr指针指向虚函数表vftable和一个vbptr指针指向虚基表vbtable,这两者所占的空间大小为:8(或8乘以多继承时父类的个数);5.还要注意编译器会插入多余的字节补齐;6.类对象的大小=各非静态数据成员的总和+vfptr指针总和+vbptr指针总和+编译器额外增加的字节。
1.没有继承情况,vptr存放在对象的开始位置2.单继承的情况下,对象只有一个vptr,它存放在对象的开始位置,派生类子对象在父类子对象的最后面;3.多继承情况下,对象会为每个有虚函数的父类子对象提供一个vptr;派生类子对象在所有父类子对象的最后面,所有父类子对象按照声明顺序排列。4.虚继承情况下,虚父类子对象会放在派生类子对象之后,派生类子对象的第一个位置存放着一个vptr,虚子类子对象也会保存一个vptr。
21.证明sizeof不是函数?
sizeof是一个操作符;其作用是返回一个对象或类型所占的内存字节数;返回值类型位size_t(size_t在头文件stddef.h中定义,它依赖于编译系统的值,一般定义为tpyedef unsigned int size_t)
sizeof有三种语法形式:1.sizeof(object);//sizeof(对象)
2.sizeof object;// sizeof 对象
3.sizeof(type_name);//sizeof(类型)
而函数的参数必须在()内,所以sizeof不是函数。
22.c++如何限制只能静态分配或者只能动态分配类对象?
动态分配类对象:就是使用运算符new来创建一个类的对象,在堆上分配内存。
静态分配类对象:就是A a,由编译器创建类对象,在栈上分配内存。
1.只能动态分配类对象的方法:把类的构造函数和析构函数设为protected属性。类对象不能访问,但是派生类可以继承,也可以访问。同时,创建create和destroy两个函数,用于创建类对象。(create函数设为static,原因是,创建对象的时候A *p = A::create()只有静态成员函数才能有类名直接访问)
class A
{
protected:
A() {}
~A() {}
public:
static A* create()
{
return new A();
}
void destroy()
{
delete this;
}
};
2.只能静态分配类对象
把new、delete运算符重载为private属性就可以了
class A
{
private:
void* operator new(size_t t){} // 注意函数的第一个参数和返回值都是固定的
void operator delete(void* ptr){} // 重载了new就需要重载delete
public:
A(){}
~A(){}
};
23.c++内存分配new,operator new
new:指我们在c++中通常用到的关键字,比如A*a = new A;
operator new:这里的new是一个操作符,并且可以被重载
二者的关系:
A *a = new A;这里分三步:1.分配内存;2.调用A()构造对象;3.返回分配指针。事实上,分配内存这一操作由operator new(size_t)来完成,如果类A重载了operator new,那么将调用A::operator new(size_t),否则调用全局::operator new(size_t),后者由c++默认提供。
24.c++类中的访问权限问题
第一:private,public,protected方法的访问范围
private:只能由该类中的函数、其友元函数访问,不能被任何其他访问,该类的对象也不能访问
protected:可以被该类中的函数、子类的函数、以及其友元函数访问,但不能被该类的对象访问
public:可以被该类中的函数、子类的函数、其友元函数访问,也可以由该类的对象访问
第二:类的继承后方法属性变化:
使用private继承,父类的所有方法在子类中变为private
使用protected继承,父类的protected和public方法在子类中均变为protected;而private方法不变;
使用public继承,父类中的方法属性不发生改变;
25. stl有哪些容器,对比vector和set
C++中有两种类型的容器:顺序容器和关联容器。顺序容器主要有vector、list、deque等。其中vector表示一段连续的内存,基于数组实现,list表示非连续的内存,基于链表实现,deque与vector类似,但是对首元素提供插入和删除的双向支持。关联容器主要有map和set。map是key-value形式,set是单值。map和set只能存放唯一的key,multimap和multiset可以存放多个相同的key。
容器类自动申请和释放内存,因此无需new和delete操作。
首先,vector是序列式容器而set是关联式容器。set的含义是集合,它是一个有序的容器,里面的元素都是排好序的,set插入的元素不能相同,但是multiset可以相同,set默认自动按升序排序。
26.红黑树的定义与解释?
红黑树是一颗二叉搜索树,它在每个节点上增加了一个存储位来表示结点的颜色,可以是red或black,通过对任何一条从根到叶子的简单路径上各个结点的颜色进行约束,红黑树确保没有一条路径会比其他路径长出2倍,因而是近似于平衡的。
一颗红黑树是满足下面红黑性质的二叉搜索树:
1. 每个结点或是红色的,或是黑色的;
2. 根结点是黑色的;
3. 每个叶结点是黑色的;
4. 如果一个结点是红色的,则它的两个子结点都是黑色的;
5. 对每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点。
27. 静态成员函数和数据成员有什么意义?
对于一个完整的程序,在内存中的分布情况如下: 代码区、全局数据区、堆区、栈区
new产生的数据在堆区;函数内部的自动变量在栈区,随着函数的退出而释放空间;静态数据存放在全局数据区,全局数据区的数据并不会因为函数退出而释放空间。
静态成员:静态成员加static修饰符,可以使用类名+静态成员名访问此静态成员,因为静态成员存在于内存,非静态成员需要实例化才会分配内存,所以静态成员不能访问非静态成员,因为静态成员存在于内存,所以非静态成员可以直接访问类中的静态成员。
静态局部变量保存在全局数据区,而不是保存在栈中,每次的值保持到下一次调用,直到下一次赋新值。
定义静态变量和静态函数的好处:不能被其他文件使用,在其他文件中可以定义相同名字的变量和函数,不会发生冲突。
面向对象的static关键字:
静态数据成员:
对于非静态数据成员,每个类对象都有自己的拷贝。而静态数据成员被当作是类的成员。无论这个类的对象定义了多少个,静态数据成员在程序中也只有一份拷贝,由该类型的所有对象共享访问。即静态数据成员是该类的所有对象所共有的。对该类的多个对象来说,静态数据成员只分配一次内存,供所有对象共用。所以,静态数据成员的值对每个对象都是一样的,它的值可以更新。
静态数据成员存储在全局数据区,静态数据成员定义时要分配空间,所以不能在类声明中定义。应该在类外定义。
静态数据成员的初始化与一般数据成员的初始化不同,即它的初始化格式为:
<数据类型><类名>::<静态数据成员> = <值>
静态数据成员主要用在各个对象都有相同的某项属性的时候。比如对一个存款类,每个实例的利息都是相同的,所以把利息可以设为存款类的静态数据成员。这有两个好处,一是不管定义多少个存款类对象,利息数据成员都共享分配在全局数据区的内存,所以节省了存储空间。二是一旦利息需要改变时,只要改变一次,则所有存款类对象的利息全改变过来了。
同全局变量相比,使用静态数据成员有两个优势:
(1)静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其他全局名字冲突的可能性;
(2)可以实现信息隐藏。静态数据成员可以是private成员,而全局变量不能。
静态成员函数:
普通的成员函数一般都隐藏了一个this指针,this指针指向类的对象本身,因为普通成员函数总是具体的属于某个类的具体对象的。通常情况下,this指针是缺省的、但是与普通函数相比,静态成员函数由于不是与任何的对象相联系,因此它不具有this指针,从这个意义上讲,它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,它只能调用其余的静态成员函数。
关于静态成员函数,可以总结以下几点:
(1)出现在类体外的函数不能指定关键字static;
(2)静态成员之间可以互相访问,包括静态成员函数访问静态数据成员和访问静态成员函数;
(3)非静态成员函数可以任意地访问静态成员函数和静态数据成员;
(4)静态成员函数不能访问非静态成员函数和非静态数据成员;
(5)由于没有this指针的额外开销,因此静态成员函数与类的全局函数相比,速度上会有少许的增长;
(6)调用静态成员函数,可以用成员访问操作符(.)和(->)为一个类的对象或指向类对象的指调用静态成员函数。
28. 模版特化的概念,为什么特化?
C++中经常为了避免重复的编码而需要使用到模板,这是C++泛型编程不可或缺的利器。然而通常又有一些特殊的情况,不能直接使用泛型模板展开实现,这时就需要针对某个特殊的类型或者是某一类特殊的类型,而实现一个特例模板————即模板特化。通常会使用到模板特化的有(应该也只能有)类模板和函数模板。
a. 类模板特化
在已有类模板
template <class T>
class stack { //...// };
定义时,可能考虑到一些特定的类型T,数据的存储可能和通用模板不一样,因而可以像如下定义一个特例化模板:
template < >
class stack<bool> { //...// };
b. 函数模板的特化
template <class T>
T max(const T t1, const T t2)
{
return t1 < t2 ? t2 : t1;
}
如上已有的模板定义可能在针对一个指针类型的参数时,工作将可能会不正常,具体到字符串指针类型时,可能就需要下面的特例化模板定义:
template < >
const char* max(const char* t1,const char*t2)
{
return (strcmp(t1,t2) < 0) ? t2 : t1;
}
这样才能使max("aaa", "bbb");的调用更如人意(通常没有人想比较两个常量字符串存储的地址的大小)。
29. explicit是干什么用的?
C++中, 一个参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参构造函数),承担了两个角色。 1 是个构造器,2 是个默认且隐含的类型转换操作符。
所以,有时候在我们写下如 AAA = XXX,这样的代码, 且恰好XXX的类型正好是AAA单参数构造器的参数类型, 这时候编译器就自动调用这个构造器, 创建一个AAA的对象。
这样看起来好象很酷,很方便。 但在某些情况下(见下面权威的例子), 却违背了我们(程序员)的本意。 这时候就要在这个构造器前面加上explicit修饰,指定这个构造器只能被明确的调用/使用,不能作为类型转换操作符被隐含的使用。
explicit构造函数是用来防止隐式转换的。请看下面的代码:
class Test1
{
public:
Test1(int n)
{
num=n;
}//普通构造函数
private:
int num;
};
class Test2
{
public:
explicit Test2(int n)
{
num=n;
}//explicit(显式)构造函数
private:
int num;
};
int main()
{
Test1 t1=12;//隐式调用其构造函数,成功
Test2 t2=12;//编译错误,不能隐式调用其构造函数
Test2 t2(12);//显式调用成功
return 0;
}
Test1的构造函数带一个int型的参数,代码23行会隐式转换成调用Test1的这个构造函数。而Test2的构造函数被声明为explicit(显式),这表示不能通过隐式转换来调用这个构造函数,因此代码24行会出现编译错误。
普通构造函数能够被隐式调用。而explicit构造函数只能被显式调用。
30. strcpy返回类型是干嘛用的?
strcpy为了实现链式操作,将目的地址返回。
int length = strlen( strcpy(str, “HelloWorld”) );
strcpy函数可以作为另外一个函数的实参,减少了内存的使用,节约大量的内存空间。
31.内存溢出有哪些因素?
溢出原因
数据类型超过了计算机字长的界限就会出现数据溢出的情况。导致内存溢出问题的原因有很多,比如:
(1) 使用非类型安全(non-type-safe)的语言如 C/C++ 等。
(2) 以不可靠的方式存取或者复制内存缓冲区。
(3)编译器设置的内存缓冲区太靠近关键数据结构。
因素分析
1.内存溢出问题是 C 语言或者 C++ 语言所固有的缺陷,它们既不检查数组边界,又不检查类型可靠性(type-safety)。众所周知,用 C/C++ 语言开发的程序由于目标代码非常接近机器内核,因而能够直接访问内存和寄存器,这种特性大大提升了 C/C++ 语言代码的性能。只要合理编码,C/C++应用程序在执行效率上必然优于其它高级语言。然而,C/C++ 语言导致内存溢出问题的可能性也要大许多。其他语言也存在内存溢出问题,但它往往不是程序员的失误,而是应用程序的运行时环境出错所致。
2. 当应用程序读取用户(也可能是恶意攻击者)数据,试图复制到应用程序开辟的内存缓冲区中,却无法保证缓冲区的空间足够时(换言之,假设代码申请了 N 字节大小的内存缓冲区,随后又向其中复制超过 N 字节的数据)。内存缓冲区就可能会溢出。想一想,如果你向 12 盎司的玻璃杯中倒入 16 盎司水,那么多出来的 4 盎司水怎么办?当然会满到玻璃杯外面了!
3. 最重要的是,C/C++编译器开辟的内存缓冲区常常邻近重要的数据结构。假设某个函数的堆栈紧接在在内存缓冲区后面时,其中保存的函数返回地址就会与内存缓冲区相邻。此时,恶意攻击者就可以向内存缓冲区复制大量数据,从而使得内存缓冲区溢出并覆盖原先保存于堆栈中的函数返回地址。这样,函数的返回地址就被攻击者换成了他指定的数值;一旦函数调用完毕,就会继续执行“函数返回地址”处的代码。非但如此,C++ 的某些其它数据结构,比如 v-table 、例外事件处理程序、函数指针等,也可能受到类似的攻击。
32. C++中的const的内存分配问题?
在c++我们知道一般是采用const来进行替代#define的。
在C中我们知道#define进行预定义的某个数是被分配内存的,其文件在编译预处理过程中就会用定义好的数据去替代文中的符号。
但是const却是不一样的,一般情况下编译器也是不为const创建空间的,只是将这个定义的数字保存在符号表中的。但是在下列几种情况下编译器会为const定义的常量分配内存的。
1.使用了extern头
因为使用了extern我们将可能在外部文件使用N,而const默认的是内部链接,所以我们必须要为之分配内存的。
2.取地址操作
const int M=3;
const int *p=&M;//当编译器发现有对const定义的常量进行取地址操作时候会对M进行内存分配,
3.const定义的常量未知的时候,这是#define无法实现的。
const intb=cin.get();//此处const定义的b是未知的所以要为它分配内存,但是一旦分配就不可以改变的。
33. new与malloc的区别,delete和free的区别?
本质区别:
1.malloc/free是C/C++语言的标准库函数,new/delete是C++的运算符
2.new能够自动分配空间大小
3.对于用户自定义的对象而言,用maloc/free无法满足动态管理对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。因此C++需要一个能对对象完成动态内存分配和初始化工作的运算符new,以及一个能对对象完成清理与释放内存工作的运算符delete---简而言之 new/delete能进行对对象进行构造和析构函数的调用进而对内存进行更加详细的工作,而malloc/free不能。
联系
既然new/delete的功能完全覆盖了malloc/free,为什么C++还保留malloc/free呢?因为C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。如果用free释放“new创建的动态对象”,那么该对象因无法执行析构函数而可能导致程序出错。如果用delete释放“malloc申请的动态内存”,理论上讲程序不会出错,但是该程序的可读性很差。所以new/delete,malloc/free必须配对使用。
34. 为什么要用static_cast转换而不用c语言中的转换?
应使用static_cast取代c风格的强制类型转换,较安全。
A、B为定义的不同类:
B b;
A* p1 = (A*) &b; // 这句是c风格的强制类型转换,编译不会报错,留下了隐患
A* p2 =static_cast<A*>(&b); // static_cast在编译时进行了类型检查,直接报错
35.c++异常机制是怎么回事?
异常处理是C++的一项语言机制,用于在程序中处理异常事件。异常事件在C++中表示为异常对象。异常事件发生时,程序使用throw关键字抛出异常表达式,抛出点称为异常出现点,由操作系统为程序设置当前异常对象,然后执行程序的当前异常处理代码块,在包含了异常出现点的最内层的try块,依次匹配catch语句中的异常对象(只进行类型匹配,catch参数有时在catch语句中并不会使用到)。若匹配成功,则执行catch块内的异常处理语句,然后接着执行try...catch...块之后的代码。如果在当前的try...catch...块内找不到匹配该异常对象的catch语句,则由更外层的try...catch...块来处理该异常;如果当前函数内所有的try...catch...块都不能匹配该异常,则递归回退到调用栈的上一层去处理该异常。如果一直退到主函数main()都不能处理该异常,则调用系统函数terminate()终止程序。
一个最简单的try...catch...的例子如下所示。我们有个程序用来记班级学生考试成绩,考试成绩分数的范围在0-100之间,不在此范围内视为数据异常:
int main()
{
int score=0;
while (cin >> score)
{
try
{
if (score > 100 || score < 0)
{
throw score;
}
//将分数写入文件或进行其他操作
}
catch (int score)
{
cerr << "你输入的分数数值有问题,请重新输入!";
continue;
}
}
}
36. 迭代器删除元素
序列性容器:(vector、list和deque)
erase迭代器不仅使所指向被删元素的迭代器失效,而且使被删元素之后的所有迭代器失效,所以不能使用erase(iter++)的方式,但是erase的返回值为下一个有效的迭代器。
正确方法:
vector<int> vec;
vector<int>::iterator iter;
for(iter = vec.begin();iter!=vec.end();)
{
if(need_delete)
{
iter = vec.erase(iter);
}else
++iter;
}
关联性容器:(map和set比较常用)
erase迭代器只是被删元素的迭代器失效,但是返回值为void,所以要采用erase(iter++)的方式删除迭代器,所以正确的方法:
map<int,int> mymap;
map<int,int>::iteraror iter;
for(iter=mymap.begin();iter!=mymap.end();)
{
if(need_delete)
{
mymap.erase(iter++)
}else
++iter;
}
mymap.erase删除的是Iter的一个副本,真正的Iter已经递增了
Tips:其实对于list两种方式都可以正常工作(list是一个线性双向链表结构)
37. 必须在构造函数初始化式里进行初始化的数据成员有哪些?
构造函数中,成员变量一定要通过初始化列表来初始化的有以下几种情况:
1、const常量成员,因为常量只能在初始化,不能赋值,所以必须放在初始化列表中;
2、引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表中;
3、没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数;
38. auto_ptr类(auto_ptr智能指针)
auto_ptr是一个模板类,用于管理动态内存分配。
例:
void remodel (string& str)
{
string * ps = new string(str);
...
if (weird_thing())
throw exception();
str = *ps;
delete ps;
return;
}
在上述代码中,当出现异常时,delete将不被执行,因此将导致内存泄露。
解决方法:如果一个指针对象,那么可以在对象过期时,让它的析构函数删除被指向的内存。这就是auto_ptr背后的思想。
auto_ptr的头文件为<memory>
使用方法:
auto_ptr<double>pd(new double);
auto_ptr<string>ps(new string);
上述函数经修改后如下所示:
void remodel (string& str)
{
auto_ptr<string>ps (new string(str));
...
if (weird_thing())
throw exception();
str = *ps;
return;
}
注意:auto_ptr的构造函数是显式的,这意味着不存在从指针到auto_ptr对象的隐式类型转换:
auto_ptr<double>pd;
double *p_reg = new double;
pd = p_reg; //not allowed
p_reg = pd; //not allowed
pd =auto_ptr<double>(p_reg);//allowed
auto_ptr<double>pauto(p_reg);//allowed
注意事项:
只能对new分配的内存使用auto_ptr对象,而不要对由new[]分配的或通过声明变量分配的内存使用它。
由于auto_ptr被销毁时会自动删除它所指之物,所以不能让多个auto_ptr同时指向同一对象,以避免对象被删除一次以上。
39.c++类的构造函数分类:
无参数构造函数
一般构造函数
类型转换构造函数(单参数)
复制(拷贝)构造函数(参数为类对象引用constA & a)
40等号运算符重载(赋值运算符函数)
// 注意,这个类似复制构造函数,将=右边的本类对象的值复制给等号左边的对象,它不属于构造函数,等号左右两边的对象必须已经被创建
// 若没有显示的写=运算符重载,则系统也会创建一个默认的=运算符重载,只做一些基本的拷贝工作
//如果传入的参数不是引用而是实例,那么从形参到实参会调用一次复制构造函数,增加无谓消耗
Complex &operator=( const Complex&rhs ) //返回引用,才能允许连续赋值
{
// 首先检测等号右边的是否就是左边的对象本,若是本对象本身,则直接返回
if ( this == &rhs )
{
return *this;
}
// 复制等号右边的成员到左边的对象中
this->m_real = rhs.m_real;
this->m_imag = rhs.m_imag;
// 把等号左边的对象再次传出
// 目的是为了支持连等eg: a=b=c 系统首先运行 b=c
// 然后运行 a= ( b=c的返回值,这里应该是复制c值后的b对象)
return *this;
}
41.
Char *m_pData;
m_pData = new char[strlen(str.m_pData)+1]
Strcpy(m_pData,str.m_pData)
在分配内存时strlen函数结果要加1,因为strlen只计算字符个数,不包括‘\0’
如果不加1,申请的内存不够,会越界。
42.单例模式:只有一个实例
singleton pattern单例模式:确保某一个类在程序运行中只能生成一个实例,并提供一个访问它的全局访问点。这个类称为单例类。
众所周知,c++中,类对象被创建时,编译系统为对象分配内存空间,并自动调用构造函数,由构造函数完成成员的初始化工作,也就是说使用构造函数来初始化对象。
1、那么我们需要把构造函数设置为私有的private,这样可以禁止别人使用构造函数创建其他的实例。
2、又单例类要一直向系统提供这个实例,那么,需要声明它为静态的实例成员,在需要的时候,才创建该实例。
3、且应该把这个静态成员设置为 null,在一个public 的方法里去判断,只有在静态实例成员为 null,也就是没有被初始化的时候,才去初始化它,且只被初始化一次。