目录
一. 列表初始化
1.1 {}列表初始化的方法
1.2 列表初始化实现的原理
二. C++11新增关键字
2.1 decltype -- 自动类型推断
2.2 nullptr -- 空指针
2.3 default -- 强制生成默认成员函数
2.4 delete -- 禁止生成默认成员函数
2.5 final -- 禁止类被继承/禁止虚函数被重写
2.6 override -- 检查子类虚函数是否重写了某个函数
三. C++11新增的两个默认成员函数
一. 列表初始化
1.1 {}列表初始化的方法
在C++98中,可以使用{}来初始化数组、结构体等,在C++11中,列表初始化的用途被扩大到所有的内置类型变量和自定义类型变量,也可以用于在new中给定构造函数的参数。在使用{}初始化时,我们甚至可以不带=。
进入C++11后,所有的STL容器也都会支持列表{}初始化。
代码1.1:列表初始化
int main()
{
//{}初始化内置类型成员变量
int i1 = { 1 };
int i2{ 2 };
//{}初始化结构体(类对象)
A a = { 1,3 };
B b = { 1,2 };
//{}用于给new出来的对象初值
B* pb = new B[2]{ {2,4}, {6,8} };
delete[] pb;
//STL容器对象列表初始化
std::vector<int> v = { 1,2,3,4,5 };
std::list<int> lt = { 1,2,3,4,5 };
return 0;
}
1.2 列表初始化实现的原理
为了实现列表初始化,C++11单独引入了一个名为initializer_list的类,这个类中包含iterator和const_iterator迭代器成员变量,还包括begin()和end()两个成员函数,用于查找列表的头部和尾部,C++11的STL容器中都会加入列表初始化函数。initializer_list的声明和成员以及部分SLT容器的列表初始化函数声明见图1.1和1.2。
- typeid(变量名).name() -- 获取变量类型(以字符串的形式表示)
代码1.2:打印initializer_list变量的类型
int main()
{
auto x = { 1,2,3,4 };
std::cout << typeid(x).name() << std::endl; //输出:class std::initializer_list<int>
return 0;
}
initializer_list的文档:initializer_list - C++ Reference
代码1.3为支持列表构造的vector类的简单模拟实现,只需要声明构造函数的参数类型为列表,用迭代器依次遍历链表的每个数据,将其push_back到vector中即可。
代码1.3:支持列表构造的vector类
namespace zhang
{
template<class T>
class vector
{
public:
vector() //默认构造
: _a(nullptr)
, _size(0)
, _capacity(0)
{ }
vector(const std::initializer_list<T>& lt) //列表构造函数
{
typename std::initializer_list<T>::const_iterator it = lt.begin();
while (it != lt.end())
{
push_back(*it);
it++;
}
}
void push_back(const T& x) //尾插数据函数
{
if (_capacity == 0 || _size == _capacity)
{
size_t newCapacity = _capacity == 0 ? 4 : 2 * _capacity;
_capacity = newCapacity;
T* tmp = new T[newCapacity];
memmove(tmp, _a, _size * sizeof(T));
std::swap(_a, tmp);
delete[] tmp;
tmp = nullptr;
}
_a[_size] = x;
++_size;
}
private:
T* _a;
size_t _size = 0;
size_t _capacity = 0;
};
}
二. C++11新增关键字
2.1 decltype -- 自动类型推断
decltype用于自动获取某个数据表达式的类型,可以用decltype获得的类型,去定义新的变量,decltype可以获取变量、字面常量、表示式的类型。
C++11中auto也可以用于自动类型推断,但其与decltype不同的是:auto定义变量时根据所赋的初值确定类型,而decltype在定义新变量时变量类型由传给decltype的数据表达式确定,与所赋初值无关。
代码2.1:decltype自动类型推断
int main()
{
int i = 10;
decltype(i) a = 10;
decltype(i) b = 10.5; //获取变量类型
auto c = 10.5;
std::cout << "a = " << a << std::endl;
std::cout << "b = " << b << std::endl;
std::cout << "c = " << c << std::endl;
decltype(1.55) d = 10.5; //获取字面常量类型
decltype(a * d) e = 20.5; //获取表达式类型
std::cout << "d = " << d << std::endl;
std::cout << "e = " << e << std::endl;
return 0;
}
2.2 nullptr -- 空指针
在C++98中,NULL被定义为字面常量0,如果用将NULL作为参数传递给函数,那么NULL会被识别为int类型的数据,如代码2.2中,将NULL传给func函数,我们希望运行参数类型为void*形式的重载函数,但实际上运行了int型参数的func,这样就存在调用错误。
为了解决上面的问题,C++11引入了一个新的空指针宏nullptr,用于取代C++98中的NULL,nullptr的定义为:#define nullptr ((void*)0)
如果将nullptr作为参数调用func,则void func(void* x)被正确调用。
代码2.2:NULL和nullptr做参数
void func(void* x)
{
std::cout << "void func(void* x)" << std::endl;
}
void func(int x)
{
std::cout << "void func(int x)" << std::endl;
}
int main()
{
func(NULL);
func(nullptr);
return 0;
}
2.3 default -- 强制生成默认成员函数
假设我们在自定义一个类的时候,显示定义了拷贝构造函数,那么编译器就会认为构造函数已经存在,不会再生成默认构造函数。但是,如果我们还是希望编译器生成默认构造函数,可以使用default强制生成,default强制生成默认成员函数语法为:函数声明 = default。
代码2.3:default强制生成成员函数
class A
{
public:
A() = default; //强制生成默认构造
A(const A& a)
: _a1(a._a1)
, _a2(a._a2)
{ }
A& operator=(const A& a) = default; //强制生成拷贝构造函数
A& operator=(A&& a) //移动拷贝
{
_a1 = a._a1;
_a2 = a._a2;
return *this;
}
private:
int _a1 =10;
int _a2 = 20;
};
2.4 delete -- 禁止生成默认成员函数
如果我们希望定义一个不允许被赋值(operator=)的类,在C++98的标准下,我们需要定义一个继承体系,将基类的赋值运算符重载函数声明为私有类型,然后将禁止被赋值的类定义为这个基类的派生类。这样,如果我们试图赋值,程序会报错。
代码2.4:通过定义继承体系禁止类对象被赋值(C++98)
class Forbid
{
private:
Forbid& operator=(const Forbid& f) { };
};
//Derive继承Forbid类,Forbid类中的operator=函数为私有
//Derive类对象不能被赋值
class Derive: public Forbid
{
public:
Derive(int x = 10, int y = 20)
: Forbid()
, _x(x)
, _y(y)
{ }
private:
int _x;
int _y;
};
int main()
{
Derive d1;
Derive d2(20, 30);
d1 = d2; //编译报错
return 0;
}
但是C++98通过继承禁止拷贝赋值十分复杂繁琐,且实际上并没有让operator=函数消失,只是由于父类的operator=函数被声明为了私有,无法被调用而已。
为此,C++11赋予了关键字delete新的用途 -- 禁止生成某个默认成员函数。
语法:函数声明 = delete
代码2.5:delete禁止特定成员函数的生成
class A
{
public:
A(int a1 = 10, int a2 = 20)
: _a1(a1)
, _a2(a2)
{ }
//禁止生成拷贝构造和赋值运算符重载函数
A(const A& a) = delete;
A& operator=(const A& a) = delete;
private:
int _a1 = 10;
int _a2 = 20;
};
2.5 final -- 禁止类被继承/禁止虚函数被重写
final关键字的两种作用:
- 声明某个类不能被继承
- 声明某个虚函数不能被重写
代码2.6:final关键字的使用
//Base类不能被继承
class A final
{ };
class B: public A //报错
{ };
class AA
{
public:
virtual void func() final { std::cout << "class AA" << std::endl; }
};
class BB : public AA
{
public:
//非法重写func函数,报错
virtual void func() { std::cout << "class BB" << std::endl; }
};
2.6 override -- 检查子类虚函数是否重写了某个函数
override关键字:放在子类中虚函数声明的后面,如果子类虚函数没有重写某个父类的虚函数,那么就编译报错。
代码2.7:override检查子类虚函数重写
class AA
{
public:
//virtual void func() { std::cout << "class AA" << std::endl; }
};
class BB : public AA
{
public:
//func为重写父类虚函数,报错
virtual void func() override { std::cout << "class BB" << std::endl; }
};
int main()
{
BB bb;
return 0;
}
三. C++11新增的两个默认成员函数
在C++98中,我们认为编译器自动生成的默认构造函数有6个,分别为:构造函数、析构函数、拷贝构造函数、赋值运算符重载函数、对于普通对象的取地址运算符重载函数、对于const对象的的取地址运算符重载函数。
由于C++11新增了右值引用,为了减少拷贝的消耗,C++11中默认成员函数增加到了8个,新增的两个为:移动构造函数、移动赋值函数。
- 编译器自动生成移动构造函数的条件:不显示定义移动构造函数、不显示定义拷贝构造、赋值运算符重载、析构三个默认成员函数的任意一个。
- 编译器自动生成的移动构造函数进行的工作:对于内置类型成员变量进行浅拷贝,对于自定义类型成员变量,如果有移动构造函数,调用其移动构造函数,如果没有,调用其拷贝构造函数。
- 移动赋值函数自动生成的条件与移动赋值函数相同,进行的工作与移动构造函数基本一致,内置类型值拷贝,自定义类型调用其拷贝赋值函数或移动赋值函数。
为了观察默认生成的移动构造函数进行的工作,编写代码3.1,类BB中包含类AA的自定义类型成员变量,将BB类的拷贝构造、拷贝赋值和析构函数全部注释掉,然后在主函数中利用拷贝构造和移动构造创建对象dd2和dd3,运行代码(结果见图3.2),可以观察到aa的移动构造函数被bb默认生成的移动构造函数调用了。
注意:由于VS2013不能很好的支持C++11的一些新特性,应当用VS2019演示代码3.1。
代码3.1:默认生成移动构造函数的功能验证
class AA
{
public:
AA(int a1 = 1)
: _a1(a1)
{ }
AA(const AA& aa)
: _a1(aa._a1)
{
std::cout << "拷贝构造 -- AA" << std::endl;
}
AA(AA&& aa)
: _a1(aa._a1)
{
std::cout << "移动构造 -- AA" << std::endl;
}
private:
int _a1 = 1;
};
class BB
{
public:
BB(int aa = 1, int b1 = 2)
: _aa(AA(1))
, _b1(b1)
{ }
/*BB(const BB& bb)
: _aa(bb._aa)
, _b1(bb._b1)
{ }*/
/*BB& operator=(const BB& bb)
{
_b1 = bb._b1;
}*/
/*~BB(){ }*/
private:
AA _aa;
int _b1;
};
int main()
{
BB bb1(10, 20);
BB bb2(bb1);
BB bb3(std::move(bb1));
return 0;
}