目录
第二章 类和对象
客观世界中任何一个事物都可以看成一个对象(object)。
对象可大可小,是构成系统的基本单位。
任何一个对象都应当具有这两个要素,即属性(attribute)和行为(behavior),它能根据外界给的信息进行相应的操作。一个对象往往是由一组属性和一组行为构成的。
类是对象的抽象,而对象则是类的特例,或者说是类的具体表现形式。
类是对象的抽象,而对象是类的具体实例。
类是抽象的,不占用内存,而对象是具体的,占用存储空间。
抽象:抽象的过程是将有关事物的共性归纳、集中的过程。
抽象的作用是表示同一类事物的本质。
类类型声明的一般形式如下:
class 类名{
private:
私有的数据和成员函数;
public:
公用的数据和成员函数;
protected:
受保护的数据和成员函数;
};
三种访问权限:private、protected、public
private/public/protectd称为成员访问限定符,有如下性质:
声明为private/public/protected的成员的次序任意。
在类体中既不写关键字private,又不写public,就默认为private。
关键字private和public 可以分别出现多次。
每个部分的有效范围到出现另一个访问限定符或类体结束时为止。
类的成员函数(简称类函数)是函数的一种,它与一般函数的区别只是:
它是属于一个类的成员,出现在类体中,可以被指定为private、public或protected。
在使用类函数时,要注意调用它的权限(它能否被调用)以及它的作用域(函数能使用什么范围中的数据和函数)。
成员函数的定义可以直接写在类中,也可以写到类外。
类内定义的成员函数,已被隐含地指定为内置函数。
如果成员函数不在类体内定义,而在类体外定义,系统并不把它默认为内置(inline)函数。
编译系统中每个对象所占用的存储空间只是该对象的数据部分所占用的存储空间,而不包括函数代码所占用的存储空间。
调用不同对象的成员函数时都是执行同一段函数代码,但是执行结果一般是不同的。
不同的对象使用的是同一个函数代码段,通过this的指针,用来指向不同的对象,从而对不同对象中的数据进行操作。this是类的成员函数的形参表中隐含的,它的类型是所属类的类型。当程序中调用类的成员函数时,this被自动初始化为发出函数调用的对象的地址。
构造函数
注意:类的数据成员是不能在声明类时初始化的。
如果一个类中所有的成员都是公用的,则可以在定义对象时对数据成员进行初始化。
#include<iostream>
#include<string>
using namespace std;
class Person {
public:
string name;
int age,id;
public:
void show(){
cout<<"[name="<<name<<",age="<<age<<",id="<<id<<"]"<<endl;
}
};
int main() {
Person p= {"zhangfs",12,1};
p.show();
return 0;
}
如果数据成员是私有的,或者类中有private或protected的成员的时候不行。
为了解决这个问题,C++提供了构造函数(constructor)来处理对象的初始化。
构造函数是一种特殊的成员函数,与其他成员函数不同,不需要用户来调用它,而是在建立对象时自动执行。构造函数的名字必须与类名同名,而不能由用户任意命名,以便编译系统能识别它并把它作为构造函数处理。它不具有任何类型,不返回任何值。
构造函数的功能是由用户定义的,用户根据初始化的要求设计函数体和函数参数。
构造函数的作用是在对象被创建时,使用特定的值构造对象,或者说将对象初始化为一个特定的状态。
可以在类内完成定义,也可以只在类内对构造函数进行声明而在类外定义构造函数。
#include<iostream>
#include<string>
using namespace std;
class Person {
public:
string name;
int age,id;
public:
void show();
Person(string _name,int _age,int _id);//构造函数
};
Person::Person(string _name,int _age,int _id){
name = _name, age = _age, id = _id;
}
void Person::show(){
cout<<"[name="<<name<<",age="<<age<<",id="<<id<<"]"<<endl;
}
int main() {
//构造函数在对象建立时,由系统自动调用,用户不能调用。
Person p("zhangsan",18,1);
p.show();
return 0;
}
有关构造函数的使用,有以下说明:
(1) 在类对象进入其作用域时调用构造函数。
(2) 构造函数没有返回值,因此也不需要在定义构造函数时声明类型,这是它和一般函数的一个重要的不同之点。
(3) 构造函数不需用户调用,也不能被用户调用。
(4) 在构造函数的函数体中不仅可以对数据成员赋初值,而且可以包含其他语句。但是一般不提倡在构造函数中加入与初始化无关的内容,以保持程序的清晰。
(5) 如果用户自己没有定义构造函数,则C++系统会自动生成一个构造函数,只是这个构造函数的函数体是空的,也没有参数,不执行初始化操作。
有时用户希望对不同的对象赋予不同的初值。
可以采用带参数的构造函数,在调用不同对象的构造函数时,从外面将不同的数据传递给构造函数,以实现不同的初始化。
用参数初始化表对数据成员初始化
C++中,除了可以在构造函数的函数体内通过赋值语句对数据成员实现初始化之外,C++还提供另一种初始化数据成员的方法:参数初始化表来实现对数据成员的初始化。
这种方法不在函数体内对数据成员初始化,而是在函数首部实现。
class T{
private:
int a,b,c;
}
T(int _a,int _b, int _c):a(_a),b(_b),c(_c){}
这种写法方便、简练,尤其当需要初始化的数据成员较多时更显其优越性。
甚至可以直接在类体中(而不是在类外)定义构造函数。
在一个类中可以定义多个构造函数,以便对类对象提供不同的初始化的方法。
这些构造函数具有相同的名字,而参数的个数或参数的类型不相同。
这称为构造函数的重载,函数重载的知识也适用于构造函数。
说明:
(1) 调用构造函数时不必给出实参的构造函数,称为默认构造函数(default constructor)。
显然,无参的构造函数属于默认构造函数。一个类只能有一个默认构造函数。
(2) 如果在建立对象时选用的是无参构造函数,应注意正确书写定义对象的语句。
(3) 尽管在一个类中可以包含多个构造函数,但是对于每一个对象来说,建立对象时只执行其中一个构造函数,并非每个构造函数都被执行。
在函数中可以使用有默认值的参数。在构造函数中也可以采用这样的方法来实现初始化。
构造函数中参数的值既可以通过实参传递,也可以指定为某些默认值,即如果用户不指定实参值,编译系统就使形参取默认值。
Person(string _name,int _age=0,int _id=0);//有默认值的参数
说明:
(1) 应该在声明构造函数时指定默认值,而不能只在定义构造函数时指定默认值。
(2) 在声明构造函数时,形参名可以省略。
(3) 如果构造函数的全部参数都指定了默认值,则在定义对象时可以给一个或几个实参,也可以不给出实参。
(4) 在一个类中定义了全部是默认参数的构造函数后,不能再定义重载构造函数。
一般不应同时使用构造函数的重载和有默认参数的构造函数
析构函数
析构函数(destructor)也是一个特殊的成员函数,它的作用与构造函数相反,它的名字是类名的前面加一个“~”符号。在C++中"~"是位取反运算符,从这点也可以想到: 析构函数是与构造函数作用相反的函数。当对象的生命期结束时,会自动执行析构函数。
具体地,如果出现以下几种情况,程序就会执行析构函数:
①如果在一个函数中定义了一个对象(它是自动局部对象),当这个函数被调用结束时,对象应该释放,在对象释放前自动执行析构函数。
②static局部对象在函数调用结束时对象并不释放,因此也不调用析构函数,只在main函数结束或调用exit函数结束程序时,才调用static局部对象的析构函数。
③如果定义了一个全局对象,则在程序的流程离开其作用域时(如main函数结束或调用exit函数) 时,调用该全局对象的析构函数。
④如果用new运算符动态地建立了一个对象,当用delete运算符释放该对象时,先调用该对象的析构函数。
析构函数的作用并不是删除对象,而是在撤销对象占用的内存之前完成一些清理工作,使这部分内存可以被程序分配给新对象使用。程序设计者事先设计好析构函数,以完成所需的功能,只要对象的生命期结束,程序就自动执行析构函数来完成这些工作。
析构函数不返回任何值,没有函数类型,也没有函数参数。因此它不能被重载。一个类可以有多个构造函数,但只能有一个析构函数。
实际上,析构函数的作用并不仅限于释放资源方面,它还可以被用来执行 "用户希望在最后一次使用对象之后所执行的任何操作”,例如输出有关的信息。这里说的用户是指类的设计者,因为析构函数是在声明类的时候定义的。也就是说,析构函数可以完成类的设计者所指定的任何操作。
类的设计者应当在声明类的同时定义析构函数,以指定如何完成“清理"的工作。
如果用户没有定义析构函数,C++编译系统会自动生成一个析构函数,但它只是徒有析构函数的名称和形式,实际上什么操作都不进行。
#include<iostream>
#include<string>
using namespace std;
class Person {
public:
string name;
int age,id;
public:
void show();
Person(string _name,int _age=0,int _id=0);//构造函数
~Person();
};
Person::~Person(){
cout<<"析构函数:~Person()"<<endl;
}
Person::Person(string _name,int _age,int _id){
name = _name, age = _age, id = _id;
}
void Person::show(){
cout<<"[name="<<name<<",age="<<age<<",id="<<id<<"]"<<endl;
}
int main() {
Person p1("zhangsan",18,1);
Person p2("zhangsan",18,2);
p1.show();
p2.show();
return 0;
}
在使用构造函数和析构函数时,需要特别注意对它们的调用时间和调用顺序。
在一般情况下,调用析构函数的次序正好与调用构造函数的次序相反: 最先被调用的构造函数,其对应的(同一对象中的)析构函数最后被调用,而最后被调用的构造函数,其对应的析构函数最先被调用。
但是并不是在任何情况下都是按这一原则处理的。
对象可以在不同的作用域中定义,可以有不同的存储类别。
这些会影响调用构造函数和析构函数的时机。
下面归纳一下什么时候调用构造函数和析构函数:
(1) 在全局范围中定义的对象,它的构造函数在文件中的所有函数(包括main函数)执行之前调用(但如果一个程序中有多个文件,而不同的文件中都定义了全局对象,则这些对象的构造函数的执行顺序是不确定的)。当main函数执行完毕或调用exit函数时(此时程序终止),调用析构函数。
(2) 如果定义的是局部自动对象,则在建立对象时调用其构造函数。如果函数被多次调用,则在每次建立对象时都要调用构造函数。在函数调用结束、对象释放时先调用析构函数。
(3) 如果在函数中定义静态(static)局部对象,则只在程序第一次调用此函数建立对象时调用构造函数一次,在调用结束时对象并不释放,因此也不调用析构函数,只在main函数结束或调用exit函数结束程序时,才调用析构函数。
有时需要用到多个完全相同的对象。
此外,有时需要将对象在某一瞬时的状态保留下来。
这就是对象的复制机制:用一个已有的对象快速地复制出多个完全相同的对象。
其一般形式为:类名 对象2(对象1); //用对象1复制出对象2。
Person p1("zhangsa", 18, 1);
Person p2(p1);
在建立对象时调用一个特殊的构造函数:复制构造函数(copy constructor)。
这个函数的形式是这样的:
Person::Person(const Person& per){
name = pre.name, age = per.age, id = per.id;
}
复制构造函数也是构造函数,但它只有一个参数,这个参数是本类的对象(不能是其他类的对象),而且采用对象的引用的形式(一般约定加 const声明,使参数值不能改变,以免在调用此函数时因不慎而使对象值被修改)。
Person p2(p1);
括号内给定的实参是对象,编译系统就调用复制构造函数(它的形参也是对象),而不会去调用其他构造函数。实参p1的地址传递给形参per(per是p1的引用),因此执行复制构造函数的函数体时,将p1对象中各数据成员的值赋给p2中各数据成员。
如果用户自己未定义复制构造函数,则编译系统会自动提供一个默认的复制构造函数,其作用只是简单地复制类中每个数据成员。
C++还提供另一种方便用户的复制形式,用赋值号代替括号。
其一般形式为:类名 对象名1 = 对象名2;
Person p2 = p1; // 用 p1 初始化 p2
可以在一个语句中进行多个对象的复制。
p2 = p1, p3 = p2;//注意执行顺序
这种形式与变量初始化语句类似,这种形式看起来很直观,用起来很方便。
但是其作用都是调用复制构造函数。
对象的复制和对象的赋值在概念和语法上的不同:
对象的赋值是对一个已经存在的对象赋值,因此必须先定义被赋值的对象,才能进行赋值。
对象的复制则是从无到有地建立一个新对象,并使它与一个已有的对象完全相同。
Person p1("zhangsan",18,1);
Person p2=p1, p3=p2;//相当于 Person p2(p1), p3(p2);
p1.show(), p2.show(), p3.show();//3个对象的状态完全相同
请注意普通构造函数和复制构造函数的区别。
(1) 在形式上
类名(形参表列); //普通构造函数的声明,如 T(int a,int b,int c);
类名(类名& 对象名); //复制构造函数的声明,如 T(const T& t);
(2) 在建立对象时,实参类型不同。
系统会根据实参的类型决定调用普通构造函数或复制构造函数。
Person p1("zhangsan",18,1);//实参为整数,调用普通构造函数
Person p2(p1), p3(p2); //实参是对象名,调用复制构造函数
(3) 在什么情况下被调用
普通构造函数在程序中建立对象时被调用。
复制构造函数在用已有对象复制一个新对象时被调用。
在以下3种情况下需要克隆对象:
① 程序中需要新建立一个对象,并用另一个同类的对象对它初始化。
② 当函数的参数为类的对象时。
在调用函数时需要将实参对象完整地传递给形参,也就是需要建立一个实参的拷贝,这就是按实参复制一个形参,系统是通过调用复制构造函数来实现的,这样能保证形参具有和实参完全相同的值。
void fun(Person per){} //形参是类的对象
int main(){
Person per("zhangsan", 18, 1);
fun(per); //实参是类的对象,调用函数时将复制一个新对象b
return 0;
}
③ 函数的返回值是类的对象。
在函数调用完毕将返回值带回函数调用处时。
此时需要将函数中的对象复制一个临时对象并传给该函数的调用处。
Person fun(){
Person per("zhangsan", 18, 1);
return per;//函数的返回值是类的对象
}
int main(){
Person p; // 定义Person的对象 p
p = fun();// 调用fun, 返回Person类的临时对象,并赋值给p
}
以上几种调用复制构造函数都是由编译系统自动实现的,不必由用户自己去调用,只要知道在这些情况下需要调用复制构造函数就可以了。
对象数组
数组不仅可以由简单变量组成,也可以由同类对象组成。
例如一个班有50个学生,每个学生的属性包括姓名、性别、年龄、成绩等。
class Student{
private:
string name,sex;
int age,score;
}
这时可以定义一个学生类对象数组,每一个数组元素是一个学生类对象。
Student stud[50]; //设已声明Student类,定义stud数组,有50个元素
如果有50个元素,需要调用50次构造函数。
需要时可以在定义数组时提供实参以实现初始化。
如果构造函数只有一个参数,在定义数组时可以直接在等号后面的花括号内提供实参。
Student::Student(int _score):score(_score){}//构造函数只有一个参数
Student stud[3]={60,70,78};//合法,3个实参分别传递给3个数组元素的构造函数
如果构造函数有多个参数,则不能用在定义数组时直接提供所有实参的方法,因为一个数组有多个元素,对每个元素要提供多个实参,如果再考虑到构造函数有默认参数的情况,很容易造成实参与形参的对应关系不清晰,出现歧义性。
例:类Student的构造函数有多个参数,且为默认参数:
编译系统只为每个对象元素的构造函数传递一个实参,所以在定义数组时提供的实参个数不能超过数组元素个数。
Student stud[3]={60,70,78,45};//不合法,实参个数超过对象数组元素个数
如果构造函数有多个参数,在定义对象数组时应当怎样实现初始化呢?
方法:在花括号中分别写出构造函数并指定实参。
Student Stud[3]={ //定义对象数组
Student(1001,18,87),
Student(1002,19,76),
Student(1003,18,72)};
第三章 深入理解类和对象
3.4 对象指针、对象引用、对象数组
(一)、指向对象的指针
在建立对象时,编译系统会为每一个对象分配一定的存储空间,以存放其成员。
对象空间的起始地址就是对象的指针。
可以定义指针变量,用来存放对象的指针。
定义指向类对象的指针变量的一般形式为:类名 *对象指针名;
可以通过对象指针访问对象和对象的成员。
Person per("zhangsan", 18, 1);
Person *p = &per;//定义一个Person类指针变量 p,指向 对象per
p->show(); //指针变量通过箭头的形式去访问
(*p).show(); //通过*访问指针变量,得到对象,再通过 .访问对象下的成员函数
(二)、指向对象成员的指针
对象有地址,存放对象初始地址的指针变量就是指向对象的指针变量。
对象中的成员也有地址,存放对象成员地址的指针变量就是指向对象成员的指针变量。
-
指向对象数据成员的指针
定义指向对象数据成员的指针变量的方法和定义指向普通变量的指针变量方法相同。
定义指向对象数据成员的指针变量的一般形式为:数据类型名 *指针变量名;
如果hour是公用的,则可以在类外访问,访问形式如下:
int *pid = &per.id; //公有权限才可访问 cout<<pid<<" "<<*pid<<endl;
-
指向对象成员函数的指针
成员函数与普通函数有一个最根本的区别: 成员函数是类中的一个成员。编译系统要求赋值的时候,指针变量的类型必须与赋值号右侧函数的类型相匹配,以下3方面都要匹配:
①函数参数的类型和参数个数;
②函数返回值的类型;
③所属的类。
定义指向成员函数的指针变量采用下面的形式:
定义指向公用成员函数的指针变量的一般形式为:数据类型名 (类名∷*指针变量名)(参数表列);
使指针变量指向一个公用成员函数的一般形式为:指针变量名=&类名∷成员函数名;void (Time∷*p2)(); //定义p2为指向Time类中公用成员函数的指针变量 p2 = &Time∷get_time; //指向一个公用成员函数
#include<iostream>
#include<cstring>
using namespace std;
class Time {
public:
int hour,minute,sec;
Time(int _hour,int _minute,int _sec);
~Time();
void getTime();
};
Time::Time(int _hour,int _minute,int _sec) {
hour = _hour, minute=_minute, sec = _sec;
cout<<"Time::Time(int _hour,int _minute,int _sec) "<<endl;
}
Time::~Time() {
cout<<"Time::~Time()"<<endl;
}
void Time::getTime(){
cout<<"time : "<<hour<<" "<<minute<<" "<<sec<<endl;
}
int main() {
Time t1(10,13,56); //定义Time类对象t1
int *p1=&t1.hour; //定义指向整型数据的指针变量p1,并使p1指向t1.hour
cout<<*p1<<endl; //输出p1所指的数据成员t1.hour
t1.getTime(); //调用对象t1的成员函数get_time
Time *p2=&t1; //定义指向Time类对象的指针变量p2,并使p2指向t1
p2->getTime(); //调用p2所指向对象(即t1)的get_time函数
void (Time::*p3)(); //定义指向Time类公用成员函数的指针变量p3
p3=&Time::getTime; //使p3指向Time类公用成员函数get_time
(t1.*p3)(); //调用对象t1中p3所指的成员函数(即t1.get_time( ))
}
(三)对象指针数组
#include <iostream>
#include <string>
#include <cstring>
#include <cstdio>
using namespace std;
class Student{
private:
int no, score;
string name;
public:
Student(int _no=0, string _name="", int _score=0){
no = _no, name = _name, score = _score;
}
~Student(){
cout<<"~Student():";
cout<<"[no="<<no<<", name="<<name<<", score="<<score<<"]"<<endl;
}
void show(){
cout<<"[no="<<no<<", name="<<name<<", score="<<score<<"]"<<endl;
}
};
int main(){
Student s1, s2(1001), s3(1002, "li"), s4(1003,"wang",98);
Student *pa[4] = {&s1, &s2, &s3, &s4};
for(int i=0; i<4; i++){
pa[i]->show();
}
pa[0]=&s4;
pa[0]->show();
return 0;
}
3.5 常对象与常成员
使用 const 修饰的对象称为常对象,常对象必须在定义时初始化,而且不能被更改。
const Person per(1, "liumei", 18); //常对象
使用 const 修饰的成员函数称为常成员函数。
常对象只能调用 const 类型的成员函数,不能调用非 const 类型的成员函数,这是为了防止通过成员函数来改变常对象中数据成员的值。
用 const 可以与不带 const 的函数进行重载。
当类中只有一个常成员函数时,一般对象可以调用常成员函数;
但当同名的一般成员函数和常成员函数同时存在时,一般对象调用一般成员函数,常对象调用常成员函数;
const Person per(1, "liumei", 18);
per.show(); //error 不允许常对象调用
void show() {
cout<<"[id="<<id<<",name="<<name<<",age="<<age<<"]"<<endl;
}
void show() const { //常成员函数
cout<<"const [id="<<id<<",name="<<name<<",age="<<age<<"]"<<endl;
}
使用 const 修饰的数据成员称为常数据成员。
常数据成员必须初始化,且不能改变。
常数据成员的初始化只能通过构造函数的成员初始化列表形式来实现。
#include <iostream>
using namespace std;
class Point{
private:
const int x,y;//常数据成员
public:
Point(int _x,int _y):x(_x), y(_y){}//初始化列表
void show() const { //常成员函数
cout<<"[x="<<x<<",y="<<y<<"]"<<endl;
}
};
int main(){
Point p1(0,0);
p1.show(); //一般对象也可调用常成员函数
return 0;
}
指向对象的常指针是常量,不可改变,但改指针指向的对象值是可以改变的。
#include <iostream>
using namespace std;
class Point{
private:
int x,y;
public:
Point(int _x,int _y):x(_x), y(_y){}//初始化列表
void move(int _x,int _y){
x = _x, y = _y;
}
void show() const { //常成员函数
cout<<"[x="<<x<<",y="<<y<<"]"<<endl;
}
};
int main(){
Point p1(0,0), p2(1,1);
Point *ptr = &p1;
ptr->show();
ptr = &p2; //一般对象指针指向的对象可以改变
ptr->show();
Point *const ptr2 = &p1;//指向对象的常指针,初始化后不可改变
// ptr2 = &p2; // error
ptr2->move(2,2); // 常指针指向的对象的值可以改变
ptr2->show();
return 0;
}
指向常对象的指针是指这个指针所指向的是一个常对象。
const Point p1(1,1), p2(2,2); //常对象
const Point *ptr = &p1; //常对象只能由指向常对象的指针指向
ptr->show();
Point p3(3, 3);
const Point *ptr2 = &p3;//指向常对象的指针指向一般对象,但是不能改变对象
//ptr2->move(4,4); //error
ptr2->show();
常对象的引用
形参是常引用,实参可以是一般对象,常对象,一般引用,常引用。
实参是一般引用,实参不能是常对象或常引用。
#include <iostream>
using namespace std;
class Point {
private:
int x,y;
public:
Point(){
cout<<"Point() "<<endl;
}
Point(int _x,int _y):x(_x), y(_y) {//初始化列表
cout<<"Point(int _x,int _y):x(_x), y(_y)"<<endl;
}
void move(int _x,int _y) {
x = _x, y = _y;
}
void show() const { //常成员函数
cout<<"[x="<<x<<",y="<<y<<"]"<<endl;
}
};
void fun1(const Point &crp){
crp.show();
cout<<"void fun1(const Point &crp)"<<endl;
// crp.move(4, 5);//error 常引用不能调用一般成员函数
}
void fun2(Point &rp){
cout<<"void fun2(Point &rp)"<<endl;
rp.move(4, 5);//可以调用一般成员函数
rp.show();
}
int main() {
Point p1, p2(1,1);
Point &rp = p1; //引用,不调用构造函数
const Point cp; //调用构造函数
const Point &crp = p2;
//形参是常引用,实参可以是一般对象,常对象,一般引用,常引用。
// fun1(p1); fun1(cp); fun1(rp); fun1(crp);
fun2(p1); fun2(rp);
//实参是一般引用,实参不能是常对象或常引用。
// fun2(cp); fun2(crp);//error
return 0;
}
3.6 动态创建对象和释放对象
Time *t1 = new Time(2022, 4, 10);//动态创建单个对象
delete t1; //释放单个对象
Time *t2 = new Time[10]; //动态创建对象数组,里面10个元素空间
delete []t2; //释放对象数组
3.7 对象的生存期
局部对象:定义开始到其后第一个'}'结束,自动调用析构函数。
静态对象:static 局部对象,第一次执行就创建,程序结束才析构。
全局对象:构造函数在主函数执行前执行,程序结束才析构。
3.8 程序实例
定义一个矩阵类,可以进行矩阵的加减运算。
#include<iostream>
using namespace std;
class Matrix{
private:
double **data;
int row, col;
public:
Matrix(){} //无参构造函数
Matrix(int m,int n){//有参构造函数
row = m, col = n;
cout<<"输入一个 m行 n列的矩阵:\n";
data = new double*[row];
for(int i=0; i<row; i++){
data[i] = new double[col];
for(int j=0; j<col; j++){
cin>>data[i][j];
}
}
}
Matrix(const Matrix& mat){
row = mat.row, col = mat.col;
data = new double*[row];
for(int i=0; i<row; i++){
data[i] = new double[col];
}
for(int i=0; i<row; i++){
for(int j=0; j<col; j++){
data[i][j] = mat.data[i][j];
}
}
}
~Matrix(){//析构函数
cout<<"~Matrix()"<<endl;
}
void plus(const Matrix& mat){//矩阵加法
for(int i=0; i<row; i++){
for(int j=0; j<col; j++){
data[i][j] += mat.data[i][j];
}
}
}
void minus(const Matrix& mat){//矩阵减法
for(int i=0; i<row; i++){
for(int j=0; j<col; j++){
data[i][j] -= mat.data[i][j];
}
}
}
void print(){
for(int i=0; i<row; i++){
for(int j=0; j<col; j++){
cout<<data[i][j]<<" ";
} cout<<endl;
}
}
};
int main(){
Matrix m1(2, 3), m2(2, 3);
m1.plus(m2);
cout<<"矩阵相加后的结果:"<<endl;
m1.print();
m1.minus(m2);
cout<<"矩阵相减后的结果:"<<endl;
m1.print();
return 0;
}
第四章 静态成员与友元
4.1 静态成员
静态数据成员是一种特殊的数据成员。它以关键字static开头。
class Box {
public:
int volume( );
private:
static int height; //把height定义为静态的数据成员
int width;
int length;
};
如果希望各对象中的height的值是一样的,就可以把它定义为静态数据成员,这样它就为各对象所共有,而不只属于某个对象的成员,所有对象都可以引用它。
静态的数据成员在内存中只占一份空间。
每个对象都可以引用这个静态数据成员。静态数据成员的值对所有对象都是一样的。
如果改变它的值,则在各对象中这个数据成员的值都同时改变了。
这样可以节约空间,提高效率。
说明:
(1) 如果只声明了类而未定义对象,则类的一般数据成员是不占内存空间的,只有在定义对象时,才为对象的数据成员分配空间。
但是静态数据成员不属于某一个对象,在为对象所分配的空间中不包括静态数据成员所占的空间。
静态数据成员是在所有对象之外单独开辟空间。
只要在类中定义了静态数据成员,即使不定义对象,也为静态数据成员分配空间,它可以被引用。
在一个类中可以有一个或多个静态数据成员,所有的对象共享这些静态数据成员,都可以引用它。
(2) 如果在一个函数中定义了静态变量,在函数结束时该静态变量并不释放,仍然存在并保留其值。
现在讨论的静态数据成员也是类似的,它不随对象的建立而分配空间,也不随对象的撤销而释放(一般数据成员是在对象建立时分配空间,在对象撤销时释放)。
静态数据成员是在程序编译时被分配空间的,到程序结束时才释放空间。
(3) 静态数据成员可以初始化,但只能在类体外进行初始化。
int Box∷height=10; //表示对Box类中的数据成员初始化
其一般形式为:数据类型类名∷静态数据成员名=初值;
不必在初始化语句中加static。
注意: 不能用参数初始化表对静态数据成员初始化。
Box(int h,int w,int len):height(h){} //错误,height是静态数据成员
如果未对静态数据成员赋初值,则编译系统会自动赋予初值0。
(4) 静态数据成员既可以通过对象名引用,也可以通过类名来引用。
如果静态数据成员声明为公有,则可以在类外引用。
可以通过类名或者对象名,引用静态数据成员。
#include<iostream>
using namespace std;
class Box {
public:
Box(int,int);
int volume( );
static int height; //把height定义为公用的静态的数据成员
int width;
int length;
};
Box::Box(int w,int len) { //通过构造函数对width和length赋初值
width=w, length=len;
}
int Box::volume( ) {
return(height*width*length);
}
int Box::height=10; //对静态数据成员height初始化
int main( ) {
Box a(15,20),b(20,30);
cout<<a.height<<endl; //通过对象名a引用静态数据成员
cout<<b.height<<endl; //通过对象名b引用静态数据成员
cout<<Box::height<<endl; //通过类名引用静态数据成员
cout<<a.volume( )<<endl; //调用volume函数,计算体积,输出结果
return 0;
}
所有对象的静态数据成员实际上是同一个数据成员。
注意: 在上面的程序中将height定义为公用的静态数据成员,所以在类外可以直接引用。
可以看到在类外可以通过对象名引用公用的静态数据成员,也可以通过类名引用静态数据成员。
即使没有定义类对象,也可以通过类名引用静态数据成员。
这说明 静态数据成员并不是属于对象的,而是属于类的,但类的对象可以引用它。
如果静态数据成员被定义为私有的,则不能在类外直接引用,而必须通过公用的成员函数引用。
(5) 有了静态数据成员,各对象之间的数据有了沟通的渠道,实现数据共享,因此可以不使用全局变量。
全局变量破坏了封装的原则,不符合面向对象程序的要求。
公用静态数据成员与全局变量的不同:
静态数据成员的作用域只限于定义该类的作用域内(如果是在一个函数中定义类,那么其中静态数据成员的作用域就是此函数内)。
在此作用域内,可以通过类名和域运算符“∷”引用静态数据成员,而不论类对象是否存在。
成员函数也可以定义为静态的,在类中声明函数的前面加static就成了静态成员函数。
static int volume();
和静态数据成员一样,静态成员函数是类的一部分,而不是对象的一部分。
如果要在类外调用公用的静态成员函数,要用类名和域运算符“∷”。
Bo::volume();
实际上也允许通过对象名调用静态成员函数
a.volume();
但这并不意味着此函数是属于对象a的,而只是用a的类型而已。
静态成员函数的作用不是为了对象之间的沟通,而是为了能处理静态数据成员。
前面曾指出: 当调用一个对象的成员函数(非静态成员函数)时,系统会把该对象的起始地址赋给成员函数的this指针。
而静态成员函数并不属于某一对象,它与任何对象都无关,因此静态成员函数没有this指针。
既然它没有指向某一对象,就无法对一个对象中的非静态成员进行默认访问(即在引用数据成员时不指定对象名)。
静态成员函数与非静态成员函数的根本区别是:
非静态成员函数有this指针,而静态成员函数没有this指针。
由此决定了静态成员函数不能访问本类中的非静态成员。
静态成员函数可以直接引用本类中的静态数据成员,在C++程序中,静态成员函数主要用来访问静态数据成员,而不访问非静态成员。
假如在一个静态成员函数中有以下语句:
cout<<height<<endl; //若height已声明为static,引用本类中的静态成员,合法
cout<<width<<endl; //若width是非静态数据成员,不合法
并不是绝对不能引用本类中的非静态成员,只是不能进行默认访问,因为无法知道应该去找哪个对象。
如果一定要引用本类的非静态成员,应该加对象名和成员运算符“.”。如
cout<<a.width<<endl; //引用本类对象a中的非静态成员
例: 静态成员函数的应用。
#include <iostream>
#include <iomanip>
using namespace std;
class Student {
public:
Student(int n,int a,double s):num(n),age(a),score(s) {}
void total();
static double average(); //声明静态成员函数
static double average(const Student& stu);//声明静态成员函数
private:
int num;
int age;
double score;
static double sum; //静态数据成员
static int count; //静态数据成员
};
void Student::total( ) { //定义非静态成员函数
sum+=score; //累加总分
count++; //累计已统计的人数
}
double Student::average() {//定义静态成员函数
return(sum/count);
}
double Student::average(const Student& stu) {
//如果在静态方法中调用非静态成员,可以使用实例化对象,但是不建议这样用
cout<<stu.score<<endl;
return(sum/count);
}
double Student::sum=0; //对静态数据成员初始化
int Student::count=0; //对静态数据成员初始化
int main( ) {
Student stud[3]= {
Student(1001,18,70),
Student(1002,19,78),
Student(1005,20,98)
};
cout<<"please input the number of students:";
int n; cin>>n; //输入需要求前面多少名学生的平均成绩
for(int i=0; i<n; i++) { //调用3次total函数
stud[i].total( );
}
cout<<"the average score of "<<n<<" students is "
<<Student::average()<<endl;//调用静态成员函数
cout<<"the average score of "<<n<<" students is "
<<Student::average(stud[1])<<endl;//调用静态成员函数
return 0;
}
运行结果为
please input the number of students:3
the average score of 3 students is 82
说明:
(1) 在主函数中定义了stud对象数组,为了使程序简练,只定义它含3个元素,分别存放3个学生的数据。
(2) 在Student类中定义了两个静态数据成员sum(总分)和count(累计需要统计的学生人数),这是由于这两个数据成员的值是需要进行累加的,它们并不是只属于某一个对象元素,而是由各对象元素共享的,可以看出: 它们的值是在不断变化的,而且无论对哪个对象元素而言,都是相同的,而且始终不释放内存空间。
(3) total是公有的成员函数,其作用是将一个学生的成绩累加到sum中。
公有的成员函数可以引用本对象中的一般数据成员(非静态数据成员),也可以引用类中的静态数据成员。score是非静态数据成员,sum和count是静态数据成员。
(4) average是静态成员函数,它可以直接引用私有的静态数据成员(不必加类名或对象名),函数返回成绩的平均值。
(5) 在main函数中,引用total函数要加对象名(今用对象数组元素名),引用静态成员函数average函数要用类名或对象名。
(6) 请思考: 如果不将average函数定义为静态成员函数行不行?程序能否通过编译?需要作什么修改?为什么要用静态成员函数?请分析其理由。
(7) 如果想在average函数中引用stud[1]的非静态数据成员score,应该怎样处理?
在C++程序中最好养成这样的习惯: 只用静态成员函数引用静态数据成员,而不引用非静态数据成员。这样思路清晰,逻辑清楚,不易出错。
4.2 友元
在一个类中可以有公用的(public)成员和私有的(private)成员。
在类外可以访问公用成员,只有本类中的函数可以访问本类的私有成员。
现在,我们来补充介绍一个例外——友元(friend)。
友元可以访问与其有好友关系的类中的私有成员。友元包括友元函数和友元类。
如果在本类以外的其他地方定义了一个函数(这个函数可以是不属于任何类的非成员函数,也可以是其他类的成员函数),在类体中用friend对其进行声明,此函数就称为本类的友元函数。
友元函数可以访问这个类中的私有成员。
将普通函数声明为友元函数
#include <iostream>
using namespace std;
class Time {
public:
Time(int,int,int);
//声明display函数为Time类的友元函数
friend void display(Time &);
private: //以下数据是私有数据成员
int hour, minute, sec;
};
Time::Time(int h,int m,int s) {//构造函数
hour=h, minute=m, sec=s;
}
void display(Time& t) {//友元函数,形参t是Time类对象的引用
cout<<t.hour<<":"<<t.minute<<":"<<t.sec<<endl;
}
int main() {
Time t1(10,13,56);
display(t1);//调用display函数,实参t1是Time类对象
return 0;
}
由于声明了display是Time类的friend函数,所以display函数可以引用Time中的私有成员hour,minute,sec。
注意在引用这些私有数据成员时,必须加上对象名,不能写成
cout<<hour<<":"<<minute<<":"<<sec<<endl; //error
cout<<t.hour<<":"<<t.minute<<":"<<t.sec<<endl; //ac
因为display函数不是Time类的成员函数,不能默认引用Time类的数据成员,必须指定要访问的对象。
友元成员函数
friend函数不仅可以是一般函数(非成员函数),而且可以是另一个类中的成员函数。
友元成员函数的简单应用。
#include <iostream>
using namespace std;
class Date; //对Date类的提前引用声明
class Time { //定义Time类
public:
Time(int,int,int);
void display(Date &);//display是成员函数,形参是Date类对象的引用
private:
int hour, minute, sec;
};
class Date { //声明Date类
public:
Date(int,int,int);
//声明Time中的display函数为友元成员函数
friend void Time::display(Date &);
private:
int year,month,day;
};
Time::Time(int h,int m,int s) { //类Time的构造函数
hour=h, minute=m, sec=s;
}
//display的作用是输出年、月、日和时、分、秒
void Time::display(Date &d) {
//引用Date类对象的私有数据
cout<<d.month<<"/"<<d.day<<"/"<<d.year<<endl;
//引用本类对象中的私有数据
cout<<hour<<":"<<minute<<":"<<sec<<endl;
}
Date::Date(int m,int d,int y) { //类Date的构造函数
month=m, day=d, year=y;
}
int main( ) {
Time t1(10,13,56); //定义Time类对象t1
Date d1(12,25,2004); //定义Date类对象d1
t1.display(d1); //调用t1中的display函数,实参是Date类对象d1
return 0;
}
运行时输出:
12/25/2004 (输出Date类对象d1中的私有数据)
10:13:56 (输出Time类对象t1中的私有数据)
在本例中定义了两个类Time和Date。
程序第3行是对Date类的声明,因为在第7行和第16行中对display函数的声明和定义中要用到类名Date,而对Date类的定义却在其后面。能否将Date类的声明提到前面来呢?也不行,因为在Date类中的第4行又用到了Time类,也要求先声明Time类才能使用它。为了解决这个问题,C++允许对类作“提前引用”的声明,即在正式声明一个类之前,先声明一个类名,表示此类将在稍后声明。
程序第3行就是提前引用声明,它只包含类名,不包括类体。如果没有第3行,程序编译就会出错。
在一般情况下,对象必须先声明,然后才能使用它。
但是在特殊情况下,在正式声明类之前,需要使用该类名。
注意: 类的提前声明的使用范围是有限的。只有在正式声明一个类以后才能用它去定义类对象。
如果在上面程序第3行后面增加一行:
Date d1; //企图定义一个对象
会在编译时出错。
因为在定义对象时是要为这些对象分配存储空间的,在正式声明类之前,编译系统无法确定应为对象分配多大的空间。编译系统只有在“见到”类体后,才能确定应该为对象预留多大的空间。
在对一个类作了提前引用声明后,可以用该类的名字去定义指向该类型对象的指针变量或对象的引用变量(如在本例中,定义了Date类对象的引用变量)。
这是因为指针变量和引用变量本身的大小是固定的,与它所指向的类对象的大小无关。
请注意程序是在定义Time∷display函数之前正式声明Date类的。
如果将对Date类的声明的位置(程序13~21行)改到定义Time∷display函数之后,编译就会出错,因为在Time∷display函数体中要用到Date类的成员month,day,year。
如果不事先声明Date类,编译系统无法识别成员month,day,year等成员。
在一般情况下,两个不同的类是互不相干的。
在本例中,由于在Date类中声明了Time类中的display成员函数是Date类的“朋友”,因此该函数可以引用Date类中所有的数据。
请注意在本程序中调用友元函数访问有关类的私有数据方法:
(1) 在函数名display的前面要加display所在的对象名(t1);
(2) display成员函数的实参是Date类对象d1,否则就不能访问对象d1中的私有数据;
(3) 在Time∷display函数中引用Date类私有数据时必须加上对象名,如d.month。
一个函数(包括普通函数和成员函数)可以被多个类声明为“朋友”,这样就可以引用多个类中的私有数据。
例如, 可以将程序中的display函数不放在Time类中,而作为类外的普通函数,然后分别在Time和Date类中将display声明为朋友。在主函数中调用display函数,display函数分别引用Time和Date两个类的对象的私有数据,输出年、月、日和时、分、秒。
友元类
不仅可以将一个函数声明为一个类的“朋友”,而且可以将一个类(例如B类)声明为另一个类(例如A类)的“朋友”。
这时B类就是A类的友元类。友元类B中的所有函数都是A类的友元函数,可以访问A类中的所有成员。
在A类的定义体中用以下语句声明B类为其友元类:
friend B;
声明友元类的一般形式为:friend 类名;
#include<iostream>
using namespace std;
class B;
class A{
private:
int Aa, Ab, Ac;
public:
A(int a,int b,int c):Aa(a), Ab(b), Ac(c){}
void show(B &b);
};
class B{
private:
int Ba, Bb, Bc;
public:
B(int a,int b,int c):Ba(a), Bb(b), Bc(c){}
friend A;
};
void A::show(B &b){
cout<<b.Ba<<" "<<b.Bb<<" "<<b.Bc<<endl;
}
int main(){
A a(1, 2, 3);
B b(11, 22, 33);
a.show(b);
return 0;
}
关于友元,有两点需要说明:
(1) 友元的关系是单向的而不是双向的。
(2) 友元的关系不能传递。
在实际工作中,除非确有必要,一般并不把整个类声明为友元类,而只将确实有需要的成员函数声明为友元函数,这样更安全一些。
关于友元利弊的分析: 面向对象程序设计的一个基本原则是封装性和信息隐蔽,而友元却可以访问其他类中的私有成员,不能不说这是对封装原则的一个小的破坏。但是它能有助于数据共享,能提高程序的效率,在使用友元时,要注意到它的副作用,不要过多地使用友元,只有在使用它能使程序精炼,并能大大提高程序的效率时才用友元。
友元总结
- 友元函数
(1) 普通函数声明为友元函数
定义矩形类,数据成员包括矩形的长和宽,定义友元函数,求两矩形面积之和。
(2) 其它类的成员函数声明为友元函数
定义学生类,包括学号、姓名和分数,定义教师类,包括可以修改学生分数的成员函数,该函数为学生类的友元函数。注意:教师类在学生类前定义,教师类的成员函数changescore在学生类定义之后定义。
教师类定义前需要加:class Student; //类的提前引用声明
(3) 友元函数提高了程序的运行效率,但破坏了类的封装性,使用时应权衡利弊。
- 友元类
友元不仅可以是函数,还可以是类。
在一个类中声明另一个类,前面加上修饰符friend,被声明的类称为友元类,友元类中的所有成员函数都是另一个类的友元函数,可以直接访问该类中私有的和受保护的成员。
(1) 友元类前向声明
(2) 友元类的所有成员函数均为友元函数
(3) 友元是单向的,友元关系不能传递
如A类是B类的友元,但不等于说B类也是A类的友员
如A类是B类的友元,B类是C类的友元,但不等于说A类也是C类的友员
4.3 模板
回忆:求两个整数的最大值;求两个浮点数的最大值;求两个双精度数的最大值。
具有同样功能的函数,能否只写一套代码? 引入模板机制。
模板的作用就是使程序能够对不同类型的数据进行相同方式的处理。
模板是实现代码重用机制的一种工具,它可以实现类型参数化,即把类型定义为参数,从而实现了真正的代码可重用性。C++中的模板分为函数模板和类模板。
- 函数模板
(1)函数模板的概念
函数模板是一组相关函数的模型或样板。
这些相关函数功能与代码相似,只是处理的数据类型不同,将这些相关函数合并成一个函数,处理的数据类型用通用类型符(即模板参数)代替,称作函数模板。
(2)函数模板的定义
【例】求两个数中的较大值和较小值
template <typename T> //模板参数说明
T max(T a,T b) {
return a>b?a:b;
}
(3)函数模板的调用
调用函数模板时,编译系统根据调用时使用的数据类型生成相应的具体函数,这一过程称作函数模板的实例化,生成的具体函数称作模板函数。
调用函数模板的格式有两种,第一种与调用普通函数的格式相同,即:函数名(实参1,实参2,……);
max(5, 6);
max(5.5, 6.6);
第二种需要给出模板实参,即:函数名<模板实参1,模板实参2,…>(实参1,实参2,……);
max<double>(a, b);
以上调用函数模板的语句中两个参数类型不同,编译时出错,需要用模板实参说明T的类型。
注意:
(1)typename可以用class代替
(2)函数模板定义可以放在主函数后面,在主函数之前则给出函数模板声明,函数模板的声明和定义的前面都要给出模板参数声明。
(3)模板参数可以有多个
(4)当由实参1,实参2,…,即可确定函数模板中模板参数的类型时,就可以采用第一种方式调用函数模板。
当由实参1,实参2,…,不能确定函数模板中模板参数的类型时,就要采用第二种方式调用函数模板。
【例】定义函数模板,求两个数的平均值
#include <iostream>
using namespace std;
template <typename T1,typename T2>
T2 average(T1 a,T1 b){
return (a+b)/2.0;
}
int main() {
cout<<average<int,float>(5,6)<<endl;
return 0;
}
【例】定义函数模板,在一维数组中查找给定的数,若存在,返回对应元素下标,若不存在,返回-1。
#include <iostream>
using namespace std;
template <typename T> //模板参数说明
int find(T array[],int n,T num) {//函数模板中可以有多个模板参数
for(int i=0; i<n; i++){
if(array[i]==num) return i;
}
return -1;
}
int main() {
int a[]= {1,4,7,200,0,-34,9,10};
int index1=find(a, sizeof(a)/sizeof(int), -34);
cout<<"index1="<<index1<<endl;
double b[]= {1.2,3.4,78.9,0,-100,5.6,7.8};
int index2=find<double>(b, sizeof(b)/sizeof(double), -100);
cout<<"index2="<<index2<<endl;
return 0;
}
【例】定义函数模板,求一维数组中元素的平均值。
#include <iostream>
using namespace std;
template <typename T1,typename T2>//模板参数说明
T2 average(T1 array[],int n) {//函数模板中模板参数有多个,注意返回类型
T1 sum=0;
for(int i=0; i<n; i++) sum += array[i];
return sum/(double)n;
}
int main() {
double b[]= {1,2,3,4,5,6,7,8}, aver;
aver=average<double,double>(b, sizeof(b)/sizeof(double));
cout<<"average="<<aver<<endl;
aver=average<double,int>(b, sizeof(b)/sizeof(double));
cout<<"average="<<aver<<endl;
return 0;
}
⑴ 当由实参b,sizeof(b)/sizeof(double)不能确定函数模板中模板参数T1、T2的类型时,调用函数模板时要用findaver<double,double>形式告知编译系统,T1、T2的类型是double、double,据此生成相应的模板函数。
⑵ 调用函数模板时,T1、T2可以是不同类型,运行结果也会不同。
- 类模板
有时,有两个或多个类,其功能是相同的,仅仅是数据类型不同:
class Compare_int {
public:
Compare(int a,int b) {
x=a, y=b;
}
int max( ) {
return(x>y)?x:y;
}
int min( ) {
return(x<y)?x:y;
}
private:
int x,y;
};
其作用是对两个整数作比较,可以通过调用成员函数max和min得到两个整数中的大者和小者。
如果想对两个浮点数(float型)作比较,需要另外声明一个类:
class Compare_float {
public:
Compare(float a,float b) {
x=a, y=b;
}
float max( ) {
return(x>y)?x:y;
}
float min( ) {
return(x<y)?x:y;
}
private:
float x,y;
}
对于功能相同而仅仅是数据类型不同的两个或多个类,可以声明一个通用的类模板,它们可以有一个或多个通用的类型参数(模板参数)。
【例】定义一个类求两个数中的较大值和较小值
template <class numtype> //声明一个模板,虚拟类型名为numtype
class Compare { //类模板名为Compare
public:
Compare(numtype a,numtype b) {
x=a, y=b;
}
numtype max( ) {
return (x>y)?x:y;
}
numtype min( ) {
return (x<y)?x:y;
}
private:
numtype x,y;
};
(1) 声明类模板时要增加一行:template <class 类型参数名>
(2) 原有的类型名int换成虚拟类型参数名numtype。
在建立类对象时,如果将实际类型指定为int型,编译系统就会用int取代所有的numtype,如果指定为float型,就用float取代所有的numtype。这样就能实现“一类多用”。
由于类模板包含类型参数,因此又称为参数化的类。
如果说类是对象的抽象,对象是类的实例,则类模板是类的抽象,类是类模板的实例。
利用类模板可以建立含各种数据类型的类。
在声明了一个类模板后,怎样使用它?
先回顾一下用类来定义对象的方法:
Compare_int cmp1(4,7); // Compare_int是已声明的类
用类模板定义对象的方法与此相似,但是不能直接写成
Compare cmp(4,7); // Compare是类模板名
Compare是类模板名,而不是一个具体的类,类模板体中的类型numtype并不是一个实际的类型,只是一个虚拟的类型,无法用它去定义对象。必须用实际类型名去取代虚拟的类型,具体的做法是:
Compare <int> cmp(4,7);
例:声明一个类模板,利用它分别实现两个整数、浮点数和字符的比较,求出大数和小数。
#include <iostream>
using namespace std;
template<class numtype> //定义类模板
class Compare {
public:
Compare(numtype a,numtype b) {
x=a, y=b;
}
numtype max() {
return (x>y)?x:y;
}
numtype min() {
return (x<y)?x:y;
}
private:
numtype x,y;
};
int main() {
Compare<int> cmp1(3,7); //定义对象cmp1,用于两个整数的比较
cout<<cmp1.max()<<" is the Maximum of two integer numbers."<<endl;
cout<<cmp1.min()<<" is the Minimum of two integer numbers."<<endl<<endl;
Compare<float> cmp2(45.78,93.6);//定义对象cmp2,两个浮点数的比较
cout<<cmp2.max()<<" is the Maximum of two float numbers."<<endl;
cout<<cmp2.min()<<" is the Minimum of two float numbers."<<endl<<endl;
Compare<char> cmp3('a','A'); //定义对象cmp3,用于两个字符比较
cout<<cmp3.max()<<" is the Maximum of two characters."<<endl;
cout<<cmp3.min()<<" is the Minimum of two characters."<<endl;
return 0;
}
运行结果如下:
7 is the Maximum of two integers.
3 is the Minimum of two integers.
93.6 is the Maximum of two float numbers.
45.78 is the Minimum of two float numbers.
a is the Maximum of two characters.
A is the Minimum of two characters.
说明: 类模板中的成员函数如果在类模板外定义,不能用一般定义类成员函数的形式:
numtype Compare::max( ) {
...
} //不能这样定义类模板中的成员函数
而应当写成类模板的形式:
template<class numtype>
numtype Compare<numtype>::max( ) {
return (x>y)?x:y;
}
归纳以上的介绍,可以这样声明和使用类模板:
(1) 先写出一个实际的类。由于其语义明确,含义清楚,一般不会出错。
(2) 将此类中准备改变的类型名(如int要改变为float或char)改用一个自己指定的虚拟类型名(如上例中的numtype)。
(3) 在类声明前面加入一行,格式为
template<class 虚拟类型参数>,如
template<class numtype> //注意本行末尾无分号
class Compare {
...
}; //类体
(4) 用类模板定义对象时用以下形式:
类模板名<实际类型名> 对象名;
类模板名<实际类型名> 对象名(实参表列);
Compare<int> cmp;
Compare<int> cmp(3,7);
(5) 如果在类模板外定义成员函数,应写成类模板形式:
template<class 虚拟类型参数>
函数类型 类模板名<虚拟类型参数>::成员函数名(函数形参表列) {
...
}
说明:
(1) 类模板的类型参数可以有一个或多个,每个类型前面都必须加class,并且可以为模板参数的最后若干个参数设置默认值。
template<class T1,class T2=double>
class someclass {
...
};
在定义对象时分别代入实际的类型名,如
someclass<int,double> obj;
(2) 和使用类一样,使用类模板时要注意其作用域,只能在其有效作用域内用它定义对象。
(3) 模板可以有层次,一个类模板可以作为基类,派生出派生模板类。
【例】有多个模板参数的类模板
#include <iostream>
using namespace std;
//声明模板参数也可以写为
//template <typename T1,typename T2=double>
template <class T1,class T2=double>
class Math {
public:
Math(T1 _a,T2 _b):a(_a), b(_b){}
T1 max() { //求最大值
return a>b?a:b;
}
T1 min() { //求最小值
return a<b?a:b;
}
T2 aver() {
return (a+b)/2.0;
}
private:
T1 a,b;
};
int main() {
int x=1, y=2;
Math<int,int>f1(x,y);
cout<<"max="<<f1.max()<<endl;
cout<<"min="<<f1.min()<<endl;
cout<<"aver"<<f1.aver()<<endl<<endl;
Math<int>f2(x,y);
cout<<"max="<<f2.max()<<endl;
cout<<"min="<<f2.min()<<endl;
cout<<"aver"<<f2.aver()<<endl;
return 0;
}
程序说明:
① 类模板的两个模板参数T1、T2中,最后一个模板参数T2有默认值。
② 定义类模板对象f1时,给出两个模板参数,编译系统根据模板参数的值确定类模板中T1、T2的类型分别为int、int,生成具体的模板类。
③ 定义类模板对象f2时,只给出一个模板参数,另一个模板参数取默认值float,编译系统根据模板参数的值确定类模板中T1、T2的类型分别为int、double,生成具体的模板类。
4.4 程序实例
实现一个 50 以内的加减法练习系统,练习者首先通过设定练习时间,在输入姓名之后,系统开始随机出题,每答对一题得1分,答错不得分。当达到规定的练习时间,将给出最终得分,同时给出最高记录保持者的姓名及最高得分。
#include<iostream>
#include<string>
#include<cstring>
#include<ctime>
using namespace std;
#define TIMES 10 //练习次数
#define NUM 500 //每次练习的最大题数
class Train {
public:
Train(char na[]="noname"):score(0) {}
void setdata(char na[]="noname"); //设置数据
void practice(); //练习函数
static void settime(int t) { //设置练习时间
practicetime = t;
}
static int gettime() { //获得练习时间
return practicetime;
}
private:
char name[20]; //姓名
int data1[NUM]; //随机数1
int data2[NUM]; //随机数2
int answer[NUM]; //答题者给出的答案
int score; //练习得分
static int practicetime; //每次练习时间
static char recordname[20];//最高记录者姓名
static int recordscore; //最高记录得分
};
void Train::setdata(char na[]) {
strcpy(name, na);
score=0;
srand(time(NULL));
int d1, d2, sign;
for(int i=0; i<NUM; i++) {
d1 = rand()%50+1;
d2 = rand()%50+1;
sign = rand();
if(sign%2) { //sign为奇数,做加法运算
data1[i]=d1;
data2[i]=d2;
} else {
data1[i] = d1>d2 ? d1:d2; //使被减数大于减数
data2[i] = -(d1<d2 ? d1:d2);
}
}
}
void Train::practice() {
long begintime, nowtime;
begintime = time(NULL);
for(int i=0; i<NUM; i++) {
cout<<"第"<<i+1<<"题:"<<data1[i]
<<(data2[i]>=0? "+":"")<<data2[i]<<"=";
cin>>answer[i];
nowtime = time(NULL);
if(answer[i]==data1[i]+data2[i]
&& (nowtime-begintime)/60 <= practicetime) {
score++;
} else {
if((nowtime-begintime)/60>practicetime) {
cout<<"已超过"<<practicetime
<<"分钟,你的得分是:"<<score<<"分"<<endl;
break;
}
}
}
if(score>recordscore) {
strcpy(recordname, name);
recordscore = score;
cout<<"祝贺"<<name<<"创造了新得分记录!"<<endl;
} else {
cout<<"最高记录者"<<recordname
<<"获得最高分"<<recordscore<<"分"<<endl;
}
}
int Train::practicetime=0;
char Train::recordname[20]="\0";
int Train::recordscore=0;
int main() {
cout<<"请输入每次练习时间(分钟):";
int time; cin>>time;
Train::settime(time);
Train train[TIMES];
char name[20], choice;
for(int i=0; i<TIMES; i++) { //下一个人
cout<<"请注意练习时间是"<<Train::gettime()<<"分钟"<<endl;
cout<<"请输入姓名:";
cin>>name;
train[i].setdata(name);
train[i].practice();
cout<<"是否开始下一次练习(y/n)?";
cin>>choice;
if(choice!='y' && choice!='Y') {
break;
} else {
system("cls");
}
}
return 0;
}
第5章 运算符重载
5.1 运算符重载的概念
在C++中,所有系统预定义的运算符都是通过运算符函数来实现的。
a+b
operator+(a,b)
operator+(int,int)
重载为成员函数:在类中定义一个同名的运算符函数来重载该函数。
TYPE X::operator@ (形参表) {
//函数体
//重新定义运算符@在指定类X中的功能
}
如果重载单目运算符,就不必另设置参数;
如果是重载双目运算符,就只要设置一个参数作为右侧运算量,而左侧运算量就是该对象本身。
【例】定义一个表示复数的类Complex,并在该类中对运算符“+”进行重载,以实现两个复数的加运算。
#include <iostream>
using namespace std;
class Complex {
public:
Complex(double r=0.0,double i=0.0) {
real = r, image = i;
}
Complex operator+ (Complex &c1){
return Complex(real+c1.real,image+c1.image);
}
void show(){
cout<<"("<<real<<"+"<<image<<"i)"<<endl;
}
private:
double real, image;
};
int main() {
Complex c1(1, 2),c2(2, 3),c3;
c3 = c1+c2;
c1.show(), c2.show(), c3.show();
return 0;
}
重载为友元函数:定义一个与某一运算符函数同名的全局函数;
然后再将该全局函数声明为类的友元函数,从而实现运算符的重载。
friend TYPE operator@ (形参表);
说明:
(1)operator是关键字、@是需要被重载的运算符,TYPE是该运算符函数的返回值类型。
关键字operator与后面的运算符@共同组成了该运算符函数的函数名。
(2)对于双目运算符,参数表中包含两个参数:TYPE operator@(TYPE a, TYPE b)
(3)对于单目运算符,参数表中只包含一个参数:TYPE operator@(TYPE a)
【例】设计一个表示复数的类,在该类中对运算符 + - 以及 = 进行重载,以实现两个复数的加,减以及赋值运算。
#include<iostream>
using namespace std;
class Complex {
public:
Complex(double r=0.0,double i=0.0) {
real=r, image=i;
}
void print() {
cout<<"("<<real<<"+"<<image<<"i"<<")"<<endl;
}
Complex operator+(const Complex& c1) {
return Complex(real+c1.real,image+c1.image);
}
Complex operator-(const Complex& c2);
void operator=(const Complex& c3);
private:
double real, image;
};
Complex Complex::operator-(const Complex& c2) {
Complex c;
c.real = real-c2.real;
c.image = image-c2.image;
return c;
}
void Complex::operator=(const Complex &c3) {
real = c3.real;
image = c3.image;
}
int main() {
Complex a(3.0,4.0),b(1.0,2.0);
Complex c1,c2,c3;
c1 = a+b; c1.print();
c2 = a-b; c2.print();
c3 = c1; c3.print();
return 0;
}
5.2 运算符重载的方法
重载为成员函数
重载为友元函数
5.3 运算符重载的规则
(1)运算符是C++系统内部定义的,具有特定的语法规则,重载运算符时应注意该运算符的优先级和结合性保持不变。
(2)C++规定“.”、“*”、“::”、“?:”、sizeof 不能重载。同时也不能创造新的运算符。
(3)重载运算符时应注意使重载后的运算符的功能与原功能类似,原运算符操作数的个数应保持不变且至少有一个操作数是自定义类型数据。因此重载运算符函数的参数不能有默认值。
(4)由于友元函数破坏了类的封装性,所以重载单目运算符时一般采用成员函数的形式。
单目运算符可以作为类的成员函数重载,也可以作为类的友元函数重载,作为成员函数重载是没有参数,而作为友元函数重载时有一个参数。
(5)重载双目运算符时,若第一个操作数是类对象,则既可以采用成员函数形式也可以采用友元函数形式,若第一个操作数不是类对象,则只能采用友元函数形式。
当重载为类的成员函数时,运算符重载函数的形参个数要比运算符操作数个数少一个;
若重载为友元函数,则参数个数与操作数个数相同。
“=”、“()”、“[]”和“->”等运算符不能用友元函数方式重载。
5.4 常用运算符的重载
5.4.1 算术运算符的重载
【例】有一个Time类,包含数据成员minute(分)和sec(秒);
模拟秒表,每次走一秒,满60秒进一分钟,此时秒又从0开始算。要求输出分和秒的值。
C++约定: 在自增(自减)运算符重载函数中,增加一个int型形参,就是后置自增(自减)运算符函数。
#include <iostream>
using namespace std;
class Time {
public:
Time(int m=0,int s=0):minute(m),sec(s) {}
//声明前置自增运算符 ++ 重载函数
Time operator++();
//声明后置自增运算符 ++ 重载函数
Time operator++(int);
void display( ) {
cout<<minute<<":"<<sec<<endl;
}
private:
int minute, sec;
};
Time Time::operator++() {
if(++sec >= 60) {
sec -= 60;
++minute;
}
return *this; //返回自加后的当前对象
}
Time Time::operator++(int) {
Time temp(*this);
sec++;
if(sec >= 60) {
sec -= 60;
++minute;
}
return temp; //返回的是自加前的对象
}
//注意前置自增运算符 ++ 和后置自增运算符 ++ 二者作用的区别。
//前者是先自加,返回的是修改后的对象本身。后者返回的是自加前的对象,然后对象自加。
int main( ) {
Time time1(34,59),time2;
cout<<" time1 : "; time1.display( );
++time1;
cout<<"++time1: "; time1.display( );
time2=time1++; //将自加前的对象的值赋给time2
cout<<"time1++: "; time1.display( );
cout<<" time2 :"; time2.display( ); //输出time2对象的值
}
5.4.2 关系运算符的重载
#include<iostream>
using namespace std;
class Time {
public:
Time(int h,int m,int s):hour(h),minute(m),second(s) {}
int times() const{
return hour*60*60+minute*60+second;
}
bool operator<(const Time& t) {
return times() < t.times();
}
bool operator>(const Time& t) {
return times() > t.times();
}
private:
int hour,minute,second;
};
int main() {
Time t1(1,2,3), t2(1,2,4);
cout<<(t1<t2)<<endl;
cout<<(t1>t2)<<endl;
return 0;
}
5.4.3 逻辑运算符的重载
&& || !
//声明
friend int operator&& (const Array &ar1,const Array &ar2);
//重载函数
int operator&& (const Array &ar1,const Array &ar2){
int num=0;
for(int i=0; i<ar.n; i++){
for(int j=0; j<ar2.n; j++){
if(ar1.data[i]==ar2.data[j]){
num++; break;
}
}
}
return num;
}
//执行调用
cout<<(arr1 && arr2)<<endl;
5.4.4 位移运算符的重载
<< >>
//重载<<运算符
Array& operator<<(int num){
int temp;
for(int pass=1; pass<=num; pass++){
temp=data[0];
for(int i=0; i<n-1; i++){
data[i]=data[i+1];
}
data[n-1]=temp;
}
}
//执行调用
cout<<(arr<<3)<<endl;
5.4.5 下标访问运算符的重载
int& operator[] (int index){
if(index>=0 && index<=n-1) return data[index];
else {
cout<<"下标超出范围"<<endl;
exit(1);
}
}
5.4.6 赋值运算符的重载
默认的重载赋值运算符的功能是逐个拷贝一个对象的所有数据成员到另外一个对象。
当对象中包含动态分配内存空间的情况有可能出错,因为类的数据成员中包含指针,简单的赋值操作会使得两个对象中的指针成员指向同一个空间,运行时会发生错误,这时则需要用户自己定义重载的赋值运算符。
#include <iostream>
using namespace std;
class Complex {
public:
Complex(double r=0.0,double i=0.0):real(r),image(i) {}
Complex& operator= (const Complex &c){//运算符 + 重载为成员函数
real = c.real, imag = c.imag;
return *this;
}
void show(){
cout<<"("<<real<<"+"<<imag<<")"<<endl;
}
private:
double real, imag;
};
int main() {
Complex c1(1,2),c2;
c2 = c1;
c1.show(), c2.show();
return 0;
}
5.4.7 流输出与流输入运算符的重载
对 << 和 >> 重载的函数形式如下:
istream & operator >> (istream &,自定义类 &);
ostream & operator << (ostream &,自定义类 &);
//只能将重载 >> 和 << 的函数作为友元函数或普通的函数,而不能将它们定义为成员函数。
#include <iostream>
using namespace std;
class Complex {
public:
Complex(double r=0.0,double i=0.0):real(r),image(i) {}
//运算符 << 重载为友元函数
friend ostream& operator<<(ostream& output, Complex & c5);
friend istream& operator>>(istream& input ,Complex& c4);
private:
double real, image;
};
ostream& operator<<(ostream& output, Complex & c5) {
output<<"("<<c5.real<<"+"<<c5.image<<"i)"<<endl;
return output;
}
istream& operator>>(istream& input ,Complex& c4) {
input>>c4.real>>c4.image;
return input;
}
int main() {
Complex c1,c2;
cin>>c1>>c2;
cout<<"c1="<<c1<<endl;
cout<<"c2="<<c2<<endl;
return 0;
}
复数的+ - =运算符的重载
重载流插入,流提取运算符实现复数的输入,输出。
#include <iostream>
using namespace std;
class Complex {
public:
Complex(double r=0.0,double i=0.0):real(r),image(i) {}
Complex& operator+ (const Complex &c);//运算符 + 重载为成员函数
Complex& operator- (const Complex &c);
Complex& operator= (const Complex &c);
friend ostream& operator<<(ostream& output, Complex & c);//运算符 << 重载为友元函数
friend istream& operator>>(istream& input ,Complex& c);
private:
double real, imag;
};
Complex& Complex::operator+ (const Complex &c) {
real += c.real, imag += c.imag;
return *this;
}
Complex& Complex::operator- (const Complex &c) {
real -= c.real, imag -= c.imag;
return *this;
}
Complex& Complex::operator= (const Complex &c) {
real = c.real, imag = c.imag;
return *this;
}
ostream& operator<<(ostream& output, Complex & c) {
output<<"("<<c.real<<"+"<<c.imag<<"i)";
return output;
}
istream& operator>>(istream& input ,Complex& c) {
input>>c.real>>c.imag;
return input;
}
int main() {
Complex c1,c2,c3,c4,c5;
cin>>c1>>c2;
c3 = c1+c2;
c4 = c2-c1;
c5 = c1;
cout<<"c1="<<c1<<endl;
cout<<"c2="<<c2<<endl;
cout<<"c3="<<c3<<endl;
cout<<"c4="<<c4<<endl;
cout<<"c5="<<c5<<endl;
return 0;
}
5.4.8 不同类型数据之间的转换
方式一:定义运算符重载函数
//第一个对象是自定义数据类型,可以使用成员函数,友元函数
friend Complex operator+ (Complex c, double d);
Complex operator+ (Complex c, double d) {
return Complex(c.real+d, c.imag);
}
//第一个对象不是是自定义数据类型,只能使用友元函数
friend Complex operator+ (double d, Complex c);
Complex operator+ (double d, Complex c) {
return Complex(c.real+d, c.imag);
}
方式二:类型转化运算符重载函数
operator double(){
return real;
}
Complex c1(5, 6), c2(3, 4);
cout<<c1+2.3<<endl; // 执行 c1.operator double() 将 c1 转换为double
cout<<2.3+c2<<endl; // 执行 c2.operator double() 将 c2 转换为double
Complex(double r):real(r), imag(0){} //转换构造函数
Complex c1(5, 6), c2(3, 4), c3;
c3 = c1+2.3; // 将 2.3 转换为 Complex 对象,并计算两个对象的值
5.5 字符串类
字符串String类
【例】创建一个字符串类String,并重载赋值运算符“=”为其成员函数。
#include <iostream>
#include <cstring>
using namespace std;
class String {
public:
String(char* p="") {
str = new char[strlen(p)+1];
strcpy(str,p);
}
~String() {
delete []str;
}
void print() {
cout<<str<<endl;
}
String& operator=(const String &s) {
delete []str;
str = new char[strlen(s.str)+1];
strcpy(str,s.str);
return *this;
}
private:
char* str;
};
int main() {
String s1("This is a string!"), s2;
s1.print();
s2.print();
s2 = s1;
s2.print();
return 0;
}
【例】创建一个字符串类String,并重载运算符 “!” 为其成员函数,用于判断对象中的字符串是否为空。
bool operator! (){
return strlen(str)!=0;
}
int main() {
String s1("This is a string!"), s2;
if(!s1) cout<<"s1 is not null"<<endl;
if(!s2) cout<<"s2 is not null"<<endl;
return 0;
}
【例】创建一个字符串类String,并重载运算符 “!” 为其友元函数,用于判断对象中的字符串是否为空。
friend bool operator! (const String &s);
bool operator! (const String &s) {
return strlen(s.str)!=0;
}
【例】设计一个字符串类String。并使其能完成以下功能:
(1)能使用“=”运算符完成两个字符串的赋值。
(2)能使用“== ” 运算符完成对两个字符串是否相等的判断。
(3)能使用“+=”运算符完成两个字符串的连接。编写相应的程序实现该类,并进行测试。
分析:
根据本题的题意,要设计的字符串类String,应包含1个指向字符串的指针p_str的数据成员和以下的成员函数:==
(1)相应的构造函数、拷贝构造函数和析构函数;==
(2)对=、==、+=三个运算符进行重载,使其能直接对两个字符串进行相应的运算;
(3)显示函数display, 用于显示对象中的字符串。
#include<iostream>
#include<cstring>
using namespace std;
class String {
public:
String(char* str=NULL);
String(String& s);
~String();
void operator=(String& str);
int operator==(String& str);
String operator+=(String& str);
void display() {
cout<<p_str<<endl;
}
private:
char* p_str;
};
String::String(char* str) {
if(str!=NULL) {
p_str=new char[strlen(str)+1];
strcpy(p_str,str);
} else p_str=NULL;
}
String::String(String& str) {
p_str=new char[strlen(str.p_str)+1];
strcpy(p_str,str.p_str);
}
String::~String() {
delete p_str;
}
void String::operator=(String& str) {
p_str=new char[strlen(str.p_str)+1];
strcpy(p_str,str.p_str);
}
int String::operator==(String& str) {
return (strcmp(p_str,str.p_str)==0);
}
String String::operator+=(String& str) {
char* s = new char[strlen(p_str)+1];
strcpy(s,p_str);
delete p_str;
p_str=new char[strlen(s)+strlen(str.p_str)+1];
strcpy(p_str,s);
strcat(p_str,str.p_str);
delete s;
return *this;
}
int main() {
String s1("ABC"),s2("abcde");
cout<<"字符串s1:"; s1.display();
cout<<"字符串s2:"; s2.display();
if(s1==s2) cout<<"字符串s1和字符串s2相等"<<endl<<endl;
else cout<<"字符串s1和字符串s2不相等"<<endl<<endl;
s2 = s1;
cout<<"赋值之后的字符串s2:"; s2.display();
if(s1==s2) cout<<"字符串s1和字符串s2相等"<<endl<<endl;
else cout<<"字符串s1和字符串s2不相等"<<endl<<endl;
s1 += s2;
cout<<"字符串s1和s2连接,连接之后的结果放在s1中,为:";
s1.display();
return 0;
}
5.6 程序实例
#include<iostream>
#include<algorithm>
#include<string>
#include<cstring>
using namespace std;
const int N=1000;
class BigInt {
public:
BigInt() {
n=0;
for(int i=0; i<N; i++) num[i]=0;
}
BigInt(string s) {
int len=s.size();
for(int i=0; i<len; i++) num[i] = s[len-1-i]-'0';
n = len;
for(int i=n; i<N; i++) num[i]=0;
}
friend bool operator< (BigInt a,BigInt b);
friend BigInt operator+ (BigInt a, BigInt b);
friend BigInt operator- (BigInt a, BigInt b);
friend istream& operator>> (istream& input,BigInt& c);
friend ostream& operator<< (ostream& output,BigInt& c);
private:
int num[N], n;
};
bool operator< (BigInt a,BigInt b) {
if(a.n!=b.n) return a.n<b.n;
for(int i=a.n-1; i>=0; i--) {
if(a.num[i]!=b.num[i]) return a.num[i]<b.num[i];
}
return 0;
}
BigInt operator+ (BigInt a,BigInt b) {
BigInt c;
c.n = a.n > b.n ? a.n : b.n;
for(int i=0; i<c.n; i++) {
c.num[i] += a.num[i]+b.num[i];
c.num[i+1] += c.num[i]/10;
c.num[i] %= 10;
}
while(c.num[c.n]) c.n++;//进位
return c;
}
BigInt operator- (BigInt a,BigInt b) {// a>=b
BigInt c;
c.n = a.n > b.n ? a.n : b.n;
for(int i=0; i<c.n; i++) {
c.num[i] = a.num[i]-b.num[i];
if(c.num[i]<0) {
a.num[i+1]--, c.num[i]+=10;
}
}
while(c.n>1 && c.num[c.n-1]==0) c.n--;
return c;
}
istream& operator>> (istream& input,BigInt& t) {
string s; cin>>s;
t.n = s.size();
for(int i=0; i<t.n; i++) t.num[i] = s[t.n-1-i]-'0';
return input;
}
ostream& operator<< (ostream& out,BigInt& t) {
for(int i=t.n-1; i>=0; i--) cout<<t.num[i];
return out;
}
int main() {
BigInt a,b; cin>>a>>b;
BigInt c = a+b;
cout<<a<<" + "<<b<<" = "<<c<<endl;
if(a<b) swap(a, b);
BigInt d = a-b;
cout<<a<<" - "<<b<<" = "<<d<<endl;
long long x = 987654321, y = 123456789;
cout<<x<<" - "<<y<<" = "<<x-y<<endl;
return 0;
}
大数四则运算
#include<iostream>
#include<string> // string
#include<cstring> // memset
#include<algorithm> // reverse
#include<iomanip> // setprecision
using namespace std;
const int N=1e6+10;
int A[N],B[N],C[N],la,lb,lc;
// 初始化,同时反转数组
void init(string a,string b) {
la = a.size(), lb = b.size();
memset(A, 0, sizeof(A));
memset(B, 0, sizeof(B));
memset(C, 0, sizeof(C));
for(int i=0; i<la; i++) A[i]=a[i]-'0';
for(int i=0; i<lb; i++) B[i]=b[i]-'0';
reverse(A, A+la), reverse(B, B+lb);
}
// 高精加高精
string add(string a,string b) {
init(a,b);
lc = max(la, lb);
for(int i=0; i<lc; i++) {
C[i] = A[i]+B[i]+C[i];
C[i+1] = C[i]/10;
C[i] %= 10;
}
while(C[lc]) lc++; //进位
string c;
for(int i=lc-1; i>=0; i--) c.append(1, C[i]+'0');
return c;
}
// 高精减高精
string sub(string a,string b) {
init(a, b);
lc = max(la, lb);
for(int i=0; i<lc; i++) {
C[i] = A[i]-B[i];
if(C[i]<0) A[i+1]--, C[i]+=10; // 借位
}
while(lc>1 && C[lc-1]==0) lc--;// 去除前导 0
string c;
for(int i=lc-1; i>=0; i--) c.append(1, C[i]+'0');
return c;
}
// 高精乘高精
string mul(string a,string b) {
init(a,b);
lc = la+lb-1;
for(int i=0; i<la; i++) {
for(int j=0; j<lb; j++) {
C[i+j] += A[i]*B[j];
}
}
for(int i=0; i<lc; i++) {
C[i+1] += C[i]/10;
C[i] %= 10;
}
while(C[lc]) lc++; // 进位
string c;
for(int i=lc-1; i>=0; i--) c.append(1, C[i]+'0');
return c;
}
// 高精除以低精
void div1(string a,int b) {
la = a.size(), lc = 0;
for(int i=0; i<la; i++) A[i]=a[i]-'0';
int x=0; // x 为被除数,最后为余数
for(int i=0; i<la; i++) {
x = x*10+A[i];
C[i] = x/b;
x %= b;
}
while(lc<la && C[lc]==0) lc++; // 去除前导 0
for(int i=lc; i<la; i++) cout<<C[i]; cout<<"..."<<x<<endl;
}
// return a>=b;
bool cmp(string a,string b) {
if(a.size() > b.size()) return 1;
if(a.size() < b.size()) return 0;
for(int i=0; i<a.size(); i++) {
if(a[i]>b[i]) return 1;
if(a[i]<b[i]) return 0;
}
return 1;
}
// 高精除以高精
void div2(string a,string b) {
string c, d;
for(int i=0; i<a.size(); i++) {
d = d.append(1, a[i]);// 余数可能为 0,需要去除前导 0
while(d.find('0')==0 && d.size()>1) d.erase(0, 1);
if(cmp(d, b)) {
for(int k=9; k>=1; k--) { //试商
string x;
x.append(1, k+'0');
x = mul(x, b);
if(cmp(d, x)) {
d = sub(d, x);
c.append(1, k+'0');
break;
}
}
} else c.append(1, '0');// 不足商,则置 0
}
while(c.find('0')==0 && c.size()>1) c.erase(0,1);// 去除前导 0
cout<<c<<"..."<<d<<endl;
}
int main() {
// freopen("data.in", "r", stdin);
string a="987654321",b="123456789"; // cin>>a>>b;
long long x=987654321, y=123456789;
cout<<x<<" + "<<y<<" = "<<x+y<<endl;
cout<<x<<" + "<<y<<" = "<<add(a, b)<<endl;
cout<<x<<" - "<<y<<" = "<<x-y<<endl;
cout<<x<<" - "<<y<<" = "<<sub(a, b)<<endl;
cout<<x<<" * "<<y<<" = "<<1ll*x*y<<endl;
cout<<x<<" * "<<y<<" = "<<mul(a, b)<<endl;
cout<<x<<" / "<<y<<" = "<<x/y<<"..."<<x%y<<endl;
cout<<x<<" / "<<y<<" = "; div1(a, y);
cout<<x<<" / "<<y<<" = "; div2(a, b);
return 0;
}
第6章 继承与派生
面向对象程序设计有4个主要特点: 抽象、封装、继承和多态性。
面向对象技术强调软件的可重用性。
C++语言提供了类的继承机制,解决了软件重用问题。
继承(Inheritance)就是在一个已存在的类的基础上建立一个新类,实质就是利用已有的数据类型定义出新的数据类型。
在继承关系中:
被继承的类称为基类(Base class)(或父类)
定义出来的新类称为派生类(Derived class)(子类)
派生类不仅可以继承原来类的成员,还可以通过以下方式扩充新的成员:
(1)增加新的数据成员
(2)增加新的成员函数
(3)重新定义已有成员函数
(4)改变现有成员的属性
多层次继承:
在派生过程中,派生出来的新类同样可以作为基类再继续派生出更新的类,依此类推形成一个层次结构。
直接参与派生出某类称为直接基类;
基类的基类,以及更深层的基类称为间接基类。
在类的层次结构中,处于高层的类表示最一般的特征,而处于底层的类则表示更具体的特征。
类族:同时一个基类可以直接派生出多个派生类。形成了一个相互关联的类族。
如MFC就是这样的类族,它由一个CObject类派生出200个MFC类中的绝大多数。
单继承:派生类只有一个直接基类。
多重继承:派生类同时有多个直接基类。
关于基类和派生类的关系,可以表述为:派生类是基类的具体化,而基类则是派生类的抽象。
声明派生类的一般形式为:
class 派生类名: [继承方式] 基类名 {
派生类新增加的成员
};
#include<iostream>
using namespace std;
class A{
private:
char s[21];
};
class B: public A{
private:
int num;
};
int main(){
A a; B b;
cout<<"sizeof(A) = "<<sizeof(A)<<endl;// 21
cout<<"sizeof(B) = "<<sizeof(B)<<endl;// 28
return 0;
}
继承方式是可选的,如果不写,则默认为private(私有的)。
基类名必须是程序中已有的一个类名
继承方式包括:public(公用的)、private(私有的)、protected(受保护的)
public:可以被该类中的函数、子类的函数、友元函数访问,也可以由该类的对象访问;
protected:可以被该类中的函数、子类的函数、友元函数访问,但不可以由该类的对象访问;
private:可以被该类中的函数、友元函数访问,但不可以由子类的函数、该类的对象访问。
private 关键字的作用在于更好地隐藏类的内部实现。
如果声明不写 public、protected、private,则默认为 private;
声明public、protected、private的顺序可以任意;
在一个类中,public、protected、private 可以出现多次,每个限定符的有效范围到出现另一个限定符或类结束为止。但为了使程序清晰,应该使每种限定符只出现一次。
不同继承方式的影响主要体现在:
- 派生类成员对基类成员的访问控制;
- 派生类对象对基类成员的访问控制
派生类拥有基类的全部成员函数和成员变量,派生类中不能访问基类中的private成员。
#include<iostream>
#include<string>
#include<cstring>
using namespace std;
class A{
private:
char s[21];
public:
A(){ strcpy(s, "hello"); }
void show(){ cout<<"s = "<<string(s)<<endl; }
};
class B: public A{
private:
int num;
public:
void show(){ // 重名覆盖
//show(); // 递归操作,不合法,猜一猜这样输出的结果会是什么
A::show();// 指定基类
cout<<"num = "<<num<<endl;
}
};
int main(){
A a; B b;
a.show(); b.show();
return 0;
}
派生新类经历了三个步骤:
-
吸收基类成员
派生类继承和吸收了基类的全部数据成员和除了构造函数、析构函数之外的全部成员函数。
-
改造基类成员
一是基类成员的访问方式问题;
二是对基类数据成员或成员函数的覆盖。
-
添加新成员
保证了派生类在功能上比基类有所发展。
继承方式对基类成员的访问属性控制
-
公有继承(public继承方式)
基类中public和protected成员的访问属性在派生类中不变;
基类中的不可访问成员和private成员在派生类中不可访问。
注意:不可访问成员与私有成员的区别。
-
私有继承(private继承方式)
基类中public和protected成员都以private成员出现在派生类中;
基类中的不可访问成员和private成员在派生类中不可访问。
相当于中止了基类功能的继续派生!
-
保护继承(protected继承方式)
基类中public和protected成员都以protected成员出现在派生类中;
基类中的不可访问成员和private成员在派生类中不可访问。
类的继承方式对基类成员的访问属性控制
#include<iostream>
using namespace std;
class A {
public:
void f1();
protected:
void f2();
private:
int i;
};
class B: public A {
public:
void f3();
int k;
private:
int m;
};
class C: protected B {
public:
void f4();
protected:
int n;
private:
int p;
};
class D: private C {
public:
void f5();
protected:
int q;
private:
int r;
};
int main() {
A a1;
B b1;
C c1;
D d1;
return 0;
}
类的范围 | f1 | f2 | i | f3 | k | m | f4 | n | p | f5 | q | r |
---|---|---|---|---|---|---|---|---|---|---|---|---|
基类A | 公用 | 保护 | 私有 | |||||||||
公用派生类B | 公用 | 保护 | 不可访问 | 公用 | 公用 | 私有 | ||||||
保护派生类C | 保护 | 保护 | 不可访问 | 保护 | 保护 | 不可访问 | 公用 | 保护 | 私有 | |||
私有派生类D | 私有 | 私有 | 不可访问 | 私有 | 私有 | 不可访问 | 私有 | 私有 | 不可访问 | 公用 | 保护 | 私有 |
#include<iostream>
using namespace std;
class A {
public:
int a;
protected :
int b;
private:
int c;
};
class B:public A {
public:
void show() {
cout<<a<<endl; // public
cout<<b<<endl; // proteced
// cout<<c<<endl; // 不可访问
}
};
class C:protected A {
public:
void show() {
cout<<a<<endl; // proteced
cout<<b<<endl; // proteced
// cout<<c<<endl; // 不可访问
}
};
class D:private A {
public:
void show() {
cout<<a<<endl;// private
cout<<b<<endl;// private
// cout<<c<<endl; //不可访问
}
};
int main() {
A p1;
B p2; p2.show();
C p3; p3.show();
D p4; p4.show();
// cout<<p4.a; // private
return 0;
}
【例】请先建立一个点(Point)类, 包含数据成员坐标点(x, y)。然后以它为基类,派生出一个圆类(Circle),增加数据成员半径(r),再以Circle类为直接基类,派生出一个圆柱体类(Cylinder),增加数据成员高(height)。
#include<iostream>
using namespace std;
class Point{
public:
double _x,_y;
};
class Circle: public Point{
public:
double _r;
};
class Cylinder: public Circle{
public:
double _height;
};
int main(){
Cylinder c1;
return 0;
}
请思考:派生类的构造函数的定义应该怎么写?
(1)基类的构造函数不能继承,从基类继承来的数据怎么初始化?
(2)派生类新增的数据成员怎么初始化?
派生类的构造函数:
(1)一方面负责调用基类的构造函数对基类成员进行初始化;
(2)另一方面还要负责对基类的构造函数所需要的参数进行必要的设置。
派生类构造函数的定义格式:
派生类名::派生类构造函数名 (总参数列表):基类构造函数名 (参数列表) {
派生类中新增数据成员初始化语句
}
总参数列表:基类和派生类的所有参数
可省略对基类构造函数的调用,但是要求基类中必须有无参构造函数或者根本没有定义构造函数
在实例化派生类的对象时,系统首先执行基类的构造函数,然后执行派生类的构造函数。
调用基类构造函数的两种方式:
- 显式方式:在派生类的构造函数中,为基类构造函数提供参数
- 隐式方式:在派生类的构造函数中,省略基类构造函数时,派生类的构造函数自动调用基类的默认构造函数,但是要求基类的默认构造函数存在。
派生类成员的访问之------同名覆盖原则
请思考:当派生类和基类中有相同的成员时,怎么办??
若未强行指明,则通过派生类的对象使用的是派生类中的同名成员。
如果要通过派生类的对象访问基类中被覆盖的同名成员,应该使用基类名来限定。
派生类对基类成员重新定义:
通过派生类的对象调用一个被重新定义过的基类的成员函数,被调用的是派生类的成员函数;
此时,若想调用基类的同名成员函数,必须在成员函数名前加基类名和作用域运算符“::”。
【例】定义一个描述圆的类Circle和一个描述圆柱体的类Cylinder。
#include<iostream>
using namespace std;
const double PI = 3.14;
class Point{
public:
double _x,_y;
Point(double x,double y) {
_x = x, _y = y;
}
};
class Circle: public Point{
public:
double _r;
Circle(double x,double y,double r):Point(x,y){
_r = r;
}
double area(){
return PI*_r*_r;
}
};
class Cylinder: public Circle{
public:
double _height;
Cylinder(double x,double y,double r,double height):Circle(x,y,r){
_height = height;
}
double area(){
return Circle::area()*2 + 2*PI*_r*_height;
}
};
int main(){
Point p1(0,0);
Circle c1(1,1,1);
Cylinder c2(2,2,2,2);
cout<<"圆的面积为:"<<c1.area()<<endl;
cout<<"圆柱体表面积:"<<c2.area()<<endl;
cout<<"圆柱体底面积:"<<c2.Circle::area()<<endl;
return 0;
}
在C++中,处理同名函数时有以下3种基本方法:
-
根据函数参数的特征进行区分,即编译器根据参数的类型或个数进行区分。
如:max(int,int) max(float,float)
-
根据类对象进行区分。
如:cylinder.area() circle.area()
-
使用作用域运算符“::”进行区分
如:Circle::area()
以上3种方法都是在程序编译过程中完成的,因此称为静态联编。
有子对象的派生类的构造函数
派生类名::派生类构造函数名(总参数列表):基类构造函数名(参数列表),子对象名(参数列),...{
派生类中新增数据成员初始化语句
}
此时,构造函数执行的一般次序为:
-
调用基类的构造函数;
-
调用子对象的构造函数,当派生类中含有多个子对象时,各子对象的构造函数的调用顺序按照它们在类中说明的先后顺序进行。
-
执行派生类构造函数的函数体。
派生类的析构函数
析构函数的作用是在对象撤销之前,进行必要的清理工作。
当对象被删除时,系统会自动调用析构函数。
析构函数的调用顺序与构造函数的调用顺序正好相反:
先执行派生类自己的析构函数,然后调用子对象的析构函数,最后调用基类的析构函数。
【例】分析以下程序的执行结果。
#include <iostream>
using namespace std;
class Base {
private:
int x;
public:
Base(int a) {
x = a;
cout<<"执行基类Base的构造函数"<<endl;
}
~Base() {
cout<<"执行基类Base的析构函数"<<endl;
}
};
class MyClass {
private:
int n;
public:
MyClass(int num) {
n = num;
cout<<"执行MyClass类的构造函数"<<endl;
}
~MyClass() {
cout<<"执行MyClass类的析构函数"<<endl;
}
};
class Derive:public Base {
private:
int y;
MyClass bobj;
public:
Derive(int a,int b,int c):bobj(c),Base(a) {
y = b;
cout<<"执行派生类Derive的构造函数"<<endl;
}
~Derive() {
cout<<"执行派生类Derive的析构函数"<<endl;
}
};
int main() {
Derive dobj(1,2,3);
return 0;
}
赋值兼容
#include<iostream>
using namespace std;
class A {
public:
void show() { cout<<"A::show(){}"<<endl; }
};
class B: public A {
public:
void show() { cout<<"B::show(){}"<<endl; }
};
//void show(B &b){ b.show(); }
void show(A &a) { a.show();}
int main() {
A a; B b;
// 下列赋值兼容的具体表现是在public继承下才可以
a = b; //(1)派生类对象可以向基类对象赋值
A& pr = b; //(2)派生类对象可以初始化基类对象的引用
A *pb = &b;//(3)派生类对象地址可以赋值给指向基类对象的指针
show(b); //(4)如果函数形参是基类对象或基类对象的引用,在调用函数时可以将派生类对象作为实参
return 0;
}
多重继承的定义方式
class 派生类名:访问方式 基类名1,访问方式 基类名2,... {
...
};
多重继承下,派生类的构造函数的定义格式:
派生类构造函数名(参数表):基类名1(参数表1),基类名2(参数表2),...{
...
}
在多重继承下,系统首先执行各基类的构造函数,然后再执行派生类的构造函数;
处于同一层次的各基类构造函数的执行顺序与声明派生类时所继承的基类顺序一致,而与派生类的构造函数定义中所调用的基类构造函数的顺序无关。
【例】在多重继承关系下基类和派生类的构造函数的执行顺序。
#include <iostream>
using namespace std;
class B1 {
protected:
int b1;
public:
B1(int val1) {
b1 = val1;
cout<<"基类B1的构造函数被调用"<<endl;
}
};
class B2 {
protected:
int b2;
public:
B2(int val2) {
b2 = val2;
cout<<"基类B2的构造函数被调用"<<endl;
}
};
class D:public B1,public B2 {
protected:
int d;
public:
D(int val1,int val2,int val3):B1(val1),B2(val2) {
d = val3;
cout<<"派生类D的构造函数被调用"<<endl;
}
};
int main() {
D dobj(1,2,3);
return 0;
}
多重继承的二义性
多重继承下,可能会产生一个类是通过多条路径从一个给定的类中派生出来的情况。
1.多重继承的二义性
情况一:被继承的多个基类中具有同名成员,在派生类中对该同名成员的访问会产生二义性。
情况二:被继承的多个基类有一个共同的基类,在派生类中访问这个共同基类的成员会产生二义性。
情况二也被称为菱形继承:一个派生类可从多个基类派生出来,又由于一个基类可派生出多个派生类。
2.多重继承的二义性问题的解决方法
一是使用作用域运算符::
二是将直接基类的共同基类设置为虚基类。
直接基类名::数据成员名
直接基类名::成员函数名(参数表)
#include<iostream>
using namespace std;//情况一:多个基类中具有同名成员
class Base1{
public:
void fun(){ cout<<"Base1::fun(){}"<<endl; }
};
class Base2{
public:
void fun(){ cout<<"Base2::fun(){}"<<endl; }
};
class Derived:public Base1, public Base2{};
int main(){
Derived d;
// d.fun();// 产生二义性
d.Base1::fun();//使用作用域运算符可以解决二义性问题
return 0;
}
#include<iostream>
using namespace std;//情况二:多个基类有一个共同的基类
class Base{
public:
void fun(){ cout<<"Base::fun(){}"<<endl; }
};
class Derived11:public Base{ };
class Derived12:public Base{ };
class Derived2:public Derived11, public Derived12{};
int main(){
Derived2 d;
// d.fun();// 产生二义性
d.Derived11::fun();//使用作用域运算符可以解决二义性问题
d.Derived12::fun();//使用作用域运算符可以解决二义性问题
return 0;
}
虚基类
虚基类及其派生类的构造函数
如果一个派生类有多个直接基类,而这些直接基类又有一个共同的基类,则在最终的派生类中会保留该间接共同基类数据成员的多份同名成员。
在一个类中保留间接共同基类的多份同名成员,这种现象是不希望出现的。
C++提供虚基类(virtual base class )的方法,使得在间接继承共同基类时只保留一份成员(在内存中只有基类成员的一份拷贝),通过把基类继承声明为虚拟的,就只能继承基类的一份拷贝,从而消除歧义。
我们已经知道,当某个派生类的部分或全部直接基类是从另一个共同基类派生而来时,在这些直接基类中从上一级共同基类继承来的成员就拥有相同的名称。在派生类的对象中,这些同名数据成员在内存中同时拥有多个拷贝,同一个成员函数名会有多个映射。
这时,如果将共同基类设置为虚基类,从不同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。这样就解决了同名成员的惟一标识问题。
虚基类的声明是在定义派生类时完成,即在定义派生类时,在基类的访问方式前加上关键字“virtual”,格式如下:
class 派生类名:virtual 访问方式 基类名 {
//声明派生类成员
};
#include<iostream>
using namespace std;
class Base{
public:
void fun(){ cout<<"Base::fun(){}"<<endl; }
};
class Derived11:virtual public Base{ };//虚基类
class Derived12:virtual public Base{ };
class Derived2:public Derived11, public Derived12{};
int main(){
Derived2 d;
d.fun();// 不产生二义性
d.Derived11::fun();// 提问:这样可以吗?
d.Derived12::fun();
return 0;
}
虚基类及其派生类的构造函数
虚基类的声明是在定义派生类时完成。
虚基类虽然被一个派生类间接地多次继承,但派生类却只继承一份该基类的成员。
对于虚基类的任何派生类,其构造函数不仅负责调用直接基类的构造函数,还需调用虚基类的构造函数。
考虑到虚基类的派生类的构造函数调用顺序,规定:
- 虚基类的构造函数在非虚基类之前调用。
- 若同一层次中包含多个虚基类,虚基类构造函数按它们说明的次序调用。
- 若虚基类由非虚基类派生,则遵守先调用基类构造函数,再调用派生类构造函数的规则。
class X:public Y, virtual public Z {
...
};
X obj; // 将产生如下调用次序: Z() Y() X()
这里Z是X的虚基类,故先调用Z的构造函数,再调用Y的构造函数,最后才调用派生类X自己的构造函数。
虚基类的几点总结和补充:
-
一个类可以在一个类族中既被用作虚基类,也被用作非虚基类。
-
在派生类的对象中,同名的虚基类只产生一个虚基类子对象,而非虚基类产生各自的子对象。
-
虚基类子对象是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。
-
最远派生类是指在继承结构中建立对象时所指定的类。
-
派生类的构造函数的成员初始化列表中必须列出对虚基类构造函数的调用;
如果未列出,则表示使用该虚基类的缺省构造函数。
-
从虚基类直接或间接派生的派生类中的构造函数的成员初始化列表中都要列出对虚基类构造函数的调用。
但仅仅用建立对象的最远派生类的构造函数调用虚基类的构造函数,而该派生类的所有基类中列出的对虚基类的构造函数的调用在执行中被忽略,从而保证对虚基类子对象只初始化一次。
-
在一个成员初始化列表中同时出现对虚基类和非虚基类构造函数的调用时,虚基类的构造函数先于非虚基类的构造函数执行。
例:由A、B、C、D类派生E类,其中A、C类为虚基类,则定义E类对象时构造函数的执行顺序。
#include <iostream>
using namespace std;
class A {
public:
A() { cout<<"A\n"; }
~A() { cout<<"~A\n"; }
};
class B {
public:
B() { cout<<"B\n"; }
~B() { cout<<"~B\n"; }
};
class C {
public:
C() { cout<<"C\n"; }
~C() { cout<<"~C\n"; }
};
class D {
public:
D() { cout<<"D\n"; }
~D() { cout<<"~D\n"; }
};
class E:public A,virtual public B,public C,virtual public D {
public:
E() { cout<<"E\n"; }
~E() { cout<<"~E\n"; }
};
int main() {
E e;
return 0;
}
运行结果:
B
D
A
C
E
~E
~C
~A
~D
~B
解决二义性的两种方法比较
-
作用域运算符::
在派生类中拥有同名成员的多个拷贝,分别通过直接基类名来惟一标识同名成员,可以存放不同的数据,进行不同的操作。可以理解为此方法并未解决这个问题,而是避免向外展示这个问题。
-
虚基类
只维护一个成员拷贝,使用更为简洁,内存空间更为节省。
对多重继承二义性的一点建议
使用多重继承时要十分小心,经常会出现二义性问题。
许多专业人员认为:不提倡在程序中使用多重继承,只有在比较简单和不易出现二义性的情况或实在必要时才使用多重继承,能用单一继承解决的问题就不要使用多重继承。也是由于这个原因,有些面向对象的程序设计语言(如Java,Smalltalk)并不支持多重继承。
【作业】分别定义一个日期类和一个时间类。然后派生出一个新的日期时间类。
#include<iostream>
using namespace std;
class Date {
int year,month,day;
public:
Date():year(2022), month(4), day(30){}
void show() {
cout<<year<<"/"<<month<<"/"<<day;
}
};
class Time {
int hour,minute,second;
public:
Time():hour(7), minute(8), second(9){}
void show() {
cout<<hour<<":"<<minute<<":"<<second;
}
};
class NewTime : public Date, public Time {
public:
void show() {
Date::show(); cout<<" ";
Time::show(); cout<<endl;
}
};
int main() {
NewTime time;
time.show();
return 0;
}
类与类之间的关系
- 继承:A类拥有B类全部属性,并且可以额外拥有一些B类没有的特征, A is B。
class B :public A{
};
- 实现:对应的是面向对象中的"接口",C++中,接口通过纯虚函数来实现。
#include<iostream>
using namespace std;
class A{
public:
virtual void fun()=0; //纯虚函数
};
class B: public A{
public:
void fun(){
cout<<"B::fun()"<<endl;
}
};
int main(){
A *pa = new B();
pa->fun();
return 0;
}
-
依赖:A类使用到了B类一部分属性或方法,但A类中不包含B类。
对于类A和类B,若出现下面情况,称为类A依赖类B:
-
类A中某个方法的形参是类B类型。
-
类A中某个方法的返回类型是类B类型。
-
类A中某个方法中的局部变量是类B类型。
class A {
public:
int a;
};
class B {
public:
void fun(){
A a;
}
};
-
关联:是对象之间的拥有关系,如果B类中某个成员变量的类型是A类, 称B关联于A。
两个类之间语义级别的一种强依赖关系,比如我和我的朋友,这种关系比依赖更强、不存在依赖关系的偶然性、关系也不是临时性的,一般是长期性的,而且双方的关系一般是平等的。关联可以是单向、双向的。表现在代码层面,为被关联类B以类的属性形式出现在关联类A中,也可能是关联类A引用了一个类型为被关联类B的全局变量。
-
聚合:A类独立于B类存在,且可以被多个类共享,在B类中用一个指针指向A。
聚合是关联关系的一种特例,它体现的是整体与部分的关系,即has-a的关系。
此时整体与部分之间是可分离的,它们可以具有各自的生命周期,部分可以属于多个整体对象,也可以为多个整体对象共享。比如计算机与CPU、公司与员工的关系等,比如一个航母编队包括海空母舰、驱护舰艇、舰载飞机及核动力攻击潜艇等。表现在代码层面,和关联关系是一致的,只能从语义级别来区分。
class B{
public:
A* a;
};
-
组合:也称为包含关系,B类的一个成员是A类的对象。
组合也是关联关系的一种特例,它体现的是一种contains-a的关系,这种关系比聚合更强,也称为强聚合。
它同样体现整体与部分间的关系,但此时整体与部分是不可分的,整体的生命周期结束也就意味着部分的生命周期结束,比如人和人的大脑。表现在代码层面,和关联关系是一致的,只能从语义级别来区分。
class B{
public:
A a;
};
参考文章:https://blog.csdn.net/weixin_38234890/article/details/80055362
多态
当一个基类被继承为不同的派生类时,各派生类可以使用与基类成员相同的成员名,如果运行时使用同一成员名调用对象的成员,会调用哪个对象的成员?
多态是指类族中具有相似功能的不同函数使用同一个名称来实现,从而可以使用相同的调用方式来调用这些具有不同功能的同名函数。
多态性就是指同样的消息被类的不同对象接收时导致的完全不同的行为的一种现象。
消息即对类的成员函数的调用,不同的行为是指不同的实现,也就是调用了不同的函数。
多态性实质是指同一个函数的多种形态。
#include<iostream>
using namespace std;
class Point{
public:
Point(double a=0,double b=0):x(a),y(b){}
double area(){ return 0; }
protected:
double x,y;
};
class Circle: public Point{
public:
Circle(double a=0,double b=0,double c=0):Point(a,b),r(c){}
double area(){ return 3.1415926*r*r; }
private:
double r;
};
int main(){
Point p(1,1);
Circle c(1,1,10);
cout<<p.area()<<endl;
cout<<c.area()<<endl;
return 0;
}
虚函数
#include<iostream>
using namespace std;
class Point{
public:
Point(double a=0,double b=0):x(a),y(b){}
double area(){ return 0; }
// virtual double area(){ return 0; } // 虚函数
protected:
double x,y;
};
class Circle: public Point{
public:
Circle(double a=0,double b=0,double c=0):Point(a,b),r(c){}
double area(){ return 3.1415926*r*r; }
private:
double r;
};
void test(Point& temp){ cout<<temp.area()<<endl; }
int main(){
Point p(1,1);
Circle c(1,1,10);
test(p); test(c); // 输出结果相同,均为 0
return 0;
}
虚函数的作用是实现动态联编,也就是在程序的运行阶段动态地选择合适的成员函数。
联编:是指把一个消息和一个方法联系在一起,也就是把一个函数名与其实现代码联系在一起,实质是把一个标识符名和一个存储地址联系在一起的过程。
根据实现联编的阶段的不同,可将其分为静态联编和动态联编两种。
两种联编过程分别对应着多态两种实现方式:
- 在编译时的多态是通过静态联编实现的;
- 在运行时的多态则是通过动态联编实现的。
普通函数及类的成员函数的重载就实现了一种多态性。
在继承与派生的环境中:
当通过对象名调用某个成员函数时,只可能是调用对象自身的成员,所以这种情况可采用静态联编实现。
当通过基类指针调用成员函数时,只有在运行时才能确定实际操作对象的类,并由此确定应该调用哪个类中的成员函数,这种运行时的多态性是由对象赋值的兼容规则所引起的。
赋值兼容规则
只有公用派生类才是基类真正的子类型,它完整地继承了基类的功能。
一个公有派生类的对象可以提供其基类对象的全部行为(基类的全部接口)。
赋值兼容规则是指在公有继承情况下,对于某些场合,一个派生类的对象可以作为基类对象来使用,也就是在需要基类对象的任何地方都可以使用公有派生类的对象来替代。
用基类指针指向公有派生类对象
基类指针、派生类指针、基类对象和派生类对象四者间有以下4种组合的情况:
(1)直接用基类指针指向基类对象。
(2)直接用派生类指针指向派生类对象。
(3)用基类指针引用其派生类对象。
基类指针仅能访问派生类中的基类部分。自动隐式类型转换
(4)用派生类指针引用基类对象。
可强制类型转换,不会自动类型转换。
对于通过基类的对象指针(或引用)对成员函数的调用,编译时无法确定对象的类,而只有在运行时才能确定并由此确定该调用哪个类的成员函数。
同名覆盖很麻烦,设想能否用同一个调用形式,既能调用派生类,又能调用基类的同名函数?
解决方法:通过同一种形式,达到不同的目的。
在程序中,不是通过不同的对象名去调用不同派生层次中的同名函数,而是通过指针去调用它们!
形式一样,例如 pt->area() 只需在调用前给pt赋予不同的值即可。
虚函数的作用是:允许在派生类中重新定义与基类同名的成员函数,并且可以通过基类指针或引用来访问基类或派生类中的同名函数。
class 类名 {
virtual 类型 函数名(参数表);
};
说明:
(1)虚函数声明只能出现在类声明中的函数原型声明中,而不能在成员函数的函数体实现的时候。
(2)只有类的普通成员函数才能声明为虚函数,全局函数及静态成员函数不能声明为虚函数。
(3)虚函数可以在一个或多个派生类中被重新定义,它属于函数重载。
它要求在派生类中重新定义时必须与基类中的函数原型完全相同。
这时,无论在派生类的相应成员函数前是否加上关键字virtual,都将其视为虚函数;
(派生类中的同名函数都自动成为虚函数)
系统会遵循以下规则来判断一个派生类的成员函数是不是虚函数:
① 该函数是否与基类的虚函数有相同的名称
② 该函数是否与基类的虚函数有相同的参数个数及相同的对应参数类型
③ 该函数是否与基类的虚函数有相同的返回值或者满足赋值兼容规则的指针、引用型的返回值。
(4)当一个类的成员函数声明为虚函数后,就可以在该类的派生类中定义与其基类虚函数原型相同的函数。
这时,当用基类指针指向这些派生类对象时,系统会自动用派生类中的同名函数来代替基类中的虚函数。
也就是说,当用基类指针指向不同派生类对象时,系统会在程序运行中根据所指向对象的不同,自动选择适当的成员函数,从而实现了运行时的多态性。
总结:用指向基类的指针+虚函数来实现了多态性。
运行过程中的多态需要满足三个条件:
- 类之间应满足赋值兼容规则;
- 要声明虚函数;
- 要由成员函数来调用或者通过指针、引用来访问虚函数。
个人总结,多态的实现需要满足条件:
- 要有继承关系;
- 基类中声明虚函数;
- 基类指针或引用指向派生类对象。
注意:如果使用对象名来访问虚函数,是静态联编
函数重载解决的是同一层次上的同名函数问题(横向重载)
虚函数解决的是不同派生层次上的同名函数问题(纵向重载)
- 根据什么考虑是否把一个成员函数声明为虚函数呢?
首先看成员函数所在的类是否会作为基类。
然后看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能的,一般应该将它声明为虚函数。
如果成员函数在类被继承后功能不需修改,或派生类用不到该函数,则不要把它声明为虚函数。
不要仅仅考虑到要作为基类而把类中的所有成员函数都声明为虚函数。
应考虑对成员函数的调用是通过对象名还是通过基类指针或引用去访问,如果是通过基类指针或引用去访问的,则应当声明为虚函数。
有时,在定义虚函数时,并不定义其函数体,即函数体是空的。
它的作用只是定义了一个虚函数名,具体功能留给派生类去添加。
- 对多态性的一点补充说明
使用虚函数,系统要有一定的空间开销。
当一个类带有虚函数时,编译系统会为该类构造一个虚函数表(virtual function table,简称vtable),它是一个指针数组,存放每个虚函数的入口地址。系统在进行动态关联时的时间开销是很少的,因此,多态性是高效的。
开发中,通常会保留基类,减少新类的开发时间;
但是基类的某些函数不完全适应派生类的需要。
如果让基类和派生类的函数不同名,则派生层次太多就会很麻烦;
如果同名,又会发生覆盖。所以用虚函数很方便。
- 虚析构函数
当派生类的对象从内存中撤销时一般先调用派生类的析构函数,然后再调用基类的析构函数。
但是,如果用new运算符建立了临时对象,若基类中有析构函数,并且定义了一个指向该基类的指针变量。
在程序用带指针参数的delete运算符撤销对象时,会发生一个情况:
系统会只执行基类的析构函数,而不执行派生类的析构函数。
class Point { //定义基类Point类
public:
Point() {}
~Point() {
cout<<"executing Point destructor"<<endl;
}
};
class Circle:public Point { //定义派生类Circle类
public:
Circle() {}
~Circle() {
cout<<"executing Circle destructor"<<endl;
}
};
int main( ) {
Point *p=new Circle; //用new开辟动态存储空间
delete p; //用delete释放动态存储空间
return 0;
}
运行结果为
executing Point destructor
表示只执行了基类Point的析构函数,而没有执行派生类Circle的析构函数
将基类的析构函数声明为虚析构函数,则运行结果为:
executing Circle destructor
executing Point destructor
当基类的析构函数为虚函数时,无论指针指的是同一类族中的哪一个类对象,系统会采用动态关联,调用相应的析构函数,对该对象进行清理工作。
如果将基类的析构函数声明为虚函数时,由该基类所派生的所有派生类的析构函数也都自动成为虚函数,即使派生类的析构函数与基类的析构函数名字不相同。
最好把基类的析构函数声明为虚函数。这将使所有派生类的析构函数自动成为虚函数。
这样,如果程序中显式地用了delete运算符准备删除一个对象,而delete运算符的操作对象用了指向派生类对象的基类指针,则系统会调用相应类的析构函数。
虚析构函数的概念和用法很简单,但它在面向对象程序设计中却是很重要的技巧。
专业人员一般都习惯声明虚析构函数,即使基类并不需要析构函数,也显式地定义一个函数体为空的虚析构函数,以保证在撤销动态分配空间时能得到正确的处理。
构造函数不能声明为虚函数。这是因为在执行构造函数时类对象还未完成建立过程,当然谈不上函数与类对象的绑定。
- 纯虚函数与抽象类
1.纯虚函数
在定义一个表达抽象概念的基类时,有时可能会无法给出某些成员函数的具体实现。
这时,就可以将这些函数声明为纯虚函数。
virtual 函数原型 = 0;
纯虚函数是只在基类中声明虚函数但未给出具体的函数定义体;
它的具体定义放在各派生类中。
#include<iostream>
using namespace std;
class A{
public:
virtual void show()=0; //纯虚函数,起到接口声明的作用
};
class B : public A{
public:
void show(){ //对虚函数进行实现
cout<<"B::show()"<<endl;
}
};
int main(){
B b;
b.show();
return 0;
}
2.抽象类
声明了纯虚函数的类,称为抽象类。
抽象类的主要作用:通过它为一个类族建立一个公共的接口,使它们能够更有效地发挥多态特性。
抽象类声明了一族派生类的共同接口,而接口的完整实现,即纯虚函数的函数体,要由派生类自己定义。
上述程序中,A类就是一个抽象类;
如果B类未对A类进行虚函数实现,则B类同于也是一个抽象类,不能使用B类进行对象的创建。
3.使用纯虚函数与抽象类的注意事项
(1)抽象类只能用作基类来派生新类,不能声明抽象类的对象,但可以声明指向抽象类的指针变量和引用变量。
(2)抽象类中可以有多个纯虚函数。
(3)抽象类中也可以定义其他非纯虚函数。
(4)抽象类派生出新类之后,如果在派生类中没有重新定义基类中的纯虚函数,则必须再将该虚函数声明为纯虚函数,这时,这个派生类仍然是一个抽象类;
(5)在一个复杂的类继承结构中,越上层的类抽象程度就越高,有时甚至无法给出某些成员函数的具体实现。
(6)引入抽象类的目的主要是为了能将相关类组织在一个类继承结构中,并通过抽象类来为这些相关类提供统一的操作接口。
第8章 异常处理
异常是指程序在运行过程中因环境条件出现意外或用户操作不当而导致程序不能正常运行。
其中有些异常情况是可以预料但不可避免的,例如在申请堆内存空间时,因内存空间不足使分配空间操作失败;
访问磁盘文件时因文件不存在使打开文件操作失败;执行打印操作时因打印机未打开使打印出错;
执行除法运算时因除数为0而出错等。对于这些可能发生的意外情况,在编写程序时应充分考虑并给予适当的处理,尽可能做到排除错误,使程序继续运行或者给出适当的提示信息后中断程序的运行。
异常Exception是一种不常见或者不可预见的情况,经常导致中断正常的程序流。
常见的可能产生异常的操作:数值越界,文件操作,内存分配,Windows资源,实时生成的对象与窗体,硬件和操作系统产生的冲突等。
异常处理的方法
异常处理的方法有多种,可以在可能发生错误的程序段中直接加上错误处理的代码,这样做的好处是阅读程序时能够直接看到对错误进行处理的情况,不足之处是使程序变得繁琐、可读性差。C++的异常处理机制允许发生异常的代码与处理异常的代码不在同一函数中。
C++异常处理机制中实现异常处理的方法是:
- 监测异常:将可能发生异常的代码包含在try语句块中。
- 抛掷异常:在try代码块中或其调用函数的代码中可能发生异常的地方进行检测,若检测到异常,则用throw语句抛出异常。
- 捕获异常:在try代码块后给出catch代码块,在其中给出处理异常的语句,用于捕获和处理异常。
如果一个异常没有被调用链中的任何函数捕捉到,那么在主函数捕捉该异常失败之后,按照默认,该程序就会自动调用abort()函数来终止
throw语句的一般形式如下:
throw <表达式>;
try catcht 语句的一般形式如下:
try{
//try语句块
} catch(类型1 [变量1]){
//针对类型1的异常处理语句块
}catch(类型2 [变量2]) {
//针对类型2的异常处理语句块
}...catch(类型N [变量N]){
//针对类型N的异常处理语句块
}catch(...){
//处理任何类型异常,放在最后
}
#include <iostream>
using namespace std;
int Div(int x,int y) {
if(y==0) throw y; //如果除数为0,throw语句抛掷整型异常
return x/y;
}
int main() {
try { //由于除法运算可能出现除0异常,因此放在try语句块中监测
cout<<"7/3="<<Div(7,3)<<endl;
cout<<"7/0="<<Div(7,0)<<endl;
cout<<"7/1="<<Div(7,1)<<endl; //该语句不被执行
} catch(int i) { //catch语句块捕获整型异常并进行处理
cout<<"exception of dividing zero\n";
}
cout<<"That is ok.\n";
return 0;
}
(1)主函数中调用了计算两个整数相除结果的函数Div,当除数为0时Div函数的运行将发生除0异常,所以在主函数中将调用Div函数的三条语句包含在try语句块中进行监测。
(2)当执行到try块的第二条语句时,调用了函数Div(7,0),由于除数为0,在Div函数中抛掷了一个异常throw y,其中y为整数,该异常为整型异常。这时将退出div函数,从这一点来看throw语句的作用类似于return语句。
(3)与return语句不同的是退出Div函数后,不是返回调用该函数语句的下一条语句:cout<<"7/1="<<Div(7,1)<<endl;
继续执行,而是在try语句块后寻找能捕获整型异常的catch语句块。
try语句块后只有一个catch语句块且该语句块带一个整型参数i,该catch语句块可以捕获整型异常。
(4)catch语句块的形参i被初始化为throw语句中y的值,因i在catch块中未用到,故可将i省略,但参数类型不能省略,然后进行退出Div函数的退栈处理并释放相关的资源。
(5)执行完catch语句块中处理异常的语句后,则执行catch块后的第一条语句:
cout<<"That is ok.\n";
#include<iostream>
#include<cmath>
#include<cstdio>
using namespace std;
int Div(int x,int y){
if(y==0) throw y;
return x/y;
}
double Sqrt(double y){
if(y<0) throw y;
return sqrt(y);
}
int main(){
double a,b; cin>>a>>b;
try{
cout<<"div("<<a<<"/"<<b<<")="<<Div(a,b)<<endl;
cout<<"sqrt("<<b<<")="<<Sqrt(b)<<endl;
}catch(int){
cout<<"exception of dividing zero\n";
} catch(...){ //处理任何类型异常
cout<<"caught exception\n";
}
cout<<"That is ok.\n";
return 0;
}
异常处理的规则
编写异常处理程序时应注意遵守以下规则:
(1)应将可能发生异常的程序代码段包括对某个函数的调用包含在try块中,以便对其进行监控,C++异常处理机制只对受监控代码段发生的异常进行处理。
(2)在受监控代码段中所有可能发生异常之处加上检测语句,一旦检测到异常情况,立即用throw语句抛掷这个异常。throw语句抛掷的异常的类型由throw后所跟的表达式值的类型所决定,如代码段中有多处要抛掷异常,应该用不同类型的操作数加以区别,类型相同而仅仅值不相同的操作数不能区别不同的异常。
(3)throw语句抛掷的异常由try语句块后的catch语句块捕获并处理,catch语句块可以有多个,通常每个catch语句块捕获一种类型的异常,捕获异常的类型由catch语句块参数类型所决定,catch块可以捕获其参数类型及其派生类型的异常。当catch语句块中未用到参数时,可只给出参数类型声明而省略参数名。
(4)当catch语句块的参数类型声明是一个省略号(...)时,该catch语句块可以捕获和处理任何类型的异常,该catch语句块应放在所有其它catch块的后面。
(5)如果抛掷异常后未找到能够处理该类异常的catch块,则自动调用运行函数terminate,该函数调用abort函数终止程序的运行。
#include<iostream>
using namespace std;
void f(int x) {
try {
if(x>0) throw 2;
if(x==0) throw 'a';
if(x<0) throw 3.14;
} catch(int n) {
cout<<"输入的为正数:"<<x<<endl;
} catch(char m) {
cout<<"输入的为零:"<<x<<endl;
} catch(double k) {
cout<<"输入的为负数:"<<x<<endl;
} catch(...) {
cout<<"捕获所有类型的异常!"<<"输入的数字是:"<<x<<endl;
}
}
int main() {
f(4); f(0); f(-5);
return 0;
}
#include<iostream>
using namespace std;
void f() {
try {
throw 'a'; //重抛异常
} catch(char x) {
cout<<"内层异常处理!"<<endl;
throw 'b';
}
}
int main() {
try {
f();
} catch(char x) {
cout<<"外层异常处理!"<<endl;
}
return 0;
}
#include<iostream>
using namespace std;
//演示C++中的异常处理对象的构造和析构
class Demo {
public:
int x;
Demo(int y) {
x = y;
cout<<"进入Demo类的构造函数"<<endl;
if(x<0) throw x;
else cout<<"调用Demo类的构造函数,构造对象"<<endl;
}
~Demo() {
cout<<"调用Demo类的析构函数,析构对象"<<endl;
}
};
void func() {
cout<<"进入函数func"<<endl;
Demo d1(4), d2(-8);
throw 'A';
}
int main() {
cout<<"主函数开始执行"<<endl;
try {
cout<<"调用func函数"<<endl;
func();
} catch(int n) {
cout<<"对象"<<n<<"发生错误"<<endl;
} catch(char m) {
cout<<"在函数中抛出异常"<<endl;
}
cout<<"主函数执行完毕"<<endl;
return 0;
}
例:异常处理中类对象的析构
#include <iostream>
using namespace std;
void func();
class Expt {
public:
Expt() {}
~Expt() {}
char *showReason()const {
return "Expt类异常。";
}
};
class Demo {
public:
Demo() {
cout<<"构造Demo。"<<endl;
}
~Demo() {
cout<<"析构Demo。"<<endl;
}
};
void func() {
Demo d;
cout<<"在func函数中抛掷Expt类异常。"<<endl;
throw Expt(); //创建Expt类无名对象以抛掷Expt类异常
}
int main() {
cout<<"在主函数中。"<<endl;
try {
cout<<"在try块中调用func函数。"<<endl;
func();
} catch(Expt e) { //捕获Expt类异常
cout<<"在catch异常处理程序中。"<<endl;
cout<<"捕获到Expt类型异常:"<<e.showReason()<<endl;
} catch(char *str) {
cout<<"捕获到其它类型的异常:"<<str<<endl;
}
cout<<"回到main函数,从这里恢复执行。"<<endl;
return 0;
}
程序运行结果:
在主函数中。
在try块中调用func函数。
构造Demo。
在func函数中抛掷Expt类异常。
析构Demo。
在catch异常处理程序中。
捕获到Expt类型异常:Expt类异常
回到main函数,从这里恢复执行。
例:构造函数中发生异常的处理
#include<iostream>
using namespace std;
class Birthday {
public:
void init(int y,int m,int d) {
if(y<1||m<1||m>12||d<1||d>31) throw y;
}
Birthday(int y,int m, int d):year(y),month(m),day(d) {
init(y,m,d);
cout<<"Birthday\n";
}
void display() {
cout<<"Birthday:"<<year<<"/"<<month<<"/"<<day<<endl;
}
~Birthday() {
cout<<"~Birthday\n"; //构造不成功,析构函数不执行
}
private:
int year, month, day;
};
int main() {
try {
Birthday birth(1996,13,0);
birth.display();
} catch(int) {
cout<<"Object Creation Failed!"<<endl;
}
return 0;
}
程序运行结果:
Object Creation Failed!
例:有子对象的派生类构造函数中发生异常的处理
#include <iostream>
using namespace std;
class Birthday {
public:
Birthday(int y=2000,int m=01,int day=01) {
cout<<"Construct Birthday."<<endl;
}
~Birthday() {
cout<<"Destruct Birthday."<<endl;
}
private:
int year, month, day;
};
class Father {
public:
Father(int i):age(i) {
cout<<"Construct Father."<<endl;
}
~Father() {
cout<<"Destruct Father."<<endl;
}
protected:
int age;
};
class Son: public Father {
public:
Son(int age):Father(age) { //包含父类对象和子对象的构造函数
cout<<"Construct Son."<<endl;
throw age; //执行子对象类和父类的析构函数,不执行派生类的析构函数
}
~Son() {
cout<<"Destruct Son."<<endl;
}
private:
Birthday bir; //子对象
};
int main() {
try {
Son(19);
} catch(int) {
cout<<"Exception."<<endl;
}
return 0;
}
程序运行结果:
Construct Father.
Construct Birthday.
Construct Son.
Destruct Birthday.
Destruct Father.
Exception.
标签:函数,int,成员,C++,面向对象,对象,基类,程序设计,构造函数
From: https://www.cnblogs.com/hellohebin/p/16388527.html