听着这首歌我突然有种学习的欲望!还好我自制力强,把这股欲望给压下去了。

第1-7章-基本语法知识

1. 布尔型:bool,取值为:true / false

编译系统处理逻辑型数据时,将false处理为0,true处理为1

int a=1; bool flag=true; a=a+flag+true; a=3

2. 内联函数

内联函数只能先定义后使用,否则编译系统也会把它认为是普通函数。

例:

1
2
3
4
5
inline int max(int ,int int);//声明内置函数

inline int max(int a, int b, int c){//定义内置函数

}

3. 函数的重载

参数个数参数类型参数顺序三者中必须至少有一种不同

4. 函数模板

用于函数体相同,参数个数相同而类型不同的情况。

定义模板的格式:

template <typename/class T>

通用函数定义

T为虚拟类型名,表示T是一个类型名,但暂未指定。等函数调用时,根据实参的类型来确定T的类型。T的名字可以自定义。

注意:C++要求函数/类模板的声明和实现必须放在一个文件里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
4.10,将例4.8程序(求3个数的最大值)改为通过模板函数实现。
template <typename T> //模板声明,其中T为类型参数
T max(T a, T b, T c){ // T为虚拟的类型名
if(b>a) a=b;
if(c>a) a=c;
return a;
}
int main(){
int i1=13, i2=34, i3=52, i;
double d1=11.2, d2=45.34, d3=32.55, d;
i=max( i1, i2, i3); //调用模板函数,此时T被int替代
d=max( d1, d2, d3); //同上,double替代T
cout<<“i_max=“<<i<<endl;
cout<<“d_max=“<<d<<endl;
}

5. 有默认参数的函数

C++中,对于多次调用同一函数时用同样的实参的情况,可以给形参一个默认值,这样形参就不必一定要从实参取值了。例:

1
2
3
float area(float r=6.5);	//指定r的默认值为6.5
//函数调用时实参值为6.5,则可以不必给出实参的值
area( ); //相当于area(6.5);

如果不想使形参取此默认值,则通过实参另行给出。例如:

1
area(7.5); 	//形参得到的值为7.5,而不是6.5

多个参数时:

1
2
3
4
5
	 //只对形参r指定默认值12.5
float volume(float h,float r=12.5);
函数调用可以采用以下形式:
volume(45.6); //相当于volume(45.6,12.5)
volume(34.2,10.4); //h的值为34.2,r的值为10.4

指定默认值的参数必须放在形参表列中的最右端。例如:

1
2
3
4
5
6
	void f1(float a,int b=0int c,char d=′a′);     //不正确
void f2(float a,int c,int b=0, char d=′a′); //正确
如果调用上面的f2函数,可以采取下面的形式:
f2(3.5 , 5 , 3 , ′x′) //形参的值全部从实参得到
f2(3.5 , 5, 3) //最后一个形参的值取默认值′a′
f2(3.5 , 5) //最后两个形参的值取默认值,b=0,d=′a′
  • 如果函数的定义在函数调用之前,则应在函数定义中给出默认值。如果函数的定义在函数调用之后,则在函数调用之前需要有函数声明,此时必须在函数声明中给出默认值,在函数定义时可以不给出默认值。

  • 一个函数不能既作为重载函数,又作为有默认参数的函数。因为当调用函数时如果少写一个参数,系统无法判定是利用重载函数还是利用默认参数的函数,出现二义性,系统无法执行。

6. 字符串类与字符串变量

C++新的数据类型:字符串类型(string类型)

必须包含头文件:#include <string>

1
2
string string1;   //定义string1为字符串变量
string string2=″China″; //定义string2同时对其初始化

对字符串变量的赋值

1
2
string1=″Canada″;
string2=string1; //将”Canada”赋值给string2
  • 在定义字符串变量时不需指定长度,长度随其中的字符串长度而改变。

对字符串变量中某一字符进行操作:下标法

1
2
string word=″Then″; //定义并初始化字符串变量	
word2]=′a′; //修改后word的值为″Than″

字符串变量的输入输出:在输入输出语句中用字符串变量名,输入输出字符串

1
2
3
4
//从键盘输入一个字符串给字符串变量string1
cin>> string1;
//将字符串string2输出
cout<< string2;

字符串间相关操作

1
2
3
4
5
6
7
(1) 字符串复制用赋值号:string1=string2;
(2) 字符串连接用加号
string string1=″C++″;
string string2=″& C″;
string1=string1 + string2; //连接后string1为″C++ &C″。
(3) 字符串比较直接用关系运算符
可以直接用 ==(等于)、>(大于)、<(小于)、!=(不等于)、>=(大于或等于)、<=(小于或等于)等关系运算符来进行字符串的比较

定义字符串数组

1
2
string name[5]; 
string name[5]={″Zhang″,″Li″,″Fun″,″Wang″,″Tan″};
  • 在字符串数组的每一个元素中存放一个字符串,而不是一个字符,这是字符串数组与字符数组的区别。
  • 每一个字符串元素中只包含字符串本身的字符而不包括′\0′( ′\0 ′是字符数组作为结束标记用的,字符串没有这个需要)。

7. const指针

  • 指向常量的指针变量

定义形式: const typename * varname

它是一个指针变量

不允许通过指针变量改变它指向的对象的值,若直接改变变量的值是可以的

指针变量的值(即指向)是可以改变

若要使变量的值始终不变,需将变量定义成为const变量

1
2
3
4
5
6
例如:
int a=12; const int b=15;
const int *pa=&a, *pb=&b;
*pa=15; //错误 ,不能通过指针修改a的值
a=15; //正确,a为变量,可以修改其值
*pb=20; b=20//错误,b为const变量,皆不可修改
  • 常指针

定义形式:typename *const varname

类型为指针

指针变量的指向不能改变但是其指向的变量的值可改变

必须在定义时初始化

1
2
3
4
5
例如:
int a=4 , b=5;
int *const p=&a ; //定义常量指针,指向变量a
*p=5; //正确,p指向a,变量a的值改为5
p=&b; //错误,p为常量指针,不能改变其指向
  • 指向常量的常指针

定义形式:const typename *const varname

是一个指针

不能改变其指向,也不能通过指针运算符改变指针所指向的变量的值。但可以直接改变变量的值

1
2
3
4
5
6
例如:
int a=4 , b=5;
const int *const pt=&a ; //定义常量指针,指向变量a
*pt=5; //错误,不能通过指针运算符改变变量的值
pt=&b; //错误,p为常量指针,不能改变其指向
a=5; //正确,可以直接改变变量的值。若是定义为const a=4,则a=5也是错误的。

8. void指针类型

格式:void *pt,不指向确定类型的指针。

void 类型的指针变量可以存储任何类型的指针,但是不能判断出指向对象的长度。

1
int  *p=&a; void *vp=p;	//合法,但是只获得变量的地址,而没有获得长度

void指针赋值给其他类型的指针时都要进行类型转换

1
type *vp = (type*)vp; 	//转换类型也就是获得指向变量/对象大小

void指针不能参与指针运算,除非进行转换

1
(type*)vp++; 	//vp=vp+sizeof(type)

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
void main(){
int x=100;
int *q=NULL;
void *p=&x;
//cout<<"*p="<<*p<<endl;//错误,非法使用指针p
cout<<"*p="<<*(char*)p<<endl;//正确,输出指针p所指向单元的内容
cout<<"*p="<<*(int*)p<<endl;
cout<<"*p="<<*(float*)p<<endl;
cout<<"*p="<<*(double*)p<<endl;
//q=p;//错误,非法赋值,将void指针赋值给整型指针
q=(int*)p;//正确,赋值时进行强制类型转换为int*型
cout<<"*q="<<*q<<endl;
}

9. 引用

格式: 类型 &引用名= 变量名 。&为引用声明符

1
2
char &d=c;	//&是引用的声明符
int *pt=&a; //&是取地址运算符
  • 引用的类型必须和其所绑定的变量的类型相同
1
2
3
4
5
6
7
#include<iostream>
using namespace std;
int main(){
double a=10.3;
int &b=a; //错误,引用的类型必须和其所绑定的变量的类型相同
cout<<b<<endl;
}
  • 声明一个引用时,必须同时初始化
1
2
3
4
5
6
#include<iostream>
using namespace std;
int main(){
int &a; //错误!声明引用的同时必须对其初始化
return 0;
}
  • 可以取引用的地址,实质上就是取引用的变量的地址

C++:引用的简单理解

10. 结构体类型

C++中结构体和类的区别

  • 结构体和类具有不同的默认访问属性。类中,对于未指定访问属性的成员,其访问属性为private,结构体中为public。

  • struct默认为public继承,class默认为private继承

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <cstdlib> 
#include <iostream>  //定义结构体  
using namespace std; 

struct point 
{  
//包含两个变量成员 
int x;  
int y; 
};  

int main(int argc, char *argv[]) 
{  
struct point pt; 
pt.x=1; 
pt.y=2; 
cout<<pt.x<<endl<<pt.y<<endl;
return EXIT_SUCCESS;
}

11. new和delete运算符

new运算符:动态分配存储空间。

格式:new 类型 [初值]

1
2
3
4
new int;	//开辟了一个存放整数的存储空间,返回其指针
new int(100); //指定该整数的初值为100,并返回指针
char *pt=new char[10]; //开辟一个存放字符数组(10个元素)的空间,返回字符数组首地址。
float *p=new float(3.1415); //开辟一个存放float的控件,其初值为3.1415,并返回其地址给指针变量p。

delete运算符:撤销上面new开辟的存储空间

格式:delete 指针变量/delete [] 指针变量

1
2
delete p;	//删除p指向的存储空间
delete [] pt; //删除pt指向的动态数组

实验五ex1中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;
//定义Array类
class Array{
private: //指针指向new空间,len为初始化数组长度
int *a;
int len;
public:
Array(int b[],int length){ //构造函数
int i;
len=length; //初始化数组长度
a=new int[length]; //动态创建一个与实参长度一样的数组
for(i=0;i<length;i++){ //给新创建的数组赋值
a[i]=b[i];
}
}
~Array(){ //析构函数,删除动态创建的数组
delete [] a;
}

12. 枚举类型

格式:enum 枚举类型名 [枚举常量表]

1
2
3
4
//声明一个枚举类型,{}中的为枚举元素/枚举常量
enum weekday{sun,mon,tue,thu,fri,sat};
weekday workday;
workday=mon; //枚举变量的值只能是枚举常量之一

枚举元素为常量,其值是一个整数,编译系统按照定义时的顺序将他们赋值为0,1,2……,所以枚举值可以按照整数比较规则进行判断比较。

不能将一个整数直接赋给枚举变量,枚举变量只能接受枚举类型数据。

1
2
workday=tue;  //正确,将枚举常量赋值给枚举变量
workday=2; //错误,它们是不同的类型。

文档——指针和引用

指针变量

指针是一种类型,此类型变量中存放的是另一个变量/对象的地址。

指针变量是一个实体,即在内存中实实在在的存在。指针变量本身具备变量的4个属性:变量名、变量值、变量地址、变量类型。它的值就是另一个变量的地址。它的类型就是指针类型

通过基类型确定指向变量/对象所占据的内存大小,所以void类型的指针为空指针,表示此变量仅仅存放一个地址,没有指向对象所占内存的信息,所以void指针是无法通过指针运算符来取值的。

引用

而引用是一个别名,它在逻辑上不是独立的,它的存在具有依附性,所以引用必须在一开始就被初始化,而且其引用的对象在其整个生命周期中是不能被改变的(自始至终只能依附于同一个变量)。由于它不是一个专门的变量,所以不分配内存空间。

它们之间的区别(下面的指针皆为指针变量的简称)

  • 指针是一个实体,而引用仅是个别名;

  • 引用必须被初始化,指针不必;

  • 引用只能在定义时被初始化一次,之后不可变;指针可以改变所指的对象;

  • 不存在指向空值的引用,但是存在指向空值的指针,即引用不能为空,指针可以为空;

  • “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小;

  • 程序为指针变量分配内存区域,而引用不需要分配内存区域;

  • 指针可以有多级,但是引用只能是一级,例如int **p是合法的,而int &&a是不合法的;
    更多详细参阅第1-7章-指针和引用.docx

第8章-类和对象的特性

1. 面向对象程序设计方法概述

面向对象程序设计的4个主要特点“抽象”、“封装”、“继承”、“多态”

C++中对象的构成:数据和函数

  • 数据即对象的属性
  • 函数:对数据进行操作,以便实现某些功能,即行为。在程序设计中称为:方法。

2. 类的声明和对象的定义

声明中未指定访问限定符的成员,系统默认为私有。

private和public可以出现多次。有效范围为开始到另一个访问限定符或类体结束时为止。

定义对象的3种方法(类似结构体)

方法1:先声明类类型,然后再定义对象,有两种形式。

1
2
1class 类名 对象名,例如 class Student  stud1; 
2) 类名 对象名 ,例如 Student stud1,stud2;

方法2:在声明类时定义对象

1
2
3
class Student{  //声明类
……
}stud1,stud2;

方法3:不出现类名,直接定义对象

1
2
3
class {		//无类名

}stud1,stud2; //定义了两个无类名的类对象

3. 类的成员函数

成员函数在类内声明,类外定义。例如:

1
2
3
4
5
6
7
8
9
10
class Student { 
public:
void display( ); //公用成员函数的函数原型
……
}; //类定义结束

//类外定义display类函数,必须在函数名前面加上类名,予以限定。
void Student∷display( ) {//“::” 作用域限定符
……
}

定义类外函数,若“∷”前面没有类名,或函数名前面无类名无“∷”,则函数为全局函数。如∷display( )display( )

inline成员函数

如果在类体中定义的成员函数中不包括循环等控制结构,C++系统会自动将它们作为内置(inline)函数来处理。所以类内定义的成员函数一般省略inline。

如果成员函数在类体外定义,系统并不把它默认为内置(inline)函数,如果想将这些成员函数指定为内置函数,需用inline作显式声明。

1
2
3
4
5
6
7
8
例:类体外定义inline函数
class Student{
public:
inline void display( ); //声明此成员函数为内置函数
};
inline void Student∷display( ) {// 在类外定义display函数为内置函数
…… // 函数的定义与类定义必须在同一个文件内
}

如果在类体外定义inline函数,则必须将类定义和成员函数的定义都放在同一个头文件中(或者写在同一个源文件中)

上文中有[函数/类模板的声明和实现必须放在一个文件里](#4. 函数模板)

4. 对象成员的引用

访问对象中的成员的3种方法:

  • 通过对象名和成员运算符 格式:对象名.成员名
1
stud1.num=1001; //num为公用的整型数据成员
  • 通过指向对象的指针 格式:指针->成员名
1
p->hour;  //输出p指向的对象中的成员hour
  • 通过对象的引用变量 格式:别名.成员名
1
2
3
Time t1;        //定义对象t1
Time &t2=t1; //定义Time类引用变量t2
cout<<t2.hour; //输出对象t1中的成员hour

5. 几个名词解释

方法:类的成员函数在面向对象程序理论中被称为“方法”(method),“方法”是指对数据的操作。一个“方法”对应一种操作

消息:所谓“消息”,其实就是一个命令,由程序语句来实现。例如:stud.display( );就是向对象stud对象发出的一个“消息”,通知它执行其中的display“方法”。

stud.display( )语句中,stud是对象,display()是方法,语句“stud.display( );”是消息。

第9章-怎样使用类和对象

1. 利用构造函数对数据成员的初始化

  • 无参(默认)构造函数(在建立对象时不需要实参)

    1. 如果类中未自定义构造函数,系统会自动生成一个无参无函数体的默认构造函数(数据成员值随机)。注意:一旦类中有其他自定义构造函数,此函数将不会再自动生成

    2. 自定义的无参构造函数

    3. 自定义的全部参数带默认值的构造函数

注:每个类只能有一个默认构造函数,不然系统无法确定调用的是哪一个默认构造函数。

例:假设A为已定义的类,此类有默认构造函数A()

1
A test; //定义对象test,自动调用默认的构造函数A()

构造函数不需要用户调用,且不能通过函数调用的方式来调用。

1
2
3
A mya;		//A为已定义类,自动调用构造函数创建对象
mya.A(); //错误,对象不能调用构造函数
A a2(); //新建对象的错误格式,此为函数原型

构造函数除了创建对象时自动调用,还可以怎么调用?

1
2
A mya; 
mya=A(); //新建一个匿名的临时对象,调用构造函数A()完成临时对象的初始化,并将临时对象赋值给mya对象,匿名对象在语句结束后即被析构。
  • 带参数的构造函数

带参数的构造函数首部的一般格式为构造函数名(类型 1 形参1,类型2 形参2,…)

实参是在定义对象时给出的。定义对象的一般格式为 类名 对象名(实参1,实参2,…);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
using namespace std;
class Box{ //声明长方柱Box类
public:
Box(int,int,int); //声明带参数的构造函数
int volume(); //声明计算体积的函数
private:
int height;
int width;
int length;
};
//在类外定义带参数的构造函数
Box∷Box(int h,int w,int len) {
height=h;
width=w;
length=len;
}
//定义计算体积的函数
int Box∷volume() {
return( height*width*length );
}

int main() {
//建立对象box1,并指定box1长、宽、高的值
Box box1(12,25,30);
cout<<″box1:″<<box1.volume()<<endl;
//建立对象box2,并指定box2长、宽、高的值
Box box2(15,30,21);
cout<<″box2:″<<box2.volume()<<endl;
return 0;
}
  • 用参数初始化表对数据成员初始化

不在函数体内对数据成员初始化,而是在函数首部实现

格式:

1
2
3
类名::构造函数名([参数表])[:成员初始化表] {
[构造函数体]
}
1
2
3
例如例9.2中定义构造函数可以改用以下形式: 
Box∷Box(int h,int w,int len):height(h),width(w),length(len){……}
用形参h/w/len的值初始化成员数据height ……

注意:如果数据成员是数组,则应当在构造函数的函数体中用语句对其赋值,而不能在参数初始化表对其初始化。

  • 构造函数的重载

一个类中可以定义多个构造函数。这些构造函数具有相同的名字,而参数的个数或参数的类型不相同

构造函数的重载使用说明:

调用构造函数时不必给出实参的构造函数,称为默认构造函数。一个类只能有一个默认构造函数。

在一个类中可以包含多个构造函数,但建立对象时只执行其中一个构造函数,并非每个构造函数都被执行。

  • 使用默认参数的构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Box{
public:
//在声明构造函数时指定默认参数
Box(int h=10,int w=10,int len=10);
……
};
//在定义函数时可以不指定默认参数
Box∷Box(int h,int w,int len) {
height=h;
width=w;
length=len;
}
//主函数
int main( ) {
Box box1; //没有给实参
cout<<″box1:″<<box1.volume( )<<endl;
Box box2(15); //只给定一个实参
cout<<″box2:″<<box2.volume( )<<endl;
Box box3(15,30); //只给定2个实参
cout<<″box3:″<<box3.volume( )<<endl;
Box box4(15,30,20); //给定3个实参
cout<<″box4:″<<box4.volume( )<<endl;
return 0;
}

注意:

  1. 应在声明构造函数时指定默认值,而不能只在定义构造函数时指定默认值。

  2. 在声明构造函数时,形参名可以省略。例如 Box(int=10, int=10, int=10);

  3. 由于不需要实参也可以调用构造函数,因此全部参数都指定了默认值的构造函数也属于默认构造函数。但一个类只能有一个默认构造函数。

1
2
3
Box();
Box(int=10, int=10, int=10);
Box b1; //错误,不能确定调用的是哪个构造函数
  1. 类中定义了全部都是默认参数的构造函数后,不能再定义重载构造函数(默认参数函数与重载函数会产生冲突)

2. 析构函数

函数名:类名的前面加一个“~”符号

执行析构函数的几种情况:

  • 函数中定义的对象(自动局部对象),此函数结束,对象会被释放,在对象释放前自动执行析构函数。
  • static局部对象和全局对象在程序结束时,调用该对象的析构函数。
  • new运算符动态创建的对象,用delete运算符释放该对象时,先调用该对象的析构函数。

注意:

  • 析构函数不返回任何值没有函数类型,也没有函数参数。因此它不能被重载
  • 一个类可以有多个构造函数,但只能有一个析构函数。
  • 如果没有定义析构函数,C++编译系统会自动生成一个析构函数,但没有任何操作。
  • 销毁对象只删除回收此对象的成员函数、成员变量以及其他这个对象所占有的内存;而它所管理或者有依赖关系的一些资源不会自动销毁,需要在析构函数中销毁,否则会成为存在却不会被使用的资源。
1
2
3
4
例如:定义类A, 在类中定义一个指针类型的成员变量:
char * pa;
定义A的对象 A a,使用pa去new一块内存进行引用。
a.pa = new char[10];

释放a的内存的时候,系统只会自动收回指针对象pa所占的内存空间。而new出来的内存不是对象的成员,不会被回收。

3. 调用构造函数和析构函数的顺序

一般情况下,程序中调用析构函数的次序与调用构造函数的次序相反: 最先被调用的构造函数(最先建立的对象),其对应的析构函数最后调用,而最后被调用的构造函数,其对应的析构函数最先被调用。

4. 对象数组

  1. 若构造函数只有一个参数,定义数组时可以直接提供实参,实现对象的初始化。
1
Student stu[2]={60,70}; //实参传给2个数组元素的构造函数
  1. 若构造函数有多个参数,初始化方法为:在花括号中分别写出构造函数并指定实参。
1
2
3
4
5
例如:如果构造函数有3个参数,分别代表学号、年龄、成绩: 
Student Stud[2]={ //定义对象数组
Student(1001,18,87), //调用第1个元素的构造函数
Student(1002,19,76), //调用第2个元素的构造函数
};

5. 对象指针

编译系统会为对象分配存储空间存放其成员。对象存储空间的起始地址即对象的地址,可以定义一个指针变量,用来存放对象的指针,此指针变量即为对象指针。

定义形式为 :类名 * 对象指针名;

例如:定义指向对象的指针变量并通过对象指针访问对象和对象成员。

1
2
3
4
5
6
7
8
9
class Time{	    //定义Time类和定义Time对象t1;
……
}t1;
Time * pt=&t1; //定义pt为指向Time类对象的指针变量并初始化
*pt //表示pt所指向的对象,即t1。
(*pt).hour //pt所指向的对象中的hour成员,即t1.hour
pt->hour //pt所指向的对象中的hour成员,即t1.hour
(*pt).get_time()//即t1.get_time
pt->get_time() // 总结:(*pname).xx/pname->xx ,均可

  • 指向对象数据成员的指针变量

数据类型名 *指针变量名;//与普通的指针变量一样

​ 例:Time类的数据成员hour为public,在类外通过指向对象数据成员的指针变量访问(较少使用)。

1
2
3
int *p1; 	//定义指向整型数据的指针变量
p1=&t1.hour; //p1指向t1.hour
cout<<*p1<<endl; //输出t1.hour的值

  • 指向对象成员函数的指针

指针变量的类型必须与成员函数的类型相匹配:

  1. 函数参数的类型和参数个数
  2. 函数返回值的类型
  3. 所属的类

定义:数据类型名(类名∷指针变量名)(参数表列);

初始化:指针变量名=&类名::成员函数名;

例:定义指针p2指向Time类中公用成员函数get_time

1
void (Time∷*p2)( ) = &Time∷get_time;

  • this 指针

在每个成员函数中都包含一个名为this的指针变量(隐式参数)。它是指向本类对象的指针,它的值是当前被调用的成员函数所在的对象的地址。所以,*this 即本对象。

1
2
3
4
5
6
7
8
成员函数volume的定义如下: 
int Box∷volume( ) {
return (height*width*length);
}
C++把它处理为:
int Box∷volume(Box *this) {
return(this->height * this->width *…);
}

注意:this指针是隐式使用的,是编译系统自动实现的,在需要时也可以显式地使用this指针。

6. 共用数据的保护

  • 常对象:对象数据成员的值不能改变。注意,常对象必须要有初值。定义格式:

    类名 const 对象名[(实参表列)];const 类名 对象名[(实参表列)];

例:

1
Time const t1(1,3,4);  //成员数据必须要赋初值。
  • 常对象的成员数据都是const类型(定义对象时,将对象的数据成员自动设置成了const成员)。

  • 注意:常对象中的成员函数不会被自动设置为常成员函数。常对象只保证其数据成员是常数据成员。

  • 常对象只能调用其const函数。const函数可以访问该对象的数据成员,但不允许修改常对象中数据成员的值(只读)。

  • 常数据成员:对象的数据成员为const数据,在类定义阶段声明。

注意:只能通过构造函数的参数初始化表对常数据成员进行初始化

1
2
3
4
5
6
7
8
class Time{
public:
//Time(){} ,报错
Time (int a):hour(a){}
private:
const int hour;
};
//若类定义中有默认构造函数,程序会报错,因为const数据成员必须通过参数初始化表来初始化

常对象的数据成员都是const数据,那常对象的构造函数只能用参数初始化表对const数据成员进行初始化

(这句话是对的),但是,//定义类时,不确定此类是否会定义常对象,不会特意定义参数初始化表。所以定义常对象时,不用参数初始化表也可以建立常对象。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Test{
public:
Test(){}
Test(int);
//定义const函数,用以访问const数据
void show() const;
private:
int a;
};
void Test::show()const{
cout<<"a="<<a<<endl;
}
Test::Test(int sa){
a=sa; //没用参数初始化表
}
int main(){
Test t1(5);
const Test t2(6);
t1.show();
t2.show();
return 0;
}
//注意,常对象只能调用const函数,所以show函数不定义为const,程序会报错。

原来参数初始化列表参数的构造函数不是一回事😂

  • 常成员函数:只能读取数据成员,不能修改。定义格式:类型名 函数名(参数表) const
1
例如:void get_time( ) const;

const修饰符在声明函数定义函数时都要有const关键字,在调用时不必加const。

常成员函数可以引用const数据成员可以被const和非const成员函数引用,但不能修改它。

const数据成员可以被const和非const成员函数引用,但不能修改它。

注意:类中的常成员函数不能调用另一个非const成员函数,即只能调用const成员函数。

表9.1

数据成员 非const的普通成员函数 const成员函数
非const的普通数据成员 可以引用,也可以改变值 可以引用,但不可以改变值
const数据成员 可以引用,但不可以改变值 可以引用,但不可以改变值
const对象 不允许 可以引用,但不可以改变值

7. 指向对象的常指针

将指针变量声明为const型,这样指针值始终保持为其初值,不能改变。例:

1
2
3
4
5
Time t1(10,12,15), t2; //定义Time对象
//const位置在指针变量名前面,规定ptr1的值是常值
Time * const ptr1;
ptr1=&t1; //ptr1指向对象t1,此后不能再改变指向
ptr1=&t2; //错误,ptr1不能改变指向

定义形式:类名 * const 指针变量名;

也可在定义时初始化,如将上面3,4行合并为:Time * const ptr1=&t1;

注意:

  1. 指向不能变,但可以改变所指对象的值
  2. 指向变量的const指针必须在定义时初始化(P178),而指向对象的指针定义时可以不立即初始化,但是一旦赋值,后面就不能再次赋值。

8. 指向常对象的指针变量

定义形式:const 类型名 * 指针变量名;

表示不能通过此指针变量来改变此常对象值。

注意:

  1. 常对象只能用指向常对象的指针变量指向它,而不能用一般的(指向非const型变量的)指针变量去指向它(防止通过指针改变其值)。
  2. 指向常对象的指针变量可以改变其指向的对象。
  3. 指向常对象的指针变量除了可以指向常对象外,还可以指向非const对象,但此时不能通过此指针变量改变该对象的值(此非const对象可以通过对象本身改变)
  4. 指向常对象的指针最常用于函数的形参,如果函数的形参是指向非const型对象的指针,实参只能用指向非const对象的指针(在函数中需要改变参数值)。如果函数的形参是指向const型对象的指针,实参可以是指向const对象的指针,或指向非const对象的指针。对应关系见教材的表9.2。

对前面指向常变量的指针变量的进一步讨论

说明

  1. 如果一个变量已被声明为常变量,只能用指向常变量的指针变量指向它
1
2
3
4
const char c[]="boy";//定义const型的char数组
const char *p1;//定义p1为指向const型的char变量的指针变量
p1=c;//合法,p1指向常变量
char *p2=c;//不合法,p2不是指向常变量的指针变量
  1. 指向常变量的指针变量除了可以指向常变量外,还可以指向未被声明为const的。此时不能通过此指针改变该变量的值
1
2
3
4
5
char c1='a';//定义字符变量,它未被声明为const
const char *p1;//定义了一个指向常变量的指针变量p
p1=&c1;//使p指向字符变量c1
*p='b';//非法,不能通过p改变变量c1的值
c1='b';//合法,没有通过p访问c1,c1不是常变量

表9.2

形参 实参 合法否 改变指针所指向的变量的值
指向非const型变量的指针 非const变量的地址 合法 可以
指向非const型变量的指针 const变量的地址 非法 /
指向const型变量的指针 const变量的地址 合法 不可以
指向const型变量的指针 非const变量的地址 合法 不可以

9. 对象的常引用

形参为对象的引用,实参为对象名,则在调用函数进行虚实结合时,并不是为形参另外开辟一个存储空间,而是把实参对象的地址传给形参(引用名),这样引用名也指向实参对象。

如果不希望在函数中修改实参的值,可以把引用对象声明为const(常引用)。例如:

1
void fun(const Time &t);

const 引用常对象时只能访问该对象的const 函数

const型数据的小结

形式 含义
Time const t1;
const Time t1;
t1是常对象,其值在任何情况下都不能改变(类似const int a=3;)。通过对象只能调用其const方法。
const int hour; 常数据成员,只能通过类的构造函数的参数初始化表对常数据成员进行初始化
void Time∷fun( )const 常成员函数,可以引用,但不能修改本类中的数据成员,可以引用const/非const数据成员。函数中只能调用const成员函数
Time * const p; 指向对象的常指针,p的值(即p的指向)不能改变
const Time *p; 指向常对象的指针,其指向的类对象的值不能通过指针来改变。但是p可改变指向。对象属性值也可以通过类对象直接改变。
const Time &t1=t; 常引用,t1是Time类对象t的引用,二者指向同一段内存空间,t值不能改变

10. 对象的动态建立和释放

  • 用new创建对象后,将返回一个指向新对象的指针
1
2
Box *pt=new Box(12,15,18);	//在pt中存放了新建对象的起始地址
cout<<pt->height; //输出该对象的height成员
  • 动态建立数组:
1
Box *pt=new Box[n];	//n为定义个int变量

每个元素都会自动调用Box类的默认构造函数,若Box类没有默认构造函数,则创建对象失败。

若new对象失败,大多数C++编译系统返回一个0指针值。

不再需要由new建立的对象时,用delete运算符予以释放。如:

1
2
delete pt;	//释放pt指向的内存空间
delete[] pt; //释放pt指向的数组空间

11. 对象的赋值

同类对象之间可以互相赋值,即一个对象的值(数据成员)赋给另一个同类的对象(被赋值对象已创建)。

对象赋值格式:
对象名1 = 对象名2; //两个对象必须属于同一个类

注意:

  1. 对象的赋值只对其中的数据成员赋值,而不对成员函数赋值。
  2. 类的数据成员中不能包括动态分配的数据(new运算符建立的动态数据),否则在赋值时可能出现严重后果。

12. 对象的复制

用一个已有的对象复制出多个完全相同的对象(拷贝构造函数)。

定义:
类名(const 类名& 对象名);

​ 参数:本类已有对象的引用,一般会用const修饰。

​ 作用:将实参对象的成员值赋给新的对象中对应的成员,若没有定义复制构造函数,系统会自动创建一个。注意:同类对象之间可以访问对方的private变量。

调用格式:
类名 对象2(对象1);//用对象1复制出对象2。
另一种调用格式:
类名 对象1 = 对象2;//对象2已存在

  • 对象的复制和对象的赋值的区别
    对象的赋值是对一个已经存在的对象赋值,因此必须先定义被赋值的对象,才能进行赋值。例如:
1
2
Box b1;
b1=Box b2; //b1已经定义,所以属于赋值
对象的复制则是从无到有地建立一个新对象,并使它与一个已有的对象完全相同(包括对象的结构和成员的值)。
1
2
Box b1; 
Box b3=b1; //新建box3,所以属于复制
  • 普通构造函数和复制构造函数的区别
    • 形式上:复制构造函数的形参为本类引用
      • 类名(形参表列); //例如:Box(int h,int w,int len);
      • 类名(类名& 对象名); //例如:Box(const Box &b);
    • 系统根据实参的类型选择调用的构造函数。
    • 拷贝构造函数的被调情况
      1. 新建一个对象,并用另一个同类的对象对它初始化。
      2. 构造函数的参数为同类的对象(有时会是临时对象,所以需要const修饰)。
      3. 继承时,子类的构造函数中用父类对象来对子类的子对象进行初始化(详见继承)。

13. 静态数据成员

静态数据成员:属于类,多个对象共有。关键字static。例如:

1
2
3
4
5
6
7
class Box{
public:
int volume( );
private:
static int height; //把height定义为静态的数据成员
……
};

静态的数据成员在内存中只占一份空间。如果改变它的值,则在各对象中这个数据成员的值都同时改变了。

  1. 只声明了类而未定义对象,类的一般数据成员不占内存空间。

  2. 类定义中有静态数据成员,不定义对象也为静态数据成员分配空间。

  3. 静态数据成员不随对象的建立而分配空间,也不随对象的撤销而释放。静态数据成员是在程序编译时被分配空间的,到程序结束时才释放空间。

  4. 静态数据成员可以初始化,但只能在类体外进行初始化。形式为:

数据类型 类名∷静态数据成员名=初值;

1
int Box∷height=10; //对Box类中的static数据成员初始化

不必在初始化语句中加static。不能用参数初始化表对静态数据成员初始化。例如:Box类中的static数据成员height:

1
Box(int h,int w,int len):height(h){ }//错误语句,height是静态数据成员
  1. 如果未对静态数据成员赋初值,则编译系统会自动赋予初值0。
  2. 静态数据成员既可以通过对象名引用,也可以通过类名来引用。若静态数据成员被定义为私有的,必须通过public成员函数引用。

14. 静态成员函数

格式:函数的前面加static

静态成员函数是类的一部分(类函数),而不是对象的一部分。

调用方式:

  • 用类名和域运算符“∷”
1
2
static int volume( );	//定义静态成员函数
Box∷volume( ); //通过类名调用静态成员函数
  • 通过对象名调用:
1
a.volume( );	 //通过对象调用静态成员函数

*静态成员函数只能直接访问本类的静态成员。 *

要引用本类的非静态成员,需先建立本类对象,通过对象引用,如:

1
2
//假设a已定义为Box类对象
cout<<a.width<<endl; //通过本类对象a引用box类的非静态成员
  • 静态成员函数和普通成员函数的区别
    • 静态成员函数不包含指向具体对象的this指针
    • 普通成员函数包含一个指向具体对象的this指针。

15. 友元

友元包括友元函数友元类

友元函数包括全局函数和类的成员函数,一个函数可以被多个类声明为“朋友”。

  • 将函数声明为友元函数,

在类体中用friend对某个类外的函数(全局函数或其他类的成员函数)进行声明,此函数就称为本类的友元函数。友元函数可以访问这个类中的私有成员。例如:友元全局函数。

1
2
3
4
5
6
7
8
9
class Time{
public:
friend void display(Time &); //声明display函数为Time类的友元函数
private: //以下数据是私有数据成员
int hour; ……
};
void display(Time& t) { //Time类的友元函数,形参Time对象的引用
cout<<t.hour<<endl; //引用t的私有数据成员时,必须加上对象名
}
  • 友元成员函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Date;   //Date类的提前引用声明,若没有提前引用声明,编译会出错。
class Time{ //类中的display函数将访问Data中的private数据。
public:
Time(int,int,int);//声明构造函数
void display(Date &); //成员函数display是Date类的友元函数,形参是Date类对象的引用
……
};
class Date{ //声明Date类,它的private数据可以被友元函数访问
public:
Date(int,int,int);
//声明Time中的display函数为友元成员函数
friend void Time∷display(Date &);
private:
int month; //Time类中的display可以通过对象访问private数据
};
void Time::display(Date &d){ //注意,只能位于正式声明类Date后,因为函数需要用到Date类的成员
cout<<d.month.....
}
  • 友元类

可以将一个类(例如B类)声明为另一个类(例如A类)的友元类。友元类B中的所有函数都是A类的友元函数,可以访问A类中的所有成员。

声明友元类的形式为: friend 类名;

例如,在A类的定义体中用以下语句声明B类为其友元类:
friend B;

友元使用注意:

  1. 友元的关系是单向的而不是双向的。
  2. 友元的关系不能传递。
  3. 一般并不把整个类声明为友元类,而只将确实有需要的成员函数声明为友元函数

16. 类模板

有两个或多个类,其功能相同,仅仅是数据类型不同,可以声明一个通用的类模板,它可以有一个或多个虚拟的类型参数。

定义及使用:

1
2
3
4
5
6
7
//与函数模板类似
//注意,不要分号
template <class 类型参数名>
class 类名{
//定义类体,使用类型参数名来作为虚拟类型
……
}

例如p290:

1
2
3
4
5
6
7
8
9
10
11
template <class numtype>
class Compare{
public:
Compare(numtype a,numtype b){
x=a;y=b;
}
numtype max(){return(x>y)?x:y;}
numtype mmin(){return(x<y)?x:y;}
private:
numtype x,y;
};

利用类模板可以建立含多种数据类型的类。例如:

template <class numtype>
​ class Compare{ //类模板名为Compare
​ ……
​ }

  • 使用类模板

类模板名 <实际类型名> 对象名(参数表);

Compare cmp(4,7); // 错误,Compare是类模板名,不是一个具体的类,类模板中的类型numtype并不是一个实际的类型,只是一个虚拟的类型,无法用它去定义对象。

Compare <int> cmp(4,7); //正确,进行编译时,编译系统就用int取代类模板中的类型参数numtype

如果在类模板外定义成员函数,应写成类模板形式(函数模板):

1
2
3
4
5
template<class 虚拟类型参数>
函数类型 类模板名<虚拟类型参数>∷成员函数名(函数形参表列) {…}
//例如
template<class numtype>
numtype Compare<numtype>::max(){return(x>y)?x:y;}

类模板使用说明:

  1. 模板的类型参数可以有一个或多个,且每个类型前面都必须加class,例如:
1
2
template<class T1, class T2>
class someclass{…};

在定义对象时分别代入实际的类型名,如

1
someclass< int , double > obj;
  1. 模板可以有层次,一个类模板可以作为基类,派生出派生模板类。
  2. 类模板的实现部分可以写在class内,也可以写在class外,但是声明与实现必须在同一个文件内实现。最好将类模板放在头文件内,其它文件使用时通过include包含即可(与函数模板类似)。

文档——const函数的重载

在使用const函数时,注意有一种重载:

1
2
3
4
Class A {
int function ();
int function () const;
};

在类中,由于隐含的this形参的存在,const的function函数使得作为形参的this指针的类型变为指向const对象的指针,而非const版本的使得作为形参的this指针就是正常版本的指针。此处是发生重载的本质。

重载函数在调用过程中,由于const对象只能调用其const函数,所以对于const对象调用的就选取const性质的成员函数,而普通的对象调用就选取非const性质的成员函数。

习题

  • 类的成员变量不能在定义时进行初始化

  • 一个函数不能既作为重载函数,又作为有默认参数的函数

  • this指针存在的目的是,保证每个对象拥有自己的数据成员,但共享处理这些数据成员的代码在一个成员函数中经常需要调用其他函数(非本类的成员函数),而有时需要把当前对象(即对象的地址)作为参数传递给被调用函数,这时必须使用this指针。

  • 静态成员函数对类的数据成员访问,只允许是静态数据成员

  • 模板函数可以用同名的另一个模板函数重载。

  • 拷贝构造函数的参数是,某个对象的引用名

  • 析构函数,无形参,也不可重载

  • 已知类A中的一个成员函数的说明如下:void Set(A &a);则该函数的参数“A &a”的含义是,类A对象引用a用作函数的形参

  • 假定AB为一个类,则执行AB x;语句时将自动调用该类的,无参构造函数

  • 定AB为一个类,则执行 “AB a(2), b[3], *p[4];”语句时共调用该类构造函数的次数为,4

    ——a(2)调用1次带参数的构造函数,b[3]调用3次无参数的构造函数,指针没有给它分配空间,没有调用构造函数,所以共调用构造函数的次数为4。

  • 类A的成员函数void get()在类外定义时的函数首部为void A::get()

1
2
假定AA是一个类,“AA* abc()const;”是该类中一个成员函数的原型,若该函数返回this值,当用x.abc()调用该成员函数后,x的值( 不变
假定AA是一个类,“AA& abc();”是该类中一个成员函数的原型,若该函数存在对*this赋值的语句,当用x.abc()调用该成员函数后,x的值()已经被改变

第10章-运算符重载

1. 运算符重载的方法

重载运算符的函数格式:

1
2
3
函数类型 operator 运算符名称 (形参表列){
……
}

通过函数实现复数相加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Complex{ //复数类
public:
Complex(){}
Complex(double r,double i):real(r),imag(i){}
Complex complex_add(Complex &c2); //声明相加函数
void display();
private:
double real; //实部
double imag; //虚部
};
void Complex::display(){ //定义输出函数
cout<<“(”<<real<<“,”<<imag<<“i)”<<endl;
}
Complex Complex::complex_add(Complex &c2) //定义复数相加函数
{
Complex c;
c.real=real+c2.real;
c.imag=imag+c2.imag;
return c;
}

通过运算符重载实现复数相加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Complex{	//复数类
Complex operator +(Complex &c2); //运算符重载函数
};
Complex Complex∷operator +(Complex &c2) {
Complex c; //定义运算后返回的复数
c.real=real+c2.real; //本对象的real与c2的real相加
c.imag=imag+c2.imag;
return c;
}
int main( ) {
Complex c1(3,4), c2(5,-10), c3;
c3=c1+c2; //运算符+用于复数运算
cout<<″c1+c2=″;
c3.display( );
}

函数实现与运算符重载都可以实现相同的功能

运算符被重载后,其原有功能仍然保留,编译器会根据表达式中的数据类型决定运算符的使用。

2. 重载运算符的规则

  1. C++不允许用户自己定义新的运算符,只能对已有的运算符进行重载。
  2. C++不允许重载的运算符
    • . (成员访问运算符)
    • * (成员指针访问运算符)
    • (域运算符)
    • sizeof (长度运算符)
    • ?: (条件运算符)
  3. 重载不能改变运算符运算对象(即操作数)的个数
  4. 重载不能改变运算符的优先级和结合性
  5. 重载运算符的函数不能有默认的参数
  6. 重载的运算符必须和用户自定义类型的对象一起使用,其参数至少有一个是类对象(或类对象的引用)。即参数不能全部是C++的标准类型。
  7. 用于类对象的运算符一般必须重载,但是运算符“=”“&”不需用户重载。
  8. 应当使重载运算符的功能类似于该运算符作用于标准类型数据时所实现的功能。
  9. 运算符重载函数可以是类的成员函数(如上例),也可以是类的友元函数(全局友元函数)。

3. 运算符重载函数作为类成员函数和友元函数

将“+”重载为适用于复数加法,重载函数为类的全局友元函数。

1
2
3
4
5
6
7
class Complex{
friend Complex operator + (Complex &c1,Complex &c2); //成员函数作为运算符重载函数的参数是?
};
//定义作为友元函数的运算符重载函数
Complex operator + (Complex &c1, Complex &c2){
return Complex(c1.real+c2.real, c1.imag+c2.imag);
}

运算符重载函数的选择:

  • 赋值运算符“=”、下标运算符“[]”、函数调用运算符“()”、成员运算符“->”必须作为成员函数重载。
  • 流插入“<<”和流提取“>>”运算符、类型转换运算符不能定义为类的成员函数,只能做为友元函数
  • 一般将单目运算符和复合运算符重载为成员函数
  • 一般将双目运算符重载为友元函数

4. 重载双目运算符

定义一个字符串类String,用来存放不定长的字符串,重载运算符“==”“<”“>”,用于两个字符串的等于、小于和大于的比较运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class String{
public:
String( ){ p=NULL; } //默认构造函数
String( char *str ){ //带参数构造函数
p=str; //p指向实参字符串
}
void display( ){
cout<<p<<endl; }
private:
char *p; //字符型指针,用于指向字符串
};
int main( ) {
String string1(″Hello″), string2(″Book″);
string1.display( ); string2.display( );
}

重载运算符“>”……

1
2
3
4
5
6
7
8
9
10
11
12
class String{
public:
//声明运算符函数为友元函数,<,==一样
friend bool operator>(String &string1,String &string2);
};
bool operator> (String &string1,String &string2) {
//调用strcmp函数比较字符串大小
if(strcmp(string1.p , string2.p)>0) //<,==
return true;
else
return false;
}

5. 重载单目运算符

单目运算符只有一个操作数,因此运算符重载函数只有一个参数,如果运算符重载函数作为成员函数,则还可省略此参数。

有一个Time类,包含数据成员minute(分)和sec(秒),模拟秒表,每次走一秒,满60秒进一分钟,此时秒又从0开始算。要求输出分和秒的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Time{
public:
Time( ){minute=0;sec=0;} //默认构造函数
//构造函数重载,参数初始化表
Time( int m, int s):minute(m), sec(s){ }
Time operator++( ); //前置++
Time operator++( int ); //后置++
void display( ){ cout<<minute<<″:″<<sec<<endl; }
private:
int minute; //分
int sec; //秒
};
//前置++运算符重载函数
Time Time∷operator++( ) { //运算符重载函数实现部分
if(++sec>=60) { //每次执行++,sec+1
sec-=60; //满60秒+1分钟,且sec-60
++minute;
}
return *this; //返回当前对象值
}
int main( ) {
Time time1(34,0);
for (int i=0;i<61;i++) {
++time1;
time1.display( );}
return 0;
}

//后置++运算符重载函数
Time Time∷operator++( int ) {
Time temp(*this); //复制构造函数新建temp对象
if(++sec>=60) { //每次执行++,sec+1
sec-=60; //满60秒+1分钟,且sec-60
++minute;
}
return temp; //返回当前对象值
}
  • int参数只是表明此为后置++运算符重载函数,此外没其他作用,且在进行后置++时不需要传递参数。

  • this为当前对象的指针,*this即当前对象

  • 通过对象调用函数后,此对象内的sec+1,但是temp保留+1之前的值,且返回的是temp对象,即sec+1之前的值。例如:

    time2=time1++; 

6. 重载流插入运算符和流提取运算符

  • 输入流类:istream。输出流类:ostream。cin和cout分别是istream类和ostream类的对象。在类中已经对“<<”和“>>”进行了重载,能用来输出和输入C++标准类型的数据

  • 自定义类型的数据不能直接用“<<”和“>>” ,须重载。

  • *对“<<”和“>>”重载的函数形式如下: *

1
2
istream  & operator >> (istream  &, 自定义类 &);
ostream & operator << (ostream &, 自定义类 &);

注:重载“>>”和“<<”的函数只能是友元函数。

假设用成员函数重载<<和>>,那么成员函数就只能本类对象才可以调用
,在使用<</>>时必须写成:本类对象<</>>cin/cout;
而不能写成: cin>>s; / cout<<s;

在前面Complex函数的基础上,用重载的“<<”输出复数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Complex{
public:
Complex( ){real=0;imag=0;}
Complex(double r,double i){real=r;imag=i;}
//运算符“+”重载为成员函数
Complex operator + (Complex &c2);
//声明“<<”友元重载函数
friend ostream& operator <<(ostream&,Complex&); private:
double real;
double imag;
};
//定义运算符“<<”重载函数
ostream& operator << (ostream& output, const Complex& c) {
output<<″(″<<c.real<<″+″<<c.imag<<″i)″<<endl;
return output;
}
//使用示例
int main( ) {
Complex c1(2,4),c2(6,10),c3;
c3=c1+c2;
//“cout<<c3”解释为operator<<(cout,c3)
cout<<c3;
}

7. 标准类型数据间的转换

隐式类型转换,例如:

1
2
int i = 6; 
i = 7.5 + i;

显式类型转换。形式为: 类型名(数据),例如:int(89.5)

注意:C语言中的显式类型转换的格式为: (int)89.5

8. 转换构造函数

转换构造函数:作用是将一个其他类型的数据转换成一个类的对象

转换构造函数只有一个形参,例如: Complex(double r) { real=r; imag=0; }

使用转换构造函数将一个指定的数据转换为类对象的方法如下:

  1. 先声明一个类。
  2. 在这个类中定义一个只有一个参数的构造函数,参数的类型是需要转换的类型,在函数体中指定转换的方法。
  3. 在该类的作用域内可以用以下形式进行类型转换:类名(指定类型的数据)

注:不仅可以将一个标准类型数据转换成类对象,也可以将另一个类的对象转换成转换构造函数所在的类对象。

例如:将一个学生类对象转换为教师类对象,Teacher类中的转换构造函数:

1
2
Teacher(Student& s) {
num=s.num; strcpy(name,s.name); sex=s.sex;}

注意: 对象s中的num,name,sex必须是公用成员,否则不能被类外引用

9. 类型转换函数

转换构造函数:其他类型的数据→类的对象

类型转换函数:类的对象→其他类型的数据,注意,不是构造函数
例如将一个Complex类对象转换成double类型数据

类型转换函数的一般形式为:

operator 类型名( ) {

实现转换的语句 }

注意:

  • 在函数名前面不能指定函数类型,函数没有参数。其返回值的类型是由函数名中指定的类型名来确定的。
  • 类型转换函数只能作为成员函数,因为转换的主体是本类的对象。不能作为友元函数或普通函数。
1
2
3
4
5
例如:
Complex →double,在Complex类中定义类型转换函数:
operator double( ) {
return real;
}

文档

习题

  • 假定M是一个类名,且M中重载了操作符=,可以实现M对象间的连续赋值,如“m1=m2=m3;”。重载操作符=的函数原型最好是( )

    M& operator=(M);
    连续赋值,返回值必定不是int,且m1=m2后,还有再次赋值,若返回M,则得到一个临时对象,临时对象不可以再次赋值,若返回M&,则可以返回m1的引用,所以再次赋值,还是对m1的赋值。

  • 假定K是一个类名,并有定义“K k; int j;”,已知K中重载了操作符 ( ),且语句“j=k(3);”和“k(5)=99;”都能顺利执行,说明该操作符函数的原形只可能是( )

int & operator ( )(int);

返回值可以赋值给int变量,还能接受int常量的赋值,所以返回值必须为int &,两个调用参数都是常量,所以形参为int

  • 如果表达式a+b中的“+”是作为成员函数重载的运算符,若采用运算符函数调用格式,则可表示为( )

a.operator+(b)

a+b,a+表示通过对象a调用operator+函数,参数为b

  • 在一个类中可以对一个操作符进行(多种 )重载。
  • C++中 函数参数的缺省值是什么?也就是函数默认值
  • 定义类动态对象数组时,其元素只能靠自动调用该类的____来进行初始化。 无参构造函数
  • 类型转换函数没有____类型,而且参数表为____返回值,
  • 重载抽取运算符>>时,其运算符函数的返回值类型应当是____istream &
  • 重载运算符时,该运算符的__、结合性以及操作符的个数不允许改变。优先级

第11章-继承与派生

1. 继承与派生的概念

子类继承了基类的所有数据成员和成员函数(除了构造函数和析构函数),并可以调整成员的属性(访问范围等)。

一个子类只从一个基类派生,这称为单继承(java)

一个派生类不仅可以从一个基类派生,也可以从多个基类派生。一个派生类有两个或多个基类的称为多重继承(C++)

2. 派生类的声明方式

声明子类的形式:

class 派生类名:[继承方式]基类名{
派生类新增加的成员
} ;

继承方式: public,private和protected,如果不写此项,默认为private

例:假设已经声明了基类Stu,在此基础上通过单继承建立子类Stu1

1
2
3
4
5
6
7
8
class Stu1: public Stu {  //基类Student,继承方式public
public:
void display_1( ) { //新增加的成员函数
cout<<″age: ″<<age<<endl;
}
private:
int age; //新增加的数据成员
};

3. 类图

  • 属性和方法书写规范:修饰符[描述信息] 属性、方法名称 [参数] [:返回类型|类型]

  • 属性和方法之前可附加的可见性修饰符:
    加号(+)表示public;减号(-)表示private;井号(#)表示protected;

  • 继承关系:子类箭头指向父类。

4. 派生类的构成

子类包括从基类继承过来的成员(数据与方法)和增加的成员两部分。

构造一个派生类包括以下3部分工作:

  1. 从基类接收成员。注意:不包括构造函数和析构函数
  2. 调整从基类接收的成员。可以对继承来的成员作某些调整。

例如:

改变派生类中基类域中成员的访问属性。
派生类中声明一个与基类同名的成员。

  1. 在声明派生类中增加的成员。

5. 派生类成员的访问属性

派生类中的基类域成员和子类域成员的访问情况:

  1. 基类的成员函数访问基类成员。
  2. 派生类的成员函数访问派生类自己增加的成员。
  3. 基类的成员函数不可访问派生类的成员。
  4. 派生类的成员函数访问基类的成员。
  5. 在派生类外只能访问派生类的公用成员
  6. 在派生类外访问基类的成员。

第(4)和(6):根据基类的成员在派生类中的访问属性,要考虑基类成员所声明的访问属性,且要考虑派生类所声明的继承方式,根据这两个因素共同决定基类成员在派生类中的访问属性。

  • 公用继承:继承方式为public的继承。

注意:在子类中只能通过调用基类的公用成员函数来引用基类的私有数据成员。

  • 私有继承:继承方式为private的继承。
  1. 不能通过派生类对象访问从基类私有继承过来的任何成员
  2. 派生类的成员函数不能访问基类的private成员,但可以访问基类的public成员
  3. 可以通过派生类的成员函数调用基类的public成员函数,通过基类的public函数访问基类中的private成员,从而让派生类访问基类中的private成员。
  • 保护继承:继承方式为protected的继承。

保护成员可以被派生类的成员函数引用(类外不可以访问)。

  • 基类成员在派生类中的访问属性
在基类的访问属性 继承方式 在派生类中的访问属性
private public 不可访问
private private 不可访问
private protected 不可访问
public public public
public private private
public protected protected
protected public protected
protected private private
protected protected protected
  • 派生类中的成员访问属性
派生类中访问属性 在派生类中 在派生类外部 在下一层公用派生类中
公用 可以 可以 可以
保护 可以 不可以 可以
私有 可以 不可以 不可以
不可访问 不可以 不可以 不可以
  • 多级派生时的访问属性
    • 无论哪一种继承方式,在派生类中是不能访问基类的私有成员的,私有成员只能被本类的成员函数所访问;
    • 如果采用私有继承,经过若干次派生后,基类的所有的成员会变成不可访问。

6. 简单派生类的构造函数

简单派生类构造函数格式:

派生类构造函数名(总参数表列): 基类构造函数名(参数表列)
{ 派生类中新增数据成员初始化语句 }

例如:

1
2
Student1(int n,string nam,char s,int a,string ad):Student(n,nam,s)
{ …… }

注意:总参数表列中包含基类派生类中所需的所有参数,参数格式为参数类型参数名

冒号后面的“基类构造函数名(参数表列)” 为调用基类构造函数,所以它的这些参数是实参。它们可以是:常量、全局变量和总参数表列中的参数。

调用基类构造函数时的实参也可以不从派生类构造函数的总参数表中传递过来,而直接使用常量或全局变量。例:

1
Student1(string nam,char s,int a,string ad):Student(10010,nam,s)

注意:编译系统是根据参数的名称来确认总参数表与基类构造函数实参之间的传递关系的,而不是参数的排列顺序。

在建立一个对象时,执行构造函数的顺序是:(自然析构顺序就知道了)

  1. 调用基类构造函数;
  2. 执行派生类构造函数;

7. 有子对象的派生类的构造函数

子对象:以类的实例作为类中的数据成员。

子对象可以是派生类的父类,也可以是其他类。

定义派生类构造函数的一般形式为:

派生类构造函数名(总参数表列): 基类构造函数名(参数表列),子对象名(参数表列){
派生类中新增数成员据成员初始化语句
}

执行派生类构造函数的顺序是:

  1. 调用基类构造函数,对基类数据成员初始化;
  2. 调用子对象构造函数,对子对象数据成员初始化;
  3. 再执行派生类构造函数本身,对派生类数据成员初始化。

派生类构造函数的总参数表列中的参数,包括基类构造函数和子对象的参数表列中的参数。

基类构造函数和子对象的次序可以是任意的,编译系统是根据相同的参数名(而不是根据参数的顺序)来确立它们的传递关系的。

8. 多层派生时的构造函数

多层派生类的构造函数:
每个类的构造函数只需写出其父类的构造函数。
构建对象时需要在参数列表中给出本类及多层父类的初始化参数。

1
2
3
4
5
6
7
8
9
class Student{ 
Student(int n, string name) { …… };

class Student1:public Student{
Student1(int n, string name,int a) : Student(n,name)
{ …… } };

class Student2:public Student1{
Student1(int n, string name,int a,int s): Student1(n,name,a) { …… }};

9. 派生类构造函数的特殊形式

  • 子类属性若不需要赋值,子类的构造函数可以为空函数。
  • 父类仅有默认构造函数,子类的构造函数不需要调用父类构造函数(自动调用)。
  • 父类和子类都是默认构造函数,且子类无属性赋值,子类可以无构造函数(自动调用默认构造函数)。
  • 父类中既有无参构造函数,又有带参构造函数,子类中既可以包含基类构造函数及其参数,也可以不包含基类构造函数。

10. 多重继承

  • 多重继承: 一个子类有两个或多个基类,派生类从两个或多个基类中继承所需的属性。

声明多重继承的方法:

class 子类名:继承方式 父类名, … ,继承方式 父类名
{ … }

例如:如果已声明了类A、类B和类C,声明多重继承的派生类D:

1
2
3
class D:public A, private B, protected C{
类D新增加的成员
};
  • 构造函数:

子类构造函数名(总参数表列): 基类1构造函数(参数表列), 基类2构造函数(参数表列), 基类3构造函数 (参数表列)
{ 派生类中新增数成员据成员初始化语句 }

调用基类构造函数的顺序是按照声明子类时基类出现的顺序。

  • 多重继承引起的二义性问题

二义性:多重继承时,继承的多个基类中有名称相同的成员

情况1: 两个基类有同名成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A{
public:
int a;
void display( ); };
class B{
public:
int a;
void display( ); };
class C :public A,public B{
public :
int b;
void show();
};
C c1;
c1.a=3; c1.display(); //错误信息,成员有歧义。
c1.A::a=3; //引用c1对象中的基类域A的数据成员a,通过作用域限定符消除歧义
c1.B::display(); //调用c1中的基类域B的成员函数display,通过作用域限定符消除歧义

情况2: 基类和派生类三者都有同名成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A{
public:
int a;
void display( ); };
class B{
public:
int a;
void display( ); };
class C :public A,public B{
public :
int a;
void display();
};
C c1;
c1.a=3; c1.display(); //访问的是C中的成员。
c1.A::a=3; //引用c1中的基类域A的数据成员a,通过作用域限定符指定访问
c1.B::display(); //调用c1中的基类域B的成员函数display
{% note warning %} 注意:子类中只要出现与基类中`同名`的函数,不管参数是否相同,通过`子类对象`调用函数时,基类的函数都将被屏蔽。若要访问,需通过::运算符。 {% endnote %}

情况3: 类A和类B是从同一个基类派生的,AB自动产生了相同名称的成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class N{
public:
int a;
void display( ); };
class A{
public:
int a1; };
class B{
public:
int a2; };
class C :public A,public B{
public :
int a3; void show(); };
c1.A::a=3;
c1.A::display(); //要访问的是类N的派生类A中的基类成员

11. 虚基类

虚基类的作用:在继承间接共同基类时只保留一份成员。

声明虚基类形式如下:
class 派生类名: virtual 继承方式 基类名

例如:

1
2
3
4
5
6
7
class A{…};	//声明基类A
class B :virtual public A
{ … };
class C :virtual public A
{ … };
class D :public B,public C
{ … };

注意:为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类。否则仍然会出现对基类的多次继承

虚基类的初始化:若在虚基类中定义了带参数的构造函数,且没有默认构造函数,则在其所有派生类(包括直接派生或间接派生的派生类)中,需要通过构造函数的初始化表对虚基类进行初始化

1
2
3
4
5
6
7
8
9
10
11
12
class A{    //定义基类A
A(int i){ } //基类构造函数,为public
int data; … };
class B :virtual public A{
B(int n):A(n){ } //在初始化表中对虚基类初始化
…};
class C :virtual public A{
C(int n):A(n){ } … };
class D :public B, public C{
D(int n):A(n),B(n),C(n){ } //在最后的派生类中不仅要负责对其直接基类进行初始化,还要负责对虚基类初始化
…};
//编译系统只执行最后的派生类对虚基类的构造函数的调用,而忽略虚基类的其他派生类(如类B和类C) 对虚基类的构造函数的调用,这就保证了虚基类的数据成员不会被多次初始化

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Person{ //声明公共基类Person
public:
Person(string nam,char s,int a)
{name=nam;sex=s;age=a;}
protected:
string name;
char sex;
int age;
};
class Teacher:virtual public Person{
public:
Teacher(string nam,char s,int a, string t):Person(nam,s,a)
{title=t; }
protected:
string title; //职称
};
class Student:virtual public Person{
public:
Student(string nam,char s,int a,float sco)
:Person(nam,s,a),score(sco){ }
protected:
float score;
};
class Graduate:public Teacher, public Student{
public:
Graduate(string nam,char s,int a, string t,float sco,float w)
:Person(nam,s,a),Teacher(nam,s,a,t),Student(nam,s,a,sco),wage(w){}
//上述参数初始化表,Person初始化了虚基类。且不会调用Teacher与Student中的基类构造函数,但在Graduate中调用他们构造函数时参数还需要对应其参数表。
void show( )
{cout<<″name:″<<name<<endl;//直接调用
cout<<″age:″<<age<<endl;
cout<<″sex:″<<sex<<endl;
cout<<″score:″<<score<<endl;
cout<<″title:″<<title<<endl;
cout<<″wages:″<<wage<<endl;
}
private:
float wage; //工资
};

12. 基类与派生类的转换(向上转型)

公用派生类完整地继承了基类的功能,派生类中包含从基类继承的成员,因此可以将派生类的值赋给基类对象,在用到基类对象的时候可以用其子类对象代替。

(情况1) 派生类对象赋值给基类:用public子类对象对其基类对象赋值

1
2
3
4
如: class B:public A
A a1;
B b1; //类A的public子类B的对象b1
a1=b1; //子类B对象b1对基类对象a1赋值

派生类对象可以向基类对象赋值
赋值只是对数据成员赋值,对成员函数不存在赋值问题。

注意: 赋值后不能通过对象a1去访问派生类对象b1中新增的成员(不存在)。

1
2
3
4
5
6
7
8
假设A类有成员数据name,而age是子类B中增加的公用数据成员。
A a1;
B b1; //类A的public子类B的对象b1
a1=b1;
a1.name=“xiaoming”; //正确
a1.age=23; //错误
b1.age=21; //正确
b1.name=“xiaohong”; //正确
{% note danger %} - 子类型关系是单向的、不可逆的 - 只能用子类对象对其基类对象赋值,而不能用基类对象对其子类对象赋值 - 同一基类的不同子类对象之间也不能互相赋值 {% endnote %}

(情况2) 子类对象可以替代基类对象向基类对象的引用进行赋值或初始化

1
2
3
4
5
6
7
8
如已定义了基类A对象a1,可以定义a1的引用变量:
A a1; //定义基类A对象a1
B b1; //定义public子类B对象b1
A& r=a1; //定义基类A对象的引用变量r,并用a1对其初始化
//或者上面一行的代码改为:
A& r=b1; //用派生类B对象b1对A类对象的引用进行初始化
//或者保留 “A& r=a1;”,而对r重新赋值:
r=b1; //用派生类B对象b1对a1的引用变量r赋值

注意: 此时r并不是b1的别名,也不与b1共享同一段存储单元。它只是b1中基类部分的别名,r与b1中基类部分共享同一段存储单元,r与b1具有相同的起始地址。

(情况3) 如果函数的参数是基类对象或基类对象的引用,相应的实参可以用子类对象.

例如函数fun:

1
2
3
4
5
void fun(A& r) { //形参是类A的对象的引用变量
cout<<r.num<<endl;
}
//函数调用时:
fun(b1);

(情况4)子类对象的地址可以赋给指向基类对象的指针变量。

定义一个基类Student(学生),再定义Student类的public子类Graduate(研究生),用指向基类对象的指针输出数据。

1
2
3
4
5
6
7
8
int main() {
Student stud1(1001,″Li″,87.5);
Graduate grad1(2001,″Wang″,98.5,563.5);
Student *pt=&stud1;
pt->display( ); //调用stud1.display函数
pt=&grad1; //指针指向grad1
pt->display( ); //调用grad1.display函数
}
  • pt是指向Student类对象的指针变量,即使让它指向了grad1,但实际上pt指向的是grad1中从基类继承的部分。

  • 通过指向基类对象的指针,只能访问派生类中的基类成员,而不能访问派生类增加的成员。所以pt->display()调用的不是派生类Graduate对象所增加的display函数,而是基类的display函数.

13. 继承与组合

  • 在一个类中可以用类对象作为数据成员,即子对象
  • 对象成员的类型可以是本派生类的基类,也可以是另外一个已定义的类。
  • 在一个类中以另一个类的对象作为数据成员的,称为类的组合
  • 类的组合和继承的区别:
    • 通过继承建立了派生类与基类的关系
    • 通过组合建立了成员类和组合类的关系
    • 继承是纵向的,组合是横向的

习题

  • 派生类只含有基类的公有成员和保护成员 × private也继承,只是不能访问

  • 派生类的构造函数的成员初始化列表中,不能包含( )。 基类中子对象的初始化

  • 派生类的对象对其基类成员中( )是可以访问的。 公有继承中的公有成员

  • 在私有继承的情况下,允许派生类直接访问的基类成员包括()。公有成员和保护成员

  • 无论何种继承方式,基类中的private在子类中都不可访问

第12章-多态性与虚函数

1. 多态性的概念

从系统实现的角度看,多态性分为两类: 静态多态性和动态多态性。

  • 静态多态性:由函数重载实现,由于重载函数的参数不同,所以在程序编译时系统就能决定要调用哪个函数,又称为编译时的多态性。
{% note warning %} 注意:函数只有在`同一个作用域内`才能构成重载。例如同一个类内,或者几个同名函数都是全局函数。注意:继承的子类中与父类域中的同名函数不构成重载(构成重写,从而隐藏) {% endnote %}
  • 动态多态性:不在编译时确定调用的是哪个函数,而是在程序运行过程中才动态地确定操作所针对的对象。它又称运行时的多态性。动态多态性通过虚函数实现。

2. 一个典型的例子(重载实现多态)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Point{
public:
friend ostream &operator<<(ostream&,const Point &);
……
}
class Circle:public Point{
public:
friend ostream &operator<<(ostream&,const Circle &);
……
}
Class Cylinder:public Circle{
public:
friend ostream &operator<<(ostream&,const Cylinder &);
……
}

上述三个类中声明的friend函数名称都为operator<<,但参数不同,且这三个operator<<函数都属于全局函数,构成重载,所以在main函数中通过参数即可在编译阶段确认调用的函数,属于静态多态性,即编译时多态。

3. 利用虚函数实现动态多态性

虚函数:类中被关键字virtual修饰的成员函数。

功能:基类中声明为virtual的函数,在子类中重写一个名称相同参数相同的函数。在将子类对象/对象地址赋值给基类引用/基类指针后,通过基类引用/基类指针可以调用子类中重写的函数(而不是基类域中的同名函数)。

{% note warning %} - 基类中的虚函数,其子类中的同名同参数函数会自动成为虚函数。(即子类中可以不写`virtual`) - C++标准允许`返回值不同`的情况,但是只有极少的编译器支持,所以程序中重写的虚函数需要与基类中的保持`返回值`、`函数名`、`参数`都一致(若参数不一致,则不构成重写关系,不构成多态)。 - 只能用virtual声明类的成员函数为虚函数。 - 同类中不能定义相同名称与参数的函数,即使返回值不同也不行(提示错误:无法构成重载)。 {% endnote %}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Parent{		//定义基类Parent
virtual void show(){ cout<<“Parent cout!”<<endl; } };//定义虚函数
class Son:public Parent{ //定义子类son
//show(int)因参数不同,没有重写基类中的show,而是与重写的show构成了重载关系。
virtual void show(int i){ cout<<“Son cout include i!”<<endl; }
virtual void show(){ cout<<“Son cout!”<<endl; } //重写虚函数show,
};
int main(){
Parent f, *pf; Son s;
f=s; //子类对象s赋值给基类对象f,向上转型
f.show(); //基类对象,调用的是基类中的show函数。
pf=&s; //将子类对象s的地址赋值给基类指针pf
pf->show(); //通过指针,调用的是子类中的show(即多态)
//pf->show(12); //错误,提示找不到fa::show(int)匹配的函数
Parent &pa=s; //定义基类引用并用子对象初始化
pa.show(); //通过引用实现多态
return 0;
}
  • 虚函数的使用方法:
  1. 在基类用virtual关键字声明成员函数为虚函数。
  2. 在子类中重写此函数,要求函数名、函数类型、函数参数与基类的虚函数相同。
  3. 定义一个指向基类对象的指针变量/引用,并使它指向同一类族中需要调用该函数的对象。
  4. 通过该指针变量/引用调用此虚函数,此时调用的就是指针变量指向的对象的同名函数。
  • 静态关联与动态关联
    • 确定调用函数的具体对象的过程称为关联/绑定。
    • 函数重载在编译时即可确定其调用的函数属于哪一个类,其过程称为静态关联,也称为早期关联。
    • 多态中,通过基类指针/引用调用虚函数,在编译阶段无法从语句本身确定调用哪一个类的虚函数,只有在运行时,根据基类指针/引用指向某个对象(所以前面需要将对象/地址赋给指针/引用),才能确定调用的是哪一个类的虚函数。其过程为动态关联,也称为滞后关联。

4. 虚析构函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A{
public:
~A() {
cout<<"destruct A"<<endl;
}
};
class B: public A{
public:
~B() {
cout<<" destruct B"<<endl;
}
};
int main(){
A * a = new B;
delete a;
return 0;
}

输出:
destruct A
delete a;
表示释放A类型指针指向的动态空间。所以调用的是类A的析构函数。导致B类中创建的内存不能释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A{
public:
virtual ~A() {
cout<<"destruct A"<<endl;
}
};
class B: public A{
public:
~B() {
cout<<" destruct B"<<endl;
}
};
int main(){
A * a = new B;
delete a;
return 0;
}

输出:
destruct B
destruct A

将基类的析构函数声明为虚函数时,该基类的所有派生类的析构函数都自动成为虚函数(即使派生类的析构函数与基类的析构函数名字不相同),且构成多态。所以会调用子类B的析构函数,从而再自动调用基类析构函数。

{% note info %} **所以,当一个类被用作其它类的基类时,建议将它的析构函数设置为virtual。** {% endnote %}

5. 纯虚函数

纯虚函数:在声明虚函数时被“初始化”为0的函数。

格式:virtual 函数类型 函数名(参数列表)= 0;

{% note warning %} - 纯虚函数没有函数体;最后面的“=0”只起形式上的作用,告诉编译系统“这是纯虚函数”,并不表示返回值为0 - 这是一个声明语句,最后结尾有“;” - 纯虚函数只有函数的名字而不具备函数的功能,因此不能被调用,在派生类中对此函数进行定义后,它才具备函数功能,可以被调用。 - 如果在一个类中声明了纯虚函数,而在其派生类中没有对该函数定义,则该虚函数在派生类中仍然为纯虚函数 {% endnote %}

纯虚函数的作用:是在基类中为其派生类保留一个函数的名字,以便派生类根据需要对它进行定义,从而实现动态多态性。

6. 抽象类

抽象类:不用来定义对象而只作为一种基本类型用作继承的类。

  • 凡是包含纯虚函数的类都是抽象类。纯虚函数不能被调用,所以抽象类无法建立对象

  • 派生类中没有对所有纯虚函数进行定义–则此派生类仍然是抽象类,不能用来定义对象。在抽象类所派生出的新类中对基类的所有纯虚函数进行了定义后,派生类才不是抽象类,可以用来定义对象。

  • 抽象类不能定义对象(或者说抽象类不能实例化),但是可以定义指向抽象类数据的指针变量

  • 当派生类成为具体类之后,就可以用这种指针指向派生类对象,然后通过该指针调用虚函数,实现多态性的操作。

虚函数与纯虚函数的区别

定义一个函数为虚函数,不代表函数是不被实现的函数。定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。

定义一个函数为纯虚函数,代表函数没有被实现。定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。

C++中重载,重写总结

1. 继承: 一个新类从已有的类那里获得其已有成员。

  • 当子类继承父类时,会将父类的全部成员(除了构造函数和析构函数以外的属性,方法)全部复制一份,作为子类的成员。
  • 子类会标记从父类中继承的成员。这里可以简单的认为将子类本身的成员存在子类域,从父类复制过来的存在父类域

2. 向上转型(子类转换成基类)

  • 子类对象赋值给基类,属于赋值运算,实际上是将子类的属性值赋值给基类。
  • 子类对象赋值给基类的引用,引用不是子类对象的别名,只是子类对象的基类域的别名。
  • 子类对象的地址赋值给基类指针,此指针仅指向子类的基类域部分。

综上所述,子类对象向上转型成基类后,始终只能调用基类函数(情况1)或子类对象中基类域部分的数据成员与成员函数(情况2、3,且无虚函数的情况下)。

3. 重载: 用同一个函数名定义多个函数,而这些的参数列表不同(个数,类型,顺序)。

重载的注意点:

  1. 重载函数之间的区别是参数列表,与返回值无关(函数调用时只需要函数名与参数)。
  2. 参数列表中的顺序指的是不同类型参数之间的顺序。例如:
1
2
3
void display(int a, char c, int b);	//参数顺序为( int , char ,int )
void display(char c, int a, int b); //参数顺序为( char , int ,int ),所以构成重载
void display(int b, char c, int a); //参数顺序为( int , char ,int ),不能构成重载,报错
  1. 只有在同一个作用域内才能构成重载。例如同一个类内,或者几个同名函数都是全局函数。注意:继承的子类中与父类域中的同名函数不构成重载(实际为隐藏关系,即子类中的同名函数会屏蔽掉基类中的同名函数)。
  2. 修饰函数的virtual 关键字对同类中函数的重载没有影响。

vitrual的作用:子类中通过重写虚函数来实现对基类虚函数函数指针的覆盖。所以,virtual函数只有在继承中才有作用,同一个类中函数的重载判定条件不包括virtual修饰符。

4. 重定义(产生隐藏现象)

子类重新定义父类中有相同名称的非虚函数 ( 参数列表可以不同 ) 。通过子类对象调用函数时,子类的函数屏蔽了与其同名的基类函数(Java中会构成重载,而C++中会构成隐藏现象)。

隐藏的注意点:

  1. 隐藏仅限于子类对象,是子类对象调用与基类同名的函数时会屏蔽基类域中的同名函数,即调用的是子类中定义的函数,而不调用基类域中的同名函数;要想使用基类域中的方法必须通过::运算符。
  2. 同名的函数不在同一个作用域,分别位于子类与基类;
  3. 函数名相同,返回值和参数可以不同;
  4. 子类中的同名函数不管参数是否与之相同,只要是子类对象对此函数的调用,基类的函数都将被隐藏;
  5. 子类对象向上转型后,赋值给基类对象/(基类引用/基类指针),通过基类对象/(引用/指针),调用的仍然是基类(基类域)中的函数(参考向上转型)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Parent{		//定义父类Parent
public:
void show(){ cout<<“fa out!”<<endl; } //定义函数show()
};
class Son:public Parent{ //定义子类Son
public:
void show(int i){ cout<<“son out include i!”<<endl; } //子类中定义show(int)函数
void show(){ cout<<“son out!”<<endl; } //子类中定义函数show()
};
int main(){
Parent f, *pf;
Son s;
s.show(12); //子类对象调用函数
s.show(); //同上
//下面的语句为向上转型,从运行结果可以看到,它们都调用了基类函数。
f=s; f.show();
Parent &fy=s; fy.show();
pf=&s; pf->show();
return 0;
}

5. 重写

基类函数有virtual修饰,子类中的函数与基类名称相同,参数相同(一般返回类型也相同),此为重写。

多态:基类中声明为virtual的函数,在子类中重写一个返回类型、名称、参数都相同的函数。在将子类对象地址/对象赋值给基类指针/基类引用后,通过基类指针/引用可以调用子类中重写的函数。

注意:基类中的虚函数,其子类中的重写函数会自动成为虚函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Parent{		//定义基类fa
public:
virtual void show(){ cout<<“Parent cout!”<<endl; } }; //定义虚函数show
class Son:public Parent{ //定义子类son
public:
//show(int)因为参数不同,实际上并没有与基类中的show构成重写关系
virtual void show(int i){ cout<<“Son cout include i!”<<endl; }
virtual void show(){ cout<<“Son cout!”<<endl; } //定义虚函数show,
};
int main(){
Parent f, *pf; Son s;
f=s; //s赋值给基类对象f
f.show(); //通过对象,调用的是基类中的show函数。
//f.show(12); //错误,提示找不到与fa::show(int)匹配的函数
pf=&s; //将子类s的地址赋值给基类指针pf
pf->show(); //通过指针,调用的是子类中定义的show函数
//pf->show(12); //错误,提示找不到fa::show(int)匹配的函数
Parent &pa=s; //定义基类引用并用子对象初始化
pa.show(); //通过引用实现多态
return 0;
}

6. 多态

编译时多态:重载,在编译阶段可以通过参数类型确定调用的函数。
运行时多态:在程序执行中,将子类对象的地址赋值给基类指针后,程序才知道调用哪个子类的虚函数。

习题

  • 对虚函数的调用( )。不一定使用动态联编

    虚函数在运行阶段和类的对象绑定在一起,这样成为动态联编。虚函数声明只能出现在类声明中虚函数原型中,而不能在成员的函数体实现的时候。必须由成员函数来调用或者通过指针、引用来访问虚函数。如果通过对象名来访问虚函数,则联编在编译过程中认为足静态联编。

  • 在C++中,虚函数用于实现运行时多态性。

  • 编译时的多态性可以通过使用 ( ) 获得。虚函数和对象

  • 静态联编所支持的多态性称为编译时多态性,动态联编所支持的多态性则称为_运行时__多态性。

  • c++中基类中只有带参数的构造函数时,派生类中一定要显示定义构造函数,并写出基类的构造函数及参数 √

    即使基类的构造函数没有参数,派生类也必须建立构造函数 ×

  • 在析构函数中调用虚函数时,采用____联编。静态

第13章-输入输出流

1. C++的输入输出流

输入输出流:由若干字节组成的字节序列,这些字节中的数据按顺序从一个对象传送到另一对象。

流中的内容可以是ASCII字符、二进制数值、图形图像、数字音频视频或其他形式的信息。

C++中,输入输出流被定义为流类。用流类定义的对象称为流对象。例如:cin、cout。

1
2
3
4
5
6
7
8
graph TB;
ios//抽象基类-->istream//通用输入类
ios//抽象基类-->ostream//通用输出类
istream//通用输入类-->ifstream//输入文件流类
istream//通用输入类-->iostream//通用输入输出类
ostream//通用输出类-->ofstream//输出文件流类
ostream//通用输出类-->iostream//通用输入输出类
iostream//通用输入输出类-->fstream

注:通用IO流类在头文件iostream中声明,文件流类在头文件fstream中声明。

流类库中不同的类的声明被放在不同的头文件中,常用的头文件有:

#include <iostream> 此头文件中包含了流常用类和对象,例如istream、ostream、iostream等类,cin、cout等对象。

#include < fstream> 用于用户管理的文件的I/O操作。

#include < strstream> 用于字符串流I/O。

#include < stdiostream> 用于混合使用C和C++的I/O机制时。

#include < iomanip> 在使用格式化I/O时应包含此头文件。

2. 标准输入流

  • istream::get():读入一个字符,包括如下函数。
  1. int get():从输入流中提取一个字符,函数返回值就是此字符。
  2. istream& get(char& ch):从输入流中提取一个字符,赋给字符变量ch
  3. istream& get(char* s, streamsize n );从输入流中读取n-1个字符(第n个是’\0’),赋值给参数中的字符数组或字符指针,结束条件为count个字符或者遇到回车符。默认终止符为回车。
  4. istream& get(char* s, streamsize n, char delim );同上,若在读取n-1个字符之前若遇到终止符delim,则结束读取过程。

例:

1
2
3
char ch[10]=“”;	 //通过输入10个字符来初始化数组。
for(int i=0;i<10;i++) //下面个函数任选其一
ch[i]=cin.get()/cin.get(ch[i]);
  • istream::getline():读入一行字符

    • istream& getline (char* s, streamsize n );

    从输入流中读取字符,并保存至字符数组s中(数组以‘\0’结束),默认结束符为‘\n‘,即输入回车后结束输入。字符数超过n,只取前n-1个字符。注:只能输入一行

    • istream& getline (char* s, streamsize n, char delim );

    同上,但结束条件不同。

    若没有遇到结束符,则输满n-1个字符结束(最后一个为‘\0’),回车并不会结束输入,只是作为其中的一个字符,所以可以输入多行

    若遇到结束符delim,回车后结束输入,且只取delim之前的字符。

1
2
3
4
5
6
7
8
9
char ch[10]="";
cin.getline(ch,10,'z');
cout<<ch;
输入:000
000
012 //连回车符一共输入12个字符,多行。
输出:000
000
0 //一共输出9个字符: 000 000+回车 0+’\0’ ( 第十个是’\0’)

例:istream:: getline()函数连续输入。

1
2
3
char ch[10] , ch1[10];
cin.getline(ch,10); //第一次输入
cin.getline(ch1,10);
{% note warning %} 注意:若第一次输入的长度超过10,则第二个输入语句不执行,解决方法: {% endnote %}
1
2
3
4
5
char ch[10] , ch1[10];
cin.getline(ch,10); //第一次输入
cin.clear(); //清除所有错误状态标志,将状态恢复为可读写状态
cin.ignore(1000,‘\n’); //clear不会清除多余字符
cin.getline(ch1,10);

ignore()函数:

原型:istream& ignore(streamsize count, int delim )

函数功能:从输入流忽略并舍弃字符,直至并包含 delim。

参数:

count:要忽略的字符数,默认为1。

delim:忽略终止的分隔字符。此字符也会被忽略舍弃掉。默认为EOF。

例如:

1
2
cin.ignore(1024,’\n’);	
//忽略输入流中1024个字符,或者遇到回车符(即使不满1024个字符)就终止忽略。
  • c++标准库中全局函数getline

原型:

1
2
istream& getline (istream& is, string& str);
istream& getline (istream& is, string& str, char delim );

getline(cin,str)是一个全局函数,属于string类。所以使用该函数的时候需#include <string>

功能:从输入流中读入字符(包括空格等),存到string变量,直到出现以下情况为止:

  1. 读到一个新行
  2. 读入了文件结束标志

例如:

1
2
string st;
getline(cin,st);

3. 对数据文件的操作与文件流

用于文件操作的文件流类:

  1. ifstream类:从istream类派生。 用来支持从磁盘文件的输入。
  2. ofstream类:从ostream类派生。 用来支持向磁盘文件的输出。
  3. fstream类:iostream类派生。 用来支持对磁盘文件的输入输出

对文件操作,须先定义一个文件流类对象(C语言中定义FILE指针),通过文件流对象访问文件中的数据。例如:
ostream outfile; //定义输出文件流对象

{% note waning %} 注意:若需要使用文件流类,在头文件需包含fstream。`#include ` {% endnote %}
  • 打开磁盘文件的两种方式
  1. 调用文件流的成员函数open,形式为:

    文件流对象.open(磁盘文件名,输入输出方式);

1
2
3
ofstream outfile;
outfile.open(″f1.dat″,ios::out);//相对路径,默认当前项目根目录
//outfile.open(″C:\\f1.dat″,ios::out); //绝对路径
  1. 定义文件流对象时指定参数(常用)

文件流类的带参数的构造函数中包含了打开磁盘文件的功能。因此,可以通过调用此构造函数来实现打开文件的功能。

1
ofstream outfile(″f1.dat″,ios::out);//以输出方式打开
  • 关闭磁盘文件

关闭文件用成员函数close。如: outfile.close( );

  • 输入输出方式: 在ios类中定义的枚举常量

    app:以追加的模式打开文件。

    binary:二进制形式访问模式。

    in:输入模式,文件不存在,则打开失败。

所以判断文件是否存在可以如下:

1
2
ifstream fin("test.txt",ios::in);
if (!fin){…}//!fin为真表示文件不存在
 `out`:输出模式,若文件存在,则清除原有内容。

`trunc`:如文件存在,则打开后清空文件,不存在,则新建文件。

`ate`:文件打开后定位到文件尾,ios:app就包含有此属性

注:可以用’|’将属性连接起来,表示或的关系。例:ios::out|ios::binary

4. 对ASCII文件的操作

ASCII文件(字符文件):文件的每一个字节中均以ASCII代码形式存放数据,即一个字节存放一个字符 。

对ASCII文件的读写操作:

  1. 用流插入运算符“<<”和流提取运算符“>>”输入输出标准类型的数据.

    特征:分隔符为空格、制表符等,所以每次只能访问一个成员。若字符串若包含空格,不能正常读取。

  2. 用文件流的get,geiline等成员函数访问文件。

    特征:都以字符形式读入,例如一次读一行。存放数据的为字符数组,成员的处理没有方法1的灵活。

1
2
3
4
5
6
7
8
9
10
11
int main(){
char ch[80];
ofstream SaveFile("d:\\ioftest.txt",ios::out);
SaveFile << "hello world";
SaveFile.close();
ifstream ifs("d:\\ioftest.txt",ios::in);
ifs.getline(ch,80); //getline(ifs,str);也可以使用
cout<<ch;
ifs.close();
return 0;
}

5. 对二进制文件的操作

二进制文件:文件中的数据以字节形式存放。又称为字节文件。

需要先打开文件,用完后要关闭文件。打开时输入输出方式要用“ios::binary”,指定为以二进制形式访问。

用成员函数read和write读写二进制文件

1
2
ostream& write(const char * buffer, int len);
istream & read(char *buffer, int len);

字符指针buffer指向内存中一段存储空间。len是读写的字节数。调用的方式为

1
2
a. write(p1,50); //a是输出文件流,将p1输出到文件
b. read(p2,30); //b是输入文件流,将数据读入p2
1
2
3
4
5
6
7
8
9
10
11
int main(){
char ch[80];
ofstream SaveFile("d:\\ioftest.txt",ios::binary);
SaveFile.write("i am happy",sizeof(char)*11);
SaveFile.close();
ifstream ifs("d:\\ioftest.txt",ios::binary);
ifs.read(ch,sizeof(char)*11);
cout<<ch;
ifs.close();
return 0;
}

6. 字符串流

字符串流:以内存中用户定义的字符数组(字符串)为输入输出的对象,即将数据输出到内存中的字符数组,或者从字符数组(字符串)将数据读入。字符串流也称为内存流。

使用注意:

  1. 输出时数据不是流向外存文件,而是流向内存中的一个存储空间。输入时从内存中的存储空间读取数据。
  2. 字符串流对象关联的不是文件,而是内存中的一个字符数组,因此不需要打开和关闭文件。
  3. 每个文件的最后都有一个文件结束符,表示文件的结束。而字符串流所关联的字符数组中没有相应的结束标志,用户要指定一个特殊字符作为结束符,在向字符数组写入全部数据后要写入此字符。
  • 建立输出字符串流对象
1
ostrstream::ostrstream(char *buffer,int n,int mode=ios::out);

buffer是指向字符数组首元素的指针,n为指定的流缓冲区的大小,第3个参数是可选的,默认为ios::out方式。

1
ostrstream strout(ch1,20);
  • 建立输入字符串流对象
1
2
istrstream::istrstream(char *buffer);
istrstream::istrstream(char *buffer,int n);

buffer是指向字符数组首元素的指针,使流对象与字符数组建立关联。

1
2
istrstream strin(ch2); 
istrstream strin(ch2,20);
  • 建立输入输出字符串流对象
1
2
strstream::strstream(char *buffer,int n,int mode);
strstream strio(ch3,sizeof(ch3),ios::in|ios::out);

注:以上3个字符串流类是在头文件strstream中定义的,在用到istrstream, ostrstream和strstream类时应包含头文件strstream。

习题

  • 已知int a, *pa=&a;输出指针pa十进制的地址值的方法是( )cout<<long(&pa);

插入符输出指针类型对象的地址值时,默认为十六进制形式。如果要输出十进制形式的地址值,必须用类型long进行强制。答案A,输出了指针pa本身的值,也即变量a的地址值。 答案B中输出了指针pa所指向的对象,即a的值。答案C,以十六进制形式输出指针pa的地址值。

  • 若磁盘上已存在某个文本文件,它的全路径文件名为 d:\kaoshi\test.txt,则下列语句中不能打开这个文件的是( )。ifstream file(“d:\kaoshi\test.txt”);

文件流的打开路径用“\”隔开,而不是“\”,但可以用“/”隔开。

  • 下列函数中,( )是对文件进行写操作的。 put()
  • 当使用ifstream流类定义一个流对象并打开一个磁盘文件时,文件的隐含打开方式为( )。ios::in
  • cin是istream类的一个对象,处理标准输入。cout、cerr、clog是ostream类的对象。
  • 标准错误流的输出发送给流对象cerrclog
  • 在C++中,打开一个文件就是将一个文件与一个___建立关联;关闭一个文件就是取消这种关联。

第14章-C++工具

1. 异常处理

程序中常见的错误有两大类: 语法错误(编译错误)和运行错误。

异常处理的任务:事先分析程序运行时可能出现的各种意外的情况,并且制订出相应的处理方法。

异常处理的方法: 出现异常→发出信息,传给它的上一级→上级捕捉到这个信息后进行处理→如果上一级的函数不能处理→再传给其上一级,由其上一级处理。如此逐级上送→如果到最高一级无法处理,则终止程序执行。

C++处理异常机制由3个部分组成:即检查(try)、抛出(throw)和捕捉(catch)。把需要检查的语句放在try块中,throw用来当出现异常时发出一个异常信息,而catch则用来捕捉异常信息并处理。

语法:
try{
检查语句 // 若有异常,会执行throw 表达式;
}
catch(异常信息类型 [变量名]) {
进行异常处理的语句
}

注意:

  1. 被检测的函数必须放在try块中。
  2. try块和catch块作为一个整体出现,在二者之间不能插入其他语句。但在一个try-catch结构中,可以只有try块而无catch块。
  3. 一个try-catch结构中只能有一个try块,但却可以有多个catch块
  4. catch后面的圆括号中,一般只写异常信息的类型名catch(int)。
  5. 如果在catch子句中没有指定异常信息的类型,而用 “…”,则表示它可以捕捉任何类型的异常信息。
  6. try-catch结构可以与throw出现在同一个函数中,也可以不在同一函数中。当throw抛出异常信息后,首先在本函数中寻找与之匹配的catch,如果在本函数中无try-catch结构或找不到与之匹配的catch,就转到离开出现异常最近的try-catch结构去处理。
  7. 在某些情况下,在throw语句中可以不包括表达式,如throw;
    表示“不处理这个异常,交给上一级处理”。
  8. 如果throw抛出的异常信息找不到与之匹配的catch块,那么系统就会调用一个系统函数terminate,使程序终止运行。

2. 在异常处理中处理析构函数

如果在try块中定义了类对象,在建立该对象时会调用类的构造函数。

在执行try块 (包括在try块中调用其他函数) 的过程中如果发生了异常,此时流程立即离开try块。

流程可能离开try中构建的对象的作用域而转到其他函数,应做好结束对象前的清理工作(析构函数)

C++的异常处理机制会在throw抛出异常信息被catch捕获时,自动对有关的局部对象进行析构(自动调用类对象的析构函数), 析构对象的顺序与构造的顺序相反,然后执行与异常信息匹配的catch块中的语句。

3. 命名空间

命名空间:一个由程序设计者命名的内存区域。根据需要指定一些有名字的空间域,把一些全局实体分别放在各个命名空间中,从而与其他全局实体分隔开来。

作用:处理程序中常见的同名冲突。

定义命名空间的格式:

​ namespace 名称{
​ 成员;
​ }

成员可以包括:变量(可以带有初始化);常量;函数(可以是定义或声明);结构体;类;模板;命名空间。

使用命名空间成员的方法:

命名空间名::命名空间成员名

  • 简化使用命名空间成员的方法

    1. 使用命名空间别名

    2. 使用“using 命名空间成员名”

    using后面的命名空间成员名必须是由命名空间限定的名字。例如

    1
    2
    using ns1::fun;	//声明其后出现的fun是属于ns1中的fun
    cout<<fun(5,3)<<endl; //fun函数相当于ns1::fun(5,3)

    注意: 在同一作用域中用using声明的不同命名空间的成员中不能有同名的成员。例如

    1
    2
    3
    using ns1::Stu;//声明其后出现的Stu是ns1中的Stu
    using ns2::Stu; //声明其后出现的Stu是ns2中的Stu
    Stu stud1; //产生了二义性,编译出错。

    using声明的有效范围是从using语句开始到using所在的作用域结束。

    1. 使用“using namespace 命名空间名;”

    用一个语句一次声明一个命名空间中的全部成员

    格式:

    using namespace命名空间名;

    例如:

    1
    using namespace ns1;

    声明了在本作用域中要用到命名空间ns1中的成员,在使用该命名空间的任何成员时都不必用命名空间限定。如果在作了上面的声明后有以下语句:

    1
    2
    3
    4
    //Student隐含指命名空间ns1中的Student
    Student stud1(101,″Wang″,18);
    //这里的fun函数是命名空间ns1中的fun函数
    cout<<fun(5,3)<<endl;

4. 标准命名空间std

标准C++库的所有的标识符都是在一个名为std的命名空间中定义的,或者说标准头文件(如iostream)中的函数、类、对象和类模板是定义在命名空间std中。

在程序中用到C++标准库时,需要使用std作为限定。

std::cout<<″OK.″<<endl;

在文件的开头加入以下using namespace声明:

using namespace std;

在std中定义和声明的所有标识符在本文件中都可以作为全局量来使用。但是应当绝对保证在程序中不出现与命名空间std的成员同名的标识符。

5. 使用早期的函数库

  1. 用C语言的传统方法。头文件名包括后缀.h

  2. 用C++的新方法。C++系统提供的头文件不包括后缀.h,例如iostream。C++所用的头文件名是在C语言的相应的头文件名(但不包括后缀.h)之前加一字母c。且这些函数都是在命名空间std中声明的,所以需在程序中声明std。

    1
    2
    3
    #include <cstdio>
    #include <cmath>
    using namespace std;

大多数C++编译系统既保留了C的用法,又提供了C++的新方法。下面两种用法等价

1
2
3
4
C传统方法 
#include <stdio.h>
#include <math.h>
#include <string.h>
1
2
3
4
5
C++新方法
#include <cstdio>
#include <cmath>
#include <cstring>
using namespace std;