所谓的类,即类型.是一种抽象的概念,是面向对象编程中的基石.而对象,就是类的实例化,是实实在在的存在.比如书籍是一个类型,而《算法导论》是书籍这个类的一个具体对象.在面向对象的程序设计语言中,核心思想就是:万物皆对象.
为什么需要使用面向对象的方法?在这里不再赘述,相信随便哪本工具书或互联网上都介绍过其优势,我们就从一些更加实用的角度去说,比如:如何使用类和对象?
要实例化一个对象,我们需要先声明一个类,在C语言中,我们会使用结构体这样的结构声明自定义类型,如:
struct Complex { //声明复数类型;
double real;
double imag;
};
struct Complex c; //复数类型的变量;
而C++对结构体进行了扩充,将struct
关键字和class
关键字进行了同质化处理,如:
//sturct Complex同上;
Complex c; //复数类型的对象;
和下面的:
class Complex { //声明复数类型;
public:
double real;
double imag;
};
Complex c; //复数类型的变量;
是一样的.
public
关键字用来指明类的成员的访问属性,一共有三种:
关键字 | 访问属性 |
---|---|
public | 所有类/函数均可访问 |
private | 仅本类内部及其友元类/友元函数可访问 |
protected | 比private多一个派生类可访问 |
因此,一般的做法是,将类的数据成员封装成private
,将公共接口封装成public
.所谓公共接口,就是用户能直接使用的函数、变量等.
通过以上例子,我们可以知道: struct
在C++中相当于默认所有成员都是public
的类,而不指明访问属性的class
相当于默认所有成员都是private
的类
注:
class
和struct
一样,使用成员运算符.
来访问其内部public属性成员
class
和struct
均可写类内函数,用来实现特定功能,这样的类内函数称为类的方法
既可以在类内部实现方法,也可以用域运算符::
在类外实现方法,称为类的声明和实现分离
类的声明一般写在类名.h
的头文件中,实现写在类名.cpp
的源文件中
类的声明和对象的实例化大概如下:
class 类名 { //声明一个类,写在类名.h中;
声明类体;
};
返回值 类名::方法名() { //写在类名.cpp中;
方法实现;
}
类名 对象名; //实例化对象;
以复数类为例,写一个简单的例子:
Complex.h:
#ifndef COMPLEX_H //预编译命令,用于处理重复包含头文件的编译错误;
#define COMPLEX_H
class Complex {
public:
void show();
private:
double real;
double imag;
};
#endif COMPLEX_H
Complex.cpp:
#include"Complex.h" //包含本类声明头文件;
#include
using namespace std;
void Complex::show() { //实现show()方法;
if(imag >= 0)
cout<<real<<"+"<<imag<<"i"<<endl;
else
cout<<real<<imag<<"i"<<endl;
}
类是一个类型,抽象的概念,不占有存储空间,但是当实例化对象时,系统会为每一个对象分配存储空间
那么,如何分配存储空间呢?
其实仔细想想,不同对象仅数据成员不同,它们调用的方法(成员函数)代码都是相同的,因此C++编译系统做了如下处理:对每个对象,仅为其数据成员分配独立的存储空间,而所有对象的方法都放在一个公共的存储单元里,这样的好处自然显而易见——节省空间,但是会遇到一个问题:如何确定是哪个对象调用公共存储单元的方法?该对哪个对象的数据成员进行操作呢?
于是C++有一个指向对象的指针this
,用于指向当前对象,我们在实现类内函数时,通常省去this
不写,实际上上面的show()
方法的实现和以下等价:
void Complex::show() { //实现show()方法;
if(imag >= 0)
cout<<this->real<<"+"<<this->imag<<"i"<<endl;
else
cout<<this->real<<this->imag<<"i"<<endl;
}
那么this
指针有什么作用呢?非常简单,this是编译系统隐式调用的,用于区分对象,用户也可以显式调用,用于区分方法的形参名和对象的数据成员同名的情况,如下:
void Complex::setReal(double real) { //假设有个方法是给实部设定值;
this->real = real; //告知编译系统,将参数的real赋值给当前对象的real成员;
}
构造函数(Constructor): 是对对象进行初始化的一类函数,构造函数和类同名,且没有返回值.
若希望在一实例化对象就对其进行初始化,则需要调用构在函数,且不管用户需不需要,在实例化对象的时候总有构造函数被调用,若未声明构造函数,系统会默认指定一个空的无参构造函数以供调用,调用的方式分隐式调用和显式调用,请看下面的例子:
//Complex.h;
class Complex {
public:
Complex(double,double);
private:
double real;
double imag;
};
//Complex.cpp;
Complex::Complex(double r,double i):real(r),imag(i){}
其中,上面的代码第9行称为带参数列表的构造函数,其写法等价于:
Complex::Complex(double r,double i) {
real = r;
imag = i;
}
如果我们这样声明一个Complex对象:
Complex c;
则会发生错误,因为它和
Complex c();
等价,而我们没有定义无参构造函数,因此无法以这样的形式声明.我们可以写一个重载构造函数:
Complex::Complex() {
real = 0;
imag = 0;
}
是的,构造函数也可以重载,下面介绍各种常见的构造函数:
Complex::Complex(double r,double i = 0):real(r),imag(i){}
和之前说的一样,使用默认参数要向最右靠,这种构造函数的作用是当不想给所有成员初始化时,一些成员将获得默认值作为初始值,不需要在传参时考虑其取值
又一说复制构造函数,但个人喜欢拷贝这个词,贴切.所谓拷贝构造函数,就是使用一个已有的对象为模板,去建立一个新的对象,使这两个对象的信息完全一致,从而达到快速生成同类对象的目的,好比流水线量产CPU一样.
当对象中没有指针成员存在时,我们可以不写拷贝构造函数,编译系统有自动生成机制,直接调用即可:
Complex c1(c); //c是已存在的Complex类型的对象;
可见其调用形式为——类名 新建对象名(已有对象名);
或——类名 新建对象名 = 已有对象名;
这种形式称为浅拷贝,相当于函数参数的值传递,是一个变量之间的的简单复制过程
深究其原理,大概就是一个这样的定义方式:
//Copying Constructor;
Complex::Complex(const Complex &t) {
this->real = t.real;
this->imag = t.imag;
}
当对象中有指针成员存在时,我们必须重载拷贝构造函数,目的是将浅拷贝转化为深拷贝
为什么要进行拷贝转换?——因为当存在指针时,新建对象指针的初始值将是原有对象指针的值,即它他们指向同一地址,这样的话,当进行内存释放时,两个类都会释放同一片内存空间(即指针指向的空间),而无论是delete
还是free()
,在对同一片空间进行清理时都不能清理两次,否则会出现异常.
如何转换?很简单,重载一下转换构造函数,为新建的类指针分配空间即可,以动态矩阵类为例:
//matrix.h;
class matrix {
public :
matrix(int,int);
matrix(const matrix &);
private :
int row,col;
int **ma;
};
//matrix.cpp;
#include"matrix.h"
matrix::matrix(int row = 0,int col = 0):row(row),col(col) { //普通构造函数;
ma = new int *[row];
for(int i = 0; i < row; i++)
ma[i] = new int[col];
}
matrix::matrix(const matrix &a) { //手动重载拷贝构造函数,将浅拷贝转化为深拷贝;
row = a.row;
col = a.col;
ma = new int *[row];
for(int i = 0; i < row; i++) {
ma[i] = new int[col];
for(int j = 0; j < col; j++)
ma[i][j] = a.ma[i][j];
}
}
接下来介绍转换构造函数,所谓转换构造函数,即将其它类型转换为当前类型的构造函数,系统不提供模板,需要用户自己实现,大致的声明方式如下:
当前类::当前类(要转换的类型&);
如,将实数转换为复数的转换构造函数,注意转换构造函数只能有一个参数:
Complex::Complex(double &d) {
this->real = d; //实部为d;
this->imag = 0; //虚部为0;
}
析构函数(Destructor),是用来将对象销毁的一类函数,在对象的生命周期由编译系统自动调用,析构函数名就是类名前加上~
符号,没有返回值且不能有参数,因此析构函数不能重载
一般的建议是,对于没有指针,没有用new
或malloc()
分配空间的对象,可以不写析构函数,系统会自动生成析构函数做清理工作,而对于有指针,使用了new或alloc()的对象,必须手写析构函数
对于matrix类,析构函数写法如下:
//matrix.cpp
matrix::~matrix() { //需先在matrix.h中声明;
for(int i = 0; i < row; i++)
delete []ma[i];
delete []ma;
ma = nullptr;
}
而对于Complex类,无需写析构函数
注:C++中有先构造的对象后析构的规则,即构造顺序和析构顺序恰好相反
成员函数(Member Function),又称类的方法,是对类数据成员进行操作的主要途径,也是外部对类数据访问的重要接口.一般而言,方法的定义和实现都是用户自己决定的,这里只介绍一个常用的方法:类型转换函数
前面介绍的转换构造函数,是将其它类型转换为当前类型,现在介绍的类型转换函数,就是将当前类型转换为其它类型的一个方法,它声明的固定格式为:
operator 类型名() {
实现转换的函数;
return 类型名的对象/变量;
}
operator
关键字意为运算符,但这里代表将类型名重载,类型转换函数没有参数,因为它是将当前对象转换为其它对象,且作为当前对象的方法,能直接使用当前对象,不需要额外参数,也没有返回值可视作operator
后面的类型名就是其返回值,因此必须返回类型名的一个对象/变量,如将复数转换为实数的转换构造函数为:
//Complex.h;
class Complex {
public:
operator double();
private:
double real;
double imag;
};
//Complex.cpp;
Complex::operator double() {
return this->real;
}
除了类型转换函数是类的成员函数以外,还有许多有用的方法如: 类型运算符的重载、静态方法,将会在后面的小结中举例,在此就不再赘述.
前面说过,const
关键字用于声明常变量,该变量在其生命周期内值不可改变,如果我们希望一个对象的数据不可改变,则将其声明为常对象即可,如下:
const 类名 对象名(实参表); //实参表应该给出所有非静态成员的初值;
注:
常对象只能调用常成员函数
常成员函数只能访问常对象中的数据成员,但不允许修改其值
常成员函数的声明和实现都要有const
限定
可以用mutable
将常对象数据成员声明为可变的,此时可以用常成员函数来改变其值,但不建议这么做
class someclass {
public:
void changeCount() const; //该函数在someclass的常对象中可用来修改Count的值;
private:
mutable int Count; //常对象中可变的常数据成员;
}
常数据成员,和常变量一样,使用const
来声明常数据成员:
//Time.h;
class Time {
public:
Time(int,int,int);
private:
const int hour;
const int minute;
const int second;
};
//Time.cpp;
Time::Time(int a,int b,int c):hour(a),minute(b),second(c){
cout<<hour<<" "<<minute<<" "<<second<<endl;
}
注意: 常数据成员只能使用带参数列表的构造函数初始化,其余函数无法对其赋值,这就是说,构造函数有访问常数据成员的权限,析构函数也有该权限.
之前说过,可以将成员函数声明为常成员函数,注意其const
修饰符的位置:
//在Time.h声明中的public部分加上void show() const;的声明;
//Tame.cpp;
void Time::show() const { //常成员函数的实现;
cout<<hour<<" "<<minute<<" "<<second<<endl;
}
那么,使用常成员函数可以且仅可以访问常数据成员,如此麻烦的设定,常对象有什么作用呢?
答案就是——常对象通常作为函数参数使用,在很多情况下,我们不希望改变实参的值,只希望使用实参的某些值来达到一些目的,将函数参数声明为常对象,使用常成员函数取值,但不会改变实参的值,这样便保证了数据安全,并且,如果使用引用&
的话,还可以免去在函数中调用复制构造函数的时间开销,是一种很好的解决方案
#include
#include
using namespace std;
//Time.h;
struct Time { //注意struct默认的访问属性;
int hour;
int minute;
int second;
Time(int,int,int);
};
//Time.cpp;
Time::Time(int a,int b,int c):
hour(a),minute(b),second(c){}
//main.cpp;
void func(const Time &t) {
cout<<t.hour<<" "<<t.minute<<" "<<t.second<<endl;
//t.second = 1; 错误,试图改变常对象数据成员的值;
}
int main() {
Time t(5,10,22);
func(t);
return 0;
}
和其他所有类型的变量一样,对象也可以用指针指向,这类指针称为指向对象的指针.结合new
关键字,对指向对象的指针赋值就显得无比方便,以Complex类为例:
#include
#include
using namespace std;
//Complex.h;
class Complex { //Complex.h;
public:
Complex(double,double);
void show();
private:
double real;
double imag;
};
//Complex.cpp;
Complex::Complex(double r,double i):real(r),imag(i){} //Complex.cpp;
void Complex::show() {
cout<<real<<" "<<imag<<endl;
}
//main.cpp;
int main() {
Complex *c = new Complex(10,5);
c->show();
delete c; //释放空间也很重要;
c = nullptr;
return 0;
}
这显然是很简单的,除了以上方式调用成员函数以外,还可以:
(*c).show();
注:请务必使用delete
将通过new
动态建立的对象的指针释放
相信稍微有C语言基础的人都能理解,下面我们来看看指向数据成员和成员函数的指针:
指向数据成员的指针显然简单,就是一个简单的指针声明:
Complex c(10,5);
double *p = &c.real;
只要声明是指向哪个对象的数据成员即可,和C语言中普通指针使用方式并无差异
在看指向成员函数的指针之前,我们先看看指向普通函数的指针:
首先知道,函数名即函数首地址,指向函数的指针举例如下:
int max(int a,int b) {
return a > b ? a : b;
}
int (*p)(int,int); //p是指向返回值为int型,含两个int参数的函数的指针;
p = Max; //p指向max(int,int);
cout<<(*p)(10,15)<<endl; //调用max(int,int);
即可以总结出,指向函数的指针的声明方式:
返回类型名 (*指针名)(参数列表);
那么,将以上两者(指向数据成员的指针 && 指向普通函数的指针)的特点结合起来,不难得出,我们只需要声明指针指向哪个类的成员函数就好了,基本语法不变,如:
void (Complex::*p)(); //p是指向Complex类型的成员函数的指针,该函数返回值为void型且无参;
p = &Complex::show; //取Complex::show的首地址(之前说过,该函数在公共内存区域存放,被所有对象共享);
//因此不能使用具体的对象来找该函数,而是应该用类名::函数名的方式;
(c.*p)(); //调用c.show();
顾名思义,指向对象的常指针,即指针是常变量,被指向的对象是变量,指向常对象的指针,即指针是变量,被指向的对象是常量,其声明方式和例子如下:
//假设Complex类型已经声明并实现;
//main.cpp;
int main() {
Complex c1(10,5),c2(5,10);
const Complex c3(1,8);
Complex *const p1 = &c1; //指针是常变量;
const Complex *p2 = &c3; //对象是常变量;
//p1 = &c3; 不合法,指针不能指向常变量的指针;
//p1 = &c2; 不合法,指针指向不能再变化;
//*p2->real = 0; 不合法,对象是常对象,成员值不能变;
//p2 = &c1; 不合法,只能指向常对象;
return 0;
}
不难看出,这样用指针指向对象的方式,因为其操作繁琐,在面向对象程序设计中并没有实质上的优势体现出来,比如Java
就已经完全将指针封装起来,使用户不能使用,而C++
之所以没有作封装,是因为其完全兼容C
的设计思路,总体来说,指针指向对象并没有太大意义,除非在需要炫耀C++
编程技巧时,才能让你出出风头.
但还是应该注意this指针及其用法,虽然大多数都是系统自动调用,但还是有用户使用的地方,由于之前已经说过,这里就不再说this
指针了.
这里还是提一下对象数组,因为[]
是一个指针的隐式调用形式,然而我们只需知道,如何初始化对象数组和如何用指针的方式调用其成员即可,还是以Complex为例:
int main() {
Complex c[5] = {Complex(1,2),Complex(2,3), Complex(3,4),Complex(4,5),Complex(5,6)};
for(int i = 0; i < 5; i++)
c[i].show();
for(Complex *i = c; i < c+5; i++)
(*i).show();
return 0;
}
可见这和C语言中的数组初始化没有什么区别,无非就是使用构造函数初始化而已,如果有无参构造函数的话,可以不写构造函数的调用,如:
Complex c[5];
即可.
在某些使用情况下,我们希望一个类型中的一些数据被所有该类型的对象所共享,这就是对象间的数据共享.
在C中,我们通过将某些变量声明为全局变量来达到数据共享的目的,但是这不符合C++面向对象所提倡的封装性,因此在C++中,通过声明static
,即静态类型数据成员,可以将这些成员声明为共享类型,即这些成员不是被某个对象所有,而是被类所有,就像类方法一样,存放于公共数据区域.
如,声明一个Circle类,其公有数据为PI = 3.14,用于计算不同圆的面积和周长,如下:
#include
#include
using namespace std;
//circle.h;
class circle {
public:
circle();
circle(double);
double area();
double circumference();
private:
double radius;
static double PI;
};
//circle.cpp;
double circle::PI = 3.14; //为静态成员赋初值;
circle::circle() {
radius = 0;
}
circle::circle(double r):radius(r){}
double circle::area() {
return PI*radius*radius;
}
double circle::circumference() {
return 2*PI*radius;
}
//main.cpp;
int main() {
circle c(10);
cout<<c.area()<<" "<<c.circumference()<<endl;
return 0;
}
注意第18行的语句:
double circle::PI = 3.14; //为静态成员赋初值;
为static
变量赋初值的形式只能是:
类型名 类名::变量名 = 初值;
因为static
属于类不属于对象,const
是对象的成员,因此,static
只能在类外初始化,const
只能在构造对象时初始化,不能使用带参数列表的构造函数初始化static变量,和只能使用带参数列表的构造函数初始化const常变量刚好相反,请不要弄混.
就和const
数据成员和const
成员函数一样,有static
数据成员就有static
成员函数,其作用是对共享的static
数据成员进行操作,包括但不限于读/写操作,注意static
是变量,是可以改变值的,规则还是一样:static
函数只能对static
数据成员进行操作
若还是原来的circle类,我们将其PI初始化改写,并声明静态成员setPI():
//circle.cpp;
double circle::PI = 0;
void circle::setPI() {
PI = 3.14;
}
//main.cpp;
int main() {
circle c(10);
cout<<c.area()<<" "<<c.circumference()<<endl;
circle::setPI();
cout<<c.area()<<" "<<c.circumference()<<endl;
return 0;
}
注:
某些编译系统会将未初始化的static
变量赋初值0,但不保证所有的编译系统都会,保险起见,建议手动初始化
static
成员函数不属于对象,没有this
指针,可以通过对象名调用,但这不意味着它是该对象的函数
建议使用类型名::静态成员的方式调用静态数据成员/成员函数
由于没有this
指针一般不可使用static
成员函数访问非静态成员,但是可以将一个实例化的对象作为参数传入静态函数,达到访问的效果,如:
//circle.h;
static void setPI(circle); //重新声明setPI(circle);
//circle.cpp;
void circle::setPI(circle c) {
PI = 3.14;
cout<<c.area()<<" "<<c.circumference()<<endl;
}
非静态方法可访问静态数据成员,建议不要用静态方法访问非静态数据成员
在实际使用中,不仅同类之间的对象需要共享数据,有时甚至会有类外函数或其他类的函数需要使用当前类的数据或方法,此时我们可以将这些函数声明为友元函数,用关键字friend
修饰.
友元类的作用:声明为当前类友元的函数,可以无视访问属性,访问当前类所有数据.
如,我们可以将Complex类中的show()函数声明为友元函数,注意声明格式:
friend 返回类型 函数名(类型&);
//Complex.h;
class Complex {
public:
Complex(double,double);
friend void show(Complex&);
private:
double real;
double imag;
};
void show(Complex &c) {
if(c.imag >= 0)
cout<<c.real<<"+"<<c.imag<<"i"<<endl;
else
cout<<c.real<<c.imag<<"i"<<endl;
}
//main.cpp;
int main() {
Complex c(10,5);
show(c);
}
注意show()是普通函数,不属于任何类,且show()访问了Complex的private成员
现在有这样的一个需求:实现一个Time类和Date类,要求在Date类中有display(Time&)方法,能够输出两个类的信息,实现如下:
//Date.h;
class Time; //提前引用声明;
class Date {
public:
Date(int,int,int);
void display(Time&);
public:
int year;
int month;
int day;
};
//Time.h;
class Time {
public:
Time(int,int,int);
friend void Date::display(Time&);
private:
int hour;
int minute;
int second;
};
//Date.cpp;
Date::Date(int m,int d,int y):month(m),day(d),year(y){}
void Date::display(Time &t) {
cout<<t.hour<<":"<<t.minute<<":"<<t.second<<" ";
cout<<month<<","<<day<<","<<year<<endl;
}
//Time.cpp;
Time::Time(int h,int m,int s):hour(h),minute(m),second(s){}
//main.cpp;
int main() {
Time t(10,40,29);
Date d(12,16,2018);
d.display(t);
return 0;
}
注意:在Date中用到Time,而Time此时未定义,且Time中用到Date,此时不管将谁的头文件包含在对方的声明中都会引起歧义,发生错误.解决方式就是——在任一类中使用类的提前引用声明即可,即,声明当前使用的类,该类在之后定义,有些类似C中的函数先声明再定义.
之后便没有什么需要注意的地方了,只要搞清楚,友元函数是哪个类的成员,在非成员的类中如何声明即可,声明格式如下:
friend 返回类型 类型名::函数名(当前类型&); //声明friend的类;
返回类型 函数名(其它类型&); //friend的目标类;
不仅可以将一个函数声明为友元,还可以将一个类声明为另一个类的友元,这样的类称为友元类,关于友元类,应注意几点:
class 本类名 {
private:
friend 另一类名; //此处是public或private无所谓;
}
举一个简单例子:
//Date.h;
class Time; //提前引用声明;
class Date {
public:
Date(int,int,int);
void display(Time&);
public:
int year;
int month;
int day;
};
//Time.h;
class Time {
public:
Time(int,int,int);
private:
friend Date; //声明Date是Time的友元类;
int hour;
int minute;
int second;
};
//Date.cpp;
void Date::display(Time &t) {
cout<<t.hour<<":"<<t.minute<<":"<<t.second<<" ";
cout<<month<<","<<day<<","<<year<<endl;
}
请注意对比上面几个程序的区别.
前面讲过模板函数,现在说一说模板类:
和模板函数一样,使用templete
修饰的类就是模板类,参用简单替换原则,将虚拟类型替换为实际类型,声明如下:
//Complex.h;
template <class T>
class Complex {
public:
Complex(T r,T i):real(r),imag(i){}
void show() { //实现show()方法;
if(imag >= 0)
cout<<real<<"+"<<imag<<"i"<<endl;
else
cout<<real<<imag<<"i"<<endl;
}
private:
T real;
T imag;
};
注意Complex的方法已经在声明是实现,我们不建议这么做,但是若要在类外实现模板类,需要每一个方法都加上template
限定,如:
//Complex.h;
template <class T>
class Complex {
public:
Complex(T,T);
void show();
private:
T real;
T imag;
};
//Complex.cpp;
template<class T>
Complex<T>::Complex(T r,T i):real(r),imag(i){}
template<class T>
void Complex<T>::show() { //实现show()方法;
if(imag >= 0)
cout<<real<<"+"<<imag<<"i"<<endl;
else
cout<<real<<imag<<"i"<<endl;
}
在使用模板类实例化对象时,应该加上实际类型,如下:
Complex<double> c(10,19);
可以不止一个虚拟类型作为模板声明,其形式为:
template <class T1,class T2,class T3,...>
class someclass {
...
}
实例化对象时确保类型个数相符就可以:
someclass s<int,double,char,...> sc(参数表);
以上就是面向对象基础的全部内容