1.
class Eye {
public:
void Look(void);
};
组合是在新类中以原有类的对象作为数据成员,继承是在不改变现有的类的基础上,采用现有类的形式并在其中添加新代码,组合一般用于在新类中使用现有类的功能而不是他的接口的情况,就是新类用户看到的只是为新类所定义的接口。而继承则是用于在新类需要向基类转化的情况(多态),这也是组合和继承使用的最清晰的判断方法。
结合本题分析,我们只需让眼睛作为头的一个成员即可,而让头去继承眼睛的特性是没有必要的。
“优先使用对象组合,而不是继承”是面向对象设计的第二原则。
组合也叫“对象持有”,就是在类中定义另一类型的成员,继承会破坏类的独立性,增加系统的复杂性,一般系统的继承层次不超过3层。组合拥有良好的扩展性,支持动态组合,因此请优先考虑组合方法。
2.分析以下程序:输出为A
#include<iostream>
#include<cstdlib>
#include<string.h>
using namespace std;
class A{
public:
A(){
printf("A");
}
};
int main() {
A*p1=new A;
A*p2=(A *)malloc(sizeof(A));
return0;
}
分析:这题主要是考的new和malloc的区别,new会分配内存,并调用类的构造函数创建对象,而malloc只是分配内存,不调用类的构造函数创建对象,这个程序应该用delete p1;显示释放掉p1所指的堆上的内存,这样才会调用类的析构函数,否则不会调用。
分配存储空间时才不调用其构造函数。不会崩溃,但是会发生内存泄露,不是好的编程习惯。应该加上delete p1;free(p2);
3.分析下列程序:输出为12
#include<iostream>
#include<cstdlib>
#include<string.h>
using namespace std;
class Base{
public:
Base(intj): i(j) {
}
virtual~Base() {
}
voidfunc1() {
i*=10;
func2();
}
intgetValue() {
returni;
}
protected:
virtualvoid func2() {
i++;
}
protected:
inti;
};
class Child:public Base {
public:
Child(intj): Base(j){
}
voidfunc1() {
i*=100;
func2();
}
protected:
voidfunc2() {
i+=2;
}
};
int main() {
Base*pb = new Child(1);
pb->func1();
cout<<pb->getValue()<<endl;
deletepb;
return0;
}
分析:Base * pb = new Child(1), 首先创建子类对象,初始化为1;func1()不是虚函数,所以pb->func1()执行的是基类的func1函数,i= 10,然后调用func2()函数;这里的func2是虚函数,要往下派生类寻找,找到后执行派生类中的func2(),此时,i = 12;最后执行pb->getValue(),结果为12。
可以让成员函数操作一般化,用基类的指针指向不同的派生类的对象时,基类指针调用其虚成员函数,则会调用其真正指向对象的成员函数,而不是基类中定义的成员函数(只要派生类改写了该成员函数)。若不是虚函数,则不管基类指针指向的哪个派生类对象,调用时都会调用基类中定义的那个函数。
4.int *a[10]; //指向int类型的指针数组a[10]
int (*a)[10]; //指向有10个int类型数组的指针a
int (*a)(int);//函数指针,指向有一个参数并且返回类型 均为int的函数
int (*a[10])(int); //函数指针的数组,指向有一个参数并且返回类型均为int的函数的数组
5. 静态数据成员可以初始化,但只能在类体外进行初始化;
静态数据成员既可以通过对象名引用,也可以通过类名来引用;
静态数据成员也是类的数据成员,当然可以用private、public等访问控制符来设定对其的访问权限。
静态数据成员同样受 private,public,protected 等权限符限制,静态数据成员由于是属于整个类的,对它们的初始化一般在类外,可以通过累的对象,指针,类名等直接调用静态成员,类的静态成员函数可以在类内进行定义,类的静态成员函数只能直接调用静态成员,如果想调用非静态成员只能通过间接的方式,而非静态成员可以直接访问静态成员。
5.不同基类型的指针变量占用字节数是相同的,但是不能混用。
所谓函数类型是指函数返回值的类型。在C语言中允许一个函数的返回值是一个指针(即地址),这种返回指针值的函数称为指针型函数;
函数的参数不仅可以是整型、实型、字符型等数据,还可以是指针类型;
6.分析下列程序,在64位机器上用gcc编译以上代码,求sizeof(A)和sizeof(B)分别为16 24
class A{
inta;
shortb;
intc;
chard;
};
class B{
doublea;
shortb;
intc;
chard;
};
分析:根据以下条件进行计算:
1)、结构体的大小等于结构体内最大成员大小的整数倍;
2)、结构体内的成员的首地址相对于结构体首地址的偏移量是其类型大小的整数倍,比如说double型成员相对于结构体的首地址的地址偏移量应该是8的倍数。
3)、为了满足规则1和2编译器会在结构体成员之后进行字节填充!
A中,a占4个字节,b本应占2个字节,但由于c占4个字节,为了满足条件2,b多占用2个字节,为了满足条件1,d占用4个字节,一共16个字节。
B中,a占8个字节,b占2个字节,但由于c占4个字节,为了满足条件2,b多占用2个字节,即abc共占用8+4+4=16个字节,为了满足条件1,d将占用8个字节,一共24个字节。
7.分析下列程序:输出为50.
#include<stdio.h>
struct mybitfields {
unsignedshort a:4;
unsignedshort b:5;
unsignedshort c:7;
} test;
int main(void) {
inti;
test.a=2;
test.b=3;
test.c=0;
i=*((short*)&test);
printf("%d\n",i);
}
分析:题的难点在于前面定义结构体里面用到的冒号。这里的冒号相当于分配几位空间,也即在定义结构体的时候,分配的成员a 4位的空间, b 5位,c 7位,一共是16位,正好两个字节。下面画一个简单的示意: 变量名 位数
test 15 14 13 12 11 10 9 |8 7 6 54 |3 2 1 0
test.a | |0 010
test.b |00011|
test.c 0000000| |
在执行i=*((short *)&test);时,取从地址&test开始两个字节(short占两个字节)的内容转化为short型数据,即为0x0032,再转为int型为0x00000032,即50。
8.若有类AB,有相应的构造函数定义,能正确执行
AB a(4),b(5),c[3],*p[2]={&a,&b};语句之后共调用该类的构造函数次数为:5.
分析:只有给对象分配内存才调用构造函数AB。
a(4)定义对象a,调用了带一个参数的构造AB;
b(5)跟上面的性质类似,调用了带一个参数的构造AB;
c[3]跟上面的性质类似,定义对象数组,调用无参构造3次AB;
*p这是一个指针,没有指向任何空间。
除非分配内存,否则不会调构造函数.
9. 构造函数初始化时必须采用初始化列表一共有三种情况,
1).需要初始化的数据成员是对象(继承时调用基类构造函数)
2).需要初始化const修饰的类成员
3).需要初始化引用成员数据
10. C++中有三种权限控制类型,分别是共有类型public,私有类型private,保护类型protected。
友元是声明一个类外的方法具有类方法同样的访问权限,目的是让类外的方法可以访问类内部的属性,不是访问控制属性。
11.如下程序,则构造函数中,成员变量一定要通过初始化列表来初始化的是:b c
class A{
private:
inta;
};
class B: public A{
private:
inta;
public:
constint b;
A&c;
staticconst char *d;
B*e;
};
分析:引用,const成员变量,基类构造函数,一定要通过初始化列表来实现。static是类成员(但不是对象成员),不需要通过初始化列表来初始化。c是对象需要初始化而b是常量型变量,所以选b,c;
d不选是因为static是类成员而不是对象成员,e不选是其只是指向对象的指针。
12. 绝不在构造和析构过程中调用virtual方法
1)、绝不在构造和析构过程中调用virtual方法,为啥? 原因很简单,对于前者,这种情况下,子类专有成分还没有构造,对于后者,子类专有成分已经销毁,因此调用的并不是子类重写的方法,这不是程序员所期望的。
2)、在构造方法和析构方法中,直接调用virtual方法,很容易识别。还有一种情况要注意,那就是间接调用。比如:构造方法调用init方法,而init方法调用virtual方法。
3)、在构造过程中,不能使用virtual从上到下调用,哪有什么办法弥补呢? 可以将子类必要的信息向上传递给父类构造方法。
在构造和析构函数中调用虚函数会执行与之所属类型相对应的虚函数版本。
#include<iostream>
using namespace std;
class A{
public:
A():m_iVal(0) {test();
}
virtualvoid func() {
std::cout<<m_iVal<<'';
}
voidtest() {func();
}
public:
intm_iVal;
};
class B: public A{
public:
B(){test();
}
virtualvoid func() {
++m_iVal;
std::cout<<m_iVal<<'';
}
};
int main() {
A*p=new B;
p->test();
return0;
}
分析:A* p=new B;先调用A的构造函数,这时虽然调用了虚函数,但行为是确定的,就是A的func,也就是0;
之后new B要调用B的构造函数,同样使用自己的func,即1;
p->test()则是动态绑定,因指向B,所以调用B的func,即2;
本问题涉及到两个方面:
1).C++继承体系中构造函数的调用顺序。
2).构造函数中调用虚函数问题。
C++继承体系中,初始化时构造函数的调用顺序如下
(1)任何虚拟基类的构造函数按照他们被继承的顺序构造
(2)任何非虚拟基类的构造函数按照他们被继承的顺序构造
(3)任何成员对象的函数按照他们声明的顺序构造
(4)类自己的构造函数
据此可知A*p =newB;先调用A类的构造函数再调用B类的构造函数。
构造函数中调用虚函数,虚函数表现为该类中虚函数的行为,即在父类构造函数中调用虚函数,虚函数的表现就是父类定义的函数的表现。why?原因如下:
假设构造函数中调用虚函数,表现为普通的虚函数调用行为,即虚函数会表现为相应的子类函数行为,并且假设子类存在一个成员变量int a;子类定义的虚函数的新的行为会操作a变量,在子类初始化时根据构造函数调用顺序会首先调用父类构造函数,那么虚函数回去操作a,而因为a是子类成员变量,这时a尚未初始化,这是一种危险的行为,作为一种明智的选择应该禁止这种行为。所以虚函数会被解释到基类而不是子类。