4 语法
约 2661 字大约 9 分钟
2025-06-22
基于范围的for循环
烦琐的容器遍历
使用for循环和迭代器遍历容器中的元素
map<int,string> m{ {1, "hello"}, {2, "world"}, {3, "!"} };
for ( auto it = m.begin(); it != m.end(); ++it) {
cout << (*it).first <<','<< (*it).second << endl;
}
为了输出容器中的内容不得不编写很多关于迭代器的代码,但迭代器本身并不是业务逻辑所关心的部分。对于这个问题的一个可行的解决方案是使用标准库提供的std::for_each函数,使用该函数只需要提供容器开始和结束的迭代器以及执行函数或者仿函数即可
map<int, string> m{
{1, "hello"},
{2, "world"},
{3, "!"}
};
for_each(m.begin(), m.end(),
[](map<int, string>::const_reference e) {
cout << e.first << ',' << e.second << endl;
}
);
这段代码使用std::for_each遍历容器比直接使用迭代器的方法要简洁许 多。实际上单纯的迭代器遍历操作完全可以交给编译器来完成,这样能让程序员专注于业务代码 而非迭代器的循环。
基于范围的for循环
该特性隐藏了迭代器的初始化和更新过程,让程 序员只需要关心遍历对象本身,其语法也比传统for循环简洁很多
语法格式
for(declaration:expression){
//循环体
}
declaration:变量,用于存储要遍历的容器的数据其类型是范围表达式中元素的类型或者元素类型的引用。 expression:要遍历的对象,可以是表达式、容器、数组、初始化列表等
基于范围的for循环只访问一次容器,在这一次访问容器的时候就已经确定循环多少次了(找好了边界)。但普通的for循环每循环一次就要判断一次边界(it!=list.end()) 这样基于范围的for循环如果在遍历的时候增删其中的元素可能就会导致遍历的失败,但普通for循环每次循环都要判断一下边界,所以不会出错
initializer_list<int> list={1,2,3,4,5,6};
for (auto it:list) {
cout<<it<<endl;
}
这里it的类型和list中元素的类型相同 但这种遍历每进行一次就会把expression中的元素拷贝给declaration一次,会浪费系统资源,可以使用引用节省系统资源
initializer_list<int> list={1,2,3,4,5,6};
for (auto &it:list) {
cout<<it<<endl;
}
使用引用后既能节省系统资源又能改变expression中的元素,不用引用改变的是declaration的值 如果不想改变expression中的值可以在declaration前加const修饰 一般来说,我们希望对于复杂的对象使用引用,而对于基础类型使用值,因 为这样能够减少内存的复制。如果不会在循环过程中修改引用对象,那么推荐在范围声明中加上 const限定符以帮助编译器生成更加高效的代码
struct X {
X() { cout << "default ctor" << endl; }
X(const X &other) {
cout << "copy ctor" << endl;
}
};
int main() {
vector<X> x(10);
cout << "for (auto n : x)" <<endl;
for (auto n: x) {
} cout << "for (const auto &n : x)" << endl;
for (const auto &n: x) {
}
}
for(auto n : x)的循环调用10次复制构造函数,如果类X的数据 量比较大且容器里的元素很多,那么这种复制的代价是无法接受的。而for(const auto &n : x)则 解决了这个问题,整个循环过程没有任何的数据复制。
遍历关系型容器时需要注意的问题
关系型容器即其中的元素是键值对类型的(pair) 使用普通的for循环获得的是迭代器类型(指针),获取元素使用 it->first it->second 使用基于范围的for循环获得的是容器中元素类型(对象),获取元素使用 it.first it.second
元素只读
在for循环内部声明一个变量的引用可以修改遍历的容器的值,但并不适用于所有情况,对于set容器而言内部的元素都是只读的
实现一个支持基于范围的for循环的类
要完成这样的类型必须先实现一个类似标准库中的迭代器。
- 该类型必须有一组和其类型相关的begin和end函数,它们可以是类型的成员函数,也可以 是独立函数。
- begin和end函数需要返回一组类似迭代器的对象,并且这组对象必须支持operator * 、 operator !=和operator ++运算符函数。
这里的operator ++应该是一个前缀版本,它需要通过声明一个不带形参的operator ++运算符函数来完成
class IntIter {
private:
int *p_;
public:
IntIter(int *p) : p_(p) {}
bool operator!=(const IntIter &other) {
return p_ != other.p_;
}
const IntIter &operator++() {
p_++;
return *this;
}
int operator*() const {
return *p_;
}
};
template<unsigned int fix_size>
class FixIntVector {
private:
int data_[fix_size]{0};
public:
FixIntVector(initializer_list<int> init_list) {
int *cur = data_;
for (auto e: init_list) {
*cur = e;
cur++;
}
}
IntIter begin() {
return IntIter(data_);
}
IntIter end() {
return IntIter(data_ + fix_size);
}
};
int main() {
FixIntVector<10> fix_int_vector{1, 3, 5, 7, 9};
for (auto e: fix_int_vector) {
cout << e << endl;
}
}
FixIntVector是存储int类型数组的类模板,类IntIter是FixIntVector的迭代 器。在FixIntVector中实现了成员函数begin和end,它们返回了一组迭代器,分别表示数组的开始 和结束位置。类IntIter本身实现了operator * 、operator !=和operator ++运算符函数,其中 operator * 用于编译器生成解引用代码,operator !=用于生成循环条件代码,而前缀版本的 operator ++用于更新迭代器。 这里使用成员函数的方式实现了begin和end,但有时候需要遍历的容器可能是第三方 提供的代码。这种情况下我们可以实现一组独立版本的begin和end函数,这样做的优点是能在不修 改第三方代码的情况下支持基于范围的for循环。
支持初始化语句的if
在C++17标准中,if控制结构可以在执行条件语句之前先执行一个初始化语句。 语法:
if (init; condition) {}
其中init是初始化语句,condition是条件语句,使用分号分隔。
bool foo(){
return true;
}
int main(){
if (bool b = foo(); b) {
cout << boolalpha<< b << endl;
}
}
该变量的生命周期会一直伴随整个if结构,包括else if和else部分。 初始化语句也可以在else if里面使用 但if里面初始化的变量生命周期和else if里面变量的生命周期不同 if初始化的变量是生命周期贯穿整个if结构,else if初始化的变量生命周期只存在于当前及后续存在的if语句中
支持初始化语句的switch
switch在通过条件判断确定执行的代码分支之前也可以接受一个初始化语句。
int ret(){
return 1;
}
int main(){
switch (int a=ret();a) {
case 3:
cout<<'3'<<endl;
break;
case 2:
cout<<'2'<<endl;
break;
case 1:
cout<<'1'<<endl;
break;
default:
break;
}
}
switch初始化语句声明的变量的生命周期会贯穿整个switch结构,这一点和if也相同
确定的表达式求值顺序
表达式求值顺序的不确定性
std::string s = "but I have heard it works even if you don't believe in it";
s.replace(0, 4, "").replace(s.find("even"), 4, "only").replace(s.find (" don't"), 6, "");
assert(s == "I have heard it works only if you believe in it"); // OK
一个表达式中 的子表达式的求值顺序,而这个顺序在C++17之前是没有具体说明的,所以编译器可以以任何顺 序对子表达式进行求值。比如说foo(a, b, c),这里的foo、a、b和c的求值顺序是没有确定的。回到上面的替换函数,如果这里的执行顺序为:
- replace(0, 4, "")
- tmp1 = find("even")
- replace(tmp1, 4, "only")
- tmp2 = find(" don't")
- replace(tmp2, 6, "") 那结果肯定是“I have heard it works only if you believe in it”,没有任何问题。但是由于 没有对表达式求值顺序的严格规定,因此其求值顺序可能会变成:
- tmp1 = find("even")
- tmp2 = find(" don't")
- replace(0, 4, "")
- replace(tmp1, 4, "only")
- replace(tmp2, 6, "") 相应的结果就不是那么正确了,我们会得到“I have heard it works evenonlyyou donieve in it”。
除了上述的例子之外,我们常用的<<操作符也面临同样的问题:
cout << f() << g() << h();
虽然我们认为上面的表达式应该按照f()、g()、h()顺序对表达式求值,但是编译器对此并不 买单,在它看来这个顺序可以是任意的。
表达式求值顺序
从C++17开始,函数表达式一定会在函数的参数之前求值。也就是说在foo(a, b, c)中,foo 一定会在a、b和c之前求值。但是请注意,参数之间的求值顺序依然没有确定,也就是说a、b和c 谁先求值还是没有规定。 从提案文档上看来,有充 分的理由说明从左往右进行参数列表的表达式求值的可行性。我想一个可能的原因是求值顺序的 改变影响到代码的优化路径,比如内联决策和寄存器分配方式,对于编译器实现来说也是不小的 挑战吧。不过既然标准已经这么定下来了,我们就应该去适应标准。在函数的参数列表中,尽可 能少地修改共享的对象,否则会很难确认实参的真实值。
对于后缀表达式和移位操作符而言,表达式求值总是从左往右
E1[E2]
E1.E2
E1.*E2
E1->*E2
E1<<E2
E1>>E2
在上面的表达式中,子表达式求值E1总是优先于E2。而对于赋值表达式,这个顺序又正好相 反,它的表达式求值总是从右往左
E1=E2
E1+=E2
E1-=E2
E1*=E2
E1/=E2
子表达式求值E2总是优先于E1。这里虽然只列出了几种赋值表达式的形 式,但实际上对于E1@=E2这种形式的表达式(其中@可以为+、−、* 、/、%等)E2早于E1求值总是成立的。
new表达式对于:
new T(E)
这里new表达式的内存分配总是优先于T构造函数中参数E的求值。 涉及重载运算符的表达式的求值顺序应由与之相应的内置运算符的求值顺序确定,而不是函 数调用的顺序规则。
贡献者
版权所有
版权归属:PinkDopeyBug