题目(一): C++中我们可以用 static 修饰一个类的成员函数,也可以用 const 修饰类的成员函数(写在函数的最后表示不能修改成员变量,不是指写在前面表示返回值为常量)。请问:能不能同时用 static 和 const 修饰类的成员函数?
分析: 答案是不可以。 C++ 编译器在实现 const 的成员函数的时候为了确保该函数不能修改类的实例的状态,会在函数中添加一个隐式的参数 const this* 。但当一个成员为 static 的时候,该函数是没有 this 指针的。也就是说此时 static 的用法和 static 是冲突的。
我们也可以这样理解:两者的语意是矛盾的。 static 的作用是表示该函数只作用在类型的静态变量上,与类的实例没有关系;而 const 的作用是确保函数不能修改类的实例的状态,与类型的静态变量没有关系。因此不能同时用它们。
题目(二): 运行下面C++代码,输出是什么?
class A
{
};
class B
{
public :
B() {}
~B() {}
};
class C
{
public :
C() {}
virtual ~C() {}
};
int _tmain(int argc, _TCHAR* argv[])
{
printf("%d, %d, %d/n" , sizeof (A), sizeof (B), sizeof (C));
return 0;
}
分析: 答案是 1, 1, 4 。 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 。
题目(三): 运行下面的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 _tmain(int argc, _TCHAR* argv[])
{
A* pA = NULL;
pA->Print1();
pA->Print2();
return 0;
}
分析: 答案是 Print1 调用正常,打印出 hello world ,但运行至 Print2 时,程序崩溃。调用 Print1 时,并不需要 pA 的地址,因为 Print1 的函数地址是固定的。编译器会给 Print1 传入一个 this 指针,该指针为 NULL ,但在 Print1 中该 this 指针并没有用到。只要程序运行时没有访问不该访问的内存就不会出错,因此运行正常。在运行 print2 时,需要 this 指针才能得到 m_value 的值。由于此时 this 指针为 NULL ,因此程序崩溃了。
题目(四): 运行下面的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 _tmain(int argc, _TCHAR* argv[])
{
A* pA = NULL;
pA->Print1();
pA->Print2();
return 0;
}
分析: 答案是 Print1 调用正常,打印出 hello world ,但运行至 Print2 时,程序崩溃。 Print1 的调用情况和上面的题目一样,不在赘述。由于 Print2 是虚函数。 C++ 调用虚函数的时候,要根据实例(即 this 指针指向的实例)中虚函数表指针得到虚函数表,再从虚函数表中找到函数的地址。由于这一步需要访问实例的地址(即 this 指针),而此时 this 指针为空指针,因此导致内存访问出错。
题目(五): C++中 静态成员函数能不能同时也是虚函数 ?
分析: 答案是不能。调用静态成员函数不要实例。但调用虚函数需要从一个实例中指向虚函数表的指针以得到函数的地址,因此调用虚函数需要一个实例。两者相互矛盾。
题目(六): 运行下列 C++ 代码,输出什么?
struct Point3D
{
int x;
int y;
int z;
};
int _tmain(int argc, _TCHAR* argv[])
{
Point3D* pPoint = NULL;
int offset = (int )(&(pPoint)->z);
printf("%d" , offset);
return 0;
}
答案: 输出 8 。由于在 pPoint->z 的前面加上了取地址符号,运行到此时的时候,会在 pPoint 的指针地址上加 z 在类型 Point3D 中的偏移量 8 。由于 pPoint 的地址是 0 ,因此最终 offset 的值是 8 。
&(pPoint->z) 的语意是求 pPoint 中变量 z 的地址( pPoint 的地址 0 加 z 的偏移量 8 ),并不需要访问 pPoint 指向的内存。只要不访问非法的内存,程序就不会出错。
题目(七): 运行下列 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 _tmain(int argc, _TCHAR* argv[])
{
A* pA = new B();
delete pA;
return 0;
}
答案: 先后打印出两行 :A is constructed. B is constructed. 调用 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 。因此虚函数在构造函数中,已经失去了虚函数的动态绑定特性。
题目( 12 ) :运行下图中的 C++ 代码,输出是什么 ?
#include <iostream>
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 _tmain(int argc, _TCHAR* argv[])
{
A a;
a.Print();
return 0;
}
答案 :输出 n1 是一个随机的数字, n2 为 0 。在 C++ 中,成员变量的初始化顺序与变量在类型中的申明顺序相同,而与它们在构造函数的初始化列表中的顺序无关。因此在这道题中,会首先初始化 n1 ,而初始 n1 的参数 n2 还没有初始化,是一个随机值,因此 n1 就是一个随机值。初始化 n2 时,根据参数 0 对其初始化,故 n2=0 。
题目( 13) : 编译运行下图中的 C++ 代码,结果是什么?( A )编译错误;( B )编译成功,运行时程序崩溃;( C )编译运行正常,输出 10 。请选择正确答案并分析原因 。
#include <iostream>
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 _tmain(int argc, _TCHAR* argv[])
{
A a = 10;
A b = a;
b.Print();
return 0;
}
答案 :编译错误。在复制构造函数中传入的参数是 A 的一 个实例。由于是传值,把形参拷贝到实参会调用复制构造函数。因此如果允许复制构造函数传值,那么会形成永无休止的递归并造成栈溢出。因此 C++ 的标准不允许复制构造函数传值参数,而必须是传引用或者常量引用。在 Visual Studio 和 GCC 中,都将编译出 错。
题目( 14) : 运行下图中的 C++ 代码,输出是什么 ?
int SizeOf(char pString[])
{
return sizeof (pString);
}
int _tmain(int argc, _TCHAR* 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" , size1, size2, size3, size4);
return 0;
}
答案 : 4, 1, 100, 4 。 pString1 是一个指针。在 32 位机器上,任意指针都占 4 个字节的空间。 *pString1 是字符串 pString1 的第一个字符。一个字符占一个字节。 pString2 是一个数组, sizeof(pString2) 是求数组的大小。这个数组包含 100 个字符,因此大小是 100 个字节。而在函数 SizeOf 中,虽然传入的参数是一个字符数组, 当数组作为函数的参数进行传递时,数组就自动退化为同类型的指针 。
题目( 15 ) :运行下图中代码,输出的结果是什么?这段代码有什么问题 ?
#include <iostream>
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 _tmain(int argc, _TCHAR* argv[])
{
A* pA = new B();
delete pA;
return 0;
}
答案 :输出三行,分别是: A is created. B is created. A is deleted 。用 new 创建 B 时,回调用 B 的构造函数。在调用 B的构造函数的时候,会先调用 A的构造函数。因此先输出 A is created. B is created.
接下来运行 delete 语句时,会调用析构函数。由于 pA 被声明成类型 A 的指针,同时基类 A 的析构函数没有标上 virtual ,因此只有 A 的析构函数被调用到,而不会调用 B 的析构函数。
由于 pA 实际上是指向一个 B 的实例的指针,但在析构的时候只调用了基类 A 的析构函数,却没有调用 B 的析构函数。这就是一个问题。如果在类型 B 中创建了一些资源,比如文件句柄、内存等,在这种情况下都得不到释放,从而导致资源泄漏。
问题( 16 ) :运行如下的 C++ 代码,输出是什么?
class A
{
public :
virtual void Fun(int number = 10)
{
std::cout << "A::Fun with number " << number;
}
};
class B: public A
{
public :
virtual void Fun(int number = 20)
{
std::cout << "B::Fun with number " << number;
}
};
int main()
{
B b;
A &a = b;
a.Fun();
}
答案 : 输出 B::Fun with number 10 。由于 a 是一个指向 B 实例的引用,因此在运行的时候会调用 B::Fun 。但缺省参数是在编译期决定的。在编译的时候,编译器只知道 a 是一个类型 a 的引用,具体指向什么类型在编译期是不能确定的,因此会按照 A::Fun 的声明把缺省参数 number 设为 10 。
这一题的关键在于理解确定缺省参数的值是在编译的时候,但确定引用、指针的虚函数调用哪个类型的函数是在运行的时候。
问题( 17 ) :运行如下的 C 代码,输出是什么?
char * GetString1()
{
char p[] = "Hello World" ;
return p;
}
char * GetString2()
{
char *p = "Hello World" ;
return p;
}
int _tmain(int argc, _TCHAR* 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" 。
问题( 19) : 运行下图中 C 代码,输出的结果是什么 ?
int _tmain(int argc, _TCHAR* 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 and str2 are not same ,第二行是 str3 and str4 are same 。
str1 和 str2 是两个字符串数组。我们会为它们分配两个长度为 12 个字节的空间,并把 "hello world" 的内容分别拷贝到数组中去。这是两个初始地址不同的数组,因此比较 str1 和 str2 的值,会不相同 。 str3 和 str4 是两个指针,我们无需为它们分配内存以存储字符串的内容,而只需要把它们指向 "hello world“ 在内存中的地址就可以了。由于 "hello world” 是常量字符串,它在内存中只有一个拷贝,因此 str3 和 str4 指向的是同一个地址。因此比较 str3 和 str4 的值,会是相同的 。