论C++如何优雅的输出数组元素。<( ̄ c ̄)y▂ξ 优雅,永不过时

今天的主题是《论如何优雅的输出数组的元素》。

平时在学习python或是golang语言的时候,常常感叹这些高级语言提供的方法真是太丰富了,一些复杂的操作只需短短几行代码便可实现,反观C/C++往往需要长篇大论才能做到相同的效果。

在这里插入图片描述

就比如对于数组内元素的输出这一场景,C++中居然没有一个现成好用的方法,还需要我们自己迭代进行输出(内心吐槽:写for循环也很累的好不好(っ °Д °;)っ)。

那今天我们就以C++中关于数组(引申到STL容器)元素如何输出为主题进行探讨,而当你阅读完本篇文章时,就会知道C++为什么不给我们提供一个现成的 show() 函数使用了。因为C++把选择权留给了我们,因为有太多种方式可以选择了。

引子:我们要优雅

众所周知,在C++中想要输出数组内的元素,我们一般会沿用C的方式,即遍历数组输出。
在这里插入图片描述
而在其他语言中,都可以直接输出数组,例如python 或是golang。
在这里插入图片描述
在这里插入图片描述

  • 我:看看人家python,看看人家golang,明明一个print() 可以解决的事,你怎么就是学不会呢?
  • C++: … 人家就是不会嘛(划掉) 。 虽然但是,人家也是很强的说,不信你接着往下看。

C++语言以灵活著称,难道就没有更优雅的方式输出数组元素了吗?

我的回答是,当然有啦,还不止一条。下面就让我们来探讨一下「C++如何优雅的输出数组元素」。

一、基于范围的for循环(推荐)

在C++11的语法更新中提供了一个特殊版本的 for 循环——基于范围的 for 循环。(以下简称为“范围for循环”)

该 for 循环处理数组时,可以自动为数组中的每个元素迭代一次,而我们只需要定义一个变量,并且给他一个范围即可。
在这里插入图片描述
除了C语言形式的数组(type varName [] )之外,对于vector,array,set等这类STL容器也普遍支持。
在这里插入图片描述

1.1 范围for循环的使用条件

对于范围的for循环使用,它有两点条件

  • 首先,对象的范围必须确定。即,需要支持begin和end函数,范围在begin和end之间;
  • 其次,要求迭代的对象实现++和==等操作符。STL中的容器都能支持,而自己实现的类,则需要实现这些操作符。

下面我们再来看以示例,我们直接使用 {1,2,3,4,5},看它是否支持范围for循环。
在这里插入图片描述
既然,{1,2,3,4,5}支持这种方式,那我们可以反向验证一下 {1,2,3,4,5}结构是否具备上述两点条件。
在这里插入图片描述
很明显它是支持的,并且我们从最后输出的类型中可以发现它其实就是class std::initializer_list类型,也就是我们使用列表初始化时用到的类型。(后文中会讲到该类型)

1.2 插入snippet代码

我们可以将上述代码通过snippet插入外部代码的方式,快速添加到当前代码中,gif演示:
在这里插入图片描述

如果你使用的是Visual Studio编译器,那么下面这篇文章将会告诉你如何在vs上添加snippet代码:

VS 自动添加代码 | 使用snippet插入外部代码

二、输出流迭代器

要知道,在C++中我们输出的内容实际上是被写入到输出流中的(C中是被写入输出缓冲区中),而在STL中存在一个ostream_iterator,它的作用是直接向输出流中写入元素。

当然也有对应的 std::istream_iterator 输入流迭代器。
具体可参考:https://zh.cppreference.com/w/cpp/iterator/ostream_iterator

按照上述原理,我们只需将数组内的数据按顺序拷贝到ostream_iterator中,即可将数组元素输出。
在这里插入图片描述
同时,ostream_iterator的构造函数提供了两个版本,其中一个允许我们通过特定的分隔符决定输出的效果。

ostream_iterator( ostream_type& stream, const CharT* delim );
	// 以 stream 为关联流并以 delim 为分隔符构造迭代器。
ostream_iterator( ostream_type& stream );
	// 以 stream 为关联流并以空指针为分隔符构造迭代器。

并且,作为STL中提供的方法,它支持大多数的容器,这里拿vector、array、set为例。
在这里插入图片描述

2.1 扩展:关于ostream_iterator

关于全局对象 std::cout ,它控制到实现定义类型流缓冲(导出自 std::streambuf )的输出,它与标准 C 输出流 stdout 关联。

有关basic_streambuf,可以参考:https://zh.cppreference.com/w/cpp/io/basic_streambuf

在构造函数 ostream_iterator( ostream_type& stream ); 中,参数类型为ostream_type,表示关联以此迭代器所访问的输出流。
在iterator头文件中,我们可以看到这样的语句 using ostream_type = basic_ostream<_Elem, _Traits>;,表示ostream_type实际上是一个basic_ostream<_Elem, _Traits>类型。
在这里插入图片描述
通过查看 cout 的类型,可以发现cout正是这样一种类型。因此,我们可以使用ostream_iterator<int>(cout, ", ")语法进行输出。
在这里插入图片描述

2.2 更灵活的使用 ostream_iterator

而根据上述的分析,我们可以更灵活的使用 ostream_iterator 。

示例:先创建ostream_iterator 对象,在通过赋值的方式向ostream_iterator 中写入数据。而写入的数据都被输出到标准输出stdout中(屏幕上,或者说是控制台上)。
在这里插入图片描述

也就是说,我们可以多次对同一个ostream_iterator 对象进行写入操作,它会立刻同步到输出流中,以至于后来的写入操作都不会被之前的所覆盖

同时,我们知道std::copy在操作时,会对迭代器对象进行++操作,这里我们每输出一次,就对out对象++。可以看到输出的结果没有什么变化。
在这里插入图片描述这意味着们也可以使用算法库中类似迭代的方式对其进行操作,要知道在STL中大多数算法都支持迭代。也就意味着,我们可以使用一些算法库函数输出我们想要的内容。
在这里插入图片描述
以前我们写python时,输出函数有个语法糖 “n次输出” ,使用起来非常甜,而我们今天了解到ostream_iterator 的用法后,也可以实现这个操作了(虽然有点长就是了)。
在这里插入图片描述
另外,如果嫌弃ostream_iterator名字太长,我们也可以使用 using 关键字进行模板别名化,定义一个短一点的别名方便使用,参考上图。

2.3 扩展:关于std::copy

对于std::copy() 函数,顾名思义,它提供从一个容器向另一个容器拷贝数据的功能。需要注意的是,被接收对象需要用于足够的空间容纳拷贝来的数据,因为std::copy只提供拷贝功能,而不提供扩容功能。

示例:拷贝数据演示
在这里插入图片描述
上述代码运行会产生错误,因为lst不具备足够的空间容纳新的数据。

需要明确的是list本身是支持在构造函数中传入vector参数的。要将vector的数据拷贝到lsit中这里提供三种方法解决:

  • 第一种,使用list自带的assign() 函数可以直接使用vector构造数据。参考下图lst2;
  • 第一种,使用list自带的resize() 函数构建足够的空间容纳数据,再使用std::copy() 。参考下图 lst3;
  • 第三种,使用迭代器构造器inserter(),它使用插入新数据的方式(自动扩容)写入数据,配合std::copy()使用。参考lst4;(这里也可以使用back_inserter尾插迭代器)
    在这里插入图片描述
    与std::copy()相似的 在一些只传入了迭代器头 为参数的算法中,也可以使用类似的方法。例如之前使用过的 fill_n 算法函数。
    在这里插入图片描述
    这里我们演示一下 back_inserter() 的用法,可以看到这次运行没有任何问题。
    在这里插入图片描述

三、使用for_each遍历

除此之外,我们还可以使用for_each函数输出数组的元素。

for_each() 函数的功能是“应用函数到范围中的元素”,即遍历容器每个元素,使用特定的可调用对象对每个元素进行操作。

关于可调用对象,我们后面会讨论,这里我们先理解为一个函数指针

关于for_each() 其实没啥可将的,它将for循环进行了一层封装:

template<class InputIt, class UnaryFunction>
UnaryFunction for_each(InputIt first, InputIt last, UnaryFunction f)
{
    for (; first != last; ++first) {
        f(*first);
    }
    return f; // C++11 起隐式移动
}

只不过,for_each()额外提供了一个参数,可以在遍历元素时对其进行某个操作。而我们可以使用可调用对象操作其遍历到的元素,这里我们先使用lambda表达式(别名:匿名函数、闭包)进行操作。
在这里插入图片描述
同样的,作为标准算法支持大多数STL容器,这里还是拿 vector、array、set举例。
在这里插入图片描述

3.1 扩展:可调用对象

c++11新增加了一个概念叫做可调用对象(Callable Objects)。

可调用对象简单来说就是可以通过括号运算符进行调用的对象,而C++中可调用对象主要有以下几种:

  • 函数
  • 仿函数(函数对象)
  • 函数指针
  • lambda表达式
  • std::function

以for_each的输出为例,分别使用上述可调用对象传参。
在这里插入图片描述
注:这里的函数指针和std::function都是间接使用了show函数。
在这里插入图片描述

四、使用模板重载输出流(推荐)

关于输出流之前已经讲过了,而对于函数的重载我们可以类比复数类中输出流的重载。


class Complex   
{
public:
	//...
    //重载<< 输出复数
	friend ostream& operator << (ostream& os,const Complex& c){
		if(c.real != 0)
			os << c.real;
		if(c.image > 0)
			os << '+' << c.image << 'i';
		if(c.image < 0)
			os << c.image << 'i';
		return os;
	} 

private:
      double real;//复数的实部
      double imag;//复数的虚部
}

复数类Complex通过重载输出流便可以通过 cout 函数直接输出。同理我们可以仿照类输出流重载的方式对每个容器的输出流进行重载,这样我们就可以直接使用cout进行输出了。

这里为了通用性而言,我们引入C++的模板编程,通过模板实现对所有容器的输出流进行兼容。

代码参考:

    template<typename Os, typename V>
    Os& operator<< (Os& os, V const& v) {
	    os << "{ ";
	    for (auto const& e : v) os << e << ' ';
	    return os << "}";
    }

这种方式甚至支持我们内置数组类型(C语言形式数组)的输出。
在这里插入图片描述
同样的,使用模板实现的重载函数依然支持STL中的容器。
在这里插入图片描述
其中,对于std::pair<> 类型我们需要提供一个特例化的版本。

// 输出容器
template<typename Os, typename V>
Os& operator<< (Os& os, V const& v) {
	os << "{ ";
	for (auto const& e : v) os << e << ' ';
	return os << "}";
}
// 输出std::pair<type,type>
template<typename Os, typename T1, typename T2 >
Os& operator<< (Os& os, std::pair<T1,T2> p) {
	os << "{ " << p.first << "," << p.second ;
	return os << "}";
}

可以看到重载后的输出流几乎可以适配STL中所有的容器,除了stack、queue、priority_queue 这三个容器适配器。因为它们内部没有实现迭代器 begin与end,仔细分析一下这三个数据结构的使用场景其实并不需要输出全部元素,因此我们也不需要做特殊处理。
在这里插入图片描述

4.1 扩展:initializer_list 列表初始化

在上述多个示例中,我们都用到了initializer_list 列表初始化,比如:

vector<int> vec1{1,2,3,4,5};	// 使用列表初始化
vector<int> vec2({1,2,3,4,5});
vector<int> vec3 = {1,2,3,4,5};

列表初始化:使用列表给对象初始化。不要把类成员的初始化列表搞混,初始化列表指构造函数通过 ‘:’ 后的初始化器进行初始化。

std::initializer_list 类型对象是一个访问 const T 类型对象数组的轻量代理对象。底层数组是 const T[N] 类型的临时数组,同时重载了迭代器begin 与 end, 使他支持迭代。initializer_list 还提供了 size方法获得数据元素个数,另外它还支持 empty、data等方法。

std::initializer_list 可以用于以下场景:

  • 用花括号初始化器列表列表初始化一个对象
    但,对应的该对象的构造函数接受一个 std::initializer_list 参数
  • 以花括号初始化器列表为赋值的右运算数,或函数调用参数
    但,对应的赋值运算符/函数接受 std::initializer_list 参数
  • 绑定花括号初始化器列表到 auto ,包括在范围 for 循环中

在以上场景中initializer_list 对象都可以自动构造(例如 vector<int> vec3 = {1,2,3,4,5}; )。

initializer_list 可由一对指针或指针与其长度实现。复制一个 std::initializer_list 不会复制其底层对象。
在这里插入图片描述
从结果可以看到,当 initializer_list 使用内置数组初始化时,实际上是对数组的引用。当原数组改变时 initializer_list 内的数据也会改变。
在这里插入图片描述

initializer_list 中每个元素都从原始初始化器列表的对应元素复制初始化(除非窄化转换非法)。底层数组的生存期与任何其他临时对象相同,除了从数组初始化 initializer_list 对象会延长数组的生存期,恰如绑定引用到临时量(有例外,例如对于初始化非静态类成员)。底层数组可以分配在只读内存。

4.2 initializer_list 的使用

initializer_list 可以加入构造函数的参数,这样我们的类就支持以花括号的方式进行初始化了。同时也可以加入函数中,例如vecor::assign函数就支持以列表的形式重新分配。

下面演示一个示例,通过initializer_list传入参数,实现对数组的输出。
在这里插入图片描述
或者我们也可以使用重载输出流的方式重载initializer_list 的operator<<()函数 。这样我们直接将元素转换为 initializer_list 后,直接使用cout就可以输出。
在这里插入图片描述


五、扩展:关于输出的一些小技巧

5.1 C语言中的printf

首先,不等不承认 C语言的printf() 函数是一个功能强大的函数,主要功能包括但不限于以下几点:

  • 格式化!
    如 %d,%x,%#x, … …, 转义符 \r,\n,\t … …
  • 搞颜色?
    printf() 是支持带颜色输出的,格式为 printf("\033[字背景颜色;字体颜色m字符串\033[0m" );
  • 字符填充。
    • 指定输出长度为 n 。不够填充空格,超出忽略。
      printf("%*s", n, "Hello World!\n");
    • 默认左边填充,%-*s 右边填充
    • 默认空格填充,%0*s 使用0填充
  • 字符串拼接 printf("part1""part2""part3");

而在C++中输出有专门的控制字符,比如输出布尔值 std::boolalpha, 输出进制 std::hex、std::doc、std::oct等。常见的有 std::endl 流操作子,它输出 ‘\n’ 并刷新输出流,其他还有 std::ends 输出 ‘\0’、
flush 刷星输出流等。

除此之外C++配置有专门的操纵器辅助输出,比如头文件<iomanip>提供很多控制符用于输入输出。这里就不展开讲了,感兴趣的可以 >>点这里<< 了解。另外,C++20语法中还提供了std::format进行格式化输出。

这里先不讨论那些无聊的格式控制,我们来聊点有趣的内容(作者菌:o( ̄▽ ̄)d我这里好康的,来我房间啊。读者菌:让我康康!!!)

5.2 C++输出小技巧:原始字符串(raw string)

在C++11语法中增加了一个原始字符串的概念,语法:R“(a string)” 。 它类似python中的多行字符串。在这里插入图片描述
可以看到在"()" 之间的所有字符都被原样输出,包括 ‘\n’ 字符也没有被解释为换行符而是作为两个字符’'与’n’保存。

https://www.luogu.com.cn/problem/P1000
这个特性又什么用呢,这里我列举几个场景,比如在将一些数据序列化存储时,为例避免\ ' "等字符干扰原序列化后的字符串,我们可以使用原始字符串存储。或者我们在访问路径时,避免Windows下需要多个"\\"表示路径分割符的尴尬。

访问百度首页得到的数据:如果让我们写类似百度首页的服务端,我们得将下面的数据通过一个字符串保存起来,然后发给客户端。
在这里插入图片描述
如果使用传统字符串需要额外增加许多控制字符,还有污染原数据的风险。使用raw string显然就方便很多了。
在这里插入图片描述
另外,这里有一道题也可以使用raw string来做 :输出超级马里奥


除此之外,关于C++中string没有split()函数也是C++程序猿一大痛点,在我的另一篇博客《C++中string如何实现字符串分割函数split()》中使用了四种方法实现了spilt()函数,欢迎补充。

  • 7
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我叫RT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值