C++ day42 C++的其他类库(除STL外)

STL已经提供了一个非常好的可重用代码源,STL工具可以被用来解决很多编程问题了,但是C++还是觉得不够,在STL之外,也提供了一些模板类,这些模板类基本都是用来做一件很专业的事情的,比如

C++的三个数组模板:vector, valarray, array

为什么要弄这么多类来表示数组???

因为使用的目的不一样。且三个模板类的开发小组也不一样。

vector模板类

  • 是STL的一部分,是一个容器类,支持很多面向容器的操作,比如排序,搜索,插入,重新排列。

valarray类模板:数值计算绝对的王者

  • 不是STL的一部分,是面向数值计算的,它给很多数学运算提供了简单而直观的接口。

  • 重载了所有算术运算符!所以可以直接把两个valarray对象相加,相乘等等。数学运算相比于另外两个数组类可简单方便太多了

valarray<double> vad1(10), vad2(10), vad3(10);
//假设已经赋值了
vad3 = vad1 + vad2;
vad3 = vad1 * vad2;
vad3 *= 4.5;//重载*=运算符
vad3 = vad3 * 4.5;//重载*

但是如果要是array或者vector,就只能用STL的transform方法,且这两个类在这些运算上基本没区别:

//vector类
vector<double> ved1(10), ved2(10), ved3(10);

//两数组相加
transform(ved1.begin(), ved1.end(), ved2.begin(), ved3.begin(), plus<double>());//把前两个数组的值对应相加,赋给第三个数组。函数对象是STL专门定义的函数符类的对象。

//数组所有元素乘以一个常数,就地存放,要用函数适配器
transform(ved3.begin(), ved3.end(), ved3.begin(),bind1st(multiplies<double>(), 2.5));
//array类
array<double, 10> a1, a2, a3;//注意数组长度是作为模板的非类型参数哦,不是构造函数的参数

//两数组相加
transform(a1.begin(), a1.end(), a2.begin(), a3.begin(), plus<double>());//注意array类不是STL的容器类,但是设计时考虑了STL的设计理念,因此很多STL方法都可以用,就像string类一样

//数组所有元素乘以一个常数,就地存放,要用函数适配器
transform(a3.begin(), a3.end(), a3.begin(),bind1st(multiplies<double>(), 2.5));
  • valarray类还重载了一种数学函数,接受一个valarray对象为参数,并返回一个valarray对象,
vad1 = log(vad1);//重载了log函数
//或者不用重载函数
vad1 = vad3.apply(log);//apply成员函数接受一个函数对象作为参数

注意apply方法是const成员方法,它不会修改调用对象,所以必须返回给另一个对象。

如果要执行很多步数学运算,绝对是valarray完胜!用vector或者array,写代码都要累死。

vad3 = 10.0 * ((vad1 + vad2) / 2.0 + vad1 * cos(vad2));
  • valarray类没有begin(), end()成员方法,要用C++11提供了接受valarray对象为参数的模板函数begin(),end()

如果想对valarray的对象使用STL算法,比如排序,就不可以写

sort(vad.begin(), vad.end());

而只能写:

sort(&vad[0], &vad[10]);

但是这么写是很不安全的!!!!!!

因为valarray类没有定义超尾,即下标超过尾部一个元素的行为。毕竟vad对象的长度为10,索引就只能是0-9, 写10虽然好多编译器都不报错,那是因为存vad对象的栈内存和堆内存没有相邻!!!一旦相邻(概率极低,但是不等于不会发生),就会出错。公司里绝对不能写这种有概率风险的代码,万一订单几个亿呢???背得起责任??

于是c++11路见不平拔刀相助,提供了接受valarray对象为参数的模板函数begin(),end()

这才是正确而安全的代码。

sort(begin(vad), end(vad));

示例:vector VS valarray

#include <vector>
#include <valarray>
#include <algorithm>
#include <iostream>

int main()
{
	using namespace std;

	vector<double> data;
	double tmp;

	cout << "Enter numbers(double) (<=0 to quit):\n";
	while (cin >> tmp && tmp > 0)
        data.push_back(tmp);

    sort(data.begin(), data.end());

    int size = data.size();
    valarray<double> numbers(size);
    int i;
    for (i=0;i<size;++i)
        numbers[i] = data[i];
    valarray<double> sqrts(size);
    sqrts = sqrt(numbers);
    valarray<double> results(size);
    results = numbers + 2.0 * sqrts;

    cout.setf(ios_base::fixed);
    cout.precision(4);
    for (i=0;i<size;++i)
    {
        cout.width(8);
        cout << numbers[i] << ": ";
        cout.width(8);
        cout << results[i] << endl;
    }
    cout << "done!\n";
	return 0;
}

通过这个程序可以看到valarray类在数值计算上确实是王者,一枝独秀。

Enter numbers(double) (<=0 to quit):
2.05 3.56 8.9 2.0 1.2 3.5 4.6 8.5 -8.4
  1.2000:   3.3909
  2.0000:   4.8284
  2.0500:   4.9136
  3.5000:   7.2417
  3.5600:   7.3336
  4.6000:   8.8895
  8.5000:  14.3310
  8.9000:  14.8666
done!
Enter numbers(double) (<=0 to quit):
4 5 2.3 0 3
  2.3000:   5.3332
  4.0000:   8.0000
  5.0000:   9.4721
done!

其中这一段也可以用copy(),只不过第三个参数要注意valarray类没有begin()成员方法,用的不是成员方法

for (i=0;i<size;++i)
    numbers[i] = data[i];
copy(data.begin(), data.end(), begin(numbers));

slice类对象用作valarray对象的数组索引:表示一组值,而非一个值

  • slice类在头文件valarray中,#include <valarray>

可以看出关系有多紧密了。毕竟slice类就是专门给valarray类服务的。
不知道slice类是否可以用于其他类呢?(我后来做了实验,不可以,后面给了代码
除了slice类可以提供这种更高级的索引方式以外,还有gslice类也可以,但gslice类主要提供多维索引,即同时获取多个多维数组出来。

  • slice类的构造函数有三个参数,分别是起始索引,索引数,跨距。

很像MATLAB很多函数的参数设置风格。

比如slice(1, 4, 3)创建的对象,表示选择4个元素,这四个元素的索引分别是:1,2,7,10(即一共4个,每两个索引间隔了3)。

比如:

valarray<int> a(10);
a[slice(0, 5, 2)] = 10;//把0,2,4,6,8处的值设为10
  • 可以用slice对象来做这样一件有趣的事情:用一个一维的valarray来表示二维数据。
//假设有一个4行3列的二维数组要存储,则可以把这12个元素存在一个valarray对象中
valarray<int> a(12);
a[slice(0, 3, 1)] = 1;//第一行
a[slice(3, 3, 1)] = 2;//第二行
a[slice(6, 3, 1)] = 3;//第三行 
a[slice(9, 3, 1)] = 4;//第四行 

a[slice(0, 4, 3)] = 1;//第一列
a[slice(1, 4, 3)] = 2;//第二列
a[slice(2, 4, 3)] = 3;//第三列 
示例:valarray类 搭配 slice类(cout.width(3);的位置很重要!!)
#include <iostream>
#include <valarray>
#include <cstdlib>

const int SZ = 12;
typedef std::valarray<int> vint;
void show(const vint & v, int cols);//显示为二维矩阵,cols是要显示为的列数

int main()
{
    using std::slice;
    using std::cout;

    vint valint(SZ);//我们用这个size为12的一维数组来存储4*3的矩阵
    for  (int i=0;i<SZ;++i)
        valint[i] = std::rand() % 10;
    cout << "Original array:\n";
    show(valint, 3);显示为3列

    vint vcol(valint[slice(1, 4, 3)]);//第2列
    cout << "Second column:\n";
    show(vcol, 1);//显示为1列

    vint vrow(valint[slice(3, 3, 1)]);//第2行
    cout << "Second row:\n";
    show(vrow, 3);//显示为3列

    valint[slice(2, 4, 3)] = 10;//给第三列赋值
    cout << "Set last column to 10:\n";
    show(valint, 3);

    cout << "Set first column to sum of next two:\n";
    valint[slice(0, 4, 3)] = vint(valint[slice(1, 4, 3)]) + vint(valint[slice(2, 4, 3)]);
    show(valint, 3);
    return 0;
}

void show(const vint & v, int cols)
{
    using std::cout;
    int size = v.size();


    for (int i = 0;i < size; ++i)
    {
        cout.width(3);
        cout << v[i];
        if (i % cols == cols - 1)
            cout << std::endl;
        else
            cout << ' ';
    }
    if (size % cols != 0)
        cout << std::endl;
}

这个示例让我深深明白了自己对输出格式有多么的不熟悉不了解,加强了学习输出格式的欲望。

Original array:
  1   7   4
  0   9   4
  8   8   2
  4   5   5
Second column:
  7
  9
  8
  5
Second row:
  0   9   4
Set last column to 10:
  1   7  10
  0   9  10
  8   8  10
  4   5  10
Set first column to sum of next two:
 17   7  10
 19   9  10
 18   8  10
 15   5  10

我刚开始把cout.width(3);放在for循环外面的,得到的结果是:

Original array:
  1 7 4
0 9 4
8 8 2
4 5 5
Second column:
  7
9
8
5
Second row:
  0 9 4
Set last column to 10:
  1 7 10
0 9 10
8 8 10
4 5 10
Set first column to sum of next two:
 17 7 10
19 9 10
18 8 10
15 5 10

原来必须每一次输出都要改格式,不是一次就搞定了的。

此外,需要把valint[slice(1, 4, 3)]进行强制类型转换,转换为valarray类的对象,才可以直接相加,否则就会报错。因为valint[slice(1, 4, 3)]的类型是std::slice_array<int>!!!我是通过编译器才知道的,不然也不知道竟然是这个类型的。
在这里插入图片描述

slice类不可以用于别的类

我只测试了vector类,不能,其他的类不知道,所以也不敢说绝对,但是毕竟slice类都是在valarray头文件定义的,大概确实只服务于valarray类。

测试代码:

#include <iostream>
#include <valarray>
#include <cstdlib>
#include <vector>
const int SZ = 12;
void show(const std::vector<int> & v, int cols);//显示为二维矩阵,cols是要显示为的列数

int main()
{
    using std::slice;
    using std::cout;

    std::vector<int> valint(SZ);//我们用这个size为12的一维数组来存储4*3的矩阵
    for  (int i=0;i<SZ;++i)
        valint[i] = std::rand() % 10;
    cout << "Original array:\n";
    show(valint, 3);显示为3列

    std::vector<int> vcol(valint[slice(1, 4, 3)]);//第2列

    return 0;
}

void show(const std::vector<int> & v, int cols)
{
    using std::cout;
    int size = v.size();


    for (int i = 0;i < size; ++i)
    {
        cout.width(3);
        cout << v[i];
        if (i % cols == cols - 1)
            cout << std::endl;
        else
            cout << ' ';
    }
    if (size % cols != 0)
        cout << std::endl;
}

在这里插入图片描述
报错说没有重载的[]运算符函数的参数是vector和slice对象。所以肯定没法啦。说明valarray类重载了[]运算符。
在这里插入图片描述

除了这一句代码之外,其他的都可以运行:

Original array:
  1   7   4
  0   9   4
  8   8   2
  4   5   5

array类

  • 专门用来替代内置数组的,它比内置数组提供了更加安全的接口,使得数组的效率更高,也更加紧凑(?啥叫紧凑)。
  • 但是它表示的数组的长度是固定的,不可以用insert方法或者push_back方法,但是可以用很多STL方法,比如begin(), end(), rbegin(), rend()。可以把很多STL算法用于array对象

initializer_list模板类(C++11新增):追究vector<int> age {10, 20, 30};可行的底层原因

  • #include <initializer_list>
  • 设计初衷:便于把一系列的值传递给构造函数或者其他函数。
  • 以前从来没有想过为什么这样的语句(即初始化列表语法)可以被编译器接受,且真的可以初始化容器。

初始化列表语法之前只用于常规数组,不能用于STL容器,C++11又进行了一番操作,搞得通用了

vector<int> age {10, 20, 30};

很惭愧,这没有激起我的好奇心和求知欲,可能觉得实在太难了,就 连问题都提不出来了吧

原来,这种代码可以实现,是因为STL容器类提供了一个构造函数,它接受一个initializer_list类的对象作为参数,{10, 20, 30}就是一个initializer_list<int>类的对象。

上面那句代码写完整,其实是:

vector<int> age({10, 20, 30});

在这里插入图片描述

  • 这个类也有begin(), end()成员方法,即又是一个虽然不是STL组成部分但是设计上和STL兼容的类,和string类,array类一样,和STL大家族攀亲戚。

示例:真的很有用诶

这个例子真的让人发现这个类的好用之处了,传入一组数字,做个随便什么数学运算,当做valarray用的感觉。

#include <iostream>
#include <initializer_list>

double sum(std::initializer_list<double> x);//按值传递参数
double average(const std::initializer_list<double> & x);//按引用传递参数

int main()
{
    using std::cout;

    cout << "List 1: sum = " << sum({2, 3, 4})
         << ", ave = " << average({2, 3, 4}) << '\n';

    std::initializer_list<double> d {1.1, 2.3, 5.6, 4.8,7.5, 9.6};
    cout << "List 2: sum = " << sum(d)
         << ", ave = " << average(d) << '\n';
    return 0;
}

double sum(std::initializer_list<double> x)
{
    double res = 0;
    for (auto it= x.begin();it!=x.end();++it)
        res += (*it);
    return res;
}
double average(const std::initializer_list<double> & x)
{
    return sum(x) / x.size();
}
List 1: sum = 9, ave = 3
List 2: sum = 30.9, ave = 5.15

这个示例检测出来的我的问题:

  • 如果按引用传参,传入的是const常量,那么必须把函数的形参设置为const的,不然会报错,因为你把const的给了非const的。过了一段竟然有点忘记,虽然一出事就知道怎么回事,但是这不够好,因为没有时刻很明白,记在脑子里深深的。毕竟是之前强调了很多次的,应该印象极其深刻才对。很惭愧。

学到的:

  • 不允许缩窄转换,如果列表的元素的类型是int,就不可以把double值付给他,否则就是缩窄,编译报错(compile-time error)。但是当然可以变宽,比如上面的代码,元素类型是double,就可以把int值赋给它,因为不会出现精度损失,没关系,,很安全。
    在这里插入图片描述在这里插入图片描述

总结

  • C++的库的功能很强大,提供了很多常见编程问题的解决方案。
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值