中科院C++复习

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

中科院C++复习
1.用你的语言说明什么是支持面向过程的程序设计风格,什么是支持面向对象的程序设计风格。

他们的特征分别是什么?
面向过程:以过程为中心,把解决问题所需要的步骤依次实现,使用的时候一个一个调用就可以了
面向对象:把要解决的问题作为一个实体,把各个不同的对象模块化,最后通过模块组合来实现功能。

2.用图示的方法简要表述下列程序执行时的运行时结构
void main()
{
Printf(“hello!”);
}
main()主入口 -> 调用Printf函数 -> 读入参数“hello!” -> 打印出hello! ->Printf函数结束 ->
main函数结束
3.分析“野”指针“野”的原理,以及可能造成什么样的危害?
当指针所指向的内存区域已经被释放的时候,指向这个区域的指针就成为“野”指针,他仍然随时可以访问这个区域。

他的存在可能会使得内存中的某些数据被意料之外的修改。

4.分别说明下面语句中7个const含义
const int a = 5; // 变量a为常数,不可被赋值
const int* ptr; // 指针ptr为常指针,所存的地址不可被修改
int* const ptr = &a; // ptr为指向常量的指针,它所指向的值不可被赋值
const int* const ptr = &a; // ptr为指向常量的常指针,代表的地址和指向的值都不可被修改
double& fn(const double& pd) // fn函数的参数为常指针pd {
static double ad = 32;
ad += pd;
return ad;
}
int Date::GetMonth() const // GetMonth()内的变量为常量,不可被赋值{
return month;
}
5.分别描述下面程序中4个static的含义,这个程序不能通过编译,请修改程序使之能够通过编译
class A
{
static int B() // 类A内定饿静态方法
{
return c;
}
protected:
static int c; // 类A内的静态常量
};
void main()
{
static int a; // 主函数main()的静态常量
static A a; // 静态类A的静态对象,与上面一行的a冲突,修改为其他字母
}
6.下面的程序是编译时出问题还是执行时出问题?描述所出的问题,分析出问题的原因。

class A
{
public:
A()
{
p = new char;
}
~A()
{
delete p;
}
protected:
char* p;
};
void main()
{
A a1;
A a2 = a1;
}
答:执行时出问题,因为在main函数结束的时候,程序会析构掉对象a1和a2,由于a2=a1是一个浅拷贝,会把相同的指针p赋给两个不同的对象,当析构了其中一个对象以后,p指针成为野指针,第二个对象析构的时候运行delete p会报错。

7.根据运行时结构,指出以下代码中隐含的错误,并分析原因
#include
float& fn(float r)
{
float a;
a = (float)(r*r*3.14);
return a;
}
void main()
{
float b = fn(5.0);
float& d = fn(5.0);
cout << b << endl;
cout << d << endl;
}
问题1:cout不能被使用,因为没有使用名字空间std
问题2:函数fn是一个指针类型的返回值,所以计算出来的结果a在返回的时
候会成为一个地址,这会让d的输出变得不正常,因为d的值是存有答案的地址;虽然b的输出表面看是正常的,但这其实是一个地址,存在安全隐患。

8.本题程序的设计初衷是想输出hello,在VC下的运行结果确实输出一个,请分析原因?
#include
#include
#include
void GetMemory(char *p,int num)
{
p = (char*)malloc(sizeof(char)*num);
strcpy(p,"hello");
}
int main()
{
char *str = NULL;
GetMemory(str,100);
printf("%s",str);
return 0;
}
答:str存的是首地址,而在函数中,对形参用malloc开辟了新的空间,这并
不会影响main函数中的str的地址,所以str指向的内容并没有
改变,依然是NULL
9.写出程序中虚函数的运行时结构(指的是虚指针、虚表)
class A
{
int a;
public:
virtual void f(); // 父类中的虚函数f
virtual void g(int); // 父类中的虚函数g
virtual void h(double); // 父类中的虚函数h
}; // 虚函数的调用情况要视A的子类实际情况而定
class B : public A
{
public:
int b;
void g(int); // 实化了A类函数g,会被实际调用
virtual void m(B*); // B类中的虚函数m,调用情况视B的子类而定};
class C : public B
{
public:
int c;
void h(double); // 实化了A类函数h,会被实际调用
virtual void n(C*); // C类中的虚函数n,调用情况视C的子类而定};
10.类的继承和类的组合之间有什么共同点,区别是什么?
共同点:可以使用上一级的成员(父类或者被组合对象)的成员
11.你认为C++中设置public、protected、private这几个概念表示类域的访问权限,及表示类的继承方式,其设计指导思想是什么?
12.有不少业内的高手是按照如下的思路来设计类的,一种思路是把实现某些功能或实现整个程序功能的所有函数做成一个类。

还有一
种思路,是把他认为“最好的”特征(包括表现这个特征的函数和数据)集中,形成一个类。

请你依据面向对象的思想,论述“全功能类”和“最好类”会给面向对象的程序设计带来什么影响。

13.在流行的C++教科书中,对多态有两种截然不同的说法,一种认为:“因为多态性增加了一些数据存储和执行指令的代价,所以能不多态最好。

”另一种认为:“这是类机制中最关键的一个方面。

只要用的好,它就能够成为面向对象程序设计的基石”(引号中的文字均引自原文)你赞同哪种说法,论述你的理解。

继承(inherit)或派生(derive)描述的是同一概念,从下向上称作继承,反之称作派生。

在C++中,继承与组合都是利用已有的类构建新类的机制。

区别是:
组合是将已有类的对象直接包含进来形成新类,旧类与新类之间可以没有任何内在的联系。

继承是依托C++语言的内在机制,延续、发展出新类,旧类与新类有鲜明的内在联系。

的成员包括数据成员和成员函数两部分。

类的成员从访问权限上分有以下三类:
公有的(public)
私有的(private)
保护的(protected),其中默认为private权限。

公有成员:提供了类的外部接口,可以被程序中的任何代码访问;
私有成员:只能被类本身的成员函数及友元类的成员函数访问,其他类的成员函数,包括其派生类的成员函数都不能访问它们;
保护成员:与私有成员类似,只是除了类本身的成员函数和说明为友元类的成员函数可以访问保护成员外,该类的派生类的成员也可以访问。

构造函数:
1.初始化对象
2.为对象分配资源
3.程序员认为应该在此时作的其它工作
析构函数:
1.撤销为对象分配的资源
2.程序员认为应该在此时作的其它工作“清道夫”
三、为什么要设计构造函数、析构函数
1.更好的体现“物化”思想(自动性)
2.更好的封装性(避免从外部初始化)
3.更有效地使类架构设计与程序架构设计分开
4.导致更清晰的面向接口编程、更有效的复用
构造函数:
1.没有返回类型,可以有无值返回语句
2.可以有参数,
3.可以重载
4.可以显式调用
析构函数:
1.没有返回类型,可以有无值返回语句
2.不允许有参数,
3.不可以重载
4.不可以显示调用
1.引用是别名、不是实体。

有声明、无定义。

2.引用必须在声明时立即初始化(绑定)。

3.引用不可重复初始化,一个外号不能用给两个(以上)人。

4.一个变量可以有两个(以上)引用。

一个人有多个外号。

5.可以用一个引用初始化另一个引用。

一个外号声明另一个外号。

6.参数为引用时,实参与形参(引用)在传参的瞬间绑定,函数结束时绑定撤销。

7.用函数返回的普通变量初始化一个引用是很危险的做法,这意味着返回的值已经被清栈,引用绑定了一个虚无的变量。

8.函数返回引用,意味着返回了一个已经初始化了的引用(别名),这个引用可以给普通变量赋值,可以初始化另一个引用,还可
以作为左值使用。

8.const引用锁死引用的被赋值。

静态数据成员
类的作用域,全局的生存期。

静态数据成员不占对象内存,不论对象有多少,内存中只有一个实例。

需要静态数据成员的理由:
(通常的理由)
同一个类的不同对象之间需要一个共享数据,如:
程序运行的任意时刻同一个类的对象数。

继承与组合概念、区别及优缺点
1.什么是继承
A继承B,说明A是B的一种,并且B的所有行为对A都有意义
eg:A=WOMAN B=HUMAN
A=鸵鸟 B=鸟(不行),因为鸟会飞,但是鸵鸟不会。

2.什么是组合
若在逻辑上A是B的“一部分”(a part of),则不允许B从A派生,而是要用A和其它东西组合出B。

例如眼(Eye)、鼻(Nose)、口(Mouth)、耳(Ear)是头(Head)的一部分,所以类Head应该由类Eye、Nose、Mouth、Ear组合而成,不是派生而成
3.继承的优点和缺点
优点:
容易进行新的实现,因为其大多数可继承而来。

易于修改或扩展那些被复用的实现。

缺点:
破坏了封装性,因为这会将父类的实现细节暴露给子类。

“白盒”复用,因为父类的内部细节对于子类而言通常是可见的。

当父类的实现更改时,子类也不得不会随之更改。

从父类继承来的实现将不能在运行期间进行改变。

4.组合的优点和缺点
优点:
容器类仅能通过被包含对象的接口来对其进行访问。

“黑盒”复用,因为被包含对象的内部细节对外是不可见。

封装性好。

实现上的相互依赖性比较小。

(被包含对象与容器对象之间的依赖关系比较少)
每一个类只专注于一项任务。

通过获取指向其它的具有相同类型的对象引用,可以在运行期间动态地定义(对象的)组合。

缺点:
导致系统中的对象过多。

为了能将多个不同的对象作为组合块(composition block)来使用,必须仔细地对接口进行定义。

5.两者的选择
is-a关系用继承表达,has-a关系用组合表达
继承体现的是一种专门化的概念而组合则是一种组装的概念
另外确定是组合还是继承,最清楚的方法之一就是询问是否需要新类向上映射,也就是说当我们想重用原类型作为新类型的内部实现的话,我们最好自己组合,如果我们不仅想重用内部实现而且还想重用接口的话,那就用继承。

6.法则:优先使用(对象)组合,而非(类)继承
继承是派生类把基类的成员继承为自己的成员,派生类可以的成员函数可以访问基类的部分成员,这要看是以哪种方式继承。

而且派生类的对象也可以访问部分基类的成员。

但是类的组合只是一个类a 的对象为另一个类b的数据成员,类b的成员函数可以通过对象访问类a 的成员。

但是类a的成员不是类b的成员。

C + +最重要的性能之一是代码重用。

但是,为了具有可进化性,我们应当能够做比拷贝代码更多的工作。

在C的方法中,这个问题未能得到很好的解决。

而用C + +,可以用类的方法解决,通过创建新类重用代码,而不是从头创建它们,
这样,我们可以使用其他人已经创建并调试过的类。

关键是使用类而不是更改已存在的代码。

这一章将介绍两种完成这件事的方法。

第一种方法是很直接的:简单地创建一个包含已存在的类对象的新类,这称为组合,因为这个新类是由已存在类的对象组合的。

第二种方法更巧妙,创建一个新类作为一个已存在类的类型,采取这个已存在类的形式,对它增加代码,但不修改它。

这个有趣的活动被称为继承,其中大量的工作由编译器完成。

继承是面向对象程序设计的基石,并且还有另外的含义,将在下一章中探讨。

对于组合和继承(感觉上,它们都是由已存在的类型产生新类型的方法),它们在语法上和行为上是类似的。

这一章中,读者将学习这些代码重用机制。

多态性(在C + +中用虚函数实现)是面向对象程序设计语言继数据抽象和继承之后的第三个基本特征。

它提供了与具体实现相隔离的另一类接口,即把“w h a t”从“h o w”分离开来。

多态性提高了代码的组织性和可读性,同时也可使得程序具有可生长性,这个生长性不仅指在项目的最初创建期可以“生长”,而且希望项目具有新的性能时也能“生长”。

封装是通过特性和行为的组合来创建新数据类型的,通过让细节p r i v a t e来使得接口与具体实现相隔离。

这类机构对于有过程程序设计背景的人来说是非常有意义的。

而虚函数则根据类型的不同来进行不同的隔离。

上一章,我们已经看到,继承如何允许把对象作为它自己的类型或它的基类类型处理。

这个能力很重要,因为它允许很多类型(从同一个基类派生的)被等价地看待就象它们是一个类型,允许同一段代码同样地工作在所有这些不同类型上。

虚函数反映了一个类型与另一个类似类型之间的区别,只要这两个类型都是从同一个基类派生的。

这种区别是通过其在基类中调用的函数的表现不同来反映的。

第12章派生类
没必要时不重复任何东西。

-W.Occam 12.1引言
C++从Simula那里借来了以类作为用户定义类型的概念,以及类
层次结构的概念。

此外,它还借来了有关系统设计的思想:类应该用于模拟程序员的和应用的世界里的那些概念。

C++提供了一些语言结构以直接支持这些设计概念。

但在另一方面,使用支持设计概念的语言特征与最有效地使用C++还是有所不同,如果只使用这些语言结构作为对更传统程序设计的类型的一种表示形式,那就更是丢掉了C++最关键最强有力的东西。

一个概念不会孤立地存在,它总与一些相关的概念共存,并在与相关概念的相互关系中表现出它的大部分力量。

举个例子,请试一试去解释什么是汽车,很快你就会引出许多概念:车轮、引擎、司机、人行横道线、卡车、救护车、公路、汽油、超速罚单、汽车旅店等。

因为我们要用类表示概念,问题就变成了如何去表示概念之间的关系。

然而,我们无法直接在程序语言里表述任意的关系。

即使能这样做,我们也未必想去做它。

我们的类应该定义的比日常概念更窄一些——而且也更精确。

派生类的概念及其相关的语言机制使我们能表述一种层次性的关系,也就是说,表述一些类之间的共性。

例如,圆和三角形的概念之间有关系,因为它们都是形状,即它们共有着形状这个概念。

因此,我们就必须明确地定义类Circle和类Triangle,使之共有类Shape。

在程序里表示出一个圆和一个三角形,然而却没有涉及到形状的概念,就应该认为是丢掉了某些最基本的东西。

本章就是对这个简单思想的内涵的一个探索,这个思想就是通常称为面向对象的程序设计的基础。

这里对语言特征和技术的展示仍将从简单而具体的事物开始,逐步进展到更复杂更抽象的事物。

对于大部分程序员而言,这也是从熟悉之物到更加未知的世界的一个旅程。

这并不是从“糟糕的老技术”到“惟一的正确途径”的简单过渡。

当我指出某种技术的局限性,作为通往另一种技术的推动力时,我总是在一个特定问题的环境中做这件事情;而对于不同的问题或者其他环境,第一种技术很可能反而成为更好的选择。

人们已经用这里展示的所有技术构造出许许多多实用软件。

这里的目标就是帮助你获得对这些技术的充分理解,以便在面对实际问题时,能在它们之中做出明智的有条不紊的选择。

在这一章里,我要首先介绍支持面向对象程序设计的基本语言特征;而后将在一个大例子的环境中,讨论如何使用这些特征去开发结构良好的程序。

支持面向对象程序设计的其他特征,例如多重继承和运行时类型识别,将在第15章讨论。

12.2派生类
现在来考虑做一个程序,处理某公司所雇佣人员的问题。

这个程序可能包含如下的一种数据结构:
(见英文版)
下一步,我们可能需要去定义经理:
(见英文版)
一个经理同时也是一个雇员,所以在Manager对象的emp成员里存储着Employee数据。

这对于读程序的人而言是很明显的——特别是细心的读者,但是却没有给编译器或者其他工具提供有为manager也是Employee的任何信息。

一个Manager*就不是Employee*,所以,在要求一个的地方也就无法简单地使用另一个。

特别是,如果不写出一些特殊代码,你将无法把一个Manager放进一个Employee的表里。

我们当然可以做这件事,或者是对Manager*做显式的类型转换,或者是将其emp成员的地址存人Employee的表。

但是,这两种解决方案都不优美,也都是相当不清晰的。

正确的途径应该能够把Manager也是Employee的事实明确地表述出来,再加上少量的信息:
(见英文版)
这个Manager是由Employee派生的,反过来说就是,Employee是Manager的一个基类。

类Manager包含了类Employee 的所有成员(first_name,department等),再加上它自己的一些成员(group,level等)。

派生关系常用图形表示,画出从派生类到基类的一个箭头,指明派生类引用了它的基类(而不是相反):派生类常常被说成是从它的基类继承了各种性质,因此这个关系也被称为继承。

基类有时被称做超类,派生类被称做子类。

然而这一对术语却常常把人们搞胡涂,因
为他们看到派生类对象的数据是基类对象的数据的一个超集。

派生类通常都比基类更大,即是说它保存了更多数据,提供了更多的函数。

对于派生类概念的一种常见且有效的实现方式,就是将派生类的对象也表示为一个基类的对象,只是将那些特别属于派生类的信息附加在最后。

例如,
(见英文版)
按照这种方式从Employee派生出Manager,就使Manager成为Employee的一个子类型,使Manager可以用在能够接受Employee的任何地方。

例如,我们现在就可以建立起一个Employee 的表,而其中的一些元素Manager:
(见英文版)
因为Manager(也)是Employee,所以Manager*就可以当作Employee*使用。

然而,因为Employee不一定是Mannger,所以Employee*就不能当做Manager*用。

总而言之,如果类Derived有一个公用基类(15.3节)Base,那么就可以用Derived*给Base*类型的变量赋值,不需要显式的类型转换。

而相反的方向,从Base*到Derived*则必须显式转换。

例如,(见英文版)
换句话说,在通过指针或者引用方式操作时,派生类的对象可以当做基类的对象看待和处理。

反过来则不真。

关于static_cast和dynamic_cast的使用将在第15.4.2节讨论。

用一个类作为基类,相当于声明一个该类的(匿名)对象。

所以,要想作为基类,这一个类就必须有定义(5.7节):
(见英文版)
12.2.1成员函数
在实际中,像Employee和Manager这样的简单数据结构不太令人感兴趣,它们也不是特别有用。

我们需要给出更多的信息,将它们做成真正的类型,提供适当的表示有关概念的一组函数,而且,在这样做时又应该避免使我们依赖于特殊的表示细节。

例如,
(见英文版)
派生类的成员可以使用其基类的公用的——和保护的(见15.3节)
——成员,就像它们也是在派生类里声明的一样。

例如,
(见英文版)
但是,派生类不能使用基类的私用名字:Manager::Print()的第二个版本将无法编译。

派生类的成员也没有访问其基类的私用成员的特许权,所以family_name对于Manager::Print ()而言是不可访问的。

这一点开始可能会使一些人吃惊。

那么请考虑另一种安排:派生类的成员函数能够访问其基类的私用成员。

这样就会使私用成员这个概念退化为一种毫无意义的东西,因为程序员可以简单地通过从一个类出发的派生而获得对其私用成员的访问权。

进一步说,我们也不能再通过查看声明为某个类的所有成员和友元的函数,而确定对该类私用成员的所有使用。

人们将必须去检查整个程序的每个源文件,去找出所有的派生类,而后检查这些类里的每一个函数;还要找出由这些类派生出的每一个类,如此等等。

这种处理方式,按最好的情况说,也是令人讨厌的,常常是不实际的。

在能够接受这些的地方,可以采用protected成员——而不是private成员。

对于派生类的成员而言,保护成员就像是公用成员;但对于其他函数它们则像是私用成员(15.3节)。

一般来说,最清晰的设计是派生类只使用它的基类的公用成员。

例如,
(见英文版)
注意,在这里必须用::,因为在Manager里重新定义了Print()。

名字的这种重新使用是很典型的。

不当心的人可能这样写
(见英文版)
然后就会发现这个程序被牵涉进一个未预料到的无穷递归里。

12.2.2构造函数和析构函数
有些派生类需要构造函数。

如果某个基类中有构造函数,那么就必须调用这些构造函数中的某一个。

默认构造函数可以被隐含地调用,但是,如果一个基类的所有构造函数都有参数,那么就必须显式地调用其中的某一个。

考虑
(见英文版)
基类构造函数的参数应在派生类构造函数的定义中有明确描述。

在这方面,基类的行为恰恰就像是派生类的一个成员(10.4.6节)。

例如,
(见英文版)
派生类的构造函数只能描述它自己的成员和自己的直接基类的初始式,它不能直接去初始化基类的成员。

例如,
(见英文版)
这个定义中包含了三个错误:它没有调用Employee的构造函数,而且还两次企图去直接初始化Employee的成员。

类对象的构造是自下而上进行的:首先是基类,而后是成员,再后才是派生类本身。

类对象的销毁则正好以相反的顺序进行:首先是派生类本身,而后是成员,再后才是基类。

成员和基类的构造严格按照在类声明中的顺序,它们的销毁则按照相反的顺序进行。

另见10.4.6节和15.2.4.l节。

12.2.3复制
类对象的复制由复制构造函数和赋值操作定义(10.4.4.l节)。

考虑
(见英文版)
由于Employee的复制函数根本不知道Manager的任何情况,所以只有Manager的Employee部分被复制。

这种情况通常被称为切割,它可能成为使人诧异和产生错误的根源。

需要在类层次结构中传递类对象的指针和引用,其中的一个原因就是为了避免切割问题。

另外的原因是为了维持多态性行为(2.5.4节、12.2.6节)和保证效率。

注意,如果你没有定义复制赋值运算符,编译器就会为你生成一个(11.7节)。

这也意味着赋值运算符是不继承的。

构造函数也是绝不继承的。

12.2.4类层次结构
派生类本身也可以作为基类。

例如,
(见英文版)
这样,一组相关的类按照习惯被称做一个类层次结构。

这种层次结构的最常见形式是一棵树,当然它也可能具有更一般的图结构。

例如,
(见英文版)
或画成图
(见英文版)
可以看出,C++能够表示类的有向无环图,这方面的情况将在15.2节解释。

12.2.5类型域
为了使派生类不仅仅是一种完成声明的方便简写形式,我们就必须解决下面的问题。

对给定的一个类型为Base*的指针,被指的对象到底属于哪个派生类型呢?这个问题有四种基本的解决方案:
[1]保证被指的只能是惟一类型的对象(2.7节,第13章)。

[2]在基类里安排一个类型域,供函数检查。

[3]使用dynamic_cast(15.4.2节、15.4.5节)。

[4]使用虚函数(2.5.5节、12.2.6节)。

指向基类的指针常常被用于设计各种容器类,如集合、向量和表等。

这时,解决方案1将产生出同质的表,即所有元素都具有同样类型的表。

解决方案2、3、4可用于构造出异质的表,即一些不同类型的对象(的指针)的表。

解决方案3是解决方案2的由语言支持的变形。

解决方案4是解决方案2的一种特殊的类型安全的变形。

解决方案1和解决方案4的组合特别有意思,而且威力强大,它们能产生出比解决方案2和解决方案3更清晰的代码。

让我们首先考察简单的类型城解决方案,看看为什么应该极力避免它。

经理/雇员的例子可以重新定义为如下形式:
(见英文版)
有了这个定义,我们就可以写出一个函数,打印与每个Employee有关的信息
(见英文版)
并将它用于打印Employee的表,如下所示:。

相关文档
最新文档