经典C++笔试题解析
1、private 与 protect 关键字的区别。
派生类能访问基类的protect成员而不能访问private成员。Private对派生类及用户都不可见,public对派生类和用户都可见,private介于二者之间,对派生类可见对用户不可见。
2、友元,friend 如何声明一个友元。
3、缺省构造函数。当类声明了一个带参数的构造函数后而没有声明无参数构造函数,编译器还会为它生成一个默认的缺省构造函数吗?分析下例的错误:
class A{
public:
A(int i){
cout<<i<<endl;
}
};
void main(){
A a;
}
对于构造函数,为了使编译器自动生成无参构造和拷贝构造函数,你不能定义任何类型的构造函数。
4、构造函数有没有返回值?析构函数有没有返回值?析构函数可不可以带参数?
5、解释一下重载
函数重载允许两个或更多个函数使用同一个名字限制条件是它们的参数表必须不同,即参数类型不同或参数的数目不同。返回值不能作为重载的标志,因为忽略返回值后将无法解析。两个函数只有在相同的作用范围内才能构成重载。
6、重载函数如何来区分彼此?
根据参数个数和类型,C++编译后将生成不同的函数名称,这是C++支持多态的一个特性。
7、解释缺省参数函数
缺省参数允许以不同的形式调用相同的函数,调用时可以省略后续参数,而采用函数声明时的默认值。
8、下例能编译通过吗?为什么。
void s(int i){
cout<<i<<endl;
}
void s(int i, int j=0){
cout<<i<<endl;
}
void main(){
s(5);
}
编译不通过。默认参数和重载函数有时候会产生冲突。重载的缺省参数函数不能与其它函数产生二义。
9、能否用返回值来区分重载函数?
不能,因为大多数时候会忽略返回值,这个时候编译器将无法解析函数调用
10、看下例回答以下问题:
a. 常量必须初始化,下面的代码有何问题:
const int i;
i=20;
有,const常量必须在定义的时候同时初始化,否则就违背了其const的性质
b. 常量有没有存储空间,或者只是编译时的符号而已?
不一定。
在C中,define常量是预处理阶段的工作,其不占据内存。但是const常量总是占据内存
在C++中,const常量是否占据存储空间取决于是否有引用该常量地址的代码。C++对于const默认为内部链接,因此const常量定义通常都放在头文件中,即使分配内存也不会出现链接错误。
若不引用常量对应的地址,则不会为其分配空间。
Const是用来替换define的,因此其必须能够放在头文件中,在C++中const变量是默认为内部链接的,即在其他文件中看不到本文件定义的const变量,因此链接不会出现问题。Const变量在定义的时候必须初始化,除非显式的指定其为extern的。通常C++中会尽量避免为const变量分配内存storage的,而是在编译阶段将其保存在符号表symbol table中。当用extern修饰const变量或引用其地址时,将强制为其分配内存,因为extern表示采用外部链接,因此其必须有某个地址保存其值。
c. 常量一定是在编译时就确定其值的吗?
const int i=100;
const int j=i+100;
long address=(long)&j; //强迫编译器为常量分配存储空间
char buf[j+10];
void main()
{
const char c=cin.get();
const char d=3;
char test1[d+10];
char test2[c+10];
const char c2=c-'a'+'A';
cout<<c<<" "<<c2<<endl;
}
常量不一定是在编译时刻就初始化的。其只是表明只能初始化一次。const char c必须到运行时刻动态获取其初值。char test2[c+10]编译无法通过,因为无法确定c就无法确定数组长度。char buf[j+10];和char test1[d+10];都可以编译通过
单步调试时可以看到j的值,证明为其分配了内存,而无法找到i的符号,因为没有为其分配存储空间。可是为什么可以看到d呢?
11、看下例回答以下问题:
a. 能否将一个非常量对象的地址赋给一个常量指针?
b. 能否将一个常量对象的地址赋给一个非常量指针?若确要如此,该如何做?
可以将非常量对象地址赋值给常量指针,但是不能将常量对象地址给非常量指针,因为其无法保证不修改常量对象。
若却要如此,则只能强制转换,能编译通过,但却可能存在隐患。因此编译器的工作就是将一切可能的隐患扼杀在摇篮中。
void main()
{
const int i=5;
int *j=const_cast(&i);
}
const int* x; //常量指针
int* const x=&d; //指针常量
int const* x; //常量指针
const int* const x=&d; //常量指针常量
12、函数调用中值传递使用常量定义无意义:
void f(const int i);
但函数返回值的常量定义有特殊作用,看下例:
class X
{
int i;
public:
void modify(){cout<<"haha"<<endl;}
};
const class X fun(){return X();}
void main()
{
//fun().modify(); //常量不能为左值,所以下例也不成立:
//error C2662: 'modify' : cannot convert 'this' pointer from 'const class X' to 'class X &'
const X g = X();
g.modify();
//!!! 常量一定不能为左值吗? 看16题
}
13、常量的典型应用:
void u(const int* p);
const char* v();
14、分析下例说出错误的原因:
class X{};
X f(){return X();}
void g1(X&){}
void g2(const X&){}
void main(){
//! g1(f()); 临时变量都是常量
g2(f());
}
零时变量对用户不可见,因此其为常量,而g1(X&)参数为引用,const实参不能给非const类型新参。若g1采用值传递则没有任何问题
15、如何使用类常量成员,如何初始化?
class Y{
public:
const size;
Y();
};
Y::Y():size(100){}
class X{
enum{i=100};
};
16、将一个成员函数声明为常量函数有什么作用?
a. 编译器保证该函数不得修改成员变量的值
b. 允许一个常量对象调用此函数
class X
{
public:
void f() const
{
cout<<"haha..."<<endl;
}
};
void main()
{
const X cs;
cs.f();
}
17、volatile关键字有什么作用?
volatile 告诉编译器不要对其做任何自作多情的假设与优化。比如建立一个 const volatile 的对象,其为一个对于程序员只读的易变变量,告诉编译器程序员不得改变此对象,但不是说该对象不会发生改变,来自外界的工具可能会改变它,比如硬件的端口通信。
18、内联函数与普通函数、宏的差别?
使用内联函数的地方,编译器会对其代码进行展开,而不是插入一个跳转。这样提高了函数调用的效率,但同时增大了编译后的目标代码量。内联函数不像宏,它是一个真正的函数,有参数类型检查。内联函数属于编译阶段的工作,而宏属于预处理阶段的工作。注意:在“类中”定义的成员函数都是内联函数。
19、哪两种情况会使编译器放弃内联?
当函数的功能过于复杂时,编译器不会实施内联,这取决于编译器,但通常情况下,循环或者过多的代码不会被内联,因为此时代码执行的时间可能比函数调用的时间多很多,内联失去了意义。
另外一种情况是需要显式或隐式的得到某函数的地址时,编译器要产生地址则必须为其分配内存空间;而进行内联替换时只是将其保存在符号表中,并不为其分配空间。
总之,inline关键词只是对编译器的一种建议,并非强制,是否内联取决于编译器的分析。
20、预处理器的特殊功能:
a. #define DEBUG(X) cout<<#X"="<<X<<endl
b. #define TRACE(X) cout<<#X<<endl, X
c. #define FILE(X) char* X##_string
#为字符串连接符,X经过扩展后将变为“X”,其和"="一起将自动组合为"X="
##为字符连接符,将和相连的字符自动合并构成一个新的字符,即X##_string扩展为X_string
TRACE(X)跟踪函数执行过程,“cout<<#X<<endl, X”为逗号表达式不好出现宏扩展后的异常问题,两句肯定一起执行,若“cout<<#X<<endl;X”则可能出现问题
DEBUG(X)跟踪打印变量的值
#define DEBUG(X) cout<<#X"="<<X<<endl
#define TRACE(X) cout<<#X<<endl, X
#define FILE(X) char* X##_string
int f(){
return 9;
}
void main()
{
char i[]="haha";
DEBUG(i);
for(int j=0;j<5;j++)
TRACE(f());
FILE(one)=i;
cout<<one_string<<endl;
}
21、有没有代码能在主函数的第一行代码之前或最后一行代码之后执行?
全局对象的构造函数将在main函数之前运行。析构在main函数之后运行。
函数内部的static对象的构造函数将在第一次调用该函数时调用,main函数结束之后执行。但其析构会在全局对象之前,因为所有对象的析构顺序和构造顺序相反,即全局对象在函数内部的static对象前构造,在后面析构。若包含静态局部对象的函数未被调用,则也不进行析构。
可以通过atexit函数指定当离开main或者调用exit时所要执行的动作。其在所有的析构函数之前运行。
class Obj
{
char cT;
public:
Obj(char c)
{
cT=c;
cout<<"Obj::Obj() for "<<c<<endl;
}
~Obj()
{
cout<<"Obj::~Obj() for "<<cT<<endl;
}
};
Obj A('A'); //全局对象在main之前构造
void f()
{
static Obj B('B');
}
void main()
{
cout<<"inside main()"<<endl;
f();
cout<<"leaving main()"<<endl;
}
void main(){
cout<<"inside main()"<<endl;
f();
cout<<"leaving main()"<<endl;
}
OUTPUT:
Obj::Obj() for A
inside main()
Obj::Obj() for B
leaving main()
Obj::~Obj() for B //在A前析构
Obj::~Obj() for A
22、static 关键字的作用:
无论是C还是C++,static都具备两个基本意义,如下:
在固定的位置只分配一次内存,对象创建在静态数据区中而非堆栈中,这就是静态内存的概念。
对特定单元可见,static控制着名字的可见性,其同时描述了链接的概念,指明了链接器可以见到的名字。
Staic无论表示内存的物理位置还是文件中的可见性,其基本意义为保持其原有位置。
以下都是全局定义的变量
extern int a=0; ===> int a=0; (静态)
extern void f(); ===> void f();
static int a=0; ===> 本文件内可见
static void f(); ===> 本文件内可见
23、名字空间的使用:
namespace simon{
class A{
public:
void print(){
cout<<"this is simon::A"<<endl;
}
};
char str[]="this is simon::str";
}
void main(){
simon::A v;
v.print();
using namespace simon;
cout<<str<<endl;
}
24、静态成员函数与普通成员函数有什么区别?
静态成员函数只能访问静态成员变量与静态成员函数。为什么呢?想一想那个this指针,静态成员函数是没有this指针的。
25、在C++中用到一个C库中的函数,为了防止编译器按照C++的方式对符号进行访问以致无法定位函数实体该如何做?
extern"C" float f(int a, float b);
c++中 f() ===> _f_int_float
c中 f() ===> _f
C++中为了支持函数重载和类型安全检查,会对编译后的函数名字进行装饰,如生成_f_int_char之类的。因此在C++中无法利用C库函数,需要使用链接交换指导符extern,告诉C++不要进行装饰。
26、如何改变一个指针常量的指向?
void main()
{
int i(1);
int j(2);
int * const p=&i;
int * pp=(int*)&p;
*pp=(int)&j;
cout<<*p<<endl;
}
强制转换,通过p的地主改变其值,从而改变其指向
27、解释下面例子错误的原因
const int iii=20;
int & q=iii; //错误,非常量引用不能引用常量
int & qq=20; //错误,非常量引用不能引用常量
const int & qqq=iii; //正确
qqq=23; //错误,常量不能作为左值
引用必须跟一块合法的存储空间联系起来,其和指针区别如下:
a.引用必须被初始化。而指针可以不
b. 引用一旦指向一个对象,就不能再指向其它。非const指针可以随意指向任意该类对象
c. 不能引用NULL ,不能引用常量(除非是常量引用)
28、当用非const引用作函数参数时,其参数必须是非常量,即不能是直接值或返回的临时变量
void f(int &){}
void g(const int &){}
void main(){
//f(1);
g(1);
}
29、什么情况下需要一个拷贝构造函数?
需要用类作值传递时value pass
class X;
void f(X x2);
X x1;
f(x1);
X x2=x1;
30、拷贝构造函数的形式: X::X(X&){}
31、尽量避免类的值传递,用什么方法可以阻止用户的类值传递?
只要声明一个私有的拷贝构造函数即可:
class X{
X(X& x) {
cout<<"hehe"<<endl;
}
public:
X(){}
};
void main(){
X xx;
//! X x2=xx;
}
31、尽量避免类的值传递,用什么方法可以阻止用户的类值传递?
只要声明一个私有的拷贝构造函数即可,其不能被编译器调用,因此无法编译通过
class X{
X(X& x) {
cout<<"hehe"<<endl;
}
public:
X(){}
};
void main(){
X xx;
//! X x2=xx;
}
32、定义一个函数指针,指向一个类的非静态成员函数,并调用:
class X;
typedef void (X::*FPT) (); // 定义一个指向X中任意一个无参返回值为void类型的成员函数的指针
class X{
public:
int xi;
FPT pp;
X(int j){
xi=j;
pp=X::f;
(this->*pp)();
}
void f(){
cout<<"f(), xi = "<<xi<<endl;
}
void g(){
cout<<"g(), xi = "<<xi<<endl;
}
};
int main(){
X x1(1),x2(2);
FPT p1,p2,p3;
p1=X::f;
p2=x1.f;
p3=X::g;
(x1.*p1)();
(x1.*p2)();
(x1.*p3)();
(x2.*p1)();
(x2.*p2)();
(x2.*p3)();
return 1;
}
f(), xi = 1
f(), xi = 2
f(), xi = 1
f(), xi = 1
g(), xi = 1
f(), xi = 2
f(), xi = 2
g(), xi = 2
指针必须指向一个实际的地址,在对象的成员尚未创建之前,对象的成员函数并没有地址,p1=X::f只是确定了p1指向了X的f成员函数,也就是确定了一个偏移地址。当X创建了对象后,p1的地址就确定了,p1必须和对象结合使用才有实际意义。P3的道理相同。
P2 呢,按道理p2 的地址就是x1.f阿,可是为什么(x2.*p2)()结果是2呢?本质上指向成员函数的指针只是确定了该指针在类中的偏移量而已,具体指向的地址由调用时关联的具体对象确定。
注意,非静态的成员始终要跟this指针结合使用,因此要用一个对象来使用它而不是一个类型。即使是在类定义内部也要显式用this来强调这种关系,以便和全局函数区分开来
33、下例发生了几次拷贝构造函数调用?一次
class X{
public:
X(){}
X(X&){
cout<<"haha..."<<endl;
}
};
void main(){
X x1;
X x2=x1; //定义的时候同时拷贝一个已经存在的对象才会调用拷贝构造函数,构造且拷贝
x2=x1; //此为赋值操作,而非构造,因为x2对象已经存在
}
34、避免使用运算符重载!!!
35、类成员运算符重载与全局运算符重载的区别? 参数的个数
36、在继承链上,子类的构造函数需要显式的调用基类的构造函数,而析构函数呢?
因为构造函数可能有多个,需要明确指定,否则会调用默认的无参构造函数,当未定义无参构造函数而定义了其他构造函数时,编译器不会为其生成默认的构造函数,此时将无法编译通过。每个类只有一个析构函数,因此不需要指定,它们会被自动调用。
调用构造函数的语法:
foo::foo(int i):base_foo(i){...}
37、什么是构造函数初始化列表?
构造函数的调用顺序是:先基类的构造函数,然后是成员对象的构造函数,构造顺序不受初始化列表中的排列顺序影响。同等级成员的初始化列表由其在类声明中的顺序决定。因为析构顺序与构造顺序相反,而析构顺序是自动且唯一的,因此构造顺序唯一,但初始化列表的顺序是可变的,这就证明了构造顺序取决于类声明中的固定顺序而与列表顺序无关。
38、举例说明为什么有时候必须要初始化列表?
class Z{
public:
Z(int i) {
cout<<"huhu..."<<endl;
}
};
class X{
public:
X(int i) {
cout<<"hehe..."<<endl;
}
};
class Y :public X{
public:
Z z1;
Y(int i):X(i),z1(6) { //初始化列表,“,”分开各个初始化项。Compostion模式应采用成员对象而非类名称
cout<<"haha..."<<endl;
}
};
void main(){
Y y(5);
}
试试将上面例子的初始化列表移到函数体内部,会发生什么?
C++必须保证每个一对象都初始化了,而对于基类私有成员,派生类无法操作,而对象在定义的时候必须初始化,这样只能在派生类的构造列表中显示的调用基类的构造函数了。
39、基类中对函数foo进行了多次重载,子类中重定义函数foo,能否在子类中调用foo的基类版本?
不能,子类重定义将屏蔽所有基类版本。
class X{
public:
f(int i,int j) {cout<<"haha"<<endl;}
f(int i){cout<<"haha"<<endl;}
};
class Y:public X{
public:
f(int i){cout<<"hehe"<<endl;}
};
void main(){
Y y;
//! y.f(3,4);
}
40、谈谈public, private, protect关键字的区别
41、谈谈私有继承的性质
去掉public或者显式指定为private的都可以将基类作为私有的。此时新类具备所有基类的成员和功能,但其是隐藏的。新的对象不能作为基类的一个实例。通常为了避免混淆,避免用私有继承。
私有继承会使得基类中的所有public成员变为private的,若希望部分可见,可在派生类的public域进行声明,无须任何参数或返回值。因此当你想隐藏基类的部分功能时,私有继承就派上用场了。
42、如何在子类中开放私有基类的接口?
class X{
public:
void f(){cout<<"haha"<<endl;}
void ff(){cout<<"hehe"<<endl;}
};
class Y: X{ //私有继承
public:
X::f; //将f重新申明为public的
};
void main(){
Y y;
y.f();
//! y.ff();
}
43、判断下列例子的输出:
class X{
public:
void foo() {cout<<"X::foo"<<endl;}
};
class Y: public X{
public:
void foo() {cout<<"Y::foo"<<endl;} //覆盖基类foo
};
void bar(X* p){
p->foo();
}
void barbar(X& x){
x.foo();
}
void main(){
Y y;
X* p;
Y* q;
X& m=y;
p=&y;
q=&y;
p->foo();
bar(p);
barbar(m);
q->foo();
bar(q);
}
X::foo
X::foo
X::foo
Y::foo
X::foo
Y中的foo覆盖了X中的的foo,因此q->foo()输出Y::foo,但其他都输出X,是因为编译器不知道调用哪一个foo,所以就固定为X的了,这样不会受到派生类的影响
要想通过p调用Y::foo()该怎么做?利用virtual 实现多态,告诉其该调用哪一个版本
44、晚捆绑late binding。解释编译器是如何来做到晚捆绑的?
a. 为每个包含虚函数的类创建一个表(VTABLE)
b. 在该表中放置所有虚函数的地址
c. 为每个有虚函数的类的对象的初始位置放置一个指针:VPTR,指向相应的VTABLE
45、class no_virtual{
int a;
void f(){};
};
class one_virtual{
int a;
virtual void f(){};
};
class only_virtual{
virtual void f(){};
};
class two_virtual{
virtual void f(){};
virtual void g(){};
};
class null{};
void main(){
cout<< sizeof(no_virtual)
<< sizeof(one_virtual)
<< sizeof(only_virtual)
<< sizeof(two_virtual)
<< sizeof(null)
<<endl;
}
OUTPUT:48441说明为什么是这个结果?
谈谈在one_virtual 中VPTR与 int a 的位置关系是怎么样的?
4,为成员a所占据的内存
8,成员a及VPTR占据内存
4,VPTR占据内存
4,尽管有两个虚函数,但只有一个VPTR,实际指向的VTABLE大些,但类本身并没有虚函数的个数增长
1,尽管类null没有任何成员,但对于该类的每个对象都需要有一个唯一的地址,不占据内存就没有地址,因此必须至少分配一个字节内存
若唯一的VPTR在a之后,那么再加一个成员变量b后,按照前面的类比关系,VPTR可以在b之后,n个成员变量呢??VPTR的位置会随着成员变量的增减而变化,这种变化编译器在upcasting时如何知道呢?无法知道,因此必须让VPTR处于含有虚函数的类的第一个内存位置
46、每个有虚函数的类都有一个自己的VTABLE,画图解释继承链中的VTABLE关系
各种类型的对象具备和instrument相同的接口,因此可以响应相同的信息。因此其地址都可以放在instrument类型的指针数组中,但是编译器只知道其指向了一个instrument对象。
当创建含有虚函数的类或者从含有虚函数的类派生而来,编译器将为每个类创建唯一个VTABLE。在该表中包含了本类的或者从基类继承而来的虚函数的地址,当派生类中没有overriding基类中的虚函数时,将采用前一个基类的虚函数地址。当采用简单的继承机制时,每个对象只有一个VPTR,其由构造函数初始化指向该类的VTABLE。
当通过基类地址调用虚函数时,因为编译器并没有完整信息实现early binding,故其将插入代码获取虚函数地址。所有类在相同的位置即通常为第一个位置含有VPTR指针,因此编译器可以通过VPTR获得VTABLE,其中所有虚函数地址按照相同顺序排列。
47、虚函数调用的实质
Instrument * pi;
Brass B;
i = &B;
i->adjust(1);
写出最后一行的汇编代码:
28: Instrument* pi;
29: Brass B;
004010A8 8D 4D F8 lea ecx,[ebp-8]
004010AB E8 73 FF FF FF call @ILT+30(Brass::Brass) (00401023)
30: pi=&B;
004010B0 8D 45 F8 lea eax,[ebp-8]
004010B3 89 45 FC mov dword ptr [ebp-4],eax
31: pi->adjust(1);
004010B6 8B F4 mov esi,esp
004010B8 6A 01 push 1 ;参数入栈
004010BA 8B 4D FC mov ecx,dword ptr [ebp-4] ;this指针
004010BD 8B 11 mov edx,dword ptr [ecx] ;VPTR指针
004010BF 8B 4D FC mov ecx,dword ptr [ebp-4] ;作参数
004010C2 FF 52 08 call dword ptr [edx+8] ;取虚函数
非静态成员函数的调用都涉及到将this指针传递给函数
48、什么是抽象基类,纯虚函数?
抽象基类可不可以有实例? 不能
纯虚函数可不可以有实现? 能
抽象基类中至少含有一个纯虚函数,其提供派生类的一些通用接口。
=0这告诉编译器在VTABLE中为该虚函数留一个位置,而不填充虚函数的地址。只要有一个成员函数被声明为纯虚函数,VTABLE表就是不完整的。当创建该类的对象时,编译器将给出错误信息告警。
也可以在基类中提供纯虚函数的实现,但是在派生类中仍然必须实现该纯虚函数,否则派生类变为纯虚函数。唯一的好处是在派生类中使用基类部分提供的该虚函数的公共代码
唯一的例外情况是,纯虚析构函数无需在派生类中重新实现
class X{
public:
virtual void f()=0 {
cout<<"haha"<<endl;
}
};
另外内联函数不能是虚函数,因为虚函数表需要显示的函数地址
49、在派生类中增加新的虚函数,能不能用基类的指针调用之,为什么?请从VTPR与VTABLE的角度来解释。
如果此时确切的知道基类指针指向的是派生类对象,需要调用该函数,该如何做?
不能调用基类中没有的虚函数。因为基类指针可以指向任意的派生类对象,有的含有该接口有些不含,为了确保正确,只能调用基类中已有的虚函数接口
通过该基类指针调用新增的虚函数,可以通过强制转换,但是其违背了虚函数的机制,因此只能采用RTTI动态类型识别机制了
class X{
public:
virtual void f(){cout<<"haha"<<endl;}
};
class Y :public X{
public:
virtual void f(){cout<<"hehe"<<endl;}
virtual void ff(){cout<<"hehehehe"<<endl;}
};
void main(){
X* p;
Y y;
p=&y;
Y* py=NULL;
py=static_cast(p);
if(py) py->ff();
}
50、看下面的例子,谈谈为什么要避免值传递:
class X{
int xi;
public:
X(int i){xi=i;}
virtual int f() {return xi; }
};
class Y :public X{
int yj;
public:
Y(int i, int j):X(i),yj(j){}
virtual int f() {return X::f()+yj;}
};
void call(X B){cout<<B.f()<<endl;}
void main(){
Y A(5,8);
call(A);
}
OUTPUT:5
类对象在进行值传递时发生了“对象切片”,无论是默认的拷贝构造函数还是手工提供的,编译器都会在其中放一段代码对VPTR进行初始化。这里call的参数类型声明为基类,则调用了基类的拷贝构造函数,将对象的VTPR指向了基类的VTABLE(原本应该指向派生类的VTABLE的)。于是错误发生了。
回忆一下,什么方法可以迫使程序员放弃值传递? 私有拷贝构造函数
或者在基类中定义纯虚函数,则编译器将阻止进行值传递时,因为纯虚基类不能定义对象
51、无论是手工提供的构造函数还是默认的构造函数,无论是拷贝构造函数还是一般构造函数,编译器都会在其中加上一段代码用以初始化VPTR(当然是有虚函数的类的构造函数)
52、编译器插入到构造函数中的秘密代码一般有哪几部分?
a. 初始化VPTR(对于有虚函数的类来说)
b. 检查this指针(因为对象有可能没有构造成功,operator new)
c. 调用基类的构造函数(如果在派生类的初始化参数列表中没有显式的调用基类的构造函数,则编译器会自动调用基类的无参数构造函数(默认的或者手工的)即使派生类构造函数在函数体内部调用了基类构造函数也不例外,如果这时基类没有无参数构造函数(比如它只手工提供了带参数的构造函数)则编译出错)
class X{
public:
X() {cout<<"X::X()"<<endl;}
X(int i){cout<<"X::X(int)"<<endl;}
};
class Y :public X{
public:
Y(int i){X(5);}
};
void main(){
Y y(6);
}
OUTPUT:X::X()
X::X(int)
53、说明下例为什么无法编译通过?
class X{
public:
X(int i) {cout<<"X::X(int)"<<endl;}
};
class Y :public X{
public:
};
void main(){
Y y;
}
如何才能通过?
为Y自动生成无参构造函数,但X没有无参构造函数
54、上例中如果派生类改为:
class Y :public X{
public:
Y(){}
};
为何不能编译通过?如何修改?
规则:应该始终在初始化列表中调用基类构造函数。尽量避免调用默认的构造函数
55、如果在构造函数中调用了虚函数,只有虚函数的本地版本被调用。
class X{
public:
virtual void f(){cout<<"X::f"<<endl;}
X() {f();}
};
class Y : public X{
public:
virtual void f(){cout<<"Y::f"<<endl;}
Y() {f();}
};
class Z : public Y{
public:
virtual void f(){cout<<"Z::f"<<endl;}
Z() {f();}
};
void main(){
Z z;
}
OUTPUT:
X::f
Y::f
Z::f
Z的构造函数被调用时,编译器会先调用X的构造函数,然后是Y的,因为构造函数在真正运行之前会自动插入段代码,其将对象的VPTR指向自己类型的VTABLE,因此当构造到达X的构造函数体内时,VPTR是指向X的VTABLE的,因此调用的函数就是X版本的函数。
56、构造函数能不能是虚函数?
不能。
但试想用一个基类指针去析构一个派生类对象时会发生什么事情?如何解决?
若通过基类指针操作对象如delete,因为不知道基类指针指向的具体类型,因此安全万能的做法只能调用基类的析构函数。
令析构函数为虚函数,则通过基类指针析构时会自动调用派生类的析构函数。
析构函数不需要像构造函数那样显式的调用基类的析构函数,编译器会自动调用基类的构造函数。
57、若在析构函数中调用了虚函数,应该调用虚函数的哪个版本?
析构函数知道其自身由谁派生而来,但不知道谁会继承自己,因此只会调用自身和基类的析构函数。
其不会去处理一个也许几经被“析构”的派生类数据。因此晚捆绑机制在析构函数中被忽略。
59、多重继承应避免。当多重集成中出现菱形继承时,为了消除继承人中包含两个base类的实体,该如何?
虚拟继承
class base{};
class d1: virtual base{};
class d2: virtual base{};
class mi: public d1, public d2{};
60. RTTI。一般我们将一个指针向上映射为一个基类指针,然后通过虚函数使用基类的接口。但是偶尔我们需要知道一个基类指针指向对象的确切类型,于是有了RTTI。
注意,RTTI依赖与驻留在虚函数表中的类型信息,所以在一个没有虚函数的类上使用RTTI是会出错的。
1、预处理器
#include"..."和#include<...>的区别
"..."一般是用户提供的头文件,从当前目录开始查找
<...>一般是系统提供的头文件,从环境变量值开始查找
还有常见的预定义名字:
__cplusplus 编译c++
__STDC__ 编译标准C
__FILE__ 当前编译文件
__LINE__ 当前行
__TIME__
__DATA__
文件名和行号在打印调试时非常方便,通常将打印函数定义两个版本,调试时带上文件名和行号便于快速定位
2、 class X{
public:
X(int a=3){
printf("this is X(int a=3)");
}; //是不是缺省构造函数?(是)
X(){
printf("this is X()");
};
};
void main()
{
X a[3];
}
//warning C4520: 'X' : multiple default constructors specified
//error C2668: 'X::X' : ambiguous call to overloaded function
分清什么是缺省构造函数,什么是编译器默认构造函数
缺省构造函数就是没有指定任何参数时调用的构造函数。而编译器默认的构造函数本身就是无参的,而不是具有默认参数的。默认构造函数(指没有参数的构造函数)在C++语言中是一种让你无中生有的方法。构造函数能初始化对象,而缺省构造函数则可以不利用任何在建立对象时的外部数据就能初始化对象。
从上面编译的提示信息可以看出,X a[3]未提供任何参数,从X(int a=3)的角度理解可以是默认参数,从X()的角度可以理解为无参数,因此调用时出现了无法确定的情况。默认参数函数经常和重载函数发生混淆,因此应避免。
multiple default constructors specified可以看出X(int a=3)是缺省构造函数。
若调用X a(4),则将明确调用X(int a=3)
class X{
public:
X(int a=3){
printf("this is X(int a=3)");
}; //是不是缺省构造函数?(是)
};
void main()
{
X a[3];
X a(4);
}
因为定义了X(int a=3),编译器不会自动生成无参构造函数,因此X a[3]和X a(4)都可以通过。因此有参构造函数提供默认参数是一种非常好的方式,可以适应很多情况。
3、所谓构造函数名符其实吗?是用来构造对象的吗?
不是,构造函数只是初始化对象数据。只所以需要构造函数是因为C中经常出现对象忘记了初始化导致的潜在问题,C++为了避免这种问题,将其扼杀在摇篮中,故提出了构造函数的概念,来保证每个对象在建立后能正确的初始化。
4、虚函数能不能内联? 不能,为什么?
在建立虚函数表时,需要指定虚函数的地址,而内联函数只是在编译时提供的一些符号,进行一些检查后替换了相关的代码,编译后就不存在内联函数了,因此也不存在一个地址。故虚函数不能是内联函数。
5、explicit 关键字有何作用?
explicit只能用作单参数构造函数,指明该构造函数不能用作隐式转换。
class X{
public:
int ix;
explicit X(int i){ix=i;}
};
void f(X){};
void g(int I){
//! f(I);
}
6、看这个宏: #define max(a,b) ((a)>(b)?(a):(b))
举一个例子使这个宏崩溃
max(++a,b)
该怎么做?
使用内联 inline int max(int a, int b){return a>b?a:b;}
为使它能像宏一样接受多类型,模板: template
inline const T& max(const T& a, const T& b){return a>b?a:b;}
7、为什么有必要重载new和delete?
为了改善缺省的new,delete的性能,缺省的new在分配对象时还分配了额外的空间用于管理内存,因此在分配大量而又小体积的的对象时这种缺省的内存管理存在效率问题,需要重载。
New和delete采用的内存分配机制只适用于一般情况,对于某些特定情况可能满足不了要求,通常有以下几种情况:
效率问题,创建大量某些对象造成系统速度瓶颈;
堆的分段问题,反复创建释放造成堆大量分段,没有大块内存满足系统需求;
实时嵌入式系统中,要求内存分配时间一定,不允许内存耗尽和分段问题。
对于上述情况,需要定制的new和delete等操作符。
8、一般为了安全将类声明中声明一个私有的拷贝构造函数和一个赋值运算符,但若非要实现拷贝构造函数也一定要实现赋值运算符。
因为将一个现有对象赋值给一个初建的对象时,是调用拷贝构造函数,而后续进行赋值时将调用赋值运算符
9、构造函数的初始化列表比在函数体内部赋值的好处?
a.效率,想想,基类构造函数可能会被调用两次
b.const的初始化必须在此处
c.基类无缺省构造函数
10、强制类型转换会引发拷贝构造函数的发生
class X {
public:
X(){};
X(X& a){cout<<"haha..."<<endl;};
~X(){cout<<"~X()..."<<endl;};
};
class Y:public X{
public:
~Y(){cout<<"~Y()..."<<endl;};
};
void main(){
Y y;
(X)y;
}
由上述输出可以看出,(X)y实际上是创建了一个X类型的临时对象,将已有的对象Y的基类部分赋值给此临时对象,此时需要调用X类型的拷贝构造函数。根据构造和析构的相反顺序,后构造的临时对象将先被析构,即第一个~X()。然后析构y,先调用派生类的析构函数再调用基类的析构函数。
11、定位new 可以将对象创建在已经分配好的缓冲池里
class X{
public:
X(){cout<<"constructed"<<endl;}
~X(){cout<<"destructed"<<endl;}
};
void main(){
char* buf=new char[sizeof(X)*20];
X* px=new(buf) X; 调用构造函数
px->~X(); 需要显式析构
delete[] buf; 不需要对定位new的对象调用delete,因为定位new并不分配空间,只是构造对象,但是要手工调用析构函数。
}
12、extern "C" 的函数能与c++函数重载吗?
可以。重载的本质就是在编译时根据参数个数和类型来为函数添加一些附加信息,以生成不同的函数标识符,这样在调用时根据参数就可以自动识别并调用对应的函数。而C中并没有参数类型和个数的附加信息,因此无法实现重载。
但是两个extern "C"引入的函数之间不能重载
struct CLS
{
int m_i;
CLS( int i ) : m_i(i) {}
CLS( )
{
CLS(0);
}
};
void main( void )
{
CLS obj1; // 调用无参数构造函数,再调用CLS(0)????;
CLS obj2(1);
cout << obj1.m_i << endl;
//-858993460
cout << obj2.m_i << endl;
//1
}
CLS(0)去掉或者改为CLS(2),结果都是一样的,证明上述结果只是一个随机数而已。为什么调用了CLS(0)没有任何作用呢??
需要清楚的是对象的创建是分两步来完成的
1、首先是先分配内存,如果是动态分配的话,需要调用operator new操作符,如果是作为一个局部变量由编译器在栈上分配内存
2、分配完内存以后,编译器或者new自动调用构造函数进行初始化
这两步是有先后顺序的,必须第一步完成以后才会做第二步的,假如在动态分配的时候内存分配失败,那编译器能够保证第二步不会执行。
显然上面代码中,CLS obj1;这里已经为obj1分配了内存,然后调用默认构造函数,但是默认构造函数还未执行完,却调用了另一个构造函数,这样相当于产生了一个匿名的临时CLS对象,它调用CLS(int)构造函数,将这个匿名临时对象自己的数据成员m_i初始化为0;但是obj的数据成员并没有得到初始化。于是obj的m_i是未初始化的,因此其值也是不确定的。会产生一个新的临时对象,可以观察一个在两个构造函数里面的this指针就知道了,构造函数执行完毕时随即析构该临时对象。
要想达到期望的效果,在构造函数里调用另一个构造函数的关键是让第二个构造函数在第一次分配好的内存上执行,而不是分配新的内存,这个可以用标准库的placement new做到
inline void *__cdecl operator new(size_t, void *_P)
{
}
调用上述的函数没有分配新的内存,这正是我们想要的。因此我们可以这样在一个构造函数里调用另一个构造函数:
class CTest
{
public:
int a;
int b;
CTest(int a, int b)
{
this->a = a;
this->b = b;
}
CTest(int a)
{
new(this) CTest(a,5);
}
};
int main(int argc, char* argv[])
{
CTest obj(1);
cout<< "a is " << obj.a << endl;
cout<< "b is "<< obj.b << endl;
return 0;
}
在C++中,成员函数的调用约定是thiscall,这个约定的意思是调用的时候第一个参数是个this指针。但是构造函数不会自动接收this指针,因为这个时候对象还没有成功建立呢。其实,构造函数和其他的成员函数也没多大的区别,最主要的区别是他一般是由编译器生成代码自动实现调用的,因此他不需要返回值(也没必要)。 但也能像调用普通成员函数一样调用构造函数,这个时候需要明确使用this来引用构造函数,如this->CTest::CTest()
class CTest
{
public:
int a;
int b;
CTest(int a, int b)
{
this->a = a;
this->b = b;
}
CTest(int a)
{
//new(this) CTest(a,5);
this->CTest::CTest(a,5);
}
};
这种情况下,第二个有参构造函数就是在第一个构造函数的基础上进行初始化。
若构造函数调用自身,则会出现无限递归调用,将会发生堆栈溢出。First-chance exception in ThinkingInCPlusPlus.exe: 0xC00000FD: Stack Overflow.
从这里,我们归纳如下:
1)在c++里,由于构造函数允许有默认参数,使得这种构造函数调用构造函数来重用代码的需求大为减少。因此应尽量避免这样使用,适用不当会带来意想不到的问题
2)如果仅仅为了一个构造函数重用另一个构造函数的代码,那么完全可以把构造函数中的公共部分抽取出来定义一个成员函数(推荐为private),然后在每个需要这个代码的构造函数中调用该函数即可
3)偶尔我们还是希望在类的构造函数里调用另一个构造函数,关键是让第二个构造函数在第一次分配好的内存上执行,而不是分配新的内存,这个可以用标准库的placement new或者明确通过第一个对象的this指针来调用第二个构造函数,此时相当于普通的函数调用。