以下题目都是来自何海涛博客的 C/C++/C#面试题精选 系列,感谢博主的整理。这些题不仅帮助我通过一些笔试,更重要的是帮助我很好的理解C/C++。以前很讨厌这类题,现在想来做些题还是很有用的,至少可以帮助你巩固知识加深理解。
为支持博主的原创,我附上文章的连接地址 点击打开链接。
在阅读中发现博主解释的还不够详细,为了能够更好的帮助网友,同时也帮助自己梳理一下知识,我重新整理了一下。
【1】C++中我们可以用static修饰一个类的成员函数,也可以用const修饰类的成员函数(写在函数的最后表示不能修改成员变量,不是指写在前面表示返回值为常量)。请问:能不能同时用static和const修饰类的成员函数?
解析:答案是不可以的。我们用const修饰类的成员函数是为了类的实例不被该成员函数修改,其实现原理是当我们使用了const修饰一个成员函数时,比如下面的一个成员函数
void method(int parament) const;
编译器编译后是这样的
void method(const *this,int parament)
我们知道在类的方法中所有访问到类的成员时,都会在编译时加上一个隐藏的this指针,而this指针指向的是具体的那个实例本身。这也是实例化于同一个类的具体实例只通过同一个函数却能够正确的访问到属于自己的成员的原因。
回到正题,对this指针加上const修饰后我们就不能在这个方法中改变其成员变量了。
对于static修饰的成员函数,我们需要的是其能够改变实例的静态变量。既然能够改变也就不能够有const修饰了,因为这是冲突的。Static修饰的成员函数只能访问静态变量,而静态变量不属于具体的实例,它是所有产生于同一个类的具体实例都能够访问的。
通俗点说就是const修饰的是不变,static修饰的是变。两者一起用是有矛盾的。
【2】运行下面C++代码,输出是什么?
class A { }; class B { public: B() {} ~B() {} }; class C { public: C() {} virtual ~C() {} }; int main(int argc,char* argv[]) { printf("%d, %d, %d\n", sizeof(A), sizeof(B), sizeof(C)); return 0; }
解析:class A是一个空类型,它的实例不包含任何信息,本来求sizeof应该是0。但当我们声明该类型的实例的时候,它必须在内存中占有一定的空间,否则无法使用这些实例。至于占用多少内存,由编译器决定。Visual Studio 2008中每个空类型的实例占用一个byte的空间。
class B在class A的基础上添加了构造函数和析构函数。由于构造函数和析构函数的调用与类型的实例无关(调用它们只需要知道函数地址即可),在它的实例中不需要增加任何信息。所以sizeof(B)和sizeof(A)一样,在Visual Studio 2008中都是1。
class C在class B的基础上把析构函数标注为虚拟函数。C++的编译器一旦发现一个类型中有虚拟函数,就会为该类型生成虚函数表,并在该类型的每一个实例中添加一个指向虚函数表的指针。在32位的机器上,一个指针占4个字节的空间,因此sizeof(C)是4。
关于虚函数有篇博文感觉写的很不错:陈皓的一篇博文点击打开链接
【3】运行下面的C++代码,得到的结果是什么?
class A { private: int m_value; public: A(int value) { m_value = value; } void Print1() { printf("hello world"); } void Print2() { printf("%d", m_value); } }; int main(int argc, char* argv[]) { A* pA = NULL; pA->Print1(); pA->Print2(); return 0; }
可见输出hello world后程序就崩溃了!
解析:答案是Print1调用正常,打印出hello world,但运行至Print2时,程序崩溃。调用Print1时,并不需要pA的地址,因为Print1的函数地址是固定的。编译器会给Print1传入一个this指针,该指针为NULL,但在Print1中该this指针并没有用到。只要程序运行时没有访问不该访问的内存就不会出错,因此运行正常。在运行print2时,需要this指针才能得到m_value的值。由于此时this指针为NULL,因此程序崩溃了。
【4】.运行下面的C++代码,得到的结果是什么?
class A { private: int m_value; public: A(int value) { m_value = value; } void Print1() { printf("hello world"); } virtual void Print2() { printf("hello world"); } }; int main(int argc, char* argv[]) { A* pA = NULL; pA->Print1(); pA->Print2(); return 0; }
解析:答案是Print1调用正常,打印出hello world,但运行至Print2时,程序崩溃。Print1的调用情况和上面的题目一样,不在赘述。由于Print2是虚函数。C++调用虚函数的时候,要根据实例(即this指针指向的实例)中虚函数表指针得到虚函数表,再从虚函数表中找到函数的地址。由于这一步需要访问实例的地址(即this指针),而此时this指针为空指针,因此导致内存访问出错。
【5】.C++中静态成员函数能不能同时也是虚函数?
分析:答案是不能。调用静态成员函数不要实例。但调用虚函数需要从一个实例中指向虚函数表的指针以得到函数的地址,因此调用虚函数需要一个实例。两者相互矛盾。
上面的5个题主要涉及cosnt成员函数,static成员函数,虚函数。
【6】.运行下列C++代码,输出什么?
struct Point3D { int x; int y; int z; }; int main(int argc, char* argv[]) { Point3D* pPoint = NULL; int offset = (int)(&(pPoint)->z); printf("%d", offset); return 0; }
解析:由于在pPoint->z的前面加上了取地址符号,运行到此时的时候,会在pPoint的指针地址上加z在类型Point3D中的偏移量8。由于pPoint的地址是0,因此最终offset的值是8。&(pPoint->z)的语意是求pPoint中变量z的地址(pPoint的地址0加z的偏移量8),并不需要访问pPoint指向的内存。只要不访问非法的内存,程序就不会出错。
【7】.运行下列C++代码,输出什么?
class A { public: A() { Print(); } virtual void Print() { printf("A is constructed.\n"); } }; class B: public A { public: B() { Print(); } virtual void Print() { printf("B is constructed.\n"); } }; int main(int argc, char* argv[]) { A* pA = new B(); delete pA; return 0; }运行结果:
解析:调用B的构造函数时,先会调用B的基类即A的构造函数。然后在A的构造函数里调用Print。由于此时实例的类型B的部分还没有构造好,本质上它只是A的一个实例,他的虚函数表指针指向的是类型A的虚函数表。因此此时调用的Print是A::Print,而不是B::Print。接着调用类型B的构造函数,并调用Print。此时已经开始构造B,因此此时调用的Print是B::Print。
同样是调用虚拟函数Print,我们发现在类型A的构造函数中,调用的是A::Print,在B的构造函数中,调用的是B::Print。因此虚函数在构造函数中,已经失去了虚函数的动态绑定特性。
【8】.运行下图中的C++代码,输出是什么?
class A { private: int n1; int n2; public: A(): n2(0), n1(n2 + 2) { } void Print() { std::cout << "n1: " << n1 << ", n2: " << n2 << std::endl; } }; int main(int argc,char* argv[]) { A a; a.Print(); return 0; }
解析:输出n1是一个随机的数字,n2为0。在C++中,成员变量的初始化顺序与变量在类型中的申明顺序相同,而与它们在构造函数的初始化列表中的顺序无关。因此在这道题中,会首先初始化n1,而初始n1的参数n2还没有初始化,是一个随机值,因此n1就是一个随机值。初始化n2时,根据参数0对其初始化,故n2=0。
【9】.编译运行下图中的C++代码,结果是什么?(A)编译错误;(B)编译成功,运行时程序崩溃;(C)编译运行正常,输出10。请选择正确答案并分析原因。
class A { private: int value; public: A(int n) { value = n; } A(A other) { value = other.value; } void Print() { std::cout << value << std::endl; } }; int main(int argc,char* argv[]) { A a = 10; A b = a; b.Print(); return 0; }
解析:编译错误。在复制构造函数中传入的参数是A的一个实例。由于是传值,把形参拷贝到实参会调用复制构造函数。因此如果允许复制构造函数传值,那么会形成永无休止的递归并造成栈溢出。因此C++的标准不允许复制构造函数传值参数,而必须是传引用或者常量引用。在Visual Studio和GCC中,都将编译出错。
【10】.运行下图中的C++代码,输出是什么?
int SizeOf(char pString[]) { return sizeof(pString); } int main(int argc, char* argv[]) { char* pString1 = "google"; int size1 = sizeof(pString1); int size2 = sizeof(*pString1); char pString2[100] = "google"; int size3 = sizeof(pString2); int size4 = SizeOf(pString2); printf("%d, %d, %d, %d\n", size1, size2, size3, size4); return 0; }运行结果:
解析:pString1是一个指针。在32位机器上,任意指针都占4个字节的空间。*pString1是字符串pString1的第一个字符。一个字符占一个字节。pString2是一个数组,sizeof(pString2)是求数组的大小。这个数组包含100个字符,因此大小是100个字节。而在函数SizeOf中,虽然传入的参数是一个字符数组,当数组作为函数的参数进行传递时,数组就自动退化为同类型的指针。
【11】.运行下图中代码,输出的结果是什么?这段代码有什么问题?
class A { public: A() { std::cout << "A is created." << std::endl; } ~A() { std::cout << "A is deleted." << std::endl; } }; class B : public A { public: B() { std::cout << "B is created." << std::endl; } ~B() { std::cout << "B is deleted." << std::endl; } }; int main(int argc,char* argv[]) { A* pA = new B(); delete pA; return 0; }
解析:用new创建B时,会调用B的构造函数。在调用B的构造函数的时候,会先调用A的构造函数。因此先输出A is created. B is created.
接下来运行delete语句时,会调用析构函数。由于pA被声明成类型A的指针,同时基类A的析构函数没有标上virtual,因此只有A的析构函数被调用到,而不会调用B的析构函数。
由于pA实际上是指向一个B的实例的指针,但在析构的时候只调用了基类A的析构函数,却没有调用B的析构函数。这就是一个问题。如果在类型B中创建了一些资源,比如文件句柄、内存等,在这种情况下都得不到释放,从而导致资源泄漏。
【12】.运行如下的C++代码,输出是什么?
class A { public: virtual void Fun(int number = 10) { std::cout << "A::Fun with number " << number << endl; } }; class B:public A { public: virtual void Fun(int number = 20) { std::cout << "B::Fun with number " << number << endl; } }; int main() { B b; A &a = b; a.Fun(); return 0; }
运行结果:
这一题的关键在于理解确定缺省参数的值是在编译的时候,但确定引用、指针的虚函数调用哪个类型的函数是在运行的时候。
【13】.运行如下的C代码,输出是什么?
char* GetString1() { char p[] = "Hello World"; return p; } char* GetString2() { char *p = "Hello World"; return p; } int main(int argc,char* argv[]) { printf("GetString1 returns: %s. \n", GetString1()); printf("GetString2 returns: %s. \n", GetString2()); return 0; }
当运行到GetString1时,p是一个数组,会开辟一块内存,并拷贝"Hello World"初始化该数组。接着返回数组的首地址并退出该函数。由于p是GetString1内的一个局部变量,当运行到这个函数外面的时候,这个数组的内存会被释放掉。因此在_tmain函数里再去访问这个数组的内容时,结果是随机的。编译程序时,编译器也会给出相关的警告信息。
当运行到GetString2时,p是一个指针,它指向的是字符串常量区的一个常量字符串。该常量字符串是一个全局的,并不会因为退出函数GetString2而被释放掉。因此在_tmain中仍然根据GetString2返回的地址得到字符串"Hello World"。
【14】.运行下图中C代码,输出的结果是什么?
int main(int argc,char* argv[]) { char str1[] = "hello world"; char str2[] = "hello world"; char* str3 = "hello world"; char* str4 = "hello world"; if(str1 == str2) printf("str1 and str2 are same.\n"); else printf("str1 and str2 are not same.\n"); if(str3 == str4) printf("str3 and str4 are same.\n"); else printf("str3 and str4 are not same.\n"); return 0; }运行结果:
解析:str1和str2是两个字符串数组。我们会为它们分配两个长度为12个字节的空间,并把"hello world"的内容分别拷贝到数组中去。这是两个初始地址不同的数组,因此比较str1和str2的值,会不相同。str3和str4是两个指针,我们无需为它们分配内存以存储字符串的内容,而只需要把它们指向"hello world“在内存中的地址就可以了。由于"hello world”是常量字符串,它在内存中只有一个拷贝,因此str3和str4指向的是同一个地址。因此比较str3和str4的值,会是相同的。
【15】.运行下图中的C++代码,打印出的结果是什么?
bool Fun1(char* str) { printf("%s\n",str); return false; } bool Fun2(char* str) { printf("%s\n",str); return true; } int _tmain(int argc, _TCHAR* argv[]) { bool res1,res2; res1 = (Fun1("a")&& Fun2("b")) || (Fun1("c") || Fun2("d")); res2 = (Fun1("a")&& Fun2("b")) &&(Fun1("c") || Fun2("d")); return res1|| res2; }运行结果:
解析:在C/C++中,与、或运算是从左到右的顺序执行的。在计算rest1时,先计算Fun1(“a”)&& Func2(“b”)。首先Func1(“a”)打印出内容为a的一行。由于Fun1(“a”)返回的是false,无论Func2(“b”)的返回值是true还是false,Fun1(“a”)&& Func2(“b”)的结果都是false。由于Func2(“b”)的结果无关重要,因此Func2(“b”)会略去而不做计算。接下来计算Fun1(“c”)|| Func2(“d”),分别打印出内容c和d的两行。
在计算rest2时,首先Func1(“a”)打印出内容为a的一行。由于Func1(“a”)返回false,和前面一样的道理,Func2(“b”)会略去不做计算。由于Fun1(“a”)&& Func2(“b”)的结果是false,不管Fun1(“c”)&& Func2(“d”)的结果是什么,整个表达式得到的结果都是false,因此Fun1(“c”) || Func2(“d”)都将被忽略。
【16】.运行下面的C++代码,打印的结果是什么?
class Base { public: void print() { doPrint();} private: virtual void doPrint() {cout << "Base::doPrint" << endl;} }; class Derived : public Base { private: virtual void doPrint() {cout << "Derived::doPrint" << endl;} }; int _tmain(int argc, _TCHAR* argv[]) { Base b; b.print(); Derived d; d.print(); return 0; }
解析:在print中调用doPrint时,doPrint()的写法和this->doPrint()是等价的,因此将根据实际的类型调用对应的doPrint。所以结果是分别调用的是Base::doPrint和Derived::doPrint2。如果感兴趣,可以查看一下汇编代码,就能看出来调用doPrint是从虚函数表中得到函数地址的。