C++中关于类和对象的讨论
“ 俗话说:九层之台,起于垒土;千里之行,始于足下。从标准C语言向C++语言转化过程中,难免会接触基于C之上更加丰富的概念,C++可以看作C的一种进阶,而要了解这种变化,免不了关于类和对象讨论。本文主要介绍关于类和对象的一些基本概念。”
目录
若需了解更多内容,请微信搜索公众号“知识补盲站”,或扫描文末二维码,欢迎交流!
首先,你想象中的面向对象的程序设计,也许是这样的:
也许是这样的:
但实际的对象到底是什么呢,类又是什么,它们的关系又如何?
好了,废话不多说,直接上干货
类和对象的意义
-
类是把属性和方法进行封装,同时对类的属性和方法进行访问控制。
-
类是由我们根据客观事物抽象而成,形成一类事物,然后用类去定义对象,形成这类事物的具体个体。
-
类是一个数据类型,类是抽象的,而对象是一个具体的变量,是占用内存空间的。
当我们创建一个对象的时候,常常需要做某些初始化操作,例如属性进行赋初值。为了解决这个问题,C++编译器提供了构造函数来处理对象的初始化。构造函数是一种特殊的成员函数,与其他成员函数不同,不需要用户来调用它,而是在创建对象时自动执行。
所以,我们先从构造函数说起。
01 构造函数
1.1对象的初始化
类的数据成员是不能在声明类时初始化的。如果一个类中的所有数据成员都是公用的,则可以在定义对象时对数据成员进行初始化。
1.2 构造函数的作用
构造函数的名字必需与类名相同,不需要用户调用,在建立对象时自动执行。它不具有任何类型,不返回任何值。
1.3 带参数的构造函数
构造函数首部的一般格式为
构造函数名(类型1形参1,类型2形参2,…);
实参是在定义对象时给出的,定义对象的一般格式为
类名 对象名(实参1,实参2);
1.4 用参数初始化表对数据成员进行初始化
这种方法不在函数体内对数据成员初始化,而是在函数首部实现。定义构造函数的形式为:
Box∷Box(int h,int w,int len):height(h),width(w),length(len){ }
1.5 构造函数的重载
构造函数具有相同的名字,而参数的个数或参数的类型不同,称为构造函数的重载。
02 析构函数
当对象的生命周期结束时,就会自动调用析构函数,具体的说出现以下四种情况就会调用析构函数:①如果在一个函数中定义了一个对象(它是自动局部对象),当这个函数被调用结束时,对象应该释放,释放对象前调用析构函数。②static局部对象在函数调用结束时对象并不释放,因此也不调用析构函数,只有在main函数结束或调用exit函数结束时,才调用static局部对象的析构函数。③如果定义了一个全局对象,则在程序的流程离开其作用域时(如main函数结束或调用exit函数结束时)调用析构函数。④new运算符动态地建立了一个对象,当用delete运算符释放该对象时,先调用该对象的析构函数
一个类可以有多个构造函数,但只能有一个析构函数。实际上,析构函数的作用并不仅限于释放资源方面,它还可以被用来执行“用户希望在最后一次使用对象之后所执行的任何操作”。
03 调用顺序
调用析构函数的次序和调用构造函数的次序相反。
04 对象数组
数组不仅可以由简单变量组成,也可由对象组成。在建立对象数组时,分别调用构造函数,对每个元素初始化。每一个元素的实参分别用括号括起来,对应构造函数的一组形参,不会混淆。如:
Student Stud[3]={ //定义对象数组
Student(1001,18,87), //调用第1个元素的构造函数,为它提供3个实参
Student(1002,19,76), //调用第2个元素的构造函数,为它提供3个实参
Student(1003,18,72) //调用第3个元素的构造函数,为它提供3个实参
};
05 对象指针
5.1指向对象的指针
对象空间的起始地址就是对象的指针。可以定义一个指针变量,用来存放对象的指针。
例如有一个类:
class Time
{public:
Int hour;
int minute;
int sec;
void get_time( );
};
void Time∷get_time( )
{
cout<<hour<<< span="">″:″<<minute<<< span="">″:″<<sec<<endl;< span=""></sec<<endl;<></minute<<<></hour<<<>
}
在此基础上有如下语句;
Time *pt; //定义pt为指向Time类对象的指针变量
Time t1; //定义t1为Time类对象
pt=&t1; //将t1的起始地址赋给pt
这样pt就是指向Time类对象的指针变量
定义指向对象的指针的一般形式是:
类名 *对象指针名
可以通过对象指针访问对象和对象的成员,如:
*pt //pt所指向的对象,即t1。
(*pt).hour //pt所指向的对象中的hour成员,即t1.hour
pt->hour //pt所指向的对象中的hour成员,即t1.hour
(*pt).get_time ( ) //调用pt所指向的对象中的get_time函数,即 //t1.get_time
pt->get_time ( ) //调用pt所指向的对象中的get_time函数,即 //t1.get_time
5.2指向对象数据成员的指针
定义指向对象数据成员的指针变量的方法与指向普通变量的指针变量的方法相同。一般形式为:数据变量名 *指针变量名 如 int *p1;
如果Time类的数据成员hour为公用的整型数据,则可以在类外通过指向数据成员的指针变量访问对象数据成员hour。
p1=&t1.hour; //将对象t1的数据成员hour的地址赋给p1,p1指向 //t1.hour
cout<<*p1<<endl; //输出t1.hour的值
5.3指向对象成员函数的指针
定义指向对象成员函数的指针变量的方法和定义普通函数的指针变量的方法有所不同。
成员函数与普通函数有一个最根本的区别:他是类中的一个成员。编译系统要求在上面的赋值语句中,指针变量的类型必须与赋值号右侧函数的类型相匹配,要求在下面三个方面都要匹配:①函数参数的类型和参数的个数②函数返回值类型③所属的类。如:
void (Time∷*p2)( );
//定义p2为指向Time类中公用成员函数的指针变量
定义指向公用成员函数的指针变量的一般形式为:
数据类型名(类名::指针变量名)(参数表列);
可以让它指向一个公用成员函数,只要把公用成员函数的入口地址赋给一个指向公用成员函数的指针变量即可。如:
p2=&Time∷get_time;
使指针变量指向一个公用成员函数的一般形式为:
指针变量名=&类名::成员函数名
5.4this指针
在每一个成员函数中都包含一个特殊的指针,这个指针的名字是固定的,称为this。它是指向本类对象的指针,它的值是当前被调用的成员函数所在的对象的起始地址。例如,当调用成员函数a.volume时,编译系统就把对象a的起始地址赋给this指针,于是在成员函数引用数据成员时,就按照this的指向找到对象a的数据成员。例如volume函数要计算height*width*length的值,实际上执行的是(this->height)*(this->width)*(this->length)
this指针是隐式使用的,它是作为参数被传递给成员函数的,例如本来volume的定义如下:
int Box∷volume( )
{
return (height*width*length);
}
C++把它处理为
intBox∷volume(Box *this)
{
return(this->height*this->width*this->length);
}
即在成员函数的形参表列中增加一个this指针。在调用该成员函数时,实际上是用以下方式调用的:a.volume(&a);将对象a的地址传给形参this指针。然后按this的指向去引用其他成员。
需要说明:这些都是编译系统自动实现的,编程序者不必人为地在形参中增加this指针,也不必将对象a的地址传给this指针。注意*this两侧的括号不能省略。
06 公用数据的保护
6.1常对象
在定义对象时指定对象为常对象。常对象必须要有初值,如Time const t1(14 ,20,36);这样在所有场合中对象t1中所有成员的值都不能被修改。凡希望数据成员不被改变的对象,可以声明为常对象。定义常对象的一般形式为:
类名 const 对象名(实参表列);
也可以把const放在最左面:
const 类名 对象名(实参表列);
如果一个对象被声明为一个常对象,则不能调该对象的非const型的成员函数(除了系统自动调用的隐式的构造函数和析构函数)。如;
const Time t1(10,15,36); //定义常对象t1
t1.get_time( ); //企图调用常对象t1中的非const型成员函数,非法
引用常对象中的数据成员很简单,只需将该成员函数声明为const即可,如:
void get_time( ) const; //将函数声明为const
常成员函数可以访问常对象的数据成员,但仍不允许修改常对象中的数据成员的值。若必须要做出修改,可以将该数据成员声明为mutable,如mutable int count;把count声明为可变的数据成员,这样就可以用 声明为const的成员函数来修改它的值。
6.2常对象成员
1)常数据成员
其作用和用法与一般常变量相似,用关键字const来声明常数据成员。常数据成员的值是不能改变的。注意:只能通过构造函数的参数初始化表对常数据成员进行初始化。如在类体中定义了常数据成员hour:const int hour;//声明hour为常数据成员。不能采用在构造函数中对常数据成员赋初值的方法。在类外定义构造函数,应写成以下形式:
Time::Time(int h):hour(h){}//通过参数初始化表对数据成员进行初始化
2)常成员函数
一般的成员函数可以引用本类中的非const数据成员,也可以修改他们。如果将数据成员声明为常数据成员,则只能引用本类中的数据成员,而不能修改他们。例如只用于输出数据等,如:void get_time( ) const; //注意const的位置在函数名和括号之后
const是函数类型的一部分,在声明函数和调用函数时都要有const关键字,在调用时不必加const,常成员函数可以引用const数据成员,也可以引用非const成员,const数据成员可以被const成员函数引用,也可以被非const成员函数引用。
怎样利用常成员函数?
1)如果一个类中,有些数据成员的值允许改变,另一些不允许改变,则可以将一部分数据成员声明为const,可以用非const成员函数 引用这些数据成员的值,并修改非const数据成员的值。
2)如果要求所有数据成员的值都不允许改变,则可以将所有的数据成员声明为const,或将对象声明为const(常对象),然后用const成员函数引用数据成员,可保证数据不被修改。
3)如果定义了一个常对象,只能调用其中的const成员函数,而不能调用非const成员函数,如果需要访问对象中的数据成员,可以将对象中的成员函数都声明为常成员函数,但应确保在函数中并不修改常对象的数据成员。常对象只保证其数据成员是常数据成员,其值不被修改,如果在常对象中的成员函数未加const声明,编译系统会把它作为非const成员函数处理。
还有一点需要指出,常成员函数不能调用另一个非const成员函数。
6.3 指向对象的常指针
将指针变量声明为const型,这样指针值始终保持为其初值,不能改变。
Time t1(10,12,15),t2; //定义对象
Time *const ptr1; //const位置在指针变量名前面,规定ptr1的值是常值
ptr1=&t1; //ptr1指向对象t1,此后不能再改变指向
ptr1=&t2; //错误,ptr1不能改变指向
定义指向对象的常指针的一般形式为:
类名 *const 指针变量名;
也可以在定义指针变量时将其初始化,如上面2、3两行可合并为:
Time *const ptr1=&t1; //指定ptr1指向t1
请注意:指向对象的常指针变量的值不能改变,即始终指向同一个对象,但可以改变其所指向对象(如t1)的值。
如果想将一个指针变量固定地与一个对象相联系(即该指针变量始终指向一个对象),可以将它指定为const常量。
往往用常指针作为函数的形参,目的是不允许在函数执行过程中改变指针变量的值,使其始终指向原来的对象。
6.4 指向常对象的指针变量
指向常对象的指针变量的一般形式是:
const *类型名 指针变量名
说明:
1)如果一个变量已经被声明为一个常变量,则只能用指向常变量的指针变量去指向它,而不能用一般的指针变量去指向它
2)指向常变量的指针变量除了可以指向常变量外,还可以指向未被声明为const的变量,此时不能通过此指针变量改变该变量的值
3)如果函数的形参是指向非const型变量的指针,实参只能用指向非const变量的指针。
如果函数的形参是指向const型变量的指针,在执行函数的过程中显然不能改变指针变量所指向的变量的值,因此允许实参是指向const变量的指针或指向非const变量的指针。
指向常对象的指针变量的概念和使用是与此类似的,只要将“变量”换成“对象”即可。
请记住这样一条规则:当希望在调用函数时对象的值不被修改,就可以把形参定义为指向常对象的指针变量,同时用对象的地址作实参(对象可以是const或非const)。如果要求该对象不仅在调用函数的过程不被改变,而且要求在程序执行的过程不被改变,则应把它定义为const型。
如果定义了一个指向常对象的指针变量,是不能通过它改变所指向的对象的值的,但是指针变量本身的值是可以改变的。
6.5 对象的常引用
一个变量的引用就是变量的别名。实质上变量名和引用名都指向同一段内存单元。如果形参为变量的引用名,实参为变量名,则在调用函数进行虚实结合时,并不为形参另外开辟一个存储空间(常称为建立实参的一个拷贝),而是把实参变量的地址传给形参(引用名),这样引用名也指向实参变量。
形式 |
含义 |
Time const t1; |
t1是常对象,其值在任何情况下都不能改变 |
void Time∷fun( )const |
fun是Time类中的常成员函数,可以引用,但不能修改本类中的数据成员 |
Time *const p; |
p是指向Time对象的常指针,p的值(即p的指向)不能改变 |
const Time *p;
|
p是指向Time类常对象的指针,其指向的类对象的值不能通过指针来改变 |
Time &t1=t; |
t1是Time类对象t的引用,二者指向同一段内存空间 |
07 对象的动态建立和释放
可以用new运算符动态的建立对象,用delete运算符撤销对象。
如果已经定义了一个Box类,可以用下面的方法动态地建立一个对象:new Box;
编译系统开辟了一段内存空间,并在此内存空间中存放一个Box类对象,同时调用该类的构造函数,以使该对象初始化(如果已对构造函数赋予此功能的话)。但是此时用户还无法访问这个对象,因为这个对象既没有对象名,用户也不知道它的地址。
用new运算符动态地分配内存后,将返回一个指向新对象的指针的值,即所分配的内存空间的起始地址。用户可以获得这个地址,并通过这个地址来访问这个对象。需要定义一个指向本类的对象的指针变量来存放该地址。如
Box *pt; //定义一个指向Box类对象的指针变量pt
pt=new Box; //在pt中存放了新建对象的起始地址
在程序中就可以通过pt访问这个新建的对象。如
cout<height; //输出该对象的height成员
cout<volume( ); //调用该对象的volume函数,计算并输出体积
C++还允许在执行new时,对新建立的对象进行初始化。如
Box *pt=new Box(12,15,18);
调用对象既可以通过对象名,也可以通过指针。用new建立的动态对象一般是不用对象名的,是通过指针访问的,它主要应用于动态的数据结构,如链表。访问链表中的结点,并不需要通过对象名,而是在上一个结点中存放下一个结点的地址,从而由上一个结点找到下一个结点,构成链接的关系。
在不再需要使用由new建立的对象时,可以用delete运算符予以释放。如
delete pt; //释放pt指向的内存空间
08 对象的赋值和复制
8.1 对象的赋值
如果一个类定义了两个或多个对象,则这些同类的对象之间可以互相赋值,这里所指的对象的值是对象中所有数据成员的值。对象赋值的一般形式为:
对象名1=对象名2;//注意对象名1和对象名2必须属于同一类
(1)对象的赋值只对其中的数据成员赋值,而不对成员函数赋值。
(2)类的数据成员中不能包括动态分配的数据,否则在赋值时可能出现严重后果。
8.2 对象的复制
一般形式为:类名 对象2(对象1);用对象1复制出对象2
C++还提供另一种方便用户的复制形式,用赋值号代替括号,如
Box box2=box1; //用box1初始化box2
其一般形式为:类名对象名1 =对象名2;
可以在一个语句中进行多个对象的复制。如
Box box2=box1,box3=box2;
对象的赋值和复制的区别:对象的赋值是对一个已经存在的对象赋值,因此必须先定义被赋值的对象,才能进行赋值。而对象的复制则是从无到有地建立一个新对象,并使它与一个已有的对象完全相同(包括对象的结构和成员的值)。
09 静态成员
如果想在同类的多个对象之间实现数据共享,也不要用全局对象,可以用静态的数据成员。
9.1 静态数据成员
静态数据成员是一种特殊的数据成员,它以关键字static开头。例如:
class Box
{public:
Int volume( );
private:
static int height; //把height定义为静态的数据成员
int width;
int length;
};
如果希望各对象中height的值是一样的,就可以把它定义为静态数据成员,这样它就为各对象所共有,而不只属于某个对象的成员,静态数据成员的值对所有对象都是一样的。
如果改变它的值,则在各对象中这个数据成员的值都同时改变了。这样可以节约空间,提高效率。
说明:
1)如果只声明了类而未定义对象,则类的一般数据成员是不占内存空间的,只有在定义对象时,才为对象的数据成员分配空间。但是静态数据成员不属于某一个对象,在为对象所分配的空间中不包括静态数据成员所占的空间。静态数据成员是在所有对象之外单独开辟空间。只要在类中定义了静态数据成员,即使不定义对象,也为静态数据成员分配空间,它可以被引用。
2)如果在一个函数中定义了静态变量,在函数结束时该静态变量并不释放,仍然存在并保留其值。现在讨论的静态数据成员也是类似的,它不随对象的建立而分配空间,也不随对象的撤销而释放(一般数据成员是在对象建立时分配空间,在对象撤销时释放)。静态数据成员是在程序编译时被分配空间的,到程序结束时才释放空间。
3)静态数据成员可以初始化,但只能在类体外进行初始化。如
intBox∷height=10; //表示对Box类中的数据成员初始化
其一般形式为:数据类型类名∷静态数据成员名=初值;
不必在初始化语句中加static。注意:不能用参数初始化表对数据成员进行初始化。
静态数据成员既可以通过对象名引用,也可以通过类名引用。静态数据成员是不属于对象对而是属于类的,但类的对象可以引用它。如果静态数据成员被定义为私有的则不可以在类外直接引用,而必需通过公用的成员函数引用。
9.2 静态成员函数
在类中声明函数的前面加上static就成了静态成员函数。
与静态数据成员不同,静态成员函数的作用不是为了对象之间的沟通,而是为了能处理静态数据成员。前面曾指出:当调用一个对象的成员函数(非静态成员函数)时,系统会把该对象的起始地址赋给成员函数的this指针。而静态成员函数并不属于某一对象,它与任何对象都无关,因此静态成员函数没有this指针。既然它没有指向某一对象,就无法对一个对象中的非静态成员进行默认访问(即在引用数据成员时不指定对象名)。可以说,静态成员函数与非静态成员函数的根本区别是:非静态成员函数有this指针,而静态成员函数没有this指针。由此决定了静态成员函数不能访问本类中的非静态成员。
静态成员函数主要用来访问静态数据成员,而不访问非静态数据成员,但并不是不能引用本类中的非静态数据成员,只是不能进行默认访问,因为无法知道应该去找哪个对象。如果一定要引用本类中的非静态数据成员,应该加对象名和成员运算符“.”。
10 友元
10.1 友元函数
友元可以访问与它有好友关系的类中的私有成员。友元包括友元函数和友元类。
1)将普通函数声明为友元函数
如果在本类以外的其他地方定义了一个函数(这个函数可以使不属于任何类的非成员函数,也可以是其他类的成员函数),在类体中用friend对其声明,此函数就称为本类的友元函数。
友元函数可以访问这个类中的私有成员。
#include
using namespace std;
class Time
{public:
Time(int,int,int);
friend void display(Time &); //声明display函数为Time类的友元函数
private: //以下数据是私有数据成员
int hour;
int minute;
int sec;
};
Time∷Time(int h,int m,int s) //构造函数,给hour,minute,sec赋初值
{hour=h;
minute=m;
sec=s;
}
void display(Time& t) //这是友元函数,形参t是Time类对象的引用
{cout<<t.hour<<< span="">″:″<<t.minute<<< span="">″:″<<t.sec<<endl;< span=""></t.sec<<endl;<></t.minute<<<></t.hour<<<>
}
Int main( )
{ Time t1(10,13,56);
display(t1);
return 0; //调用display函数,实参t1是Time类对象
}
友元函数在引用这些私有成员时,必须加上对象名。因为友元函数不是类的成员函数,不能默认引用类的数据成员,必须指定要访问的对象。
2)友元成员函数
friend函数不仅可以是一般函数(非成员函数),而且可以是另一个类中的成员函数。
#include
using namespace std;
class Date; //对Date类的提前引用声明
class Time //定义Time类
{public:
Time(int,int,int);
void display(Date &); //display是成员函数,形参是Date类对象的引用
private:
int hour;
int minute;
int sec;
};
class Date //声明Date类
{public:
Date(int,int,int);
friend void Time∷display(Date &); //声明Time中的display函数为友元成员函数
private:
int month;
int day;
int year;
};
Time∷Time(int h,int m,int s) //类Time的构造函数
{hour=h;
minute=m;
sec=s;
}
void Time∷display(Date &d) //display的作用是输出年、月、日和时、分、秒
{cout<<d.month<<< span="">″/″<<d.day<<< span="">″/″<<d.year<<endl; //引用Date类对象中的私有数据</d.day<<<></d.month<<<>
cout<<hour<<< span="">″:″<<minute<<< span="">″:″<<sec<<endl; //引用本类对象中的私有数据</minute<<<></hour<<<>
}
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;
}
10.2 友元类
不仅可以将一个函数声明为一个类的“朋友”,而且可以将一个类(例如B类)声明为另一个类(例如A类)的“朋友”。这时B类就是A类的友元类。友元类B中的所有函数都是A类的友元函数,可以访问A类中的所有成员。
在A类的定义体中用以下语句声明B类为其友元类:
friend B;
声明友元类的一般形式为
friend 类名;
关于友元,有两点需要说明:
1)友元的关系是单向的而不是双向的。
2)友元的关系不能传递
11 类模板
有时,有两个或多个类,其功能是相同的,仅仅是数据类型不同,可以声明一个通用的类模板,它可以有一个或多个虚拟的类型参数,
归纳以上的介绍,可以这样声明和使用类模板:
(1)先写出一个实际的类。由于其语义明确,含义清楚,一般不会出错。
(2)将此类中准备改变的类型名(如int要改变为float或char),改用一个自己指定的虚拟类型名。
(3)在类声明前面加入一行,格式为template<class< span="">虚拟类型参数>,如</class<>
template//注意本行末尾无分号
class Compare
{…}; //类体
(4)用类模板定义对象时用以下形式:
类模板名<< span="">实际类型名>对象名;
类模板名<< span="">实际类型名>对象名(实参表列);
如Comparecmp;
Comparecmp(3,7);
(5)如果在类模板外定义成员函数,应写成类模板形式:
template<class< span="">虚拟类型参数>函数类型</class<>
类模板名<< span="">虚拟类型参数>∷成员函数名(函数形参表列)
{…}。
欢迎关注!