c++面向对象程序设计教程(第四版)
1.面向对象程序设计
程序=对象+消息
程序一般由类的定义和类的使用两部分组成
程序中的操作都是通过想对象发送消息来实现的,对象接收到消息后,启动有关方法完成操作
对象=属性+行为(数据+函数)
类
一组具有相同属性和行为的对象的抽象
类和对象之间的关系:抽象和具体的关系
一个对象是类的一个实例
在面向对象程序设计中,先声明类,再由类生成其对象
消息与方法
对象的交互:对象之间的联系
消息传递机制:允许一个对象与另一个对象的交互
消息的性质:
- 同一个对象可以接受不同形式的多个消息,做出不同的响应
- 相同形式的消息可以传递给不同的对象,做出的响应可以是不同的
- 对消息的响应不是必须的,可以不响应
方法包括:界面,方法体
方法的界面:给出了 方法名 和 调用协议(成员函数的函数名和参数变)
方法体:实现某种操作的一系列计算步骤,也就是一段程序(成员函数的函数体)
基本体征
抽象:通过特定的实例抽取共同性质后形成概念的过程
封装:把数据和实现操作的代码集中起来放在对象内部,并尽可能隐蔽对象的内部细节
C++对象中的函数名就是对象的对外接口
好处:降低了人们操作对象的复杂程度,隐藏了复杂性,提供了代码重用性,减轻了开发软件系统的难度
各个对象相互独立、互不干扰
信息隐蔽:对外界隐蔽的做法,有利于数据安全,防止无关人员访问和修改数据
继承:某个类可以继承另一个类的特征和能力
若类之间具有继承关系,则它们具有以下特征:
1.类间具有共享特征
2.类间具有差别或新增部分
3.类间具有层次结构
作用:避免公用代码的重复开发,减少代码和数据冗余,增强一致性来减少模块间的接口和界面
多态:不同的对象收到相同的消息时执行不同的操作
增强了软件的灵活性和重用性,为软件开发与维护提供了极大的便利
优点
可提高程序的重用性
可控制程序的复杂性
可改善程序的可维护性
能更好地支持大型程序设计
增强了计算机处理信息的范围
能很好地适应新的硬件环境
2.C++概述
注释:1. //开始,到行尾结束 2. /**/,可以嵌套//
C++源程序文件扩展名.cpp
头文件: iostream:声明了程序所需的输入和输出操作的有关信息
using namespace std 使用命名空间std,可以保证对C++标准库操作的每一个特性都是唯一的,不至于发生命名冲突
使用、等都需要加using namespace std ,若采用带.h的头文件时,则不需要加
两种头文件不可混用
命名空间
见7.6
输入输出
标准输入流对象cin(c+in)标准输出流对象cout(c+out)更安全更方便
cin:在程序中用于代表标准输入设备,通常指键盘
>>在c++中仍保持右移功能,用于输入时表示将从标准输入流对象cin读取的数值传送给右方指定的变量
当输入字符串(即类型为string的变量)时,提取运算符“>>”的作用是跳过空白字符,读入后面的非空白字符,直到遇到另一个空白字符为止,并在串尾放一个字符串结束标志‘\0’。
cin>>x 必须加>>,x必须是基本数据类型不能是,void类型
允许输入一连串数据cin>>a>>b>>c,两个数据间用空白符(空格、回车、Tab键)分隔
cout:在程序中用于代表标准输出设备,通常指屏幕
<<在c++中仍保持左移功能,用于输入时表示将右方变量的值写到标准输出流对象cout中
cin<<y 必须加<<,y必须是基本数据类型不能是,void类型
允许输入一连串数据cout<<a+b<<c
I/O格式:设置转换基数的操作符:dec(十进制)、hex(十六进制)、oct(八进制)
换行操作符endl,作用与\n一样
cout<<hex<<x<<’ '<<dec<<x<< ’ '<<endl
灵活的局部变量说明
允许变量声明与可执行语句在程序中交替出现
允许在代码块中的任何地方说明局部变量,从说明点到该变量所在的最小分程序末的范围有效,且符合先定义后使用
结构名、联合名、枚举名可直接作为类型名
在定义变量时,不必在结构名、联合名、枚举名前加struct、union、enum
enum Bool {FALSE,TRUE};
struct String
{ char * ptr;
int length;
};
Bool done; //定义时不加struct、union、enum
String str; //c语言中必须加struct、union、enum
const修饰符
#define:#define PI 3.14 将程序中的PI替换为3.14,编译后程序中不再出现PI这个标识符,PI不是变量,无类型,不占存储单元,易出错
const:const int i=10 这个常量i是有类型的,占用存储单元,有地址,可以用指针指向它,但是不可修改,消除了#define的不安全性
const和指针:
- 指向常量的指针:指向常量的指针变量
const char * name="chen"; name[3]='a'; //出错,不允许改变指针所指的常量 name="zhang" //合法,可以改变指针所指的地址```
- 常指针:把指针所指的地址声明为对象
char * const name="chen";
name[3]='a'; //合法,允许改变指针所指的常量
name="zhang" //出错,不允许改变指针所指的地址
- 指向常量的常指针:这个指针本身不能改变,它所指向的地址中的数据也不能改变
const char * const name="chen";
name[3]='a'; //出错,不允许改变指针所指的常量
name="zhang" //出错,不允许改变指针所指的地址
说明:
- 用const定义int型常量,可以省略int
- 常量一旦建立,在程序的任何地方都不可改
- const定义的常量可以有自己的数据类型,c++的编译程序可以进行更严格的类型检查,具有连号的编译式的检查性
- 函数的形参也可以用const说明,用于保证形参在该函数内部不被改动
函数原型
在C++中,如果函数调用的位置在函数定义之前,则要求在函数调用之前必须对所调用的函数作函数原型声明,以说明函数的名称、参数类型与个数,以及函数返回值的类型,其主要目的是让c++编译程序进行检查,以确定调用函数的参数以及返回值类型与事先定义的原型是否相符,以保证程序的正确选。
- 函数原型的参数表中可以不包含参数的名字,只包含类型
- 函数定义由函数说明和函数体两个部分构成。函数说明部分与函数原型基本一样,但函数说明部分中的参数必须给出参数的名字,而且不能包含结尾的分号
- 主函数 main 不必进行原型说明,因为它被看成一个自动说明原型的函数。主函数是第1 个被执行的函数,而且不存在被别的函数调用的问题。
- 标准C++要求 main 函数必须声明为 int 型,即要求主函数带回一个整型函数值,如果函数执行不正常,则返回数值-1。
- 如果一个函数没有返回值,则必须在函数原型中注明返回类型为 void
- 如果函数原型中未注明参数,C++假定该函数的参数表为空(void)。
在c++中,f();与f(void);完全一样,表示该函数不带任何参数
在C语言中,f(void);表示该函数不带任何参数,f();的参数信息没有给出,可能有参数
void型指针
void通常表示无值,但将void作为指针的类型时,它却表示不确定的类型。这种void型指针是一种通用型指针,也就是说任何类型的指针值都可以赋给void类型的指针变量。
需要指出的是,这里说void型指针是通用指针,是指它可以接受任何类型的指针的赋值,但对已获值的void型指针,对它进行再处理,如输出或者传递指针值时,则必须再进行显式类型转换,否则会出错。
void* pc;
int i = 123;
char c = 'a';
pc = &i;
cout << pc << endl; //输出指针地址006FF730
cout << *(int*)pc << endl; //输出值123
pc = &c;
cout << *(char*)pc << endl; //输出值a
内联函数
在函数说明前冠以关键字“inline”,该函数就被声明为内联函数,又称内置函数。每当程序中出现对该函数的调用时,C++编译器使用函数体中的代码插入到调用该函数的语句处,同时用实参代替虚参,以便在程序运行时不再进行函数调用。
引入内联函数主要是为了消除调用函数时的系统开销,以提高运行速度。
说明:
- 内联函数在第一次被调用之前必须进行完整的定义,否则编译器将无法知道应该插入什么代码
- 在内联函数体内一般不能含有复杂的控制语句,如for语句和switch语句等
- 是一种空间换时间的措施,若内联函数较长,较复杂且调用较频繁时会使程序加长很多(一般1~5条语句且使用频繁才定义为内联函数)
- 使用内联函数替代宏定义(#define),能消除宏定义的不安全性
带有默认参数的函数
C++允许实参个数与形参个数不同
当进行函数调用时,编译器按从左到右的顺序将实参与形参结合,若未指定足够的实参,则编译器按顺序用函数原型中的默认值来补足所缺少的实参。
- 在函数原型中,所有取默认值的参数都必须出现在不取默认值的参数的右边。
int fun(int a, int b, int c = 111);
- 在函数调用时,若某个参数省略,则其后的参数皆应省略而采取默认值。不允许某个参数省略后,再给其后的参数指定参数值。
- 定义在调用前,应在函数定义中指定默认值,定义在调用后,必须在声明中给出默认值,此时定义中不再给默认值(可能会报错)
函数的重载
在同一作用域内,只要函数参数的类型不同,或者参数的个数不同,或者二者兼而有之,两个或者两个以上的函数可以使用相同的函数名。
说明:
- 调用重载函数时,函数返回值类型不在参数匹配检查之列。若函数的参数个数和类型都相同,而只有返回值类型不同,则不允许重载。
- 函数的重载与带默认值的函数一起使用时,有可能引起二义性。
void Drawcircle(int r = 0, int x = 0, int y = 0); void Drawcircle(int r); Drawcircle(20);
- 在调用函数时,如果给出的实参和形参类型不相符,C++的编译器会自动地做类型转换工作。如果转换成功,则程序继续执行,在这种情况下,有可能产生不可识别的错误。
void f_a(int x);
void f_a(long x);
int c=f_a(20.83); //无法确定转换为int还是long
作用域运算符 ::
通常情况下,如果有两个同名变量,一个是全局的,另一个是局部的,那么局部变量在其作用域内具有较高的优先权,它将屏蔽全局变量。
如果希望在局部变量的作用域内使用同名的全局变量,可以在该变量前加上“::”,此时::value代表全局变量value,“::”称为作用域标识符。
无名联合
C++中的一种特殊联合,在关键字union后没有给出联结名,可使得一组数据成员共享同一内存地址
union
{ int i;
double d;
}x;
在访问无名变量时,不能访问无名联合变量,而是应该访问联合变量中的成员,如 x.d x.i
强制类型转换
C语言
int i=10;
double x=(double)i;
C++
int i=10;
double x=double(i);
运算符new和delete
程序运行时,计算机的内存被分为4个区:程序代码区、全局数据区、堆和栈。其中,堆可由用户分配和释放。C语言中使用函数malloc()和free()来进行动态内存管理。C++则提供了运算符new和delete来做同样的工作,而且后者比前者性能更优越,使用更灵活方便。
说明:
- 用运算符new分配的空间,使用结束后应该用也只能用delete显式地释放,否则这部分空间将不能回收而变成死空间。
- 在使用运算符new动态分配内存时,如果没有足够的内存满足分配要求,new将返回空指针(NULL)。
- 使用运算符new可以为数组动态分配内存空间,这时需要在类型后面加上数组大小。
- new 可在为简单变量分配空间的同时,进行初始化
指针变量名=new 类型;
int * p;
p=new int;
指针变量名 = new 类型名[下标表达式];
int * p = new int[10];
delete []指针变量名;
指针变量名 = new 类型名(初值);
int *p;
p = new int(99)
delete 指针变量名; //指针变量保存着new分配的内存的首地址
delete p;
使用malloc函数时必须使用sizeof函数来计算所需要的字节数,new可以根据数据类型自动计算所要分配内存的大小,减少错误发生
new能够自动返回正确的指针类型,mall哦草必须在程序中进行强制数据类型转换
引用
建立引用的作用:为变量另起一个名字,变量的引用被认为变量的别名
- 引用与其所代表的变量共享同一内存单元,系统并不为引用另外分配存储空间。编译系统使引用和其代表的变量具有相同的地址。
- 引用并不是一种独立的数据类型,它必须与某一种类型的变量相联系。在声明引用时,必须立即对它进行初始化,不能声明完成后再赋值。声明一个引用时,必须同时用另一个变量的名字将它初始化
- 为引用提供的初始值,可以是一个变量或者另一个引用。
- 引用在初始化后不能再被重新声明为另一个变量的引用
- 指针是通过地址间接访问某个变量,而引用则是通过别名直接访问某个变量。
- 每次使用引用时,可以不书写 间接运算符*
- 不允许建立void类型的引用
- 不能建立引用的数组
- 不能建立引用的引用。不能建立指向引用的指针。引用本身不是一种数据类型,所以没有引用的引用,也没有引用的指针。
- 可以将引用的地址赋值给一个指针,此时指针指向的是原来的变量。
- 可以用const对引用加以限定,不允许改变该引用的值,但是它不阻止引用所代表的变量的值。
引用作为函数参数、使用引用作为函数值
#include <iostream>
using namespace std;
void swap(int &a, int &b)
{
int t = a;
a = b;
b = t;
}
int a[] = {1, 3, 5, 7, 9};
int & index(int i)
{
return a[i];
}
int main()
{
int a = 5, b = 10;
//交换数字a和b
swap(a, b);
cout << "a = " << a << " b = " << b << endl;
cout << index(2) << endl; //等价于输出元素a[2]的值
index(2) = 100; //等价于将a[2]的值赋为100;
cout << index(2) << endl;
return 0;
}
3.类和对象
1.基本概念
类的声明
比结构体类型更安全有效的数据类型——类
class 类名{
public:
公有数据成员;
公有成员函数;
protected:
保护数据成员;
保护成员函数;
private:
私有数据成员;
私有成员函数;
}; //最后一定要加分号 //从 { 到 } 是类体
- 一般情况下,一个类的数据成员应该声明为私有成员,成员函数声明为共有成员。这样,内部的数据整个隐蔽在类中,在类的外部根本就无法看到,使数据得到有效的保护,也不会对该类以外的其余部分造成影响,程序之间的相互作用就被降低到最小。
- 类声明中的关键字private、protected、public可以任意顺序出现。
- 若私有部分处于类的第一部分时,关键字private可以省略。这样,如果一个类体中没有一个访问权限关键字,则其中的数据成员和成员函数都默认为私有的。结构体默认公有
- 不能在类声明中给数据成员赋初值。
- 数据成员可以是任何数据类型,但是不能用 自动(auto)、寄存器(register)、外部(extern)进行说明
成员访问限定符:(声明各成员的访问属性)
- private:只能由本类的成员函数访问
- public:既可以本类的成员函数访问,也可以被类外的对象访问
- protected:只能由本类及其派生类的成员函数访问
成员函数的定义
可以访问本类中的任何组成员
- 在类的声明中只给出成员函数的原型,而成员函数的定义写在类的外部。在类外定义的一般形式是:
返回值类型 类名::成员函数名(参数表)
{
函数体
}
- 隐式定义:将成员函数直接定义在类的内部,没有inline,隐含地定义为内联
- 显式定义:在类声明中只给出成员函数的原型,而将成员函数的定义放在类的外部。加 inline
声明和定义时可以都加inline,也可以只在一处加
使用inline定义内联函数时,必须将类的声明和内联成员函数的定义放在同一个文件或头文件中,否则编译时无法进行代码置换
对象的定义及使用
一个类也就是用户声明的一个数据类型
声明一个类,并不接受和存储具体的值
对象的定义:
- 在声明类的同时,直接定义对象。在}后直接写出属于该类的对象名表
}op1, op2;
- 声明了类之后,在使用时再定义对象 ` Score op1, op2;
对象中成员的访问:
- 通过对象名和对象选择符 . (简称点运算符)
对象名.数据成员名 对象名.成员函数名[ (实参表) ]
- 通过指向对象的指针(在定义对象时,定义的是指向此对象的指针) ->操作符
class date{
public:
int year;
};
date d,*p;
p=&d;
cout<<p->year; // d.year (* p).year p->year 三者等价
类的作用域和类成员的访问属性
类的作用域:在类的声明中的一对花括号所形成的作用域。在类的作用域内,任何成员函数可以不受限制地访问该类中的其他成员
一般,公有成员是类的对外接口,私有成员是类的内部数据和内部实现不希望外界访问
将类的成员划分为不同的访问级别好处:信息隐蔽,数据保护
2.构造函数和析构函数
对象的初始化和构造函数
类声明中不能给数据成员赋初值
class Complex{
double real=0;/**/错误的**
};
如果一个类中的所有成员都是公有的,可以在定义对象时对数据成员进行初始化
class Complex{
public:
double real;
double imag;
};
Complex c1={1.1,2.2};
如果类中包含私有成员或保护成员,可以采用类中的公有成员函数来对对象中的数据成员赋初值
- 构造函数是是属于某一个类的,一种特殊的成员函数,它主要用于为对象分配空间,进行初始化。
- 构造函数的名字必须与类名相同,而不能由用户任意命名。否则编译程序将把它当做一般的成员函数来处理。
- 它可以有任意类型的参数,但不能具有返回值。可以由用户提供,也可以由系统生成。
- 编译系统自动生成的默认构造函数,不带任何参数,函数体是空的,只能为对象开辟数据成员存储空间,不能赋初值
- 与普通的成员函数一样,构造函数的函数体可以写在类体内,也可写在类体外。
- 当构造函数直接定义在类内时,系统将构造函数作为内联函数处理
- 构造函数一般声明为公有成员,但它不需要也不能像其他成员函数那样被显式地调用,它是在定义对象的同时被自动调用,而且只执行一次。
- 构造函数可以不带参数,此时对对象的初始化是固定的,
Complex(){ //不带参数的构造函数 real=0; imag=0; }
在建立对象时采用构造函数给数据成员赋初值,有两种形式:
-
类名 对象名 [ (实参表) ];
-
类名 * 指针变量名=new 类名 [ (实参表) ]; //这个对象没有名字,称为 无名对象 // delete 指针变量名
用成员初始化列表对数据成员初始化
在函数首部实现
类名 ::构造函数名 ( [参数表] ) :数据成员名1(初始值1),数据成员名2(初始值2), ......
{ //构造函数体
}
对于用const修饰的数据成员,或是引用类型的数据成员,不允许用赋值语句直接赋值,可以用成员初始化列表
构造函数的重载
#include <iostream>
using namespace std;
class Date
{ public:
Date (); //声明1个无参数的构造函数
Date (int y,int m,int d); //声明1个带有3个参数的构造函数
void showDate();
private:
int year;
int month;
int day; //一个类中可以包含多个构造函数,但对每个对象而言,建立对象时只执行其中一个构造函数
};
Date::Date () //只要类中定义了一个构造函数,系统将不再提供默认构造函数
{ year=2000; month=4; day=28;
}
Date::Date (int y,int m,int d)
{ year=y; month=m day=d;
}
inline void showDate()
{ cout<<year<<"."<<month<<"."<<day<<endl;
}
int main()
{ Date date1; //使用无参构造函数创建对象时,用这个Date date1; 不能用Date date1();表示声明一个函数名为date1返回值类型为Date的普通函数
date1.showDate();
Date date2(2002,11,14);
date2.showDate();
return 0;
}
带默认参数的构造函数
Complex (double r=0.0,double i=0.0); //在声明构造函数时指定默认参数
Complex::Complex(double r,double i) //在类外定义构造函数时,可以不再指定参数的默认值
{real=r; imag=i}
- 构造函数在类的声明外定义,默认参数必须在声明时指定
- 如果构造函数的全部参数都指定了默认值,则在定义对象时可以指定0个或1个或几个实参,这时的构造函数属于默认构造函数
- (一个类中只能有一个默认构造函数)
- 如果构造函数的全部参数都指定了默认值不能再定义重载构造函数
析构函数
- 通常用于撤销对象时的一些清理任务,如释放分配给对象的内存空间等。
- 析构函数与构造函数名字相同,但它前面必须加一个波浪号(~)。
- 析构函数没有参数和返回值,
- 不能被重载,因此只有一个。
- 当撤销对象时,编译系统会自动调用析构函数。
- 每个类必须有析构函数,如果没有显式的定义,编译系统会自动生成一个默认析构函数(函数体是空的)
- 除了主函数结束时,对象被撤销,系统会自动调用析构函数,在以下情况下,析构函数也会被调用
- 一个对象被定义在一个函数体内,当该函数被调用结束时,该对象会被释放
- 对象是使用new运算符动态创建的,在使用delete释放它时
3.对象数组和对象指针
对象数组
每一个数组元素都是对象的数组
有几个数组元素就调用几次构造函数
类名 数组名[下标表达式]
exam ob1 [4] = {89, 97, 79, 88}; //用只有1个参数的构造函数给对象数组赋值
Exam ob2 [4] = {89, 90}; //用不带参数和带1个参数的构造函数给对象数组赋值,
//先调用带1个参数的构造函数初始化ob2[0]和ob2[1],再调用不带参数的构造函数初始化ob2[2]和ob2[3],
Complex com [3]={
Complex (1,2),
Complex (3,4),
Complex (5,6)
}; //用带有多个参数的构造函数给对象数组赋值
对象指针
每一个对象在初始化后都会在内存中占有一定的空间。因此,既可以通过对象名访问对象,也可以通过对象地址来访问对象。对象指针就是用于存放对象地址的变量。声明对象指针的语法形式为:
类名 * 对象指针名;
用指针访问单个对象成员 exe ob,*p; p=&ob; p->show(); (*p).show();
用对象指针访问对象数组 exe ob[2],*p; p=ob;
this指针
实际上,给对象赋值就是给对象的数据成员赋值
C++的编译系统只用了一段空间来存放共同的函数代码,
每一个对象的存储空间都只是数据成员所占用的存储空间,函数代码是存储在对象空间之外
称为自引用指针
每当创建一个对象时,系统把this指针初始化为指向该对象,this指针的值是当前调用成员函数的对象的起始地址
this指针是隐式使用的,是作为参数被传递给成员函数的
void disp() {cout<<"x="<<x<<endl;}
实际使用时,c++编译系统把它处理为:
void disp(*this) {cout<<"x="<<this->x<<endl;}
4.string类
C++支持两种类型的字符串,第一种是C语言中介绍过的、包括一个结束符’\0’(即以NULL结束)的字符数组,标准库函数提供了一组对其进行操作的函数,可以完成许多常用的字符串操作。如:strcpy、ctrcat、strlen等
C++仍保留了这种格式字符串;
C++标准库中声明了一种更方便的字符串类型,即字符串类string,类string提供了对字符串进行处理所需要的操作。使用string类必须在程序的开始包括头文件string,即要有以下语句:#include <string>
string 对象1,对象2,......;
strnig str1, str2("china"), str3="administrator"
在表达式中,可以string类对象和以‘\0’结束的字符串混在一起使用
5.向函数传递对象
- 使用对象作为函数参数:
void show(类名 对象名)
对象可以作为参数传递给函数,其方法与传递其他类型的数据相同。在向函数传递对象时,是通过“传值调用”的方法传递给函数的。因此,函数中对对象的任何修改均不影响调用该函数的对象(实参本身)。 - 使用对象指针作为函数参数:
对象指针可以作为函数的参数,使用对象指针作为函数参数可以实现传值调用,即在函数调用时使实参对象和形参对象指针变量指向同一内存地址,在函数调用过程中,形参对象指针所指的对象值的改变也同样影响着实参对象的值。 - 使用对象引用作为函数参数:
在实际中,使用对象引用作为函数参数非常普遍,大部分程序员喜欢使用对象引用替代对象指针作为函数参数。因为使用对象引用作为函数参数不但具有用对象指针做函数参数的优点,而且用对象引用作函数参数将更简单、更直接。
6.对象的赋值和复制
对象赋值语句
B=A; 将对象A的数据成员逐位复制给B
- 两个对象的类型必须相同
- 当类中存在指针时,可能会出错
拷贝构造函数
- 形参是本类对象的引用,作用是:在建立一个对象时使用一个已经存在的对象去初始化这个新对象
Point p2(p1);
- 因为拷贝构造函数也是一种构造函数,所以其函数名与类名相同,并且该函数也没有返回值。
- 拷贝构造函数只有一个参数,并且是同类对象的引用。
- 每个类都必须有一个拷贝构造函数。可以自己定义拷贝构造函数,用于按照需要初始化新对象;如果没有定义类的拷贝构造函数,系统就会自动生成一个默认拷贝构造函数,用于复制出与数据成员值完全相同的新对象。
自定义:
类名::类名(const 类名 &对象名)
{
//函数体 通过自定义可以 有选择有变化地复制,如乘2后赋值
}
调用:
类名 对象2(对象1);//代入法
类名 对象2=对象1; //赋值法
当类中有指针类型时,调用默认拷贝构造函数有时会产生错误
调用拷贝构造函数的三种情况:
- 当用类的一个对象去初始化该类的另一个对象时;
- 当函数的形参是类的对象,调用函数进行形参和实参结合时;
- 当函数的返回值是对象,函数调用完毕将返回值(对象)带回函数调用处时。
7.静态成员
为了实现一个类的多个对象之间的数据共享
可以全局变量
将一个数据成员说明为static,静态数据成员。无论建立多少个类的对象,都只有一个静态数据成员的拷贝
static 数据类型 数据成员名;
- 静态数据成员的定义与普通数据成员相似,但前面要加上static关键字。
- 静态数据成员初始化应在类外单独进行,而且应在定义对象之前进行。一般在main()函数之前、类声明之后的特殊地带为它提供定义和初始化。
数据类型 类名::静态数据成员名=初始值;
- 静态数据成员属于类(准确地说,是属于类中对象的集合),而不像普通数据成员那样属于某一对象,因此,可以使用“类名::”访问静态的数据成员。格式如下:
类名::静态数据成员名
- 静态数据成员与静态变量一样,是在编译时创建并初始化。它在该类的任何对象被建立之前就存在。因此,公有的静态数据成员可以在对象定义之前被访问。对象定以后,公有的静态数据成员也可以通过对象进行访问。
对象名.静态数据成员名; 对象指针->静态数据成员名;
- 私有静态数据成员不能在类外直接访问
在类定义中,前面有static说明的成员函数称为静态成员函数。静态成员函数属于整个类,是该类所有对象共享的成员函数,而不属于类中的某个对象。静态成员函数的作用不是为了对象之间的沟通,而是为了处理静态数据成员。定义静态成员函数的格式如下:
static 返回类型 静态成员函数名(参数表);
类名::静态成员函数名(实参表);
对象.静态成员函数名(实参表);
对象指针->静态成员函数名(实参表);
- 静态函数成员主要用来访问静态成员函数。当它与静态数据成员一起使用时,达到了对同一个类中对象之间共享数据的目的。
- 私有静态成员函数不能被类外部的函数和对象访问。
- 使用静态成员函数的一个原因是,可以用它在建立任何对象之前调用静态成员函数,以处理静态数据成员,这是普通成员函数不能实现的
- 编译系统将静态成员函数限定为内部连接,也就是说,与现行文件相连接的其他文件中的同名函数不会与该函数发生冲突,维护了该函数使用的安全性,这是使用静态成员函数的另一个原因。
- 静态成员函数是类的一部分,而不是对象的一部分。如果要在类外调用公有的静态成员函数,
类名::静态成员函数名()
- 非静态成员函数有this指针。静态成员函数没有this指针,可以直接访问本类中的静态数据成员,一般不访问非静态成员,只能通过对象名(或对象指针,对象引用)访问对象的非静态成员
8.友元
类的主要特点之一是数据隐藏和封装,即类的私有成员(或保护成员)只能在类定义的范围内使用,也就是说私有成员只能通过它的成员函数来访问。但是,有时为了访问类的私有成员而需要在程序中多次调用成员函数,这样会因为频繁调用带来较大的时间和空间开销,从而降低程序的运行效率。为此,C++提供了友元来对私有或保护成员进行访问。友元包括友元函数和友元类。
友元函数
友元函数既可以是不属于任何类的非成员函数,也可以是另一个类的成员函数。友元函数不是当前类的成员函数,但它可以访问该类的所有成员,包括私有成员、保护成员和公有成员。
- 在类中声明友元函数时,需要在其函数名前加上关键字friend。
- 此声明可以放在公有部分,也可以放在保护部分和私有部分。
- 友元函数可以定义在类内部,也可以定义在类外部。
将非成员函数声明为友元函数
class Girl{
public;
......
friend void disp(Girl &);
}
void disp(Girl &x)
{ cout<<x.name<<endl; }
- 友元函数虽然可以访问类对象的私有成员,但他毕竟不是成员函数。因此,在类的外部定义友元函数时,不必像成员函数那样,在函数名前加上“类名::”。
- 因为友元函数不是类的成员,所以它不能直接访问对象的数据成员,也不能通过this指针访问对象的数据成员,它必须通过作为入口参数传递进来的对象名(或对象指针、对象引用)来访问该对象的数据成员。
- 友元是对类的封装机制的补充,友元函数提供了不同类的成员函数之间、类的成员函数与一般函数之间进行数据共享的机制。尤其当一个函数需要访问多个类时,友元函数非常有用,普通的成员函数只能访问其所属的类,但是多个类的友元函数能够访问相关的所有类的数据。
class Girl{
public;
......
friend void prdata(const Girl &,const Boy &);
}
class Boy{
public;
......
friend void prdata(const Girl &,const Boy &);
}
void prdata(const Girl &g,const Boy &b)
{ cout<<g.name<<endl;
cout<<b.name<<endl; }
将成员函数声明为友元函数
- 一个类的成员函数可以作为另一个类的友元,它是友元函数中的一种,称为友元成员函数。
- 友元成员函数不仅可以访问自己所在类对象中的私有成员和公有成员,
- 还可以访问friend声明语句所在类对象中的所有成员,这样能使两个类相互合作、协调工作,完成某一任务。
- 一个类的成员函数作为另一个类的友元函数时,必须先定义这个类。并且在声明友元函数时,需要加上成员函数所在类的类名;
class Girl; //为Girl类的提前引用声明
class Boy{
public;
......
void prdata(Girl &);
}
class Girl{
public;
......
friend void Boy::prdata(Girl &); //声明类Boy的成员函数prdata为类Girl的友元函数
}
void prdata(Girl &x)
{ cout<< name<<endl;
cout<<x.name<<endl; }
int main ()
{ Boy b("李华",11);
Girl g("小红",22);
b.prdata(g);
return 0; } //结果是两个对象的内容
友元类
friend 类名; //此类名为友元类的,这条语句可以放在公有部分,也可在私有部分
class Y{......};
class X{
....
friend Y;
};
当一个类被说明为另一个类的友元类时,它所有的成员函数都成为另一个类的友元函数,这就意味着作为友元类中的所有成员函数都可以访问另一个类中的所有成员。
友元关系单向的,不具有交换性和传递性。
9.类的组合
在一个类中内嵌另一个类的对象作为数据成员,称为类的组合。该内嵌对象称为对象成员,又称为子对象。
class A{
···
};
class B{
A a;
public:
···
};
class X{
类名1 对象成员1;
类名2 对象成员2;
...
};
类X的构造函数的定义形式:
X::X(形参表0) : 对象成员1(形参表1),对象成员2(形参表2),.......
{ 函数体 } //对象成员1(形参表1),对象成员2(形参表2),.......是 初始化表,作用是对对象成员进行初始化
当调用构造函数X::X()时,首先按各内嵌对象成员在类声明中的顺序依次调用它们的构造函数进行初始化,最后再执行类X的构造函数体,初始化其他成员。析构函数的调用顺序与构造函数相反
10.常类型
常类型的引入就是为了既保护数据共享又防止数据被改动。
常类型是指使用类型修饰符const说明的类型,
常类型的变量或对象成员的值在程序运行期间是不可改变的。
常引用
如果在说明引用时用const修饰,则被说明的引用为常引用。常引用所引用的对象不能被更新。如果用常引用做形参,便不会产生对实参的不希望的更改。
const 类型 & 引用名;
经常用作函数的形参,常参数`
常对象
如果在说明对象时用const修饰,则被说明的对象为常对象。常对象的数据成员在对象的整个生存期内不能改变,为常量且必须要有初值。
类名 const 对象名[ 参数表 ];
或者
const 类名 对象名[ 参数表 ];
在定义对象是必须进行初始化
常对象成员
- 类的数据成员可以是常量或常引用,
- 使用const说明的数据成员称为常数据成员。
- 如果在一个类中说明了常数据成员,那么构造函数就只能通过成员初始化列表对该数据成员进行初始化,而任何其他函数都不能对该成员赋值。
类型说明符 函数名(参数表) const;
- const是函数类型的一个组成部分,因此在声明函数和定义函数时都要有关键字const。在调用时不必加const。
- 关键字const可以被用于对重载函数进行区分。
- 常成员函数可以访问常数据成员,也可以访问普通数据成员。
- 常数据成员可以被常成员函数访问,也可以被普通成员函数访问
- 常对象只能调用它的常成员对象,而不能调用普通成员函数。常成员函数是常对象唯一的对外接口。
- 常对象函数不能更新对象的数据成员,也不能调用该类的普通成员函数,这就保证了在常成员函数中绝不会更新数据成员的值。
4.派生类与继承
继承可以在已有类的基础上创建新的类,新类可以从一个或多个已有类中继承成员函数和数据成员,而且可以重新定义或加进新的数据和函数,从而形成类的层次或等级。其中,已有类称为基类或父类,在它基础上建立的新类称为派生类或子类。
从已有类产生新类的过程,类的派生
1.派生类的概念
派生类的声明
class 派生类名: 继承方式 基类名{
派生类新增的数据成员和成员函数
}; //继承方式:public或private或protected 默认为私有继承
派生类的构成
派生类中的成员包括从基类继承过来的成员和自己增加的成员
1)派生类从基类接受成员
2)调整从基类接受来的成员
一方面是改变基类成员在派生类中的访问属性
另一方面是派生类可以对基类的成员进行重定义:
在派生类中声明一个与基类成员同名的成员,则派生类中的新成员会覆盖基类中的同名成员
3)在派生类中增加新成员
基类中的构造函数和析构函数是不能被继承的
基类成员在派生类中的访问属性
基类中的成员 | 在公有派生类中的访问属性 | 在私有派生类中的访问属性 | 在保护派生类中的访问属性 |
---|---|---|---|
私有成员 | 不可直接访问 | 不可直接访问 | 不可直接访问 |
公有成员 | 公有 | 私有 | 保护 |
保护成员 | 保护 | 私有 | 保护 |
派生类对基类成员的访问规则
内部访问:有派生类中的新增成员函数对基类继承来的成员的访问
对象访问:在派生类外部,通过派生类的对象对从基类继承来的成员的访问
- 私有继承的访问规则
基类中的成员 | 私有成员 | 公有成员 | 保护成员 |
---|---|---|---|
内部访问 | 不可访问 | 可访问 | 可访问 |
外部访问 | 不可访问 | 不可访问 | 不可访问 |
- 公有继承的访问规则
基类中的成员 | 私有成员 | 公有成员 | 保护成员 |
---|---|---|---|
内部访问 | 不可访问 | 可访问 | 可访问 |
外部访问 | 不可访问 | 可访问 | 不可访问 |
- 保护继承的访问规则
基类中的成员 | 私有成员 | 公有成员 | 保护成员 |
---|---|---|---|
内部访问 | 不可访问 | 可访问 | 可访问 |
外部访问 | 不可访问 | 不可访问 | 不可访问 |
2.派生类的构造函数和析构函数
派生类的构造函数和析构函数的执行顺序
当创建派生类对象时,首先执行基类的构造函数,后调用派生类的构造函数的顺序执行。析构函数的调用顺序与构造函数的调用顺序正好相反,先调用派生类的析构函数,后调用基类的析构函数。
派生类的构造函数和析构函数的构造规则
- 当基类的构造函数没有参数,或没有显示定义构造函数,派生类可以不向基类传递参数,可以不定义构造函数
- 当基类含有带参数的构造函数时,派生类必须定义构造函数,以提供把参数传递给基类构造函数的途径
- 若基类使用默认构造函数或不带参数的构造函数,则在派生类中定义构造函数时可略去“:基类构造函数名(参数表)”,此时若派生类也不需要构造函数,则可不定义构造函数。
- 如果派生类的基类也是一个派生类,每个派生类只需负责其直接基类数据成员的初始化,依次上溯。
派生类名 (参数总表): 基类名(参数表)
{
派生类新增数据成员的初始化语句
}
派生类名 (参数总表)中包括参数的类型和参数名
基类名(参数表)中只有参数名,没有类型,因为这里是调用基类构造函数,这些参数是实参,可以为派生类构造函数总参数中的参数,也可以是常量或全局部变量
- 派生类中可以根据需要定义析构函数,用于对派生类中的新增成员清理,基类的清理仍有基类的析构函数负责
- 析构函数是不带参数的,派生类中是否要自定义析构函数与它所属的基类的析构函数无关
当派生类中含有内嵌的对象成员(子对象)时:
派生类名 (参数总表): 基类名(参数表0),对象成员名1(参数表1),......
{
派生类新增数据成员的初始化语句
}
定义派生类对象时,,构造函数的执行顺序:
- (1)调用基类的构造函数,进行初始化
- (2)调用内嵌的对象成员的构造函数,进行初始化
- (3)执行派生类的构造函数体,进行初始化
- 派生类中含有多个内嵌的对象成员时,调用内嵌的对象成员的构造函数顺序由在类中声明的顺序确定
- 如果一个派生类的基类也是一个派生类,每个派生类只需负责其直接基类数据成员的初始化,依次上溯
3.调整基类成员在派生类中的访问属性的其他方法
同名成员
派生类可以声明与基类成员同名的成员。在没有虚函数的情况下,如果在派生类中定义了与基类成员同名的成员,则称派生类成员覆盖了基类的同名成员,在派生类中使用这个名字意味着访问在派生类中声明的成员。为了在派生类中使用与基类同名的成员,必须在该成员名之前加上基类名和作用域标识符“::”,即
基类名::成员名
class X{
public:
int f();
};
class Y:public X{
public:
int f();
void q();
};
void Y::q()
{ f(); //表示访问派生类中的f(),即Y::f()
X::f(); //表示访问基类中的f()
}
Y obj;
obj,f(); //表示访问派生类中的f(),即Y::f()
obj.X::f();
访问声明
访问声明的方法就是把基类的保护成员或共有成员直接写在私有派生类定义式中的同名段中,同时给成员名前冠以基类名和作用域标识符“::”。利用这种方法,该成员就成为派生类的保护成员或共有成员了。
class B:private A{
public: //
.....
A::print; //printA中的函数
......
};
- 数据成员也可以使用访问声明
- 访问声明中只包含不带类型和参数的函数名或变量名
- 不能改变成员在基类中的访问属性
- 对于基类中的重载函数名,访问声明将对基类中所有同名函数起作用
4.多重继承
多重继承派生类的声明
class 派生类名:继承方式1 基类名1,.......,继承方式n 基类名n{
派生类新增的数据成员和成员函数
};
//不写继承方式则默认是private
多重继承派生类的构造函数和析构函数
派生类名 (参数总表): 基类名1(参数表1),基类名2(参数表2),......,基类名n(参数表n)
{
派生类新增成员的初始化语句
}
- 多重继承派生类构造函数必须同时负责该派生类所有基类构造函数的调用。
- 多继承构造函数的调用顺序与单继承构造函数的调用顺序相同,也是遵循先调用基类的构造函数,再调用对象成员的构造函数,最后调用派生类构造函数的原则。析构函数的调用与之相反。
- 处于同一层次的各个基类构造函数的执行顺序,取决于声明派生类时所指定的各个基类的顺序,与派生类构造函数中所定义的成员初始化列表的各项顺序没有关系
虚基类
- 虚基类的作用: 如果一个类有多个直接基类,而这些直接基类又有一个共同的基类,则在最低层的派生类中会保留这个间接的共同基类数据成员的多份同名成员。在访问这些同名成员时,必须在派生类对象名后增加直接基类名,使其唯一地标识一个成员,以免产生二义性。
- 在C++中,如果想使这个公共的基类只产生一个复制,可以将这个基类说明为虚基类
虚基类在派生类中的声明 //用在中间的类,base,base1,base2,derved,在base1,base2需要
class 派生类名:virtual 继承方式 基类名{
...}
#include <iostream>
#include <string>
using namespace std;
class Base{
protected:
int a;
public:
Base(){
a = 5;
cout << "Base a = " << a << endl;
}
};
class Base1: public Base{
public:
Base1() {
a = a + 10;
cout << "Base1 a = " << a << endl;
}
};
class Base2: public Base{
public:
Base2() {
a = a + 20;
cout << "Base2 a = " << a << endl;
}
};
class Derived: public Base1, public Base2{
public:
Derived() {
cout << "Base1::a = " << Base1::a << endl;
cout << "Base2::a = " << Base2::a << endl;
}
};
int main() {
Derived obj;
return 0;
}
Base a = 5
Base1 a = 15
Base a = 5
Base2 a = 25
Base1::a = 15
Base2::a = 25
class Base1:virtual public Base{
public:
Base1() {
a = a + 10;
cout << "Base1 a = " << a << endl;
}
};
class Base2:virtual public Base{
public:
Base2() {
a = a + 20;
cout << "Base2 a = " << a << endl;
}
};
- 虚基类的初始化与一般的多继承的初始化在语法上是一样的,但构造函数的调用顺序不同。
- 如果在虚基类中定义有带形参的构造函数,并且没有定义默认形式的构造函数,则整个继承结构中,所有直接或间接的派生 类都必须在构造函数的成员初始化列表中列出对虚基类构造函数的调用,以初始化在虚基类中定义的数据成员。
- 建立一个对象时,如果这个对象中含有从虚基类继承来的成员,则虚基类的成员是由最远派生类的构造函数通过调用虚基类 的构造函数进行初始化的。该派生类的其他基类对虚基类构造函数的调用都被自动忽略。
- 若同一层次中同时包含虚基类和非虚基类,应先调用虚基类的构造函数,再调用非虚基类的构造函数,最后调用派生类构造函数。
- 对于多个虚基类,构造函数的执行顺序仍然是先左后右,自上而下。
- 若虚基类由非虚基类派生而来,则仍然先调用基类构造函数,再调用派生类的构造函数。
- 关键字virtual与派生方式关键字(public/private/protected)的先后顺序无关
- 允许一个基类在作为某些派生类虚基类的同时,又作为另一些派生类的非虚基类
5.基类与派生类对象之间的赋值兼容关系
在一定条件下,不同类型的数据之间可以进行类型转换,如可以将整型数据赋值给双精度型变量。在赋值之前,先把整型数据转换成双精度数据,然后再把它赋给双精度变量。这种不同数据类型之间的自动转换和赋值,称为赋值兼容。在基类和派生类对象之间也存有赋值兼容关系,
基类和派生类对象之间的赋值兼容规则是指在需要基类对象的任何地方,都可以用公有派生类的对象代替。
- 派生类可以向基类对象赋值
- 派生类可以初始化基类对象的引用
- 派生类对象的地址可以赋给指向基类对象的指针
- 如果函数的形参是基类对象或基类对象的引用,在调用函数时可以用派生类对象作为实参
- 声明为指向基类对象的指针可以指向它的公有派生对象,但不允许指向它的私有派生对象
- 不允许将一个声明为指向派生类对象的指针指向它的基类的对象
5.多态性
多态性是面向对象程序设计的重要特征之一。
多态性机制不仅增加了面向对象软件系统的灵活性,进一步减少了冗余信息,而且显著提高了软件的可重用性和可扩充性。
多态性就是不同对象收到相同的消息时,产生不同的行为
1.编译时的多态性和运行时的多态性。
在C++中,多态的实现和联编这一概念有关。
一个源程序经过编译、连接,成为可执行文件的过程是把可执行代码联编在一起的过程
接口访问功能不同的函数,从而实现“一个接口,多种方法”。
在C++中,多态性的实现和联编(也叫绑定)这一概念有关。一个源程序经过编译,连
接,成为可执行文件的过程是把可执行代码联编(或称装配)在一起的过程。其中在运愆多
前就完成的联编称为静态联编,又叫前期联编;而在程序运行时才完成的联编叫动态联编,
也称后期联编。
静态联编是指系统在编译时就决定如何实现某一动作。静态联编要求在程序编译时就
知道调用函数的全部信息。因此,这种联编类型的函数调用速度很快。效率高是静态联编
的主要优点。
动态联编是指系统在运行时动态实现某一动作。采用这种联编方式,一直要到程序运
行时才能确定调用哪个函数。动态联编的主要优点是:提供了更好的灵活性、问题抽象性
和程序易维护性。
静态联编支持的多态性称为编译时多态性,也称静态多态性。在C++中,编译时多。
态性是通过函数重载(包括运算符重载)和模板(在第 6 章企绍)实现的。利用函数重载
机制,在调用同名的函数时,编译系统可根据实参的具体情况确立所要调用的是哪个
函数。
动态联编所支持的多态性称为运行时多态性,也称动态多态性。在C++ 中,运行时多
态性是通过虚函数来实现的。
2.运算符重载
运算符重载:通过创建运算符重载函数来实现
运算符重载函数可以是类外定义的普通函数,也可以是类的成员函数或友元函数
在进行运算符重载时,必须定义一个运算符重载函数,名字为operator运算符 如operator+
类Complex的两个对象的相加
Complex operator+ (Complex om1,Complex om2)
{
Complex temp;
temp.real=om1.real+om2.real;
temp.imag=om1.imag+om2.imag;
return temp;
}
total=com1+com2; //或者
total=operator+(com1,com2);
- C++不能重载的运算符:
. 成员访问运算符
. * 成员指针访问运算符
:: 作用域运算符
sizeof 长度运算符
?: 条件运算符
- 只能对已有的c++运算符进行重载,不允许自定义新的运算符
- 一般,重载的功能应该与原有的功能相类似
- 重载不能改变运算符的操作对象的个数
- 重载不能改变运算符原有的优先级
- 重载不能改变运算符原有的结合特性
- 运算符重载函数的参数至少应有一个是类对象(或类对象的引用),防止用户修改用于标准类型数据的运算符性质
- 运算符重载函数可以是类外定义的普通函数,也可以是类的成员函数或友元函数
- 一般用于类对象的运算符必须重载,但赋值运算符“=”不需要。
- 某些情况下,如数据成员中包含指向动态分配内存的指针成员时,系统提供的对象赋值运算符函数不能满足需求,在赋值时可能出错
友元运算符重载函数
- 在类外定义的运算符重载函数只能访问类中的公有数据成员
在类的内部,定义
friend 函数类型 operator 运算符 (形参表)
{
函数体
}
class X{
...
friend 函数类型 operator 运算符 (形参表);
...
};
函数类型 operator 运算符 (形参表)
{
函数体
}
- 运算符重载函数可以是返回任何类型,也可以是void类型,通常与它所操作的类的类型相同
- 有的运算符不能定义为友元运算符重载函数,如赋值运算符=、下标运算符[]、函数调用运算符()
- 友元运算符重载函数重载的是双目运算符,则参数表中有两个操作数
- 友元运算符重载函数重载的是单目运算符,则参数表中有一个操作数
成员运算符重载函数
在类的内部,定义
函数类型 operator 运算符 (形参表)
{
函数体
}
class