第一章 C++编程基础
1.1 如何写c++程序
#include <iostream> //输入输出库
#include <string>
int main() //main函数 程序入口
{
string user_name;
std::cout << "Enter your name" << std::endl;
std::cin >> user_name;
std::cout << "hello" << user_name << ".... goodbye" << std::endl;
return 0;
}
class,用户自定义的数据类型,可以增强程序内之类型抽象化能力,一般包括头文件和源码文件
- 头文件: 声明class所提供的的各种操作行为。
- 源码文件:包含了操作行为的具体实现。
using namespace std;
- using和namespace都是关键字,std是标准库所驻值命名空间的名称,是为了防止命名冲突。
- 命名空间是一种将库名称封装起来的方法,可以避免和应用程序发生命名冲突的问题,像是在众多名称可见的范围之间建起一道道围墙
1.2 对象定义和初始化
#include <iostream>
#include <string>
int main()
string user_name;
int user_val;
int num_tries=0,num_right =0;
/*
初始化方法:
1 = 如果初始化有多个初始 就需要构造函数法
2 构造函数语法 int a(10);
- 可以解决多值初始化
- 内置类型和 自己的class统一
*/
double user_socre = 0.0;
chaer user_more;
bool go_for_it = true;
return 0;
}
1.3 撰写表达式
条件表达式:
expr
? 真执行这里
:false 执行这里
逻辑运算法:
|| 只有一个为true就行(左为真 右边直接跳过)
&& 两者都为true才行 (左为假 后面直接跳过)
1.4 条件语句 循环语句
switch(num)
{
case 1:
....;
break;
case 2:
,,,,,,;
break;
...
default:
.....;
break;
}
1.5 运用Array和Vector
Array
-
定义 必须指定元素类型 名称 指定大小
int pell_seq[18]
-
通过索引指定元素 也可以通过索引遍历元素
pell_seq[0] = 1
-
可以初始化 如果初始元素小于大小 其余会初始化0
int elem[] = {1,2,3,4}
vector
-
定义 包含头文件 指定类型 有默认初值
#include
vector pell_seq(18)
-
通过索引指定元素 也可以通过索引遍历元素
pell_seq[0] = 1
-
初始化
1 索引指定
2 通过array初始化(其实都是指定地址)
vector new(pell_sqe,pell_sqe+seq_size)
差异
- vecotr 可以获得大小 vector.size()
1.6 指针带来的弹性
指针:内含某特定类型对象的内存地址
一个未指向任何对象的指针,地址为0,成为null指针
const int seq_cnt = 6;
vector<int> * seq_addrs[seq_cnt] =
{
&a,&b,&c,&d,&e,&f
}
//seq_addrs 是一个数组 数组里面有六个元素 每一个元素都是指向vector的地址
获取随机下表
#include <cstdlib>
srand(num); //参数是随机数生成器的种子
rand_index = rand()%num // 每次调用rnad()都是生成一个0和int表示的最大整数之间的整数
1.7 文件读写
写文件
#include <fstream>
ofstream outfile("xxx.txt",ios_base::app); //追加模式
if( ! outfile)
{
cout << "失败" << endl;
}
esle
{
outfile << "..." << "......." << endl;
}
读文件
#include <fstream>
ifstream infile("XXX.txt");
if( !infile)
{
cout << "打开失败" << endl;
} else
{
//数据格式 xxx.txt name num_tiress num_correct
string name;
int nt;
int nc;
while(infile >> name){
infile >> nt >> nc;
..........;
}
}
第二章 面向过程的编程风格
抽取出通用的函数,这样的优点:1 取代重复的代码 2 可以在不同程序中使用这些函数 3 便于协作开发
2.1 如何编写函数
函数定义:
- 返回类型
- 函数名
- 参数列表
- 函数体
函数声明:
可以让编译器得以检查后续出现的使用方式是否正确,不比提供函数体,也称为函数原型。
2.2 调用函数
函数参数为引用
- 可以对传入的对象进行次改
- 降低因为复制而产生的额外负担
指针参数和引用参数区别
指针可能(也可能不)指向有某个实际对象,所以要先进行判断。引用就一定会代表某个对象,不需要做检查。
2.3 默认参数
两个点:
- 默认值的解析操作是最右边开始的,如果我某个参数有默认值,那么这一参数右边的所有参数必须也有默认值。
- 默认值只能指定一次,可以在声明处也可以在函数定义初。
2.4 局部静态变量
void addnum(int& num) {
static vector<int> vs;
vs.push_back(num);
for (int i = 0; i < vs.size(); i++) {
cout << vs[i] << endl;
}
cout << "------------" << endl;
}
局部静态对象所处的内存空间,即使在不同的函数调用过程中,依然保持存在。
2.5 声明inline函数
将函数声明为inline,表示要求编译器在每个函数调用点上,将函数的内容展开。面对一个inline函数,编译器可以将该函数的操作改为以一份函数代码副本代替。这样会获得性能改善。
适合声明为inline的函数:体积小、常常被调用,计算不复杂。
2.6 模板函数
一般而言,如果一个函数具备多种实现方式,我们可以使用重载。如果我们希望让程序主体不变,仅仅改变数据类型,那么可以使用模板
template <typename class>
void peintnews(const string & s;const vector<class> &vec)
{
......;
}
关键字typename表示,class在这个函数中仅仅是一个暂时放置类型的占位符。
2.7 函数指针
定义:
函数存放在内存的代码区域内,他们同样有地址,如果我们有一个 int test(int a)的函数,那么它的地址就是函数名字,如同数组一样,数组的名字就是数组的起始地址。
格式:必须指明返回值类型、参数列表
data_types (*func_pointer)( data_types arg1, ...,data_types argn);
#include <iostream>
int add(int a, int b){
return a+b;
}
int sub(int a, int b){
return a-b;
}
void func(int e, int d, int(*f)(int a, int b)){ // 这里才是我想说的,
// 传入了一个int型,双参数,返回值为int的函数
std::cout<<f(e,d)<<std::endl;
}
int main()
{
func(2,3,add);
func(2,3,sub);
return 0;
}
第三章 泛型编程风格
泛型算法提供了许多可用于容器类以及数组类型上的操作,这些算法之所以被称之为泛型,是因为他们和他们想要操作的元素类型无关。
泛型算法通过模板技术实现与对象类型无关。而实现与容器无关(不直接在容器是上操作),是借由一堆iterator(first 和 last),表示我们要进行迭代的元素范围。如果first等于last,算法只会作用域first指的元素。如果不相等,会先作用于first所指的元素,然后first递增,指向下一个位置,比较first和last是否相等,依次继续。
3.1 指针的算术运算
为了实现一个算法跟数据类型无关容器无关,费劲心思出来了一版
//在一个数组里面找一个元素 找到了返回对象的指针
template <typename T>
T * find(const T * first,const T * last, const T & value)
{
if( !first || ! last){
return 0;
}
for(;first != last;first++){
if(*first == value)
{
return first;
}
}
return 0;
}
//使用
int a[3] = {1,2,3};
int *p = find(a,a+8,a[2]);
//这样有问题 数据类型解决了 但是容易五官没有 如果是vector存储在连续的空间 这样算也马马虎虎 但是如果是list 他的存储并不联系 不能通过++获取下一个
3.2 了解Iterator(泛型指针)
如何取得iterator?
每一个标砖容器都提供一个begin()函数,返回一个iterator,指向第一个元素,同理end()指向容器的最后一个元素的下一位置。
//利用泛型指针来解决 跟数据类型无关容器无关 找元素并返回指针
template <typename Itertype,typename Numtype>
Itertype
find(Itertype first,Itertype last,const Numtype &num)
{
for(;first != last;first++)
{
if(*first == num){
return first;
}
}
return last;
}
3.3 容器的共通操作
所有容器类(包括string)的通用操作:
- equality(==) 和inequality(!=),返回true和false
- assignment(=),将容器复制给另一个容器
- empty() 判断容器是否为空
- size() 返回容器目前的元素个数
- clear() 删除所有元素
3.4 顺序性容器
最常见的两个
vector
- 以一块连续的内存来存放元素,每一个元素都被存储在距离起点固定的偏移位置上。
- 随机访问效率高(第…个元素);
- 插入元素不是在末端效率低(插入右端的元素会被复制,向右移动)
- 适用于有很多随机访问的数列
list
- 以双向链表而非连续内存来存储,因此可以执行前进或者后退的操作
- 在list的任意位置进行元素插入或者删除操作,效率比较高
- 随机访问效率低(第…个元素),因为需要一个个的遍历
- 适用于多随机插入的情景
deque:双端队列
- 跟vector类似—连续内存存储元素
- 在双端的插入删除操作效率比较高
3.5 使用泛型算法
需包含头部文件
#include
四种泛型搜索算法:
- find() 用于搜索无序集合中是否存在某值。搜索范围[first,last],如果找到,返回一个iterator指向该值,否则指向last
- binary_search() 用于有序集合搜索,找到返回true,否则返回false。效率更高。二分查找。
- count() 返回数值相符的元素数目
- search() 比对容器内是否有某个子序列。{1,3,4,7,2,9}搜索{4,7,2},则会返回一个iterator指向子序列的起始处,否则返回末尾
3.6 设计一个泛型算法
任务:
给用户一个vector 返回一个新的vector 里面包含vector中小于(也可能大于 等于)某个元素的值
//利用函数指针传入比较操作
vector<int> filter_val(const vector<int> & v,
int filter_num.
bool (*comp)(int,int))
{
vector<int> nvec;
for(int i=0;i< v.size();i++){
if(comp(v[i],filter_num))
{
nvec.push_back(v[i])
}
}
return nvec;
}
//使用
bool less_than(int a,int b){
return a < b?true:flase;
}
vector<int> vv = filter_val(v,18,less_than)
Function Object:函数对象、仿函数
函数对象是某种class的实例对象,即一个重载了括号操作符“()”的对象,如此一来,当调用此操作符时,其表现形式如同普通函数调用一般。
标准库事先定义了异步函数对象有三大类:
#include
- 算数运算 plus<type>;minus<type>;negate<type>;multiplies<type>;divides<type>;modules<type>
- 关系运算 less<>;less_equal<>;greater<>;greater_equal<>;equal_to<>;not_equal_to<>
- 逻辑运算 logical_and<>;logical_or<>;logical_not<>
Function Object Adapoter:函数对象适配器:
没怎么看。。。。
3.7 使用Map
map定义为一对(pair)数值,包括key和value.
-
find函数 查询k是否存在 如果存在 返回一个iterator,指向k/v形成的一个pair,反之则返回end();
map<string,int>::iterator it; it = words.find("haha");
3.8 使用set
-
insert
可以用于插入元素,可以插入单一元素也可以插入范围内的元素
第四章 基于对象的编程风格
4.1 实现一个class
成员函数声明:
所有的成员函数都必须要在class里面声明,但是否进行定义可以自己决定。如果在class里面定义,这个成员函数会自动被视为inline函数。如果要在主体之外定义,必须使用特殊语法(::),用以分辨属于哪个class。如果希望声明为inline,可以在前面指定关键字inline。
4.2 构造函数VS析构哈数
构造函数
最简单的构造函数是所谓的默认构造,他不需要任何参数。但是不需要任何参数意味着两种情况
第一种:构造函数不接受参数
第二种更常见,声明的时候为每个参数提供了默认值
遇到指针和复制
- 问题: 两个对象的指针都会指向同一个内存,如果其中一个对象 析构了之后,另一个再来操作,就会发生错误
- 解决:写复制构造函数(里面在复制的同时,遇到指针重新new)
4.3 mutable(可变) 和const(不变)
const:
-
const表示不会修改class里面的内容
-
修饰成员函数 同时在声明和定义中指定const,紧跟在函数参数列表后
-
const修饰 返回引用的情况如下
-
#include<iostream> #include<vector> using namespace std; class BigClass {}; class val_class { public: val_class(const BigClass& c) :_val(c) {}; const BigClass& val() const { cout << "const " << endl; return _val; }; BigClass& val(){ cout << " no const" << endl; return _val; }; /*此处提供了两个版本 这种事错误的 BigClass& val() const { return _val;}; 这种错误的原因是 虽然声明为const 但是是通过引用传递出去 不能保证在传出去之后不会被修改 */ private: BigClass _val; }; int main() { BigClass b; { const val_class va(b); va.val(); } val_class va(b); va.val(); return 0; }
mutable:
将class的参数声明为mutable时,如果里面有成员函数对其改变此时并不会改变class的常量性。
4.4 this指针
this:
this指针在成员函数内 用来指向其调用者(一个对象)。
内部工作过程是:编译器自动将this指针加到每一个成员变量的参数列表里面。
//我们有自定义的copy函数
tr1.copy(tr2) 相当于copy(& tr1,tr2)
成员函数内部,this指针可以让我们访问其调用者的一切。
以一个对象复制出另一个对象,使用利用this指针判断是一个好习惯。(this == & other)
4.5 静态成员
- 静态成员变量 表示唯一的、可共享的变量,可以在同一个类的所有对象中访问
- 静态成员函数 里面没有访问任何非静态变量,在声明前加上关键字static即可
4.6打造一个Iterator Class
运算符重载
- 两种实现方式(类成员函数 和全局普通函数)
#include <iostream>
class Person
{
private:
int m_age;
public:
Person(int nAge)
{
this->m_age = nAge;
}
int age() const
{
return m_age;
}
bool operator==(const Person& other)
{
std::cout << "call member function operator==" << std::endl;
if (this->m_age == other.m_age) //m_age不是私有成员变量么,为什么这样里可以直接写other.m_age?
{
return true;
}
return false;
}
};
bool operator==(const Person& one, const Person& other)
{
std::cout << "call normal function operator==" << std::endl;
if (one.age() == other.age())
{
return true;
}
return false;
}
/*
结论:
(1)两种方式均可
(2)调用优先等级: 类成员函数 > 全局普通函数
(3)你可以发现竟然能查看别人的私有属性
那是因为同一个class的object互为友元
*/
前置、后置递增
前置递增:先让变量递增,在计算表达式值。
后置递增:先做表达式运算,再让变量递增
#include <iostream>
using namespace std;
class MyInteger
{
friend ostream & operator<<(ostream &out, MyInteger mi);
private:
int m_Number;
public:
MyInteger()
{
m_Number = 0;
}
//重载 前置++ 运算符 一定返回的是引用 只在原始值上++
MyInteger& operator++()
{
// 先进行++运算
m_Number++;
// 在将自身返回,返回一个引用
return *this;
}
//重载后置++运算符
// 参数int是占位参数,用来区分前置还是后置,固定写法
MyInteger operator++(int)
{
//先记录当时结果
MyInteger temp = *this;
//然后变量递增
m_Number++;
//最后返回当时记录的结果
return temp;
}
};
ostream & operator<<(ostream &out, MyInteger mi)
{
out << mi.m_Number;
return out;
}
void test01()
{
MyInteger mi;
cout << mi++ << endl;
cout << mi << endl;
}
int main()
{
test01();
system("pause");
return 0;
}
嵌套类型:
typedef可以为某个类型设定另一个不同的名称。通用形式:
typedef existing_type new_type;
其中:existing_type可以是任何一个内置类型、复合类型或者class类型。
4.7 友元
在某个函数的原型前加上关键字friend,就可以将它声明为某个class的friend。这份声明可以出现在class定义的任意位置撒花姑娘,不受private和public的影响。
- 可以是类的某个函数成为另一个类友元
- 可以是一个类成为另一个类的友元
4.8 赋值函数
对象本来就存在,用别的对象来给它赋值,就是赋值函数(赋值运算符重载),需要重写操作符=。
String &
String::operator=(const String& other) //赋值运算符
{
cout<<"operator =funtion"<<endl ;
if(this==&other) //如果对象和other是用一个对象,直接返回本身
{
return *this;
}
delete []m_string; //先释放原来的内存
m_string= new char[strlen(other.m_string)+1];
strcpy(m_string,other.m_string);
return * this;
}
区别于拷贝构造函数:
对象不存在,用别的对象来初始化,就是拷贝构造函数
String::String(const String&other) //拷贝构造函数
{
cout<<"copy construct"<<endl;
m_string=new char[strlen(other.m_string)+1]; //分配空间并拷贝
strcpy(m_string,other.m_string);
}
4.9 实现Function Object**😗*函数对象、仿函数
就是重载操作符(),可以像函数一样使用
4.10 重载iostream运算符
**注:**返回值是引用,会使得传入函数的ostream对象又原封不动的返回,如此可以串联多哥操作符。
ostream &
operator<< (ostream &out, complex &A)
{
out << A.m_real <<" + "<< A.m_imag <<" i ";
return out;
}
4.11 指向Class Member Function(指向类成员函数)
定义:
用于存储一个 指定类 具有给定的形参列表和返回值类型的成员函数的访问信息。
**格式:**指定返回类型和参数 指定属于哪一个类
例: void (A::*p)(int) = 0;
注意两点:
- 赋值需要用到取址符号&
- 使用 .* (实例对象)或者 ->*(实例对象指针)调用类成员函数指针所指向的函数
- 对于 **nonstatic member function (非静态成员函数)**取地址,获得该函数在内存中的实际地址
- 对于 virtual function(虚函数), 其地址在编译时期是未知的,所以对于 virtual member function(虚成员函数)取其地址,所能获得的只是一个索引值
比较一般的教程解释:
//指向类成员函数的函数指针
#include <iostream>
#include <cstdio>
using namespace std;
class A
{
public:
A(int aa = 0):a(aa){}
~A(){}
void setA(int aa = 1)
{
a = aa;
}
virtual void print()
{
cout << "A: " << a << endl;
}
virtual void printa()
{
cout << "A1: " << a << endl;
}
private:
int a;
};
class B:public A
{
public:
B():A(), b(0){}
B(int aa, int bb):A(aa), b(bb){}
~B(){}
virtual void print()
{
A::print();
cout << "B: " << b << endl;
}
virtual void printa()
{
A::printa();
cout << "B: " << b << endl;
}
private:
int b;
};
int main(void)
{
A a;
B b;
void (A::*ptr)(int) = &A::setA;
A* pa = &a;
//对于非虚函数,返回其在内存的真实地址
printf("A::set(): %p\n", &A::setA);
//对于虚函数, 返回其在虚函数表的偏移位置
printf("B::print(): %p\n", &A::print);
printf("B::print(): %p\n", &A::printa);
a.print();
a.setA(10);
a.print();
a.setA(100);
a.print();
//对于指向类成员函数的函数指针,引用时必须传入一个类对象的this指针,所以必须由类实体调用
(pa->*ptr)(1000);
a.print();
(a.*ptr)(10000);
a.print();
return 0;
}
//jieguo
A::set(): 0x8048a38
B::print(): 0x1
B::print(): 0x5
A: 0
A: 10
A: 100
A: 1000
A: 10000
#include <iostream>
using namespace std;
class A{
public:
//p1是一个指向非static成员函数的函数指针
void (A::*p1)(void);
//p2是一个指向static成员函数的函数指针
void (*p2)(void);
A(){
/*对
**指向非static成员函数的指针
**和
**指向static成员函数的指针
**的变量的赋值方式是一样的,都是&ClassName::memberVariable形式
**区别在于:
**对p1只能用非static成员函数赋值
**对p2只能用static成员函数赋值
**
**再有,赋值时如果直接&memberVariable,则在VS中报"编译器错误 C2276"
**参见:http://msdn.microsoft.com/zh-cn/library/850cstw1.aspx
*/
p1 =&A::funa; //函数指针赋值一定要使用 &
p2 =&A::funb;
//p1 =&A::funb;//error
//p2 =&A::funa;//error
//p1=&funa;//error,编译器错误 C2276
//p2=&funb;//error,编译器错误 C2276
}
void funa(void){
puts("A");
}
static void funb(void){
puts("B");
}
};
int main()
{
A a;
//p是指向A中非static成员函数的函数指针
void (A::*p)(void);
(a.*a.p1)(); //打印 A
//使用.*(实例对象)或者->*(实例对象指针)调用类成员函数指针所指向的函数
p = a.p1;
(a.*p)();//打印 A
A *b = &a;
(b->*p)(); //打印 A
/*尽管a.p2本身是个非static变量,但是a.p2是指向static函数的函数指针,
**所以下面这就话是错的!
*/
// p = a.p2;//error
void (*pp)(void);
pp = &A::funb;
pp(); //打印 B
return 0;
}
一个比较好的例子:
- 一般用法
//1 已经知道一个类和一个类成员函数
class Myclass
{
public:
int sum(int a,int b){
return a+b;
}
}
//2 声明一个指向成员变量的指针 不知道具体函数
int (Myclass:: *p)(int,int);
//3 赋值给一个具体的函数
p = & Myclass::sum;
//4 调用 如果想要使用指针调用一个类成员函数 必须要提供给一个类的实例
Myclasss my;
int result = (my.*p)(1,2)
-
在类中使用类成员函数指针
假设你正在创建一个带有后端和前端的 客户端/服务器 原理架构的应用程序。你现在并不需要关心后端,相反的,你将基于 C++ 类的前端。前端依赖于后端提供的数据完成初始化,所以你需要一个额外的初始化机制。同时,你希望通用地实现此机制,以便将来可以使用其他初始化函数(可能是动态的)来拓展你的前端。
//1 定义一个数据类型 来存储 初始化函数(类函数指针)和描述何时调用此函数的信息
template<typename T>
struct DynamicInitCommand{
void (T:: *p)();
unsigned int ticks;
}
//2 一个Frontend类实例代码
class Frontend
{
public:
Frontend()
{
DynamicInitCommand <Frontend> init1,init2,init3;
init1={ & Frontend::dynamicInit1,5};
init2={ & Frontend::dynamicInit2,10};
init3={ & Frontend::dynamicInit3,15};
m_dynamicInit.push_back(init1);
m_dynamicInit.push_back(init2);
m_dynamicInit.push_back(init3);
}
void tick()
{
std::cout << "tick: " << ++m_ticks << std::endl;
std::vector<DynamicInitCommand<Frontend> >::iterator it = m_dynamicInit.beign();
while(it != m_dynamicInit.end())
{
if(it->ticks < m_ticks){
if(it -> p)
{
((*this).*(it ->p))() //调用具体初始
}
it = m_dynamicInit.erase(it)
}else{
it++
}
}
}
unsigned int m_ticks{0};
private:
void dynamicInit1(){
std::cout << "dynamicInit1 called" << std::endl;
};
void dynamicInit2(){
std::cout << "dynamicInit2 called" << std::endl;
}
void dynamicInit3(){
std::cout << "dynamicInit3 called" << std::endl;
}
unsigned int m_initCnt{0};
std::vector<DynamicInitCommand<Frontend> > m_dynamicInit; //> >有一个空格
}
//Frontend完成实例化后 tick()函数会被后端以福鼎时间调用
//例
int main()
{
Frontend ft;
while(true)
{
ft.tick();
std::this_thread::sleep_for(std::cheono::milliseconds(200));
}
}
第五章 面向对象的编程风格
5.1 面向对象编程概念
两个主要特质:继承、多态
继承定义了父类(基类)和子类(派生类)的关系,基类定义了所有派生类共通的公共接口和私有实现。派生类可以增加或者覆盖继承来的东西,以实现自己的特有行为。
多态:
让基类的指针或者引用能够指向其任意一个派生类对象
静态绑定:
在非面向对象的编程中,编译器在编译时就能根据类决定究竟执行的哪一个函数,由于在程序执行前就已经解析出该嗲用哪个函数,称为静态绑定。
动态绑定:
在面向对象编程方式中,编译器只能在执行的过程中根据所指的实际对象来决定调用哪一个函数,找出调用哪个函数这个操作延迟到运行时确定,称为动态绑定。
5.2 面向对象编程思维
- 如果都多层继承,最底层的派生类在构造时,会根据血缘从上往下全部都构造一遍。
- 要想实现 多态的效果,声明为virtual必不可少
5.3 不带继承的多态
索然无味
5.4 定义一个抽象基类
步骤:
-
找出所有子类共通的操作行为
-
判断有哪些操作行为必须根据不同的派生类有不同的实现方式,也就是该操作行为要设为虚函数。
-
设置每个操作行为的访问层级
public:某个操作行为一般程序都能访问
private:操作行为在基类之外不需要被用到
protected:操作行为可以让派生类访问到,不允许一般程序程序使用
纯虚函数:
对于该类而言,这个虚函数没有实际意义,赋值为0。
析构函数设为虚析构:
凡是基类定义有一个(或者多个)虚函数,都需要将析构函数设置为虚函数。
原因:多态的时候只有这样才能准确析构指定的函数,不会出现问题。
5.5 定义一个派生类
- 派生类必须为从基类继承而来的每个纯虚函数提供对应的实现。
- 每当派生类有某个 成员 与其基类的 成员同名,便会遮盖住基类的那份成员。派生类内对该名称的任何使用,都会被解析为该派生类自身的那份成员,而非继承来的成员。如果要想使用继承来的成员,需要利用::来加以限定。
5.6 运用继承体系
平平无奇
5.7 基类应该多么抽象
数据成员如果是个引用,必须在构造函数中的成员列表进行初始化,一旦初始化,就没有办法再指向另一个对象。但如果是个指针,就没有此限制,可以在构造函数初始化,也可以先指向null ,稍后再指向某个有效内存地址。
5.8 初始化、析构、复制
-
基类数据成员初始化。比较好的方式是,为基类提供构造函数,利用这个构造函数对数据成员进行初始化。
-
构造函数调用:派生类初始化会调用基类的构造函数,然后再自己的。
-
派生类的构造函数不仅必须为派生类的数据成员进行初始化,还需要为其基类的数据成员提供相应的值。
如果派生类构造函数没有明确指出调用基类的哪一个构造函数,编译器会自动调用基类的默认构造。
-
复制、赋值构造 见 4.8。
5.9 在派生类中定义一个虚函数
-
定义派生类时,需要决定将基类的虚函数覆盖还是原封不动的继承,
如果继承纯虚函数,那么此派生类也是抽象类,无法实例化。
如果要覆盖,那么函数原型必须完全符合基类所声明的函数原型,包括 参数列表、返回类型、常量性
-
有两种情况下,虚函数机制不会出现预期行为
(1)基类的构造函数和析构函数内
构造派生类对象时,基类的构造函数会被调用,如果此时基类构造函数调用了虚函数,虚函数会是派生类那一份吗?
由于此时派生类还没有初始化,所以派生类的虚函数是不可能被调用的,会调用基类的。
(2)使用基类的对象,而不是基类对象的指针或者引用
5.10 运行时的类型鉴定机制
typeid:
- typeid是运行时类型鉴定机制的一部分,他让我们得以查询多态化的类指针或者类引用,获得其所指对象的实际类型。
#include <typeinfo> //必须包含头文件
{
return typeid(*this).name();
}
/*
typeid(*this) 会返回一个type_info对象 关联至this指针所指对象的实际类型
name()函数会返回一个const char *,用来表示类名
*/
- 支持相等 不等两个比较操作
//可以决定ps是都指向某个Fibonacci对象
num_sequence * ps = & fib;// num_sequence 是 Fibonacci的基类
if( typeid(*ps) == typeid(Fibonacci))
{
//ps -> gen_elems(); 会报错 注意 gen_elems并不是虚函数 而是Fibonacci独有的 这是因为 ps并不知道它所指的对象实际上是什么类型 为了能都调用 我们必须显示的指示编译器 将ps的类型转换为Fibonacci指针 可以如下这么做
// Fibonacci * pf = static_cast<Fibonacci *>(ps); 这个代表着无条件转换 具有潜在危险 因为编译器没有办法确认我们的转换操作是否完全正确
if(Fibonacci * pf = dynamic_cast<Fibonacci *>(ps))
{
......;
}
//dynamic_cast 会进行运行时检验操作 检验ps所指的对象是否属于Fibonacci类 如果是 转换操作就会发生 pf指向该Fibonacci类。如果不是 就会返回0
}
第六章 以template进行编程
利用模板实现一个二叉树,我们的二叉树包含两个class,一个是BinaryTree,用来存储一个指针,指向根节点,另一个是BTnode,用来存储节点实值以及连接左右两个子节点的链接。节点值的类型需要模板。
- BinaryTree 提供插入、移除、寻找、清除、遍历
- BTnode提供 节点实值、节点实值重复次数、左右节点指针
6.1 被参数化的类型
模板机制可以帮助我们 将定义中 与类型相关 和 独立于类型之外的 两个部分分离开来。跟类型相关的部分会被抽取出来,形成一个或者多个参数。
6.2 定义
template <typename elemType>
class BinaryTree
{
public:
BinaryTree();
BinaryTree(const BinaryTree &);
~BinaryTree();
BinaryTree & operator=(const BinaryTree &);
bool empty(){return _root ==0};
void clear();
private:
BTnode<elemType> *_root;
//将src所指 子树()复制到tar所指子树
void copy(BTnode<elemType> * tar,BTnode<elemType> * src);
};
//模板类函数定义
template <typename elemType>
inline BinaryTree<elemType>::
BinaryTree(const BinaryTree & rhs)
{
copy(_root,rhs._root);
}
template <typename elemType>
inline BinaryTree<elemType>::
~ BinaryTree()
{
clear();
}
template <typename elemType>
inline BinaryTree<elemType> &
BinaryTree<elemType>::
operator=(const BinaryTree & rhs)
{
if(this != &rhs)
{
clear();
copy(_root,rhs._root);
}
return *this;
}
6.3 模板类型参数的处理
参数有内置类型和我们自己书写的class类型,我们建议将所有的模板类型视为class类型处理,这样在参数初始化时,我们选择初始化列表的方式,因为这样效率更高!
class Man
{
public:
Man(string id,int age):_id(id),_age(age){};
string _id;
int _age;
}
第七章 异常处理
7.1 抛出异常
利用throw表达式产生异常。
- 最简单的异常对象可以设计为 整数 或者字符串
- 大部分,被跑出的异常都属于特定的异常类
7.2 捕获异常
利用单条或者一连串的catch子句来捕获被抛出的异常对象。
catch子句由三部分组成:关键字catch、小括号里面的一个类型或者对象、大括号里面的语句(用于处理异常)