CPP 17. 输入、输出和文件 2020-12-03 浏览量 565 暂无评论 # 第17章 输入、输出和文件 ## 17.4 文件输入和输出 ```cpp ofstream fout; fout.open("jar.txt"); // 默认模式打开文件进行输出,如果没有这样的文件,将创建一个新文件, // 如果有这样的文件,则打开文件将清空文件 // 可以通过ios_base::xx设置模式 // ofstream fout("jar.txt"); fout << "Dull Data"; ifstream fin; fin.open("jar.txt"); // ifstream fin("jar.txt") // 检测文件是否打开成功 if (fin.fail()) {...} if (!fin.good()) {...} if (!fin) {...} if (fin.is_open()) {...} // best ``` - 阅读全文 -
CPP 16. String类和标准模板库 2020-12-03 浏览量 604 暂无评论 # 第16章 String类和标准模板库 - 标准C++ string类 - 模板auto_prt, unique_ptr, shared_ptr--智能指针 - 标准模板库(STL)--泛型编程 - 容器类 - 迭代器 - 函数对象(functor) - STL算法 - 模板initializer_list ## 16.1 string类 ### 16.1.1 构造字符串 ### 16.1.2 string类输入 C-风格字符串 ```cpp char info[100]; cin >> info; cin.getline(info, 100); // discard \n cin.get(info, 100); // leave \n in queue ``` string对象 ```cpp string stuff; cin >> stuff; - 不断读取,直到遇到使isspace()为true的空白字符并将其留在输入队列中 getline(cin, stuff); ``` 两个版本的getline()都有一个可选参数,用于指定使用哪个字符来确定输入的边界; ```cpp cin.getline(info, 100, ':'); getline(info, ':'); // 自动调整目标string对象的大小 - 到达文件尾,设置输入流的eofbit,fail()和eof()都将返回true - 遇到分解字符(默认为\n) - 读取的字符数达到最大允许值,设置输入流的failbit,fail()返回true ``` ### 16.1.3 使用字符串 1. 6个关系运算符 2. 长度,size(), length() 3. 搜索,find(), rfind(), find_first_of(), find_last_of(), find_first_not_of(), find_last_not_of() ```cpp size_type find(const string & str, size_type pos = 0) const size_type find(const char * s, size_type pos = 0) const size_type find(const char * s, size_type pos = 0, size_type n) size_type find(char ch, size_type pos = 0) const ``` 4. 删除 5. 替换 6. 插入 7. 当前分配给字符串的内存块的大小,capacity() 8. 请求内存块的最小长度,reserve() 9. 指向C-风格字符串的指针,c_str() ## 16.2 智能指针模板类 思想:定义类似指针的对象,当智能指针过期时,其析构函数将使用delete来释放内存。 有:auto_ptr, unique_ptr, shared_ptr,后两个是C++11的,摒弃了第一个,因为auto_ptr可能会出现所有权丢失的问题:当使用auto_ptr指向已有auto_ptr时,原来的所有权就会丢失。 ```cpp #include void remodel(std::string & str) { std::auto_prt ps (new std::string(str)) if (weird_thing()) throw exception(); str = *ps; return; } // 所有权丢失,好处:防止删除一个对象两次,坏处:无法再使用p1 auto_ptr p1 (new string("auto")); auto_ptr p2; p2 = p1; // 当把auto_ptr换成shared_ptr时,不会丢失所有权,而是将引用次数加1 // 当把auto_ptr换成unique_ptr时,p2 = p1将非法 ``` ## 16.3 标准模板库 ### 16.3.1 模板类vector 容器、迭代器 ```cpp vector scores; vector::iterator pd; // auto pd = scores.begin(); 更简洁 pd = scores.begin(); *pd = 22.3; ++pd; for (pd = scores.begin(); pd != scores.end(); pd++) cout << *pd << endl; // 一些方法 push_back(): 将元素添加到元素末尾 erase(): 删除矢量中给定区间的元素,左闭右开 insert(): 向一个容器的某个位置插入另一个容易对象的某个区间元素 vector old_v; vector new_v; old_v.insert(old_v.begin(), new_v.begin() + 1, new_v.end()); ``` STL函数 ```cpp for_each(): 将被指向的函数应用于容器区间中的各个元素,很像python的map呀 random_shuffle(): 随机排列容器区间中的元素 sort(): 两个版本 一个默认使用为存储在容器中的类型元素定义的<运算符,升序 如果容器是用户定义的,则需要提供成员或非成员函数operator<() 另一个版本接受三个参数,前两个同上,为区间,第三个为自定义的排序规则函数名称,返回值为bool ``` ### 16.3.2 基于范围的for循环(C++11) ```cpp double prices[3] = {4.55, 234.3, 23.3}; for (double x : prices) // 括号内的代码声明一个类型与容器存储的内容相同的变量 cout << x << std::endl; for_each(books.begin(), bookd.end(), ShowReview); 类似于 for (auto x : books) ShowReview(x); 但是基于范围的for循环可以修改容器的内容,只要指定一个引用参数 void InflateReview(Review &r){r.rating++;} for (auto x : books) InflateReview(x); ``` ## 16.4 泛型编程 面向对象编程关注编程的数据方面,而泛型编程关注的是算法,都是抽象和创建可重用代码。 模板使得算法独立于存储的数据类型,迭代器使算法独立于使用的容器类型。 ### 16.4.1 迭代器的基本特征 1. 可对迭代器执行解除引用操作,即定义*p; 2. 可将一个迭代器赋给另一个,即定义p=q; 3. 可比较迭代器,即定义p==q,p!=q; 4. 可通过迭代器遍历容器中的所有元素,即定义++p和p++。 **5种迭代器:** 输入迭代器:读取,单向,可以递增,但不能倒退,每次迭代的顺序不一定相同 输出迭代器:只写,单向 正向迭代器:读写,总是按相同的顺序遍历一系列值,使用++运算符 双向迭代器:具有正向迭代器的所有特性,还可-- 随机访问迭代器:具有双向迭代器的所有特性,还可使用a[10] ```cpp typedef double * iterator; iterator find_ar(iterator begin, iterator end, const double & val) { iterator ar; for (ar = begin; ar != end; ar++) if (*ar == val) return ar; return end; } ``` 容器有一些基本的特征和要求。 下面就是一些容器类型和用法。 1. vector,强调快速访问 2. deque,双端队列 3. list,强调快速插入和删除,它表示双向链表 4. list工具箱 5. forward_list 6. queue,适配器类 7. priority_queue,适配器类,与queue的区别在于,它将最大的元素移到队首 8. stack,适配器类 9. array,不是STL容器,因为它的长度是固定的 **关联容器:基于树结构** 1. set,可翻转,经过排序 2. multiset, 3. map, 4. multimap,与set类似,但键和值的类型不同,且同一个键可能与多个值相关联 ```cpp #include #include typedef int KeyType; typedef std::pair Pair; typedef std::multimap MapCode; int main() { using namespace std; MapCode codes; codes.insert(Pair(415, "Beijing")); codes.insert(Pair(110, "Nanjing")); // Number of cities with area code 415 codes.count(415) MapCode::iterator it; for (it = codes.begin(); it != codes.end(); ++it) cout << " " << (*it).first << " " << (*it).second << endl; pair range = codes.equal_range(110); for (it = range.first; it != range.second; ++it) cout << (*it).second << endl; return 0; } ``` 无序关联容器:基于哈希表 ## 16.5 函数对象 函数对象,也叫函数符(functor),包括**函数名、指向函数的指针和重载了()运算符的类对象**,它们都可以以函数方式与()结合使用。 STL通过模板解决for_each处理容器中不同类型数据的问题。 ```cpp template // 这就是为什么for_each无法修改 Function for_each(InputIterator first, InputIterator last, Function f); ``` 头文件functional定义了多个模板类函数对象 ![](http://lcipm.com/Jineng/picture-bed/uploads/5b0696531040b6fc657bbffe1a54f9c9/20201201223350.png) **自适应函数符和函数适配器** 自适应生成器、自适应一元函数、自适应二元函数、自适应谓词、自适应二元谓词 假设有一个自适应二元函数对象f2(),则可以创建一个binder1st对象,使之与第一个参数相关联 ```cpp binder1st(f2, val) f1; 这样,使用f1(x)时等价于f2(val, x) 简化版:不需要f2为自适应函数 bind1st(multiplies(), 2.5) ``` ## 16.6 算法 ### 16.6.1 算法组 STL将算法库分成4组: 头文件algorithm - 非修改式序列操作; - 修改式序列操作; - 排序和相关操作; 头文件numeric - 通用数字运算。 就地算法(in-place algorithm):sort() 复制算法(copying algorithm):copy() transform()可以以这两种方式完成工作。 ## 16.7 其他库 vector, valarray, array initializer_list vector是一个容器类和算法系统的一部分,valarray更适合于数组计算,array为替代内置数组而设计,提供更好、更安全的接口,让数组更紧凑、效率更高。 - 阅读全文 -
CPP 15. 友元、异常和其他 2020-11-29 浏览量 583 暂无评论 # 第15章 友元、异常和其他 - 友元类 - 友元类方法 - 嵌套类 //上述都是在其他类中声明的类 - 引发异常、try块和catch块 - 异常类 - 运行阶段类型识别(RTTI) - dynamic_cast和typeid - static_cast、const_cast和reiterpret_cast ## 15.1 友元类 友元类适用于两个类既不是继承关系,又不是包含关系,但两个类又有关系的情况,如遥控器和电视机。 ```cpp // tv.h #ifndef TV_H_ #define TV_H_ class Tv { public: friend class Remote; // Remote可以使用Tv的私有成员 enum {Off, On}; enum {MinVal, MaxVal = 20}; enum {Antenna, Cable}; enum {TV, DVD}; Tv(int s = Off, int mc = 125) : state(s), volume(5), maxchannel(mc), channel(2), mode(Cable), input(TC) {} void onoff() {state = (state == On) ? Off : On;} // void onoff() {state ^= 1;} bool ison() const {return state == On;} bool volup(); bool voldown(); void chanup(); void chandown(); void set_mode() {mode = (mode == Antenna) ? Cable : Antenna;} void set_input() {input = (input == TV) ? DVD : TV;} void setting() const; private: int state; int volume; int maxchannel; int channel; int mode; int input; }; class Remote { ... }; ... ``` **友元成员函数:** 仅让特定的类成员成为另一个类的友元。 ```cpp // 前向声明,因为Remote的方法提到了Tv对象,且Tv对象中也有Remote方法的声明 // 在Tv类的声明中看到Remote的一个方法被声明为Tv类的友元之前,应该先看到Remote类 // 的声明和set_chan()方法的声明;同理,Remote类声明中不要有内联代码,将方法的 // 定义放在Tv类声明之后,并使用关键字inline使其成为内联方法 class Tv; class Remote {...}; class Tv { friend void Remote::set_chan(Tv & t, int c); // 这样Remote类的set_chan成员就可以访问Tv类的私有成员 }; ``` **其他友元关系:** 两个类互为友元 对于使用Remote对象的Tv方法,其原型可在Remote类声明之前声明,但必须在Remote类声明之后定义。 ```cpp class Tv { friend class Remote; public: void buzz(Remote & r); ... }; class Remote { friend class Tv; public: void Bool volup(Tv & t) {t.volup();} ... }; inline void Tv::buzz(Remote & r) { ... } ``` **共同的友元:** ```cpp // 前向声明 class Analyzer; class Probe { friend void sync(Analyzer & a, const Probe & p); friend void sync(Probe & p, const Analyzer & a); ... } class Analyzer { friend void sync(Analyzer & a, const Probe & p); friend void sync(Probe & p, const Analyzer & a); ... }; inline void sync(Analyzer & a, const Probe & p) { ... } inline void sync(Probe & p, const Analyzer & a) { ... } ``` ## 15.2 嵌套类 在另一个类中声明的类被称为嵌套类(nested class)。包含类的成员函数可以创建和使用被嵌套类的对象,而仅当声明位于共有部分,才能在包含类的外面使用嵌套类,而且必须使用作用域解析运算符。 什么时候需要使用嵌套类:通常是为了帮助实现另一个类,并避免名称冲突,使用类来存放变量可以利用类构造函数方便地赋值。 #### 15.3 异常 1. throw语句将控制权向上返回到第一个这样的函数:包含能够捕获相应异常的try-catch组合。 2. 引发异常时编译器总是创建一个临时拷贝。 3. catch块的排序顺序应该与异常类派生顺序相反,因为引发的异常对象将被第一个与之匹配的catch块捕获。 ![](http://lcipm.com/Jineng/picture-bed/uploads/2c3fcc85d5010595420028c5c417529e/20201128223836.png) 4. 可以使用省略号来表示异常类型,从而捕获任何异常。 ```cpp #include std::abort(); // 发送消息:abnormal program termination,终止程序 std::exit(); // 不发送消息,终止程序 // 更希望遇到异常时进行一些处理,而不是直接终止程序,这时候可以返回错误码或者 // throw引发异常并用catch捕获异常,try后面可以跟好几个catch与对应异常类型匹配 // 如果没找到对应的catch,默认异常终止 // 程序进行栈解退以回到能够捕获异常的地方时,将释放栈中动存储型变量。 // 如果变量是类对象,将为该对象调用析构函数。 #include double hmean(double a, double b); int main() { double x, y, z; std::cout << "Enter two numbers: "; while (std::cin >> x >> y) { try { z = heamn(x, y); } catch (const char * s) // 与字符串异常匹配 { std::cout << s << std::endl; std::cout << "Enter a new pair of numbers: "; continue; } std::cout << "Harmonic mean of " << x << " and " << y << " is " << z << std::endl; std::cout << "Enter next set of numbers. : "; } std::cout << "Bye!\n"; return 0; } double hmean(double a, double b) { if (a == -b) throw "some description"; return 2.0 * a * b / (a + b); } ``` **exception类** 可以将exception类用作异常的基类,里面有个名为what()的虚拟成员函数,它返回一个字符串。 一些基于exception的异常类型。 1. stdexcept异常类 这些类中都有一个可以为what()提供返回字符串的构造函数。 两个派生类系列的基类: a. logic_error系列:可通过编程修复 - domain_error:定义域 - invalid_argument:有效参数 - length_error:空间 - out_of_bounds:索引错误 b. runtime_error系列:存在无法避免的问题 - range_error: - overflow_error:上溢 - underflow_error:下溢 2. bad_alloc异常和new 当使用new出现内存分配问题时,以前是返回一个空指针,现在是让new引发bad_alloc异常。 3. 空指针和new 为了兼容以前的返回空指针的new ```cpp int * pi = new (std::nothrow) int; ``` 异常的意外情况: 意外异常:不与规范列表中的某种异常匹配,先调用unexpected()函数,这个函数将调用terminate()函数,可修改,set_unexpected(func_name) 未捕获异常:不会导致程序立刻终止,而是先调用函数terminate(),默认情况下,terminate()调用abort()函数,可修改,set_terminate(func_name) 可以设计一个替代函数,将意外异常转换为bad_exception异常 ```cpp #include using namespace std; void myUnexpected() { throw std::bad_exception(); // 仅使用throw,而不指定异常将导致重新引发原来的异常 } ``` ## 15.4 RTTI(Runtime Type Identification) 为什么会出现RTTI?想知道类型,那为什么要知道类型呢?1. 派生对象可能包含不是继承而来的方法,只有某些类型的对象可以使用该方法;2. 想跟踪生成的对象的类型。 **注意:只能将RTTI用于包含虚函数的类层次结构,因为只有对于这种类层次结构,才应该将派生对象的地址赋给基类指针。** 如何支持RTTI? 1. 如果可能的话,dynamic_cast运算符,使用一个指向基类的指针来生成一个指向派生类的指针,否则,该运算符返回0--空指针; ```cpp // 指针pg的类型是否可以被安全地转换为Superb *? // 可以的话,就返回对象的地址 // 不可以的话,返回空指针 Superb * pm = dynamic_cast(pg); ``` 2. typeid返回一个指出对象的类型的值; ```cpp // 头文件typeinfo // typeid运算符使得能够确定两个对象是否为同种类型 // 接受两种参数:类名;结果为对象的表达式 // 返回一个对type_info对象的引用,包含一个name()成员 typeid(Magnificent) == typeid(*pg) // 如果pg是一个空指针,程序将引发bad_typeid异常 ``` 3. type_info结构存储了有关特定类型的信息 ## 15.5 类型转换运算符 ![](http://lcipm.com/Jineng/picture-bed/uploads/674f7007b4be6d7e7498d4d8eeb444aa/20201129214253.png) 🐸:给爷看笑了。 出现的原因:C语言中的类型转换运算符太过松散。 4个类型转换运算符:dynamic_cast; const_cast; static_cast; reinterpret_cast。 1. dynamic_cast ```cpp // 假设High和Low是两个类,ph和pl的类型分别为High*和Low* // 则仅当Low是High的可访问基类时,下面的语句才将一个Low*指针赋给pl // 这个指针指向谁? pl = dynamic_cast ph; ``` 2. const_cast ```cpp // 用于执行只有一种用途的类型转换,即const或volatile const_cast (expression) // 除了const或volatile特征可以不同外,type_name和expression的类型必须相同 High bar; const High * pbar = &bar; High * pb = const_cast (pbar); // 使*pb成为一个可用于修改bar对象值的指针 // 只有当指向的值不是const时才可行 ``` 3. static_cast ```cpp // 仅当type_name可被隐式转换为expression所属的类型或反过来时,转换才是合法的 static_cast (expression) ``` 4. reinterpret_cast ```cpp // 用于天生危险的类型转换 struct dat {short a; short b;}; long value = 0xA224B11B; dat * pd = reinterpret_cast (&value); cout << hex << pd->a; // 显示value的前2个字节 ``` - 阅读全文 -
CPP 14. C++中的代码重用 2020-11-26 浏览量 546 暂无评论 # 第14章 C++中的代码重用 🐸小知识: 1. 获得接口是is-a关系的组成部分,不继承接口是has-a关系的组成部分; 2. 当初始化列表包含多个项目时,这些项目被初始化的顺序为它们被声明的顺序,而不是它们在初始化列表中的顺序; 3. 实现has-a关系的途径:包含、私有继承; 4. 通常,使用包含来建立has-a关系,如果需要访问原有类的保护成员,或需重新定义虚函数,则使用私有继承; ## 14.1 包含对象成员的类 **valarray模板类** ```cpp double gpa[5] = {3.1, 3.5, 3.8, 2.9, 3.3}; valarray v1; valarray v2(8); valarray v3(10, 8); // 8个整型元素,每个都为10 valarray v4(gpa, 4); // 4个双浮点型,gpa前四个 operator[]() size() sum() max() min() ``` ## 14.2 私有继承 使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员,派生类不继承基类的接口。 使用强制类型转换来访问基类对象 ```cpp class Student : private std::string, private std::valarray { public: ... }; ``` ### 14.2.1 保护继承:保护继承是私有继承的变体 使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员,这样就可以在第三代派生类中使用基类的公有方法。 ![](http://lcipm.com/Jineng/picture-bed/uploads/ab2330cd4dc16379476536567505fa82/20201124194025.png) ### 14.2.2 使用using重新定义访问权限 目的:让基类的方法在派生类外面可用 1. 定义一个使用该基类方法的派生类方法; ```cpp double Student::sum() const { return std::valarray::sum(); } ``` 2. 使用一个using声明来指出派生类可以使用特定的基类成员 ```cpp class Student : private std::string, private std::valarray { public: using std::valarray::min; using std::valarray::max; }; ``` ## 14.3 多重继承(MI) 需要处理的问题: 1. 从两个或更多相关基类那里继承同一个类的多个实例; ```cpp SingingWaiter ed; Worker * pw = &ed; 通常,这种赋值将把基类指针设置为派生类对象中的基类对象的地址,但ed中包含两个Worker对象,有两个地址可供选择,所以应使用类型转换来指定对象;(这样将会很复杂) Worker * pw1 = (Waiter *) &ed; Worker * pw2 = (Singer *) &ed; ``` 幸好,C++引入多重继承的同时,引入了一种新技术--虚基类,它使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。 ```cpp class Singer : virtual public Worker {...}; class Waiter : public virtual Worker {...}; ``` 这样做会出现一个问题:派生类构造函数在向基类传递参数时,由于有2条不同的途径,会产生冲突,所以C++在基类是虚的时,禁止信息通过中间类自动传递给基类。 ```cpp SingingWaiter(const Worker & wk, int p = 0, int v = Singer::other) : Waiter(wk, p), Singer(wk, v) {} 上构造函数将初始化Waiter的成员panache和Singer的成员voice,但wk参数中的信息不会传递给子对象Waiter,这时,编译器将使用Worker的默认构造函数。 SingingWaiter(const Worker & wk, int p = 0, int v = Singer::other) : Worker(wk), Waiter(wk, o), Singer(wk, v) ``` 2. 从两个不同的基类继承同名方法 a. 使用作用域解析运算符 ```cpp SingingWaiter newhire("jhan", 2005, 6, soprano); newhire.Singer::Show(); ``` b. 重新定义 ```cpp void SingingWaiter::Show() { Singer::Show(); } ``` ## 14.4 类模板 生成通用的类声明。模板的具体实现被称为实例化或具体化。 如果在类声明中定义了方法(内联定义),则可以省略模板前缀和类限定符。 ```cpp // stacktp.h -- a stack template #ifndef STACKTP_H_ #define STACKTP_H_ template class Stack { private: enum {MAX = 10}; Type items[MAX]; int top; public: Stack(); bool isempty(); bool isfull(); bool push(const Type & item); bool pop(Type & item); }; template Stack::Stack() { top = 0; } template bool Stack::isempty() { return top == 0; } template bool Stack::isfull() { return top == MAX; } template bool Stack::push(const Type & item) { if (top < MAX) { items[top++] = item; return true; } else return false; } template bool Stack::pop(Type & item) { if (top > 0) { item = items[--top]; return true; } else return false; } // stackem.cpp -- testing the template stack class #include #include #include #include "stacktp.h" using std::cin; using std::cout; int main() { Stack st; char ch; std::string po; ... } ``` 其他类型的模板头: ```cpp 1. template 第二个参数称为非类型或表达式参数,表达式参数可以使整型、枚举、引用或指针。 缺点:每种数组大小都生成自己的模板,即不同的n将生成不同的独立的类声明。 2. template 3. template class Topo { ... }; ``` 模板的具体化 1. 隐式实例化 `ArrayTP stuff;` 2. 显式实例化 `template class ArrayTP;` 3. 显式具体化 用于为特殊类型实例化时,对模板进行修改,使其行为不同。 `template <> class Classname { ... };` **成员模板** ```cpp // tempmemb.cpp -- template members #include using std::cout; using std::endl; template class beta { private: template class hold { private: V val; public: hold(V v = 0) : val(v) {} void show() const {cout << val << endl;} V Value() const {return val;} }; hold q; hold n; public: beta(T t, int i) : q(t), n(i) {} // 模板函数 template U blab(U u, T t) {return (n.Value() + q.Value()) * u / t;} void Show() const {q.show(); n.show();} }; q成员是基于T类型的hold对象,blab()方法的U类型由该方法被调用时的参数值显示确定,T类型是由对象的实例化类型确定。 ``` 模板用作参数 ```cpp template class Thing> class Crab { private: Thing s1; Thing s2; public: Crab() {}; bool push(int a, double x) {return s1.push(a) && s2.push(x);} bool pop(int & a, double & x) {return s1.pop(a) && s2.pop(x);} }; Crab nebula; // Thing将被实例化为Stack ``` 头疼,模板类和友元,每种都适合于什么样的场景呢? 非模板友元函数、约束模板友元函数、非约束模板友元函数 - 阅读全文 -
CPP 13. 类继承 2020-11-19 浏览量 577 暂无评论 # 第13章 类继承 🐸小知识: 1. 以前一直以为继承跟直接复制再添加没啥区别,而且就算使用继承,也得先了解基类的特性,熟悉基类,现在有一点让我对继承有了新的看法:它甚至不需要访问源代码就可以派生出类,可以在不公开实现的情况下将自己的类分发给其他人,不过对个人来讲,这没啥用; 2. 派生类需要自己的构造函数; 3. 基类指针可以在不进行显式类型转换的情况下指向派生类对象,基类引用可以在不进行显式类型转换的情况下引用派生类对象,但是只能调用基类方法; 4. C++继承的3种方式:公有继承、保护继承、私有继承; 5. 方法在基类中被声明为虚的后,它在派生类中将自动成为虚方法,在派生类声明中使用关键字virtual来指出哪些函数是虚函数也不失为一个好办法; 6. 通常为基类声明一个虚析构函数,这样做是**为了确保释放派生对象时,按正确的顺序调用析构函数(怎么确保呢?)**; 7. 关键字virtual只用于类声明的方法原型中,而不用在定义中; ## 13.1 简单基类 ```cpp // tabtenn0.h #ifndef TABTENN0_H_ #define TABTENN0_H_ #include using std::string; class TableTennisPlayer { private: string firstname; string lastname; bool hasTable; public: TableTennisPlayer (const string & fn = "none", const string & ln = "none", bool ht = false); void Name() const; bool HasTable() const {return hasTable;}; void ResetTable(bool v) {hasTable = v;}; }; // tabtenn0.cpp #include "tabtenn0.h" #include TableTennisPlayer::TableTennisPlayer (const string & fn, const string & ln, bool ht) : firstname(fn), lastname(ln), hasTable(ht) {} void TableTennisPlayer::Name() const { std::cout << lastname << ", " << firstname; } ``` **派生一个类** ```cpp // 公有派生:基类的公有成员将成为派生类的公有成员, // 基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问 class RatedPlayer : public TableTennisPlayer { private: unsigned int rating; public: RatedPlayer (unsigned int r = 0, const string & fn = "none", const string & ln = "none", bool ht = false); RatedPlayer (unsigned int r, const TableTennisPlayer & tp); unsigned int Rating() const {return rating;} void ResetRating (unsigned int r) {rating = r;} }; // 创建派生类对象时,程序首先创建基类对象, // 这意味着基类对象应当在程序进入派生类构造函数之前被创建 RatedPlayer::RatedPlayer(unsigned int r, const string & fn, const string & ln, bool ht) : TableTennisPlayer(fn, ln, ht) { rating = r; } RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer & tp) : TableTennisPlayer(tp) { rating = r; } // 如果不调用基类构造函数,程序将使用默认的基类构造函数 RatedPlayer::RatedPlayer(unsigned int r, const string & fn, const string & ln, bool ht) { rating = r; } 等价于 RatedPlayer::RatedPlayer(unsigned int r, const string & fn, const string & ln, bool ht) : TableTennisPlayer() { rating = r; } ``` **基类指针与引用** ```cpp RatedPlayer rplayer(1140, "Jineng", "Han", true); TableTennisPlayer & rt = rplayer; TableTennisPlayer * pt = &rplayer; rt.Name(); pt->Name(); rt.Rating(); // invalid ``` ## 13.2 继承:Is-a关系 C++继承的3种方式:公有继承、保护继承、私有继承。 公有继承建立一种is-a-kind-of关系(is-a,香蕉是一种水果),不建立has-a关系(午餐有水果),不能建立is-like-a关系(我像小太阳),不建立is-implemented-as-a关系(用数组实现栈),不建立uses-a关系(计算机使用打印机)。 ## 13.3 多态公有继承 它是为了处理这样的情况的:希望同一个方法在派生类和基类中的行为不同。 可以使用两种机制实现多态公有继承: 1. 在派生类中重新定义基类的方法; 2. 使用虚方法(经常在基类中将派生类会重新定义的方法声明为虚方法); ```cpp // brass.h #ifndef BRASS_H_ #define BRASS_H_ #include // 基类中也要加关键字virtual?修改不方便啊,谁知道写基类的时候哪个方法要被重用呢 class Brass { private: std::string fullName; long accNum; double balance; public: Brass(const std::string & s = "Nullbody", long an = -1, double bal = 0.0); void Deposit(double amt); virtual void Withgraw(double amt); double Balance() const; virtual void ViewAcct() const; virtual ~Brass() {} }; class BrassPlus : public Brass { private: double maxLoan; double rate; double owesBank; public: BrassPlus(const std::string & s = "Nullbody", long an = -1, double bal = 0.0, double ml = 500, double r = 0.11125); BrassPlus(const Brass & ba, double ml = 500, double r = 0.11125); virtual void ViewAcct() const; virtual void Withgraw(double amt); void ResetMax(double m) {maxLoan = m;} void ResetRate(double r) {rate = r;} void ResetOwes() {owesBank = 0;} }; #endif ``` 如果没有使用关键字virtual,程序将根据引用类型或指针类型选择方法;如果使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法,例如: ```cpp 如果ViewAcct()不是虚的: Brass dom("jhan", 123421, 12435.65); BrassPlus dot("han"m 12434, 1243.54); Brass & b1_ref = dom; Brass & b2_ref = dot; b1_ref.ViewAcct(); // use Brass::ViewAcct() b2_ref.ViewAcct(); // use Brass::ViewAcct() 如果ViewAcct()是虚的: b1_ref.ViewAcct(); // use Brass::ViewAcct() b2_ref.ViewAcct(); // use BrassPlus::ViewAcct() ``` ## 13.4 静态联编和动态联编 隐式向上强制转换使基类指针或引用可以指向基类对象或派生类对象。 **虚函数的工作原理:** 实现虚函数功能的是一个保存了指向函数地址数组的指针的隐藏成员(虚函数表),若派生类重新定义了基类的虚函数,则派生类的虚函数表存储的地址替换为新的虚函数地址,否则地址仍为基类虚函数的地址,调用函数时,到对应的虚函数表中查找地址。 **注意点:** 1. 构造函数不能是虚函数; 2. 析构函数应当是虚函数,除非类不用做基类,要不然指向基类的指针就不能调用派生类的析构函数来释放内存了; 3. 友元不能是虚函数,因为友元不是类成员; 4. 如果派生类没有重新定义函数,将使用该函数的基类版本,如果派生类位于派生链中,则将使用最新的虚函数版本(递进原则); 5. 派生类的虚函数将隐藏基类的; ## 13.5 访问控制:protected protected与private的共同点:在类外只能用公有类成员来访问protected部分中的类成员; protected与private的区别:派生类的成员可以直接访问基类的保护成员,但不能访问基类的私有成员。 **小提示:** 尽量对类数据成员采用私有访问控制。 ## 13.6 抽象基类 用来处理两个类有很多共同点,但实际继承时又有许多麻烦的情况,这时,可以从这两个类中抽象出来它们的共性,将这些特性放到一个ABC中。通过纯虚函数来实现ABC。 🐸小知识: 1. 当类声明中包含纯虚函数时,不能创建该类的对象,包含纯虚函数的类只用作基类; 2. 纯虚函数的结尾处为=0,`virtual double Area() const = 0;`; 3. 如果所有共性的基类方法都一样,可以通过将原型声明为虚的来使基类成为抽象的`void Move(int nx, ny) = 0;`; ## 13.7 继承和动态内存分配 如果基类使用动态内存分配,并重新定义赋值和复制构造函数,派生类的实现会受到怎样的影响? 1. 派生类不使用new 不需要为派生类定义显式析构函数、复制构造函数和赋值运算符,默认的就能满足了; 2. 派生类使用new 必须为派生类定义显式析构函数、复制构造函数和赋值运算符。 1. 析构函数:依赖于基类的析构函数释放基类中的指针数据成员,并释放派生类指针数据成员管理的内存。 ```cpp baseDMA::~baseDMA() { delete [] label; } hasDMA::~hasDMA() { delete [] style; } ``` 2. 复制构造函数 复制构造函数baseDMA有一个baseDMA引用参数,基类引用可以指向派生类型,因此,baseDMA复制构造函数将使用hasDMA参数的baseDMA部分来构造新对象的baseDMA部分。 ```cpp hasDMA::hasDMA(const hasDMA & hs) : baseDMA(hs) { style = new char[std::strlen(hs.style) + 1]; std::strcpy(style, hs.style); } ``` 3. 赋值运算符 显式调用基类赋值运算符`baseDMA::operator=(hs)`。 **派生类如何使用基类的友元?** 使用基类的;强制类型转换 - 阅读全文 -