c++整理.docx
《c++整理.docx》由会员分享,可在线阅读,更多相关《c++整理.docx(19页珍藏版)》请在冰豆网上搜索。
c++整理
1.new、、malloc、free关系
会调用对象的析构函数,和new对应free只会释放内存,new调用构造函数。
malloc与free是C++/C语言的标准库函数,new/是C++的运算符。
它们都可用于申请动态内存和释放内存。
对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。
对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。
由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符。
注意new/不是库函数。
总结:
new和会自动调用对象的构造与析构函数而malloc与free不会;
new和式C++运算符,而malloc和free是C/C++标准库函数。
——————————————————————————————–
2.与[]区别
只会调用一次析构函数,而[]会调用每一个成员的析构函数。
在MoreEffectiveC++中有更为详细的解释:
“当操作符用于数组时,它为每个数组元素调用析构函数,然后调用operator来释放内存。
”与New配套,[]与new[]配套
MemTest*mTest1=newMemTest[10];
MemTest*mTest2=newMemTest;
int*pInt1=newint[10];
int*pInt2=newint;
[]pInt1;//-1-
[]pInt2;//-2-
[]mTest1;//-3-
[]mTest2;//-4-
在-4-处报错。
这就说明:
对于内建简单数据类型,和[]功能是相同的。
对于自定义的复杂数据类型,和[]不能互用。
[]删除一个数组,删除一个指针简单来说,用new分配的内存用删除用new[]分配的内存用[]删除[]会调用数组元素的析构函数。
内部数据类型没有析构函数,所以问题不大。
如果你在用时没用括号,就会认为指向的是单个对象,否则,它就会认为指向的是一个数组。
总结:
只会调用一次析构函数,而[]会调用每一个成员的析构函数。
——————————————————————————————–
3.CC++JAVA共同点,不同之处?
相同点:
都是面向对象的语言
不同点:
c/c++是编译型语言,还有一些语言完全是解释型的(如Basie),而java既是编译型的又是解释型的语言
c/c++存在指针运算,Basie没有显示指针,而java有指针,但取消了指针的运算
——————————————————————————————–
4.继承优缺点。
类继承是在编译时刻静态定义的,且可直接使用,类继承可以较方便地改变父类的实现。
但是类继承也有一些不足之处。
首先,因为继承在编译时刻就定义了,所以无法在运行时刻改变从父类继承的实现。
更糟的是,父类通常至少定义了子类的部分行为,父类的任何改变都可能影响子类的行为。
如果继承下来的实现不适合解决新的问题,则父类必须重写或被其他更适合的类替换。
这种依赖关系限制了灵活性并最终限制了复用性。
——————————————————————————————–
5.C++有哪些性质(面向对象特点)
封装,继承和多态。
在面向对象程序设计语言中,封装是利用可重用成分构造软件系统的特性,它不仅支持系统的可重用性,而且还有利于提高系统的可扩充性;消息传递可以实现发送一个通用的消息而调用不同的方法;封装是实现信息隐蔽的一种技术,其目的是使类的定义和实现分离。
——————————————————————————————–
6.子类析构时要调用父类的析构函数吗?
析构函数调用的次序是先派生类的析构后基类的析构,也就是说在基类的的析构调用的时候,派生类的信息已经全部销毁了定义一个对象时先调用基类的构造函数、然后调用派生类的构造函数;析构的时候恰好相反:
先调用派生类的析构函数、然后调用基类的析构函数JAVA无析构函数深拷贝和浅拷贝
——————————————————————————————–
7.多态,虚函数,纯虚函数
这么一大堆名词,实际上就围绕一件事展开,就是多态,其他三个名词都是为实现C++的多态机制而提出的一些规则,下面分两部分介绍,第一部分介绍【多态】,第二部分介绍【虚函数,纯虚函数,抽象类】
一【多态】
多态的概念:
关于多态,好几种说法,好的坏的都有,分别说一下:
1指同一个函数的多种形态。
个人认为这是一种高手中的高手喜欢的说法,对于一般开发人员是一种差的不能再差的概念,简直是对人的误导,然人很容易就靠到函数重载上了。
以下是个人认为解释的比较好的两种说法,意思大体相同:
2多态是具有表现多种形态的能力的特征,在OO中是指,语言具有根据对象的类型以不同方式处理之,特别是重载方法和继承类这种形式的能力。
这种说法有点绕,仔细想想,这才是C++要告诉我们的。
3多态性是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。
简单的说,就是一句话:
允许将子类类型的指针赋值给父类类型的指针。
多态性在Objectpascal和C++中都是通过虚函数(VirtualFunction)实现的。
这种说法看来是又易懂,又全面的一种,尤其是最后一句,直接点出了虚函数与多态性的关系,如果你还是不太懂,没关系,再把3读两遍,有个印象,往后看吧。
-–-–-–-–-–-–-–-–-–-–-–-–-–-–-–-
二【虚函数,纯虚函数,抽象类】
多态才说了个概念,有什么用还没说就进入第二部分了?
看看概念3的最后一句,虚函数就是为多态而生的,多态的作用的介绍和虚函数简直关系太大了,就放一起说吧。
多态的作用:
继承是子类使用父类的方法,而多态则是父类使用子类的方法。
这是一句大白话,多态从用法上就是要用父类(确切的说是父类的对象名)去调用子类的方法,例如:
【例一】
classA{
public:
A(){}
(virtual)voidprint(){
cout<<“ThisisA.”<}
};
classB:
publicA{
public:
B(){}
voidprint(){
cout<<“ThisisB.”<print();———————————-make2
//A*a=newB();a->print();——————————–make3
return0;
}
这将显示:
ThisisB.
如果把virtual去掉,将显示:
ThisisA.
(make1,2,3分别是对应兼容规则(后面介绍)的三种方式,调用结果是一样的)
加上virtual,多态了,B中的print被调用了,也就是可以实现父类使用子类的方法。
对多态的作用有一个初步的认识了之后,再提出更官方,也是更准确的对多态作用的描述:
多态性使得能够利用同一类(基类)类型的指针来引用不同类的对象,以及根据所引用对象的不同,以不同的方式执行相同的操作。
把不同的子类对象都当作父类来看,可以屏蔽不同子类对象之间的差异,写出通用的代码,做出通用的编程,以适应需求的不断变化。
赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作(也就是可以调用子对象中对父对象的相关函数的改进方法)。
那么上面例子中为什么去掉virtual就调用的不是B中的方法了呢,明明把B的对象赋给指针a了啊,是因为C++定义了一组对象赋值的兼容规则,就是指在公有派生的情况下,对于某些场合,一个派生类的对象可以作为基类对象来使用,具体来说,就是下面三种情形:
ClassA;
classB:
publicA
1.派生的对象可以赋给基类的对象
Aa;
Bb;
a=b;
2.派生的对象可以初始化基类的引用
Bb;
A&a=b;
3.派生的对象的地址可以赋给指向基类的指针
Bb;
A*a=&b;
或
A*a=newB();
由上述对象赋值兼容规则可知,一个基类的对象可兼容派生类的对象,一个基类的指针可指向派生类的对象,一个基类的引用可引用派生类的对象,于是对于通过基类的对象指针(或引用)对成员函数的调用,编译时无法确定对象的类,而只是在运行时才能确定并由此确定调用哪个类中的成员函数。
看看刚才的例子,根据兼容规则,B的对象根本就被当成了A的对象来使用,难怪B的方法不能被调用。
【例二】
#include
usingnamespacestd;
classA
{
public:
void(virtual)print(){cout<<“Aprint”<
private:
};
classB:
publicA
{
public:
voidprint(){cout<<“Bprint”};
voidtest(A&tmpClass)
{
tmpClass.print();
}
intmain(void)
{
Bb;
test(b);
get);
return0;
}
这将显示:
Bprint
如果把virtual去掉,将显示:
Aprint
那么,为什么加了一个virtual以后就达到调用的目的了呢,多态了嘛~那么为什么加上virtual就多态了呢,我们还要介绍一个概念:
联编
函数的联编:
在编译或运行将函数调用与相应的函数体连接在一起的过程。
1先期联编或静态联编:
在编译时就能进行函数联编称为先期联编或静态联编。
2迟后联编或动态联编:
在运行时才能进行的联编称为迟后联编或动态联编。
那么联编与虚函数有什么关系呢,当然,造成上面例子中的矛盾的原因就是代码的联编过程采用了先期联编,使得编译时系统无法确定究竟应该调用基类中的函数还是应该调用派生类中的函数,要是能够采用上面说的迟后联编就好了,可以在运行时再判断到底是哪个对象,所以,virtual关键字的作用就是提示编译器进行迟后联编,告诉连接过程:
“我是个虚的,先不要连接我,等运行时再说吧”。
那么为什么连接的时候就知道到底是哪个对象了呢,这就引出虚函数的原理了:
当编译器遇到virtual后,会为所在的类构造一个表和一个指针,那个表叫做vtbl,每个类都有自己的vtbl,vtbl的作用就是保存自己类中虚函数的地址,我们可以把vtbl形象地看成一个数组,这个数组的每个元素存放的就是虚函数的地址.指针叫做vptr,指向那个表。
而这个指针保存在相应的对象当中,也就是说只有创建了对象以后才能找到相应虚函数的地址。
【注意】
1为确保运行时的多态定义的基类与派生类的虚函数不仅函数名要相同,其返回值及参数都必须相同,否则即使加上了virtual,系统也不进行迟后联编。
2虚函数关系通过继承关系自动传递给基类中同名的函数,也就是上例中如果A中print有virtual,那么B中的print即使不加virtual,也被自动认为是虚函数。
*3没有继承关系,多态机制没有意义,继承必须是公有继承。
*4现实中,远不只我举的这两个例子,但是大的原则都是我前面说到的“如果发现一个函数需要在派生类里有不同的表现,那么它就应该是虚的”。
这句话也可以反过来说:
“如果你发现基类提供了虚函数,那么你最好override它”。
纯虚函数:
虚函数的作用是为了实现对基类与派生类中的虚函数成员的迟后联编,而纯虚函数是表明不具体实现的虚函数成员,即纯虚函数无实现代码。
其作用仅仅是为其派生类提过一个统一的构架,具体实现在派生类中给出。
一个函数声明为纯虚后,纯虚函数的意思是:
我是一个抽象类!
不要把我实例化!
纯虚函数用来规范派生类的行为,实际上就是所谓的“接口”。
它告诉使用者,我的派生类都会有这个函数。
抽象类:
含有一个或多个纯虚函数的类称为抽象类。
【例三】
#include
usingnamespacestd;
classA
{
public:
virtualfloatprint()=0;
protected:
floath,w;
private:
};
classB:
publicA
{
public:
B(floath0,floatw0){h=h0;w=w0;}
floatprint(){returnh*w;}
private:
};
classC:
publicA
{
public:
C(floath0,floatw0){h=h0;w=w0;}
floatprint(){returnh*w/2;}
private:
};
intmain(void)
{
A*a1,*a2;
Bb(1,2);
Cc(1,2);
a1=&b;
a2=&c;
cout<print()print就不能用了),给多态性造成不便,这里要强调的是,我们是希望用基类的指针调用派生类的方法,希望用到多态机制,如果读者并不想用基类指针,认为用b,c指针直接调用更好,那纯虚函数就没有意义了,多态也就没有意义了,了解一下多态的好处,再决定是否用纯虚函数吧。
【注意】
1抽象类并不能直接定义对象,只可以如上例那样声明指针,用来指向基类派生的子类的对象,上例中的A*a1,*a2;该为Aa1,a2;是错误的。
2从一个抽象类派生的类必须提供纯虚函数的代码实现或依旧指明其为派生类,否则是错误的。
3当一个类打算被用作其它类的基类时,它的析构函数必须是虚的。
【例三】
classA
{
public:
A(){ptra_=newchar[10];}
~A(){[]ptra_;}//非虚析构函数
private:
char*ptra_;
};
classB:
publicA
{
public:
B(){ptrb_=newchar[20];}
~B(){[]ptrb_;}
private:
char*ptrb_;
};
voidfoo()
{
A*a=newB;
a;
}
在这个例子中,程序也许不会象你想象的那样运行,在执行a的时候,实际上只有A:
:
~A()被调用了,而B类的析构函数并没有被调用!
这是否有点儿可怕?
如果将上面A:
:
~A()改为virtual,就可以保证B:
:
~B()也在a的时候被调用了。
因此基类的析构函数都必须是virtual的。
纯虚的析构函数并没有什么作用,是虚的就够了。
通常只有在希望将一个类变成抽象类(不能实例化的类),而这个类又没有合适的函数可以被纯虚化的时候,可以使用纯虚的析构函数来达到目的。
最后通过一个例子说明一下抽象类,纯虚函数以及多态的妙用吧:
我们希望通过一个方法得到不同图形面积的和的方式:
#include
usingnamespacestd;
classA//定义一个抽象类,用来求图形面积
{
public:
virtualfloatarea()=0;//定义一个计算面积的纯虚函数,图形没确定,当
//不能确定具体实现
protected:
floath,w;//这里假设所有图形的面积都可以用h和w两个元素计算得出
//就假设为高和长吧
private:
};
classB:
publicA//定义一个求长方形面积的类
{
public:
B(floath0,floatw0){h=h0;w=w0;}
floatarea(){returnh*w;}//基类纯虚函数的具体实现
private:
};
classC:
publicA//定义一个求三角形面积的类
{
public:
C(floath0,floatw0){h=h0;w=w0;}
floatarea(){returnh*w/2;}//基类纯虚函数的具体实现
private:
};
floatgetTotal(A*s[],intn)//通过一个数组传递所有的图形对象
//多态的好处出来了吧,不是多态,不能用基类A调用
//参数类型怎么写,要是有100个不同的图形,怎么传递
{
floatsum=0;
for(inti=0;iarea();
returnsum;
}
intmain(void)
{
floattotalArea;
A*a[2];
a[0]=newB(1,2);//一个长方形对象
a[1]=newC(1,2);//一个三角形对象
totalArea=getTotal(a,2);//求出两个对象的面积和
get);
return0;
}
——————————————————————————————–
8.求下面函数的返回值(微软)
intfunc(x)
{
intcountx=0;
while(x)
{
countx++;
x=x&(x-1);
}
returncountx;
}
假定x=9999。
答案:
8
思路:
将x转化为2进制,看含有的1的个数。
——————————————————————————————–
9.什么是“引用”?
申明和使用“引用”要注意哪些问题?
答:
引用就是某个目标变量的“别名”(alias),对应用的操作与对变量直接操作效果完全相同。
申明一个引用的时候,切记要对其进行初始化。
引用声明完毕后,相当于目标变量名有两个名称,即该目标原名称和引用名,不能再把该引用名作为其他变量名的别名。
声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。
不能建立数组的引用。
——————————————————————————————–
10.将“引用”作为函数参数有哪些特点?
(1)传递引用给函数与传递指针的效果是一样的。
这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。
(2)使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。
因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。
(3)使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用“*指针变量名”的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。
而引用更容易使用,更清晰。
——————————————————————————————–
11.在什么时候需要使用“常引用”?
如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。
常引用声明方式:
const类型标识符&引用名=目标变量名;
例1
inta;
constint&ra=a;
ra=1;//错误
a=1;//正确
例2
stringfoo();
voidbar(string&s);
那么下面的表达式将是非法的:
bar(foo());
bar(“helloworld”);
原因在于foo()和“helloworld”串都会产生一个临时对象,而在C++中,这些临时对象都是const类型的。
因此上面的表达式就是试图将一个const类型的对象转换为非const类型,这是非法的。
引用型参数应该在能被定义为const的情况下,尽量定义为const。
——————————————————————————————–
12.将“引用”作为函数返回值类型的格式、好处和需要遵守的规则?
格式:
类型标识符&函数名(形参列表及类型说明){//函数体}
好处:
在内存中不产生被返回值的副本;(注意:
正是因为这点原因,所以返回一个局部变量的引用是不可取的。
因为随着该局部变量生存期的结束,相应的引用也会失效,产生runtimeerror!
注意事项:
(1)不能返回局部变量的引用。
这条可以参照EffectiveC++[1]的Item31。
主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了”无所指”的引用,程序会进入未知状态。
(2)不能返回函数内部new分配的内存的引用。
这条可以参照EffectiveC++[1]的Item31。
虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。
例如,被函数返回的引用只是作为一个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memoryleak。
(3)可以返回类成员的引用,但最好是const。
这条原则可以参照EffectiveC++[1]的Item30。
主要原因是当对象的属性是与某种业务规则(businessrule)相关联的时候,其赋值常常与某些其它属性或者对象的状态有关,因此有必要将赋值操作封装在一个业务规则当中。
如果其它对象可以获得该属性的非常量引用(或指针),那么对该属性的单纯赋值就会破坏业务规则的完整性。
(4)流操作符重载返回值申明为“引用”的作用:
流操作符<<和>>,这两个操作符常常希望被连续使用,例如:
cout<<“hello”<可选的其它方案包括:
返回一个流对象和返回一个流对象指针。
但是对于返回一个流对象,程序必须重新(拷贝)构造一个新的流对象,也就是说,连续的两个<<操作符实际上是针对不同对象的!
这无法让人接受。
对于返回一个流指针则不能连续使用<<操作符。
因此,返回一个流对象引用是惟一选择。
这个唯一选择很关键,它说明了引用的重要性以及无可替代性,也许这就是C++语言中引入引用这个概念的原因吧。
赋值操作符=。
这个操作符象流操作符一样,是可以连续使用的,例如:
x=j=10;或者(x=10)=100;赋值操作符的返回值必须是一个左值,以便可以被继续赋值。
因