第7章 类
7.1 定义抽象数据类型
类的基本思想:数据抽象、封装。
数据抽象:一种依赖于接口和实现分离的编程及设计技术。接口包括用户所能执行的操作;实现包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数。
封装实现类的接口和实现的分离。封装后的类隐藏其实现细节,用户只能使用接口而无法访问实现部分。
- 类的用户是程序员,而非应用程序的最终使用者。
- 类的设计者考虑类的实现,使类易于使用;类的使用者只使用类的接口,而不考虑类的实现机制。
引入成员函数
- 成员函数只能在类内声明,可以在类内或类外定义;非成员函数只能在类外声明和定义。
- 在类内定义的成员函数是隐式的inline函数。
引入this
- this隐式形参是一个本身是常量的指针,它指向调用该函数的对象。
- this形参隐式定义,任何自定义名为this的参数或变量都是非法的。
//this的类型:Sales_data * const
total.isbn(); // this=&total;
引入const成员函数
- 在const成员函数中,this是一个本身和其所指都是常量的指针。
- 常量对象,及对常量对象的引用和指针只能调用const成员函数 ;普通对象可以调用普通成员函数和const成员函数。
- 对于const成员函数,其this指向常量,可以被常量对象和非常量对象调用,函数灵活性更好;对于普通成员函数,其this指向非常量,只能被非常量对象调用。
// f:成员函数
// 第1个const:返回值是常量
// 第2个const:形参是常量
// 第3个const:函数是常量成员函数
const int A::f(const int ci) const
{
return ci;
}
可定义一个返回this对象的函数
A& A::f(A& a)
{
.....
return *this; // 返回调用该函数的对象
}
a1.f(a2); // 返回对a1的引用
定义类相关的非成员函数
- 函数声明与定义分开存放。非成员函数声明一般与类声明在同一头文件。
- IO类与数组一样属于不可拷贝类型,只能通过引用来传递参数。
- 执行输出任务的函数应减少对格式控制,如换行符一般由用户自己添加。
构造函数
- 构造函数名字与类名相同,无返回类型,不能被声明成const,用于初始化类对象的数据成员。
- 默认构造函数(无须任何实参);合成的默认构造函数(由编译器创建的构造函数)。
- 合成的默认构造函数只适合非常简单的类,普通的类必须定义默认构造函数。因为只有当类未声明任何构造函数时,编译器才会合成默认构造函数;内置类型或复合类型(数组,指针)的数据成员被默认初始化时是未定义的;类中包含其它类类型的成员,且该类类型没有默认构造函数,编译器无法合成默认构造函数。
- 构造函数初始值列表。
类的拷贝、赋值和析构
- 编译器可以合成拷贝、赋值和销毁操作。但当类需要分配类对象之外的资源(如管理动态内存),合成版本常常失效。
- 使用vector或string可避免分配和释放内存的复杂性。
7.2 访问控制与封装
访问说明符:public、private
- public用于定义接口,对于类的对象和成员函数可见;private用于封装实现接口,对于成员函数可见。
- struct默认访问权限是public,class默认访问权限是private。若类中所有成员都是public,则使用struct;若类中有private,则使用class。
友元:让其它类或函数访问类本身的非公有成员
- 友元声明只能出现在类内,它仅指定访问权限,而非一个通常的函数声明,故函数声明不能省略。
- 最好在类定义开始或结束前的位置集中声明友元。
7.3 类的其它特性
类成员:类型成员,内联成员函数,重载成员函数,可变数据成员(mutable),类数据成员初始化
- 普通成员的定义和使用可以没有次序,但类型成员需先定义再使用。类型成员通常出现在类型开始地方,通常使用typedef和using。
- 若在类中声明任何构造函数,编译器不会合成默认构造函数,若需要默认构造函数,必须进行显式声明。
- 成员函数可隐式内联,显式内联,类外内联。
- 普通函数声明与定义分离,在头文件中声明函数,在同名源文件中定义函数。例外:inline函数、constexpr函数和类在头文件中定义。
- 可变数据成员对于const对象、const成员函数都是可修改的。
- 类内初始值必须用等号或花括号表示。
返回*this的成员函数。
- 对于返回*this的函数,若返回类型不是引用,则函数返回的是对象副本;若返回类型是引用,则返回的是对象本身。
- const成员函数以引用的形式返回*this,则它的返回类型是常量引用。
- 可根据形参是否是const、成员函数是否const决定重载函数。
class A
{
A &f1(int x) // 返回*this
{
return *this;
}
A &f2(const int x) // 形参是const
{
return *this;
}
A &f3(int x) const; // 成员函数是const
{
return *this;
}
};
类类型
- 类类型与类的成员列表无关。对于两个类,即使成员列表相同,它们也是不同类型。
- 前向声明:仅声明类而暂时不定义类,此时类是不完全类型。不完全类型只能用于定义指向该类型的引用或指针,声明以不完全类型为参数或返回值的函数。
- 只有类全部完成后类才算被定义,故在声明类的数据成员时,类是不完全类型,只能声明指向该类类型的引用或指针,不能声明该类型。类定义之后才能创建类的对象或访问类的成员。
友元再探
- 类本身和类的成员函数都可以作为友元。
- 一个类指定友元类,则友元类的成员函数可以访问此类的所有成员。
- 友元关系不可传递。每个类只负责控制自己的友元类和友元函数。
- 若将重载函数作为友元,需对重载函数中的每个函数都作友元声明。
- 友元声明只影响访问权限,并非真正声明。在类中声明和使用友元函数前,最好先进行真正声明。
7.4 类的作用域
- 一个类就是一个作用域。在作用域之外,对象、引用和指针使用成员选择运算符(.和->)访问类的数据成员和成员函数,通过作用域运算符(::)访问类类型成员。
- 类名之后的参数列表和函数体都在类的作用域中。
- 名字查找(寻找与所用名字最匹配的声明的过程)一般从内到外,先在块中名字使用之前的代码查找,再在外层作用域查找。
- 在定义类时,编译器会先编译成员声明,直到类全部可见后才编译函数体。
- 在类中不能重新定义外层作用域中代表一种类型的名字。
- 不建议在类中使用成员的名字作为成员函数的参数。
- 若名字查找在成员函数的函数体内,则先在函数体内且在名字使用前的代码查找,再在类内查找,最后在成员函数定义前的作用域内查找。
7.5 构造函数再探
构造函数初始值列表
- 建议使用构造函数初始值。初始化和赋值的区别事关底层效率问题,前者直接初始化数据成员,后者先初始化再赋值。特别是,当成员是const、引用、或者属于某种未提供默认构造函数的类类型时,必须通过构造函数初始值列表来提供初值。
- 构造函数初始值列表只说明用于初始化成员的值,其初始化顺序是按成员定义的顺序执行。最好令构造函数初始值的顺序与成员声明的顺序一致,避免使用某些成员初始化其它成员。
- 若构造函数为所有实参提供默认实参,则该构造函数是默认构造函数。
委托构造函数
- 委托构造函数使用它所属类的其它构造函数执行它自己的初始化过程,或者说它把自己的的一些(或者全部)职责委托给其它构造函数。
#include <iostream>
#include <string>
using namespace std;
std::istream &read(std::istream &is, Sales_data &item);
class Sales_data
{
public:
Sales_data(string s, unsigned cnt, double rev) : bookNo(s), units_sold(cnt), revenue(cnt * rev) {}
Sales_data() : Sales_data("", 0, 0) {}
Sales_data(string s) : Sales_data(s, 0, 0) {}
Sales_data(std::istream &is) : Sales_data()
{
read(is, *this);
}
private:
string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
friend std::istream &read(std::istream &is, Sales_data &item);
};
std::istream &read(std::istream &is, Sales_data &item)
{
int price;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = item.units_sold * price;
return is;
}
int main()
return 0;
}
默认构造函数的作用
- 当对象被默认初始化或值初始化时自动执行默认构造函数。
- 默认初始化发生情况:在块作用域中不使用任何初始值定义一个非静态变量或数组;类本身含有类类型的成员且使用合成的默认构造函数;类类型的成员没有在构造函数初始值列表中显式初始化。
- 值初始化发生情况:数组初始化时提供的初始值数量少于数组大小;不使用初始值定义一个局部静态变量;书写形如T()的表达式显式地请求值初始化。
- 若定义其它构造函数,那么最好提供一个默认构造函数。
- A a();是一个函数声明,A a;是创建一个A类对象。
隐式的类类型转换。
- 转换构造函数:若构造函数只接受一个实参,则它可将参数隐式转换为此类类型。
- 编译器只允许执行一步的类类型转换。
- explicit可抑制构造函数定义的隐式转换。explicit只对一个实参的构造函数有效,含多个实参的构造函数不能隐式转换;只需在类内声明构造函数时使用explicit,类外定义时不应重复;explicit构造函数只能用于直接初始化,而非拷贝初始化;可对explicit构造函数进行显示地强制转换。
- 接受一个单参数的const char *的string构造函数不是explicit的;接受一个容量参数的vector构造函数是explicit的。
- 隐式类类型转换时,其临时变量不可修改,可看成常量,只能传递给常量引用,而不能传递给非常量引用(临时变量是右值,非常量引用只能使用左值初始化)。
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int main()
{
//非explicit构造函数
const char c = 'a';
const char *p = &c;
string s1(p);
string s2 = (p);
//explicit构造函数
// vector<int> v1 = (10);
vector<int> v1(10);//explicit构造函数只能直接初始化
return 0;
}
聚合类
- 聚合类满足条件:所有成员都是public;没有定义任何构造函数;没有类内初始值;没有基类和virtual函数。
- 聚合类可用花括号括起来的成员初始值列表初始化。
示例:
#include <iostream>
using namespace std;
struct Sales_data
{
string bookNo;
unsigned units_sold; //聚合类无类内初始值
double revenue;
};
int main()
{
Sales_data item = {"978-0590353403", 25, 15.99};
return 0;
}
字面值常量类
- 数据成员都是字面值类型的聚合类属于字面值常量类。
- 其它字面值常量类满足条件:数据成员都是字面值类型;类必须至少含有一个constexpr构造函数;若一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式,或者若成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数;类必须使用析构函数的默认定义,该成员负责销毁类的对象。
- constexpr构造函数的函数体一般为空,必须初始化所有数据成员,初始值或使用constexpr构造函数,或是一条常量表达式。
class Debug
{
public:
constexpr Debug(bool b = true) : hw(b), io(b), other(b){};
constexpr Debug(bool h, bool i, bool o) : hw(h), io(i), other(o){};
constexpr bool any()
{
return hw | io | other;
}
void set_io(bool b)
{
io = b;
}
void set_hw(bool b)
{
hw = b;
}
void set_other(bool b)
{
other = b;
}
private:
bool hw;
bool io;
bool other;
};
7.6 类的静态成员
- 类的静态成员与类相关,与类的对象无关。
- 静态成员可以是public或private,可以是常量、引用、指针、类类型。它无this指针,不能声明为常量成员函数。
- 静态成员在类内使用static声明,用作用域运算符访问。成员函数可直接访问静态成员。
- 静态成员函数可在类内或类外定义(类外定义时不能重复static);静态数据成员必须在类外定义和初始化,且只能定义和初始化一次。
- static const整型成员可以使用类内初始值;static constexpr必须使用类内初始值。
- 若编译器可替换static成员的值,static const 或static constexpr可以不进行定义;若不能,static成员必须有定义语句。
- 静态成员、引用和指针可以是不完全类型(仅声明类而暂时未定义类),类只有在定义后才能访问其一般成员,不完全类型除外;静态成员可以作默认实参,非静态数据成员不能作默认实参。