C++11新增关键字和范围for循环
C++11新增关键字和范围for循环
- auto和decltype(c++11)
- auto简介
- auto使用
- decltype使用
- 范围for循环(C++11)
- 范围for 循环使用
- 范围for的使用条件
- 指针空值nullptr(c++11)
- default
- delete
auto和decltype(c++11)
随着程序越来越复杂,程序中用到的类型也越来越复杂,经常体现在:
-
类型难于拼写。感受最明显的就是迭代器和函数指针。
-
含义不明确导致容易出错。
#include<iostream>
#include<string>
#include<vector>
#include<map>
using namespace std;int main() {map<string, string>dct = {{"insert","插入"},{"erase","删除"}};map<string, string>::iterator it = dct.begin();while (it != dct.end()) {cout << "[" << it->first << ":" << it->second << "]";++it;}return 0;
}
使用typedef
的话,还会有新的问题:
#include<iostream>
using namespace std;typedef char* pstring;
int main() {//const pstring p1;//编译失败const char* p3;//可以编译通过const pstring* p2;//可以编译通过return 0;
}
用typedef
给char*
取别名后,指针可以事先声明不用初始化,但会指向编译器初始化的随机值。
const pstring
的本意是pstring
是char*
,在前面加const
可以组成const char*
,但实际上const
和pstring
都只会修饰p1
,这会使p1
实际的类型是char* const
(指向char
的const
指针),p1
自带常属性,所以必须在定义时初始化,否则编译错误。
而p3
是const char*
,const
在*
前,修饰的是p3
指向的内存空间,且p3
指向哪里可以通过赋值决定,所以可以暂时不初始化。p2
则是指向char* const
的指针,自身不带常属性可以不先初始化。
所以随着对象的类型越来越长,需要在声明变量的时候清楚地知道表达式的类型。
auto简介
在早期c/c++中auto
的含义是:使用auto
修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它,因为即使不用它,局部变量也是自动创建和自动销毁,而auto
不能用于全局变量(生命周期和程序相同),导致auto
在c++11之前又没有别的功能。
c++11中,标准委员会赋予了auto
全新的含义即:auto
不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto
声明的变量必须由编译器在编译时期推导而得。
使用auto
定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto
的实际类型。因此auto
并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto
替换为变量实际的类型。其中感受最明显的就是迭代器的自动推导。
例如:
#include<iostream>
#include<vector>
using std::cout;
using std::endl;int TestAuto() {return 10;
}int main() {int a = 10;auto b = a;auto c = 'a';auto d = TestAuto();//输出对象的类型//typeid().name是将()内的类型推导成字符串。cout << typeid(b).name() << endl;cout << typeid(c).name() << endl;cout << typeid(d).name() << endl;//无法通过编译,使用auto定义变量时必须对其进行初始化//auto e;//初始化列表需要编译器支持c++11std::vector<int>arr = { 1,2,3,4,5,6,7 };//声明迭代器auto it1 = arr.begin();std::vector<int>::iterator it2= arr.begin();//it1和it2是同一类型,同一性质的变量if (it1 == it2)cout << "==";return 0;
}
auto使用
- auto与指针和引用结合起来使用
用auto
声明指针类型时,用auto
和auto*
没有任何区别,但用auto
声明引用类型时则必须加&
。
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;
- 在同一行定义多个变量
当用auto
在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
例如:
auto a = 1, b = 2; //可以auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
-
auto
不能作为函数的参数。 -
auto
不能直接用来声明数组。 -
为了避免与c++98中的
auto
发生混淆,c++11只保留了auto
作为类型指示符的用法。这意味着c++11的
auto
不再保留c++98的功能,c++98的auto
和c++11的auto
是2个完全不同的关键字。 -
auto
在实际中最常见的优势用法就是跟以后会讲到的c++11提供的新式for
循环,还有lambda
表达式等进行配合使用。 -
auto
也不是所有情况都好用,若auto
推导出来的是复杂的类,会增加调试难度(比如类模板的模板参数是已定义的复杂类型,而auto
推导出来的是另一个类型)。#include<iostream> using namespace std;int main() {auto pr = {1,3};pair<int, int> pr2 = { 1,3 };//class std::initializer_list<int>cout << typeid(pr).name() << endl;//struct std::pair<int,int>cout << typeid(pr2).name() << endl;return 0; }
-
auto
会丢弃引用和const
限定符(除非显式指定)。int main() {const int a = 10;auto b = a; //int(const被丢弃)b = 3;auto& c = a; //const int&(显式保留引用和const)//c = 4;//c是const int&,不可被修改return 0; }
-
auto
可以用于推导std::initializer_list
。#include<iostream> using namespace std;int main() {auto il = { 1,2,3,4 };cout << typeid(il).name() << endl;return 0; }
输出:
class std::initializer_list<int>
decltype使用
decltype
关键字能将变量的类型声明为表达式指定的类型。也可以用于推导声明的变量的类型。用来当成模板参数实例化类模板、函数模板同样适用。
#include<iostream>
#include<vector>
using std::cout;
using std::endl;
using std::vector;int main() {int i = 1;double d = 2.2;auto ret = i * d;//ret为double型decltype(ret) x;//声明double型对象xdecltype(ret) pi = 3.14;//定义double型对象picout << pi << endl;pi = 3.14159;cout << pi << endl;cout << typeid(pi).name() << endl;// 用ret的类型去实例化vector// 用来当做模板参数vector<decltype(ret)> v;v.push_back(1);v.push_back(1.1);for (auto e : v) cout << e << " "; cout << endl;cout << typeid(v).name() << endl;return 0;
}
范围for循环(C++11)
范围 for
循环是受到其他语言的影响,新增的语法。
比如Python的for
循环:
for i in Container:print(i,sep=' ')
这个循环相当于将类似STL的容器Container
中偶有的元素进行遍历。
范围for 循环使用
c++11中引入了基于范围的for
循环。for
循环后的括号由冒号:
分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
#include<iostream>
using std::cout;
using std::endl;int main() {int a[] = { 1,2,3,4,5,6,7,8 };for (int x : a)cout << x << ' ';cout << endl;for (auto x : a)cout << x << ' ';cout << endl;for (auto& x : a)//加引用,元素可修改cout << x << ' ';cout << endl;for (const auto& x : a)//加const,限制对x的修改cout << x << ' ';return 0;
}
与普通循环类似,可以用continue
来结束本次循环,也可以用break
来跳出整个循环。
但auto
前加const
,仅用于限制通过符号来修改要遍历的底层数据,底层调用的迭代器是begin
还是cbegin
(const this
指针的begin
迭代器),取决于容器的对象是否用const
修饰。
因此范围for
不会主动选择const
迭代器,除非容器对象本身是const
。
范围for的使用条件
-
for
循环迭代的范围必须是确定的。对于数组而言,就是数组中第一个元素和最后一个元素的范围;
对于类而言,应该提供
begin
和end
的方法,begin
和end
就是for
循环迭代的范围。
例如以下代码就有问题,因为for
的范围不确定、
void TestFor(int array[]) {//array作为形参实际是一个int*指针for(auto& e : array)cout<< e <<endl;
}
且这个对外公开的函数接口只能是begin
和end
,但凡有一个字母是大写都不行。
- 迭代的对象要实现
++
和!=
的操作。(++
表示往下走,!=
用于判断结尾)。
据说c++20有了Ranges
库,可以简化部分条件,具体我也没研究过,这里重点写c++11。
范围for
循环的本质就是通过迭代器来枚举所有元素。
#include<iostream>
#include<vector>
using namespace std;int main() {vector<int>a = { 2,7,1,8,2,8 };//范围for的本质是迭代器循环for (vector<int>::iterator it = a.begin(),ed=a.end(); it != ed; it++) {auto& tmp = *it;//操作*itcout << tmp << ' ';}cout << endl;for (auto& tmp : a)cout << tmp << ' ';cout << endl;return 0;
}
在之前的string的模拟实现、vector的模拟实现、list的模拟实现、红黑树的模拟实现和哈希相关的模拟实现都支持范围for
循环。
例如,这里随便设计一个vector
。这个vector
提供了begin
和end
接口,指针支持++
和!=
的操作,所以它支持范围for
循环。
#include<iostream>
#include<initializer_list>
using namespace std;namespace mystd {template<class T>class vector {public:typedef T* iterator;typedef const T* const_iterator;//初始化列表为形参的构造函数vector(initializer_list<T> il) {_arr = new T[il.size()];_size = _capacity = il.size();iterator itv = begin();typename initializer_list<T>::iterator iti = il.begin();while (iti != il.end()) {*itv++ = *iti++;}}//析构函数~vector() {delete[] _arr;_size = _capacity = 0;}//迭代器//提供begin和end接口iterator begin() {return _arr;}iterator end() {return _arr + _size;}//提供begin和end的带const的重载,因为对象可能是const对象const_iterator begin() const {return _arr;}const_iterator end() const {return _arr + _size;}private:T* _arr;size_t _size;size_t _capacity;};
}int main() {//推导为iteratormystd::vector<int>a = { 3,1,4,1,5,9 };for (auto& x : a)cout << x << ' ';cout << endl;//推导为const_iteratorconst mystd::vector<int>a2 = { 3,1,4,1,5,9 };for (auto& x : a2)cout << x << ' ';cout << endl;return 0;
}
指针空值nullptr(c++11)
未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:
void TestPtr(){int* p1 = NULL;int* p2 = 0;
}
NULL
实际是一个宏,在传统的c头文件(stddef.h
)中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
但NULL
的使用仍然有限制,比如这个代码:
#include<iostream>
using std::cout;
using std::endl;void f(int a) {cout << "f(int)" << endl;
}void f(int* a) {cout << "f(int*)" << endl;
}int main() {f(0);f(NULL);//这个函数调用的是哪一个?f((int*)NULL);return 0;
}
输出:
f(int)
f(int)
f(int*)
在c++98中,字面常量 0 既可以是一个整形数字,也可以是无类型的指针(void*)
常量,但是编译器默认情况下将其看成是一个整型常量,所以第2个函数用的是f(int)
。
如果要将其按照指针方式来使用,必须对其进行强转(void*)0
。(c++会把NULL
看成0,c语言看成(void*)0
)。
因此c++11引入nullptr
作为c++的空指针在某些场合代替宏NULL
。
用来补c++的坑,我的评价是旧bug不敢改就加新bug,新bug在N多年后又变成旧bug,之后的人们又会添加怎样的新bug就不清楚了。
注意:
-
在使用
nullptr
表示指针空值时,不需要包含头文件,因为nullptr
是c++11作为新关键字引入的。 -
在c++11中,
sizeof(nullptr)
与sizeof((void*)0)
所占的字节数相同。 -
为了提高代码的健壮性,在后续表示指针空值时建议最好使用
nullptr
。
default
某些情况使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default
关键字显示指定移动构造生成。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include"mystd.h"
using namespace std;class Person {
public:Person(const char* name = "", int age = 0):_name(name), _age(age) {}//拷贝构造Person(const Person& p):_name(p._name), _age(p._age) {}//强制生成默认的移动构造函数Person(Person&& p) = default;~Person() {}
private:mystd::string _name;int _age;
};
int main() {Person s1 = { "shawshank",666 };Person s3 = std::move(s1);return 0;
}
delete
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private
,并且只声明补丁已,这样只要其他人想要调用就会报错。在C++11中只需在该函数声明加上=delete
即可,该语法指示编译器不生成对应函数的默认版本,称=delete
修饰的函数为删除函数。
delete
原本的用处是释放通过new
申请的空间。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include"mystd.h"
using namespace std;class Person {
public:Person(const char* name = "", int age = 0):_name(name), _age(age) {}//拷贝构造Person(const Person& p):_name(p._name), _age(p._age) {}//强制删除移动构造Person(Person&& p) = delete;~Person() {}
private:mystd::string _name;int _age;
};
int main() {Person s1 = { "shawshank",666 };//Person s3 = std::move(s1);//无法调用被删除的函数return 0;
}
此外,在c++11,新增新的关键字final
,经过final
修饰的类是最终类,同样无法被继承。final
修饰虚函数,表示该虚函数不能再被重写。
override
则是修饰派生类,检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。所以override
可以规范程序员的行为,让程序员不忘记重写虚函数。
关于这2个关键字的解释,详见类与对象—继承-CSDN博客和类和对象—多态-CSDN博客。