C/C++经典题解析

以下题目都是来自何海涛博客的 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++代码,输出是什么?

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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的空间。<喎�"http://www.2cto.com/kf/ware/vc/" target="_blank" class="keylink">vcD4KPHA+PGJyPgo8L3A+CjxwPmNsYXNzIELU2mNsYXNzIEG1xLv5tKHJz8ztvNPBy7m51Oy6r8r9us3O9rm5uq/K/aGj08nT2rm51Oy6r8r9us3O9rm5uq/K/bXEtffTw9PrwODQzbXEyrXA/c7eudijqLX308PL/MPH1rvQ6NKq1qq1wLqvyv212Na3vLS/yaOpo6zU2sv8tcTKtcD91tCyu9Do0qrU9rzTyM66ztDFz6Kho8v50tRzaXplb2YoQim6zXNpemVvZihBKdK70fmjrNTaVmlzdWFsCiBTdHVkaW8gMjAwONbQtrzKxzGhozwvcD4KPHA+Y2xhc3MgQ9TaY2xhc3MgQrXEu/m0ocnPsNHO9rm5uq/K/bHq16LOqtDpxOK6r8r9oaNDJiM0MzsmIzQzO7XEseDS68b30ru1qbeiz9bSu7j2wODQzdbQ09DQ6cTiuq/K/aOsvs274c6quMPA4NDNyfqzydDpuq/K/bHto6yyotTauMPA4NDNtcTDv9K7uPbKtcD91tDM7bzT0ru49ta4z/LQ6bqvyv2x7bXE1rjV66Gj1NozMs67tcS7+sb3yc+jrNK7uPbWuNXr1bw0uPbX1r3atcS/1bzko6zS8rTLc2l6ZW9mKEMpysc0oaM8L3A+CjxwPrnY09rQ6bqvyv3T0MaqsqnOxLjQvvXQtLXEutyyu7Tto7qzwvCptcTSu8aqsqnOxLXju/e08r+qwbS90yAKPGJyPgo8L3A+CjxwPqG+PHN0cm9uZz4zPC9zdHJvbmc+ob88c3Ryb25nPtTL0NDPwsPmtcRDJiM0MzsmIzQzO7T6wuujrLXDtb21xL3hufvKx8qyw7Sjvzwvc3Ryb25nPjxicj4KPC9wPgo8cHJlIGNsYXNzPQ=="brush:java;">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++代码,得到的结果是什么?

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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 ;  
}

程序的运行结果和第3题的结果是一样的,出hello world后程序就崩溃了!

解析:答案是Print1调用正常,打印出hello world,但运行至Print2时,程序崩溃。Print1的调用情况和上面的题目一样,不在赘述。由于Print2是虚函数。C++调用虚函数的时候,要根据实例(即this指针指向的实例)中虚函数表指针得到虚函数表,再从虚函数表中找到函数的地址。由于这一步需要访问实例的地址(即this指针),而此时this指针为空指针,因此导致内存访问出错。

【5】.C++中静态成员函数能不能同时也是虚函数?

分析:答案是不能。调用静态成员函数不要实例。但调用虚函数需要从一个实例中指向虚函数表的指针以得到函数的地址,因此调用虚函数需要一个实例。两者相互矛盾。

上面的5个题主要涉及cosnt成员函数,static成员函数,虚函数。

【6】.运行下列C++代码,输出什么?

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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++代码,输出什么?

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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++代码,输出是什么?

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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。请选择正确答案并分析原因。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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++代码,输出是什么?

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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】.运行下图中代码,输出的结果是什么?这段代码有什么问题?

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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++代码,输出是什么?

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 ;
}

运行结果:


解析:由于a是一个指向B实例的引用,因此在运行的时候会调用B::Fun。但缺省参数是在编译期决定的。在编译的时候,编译器只知道a是一个类型A的引用,具体指向什么类型在编译期是不能确定的,因此会按照A::Fun的声明把缺省参数number设为10。

这一题的关键在于理解确定缺省参数的值是在编译的时候,但确定引用、指针的虚函数调用哪个类型的函数是在运行的时候。


【13】.运行如下的C代码,输出是什么?

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 returns: 后面跟的是一串随机的内容,而第二行GetString2 returns: Hello World.两个函数的区别在于GetString1中是一个数组,而GetString2中是一个指针。

当运行到GetString1时,p是一个数组,会开辟一块内存,并拷贝"Hello World"初始化该数组。接着返回数组的首地址并退出该函数。由于p是GetString1内的一个局部变量,当运行到这个函数外面的时候,这个数组的内存会被释放掉。因此在_tmain函数里再去访问这个数组的内容时,结果是随机的。编译程序时,编译器也会给出相关的警告信息。

当运行到GetString2时,p是一个指针,它指向的是字符串常量区的一个常量字符串。该常量字符串是一个全局的,并不会因为退出函数GetString2而被释放掉。因此在_tmain中仍然根据GetString2返回的地址得到字符串"Hello World"。


【14】.运行下图中C代码,输出的结果是什么?

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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++代码,打印出的结果是什么?

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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++代码,打印的结果是什么?

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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是从虚函数表中得到函数地址的。

【程序1】 目: 有1、2、3、4个数字,能组成多少个互不相同且无重复数字的三位数?都是多少? 【程序2】 目:企业发放的奖金根据利润提成。利润(I)低于或等于10万元时,奖金可提10%;利润高    于10万元,低于20万元时,低于10万元的部分按10%提成,高于10万元的部分,可可提    成7.5%;20万到40万之间时,高于20万元的部分,可提成5%;40万到60万之间时高于    40万元的部分,可提成3%;60万到100万之间时,高于60万元的部分,可提成1.5%,高于    100万元时,超过100万元的部分按1%提成,从键盘输入当月利润I,求应发放奖金总数? 【程序3】 目:一个整数,它加上100后是一个完全平方数,再加上168又是一个完全平方数,请问该数是多少? 【程序4】 目:输入某年某月某日,判断这一天是这一年的第几天? 【程序5】 目:输入三个整数x,y,z,请把这三个数由小到大输出。 【程序6】 目:用*号输出字母C的图案。 【程序8】 目:输出9*9口诀。 【程序9】 目:要求输出国际象棋棋盘。 【程序10】 目:古典问:有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第三个月    后每个月又生一对兔子,假如兔子都不死,问每个月的兔子总数为多少? 【程序11】 目:判断101-200之间有多少个素数,并输出所有素数。 【程序12】 目:打印出所有的“水仙花数”,所谓“水仙花数”是指一个三位数,其各位数字立方和等于该数    本身。例如:153是一个“水仙花数”,因为153=1的三次方+5的三次方+3的三次方。 【程序13】 目:将一个正整数分解质因数。例如:输入90,打印出90=2*3*3*5。 【程序14】 目:利用条件运算符的嵌套来完成此:学习成绩>=90分的同学用A表示,60-89分之间的用B表示,    60分以下的用C表示。 【程序15】 目:输入两个正整数m和n,求其最大公约数和最小公倍数。 【程序16】 目:输入一行字符,分别统计出其中英文字母、空格、数字和其它字符的个数。 【程序17】 目:求s=a+aa+aaa+aaaa+aa...a的值,其中a是一个数字。例如2+22+222+2222+22222(此时    共有5个数相加),几个数相加有键盘控制。 【程序18】 目:一个数如果恰好等于它的因子之和,这个数就称为“完数”。例如6=1+2+3.编程    找出1000以内的所有完数。 【程序19】 目:一球从100米高度自由落下,每次落地后反跳回原高度的一半;再落下,求它在    第10次落地时,共经过多少米?第10次反弹多高? 【程序20】 目:请输入星期几的第一个字母来判断一下是星期几,如果第一个字母一样,则继续    判断第二个字母。 【程序21】 目:求100之内的素数    【程序22】 目:对10个数进行排序 【程序23】 目:求一个3*3矩阵对角线元素之和 【程序24】 目:有一个已经排好序的数组。现输入一个数,要求按原来的规律将它插入数组中。 【程序25】 目:将一个数组逆序输出。 【程序26】 目:取一个整数a从右端开始的4~7位。 【程序27】 目:打印出杨辉三角形(要求打印出10行)    【程序28】 目:输入3个数a,b,c,按大小顺序输出。    【程序29】 目:输入数组,最大的与第一个元素交换,最小的与最后一个元素交换,输出数组。 【程序30】 目:有n个人围成一圈,顺序排号。从第一个人开始报数(从1到3报数),凡报到3的人退出    圈子,问最后留下的是原来第几号的那位。 【程序31】 目:编写一个函数,输入n为偶数时,调用函数求1/2+1/4+...+1/n,当输入n为奇数时,调用函数    1/1+1/3+...+1/n(利用指针函数) 【程序32】 目:海滩上有一堆桃子,五只猴子来分。第一只猴子把这堆桃子凭据分为五份,多了一个,这只    猴子把多的一个扔入海中,拿走了一份。第二只猴子把剩下的桃子又平均分成五份,又多了    一个,它同样把多的一个扔入海中,拿走了一份,第三、第四、第五只猴子都是这样做的,    问海滩上原来最少有多少个桃子? 【程序33】 目:求0—7所能组成的奇数个数。 【程序34】 目:一个偶数总能表示为两个素数之和
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值