类的基本思想是数据抽象和封装, 数据抽象是一种把接口和实现分离的编程技术。类的接口包括用户所能够执行的操作,类的实现包括类的数据成员、负责接口实现的函数体和各种私有函数。
封装实现了类的接口和实现的分离。封装隐藏了类的实现,封装过后,用户只能访问类的接口,而不能访问类的实现。
类要想要实现数据抽象和封装,需要首先定义一个抽象数据类型。在抽象数据类型中,由类的设计者负责考虑类的实现过程;使用该类的程序员则只需要抽象地思考类型做了什么,而无需了解细节。
《C++ primer 》这本书上的源码,类Sales_data.h实现
#include
#include
using namespace std;
class Sales_data
{
public:
Sales_data();//声明一个无参的构造函数
Sales_data(string b, int u, double p) :bookno(b), units_sold(u), price(p){}//声明一个有参数的默认构造函数,用参数的初始化表对数据成员初始化
friend istream& operator >> (istream &, Sales_data &);//运算符>>重载为友元函数
friend ostream& operator << (ostream &, Sales_data &);//运算符<<重载为友元函数
friend Sales_data operator + (Sales_data & lhs, Sales_data & rhs);//声明友元的运算符重载 + 函数
Sales_data& operator = (const Sales_data &);//重载赋值运算符
friend bool operator == (Sales_data &, Sales_data &);//声明有元的重载双目运算符==函数
Sales_data &operator += (const Sales_data &);//声明一个返回sales_item类型的重载运算符+=函数,形参采用常量引用
double avg_price();
string isbn() const;//声明isbn函数,并返回书编号
Sales_data& combine(const Sales_data &);//一个combine成员函数,用于将一个Sales_data对象加到另一个对象上
//声明Sales_data类的非成员接口函数
friend istream &read(istream&, Sales_data&);//声明一个read函数,将数据从istream读入到Sales_data对象中,函数返回类型为istream &
friend ostream &print(ostream&, const Sales_data&);//声明一个print函数,函数返回类型为ostream &
friend Sales_data add(const Sales_data &, const Sales_data &);//一个add的函数,执行两个Sales_data对象的加法
private:
string bookno;//书号
double units_sold;//销售出的册数
double price;//单本售价
double revenue;//总销售额
double average;
};
Sales_data::Sales_data()//定义无参数的构造函数
{
bookno = "null";
units_sold = 0;
price = 0.0;
}
istream& operator >>(istream &input, Sales_data &s)//对重载运算符>>进行定义
{
input >> s.bookno >> s.units_sold >> s.price;
if (input)
{
s.revenue = s.units_sold * s.price;
}
return input;
}
ostream& operator << (ostream &output, Sales_data &s)//对重载运算符<<进行定义
{
//output << s.bookno << " "<< s.units_sold << " " << s.revenue << " " << s.price << endl;
output << s.bookno << " " << s.units_sold << " " << s.revenue << " " << s.avg_price() << endl;
return output;
}
//将两个sales_item对象相加时,程序应该检测其两个对象的isbn书号是否相同
Sales_data operator + (Sales_data & lhs, Sales_data & rhs)//定义重载运算符+函数,lhs和rhs是sales_item的对象
{
Sales_data ret;
ret.bookno = lhs.bookno;
ret.units_sold = lhs.units_sold + rhs.units_sold;
ret.revenue = lhs.revenue + rhs.revenue;
ret.avg_price();
return ret;
}
bool operator == (Sales_data &lhs, Sales_data &rhs)
{
return lhs.units_sold == rhs.units_sold && lhs.price == rhs.price && lhs.isbn() == rhs.isbn();
}
Sales_data& Sales_data:: operator = (const Sales_data &lhs)//重载赋值运算符=
{
bookno = lhs.bookno;
units_sold = lhs.units_sold;
price = lhs.price;
return *this;
}
Sales_data& Sales_data ::operator += (const Sales_data &rhs)//
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;//将this对象作为左值返回,*this相当于一个sales_item对象
}
//定义一个avg_price()常量成员函数
double Sales_data::avg_price()
{
average = revenue / units_sold;
return average;
}
//定义一个isbn()常量成员函数
string Sales_data::isbn() const
{
return bookno;
}
//一个combine成员函数,用于将一个Sales_data对象加到另一个对象上
Sales_data& Sales_data::combine(const Sales_data &rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
//友元函数read()的具体实现,将数据从istream读入到Sales_data对象中
istream &read(istream &input, Sales_data &rhs)
{
input >> rhs.bookno >> rhs.units_sold >> rhs.price;
rhs.revenue = rhs.units_sold * rhs.price;
return input;
}
//友元函数add()的具体实现,执行两个Sales_data对象的加法
Sales_data add(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs;
sum.combine(rhs);
sum.avg_price();
return sum;
}
通常在class定义时要用到类型变量自身时,因为这个时候还不知道变量名,就用this这样的指针来使用变量名。
#include
using namespace::std;
class Date
{
public:
void Display()
{
cout << _year << endl;
}
void setDate(int year)
{
_year = year;
}
private:
int _year = 2000;
};
int main()
{
Date firstDate, secondDate;
firstDate.setDate(2018);
secondDate.setDate(2019);
firstDate.Display();
secondDate.Display();
return 0;
}
当我们对Date这个类实例化出很多对象,这些不仅仅只限于firstDate和secondDate,但是对象内部包含的成员函数和对象是一样的,当调用函数setDate对_year的数值进行更改,但是并没有像普通函数一样传递形参或者形参的地址,但是已得到了结果:
结果表明:我们并没有传递对象或指针,但是达到了我们预期想要的结果。
原因在于this指针,每个对象都拥有一个隐式this指针,通过this指针来访问自己的地址;每个成员函数都有一个隐式this指针形参》(类构造函数没有隐含的this指针形参),是编译器自己处理的,我们不能显式的添加this指针参数定义,也不能在调用时显式传递对象的地址给this指针。
注意:全局变量、静态变量么有this指针。this指针在成员函数开始执行前构造,在成员函数执行结束后清除。
每个类都分别定义它的对象被初始化,类通过一个或者几个特殊的成员函数来控制其对象的初始化过程,这些函数被称为构造函数。类可以包含多个构造函数 ,不同的构造函数之间必须在参数数量或参数类型上有所区别。构造函数不能被声明成const对象。
#include
#include
using namespace std;
class Sales_data{
public:
//两种重载构造函数
Sales_data(){
cout<<"调用无参数构造函数!!!"<
上述代码的执行结果:
如果一个构造函数的第一个参数时自身类类型的引用,且任何额外参数都有默认值,则次函数为拷贝构造函数。
class Foo{
public:
Foo(); //默认构造函数
Foo(const Foo&);//拷贝构造函数
...
}
注意:拷贝函数的第一个参数必须是一个引用类型。
合成拷贝构造函数: 如果我们没有为一个类定义拷贝构造函数,则编译器会为我们定义一个。
对于某些类而言,合成拷贝构造函数用来阻止我们拷贝该类类型的对象,而一般情况,合成的拷贝构造函数会将其参数的成员逐个拷贝到正在创建的对象中,编译器从给定对象中依次将每个非static成员拷贝到正在创建的对象中。
拷贝初始化
string dots(10, '.'); //直接初始化
string s(dots); //直接初始化
string s2 = dots; //拷贝初始化
string null_book = "9-999-8999"; //拷贝初始化
string nines = string(100, '9'); //拷贝初始化
除了以上使用=初始化变量时拷贝构造函数,在下列情况下也会发生:
参数和返回值
在函数调用的过程中,具有非引用类型的参数要进行拷贝初始化。当一个函数具有非引用的返回类型时,返回值会被用来初始化调用方的结果。
拷贝构造函数第一个参数必须是引用原因:由于拷贝构造函数被用来初始化非引用类类型的参数。如果其自身参数不是引用类型,则调用永远也不会成功——为了调用拷贝构造函数,我们必须拷贝它的实参,但为了拷贝实参,我们又必须调用拷贝构造函数,如此无限循环。
与类控制其对象如何初始化一样,类也可以控制其对象如何赋值
Sales_data trans,accum;
trans = accum;
重载赋值运算符
重载赋值运算符本质上是函数,其名字由operator关键字后接表示要定义的运算符的符号组成。因此,赋值运算符就是一个名为operator=的函数。类似于任何其他函数,运算符函数也有一个返回类型和一个参数列表。
class Foo{
public:
Foo& operator=(const Foo&);
//...
};
值得注意的是,标准库通常要求保存在容器中的类型要具有赋值运算符,通常应该返回一个指向其左侧运算对象的引用。
合成拷贝赋值运算符
与处理拷贝构造函数一样,如果一个类未定义自己的拷贝赋值运算符,编译器会为他它生成一个合成拷贝赋值运算符。类似拷贝构造函数,对于某些类,合成拷贝构造运算符用来禁止该类型对象的赋值。
Sales_data& Sales_data::operator=(const Sales_data &rhs)
{
bookNo = rhs.bookNo;
units_sold = rhs.units_sold;
revenue = rhs.revenue;
return *this;
}
合成拷贝赋值运算符会将右侧运算对象的每个非static成员赋予左侧运算对象的对应成员,这一工作是通过成员类型的拷贝赋值运算符来完成的,对于数组的成员,逐个赋值数组元素。合成拷贝赋值运算符返回一个指向其左侧运算对象的引用。
析构函数是类的一个成员函数,它的作用是释放对象使用资源,并销毁对象的非static数据成员。
class Foo{
public:
~Foo();//析构函数
//...
};
析构函数不接受参数,所以它不能被重载。对于一个给定类,只会有唯一一个析构函数。
下面完整的演示析构函数的执行过程。
#include
using namespace std;
class Foo{
public:
Foo(){
cout << "调用无参构造函数!!!" << endl;
}
~Foo(){
cout << "调用析构函数!!!" << endl;
}
private:
int pi;
};
void test(){
Foo p1; //创建对象
//在结束时,由系统自动调用析构函数释放对象
cout << "调用test()函数!!!" << endl;
}
int main()
{
test();
return 0;
}
执行结果:
在什么情况下调用析构函数,总结如下:
无论何时调用一个对象被销毁,就会自动调用其析构函数。
由于析构函数自动运行,我们的程序可以按需要分配资源,而(通常)无须担心何时释放这些资源。
注意:当指向一个对象的引用或者指针离开作用域时,析构函数不会执行。