类的基本思想是是数据抽象和封装。数据抽象是一种依赖于接口(interface)和实现(implementation)分离的编程(以及设计)技术。类的接口包括用户所能执行的操作;类的实现则包括类的数据成员,负责接口实现的函数体以及定义类所需的各种私有函数。
封装实现了类的接口和实现的分离,封装后的类隐藏了它的实现细节,也就是说,类的用户只能使用接口而无法访问实现细节。
内联函数
我一直对内联函数的概念不是很清楚,趁这个机会梳理一下。内联函数可以避免函数调用的开销。将函数指定为内联函数,通常就是将它在每个调用点上“内联地”展开。假设我们把shorterString函数定义为内联函数,,则如下调用:
cout<<shortString(s1,s2)<<endl;
在编译过程中展开成类似于下面的形式:
cout<<(s1.size()<s2.size() ?s1 :s2);
在shortString函数的返回类型前面加上关键字inline,这样就可以将它声明为内联函数了:
inline const string &shortString(const string &s1,const string &s2){
return s1.size()<s2.size() ?s1 :s2;
}
创建类
首先我们来创建一个书店程序的类:
struct Sales_data{
string isbn() const{return bookNo;}
Sale_datas& combine(const Sales_data&);
double avg_price() const;
string bookNo;
unsigned units_sold=0;
double revenue=0.0;
}
//Sales_data的非成员函数接口
Sales_data add(const Sale_data&,const Sales_data&);
ostream &print(ostream&, const Sales_data&);
istream &read(istream&,Sales_data);
在类的外部定义成员函数
我们可以看到,当我们在创建类的时候,对combine()函数和avg_price()函数只是做了声明,它们的声明在类Sale_datas的作用域内。我们可以在类的外部定义成员函数时,成员函数的定义必须与它的声明匹配,也就是说,返回类型,参数列表和函数名都得与类内部的声明保持一致。如果成员被声明成常量成员函数,那么它的定义也必须在参数列表后明确指定const属性。同时,类外部定义的成员的名字必须包含它所属的类名:
double Sales_data::avg_price() const {
if(units_sold)
return revenue/units_sold;
else
return 0;
}
定义一个返回this对象的函数
Sales_datas Sales_data::combine(const Sales_data &rhs){
units_sold +=rhs.units_sold;
revenue +=rhs.revenue;
return *this;
}
先看形参变量:const Sales_data &rhs,这是一个对常量的引用,它的意思就是我们不能改变引用对象的值,比如说,我们调用这个函数
total.combine(trans);
我们不能改变trans成员变量的值。在这个调用中,total的地址被绑定到隐式的this参数上,而rhs绑定到了trans上。return语句解引用this指针以获得执行该函数的对象。
定义类相关的非成员函数
类的作者常常需要定义一些辅助函数,比如我们上面定义的add,read,print等等。尽管这些函数定义的操作从概念上来说属于类的接口的组成部分,但它们实际上并不属于类本身。
一般来说,如果非成员函数是类接口的组成部分,则这些函数的声明应该与类写在同一个头文件内。
构造函数
每个类都分别定义了它的对象被初始化的方式,类通过一个或者几个特殊的成员函数来控制其对象的初始化过程,这些函数叫构造函数(constructor)。构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。
首先构造函数是没有返回类型的,其次构造函数也有一个(可能为空的)参数列表和一个(可能为空的)函数体,最后类可以包含多个构造函数。但是,构造函数不能被声明称const的。但是我们创建类的一个const对象时(对象就是具体的实例),直至构造函数完成初始化的过程,对象才能真正取得其“常量”属性。因此,构造函数在const对象的构造过程中可以向其写值。
当类没有声明任何构造函数时,编译器会自动地生成默认构造函数,这个默认构造函数又称为合成的默认构造函数,它将按照以下规则初始化类的数据成员:
1.如果存在类内的初始值,,用它来初始化成员
2.否则,默认初始化该成员,根据C++的成员初始化规则。
下面我们在类内新增三个构造函数:
Sales_data()=default;
Sales_data(const string &s):bookNo(s){}
Sales_data(const string &s,unsigned n,double p):bookNo(s),units_sold(n),revenue(p*n);
可以看到在第一个构造函数之中,它不接受任何实参,所以它是一个默认构造函数。我们希望当我们需要用到合成默认构造函数的时候,这个默认构造函数可以取得一样的效果。
下面的两个构造函数使用构造函数初始值列表来初始化类的每个成员。当然,在类的外部也可以定义构造函数,与其他几个构造函数不同,以istream为参数的构造函数需要执行一些实际的操作,在它的函数体内,调用了read函数来给数据数据成员赋值:
Sales_datas::Sales_data(istream &is){
read(is,*this);
}
和其他成员函数一样,我们在类的外部定义构造函数时,必须指明该构造函数是哪个类的成员。
友元
下面,我们给我们的类添加访问说明符:private(封装)和public,以及添加友元关键字
struct Sales_data{
friend Sales_data add(const Sale_data&,const Sales_data&);
friend ostream &print(ostream&, const Sales_data&);
friend istream &read(istream&,Sales_data);
public:
Sales_data()=default;
Sales_data(const string &s):bookNo(s){}
Sales_data(const string &s,unsigned n,double p):bookNo(s),units_sold(n),revenue(p*n);
string isbn() const{return bookNo;}
Sale_datas& combine(const Sales_data&);
double avg_price() const;
privete:
string bookNo;
unsigned units_sold=0;
double revenue=0.0;
};
//Sales_data接口的非成员组成部分声明
Sales_data add(const Sale_data&,const Sales_data&);
ostream &print(ostream&, const Sales_data&);
istream &read(istream&,Sales_data);
当没有friend关键字的时候,我们可以看到Sales_data的数据成员是private的,我们的read,print和add函数是无法被正常编译的,这是因为尽管这几个函数是类的接口的一部分,但它们不是类的成员。类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数称为它的友元(friend)。
友元声明只能出现在类的内部,但是在类内部出现的具体位置不限。但是友元不是类的成员函数,所以其不受所在区域访问控制级别的约束。友元的声明仅仅指定了访问权限,而非一个通常意义上的函数声明。如果我们希望用户可以调用某个友元声明,那么我们就必须在友元声明之外再专门对函数进行一次声明。
类的静态成员
有时候类需要它的一些成员与类本身直接相关,而不是与类的各个对象保持关联。假如,有一个银行账户类可能需要一个数据成员来表示当前的基准利率。在此例中,我们希望利率与类关联,而非与类的每个对象关联。
下面我们新建一个类,我们通过在成员的声明之前加上关键字static使得其与类关联在一起,和其他成员一样,静态成员可以是public的或是private的。静态数据成员可以是常量,引用,指针,类类型:
Class Account{
public:
void caclulate(){amount+=amount+interestRate ;}
static double rate() {return interestRate;}
static void rate(double);
private:
string owner;
double amount;
static double interestRate;
static double initRate();
}
根据静态成员的性质,静态成员函数不与任何对象绑定在一起,它们不包含this指针。作为这个结果,静态成员函数不能声明成const的,而且我们也不能在static函数体内使用this指针。
下面我们去定义一个静态成员,和其他成员函数一样,我们既可以在类的内部定义也可以在类的外部去定义静态成员函数。当在类的外部定义静态成员是,不能重复static关键字,该关键字只出现在类内部的声明语句:
void Account::rate(double newRate){
interestRate=newRate;
}
因为静态数据成员不属于类的任何一个对象,所以它们并不是在创建类的对象时被定义的,这就意味着它们不是由类的构造函数初始化的。而且一般来说,我们不会在在类的内部初始化静态成员。相反的,必须在类的外部定义和初始化每个静态成员:意思就是说我们不能在类创建对象的时候才初始化静态成员,必须在定义类的时候在外部定义和初始化每个静态成员。而且每个静态成员只能被定义一次。