《essential c++》阅读笔记

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/winter_wu_1998/article/details/85156224

容器

  • 所有容器的公共操作
    • == 和 !=
    • empty()
    • size()
    • clear()
    • begin() 和 end()
    • insert()
    • erase()
      • 因为 delete 为关键字不能用,所以改用 erase

顺序容器

  • 一般指 vector,list或deque

  • 五种初始化方式

    • 直接产生空容器
      • list<string>;
    • 产生特定大小的容器
      • vector<string> strVec(10);
    • 产生特定大小的容器并指定统一初值
      • vector<int> intVec(5,0);
    • 复制已经存在的容器
      • list<string> newList = oldList;
    • 如果想要用数个不同初值初始化,无法像数组那样直接用大括号,所以只能借助数组用 iterator 初始化
    int ele[5] = {1, 2, 3, 4, 5};
    vector<int> eleVec(ele, ele +5);
    
  • 基本操作

    • push_back()
    • pop_back()
      • 注意这个方法没有返回值
    • push_front()
      • vector没有这个方法
    • pop_front()
      • vector没有这个方法
      • 没有返回值
    • back()
    • front()

关联容器

  • map
    • 有两个成员 first和second
    • first 是key, second 是value
    • 使用下标访问map容器时,如果key不在map中,会被自动加入map
      • 因此查找一个key是否在map中应该调用 find方法
      • 也可以用 count方法代替,因为map中所有key都最多有一个

指针

  • 在不确定指针非空前,if语句使用指针时首先应当判断指针存在性,否则会会出现运行错误
if (!pt && ...)
  • 指针和引用的差异就在于指针可能不指向某个对象(nullptr),而引用一定要显式绑定当一个对象
    • 另一个差异是引用一旦绑定就不可更改对象
  • ptr+ 1前进的地址与 ptr 是什么类型的指针有关
    • 如果 ptr 是int(4byte长度)指针,则地址事实上+4

函数

  • 函数指针数组的写法
    • int (*pt[])(double, string)
  • 内联函数的定义必须放在头文件中

泛型

  • 包含在头文件

  • 为了实现容器独立,不要直接在容器上操作,而是借助指针标识要迭代的范围**

    • 比如用first和last指针来代替数组(容器)名和大小
    • 其中last指向最后一个元素的后一个地址
  • 为了防止容器为空时指针的操作导致运行时错误,应该将取第一个/最后一个元素的操作封装为一个函数

    Val* begin(const vector<Val> &vec)
    {
        return vec.empty() ? 0 : &vec[0];
    }
    
  • 然而不同类型的容器指向下一个元素的操作是不同的(比如vector和list)

    • 为了统一语法,我们在指针行为之上多提供一层抽象,于是产生了iterator
    • iterator的定义应该提供容器类型和 iterator所指向元素类型两个信息
      • vector<string>::iterator iter;
      • list<int>::const_iterator constIter;
  • 泛型算法还应该能让用户自己定义equality运算符

    • 传入函数指针或者运用 function object
    • 一般命名时加上 _if 后缀
  • **find()**一般用来搜索无序容器,**binary_search()**搜索有序容器

    • **find()**返回元素所在位置的迭代器,如果没找到返回 end()
    //避免重复查找
    while((iter = find(iter, vec.end(), val)) != vec.end()) {...}
    
  • **search()**寻找子序列

  • STL中的 sort算法在数据较小时采用插入排序,递归深度过高时采用堆排序,否则采用快排

    • 因为递归需要用到栈空间,而占空间大小有限,所以递归过深时会栈溢出
    • 数据大不一定递归深度大

  • 在类的声明中定义的成员函数自动被视为内联函数

  • 当用一个类对象初始化另一个类对象时会自动调用拷贝构造函数

  • 静态类

    • 静态成员对象

      • 只被创造一次,是唯一的
        • 很像全局对象
      • 该类的每一个实例都可以访问该成员对象
    • 静态成员函数

      • 只能访问静态类成员对象

      • 可以不依赖特定实例调用,直接在该函数前加上类作用域即可

        bool isElement = Object::isElement(val);
        
      • 无法被声明为虚函数

    • 静态成员只能在内部声明,必须在外部单独定义

      • 定义方法类似成员函数的定义,需要写出类型和类作用符
      const int Object::num;
      vector<int> Object::vec(num);
      
    • 如果为const类型静态变量则可以在声明类时同时定义,但也只能用常量定义

      • 但即使这样最好也要显示在外部声明下,例如上面的num
  • 友元

    • 如果要声明一个类的成员函数为另一个类的友元,要在之前在另一个类声明该类
      • 如果直接声明友元类则无需预先声明该类
  • 重载运算符

    //相等运算
    bool operator==(const Object&) const;
    
    // ++i
    objectIterator& operator++();
    
    //i++
    //int参数无意义,只是为了区分前置和后置
    //由于很多编译器在参数没有被用到时会有警告,所以参数未命名
    objectIterator operator++(int);
    
    //输出运算
    //不设计为成员函数,否则在写输出时必须为 obj << cout 的形式
    //return对象为参数os本身
    //由于该函数为非成员函数,所以要访问成员变量需要成为友元,避免这种写法的办法就是让该函数调用成员函数
    //当然,这个成员函数必须是public的,如果是private的还是要声明为友元函数
    ostream& operator<<( ostream & os, const Object &rhs)
    {
        return rhs.print(os);
    }
    

继承和多态

  • 继承让相关联的类共享共通的接口
  • 多态使得我们得以用一种与类型无关的方式来操作类对象
  • 派生对象创建时基类和派生类的构造器都会被执行(销毁时析构器也都会执行)
  • 动态绑定
    • 对于一个类方法的调用,到底调用哪个派生类的方法要到运行时才被解析
    • 默认成员函数在编译时静态解析,通过在声明前加上 virtual关键字使其子运行时动态解析
    • 析构器应该声明前加上 virtual以实现运行时解析(当然基类的析构函数也会被执行)
    • 有时候我们不想通过动态绑定来调用某一方法,这是就要在方法前加上我们想调用类的类作用域符,此时显式调用该类方法的版本
  • 构造函数不支持 virtual
    • 虚函数调用是在部分信息下完成工作的机制,允许我们只知道接口而不知道对象的确切类型。 要创建一个对象,你需要知道对象的完整信息。 特别是,你需要知道你想要创建的确切类型。 因此,构造函数不应该被定义为虚函数
    • 虚函数的作用在于通过子类的指针或引用来调用父类的那个成员函数。而构造函数是在创建对象时自己主动调用的,不可能通过子类的指针或者引用去调用
  • 虚函数
    • 对于虚函数,基类可以不实现,直接用基类的版本就可,但是对于纯虚函数,由于基类并没有实现,所以每个子类都必须实现
    • 虚函数机制的实现只能通过指针和引用
      • 例如参数是一个基类实例而非引用或者指针,那么该参数调用的方法一定为基类方法,即使传入的实参是一个子类对象
      • 而子类对象自己多加的成员变量和函数都会被切割掉
      • 原因是该参数在函数调用时就分配了容纳实际对象的空间,而引用或指针只是分配了地址

模板

//定义
template <int len, int pos>
class Object{...};

//使用
Object<10, 0> obj;
  • 非类型参数

    • 模板的参数不一定是某种类型,也可以是常量表达式
      • 一般是整数(或者枚举),也可以是全局变量或函数的地址
      • 所以想传入浮点数或者类对象时,可以先定义一个全局变量,然后传入地址
    • 这样做是为了将一些运行时的计算转移到了编译时,节省了运行时开销和内存占用
    • 一般这样的编程风格叫做模板元编程
    • 比如对于向量的计算可以避免for循环,缺点是导致代码膨胀
    //模板
    template <int length>
    Vector<length>& Vector<length>::operator+=(const Vector<length>& rhs) 
    {
        for (int i = 0; i < length; ++i)
            value[i] += rhs.value[i];
        return *this;
    }
    
    //当length实际传入为2时,编译器将上面的模板转换如下
    template <>
    Vector<2>& Vector<2>::operator+=(const Vector<2>& rhs) 
    {
        value[0] += rhs.value[0];
        value[1] += rhs.value[1];
        return *this;
    }
    
  • 成员模板函数

    • 如果我们想要对类中的某个成员函数使用模板,但是又不想让整个类都模板化,此时可以使用成员模板函数
    • 好处是无需在调用时显示声明传入参数的类型,编译器自动匹配
    #include<iostream>
    #include<string>
    using namespace std;
    
    class PrintIt {
    public:
        PrintIt(ostream &os) :_os(os) {
        }
    
        template <typename elemType>
        void print(const elemType& elem, char delimiter = '\n') {
            _os << elem << delimiter;
        }
    private:
        ostream & _os;
    };
    
    int main()
    {
        PrintIt to_standard_out(cout);
        to_standard_out.print("hello");
        to_standard_out.print(1024);
    
        string my_string("this is a string!");
        to_standard_out.print(my_string);
    
    
        getchar();
        return 0;
    }
    
    //输出
    hello
    1024
    this is a string!
    

异常

  • 这里的异常和segmentation fault等硬件异常不一样
  • 异常是某种对象,继承与特定的异常类
class overflow
{
	int index;
    string name;
    
    public:
    overflow(int Index, string Name):
    	index(Index), name(Name);
        
    void print(ostream &os = cerr) 
    {
    	os << "error:" << name << " at current index" << index << endl;
    }
}
  • 两种抛出异常的方式
throw overflow(index, name);

overflow ex(index, name);
throw ex;
  • 捕获异常

    • try和catch一定要一起出现,可能引发异常的代码放在try中
      • throw本身可以单独出现,但是当一个函数throw异常后,函数剩余的部分不会被执行,而是直接退出函数,查看调用端是否存在异常捕获(同样调用端在该函数后的语句也不会被执行)
    • 当抛出的异常对象和catch中的对象类型一致时异常被捕获
    • 如果该异常不能现在被完整处理(或许要在上级函数继续处理)那么可以再次抛出异常
      • 如果异常一直到main函数还是没有合适的catch,则会被默认处理,即中断程序
    • 如果想要捕捉任何类型的异常,catch的括号中用省略号即可
    try
    {
      //code   
    }
    catch(overflow& ex)
    {
        ex.print();
        throw;
    }
    catch(...)
    {
        exit(1);
    }
    

    标准异常

  • C++为很多异常情况定义了标准异常体系(比如new失败抛出bad_alloc异常)

  • 这些标准异常都继承自没exception的抽象基类,且都有 **what()**方法,用于输出相关文字表述

  • 我们可以将自己定义的异常类继承于exception基类

    • 好处是可以被任何打算捕获标准异常的catch捕获,不用再用**catch(…)**捕获所有未考虑的异常
    class overflow : public exception
    {
    	int index;
        string name;
        
        public:
        overflow(int Index, string Name):
        	index(Index), name(Name);
            
        const char* what() const
        {
        	ostringstream ex_msg;
        	static string msg;
        
            //ostringstream 可以将不同类型的数据格式化为字符串
            //需要 #include <sstream>
            ex_msg << "error:" << name << " at current index" << index << endl;
            
            msg = ex_msg.str();
            
            return msg.c_str();
        }  
    }
    
    //捕获
    catch( const exception& ex)
    {
        cerr << ex.what() << endl;
    }
    

猜你喜欢

转载自blog.csdn.net/winter_wu_1998/article/details/85156224