C++11 —— 常用语法
C++11简介
在2003年,C++标准委员会提交了一份技术勘误表(简称TC1),使得C++03这个名称取代了C++98,成为C++11之前的最新C++标准名称。不过,由于C++03(TC1)主要是对C++98标准中的漏洞进行修复,语言的核心部分没有改动,因此人们习惯性地将这两个标准合并称为C++98/03标准。
从C++0x到C++11,C++标准经历了十年的磨砺,第二个真正意义上的标准终于问世。相比于C++98/03,C++11带来了数量可观的变化,包含约140个新特性,并修正了C++03标准中约600个缺陷,使得C++11更像是从C++98/03中孕育出的一种新语言。
相比较而言,C++11能够更好地用于系统开发和库开发,语法更加丰富和简单化,且更加稳定和安全。不仅功能更强大,还能提升程序员的开发效率,因此在公司实际项目开发中也得到了广泛应用,成为学习的重点。
C++11增加的语法特性非常多,篇幅较大,无法一一讲解。本节课程将主要讲解实际中比较实用的语法特性。
小故事:C++标准的演变
1998年是C++标准委员会成立的第一年,最初计划每5年视实际需要更新一次标准。在研究C++03的下一个版本时,委员会一开始计划在2007年发布,因此最初这个标准被称为C++07。然而,到2006年时,委员会意识到2007年肯定无法完成C++07的发布,甚至2008年也可能无法完成。最终,他们决定将其称为C++0x,其中"x"表示不确定具体的完成年份。
结果,2010年时仍未完成,最终在2011年,C++标准终于完成。因此,这个标准被正式命名为C++11。
统一的列表初始化
{}初始化
在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。比如:
struct Test
{
int a; // 成员变量a
int b; // 成员变量b
};
int main()
{
// 1. 使用初始化列表初始化整型数组
int arr1[] = { 1, 2, 3, 4 }; // 数组大小由编译器自动推导为4
// 2. 使用初始化列表初始化整型数组并设置默认值
int arr2[5] = { 0 }; // 数组大小为5,所有元素初始化为0
// 3. 使用初始化列表初始化结构体变量
Test t = { 1, 2 }; // 结构体t的成员a初始化为1,b初始化为2
return 0;
}
C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自
定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
#include <iostream>
// 定义一个结构体Point,包含两个整型成员变量_x和_y
struct Point
{
int _x; // x坐标
int _y; // y坐标
};
int main()
{
// 1. 使用普通赋值初始化整型变量x1
int x1 = 1; // x1被初始化为1
// 2. 使用统一初始化(C++11引入)初始化整型变量x2
int x2{ 2 }; // x2被初始化为2
// 3. 使用初始化列表初始化整型数组array1
int array1[]{ 1, 2, 3, 4, 5 }; // array1的大小由编译器自动推导为5,元素分别为1, 2, 3, 4, 5
// 4. 使用初始化列表初始化整型数组array2,所有元素初始化为0
int array2[5]{ 0 }; // array2的大小为5,第一元素为0,其余元素自动初始化为0
// 5. 使用初始化列表初始化结构体Point的对象p
Point p{ 1, 2 }; // p的成员_x被初始化为1,_y被初始化为2
// 6. 使用new表达式动态分配一个大小为4的整型数组,并初始化所有元素为0
// C++98中,不能使用初始化列表,必须手动初始化
// int* pa = new int[4]; // C++98: 只分配内存,元素未初始化
// for (int i = 0; i < 4; ++i) pa[i] = 0; // 手动初始化为0
// C++11中,支持使用初始化列表进行初始化
int* pa = new int[4]{ 0 }; // pa指向动态分配的数组,所有元素初始化为0
// 7. 释放动态分配的内存(避免内存泄漏)
delete[] pa; // 使用delete[]释放数组
return 0; // 返回0表示程序正常结束
}
创建对象时也可以使用列表初始化方式调用构造函数初始化。比如:
// 定义一个类A
class A
{
public:
// 构造函数,接受一个int参数x,初始化_x和_y为x
A(int x)
: _x(x), _y(x) // 使用初始化列表初始化成员变量
{}
// 构造函数,接受两个int参数x和y,分别初始化_x和_y
A(int x, int y)
: _x(x), _y(y) // 使用初始化列表初始化成员变量
{}
private:
int _x; // 私有成员变量_x
int _y; // 私有成员变量_y
};
int main()
{
// 1. 使用统一初始化(C++11引入)初始化对象a1,调用A(int x)构造函数
A a1{ 1 }; // a1的_x和_y都被初始化为1
// 2. 使用赋值初始化方式初始化对象a2,调用A(int x)构造函数
A a2 = { 2 }; // a2的_x和_y都被初始化为2
// 3. 使用统一初始化初始化对象a3,调用A(int x, int y)构造函数
A a3{ 1, 2 }; // a3的_x被初始化为1,_y被初始化为2
// 4. 使用赋值初始化方式初始化对象a4,调用A(int x, int y)构造函数
A a4 = { 1, 3 }; // a4的_x被初始化为1,_y被初始化为3
// 5. 使用统一初始化创建一个临时对象,并绑定到常量引用aa3
const A& aa3 = { 2, 2 }; // 创建一个临时对象,_x和_y都被初始化为2,aa3引用这个临时对象
return 0; // 返回0表示程序正常结束
}
注: 带有
=
的初始化通常会创建临时对象并进行拷贝赋值,而不带=
的初始化直接调用构造函数创建对象,效率更高。
std::initializer_list
类型定义:
std::initializer_list<T>
是一个模板类,其中 T
是元素的类型。它提供对一组 const T
对象的访问。
构造与初始化:
当你使用大括号初始化时,例如 X x{ 1, 2, 3 };
,编译器会自动创建一个 std::initializer_list
对象,包含这些元素。
成员函数:
- begin() 和 end():返回指向元素的迭代器。
- size():返回元素的数量。
使用场景:
std::initializer_list
常用于容器的初始化、构造函数参数、以及在需要传递多个值时。
代码示例:
#include <iostream> // 引入输入输出流库
#include <initializer_list> // 引入initializer_list支持
using namespace std; // 使用标准命名空间
// 定义一个类模板Test
template <typename T>
class Test
{
public:
// 构造函数,接受一个initializer_list<T>类型的参数
Test(initializer_list<T> list)
{
// 遍历initializer_list中的每个元素
for (const auto& e : list)
{
cout << e << " "; // 输出每个元素
}
cout << "\nSize: " << list.size() << endl; // 输出元素的数量
}
};
int main()
{
// 创建Test<int>类型的对象t1,并使用initializer_list初始化
Test<int> t1{ 1, 2, 3, 4 };
//1 2 3 4
//Size: 4
return 0; // 返回0表示程序正常结束
}
上述示例展示了如何使用 initializer_list
在类模板中接收一组初始化值,方便地使用大括号来初始化一些值,并直接通过构造函数进行处理。
让模拟实现的vector也支持{}初始化和赋值
#pragma once
#include<assert.h>
#include <initializer_list>
using namespace std;
namespace qq
{
template<class T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
vector():_start(nullptr),_finish(nullptr),_endofstorage(nullptr)
{}
vector(const vector<T>& v)
{
reserve(v.capacity());
for (auto& e : v)
{
push_back(e);
}
}
vector(std::initializer_list<T> l)
{
/*_start = new T[l.size()];
_finish = _endofstorage = _start + l.size();*/
reserve(l.size());
iterator vit = _start;
typename initializer_list<T>::iterator lit = l.begin();
while (lit != l.end())
{
*vit++ = *lit++;
}
_endofstorage = _finish = _start + l.size();
}
~vector()
{
delete[] _start;
_start = _finish = _endofstorage = nullptr;
}
size_t size()const
{
return _finish - _start;
}
size_t capacity() const
{
return _endofstorage - _start;
}
T& operator[](size_t pos)
{
assert(pos < size());
return _start[pos];
}
void reserve(size_t n)
{
//if (capacity() == 0) //当为空数组时,专门为initializer_list服务。
//{
// T* tmp = new T[n];
// size_t old_size = size();
// memcpy(tmp, _start, old_size * sizeof(T));
// delete[] _start;
// _start = tmp;
// _finish = _endofstorage = tmp + n;
//}
if (n > capacity())
{
T* tmp = new T[n];
size_t old_size = size();
memcpy(tmp, _start, old_size * sizeof(T));
delete[] _start;
_start = tmp;
_finish = tmp + old_size;
_endofstorage = tmp + n;
}
}
void resize(size_t n, const T& val = T())
{
if (n > size())
{
reserve(n);
//插入
while (_finish < _start + n)
{
*_finish = val;
_finish++;
}
}
else
{
//删除
_finish = _start + n;
}
}
void push_back(const T& val)
{
if (_finish == _endofstorage)
{
reserve(capacity() == 0 ? 4 : capacity() * 2);
}
*_finish = val;
_finish++;
}
void pop_back()
{
assert(!empty());
_finish--;
}
bool empty()
{
//return _start == nullptr;
return size() == 0;
}
void insert(iterator pos, const T& val)
{
assert(pos >= _start);
assert(pos <= _finish);
size_t n = pos - _start;
reserve(size()+1);
pos = _start + n; //一定要更新pos的位置
iterator it = _finish - 1;
while (it >= pos)
{
*(it + 1) = *(it);
it--;
}
*pos = val;
_finish++;
}
void erase(iterator pos)
{
assert(pos >= _start);
assert(pos < _finish);
/*iterator it = pos ;
while (it < _finish -1)
{
*it = *(it + 1);
it++;
}
_finish--;*/
iterator it = pos + 1;
while (it < _finish)
{
*(it - 1) = *(it);
it++;
}
_finish--;
}
void print_Vector()
{
for (auto e : *this)
{
std::cout << e << " ";
}
std::cout << std::endl;
}
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _endofstorage = nullptr;
};
}
注意: 在
initializer_list
中复用reserve
进行扩容时,因为此时数组为空,所以需要特殊化处理_finish 与_endofstorage 。
为什么
typename initializer_list<T>::iterator lit = l.begin();
要用typename
?
答:因为typename initializer_list<T>::iterator
是依赖名称,在模板中,当一个名称依赖于模板参数的时候,编译器无法确定它是一个非类型,这种情况下,编译器需要你明确的告诉他是一个类型,此时就需要使用typename
来声明。
声明
c++11提供了多种简化声明的方式,尤其是在使用模板时。
auto
在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型腿断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。
代码示例:
int main()
{
// 声明整型变量i并初始化为10
int i = 10;
// 声明指向int类型的指针p,并让p指向i
auto p = &i;
// 声明函数指针pf,并让其指向strcpy函数
auto pf = strcpy;
// 输出指针p的类型名称
cout << typeid(p).name() << endl;
// 输出函数指针pf的类型名称
cout << typeid(pf).name() << endl;
// 创建一个string到string的映射dict,并初始化一些键值对
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
// 使用auto声明迭代器it,并让其指向dict的起始位置
auto it = dict.begin();
return 0;
}
decltype
关键字decltype将变量的类型声明为表达式指定的类型。
#include <iostream> // 包含输入输出流库
#include <typeinfo> // 包含typeid用于获取类型信息
using namespace std;
// 模板函数F,接受两个参数t1和t2
template<class T1, class T2>
void F(T1 t1, T2 t2)
{
// 使用decltype推导t1和t2相乘的结果类型,并声明ret
decltype(t1 * t2) ret;
// 输出ret的类型名称
cout << typeid(ret).name() << endl;
}
int main()
{
const int x = 1; // 声明一个常量整型变量x,值为1
double y = 2.2; // 声明一个双精度浮点型变量y,值为2.2
// 使用decltype推导x和y相乘的结果类型,并声明ret
decltype(x * y) ret; // ret的类型是double
// 使用decltype推导x的地址类型,并声明p
decltype(&x) p; // p的类型是int*
// 输出ret的类型名称
cout << typeid(ret).name() << endl;
// 输出p的类型名称
cout << typeid(p).name() << endl;
// 调用模板函数F,传入整型1和字符'a'
F(1, 'a');
return 0;
}
这段代码展示了C++中的decltype
和模板函数的使用。decltype
用于在编译时推导表达式的类型,例如,decltype(x * y)
推导出ret
的类型为double
,而decltype(&x)
推导出p
的类型为int*
。模板函数F
接受两个参数,编译器根据传入的参数自动推导模板参数类型,并使用decltype
推导其乘积的类型。通过typeid
,可以输出变量的实际类型名称,增强了代码的灵活性与可读性。
nullptr
C++11引入nullptr
的主要原因是为了解决在C++98/03中使用NULL
作为空指针常量时可能出现的问题。
C++98/03中的问题:
-
重载函数的歧义:
当有重载函数时,使用NULL
作为参数可能会导致调用歧义。例如:void foo(int); void foo(char*); foo(NULL); // 调用foo(int),而不是foo(char*)
-
隐式类型转换:
NULL可以隐式转换为整型,这可能会导致一些意料之外的行为。 -
可读性:
使用0作为空指针常量可能会降低代码的可读性,因为不清楚它是表示整数0还是空指针。
nullptr的特点:
为了解决这些问题,C++11引入了nullptr
关键字。nullptr
具有以下特点:
- 类型为std::nullptr_t:
nullptr
的类型为std::nullptr_t
,这是一个独立的类型。 - 可以隐式转换为指针类型:
nullptr
可以隐式转换为任何指针类型或指向成员类型。 - 不能隐式转换为整型:
nullptr
不能隐式转换为整型,除了bool
类型。 - 不会导致重载函数的歧义:
使用nullptr
调用重载函数时不会产生歧义。
范围for
C++11引入的 范围for循环(Range-based for loop) 是一种简化遍历容器和数组的语法,使代码更加简洁和易于理解。其基本语法格式为:
for (元素类型 变量名 : 可迭代对象) {
// 循环体
}
主要特点:
- 简洁性:范围for循环避免了传统for循环中需要手动管理索引或迭代器的繁琐,使代码更加清晰。例如,遍历一个std::vector时,不再需要显式调用begin()和end()。
- 安全性:由于不直接操作索引或迭代器,范围for循环减少了因索引越界或迭代器失效导致的问题。
- 通用性:适用于任何定义了begin()和end()方法的类型,包括标准库容器、数组和自定义容器。
使用示例:
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec{1, 2, 3, 4, 5};
// 使用范围for循环遍历vector
for (auto num : vec) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
注意事项:
- 只读访问:默认情况下,范围for循环对元素的访问是只读的。如果需要修改元素,必须将循环变量声明为引用类型,例如for (auto& item : container)。
- 不能修改容器大小:在遍历过程中,不能通过添加或删除元素来修改容器的大小,因为这可能导致迭代器失效。
范围for循环极大地提高了C++代码的可读性和可维护性,是C++11中一个非常实用的特性。