类是一种复杂的数据类型,它是将不同类型的数据和与这些数据相关的操作封装在一起的集合。
老师的属性和行为就可以封装成老师这一个类
我们把具体物体的特征提炼出两种不同的特性,
一种是描述物体的相对静止的特征,如名称、颜色、数量等,称之为类的静态属性,我们可以用不同的数据来表现这些静态属性。
另一种是描述物体运动的特性,即与静态属性相关的操作,如计算、查找、排序等,这些动态的操作的称为类的动态属性,可以用函数来描述这些操作的执行过程。
就像人,自己的姓名、年龄、体重、身高等等的“名词”,就是类的静态属性;而自己能做的事情,比如说吃饭、睡觉、活着等,这些”动词“就是类通过函数定义要去完成的功能,叫做类的动态属性。
在类中,我们将表征静态属性的数据和与这些数据相关的操作封装在一起构成了一个相对封闭的集合体;其中用来描述这一类物品所共同拥有的代表静态属性的数据,称为数据成员,说明这一类对象共同拥有的动态特征的行为(即描述相关操作的函数),称为成员函数。
为了用编程语言来描述一个人,人的“名词们”就统一搞在一起,叫数据成员;而用函数去描述人的行为的函数就叫做这个类里面的成员函数
在设计一个类时,要周密考虑如何进行封装,把不必要让外界知道的部分隐藏起来,也就是说,把一个类的内部实现和外部行为分割开来。类的设计者需要用准确的数据类型来描述类的静态特征,并用各种功能函数来说明该类如何进行对数据的操作。这样面向对象程序设计的第一要务就是定义一个合理的类
定义一个合理的类,就好比创建一个完整的生物,他有名字,身高,会活着,会吃饭…,但是它的有些东西是加密的,你无法直接通过看见这个生物,直接直到它的身高,出生年龄,三围(bushi)…,它会通过某种行为告诉你它的信息,所以它的“名词们”的类型是私有的,一开始只有自己知道的。而它会通过某种行为告诉你,那就是通过行为(成员函数),来传输或者加载自己的信息(数据成员),行为便是公共的。你问我,我不管回不回答,你都有一个结果,这就是成员函数返回的结果。
类的定义格式一般分为说明部分和实现部分。
说明部分用来声明该类中的成员,包含数据成员的声明和成员函数的声明。数据成员可以是基本数据类型、数组、指针或其他类的对象。类的类型在可以包括的数据方面拥有非常大的灵活性。
实现部分用来对成员函数进行具体的定义。
说明部分就是告诉别人这个类中“有什么”和“能干什么”,而实现部分就是告诉别人“怎么做的”
以下是类的一般定义格式
class 类名
{
[private:]
私有数据成员和成员函数
[protected:]
保护数据成员和成员函数
[public:]
公有数据成员和成员函数
};
举个例子
class CDate
{
public:
int Date_Year; //日期中的年份
int Date_Month; //日期中的月份
int Date_Day; //日期中的某一天
}
整个类的定义以分号结束,注意这个分号一定不能少。所有类成员的名称都是该类的局部变量,也就是说在类外使用相同的名称不会引起重名问题。
public是类的访问控制关键字,被称为访问权限修饰符或访问控制修饰符,将数据成员指定为public(公有属性),意味着类对象在作用域的任何位置都可以访问它们。还可以将类成员指定为private(私有属性)和protected(保护属性),这些关键字决定后跟着的类成员的访问属性(访问权限)。
类的封装和信息隐藏功能就是通过对类的成员设置访问属性进行控制的。
如果忽略访问属性的说明,那成员的默认属性就是private。
类的数据成员和成员函数可以根据需要声明为任意一种访问属性,声明时,访问属性出现的顺序和次数也是任意的,不受限制
别人问你的年龄、体重的次数没有个上限次数的,类的访问也是一样。
定义类的对象与定义基本数据类型的变量方法完全相同,雷雨对象的关系可以用基本数据类型和基本数据类型变量之间的关系来类比。
类类型是抽象概念,对象是具体的实例。只有定义了对象,系统才会给对象分配相应的存储空间,定义对象的常用格式如下:
类名 对象名1[,对象名2, ... , 对象名n];
例如
CDate date1;
CDate date2;
CDate date3,date4;
类的数据成员只有在类定义对象以后才有存储空间,此时的访问才有意义,访问对象的成员可以使用成员运算符“.”,其一般格式如下:
对象名.成员
当你这个人都没有的时候,讨论你长大以后会干什么这件事情是没有意义的,所以只有定义完类之后(有了人类,这个类),定义了类的对象之后(有了你这个对象人),知道你会干什么,你性甚,做甚,才有意义。
而在要了这么多的类的对象,你调用成员函数,那是调用谁的函数,访问的是谁的数据。就好比世界上有这么多人,你问他的姓名,是谁的姓名呢?所有格式是对象名.成员
,访问的是这个对象的数据。
成员运算符“.”之前必须是能解释为类对象的内容,成员运算符“.”之后的成员包括数据成员和成员函数。
你操作的类必须是已经存在的了,总不可能去访问外星人的姓名年龄(我们没有任何相关的信息);而操作的函数或者是数据也应该是类中有的,总不可能去访问和尚的头发数量,这不科学,也没有意义。
在类定义内部,所有成员之间可以互相直接访问;但是在类的外部,只能以上述格式访问类的公有成员。主函数main()也在类的外部,所以,在主函数中定义的类对象,在操作时只能访问类中的公有成员。
类中的访问就好像大脑知道你自己所有的信息,自己调用自己的信息不用知道这个私不私密。
在平常的生活中会遇到要设置一个具体的日期、调整日期、输出日期等,这一系列对日期的操作,是动态属性,在c++语言中以函数的形式表现出来,这样的函数就是类中的成员函数,它有权访问本类对象的所有成员。
一般情况下,类的成员都会指定访问属性,因为公有成员是类的对外接口,所以通常将成员函数定义为公有成员,这时该成员函数的原型对外公开,但其具体实现代码仍是封装在类内的。为了体现对数据的封装性,通常将数据成员定义为私有成员或保护成员
就像人,知道是如何发出声音的,如何能自己活着的。但是在我不说出来之前,在我脑中的信息对方是无法知道的,自己的身体结构就相当于一个公共属性的数据成员和成员函数,在我脑中的信息,则是private(私有)的数据。
class CDate{
private://private可以省略,默认为私有
int Date_Year,Date_Month,Date_Day;
public:
void SetDate(int , int , int){}//对哦数据成员初始化的公有成员函数
void Display();//执行显示功能的公有函数
int GetYear();//公有成员函数,提取Date_Year变量值
}
上述的代码还不是完整的类的定义,只有类的说明部分,还缺少实现部分,即只告诉使用者类中"有什么"和"做什么",而没有告诉使用者"怎么做",即各成员函数的具体实现代码没有给出。成员函数具体的实现代码既可以放在类的说明的内部实现,也可以放在类说明之后单独定义。
class CDate{
private://可以省略,默认是私有
int Date_Year,Date_Month,Date_Day;
public:
void SetDate(int y, int m, int d){
Date_Year = y;
Date_Month = m;
Date_Day = d;
}
void Display(){//执行显示功能的公有函数
cout<
上述的代码就是完整的类的定义
那么如何实现类外完善成员函数的定义
与普通的函数相比,在类外实现的成员函数名前一定要用"类名::"来表明该函数不是一个普通函数,而是特定类的成员函数。
void CDate:: SetDate(int year ,int month ,int day){
Date_Year = year;
Date_Month = month;
Date_Day = day;
}
void CDate::Display(){
cout<
通常不用成员函数在类内完成声明定义,因为这样会导致成员函数被默认认为内联函数。
在一个函数首部的最前面增加关键字inline,该函数就被声明为内联函数。其工作原理是:编译器在编译代码时会设法以内联函数的函数体代码代替函数调用,这样可以避免调用函数时的大量系统开销,从而加速代码运行;同时,编译器拓展内联函数时,必须考虑代码的语义,以避免有可能产生的歧义。
简单来说,就是把成员函数的声明和定义放在类内总体性能不高,无伤大雅,看自己喜好。
在上面类的定义中出现了private,public,这种就是访问权限修饰符或者叫访问控制修饰符,这玩意总共有三个,private、protected、public,表示私有,保护和公有属性。
在一个类的定义里面不一定要三个都有,但是至少有一个(废话文学,一个都没有的话,这个类就是空的)
public:即允许该类的成员函数访问,也允许类外部的其他函数访问。
private:只允许被该类的成员函数访问,不能被其他函数访问。
protected:只允许该类及其派生类的成员函数访问,不能被外部的函数访问。
处于保护数据安全的需要,对于private和protected成员,外部函数是不能访问的,这也是类封装性的体现。但需要补充说明的是,在极少情况下C++语言有特殊机制允许外部访问private和public(友元,详情在下一章讲解)
#include
using namespace std;
int main(){
CDate date1 , date2;
date1.SetDate(2019,3,9);
date2.SetDate(1999,3,9);
cout<<"date1.Date_Year="<
在上述的代码中润会出现以下的错误
main.cpp(8):error C2248:"CDate::Date_Year":无法访问private成员(在CDate类中声明)
报错原因在注释中解释了。因为Date_Year是CDate类的私有数据成员,在外部是无法直接访问的,只能叫CDate主动告诉我们。那么就调用GetYear函数。
在C++程序中,每个成员函数都有一个特殊的隐含指针,称为this指针。这个this指针用来存放当前对象的地址。当对象调用成员函数时,系统将当前调用成员函数的对象在内存中的地址传递给this指针,然后调用成员函数。当成员函数处理数据成员时,可以通过this指针指向的位置来提取当前对象的数据成员信息,从而使得不同对象调用同一成员函数处理的是对象自己的数据成员,不会造成混乱。
“我”很常用,但是“我”有具体的一个人固定指的是“我”吗?没有,所以this相当于一个人说话时带有“我”这个字眼,而在c++程序中,他会分析这个“我”具体指的是谁,张三还是李四,返回他存储数据的地址再进行操作。
#include
using namespace std;
class CDate {
private:
int Date_Year, Date_Month, Date_Day;
public:
void SetDate(int, int, int);
void Display();
};
void CDate::SetDate(int year, int month, int day) {
Date_Year = year;
Date_Month = month;
Date_Day = day;
}
void CDate::Display() {
cout << "调用该函数的对象的this指针是";
cout << this << endl;
//输出当前主调对象的地址。
cout << "当前对象Date_Year成员的起始地址:";
cout << &this->Date_Year << endl;
cout << "当前对象Date_Month成员的起始地址:";
cout << &this->Date_Month << endl;
cout << "当前对象Date_Day成员的起始地址:";
cout << &this->Date_Day << endl;
//输出this所指对象的数据成员值
cout << "year = " << this->Date_Year
<< " , month = " << this->Date_Month
<< " , day = " << this->Date_Day;
}
int main() {
CDate dateA, dateB;
dateA.SetDate(2023, 4, 5);
dateB.SetDate(2023, 1, 1);
cout << "dateA地址:" << &dateA << endl;
dateA.Display();
cout << "dateB地址:" << &dateB << endl;
dateB.Display();
return 0;
}
当定义类的对象时,编译系统同样根据其所属的类的类型分配相应的存储空间民兵进行合理的初始化,在C++语言中,这部分工作由构造函数完成。
当对象生命期结束时,析构函数完成对象存储空间的回收和相关的善后事务。
构造函数和析构函数是类的两种特殊函数,每一个类中都包含这两种特殊函数,并且都由系统自动调用。
人的生死,就对应了构造函数和析构函数,生的时候就是构造函数的定义、初始化,挂掉的时候就是析构函数给火化了。
构造函数是类的一种特殊的成员函数。在定义类的对象时,系统会自动调用构造函数来创建并初始化对象。
直白的代码例子就是不写关于这个类的任何构造函数时,系统是会自动生成一个无参构造在“心里”。但是如果你写出了无参构造后,调试过一遍,再把所有参数构造删掉,程序再次运行会报错。
int main(){
CDate today;
cout<<"One day is :";
today.Display();
return 0;
]
因为对象today没有初始化,所以输出该程序会输出三个随机数。但是程序在编译过程中并没有指出这个错误,这个就会给用户带来不必要的麻烦。而对于没有设置为public属性的数据成员,更不能以任何方式从类外部访问这些成员。必须有更好的方法,那就是使用构造函数。系统可以自动调用构造函数在创建对象的同时使数据成员获得初始值。
构造函数与普通成员函数的定义方式完全相同,其实现可以在类内,也可以在类外。除了具有一般成员函数特征外,构造函数还具有以下特殊的性质。
(1)、构造函数的函数名必须与类名相同,以类名为函数名的函数一定是类的构造函数。
(2)、构造函数没有返回值,给构造函数指定返回类型是错误的,即使添加“void"也是不允许的。(如果不小心指定一个构造函数的返回类型,编译器会报告一个错误信息,错误号为C2380)
(3)、构造函数为public属性,否则定义对象时无法自动调用构造函数,编译时会出现错误提示。
(4)、构造函数只会创建对象时由系统自动调用,所定义的对象在对象名后要提供初始化对象所需的实际参数。注意,既然在定义对象的同时,系统已经完成了对象的初始化工作,就不能在程序中写出形如:对象名.构造函数名(实际参数表)的构造函数调用。
其实用构造成员函数对类进行初始化也不是不行,有返回值的前面加类型,没有的就为”void“类型。此处的构造函数更多的是在定义的时候就完成初始化。就像int、char类之类的,后面可以直接给变量名赋值上初值。所以引入此的更多意义我觉得是,能让我们完整的,方方面面的构造出一个类,能考虑到使用时可能会出现、遇到的任何问题。
#include
using namespace std;
class CDate{
private:
int Date_Year,Date_Month,Date_Day;
public:
CDate(int ,int ,int );
void Display();
};
CDate::CDate(int y ,int m ,int d ){
cout<<"Executing constructor...\n";
Date_Year = y;
Date_Month = m;
Date_Day = d;
}
void CDate::Display(){
cout<< Date_Year << " - " << Date_Month << " - " << Date_Day << endl;
}
int main(){
CDate today(2023 , 6 , 11 );//定义对象同时完成对象的初始化
cout<<"Today is:";
today.Display();
return 0;
}
int main() {
CDate today(2023, 6, 11);
cout << "Today is :";
today.Display();
today = CDate(2004, 4, 5);
cout << "Today is :";
today.Display();
return 0;
}
这个就相当于
int a = 10;a=8
最终a=8一样,赋值过后再次赋值,覆盖掉旧值。
C++规定,如果在类定义中,没有定义构造函数,编译器就会自动生成一个默认的构造函数(这玩意就定义在“心里”,不会帮你写到你的代码里的),该构造函数无形式参数,也无任何语句,其功能仅用于创建对象,为对象分配空间,但不初始化其中的数据成员,系统默认构造函数的形式如下。
类型(){//为什么要左大括号要在第一行,这是一个代码编写的规范,你喜欢的话我也不能阻止你括号另起一行写。
}
但是编译器并非总是会自动生成一个默认构造函数,否则就不会出现错误代码C2512。实际情况是:在类定义时,若没有自定义构造函数,编译器就会生成一个默认的无参构造函数。如果类定义中已经为类提供了任意一种形式的构造函数,编译器就不会再提供默认的无参构造函数。
每次定义类的对象时,编译器都会自动查找并匹配最合适的构造函数。
在使用构造函数时务必注意一下两点:
(1)、一个类可以拥有多个构造函数。对构造函数可以进行重载,重载的多个构造函数必须在形式参数的类型、个数和顺序等至少一方面不一样,要注意避免出现二义性。
(2)、若用户美欧定义构造函数,系统会为每个类自动提供一个不带形式参数的构造函数。但是此时该构造函数只负责为对象的各个数据成员分配空间,而不提供初值。一旦用户自己定义了构造函数,系统就不再提供默认的无参构造函数,这时的无参构造函数需要用户自己定义。
对于带有参数的构造函数,在定义对象时必须给构造函数传递参数。
#include
using namespace std;
class CDate {
private:
int Date_Year, Date_Month, Date_Day;
public:
CDate(int y = 2000, int m = 1, int d = 1);
void Display();
};
CDate::CDate(int y, int m, int d) {
cout << "Executing constructor..." << endl;
Date_Year = y;
Date_Month = m;
Date_Day = d;
}
void CDate::Display() {
cout << Date_Year << "-" << Date_Month << "-" << Date_Day << endl;
}
int main() {
CDate initiateday;//定义对象不提供实际参数,全部采用默认值
CDate newday(2023);//只提供一个实际参数,其余两个采用默认值
CDate today(2023, 6, 11);//提供三个参数
cout << "Intiateday is :";
initiateday.Display();
cout << "Newday is :";
newday.Display();
cout << "Today is :";
today.Display();
return 0;
}
在定义构造函数时,为避免出现因参数数量不同而找不到合适的构造函数,建议构造函数采用带默认参数值的形式比较安全。(万一运行的时候,那个地方值没有赋上,也不至于报错)
在此前的代码中,我们在构造函数体中使用赋值语句初始化对象的数据成员,还可以采用另一种方式——初始化列表。
CDate::CDate(int y, int m, int d):Date_Year(y),Date_Month(m),Date_Day(d){
cout<<"Executing constructor..."<
简便的写法。看个人习惯,对于自己来说哪个写起来更顺手就用哪个,对于另一种了解就可以了。
复制构造函数也是一张重载版本的构造函数,它是一个已存在的对象初始化另一个新创建的同类对象
类名(const 类名&对象名);//复制构造函数声明
如果设计类时不写复制构造函数,编译器就会自动生成。在大多数情况下,其作用是实现从源对象到目标对象逐字节的复制,使得目标对象的每个成员变量都与源对象相等。
为什么要引入这种复制构造函数?更多的还是为了能够完善整个类的定义过程,就像
int a=10;int b=a;
此处引入的复制构造函数就是为了让b=a这种形式成立。
谈谈系统默认的复制构造函数,这种构造函数是浅复制,在下面会有讲到
int main() {
CDate newday(2023);
CDate day = newday;
cout << "Newdat is :";
newday.Display();
cout << "day is :";
day.Display();
return 0;
}
调试结果如下
这里的CDate day = newday;
用的就是系统默认给的复制构造函数。
复制构造函数在以下三种情况下由系统自己调用。
(1)、明确表示由一个已定义的对象初始化一个新对象。
(2)、函数的形式参数为一个对象,当发生函数调用、对象作为实参传递给函数形参时,注意,如果形式参数时引用或指针,就不会调用复制构造函数,因为此时不会产生新对象。
(3)、对象作为函数返回值。
#include
using namespace std;
class CDate {
private:
int Date_Year, Date_Month, Date_Day;
public:
CDate(int y = 2000, int m = 1, int d = 1);
CDate(const CDate& date);
void Display();
};
CDate::CDate(int y, int m, int d) {
cout << "Executing constructor..." << endl;
Date_Year = y;
Date_Month = m;
Date_Day = d;
}
CDate::CDate(const CDate& date) {
Date_Year = date.Date_Year;
Date_Month = date.Date_Month;
Date_Day = date.Date_Day+1;//通过函数体将日期后延一天
cout << "Copy Constructor called.\n";
}
void CDate::Display() {
cout << Date_Year << "-" << Date_Month << "-" << Date_Day << endl;
}
CDate fun(CDate newdate1) {//普通函数,以类对象作为值形式参数
CDate newdate2(newdate1);//第一种调用复制构造函数的情况
return newdate2;//第三种调用复制构造函数情况。
}
int main() {
CDate day1(2023, 6, 12);//调用普通构造函数
CDate day3;//调用普通构造函数
CDate day2(day1);//第一种调用复制构造函数的情况
CDate day4 = day2;//等效于Date day4(day2);
day3 = day2;//此语句为赋值语句,不调用任何构造函数
day3 = fun(day2);//第二种调用复制构造函数的情况
day3.Display();
return 0;
}
创建类的对象,系统会自动调用构造函数。同样,当对象生命期结束时,需要释放所占的内存空间,程序将自动调用类的析构函数来完成。。
把尸体火化,不留痕迹。
~ 类名();//火化申请
析构函数的实现可以在类内,也可以在类外,与普通成员函数相同。
关于析构函数的几点说明如下。
(1)析构函数也是类的成员函数,其函数名与类名相同,但在类名前要加“~”号。
(2)析构函数没有返回值类型,前面不能加void,且必须定义为公有成员函数。
(3)析构函数没有形式参数,也不能被重载,每个类有且只有一个析构函数。
(4)析构函数由系统自动调用执行,在两种情况下会发生析构函数调用:第一种时对象生命结束时由系统自动调用;第二种是用new动态创建的对象,用delete释放申请的内存时,也会自动调用析构函数。。
一般情况下,使用系统默认的析构函数就可以了。但是,如果一个类中申请了一些系统资源,比如在构造函数中申请了动态空间,当对象生命期结束时,通常就应当定义一个析构函数,并在析构函数中释放所有申请的动态空间。
有借的东西就应该还,就算爹挂了做儿子也得还。new从系统借来的资源在“自己”挂掉以后就应该还掉。
我轻轻的走了,正如我轻轻的来。轻轻的我走了,不带走一片云彩。所以一般系统默认的析构函数里面没什么东西。
正在秃头补充ing