上海大学面向对象A课程(即C++语言)学习的一点总结,可供参考。
类与类的对象
类的声明
类的声明指的是数据成员组织形式(?)的描述、成员函数的原型声明和成员函数的定义
成员的访问属性有3种:public
(公开的) protected
(受保护的) private
(私有的)(称属性为protocted
或private
的成员为内部的)
结构体与类的区别:struct
的访问属性默认为public
,而class
成员的访问默认属性为private
对象的基本空间
对象的空间 = 对象的基本空间 + 对象的资源空间
创建对象就意味着给对象分配了内存空间
对象的基本空间 = 对象的非静态数据成员所占用的空间总和 (占sizeof(类型名或对象名)
字节)
因此对象的成员函数其实是不占用对象的空间的,成员函数的代码都存放在内存的代码区,这样就可以节约宝贵的内存空间。
对象的自我表现
其实就是对象调用类的成员函数。
也称对象调用成员函数为发送消息给对象,令对象自己表现自己。
成员函数
成员函数是类的成员之一。
类的成员函数对同类的所有对象的所有数据成员都有无限制的访问能力。
在类体中定义的成员函数包含默认的inline建议,而在类体外定义时,必须使用函数全名名字空间名::类名::成员函数名
来定义
常量成员函数
有一些成员函数对于对象的内容只进行“读”的访问,而不需要修改。**为了防止误改,可以将该函数设置为常量成员函数。**设置
此时这种函数的隐含形式参数为 const * const this
(指向本对象的常量指针常量)
在类体内声明的格式为(后面加个const):
int function() const;
在类体外声明的格式为:
int Class::function() const;
(要说清楚在哪个类)
常量对象只能调用常量成员函数
构造函数
构造函数的大概介绍和规则
eg. 为Date类(日期类)定义一个构造函数↓
class Date{
int d,m,y;
public:
//....
Date(int,int,int); //日、月、年
Date(int,int); //日、月、今年
Date(); //默认日期,今天
};
//上面多写几个Date是为适应不同参数情况,还可增加(重载规则)
——————————————————————————————————————————
1.构造函数的函数名和类名一样
2.该函数符合重载规则(可以根据不同参数创建不同初始值的对象)
3.如果一个类有一个构造函数,那么这个类中所有对象的初始化,都将由该构造函数来完成
4**.构造函数没有返回类型(但函数体内可以用return语句)**
5.在创建对象时,系统将自动调动构造函数(完成处理对象空间、构造结构和初始化数据成员等工作)
6.构造函数必须是公有的成员函数
7**.任何类都至少有两个构造函数**,其中一个一定是拷贝构造函数
8.当进入到构造函数的函数体时,其实编译系统已经完成了 对象数据空间处理、构造结构、初始化数据成员 这些事儿,只不过是进去以后再修改数据成员的值(完成这些事的时机:构造函数的首部和左边的花括号之间)
好处:
以防程序员忘记对函数作初始化 或 对函数多做了几次初始化。因此定义一个函数来帮程序员干“初始化”这件事儿。
缺省的构造函数(默认构造函数)
它是一个不需要实参的构造函数
className::className() {}
只有当程序员没有编译类的构造函数时,编译器自动产生的构造函数。系统自动产生的构造函数是不会对数据成员进行初始化的,因此此时对象值是不确定的。如果程序员已经定义了构造函数,默认构造函数那么就不会生成了。
此时,所创建对象的数据成员初值,全取决于对象的存储类型(全局对象、静态全局对象和静态局部对象都是0,其他的如局部自动对象,动态对象等等都是不可预知的)
但是!如果仍旧需要默认函数,可以进行重载!
做法:**设计所有参数均带默认值的构造函数,**当不提供实参的时候,这个构造函数就是默认构造函数。(帮助给每个参数赋初值)
调用情况:
Time t
创建了对象 t Complex z
创建了对象 z
转换构造函数
带有一个实参的构造函数被称为转换构造函数(因为它可以实现数据类型的自动转换或强制转换)
个人理解:给一点值,剩余没给的值都由转换构造函数自己补。
用 转换构造函数初始化新创建的类对象 的语法
类名 对象名(实参,实参,.....)
Student s2 (“00001357")
类名 对象名 = 转换构造函数的实参
(其中"="是初始化记号) Student s2 = "00001357"
拷贝构造函数
拷贝构造函数
用已经存在的对象构造新的对象的函数称为拷贝构造函数
默认的拷贝构造函数按对象的数据成员依次调用成员的拷贝构造函数进行拷贝构造
几个要点:
1**.使用拷贝构造函数的时候就不会再用别的构造函数了**
2.拷贝构造函数不能被重载,没有返回类型
3.一个类如果没定义拷贝构造函数,系统会自动提供一个(只执行浅拷贝操作)
4.拷贝构造函数是可以访问另一个对象的所有成员的(因为类的成员函数对该类所有对象的成员都有无限制的访问权限)
被调用的原则:
用一个已经存在的对象去初始化一个正在创建的对象时
1.创建新对象时,用一个已经存在的对象作为初始化值
2.调用函数时,用实参初始化值传递创建的形参
3.函数类类型对象值返回时,用函数所返回的表达式 初始化 值返回时创建的临时对象
(其中2、3都创建了对象的副本)
在类内声明【带一个 & 】:
类名(const 类名&);
在类外声明
类名::类名(const 类名 &形式参数);
浅拷贝构造
在拷贝对象的时候仅根据对象基本空间的数据成员进行拷贝。
可能导致不同的对象共享同一资源(可能会出错!)
深拷贝构造
深拷贝构造在创建对象时,会申请属于自己的资源空间,然后仿造已经存在的那个对象,拷贝一份数据到自己的资源空间中。
(也就是相比起浅拷贝构造,多加一个new,即申请属于自己的资源空间)
构造函数的使用
1. 创建对象数组
需要提供一个常量作为数组的元素个数,依次创建数组的每个元素时都要调用一次构造函数或拷贝构造函数
类名 对象数组名[元素个数] = {转换构造函数的实参表}
类名 对象数组名[元素个数] = {构造函数名(实参表)}
2.创建动态对象或动态对象数组
用 new 和 delete 搞定 (构造函数自己申请堆内存空间,该空间的首地址仅由该对象掌握,保证良好的封装性)
依次创建数组的每个元素时都要调用一次构造函数或拷贝构造函数
new可以一次性搞定分配空间、构造结构和初始化对 象数据成员
new返回的是从堆空间中所切下的那一片连续空间的地址
new 类名;
new 类名 (实参, 实参,....);
new 类名 [元素个数] {实参, 实参, .......}
static静态成员
如果只是上面的构造函数,还有一个问题,就是依靠着全局变量 today ,并且只能在有 today 的环境中使用,这样就造成了限制,也很难对代码进行维护。而解决的方式是可以增加一个static静态成员(static成员函数)
static静态成员:属于这个类中的某一部分而不是属于全部部分。(如果非static静态成员,则这个变量在各个对象里都各有副本。)
重新设计:
class Date{
int d,m,y;
static Date default_date;//这个就是static静态成员
public:
Date(int dd=0,int mm=0,int yy=0);
//...
static void set_default(int int int);
};
引用:
Date::set_default(1 , 8 , 2002)
定义方式:
静态成员(函数和数据成员)都要在别的地方自己定义
Date Date::default_date(1,8,2002)
//将默认值定义为生日
void Date::set_default(int d,int m,int y)
{
Date::default_date = Date(d,m,y);
}
析构函数
析构函数
析构函数通常完成一些清理和释放资源的工作(对象使用后的销毁工作),最常见的用途是释放构造函数请求的存储空间。
1.析构函数大部分时候都是被隐式调用。
2.记法形式采用 ~函数名() 作为提示
3.可以写在类内也可以写在类外
4.析构函数没有返回类型,没有参数,不允许重载
6.若没有设计析构函数,系统会自动提供一个默认的析构函数,它仅完成对象基本数据空间的清理工作
7.析构对象数组时,将为每一个元素调用一次析构函数
例子
class Name{
const char*s;
};
class Table{
Name *p;
size_t sz;
public:
Table(size_t s=15){
p = new Name[sz=s];
}//构造函数
~Table() {
delete[] p;//把p删除了
}//析构函数
};
对象构造和析构的顺序
-
全局对象、静态局部对象在主函数执行之前被构造,主函数结束之后被析构
-
静态局部对象在其所在函数第一次被调用时被构造,直至主函数结束后析构 (该对象的基本空间在全局数据区,而函数返回后,它只是变为不可见的状态,但仍然保留当前的属性值,再次调用函数时就又会变成可见的状态,因此只是改变可见性而不会重复创建)
-
局部自动对象在其所在函数每一次调用时创建,函数返回后析构 (局部自动对象:包括值传递的形参对象)
-
函数值返回的临时对象在函数返回时创建,在参与其所在表达式运算后析构
-
动态对象(堆对象)在执行 new 操作时创建,执行 delete 时析构
在上诉原则下,先创建的对象后析构
this指针常量
出现它的原因是,成员函数身在代码区,而不同对象有各自不同的对象空间,但类的成员函数又可以被该类的所有对象所调用共享,那成员函数是怎么做到操纵不同对象的数据空间?(也就是它是怎么知道对象数据在哪的?)
C++在成员函数体中,在访问数据成员 或者 成员函数时,都在成员名前偷偷添加了this->
(意指本对象的)
它是一个隐式形式参数
保留字:this
对象:所有的非静态成员函数
数据类型:该类的指针常量(指向目标为变量)
指针常量
:指向不能被改变,且指针常量的指向目标必须为变量
常量指针
:指向不能被改变,且将常量指针的指向目标视为常量
this的指向被“锁定”,无法进行更改
意义:类的成员函数是不会随对象数据而“身处代码区”的,但因为this为其提供了具体对象的起始地址,所以成员函数仍能正确有效地访问具体对象的数据空间
类模板与模板类
函数模板/类模板
写源代码时,数据类型不确定,但对数据的操作内容已经确定的函数描述(即讲一下这个函数,但并不说具体的类型,由编译器自己判断)
类模板及其所有成员的声明、成员函数的描述(不管写在类模板的里面还是外面)都应该放在头文件中(让其他文件知道)。
描述方式:template <typename type> class name
//省略public中给a、b赋值的函数
template <typename type>class Vec //类模板的声明
{
public:
//...
private:
type a,b;
};
1.type即为待定的数据类型
2.如果有成员函数,那么成员函数的描述可以在类内也可以在类外
3.只有所有的待定数据类型都确定了以后,如果类模板中所涉及的形式数据类型数据的运算操作这个具体的实际数据类型都可以进行,编译器才会自动先生成一个实际的类——模板类,之后再对模板类进行编译**(编译器只编译模板类)**
模板类
是形式类中的形式参数全部都换成实际数据类型的类,是实际的类
用模板类创建对象的语法:模板名<实际数据类型名>对象名(初始化表)
Vec<int>a, b(10); //Vec<int>为模板类类名
Vec<char>c('A','B'); //Vec<char>为模板类类名
例题.
例子:日期类
编写一个日期类,具有设置日期,返回年、月、日,输出日期,计算本日期的下一天的日期等方法(成员函数)。要求采用多文件结构编写,并注意将一些函数设计成常量成员函数。最后编写主函数进行测试。
文件1. Data.h (头文件)
//Date.h
#ifndef DATE_H
#define DATE_H
class Date
{
public:
Date(int year=2021,int month=9,int day=22);//构造函数,设置当前日期
void Set(int year,int month,int day);//设置时间的函数
int GetYear() const;
int GetMonth() const;
int GetDay() const;
void Show() const;
void NextDay();
private:
int y,m,d;
};
#endif
文件2. date.cpp (成员函数的定义文件)
#include <iostream>
#include "Date.h"
using namespace std;
//构造函数
Date::Date(int year,int month,int day)
{
y = year;
m = month;
d = day;
}
void Date::Set(int year,int month,int day)
{
y = year;
m = month;
d = day;
}
int Date::GetYear() const//const代表返回值是const类型 不可以更改
{
return y;
}
int Date::GetMonth() const
{
return m;
}
int Date::GetDay() const
{
return d;
}
void Date::Show() const
{
cout<<y<<"."<<m<<","<<d<<endl;
}
void Date::NextDay()//下一天
{
int days[]={31,28,31,30,31,30,31,31,30,31,30,31};
if(y%4==0 && y%100==0 || y%400==0)//如果是闰年的话
days[1]=29;
d++;
if(d>days[m-1])
{
d=1;
m++;
}
if(m>12)
{
m=1;
y++;
}
}
文件3. 测试文件(输出当前日期和当前日期往后3天)
//这里是日期类的测试函数
#include <iostream>
#include "Date.h"
using namespace std;
int main()
{
Date d;
d.Show();
d.Set(2021,9,22);
cout << d.GetYear() <<"."<< d.GetMonth() <<"."<<d.GetDay()<<endl;
for(int i=0;i<3;i++)
{
d.NextDay();
d.Show();
}
return 0;
}
输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2AXkF7Yl-1638005922398)(C:\Users\Kother\AppData\Roaming\Typora\typora-user-images\image-20210922190300365.png)]
赋值运算
赋值表达式为 对象名.operator=(右值)
或 对象名=右值
(可以把 operator= 看做函数名)
赋值运算符函数必须为非静态成员函数(因为类的非静态成员函数中第一个形式参数是隐含的this指针常量,系统会隐含地传递本对象的地址)
类名& 类名:operator = (参数)
{
//执行语句
return *this;
}
深赋值运算:
目的是就算赋值以后,对象所带的资源也是独立的,不和任何别的对象分享。
需要完成如下操作:
1.主动释放该对象原有的资源
2.重新构建一份新资源空间
3**.复制**右边对象资源的数据来填充新资源空间
4.引用返回本对象( *this )
组合成员
类的组合:该类的数据成员者是另一个类的对象(这个数据成员被称为组合成员)
**初始化:**定义变量的同时给该变量直接确定初值 int x=999
(显式初始化)
组合成员的构造:冒泡语法
冒泡语法只能用在构造函数和拷贝构造函数中(位置在函数首部与花括号之间)
构造函数(总参数表) : 组合成员名1(参数表),组合成员2(参数表),...
{
//函数体语句
}
//用冒泡语法前
Student::Student(char *Id, string Name, char Gender, int Age)
{
strncpy(id, Id, sizeof(id));
id[sizeof(id)-1]='\0';
gender = (Gender=='M' || Gender=='m')?'m':'f';
name = Name;
age = Age;
}
//用冒泡语法后
Student::Student(char *Id,string Name,char Gender,int Age)
:name(Name),gender(Gender),age(Age)
{
strncpy(id, Id, sizeof(id));
id[sizeof(id)-1]='\0';
gender = (Gender=='M' || Gender=='m')?'m':'f';
}
//让成员name,age等构造一步到位
- 先调用组合成员的构造函数,构造组合成员。若有多个组合成员,就依照它们在数据成员描述时声明的顺序来构造,和冒号后的书写顺序无关。
- 再进入本类的构造函数的函数体中,执行函数语句来调整和修改成员的值。
必须使用冒泡语法的场所:
1.初始化类的常量数据成员(因为常量要在定义时就初始化)
2.初始化类的引用成员声明
3.组合成员所属的类没有默认构造函数时,构造组合成员必须带参数,这个参数只可以通过冒泡语法传递
Class ColonRule
{
public:
ColonRule(int size=1):n(size),len(n),num(size) //这3个量必须用冒泡语法
private:
const int n; //常量数据成员
const int &len; //引用成员
A nun; //组合成员,且类A是没有构造函数的
}
s
组合成员的析构
析构含组合成员的对象时,顺序和构造时是相反的
- 先执行本对象的析构函数体
- 然后执行组合成员的析构函数。若有多个组合成员,则按其声明时的相反顺序依次析构
静态成员与友元
当类的某些属性是需要全体对象共用的时候,需要一份与对象分离,不占用对象的数据空间来储存这些属性,因此静态成员诞生了。
静态成员
static静态成员
属于这个类中的某一部分而不是属于全部部分(如果非static静态成员,则这个变量在各个对象里都各有副本)
-
编译系统为类的每个静态数据成员创建一份,存放在计算机内存的全局数据区(代码区),不占据对象的数据空间(也就是只和类有关)
-
静态数据成员的创建与具体对象的创建无关
-
静态数据成员不能没有定义,也不可以重复定义
-
静态数据成员在创建类的对象之前就已经存在了,所以要在主函数之前先行定义和初始化它
-
静态数据成员的生命期是全局的(在main函数执行之前创建,直到main函数返回后销毁)
设计类只是声明(描述)。问:何时、何处定义静态数据成员?其生命期起、止分别为何时?
答:
静态数据成员的创建应该与具体对象的创建无关,即使不创建任何对象,静态成员应该事先存在,全局生命期。
如:一个新班级可能人数为0,无法指定班长,但可以预先指派班导师(班主任)。
结论 (类比全局变量“一处定义,多处声明”)
在类体中声明静态数据成员(连同类的设计写入某头文件中,随包含指令多处声明);
将静态数据成员的定义及初始化写入某源程序文件*中(一处定义)。
定义方式
数据类型 类名::静态数据成员名 = 初始化值
数据类型 类名::静态数据成员名(初始化值)
(此处不可以再写 static 了)
访问方式
-
可以通过已经创建的具体对象访问类的静态数据成员 方式:
对象名.静态数据成员名
-
又因为系统不关心是哪个对象访问了静态数据成员,只关心对象所在的类,即只认类型不认对象,因此更一般的访问方法是用类名来访问(因为有可能该类什么对象都没有创建)
类名::静态数据成员名
(要用::
即作用域区分符)
静态成员函数
Q:为什么需要单独的静态成员函数?
A:是为了更好地访问静态成员对象
在没有任何对象存在(即尚未创建任何具体对象时),且类的静态数据成员是受保护的或私有的情况下,要访问该静态数据成员只能通过公开的被称为静态成员函数的函数来实现
静态成员函数与非静态成员函数的异同
———————相同点————————
均存放在计算机内存的代码区,被该类所有对象共享
———————不同点————————
静态成员函数不隐含与任何具体对象联系。亦即:
静态成员函数没有隐含传递所谓"本对象地址"的指针形式参数
this
。
若需要静态成员函数在类的层次高度处理某个具体对象,则需要将该对象(或对象的地址、或对象的引用)作为实参进行显式地传递。
(不怎么理解)
声明语句
static 返回类型 静态成员函数名(形式参数表)
访问方式
-
通过具体对象访问成员的方式调用静态成员函数
对象名.静态成员函数名(实际参数表)
-
当然也可以通过类名访问,这是更好的方式,因为静态成员函数也是只认类型,不认对象
类名::静态成员函数名(实际参数表)
友元
友元函数
为什么需要友元函数?
利用类成员的 protected 及 private 访问控制属性能很好地屏蔽对象的内部细节,起到保护作用。
通常将对象的属性(数据成员)加以保护,仅允许类的成员函数(即通过函数调用)访问它们。
若对象带有大量的数据,且访问这些数据时全都通过函数调用及返回操作,则将导致程序执行的效率不高。
我们希望在保证数据安全的情况下适当开放数据的访问权限给指定的“朋友”,使之不通过函数调用而能直接访问类中的受保护的或私有的成员(包括数据成员和成员函数)。也就是为类与类之间访问各自数据成员和成员函数提供便利
友元不是类的成员,只是类的朋友
关于定义
-
友元函数可以是一个普通的函数,也可以是另一个类的成员函数(对友元函数有没有类、在哪个类都没有限制)
-
在类声明中,在函数返回类型前使用保留字
friend
表示该函数不是本类的成员,而是本类的友元函数 -
友元函数可以在类体内声明并定义,也可以在类体内声明后,在类体外定义
-
在类体外定义时不能用保留字friend,也不存在类名及作用域区分符
::
,因为友元函数可以是一个普通的C++函数(若该友元函数是另一个类的成员函数,则按其类的格式定义之)
其他
- 由于类的友元函数不是该类的成员函数,自然不默认联系具体的对象(也是只看类)
- 友元函数访问类的具体对象及对象的成员时,应该将该对象(最好采用引用或常量引用传递以避免拷贝构造对象的副本)作为实参显式地进行传递。
友元类
可以声明一个类是另一个类的友元,称为友元类
友元类中的所有成员函数均成为本类的友元函数,均能访问本类的所有成员
友元类的友元函数不一定是本类的友元(朋友的朋友不一定是朋友)
例:
类A的成员函数可以访问(读或写)类B对象的所有成员
但是,类A的友元函数f不再是类B的友元函数,不能访问类B对象的受保护的或私有的成员
运算符重载
为什么需要运算符重载?
使我们所设计的类类型更加“向基本数据类型看齐”
类类型向基本数据类型看齐后,可以直接应用到已经设计好了的类模板中
运算符 / 运算符函数
函数定义
- 运算符是一种特殊的函数
- 它们的名称、定义和调用格式与普通函数有些差别
- 运算符函数可以作为某个类的成员函数,也可以作为普通的C++函数(常作为类的友元函数)。
函数名为operator=
运算符重载规定
- C++不允许用户自己定义新的运算符
- 不允许改变运算符操作数的个数(自然不允许使用带默认值的参数)
- 不能改变运算符的运算优先级
- 不能改变运算符的运算结合方向
- 下列5个运算符不允许被重载
::
作用域区分符
.
成员访问运算符
.*
成员指针访问运算符
sizeof
数据尺寸运算符
? :
三目条件运算符(唯一的三目运算) - 重载运算符时,至少要有一个操作数为用户自定义的类类型(因为对基本数据类型及其指针而言,其定义已经存在,而构成重载必须有不同于基本数据类型的参数)。
- 重载的运算符函数不能为类的静态成员函数。
- 系统不会将运算符“+”与运算符“=”自动组合成运算符“+=”。需要使用运算符“+=”时,应该单独重载它。
运算符重载原则
-
尽可能地使用引用型形式参数,并尽可能地加以const限制
- 其作用是尽可能地避免拷贝构造形参操作数
- 尽可能地保护实参操作数;
- 同时使操作符具有与常量运算的能力。
-
尽可能地采用引用返回
其作用是尽可能地避免拷贝构造临时对象。
-
若第一个操作数可能为非本类的对象时,应考虑将运算符重载成类的友元函数;
-
尽可能地保持运算符原有的含义、保持运算符的直观可视性;
-
充分利用类型转换函数、转换构造函数
重载双目运算符
(双目运算符:对两个变量进行操作)
双目运算符需要两个操作数
运算符函数作为类的成员函数时,只需一个显式形式参数
此时,编译器将运算符函数的实参对象兼容地处理成对象的地址,以初始化隐含的指针this。作为友元的双目运算符函数,其两个形式参数均应该为显式的
重载双目运算符时,要特别注意:当两个操作数联系同一个实参对象时的处理,要避免因修改左操作数(同时也是右操作数,即使右操作数有const限制)而“无意”中改变了右操作数导致计算错误。
重载算数运算符
例:日期类
Date operator+(const Date &d, int n) // 友元函数
{ // 不能改变操作数,只能采用值返回,创建临时对象
Date temp(d);
int m = temp.DaysOfYear() + n;
temp.SetDate(d.year, m);
return temp; // 返回一个局部对象,不得不采用值返回
}
Date operator-(const Date &d, int n) // 友元函数
{
return d + (-n); // 利用已经重载的运算符函数
}
Date operator+(int n, const Date &d) //必须为友元函数
{
return d + n;
}
int Date::operator-(const Date &d)const // 成员函数
{
return GetTotalDays()-
GetTotalDays(d.year, d.month, d.day);
}
由于第三个函数的第一个操作数为非Date对象,因此不能将该函数设计成成员函数,而将其作为友元函数。(其余的函数都可以设计成成员函数或者友元函数)
重载单目运算符
前增量、前减量运算符都是单目运算符,特点是:先修改操作数的值,然后以新值参与所在表达式的其他运算。并且前增量、前减量运算的结果都可以做左值。这就要求引用传递对象及引用返回对象本身,所返回的对象具有新的值。
后增量、后减量运算符也都是单目运算符,但为与上面的区分,C++规定在操作符函数原型和函数首部加上纯形式上的参数int
。这个参数仅仅起到了记号的作用,告诉编译器作为后增量、后减量处理。特点是:先以操作数原值参与所在表达式的其他运算一次,然后修改操作数的值。
Data Data::operator++(int)
{
Data temp(*this);
++(*this);
return temp;
}
Data Data::operator--(int)
{
Data temp(*this);
--(*this);
return temp;
}
C++中,基本数据类型的后增(减)量运算表达式不能做左值。用户自定义的类类型后增(减)量运算表达式虽可做左值,但被操作者却不是对象本身而是临时对象。
重载关系运算符
bool Date::operator> (const Date &d) const{ return *this-d > 0;}
bool Date::operator>=(const Date &d) const{ return *this-d >= 0;}
bool Date::operator< (const Date &d) const{ return *this-d < 0;}
bool Date::operator<=(const Date &d) const{ return *this-d <= 0;}
bool Date::operator==(const Date &d) const{ return *this-d == 0;}
bool Date::operator!=(const Date &d) const{ return *this-d != 0;}
重载迭代赋值运算符
Date & Date::operator+=(int n)
{
*this = *this + n; // 利用 operator+ 和 默认的赋值运算符函数
return *this; // 采用引用返回
}
Date & Date::operator-=(int n)
{
*this = *this + (-n);
return *this;
}
重载I/O流操作运算符
- 插入运算(<<)、抽取运算(>>)都是双目运算
- 可以连续操作(相当于嵌套调用“复合函数”)
- 第一个实参常常分别为 cout,cin
cout是类ostream的对象;cin是istream的对象,它们均在std名字空间中
例如:
cout << "Input n x :";
相当于operator<<(cout, ”Input n x:”);
cin >> n >> x;
相当于opeartor>>(operator>>(cin, n), x);
双目运算(<<) 输出
第一操作数 ostream 的对象 (引用传递)
第二操作数 类对象 (常量引用传递)
返回类型 ostream 的对象 (引用返回)
例:重载cout
以同时输出日期类中的年月日 (<<)
ostream & operator<<(ostream &out, const Date &d)
{
out << setfill(’0’) << setw(4) << d.year
<< ’/’ << setw(2) << d.month
<< ’/’ << setw(2) << d.day
<< setfill(’ ’);
return out; // 使连续输出成为可能
}
eg.重载输出链表
template <typename T>
void LinkList<T>::ShowList(ostream &out) const
{
out << "head ";
for(Node<T> *p=head; p!=NULL; p=p->next)
out << " -> " << p->data;
out << " -> NULL";
}
template <typename T>
ostream & operator<<(ostream &out, const LinkList<T> &link)
{
link.ShowList(out);
return out;
}
类型转换运算符
- 类型转换运算符函数必须为非静态成员函数
- 类型转换运算符函数无返回类型,因为函数名
operator 数据类型
中已经指定了转换结果的数据类型 - 若只重载了一个类型转换运算符函数,系统将在适当的适合进行自动转换(称为默认的类型转换)。若定义了多个类型转换运算符函数,则需要使用强制类型转换运算符进行强制转换
- 若声明一个类时,若未定义类型转换运算符函数,系统不会提供任何类型转换运算符函数
继承与多态性
继承与派生概述
-
继承方式:
公开继承
public
保护继承
protected
私有继承
private
-
基类是已经存在的类,任何已存在的类都可以做基类,而通过它生成的派生类是一个新类
-
不论哪种继承方式,除了基类的构造函数、拷贝构造函数和析构函数,派生类都全部继承了基类的一切成员
-
从基类继承而来的成员函数可以被覆盖也可以被重载(此时需要重新声明基类成员函数)
-
派生类可以增加新的数据成员和成员函数
派生类
继承与派生语法格式
class 派生类名 : 继承方式 基类类名{
// 新增加的属性和行为,或者基类成员函数的覆盖、重载
}
派生类构造函数的格式
派生类名::派生类构造函数名(总参数表)
: 基类构造函数名(部分参数表),组合成员名(部分参数表),其他成员初始化
{
//这用的仍然是冒号语法 ??
//派生类新增数据成员处理语句
}
构造派生类对象的顺序
首先调用基类的构造函数/拷贝构造函数来构造基类继承的数据成员部分(用显示或隐式的冒号语法)
然后调用组合成员的相应构造函数/拷贝构造函数来构造组合成员(用显示或隐式的冒号语法)
最后再构造并初始化其他数据成员
(基类->组合成员->其他)
多态性
先期联编 / 静态多态性 / 编译时的多态性:是在程序编译阶段就完全确定所要调用的函数(比如函数重载,运算符重载和模板技术)
迟后联编 / 动态多态性 / 运行时的多态性:是在程序运行过程中根据实际对象动态地确定的
C++编译器默认在先期联编状态下编译程序,只有当编译器遇到了虚函数、纯虚函数后才将该函数作为迟后联编处理。
虚函数
需要一个函数来实现运行时的多态性,因此虚函数诞生了
对于含有虚函数、纯虚函数的类,**编译系统为该类建一个虚函数表,它是一个指针数组,存放着每个虚函数的入口地址,供该类的所有对象共享。**同时也在类中悄悄添加一个特殊的指针成员指向该虚函数表。**每当对象调用成员函数时,将首先访问该特殊指针,找到虚函数表,最后找到要执行的函数。**实现运行的多态性。
保留字virtual
-
虚函数是成员函数,使用虚函数会略微增加一些空间开销和很少的时间开销
-
用虚函数可以给程序带来方便,实现 “一个接口,多种方法”
-
虚函数在类体外定义时不能使用保留字
virtual
-
友元函数不可以是虚函数,但在友元函数中可以调用虚函数
-
构造函数和拷贝构造函数不能是虚函数,因为虚函数是认具体对象的,但执行构造函数时所处理的对象尚未构造完成,对象未成形(即认不到)
-
析构函数可以是虚函数,而且它最好是。以便当对象生命期结束时可以调用自己的析构函数完成那些善后工作。
-
静态成员函数不能是虚函数
虚函数与派生类
-
派生类会继承基类的虚函数
-
在派生类中覆盖基类的虚函数时已经隐含它是虚函数,可以省略virtual,但为了程序的可读性,不建议省略
-
用基类的引用或基类指针访问派生类对象时可以实现迟后联编
-
如果用派生类的对象初始化基类对象后,在访问基类对象则与派生类对象无关,仅仅是基类对象的自我表现
-
如果在基类中定义一个成员函数为虚函数,在其派生类中定义的虚函数必须与基类中的虚函数同名,参数的类型、顺序、个数也要一一对应(如果不一样就属于重载了)
重载运算符享受多态性
若被重载的运算符是成员函数,则可以将该运算符函数声明成虚函数来实现动态多态性
但有些运算符的第一个操作数不是本类的对象,因此常将这张运算符声明成友元函数,**但友元函数又不能是虚函数。**要想让该运算符函数实现动态多态性,常用的方法是编写一个与运算符功能相同的成员函数,并将该成员函数设置为虚函数,然后在重载运算符中调用该函数就可以了
虚析构函数
若某个类可能成为基类,最好将它的虚构函数声明为虚函数。
为什么?
当使用基类的指针操作派生类对象时,对非虚函数仅操作了基类部分,无法处理派生类所产生的新的数据空间(派生类的虚构函数不会被调用,因此派生类对象所带的资源内存未被释放,会造成内存泄漏),但虚函数是根据对象的类型操作对象的数据空间,可以正常释放。
纯虚函数与抽象类
**纯虚函数是一种特殊的虚函数。**在虚函数声明的函数首部后(分号前)写下记号“=0”后该虚函数就是一个纯虚函数。
virtual void function() = 0;
- 纯虚函数没有函数体(它也不需要)
- 作用是**“占个位置”**,便于实现多态性
- 至少有一个纯虚函数的类被称为抽象类,抽象类只有一个作用:被继承
- 派生类中可以覆盖定义基类的纯虚函数
- 抽象类的派生类有可能仍是抽象类
- 当一个类中没有虚函数,这个类便成为具体类
- 在以抽象类作为基类的派生类中必须有纯虚函数的实现部分(即必须有重载纯虚函数的函数体),否则,这样的派生类也是不能产生对象的。
抽象类
-
不能创建抽象类的对象
-
可以定义抽象类的指针变量 指向 由其派生的具体类的对象
-
可以声明抽象类的引用,但声明引用时必须用 其派生的具体类的对象 进行初始化
多重继承
class 派生类名 : 继承方式 基类类名1, 继承方式 基类类名2 …
{
//新增加的属性和行为,或基类成员函数的覆盖、重载。
}
虚拟继承
假定基类为Base,有C1类和C2类是它的派生类
**采用虚拟继承派生类C1,C2,则它们共同的基类被称为虚基类。**虚拟继承仍然用virtual
,用虚拟继承的好处是虚拟继承能有效地除去数据成员的冗余、消除成员函数的重复,格式如下↓
class 派生类名 : virtual 继承方式 基类类名{
//新增加的属性和行为,或基类成员函数的覆盖、重载。
}
构造派生类对象时的构造顺序
构造函数调用顺序:
虚函数的构造函数:按它们被继承的顺序构造
非虚基类的构造函数:按它们被继承的顺序构造
组合成员对象的构造函数:按声明的顺序构造
I/O 流
标准输入设备:键盘
标准输出设备:显示器
对象名 | 所属类 | 设备名 | 说明 |
---|---|---|---|
cin | istream | 键盘 | 标准输入。可重新定向,有缓冲 |
cout | ostream | 显示器 | 标准输出,可重新定向,有缓冲 |
cerr | ostream | 显示器 | 常用于错误信息的标准输出。不可重新定向,无缓冲 |
clog | ostream | 显示器 | 常用于错误信息的标准输出。不可重新定向,有缓冲 |
抽取数值不当的处理:
template <typename T> T & input(T &x){
while((cin >> x) == NULL){
cin.clear();
cin.sync();
}
}
异常处理
异常≠错误
常见的异常状况
1.用0作除数
2.打开输入文件错
3.内存空间分配错
4.输入数据时类型不匹配
异常处理机制由2个部分组成,包括:
1.预料函数中的哪个部分会有异常,并利用throw
抛掷异常
2.尝试调用try
,再进行catch
捕捉处理
【圈定及捕捉处理】try-catch的结构↓
//语法格式
try{
被圈定的语句序列;
}
catch(数据类型1 形式参数)
{
具体的异常处理语句;
}
catch(数据类型2 形式参数)
{
具体的异常处理语句;
}
catch(...)
{
处理其他任何类型的异常;
}
1.只能有一个try
子快,可以有多个catch
-
抛掷何种类型,什么值由程序员自行确定
-
一个函数可抛掷任何数据类型的异常(包括用户自定义的类类型,任何指针类型)
-
函数在执行
throw
操作后将终止所在函数的执行,自动析构所在函数内已经创建,但尚未析构的局部自动对象,程序控制转向本函数的调用者(其上一层函数)来进行捕捉处理。 -
可以声明不抛掷出异常的函数,但若执行了throw,程序也非正常终止
int function( int a, double b ) throw()
总结:一抛了之,上级处理
函数调用是通过栈结构实现的。函数的嵌套调用是一个压栈过程,函数的返回是逐次退栈过程。
异常的传播方向与函数退栈过程相同──使最近的能处理异常的层次处理(不一定蔓延到最高层,直至崩溃)。
异常不一定是错误。C++异常处理机制也给程序员提供了一种新的流程控制方法。
设计函数(包括类的成员函数)时,在预料出现异常处只管抛掷可能的异常,而不必考虑本函数将可能在何种情形下、何种深层次中被调用。
调用有抛掷异常的函数时,应该将其调用语句进行圈定,并根据异常类型分情况捕捉及进行处理。
看个例子↓
Div(int a, int b) throw(char) //函数原型
int Div(int a, int b) throw(char) //函数定义
{
if(b==0)
throw (char)0; //抛掷char类型异常
if(a%b)
throw (double)b;//抛掷double类型异常
return a/b;
}
//其中,b==0成立时为了避免错误而抛掷一个char型异常
//而 a%b!=0 成立时则是为了回避一种特定的状态(不是错误),此处约定抛掷一个double型的异常
//简单的异常处理.cpp
#include <iostream>
using namespace std;
int Div(int a, int b) throw(char, double){
if(b==0) throw (char)0; // 抛掷异常
if(a%b) throw (double)b; // 抛掷异常
return a/b;
}
int main(){
int x[5] = {10, 10, 8, 6, 4};
int y[5] = { 5, 3, 0, 2, 0};
int n=0, i, z;
for(i=0; i<5; i++){
try // 圈定
{
cout << ”No. ” << i+1 << ”\t”
<< x[i] << ”/” << y[i];
z = Div(x[i], y[i]); // 调用可能有异常抛掷的函数
cout << ” = ” << z << endl;
n++;
}
catch(char) // 捕捉及处理
{
cout << ”\t除数为 0 。跳过此题。” << endl;
}
catch(double &x) // 此处的x为引用型形式参数
{
cout << ”\t除不尽,除数为” << x
<< ”。跳过此题。”<<endl; // 访问被抛掷的变量
}
}
cout << ”共 ” << n << ” 题。” << endl;
return 0;
}