C 11-列表初始化/变量类型推导/范围for/final&override/默认成员函数控制
- 零、前言
- 一、C 11简介
- 二、列表初始化
- 1、内置类型列表初始化
- 2、自定义类型列表初始化
- 三、变量类型推导
- 1、auto类型推导
- 2、decltype类型推导
- 四、范围for循环
- 五、final和override
- 1、final
- 2、override
- 六、默认成员函数控制
零、前言
本章将开始学习C 11的新语法特性,主要是一些比较常用的语法
一、C 11简介
- 发展历程:
- 在2003年C 标准委员会曾经提交了一份技术勘误表(简称TC1),使得C 03这个名字已经取代了C 98称为C 11之前的最新C 标准名
- 不过由于TC1主要是对C 98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C 98/03标准
- 从C 0x到C 11,C 标准10年磨一剑,第二个真正意义上的标准珊珊来迟。相比于C 98/03,C 11则带来了数量可观的变化,其中包含了约140个新特性,以及对C 03标准中约600个缺陷的修正,这使得C 11更像是从C 98/03中孕育出的一种新语言。
- 相比较而言,C 11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率
二、列表初始化
- 背景引入:
在C 98中,标准允许使用花括号{}对数组元素进行统一的列表初始值设定
- 示例:
int array1[] = {1,2,3,4,5};
int array2[5] = {0};
注:对于一些自定义的类型,却无法使用这样的初始化
1、内置类型列表初始化
C 11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加
- 示例:
int main()
{
// 内置类型变量
int x1 = {10};
int x2{10};
int x3 = 1 2;
int x4 = {1 2};
int x5{1 2};
// 数组
int arr1[5] {1,2,3,4,5};
int arr2[]{1,2,3,4,5};
// 动态数组,在C 98中不支持
int* arr3 = new int[5]{1,2,3,4,5};
// 标准容器
vector<int> v{1,2,3,4,5};
map<int, int> m{{1,1}, {2,2,},{3,3},{4,4}};
return 0;
}
- 效果:
注:列表初始化可以在{}之前使用等号,其效果与不使用=没有什么区别
2、自定义类型列表初始化
- 标准库支持单个对象的列表初始化
class Pointer
{
public:
Pointer(int x = 0, int y = 0) : _x(x), _y(y)
{}
private:
int _x;
int _y;
};
int main()
{
Pointer p{ 1, 2 };
//等同于与调用构造函数
//Pointer p( 1, 2 );
return 0;
}
- 多个对象的列表初始化
多个对象想要支持列表初始化,需要实现initializer_list类型参数的构造函数
- 示例:
#include <initializer_list>
template<class T>
class Vector {
public:
// ...
Vector() : _capacity(0), _size(0){}
Vector(initializer_list<T> l) : _capacity(l.size()), _size(0)
{
_array = new T[_capacity];
for (auto e : l)
_array[_size ] = e;
}
Vector<T>& operator=(initializer_list<T> l) {
delete[] _array;
size_t i = 0;
for (auto e : l)
_array[i ] = e;
return *this;
}
// ...
private:
T* _array;
size_t _capacity;
size_t _size;
};
int main()
{
Vector<int> v1{ 1,2,3,4 };
Vector<int> v2;
v2 = { 1,2,3,4,5 };
return 0;
}
- 效果:
注:initializer_list是系统自定义的类模板,该类模板中主要有三个方法:begin()**、**end()迭代器以及获取区间中元素个数的方法size()
三、变量类型推导
1、auto类型推导
在定义变量时,必须先给出变量的实际类型,编译器才允许定义,但有些情况下可能不知道需要实际类型怎么给,或者类型写起来特别复杂
- 示例:
void test1()
{
short a = 32670;
short b = 32670;
// c如果给成short,会造成数据丢失,如果能够让编译器根据a b的结果推导c的实际类型,就不会存在问题
short c = a b;
cout<<c<<endl;
}
代码语言:javascript复制void test2()
{
std::map<std::string, std::string> m{
{"apple", "苹果"}, {"banana","香蕉"}
};
// 使用迭代器遍历容器, 迭代器类型太繁琐
std::map<std::string, std::string>::iterator it = m.begin();
while(it != m.end())
{
cout<<it->first<<" "<<it->second<<endl;
it;
}
}
C 11中,可以使用auto来根据变量初始化表达式类型推导变量的实际类型,可以给程序的书写提供许多方便。将程序中c与it的类型换成auto,程序可以通过编译,而且更加简洁
- 示例:
void test3()
{
short a = 32670;
short b = 32670;
// c如果给成short,会造成数据丢失,如果能够让编译器根据a b的结果推导c的实际类型,就不会存在问题
auto c = a b;
cout<<c<<endl;
std::map<std::string, std::string> m{
{"apple", "苹果"}, {"banana","香蕉"}
};
// 使用迭代器遍历容器, 迭代器类型太繁琐
auto it = m.begin();
while(it != m.end())
{
cout<<it->first<<" "<<it->second<<endl;
it;
}
}
- 效果:
2、decltype类型推导
- 为什么需要decltype:
- auto使用的前提是:必须要对auto声明的类型进行初始化,否则编译器无法推导出auto的实际类型
- 但有时候可能需要根据表达式运行完成之后结果的类型进行推导,因为编译期间,代码不会运行,此时auto也就无能为力
- 示例:
template<class T1, class T2>
T1 Add(const T1& left, const T2& right)
{
return left right;
}
注:如果能用加完之后结果的实际类型作为函数的返回值类型就不会出错,但这需要程序运行完才能知道结果的实际类型,即RTTI(Run-Time Type Identification 运行时类型识别)
- C 98中确实已经支持RTTI:
- typeid只能查看类型不能用其结果类定义类型
- dynamic_cast只能应用于含有虚函数的继承体系中
注:运行时类型识别的缺陷是降低程序运行的效率
- decltype的使用:
decltype是根据表达式的实际类型推演出定义变量时所用的类型
- 推演表达式类型作为变量的定义类型
- 示例:
int main()
{
int a = 10000000000000;
int b = 10000000000000;
// 用decltype推演a b的实际类型,作为定义c的类型
decltype(a b) c;
cout<<typeid(c).name()<<endl;
return 0;
}
- 推演函数返回值的类型
- 示例:
void* GetMemory(size_t size)
{
return malloc(size);
}
int main()
{
// 如果没有带参数,推导函数的类型
cout << typeid(decltype(GetMemory)).name() << endl;
// 如果带参数列表,推导的是函数返回值的类型,注意:此处只是推演,不会执行函数
cout << typeid(decltype(GetMemory(0))).name() << endl;
return 0;
}
- 效果:
四、范围for循环
在 C 98/03 中,不同的容器和数组遍历的方式不尽相同,写法不统一,也不够简洁,而 C 11 基于范围的 for 循环可以以简洁、统一的方式来遍历容器和数组,用起来也更方便了
- 示例:
int main(void)
{
vector<int> v = { 1, 2, 3, 4, 5, 6 };
for (auto it = v.begin(); it != v.end(); it)
{
cout << *it << " ";
}
cout << endl;
for (auto& value : v)
{
cout << value << " ";
}
cout << endl;
return 0;
}
- 效果:
- C 11基于范围的for循环语法格式:
for (declaration : expression)
{
// 循环体
}
- 解释:
declaration 表示遍历声明,在遍历过程中,当前被遍历到的元素会被存储到声明的变量中。expression 是要遍历的对象,它可以是
表达式
、容器
、数组
、初始化列表
等
五、final和override
1、final
C 中增加了 final 关键字来限制某个类不能被继承,或者某个虚函数不能被重写。如果使用 final 修饰函数,只能修饰虚函数,并且要把final关键字放到类或者函数的后面
- 修饰函数:
代码语言:javascript复制如果使用 final 修饰函数,只能修饰虚函数,这样就能阻止子类重写父类的这个函数了:
class Base
{
public:
virtual void test() final
{
cout << "Base class..."<<'n';
}
virtual void test2()
{
cout << "Base class test2..."<<'n';
}
};
class Child : public Base
{
public:
//带有final, 无法重写
void test()
{
cout << "Child class..." << 'n';
}
void test2()
{
cout << "Child class test2..." << 'n';
}
};
- 效果:
- 修饰类
使用 final 关键字修饰过的类是不允许被继承的,也就是说这个类不能有派生类
- 示例:
class Base final
{
public:
virtual void test()
{
cout << "Base class..." << 'n';
}
virtual void test2()
{
cout << "Base class test2..." << 'n';
}
};
class Child : public Base
{
public:
void test()
{
cout << "Child class..." << 'n';
}
void test2()
{
cout << "Child class test2..." << 'n';
}
};
- 效果:
2、override
override 关键字确保在派生类中声明的重写函数与基类的虚函数有相同的签名,同时也明确表明将会重写基类的虚函数。这样就可以保证重写的虚函数的正确性,也提高了代码的可读性,和 final 一样这个关键字要写到方法的后面
- 示例:
class Base
{
public:
void test()
{
cout << "Base class...";
}
};
class Child : public Base
{
public:
void test() override
{
cout << "Child class...";
}
};
- 效果:
注:使用了 override 关键字之后,假设在重写过程中因为误操作,写错了函数名或者函数参数或者返回值编译器都会提示语法错误
六、默认成员函数控制
- 引入背景:
- 在C 中对于空类编译器会生成一些默认的成员函数,如果在类中显式定义了,编译器将不会重新生成默认版本
- 有时候这样的规则可能被忘记,最常见的是声明了带参数的构造函数,必要时则需要定义不带参数的版本以实例化无参的对象。而且有时编译器会生成,有时又不生成,容易造成混乱,于是C 11让程序员可以控制是否需要编译器生成
- 显式缺省函数
在C 11中,可以在默认函数定义或者声明时加上=default,从而显式的指示编译器生成该函数的默认版本,用=default修饰的函数称为显式缺省函数
- 示例:
class A
{
public:
A(int a) : _a(a)
{}
// 显式缺省构造函数,由编译器生成
A() = default;
// 在类中声明,在类外定义时让编译器生成默认赋值运算符重载
A& operator=(const A& a);
private:
int _a;
};
A& A::operator=(const A& a) = default;
int main()
{
A a1(10);
A a2;
a2 = a1;
return 0;
}
- 删除默认函数
- 如果能想要限制某些默认函数的生成,在C 98中,是该函数设置成private,并且不给定义,这样只要其他人想要调用就会报错
- 在C 11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数
- 示例:
class A
{
public:
A(int a) : _a(a)
{}
// 禁止编译器生成默认的拷贝构造函数以及赋值运算符重载
A(const A&) = delete;
A& operator=(const A&) = delete;
private:
int _a;
};
int main()
{
A a1(10);
// 编译失败,因为该类没有拷贝构造函数
A a2(a1);
// 编译失败,因为该类没有赋值运算符重载
A a3(20);
a3 = a2;
return 0;
}