在C++98标准之后,C++标准委员会于2003年其实还提出过一份技术勘误表(TC1),使得C++03 这个名字已经取代了C++98为C++11之前的最新的C++标准名称。不过由于 TC1 主要是对 C++98 标准中的漏洞进行修复,语言的核心部分并没有改动,因此大多数人习惯将这两个标准合称为 C++98/03
相比于 C++98/03 ,C++11 带来了数量可观的变化,不仅增加了新的特性,并且对 C++03 的漏洞进行了一定的修正。相比而言,C++11 能更好地用于系统开发和库开发,语言更加泛化和简单化,更加稳定和安全,不仅功能更强大,而且能提高开发效率
在C++98标准中,允许使用花括号对数组元素进行统一的列表初始化值设定。如:
int array1[] = { 1, 2, 3, 4, 5 };
int array2[5] = {0};
但是,对于一些自定义类型却无法使用这种方式的初始化。如:
vector<int> v{ 1, 2, 3, 4, 5 };
如上述例子中,C++98 标准就无法通过编译。需要将 vector 首先定义出来,然后循环对其赋初始值,这样的话显得十分不方便…
** C++11 扩大了用花括号括起来的列表的适用范围,使其可以用于所有的内置类型 和 用户自定义的类型,使用初始化列表时,可以加“=”,也可以不加 **
int main()
{
// 内置类型变量
int a = {10};
int b{10};
int c = 10 + 20;
int d = {10 + 20};
int e{10 + 20};
// 数组
int array1[] = { 1, 2, 3, 4, 5 };
int array2[5] = {0};
// 动态数组----C++11
int* array3 = new int[5]{ 1, 2, 3, 4, 5 };
// STL容器----C++11
std::vector<int> v{ 1, 2, 3, 4, 5 };
std::map<int, int> m{{1, 1}, {2, 2}, {10, 20}};
return 0;
}
1、对单个自定义类型对象的列表初始化
class person
{
public:
person(std::string = "", int age = 0)
: _name(name)
, _age(age)
{}
private:
std::string _name;
int _age;
};
int main()
{
person p1{"AdamXi", 20};
return 0;
}
2、对多个自定义类型对象的列表初始化
多个对象想要支持列表初始化,需要给该类(模板类)添加一个带有 initializer_list 类型参数的构造函数即可。
【注意】:initializer_list 是系统自定义的类模板,该类模板中主要有三个方法:begin()、end()迭代器以及获取区间中元素个数的方法 size()
#include
template<class T>
class Vector
{
public:
// ...
Vector(initializer_list<T> list)
: _capacity(list.size())
, _size(0)
{
_array = new T[_capacity];
for(auto e : list)
{
_array[_size++] = e;
}
}
Vector<T>& operator=(std::initializer_list<T> list)
{
delete[] _array;
size_t i = 0;
for(auto e : list)
{
_array[i++] = e;
}
return *this;
}
// ...
private:
T* _array;
size_t _size;
size_t _capacity;
};
其实 initializer_list 不单单只是上述代码中的对多个自定义类型对象的列表初始化,它还有更多的用处:
std::vector<int> v({ 1, 2, 3, 4, 5 });
在上述例子中,在实例化对象时候调用了构造函数,传进去的实参是 { 1, 2, 3, 4, 5 },这样就会创建一个 initializer_list 的临时对象,用来构造vector
在C语言中的printf、scanf等就是变长参数列表,可是在使用这一类的变长参数列表时,它们的共性就是第一个参数为格式化字符串,**C语言必须要通过第一个格式化字符串中的格式控制符来确定后面的参数个数。**这样的话,如果不遵守这个规则的话就会报错。那么,此时,initializer_list 它就可以起到作用:
void print(std::initializer_list<int> list)
{
for (auto it = list.begin(); it != list.end(); it++)
{
std::cout << *it << " ";
}
std::cout << endl;
}
int main()
{
printf("%d %d %d %d %d\n", 1, 2, 3, 4, 5);
print({ 1, 2, 3, 4, 5 });
return 0;
}
当然,这样的话,细心的朋友会发现,initializer_list 对象里面的话能放置相同类型的参数。当然同样也有办法放置不同类型的参数----C++17中引入了一个万能容器any,有兴趣的朋友有可以了解一下…
在定义变量时候,需要先给出变量的实际类型,编译器才允许定义,但在有些情况下可能不知道需要实际类型怎么给,或者类型写起来特别复杂。如:
int main()
{
short a = 32765;
short b = 32766;
// c如果给成short,会造成数据丢失,如果能让编译器根据 a+b 的结果推导出c的实际类型,就不会存在问题
short c = a + b;
std::map<std::string, int> stu{ { "AdamXi", 20 }, { "Sofency", 22 } };
// 使用迭代器遍历容器,迭代器类型太过繁琐
std::map<std::string, int>::iterator it = stu.begin();
while (it != stu.end())
{
std::cout << it->first << "\t" << it->second << std::endl;
++it;
}
return 0;
}
早在C++98 中就已经存在了 auto 关键字,那时的 auto 用于声明变量为自动变量,自动变量意为拥有自动的生命周期,只是多余的,因为即使没有用auto声明,变量依旧拥有自动的生命周期
C++11 中删除了auto的旧有语法,取而代之的是全新的auto关键字。其作用有如下几点:
int main()
{
short a = 32765;
short b = 32766;
// c如果给成short,会造成数据丢失,如果能让编译器根据 a+b 的结果推导出c的实际类型,就不会存在问题
auto c = a + b;
std::map<std::string, int> stu{ { "AdamXi", 20 }, { "Sofency", 22 } };
// 使用迭代器遍历容器,迭代器类型太过繁琐
auto it = stu.begin();
while (it != stu.end())
{
std::cout << it->first << "\t" << it->second << std::endl;
++it;
}
std::cout << typeid(c).name() << std::endl;
std::cout << typeid(it).name() << std::endl;
return 0;
}
运行结果如下:
AdamXi 20
Sofency 22
int
class std::_Tree_iteratorconst, int> > > >
template <typename _Tx,typename _Ty>
void Multiply(_Tx x, _Ty y)
{
auto v = x*y;
std::cout << v;
}
template <typename _Tx, typename _Ty>
auto multiply(_Tx x, _Ty y)->decltype(_Tx*_Ty)
{
return x*y;
}
此时auto作用仅为占位符,真正的返回值是后面的 decltype(_Tx*_Ty)。那为什么要这样做呢?
原因:如果没有后置返回值,则函数声明时为:
decltype(_Tx*_Ty)multiply(_Tx x, _Ty y)
而此时_Tx 和 _Ty都还未曾声明过,编译自然无法通过
有了上面的例子中 decltype 的应用,下面还是简单介绍一下:
auto 使用的前提是:必须要对 auto 声明的类型进行初始化,否则编译器无法推导出 auto 的实际类型
decltype 是根据表达式的实际类型推演出定义变量时所用的类型。如:
int main()
{
int a = 10;
int b = 20;
decltype(a + b) c;
std::cout << typeid(c).name() << std::endl;
return 0;
}
void* GetMemory(size_t size)
{
return malloc(size);
}
int main()
{
// 如果没有带参数,推导函数的类型
std::cout << typeid(decltype(GetMemory)).name() << std::endl;
// 如果带参数列表,推导的是函数返回值的类型。注意:此处只是推演,不会执行函数
std::cout << typeid(decltype(GetMemory(0))).name() << std::endl;
return 0;
}
一个类只要提供了分别获取起始和终止迭代器的begin和end函数,就可以支持基于范围的for循环
for(元素类型 元素对象:容器对象)
{
循环体
}
【注意】
在C语言中,使用NULL表示空指针。实际在C中,NULL通常被如下定义:
#define NULL ((void *)0)
也就是说NULL实际上是一个 void * 的指针,然后将 void * 指针赋值给对应类型的指针的时候,隐式转换成相应的类型。而如果换做一个C++编译器来编译的话是要出错的,因为C++是强类型的,void *是不能隐式转换成其他指针类型的,所以通常情况下,编译器提供的头文件会这样定义NULL:
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
C++中不能将 void * 类型的指针隐式转换为其他指针类型,而又为了解决空指针的问题,所以C++中引入0来表示空指针,就有了类似上面的代码来定义NULL
但是实际上,用NULL代替0表示空指针在函数重载时会出现问题,程序执行的结果会与我们的想法不同。如:
void func(void* i)
{
cout << "func1" << endl;
}
void func(int i)
{
cout << "func2" << endl;
}
int main()
{
func(NULL);
func(nullptr);
return 0;
}
函数运行结果:
func2
func1
上述代码中,运行结果与我们使用NULL的初衷是相违背的,因为我们本来是想用NULL来代替空指针,但是在将NULL输入到函数中时,它却选择了int形参这个函数版本,所以是有问题的,这就是用NULL代替空指针在C++程序中的二义性
故此,C++11 中引入了 nullptr 这个关键字来代指空指针
C++语法上定义:如果程序员没有显式定义构造函数、拷贝构造函数、赋值运算符重载、析构函数、取地址运算符重载 和 const类型的取地址运算符重载 这6个默认成员函数,编译器会自动生成这些默认的成员函数
但是,实际上并不是所有编译器都会这样进行实现的!
实际情况:因为有些情况下编译器生成的默认函数意义并不大,而且如果生成就需要调用 会影响程序的运行效率。因此,编译器并没有严格地按照语法实现。本文通过调研 VS2013 会发现:编译器是否会生成构造函数取决于是否需要,如果需要编译器才会生成
defalut:告诉编译器生成默认的成员函数
class Date
{
private:
Date() = default; // 即使用户定义了带有参数的构造函数,编译器也会生成无参的构造函数
}
delete:告诉编译器删除默认的成员函数,即不生成
class unique_ptr
{
private:
unique_ptr(const unique_ptr& ) = delete; // 禁止编译器生成拷贝构造函数
}
1、final:修饰虚函数,表示该虚函数不能再被继承
final是C++11中定义的继承控制关键字,它可以修饰虚函数,表示该虚函数不能被继承
若直接修饰基类,无任何意义。
2、override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写,则编译报错
C++11 之前的标准中提供了 auto_ptr 智能指针,但是,这种智能指针有着致命的缺陷,所以,C++11 标准中新增加了 unique_ptr 、shared_ptr 、weak_ptr 这几种智能指针。详情请参考:CSDN–AdamXi–C++智能指针
unordered_list 系列的容器,底层结构依赖哈希结构。之前C++部分文章都介绍过~~~