S07类
一、定义抽象数据类型
1、类的基本思想是数据抽象和封装
(1)数据抽象是一种依赖于接口和实现分离的编程技术
(2)封装实现了类的接口和实现的分离
2、定义在类内部的函数都是隐式的inline函数
3、this:成员函数通过this这个额外的隐式参数访问调用它的那个对象,任何对类成员的直接访问都被看作是this的隐式调用,this总是指向“这个”对象故是常量指针
4、const成员函数:成员函数参数列表后加上const表示this是一个指向常量的常量指针,const成员函数不能修改调用它的对象的内容
5、类本身就是一个作用域,成员函数体可以随意使用类中的其他成员而无须在意这些成员出现的次序
6、在类的外部定义成员函数时,除了类型要完全相匹配,名字还必须包含它所属的类名,同时对于常量成员函数需要在参数列表后加上const,这里的const指的是被隐藏的this所指的对象是常量
double Sales_data::avg_price() const //类名Sales_data;域作用符::;常量成员函数const
{
if(units_sold)
return revenue/units_sold; //隐式使用了Sales_data的成员
else
return 0;
}
7、返回this对象的函数
Sales_data& Sales_data::combine(const Sales_data &rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this; //返回调用该函数的对象,函数返回的是引用,是左值
}
total.combine(trans); //实际上是Sales_data(&total, trans)
8、构造函数:每个类都分别定义了它的对象被初始化的方式(一个或多个方式,通过重载来实现不同的初始化),类通过特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数,无论何时只要类的对象被创建,就会执行构造函数来初始化对象的数据成员,当没有提供任何实参时会执行的构造函数称为默认构造函数
注意:构造函数没有返回类型且不能被声明成const,构造函数在const对象的构造过程中可以向其写值,若没有显式定义构造函数,编译器就会隐式定义合成的默认构造函数进行默认初始化
(1)由于只有在没有声明任何构造函数时才会生成合成的默认构造函数,因此当已经定义了部分构造函数时,将不会生成合成的默认构造函数
(2)对于部分内置类型或复合类型,执行合成的默认构造函数可能导致错误的操作,此时可能需要对每个成员都赋予初始值才能正常使用合成的默认构造函数
(3)当类中包含一个其他类类型的成员且这个成员没有默认构造函数,那么编译器无法初始化这个成员,必须自定义构造函数
注意:如果需要默认构造函数来初始化,可以使用 = default
来要求编译器生成默认构造函数
9、拷贝、赋值与析构:类除了需要定义初始化(构造函数)外还要定义拷贝、赋值与销毁对象时的操作
注意:一般编译器会默认生成拷贝、赋值与销毁操作,但有时使用默认方式会导致错误
total = trans;
//默认的赋值操作,等价于
total.bookNo = trans.bookNo;
total.units_sold = trans.units_sold;
total.revenue = trans.revenue;
//尽可能使用vector和string对象作为类的成员,这样默认生成的拷贝、赋值与销毁操作可以正常工作
二、访问控制与封装
1、访问说明符:
(1)public:在该说明符下的成员在整个程序内可以被访问,public定义类的接口
(2)private:在该说明符下的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,即封装(隐藏)了类的实现细节,同一个类的不同对象也可以访问互相的私有成员
(3)protected:参考S15面向对象程序设计
注意:C++的访问控制是编译器控制的,在运行时就不再有这个访问控制了
2、struct和class定义类的唯一区别在于默认的访问权限,struct默认成员是public的,class默认成员是private的,为了统一,一般当希望所有成员都是public时使用struct否则使用class
3、封装:保护类的成员不被随意访问的能力,封装实现了接口和实现的分离,确保了用户代码不会无意间破坏封装对象的状态、被封装的类的具体实现细节可以随时改变而无须调整用户级的代码
4、友元:类可以允许其他类或者函数访问它的非公有成员,只需要设置友元,以friend
开头声明函数
class Sales_data
{
friend std::istream &read(std::istream &, Sales_data&); //最好在类的开始或结束前集中声明友元
friend class Sales;
...
}
(1)类之间的友元关系:如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员
注意:友元不存在传递性,A是B的友元,A能访问B,B是C的友元,B能访问C,但A不是C的友元就不能访问C
(2)成员函数作为友元:声明时必须用域作用符明确指出函数所属的类,同时要满足声明和定义的依赖关系
(3)重载函数作为友元:声明友元时需要声明重载函数组中的每一个函数,否则没有声明到的重载函数不能访问类
(4)友元声明和作用域:部分编译器不强制要求执行如下的规则
struct X
{
friend void f() {}; //友元函数可以定义在类内部
x() { f(); } //错误,f实际上还未被声明
void g();
void h();
}
void X::g() { return f(); } //错误,f实际上还未被声明
void f(); //定义在类X内的f被真正声明
void X::h() { return f();} //正确,f已经被声明过了
注意:相同class的各个objects互为友元
三、类的其他特性
1、类型成员:在public部分使用using
或typedef
定义类型成员,与其他成员不同,类型成员必须先定义后使用
2、可变数据成员:即使是操作const对象的成员,有时候也希望能修改数据,在其声明中加入mutable
,这样在const成员函数内也可以修改mutable成员的值
class Screen
{
public:
void some_member() const;
private:
mutable size_t access_ctr;
};
void Screen::some_member() const //声明了mutable后即使在const对象中也可以被修改
{
++access_ctr;
}
3、类数据成员的初始化
class Windows_mgr
{
private:
string a = "123"; //类内初始值,默认构造函数将用这个值来初始化
vector<Screen> screens{Screen(24, 80, ' ')};
}
4、返回*this的成员函数:显然返回this所指对象的引用是左值,因此可以用一句表达式完成一系列操作
inline Screen &Screen::move(pos ht, pos wd)
{
cursor = ht * width + wd;
return *this;
}
inline Screen &Screen::set(char c)
{
contents[cursor] = c;
return *this;
}
inline Screen &Screen::set(pos ht, pos wd, char c)
{
contents[ht * width + wd] = c;
return *this;
}
myScreen.move(4,0).set('#'); //将光标移动到(4,0)然后设置这个位置的字符为'#'
5、基于const重载,区分成员函数是否是const的并且利用私有函数完成实际操作,避免每次都要判定是否是const而调用不同的代码
class Screen
{
public:
Screen &display(ostream &os) { do_display(os); return *this; }
const Screen &display(ostream &os) const { do_display(os); return *this; }
private:
//对non-const对象,this传递进do_display时会隐式转换为指向const的this传递;对于const对象,this直接传递
void do_display(ostream &os) const { os << contents; }
}
Screen myScreen(5,3);
const Screen blank(5,3);
myScreen.set('#').display(cout); //匹配non-const的display
blank.display(cout); //匹配const版本的display
注意:non-const成员函数与const成员函数要完成重复的工作时,除了上述做法,还可以考虑用转型实现
class TextBlock
{
public:
const char &operator[](size_t position) const
{
do_some_things;//复杂的工作
return text[postion];
}
char &operator[](size_t postion)
{
//传入的是non-const的对象*this,使用static_cast转成const的*this再调用const operator[]
//(不加上const会导致递归调用自身)
//调用完成返回的是const char &,使用const_cast将常量引用去掉const,返回char &
return const_cast<char &>(static_cast<const TextBlock &>(*this)[position]);
}
}
四、类的作用域
1、一个类就是一个作用域,因此当在类的外部定义成员函数时需要类名及作用域符
2、对于类类型成员用作用域符::来访问,对于其他成员用成员访问运算符.
Screen::pos ht = 24;
Screen scr(10, 10, '*');
Screen *p = &scr;
char c = scr.get();
c = p->get();
注意:当成员函数返回的类型是在类中定义的时,需要在返回类型前夜加上类名和作用域符
3、为了避免歧义与错误,尽可能避免采用其他成员名字作为成员函数的参数名,这与查找名字的顺序有关
//bad codes
int height; //全局的height
class Screen
{
public:
typedef string::size_type pos;
void dummy_fcn(pos height)
{
cursor = width * height; //此height是函数的参数(pos height)里的height
cursor = width * this->height; //此height代表的是类的成员height
cursor = width * Screen::height; //此height代表的是类的成员height
cursor = width * ::height; //此height代表的是最开始的int height定义的全局height
}
private:
pos cursor = 0, height = 0, width = 0;
}
五、构造函数再探
1、初始化与赋值:对于有些必须有初始化的数据类型,例如引用、常量等,通过赋值来初始化是错误的,必须构造函数初始值列表来完成初始化
class ConstRef
{
public:
ConstRef(int ii) //错误的初始化方式
{
i = ii; //正确
ci = ii; //错误,不能给const变量赋值
ri = i; //错误,ri没有初始化,即未绑定到任何变量上,不能赋值
}
ConstRef(int ii) : i(ii), ci(ii), ri(ii) { } //正确的初始化方式,不通过赋值来初始化
private:
int i;
const int ci;
int &ri;
}
注意:尽可能构造函数初始值列表来完成初始化,不使用赋值初始化
注意:初始化顺序与定义顺序一致,而与初始化列表顺序无关,因此尽可能构造函数初始值列表顺序与定义顺序一致,同时避免用某些成员来初始化另一些成员
2、可以为构造函数提供默认实参来简化过程,当构造函数每个形参都有默认实参时,相当于提供了默认构造函数,不能存在多个默认构造函数,因此不能给每个构造函数都提供所有形参的默认实参,这会造成二义性错误
3、委托构造函数:委托构造函数使用所属类的其他构造函数来完成初始化
class Sales_data
{
public:
Sales_data(string s, unsigned cnt, double price) : bookNo(s), units_sold(cnt), revenue(price) { }
Sales_data() : Sales_data("", 0, 0) { } //委托构造函数
Sales_data(string s) : Sales_data(s, 0, 0) { } //委托构造函数
Sales_data(istream &is) : Sales_data() { read(is, *this); } //嵌套委托的委托构造函数
}
4、隐式类类型转换
(1)如果构造函数只接受一个实参,则可以称为转换构造函数,提供了一种从接受的实参类型到类类型隐式转换的路
string null_book = "9-99-999";
item.combine(null_book); //自动根据Sales_data(const string &)生成临时对象传递给combine
item.combine("9-99-999"); //错误,不会隐式转换成string再隐式转换成Sales_data
item.combine(string("9-99-999")); //正确,显式先将转换成string再隐式转换成Sales_data
(2)抑制隐式转换可以在相应的构造函数前加上explicit
表明需要显式调用这个构造函数,不会隐式转换
注意:explicit只能出现在类内函数声明处,不能出现在类外,且有多个实参的构造函数不需要explicit,explicit函数只能用于直接初始化而不能用于拷贝初始化
item.combine(null_book); //错误,隐式转换被抑制
item.combine(Sales_data(null_book)); //正确,显式初始化
item.combine(static_cast<Sales_data>(null_book)); //正确,可以使用强制类型转换
5、聚合类:所有成员都是public,没有定义任何构造函数,没有类内初始值,没有基类或virtual函数,是字面值常量类的一种
6、字面值常量类
- 数据成员都必须是子面值类型
- 类至少有一个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(b), io(b), other(b) { }
constexpr bool any() { return hw || io || other; }
...
private:
bool hw;
bool io;
bool other;
}
constexpr Debug io_sub(false, true, false);
if(io_sub.any()) cerr << "something" << endl; //等价于if(true)
六、类的静态成员
1、通过在声明前加上static
来表明静态成员,静态成员与类class关联在一起,而存在于每个对象object之外,对象中不包含任何与静态成员相关的数据,静态成员是所有对象共享的,静态成员函数没有this指针绑定某个对象,因此静态成员不在对象创建时初始化
2、访问类的静态成员
(1)作用域运算符直接访问
(2)通过类的对象、引用、指针访问,虽然静态成员不属于类的任何对象,但是可以这样访问
(3)成员函数可以直接使用静态成员,不用作用域运算符
3、定义类的静态成员
(1)在类的外部定义静态成员时需要类名和作用域运算符,而不能重复static
,只在类内声明时出现static
(2)静态成员不属于任何对象,因此不是在对象创建时初始化的,故不是由类的构造函数初始化的
(3)静态成员的初始化通常在类外初始化
- 类外初始化:静态数据成员定义在任何函数之外,且一旦定义就一直存在程序的整个生命周期,类似全局变量
- 类内初始化:只允许静态
const
或constexpr
成员在类内初始化,即使一个静态常量数据成员在类内初始化了也应该再在类外定义,此时不能重复提供初始值
class test
{
public:
string st = "123";
static constexpr int a = 5;
static int b;
};
constexpr int test::a;
int test::b = 5;
4、静态成员的额外特性:静态成员可以是不完全类型(例如可以就是它所属类的类类型,其他成员不可以),静态成员可以作为默认实参而其他成员不可以