类的基本思想是数据抽象和封装;
数据抽象依赖于接口和实现分离的编程技术;类的接口包括用户所能执行的操作;类的实现包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数;
封装实现了类的接口和类的实现的分离;
7.1 定义抽象数据类型
- 定义在类内部的函数是隐式的内联函数inline;
- 成员函数通过一个名为this的额外的隐式参数来访问调用它的对象,当我们用一个成员函数时,用请求该函数的对象地址初始化this;
- this是一个常量指针,所以在成员函数内不允许改变this保存的地址;
- 通常情况下对于一个常量对象,不能直接使用this指针指向它(this的类型为指向类类型的非常量版本的常量指针),因此要对一个常量对象如果需要用this指针的话需要在成员函数列表后面补一个const,用于修改隐式this指针的类型
std::string isbn() {return this->bookNo;} //this的类型是sales_data* const std::string isbn() const{return this->bookNo;} //this的类型是const Sales_data *const
-
当定义的函数类似于某个内置运算符时,应该令函数的行为尽量模仿这个运算符,最常见的情况就是函数的返回值为引用;
-
练习7.5
#include <string> #include <iostream> using namespace std; class Person { private: string name; string address; public: Person(); Person(string n, string add) { name = n; address = add; } string get_name() const; string get_address() const; }; string Person::get_address() const { return this->address; } string Person::get_name() const { return this->name; } int main() { Person p1("xiaoyao", "huanggang"); Person p2("nihao", "wojia"); cout << p1.get_address() << " " << p1.get_name() << endl; }
-
一般来说,如果非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个头文件内,而不是声明在类内部;
-
练习7.9
std::istream& read(std::istream& is, Person& p1) //这里Person前不能加const { is >> p1.name >> p1.address; return is; } std::ostream& print(std::ostream& os, const Person& p2) { os << p2.name << " " << p2.address; return os; }
-
构造函数不能声明称const的,当创建一个类的const对象时,直到构造函数完成初始化过程,对象才能真正取到其“常量”属性,因此,构造函数在const对象的构造过程中可以向其写值;
-
对于构造函数,一旦我们定义了一些其他的构造函数,除非再定义一个默认的构造函数,否则类将没有默认构造函数;只有当类没有声明任何构造函数时,编译器才会自动生成默认构造函数;
-
C++11标准中,如果需要默认的行为,可以在参数列表后面写上 = default来要求编译器生成构造函数
struct sales_data{ sales_data() = default; //生成默认构造函数 sales_data(const std::string &s); //生成含参构造函数 }
-
没有出现在构造函数初始值列表中的成员将通过相应的类内初始值(如果存在的话)初始化,或者执行默认初始化;
-
对象在集中情况下会被拷贝,初始化变量以及以值的方式传递或返回一个对象等;
7.2 访问控制与封装
- 使用class和struct定义类唯一的区别是默认的访问权限,class默认成员为private, struct默认成员为public;
- 友元声明只能出现在类定义的内部,但是在类内出现的具体位置不限,友元不是类的成员也不受它所在区域访问控制级别的约束;友元的声明仅仅指定了访问的权限,而不是一个通常意义上的函数声明;如果希望类的用户能够调用某个友元函数,那么就必须在友元声明之外再专门对函数进行一次声明;
7.3 类的其他特性
- 用来定义类型的成员必须先定义(typedef或者using)后使用,与普通成员有所区别;类型成员通常出现在类开始的地方;
- 在类的成员函数声明和定义的地方不需要同时说明inline,但这么做是合法的,不过最好还是只在类外部定义的地方说明inline;
- 可变数据成员声明前加上mutable关键字,其永远不会是const,即使它是const对象的成员,因此一个const成员函数可以改变一个可变成员的值
class Screen { public: void some_member() const; private: mutable size_t access_ctr; }; void Screen:some_member() const { ++access_ctr; //保存一个计数值,用于记录成员函数被调用的次数 }
-
提供一个类内初始值时,必须以符号=或者花括号表示
class Windos_mgr { private: std::vector<Screen> screens = {Screen(24,80,' '}; std::vector<Screen> screens{Screen(24,80,' '}; //等价的声明,用花括号初始化 };
-
练习7.25,是可以依赖的,默认的拷贝和赋值操作是简单的将类内成员复制一遍,对于非指针成员是没有影响的,当涉及到有的类内成员是指针的话,这样就是一个浅复制,新赋值的对象的指针跟原对象的指针指向同一个内存,这显然不是我们最初的目的,代码如下,这里s1,s2能输出相同的结果
class Screen { private: using pos = string::size_type; pos width = 0; pos height = 0; string contents; public: Screen() = default; Screen(pos wid, pos hght, char c): width(wid), height(hght),contents(wid*hght,c){} Screen(pos wid, pos hght):width(wid),height(hght),contents(wid*hght,' '){} void print(); }; void Screen::print() { cout << this->contents << endl; } int main() { Screen s1(4, 8, 'a'); Screen s2 = s1; s1.print(); cout << endl; s2.print(); cout << endl; return 0; }
-
一个const成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用,在一些对对象的连续函数调用中可能会出现问题
Screen myScreen; myScreen.display().set('*'); //如果display返回的是常量引用,则不能通过set来改变其值;
-
一个成员函数出现了两个const,表示的含义是不同的
const Screen& display(ostream &os) const;
在screen前面的const表示该函数返回值是const,只能作为右值,无法更改;在参数列表后面的const表示该函数调用的是常量对象,对象的成员无法在函数内被更改;
-
练习7.27, display函数中必须要有两个const,因为后面一个const指定了调用的对象为常量对象,因此返回的必须是常量对象,否则会报错
Screen& Screen::move(pos wid, pos hght) { cur = hght * width + wid; return *this; } const Screen& Screen::display(ostream& os) const { os << contents; return *this; } Screen& Screen::set(pos wid, pos hght,char c) { contents[hght * width + wid] = c; return *this; } Screen& Screen::set(char c) { contents[cur] = c; return *this; }
-
一旦一个类的名字出现后,它就被认为是声明过了(但尚未定义),因此类允许包含指向它自身类型的引用或指针;
-
每个类负责控制自己的友元类或友元函数,友元关系不存在传递性;
-
友元声明的作用仅仅是影响访问权限,并非普通意义上的声明,当我们使用类的成员来调用声明在类内的友元函数,该友元函数也必须在类外声明过才可以调用(有的编译器不强制执行该限制规则,但是还是按C++标准操作来比较稳妥)
struct X{ friend void f(){ } X() {f();} //错误,f还没有声明 void g(); void h(); }; void X::g() {return f();} //错误,f还没有声明 void f(); //f在类外声明 void X::h() {return f();} //正确,f在类外已被声明
7.4 类的作用域
-
函数的返回类型通常出现在函数名之前,因此当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外,这时,返回类型必须指明它是哪个类的成员;详见283页例子
-
类型名的定义(typedef, using)通常出现在类的开始处,这样就能确保所有使用该类型的成员都出现在类名的定义之后;
7.5 构造函数再探
-
如果类的成员是const 、引用,或者属于某种未提供默认构造函数的类类型,必须通过构造函数初始值列表为这些成员提供初值而不能使用赋值语句块;优先使用构造函数初始值列表的形式来给类的成员初始化;
-
初始值列表只用于初始化成员的值,而不限定初始化的具体执行顺序;初始化顺序与在类定义中出现的顺序一致; 最好令构造函数的初始值的顺序与成员声明的顺序保持一致;而且如果可能的话,尽量避免使用某些成员初始化其他成员;
-
默认构造函数是没有显示提供初始化式时调用的函数,并不是说参数列表为空,可以是所有的形参提供了默认实参;
-
能通过一个实参调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则;但是只允许一步类类型转换;需要多个实参的构造函数不能用于执行隐式转换,单个实参的构造函数如果强调不能执行隐式转换的话可以在函数前加关键字explicit,explicit只能在类内声明构造函数时使用,在类外部定义时不应重复,用explicit声明构造函数时,它将只能以直接初始化的形式使用(即圆括号())而不能用拷贝形式初始化(即赋值等号=);
-
聚合类的特殊初始化语法,详见267页;
7.6 类的静态成员
- 静态成员与静态成员函数不与任何对象绑定在一起,也不包含this指针。静态成员函数不能声明成const的;
- 可以在类的内部和外部定义静态成员函数,在外部定义时不能重复static关键字,该关键字只出现在类内部的声明语句
- 一般来说,不能在类的内部初始化静态成员,必须在类的外部定义和初始化每个静态成员,而且一个静态数据成员只能定义一次,否则会报重复定义的错误;
- 静态数据成员可以是不完全类型,即它可以是自己所属的类的类型,而非静态数据成员就受到限制,只能声明成它所属的类的指针或引用;