第7章 类
7.1 定义抽象类数据类型
成员函数的声明必须在类的内部,它的定义则既可以在类的内部也可以在类的外部
string isbn() const {return bookNo;} string isbn() const {return this->bookNo;}
this的目的总是指向“这个”对象,所以this是一个常量指针
const的作用是修改隐式this指针的类型
默认情况下,this的类型是指向类类型非常量版本的常量指针;例如Sales_data *const
const使其成为指向常量的常量指针;const Sales_data *const
类似于:
std::string Sales_data::isbn(const Sales_data *const this){ return this->isbn; }
成员函数体可以随意使用类中的其他成员而无需在意这些成员出现的次序。
定义read和print函数
read函数从给定流中将数据读到给定的对象里,print函数则负责将给定对象的内容打印到给定流中。
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; }
ostream &print(ostream &os, const Sales_data &item){ os << item.isbn() << " " << item.units_slod << " " << item.revenue << " " << item.avg_price(); return os; }
构造函数
构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。
构造函数的名字与类名相同
构造函数没有返回值
构造函数不能被声明成const
构造函数在const对象的构造过程中可以向其写值
默认构造函数无需任何实参
编译器创建的构造函数又被称为合成的默认构造函数
- 如果存在类内的初始值,用它来初始化成员
- 否则,默认初始化该成员
只有当类没有声明任何构造函数时,编译器才会自动地生成默认构造函数
如果类包含有内置类型或者复合类型的成员,则只有当这些成员全部被赋予了类内的初始值时,这个类才适合使用合成默认构造函数
构造函数也可以重载
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) {} Sales_data(istream &);
构造函数不应该轻易覆盖掉类内初始值,除非新赋的值与原值不同
在类外部定义构造函数
Sales_data::Sales_data(istream &is){ read(is, *this) //从is中读取一条交易信息然后存入this对象中 }
使用this来把对象当成一个整体访问
7.2 访问控制与封装
- 定义在public说明符之后的成员在整个程序内可被访问,public成员定义类的接口
- 定义在private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问。private部分封装了类的实现细节
使用class和struct定义类唯一的区别就是默认的访问权限不同。struct默认为public,class默认为private
友元
类可以允许其他类或者函数访问它的非公有成员,用友元friend
友元声明只能出现在类定义的内部,但是在类内出现的具体位置不限。
友元不是类的成员也不受它所在区域访问控制级别的约束
一般来说,最好在类定义开始或结束前的位置集中声明友元
封装的两个优点:
- 确保用户代码不会无意间破坏封装对象的状态
- 被封装的类的具体实现细节可以随时改变,而无需调整用户级别的代码
友元的声明仅仅指定了访问的权限,而非一个个通常意义上的函数声明
7.3 类的其他特性
可变数据成员
mutable关键字
class Screen{ public: void some_member() const; private: mutable size_t access_ctr; //mutable关键字 }; void Screen::some_member() const{ //就算是const也能修改 ++access_ctr; }
设置为内联函数:在类内声明或者类外定义加上一个inline就行
返回*this的成员函数
class Screen{ public: Screen &set(char); Screen &set(pos, pos, char); }; inline Screen &Screen::set(char c){ contents[cursor] = c; return *this; } inline Screen &Screen::set(pos r, pos col, char ch){ contents[r*width + col] ch; return *this; }
myScreen.move(4,0).set('#');
类类型
每个类定义了唯一的类型。即使两个类的成员列表完全一致,它们也是不同的类型
我们可以把类名作为类型的名字使用,从而直接指向类类型
Sales_data item1; class Sales_data item1; //等价
类的声明
class Screen; //Screen类的声明
此时我们已知Screen是一个类类型,但是不清楚它到底包含哪些成员
对于一个类来说,在我们创建它的对象之前该类必须被定义过,而不能仅仅被声明
一个类的成员类型不能是该类自己
然而一旦一个类的名字出现后,它就被认为是声明过了(但尚未定义),
因此类允许包含指向自身类型的引用或指针
class Link_screen{ Screen window; Link_screen *next; Link_screen *prev; };
友元再探
函数可以定义为友元,类也可以,类的成员函数也可以
class Screen{ friend class Window_mgr; //类做友元 friend void Window_mgr::clear(ScreenIndex); //成员函数做友元 };
友元关系不存在传递性
如果一个类想把一组重载函数声明成它的友元,它需要对这组函数中的每一个分别声明。
类和非成员函数的声明不是必须在它们的友元声明之前。当一个名字第一次出现在一个友元声明中时,我们隐式地假定该名字在当前作用域中时可见的。
7.4 类的作用域
类外定义时:返回类型也需指明作用域
class Window_mgr{ public: ScreenIndex addScreen(const Screen&); }; Window_mgr::ScreenIndex Window_mgr::addScreen(const Screen&) { /***/ }
名字查找与类的作用域
typedef string Type; Type initVal(); class Exercise { public: typedef double Type; Type setVal(Type); //double Type initVal(); //double private: int val; }; Exercise::Type Exercise::setVal(Type parm) { //double val = parm + initVal(); //double-->int return val; //int--->double }
7.5 构造函数在探
构造函数初始值列表
class ConstRef { public: ConstRef(int ii); private: int i; const int ci; int& ri; };
错误类外构造函数初始化如下:
ConstRef::ConstRef(int ii){ //赋值初始化 i = ii; ci = ii; //错误:不能被ci赋值 ri = ii; //错误 }
初始值列表初始化:
ConstRef::ConstRef(int ii) : i(ii), ci(ii), ri(ii) {} //正确
养成使用构造函数初始值的习惯
构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序
class X{ int i; int j; public: //未定义的i在j之前被初始化 X(int val) : j(val), i(j) {} };
最好令构造函数初始值的顺序与成员声明的顺序保持一致;尽量避免使用成员函数初始化其他成员函数
如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。
委托构造函数
C++11新标准
委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它自己的一些职责委托给了其他构造函数
class Sales_data{ public: Sales_data(string s, unsigned cnt, double price) : bookNo(s), units_sold(cnt), revenue(cnt*price) {} //其他构造函数委托上一个构造函数 Sales_data() : Sales_data("", 0, 0) {} Sales_data(string s) : Sales_data(s, 0, 0) {} };
聚合类
聚合类使得用于可以直接访问其成员,并且具有特殊的初始化语法形式
聚合类条件:
- 所有成员都是public的
- 没有定义任何构造函数
- 没有类内初始值
- 没有基类,也没有virtual函数
初始化聚合类:
struct Data{ int ival; string s; }; Data vall = {0, "anna"}; //参数要一一对应 Data vall2 = {"anna", 0}; //错误
字面值常量类
数据成员都是字面值类型的聚合类;或者满足一下要求:
- 数据成员都必须是字面值类型
- 类必须至少含有一个constexpr构造函数
- 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数
- 类必须使用析构函数的默认定义,该成员负责销毁类的对象
尽管构造函数不能是const的,但是字面值常量类的构造函数可以是constexpr函数。
正常构造函数前面加constexpr变成constexpr构造函数。
constexpr构造函数必须初始化所有数据成员,初始值或者使用constexpr构造函数,或者是一条常量表达式
7.6 类的静态成员
有时候类需要它的一些成员与类本身直接相关,而不是与类的各个对象保持关联。
静态数据成员的类型可以是常量、引用、指针、类类型等。
类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。
静态成员函数不与任何对象绑定在一起,它们不包含this指针
与其他成员函数一样,既可以在类内部也可以在类外部定义静态成员函数
当在类外部定义静态成员时,不能重复static关键字,该关键字只出现在类内部的声明语句
静态数据成员不属于类的任何一个对象,所以它们并不是在创建类的对象时被定义的。
意味着它们不是由类的构造函数初始化的。
一般来说不能在类内部初始化静态成员;必须在类的外部定义和初始化每个静态成员
可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr
class A{ static constexpr int period = 30; };
即使一个常量静态成员在类内被初始化了,通常情况下也应该在类外定义以下该成员
静态成员独立于任何对象
静态数据成员可以是不完全类型
class Bar{ public: //... private: static Bar mem1; //正确,静态成员可以是不完全类型 Bar *mem2; //正确,指针也行 Bar mem3; //错误,数据成员必须是完全类型 }
与普通成员不同,静态成员可以作为默认实参
class Screen{ public: Screen& clear(char = bkground); private: static const char bkground; //去掉static就报错 };
非静态成员不能作为默认实参,因为它的值属于对象的一部分