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 << '\n';
    std::cout << "The cos is " << cos << '\n';
    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 << '\n';

    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) << '\n'; // 用std::get<n>获取元组中元素
    /*或者使用如下方式
    int a;
    double b;
    std::tie(a, b) = returnTuple(); // 用std::tie拆解元组
    //auto [a,b]{returnTuple()}; // c++17,效果同上,拆解元组
    std::cout << a << ' ' << b << '\n';
    */
    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 << '\n';
}

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() << '\n';
    array.resize(5); // 长度变为5,元素变为0,1,2,0,0
    std::cout << "length: " << array.size() << "  capacity: " << array.capacity() << '\n';
    array.resize(2); // 长度变为2,元素只剩0,1
    std::cout << "length: " << array.size() << "  capacity: " << array.capacity() << '\n';
}

【堆栈操作】

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() << '\n'; // 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 << '\n';

    if (count > 1) // 终止条件
        countDown(count - 1); //递归前、后会推入和取出堆栈帧。因为参数count会被int很多次,即有新内存,这些新内存需要释放掉。

    std::cout << "pop " << count << '\n';
}

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() << ')' << '\n';
    }
};

main.cpp

#include <iostream>
#include"Creature.h"
#include"Point2D.h"

int main()
{
    std::cout << "Enter a name\n";
    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() << '\n';
    derived.setId(10); //通过基类的访问函数修改基类的成员变量
    std::cout << derived.getId() << '\n';

    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() << '\n';
    std::cout << derived.getId() << '\n';

    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()<<'\n';//Tom Meow

    Animal *p{&cat}; //因为p是Animal基类的指针,所以p->是Animal::而不是Cat::
    std::cout<<p->getName()<<" "<<p->speak()<<'\n'; //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()<<'\n'; //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()<<'\n'; //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()\n"; return this; } //返回指向Base类的指针
    void printType() { std::cout << "returned a Base\n"; }
};

class Derived : public Base
{
public:
    // 通常,重写,返回类型必须一致,但是Derived继承自Base, 允许写成Derived*,真正返回的依然是Base*
    virtual Derived* getThis() override { std::cout << "called Derived::getThis()\n";  return this; }
    void printType() { std::cout << "returned a Derived\n"; }
};

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()\n";
    }
};

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()\n";
        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 << '\n'; //7
    int b = min(3, 7.1);
    std::cout << b << '\n'; //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 << '\n';
    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) << '\n';

    double array2[]{ 3.12, 3.45, 9.23, 6.34 };
    std::cout << average(array2, 4) << '\n';

    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) << '\n';

    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] << '\t' << doubleArray[count] << '\n';

    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) << '\n';
    }
    catch (const char* exception) // 处理const char*的异常
    {
        std::cerr << "Error: " << exception << '\n';
    }
}

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

#include <iostream>

void last() // 被third()调用
{
    std::cout << "Start last\n";
    std::cout << "last throwing int exception\n";
    throw - 1; //触发throw,之后语句不再执行,直接跳转到对应的catch(first函数中有int型异常处理catch)
    std::cout << "End last\n";
}

void second() // 被first()调用
{
    std::cout << "Start second\n";
    try
    {
        last(); //second()不处理last()throw的异常信息,因为没有匹配的catch。将处理权交给它的调用者first()
    }
    catch (double) 
    {
        std::cerr << "second caught double exception\n";
    }
    std::cout << "End second\n";
}

void first() // 被main()调用
{
    std::cout << "Start first\n";
    try
    {
        second();
    }
    catch (int) //last()中throw出int型异常,跳转到此catch。first()的catch处理后,main()不再处理。
    {
        std::cerr << "first caught int exception\n";
    }
    catch (double)
    {
        std::cerr << "first caught double exception\n";
    }
    std::cout << "End first\n";
}

int main()
{
    std::cout << "Start main\n";
    try
    {
        first(); //first()调用second(),second()调用last(),last()中throw出int型异常,second与last都不处理。
    }
    catch (int) //被first()中的catch (int)处理了,此处不再处理
    {
        std::cerr << "main caught int exception\n";
    }
    std::cout << "End main\n";

    return 0;
}

24、全捕获处理程序

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

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

#include <iostream>

int main()
{

    try
    {
        runGame();
    }
    catch (...) //若runGame()异常,则捕获处理。否则main异常终止。
    {
        std::cerr << "Abnormal termination\n";
    }

    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!" << '\n';
    }
    catch (const std::exception& exception) //std::exception中包含的异常和std::exception派生类中的异常。
    {
        std::cerr << "Standard exception: " << exception.what() << '\n';
    }

    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\n";
    }
};

int main()
{
    try
    {
        B b(0);
    }
    catch (int) //此处最终处理
    {
        std::cout << "Oops\n";
    }
}

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\n";
    v.push_back(str); // 复制str到容器v中——复制语义

    std::cout << "str: " << str << '\n';
    std::cout << "vector: " << v[0] << '\n';

    std::cout << "\nMoving str\n";

    v.push_back(std::move(str)); // 直接拿走str中的Knock,放入容器v中——移动语义

    std::cout << "str: " << str << '\n'; //str里什么都没有了。注意str依然存在,变成了未初始化状态。
    std::cout << "vector:" << v[0] << ' ' << v[1] << '\n';

    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 << '\n';
    std::cout << "y: " << y << '\n';
 
    myswap(x, y);
 
    std::cout << "x: " << x << '\n';
    std::cout << "y: " << y << '\n';
 
    return 0;
}

 

posted @ 2020-10-22 17:53  夕西行  阅读(483)  评论(1编辑  收藏  举报