C++ 类 笔记
一、类
类基本思想是抽象和封装。类的实现,包含三个部分:
- 类的数据成员
- 负责接口实现的函数体
- 定义类所需的各种私有函数
需要注意的是:成员函数的声明必须在类的内部,它的定义可以在类的内部,也可以在类的外部。
下面举个类的例子:
Sales_data total; //定义total为Sales_data类
struct Sales_data
{
//声明成员函数:关于Sales_data对象的操作
std::string isbn() const { return bookNo};
Sales_data& combine(const Sales_data&);
double avg_price() const;
//数据成员
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
//在类的外部定义成员函数
double Sales_data::avg_price() const
{
if (units_sold)
return revenue/units_sold;
else
return 0;
}
Sales_data& Sales_data::combine(const Salse_data &rhs)
{
units_sold += rhs.units_sold; //把rhs的成员加到this对象的成员上
revenue += rhs.revenue;
return *this;
}
详细说明下列几点:
-
有一个this的概念
成员函数中,有一个isbn(), 它返回的是bookNo,实际上它是隐式地返回total.bookNo。成员函数通过一个名为this的额外隐式参数来访问调用它的那个对象, this是一个常量指针,不允许改变this中保存的地址。它也可以写成 return this->bookNo; ,但是没有必要。 -
类作用域和成员函数
值得注意的是,即使bookNo定义在isbn()函数之后,isbn也还是能够使用这个变量。 编译器分两步处理类:首先编译成员的声明,然后才轮到成员函数体。因此,成员函数体可以随意使用类中的其他成员而无须在意这些成员出现的次序。 -
在类的外部定义成员函数
在类外部定义成员函数的时候,返回类型,参数列表和函数名都必须和类里面声明的一致,且类外部定义的成员的名字必须包含它的所属类名。 -
定义一个返回this对象的函数
combine函数返回了*this,也就是解引用this指针以获得执行该函数的对象,也就是返回total的引用。
二、构造函数
每个类都分别定义了它的对象被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。 所以构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。
构造函数的名字和类名相同。构造函数没有返回类型,也不能被声明成const。 一个类可以包含多个构造函数,但不同构造函数之间,参数数量或者参数类型必须不一样。
上面语句
Sales_data total
没有提供初始化,但是类通过一个特殊的构造函数来控制默认初始化过程,这个函数就是默认构造函数。 默认构造函数不需要任何实参。编译器创建的构造函数又被称为合成的默认构造函数。
虽然上面那个例子可以使用,但是值得注意的是,某些类不能依赖于默认构造函数来进行初始化,理由是:
- 编译器只有在发现类不包含任何构造函数的情况下才会替我们生成一个默认的构造函数。
- 合成的默认构造函数可能执行错误的操作。
- 编译器不能为某些类合成默认的构造函数。
现在基于上面那个例子,增加构造函数:
Sales_data total; //定义total为Sales_data类
struct Sales_data
{
//新增的构造函数
Sales_data() = default;
Sales_data(const std::string &s): bookNo(s) {}
Sales_data(const std::string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p*n) {}
Sales_data(std::istream &);
//声明成员函数:关于Sales_data对象的操作
std::string isbn() const { return bookNo};
Sales_data& combine(const Sales_data&);
double avg_price() const;
//数据成员
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
//在类的外部定义成员函数
double Sales_data::avg_price() const
{
if (units_sold)
return revenue/units_sold;
else
return 0;
}
Sales_data& Sales_data::combine(const Salse_data &rhs)
{
units_sold += rhs.units_sold; //把rhs的成员加到this对象的成员上
revenue += rhs.revenue;
return *this;
}
//在类的外部定义构造函数
Sales_data::Sales_data(std::istream &is)
{
read(is, *this)//read的作用是从is中读取一条交易信息然后存入this对象中
}
详细说明下列几点:
-
=default 的含义
该构造函数不接受任何实参,所以它是一个默认的构造函数。 我们定义这个构造函数的目的仅仅是因为我们既需要其他形式的构造函数,也需要默认的构造函数。我们希望这个函数的作用完全等同于之前使用的合成默认构造函数。 -
构造函数初始值列表
Sales_data(const std::string &s): bookNo(s) {}
这个只是初始花了bookNo成员,其他数据成员的初始化与合成默认构造函数相同的方式隐式初始化。
Sales_data(const std::string &s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p*n) {}
这个构造函数初始化所有的数据成员。
可以看到,上面两个构造函数中函数体都是空的。这是因为这些构造函数的唯一目的就是为了给数据成员赋初始值,一旦没有其他任务需要执行,函数体也就为空。 -
在类的外部定义构造函数
在类的外部定义构造函数,可以看到Sales_data::Sales_data, 就是定义了Sales_data类的成员,然后名字和类的名字一样。所以是构造函数。
三、访问控制和封装
到目前为止,上面的例子没有进行封装。所以引入了访问说明符加强类的封装性。主要是两个关键字:public和private。public说明符之后的成员在这个程序内可以被访问,private只有类的成员可以访问。
具体例子为:
Sales_data total; //定义total为Sales_data类
class Sales_data
{
public:
//新增的构造函数
Sales_data() = default;
Sales_data(const std::string &s): bookNo(s) {}
Sales_data(const std::string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p*n) {}
Sales_data(std::istream &);
//声明成员函数:关于Sales_data对象的操作
std::string isbn() const { return bookNo};
Sales_data& combine(const Sales_data&);
double avg_price() const;
private:
//数据成员
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
//在类的外部定义成员函数
double Sales_data::avg_price() const
{
if (units_sold)
return revenue/units_sold;
else
return 0;
}
Sales_data& Sales_data::combine(const Salse_data &rhs)
{
units_sold += rhs.units_sold; //把rhs的成员加到this对象的成员上
revenue += rhs.revenue;
return *this;
}
//在类的外部定义构造函数
Sales_data::Sales_data(std::istream &is)
{
read(is, *this)//read的作用是从is中读取一条交易信息然后存入this对象中
}
详细说明下列几点:
- 一个类可以包含0个或者多个访问说明符。
- 上面使用class替换了struct。这两者唯一的一点区别就是,struct和class的默认访问权限不太一样。 换句话说,类可以在它的第一个访问说明符之前定义成员,对这种成员的访问权限依赖于类的定义方式。如果我们使用struct关键字,则定义在第一个访问说明符之前的成员是public的。反之,如果我们使用了class关键字,则这些成员是private的。 当我们希望定义的类的所有成员是public的,使用struct。如果有任何的成员是private的,使用class。
封装的好处是,类的作者可以比较*的修改数据,只要接口不变,用户代码就不用改变。其次,封装能防止由于用户的原因造成数据被破坏。
四、友元
如果有数据成员是private的,有一些函数就无法编译通过。类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数称为它的友元。 只需要添加friend关键字即可。
具体的例子:
Sales_data total; //定义total为Sales_data类
class Sales_data
{
//为Sales_data的非成员函数所做的友元声明
friend Sales_data add(const Sales_data&, const Sales_data&);
friend std::istream &read(std::istream&, Sales_data&);
friend std::ostream &print(std::ostream&, const Sales_data&);
public:
//新增的构造函数
Sales_data() = default;
Sales_data(const std::string &s): bookNo(s) {}
Sales_data(const std::string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p*n) {}
Sales_data(std::istream &);
//声明成员函数:关于Sales_data对象的操作
std::string isbn() const { return bookNo};
Sales_data& combine(const Sales_data&);
double avg_price() const;
private:
//数据成员
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
//在类的外部定义成员函数
double Sales_data::avg_price() const
{
if (units_sold)
return revenue/units_sold;
else
return 0;
}
Sales_data& Sales_data::combine(const Salse_data &rhs)
{
units_sold += rhs.units_sold; //把rhs的成员加到this对象的成员上
revenue += rhs.revenue;
return *this;
}
//在类的外部定义构造函数
Sales_data::Sales_data(std::istream &is)
{
read(is, *this)//read的作用是从is中读取一条交易信息然后存入this对象中
}
//Sales_data接口的非成员组成部分的声明
Sales_data add(const Sales_data&, const Sales_data&);
std::istream &read(std::istream&, Sales_data&);
std::ostream &print(std::ostream&, const Sales_data&);
一般来说,最好在类定义开始或者结束前的位置集中声明友元。