作者简介:努力的clz ,一个努力编程的菜鸟
文章专栏:C++ Primer 学习笔记
专栏简介: 本专栏是博主学习 C++ Primer 的学习笔记,因为这本书内容超级多,所以博主将其中的 重点 概括提炼出来,于是有了这个专栏的诞生。
C++ Primer 学习笔记 第7章 类
笔记导航
- C++ Primer 第7章 类 - 上
⇦当前位置
- C++ Primer 第7章 类 - 中
(加班中)
- C++ Primer 第7章 类 - 下
(加班中)
- C++ Primer 总目录 传送门
如果本篇文章对大家起到帮助的话,跪求各位帅哥美女们,
求赞 、求收藏 、求关注!
关于
类的思想与定义
会专门出一期博文来好好聊一聊。
对于初学者来说,这个很重要,要好好理解类的基本思想:数据抽象、封装
。
本节将围绕
Sales_data
这个例子,展开介绍类的入门知识。直接通过代码去理解书中繁琐的文字描述,节约读者学习时间。
跳过,直接看
改进的 Sales_data 类
#include
#include
using std::istream;
using std::ostream;
using std::string;
// todo 销售数据类
struct Sales_data
{
string bookNo; // 书号
unsigned units_sold = 0; // 销量
double revenue = 0.0; // 总销售收入
/**
* @brief 返回书的编号
*
* @return string
*/
string isbn() const
{
return this->bookNo;
}
/**
* @brief 合并书的销售数据
*
* @return Sales_data&
*/
Sales_data &combine(const Sales_data &);
/**
* @brief 返回售出书籍的平均价格
*
* @return double
*/
double avg_price() const;
};
// Sales_data的非成员接口函数
Sales_data add(const Sales_data &, const Sales_data &);
ostream &print(ostream &, const Sales_data &);
istream &read(istream &, Sales_data &);
1. 定义成员函数
类的成员 (变量、函数) 都必须定义在类里面,但成员函数体可以在类里面,也可以在类外面;
2. 引入 this
this
表示 “这个”,也就是所在位置的类对象,成员函数通过this
隐式参数来访问调用它的对象。
this
是隐式定义的。我们只需要知道不可以把 变量或函数参数 命名为this
即可。
this
是一个常量指针,不能改变this
中保持的地址。
3. 引入 const 成员函数
如下图
isbn()
,在参数列表后添加const 关键字
;这类成员函数称为 常量成员函数 ,表明其不被允许修改类的数据成员。
换句话来说,常量成员函数 可以读取变量的值,不能修改变量的值。
4. 类作用域和成员函数
类本身就是一个作用域,成员函数可以随意使用类中的成员,不管它在类中的哪个位置定义(函数前、后都可以);
编译器分两步处理类:
- 编译成员的声明;
- 编译成员函数体。
5. 在类的外部定义成员函数
类外定义成员函数需要注意的点:
- 返回类型、参数列表和函数名要和类内的声明一致;
- 若成员被声明为常量成员函数,类外定义也得加上
const
;
注意看:
Sales_data::avg_price()
表示告诉编译器avg_price()函数
被声明在类Sales_data
的作用域内。
/**
* @brief 返回售出书籍的平均价格
*
* @return double
*/
double Sales_data::avg_price() const
{
if (units_sold)
{
return revenue / units_sold; // ! avg_price 使用revenue、units_sold时,实际是隐式地使用 Sales_data 的成员变量。
}
else
{
return 0;
}
}
6. 定义一个返回 this 对象的函数
combine 函数
必须返回引用类型,也就是Sales_data&
;
return 语句
解引用this 指针
以获得执行该函数的对象。
/**
* @brief 合并书的销售数据
*
* @param rhs
* @return Sales_data&
*/
Sales_data &Sales_data::combine(const Sales_data &rhs)
{
units_sold += rhs.units_sold; // 把rhs的成员加到this对象的成员上
revenue += rhs.revenue;
return *this; // 返回调用该函数的对象
}
实现
read、print 函数
,需要注意两点:
read、print
分别接受一个各自 IO类型的引用作为参数。因为IO类不能被拷贝,只能通过引用来传递它们。
(“ 拷贝” 现阶段先理解为时用它来给其它变量赋值)print 函数
不负责换行。这个是编码规范(可以不遵守),执行输出任务的函数要求尽可能减少对格式的控制。
/**
* @brief 从给定流中将数据读到给定得对象里
*
* @param is
* @param item
* @return istream&
*/
istream &read(istream &is, Sales_data &item)
{
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price * item.units_sold;
return is;
}
/**
* @brief 负责将给定对象的内容打印到给定的流中
*
* @param os
* @param item
* @return ostream&
*/
ostream &print(ostream &os, const Sales_data &item)
{
os << item.isbn() << " " << item.units_sold << " "
<< item.revenue << " " << item.avg_price();
return os;
}
定义 add 函数
add() 函数
,求两个Sales_data对象的和。
/**
* @brief 求两个Sales_data对象的和
*
* @param lhs Sales_data对象
* @param rhs Sales_data对象
* @return Sales_data
*/
Sales_data add(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs; // 把lhs的数据成员拷贝给sum
sum.combine(rhs); // 把rhs的数据成员加到sum当中
return sum;
}
构造函数
负责初始化类对象的数据成员,具有如下几个特点:
构造函数
的名字和类相同;构造函数
没有返回类型;构造函数
参数列表、函数体可以有也可以为空;构造函数
不能被声明为 const 。
需要注意的是,
构造函数
在对const 对象
进行初始化时,是可以修改值的。只有完成构造初始化,对象才真正具备 常量 属性。
1.合成的默认构造函数
到目前为止,我们还未给
Sales_data 类
添加构造函数,但当我们声明一个Sales_data对象:Sales_data sd
时,仍会完成初始化工作。
编译器会创建一个 默认构造函数 ,去完成如下工作:
- 若存在类内的初始值,用它来初始化成员;
- 否则,默认初始化该成员。
例如下方代码:
revenue
被赋值为 0.0;bookNo
默认初始化为空字符串;
2. 某些类不能依赖于合成的默认构造函数
合成的默认构造函数 仅适用于简单类 (变量成员是基本数据类型,不包含复合类型或类类型),建议用户自己编写默认构造函数,原因:
- 只有当类没有什么任何构造函数时,编译器才会自动生成默认规则函数;
- 对于某些类,合成的默认构造函数可能执行错误的操作;
- 有些时候编译器不能为某些类合成默认的构造函数。
3. 定义 Sales_data 的构造函数
根据参数的不同,给 Sales_data 定义了4个不同的构造函数:
- 空参数列表 (默认构造函数);
- 一个 istream& 参数,从数据流中读取信息完成初始化;
- 一个 const string &s,对书号赋值,其它变量默认值;
- 一个 const string &s 书号、一个 unsigned 销售数量、一个 double 价格。
struct Sales_data
{
// * 新增成员 - 构造函数
Sales_data() = default;
Sales_data(istream &);
Sales_data(const string &s) : bookNo(s) {}
Sales_data(const string &s, unsigned n, double p) : bookNo(s), units_sold(n), revenue(p * n) {}
// ? 下面是之前已有的成员
/**
* @brief 返回书的编号
*
* @return string
*/
string isbn() const
{
return this->bookNo;
}
/**
* @brief 合并书的销售数据
*
* @return Sales_data&
*/
Sales_data &combine(const Sales_data &);
/**
* @brief 返回售出书籍的平均价格
*
* @return double
*/
double avg_price() const;
string bookNo; // 书号
unsigned units_sold = 0; // 销量
double revenue = 0.0; // 总销售收入
};
4. = default 的含义
默认构造函数不接受任何实参,如果我们想让它功能和 合成默认构造函数 一样,只需要在参数列表后添加
= default
即可。
Sales_data() = default;
5. 构造函数初始值列表
下图中红框部分,叫做
构造函数初始值列表
,负责给数据成员赋初值。
第一个红框的构造函数,只对bookNo 赋值
,其余的两个变量成员会以 合成默认构造函数 相同的方式隐式初始化。
两个 构造函数初始值列表 后方的函数体都为空,主要是构造函数唯一任务是为数据成员赋初值。
Sales_data(const string &s) : bookNo(s) {}
// 上行代码等价于下列
Sales_data(const string &s) : bookNo(s), units_sold(0), revenue(0) {}
// 函数体为空
Sales_data(const string &s) : bookNo(s) {
cout<<"一般这里没有代码,函数体为空!"<<endl;
}
6. 在类的外部定义构造函数
在类的外部定义构造函数,和之前 7.1.2 节中的 5. 在类的外部定义成员函数 语法格式几乎相同,可以对比学习。
下方的构造函数并没有 初始值列表 ,但是可以通过函数体代码去数据成员进行赋初值。
/**
* @brief Construct a new Sales_data::Sales_data object
*
* @param is
*/
Sales_data::Sales_data(std::istream &is)
{
read(is, *this); // read 函数的作用是从 is 读取一条交易信息,然后存入 this 对象中
}
// read 函数 (7.1.3节 学习过该代码)
/**
* @brief 从给定流中将数据读到给定得对象里
*
* @param is
* @param item
* @return istream&
*/
istream &read(istream &is, Sales_data &item)
{
double price = 0;
is >> item.bookNo >> item.units_sold >> price; // bookNo,units_sold 此时被赋值了
item.revenue = price * item.units_sold; // revenue 此时被赋值了
return is;
}
除了
初始化操作
之外,类还需要控制拷贝、赋值和销毁对象
时发生的行为。
如果我们不主动定义这些操作,则编译器会替我们合成它们。
某些类不能依赖于合成的版本
编译器替我们合成
拷贝、赋值和销毁对象
的操作,仅能适用于简单场景,对于某些类就无法正常工作了。
具体将在 第12章 动态内存 、第13章 拷贝控制 中介绍。
在
7.1 节
中定义的Sales_data 类
中并未使用任何机制去限制用户对类中数据成员的访问。
在 C++ 语言中,可以使用访问说明符
来加强类的封闭性:
- 定义在
public 说明符
之后的成员在整个程序内可被访问;- 定义在
private 说明符
之后的成员可以被类的成员函数访问,不能被使用该类的代码访问;
下面将使用 访问说明符 来对
7.1 节
中的Sales_data 类
进行改造。
#include
#include
using std::istream;
using std::ostream;
using std::string;
// todo 销售数据类
class Sales_data
{
public:
Sales_data() = default;
Sales_data(istream &);
Sales_data(const string &s) : bookNo(s) {}
Sales_data(const string &s, unsigned n, double p) : bookNo(s), units_sold(n), revenue(p * n) {}
/**
* @brief 返回书的编号
*
* @return string
*/
string isbn() const
{
return this->bookNo;
}
/**
* @brief 合并书的销售数据
*
* @return Sales_data&
*/
Sales_data &combine(const Sales_data &);
private:
string bookNo; // 书号
unsigned units_sold = 0; // 销量
double revenue = 0.0; // 总销售收入
/**
* @brief 返回售出书籍的平均价格
*
* @return double
*/
double avg_price() const
{
return units_sold ? revenue / units_sold : 0;
}
};
// 1. Sales_data的成员接口函数
/**
* @brief Construct a new Sales_data::Sales_data object
*
* @param is
*/
Sales_data::Sales_data(std::istream &is)
{
read(is, *this); // read 函数的作用是从 is 读取一条交易信息,然后存入 this 对象中
}
/**
* @brief 合并书的销售数据
*
* @param rhs
* @return Sales_data&
*/
Sales_data &Sales_data::combine(const Sales_data &rhs)
{
units_sold += rhs.units_sold; // 把rhs的成员加到this对象的成员上
revenue += rhs.revenue;
return *this; // 返回调用该函数的对象
}
// 2. Sales_data的非成员接口函数
istream &read(istream &, Sales_data &);
ostream &print(ostream &, const Sales_data &);
Sales_data add(const Sales_data &, const Sales_data &);
/**
* @brief 从给定流中将数据读到给定得对象里
*
* @param is
* @param item
* @return istream&
*/
istream &read(istream &is, Sales_data &item)
{
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price * item.units_sold;
return is;
}
/**
* @brief 负责将给定对象的内容打印到给定的流中
*
* @param os
* @param item
* @return ostream&
*/
ostream &print(ostream &os, const Sales_data &item)
{
os << item.isbn() << " " << item.units_sold << " "
<< item.revenue << " " << item.avg_price();
return os;
}
/**
* @brief 求两个Sales_data对象的和
*
* @param lhs Sales_data对象
* @param rhs Sales_data对象
* @return Sales_data
*/
Sales_data add(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs; // 把lhs的数据成员拷贝给sum
sum.combine(rhs); // 把rhs的数据成员加到sum当中
return sum;
}
使用 class 或 struct 关键字
按照
7.2 节
给的代码修改,发现程序爆红,read、print 函数
报错了!
这是因为在7.2 节
代码中,将bookNo,units_sold、revenue
设置成priavte
,所以这三个变量只能被类的成员函数访问。
如果想要
read、print 函数
访问,需要将其设置为类的友元
。
在Sales_data 类
中,添加以friend 关键字
开头的函数声明即可。(添加完下方代码,read、print 函数就不报错了)
PS:友元声明只能出现在类定义的内部。
// 为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 &);
友元的声明
友元的声明仅仅指定了访问的权限,而非真正的函数声明,在类外需要专门对函数再进行一次声明。
C++ Primer 学习笔记 第7章 类
笔记导航
- C++ Primer 第7章 类 - 上
⇦当前位置
- C++ Primer 第7章 类 - 中
(加班中)
- C++ Primer 第7章 类 - 下
(加班中)
- C++ Primer 总目录 传送门
如果本篇文章对大家起到帮助的话,跪求各位帅哥美女们,
求赞 、求收藏 、求关注!