C++11杂项
constexpr
常量表达式主要是允许一些计算发生在编译时,即发生在代码编译而不是运行的时候。
constexpr int multiply (int x, int y)
{
return x * y;
}
// 将在编译时计算
const int val = multiply( 10, 10 );
constexpr可以修饰函数,简单的来说,如果其传入的参数可以在编译时期计算出来,那么这个函数就会产生编译时期的值。但是,传入的参数如果不能在编译时期计算出来,那么constexpr修饰的函数就和普通函数一样了。
constexpr int getDefaultArraySize (int multiplier)
{
return 10 * multiplier;
}
int my_array[ getDefaultArraySize( 3 ) ];
int a = 4; //非常量表达式
getDefaultArraySize(a); //ok
std::array
与 std::vector 不同,std::array 对象的大小是固定的,如果容器大小是固定的,那么可以优先考虑使用 std::array 容器。 另外由于 std::vector 是自动扩容的,当存入大量的数据后,并且对容器进行了删除操作, 容器并不会自动归还被删除元素相应的内存,这时候就需要手动运行 shrink_to_fit() 释放这部分内存。
使用 std::array 能够让代码变得更加“现代化”,而且封装了一些操作函数,比如获取数组大小以及检查是否非空,同时还能够友好的使用标准库中的容器算法,比如 std::sort。
std::array<int, 4> arr = {1, 2, 3, 4};
arr.empty(); // 检查容器是否为空
arr.size(); // 返回容纳的元素数
// 迭代器支持
for (auto &i : arr)
{
// ...
}
// 用 lambda 表达式排序
std::sort(arr.begin(), arr.end(), [](int a, int b) {
return b < a;
});
// 数组大小参数必须是常量表达式
constexpr int len = 4;
std::array<int, len> arr = {1, 2, 3, 4};
auto和for循环
c++11引入了auto类型说明符,auto让编译器通过初始值来推算变量的类型,所以auto定义的变量必须有初始值,可以和范围for循环组合使用。
#include<iostream>
using namespace std;
int main()
{
string s("hello world");
for(auto c:s)
c='t';
cout<<s<<endl;//结果为hello world
for(auto &c:s)
c='t';
cout<<s<<endl; //结果为ttttttttttt
}
nullptr
C语言中,我们使用NULL表示空指针,也就是我们可以写如下代码:
int *i = NULL;
实际上在C语言中,NULL通常被定义为如下:
#define NULL ((void *)0)
也就是说NULL实际上是一个void *的指针,然后吧void *指针赋值给int *指针的时候,隐式转换成相应的类型。而如果换做一个C++编译器来编译的话是要出错的,因为C++是强类型的,void *是不能隐式转换成其他指针类型的,所以通常情况下,编译器提供的头文件会这样定义NULL:
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
但使用0容易出现二义性问题,如下:
void bar(sometype1 a, sometype2 *b);
void bar(sometype1 a, int i);
bar(a, NULL); //调用void bar(sometype1 a, int i);因为C++中NULL其实是0
为了解决这个问题,C++11 引入了 nullptr 关键字,专门用来区分空指针、0。而 nullptr 的类型为 nullptr_t,能够隐式的转换为任何指针或成员指针的类型,也能和他们进行相等或者不等的比较。
委托构造函数
委托构造函数:通过初始化列表调用同一个类的其他构造函数,目的是简化构造函数的书写。
一个委托构造函数也有一个成员初始化列表和一个函数体,成员初始化列表只能包含一个其它构造函数,不能再包含其它成员变量的初始化;被委托的构造函数应该包含较大数量的参数,初始化较多的成员变量,未包含的参数会默认初始化,然后i在函数体内对未包含的参数重新赋值。
class A
{
private:
int a;
int b;
char c;
char d;
public:
A(int num0,int num1,char C):a(num0),b(num1),c(C){}
A(int num0,char C):A(num0,0,C){}//b默认初始化为0
A(int num0):A(num0,'p'){b=1;}//b重新赋值为1
void getMembers()
{
cout<<a<<" "<<b<<" "<<c<<" "<<d<<endl;
}
};
=default 和 =delete
=default
C++ 的类有四类特殊的成员函数,分别为:默认构造函数,析构函数,拷贝函数以及拷贝赋值函数。如果程序没有显式地为一个类定义某个特殊成员函数,而又需要用到该特殊成员函数时,编译器会隐式地为这个类生成一个默认的特殊成员函数。
但如果X 显式的自定义了非默认构造函数,编译器便不会生成默认构造函数,需要自定义默认构造函数。
但手动编写存在两个问题:1. 程序员工作量变大 2. 没有编译器自动生成的默认特殊构造函数效率高。
为了解决上述的两个问题,C++ 11标准引入了一个新特性:defaulted 函数。
defaulted 函数特性仅用于类的特殊成员函数,既可以在类体里定义(inline),也可以在类体外(out-of-line)定义。
```cpp
class X {
public:
X() = default; // inline
X(const X&); //
X& operator = (const X&);
~X() = default; // inline
}
X::X(const X&) = default; // out of line
X& X::operator = (const X&) = default; // out of line
```
=delete
为了显式的禁用某个函数,C++11 标准引入了一个新特性:deleted 函数。
```cpp
class X{
public:
X();
X(const X&) = delete; // 声明拷贝构造函数为 deleted 函数
X& operator = (const X &) = delete; // 声明拷贝赋值操作符为 deleted 函数
};
class Y{
public:
// 禁止使用者在堆上申请对象
void *operator new(size_t) = delete;
void *operator new[](size_t) = delete;
};
```
- 必须在函数第一次声明的时候将其声明为 deleted 函数
- 不同于default,delete没有限制为特殊成员函数才能使用delete
显式类型转换操作符
隐式类型转换是C++的一个既好又坏的特性。它给人以方便,但可能造成一些十分隐晦的错误。为了防止这样的异常情况,C++11引入了显式的类型转换运算符,用explicit修饰的类型转换运算符,则相应的类型转换必须显式地进行。
class X
{
public:
explicit operator int() const noexcept
{
return 42;
}
};
void Func(int) {}
int wmain()
{
X x0;
Func(x0); // 错误,不存在从 X 到 int 的(隐式)转换
int y = x0; // 错误,不存在从 X 到 int 的(隐式)转换
Func((int)x0); // 正确1
Func(int(x0)); // 正确2
Func(static_cast<int>(x0)); // 正确3
return 0;
}
但是,显式的类型转换有一个例外。如果表达式被用作条件,那么显式的operator bool()也可以隐式地进行(仅限转换到bool)。“被用作条件”即:
-
if、while及do语句的条件部分;
-
for语句头的条件表达式;
-
逻辑非运算符(!)、逻辑或运算符(||)、逻辑与运算符(&&)的运算对象;
-
条件运算符(x ? y : z)的条件表达式。
class K
{
public:
explicit operator bool() const noexcept
{
return false;
}
};
int wmain()
{
K k0;
if (k0) // 正确
{
std::cout << "qwer" << std::endl;
}
else
{
std::cout << "zxcv" << std::endl;
}
return 0;
}
模板元编程
元编程是借助语言提供的模板 (template) 机制,通过编译器推导,计算出运行时 (runtime) 需要的常数、类型、代码的方法。
示例,将对10的阶乘的计算提前到编译期:
#include <iostream>
using namespace std;
template <unsigned int n>
struct TMPFactorial {
enum { value = n * TMPFactorial<n-1>::value };
};
template<>
struct TMPFactorial<0> {
enum { value = 1 };
};
int main()
{
cout<<TMPFactorial<10>::value<<endl;
}
模板别名
在C++98/03里,我们可以通过typedef 关键字定义一个类型的别名。
typedef unsigned int uint_t;
考虑下面例子,如果MAP的key值始终是不变的,我们是否像下面一样可以用typedef+模板来定义一个别名呢?
template <typename T>
typedef std::map<std::string, T> map;
map<int> map_i;
map<std::string> map_str;
遗憾的是上述的定义不能通过编译,也就是C++ 98/03并不支持这样的操作,通常是通过一个包裹类的方式来实现上述的需求:
template <typename T>
struct alias_map
{
typedef std::map<std::string, T> map;
};
alias_map<int>::map map_t;
alias_map<int>::map map_str;
C++11中,新增了一个特性就是可以通过使用using来为一个模板定义别名,比如说上述的需求,使用C++11就可以这样实现:
template <typename T>
using alias_map = std::map < std::string, T > ;
alias_map<int> map_t;
alias_map<std::string> map_str;
实际上using包含了typedef的所有功能,来看下使用using关键字和typedef关键字定义普通类型别名的用法。
typedef unsigned int uint_t;
using uint_t = unsigned int;
typedef std::map<std::string, int> map_t;
using map_t = std::map < std::string, int > ;
typedef void(*func)(int, int);
using func = void(*)(int, int);
decltype关键字
typeid用于程序运行时推导数据类型,在泛型编程中,我们更需要的是编译时就要确定类型,RTTI并无法满足这样的要求。
decltype与auto关键字一样,用于进行编译时类型推导,不过它与auto还是有一些区别的。decltype的类型推导并不是像auto一样是从变量声明的初始化表达式获得变量的类型,而是总是以一个普通表达式作为参数,返回该表达式的类型,而且decltype并不会对表达式进行求值。
decltype用法
-
推导出表达式类型
int i = 4; decltype(i) a; //推导结果为int。a的类型为int。
-
与using/typedef合用,用于定义类型。
vector<int >vec; typedef decltype(vec.begin()) vectype; for (vectype i = vec.begin; i != vec.end(); i++) { //... }
-
重用匿名类型
struct { int d ; doubel b; }anon_s; decltype(anon_s) as ;//定义了一个上面匿名的结构体
-
泛型编程中结合auto,用于追踪函数的返回值类型
template <typename _Tx, typename _Ty> //编译器只会从左到右读入符号,只有推导出了_Tx和_Ty的类型,decltype(_Tx*_Ty)才能推导出_Tx*_Ty的类型 //所以decltype(_Tx*_Ty)必须放函数后面 auto multiply(_Tx x, _Ty y)->decltype(_Tx*_Ty) { return x*y; }
decltype推导四规则
- 如果e是一个没有带括号的标记符表达式或者类成员访问表达式,那么的decltype(e)就是e所命名的实体的类型。此外,如果e是一个被重载的函数,则会导致编译错误。
- 否则 ,假设e的类型是T,如果e是一个将亡值,那么decltype(e)为T&&
- 否则,假设e的类型是T,如果e是一个左值,那么decltype(e)为T&。
- 否则,假设e的类型是T,则decltype(e)为T。
noexcept
C++11 将异常的声明简化为以下两种情况:
- 函数可能抛出任何异常
- 函数不能抛出任何异常
并使用 noexcept 对这两种行为进行限制,例如:
void may_throw(); // 可能抛出异常
void no_throw() noexcept; // 不可能抛出异常
使用 noexcept 修饰过的函数如果抛出异常,编译器会使用 std::terminate() 来立即终止程序运行。
noexcept 还能够做操作符,用于操作一个表达式,当表达式无异常时,返回 true,否则返回 false。
#include <iostream>
void may_throw() {
throw true;
}
auto non_block_throw = []{
may_throw();
};
void no_throw() noexcept {
return;
}
auto block_throw = []() noexcept {
no_throw();
};
int main()
{
std::cout << std::boolalpha
<< "may_throw() noexcept? " << noexcept(may_throw()) << std::endl
<< "no_throw() noexcept? " << noexcept(no_throw()) << std::endl
<< "lmay_throw() noexcept? " << noexcept(non_block_throw()) << std::endl
<< "lno_throw() noexcept? " << noexcept(block_throw()) << std::endl;
return 0;
}
字面量
C++11 提供了原始字符串字面量的写法,可以在一个字符串前方使用 R 来修饰这个字符串, 同时,将原始字符串使用括号包裹,例如:
#include <iostream>
#include <string>
int main() {
std::string str = R"(C:\File\To\Path)";
std::cout << str << std::endl;
return 0;
}
stringstream使用示例
stringstream通常是用来做数据转换的,相比c库的转换,它更加安全,自动和直接。
#include <iostream>
#include <string>
#include <sstream>
using namespace std;
int main()
{
stringstream ss;
string s;
int i = 1000;
ss << i;
ss >> s;
cout << s << endl;
return 0;
}
注意事项:
1 . 如果你打算在多次转换中使用同一个stringstream对象,记住在每次转换前要使用stream.clear()方法。
2 . stream.clear() 这个名字让人想当然地认为它会清除流的内容,实际上它并不清空任何内容,它只是重置了流的状态标志。 清除缓冲需要用 stream.str("") 。
to_string()函数
#include <iostream>
#include <string>
int main()
{
double f = 23.43;
std::string f_str = std::to_string(f);
std::cout << f_str << '\n';
}
c++ sort函数
sort(begin,end,cmp)
需要头文件;
该函数可以给数组,或者链表list、向量排序;
cmp参数可以没有,如果没有默认升序。
自己编写排序规则函数,例如:
bool compare(int& a,int& b)
{
return a<b; //升序排列,如果改为return a>b,则为降序
}
实现原理:sort并不是简单的快速排序,它对普通的快速排序进行了优化,此外,它还结合了插入排序和推排序。系统会根据你的数据形式和数据量自动选择合适的排序方法,这并不是说它每次排序只选择一种方法,它是在一次完整排序中不同的情况选用不同方法,比如给一个数据量较大的数组排序,开始采用快速排序,分段递归,分段之后每一段的数据量达到一个较小值后它就不继续往下递归,而是选择插入排序,如果递归的太深,他会选择推排序。
类的非静态常量成员必须在类外初始化,且不能使用static修饰
c++规定const静态类成员可以直接初始化,其他非const的静态类成员需要在类外初始化,且不能使用static关键字。
那么为什么要这样规定?
C++的静态成员成员变量在类中仅仅是声明,没有定义。
#include <stdio.h>
class A {
public:
static int a; //声明但未定义
};
int A::a = 3; //定义了静态成员变量,同时初始化。也可以写"int A:a;",即不给初值,同样可以通过编译
int main() {
printf("%d", A::a);
return 0;
}
静态成员是单独存储的,并不是对象的组成部分。如果在类的内部进行定义,在建立多个对象时会多次声明和定义该变量的存储位置。 static const int可以在类里面初始化,是因为它既然是const的,那程序就不会再去试图初始化了。
为了避免与一般静态变量或对象相混淆,在类外初始化,不能使用static关键字。
char范围为什么是-128到127
计算机用最高位,1代表负数,0代表正数,以8位来表示就是 0xxxxxxxx 的第一个0代表正数, 1xxxxxxxx的第一个1代表负数 ,那么8位的第一个位就不能算,也就是说只有7位, 即2^7=128 个数字,一共正负各128种状态,如果不采用特殊处理,这时候0占用2个编码(10000000和00000000),数据表示范围为-127到-0及+0到127,这样总体上一个字节只有255种状态,因为其中0具有正0和负0之分,这不符合数学意义也浪费一个编码(早期硬件很昂贵,一位或者一个编码的浪费都是不可饶恕的)。所以计算机规定,如果是正0,则表示0,如果是负0,则表示-128
INT_MAX和INT_MIN
INT_MIN在标准头文件limits.h中定义。
#define INT_MAX 2147483647
#define INT_MIN (-INT_MAX - 1)
指针与引用的区别
从编译的角度来阐述它们之间的区别:
程序在编译时分别将指针和引用添加到符号表上,符号表上记录的是变量名及变量所对应地址。指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值。符号表生成后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。
RTTI
RTTI(Run Time Type Identification)即通过运行时类型识别,程序能够使用基类的指针或引用来检查着这些指针或引用所指的对象的实际派生类型。
RTTI提供了以下两个非常有用的操作符:
- typeid操作符,返回指针和引用所指的实际类型。
- dynamic_cast操作符,将基类类型的指针或引用安全地转换为派生类型的指针或引用。
RAII
RAII (Resource Acquisition Is Initialization),也称为“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的惯用法。
简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。
map和unordered_map的区别
map: map内部实现了一个红黑树,红黑树具有自动排序的功能,因此map内部的所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找,删除,添加等一系列的操作都相当于是对红黑树进行的操作。map中的元素是按照二叉搜索树(又名二叉查找树、二叉排序树,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值)存储的,使用中序遍历可将键值按照从小到大遍历出来。
unordered_map: unordered_map内部实现了一个哈希表(也叫散列表,通过把关键码值映射到Hash表中一个位置来访问记录,查找的时间复杂度可达到O(1))。因此,其元素的排列顺序是无序的。