作为一种典型的面向对象程序设计语言,C++ 语言将抽象之后的数据和函数封装在一起,就构成了 C++ 的类。
类是面向对象程序设计方法的核心——通过类,我们可以实现对于数据的封装和隐藏。
C++ 语言中,继承自 C 语言的部分——面向过程的编程范式。在面向过程的结构化程序设计中,程序的模块是由函数构成的,函数将逻辑上相关的语句与数据封装,用于完成特定的功能。
在面向对象的程序设计中,程序模块是由一个个的类构成的——类是对逻辑上相关的函数与数据的封装,是对问题的抽象描述。因此,后者的集成度更高,也就更适合用于大型复杂程序的开发。
C++ 语言提供了一系列基本的数据类型,比如int,double,bool等,声明一个变量的时候,系统就根据这个变量的类型为它分配了一定的内存空间。然而变量类型的作用并不只是告诉系统要分配多大的内存空间,还同样决定了这个变量可以被执行什么样的操作——比如对于int可以对其进行加减乘除四则运算,对于bool可以判断它是不是true。
C++ 自身提供的类型远远不足以满足实际开发的需求——所以 C++ 提供了对自定义类型的支持,也就是类。
类实际上相当于一种用户自定义的类型——原则上可以定义无限多种的自定义类型,因此不仅可以用int类型表示整数,也可以用自定义类的变量表示“时钟”、“汽车”或者“人”,也就是说所谓的对象。
当定义了一个类之后,就可以定义这个类的变量,这个变量就是类的对象,或者叫做实例——而这个定义的过程,则称为实例化。
以时钟类为例,介绍一下类的具体用法:
class Clock
{
public:
void setTime(int newH,int newM,int newS);
void showTime();
private:
int hour,minute,second;
};
在Clock类中定义的数据和函数,分别称为 数据成员 和 函数成员。
类定义的正式语法形式如下:
class 类名称
{
public:
外部成员
protected:
保护型成员
private:
私有成员
};
其中,public,protected和private都是对类成员的不同访问权限控制
类的成员函数定义必须写在类内
1、类的对象和使用
举个例子
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
class Clock
{
public: // public:定义的成员,是类的公有成员,可以直接在类外访问
void setTime(int newS,int newM,int newH) // 可通过一个公有的函数作为类的接口,用这个接口来访问类的私有成员。
{
second = newS;
minute = newM;
hour = newH;
}
void showTime(); // 函数的声明
private: // private:定义的类成员,是私有成员,不能在类外被访问
int second,minute,hour;
};
void Clock::showTime() // 函数的定义 注意与普通函数不同,在类外实现的时候类的成员函数名需要用类名加以限制
{
cout << hour <<":"<< minute <<":"<< second << endl; // 它的作用是显示当前时间(也就是三个成员变量的值)。
}
int main()
{
Clock MyClock;
int second,minute,hour;
cin >> second >> minute >> hour;
MyClock.setTime(second,minute,hour);
MyClock.showTime();
return 0;
}
当把 public 改为 private 再运行程序就会发现编译器报错,因为没有访问权限
一般来说,类中定义为 private 的成员变量或者成员函数,是没有办法直接在类外访问的,即在主函数中是不能读取或者修改变量的。
使用public:定义的成员,是类的公有成员,它们是可以直接在类外访问的
而使用private:定义的类成员,则是私有成员,不能在类外被访问——但是可以被同一个类里的成员访问
注意与普通函数不同,在类外实现的时候类的成员函数名需要用类名加以限制
2、类的构造函数
为了方便编程人员通过程序来规定如何对自己定义的对象进行初始化,C++ 语言的语法中提供了一种特殊的机制——构造函数。构造函数是一种定义在类体中的特殊函数,它可以被用于描述对类进行初始化的算法。
在语法上,构造函数具有这样的性质:
1、函数名与类名完全相同
2、不能定义返回值类型,也不能有return语句
3、可以有形参,也可以没有形参,可以带有默认参数
4、可以重载
从定义上看,构造函数跟自己定义的成员函数看起来很相似——不过在使用上,不需要手动调用构造函数。当创建对象的时候,构造函数会自动被调用。比如说,现在定义了一个构造函数,它可以用参数来初始化clock的三个成员变量——当创建对象的时候,使用以下的代码:
Clock MyClock(0,0,0);
然后clock类中对应的构造函数就会自动被调用,并且按照参数列表,将MyClock对象的三个成员变量都设置为0。
在定义构造函数的时候,可以有参数表,也可以让参数表空着——同样,即使是一个构造函数有参数,也可以给它的所有参数都设置一个默认值。这样的构造函数,称为默认构造函数。
注意,同一个类中,不能出现两个默认构造函数——例如,对于下面这段代码,编译器将会报错:
class clock
{
public:
clock();
clock(int newH=0,int newM=0,int newS=0);//编译器会报错
}
定义一个类的时候,不声明任何构造函数,那么编译器在编译的时候,就会自动生成一个默认构造函数,它具有这样的特点:
1、参数列表为空,不为数据成员赋初值
2、如果类内定义了成员的初始值,则使用内类定义的初始值
3、如果没有定义类内的初始值,则以默认方式初始化
4、基本类型的数据默认初始化的值是不确定的(类似于我们在主函数中声明一个类却不赋初始值的情况)
简而言之,这样一个构造函数,它的特点就是“什么都不做”,单纯只是创建一个类而已——相当于这样的一个形式:
clock(){}
需要注意的是,如果在类中已经定义了一个构造函数(可以是任意形式)的话,那么编译器就不会再定义默认构造函数了——这个时候,如果需要使用到默认构造函数的话,不要忘记自己再定义它。
(1)使用构造函数
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
class Clock
{
public:
Clock() // 默认一个构造函数
{
second = 0;
minute = 0;
hour = 0;
}
Clock(int newS,int newM,int newH); // 带参的构造函数
void setTime(int newS,int newM,int newH)
{
second = newS;
minute = newM;
hour = newH;
}
void showTime();
private:
int second,minute,hour;
};
void Clock::showTime()
{
cout << hour << ":" << minute << ":" << second << endl;
}
Clock::Clock(int newS,int newM,int newH) // 构造函数的实现
{
second = newS;
minute = newM;
hour = newH;
}
int main()
{
Clock MyClock; // 调用无参数的构造函数
MyClock.showTime();
int second,minute,hour;
cin >> second >> minute >> hour;
MyClock.setTime(second,minute,hour);
MyClock.showTime();
Clock MyClock2(1,2,3); //调用有参数的构造函数
MyClock2.showTime();
return 0;
}
(2)复制构造函数
定义了一个变量,然后用另一个变量来对它进行赋值,这实际上是一个变量复制的过程。
生成一个对象的副本有两种途径:
第一种途径是建立一个新的对象,然后将一个已有对象的数据成员值取出来,赋值给新的对象。这样做虽然可行但是实在是太麻烦了。
第二种途径复制构造函数——它的作用就是用一个已有的对象,来执行一个新的对象的构造。
复制构造函数具有一般构造函数的所有特性——它的形参是本类的一个对象的引用,作用是用一个已经存在的对象(即为函数的参数)来初始化一个新的对象。
复制构造函数使用的就是引用传参。为什么这里要使用引用来传参呢?因为值传递就是当函数发生调用的时候,给形参分配内存空间,然后用实参的值来初始化形参——如果参数是一个对象的话,那么对于值传递来说,“初始化形参”这个过程就会造成很多额外的时间开销,浪费系统资源。而使用引用,则不会有这样的问题。
在实际的开发过程中,可以根据现实中所遇到问题的需要来定义一个复制构造函数,以实现同类对象之间数据成员的传递。
如果自己没有定义类的构造函数的话,那么系统就会在必要的时候,自动生成一个隐含的复制构造函数。这个隐含的复制构造函数的作用是,把初始值对象的每一个数据成员的值都复制到新建的对象中——因此,也可以说是完成了同类对象的复制。复制构造函数具体的声明方式如下所示:
class 类名
{
public:
类名(形参表); // 构造函数
类名( 类名 & 对象名 ) // 复制构造函数
{
//实现
}
};
或者这样:
class 类名
{
public:
类名(形参表); // 构造函数
类名( 类名 & 对象名 ) // 复制构造函数
};
类名::类名( 类名 & 对象名 ) // 复制构造函数的实现
{
// 实现
}
举例:现在有一个Point类,表示屏幕上的一个点——它包括两个int类型的私有成员x,y,表示这个点的坐标。
现在定义这个类的复制构造函数:
class Point
{
public:
Point(int xx = 0 , int yy = 0) // 构造函数
{
x = xx;
y = yy;
}
Point(Point &p ) ; // 复制构造函数
private:
int x,y;
};
这里按照以下代码,实现复制构造函数:
Point : : Point( Point &p)
{
x = p.x;
y = p.y ;
}
这里可以注意到,复制构造函数通过一种看似“不合法”的方式,访问了Point类的实例对象p的两个私有成员变量。
需要注意的是——private与public的区别是对类来说的,而不是对对象来说的。
拷贝构造函数是Point类的成员函数——所以它可以访问类的私有成员变量,这跟具体的对象无关。
普通的构造函数(包括默认构造函数)是在对象创建的时候被调用的——而复制构造函数会在什么时候被调用呢?
主要是以下的三种情况:
1、当用类的一个对象去初始化该类的另一个对象的时候:
Point a(1,2);
Point b(a); // 用对象a初始化对象b,复制构造函数被调用
Point c = b; // 用对象b给对象c赋值,复制构造函数被调用
2、当函数的形参是类的对象,调用函数时进行形实结合的时候:
void f(Point p)
{
//code here
}
int main()
{
Point a(1,2);
f(a); // 函数的形参为类的对象,当调用函数时,复制构造函数被调用
return 0;
}
3、当函数的返回值是类的对象,函数执行完成返回调用者的时候:
Point g()
{
point a(1,2);
return a; // 函数的返回值是类的对象,返回函数值时,调用复制构造函数
}
同样,对于在函数中创建的对象,也是如此——例子中的return a;返回的并不是a这个对象本身,而是通过复制构造函数,在主调函数中用a重新构造的对象。在函数调用返回的时候,原来的临时对象a的使命已经完成,随着整个函数中的其他临时变量一起被销毁了。
最后,可能会有这样的疑问:就算是不自己定义复制构造函数,编译器也可以自动生成一个隐含构造函数——而上面的示例中写的复制构造函数,功能跟隐含的复制构造函数其实并没有什么区别。那么问题来了——这种情况下,还有必要自己写一个复制构造函数吗?
的确,很多情况下确实没必要自己去定义一个复制构造函数——但是需要考虑另外一种情况:有些时候,并不需要复制一个对象的所有成员——就好像在复印的时候,有的时候只需要复印一本书的某一页,甚至某一个段落。同样,对于复制构造函数来说,也可以自己实现一些有选择、有变化的复制——例如下面的代码,可以把每一个由复制构造得到的Point对象,横坐标增加10
Point(Point &p)
{
x = p.x+10;
y = p.y;
}
除此之外,有的时候类的数据成员中会有指针类型,这个时候默认的复制构造函数能够实现的就只有浅复制——这会带来数据安全上的隐患。要实现正确的复制,也就是所谓的深复制,就必须重新编写复制构造函数才行。
展示一个完整的程序
#include <iostream>
using namespace std;
class Point
{
public:
Point(int xx = 0, int yy = 0) // 构造函数
{
x = xx;
y = yy;
}
Point(Point &p); // 复制构造函数
int getX()
{
return x;
}
int getY()
{
return y;
}
private:
int x, y;
};
//成员函数的实现
Point::Point(Point &p)
{
x = p.x;
y = p.y;
cout << "calling the copy constructor" << endl;
}
//形参为point类对象的函数
void fun1(Point p)
{
cout << p.getX() << endl;
}
//返回值为point类对象的函数
Point fun2()
{
Point a(1, 2);
return a;
}
int main()
{
Point a(4, 5); // 第一个对象a
Point b = a; // 情况一:用a初始化b。第一次调用复制构造函数
cout << b.getX() << endl;
fun1(b); // 情况二:对象b作为fun1的实参。第二次调用复制构造函数
b = fun2(); // 情况三:函数的返回值是类对象,函数返回时,调用复制构造函数
cout << b.getY() << endl;
return 0;
}
运行结果:
(3)初始化列表和析构函数
初始化列表
对于类的成员变量,可以在类的构造函数函数体中为它赋初始值。而另一方面,如果在构造函数中,需要做的事情仅仅是给类的成员变量赋初始值的话,那么其实可以使用C++提供的另外一种非常便捷的工具:初始化列表。
所谓“初始化列表”,就是在类的构造函数的参数列表后面,写上要用哪个参数,来初始化哪个变量——写法如下所示:
构造函教(参数列表):成员变里(常里或者参数)
{
//构造函数的函数体
}
举个简单的例子——对于之前定义的clock类,构造函数写成了这个样子:
clock::clock(int news, int newM, int newH)
{
second = news;
minute - newM;
hour = newH;
}
实际上可以理解成这样的一个过程——创建一个新的对象的时候,首先对这个对象的成员变量进行了初始化,然后再在构造函数中,执行使用参数为成员变量赋值的过程。
于是问题来了——已知在主函数中,完全可以一步到位,直接在声明变量的时候用一个初始值来对变量进行初始化,如下所示:
int i;
i = 1;
//可以直接写成int i = 1;
同样的道理,也可以在对象构造的时候,直接就用初始值来初始化成员变量——比如上面那个构造函数,就可以写成这种形式:
clock(int newls , int newM,int newH ):minute(newM) , second(newS) , hour(newH)
{
//注意必须写在类的声明中
//如果还需要构造函数干别的事,那可以跟以前一样在构造函数体中写下代码
};
析构函数
析构函数 与构造函数的作用几乎是相反的,他用来完成对象被删除前的一些清理工作,,在对象的生存期即将结束的时刻被自动调用。
与构造函数一样,析构函数通常也是类的一个公有函数成员,它的名称是由类名前面加“~”构成,没有返回值,也没有任何参数,但可以是虚函数
同样,如果自己不去定义的话,编译器也同样会自动生成一个隐含的析构函数——跟默认构造函数一样,这个隐含的析构函数什么都不会做。
比如说,这里可以给clock类定义一个空的析构函数——其功能与系统自动生成的析构函数相同:
class clock
{
//其他代码略
~clock(){} //析构函数
};
简而言之,如果希望程序在对象被删除之前,自动地(不需要人为进行函数调用)来完成某些事情,就可以把代码写到析构函数里。
3、类的组合
事实上,“组合”这个概念离并不遥远。其实一直都在用组合的方法构建类。比如=之前构建的clock类:
class clock
{
public:
void setTime(int newS,int newM ,int newH)
{
}
private:
int second ,minute, hour;
};
在这里,其实已经应用到了类的组合——定义的类clock,实际上可以被认为是用基本数据类型int组合而成的。
类的组合描述的就是一个类内嵌其他的对象作为成员的情况,它们之间的关系是一种包含与被包含的关系
当创建类的对象时,如果这个类具有内嵌对象成员,那么各个内嵌对象将会首先被自动创建,因为部件对象是复杂对象的一部分——就像是现实中,必须先把各个部件生产出来,然后才能把它们组装成手机一样。
在创建对象的时候,既要对类中的基本数据类型进行初始化,也要对内嵌对象成员进行初始化——因此,首先需要理解这些对象的构造函数调用顺序。
组合类构造函数一般的定义形式如下:
类名::类名(形参表):内嵌对象1(形参表),内嵌对象2(形参表)...
{
//初始化函数体
}
大家会注意到,内嵌对象的初始化使用了初始化列表来完成——内嵌对象必须使用初始化列表来实现初始化。
举个简单的例子——现在有一个圆的类circle,以及一个点坐标的类Point,定义如下:
class Point
{
private:
int x,y;
public:
Point(int newX,int newY ) : x(newX ) ,y(newY){}
};
class circle
{
private:
double radius;
point center; //圆的圆心点
public:
};
然后,对于circle类,它的构造函数就可以写成这个样子:
circle(double r,point p):radius(r),center(p)
{
//构造函数初始化代码
}
创建一个组合类的实例对象的时候,不光是它自己的构造函数的函数体被调用执行,而且还将调用其内嵌对象的构造函数。这个时候,构造函数的调用顺序如下所示:
1.调用内嵌对象的构造函数,调用顺序按照内嵌对象在组合类中的定义中出现的顺序。需要注意的是,写在初始化列表中的内嵌对象顺序,跟内嵌对象构造函数的调用顺序是没有关系的。
2.执行本类构造函数的函数体。
析构函数的调用执行顺序与构造函数正好相反,析构函数的函数体被执行完毕之后,内嵌对象的析构函数开始被一一执行——它们的调用顺序,跟它们在组合类的定义中的顺序正好相反。需要注意的是,因为需要调用内嵌对象的析构函数——所以,虽然有的时候并没有显式定义析构函数,但是编译器自己生成的析构函数,也并不是跟以前一样“什么都不做”。
组合类的复制构造函数,默认状态下同样由编译器自行生成——它会自动调用各个内嵌对象的复制构造函数,为它们初始化。
如果想要为组合类编写复制构造函数的话,那么需要为内嵌对象成员的复制构造函数传递参数。
比如说,对于circle类,它的复制构造函数应该这样定义:
Circle::Circle(circle &c1):center(c1.center)
{
radius = ci.radius;
}
4、组合类的应用
#include <iostream> // 输入输出所在头文件
#include <cmath> // 数学公式所在头文件
using std::cin; // 从外部获取数值
using std::cout; // 输出
using std::endl; // 换行
class Point // point类
{
public: // 外部接口
Point(int newX = 0,int newY = 0)
{
x = newX;
y = newY;
}
Point(Point &p)
{
x = p.x;
y = p.y;
}
int getX()
{
return x;
}
int getY()
{
return y;
}
private:
int x,y;
};
class Line // line类
{
public:
Line(Point new_p1,Point new_p2):p1(new_p1),p2(new_p2) // Line类的默认构造函数,且用初始化列表来为p1和p2进行初始化
{
double x = static_cast<double>(p1.getX()-p2.getX()); // static_cast <new_type> (expression)强制类型转换
double y = static_cast<double>(p1.getY()-p2.getY());
len = sqrt(x*x+y*y ); // 求出线段的长度len的值,然后赋值给len成员变量。
}
Line(Line &l):p1(l.p1),p2(l.p2) // Line类的复制构造函数
{
len = l.len;
}
double getLen() // getLen公有成员函数,作用是返回len的值
{
return len;
}
private:
Point p1,p2; // point类型成员p1,p2,分别表示线段的两个端点
double len; // double类型成员len,表示线段的长度。
};
int main()
{
Point pa(1,2);
Point pb(3,5);
Line L1(pa,pb);
Line L2(L1);
cout <<"The length of L1 is " << L1.getLen() << endl;cout <<"The length of L2 is " << L2.getLen() << endl;
return 0;
}