zoukankan      html  css  js  c++  java
  • c++笔记2(参考learncpp.com)

    由于learncpp.com内容过多,此篇博客记录后半部分,前半部分请移步我的博客c++笔记1(参考learncpp.com)

    1、函数传参的3种方式引用实现多返回值函数

    3种传参方式:值传递、引用传递、地址传递。

    单个基本数据类型,用值传递。
    其他类型(string、array等)用引用传递,若不想参数被更改,用const修饰。

    引用、指针需要注意的是函数参数必须为左值(如i=5,i是左值,5是右值),因为这两者有“地址”观念。

    int foo1(int x);    // pass by value
    int foo2(int &x);    // pass by reference
    int foo3(int *x);    // pass by address
    
    int i {};
    
    foo1(i);  // i不被更改,因为有拷贝
    foo2(i);  // i可能被改,若不想被改,用int foo2(const int &x);
    foo3(&i); // i可能被改

    引用实现多返回值函数

    #include <iostream>
    #include <cmath>    // for std::sin() and std::cos()
     
    void getSinCos(double degrees, double &sinOut, double &cosOut)
    {
        static constexpr double pi { 3.14159265358979323846 }; // the value of pi
        double radians = degrees * pi / 180.0; //度转为弧度,如30°→Π/6
        sinOut = std::sin(radians);
        cosOut = std::cos(radians);
    }
     
    int main()
    {
        double sin(0.0);
        double cos(0.0);
     
        getSinCos(30.0, sin, cos);
     
        std::cout << "The sin is " << sin << '
    ';
        std::cout << "The cos is " << cos << '
    ';
        return 0;
    }

    另外可以参考我的另外一篇博客,函数间参数传递的3种方式

    2、函数多返回值的3种实现方式

    方式一:引用,参考上节内容。OpenCV图像处理框架中常见此用法。

    方式二:结构体。

    #include <iostream>
    
    struct S
    {
        int m_x;
        double m_y;
    };
    S returnStruct() //返回结构体
    {
        S s;
        s.m_x = 5;
        s.m_y = 6.7;
        return s;
    }
    
    int main()
    {
        S s{ returnStruct() };
        std::cout << s.m_x << ' ' << s.m_y << '
    ';
    
        return 0;
    }

    方式三:元组 std::tuple。

    #include <tuple>
    #include <iostream>
    
    std::tuple<int, double> returnTuple() // 返回元组
    {
        return { 5, 6.7 };
    }
    
    int main()
    {
        std::tuple s{ returnTuple() }; // 调用函数
        std::cout << std::get<0>(s) << ' ' << std::get<1>(s) << '
    '; // 用std::get<n>获取元组中元素
        /*或者使用如下方式
        int a;
        double b;
        std::tie(a, b) = returnTuple(); // 用std::tie拆解元组
        //auto [a,b]{returnTuple()}; // c++17,效果同上,拆解元组
        std::cout << a << ' ' << b << '
    ';
        */
        return 0;
    }

    3、指向函数地址的指针,std::function的使用

    功能:
    ① 函数指针主要用于在数组(或其他结构)中存储函数,
    ② 在需要将函数(此函数又称回调函数)传递给另一个函数时。
    因为声明函数指针的本机语法很难看而且容易出错,所以我们建议使用std::function。

    严格按以下格式定义:
    int (*fcnPtr)(); //指针fcnPtr是指向“无参且返回int型”函数的指针,fcnPtr可指向任何同类型的函数。
    int (*const fcnPtr)(); //const函数

    如下3种等效,fcnPtr指向“两个参数int、double型且返回int型”的函数
    int (*fcnPtr)(int,double);
    std::function<int(int,double)> fcnPtr; //<返回类型(每个参数类型)>
    auto fcnPtr;

    #include <iostream>
    // #include <functional> //for std::function
    
    int foo(){    return 5;}
    int goo(){    return 6;}
    
    void main()
    {
        int(*fcnPtr)(){&foo}; //指针只能指向“无参且返回int型”的函数
        //auto fcnPtr{ &foo }; //使用auto关键字,同上等效
        //std::function<int()> fcnPtr{ &foo }; //使用std::function,同上等效
        fcnPtr=&goo; //指向函数goo的地址,不需要()
        std::cout << fcnPtr(); //隐式使用,更简洁
        //std::cout << (*fcnPtr)(); //显式使用,同上等效
    }

    注意,带默认参数的函数,必须显式地传递任何默认参数的值。默认参数是在编译时解析的,而函数指针是在运行时解析的。因此,当使用函数指针进行函数调用时,无法解析默认参数。

    【函数作为参数实现升序、降序】 

    #include <utility> // for std::swap
    #include <iostream>
    
    // 作为参数的函数,又称回调函数,含两个int参数、返回bool。ascending、descending两个回调函数。
    void selectionSort(int *array, int size, bool(*comparisonFcn)(int, int))
    {
        // 前 n-1个元素,与之后的比较大小
        for (int startIndex{ 0 }; startIndex < (size - 1); ++startIndex)
        {
            int bestIndex{ startIndex }; //存储最大或最小元素的下标
            // 后n-1个元素
            for (int currentIndex{ startIndex + 1 }; currentIndex < size; ++currentIndex)
            {            
                if (comparisonFcn(array[bestIndex], array[currentIndex])) // 使用回调函数作为判断条件
                {
                    bestIndex = currentIndex; // 存储最大或最小元素的下标
                }
            }
    
            // 交换元素,之前是交换下标
            std::swap(array[startIndex], array[bestIndex]);
        }
    }
    
    // 回调函数,bool类型当作触发升序的“开关”
    bool ascending(int x, int y)
    {
        return x > y; // 前>后则返回true,true则触发交换
    }
    
    // 回调函数
    bool descending(int x, int y)
    {
        return x < y; 
    }
    
    void printArray(int *array, int size) //数组会退化为指针,丢失长度信息,所以显式指定
    {
        for (int index{ 0 }; index < size; ++index)
        {
            std::cout << array[index] << ' ';
        }
        std::cout << '
    ';
    }
    
    int main()
    {
        int array[9]{ 3, 7, 9, 5, 6, 1, 8, 2, 4 };
        // 降序
        selectionSort(array, 9, descending);
        printArray(array, 9);
        // 升序
        selectionSort(array, 9, ascending);
        printArray(array, 9);
    
        return 0;
    }

    4、 5个内存区域(又称为段)

    The code segment (also called a text segment),代码段(也称为文本段),编译后的程序位于内存中,代码段通常是只读的。
    The bss segment (also called the uninitialized data segment),bss段(也称为未初始化数据段),用于存储未初始化的全局变量和静态变量。
    The data segment (also called the initialized data segment),数据段(也称为初始化的数据段),用于存储初始化的全局变量和静态变量。
    The heap,堆段,从堆中动态分配的变量。
    The call stack,调用堆栈,其中存储函数参数、局部变量和其他函数相关信息。

    5、std::vector的堆栈操作(Stack behavior)

    Stack 是后进先出的结构(LIFO),如果你在堆栈顶部放一个新盘子,从堆栈中移出的第一个盘子将会是你最后推入的盘子。当项目被推入堆栈时,堆栈会变得更大。当项目被弹出时,堆栈会变得更小。

    【容量与长度】
    int *array{ new int[10] { 1, 2, 3, 4, 5 } }; //容量是10,长度是5。
    容量会依据长度自动扩容,但未必会随着长度缩小。

    #include <iostream>
    #include <vector>
    
    int main()
    {
        std::vector<int> array{ 0, 1, 2 };
        std::cout << "length: " << array.size() << "  capacity: " << array.capacity() << '
    ';
        array.resize(5); // 长度变为5,元素变为0,1,2,0,0
        std::cout << "length: " << array.size() << "  capacity: " << array.capacity() << '
    ';
        array.resize(2); // 长度变为2,元素只剩0,1
        std::cout << "length: " << array.size() << "  capacity: " << array.capacity() << '
    ';
    }

    【堆栈操作】

    push_back()   将元素压入堆栈。堆栈变大。
    back()     返回堆栈顶部元素的值。堆栈不变,只是看最顶部的元素。
    pop_back()  从堆栈中弹出一个元素。堆栈变小。

    std::vector<int> stack{};
    stack.push_back(5); 
    stack.push_back(3);
    stack.push_back(2); //stack中元素5,3,2
    std::cout << "top: " << stack.back() << '
    '; // 2
    stack.pop_back(); //拿走最顶部的2,stack中元素剩5,3

    最终stack容量为3,长度为2

    6、递归与迭代(Recursive vs iterative)

    递归就是函数自己调用自己,迭代就是常见的for、while循环遍历。

    迭代效率高于递归,因为递归完成前、后,会推入和取出堆栈帧,都会产生一些开销。

    如下图所示,每递归一次,count被int一次,新开辟一次内存,这些内存在递归完成后,需要释放掉。stack栈是后进先出的。

     

     代码:

    #include <iostream>
    
    void countDown(int count)
    {
        std::cout << "push " << count << '
    ';
    
        if (count > 1) // 终止条件
            countDown(count - 1); //递归前、后会推入和取出堆栈帧。因为参数count会被int很多次,即有新内存,这些新内存需要释放掉。
    
        std::cout << "pop " << count << '
    ';
    }
    
    int main()
    {
        countDown(5);
        return 0;
    }

    7、assert 与 static_assert

    assert语句是一个预处理器宏,它在运行时计算条件表达式。
    static_assert的条件部分必须能够在编译时计算。因为static_assert不是在运行时计算的,所以static_assert语句也可以放在代码文件中的任何位置(甚至在全局空间中)。

    如果条件表达式为真,则assert语句不执行任何操作。
    若为false,则显示一条错误消息并终止程序。此错误消息包含失败的条件表达式,以及代码文件的名称和断言的行号。
    断言会损耗一点性能,一般Debug时使用,Release时关闭。IDE一般默认设置了此功能(即宏NDEBUG)

    【让断言具有描述性】
    assert(found && "你的描述文字"); 或 static_assert(found, "你的描述文字");
    原理:"你的描述文字"永远为true,若found为false则触发断言,字符串也会输出出来。

    【注意】
    exit()函数和assert()函数(如果触发)会立即终止程序,而没有机会做任何进一步的清理(例如关闭文件或数据库)。因此,应该明智地使用它们(仅在程序意外终止而不太可能发生损坏的情况下使用)。

    #include <iostream>
    #include <cassert> //for assert()
    
    int main()
    {
        const int i{ 1 }; //若无const,则i在编译时是未知的,运行以后才知道是1
        static_assert(i < 0, "必须是负数"); //条件必须是编译时已知的
    
        int j{ 1 };
        assert(i < 0 && "必须是负数"); //运行时
        
        return 0;
    }

    8、Lambda

    F.7.15与F.7.16章节暂时搁置,后期再回来更新。

    https://www.learncpp.com/cpp-tutorial/introduction-to-lambdas-anonymous-functions/

    https://www.learncpp.com/cpp-tutorial/lambda-captures/

    9、struct与class

    在C++中,对只有数据的对象使用struct关键字,对既有数据又有函数的对象使用class关键字。

    因为,类会清理自己的内存是合理的(例如,一个分配内存的类会在销毁之前释放内存),但一个结构这样做就不安全了。

    注意,struct成员默认public,class成员默认private。

    一般将class成员变量设置为私有,成员函数设置为公有,除非您有充分的理由不这样做。

    struct DateStruct //类与结构体,在纯数据时几乎无差别。
    {
        int year{};
        int month{};
        int day{};
    };
     
    class DateClass
    {
    public:
        int m_year{};
        int m_month{};
        int m_day{};
    };

    10、类与类的关系——Composition组合

     如,游戏中,生物有名称、位置这2个属性,位置可以单独拿出来作为类,成为生物类的一部分。

    生物消亡,位置也会消亡。即整体负责局部的释放。

    Point2D.h 简单的函数实现直接写头文件里了,复杂的可以写对应的cpp里。

    #pragma once
    
    #include<iostream>
    
    class Point2D
    {
    private:
        int m_x;
        int m_y;
    
    public:
        Point2D():m_x{0},m_y{0} //默认构造
        {
        }
        Point2D(int x, int y) :m_x{ x }, m_y{ y } //含参构造
        {
        }
        void setPoint(int x, int y) //访问函数set、get
        {
            m_x = x;
            m_y = y;
        }
        int getX () const
        {
            return m_x;
        }
        int getY() const
        {
            return m_y;
        }
    };

    Creature.h

    #pragma once
    
    #include<string>
    #include"Point2D.h"
    class Creature
    {
    private:
        std::string m_name;
        Point2D m_location; //点是生物的一部分,生物负责点的消亡。
    public:
        Creature(std::string name, Point2D location) :m_name{ name }, m_location{ location }
        {
        }
        void moveTo(int x, int y) //生物只需负责运动到哪,无需为创建点担忧
        {
            m_location.setPoint(x, y);
        }
        void printMsg()
        {
            std::cout << m_name << " is at" << '(' << m_location.getX() << ',' << m_location.getY() << ')' << '
    ';
        }
    };

    main.cpp

    #include <iostream>
    #include"Creature.h"
    #include"Point2D.h"
    
    int main()
    {
        std::cout << "Enter a name
    ";
        std::string name;
        std::cin >> name;
        Creature creature{ name,{4,7} };
        creature.printMsg();
        creature.moveTo(5, 8);
        creature.printMsg();
        return 0;
    }

    11、类与类的关系——Aggregation聚合 

    Aggregation聚合,整体不负责局部的创建与释放。局部可以在同一时刻属于不同的整体。

    Composition组合,偏向于对象的固有属性,对象不存在,属性也就无意义了(消亡)。

    人——出生日期,Composition组合

    人——国家,Aggregation聚合

    聚合可能更危险,因为聚合不处理其部分的分配。分配由外部方完成。如果外部方不再有指向废弃部分的指针或引用,或者它只是忘记做清理(假设类会处理),那么内存将会泄漏。

    12、类与类的关系——Association关联

    对象之间属于弱关系,可单向/双向,各自独立。如病人与医生。

    通常,应该避免双向关联,因为它们增加了复杂性,而且往往难以不出错地编写。

    13、类与类的关系——Dependencies依赖

    当一个对象为了完成某些特定任务而调用另一个对象的功能时,就会发生依赖。这是一种比关联更弱的关系,但是,对所依赖的对象的任何更改都可能破坏(依赖的)调用者的功能。依赖关系始终是单向关系。

    依赖关系通常不在类级别表示——也就是说,所依赖的对象没有作为成员链接。相反,所依赖的对象通常在需要时实例化(比如打开文件向其写入数据),或者作为参数传递到函数中。

    关联是类级别上两个类之间的关系。也就是说,一个类将关联类的直接或间接“链接”作为成员保存。例如,Doctor类有一个指向其患者的指针数组作为成员。

    14、类与类的关系——Inheritance继承

    类与类间满足is-a,就可以使用继承。如苹果is-a水果。

    直接在派生类的初始化列表中赋值基类的成员变量是无效的,可以在初始化列表中使用基类构造函数,其位置并不重要——它总是首先执行。
    派生类不能直接访问基类的私有成员,可以使用访问函数来访问。

    #include <iostream>
    #include <string>
    
    class Base
    {
    public:
        Base(int id=0)
            :m_id{id} //构造函数,初始化成员变量
        {}
        int getId() const{ return m_id; } //访问函数
        void setId(int temp) { m_id = temp; }
    private:
        int m_id;
    };
    
    class Derived :public Base
    {
    public:
        Derived(double cost=0.0)
            :m_cost{cost}
        {}
        double getCost() const{ return m_cost; } //访问函数
    
    private:
        double m_cost;
    };
    
    int main()
    {
        Derived derived{1.3};
        std::cout << derived.getCost() << '
    ';
        derived.setId(10); //通过基类的访问函数修改基类的成员变量
        std::cout << derived.getId() << '
    ';
    
        return 0;
    }

    也可以在派生类的初始化列表中使用基类的构造函数,赋值基类的成员变量

    #include <iostream>
    #include <string>
    
    class Base
    {
    public:
        Base(int id=0)
            :m_id{id} //构造函数,初始化成员变量
        {}
        int getId() const{ return m_id; } //访问函数
        //void setId(int temp) { m_id = temp; }
    private:
        int m_id;
    };
    
    class Derived :public Base
    {
    public:
        Derived(double cost=0.0,int id=0)
            :m_cost{cost},
            Base{id} //m_id{id}无效,Call Base(int) constructor with value id!
        {}
        double getCost() const{ return m_cost; } //访问函数
    
    private:
        double m_cost;
    };
    
    int main()
    {
        Derived derived{1.3,10}; //省去了derived.setId(10);
        std::cout << derived.getCost() << '
    ';
        std::cout << derived.getId() << '
    ';
    
        return 0;
    }

    15、公有继承、基类成员的访问说明符

    继承时推荐公有继承,其他继承参考标题链接。

    任何人都可以访问公共成员。
    受保护的基类成员可以被派生类直接访问,但不能被公众访问。
    私有的基类成员不可被派生类、公众访问。

    class Base
    {
    public: //公有成员
        int m_public;
    protected: //受保护成员
        int m_protected;
    private: //私有成员
        int m_private;
    };
    
    class Derived : public Base //派生类,公有继承
    {
    public:
        Derived()
        {
            m_public = 1; // 类内可访问
            m_protected = 2; // 类内可访问
            m_private = 3; // 类内不可访问
        }
    };
    
    int main()
    {
        Base base; //Derived derived同理
        base.m_public = 1; // 类外可访问
        base.m_protected = 2; // 类外不可访问
        base.m_private = 3; // 类外不可访问
    }

    16、基类指针

    基类指针等效于基类::,即使指向其他类,调用的仍是基类的成员。

    即,基指针或引用只能调用函数的基版本,而不能调用派生版本。

    #include<iostream>
    #include<string_view>
    #include<string>
    
    class Animal
    {
    public:
        Animal(const std::string &name)
            :m_name{name}
        {}
        const std::string &getName() const{return m_name;}
        
        std::string_view speak() const{return "???";}
        
    private:
        std::string m_name;
    };
    
    class Cat:public Animal
    {
    public:
        Cat(const std::string &name)
            :Animal{name} //派生类初始化基类的私有成员变量
        {}
        std::string_view speak() const {return "Meow";}
    };
    
    int main()
    {
        Cat cat{"Tom"}; //基类的私有成员变量被初始化为Tom
        std::cout<<cat.getName()<<" "<<cat.speak()<<'
    ';//Tom Meow
    
        Animal *p{&cat}; //因为p是Animal基类的指针,所以p->是Animal::而不是Cat::
        std::cout<<p->getName()<<" "<<p->speak()<<'
    '; //Tom ???
    
        return 0;
    }

    如下是一种让cat发出Meow而不是???的方法,与上述没有本质区别,基类中增加m_speak成员变量,在派生类中初始化它。

    #include<iostream>
    #include<string_view>
    #include<string>
    
    class Animal
    {
    public:
        Animal(const std::string &name,std::string_view speak)
            :m_name{name},m_speak{speak}
        {}
        const std::string &getName() const{return m_name;}
        std::string_view speak() const { return m_speak; }
    
    private:
        std::string m_name;
        std::string_view m_speak;
    };
    
    class Cat:public Animal
    {
    public:
        Cat(const std::string &name)
            :Animal{name,"Meow"} //派生类初始化基类的私有成员变量
        {}
    };
    
    int main()
    {
        Cat cat{"Tom"};
        Animal *p{&cat}; //p->依然是Animal::而不是Cat::
        std::cout<<p->getName()<<" "<<p->speak()<<'
    '; //Tom Meow
    
        return 0;
    }

    17、虚函数(c++中重量级内容)

    为了解决“基指针或引用只能调用函数的基版本,而不能调用派生版本”问题,虚函数闪亮登场。

    虚函数是一种特殊类型的函数,在调用时,它解析为存在于基类和派生类之间的函数的最终派生版本。这种能力称为多态性。
    如果派生函数具有与基版本函数相同的签名(名称、参数类型以及是否为常量)和返回类型,则认为该派生函数是匹配的,这样的函数称为覆盖(重写)。

    #include<iostream>
    #include<string_view>
    #include<string>
    
    class Animal
    {
    public:    
        virtual std::string_view speak() const{return "???";} //加上virtual关键字
    };
    
    class Cat:public Animal
    {
    public:
        virtual std::string_view speak() const {return "Meow";} //加上virtual关键字
    };
    
    int main()
    {
        Cat cat;
        Animal *p{&cat};
        std::cout<<p->speak()<<'
    '; //Meow,如果上述不加virtual,则???
    
        return 0;
    }

    通常解析为Animal::speak()。但是,Animal::speak()是虚拟的,它告诉程序去查看基函数和派生函数之间是否有更多派生版本可用,有则用派生版本。在本例中,它将解析为派生的::speak()。

    基类、派生类中,虚函数的名称、参数、返回类型必须完全一致。否则即使virtual修饰,也认为是独立的函数。

    若基类中函数是虚的,那么派生类中默认此函数也是虚的,不过用virtual修饰是一种好习惯。

    class Base //这两个函数是独立的
    {
    public:
        virtual int getValue() const { return 5; }
    };
     
    class Derived: public Base
    {
    public:
        virtual double getValue() const { return 6.78; }
    };

    18、为虚函数而生的override、final关键字

    不加override,虚函数也可以运行,但是对于不是真正的虚函数(认为重写了,其实没有),编译器不提醒错误。

    class A
    {
    public:
        virtual const char* getName1(int x) { return "A"; }
        virtual const char* getName2(int x) { return "A"; }
        virtual const char* getName3(int x) { return "A"; }
    };
     
    class B : public A
    {
    public:
        virtual const char* getName1(short int x) override { return "B"; } // compile error, function is not an override
        virtual const char* getName2(int x) const override { return "B"; } // compile error, function is not an override
        virtual const char* getName3(int x) override { return "B"; } // okay, function is an override of A::getName3(int)
     
    };
     
    int main()
    {
        return 0;
    }

    若禁止虚函数被重写或类被继承,使用final说明符。若用户试图覆盖已指定为final的函数或类,编译器将给出编译错误。

    class A
    {
    public:
        virtual const char* getName() { return "A"; }
    };
     
    class B : public A
    {
    public:
        virtual const char* getName() override final { return "B"; } // okay, overrides A::getName()
    };
     
    class C : public B
    {
    public:
        virtual const char* getName() override { return "C"; } // compile error: overrides B::getName(), which is final
    };

    类不可被继承

    class A
    {
    public:
        virtual const char* getName() { return "A"; }
    };
     
    class B final : public A // note use of final specifier here
    {
    public:
        virtual const char* getName() override { return "B"; }
    };
     
    class C : public B // compile error: cannot inherit from final class
    {
    public:
        virtual const char* getName() override { return "C"; }
    };

    19、虚析构

    在17节中讲到,基类、派生类中,虚函数的名称、参数、返回类型必须完全一致。否则即使virtual修饰,也认为是独立的函数。

    但有特殊情况,协变返回类型

    #include <iostream>
    
    class Base
    {
    public:
        virtual Base* getThis() { std::cout << "called Base::getThis()
    "; return this; } //返回指向Base类的指针
        void printType() { std::cout << "returned a Base
    "; }
    };
    
    class Derived : public Base
    {
    public:
        // 通常,重写,返回类型必须一致,但是Derived继承自Base, 允许写成Derived*,真正返回的依然是Base*
        virtual Derived* getThis() override { std::cout << "called Derived::getThis()
    ";  return this; }
        void printType() { std::cout << "returned a Derived
    "; }
    };
    
    int main()
    {
        Derived d;
        Base* b = &d;
        d.getThis()->printType(); // 调用Derived::getThis(), 返回Derived*, 调用Derived::printType
        b->getThis()->printType(); // 调用Derived::getThis(), 返回Base*, 调用Base::printType
    
        return 0;
    }

    b->本质是Base::,但Base :: getThis()是虚函数,因此调用Derived :: getThis()。

    尽管Derived :: getThis()返回Derived *,Derived *会被向上转换为基版本Base *,因此,将调用Base :: printType()。

    虚析构也如此,在处理继承时,应该将任何显式析构函数设为虚函数

    #include <iostream>
    class Base
    {
    public:
        virtual ~Base() // note: virtual
        {
            std::cout << "Calling ~Base()
    ";
        }
    };
    
    class Derived : public Base
    {
    private:
        int* m_array;
    
    public:
        Derived(int length)
            : m_array{ new int[length] }
        {   }
    
        virtual ~Derived() // 基函数虚,默认也虚。如果~Base()不是虚函数,则认为此函数没有重写~Base()
        {
            std::cout << "Calling ~Derived()
    ";
            delete[] m_array;
        }
    };
    
    int main()
    {
        Derived* derived{ new Derived(5) };
        Base* base{ derived };
    
        delete base; //若~Base()非虚,则只执行~Base()。否则,先~Derived()再~Base()
    
        return 0;
    }

    base是一个Base*,当base被删除时,程序会查看~Base()是否为虚函数。若不是,则不会再找其他版本,直接执行~Base()。若是,则执行~Derived()再执行~Base()。

    20、纯虚函数(抽象函数)、抽象基类、接口类

    纯虚函数使得基类不能被实例化,派生类被迫在实例化这些函数之前定义这些函数。这有助于确保派生类不会忘记重新定义基类所期望的函数。
    virtual int getValue() const = 0; //纯虚函数格式

    ① 任何具有一个或多个纯虚函数的类会变为抽象基类,抽象基类不能实例化(因为其中的纯虚函数它不知道要干什么)。
    ② 任何派生类都必须为这个函数定义一个主体,否则派生类也将被视为一个抽象基类。

    如动物类都会有叫声,具体的派生类才知道(实现)具体的叫声。
    virtual const char* speak() = 0; // class Animal
    const char* speak() const override { return "Moo"; } //class Cow: public Animal

    【接口类】

    接口类是没有成员变量的类,其中所有的函数都是纯虚的。
    换句话说,这个类纯粹是一个定义,没有实际的实现。
    注意,类中要有虚析构。

    21、函数模板

    只有函数返回类型、参数类型不同,其他样式一致的,可以使用模板。

    template <typename T>template <class T>无区别,推荐typename这个关键字。

    由于类型T传入的函数参数可能是类类型,而且按值传递类通常不是一个好主意,因此最好将模板化函数的参数和返回类型设置为常量引用

    #include <iostream>
    
    template <typename T> //声明模板类型参数(单类型)
    const T& max(const T& x, const T& y) //函数模板
    {
        return (x > y) ? x : y;
    }
    
    template <typename T1, typename T2> //声明多个类型参数
    const T1& min(const T1& x, const T2& y) //函数模板
    {
        return (x < y) ? x : y;
    }
    
    int main()
    {
        int a = max(3, 7);
        std::cout << a << '
    '; //7
        int b = min(3, 7.1);
        std::cout << b << '
    '; //3
    
        return 0;
    }

     有时候需要重载操作符,以支持自定义的类

    #include <iostream>
    
    template <typename T> //声明模板类型参数
    const T& max(const T& x, const T& y) //函数模板
    {
        return (x > y) ? x : y; // >重载后,可以比较Cents类型的变量
    }
    
    class Cents
    {
    public:
        Cents(int cents)
            :m_cents{cents}
        {}
    
        friend bool operator > (const Cents& c1, const Cents& c2) //重载>
        {
            return (c1.m_cents > c2.m_cents);
        }
    
        friend std::ostream& operator << (std::ostream& out, const Cents& cents) //重载<<,以支持输出Cents类型
        {
            out << cents.m_cents;
            return out;
        }
    
    private:
        int m_cents;
    };
    
    int main()
    {
        Cents a{ 5 };
        Cents b{ 10 };
        Cents result{ max(a,b) }; //const Cents& max(const Cents& x,const Cents& y),对于Cents类需重载操作符>
        std::cout << result << '
    ';
        return 0;
    }

     array中前length项的平均值

    #include <iostream>
    
    template <typename T>
    T average(T * array, int length)
    {
        T sum(0);
        for (int count{ 0 }; count < length; ++count)
            sum += array[count];
    
        sum /= length;
        return sum;
    }
    
    int main()
    {
        int array1[]{ 5, 3, 2, 1, 4 };
        std::cout << average(array1, 5) << '
    ';
    
        double array2[]{ 3.12, 3.45, 9.23, 6.34 };
        std::cout << average(array2, 4) << '
    ';
    
        return 0;
    }

    array中自定义类型元素的均值

    #include <iostream>
    
    template <typename T>
    T average(T * array, int length)
    {
        T sum(0);
        for (int count{ 0 }; count < length; ++count)
            sum += array[count]; //自定义类型,需重载 +=
    
        sum /= length; //自定义类型,需重载 /=
        return sum;
    }
    
    class Cents
    {
    private:
        int m_cents;
    public:
        Cents(int cents)
            : m_cents{ cents }
        {
        }
    
        friend bool operator>(const Cents& c1, const Cents& c2)
        {
            return (c1.m_cents > c2.m_cents);
        }
    
        friend std::ostream& operator<< (std::ostream& out, const Cents& cents)
        {
            out << cents.m_cents ;
            return out;
        }
    
        Cents& operator+=(const Cents& cents) //重载 +=
        {
            m_cents += cents.m_cents;
            return *this;
        }
    
        Cents& operator/=(int value) //重载 /=
        {
            m_cents /= value;
            return *this;
        }
    };
    
    int main()
    {
        Cents array3[]{ Cents(5), Cents(10), Cents(15), Cents(14) };
        std::cout << average(array3, 4) << '
    ';
    
        return 0;
    }

    22、类模板

    类模板是实现容器类的理想选择,因为我们非常希望容器能够跨各种数据类型工作,而模板允许您在不复制代码的情况下这样做。尽管语法很难看,错误消息可能很神秘,但模板类确实是c++最好、最有用的特性之一。std::vector 就是经典的类模板。

    Array.h,推荐都放到h头文件中,省事方便。

    #pragma once
    
    #include <cassert> //断言
    
    template <typename T>
    class Array //类,与函数用法一样,将类型替换为T即可
    {
    private:
        int m_length{};
        T* m_data{};
    
    public:
    
        Array(int length)
        {
            assert(length > 0);
            m_data = new T[length]{};
            m_length = length;
        }
    
        Array(const Array&) = delete;
        Array& operator=(const Array&) = delete;
    
        ~Array()
        {
            delete[] m_data;
            m_data = nullptr;
            m_length = 0;
        }
    
        T& operator[](int index)
        {
            assert(index >= 0 && index < m_length);
            return m_data[index];
        }
    
        int getLength() const { return m_length; };
    };

    使用方式

    #include <iostream>
    #include "Array.h" //引入头文件
    
    int main()
    {
        Array<int> intArray(12);
        Array<double> doubleArray(12);
    
        for (int count{ 0 }; count < intArray.getLength(); ++count)
        {
            intArray[count] = count;
            doubleArray[count] = count + 0.5;
        }
    
        for (int count{ intArray.getLength() - 1 }; count >= 0; --count)
            std::cout << intArray[count] << '	' << doubleArray[count] << '
    ';
    
        return 0;
    }

    23、异常处理throw、try、catch 

    try检查异常,throw抛出自定义语句,依据语句的类型跳转到对应的catch去处理异常。如果catch也有异常,那么下一个try去捕获。 

    #include "math.h" // for sqrt() function
    #include <iostream>
    
    int main()
    {
        std::cout << "Enter a number: ";
        double x;
        std::cin >> x;
    
        try // 查找try块中异常,并跳转到相应的catch中,触发throw则之后的语句不会执行。
        {
            if (x < 0.0)
                throw "Can not take sqrt of negative number"; // 抛出const char*类型的异常语句
    
            std::cout << "The sqrt of " << x << " is " << sqrt(x) << '
    ';
        }
        catch (const char* exception) // 处理const char*的异常
        {
            std::cerr << "Error: " << exception << '
    ';
        }
    }

    throw不一定在try中, 抛出异常的函数的直接调用者如果不想处理异常,就不必处理异常。可以将任务交给调用者的调用者。一旦有调用者catch处理了这个异常,其他就不再处理。

    #include <iostream>
    
    void last() // 被third()调用
    {
        std::cout << "Start last
    ";
        std::cout << "last throwing int exception
    ";
        throw - 1; //触发throw,之后语句不再执行,直接跳转到对应的catch(first函数中有int型异常处理catch)
        std::cout << "End last
    ";
    }
    
    void second() // 被first()调用
    {
        std::cout << "Start second
    ";
        try
        {
            last(); //second()不处理last()throw的异常信息,因为没有匹配的catch。将处理权交给它的调用者first()
        }
        catch (double) 
        {
            std::cerr << "second caught double exception
    ";
        }
        std::cout << "End second
    ";
    }
    
    void first() // 被main()调用
    {
        std::cout << "Start first
    ";
        try
        {
            second();
        }
        catch (int) //last()中throw出int型异常,跳转到此catch。first()的catch处理后,main()不再处理。
        {
            std::cerr << "first caught int exception
    ";
        }
        catch (double)
        {
            std::cerr << "first caught double exception
    ";
        }
        std::cout << "End first
    ";
    }
    
    int main()
    {
        std::cout << "Start main
    ";
        try
        {
            first(); //first()调用second(),second()调用last(),last()中throw出int型异常,second与last都不处理。
        }
        catch (int) //被first()中的catch (int)处理了,此处不再处理
        {
            std::cerr << "main caught int exception
    ";
        }
        std::cout << "End main
    ";
    
        return 0;
    }

    24、全捕获处理程序

    catch(...) { //省略号,捕获处理任何类型的异常。

    上述语句通常是空的,并且放在所有特定catch类型异常处理的最后,防止特定catch没有考虑全,程序异常终止。

    #include <iostream>
    
    int main()
    {
    
        try
        {
            runGame();
        }
        catch (...) //若runGame()异常,则捕获处理。否则main异常终止。
        {
            std::cerr << "Abnormal termination
    ";
        }
    
        saveState(); // 保存用户数据
        return 1;
    }

    25、继承类异常、标准库中的异常

     推荐派生类放到基类前面。因为谁在前谁处理。

    #include <iostream>
    class Base
    {
    public:
        Base() {}
    };
    
    class Derived : public Base
    {
    public:
        Derived() {}
    };
    
    int main()
    {
        try
        {
            throw Derived();
        }
        catch (const Derived& derived) //推荐派生类放到基类前面。因为谁在前谁处理。
        {
            std::cerr << "caught Derived";
        }
        catch (const Base& base)
        {
            std::cerr << "caught Base";
        }
        return 0;
    }

    标准库中的异常如下:

    #include <iostream>
    
    int main()
    {
        try
        {
            //throw std::runtime_error("Bad things happened"); //被catch (const std::exception& exception)处理
            std::string s;
            s.resize(-1); // 触发std::length_error        
        }
        catch (const std::length_error& exception) //我们知道的特定异常
        {
            std::cerr << "You ran out of memory!" << '
    ';
        }
        catch (const std::exception& exception) //std::exception中包含的异常和std::exception派生类中的异常。
        {
            std::cerr << "Standard exception: " << exception.what() << '
    ';
        }
    
        return 0;
    }

    26、Function try

    Function try主要用于在将异常向上传递到堆栈之前记录失败,或者用于更改抛出的异常的类型。对应的catch不处理此异常,自动throw。 

    #include<iostream>
    class A
    {
    private:
        int m_x;
    public:
        A(int x) : m_x(x)
        {
            if (x <= 0)
                throw 1;
        }
    };
    
    class B : public A
    {
    public:
        B(int x) try: A(x) //Function try
        {
        }
        catch (...) //如果A创建失败,则catch。注意,Function try的catch会自动throw这个异常,它不处理。
        {
            std::cerr << "Exception caught
    ";
        }
    };
    
    int main()
    {
        try
        {
            B b(0);
        }
        catch (int) //此处最终处理
        {
            std::cout << "Oops
    ";
        }
    }

    27、移动语义,std::move() 

    移动语义意味着类将转移对象的所有权,而不是复制。移动比复制效率高。
    例如不是让复制构造函数或赋值运算符复制指针(“复制语义”),而是将指针的所有权从源转移/移动到目标对象。

    std::string str = "Knock"; //str是左值,Knock是右值。
    std::move(str); //std::move会将左值变右值,说人话就是直接从str偷走Knock,str里空了。

    #include <iostream>
    #include <string>
    #include <utility> // for std::move
    #include <vector>
    
    int main()
    {
        std::vector<std::string> v;
        std::string str = "Knock";
    
        std::cout << "Copying str
    ";
        v.push_back(str); // 复制str到容器v中——复制语义
    
        std::cout << "str: " << str << '
    ';
        std::cout << "vector: " << v[0] << '
    ';
    
        std::cout << "
    Moving str
    ";
    
        v.push_back(std::move(str)); // 直接拿走str中的Knock,放入容器v中——移动语义
    
        std::cout << "str: " << str << '
    '; //str里什么都没有了。注意str依然存在,变成了未初始化状态。
        std::cout << "vector:" << v[0] << ' ' << v[1] << '
    ';
    
        return 0;
    }

    移动语义的经典使用场合:
    ① 许多排序算法(例如选择排序和冒泡排序)都是通过交换元素对来工作的。在以前我们不得不求助于复制语义来做交换。现在我们可以使用move语义,这样效率更高。
    ② 由一个智能指针管理的内容移动到另一个智能指针。

    #include <iostream>
    #include <string>
    #include <utility> // for std::move
     
    template<class T>
    void myswap(T& a, T& b) 
    { 
      T tmp { std::move(a) }; // 不复制,直接移动值
      a = std::move(b); // a=b会触发拷贝
      b = std::move(tmp); 
    }
     
    int main()
    {
        std::string x{ "abc" };
        std::string y{ "de" };
     
        std::cout << "x: " << x << '
    ';
        std::cout << "y: " << y << '
    ';
     
        myswap(x, y);
     
        std::cout << "x: " << x << '
    ';
        std::cout << "y: " << y << '
    ';
     
        return 0;
    }
  • 相关阅读:
    msfvenom常用脚本生成
    CC攻击防护方法
    上传漏洞分类
    cmd下载文件(利用powershell)
    发送HTTP请求的新姿势
    (转载).NET Core 深度克隆对象,引用类型的平行世界
    使用VMware安装centos7并配置网络
    (转载)这才是真正的远程开发——VS Code Remote 环境搭建
    (转载)基于Vue,ElementUI开发的一款表单设计器
    (转载)VMWare VMNet 8 的配置使用
  • 原文地址:https://www.cnblogs.com/xixixing/p/13540520.html
Copyright © 2011-2022 走看看