C++学习笔记
本笔记基于C语言基础上学习C++,参考数据 Essential C++
一、C++编程基础
二、面向过程编程风格
基于C编程基础上重点需要注意一下几点:
1、引用和指针
简而言之,引用是别名,指针是指向地址(灵活,使用前判空)
2、默认参数
默认值从右至左放
3、局部静态对象
static 修饰的变量出了作用域同样存在,全局变量打乱函数之间的逻辑
4、inline内联
将函数定义直接内联到调用语句块中,对于经常调用的“小”函数非常适合
5、函数重载
函数名一致,参数列表不一致,返回类型不能作为重载识别(编译器识别不了用返回类型区分调用哪一个重载函数)
6、模板函数
模板函数没有声明,只有定义,所以模板函数一般直接定义在.h头文件中
template<typename elemType>
void fun(const int &a, const vector<elemType> &vec)
{
}
7、函数指针
指向函数的指针,需要在定义时指明指向函数的 返回类型和参数列表,因此定义如下:
const vector<int>* fun1(const int &a)//函数1
const vector<int>* fun2(const int &a)//函数2
const vector<int>* fun3(const int &a)//函数3
函数指针定义、初始化、调用
const vector<int>* //函数返回类型(*ptr)//指针名 (const int)//参数列表 = fun1//初始化
int (*ptr[])(const int &a) = {fun1,fun2,fun3};//指针函数数组定义
const vector<int>* //函数返回类型(*ptr)//指针名 (const int)//参数列表 = fun1(a)//调用
8、头文件
需要注意inline是加载到调用处,所有inline函数只能定义在.h文件里面,否则编译提示多处定义inline修饰的函数
9、const常量
防止不必要的误修改,常用在常量变量,函数参数引用传递
10、作用域
三、STL泛型编程
STL: Standard Template Library 标准模板库
标准模板库由两种组件组成:容器,泛型算法,支持泛型编程的底层是迭代器(指针)
容器:
vector:顺序容器,地址连续,支持随机访问
list:顺序容器,地址指针连接,支持增删改
deque:顺序容器,地址连续,双端插入删除效率更高
map:字典容器
set:集合容器
泛型算法:
find()
sort()
merge()
replace()
泛型编程需要解决的两个问题:
1、泛型支持不同数据类型:模板来实现
2、泛型算法对不同容器操作:指针(迭代器)
1、指针+模板支持泛型编程
template<typename elemType>
elemType* find(const elemType *begin, const elemType *end, const elemType &value)
{
if(!first || !end)return 0;
for(;first !=end;first++)
if(*first == value) return first;
}
上面是一个泛型函数find的指针加模板的设计方式,同样容器的基本push,back操作同样可以实现,但考虑一个问题,elemType的指针在执行指针运算时不会考虑容器的性质而作不同的指针运算如:++,–,==。因此,迭代器就是解决这个问题的一种利器。
需要注意的是指针的使用每次都需要判断空处理:
template<typename elemType>
inline elemType* begin(const vector<elemtype> &vec)
{
return vector.empty() ? 0 : &vec[0];
}
2、迭代器(Iterator)
vector<int>::iterator iter = vec.bengin()
iter++//针对vector容器而作++操作,++操作被重写了
for(vector<int>::iterator iter = vec.bengin(); iter != vec.end(); iter++)//==操作被重写了
map<string,int>::iterator iter = mp.bengin()
iter++//针对map容器而作++操作
3、常用容器及常用泛型算法
#include<vector>
#include<list>
#include <deque>
#include <map>
#include <set>
#include <algorithm>
4、设计泛型算法
支持vector的返回小于10的vector
vector<int> less_then_10(const vector<int> &vec)
{
vector<int> nvec;
for(int ix = 0; ix < vec.size(); ix++)
{
if(vec[ix] < 10)
nvec.push_back(vec[ix]);
}
return nvec;
}
支持小于n的vector,引入第二个参数来接收比较值
vector<int> less_then_n(const vector<int> &vec,const int &value)
*支持不同比较方式的vector,引入第三个参数接收比较方式的函数指针,该函数返回bool类型,其中(pred)是一个指向函数的指针:
vector<int> filter(const vector<int> &vec,const int &value,bool (*pred)(int,int))
{
vector<int> nvec;
for(int ix = 0; ix < vec.size(); ix++)
{
if(pred(vec[ix],value))//该if 可以抽象出一个find_if泛型函数
nvec.push_back(vec[ix]);
}
return nvec;
}
inline bool less_then(int v1, int v2) {return v1 < v2 ? true : false;}
inline bool greater_then(int v1, int v2) {return v1 > v2 ? true : false;}
vector<int> res = filter(vec, value, less_then)//调用
标准库中定义了多种function object :算术运算plus,minus,关系运算less,not_equal,逻辑运算 logical_and等,例如:
#include <functional>
//function object 头文件
sort(vec.begin, vec.end(), greater<int>())//可见greater支持模板
考虑上面的泛型函数设计是针对vector设计的,使用模板加迭代器来消除vector限制
template <typename inputIterator,typename outputIterator,typename ElemType,typename comp>
outputIterator filter(inputIterator first, inputIterator last, outputIterator at//输出copy迭代器的起始位置, const ElemType &value, comp pred)
{
while((first = find_if(first, last, bind2and(pred, value)) != last)
//bind2and是一个绑定函数指针pred的函数指针
{
cout<< "found value:" << *first << endl;
*at++ = *first++;//指针++,copy 值到at指针上
}
return at;//at是输出迭代器
}
5、Iterator Inserter
上面*at++ = *first++;
存在目标端at容器必须足够大的问题,,但filter没办法知道at++是否依然有效,Inserter adapter提供
back_inserter 取代容器的push_back操作类似
inserter 取代容器的insert操作类似
front_inserter 取代容器的push_front操作类似
因此filter重新定义为
filter(inputIterator first, inputIterator last, back_inserter(outputIterator at), const ElemType &value, comp pred)//back_inserter来代替*at++ = *first++;
6、iostream iterator
#include <iterotar>
//iostream Iterator头文件
四、基于对象编程
1、类定义和几个关键词
class xxx_class;//xxx_class声明
class Stack{
friend xxx_class;
public:
bool push (const string &);
bool pop(const string &);
private:
vector<string> _stack;
};
bool Stack::push (const string &val){
if(full) return false;
_stack.push_back(val);
return true;
}
public:类内类外都能访问
protect:类内和其子类能访问
private:类内和其友元能访问
friend:友元类修饰
2、构造函数、拷贝构造函数、析构函数
构造函数
class Stack
{
public:
Stack();//默认构造函数
Stack(const size_t size);
Stack(const size_t size, const size_t pos);//
//
};//类的声明和定义
Stack::Stack()//默认构造函数
{
_size = 1;
_pos = 1;
next = 0;
}
Stack::Stack(const size_t size, const size_t pos)//带参构造函数,函数体内初始化
{
_size = size;
_pos = pos;
next = 0;
}
Stack s ;//调用默认构造函数
Stack s = 3; //调用单一参数的构造函数,而不是拷贝构造函数
Stack s();//错误,默认构造函数的调用不能带(),因为这与函数ss定义方式一致
Stack s2(10,2);//调用带参构造函数
Stack::Stack(const size_t size, const size_t pos) : _size(size), _pos(pos), _next(0) //成员初始化列表
{...}带参构造函数,该初始化方式在初始化值上调用构造函数初始化,与构造函数体内初始化不同,优势是在非基本类型初始化时效率更高
析构函数
析构函数在类对象结束生命时,自动调用destructor函数善后处理
class Matrix
{
public:
Matrix(const int row, const int row) : _row(row), _col(col)
{
_pmat = new double [row * col]
}
~Matirx()
{
delete [] _pmat;
}
private:
int _row;
int _col;
double *_pmat;
};
在Matrix构造函数被调用时,_pmat指向一块空闲的内存,16个double元素,结束之时调用析构函数释放 _pmat所指向的16个double元素。(何时需要定义析构函数需要思考)
拷贝构造函数
当我们在拷贝一个类对象到另一个对象(通过一个对象来初始化另一个对象)时,被初始化的对象与原对象的成员一致,即对象的成员值逐一初始化到新对象上:考虑Matrix类,逐一初始化的话则 原对象_pmat与新对象的 _pmat指向同一个地址,这是非常危险的,当其中一个对象析构后,那个另一个对象的指针就悬空了,因此,定义拷贝构造函数解决该问题
Matirx m1(3,4);
Matrix m2 = m1;
Matrix:: Matrix(const Matrix &rhs) : _row(rhs.row), _col(rhs.col)
{
int elem_cnt = _row * _col;
_pmat = new double[elem_cnt];
for(int i = 0 ; i < elem_cnt; i++)
_pmat[i] = rhs. _pmat[i]
}
3、常量(const)和可量成员(mutable)
const修饰函数的参数
const参数(类)在函数体内不能被修改,如果参数类一个const Matrix &m,则m调用的函数也必须是const函数
int sum(const Matrix &m)
{
m = n; //函数体内不能修改const参数变量
m.next();//该next修改了m的成员变量,则违背了const修饰的参数m
}
const修饰的函数
const成员函数不能修改类的成员变量,同时如果返回值为成员变量时也必须是const修饰
int Matrix:: get_row () const{ _row++, return _row}//错,const成员函数不能修改类成员变量
int Matrix:: get_row () const{return _row}//错,虽然函数体内没修改成员变量,但是却将成员变量以mutable返回
const int Matrix:: get_row () const{return _row}// 返回值也必须用const修饰
如果需要两个版本的,可以定义一份const版和 non-const版
mutable
对于一些成员变量,在类对象中本身就是多变的(如记录位置的成员变量),然而函数接收一个const变量时,我们需要改变某个类成员变量,该变量可以用mutable修饰
int sum(const Matrix &m)
{
m.next();//该函数修改了成员变量_next的值,且必须修改的那种,
//那么Matrix定义成员变量 _next时可以mutable int _next;
}
4、静态类成员
类的static成员属于类,不属于对象,因此使用只能通过类来使用,对象无权操作
static成员是全局的,可以通过类作用域下全局变量访问
static成员变量
class Matrix
{
static const int buf_size = 1024;//一般buf_size这样的成员变量定义为static
static vector<int> _elem;//静态成员
static bool is_elem(const int &val);//声明加static,定义可以不加
}
类内访问同局部变量一样,类外访问需要加类作用域下,类对象下无权操作
static成员函数
静态成员函数属于类,类内访问直接调用,类外需要加类作用域下,且只能操作类下静态成员变量,调用类的成员函数只能是静态成员函数
bool Matrix:: is_elem(cosnt int &val)
{
for(int i = 0; i < _elem.size(); i++)
if (val == _elem[i])
return true;
return flase;
}
Matrix:: is_elem(val);//类外调用方式
5、this指针
this是指向本对象指针
m1.copy(m2)
Matrix& Matirx:: copy(const Matrix &m1)
{
_row = m1. _row;
_col = m2. _col;
return *this;
}
Matrix& Matirx:: copy(Matrix *this, const Matrix &m2)
{
this->_row = m1. _row;
this->_col = m2. _col;
return *this;
}
6、类的重载操作符
重载运算符=
Matrix& Matirx:: operator=(const Matrix &m2)
{
if(this != &m2){
this->_row = m1. _row;
this->_col = m2. _col;
delete [] _pmat;
_pmat = new double[elem_cnt];
int elem_cnt = _row * _col;
for(int i = 0 ; i < elem_cnt; i++)
_pmat[i] = rhs. _pmat[i]
}
return *this;
}
重载iostream
ostream& operator<< (ostream &os, const Matrix &m)
{
os << "row:" << m._row << "col:" << m._col <<endl;
m.print();
return os;
}
传入对象os,并返回os对象,如此一来就可以多个 << 串联起来了 如 cout<<m1<<m2;
运算符=的重载可以看作是类的一个成员函数,但是 << 和 >> 的重载确实定义在类外的重载,原因时,如果定义为Matrix类的成员函数,则 <<调用方式如下
m<<cout // cout作为os实参,调用成员函数 <<,这种方式不符合cout习惯
6、类的迭代器,类的友元,指向类成员函数的指针(实现类的多态性质的一种低能的方式)
五、面向对象编程
基于对象编程如果理解为将数据和方法封装到一个类内,类内共享数据和方法。
面向对象编程则是继承和多态,抽象出共同的数据和方法放到基类里面,与子类类型相关的方法在基类中定义虚函数作为接口,基类的虚函数由各自的子类自己实现,每个子类独有数据和方法由自己定义。由此,面向对象编程提供了多态的编程方式。
继承:类依赖关系,抽象从共同的资源(数据和方法)供子类使用
多态:与类型无关的方式来操作对象,配合指针或者引用来使用,基类必须至少一个virtural虚函数,基类无法定义对象,只能由其派生的子类来定义对象
虚函数:默认情况下,成员函数皆在编译时静态进行,若要令其在运行时动态进行,需要在声明前加上关键字virtual,即虚函数的意义就在于动态运行时才绑定。
1、继承和多态
class Libmat
{
public:
Libmat() { cout << "Libmat construct" <<endl;}//默认构造函数
virtual ~Libmat();// 析构函数
virtual void print() const{ cout<< "Libmat print" <<endl;};//虚函数,并不表示该函数没作用,而是说明该函数会在子类中被重写
}
class Book : public Libmat
{
public:
Book(const string &title, const string &author) : _title(title), aurthor(author){cout << "Book construct" << endl;}
virtural ~Book();//虚函数,析构函数
virtual void print() { cout << "Book print " << endl;}//重写父类的print方法,定义为虚函数的原因是声明存在子类继承Book,重写Book的print方法
protectd://访问权限,类内,子类
string _title;
string _author;
}
子类定义时先调用父类的构造方法,子类对象调用print函数是调用自己的print,结束时先调用子类的析构函数,然后调用父类的析构函数结束对象生命。
重写:重写父类的方法,需要注意const支持重载
虚函数:对于父类的虚函数,子类必须实现父类的虚函数,如果重写基类的虚函数,则子类也带有虚函数,也无法提供对象
2、定义一个基类(基类设计)
基类必须至少一个虚函数,有虚函数的类都不能定义对象
class num_sequence
{
public:
num_sequence () {};//由于该基类没有任何非static成员变量,因此构造函数也可以不定义
virtual ~num_sequence(){};//基类的析构函数必须为虚函数,因为对象在结束生命时动态的确定结束那个对象,因此需要调用不同的类对象的析构函数
virtual void print(){ cout<< "num_sequence " << endl;};//与不同子类相关,定义为虚函数
protected:
virtual void gen_elem(const int &pos) const = 0; //基类完全没有任何操作,定义为纯虚函数
bool check_intergrity(int pos) const;//声明,为const成员函数
const static int _max_len = 1024; //静态成员变量
}
1、抽象出所有子类共同的操作行为(函数)以及共同的数据,都放到基类中实现
2、找到可能与不同子类相关的操作行为定义为虚函数,为子类提供一个接口,static修饰的成员函数属于类,不能定义为虚函数
3、成员的访问层级确定,类内private,子类内 protected,公共public
基类的改进版,提供一些公共的方法供子类继承
class num_sequence
{
public:
virtual ~num_sequence(){};//基类的析构函数必须为虚函数,因为对象在结束生命时动态的确定结束那个对象,因此需要调用不同的类对象的析构函数
virtual void print(){ cout<< "num_sequence " << endl;};//与不同子类相关,定义为虚函数
int length() {return _len;}//子类共有的操作统一抽象到基类中
int beg_pos() {return _beg_pos;}//子类共有的操作统一抽象到基类中
protected:
virtual void gen_elem(const int &pos) const = 0; //基类完全没有任何操作,定义为纯虚函数
bool check_intergrity(int pos) const;//声明,为const成员函数
num_sequence(const int &beg_pos, const int &len) : _beg_pos(beg_pos), _len(len) {}
int _beg_pos;
int _len;
const static int _max_len = 1024; //静态成员变量
static vector<int> _elems; //静态成员变量
}
3、定义一个派生类(派生类设计)
class fibonacci : public num_sequence
{
public:
fibonacci (const int &beg_pos, const int &len) : _beg_pos(beg_pos), _len(len) {}
virtual ~num_sequence(){};//基类的析构函数必须为虚函数,因为对象在结束生命时动态的确定结束那个对象,因此需要调用不同的类对象的析构函数
virtual void print(){ cout<< "fibonacci _sequence " << endl;};//与不同子类相关,定义为虚函数
int length() {return _len;}//子类共有的操作统一抽象到基类中
int beg_pos() {return _beg_pos;}//子类共有的操作统一抽象到基类中
protected:
virtual void gen_elem(const int &pos) const = 0; //基类完全没有任何操作,定义为纯虚函数
bool check_intergrity(int pos) const;//声明,为const成员函数
num_sequence(const int &beg_pos, const int &len) : _beg_pos(beg_pos), _len(len) {}
int _beg_pos;
int _len;
const static int _max_len = 1024; //静态成员变量
static vector<int> _elems; //静态成员变量
}
1、子类包含父类所有非static成员以及自身成员
2、子类必须实现父类中的虚函数(如果不实现,则默认继承父类的虚函数,该子类也不能实例化对象),对于虚函数的运行时绑定机制,调用时可以通过类作用域舍弃动态绑定操作,即在编译时就确定调用哪个实际函数
int Fibonacci::
elem (int pos) const
{
if(pos > _elem.size())
Fibonacci::gen_elem(pos);//调用时通过作用域来掩盖虚函数表,直接调用Fibonacci下的gen_elem
return elem[pos-1];
}
3、子类重写基类的方法,一般而言尽可能不去重写父类方法,而是在设计之初将与子类相关的函数定义为虚函数,通过虚函数来实现动态绑定,如果重写了父类的非虚方法,则是重定义了基类中的函数(重定义:只要函数名相同即重定义),重定义的危险很大,重定义父类的函数会把父类函数给覆盖,被重定义的父类函数失效,没有动态绑定效果,所有的子类对象调用该函数都为子类重定义的那个
重载:函数名相同,参数、返回值不同
重写:指重写父类的虚函数(所有一致,函数名,参数,返回值)
重定义:指重定义父类的非虚函数,父类被重定义的函数失效
4、重写父类的虚函数,必须 函数修饰符 const,参数,返回值 const都必须一致,虚函数机制会在两种情况下失效
1)constructor和destructor内(基类中的构造函数不可能调用子类中实现的虚函数)
2)使用的是基类的对象,而非基类的指针或者引用,即多态必须通过引用或者指针配合生效
2)错误导致失效子类实现父类的虚函数存在重写问题,比如类型,参数,修饰符
4、初始化,析构,拷贝
构造函数
基类中的构造函数:用于初始化基类中定义的成员变量,由于无法实例化对象,构造函数由子类来调用,所以基类的构造函数可以声明为protected
子类中的构造函数:一般初始化子类中的成员变量,也会修改父类中成员变量值
inline Fibonacci::Fibonacci(int len, int beg_pos) : num_sequence(int len, int beg_pos) {}//子类的构造函数指定调用哪个父类的构造函数
inline Fibonacci::Fibonacci(int len, int beg_pos) : _len(len), _beg_pos(beg_pos), _pelem(pm) {}//默认调用父类的默认构造函数
另一点,当父类中存在引用类型的成员变量时必须修改为指针,因为引用不能修改,而指针是动态,且父类中的成员变量是为子类服务的,肯定会被修改的
拷贝构造函数
继承中的拷贝构造函数同构造函数一样,另外,拷贝构造函数需要注意对于指针成员在拷贝构造函数中需要重新初始化
析构函数
5、多态(非继承实现多态,继承体系的动态、动态机制)
非继承实现多态
这种方式程序员维护函数列表,数据列表的成本很大
1、维护一套视子类而定的成员函数以及成员变量
2、父类中通过成员函数指针以及成员变量指针
3、父类中提供一个记录子类类型的id
class num_sequence
{
public:
enum ns_type{ns_unset, ns_fabonacci,ns_pell,ns_lucas};//枚举类型,用于标识子类类型对于的下标
private:
vector<int> *_elem;//用于指定视子类而定的成员变量
PtrType _pmf; //PtrType是函数指针类型,_pmf用于指向视子类而定的成员函数
ns_type _isa;//标识子类类型的id
}
配合一个set_sequence函数来设置与子类相关的成员函数、变量、标识
void num_sequence:: set_sequence(ns_type nst)//根据枚举类型来设置成员函数、变量、标识
{
switch(nst):
case ns_unset://默认没有设置时
_pmf = 0;
_elem = 0;
_isa = 0;
break;
case ns_fibonacci: ns_fabonacci: ns_pell: ns_lucas: //对应相应的子类
_pmf = func_table[nst];//函数指针指向维护的一套函数指针列表中某一个函数
_elem = &sequence[nst];//指向数据表中某一数据
_isa = nst;//之类相关标识
break;
}
运用继承体系的多态
运用继承体系,将num_sequence作为基类(将与子类相关的操作定义为虚函数),ns_fabonacci: ns_pell: ns_lucas作为子类继承num_sequence,那么对于与子类相关的操作可以通过指针或者引用来实现多态
inline void disylay(ostream &os, const num_sequence &ns, int pos)//num_sequence &ns 接收一个父类的引用,实际为实参传递的对象
{
os << "xxx" << ns.what_am_i() << ns.elem(pos) << std::endl;//其中what_am_i和elem在父类中是虚函数,实际调用是子类对象实现的虚函数
}
display(cout, ns_fabonacci);
display(cout, ns_pell);
ostream& operator<< (ostream &os, const num_sequence &ns){ return ns.print();}//重载<<操作符,print定义为虚函数
运行时类型鉴别动态机制
六、模板编程
模板函数主要是通过参数模板化(占位符代表参数的类型),参数的实际类型由主调函数给出,模板函数已在第二部分介绍,这里主要介绍类模板编程
1、Class Template 定义
template <typename valType>
class BinaryTree; //前置声明友元类
class BTNone {
friend class BinaryTree<valType>
public: //
private: //
valType _val;
int _cnt;
BTNode *_lchild;;//类模板变量定义时,在类内可以不加<valType>修饰
BTNode *_rchild;;//类模板变量定义时,在类内可以不加<valType>修饰
};
template <typename valType>
class BinaryTree{
public:
BinaryTree();
BinaryTree(const BinaryTree &);
~BinaryTree();
BinaryTree& operator= (const BinaryTree &);
bool empty();
void clear();
template <typename valType>
void print(const valType &val, char delimiter = '\n');
private:
BTnode<valType> *root;//类模板变量定义时,在类内可以不加<valType>修饰,类外需要BTnode<valType>限定
void copy(BTnode<valType> &tar, BTnode<valType> *src);
}
类模板下成员变量与成员函数
成员变量和成员函数在类内无需考虑作用域,类外需要加类命名空间限定
template <typename valType>
inline BinaryTree<valType>&//返回类型 BinaryTree<valType>:://作用域限定 operator= (const BinaryTree &rhs){
if(this != rhs){clear(); copy(_root, rhs._root);}
return *this;
}
2、类模板下参数传递处理
考虑参数传递 by value,by reference两种,对于常用基本类型 int double 等两种效率差别不大,对于类类型参数传递,当然首选by reference
bool find(const Matrix &val);
同样,在类的构造函数中,参数的初始化也有两种,一种是基于构造函数,一种是基于拷贝构造函数
template<typename valType>
inline BTnode<valType>:: BTnode(const valType &val) : _val(val) //不保证内置类型,首选构造函数初始化,调用val的构造函数初始化{
_cnt = 1;//内置类型,构造函数体内初始化效率差不多
_lchild = _rchild = 0;
_val = val; //切记 体内初始化是调用val的拷贝构造函数
}
如:BTnode<Matrix> btnm(matrix1)//当模板类传递Matrix类,则效率上首选 :_val(val)
3、类的成员函数模板
template <typename valType>//模板声明
inline void BinaryTree<valType>//作用域限定:: void print(const valType &val, char delimiter = '\n'){
cout<< val << delimiter;
}
4、函数模板接收参数为类/模板类
ostream& operator<< (ostream &, const int_BinaryTree &);//接收int类型的模板类,不可取
ostream& operator<< (ostream &, const BinaryTree<int> &);//接收int类型的模板类,不可取
template <typaname elemType>
ostream& operator<< (ostream &os, const BinaryTree<elemType> &bt){//直接以类模板作为函数模板的参数
bt.print(os);//根据bt类模板实际的elemType来操作
}
5、常量表达式与默认参数
将模板中的参数占位符当做参数传递来用
template <int len, int beg_pos>
class NumberSerise{
};