此文章不断更新从 ”一问一答“上遇到的做错的题目,持续更新
No.1 by 17th Sep 2013
以下代码的输出结果
#include <iostream> using namespace std; int compare(char *a, char *b){ if((strlen(a)-strlen(b))>=0) return 1; else return 0; } int main(){ cout<<compare("ab","abc")<<endl; cout<<compare("abce","abc"); system("pause"); return 0; }
当时百思不得其解为什么会输出11 而不是01;
后来google了后知道strlen返回值为size_t,而size_t 是unsigned int型的,所以 strlen("ab")-strlen("abc")得到的是很大的正数
No.2 by 22nd September 2013
假设AB是一个类,则以下代码中,得到的动态变量为:
AB *s = new AB(a,5);A: *s B: s->a C:s.a D:s
Answer: 一般来说,编译器将内存分为3部分:静态存储区,栈和堆。 静态存储区存储的主要是全局变量和静态变量,栈主要存储函数调用中的变量,地址等,堆主要存储动态变量,C中指 malloc,free等操作,C++中有new和delete等操作
题目中,首先编译器利用new 为对象s创建分配内存,再调用AB类的构造函数,对存储区域进行初始化,语句返回对象的地址,及一个AB类的指针。如果内存不足,则分配失败,返回0,并不会调用AB类的构造函数,此时s为0.
可以对全局以及类内部的new和delete进行重载,以完成需要的功能;
还可以定位new和delete,使其对在指定的内存地址为对象分配空间。
delete只能删除new生成的对象,并且不能删除void* 的指针,因为对象类型未定义,无法调用其析构函数,容易引起内存泄露。
C++中, 一个对象或对象的引用a,则用a. 来访问其成员,一个对象指针b,则用b->来访问其成员
答案A s是一个指针,指向的内容是动态生成的变量,及*s
No.3 by 26th Sep 2013
class A { public: int iValue; }; class B:public A { public: void bPrintf(){cout<<"This is class B"<<endl;}; }; class C:public A { public: void cPrintf(){cout<<"This is class C"<<endl;}; }; class D:public B,public C { public: void dPrintf(){cout<<"This is class D"<<endl;}; }; void main() { D d; cout<<d.iValue<<endl; //错误,不明确的访问 cout<<d.A::iValue<<endl; //正确 cout<<d.B::iValue<<endl; //正确 cout<<d.C::iValue<<endl; //正确 }代码中类B和类C都public继承自类A,因此类B和类C都有一个成员 iValue,而类D又多重继承自B C,这样类D就有一个重名的成员iValue,一个来自B一个来自C,在主函数中调用d.iValue就让编译器产生困惑不知道该调用哪一个iValue,就产生了二义性。正确的做法是采用作用域限定符来告诉编译器到底调用那个成员。但是这样的代码会是的在类D的实例中会有多个iValue的实例,这样就占用了额外的空间,因此C++中引入了虚基类的概念来解决该问题。
class A { public: int iValue; }; class B:virtual public A { public: void bPrintf(){cout<<"This is class B"<<endl;}; }; class C:virtual public A { public: void cPrintf(){cout<<"This is class C"<<endl;}; }; class D:public B,public C { public: void dPrintf(){cout<<"This is class D"<<endl;}; }; void main() { D d; cout<<d.iValue<<endl; //正确 }在继承的类的前面加上关键字virtual表示被继承的类是一个虚基类,它的被继承的成员在派生类中只保留一个实例,例如iValue,类D的对象d虽然是继承自B和C,但是B和C中只有一个iValue的副本,因此在主函数中调用d.iValue就不会产生二义性,同时又节省了存储空间。
因此,本题的答案为 正确。
No.5 By 6th November 2013
求下面代码的输出结果:
class A { public: void funPrint(){cout<<"funPrint of class A"<<endl;}; }; class B:public A { public: void funPrint(){cout<<"funPrint of class B"<<endl;}; }; void main() { A *p; //定义基类的指针 A a; B b; p=&a; p->funPrint(); p=&b; p->funPrint(); }不管引用的实例是哪个类的当你调用的时候系统会调用左值那个对象所属类的方法。比如说 上面的代码类A B都有一个funPrint 函数,因为p是一个A类的指针,所以不管你将p指针指向类A或是类B,最终调用的函数都是类A的funPrint 函数。这就是静态联篇,编译器在编译的时候就已经确定好了。可是如果我想实现跟据实例的不同来动态决定调用哪个函数呢?这就须要用到 虚函数(也就是动态联篇)。
class A { public: virtual void funPrint(){cout<<"funPrint of class A"<<endl;}; }; class B:public A { public: virtual void funPrint(){cout<<"funPrint of class B"<<endl;}; }; void main() { A *p; //定义基类的指针 A a; B b; p=&a; p->funPrint(); p=&b; p->funPrint(); }在基类的成员函数前加virtual关键字表示这个函数是一个虚函数,所谓虚函数就是在编译的时候不确定要调用哪个函数,而是动态决定将要调 用哪个函数,要实现虚函数必须派生类的函数名与基类相同,参数名参数类型等也要与基类相同。但派生类中的virtual关键字可以省略,也表 示这是一个虚函数。下面来解决一下代码,声明一个基类的指针(必须是基类,反之则不行)p,把p指向类A的实例a,调用funPrint函数,这 时系统会判断p所指向的实例的类型,如果是A类的实例就调用A类的funPrint函数,如果是B类的实例就调用B类的funPrint函数。
因此原题目中两个函数调用的输出结果都是funPrint of class A。
小节:关于虚基类和虚函数:
虚基类
1, 一个类可以在一个类族中既被用作虚基类,也被用作非虚基类。
2, 在派生类的对象中,同名的虚基类只产生一个虚基类子对象,而某个非虚基类产生各自的子对象。
3, 虚基类子对象是由最派生类的构造函数通过调用虚基类的构造函数进行初始化的。
4, 最派生类是指在继承结构中建立对象时所指定的类。
5, 派生类的构造函数的成员初始化列表中必须列出对虚基类构造函数的调用;如果未列出,则表示使用该虚基类的缺省构造函数。
6, 从虚基类直接或间接派生的派生类中的构造函数的成员初始化列表中都要列出对虚基类构造函数的调用。但只有用于建立对象的最派生 类的构造函数调用虚基类的构造函数,而该派生类的所有基类中列出的对虚基类的构造函数的调用在执行中被忽略,从而保证对虚基类子对象 只初始化一次。
7, 在一个成员初始化列表中同时出现对虚基类和非虚基类构造函数的调用时,虚基类的构造函数先于非虚基类的构造函数执行。
虚函数
1, 虚函数是非静态的、非内联的成员函数,而不能是友元函数,但虚函数可以在另一个类中被声明为友元函数。
2, 虚函数声明只能出现在类定义的函数原型声明中,而不能在成员函数的函数体实现的时候声明。
3, 一个虚函数无论被公有继承多少次,它仍然保持其虚函数的特性。
4, 若类中一个成员函数被说明为虚函数,则该成员函数在派生类中可能有不同的实现。当使用该成员函数操作指针或引用所标识的对象时 ,对该成员函数调用可采用动态联编。
5, 定义了虚函数后,程序中声明的指向基类的指针就可以指向其派生类。在执行过程中,该函数可以不断改变它所指向的对象,调用不同 版本的成员函数,而且这些动作都是在运行时动态实现的。虚函数充分体现了面向对象程序设计的动态多态性。 纯虚函数 版本的成员函数,而且这些动作都是在运行时动态实现的。虚函数充分体现了面向对象程序设计的动态多态性。
No.6 Friend function by 6th Nov 2013
Question:判断:
一个类的友元函数能访问该类的所有成员。
如果将类的封装比喻成一堵墙的话,那么友元机制就像墙上了开了一个门,那些得 到允许的类或函数允许通过这个门访问一般的类或者函数无法访问的私有属性和方法。友元机制使类的封装性得到消弱,所以使用时一定要慎重。友元类的说明将外界的某个类在本类别的定义中说明为友元,那么外界的类就成为本类的“朋 友”,那个类就可以访问本类的私有数据了。
通常对于普通函数来说,要访问类的保护成员是不可能的,如果想这么做那么必须把类的成员都生命成为public(共用的),然而这做带来的问题遍是任何外部函数都可以毫无约束的访问它操作它,c++利用friend修饰符,可以让一些你设定的函数能够对这些保护数据进行操作,避免把类成员全部设置成public,最大限度的保护数据成员的安全。友元能够使得普通函数直接访问类的保护数据,避免了类成员函数的频繁调用,可以节约处理器开销,提高程序的效率,但所矛盾的是,即使是最大限度大保护,同样也破坏了类的封装特性,这即是友元的缺点,在现在cpu速度越来越快的今天我们并不推荐使用它,但它作为c++一个必要的知识点,一个完整的组成部分,我们还是需要讨论一下的。在类里声明一个普通数学,在前面加上friend修饰,那么这个函数就成了该类的友元,可以访问该类的一切成员。
No.7 派生类构造函数的创建顺序 by 6th Nov 2013
Question: 创建派生类成员时,各构造函数的构造顺序为?
当派生类中不含对象成员时
●在创建派生类对象时,构造函数的执行顺序是:基类的构造函数→派生类的构造函数;
●在撤消派生类对象时,析构函数的执行顺序是:派生类的构造函数→基类的构造函数。
当派生类中含有对象成员时
●在定义派生类对象时,构造函数的执行顺序:基类的构造函数→对象成员的构造函数→派生类的构造函数;
●在撤消派生类对象时,析构函数的执行顺序:派生类的构造函数→对象成员的构造函数→基类的构造函数。.
构造和析构的过程是在stack上进行的,所以析构时和构造时是相反的。
No.7 by 6th Nov 2013
int a = 1; int b = 2; int c = 3; int d = 4; int m = 5; int n = 6; int e = (m = a>b)&&(n = c>d); cout<<e<<endl; cout<<m<<endl; cout<<n<<endl;a>b为0, m为0之后,由于是与运算,后面的式子就不需要计算了,所有n的值不变
最后结果为 0 0 6
No.8 Pointer Operate by 6th Nov 2013
对于同类型的指针变量,不能进行的操作是: A. == B. + C. - D. = E. >
指针指的是内存中的地址,例如一个指针指向的地址是0x80008000, 另一个指向的是0x80008004
两个指针可以进行大小,相等等比较操作,两个指针相减,得到两个指针的差值,及两指针之间的距离,但是两个指针相加,得到的结果没有意义,所以没有指针相加的操作。
No.9 Virtual Function by 14th Nov 2013
构造函数,内联函数和静态函数都不能是虚函数
构造函数:
从语义上理解,构造函数本身就是为了明确初始化对象本身才有的,而虚函数的作用是告诉编译器现在还不明确,不确定什么时候调用该函数。
内联函数:
内联函数是在编译时被展开,减少函数调用的花销,而虚函数是在运行时才被动态调用,这两者本身就是矛盾的
静态函数:
静态函数对于一个类来说只有一份代码,所有的成员都共享该份代码,同内联函数一样,静态函数是在编译期时就需要确定的,所以静态函数也不能成为虚函数。