7.1 定义抽象数据类型
类调用成员函数时把对象地址传入函数的隐式形参this,任何对类成员的直接访问都被看做this的隐式引用。
const成员函数
string isbn() const
{
...
}
const作用是修改隐式this指针的类型,将其声明成常量类型。
常量对象以及常量对象的引用或指针都只能调用常量成员函数。
类相关的非成员函数的定义应与类声明在通过一个头文件内。
构造函数
类通过构造函数初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。
构造函数不能被声明成const。
合成的默认构造函数初始化数据成员:
- 利用类内初始值来初始化成员
- 否则,默认初始化成员
含有内置类型或者复合类型成员的类应该在类的内部初始化这些成员,或者定义一个自己的默认构造函数,防止因为初始化时得到未定义的值。
如果类中包含一个其他类类型的成员,且这个成员的类型没有默认构造函数,编译器无法初始化该成员,必须自定义默认构造函数进行初始化。
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 &);
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;
};
=default
在类内部,默认构造函数是内联的,在类外部,不是内联的。
如果编译器不支持类内初始值,默认构造函数应该初始化类的每个成员。
在类的外部定义构造函数
Sales_data::Sales_data(std::istream &is)
{
read(is, *this);
// read 从is中读取一条信息后存入this对象中
}
拷贝、赋值、析构
拷贝
- 初始化变量
- 值传递方式传参、返回值
- 赋值运算符
一般情况下,不主动定义这些操作,编译器生成时将对对象的每个成员进行拷贝、赋值和销毁操作。
7.2 访问控制于封装
- public
public说明符之后的成员在整个程序内可访问,一般是类的接口
- private
private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问。
struct
和class
区别:
默认访问权限不太一样。定义在类中的第一个访问符之前的成员的属性,在struct
中是public
的,在class
中是private
的。
友元
类可以允许其他类型或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元。
友元只能出现在类定义的内部,但是在类内出现的具体位置不限。友元不是类的成员也不受它所在的区域访问控制级别的约束。
友元的声明
在类内声明友元函数后,如果我们希望类的用户能够调用某个友元函数,在类定义外必须专门对函数进行一次声明。一般在同一个文件中。许多编译器并未强制限定友元函数必须在使用之前在类的外部声明。
7.3 类的其他特性
内联函数:
- 隐式
- 显式
- 类外
inline成员函数也应该与相应的类定义在一个头文件中。
可变数据成员
mutable size_t access_ctr;
一个可变数据成员,永远不是const,即使他是const对象成员。因此,一个const成员函数可以改变一个可变成员的值。
返回*this的成员函数
返回引用类型,将对线作为左值返回
从const成员函数返回*this
一个cosnt成员函数如果返回*this的引用形式,则函数返回类型是常量引用。
Screen myScreen;
// 调用set时引发错误,因为display返回的是const常量引用
myScreen.display(cout).set("#");
基于const的重载
根据对象是否是const决定了调用具体的版本。在函数调用时,指向非常量的指针可以转换成指向常量的指针。
类类型
类的声明
声明和定义可以分开,但使用场景有限。在创建类对象之前,类必须被定义过。
class class_name;
类友元
class Screen{
friend class Window_mgr;
};
如果一个类指定了友元类,友元类的成员函数可以访问此类包括非公有成员在内的所有成员,即 Screen
所有成员对于 Window_mgr
都变成可见。
还可以只为某个成员函数设置为友类,这就涉及到了两个类之间的依赖关系,程序的组织结构要特别注意。包括相关类的先后声明、定义顺序等。
class Screen{
friend void Window_mgr::clear(int);
};
一般方式:
- 定义
Window_mgr
类,其中声明clear
函数,但是不能定义。在clear
使用Screen
的成员之前必须先声明Screen
- 定义
Screen
,包括对于clear
的友元声明 - 最后定义
clear
友元声明影响的只是访问权限,并不具有不同意义上的声明的作用。
7.4 类的作用域
如果返回类型也是在类中定义的,则在类外定义函数时,要明确返回类型所在的类。
Window_mgr::ScreenIndex
Window_mgr::addScreen(const Screen &s)
{
...
}
名字查找与类的作用域
由内到外 逐层,向前 查找。
7.5 构造函数再探
构造函数初始值列表
使用构造函数初始值列表初始化对象数据成员和在构造函数中进行赋值并不完全一样。
必须使用初始值列表初始化成员的情况:
- 成员是const或者引用
- 成员是某种类类型且该类没有定义默认构造函数
默认实参和构造函数
class Sales_data{
public:
// 相当于默认构造函数,因为其默认参数
Sales_data(std::string s = ""):bookNo(s){}
...
};
委托构造函数
一个委托构造函数使用它所属的类的其他构造函数执行它自己的初始化过程,或者说它把自己的一些(或者全部)职责委托给了其他构造函数。
class Sales_data{
public:
// 普通构造函数
Sales_data(const std::string &s, unsigned n, double p):
:bookNo(s), units_sold(n), revenue(p*n){ }
// 委托构造函数
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);
}
...
};
隐式的类类型转换
string null_book = "13754546465";
item.combine(null_book);
利用 null_book
通过 Sales_data
构造函数生成一个临时对象,作为参数传递给 combine
。
只允许一步类类型转换
item.combine("13754546465")
错误,要经过两次装换
- “13754546465” ⇒ string
- string ⇒ Sales_data
修正:
// 显式转 string ,隐式转 Sales_data
item.combine(string("13754546465"));
// 显式转 Sales_data ,隐式转 string
item.combine(Sales_data("13754546465"));
隐式的类类型转换在某些情况下会造成预料之外的错误,需要抑制隐式转换。
抑制构造函数定义的隐式转换
explicit 关键字只对一个实参的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换,所以无需将其指定为explicit。
explicit 只能在类内声明时使用,类外定义时不需要。
class Sales_data{
Sales_data() = default;
Sales_data(const std::string &s, unsigned n, double p):
:bookNo(s), units_sold(n), revenue(p*n){ }
explicit Sales_data(const std::string &s):bookNo(s){ }
explicit Sales_data(std::istream &);
Sales_data & combine(const Sales_data&);
};
通过 explicit
限定,之前的情况都失效了。
item.combine(null_book); // 失效
item.combine(cin); // 失效
explicit 构造函数只能用于直接初始化
// 直接初始化 right
Sales_data item1(null_book);
// wrong 拷贝初始化
Sales_data item2 = null_book;
为转换显式地使用构造函数
item.combine(Sales_data(null_book0));
item.combine(static_cast<Sales_data>(cin));
聚合类
- 所有成员都是public
- 没有任何构造函数
- 没有类内初始值
- 没有基类
- 没有virtual函数
struct Data{
int ival;
string s;
};
Data vall = {0, "anna"};
字面值常量
除了算数类型、引用、和指针外,某些类也是字面值类型。字面值类可能含有 constexpr
函数成员。这样的成员必须符合 constexpr
函数要求,他们是隐式 const
。
数据成员都是字面值类型的聚合类是字面值常量类,如果不是聚合类,但符合以下要求,仍是字面值类:
- 数据成员都是字面值类型
- 至少含有一个constexpr构造函数
- 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数。
- 类必须使用析构函数的默认定义,该成员负责销毁类的对象。
constexpr构造函数
即使构造函数不能是const,但字面值类构造函数可以是constexpr函数(至少一个)。
constexpr 构造函数函数体一般是空的,或者一条返回语句。其必须初始化所有数据成员,初始值或者使用 constexpr 构造函数,或者是一条常量表达式。
constexpr 构造函数用于生成 constexpr 对象以及 constexpr 函数的参数或返回类型。
7.6 类的静态成员
类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据,且被所有同类对象共享。
静态成员函数也不与任何对象绑定在一起,不包含this指针。
静态成员函数不能声明成const。
静态数据成员不属于类的任何一个对象,这意味着他们不是由类的构造函数初始化的。一般来说,我们不能在类的内部初始化静态成员,相反,必须在类的外部定义和初始化每个静态成员。和其他对象一样,一个静态数据成员只能定义一次。
// 定义并初始化一个静态成员
double Account::interestRate = initRate();
静态成员的类内初始化
一般情况下,类的静态成员不应该在类的内部初始化。特殊情况:
- 静态成员提供 const 整数类型类内初始值
- 静态成员必须是字面值常量类型 constexpr
- 初始值必须是常量表达式
class Account{
private:
static constexpr int period = 30;
double daily_tbl[period];
};
constexpr int Account::period;
一个常量静态数据成员在类内部被初始化了,通常情况下也应该在类的外部定义一下该成员。
使用类的静态成员
double r;
r = item::rate();
r = item.rate(); // item 是对象或引用
r = item->rate(); // item 是对象的指针
静态成员更广的场景
静态成员类型可以是它所属的类型,而非静态成员受到限制,只能是它所属类的指针或者引用。
class Bar{
private:
static Bar mem1; // right
Bar *mem2; // right
Bar mem3; // wrong
};
静态成员可以作为默认实参,而普通成员不行。
class Screen{
public:
Screen & clear(char = bkground);
private:
static const char bkground;
};