7.1 抽象数据类型
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;
};
// Sales_data的非成员接口函数
Sales_data add(const Sales_data&,const Sales_data&);
std::ostream &print(std::ostream&,const Sales_data&);
std::istream &read(std::istream,Sales_data&);
- 因为this总是指向"这个"对象,所以this是一个常量指针(不允许改变this中保存的地址)
- const成员函数
- isbn()函数后面的const修饰隐式的this指针
- 默认情况下,this指向类类型非常量版本的常量指针,这样的函数称为常量成员函数
常量对象,以及常量对象的引用或指针都只能调用常量成员函数
- 类作用域和成员函数
- 类本身是一个作用域
- 在类的外部定义成员函数
- 定义与申明需要匹配,必须包含所属类名,如:
double Sales_data::avg_prise() const { if(units_sold) return revenue/units_sold; else return 0; }
- 定义与申明需要匹配,必须包含所属类名,如:
- 定义一个返回this对象的函数
// 内置运算符将左侧对象作为当成左值返回,故这里保持一致
Sales_data &Sales_data::combine(const Sales_data &rhs){
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;//返回调用该函数的对象
}
//调用:
total.combine(trans); //解引用得到total的引用
7.1.3 定义类相关的非成员函数
属于类接口的组成部分但不属于类本身
// IO类属于不能被拷贝的类,只能用引用方式
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;
}
7.1.4 构造函数
控制对象的初始化过程,没有返回类型,
不能被声明成const,因为创建const对象时,知道构造函数完成初始化过程,对象才取得常量属性
-
合成的默认构造函数
- 不提供任何构造函数则隐式定义,无需实参
- 如果类内存在初始值则用它初始化成员,否则默认初始化
-
=default
- 定义一个默认构造函数,作用等同于默认构造函数
- 在类内表该构造函数是内联的,类外不是
-
初始话列表
- 冒号和花括号之间的代码称为构造函数初始化列表
- 被初始值列表忽略的成员使用默认构造函数相同的方式初始化
- 有些编译器不支持类内初始化
-
在类外部定义构造函数
Sales_data::Sales_data(std::istream &is){ read(is,*this);//从is中读取一条交易信息存入this中 }
- 该构造函数初始值列表是空的,在构造函数体内初始化成员
7.1.5 拷贝、赋值和析构
- 如果类包含vector或string成员,则其拷贝、赋值或销毁的合成版本能正常工作,管理动态内存通常不能依赖于合成版本
7.2 访问控制与封装
- public:说明符之后的成员在整个程序内可被访问
- private:说明符之后的成员可以内类的成员函数访问
- class和struct的唯一区别是默认访问权限不同
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&); private: double avg_price() const {return units_sold?revenue/units_sold:0;} //数据成员 std::string bookNo; unsigned units_sold = 0; double revenue = 0.0; }; // Sales_data的非成员接口函数 Sales_data add(const Sales_data&,const Sales_data&); std::ostream &print(std::ostream&,const Sales_data&); std::istream &read(std::istream,Sales_data&);
7.2.1 友元
类可以允许其他类或函数访问它的非公有成员,方法是令其他类或函数成为它的友元
- 友元的声明仅仅指定了访问权限,如果希望类用户能调用,必须再友元声明之外再声明一次(不过很多编译器未强制限定)
7.3 类的其他特性
- 除了定义数据成员,类还能自定义某种类型在类中的别名(存在访问限制)
#ifndef _SCREEN_H
#define _SCREEN_H
#include <string>
class Screen{
public:
// 必须先定义后使用
typedef std::string::size_type pos;
Screen() = default;//已有构造函数,不加不会自动生成默认构造函数
Screen(pos ht,pos wd,char c):height(ht),width(wd),
contents(ht *wd,c){}
char get() const
{return contents[cursor];} //去读光标处字符,隐式内联
inline char get(pos ht,pos wd) const; //显示内联
Screen &move(pos r,pos c); // 能在之后被设置为内联
// 类型别名,与上等价
// using pos = std::string size_type;
private:
pos cursor = 0;
pos height = 0, width = 0;
std::string contents;
};
inline
Screen &Screen::move(pos r,pos c){
pos row = r*width;
cursor = row + c;
return *this;
}
char Screen::get(pos r,pos c)const //在类内声明成inline
{
pos row = r*width; //计算行的位置
return contents[row + c];//返回给定列字符
}
#endif
- 虽然无需在声明和定义的地方同时声明inline,不过最好只在类外定义的地方说明,这样容易理解
- 成员函数也能被重载
- 可变数据成员:
- mutable修饰的成员可在const成员函数中被修改
class Window_mgr{ std::vector<Screen> screens{Screen(24,80,' ')}; };
- 提供类内初始值时必须使用
符号=
或花括号
7.3.2 返回*this的成员函数
- 如:myScreen.set(’#’).display(count);
7.3.3 类类型
- 使用方法:
Sales_data item1; class Sales_data item1; //等价声明,从C语言继承而来
- 类的声明:
class Screen; //前向声明,只声明不定义,不完全类型,可已指向这种类型的指针或引用,也可以声明以不完全类型为参数或者返回类型的函数
- 只有当类全部完成后才算被定义,所以类成员不能时类自己
- 一旦一个名字出现后,它就被认为出现过了(尚未定义),因此允许包含指向自身类型的引用或指针
7.3.4 友元再探
- 友元函数定义在类内部,隐式内联
- 如果一个类指定了友元类,则友元类的成员函数可以访问此类的所有成员(包括私有成员)
- 令成员函数作为友元:
calss Screen{ // Window_mgr::clear 需要在之前被声明 friend void Window_mgr::clear(ScreenIndex); }
- 定义Window_mgr类,申明clear()但不能定义,使用screen前先声明
- 定义Screen,包括对clear的友元声明
- 定义clear
- 函数重载和友元
- 需要对函数中的每一个分别声明
- 友元声明和作用域
- 类和成员函数的声明不是必须出现在友元声明之前,当一个名字第一次出现在一个友元声明中时,我们隐式假定该名字在当前作用域中是可见的,然而友元本身不一定真的声明在当前作用域中,
甚至在类内部定义该函数,也必须在类外提供相应的声明
,换句话说,仅仅用声明友元函数的类的成员调用该友元函数,它也必须是被声明过的:struct X { friend void f(){/*友元函数可以在类内部定义*/} X(){f();} //错误,f还没被声明 void g(); void h(); } void X::g(){return f();}//错误,f还没被声明 void f(); void X::h(){return f();}//正确,现在f的声明在作用域中了
- 重要的是理解友元函数声明的作用是影响访问权限,它本身并非普通意义上的声明
有的编译器并不强制执行上述关于友元的限定
- 类和成员函数的声明不是必须出现在友元声明之前,当一个名字第一次出现在一个友元声明中时,我们隐式假定该名字在当前作用域中是可见的,然而友元本身不一定真的声明在当前作用域中,
7.4 类的作用域
- 每个类都会定义它自己的作用域,作用域之外,普通数据和成员只能通过对象、引用或者指针使用成员访问运算符。
Screen::pos ht=24; //使用Screen定义的pos类型
7.4.1 名字查找与类作用域
- 类的定义分成两步:
- 编译成员声明
- 直到类全部可见才编译函数体(所以能使用类中定义的任何名字)
7.5 构造函数再探
- 构造函数的初始值有时必不可少(比如const或引用)
- 成员初始化的顺序:
- 与在类定义中出现的顺序一致,与初始化列表中出现的顺序没有关系
- 默认实参和构造函数
7.5.2 委托构造函数
- 一个委托构造函数使用它所属类的其他构造函数执行自己的初始化过程
class Sales_data {
public:
//非委托构造函数使用对应的实参初始化成员
Sales_data(std::string s,unsigned cnt,double price):
bookNo(s),units_sold(cnt),revenue(cnt*price){}
//其余构造函数全都委托给另一个构造函数
Sales_data():Sales_data("",0,0){}
Sales_data(std::string s):Sales_data(s,0,0){}
Sales_data(std::istream &is):Sales_data()
{read(is,*this);} //委托默认构造函数
}
7.5.3 默认构造函数的作用
- 默认初始化一下情况会发生:
- 不适用任何初始值定义非静态变量或数组
- 类本身含有类类型成员且使用合成默认构造函数
- 类类型成员没有在构造函数初始化列表显示初始化时
7.5.4 隐式类型转换
- 转换构造函数:构造函数只接受一个实参,则实际上定义了转换为此类型的隐式转换机制
- 只允许一步类类型转换
item.combine("9-999-999"); //错误 item.combine(string("9-999-999")); //正确
- 抑制构造函数定义的隐式转换
class Sales_data{ explicit Sales_data(std::istream&); }
- explicit关键字只对一个实参的构造函数有效
- 只能在类内部声明构造函数时使用,类外不能重复定义
- explicit构造函数只能用于直接初始化
- 为转换显示使用构造函数
item.combine(Sales_data(nukk_book));//正确 item.combine(static_cast<Sales_data>(cin));//正确
7.5.5 聚合类
用户可以直接访问其成员,且具有特殊的初始化语法形式,类似C语言中的struct
- 所有成员是public
- 没有定义任何构造函数
- 没有类内初始值
- 没有基类、virtual函数
struct Data{ int ival; string s; } Data val={0,"Anna"};
7.5.6 字面值常量类
- 字面值类型的类可能含有constexpr函数成员,成员必须符合constexpr函数的所有要求,他们是隐式const的
- 数据成员都是字面值类型的聚合类是字面值常量类,不是聚合类但满足以下要求也是字面值常量类:
- 成员都是字面值类型
- 至少含有有一个constexpr构造函数
- 若成员有类内初始值,则必须是常量表达式、或成员使用自己的constsxpr构造函数初始化
- 类必须使用析构函数的默认定义
- constexpr构造函数:一般来说应该是空的
class Debug{
public:
constexpr Debug(bool b=true):hw(b),io(b),other(b){} //必须初始化所有数据成员
...
}
7.6 类的静态成员
有时候类需要一些成员与类本身相关,而非与类各个对象保持联系
class Account{
public:
void calculate(){amount += amount*interestRate;}//成员函数可以直接访问
static double rate(){return interestRate;}
static void rate(double);
private:
std::string owner;
double amount;
static double interestRate;
static double initRate();
}
//在外部定义静态成员时不能重复使用static关键字
void Account::rate(double newRate){
interestRate=newRate;
}
//定义并初始化静态成员
double Account::interestRate=initRate();
- 类的静态成员存在于任何对象之外,对象中不包含任何于静态成员有关的数据
- 静态成员函数也不与任何对象绑定一起,不包含this指针,不能声明为const
- 使用静态成员:
Account c1; double r=Account::rate(); //使用作用域运算符访问静态成员 r=c1.rate(); //通过对象访问
- 静态函数的类内初始化
class Account{ private: static constexpr int period=30; double daily_tbl[period]; } //即使在类内初始化了,类外部也该定义一下 constexpr int Account::period;
- 静态数据成员可以是不完全类型
- 静态数据成员可以作为默认实参