(1) nullptr
用 nullptr 代替NULL,
NULL是一个宏定义,在c和c++中的定义不同,c中NULL为(void*)0,而c++中NULL为整数0
void foo(char *);
void foo(int);
对于这两个函数,如果NULL定义为0的话,foo(NULL)将会出现歧义
使用NULL的情景均可用 nullptr 代替
(2) constexpr
constexpr 让用户显式的声明函数或对象构造函数在编译器为常数
constexpr 的函数可以使用递归,从 C++ 14 开始, constexpr 函数可以在内部使用局部变量、循环和分支等简单语句,但 C++ 11 中是不可以的
constexpr int two() {
return 2;
}
int _two() {
return 2;
}
constexpr int fib(int n) {
return n == 1 || n == 2 ? 1 : fib(n-1)+fib(n-2);
}
int main() {
int a[two()];
//int _a[_two()]; //编译出错
int b[fib(5)];
int x;
cin >> x;
int c[fib(x)];//错误用法,constexpr必须编译期可求
cout << sizeof (c) / sizeof (int);
return 0;
}
constexpr 还能用于修饰类的构造函数,即保证如果提供给该构造函数的参数都是 constexpr ,那么产生的对象中的所有成员都会是 constexpr ,该对象也就是 constexpr 对象了,可用于各种只能使用 constexpr 的场合。注意, constexpr 构造函数必须有一个空的函数体,即所有成员变量的初始化都放到初始化列表中。
struct A {
constexpr A(int xx, int yy): x(xx), y(yy) {}
int x, y;
};
constexpr A a(1, 2);
enum {SIZE_X = a.x, SIZE_Y = a.y};
C++中的const和constexpr
(3) auto
auto 的意义是使C++编译器可以在编译时推导数据类型,这样就不用每次都要声明数据类型了.
注意 auto 不能用于函数传参以及推导数组类型
vector<int> vec{1,2,3};
for (auto it = vec.begin(); it != vec.end(); ++it)
cout << *it << " ";//迭代器
auto n = 10;//int
auto x = new auto(12); //int*
(4) decltype
有时我们希望从表达式的类型推断出要定义的变量类型,但是不想用该表达式的值初始化变量(初始化可以用 auto )。为了满足这一需求,C++11新标准引入了 decltype 类型说明符,它的作用是选择并返回操作数的数据类型,在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。
auto x = 1;
auto y = 2;
decltype(x+y) z = 0; // z 是一个 int 型的
C++11新标准学习:decltype关键字
(5)尾返回类型
template<typename T, typename U>
auto add(T x, U y) -> decltype(x+y) {
return x+y;
}
C++ 14 开始是可以直接让普通函数具备返回值推导
template<typename T, typename U>
auto add(T x, U y) {
return x+y;
}
(6)基于范围的 for 循环
int array[] = {1,2,3,4,5};
for(auto &x : array) {
std::cout << x << std::endl;
x = -x;
}
for(auto x : array) {
std::cout << x << std::endl;
}
(7)初始化列表
int a[3]{1,2,3}; //统一的初始化语法
vector<int> vetc{1,2,3};
set<int> f{1,1,2,3};
(8) 外部模板
传统 C++ 中,模板只有在使用时才会被编译器实例化。换句话说,只要在每个编译单元(文件)中编译的代码中遇到了被完整定义的模板,都会实例化。这就产生了重复实例化而导致的编译时间的增加。并且,我们没有办法通知编译器不要触发模板实例化。
// test1.cpp
#include "test.h"
template void fun<int>(int); // 显式地实例化
void test1()
{
fun(1);
}
// test2.cpp
#include "test.h"
extern template void fun<int>(int); // 外部模板的声明
void test2()
{
fun(2);
}
(9) 尖括号 “>”
std::vector<std::vector<int>> wow;
两个>相连不用加空格了。
(10) 类型别名模板
template< typename T, typename U, int value>
class SuckType {
public:
T a;
U b;
SuckType():a(value),b(value){}
};
template< typename U>
using NewType = SuckType<std::vector<int>, U, 1>;
NewType<int> Tmp;
typedef int (*process)(void *); // 定义了一个返回类型为 int,参数为 void* 的函数指针类型,名字叫做 process
using process = int(*)(void *); // 同上, 更加直观
(11)默认模板参数
template<typename T = int, typename U = int>
auto add(T x, U y) -> decltype(x+y) {
return x+y;
}
(12)变长参数模板
允许任意个数、任意类别的模板参数,同时也不需要在定义时将参数的个数固定。
template<typename... Ts> class Magic;
模板类 Magic 的对象,能够接受不受限制个数的 typename 作为模板的形式参数,例如下面的定义:
class Magic<int,
std::vector<int>,
std::map<std::string,
std::vector<int>>> darkMagic;
既然是任意形式,所以个数为 0 的模板参数也是可以的:class Magic<> nothing;
。
除了在模板参数中能使用 ...
表示不定长模板参数外,函数参数也使用同样的表示法代表不定长参数,这也就为我们简单编写变长参数函数提供了便捷的手段,例如:
template<typename... Args> void printf(const std::string &str, Args... args);
那么我们定义了变长的模板参数,如何对参数进行解包呢?
首先,我们可以使用 sizeof...
来计算参数的个数,:
template<typename... Args>
void magic(Args... args) {
std::cout << sizeof...(args) << std::endl;
}
我们可以传递任意个参数给 magic
函数:
magic(); // 输出0
magic(1); // 输出1
magic(1, ""); // 输出2
其次,对参数进行解包,到目前为止还没有一种简单的方法能够处理参数包,但有两种经典的处理手法:
1. 递归模板函数
递归是非常容易想到的一种手段,也是最经典的处理方法。这种方法不断递归的向函数传递模板参数,进而达到递归遍历所有模板参数的目的:
#include <iostream>
template<typename T>
void printf(T value) {
std::cout << value << std::endl;
}
template<typename T, typename... Args>
void printf(T value, Args... args) {
std::cout << value << std::endl;
printf(args...);
}
int main() {
printf(1, 2, "123", 1.1);
return 0;
}
2. 初始化列表展开
递归模板函数是一种标准的做法,但缺点显而易见的在于必须定义一个终止递归的函数。
这里介绍一种使用初始化列表展开的黑魔法:
// 编译这个代码需要开启 -std=c++14
template<typename T, typename... Args>
auto print(T value, Args... args) {
std::cout << value << std::endl;
return std::initializer_list<T>{([&] {
std::cout << args << std::endl;
}(), value)...};
}
int main() {
print(1, 2.1, "123");
return 0;
}
在这个代码中,额外使用了 C++11 中提供的初始化列表以及 Lambda 表达式的特性,而 std::initializer_list 也是 C++11 新引入的容器。
通过初始化列表,(lambda 表达式, value)...
将会被展开。由于逗号表达式的出现,首先会执行前面的 lambda 表达式,完成参数的输出。唯一不美观的地方在于如果不使用 return
编译器会给出未使用的变量作为警告。
(13)面向对象增强
(1)委托构造
class Base {
public:
int value1;
int value2;
Base() {
value1 = 1;
}
Base(int value) : Base() { // 委托 Base() 构造函数
value2 = 2;
}
};
int main() {
Base b(2);
std::cout << b.value1 << std::endl;
std::cout << b.value2 << std::endl;
}
(2)继承构造
class Base {
public:
int value1;
int value2;
Base() {
value1 = 1;
}
Base(int value) : Base() { // 委托 Base() 构造函数
value2 = 2;
}
};
class Subclass : public Base {
public:
using Base::Base; // 继承构造
};
int main() {
Subclass s(3);
std::cout << s.value1 << std::endl;
std::cout << s.value2 << std::endl;
}
(3)显式虚函数重载
C++ 11 引入了 override
和 final
这两个关键字来防止上述情形的发生。
当重载虚函数时,引入 override
关键字将显式的告知编译器进行重载,编译器将检查基函数是否存在这样的虚函数,否则将无法通过编译:
struct Base {
virtual void foo(int);
};
struct SubClass: Base {
virtual void foo(int) override; // 合法
virtual void foo(float) override; // 非法, 父类没有此虚函数
};
final
则是为了防止类被继续继承以及终止虚函数继续重载引入的。
struct Base {
virtual void foo() final;
};
struct SubClass1 final: Base {
}; // 合法
struct SubClass2 : SubClass1 {
}; // 非法, SubClass 已 final
struct SubClass3: Base {
void foo(); // 非法, foo 已 final
};
(6)显式禁用默认函数
在传统 C++ 中,如果程序员没有提供,编译器会默认为对象生成默认构造函数、复制构造、赋值算符以及析构函数。另外,C++ 也为所有类定义了诸如 new
delete
这样的运算符。
class Magic {
public:
Magic() = default; // 显式声明使用编译器生成的构造
Magic& operator=(const Magic&) = delete; // 显式声明拒绝编译器生成构造
Magic(int magic_number);
}
(14)强类型枚举
C++ 11 引入了枚举类(enumaration class),并使用 enum class
的语法进行声明:
enum class new_enum : unsigned int {
value1,
value2,
value3 = 100,
value4 = 100
};
这样定义的枚举实现了类型安全,首先他不能够被隐式的转换为整数,同时也不能够将其与整数数字进行比较,更不可能对不同的枚举类型的枚举值进行比较。但相同枚举值之间如果指定的值相同,那么可以进行比较。
(15) Lambda 表达式
C++ 11 中的 Lambda 表达式用于定义并创建匿名的函数对象,以简化编程工作。 Lambda 的语法形式如下:
[函数对象参数] (操作符重载函数参数) mutable 或 exception 声明 -> 返回值类型 {函数体}
函数对象参数
标识一个 Lambda 表达式的开始,这部分必须存在,不能省略。函数对象参数是传递给编译器自动生成的函数对象类的构造函数的。函数对象参数只能使用那些到定义 Lambda 为止时 Lambda 所在作用范围内可见的局部变量 (包括 Lambda 所在类 的 this)。函数对象参数有以下形式:
- 空。没有任何函数对象参数。
- =。函数体内可以使用 Lambda 所在范围内所有可见的局部变量(包括 Lambda 所在类的 this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)。
- &。函数体内可以使用 Lambda 所在范围内所有可见的局部变量(包括 Lambda 所在类的 this),并且是引用传递方式(相当于是编译器自动为我们按引用传递了所有局部变量)。
- this。函数体内可以使用 Lambda 所在类中的成员变量。
- a。将 a 按值进行传递。按值进行传递时,函数体内不能修改传递进来的 a 的拷贝,因为默认情况下函数是 const 的,要修改传递进来的拷贝,可以添加 mutable 修饰符。
- &a。将 a 按引用进行传递。
- a,&b。将 a 按值传递,b 按引用进行传递。
- =,&a,&b。除 a 和 b 按引用进行传递外,其他参数都按值进行传递。
- &,a,b。除 a 和 b 按值进行传递外,其他参数都按引用进行传递。
操作符重载函数参数
标识重载的 () 操作符的参数,没有参数时,这部分可以省略。参数可以通过按值(如: (a, b))和按引用 (如: (&a, &b)) 两种方式进行传递。
mutable 或 exception 声明
这部分可以省略。按值传递函数对象参数时,加上 mutable 修饰符后,可以修改传递进来的拷贝(注意是能修改拷贝,而不是值本身)。exception 声明用于指定函数抛出的异常,如抛出整数类型的异常,可以使用 throw (int)。
-> 返回值类型
标识函数返回值的类型,当返回值为 void,或者函数体中只有一处 return 的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略。
函数体
标识函数的实现,这部分不能省略,但函数体可以为空。
实例
[] (int x, int y) { return x + y; } // 隐式返回类型
[] (int& x) { ++x;} // 没有 return 语句 -> Lambda 函数的返回类型是 'void'
[] () { ++global_x; } // 没有参数,仅访问某个全局变量
[] { ++global_x; } // 与上一个相同,省略了 (操作符重载函数参数)