1.字符串原始字面量
在C++11中添加了定义原始字符串的字面量,定义方式为:R “xxx(原始字符串)xxx”其中()两边的字符串可以省略。原始字面量R可以直接表示字符串的实际含义,而不需要额外对字符串做转义或连接等操作。
比如:编程过程中,使用的字符串中常带有一些特殊字符,对于这些字符往往要做专门的处理,使用了原始字面量就可以轻松的解决这个问题了,比如打印路径:
#include<iostream>
#include<string>
using namespace std;
int main()
{
string str = "D:\hello\world\test.text";
cout << str << endl;
string str1 = "D:\\hello\\world\\test.text";
cout << str1 << endl;
string str2 = R"(D:\hello\world\test.text)";
cout << str2 << endl;
return 0;
}
输出的结果为:
D:helloworld est.text
D:\hello\world\test.text
D:\hello\world\test.text
- 在D:\hello\world\test.text中\h和\w转义失败,对应的字符会原样输出
- 在D:\\hello\\world\\test.text中路径的间隔符为\但是这个字符又是转义字符,因此需要使用转义字符将其转义,最终才能得到一个没有特殊含义的普通字符\
- 在R"(D:\hello\world\test.text)"使用了原始字面量R()中的内容就是描述路径的原始字符串,无需做任何处理
通过测试可以看到,使用原始字面量R可以直接得到其原始意义的字符串,再看一个输出HTML标签的例子:
#include<iostream>
#include<string>
using namespace std;
int main()
{
string str = "<html>\
<head>\
<title>\
海贼王\
</title>\
</head>\
<body>\
<p>\
姬霓太美!!!\
</p>\
</body>\
</html>";
cout << str << endl;
return 0;
}
在C++11之前如果一个字符串分别写到了不同的行里边,需要加连接符,这种方式不仅繁琐,还破坏了表达式的原始含义,如果使用原始字面量就变得简单很多,很强直观,可读性强。
#include<iostream>
#include<string>
using namespace std;
int main()
{
string str = R"(<html>
<head>
<title>
海贼王
</title>
</head>
<body>
<p>
姬霓太美!!!
</p>
</body>
</html>)";
cout << str << endl;
return 0;
}
#include<iostream>
#include<string>
using namespace std;
int main()
{
string str1 = R"(D:\hello\world\test.text)";
cout << str1 << endl;
string str2 = R"luffy(D:\hello\world\test.text)luffy";
cout << str2 << endl;
#if 0
string str3 = R"luffy(D:\hello\world\test.text)robin"; // 语法错误,编译不通过
cout << str3 << endl;
#endif
return 0;
}
测试代码输出的结果为:
D:\hello\world\test.text
D:\hello\world\test.text
通过输出的信息可以得到如下结论:使用原始字面量R “xxx(raw string)xxx”,()两边的字符串在解析的时候是会被忽略的,因此一般不用指定。如果在()前后指定了字符串,那么前后的字符串必须相同,否则会出现语法错误。
2.指针空值类型 - nullptr
在C++程序开发中,为了提高程序的健壮性,一般会在定义指针的同时完成初始化操作,或者在指针的指向尚未明确的情况下,都会给指针初始化为NULL,避免产生野指针(没有明确指向的指针,操作也这种指针极可能导致程序发生异常)。C++98/03 标准中,将一个指针初始化为空指针的方式有 2 种:
char *ptr = 0;
char *ptr = NULL;
在底层源码中NULL这个宏是这样定义的:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
也就是说如果源码是C++程序NULL就是0,如果是C程序NULL表示(void*)0。那么为什么要这样做呢? 是由于 C++ 中,void * 类型无法隐式转换为其他类型的指针,此时使用 0 代替 ((void *)0),用于解决空指针的问题。这个0(0x0000 0000)表示的就是虚拟地址空间中的0地址,这块地址是只读的。
C++ 中将 NULL 定义为字面常量 0,并不能保证在所有场景下都能很好的工作,比如,函数重载时,NULL 和 0 无法区分:
#include <iostream>
using namespace std;
void func(char *p)
{
cout << "void func(char *p)" << endl;
}
void func(int p)
{
cout << "void func(int p)" << endl;
}
int main()
{
func(NULL); // 想要调用重载函数 void func(char *p)
func(250); // 想要调用重载函数 void func(int p)
return 0;
}
测试代码打印的结果为:
void func(int p)
void func(int p)
通过打印的结果可以看到,虽然调用func(NULL);最终链接到的还是void func(int p)和预期是不一样的,其实这个原因前边已经说的很明白了,在C++中NULL和0是等价的。
出于兼容性的考虑,C++11 标准并没有对 NULL 的宏定义做任何修改,而是另其炉灶,引入了一个新的关键字nullptr。nullptr 专用于初始化空类型指针,不同类型的指针变量都可以使用 nullptr 来初始化:
int* ptr1 = nullptr;
char* ptr2 = nullptr;
double* ptr3 = nullptr;
对应上面的代码编译器会分别将 nullptr 隐式转换成 int*、char* 以及 double* 指针类型。使用nullptr可以很完美的解决上边提到的函数重载问题:
#include <iostream>
using namespace std;
void func(char *p)
{
cout << "void func(char *p)" << endl;
}
void func(int p)
{
cout << "void func(int p)" << endl;
}
int main()
{
func(nullptr);
func(250);
return 0;
}
测试代码输出的结果:
void func(char *p)
void func(int p)
通过输出的结果可以看出,nullptr 无法隐式转换为整形,但是可以隐式匹配指针类型。在 C++11 标准下,相比 NULL 和 0,使用 nullptr 初始化空指针可以令我们编写的程序更加健壮。
3.constexpr
3.1 const
在C++11之前只有const关键字,从功能上来说这个关键字有双重语义:变量只读,修饰常量,举一个简单的例子:
void func(const int num)
{
const int count = 24;
int array[num]; // error,num是一个只读变量,不是常量
int array1[count]; // ok,count是一个常量
int a1 = 520;
int a2 = 250;
const int& b = a1;
b = a2; // error
a1 = 1314;
cout << "b: " << b << endl; // 输出结果为1314
}
- 函数void func(const int num)的参数num表示这个变量是只读的,但不是常量,因此使用int array[num]; 这种方式定义一个数组,编译器是会报错的,提示num不可用作为常量来使用。
- const int count = 24;中的count却是一个常量,因此可以使用这个常量来定义一个静态数组。另外,变量只读并不等价于常量,二者是两个概念不能混为一谈,分析一下这句测试代码const int& b = a1;:
- b是一个常量的引用,所以b引用的变量是不能被修改的,也就是说b = a2; 这句代码语法是错误的。
- 在const对于变量a1是没有任何约束的,a1的值变了b的值也就变了
- 引用b是只读的,但是并不能保证它的值是不可改变的,也就是说它不是常量。
3.2 constexpr
在C++11中添加了一个新的关键字constexpr,这个关键字是用来修饰常量表达式的。所谓常量表达式,指的就是由多个(≥1)常量(值不会改变)组成并且在编译过程中就得到计算结果的表达式。
在介绍gcc/g++工作流程的时候说过,C++ 程序从编写完毕到执行分为四个阶段:预处理、 编译、汇编和链接4个阶段,得到可执行程序之后就可以运行了。需要额外强调的是,常量表达式和非常量表达式的计算时机不同,非常量表达式只能在程序运行阶段计算出结果,但是常量表达式的计算往往发生在程序的编译阶段,这可以极大提高程序的执行效率,因为表达式只需要在编译阶段计算一次,节省了每次程序运行时都需要计算一次的时间。
那么问题来了,编译器如何识别表达式是不是常量表达式呢?在C++11中添加了constexpr关键字之后就可以在程序中使用它来修饰常量表达式,用来提高程序的执行效率。在使用中建议将 const 和 constexpr 的功能区分开,即凡是表达“只读”语义的场景都使用 const,表达“常量”语义的场景都使用 constexpr。
在定义常量时,const 和 constexpr 是等价的,都可以在程序的编译阶段计算出结果,例如:
const int m = f(); // 不是常量表达式,m的值只有在运行时才会获取。
const int i=520; // 是一个常量表达式
const int j=i+1; // 是一个常量表达式
constexpr int i=520; // 是一个常量表达式
constexpr int j=i+1; // 是一个常量表达式
对于 C++ 内置类型的数据,可以直接用 constexpr 修饰,但如果是自定义的数据类型(用 struct 或者 class 实现),直接用 constexpr 修饰是不行的。
// 此处的constexpr修饰是无效的
constexpr struct Test
{
int id;
int num;
};
如果要定义一个结构体/类常量对象,可以这样写:
struct Test
{
int id;
int num;
};
int main()
{
constexpr Test t{ 1, 2 };
constexpr int id = t.id;
constexpr int num = t.num;
// error,不能修改常量
t.num += 100;
cout << "id: " << id << ", num: " << num << endl;
return 0;
}
在第13行的代码中t.num += 100;的操作是错误的,对象t是一个常量,因此它的成员也是常量,常量是不能被修改的。
4.自动类型推导
在C++11中增加了很多新的特性,比如可以使用auto自动推导变量的类型,还能够结合decltype来表示函数的返回值。使用新的特性可以让我们写出更加简洁,更加现代的代码。
4.1 auto
在C++11之前auto和static是对应的,表示变量是自动存储的,但是非static的局部变量默认都是自动存储的,因此这个关键字变得非常鸡肋,在C++11中他们赋予了新的含义,使用这个关键字能够像别的语言一样自动推导出变量的实际类型。
4.1.1 推导规则
C++11中auto并不代表一种实际的数据类型,只是一个类型声明的 “占位符”,auto并不是万能的在任意场景下都能够推导出变量的实际类型,使用auto声明的变量必须要进行初始化,以让编译器推导出它的实际类型,在编译时将auto占位符替换为真正的类型。使用语法如下:
auto 变量名 = 变量值;
根据上述语法,来列举一些简单的例子:
auto x = 3.14; // x 是浮点型 double
auto y = 520; // y 是整形 int
auto z = 'a'; // z 是字符型 char
auto nb; // error,变量必须要初始化
auto double nbl; // 语法错误, 不能修改数据类型
不仅如此,auto还可以和指针、引用结合起来使用也可以带上const、volatile限定符,在不同的场景下有对应的推导规则,规则内容如下:
- 当变量不是指针或者引用类型时,推导的结果中不会保留const、volatile关键字
- 当变量是指针或者引用类型时,推导的结果中会保留const、volatile关键字
先来看一组变量带指针和引用并使用auto进行类型推导的例子:
int temp = 110;
auto *a = &temp;
auto b = &temp;
auto &c = temp;
auto d = temp;
- 变量a的数据类型为 int*,因此auto关键字被推导为 int类型
- 变量b的数据类型为 int*,因此auto关键字被推导为 int*类型
- 变量c的数据类型为 int&,因此auto关键字被推导为 int类型
- 变量d的数据类型为 int,因此auto关键字被推导为 int类型
在来看一组带const限定的变量,使用auto进行类型推导的例子:
int tmp = 250;
const auto a1 = tmp;
auto a2 = a1;
const auto &a3 = tmp;
auto &a4 = a3;
- 变量a1的数据类型为 const int,因此auto关键字被推导为 int类型
- 变量a2的数据类型为 int,但是a2没有声明为指针或引用因此 const属性被去掉, auto被推导为 int
- 变量a3的数据类型为 const int&,a3被声明为引用因此 const属性被保留,auto关键字被推导为 int类型
- 变量a4的数据类型为 const int&,a4被声明为引用因此 const属性被保留,auto关键字被推导为 const int类型
4.1.2 auto的限制
auto关键字并不是万能的,在以下这些场景中是不能完成类型推导的:
1.不能作为函数参数使用。因为只有在函数调用的时候才会给函数参数传递实参,auto要求必须要给修饰的变量赋值,因此二者矛盾。
int func(auto a, auto b) // error
{
cout << "a: " << a <<", b: " << b << endl;
}
2.不能用于类的非静态成员变量的初始化
class Test
{
auto v1 = 0; // error
static auto v2 = 0; // error,类的静态非常量成员不允许在类内部直接初始化
static const auto v3 = 10; // ok
}
3.不能使用auto关键字定义数组
int func()
{
int array[] = {1,2,3,4,5}; // 定义数组
auto t1 = array; // ok, t1被推导为 int* 类型
auto t2[] = array; // error, auto无法定义数组
auto t3[] = {1,2,3,4,5};; // error, auto无法定义数组
}
4.无法使用auto推导出模板参数
template <typename T>
struct Test{}
int func()
{
Test<double> t;
Test<auto> t1 = t; // error, 无法推导出模板类型
return 0;
}
4.1.3 auto的应用
了解了auto的限制之后,我们就可以避开这些场景快乐的编程了,下面列举几个比较常用的场景:
1.用于STL的容器遍历。
在C++11之前,定义了一个stl容器之后,遍历的时候常常会写出这样的代码:
#include <map>
int main()
{
map<int, string> person;
map<int, string>::iterator it = person.begin();
for (; it != person.end(); ++it)
{
// do something
}
return 0;
}
可以看到在定义迭代器变量 it 的时候代码是很长的,写起来就很麻烦,使用了auto之后,就变得清爽了不少:
#include <map>
int main()
{
map<int, string> person;
// 代码简化
for (auto it = person.begin(); it != person.end(); ++it)
{
// do something
}
return 0;
}
2.用于泛型编程,在使用模板的时候,很多情况下我们不知道变量应该定义为什么类型,比如下面的代码:
#include <iostream>
#include <string>
using namespace std;
class T1
{
public:
static int get()
{
return 10;
}
};
class T2
{
public:
static string get()
{
return "hello, world";
}
};
template <class A>
void func(void)
{
auto val = A::get();
cout << "val: " << val << endl;
}
int main()
{
func<T1>();
func<T2>();
return 0;
}
在这个例子中定义了泛型函数func,在函数中调用了类A的静态方法 get() ,这个函数的返回值是不能确定的,如果不使用auto,就需要再定义一个模板参数,并且在外部调用时手动指定get的返回值类型,具体代码如下:
#include <iostream>
#include <string>
using namespace std;
class T1
{
public:
static int get()
{
return 0;
}
};
class T2
{
public:
static string get()
{
return "hello, world";
}
};
template <class A, typename B> // 添加了模板参数 B
void func(void)
{
B val = A::get();
cout << "val: " << val << endl;
}
int main()
{
func<T1, int>(); // 手动指定返回值类型 -> int
func<T2, string>(); // 手动指定返回值类型 -> string
return 0;
}
4.2. decltype
在某些情况下,不需要或者不能定义变量,但是希望得到某种类型,这时候就可以使用C++11提供的decltype关键字了,它的作用是在编译器编译的时候推导出一个表达式的类型,语法格式如下:
decltype (表达式)
decltype 是“declare type”的缩写,意思是“声明类型”。decltype的推导是在编译期完成的,它只是用于表达式类型的推导,并不会计算表达式的值。来看一组简单的例子:
int a = 10;
decltype(a) b = 99; // b -> int
decltype(a+3.14) c = 52.13; // c -> double
decltype(a+b*c) d = 520.1314; // d -> double
可以看到decltype推导的表达式可简单可复杂,在这一点上auto是做不到的,auto只能推导已初始化的变量类型。
4.2.1 推导规则
通过上面的例子我们初步感受了一下 decltype 的用法,但不要认为 decltype 就这么简单,在它简单的背后隐藏着很多的细节,下面分三个场景依次讨论一下:
1.表达式为普通变量或者普通表达式或者类表达式,在这种情况下,使用decltype推导出的类型和表达式的类型是一致的。
#include <iostream>
#include <string>
using namespace std;
class Test
{
public:
string text;
static const int value = 110;
};
int main()
{
int x = 99;
const int &y = x;
decltype(x) a = x;
decltype(y) b = x;
decltype(Test::value) c = 0;
Test t;
decltype(t.text) d = "hello, world";
return 0;
}
- 变量a被推导为 int类型
- 变量b被推导为 const int &类型
- 变量c被推导为 const int类型
- 变量d被推导为 string类型
2.表达式是函数调用,使用decltype推导出的类型和函数返回值一致。
class Test{...};
//函数声明
int func_int(); // 返回值为 int
int& func_int_r(); // 返回值为 int&
int&& func_int_rr(); // 返回值为 int&&
const int func_cint(); // 返回值为 const int
const int& func_cint_r(); // 返回值为 const int&
const int&& func_cint_rr(); // 返回值为 const int&&
const Test func_ctest(); // 返回值为 const Test
//decltype类型推导
int n = 100;
decltype(func_int()) a = 0;
decltype(func_int_r()) b = n;
decltype(func_int_rr()) c = 0;
decltype(func_cint()) d = 0;
decltype(func_cint_r()) e = n;
decltype(func_cint_rr()) f = 0;
decltype(func_ctest()) g = Test();
- 变量a被推导为 int类型
- 变量b被推导为 int&类型
- 变量c被推导为 int&&类型
- 变量d被推导为 int类型
- 变量e被推导为 const int &类型
- 变量f被推导为 const int &&类型
- 变量g被推导为 const Test类型
函数 func_cint() 返回的是一个纯右值(在表达式执行结束后不再存在的数据,也就是临时性的数据),对于纯右值而言,只有类类型可以携带const、volatile限定符,除此之外需要忽略掉这两个限定符,因此推导出的变量d的类型为 int 而不是 const int。
3.表达式是一个左值,或者被括号( )包围,使用 decltype推导出的是表达式类型的引用(如果有const、volatile限定符不能忽略)。
#include <iostream>
#include <vector>
using namespace std;
class Test
{
public:
int num;
};
int main() {
const Test obj;
//带有括号的表达式
decltype(obj.num) a = 0;
decltype((obj.num)) b = a;
//加法表达式
int n = 0, m = 0;
decltype(n + m) c = 0;
decltype(n = n + m) d = n;
return 0;
}
- obj.num 为类的成员访问表达式,符合场景1,因此 a 的类型为int
- obj.num 带有括号,符合场景3,因此b 的类型为 const int&。
- n+m 得到一个右值,符合场景1,因此c的类型为 int
- n=n+m 得到一个左值 n,符合场景3,因此d的类型为 int&
4.2.2 decltype的应用
关于decltype的应用多出现在泛型编程中。比如我们编写一个类模板,在里边添加遍历容器的函数,操作如下:
#include <list>
using namespace std;
template <class T>
class Container
{
public:
void func(T& c)
{
for (m_it = c.begin(); m_it != c.end(); ++m_it)
{
cout << *m_it << " ";
}
cout << endl;
}
private:
??? m_it; // 这里不能确定迭代器类型
};
int main()
{
const list<int> lst;
Container<const list<int>> obj;
obj.func(lst);
return 0;
}
在程序的第17行出了问题,关于迭代器变量一共有两种类型:只读(T::const_iterator)和读写(T::iterator),有了decltype就可以完美的解决这个问题了,当 T 是一个 非 const 容器得到一个 T::iterator,当 T 是一个 const 容器时就会得到一个 T::const_iterator。
#include <list>
#include <iostream>
using namespace std;
template <class T>
class Container
{
public:
void func(T& c)
{
for (m_it = c.begin(); m_it != c.end(); ++m_it)
{
cout << *m_it << " ";
}
cout << endl;
}
private:
decltype(T().begin()) m_it; // 这里不能确定迭代器类型
};
int main()
{
const list<int> lst{ 1,2,3,4,5,6,7,8,9 };
Container<const list<int>> obj;
obj.func(lst);
return 0;
}
decltype(T().begin())这种写法在vs2017/vs2019下测试可用完美运行。
4.3 返回类型后置
在泛型编程中,可能需要通过参数的运算来得到返回值的类型,比如下面这个场景:
#include <iostream>
using namespace std;
// R->返回值类型, T->参数1类型, U->参数2类型
template <typename R, typename T, typename U>
R add(T t, U u)
{
return t + u;
}
int main()
{
int x = 520;
double y = 13.14;
// auto z = add<decltype(x + y), int, double>(x, y);
auto z = add<decltype(x + y)>(x, y); // 简化之后的写法
cout << "z: " << z << endl;
return 0;
}
关于返回值,从上面的代码可以推断出和表达式 t+u的结果类型是一样的,因此可以通过通过decltype进行推导,关于模板函数的参数t和u可以通过实参自动推导出来,因此在程序中就也可以不写。虽然通过上述方式问题被解决了,但是解决方案有点过于理想化,因为对于调用者来说,是不知道函数内部执行了什么样的处理动作的。
因此如果要想解决这个问题就得直接在 add 函数身上做文章,先来看第一种写法:
template <typename T, typename U>
decltype(t+u) add(T t, U u)
{
return t + u;
}
当我们在编译器中将这几行代码改出来后就直接报错了,因此decltype中的 t 和 u 都是函数参数,直接这样写相当于变量还没有定义就直接用上了,这时候变量还不存在,有点心急了。
在C++11中增加了返回类型后置语法,说明白一点就是将decltype和auto结合起来完成返回类型的推导。其语法格式如下:
// 符号 -> 后边跟随的是函数返回值的类型
auto func(参数1, 参数2, ...) -> decltype(参数表达式)
通过对上述返回类型后置语法代码的分析,得到结论:auto 会追踪 decltype() 推导出的类型,因此上边的add()函数可以做如下的修改:
#include <iostream>
using namespace std;
template <typename T, typename U>
// 返回类型后置语法
auto add(T t, U u) -> decltype(t+u)
{
return t + u;
}
int main()
{
int x = 520;
double y = 13.14;
// auto z = add<int, double>(x, y);
auto z = add(x, y); // 简化之后的写法
cout << "z: " << z << endl;
return 0;
}
为了进一步说明这个语法,我们再看一个例子:
#include <iostream>
using namespace std;
int& test(int &i)
{
return i;
}
double test(double &d)
{
d = d + 100;
return d;
}
template <typename T>
// 返回类型后置语法
auto myFunc(T& t) -> decltype(test(t))
{
return test(t);
}
int main()
{
int x = 520;
double y = 13.14;
// auto z = myFunc<int>(x);
auto z = myFunc(x); // 简化之后的写法
cout << "z: " << z << endl;
// auto z = myFunc<double>(y);
auto z1 = myFunc(y); // 简化之后的写法
cout << "z1: " << z1 << endl;
return 0;
}
在这个例子中,通过decltype结合返回值后置语法很容易推导出来 test(t)函数可能出现的返回值类型,并将其作用到了函数myFunc()上。
// 输出结果
z: 520
z1: 113.14
5.final和override
5.1 final
C++中增加了final关键字来限制某个类不能被继承,或者某个虚函数不能被重写,和Java的final关键字的功能是类似的。如果使用final修饰函数,只能修饰虚函数,并且要把final关键字放到类或者函数的后面。
5.1.1 修饰函数
如果使用final修饰函数,只能修饰虚函数,这样就能阻止子类重写父类的这个函数了:
class Base
{
public:
virtual void test()
{
cout << "Base class...";
}
};
class Child : public Base
{
public:
void test() final
{
cout << "Child class...";
}
};
class GrandChild : public Child
{
public:
// 语法错误, 不允许重写
void test()
{
cout << "GrandChild class...";
}
};
在上面的代码中一共有三个类:
- 基类:Base
- 子类:Child
- 孙子类:GrandChild
test()是基类中的一个虚函数,在子类中重写了这个方法,但是不希望孙子类中继续重写这个方法了,因此在子类中将test()方法标记为final,孙子类中对这个方法就只有使用的份了。
5.1.2 修饰类
使用final关键字修饰过的类是不允许被继承的,也就是说这个类不能有派生类。
class Base
{
public:
virtual void test()
{
cout << "Base class...";
}
};
class Child final: public Base
{
public:
void test()
{
cout << "Child class...";
}
};
// error, 语法错误
class GrandChild : public Child
{
public:
};
Child类是被final修饰过的,因此Child类不允许有派生类GrandChild类的继承是非法的,Child是个断子绝孙的类。
5.2. override
override关键字确保在派生类中声明的重写函数与基类的虚函数有相同的签名,同时也明确表明将会重写基类的虚函数,这样就可以保证重写的虚函数的正确性,也提高了代码的可读性,和final一样这个关键字要写到方法的后面。使用方法如下:
class Base
{
public:
virtual void test()
{
cout << "Base class...";
}
};
class Child : public Base
{
public:
void test() override
{
cout << "Child class...";
}
};
class GrandChild : public Child
{
public:
void test() override
{
cout << "Child class...";
}
};
上述代码中第13行和第22行就是显示指定了要重写父类的test()方法,使用了override关键字之后,假设在重写过程中因为误操作,写错了函数名或者函数参数或者返回值编译器都会提示语法错误,提高了程序的正确性,降低了出错的概率。
6.using的使用
在C++中using用于声明命名空间,使用命名空间也可以防止命名冲突。在程序中声明了命名空间之后,就可以直接使用命名空间中的定义的类了。在C++11中赋予了using新的功能,让C++变得更年轻,更灵活。
6.1. 定义别名
在 C++中可以通过 typedef 重定义一个类型,语法格式如下:
typedef 旧的类型名 新的类型名;
// 使用举例
typedef unsigned int uint_t;
被重定义的类型并不是一个新的类型,仅仅只是原有的类型取了一个新的名字。和以前的声明语句一样,这里的声明符也可以包含类型修饰,从而也能由基本数据类型构造出复合类型来。C++11中规定了一种新的方法,使用别名声明(alias declaration)来定义类型的别名,即使用using。
在使用的时候,关键字using作为别名声明的开始,其后紧跟别名和等号,其作用是把等号左侧的名字规定成等号右侧类型的别名。类型别名和类型的名字等价,只要是类型的名字能出现的地方,就能使用类型别名。使用typedef定义的别名和使用using定义的别名在语义上是等效的。
使用using定义别名的语法格式是这样的:
using 新的类型 = 旧的类型;
// 使用举例
using uint_t = int;
如果不是特别熟悉函数指针与typedef,第一眼很难看出func_ptr其实是一个别名,其本质是一个函数指针,指向的函数返回类型是int,函数参数有两个分别是int,double类型。
使用using定义函数指针别名的写法看起来就非常直观了,把别名的名字强制分离到了左边,而把别名对应的实际类型放在了右边,比较清晰,可读性比较好。
6.2. 模板的别名
使用typedef重定义类似很方便,但是它有一点限制,比如无法重定义一个模板,比如我们需要一个固定以int类型为key的map,它可以和很多类型的value值进行映射,如果使用typedef这样直接定义就非常麻烦:
typedef map<int, string> m1;
typedef map<int, int> m2;
typedef map<int, double> m3;
在这种情况下我们就不自觉的想到了模板:
template <typename T>
typedef map<int, T> type; // error, 语法错误
使用typename不支持给模板定义别名,这个简单的需求仅通过typedef很难办到,需要添加一个外敷类:
#include <iostream>
#include <functional>
#include <map>
using namespace std;
template <typename T>
// 定义外敷类
struct MyMap
{
typedef map<int, T> type;
};
int main(void)
{
MyMap<string>::type m;
m.insert(make_pair(1, "luffy"));
m.insert(make_pair(2, "ace"));
MyMap<int>::type m1;
m1.insert(1, 100);
m1.insert(2, 200);
return 0;
}
通过上边的例子可以直观的感觉到,需求简单但是实现起来并不容易。在C++11中,新增了一个特性就是可以通过使用using来为一个模板定义别名,对于上面的需求可以写成这样:
template <typename T>
using mymap = map<int, T>;
完整的示例代码如下:
#include <iostream>
#include <functional>
#include <map>
using namespace std;
template <typename T>
using mymap = map<int, T>;
int main(void)
{
// map的value指定为string类型
mymap<string> m;
m.insert(make_pair(1, "luffy"));
m.insert(make_pair(2, "ace"));
// map的value指定为int类型
mymap<int> m1;
m1.insert(1, 100);
m1.insert(2, 200);
return 0;
}
上面的例子中通过使用using给模板指定别名,就可以基于别名非常方便的给value指定相应的类型,这样使编写的程序变得更加灵活,看起来也更加简洁一些。
最后在强调一点:using语法和typedef一样,并不会创建出新的类型,它们只是给某些类型定义了新的别名。using相较于typedef的优势在于定义函数指针别名时看起来更加直观,并且可以给模板定义别名。
标签:11,const,推导,int,auto,特性,C++,类型,decltype From: https://blog.csdn.net/qq_55882840/article/details/139564283