目录
第 1 章 C++的初步知识
1.1 从C到C++
-
C语言是结构化和模块化的语言,它是面向过程的,在处理较小规模的程序时,用C语言较得心应手
Q: 那为什么当问题比较复杂、程序规模较大时不适用C程序呢?
A:因为C程序的设计者必须细致地设计程序中的每一个细节,准确地考虑到程序运行时每一时刻发生的事情,例如各个变量的值是如何变化的,什么时候应该进行哪些输入,在屏幕上应该输出什么等,这对程序员的要求是比较高的,如果面对的是一个程序规模较大的复杂问题,程序员往往会感到力不从心。而面向对象程序设计就是针对开发较大规模的程序而提出来的,目的是提高软件开发的效率。
-
C++保留了C语言原有的所有优点,并增加了面向对象的机制,与C兼容,C++既可以用于面向过程的结构化程序设计,又可用于面向对象的程序设计。
-
C++对C的“增强”,表现在两个方面:①在原来面向过程的机制基础上,对C语言的功能做了不少扩充。②增加了面向对象的机制。
1.2 C++对C的扩充
-
C++的输入输出 : cin 和 cout
-
cout:cout 必须和输出运算符"<<" 一起使用。 C语言中"<<"是左移运算符,C++中赋予了新的含义:作为输出信息时的“插入运算符” 。 例如:
“cout << "Hello!\n” ;
的作用是将字符串"Hello!\n" 插入到 输出流cout中,也就是说把所指定的信息输出在标准输出设备上。 -
setw用法:如果要指定输出所占的列数,可以用控制符 setw 进行设置,如 setw(5) 的作用是为其后面一个输出项预留5列的空间,如果输出数据项的长度不足5列,则数据向右对齐,若超过5列则按实际长度输出。
例子:
float a = 3.45; int b = 5; char c = 'A'; cout << "a=" << setw(6) << a << endl << "b=" << setw(6) << b << endl << "c=" << setw(6) << c << endl;
a=_ _3.45
b=_ _ _ _ _5
c=_ _ _ _ _A
Tip: _ 代表一个空格
-
cin: 从输入设备向内存流动的数据流称为输入流。C++中,用“>>”运算符从输入设备键盘取得数据并送到输入流cin中,然后再送到内存。这种输入操作成为“提取”或“得到”, 常称为提取运算符。
#include < iostream > using namespace std; int main(){ cout << "ok" << endl; int a; cout << a << endl; }
上面的程序中可以看到,对变量a的定义放在了执行语句(cout ...)之后,在C语言程序中是不允许这样的,它要求声明部分必须在执行语句之前。而C++允许对变量的声明放在程序的任何位置(但必须在使用该变量之前)。这是C++对C限制的放宽。Tip:当面试官问C++对C语言进行功能扩充的实例时,可以用。
Q:可为什么现在绝大多数编译器写C语言时即时没有在执行语句之前声明,也能通过编译且运行呢?。
A:回答这个问题之前,首先要了解C语言标准,即C89/C99/C11的区别。1989年,美国国家标准协会(ANSI)推出C语言和C标准库的标准,该标准通常被称为ANSI C,由于该标准是1989年推出的,因此也被称为C89。同理, 1999 年,ANSI 和 ISO 又通过了最新版本的 C 语言标准和技术勘误文档,该标准被称为 C99 。2011年,标准委员会推出了C11标准。C17(也被称为为 C18)是于2018年6月发布的 ISO/IEC 9899:2018 的非正式名称,也是目前(截止到2020年6月)为止最新的 C语言编程标准,被用来替代 C11 标准。C17 没有引入新的语言特性,只对 C11 进行了补充和修正。C2x是下一个版本的 C 标准,预计将于2022年12月1日完成。
现在主流的编译器基本都是用的C11, C89规定,在任何执行语句之前,需要在块的开头声明所有局部变量。但是在C99以及C++中则没有这个限制,即在首次使用之前,可在块的任何位置都可以声明变量。
1.3 用const 定义常变量
-
在C语言中常用 #define 来定义符号常量, 例如
#define PI 3.14159
。但实际上,只是在预编译时进行字符置换,把程序中出现的字符串PI 全部置换为3.14159。在预编译之后,程序中不再有PI这个标识符。PI不是变量,没有类型,不占用存储单元,而且容易出错,如
int a = 1; b = 2; #define PI 3.14159 #define R a+b cout << PI * R * R << endl;
这时输出的并不是我们以为的
3.14159*(a+b)*(a+b)
, 而是3.14159*a+b*a+b
。程序因此常常出错。 -
但是在C++语言中提供了const定义常变量的方法,如
const float PI = 3.14159;
, 定义了常变量PI,它具有变量的属性,有数据类型,占用存储单元,有地址,可以用指针指向它,只是在程序运行期间变量的值是固定的,不能改变。因为不像#define那样容易出错,const问世后,已经取代了用#define定义符号常量的作用。 -
const 常与指针结合使用,有指向常变量的指针、常指针、指向常变量的常指针等,这些概念容易模糊,将在第3章介绍。
1.4 C++的函数声明
-
在C++中,如果函数调用的位置在函数定义之前,则要求在函数调用之前必须对所调用的函数作函数原型声明(强制)。
函数声明形式:
函数类型 函数名(参数表)
参数表中一般要包括参数类型和参数名(int a), 也可以只包括参数类型而不包括参数名,如下:
int max(int x, int y);// 函数声明1 int max(int, int);//函数声明2
1.5 C++重载
-
C语言规定在同一作用域中不能有同名的函数,C++允许在同一作用域中用同一函数名定义多个函数,这些函数的参数个数和参数类型不相同,同名函数用来实现不同的功能。这就是函数的重载,即一个函数名多用。
1.6 函数模板
-
定义函数模板的一般形式:
template < typename T >
, 其中template 的含义是“模板”, 尖括号中先写关键字typename( 或 class),后面跟一个类型参数 T(也可以用任意一个标识符)。
Q:C++中的函数模板是干什么的,解决了什么问题?
A:①首先回答函数模板是什么,所谓函数模板,实际上是建立一个通用函数,其函数类型和形参类型不具体指定,用一个虚拟的类型来代表,这个通用函数就称为函数模板。例如:
template<typename T> T max (T a, T b, T c){...}
②假如有一个函数是用来返回最大值的,但是这个函数返回的类型需要有三种分别是int , float, long,如果你不用template函数模板,你就需要对这个函数进行重载,写三个形参类型不同,返回值类型不同,但是函数名相同的的三个max()函数。这三个max()函数的函数体其实是完全相同的只是类型不同,所以为了简化这种函数体相同的函数编写,C++提供了函数模板(template),凡是函数体相同的函数都可以用这个模板来代替,不必定义多个函数,只需在模板中定义一次即可。
1.7 有默认参数的函数
-
float area(float r = 6.5); area();//相当于area(6.5) area(7.5);//形参得到的值为7.5,而不是6.5
-
Q:为什么指定默认值的参数必须放在形参列表中的最右端,否则出错?例如为什么
void f1(int a, int b = 0, int c);
是错误的,而void f2(int a, int c, int b=0);
是正确的?A:因为第1个实参必然与第一个形参结合,第2个实参必然与第2个形参结合...,如果你调用函数
f2(2,3)
,是可以运行的,它相当于f2(2,3,0)
,而如果以同样的方式调用函数f1(2,3)
是必定错误的,因为它相当于f1(2,3,未赋值)
。
1.8 变量的引用
-
C语言中,没有引用(reference)的概念,引用是C++对C的一个重要扩充。
在C++中,变量的“引用”就是变量的别名, 因此引用又称为别名。对一个变量的“引用”的所有操作,实际上都是对其所代表的(原来的)变量的操作。
假如有一个变量a, 想给它起一个别名b,可以这样编写:
int a; int &b = a;
这就声明了b是a的“引用”,即a的别名。经过这样的声明后,使用a或b的作用相同,都代表同一变量。
Q:其中&符号是指“取地址符号”吗?,另外引用需要另开辟内存空间吗?
A:在上述声明中,&是指"引用声明符",此时并不代表地址,并不是"把a的值赋予给了b的地址"。对变量声明一个引用,并不另开辟内存单元,引用与其所代表的变量共享同一内存单元。实际上,编译系统使引用和其代表的变量具有相同的地址。
TIP:
-
类型 &变量名,表示引用。//
float &b = a;
-
数据&数据,表示按位相与,即AND。
sum = 5 & 4
; -
&变量名,表示地址。
int *p = &a;
-
-
当声明一个变量的引用后,在本函数执行期间,该引用一直与其代表的变量相联系,不能再作为其他变量的别名。
错误用法:
int a1, a2; int &b = a1; int &b = a2;//又企图使b成为变量a2的引用是不行的
-
引用并不是一种独立的数据类型,它必须与某一种类型的数据相联系。声明引用时必须指定它代表是哪个变量,即对它初始化。例如:
int &b = a; //正确,指定b是整型变量a的别名 int &b; //错误,没有指定b代表哪个变量 float a; int &b = a; //错误,声明b是一个整形变量的别名,而a不是整形变量
Tip:不要把声明语句
int &b = a;
理解为“将变量a的值赋给引用b“,它的作用是使b成为a的引用,即a的别名。 -
int a = 3; //定义a是整形变量 int &b = a; //声明b是整形变量的别名 int &c = b; //声明c是整形引用b的别名
这是合法的,这样,整型变量a有两个别名,即a和b。
-
C++之所以增加引用,主要是利用它作为函数参数,以扩充函数传递数据的功能。
在C语言中,函数传递主要有俩种情况
-
将变量名作为实参。这时传给形参的是变量的值,形参是实参的一个拷贝,在执行函数期间形参值发生变化并不传回给实参,因为在调用函数时,形参和实参不是同一个存储单元。
void swap(int a, int b) { int temp; temp = a; a = b; b = temp; } int main() { int i = 3, j = 5; swap(i, j); cout << i << ' ' << j << endl;//输出 3 5 }
-
传递变量的指针,使形参得到一个变量的地址,这时形参指针变量指向实参变量单元。
void swap(int *a, int *b) { int temp; temp = *a; *a = *b; *b = temp; } int main() { int i = 3, j = 5; swap(&i, &j); cout << i << ' ' << j << endl;//输出 5 3 }
在C++中,函数传递主要传送变量的别名。
-
void swap(int &a, int &b) { int temp; temp = a; a = b; b = temp; } int main() { int i = 3, j = 5; swap(i, j); cout << i << ' ' << j << endl;//输出 5 3 }
-
-
Q:分析使用引用比使用指针变量作函数形参有什么优点?
A:用引用能完成的工作,用指针也能完成。但用引用比用指针直观、方便,直截了当,不必”兜圈子“,容易理解。有些过去只能用指针来处理的问题,现在可以用引用来代替,从而降低了程序设计的难度。
第 2 章 类和对象的特性
2.1 面向对象的程序设计
Q:什么是面向对象的程序设计?和面向过程程序设计有什么区别?
A: 面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描述某个事物在整个解决问题的步骤中的行为。而面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。
例如:五子棋,面向过程的设计思路就是首先分析问题的步骤:1、开始游戏,2、黑子先走,3、绘制画面,4、判断输赢,5、轮到白子,6、绘制画面,7、判断输赢,8、返回步骤2,9、输出最后结果。把上面每个步骤用分别的函数来实现,问题就解决了。
而面向对象的设计则是从另外的思路来解决问题。整个五子棋可以分为 1、黑白双方,这两方的行为是一模一样的,2、棋盘系统,负责绘制画面,3、规则系统,负责判定诸如犯规、输赢等。第一类对象(玩家对象)负责接受用户输入,并告知第二类对象(棋盘对象)棋子布局的变化,棋盘对象接收到了棋子的i变化就要负责在屏幕上面显示出这种变化,同时利用第三类对象(规则系统)来对棋局进行判定。
此问题解答参考(面向对象和面向过程程序设计理解及区别 - 傲世九重天~楚阳 - 博客园)
2.2 对象
- 客观世界中任何一个事务都可以看成一个对象(object)。或者说,客观世界是由千千万万个对象组成的。对象可以是自然物体(如汽车、房屋、狗熊),也可以是社会生活中的一种逻辑结构(如班级、支部、连队),甚至一篇文章、一个图形、一项计划等都可以视作对象。
-
任何一个对象都应当具有属性和行为这两个要素。对象应能根据外界给的消息进行相应的操作。一个对象一般是由一组属性和一组行为构成的。
例子:一个班级作为一个对象时有两个要素:一是班级的静态特征,如班级所属系和专业、学生人数、所在的教室等,这种静态特征为属性;二是班级的动态特征,如学习、开会、体育比赛等,这种动态特征称为“行为”。如果想从外部控制班级中学生的活动,可以从外界向班级发一个信息(如听到广播声就去上早操,听到打铃就下课等),一般称它为消息
2.3 封装性
-
封装就是把数据和实现操作的代码集中起来放在对象内部,并尽可能的屏蔽对象的内部细节,不能从外部直接访问或修改这些数据及函数。使用一个对象的时候只需要把它看作成一个“黑箱子”,在它的表面有几个按键,即对象向外界提供的接口,人们无需知道对象内部的具体细节,只需知道每个按键对应的功能就行。
-
封装的其他解释:
封装指两方面的含义:一是将有关的数据和操作代码封装在一个对象中,形成一个基本单位,各个对象之间相互独立,互不干扰。二是将对象中某些部分对外隐蔽,即隐蔽其内部细节,只留下少数接口,以便于外界联系,接收外界的消息。
2.4 抽象
- 抽象的作用是表示同一类事务的本质。例如,我们常用的名词“人”,就是一种抽象。因为世界上只有具体的人,如张三、李四、王五。
2.5 继承与重用
-
Q:什么是继承机制?
A:如果在软件开发中已经建立了一个名为A的“类”, 又想另外建立一个名为B的“类”,而后者与前者内容基本相同,只是在前者的基础上增加一些属性和行为,显然不必再从头设计一个新类,而只须在类A的基础上增加一些新内容即可。这就是面向对象程序设计中的继承机制。
-
白马”继承了“马的”基本特征,又增加了新的特征(颜色),“马”是父类,或称为基类,“白马”是从“马”派生出来的,称为子类 或 派生类。
-
Q:什么是“软件重用”,有什么意义?
A:C++提供了继承机制,采用继承的方法可以很方便地利用一个已有的类建立一个新的类,这就可以重用已有软件中的一部分甚至大部分,大大节省了编程工作量。这就是常说的“软件重用“的思想,不仅可以利用自己过去所建立的类,而且可以利用别人使用的类或存放在类库中的类,对这些类作适当加工即可使用,大大缩短了软件开发周期,对于大型软件的开发具有重要意义。
2.6 多态性
-
Q:解释一下多态现象。
A:如果有几个相似而不完全相同的对象,有时人们要求在向它们发出同一个消息时,它们的反应各不相同,分别执行不同的操作。这种情况就是多态现象。
例如:在Windows环境下,用鼠标双击一个文件对象(这就是向对象传送一个消息),如果对象时一个可执行文件,则会执行此程序,如果对象是一个文本文件,则启动文本编辑器并打开该文件。
-
在C++中,所谓多态性是指:由继承而产生的相关的不同的类,其对象对同一消息会作出不同的响应。多态性是面向对象程序设计的一个重要特征,能增加程序的灵活性。
2.7 面向过程及面向对象的公式表述
-
面向过程的结构化程序设计:程序 = 算法 + 数据结构
算法和数据结构两者是相互独立、分开设计的,面向过程的程序设计是以算法为主体的。
-
面向对象程序设计:对象 = 算法 + 数据结构,程序 = (对象 + 对象 + ...)+消息。或表示为 程序 = 对象s + 消息。
-
对象s表示多个对象,消息的作用就是对对象的控制。程序设计的关键是设计好每一个对象,以及确定向这些对象发出的命令,使各对象完成相应的操作。
2.8 类和对象的关系
-
在C++中对象的类型称为类(class),类代表了某一批对象的共性和特征。
-
类是对象的抽象,而对象是类的具体实例。
-
类是抽象的,不占用内存,而对象是具体的,占用存储空间。
-
如果在类的定义中既不指定private,也不指定public,则系统就默认为是私有的(private)。
2.9 类的成员函数
-
一般的做法是将需要被外界调用的成员函数指定为public,它们是类的对外接口。
-
在类外定义成员函数:
Class Student { public: void display();//公用成员函数原型声明 private: int num; string name; char sex;//以上3行是私有数据成员 }; void Student::display() { cout << "num:" << num << endl; cout << "name:" << name << endl; cout << "sex:" << sex << endl; }//类外定义成员函数 Student stud1, stud2; //定义两个类对象
注意:在类体中直接定义函数时,不需要在函数名前面加上类名,因为函数属于哪一个类是不言而喻的。但成员函数在类外定义时,必须在函数名前面加上类名,予以限定。
Q:"::"是什么符号,起到什么作用?
A:"::"是作用域限定符或称作用域运算符,用它声明函数是属于哪个类的。Student::display() 表示Student类的作用域中的 display 函数,也就是Student类中的display函数。用作用域限定符加以限定,就明确地指明了是哪一个作用域中的函数,也就是哪一个类的函数。
-
类函数必须先在类体中作原型声明,然后在类外定义,也就是说类体的位置应在函数定义之前,否则编译时会出错。
2.10 内置函数(inline)
-
Q:什么是内置函数(inline)?
A:调用函数时需要一定的时间,如果有些函数需要频繁使用,则累计所用时间会很长,从而降低程序的执行效率。C++提供一种提高效率的方法,即在编译时将所调用函数的代码嵌入到主调函数中。这种嵌入到主调函数中的函数称为内置函数(inline function),又称内嵌函数。有些书也把它译为内联函数。
-
当编译器发现某段代码在调用一个内联函数时,它不是去调用该函数,而是将该函数的代码,整段插入到当前位置。这样做的好处是省去了调用的过程,加快程序运行速度,因为调用一个函数的时间开销远远大于小规模函数体中全部语句的执行时间。
举个例子:
#include <iostream> using namespace std; inline int max(int a, int b, int c) { if (b > a) a = b; if (c > a) a = c; return a; } int main() { int i = 1, j = 2, k = 3, ans; ans = max(i, j, k); cout << ans << endl; return 0; } //运行结果 3
程序分析:在定义函数时指定它为内置函数,因此编译系统在遇到函数调用max(i, j, k)时,就用max函数体的代码代替max(i, j, k), 同时将实参代替形参。这样,m = max(i, j, k)就被置换成
{ a = i; b = j; c = k; if (b > a) a = b; if (c > a) a = c; ans = a; }
内置函数与用#define命令实现的带参宏定义有些相似,但不完全相同。宏定义是在编译前由预处理程序对其预处理的,它只作简单的字符置换而不作语法检查,往往会出现意想不到的错误。
注意:使用内置函数可以节省运行时间,但却增加了目标程序的长度。假设要调用10次max函数,则在编译时先后10次将max的代码复制并插入main函数,大大增加了main函数的长度。因此只有对于规模很小且使用频繁的函数指定为内置函数,才可大大提高运行速度。
-
在类体中定义的成员函数中不包括循环等控制结构,C++系统会自动地对它们作为内置函数来处理。但如果成员函数不在类体内定义,而在类体外定义,系统并不把它默认为内置函数,调用这些成员函数的过程和调用一般函数的过程是相同的。
-
成员函数的存储:C++中,每个对象所占用的存储空间只是该对象的数据成员所占用的存储空间,而不包括函数代码所占用的存储空间。不论成员函数在类内定义还是在类外定义,成员函数的代码段的存储方式是相同的,都不占用对象的存储空间。(不要误以为在类内定义的成员函数的代码段占用对象的存储空间,而在类外定义的成员函数的代码段不占用对象的存储空间。)
-
inline函数只影响程序的执行效率,而与成员函数是否占用对象的存储空间无关。
-
虽然成员函数并没有放在对象的存储空间中,但从逻辑的角度,成员函数是和数据一起封装在一个对象中的,只允许本对象中成员的函数访问同一对象中的私有数据。
第 3 章 类和对象的使用
3.1 构造函数
-
Q:构造函数是什么?起什么作用?
A:在C++程序中,对象的初始化是一个不可缺少的问题。为了解决这个问题,C++提供了构造函数来处理对象的初始化。构造函数是一种特殊的成员函数,与其他成员函数不同,不需要用户来调用它,而是在建立对象时自动执行。
注意:构造函数的名字必须与类名同名,而不能任意命名,以便编译系统能识别它,并把它作为构造函数处理。它不具有任何类型,不返回任何值。
-
有关构造函数的使用说明
(1)
Q:什么时候调用构造函数呢?
A:在建立类对象时会自动调用构造函数。在建立对象时系统为该对象分配存储单元,此时执行构造函数,就把指定的初值送到有关数据成员的存储单元中。每建立一个对象,就调用一次构造函数。
(2)构造函数没有返回值,它的作用只是对对象进行初始化。因此也不需要在定义构造函数时声明类型,这是它和一般函数的一个重要的不同之点。
(3)构造函数不需用户调用,也不能被用户调用。
例如,下面的用法是错误的:
class Time { public: Time() { hour = 0; minute = 0; sec = 0; } void set_time(); private: int hour; int minute; int sec; } void Time::set_time() { cin >> hour; cin >> minute; cin >> sec; } int main() { Time t1; t1.Time();//企图用调用一般成员函数的方法来调用构造函数 }
(4)可以用一个类对象初始化另一个类对象,
Time t1;//建立对象t1,同时调用构造函数t1.Time() Time t2 = t1;//建立对象t2,并用一个t1初始化t2
此时,把对象t1的各数据成员的值copy到t2相应各成员,而不调用构造函数t2.Time()。
(5)在构造函数的函数体中不仅可以对数据成员赋初值,而且可以包含其他语句,例如cout语句。但是不提倡加入与初始化无关的内容,以保持程序的清晰。
(6)如果用户自己没有定义构造函数,则C++系统会自动生成一个构造函数,只是这个构造函数函数体是空的,也没有参数,不执行初始化操作。
3.2 析构函数
-
当对象的生命期结束时,会自动执行析构函数。
-
析构函数的作用并不是删除对象,而是在撤销对象占用的内存之前完成一些清理工作,使这部分内存可以被程序分配给新对象使用。
-
析构函数不返回任何值,没有函数类型,也没有函数参数。
注意:由于没有函数参数,因此它能被重载;一个类中可以有多个构造函数,但是只能有一个析构函数。
3.3 调用构造函数和析构函数的顺序
-
最先被调用的构造函数,其对应的(同一对象中的)析构函数最后被调用,而最后被调用的构造函数,其对应的析构函数最先被调用。先构造的后析构,后构造的先析构。它相当于一个栈,先进后出。
因为对象是定义在函数中的,函数调用会建立栈帧,栈帧中的对象构造和析构也要符合先进后出的原则
3.4 对象指针
-
指向对象的指针:
在建立对象时,编译系统会为每一个对象分配一定的存储空间,以存放其数据成员的值。一个对象存储空间的起始地址就是对象的指针。可以定义一个指针变量,用来存放对象的地址,这就是指向对象的指针变量。 定义指向类对象的指针变量的一般形式为:类名 * 对象指针名;
class Time { ... } Time *pt;//定义pt为指向Time类对象的指针变量 Time t1;//定义t1为Time类对象 pt = &t1;//将t1的起始地址赋给pt
-
this 指针
如果对同一个类定义了n个对象,则有n组同样大小的空间以存放n个对象中的数据成员。但是,不同的对象都调用同一个函数的目标代码。
Q:那么,当不同对象的成员函数引用数据成员时,怎么能保证引用的是所指定的对象的成员呢?
A:在每一个成员函数中都包含一个特殊的指针,这个指针的名字是固定的,称为this。它是指向本类对象的指针,它的值是当前被调用的成员函数所在的对象的起始地址。当调用成员函数a.fun()时,编译系统就把a的起始地址赋给this指针。
-
const型数据小结
设类名为Time
形式 含义 Time const t1; t1是常对象,其值不能改变 void Time::fun()const; fun是Time类中的常成员函数,可以引用,但不能修改本类中的数据成员 Time *const p; p是指向Time类对象的常指针变量,p的值不能改变 const Time *p; p是指向Time类常对象的指针变量,p指向的类对象的值不能通过p来改变 const Time&t1 = t; t1是Time类对象t的引用,二者指向同一存储空间,t的值不能改变。
3.5 静态成员
-
Q:全局变量的优缺点?
A:全局变量可以实现数据共享,如果在一个程序文件中有多个函数,在每一个函数中都可以改变全局变量的值,全局变量的值为各函数共享。但是用全局变量的安全性得不到保证,由于在各处都可以自由地修改全局变量的值,很有可能偶一失误,全局变量的值就被修改,导致程序的失败。因此在实际工作中很少使用全局变量。
-
如果希望各对象中的数据成员的值是一样的,就可以把它定义为静态数据成员,这样它就为各对象所共有,而不只属于某个对象的成员,所有对象都可以引用它。静态的数据成员在内存中只占一份空间(而不是每个对象都分别为它保留一份空间)。
注意:①静态数据成员是在所有对象之外单独开辟空间,即使不定义对象,也为静态数据成员分配空间。
②一般数据成员是在对象建立时分配看空间,在对象撤销时释放,但是静态数据成员是在程序编译时被分配空间的,到程序结束时才释放空间。
③静态数据成员可以初始化,但只能在类体外进行初始化。如
int Box::height = 10; // 表示对Box类中的数据成员初始化
其一般形式为 数据类型 类名::静态数据成员名 = 初值;
只在类体中声明静态数据成员时加static,不必在初始化语句中加static。
tip:不能用参数初始化表对静态数据成员初始化。
④静态数据成员可以通过对象名引用,也可以通过类名引用。
⑤有了静态数据成员,各对象之间的数据有了沟通的渠道,实现数据共享,因此可以不使用全局变量。全局变量破化了封装的原则,不符合面向对象程序的要求。
-
成员函数也可以定为静态的,在类中声明函数的前面加static就成了静态成员函数,如
static int volume();
和静态数据成员一样,静态成员函数是类的一部分而不是对象的一部分。如果要在类外调用公用的静态成员函数,要用类名和域运算符"::"。如:Box::volume();
注意:静态成员函数与非静态成员函数的根本区别是:非静态成员函数有this指针,而静态成员函数没有this指针。由此决定了静态成员函数不能访问本类中的非静态成员。
Q:为啥静态成员函数没有this指针,且不能访问本类中的非静态成员?
A:当调用一个对象的成员函数(非静态)时,系统会把该对象的起始地赋给成员函数的this指针。而静态成员函数并不属于某一对象,它与任何对象都无关,因此静态成员函数没有this指针。既然它没有指向某一对象,就无法对一个对象中的非静态成员进行默认访问(即在引用数据成员时不指定对象名)。
注意:并不是绝对不能引用本类中的非静态成员,只是不能进行默认访问,因为无法知道应该去找哪个对象。如果一定要引用本类中的非静态成员,应该加对象名和成员运算符“.”。
-
静态成员函数可以直接引用本类中的静态成员,因为静态成员同样是属于类的,可以是直接引用。在C++程序中,静态成员函数主要用来访问静态数据成员,而不访问非静态成员。
-
Q:说说全局变量与静态变量,内部外部访问的差别
A:
3.6 友元
-
友元(friend)的意思是朋友,或者说是好友,在C++中,这种关系以friend声明。友元可以访问与其有好友关系的类中的私有成员。友元包括友元函数和友元类
-
友元成员函数:
-
friend函数不仅可以是一般函数(非成员函数),而且可以是另一个类中的成员函数。
-
一个函数可以被多个类声明为“朋友”,这样就可以引用多个类中的私有数据。
-
-
友元类:声明友元类的一般形式为
friend 类名;
注意: ①友元的关系是单向的而不是双向的。如果声明了类B是类A的友元类,不等于类A是类B的友元类,类A中的成员函数不能访问类B中的私有数据。
②友元的关系不能传递,如果类B是类A的友元类,类C是类B的友元类,不等于类C是类A的友元类。
-
Q:能否分析一下用友元的利弊?
A:面向对象程序设计的一个基本原则是封装性和信息隐蔽,而友元却可以访问其它类中的私有成员,不能不说这是对封装原则的一个小小的破坏。但是它能有助于数据共享,能提高程序的效率,在使用友元始,要注意到它的副作用,不要过多地使用友元,只有在使用它能使程序精炼,较大地提高程序效率时才用友元。也就是说,要在数据共享与信息隐蔽之间选择一个恰当的平衡点。
3.7 类模板
-
声明类模板:
template<class 类型参数名>
template意思是模板,是声明类模板时必须写的关键字。 -
类模板是类的抽象,类是类模板地实例。
第 4 章 继承与派生
4.1 继承与派生的概念
-
面向对象技术强调软件的可重用性。C++语言提供了类的继承机制,解决了软件重用问题。
-
在C++中可重用性是通过“继承”这一机制来实现的。因此,继承是C++的一个重要组成部分。所谓继承就是在一个已存在的类的基础上建立一个新的类。已存在的类称为基类或父类,新建立的类称为派生类或子类。
一个新类从已有的类那里获得其已有特性,这种现象称为类的继承。
-
一个派生类只从一个基类派生,这称为单继承,这种继承关系所形成的层次是一个树形结构。
-
一个派生类有两个或多个基类的称为多重继承。
-
关于基类和派生类的关系,可以表述为:派生类是基类的具体化,而基类则是派生类的抽象。例如:小学生、中学生、大学生、研究生、留学生是学生的具体化,他们是在学生的共性基础上加上某些特点形成的子类。
-
声明派生类的一般形式为:
class 派生类名:[继承方式] 基类名
{
派生类新增加的成员
};
4.2 继承方式
-
继承方式包括:public(公用的),private(私有的),protected(受保护的),默认为私有的。
-
公用继承(public):基类的公有成员和保护成员在派生类中保持原有访问属性,其私有成员仍为基类私有。(基类的公用成员和保护成员在派生类中仍然保持其公用成员和保护成员的属性,而基类的私有成员在派生类中并没有成为派生类的私有成员,它仍然是基类的私有成员,只有基类的成员函数可以引用它,而不能被派生类的成员函数引用,因此就成为派生类中的不可访问的成员。
私有继承(private):基类的公有成员和保护成员在派生类中成了派生类的私有成员。其私有成员仍为基类私有(派生类不可访问)。
保护继承(protected):基类的公有成员和保护成员在派生类中成了保护成员,其私有成员仍为基类私有。保护成员可以被派生类的成员函数引用(即有血缘关系就能引用。)
-
类A为基类,类B是类A的派生类,类C是类B的派生类,则类C也是类A的派生类。类B称为类A的直接派生类,类C称为类A的间接派生类。类A是类B的直接基类,是类C的间接基类。
4.3 派生类的构造函数和析构函数
-
基类的构造函数是不能继承的,在声明派生类时,派生类并没有把基类的构造函数继承过来,因此,对继承过来的基类成员初始化的工作也要由派生类的构造函数承担。所以在设计派生类的构造函数时,不仅要考虑派生类所增加的数据成员的初始化,还应当考虑基类的数据成员初始化。解决这个问题的思路是:在执行派生类的构造函数时,调用基类的构造函数。
-
派生类构造函数一般形式为:
派生类构造函数名(总参数表):基类构造函数名(参数表){派生类中新增数据成员初始化语句}
-
析构函数调用的顺序与构造函数正好相反:先执行派生类自己的析构函数,对派生类新增加的成员进行清理,然后调用子对象的析构函数,对子对象进行清理,最后调用基类的析构函数,对基类进行清理。
-
多重继承派生类的构造函数:
派生类构造函数名(总参数表):基类1构造函数(参数表),基类2构造函数(参数表),基类3构造函数(参数表列){派生类中新增数据成员初始化语句}
4.4 虚基类
-
虚基类要解决的问题:如果一个派生类有多个直接基类,而这些直接基类又有一个共同的基类,则在最终的派生类中会保留该间接共同基类数据成员的多份同名成员。在引用这些同名的成员时,必须在派生类对象名后增加直接基类名,以避免产生二义性,使其唯一地标识一个成员,如c1.A::display()。
在一个类中保留间接共同基类的多份同名成员,虽然有时是有必要的,但是在大多数情况下,因为保留多份数据成员的拷贝,不仅占用较多的存储空间,还增加了访问这些成员时的困难,容易出错。
所以C++提供虚基类的方法,使得在继承间接共同基类时只保留一份成员。
-
声明虚基类的一般形式为
class派生类名:virtual继承方式 基类名
经过这样的声明后,当基类通过多条派生路径被一个派生类继承时,该派生类只继承该基类一次,也就是说,基类成员只保留一次。
第 5 章 多态性与虚函数
5.1 什么是多态性
-
多态性是面向对象程序设计的一个重要特征。在面向对象方法中一般是这样表述多态性的:向不同的对象发送同一个消息,不同的对象在接收时会产生不同的行为(即方法)。也就是说,每个对象可以用自己的方式去响应共同的消息。所谓消息,就是调用函数,不同的行为就是指不同的实现,即执行不同的函数。
-
在C++中,多态性表现形式之一是:具有不同功能的函数可以用同一个函数名,这样就可以用一个函数名调用不同内容的函数。可以说,多态性是“一个接口,多个方法”。
-
多态性分为静态多态性和动态多态性。
静态多态性是通过函数的重载实现的。由函数重载和运算符重载形成的多态性属于静态多态性,要求在程序编译时就知道调用函数的全部信息,因此,在程序编译时系统就能决定要调用的是哪个函数。
优缺点:函数调用速度快、效率高,但缺乏灵活性,在程序运行前就应决定执行的函数和方法。
动态多态性的特点是:不在编译时确定调用的是哪个函数,而是在程序运行过程中才动态地确定操作所针对的对象。动态多态性是通过虚函数实现的。
5.2 虚函数
-
虚函数的作用:
C++中的虚函数就是用来解决动态多态问题的。所谓虚函数,就是在基类声明函数是虚拟的,并不是实际存在的函数,然后在派生类中才正式定义此函数。在程序运行期间,用指针指向某一派生类对象,这样就能调用指针指向的派生类对象中的函数,而不会调用其他派生类中的函数。
虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过对基类指针或引用来访问基类和派生类中的同名函数。
-
由虚函数实现的动态多态性就是:同一类族中不同类的对象,对同一函数调用作出不同的响应。
-
Q:由虚函数实现的动态多态与重载实现的多态不同点?
A:函数重载处理的是同一层次上的同名函数问题,而虚函数处理的是不同派生层次上的同名函数问题,前者是横向重载,后者可以理解为纵向重载,但与重载不同的是:同一类族的虚函数的首部是相同的,而函数重载时函数的首部是不同的(参数个数或类型不同)。
-
使用虚函数时,有两点要注意:
①只能用virtual声明类的成员函数,把它作为虚函数,而不能将类外的普通函数声明为虚函数.因为虚函数的作用是允许在派生类中对基类的虚函数重新定义.
②一个成员函数被声明为虚函数后,在同一类族中的类就不能再定义一个非virtual的但与该虚函数具有相同的参数(包括个数和类型)和函数返回值类型的同名函数.
-
Q:根据什么考虑是否把一个成员函数声明为虚函数呢? 主要考虑以下几点:
A:①首先看成员函数所在的类是否会作为基类.然后看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能的,一般应该将它声明为虚函数。
②如果成员函数在类被继承后功能不需修改,或派生类用不到该函数,则不要把它声明为虚函数。
③应考虑对成员函数的调用是通过对象名还是通过基类指针或引用去访问,如果是通过基类指针或引用去访问的,则应当声明为虚函数。
④有时,在定义虚函数时,并不定义其函数体,即函数体是空的。它的作用只是定义了一个虚函数名,具体功能留给派生类去添加。
-
需要说明的是,使用虚函数,系统要有一定的空间开销.当一个类带有虚函数时,编译系统会为该类构造一个虚函数表,它是一个指针数组,存放每个虚函数的入口地址。
5.3 虚析构函数
-
析构函数可以是虚函数。当子类继承父类,父类指针指向子类时
如果父类的析构函数不加virtual关键字 ,只调动父类的析构函数,而不调动子类的析构函数
如果父类的析构函数加virtual关键字 ,先调动子类的析构函数,再调动父类的析构函数(即如果将基类的析构函数声明为虚函数,由该基类所派生的所有派生类的析构函数也都自动成为虚函数。
-
在程序中最好把基类的析构函数声明为虚函数。这将使所有派生类的析构函数自动成为虚函数。
5.4 纯虚函数与抽象类
-
纯虚函数只有函数的名字而不具备函数的功能,不能被调用。它只是通知编译系统:“在这里声明一个虚函数,留待派生类中定义”。在派生类中对此函数提供定义后,它才能具备函数的功能,可以被调用。
纯虚函数的作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要对它进行定义。
注意:①纯虚函数没有函数体;②声明纯虚函数的一般形式是
virtual 函数类型 函数名(参数列表)=0;
,最后面的“=0”并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是纯虚函数”;③这是一个声明语句,最后应有分号。 -
不用来定义对象而只作为一种基本类型用作继承的类,称为抽象类,由于它常用作基类,通常称为抽象基类。 凡是包含纯虚函数的类都是抽象类。因为纯虚函数是不能被调用的,包含纯虚函数的类是无法建立对象的。抽象类的作用是作为一个类族的共同基类,或者说,为一个类族提供一个公共接口。
第 6 章 面试问题汇总
------------------------------------------------------持续更新中-----------------------------------------------------------