7.1定义抽象数据类型
成员函数对类成员变量的访问 是通过 隐式地使用this指向的成员的方式
this->membervar;
对于我们来说,this形参时隐士定义的。实际上,任何自定义名为this的参数或变量的行为都是非法的。
因为this的目的总是指向“这个”对象,所以this是一个常量指针,不允许改变this中保存的地址。
std::string isbn() const { return this->bookNo;}
这里,const的作用是修改隐式this指针的类型 将this变成指向常量的常量指针。
默认情况下,this的类型是指向类类型非常量版本的的常量指针。例如在Sales_data成员函数中,this的类型是Sales_data *const 。尽管this是隐式的,但它仍然需要遵循初始化规则,意味着(在默认情况下)我们不能把this绑定到一个常量对象上,这一情况也就是的我们不能在一个常量对象上调用普通的成员函数。
类作用域和成员函数
即使成员函数定义在成员变量之前,成员函数依然可以使用成员函数,因为在编译器中,首先编译成员的声明,然后才轮到成员函数体。因此,成员函数体可以随意使用类中的其他成员而无须在意成员出现的次序。
7.1.4 构造函数
不同于其他成员函数,构造函数不能被声明成const的,当我们创建类的一个const对象是,直到构造函数完成初始化过程,对象才能真正去的其“常量”属性。因此,构造函数在const对象的构造过程中可以向其写值。
类通过一个特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数。默认构造函数无须任何实参。
编译器创建的构造函数又被称为合成的默认构造函数,对于大多数类来说,这个合成的默认构造函数将按照如下规则初始化类的数据成员。
1.如果存在类内的初始值,用它来初始化成员。(类内初始值就只在定义成员时给定一个初始值)
2.否则,默认初始化该成员
某些类不能依赖于合成的默认构造函数 ,因为对于某些类来说,合成的默认构造函数可能执行错误的操作,因为定义在块中的内置类型或复合类型(比如指针和数组)的对象被默认初始化,则他们的值是未定义的,该准则同样适用于默认初始化的内置类型成员。因此,含有内置类型或复合类型成员的类应该在类的内部初始化这些成员,或者定义一个自己的默认构造函数。否则,用户在创建类的对象时就可能得到未定义的值。
提示: 如果类包含有内置类型或者复合类型的成员,则只有当这些成员全部被赋予了类内的初始值时,这个类才适合于使用合成的默认构造函数。
7.2.1友元
友元声明只能出现在类定义的内部,但是在类内出现的具体位置不限。友元不是类的成员,也不受它所在的区域访问控制级别的约束。
一般来说,最好在类定义开始或结束前的位置集中声明友元。
7.3 类的其他特性
class Screen{
public:
typedef std::string::size_type pos;
// using pos = std::string::size_type;
private:
pos cursor = 0;
pos height = 0, width = 0;
std::string contents;
};
上面的 typedef std::string::size_type pos; 表明 除了定义数据和函数成员之外,类还可以自定义某种类型在类中的别名。由类定义的类型名字和其他成员一样存在访问权限,可以是public或者private中的一种。
用来定义类型的成员必须先定义后使用,这一点与普通成员有所区别,因此,类型成员通常出现在类开始的地方。
可变数据成员
一个可变数据成员永远不会是const ,即使它是const对象的成员。因此,一个const成员函数可以改变一个可变成员的值。比如:
class Screen{
public:
void some_member() const;
private:
mutable size_t access_ctr;
};
void Screen::some_member() const
{
++access_ctr;
}
尽管在以上代码中some_memer 是一个const成员函数,它仍然能够改变access_ctr的值。该成员是个可变成员,因此任何成员函数,包括const函数在内都能改变它的值。
类内初始值 必须使用=的初始化形式 或者 花括号括起来的直接初始化形式。
比如 vector<int> vec{1}; 或者vector<int> vec= vetor<int>(1);
基于const的重载
class Screen{
public:
//
Screen &display(std::ostream &os)
{ do_display(os);return *this;}
const Screen &display(std::ostream &os) const
{ do_display(os);return *this;}
private:
void do_display(std::ostream &os) const
{ os << contents;}
};
类的声明
就像可以把函数的声明和定义分离开来一样,也能仅仅声明类而暂时不定义它。
class Screen; // Screen 的声明
这种声明有时被称作向前声明,对于类型Screen来说在它声明之后定义之前是一个不完全类型,也就是说,此时我们已知Screen 是一个类类型,但是不清楚它到底包含哪些成员。
不完全类型只能在非常有限的情境下使用,可以定义指向这种类型的指针或引用,也可以声明(但是不能定义)以不完全类型作为参数或者返回类型的函数。
对于一个类来说,在我们创建它的对象之前该类必须定义过,而不能仅仅被声明。否则,编译器就无法了解这样的对象需要多少存储空间。类似的,类也必须首先被定义,然后才能用引用或者指针访问其成员。因为如果类尚未定义,编译器也就不清楚该类到底有哪些成员。
一旦一个类的名字出现后,它就被认为是声明过了(但尚未定义),因此类允许包含指向它自身类型的引用或指针。
如:
class Link_screen{
Screen window;
Link_screen *next;
Link_screem *prev;
};
7.3.4 友元再探
类可以把普通的非成员函数定义成友元函数,还可以把其他类定义为友元,也可以把其他类(之前已定义过的)的成员函数定义成友元。此外,友元函数能定义在类的内部,这样的函数是隐式内联的。
如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。
友元关系不存在传递性。每个类负责控制自己的友元类或友元函数。
如下代码:
class Screen{
// Window_mgr::clear 必须在Screen 类之前被声明
friend void Window_mgr::clear(ScreenIndex);
// Screen 类的剩余部分
};
如上代码在Screen 类中将Window_mgr::clear(ScreenIndex) 函数定义为Screen的友元类,在需要这样定义时我们必须仔细组织程序的结构以满足声明和定义的彼此依赖关系。在这个例子中我们必须按照如下方式设计程序:
首先定义Window_mgr 类,其中声明clear函数,但是不能定义(因为需要用到的Screen类还没有定义),在clear使用Screen的成员之前必须先声明Screen
接下来定义Screen,包括对于clear的友元声明。
最后定义clear,此时它才能使用Screen的成员。
友元声明和作用域
类和非成员函数的声明不是必须在他们的友元声明之前,当一个名字第一次出现在一个友元声明中,我们隐式第假定该名字在当前作用域中是可见的。然而,友元本身不一定真的声明在当前作用域中
甚至就算在类的内部定义该函数,我们也必须在类的外部提供相应的声明从而使得函数可见。换句话说,即使我们仅仅是用声明友元的类的成员调用该友元函数,它也必须是被声明过的。
struct X
{
friend void f(){}
X(){f();}
void g();
void h();
};
void X::g(){return f();}
void f(); // 如果没有这句话 所有调用到f() 的地方都会报错 该友元函数的声明没有文本代码位置限制
void X::h(){return f();}
函数重载和友元
尽管重载函数的名字相同,但它们仍然是不同的函数。因此,如果一个类想把一组重载函数声明成它的友元,它需要对这组函数中的每一个分别声明。
extern std::ostream& storeOn(std::ostream& ,Screen&);
extern BitMap& storeOn(BitMap &,Screen&);
class Screen{
// storeOn 的 ostream 版本能访问Screen对象的私有部分
friend std::ostream& storeOn(std::ostream&, Screen&);
};
Screen 类能把接受ostream&的storeOn 函数声明成它的友元,但是接受BitMap& 作为参数的版本仍然不能访问Screen。
7.4.1 名字查找与类的作用域
一般的名字查找的过程:
*首先,在名字所在的块中寻找其声明语句,只考虑在名字的使用之前出现的声明
* 如果没找到,继续查找外层的作用域
*如果最终没有找到匹配的声明,则程序报错
对于定义在类内部的成员函数来说,解析其中名字的方式与上述的查找规则有所区别,
类的定义分两步处理:
*首先,编译成员的声明
*直到类全部可见后才编译函数体
tips :编译器处理完类中的全部声明后才会处理成员函数的定义
用于类成员声明的名字查找
以上的名字查找只是限于在函数中使用类中定义的成员变量,如果成员函数中使用了类内声明的类型,那么必须保证在函数声明之前该类型的声明已经存在。
声明中使用的名字,包括返回类型或者参数列表中使用的名字,都必须在使用前确保可见。如果某个成员的声明使用了类中尚未出现的名字,则编译器将会在该类的作用域中继续查找。例如:
typedef double Money;
string bal;
class Account{
public:
Money balance(){return bal;}
private:
Money bal;
};
当编译器看到balance函数的声明语句时,它将在Account类的范围内寻找Money的声明。编译器只考虑Account中在使用Money前出现的声明,因为没有找到匹配的成员,所以编译器会接着到Account的外层作用域中查找。在这个例子中,编译器会找到Money的typedef语句,该类型被用作balance函数的返回值类型以及数据成员bal的类型。另一方面,balance函数体在整个类可见后才被处理,因此,该函数return语句返回名为bal的成员,而非外层作用域的string对象。
类型名要特殊处理
一般来说,内层作用域可以重新定义外层作用域的名字,即使该名字已经在内层作用域使用过。然而在类中,如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字:
typedef double Money;
class Account {
public:
Money balance(){ return bal;} // 使用外层作用域的Money
private:
typedef double Money; // 不能重新定义Money(c++ primer书上说的)(但是visual
Money bal; // studio 2019 上面编译通过)
};
成员定义中的普通快作用域的名字查找
成员函数中使用的名字按照如下方式查找:
*首先,在成员函数内查找该名字的声明。
*如果成员函数内没有找到,则在类内继续查找,这时类的所有成员都可以被考虑。
*如果类内也没找到改名字的声明,在成员函数定义之前的作用域内继续查找。
7.5.1 构造函数初始值列表
如果没有在构造函数的初始值列表中显示地初始化成员,则该成员将在构造函数体之前执行默认初始化。例如:
Sales_data::Sales_data(const string &s,unsigned cnt,double price)
{
bookNo = s;
units_sold = cnt;
revenue = cnt * price;
}
上面的Sales_data类的构造函数中,并没有在初始值列表中显示地初始化成员,其实不管有没有编译器已经做了,所以在函数体中对类的成员的操作只是赋值并非是初始化操作。
tips: 如果成员是 const、引用,或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初始值。
使用构造函数初始值(使用构造函数初始值列表)
在很多类中,初始化和赋值的去呗事关底层效率问题:前者直接初始化数据成员,后者则先初始化在赋值。
成员初始化顺序:
成员的初始化顺序与它们在类定义中出现的顺序一致;第一个成员先被初始化,然后第二个,以此类推,构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺。
只允许一步类类型转换
编译器只会自动执行一步类型转换。
为转换显示地使用构造函数