文章目录
auto类型
int x = 0;
auto *p1 = &x; //p1 为 int *,auto 推导为 int
auto p2 = &x; //p2 为 int*,auto 推导为 int*
auto &r1 = x; //r1 为 int&,auto 推导为 int
auto r2 = r1; //r2 为 int,auto 推导为 int
auto 的限制
- auto 不能在函数的参数中使用
- 不能作用于类的非静态成员变量(也就是没有 static 关键字修饰的成员变量)中
- 不能定义数组
- 不能作用于模板参数
auto 的应用
vector< vector<int> > v;
auto i = v.begin(); //使用 auto 代替具体的类型
#include <iostream>
using namespace std;
class A{
public:
static int get(void){
return 100;
}
};
class B{
public:
static const char* get(void){
return "http://c.biancheng.net/cplus/";
}
};
template <typename T>
void func(void){
auto val = T::get();
cout << val << endl;
}
int main(void){
func<A>();
func<B>();
return 0;
}
decltype类型推导
在编译时期进行自动类型推导
decltype 是“declare type”的缩写,译为“声明类型”
decltype 可以写成下面的形式:
decltype(exp) varname;
decltype 推导规则
- 如果 exp 是一个不被括号( )包围的表达式,或者是一个类成员访问表达式,或者是一个单独的变量,那么 decltype(exp) 的类型就和 exp 一致
int a = 0;
decltype(a) b = 1; //b 被推导成了 int
decltype(10.8) x = 5.5; //x 被推导成了 double
decltype(x + 100) y; //y 被推导成了 double
- 如果 exp 是函数调用,那么 decltype(exp) 的类型就和函数返回值的类型一致
//函数声明
int& func_int_r(int, char); //返回值为 int&
int&& func_int_rr(void); //返回值为 int&&
int func_int(double); //返回值为 int
const int& fun_cint_r(int, int, int); //返回值为 const int&
const int&& func_cint_rr(void); //返回值为 const int&&
//decltype类型推导
int n = 100;
decltype(func_int_r(100, 'A')) a = n; //a 的类型为 int&
decltype(func_int_rr()) b = 0; //b 的类型为 int&&
decltype(func_int(10.5)) c = 0; //c 的类型为 int
decltype(fun_cint_r(1,2,3)) x = n; //x 的类型为 const int &
decltype(func_cint_rr()) y = 0; // y 的类型为 const int&&
- 如果exp是一个左值,或者被括号( )包围,那么 decltype(exp) 的类型就是 exp 的引用;假设 exp 的类型为 T,那么 decltype(exp) 的类型就是 T&
using namespace std;
class Base{
public:
int x;
};
int main(){
const Base obj;
//带有括号的表达式
decltype(obj.x) a = 0; //obj.x 为类的成员访问表达式,符合推导规则一,a 的类型为 int
decltype((obj.x)) b = a; //obj.x 带有括号,符合推导规则三,b 的类型为 int&。
//加法表达式
int n = 0, m = 0;
decltype(n + m) c = 0; //n+m 得到一个右值,符合推导规则一,所以推导结果为 int
decltype(n = n + m) d = c; //n=n+m 得到一个左值,符号推导规则三,所以推导结果为 int&
return 0;
}
应用
#include <vector>
using namespace std;
template <typename T>
class Base {
public:
void func(T& container) {
m_it = container.begin();
}
private:
typename T::iterator m_it; //注意这里
};
int main()
{
const vector<int> v;
Base<const vector<int>> obj;
obj.func(v);
return 0;
}
报错:
T::iterator并不能包括所有的迭代器类型,当 T 是一个 const 容器时,应当使用 const_iterator
优化策略如下:
template <typename T>
class Base {
public:
void func(T& container) {
m_it = container.begin();
}
private:
decltype(T().begin()) m_it; //注意这里
};
auto和decltype的区别
- 语法格式
auto varname = value; //auto的语法格式
decltype(exp) varname [= value]; //decltype的语法格式
auto | decltype |
---|---|
根据=右边的初始值 value 推导出变量的类型 | 根据 exp 表达式推导出变量的类型,跟=右边的 value 没有关系 |
变量必须初始化 | 初始化与否都不影响变量的类型 |
- 对 cv 限定符的处理(const 和 volatile )
decltype 会保留 cv 限定符,而 auto 有可能会去掉 cv 限定符
- 如果表达式的类型不是指针或者引用,auto 会把 cv 限定符直接抛弃,推导成 non-const 或者 non-volatile 类型。
- 如果表达式的类型是指针或者引用,auto 将保留 cv 限定符。
非指针非引用类型
const int n1 = 0;
auto n2 = n1;
n2 = 99; //赋值不报错
decltype(n1) n3 = 20;
n3 = 5; //赋值报错
指针类型
const int *p1 = &n1;
auto p2 = p1;
*p2 = 66; //赋值报错
decltype(p1) p3 = p1;
*p3 = 19; //赋值报错
引用
int n = 10;
int &r1 = n;
//auto推导
auto r2 = r1;
r2 = 20;
cout << n << ", " << r1 << ", " << r2 << endl;
// 10, 10, 20
//decltype推导
decltype(r1) r3 = n;
r3 = 99;
cout << n << ", " << r1 << ", " << r3 << endl;
// 99, 99, 99
返回值类型后置(跟踪返回值类型)
考虑下面这个场景:
template <typename R, typename T, typename U>
R add(T t, U u)
{
return t+u;
}
int a = 1; float b = 2.0;
auto c = add<decltype(a + b)>(a, b);
使用新的语法可以写成
template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u)
{
return t + u;
}
using定义别名(替代typedef)
// 重定义unsigned int
typedef unsigned int uint_t;
using uint_t = unsigned int;
// 重定义std::map
typedef std::map<std::string, int> map_int_t;
using map_int_t = std::map<std::string, int>;
template <typename T>
using func_t = void (*)(T, T);
// 使用 func_t 模板
func_t<int> xx_2;
using 重定义的 func_t 是一个模板,但 func_t 定义的 xx_2 并不是一个由类模板实例化后的类,而是 void(*)(int, int) 的别名,模板别名(alias template)
支持函数模板的默认模板参数
template <typename R = int, typename U>
R func(U val)
{
return val;
}
int main()
{
func(97); // R=int, U=int
func<char>(97); // R=char, U=int
func<double, int>(97); // R=double, U=int
return 0;
}
- 当默认模板参数和自行推导的模板参数同时使用时,若无法推导出函数模板参数的类型,编译器会选择使用默认模板参数;
- 如果模板参数即无法推导出来,又未设置其默认值,则编译器直接报错
template <typename T, typename U = double>
void func(T val1 = 0, U val2 = 0)
{
//...
}
int main()
{
func('c'); //T=char, U=double
func(); //编译报错
return 0;
}
在函数模板和类模板中使用可变参数
可变参数,指的是参数的个数和类型都可以是任意的
C++ 一直都支持为函数设置可变参数,最典型的代表就是 printf() 函数,它的语法格式为:
int printf ( const char * format, ... );
…就表示的是可变参数,即 printf() 函数可以接收任意个参数,且各个参数的类型可以不同
使用 … 可变参数的过程中,需注意以下几点:
- … 可变参数必须作为函数的最后一个参数,且一个函数最多只能拥有 1 个可变参数。
- 可变参数的前面至少要有 1 个有名参数
- 当可变参数中包含 char 类型的参数时,va_arg 宏要以 int 类型的方式读取;当可变参数中包含 short 类型的参数时,va_arg 宏要以 double 类型的方式读取
可变参数模板
如下定义了一个可变参数的函数模板:
template<typename... T>
void vair_fun(T...args) {
//函数体
}
vair_fun() 函数中,args 参数的类型用 T… 表示,表示 args 参数可以接收任意个参数,又称函数参数包。
使用可变参数模板的难点在于,如何在模板函数内部“解开”参数包(使用包内的数据)
递归
//模板函数递归的出口
void vir_fun() {
}
template <typename T, typename... args>
void vir_fun(T argc, args... argv)
{
cout << argc << endl;
//开始递归,将第一个参数外的 argv 参数包重新传递给 vir_fun
vir_fun(argv...);
}
借助逗号表达式和初始化列表
template <typename T>
void dispaly(T t) {
cout << t << endl;
}
template <typename... args>
void vir_fun(args... argv)
{
//逗号表达式+初始化列表
int arr[] = { (dispaly(argv),0)... };
}
int main()
{
vir_fun(1, "http://www.biancheng.net", 2.34);
return 0;
}
可变参数类模板
#include <iostream>
//声明模板类demo
template<typename... Values> class demo;
//继承式递归的出口
template<> class demo<> {};
//以继承的方式解包
template<typename Head, typename... Tail>
class demo<Head, Tail...>
: private demo<Tail...>
{
public:
demo(Head v, Tail... vtail) : m_head(v), demo<Tail...>(vtail...) {
dis_head();
}
void dis_head() { std::cout << m_head << std::endl; }
protected:
Head m_head;
};
int main() {
demo<int, float, std::string> t(1, 2.34, "http://www.biancheng.net");
return 0;
}
tuple元组
实例化的对象可以存储任意数量、任意类型的数据。
本质是一个以可变模板参数定义的类模板
#include <iostream> // std::cout
#include <tuple> // std::tuple
int main()
{
std::tuple<int, char> first; // 1) first{}
std::tuple<int, char> second(first); // 2) second{}
std::tuple<int, char> third(std::make_tuple(20, 'b')); // 3) third{20,'b'}
std::tuple<long, char> fourth(third); // 4)的左值方式, fourth{20,'b'}
std::tuple<int, char> fifth(10, 'a'); // 5)的右值方式, fifth{10.'a'}
std::tuple<int, char> sixth(std::make_pair(30, 'c')); // 6)的右值方式, sixth{30,''c}
auto test = std::make_tuple (10,'a');
return 0;
}
常用函数示例:
#include <iostream>
#include <tuple>
int main()
{
int size;
//创建一个 tuple 对象存储 10 和 'x'
std::tuple<int, char> mytuple(10, 'x');
//计算 mytuple 存储元素的个数
size = std::tuple_size<decltype(mytuple)>::value;
//输出 mytuple 中存储的元素
std::cout << std::get<0>(mytuple) << " " << std::get<1>(mytuple) << std::endl;
//修改指定的元素
std::get<0>(mytuple) = 100;
std::cout << std::get<0>(mytuple) << std::endl;
//使用 makde_tuple() 创建一个 tuple 对象
auto bar = std::make_tuple("test", 3.1, 14);
//拆解 bar 对象,分别赋值给 mystr、mydou、myint
const char* mystr = nullptr;
double mydou;
int myint;
//使用 tie() 时,如果不想接受某个元素的值,实参可以用 std::ignore 代替
std::tie(mystr, mydou, myint) = bar;
//std::tie(std::ignore, std::ignore, myint) = bar; //只接收第 3 个整形值
//将 mytuple 和 bar 中的元素整合到 1 个 tuple 对象中
auto mycat = std::tuple_cat(mytuple, bar);
size = std::tuple_size<decltype(mycat)>::value;
std::cout << size << std::endl;
return 0;
}
程序执行结果为:
10 x
100
5
列表初始化
int* a = new int { 123 };
double b = double { 12.12 };
int* arr = new int[3] { 1, 2, 3 };
int a6 { 3 };
lambda匿名函数
语法格式如下:
[外部变量访问方式说明符] (参数) mutable noexcept/throw() -> 返回值类型
{
函数体;
};
- [外部变量方位方式说明符]
[ ] 方括号用于向编译器表明当前是一个 lambda 表达式,其不能被省略。在方括号内部,可以注明当前 lambda 函数的函数体中可以使用哪些“外部变量”。 - (参数)
lambda 匿名函数也可以接收外部传递的多个参数。如果不需要传递参数,可以省略() - mutable
此关键字可以省略,如果使用则之前的 () 小括号将不能省略(参数个数可以为 0)。默认情况下,对于以值传递方式引入的外部变量,不允许在 lambda 表达式内部修改它们的值(可以理解为这部分变量都是 const 常量)。而如果想修改它们,就必须使用 mutable 关键字。
对于以值传递方式引入的外部变量,lambda 表达式修改的是拷贝的那一份,并不会修改真正的外部变量;
- noexcept/throw()
可以省略,如果使用,在之前的 () 小括号将不能省略(参数个数可以为 0)。默认情况下,lambda 函数的函数体中可以抛出任何类型的异常。而标注 noexcept 关键字,则表示函数体内不会抛出任何异常;使用 throw() 可以指定 lambda 函数内部可以抛出的异常类型。
如果 lambda 函数标有 noexcept 而函数体内抛出了异常,又或者使用 throw() 限定了异常类型而函数体内抛出了非指定类型的异常,这些异常无法使用 try-catch 捕获,会导致程序执行失败
- -> 返回值类型
指明 lambda 匿名函数的返回值类型。如果 lambda 函数体内只有一个 return 语句,或者该函数返回 void,则编译器可以自行推断出返回值类型,此情况下可以直接省略-> 返回值类型
lambda匿名函数中的[外部变量]
注意,单个外部变量不允许以相同的传递方式导入多次。例如 [=,val1] 中,val1 先后被以值传递的方式导入了 2 次,这是非法的。
#include <iostream>
using namespace std;
int main()
{
//display 即为 lambda 匿名函数的函数名
auto display = [](int a,int b) -> void{cout << a << " " << b;};
//调用 lambda 函数
display(10,20);
return 0;
}
auto lambda1 = [=]() mutable{
num_1 = 10;
num_2 = 20;
num_3 = 30;
//函数体内只能使用外部变量,而无法对它们进行修改
cout << num_1 << " "
<< num_2 << " "
<< num_3 << endl;
};
程序崩溃,捕获类型错误
#include <iostream>
using namespace std;
int main()
{
auto except1 = []()noexcept{
throw 100;
};
auto except2 = []()throw(char){
throw 10;
};
try{
except1();
except2();
}catch(int){
cout << "捕获到了整形异常"<< endl;
}
return 0;
}
如果不使用 noexcept 或者 throw(),则 lambda 匿名函数的函数体中允许发生任何类型的异常。
非受限联合体(union)
在一个联合体内,我们可以定义多个不同类型的成员,这些成员将会共享同一块内存空间
C++11 标准规定,任何非引用类型都可以成为联合体的数据成员,这种联合体也被称为非受限联合体。
允许非 POD 类型
POD 是英文 Plain Old Data 的缩写,用来描述一个类型的属性。
POD 类型一般具有以下几种特征(包括 class、union 和 struct等):
- 没有用户自定义的构造函数、析构函数、拷贝构造函数和移动构造函数
- 不能包含虚函数和虚基类
- 非静态成员必须声明为 public
- 类中的第一个非静态成员的类型与其基类不同
class B1{};
class B2 : B1 { B1 b; };
// class B2 的第一个非静态成员 b 是基类类型,所以它不是 POD 类型。
- 在类或者结构体继承时,满足以下两种情况之一:
- 派生类中有非静态成员,且只有一个仅包含静态成员的基类
- 基类有非静态成员,而派生类没有非静态成员
class B1 { static int n; };
class B2 : B1 { int n1; };
class B3 : B2 { static int n2; };
// 派生类 B2 中有非静态成员,且只有一个仅包含静态成员的基类 B1,所以它是 POD 类型
// 基类 B2 有非静态成员,而派生类 B3 没有非静态成员,所以它也是 POD 类型
- 所有非静态数据成员均和其基类也符合上述规则(递归定义),也就是说 POD 类型不能包含非 POD 类型的数据
- 所有兼容C语言的数据类型都是 POD 类型(struct、union 等不能违背上述规则)
允许联合体有静态成员
union U {
static int func() {
int n = 3;
return n;
}
};
静态成员变量只能在联合体内定义
非受限联合体的赋值注意事项
如果非受限联合体内有一个非 POD 的成员,而该成员拥有自定义的构造函数,那么这个非受限联合体的默认构造函数将被编译器删除;其他的特殊成员函数,例如默认拷贝构造函数、拷贝赋值操作符以及析构函数等,也将被删除
#include <string>
using namespace std;
union U {
string s;
int n;
};
int main() {
U u; // 构造失败,因为 U 的构造函数被删除
return 0;
}
string 类拥有自定义的构造函数,所以 U 的构造函数被删除;定义 U 的类型变量 u 需要调用默认构造函数,所以 u 也就无法定义成功
解决方案如下:
union U {
string s;
int n;
public:
U() { new(&s) string; } // 采用 placement new
~U() { s.~string(); }
};
placement new
placement new 的语法格式如下:
new(address) ClassConstruct(…)
address 表示已有内存的地址,该内存可以在栈上,也可以在堆上;ClassConstruct(…) 表示调用类的构造函数,如果构造函数没有参数,也可以省略括号。
非受限联合体的匿名声明和“枚举式类”
#include <cstring>
using namespace std;
class Student{
public:
Student(bool g, int a): gender(g), age(a){}
bool gender;
int age;
};
class Singer {
public:
enum Type { STUDENT, NATIVE, FOREIGENR };
Singer(bool g, int a) : s(g, a) { t = STUDENT; }
Singer(int i) : id(i) { t = NATIVE; }
Singer(const char* n, int s) {
int size = (s > 9) ? 9 : s;
memcpy(name , n, size);
name[s] = '\0';
t = FOREIGENR;
}
~Singer(){}
private:
Type t;
union {
Student s;
int id;
char name[10];
};
};
int main() {
Singer(true, 13);
Singer(310217);
Singer("J Michael", 9);
return 0;
}
代码中使用了一个匿名非受限联合体,它作为类 Singer 的“变长成员”来使用,这样的变长成员给类的编写带来了更大的灵活性
for循环
C++ 11 标准中,为 for 循环添加了一种全新的语法格式,如下所示:
for (declaration : expression){
//循环体
}
其中,两个参数各自的含义如下:
declaration:表示此处要定义一个变量,该变量的类型为要遍历序列中存储元素的类型。需要注意的是,C++ 11 标准中,declaration参数处定义的变量类型可以用 auto 关键字表示,该关键字可以使编译器自行推导该变量的数据类型。
expression:表示要遍历的序列,常见的可以为事先定义好的普通数组或者容器,还可以是用 {} 大括号初始化的序列。
char arc[] = "http://c.biancheng.net/cplus/11/";
for (char ch : arc)
cout << ch;
// 新语法格式的 for 循环还支持遍历用{ }大括号初始化的列表
for (int num : {1, 2, 3, 4, 5})
cout << num << " ";
注意事项
基于范围的 for 循环完成对容器的遍历,其底层也是借助容器的迭代器实现的
std::vector<int>arr = { 1, 2, 3, 4, 5 };
for (auto val : arr)
{
std::cout << val << std::endl;
arr.push_back(10); //向容器中添加元素
}
// 1 -572662307 -572662307 4 5(输出结果不唯一)
因为在 for 循环遍历 arr 容器的同时向该容器尾部添加了新的元素(对 arr 容器进行了扩增),致使遍历容器所使用的迭代器失效,整个遍历过程出现错误。
constexpr:验证是否为常量表达式
常量表达式的计算往往发生在程序的编译阶段,这可以极大提高程序的执行效率,因为表达式只需要在编译阶段计算一次,节省了每次程序运行时都需要计算一次的时间
constexpr 可用于修饰普通变量、函数(包括模板函数)以及类的构造函数
constexpr修饰普通变量
constexpr int num = 1 + 2 + 3;
int url[num] = {1,2,3,4,5,6};
couts<< url[1] << endl;
当常量表达式中包含浮点数时,考虑到程序编译和运行所在的系统环境可能不同,常量表达式在编译阶段和运行阶段计算出的结果精度很可能会受到影响,因此 C++11 标准规定,浮点常量表达式在编译阶段计算的精度要至少等于(或者高于)运行阶段计算出的精度
constexpr修饰函数
constexpr 还可以用于修饰函数的返回值,这样的函数又称为“常量表达式函数”
一个函数要想成为常量表达式函数,必须满足如下 4 个条件:
- 整个函数中,除了包含 using 指令、typedef 语句以及 static_assert 断言外,只能包含一条 return 返回语句。
constexpr int display(int x)
{
//可以添加 using 执行、typedef 语句以及 static_assert 断言
return 1 + 2 + x;
}
- 该函数必须有返回值,即函数的返回值类型不能是 void。
- 函数在使用之前,必须有对应的定义语句
#include <iostream>
using namespace std;
//普通函数的声明
int noconst_dis(int x);
//常量表达式函数的声明
constexpr int display(int x);
//常量表达式函数的定义
constexpr int display(int x){
return 1 + 2 + x;
}
int main()
{
//调用常量表达式函数
int a[display(3)] = { 1,2,3,4 };
cout << a[2] << endl;
//调用普通函数
cout << noconst_dis(3) << endl;
return 0;
}
//普通函数的定义
int noconst_dis(int x) {
return 1 + 2 + x;
}
- return 返回的表达式必须是常量表达式
#include <iostream>
using namespace std;
int num = 3;
constexpr int display(int x){
return num + x;
}
int main()
{
//调用常量表达式函数
int a[display(3)] = { 1,2,3,4 };
return 0;
}
编译器报“display(3) 的结果不是常量”的异常
在常量表达式函数的 return 语句中,不能包含赋值的操作(例如 return x=1 在常量表达式函数中不允许的)。另外,用 constexpr 修改函数时,函数本身也是支持递归的
constexpr修饰类的构造函数
对于 C++ 内置类型的数据,可以直接用 constexpr 修饰,但如果是自定义的数据类型(用 struct 或者 class 实现),直接用 constexpr 修饰是不行的
当我们想自定义一个可产生常量的类型时,正确的做法是在该类型的内部添加一个常量构造函数
#include <iostream>
using namespace std;
//自定义类型的定义
struct myType {
constexpr myType(char *name,int age):name(name),age(age){};
const char* name;
int age;
//其它结构体成员
};
int main()
{
constexpr struct myType mt { "zhangsan", 10 };
cout << mt.name << " " << mt.age << endl;
return 0;
}
// zhangsan 10
#include <iostream>
using namespace std;
//自定义类型的定义
class myType {
public:
constexpr myType(const char *name,int age):name(name),age(age){};
constexpr const char * getname(){
return name;
}
constexpr int getage(){
return age;
}
private:
const char* name;
int age;
//其它结构体成员
};
int main()
{
constexpr struct myType mt { "zhangsan", 10 };
constexpr const char * name = mt.getname();
constexpr int age = mt.getage();
cout << name << " " << age << endl;
return 0;
}
不支持用 constexpr 修饰带有 virtual 的成员方法
constexpr修饰模板函数
如果 constexpr 修饰的模板函数实例化结果不满足常量表达式函数的要求,则 constexpr 会被自动忽略,即该函数就等同于一个普通函数
#include <iostream>
using namespace std;
//自定义类型的定义
struct myType {
const char* name;
int age;
//其它结构体成员
};
//模板函数
template<typename T>
constexpr T dispaly(T t){
return t;
}
int main()
{
struct myType stu{"zhangsan",10};
//普通函数
struct myType ret = dispaly(stu);
cout << ret.name << " " << ret.age << endl;
//常量表达式函数
constexpr int ret1 = dispaly(10);
cout << ret1 << endl;
return 0;
}
// zhangsan 10
// 10
- 模板函数中以自定义结构体 myType 类型进行实例化时,由于该结构体中没有定义常量表达式构造函数,所以实例化后的函数不是常量表达式函数,此时 constexpr 是无效的;
- 模板函数的类型 T 为 int 类型,实例化后的函数符合常量表达式函数的要求,所以该函数的返回值就是一个常量表达式。
constexpr和const的区别
#include <iostream>
#include <array>
using namespace std;
void dis_1(const int x){
//错误,x是只读的变量
array <int,x> myarr{1,2,3,4,5};
cout << myarr[1] << endl;
}
void dis_2(){
constexpr int x = 5;
array <int,x> myarr{1,2,3,4,5};
cout << myarr[1] << endl;
}
int main()
{
dis_1(5);
dis_2();
}
dis_1() 函数中的“const int x”只是想强调 x 是一个只读的变量,其本质仍为变量,无法用来初始化 array 容器;
dis_2() 函数中的“const int x”,表明 x 是一个只读变量的同时,x 还是一个值为 5 的常量,所以可以用来初始化 array 容器
凡是表达“只读”语义的场景都使用 const,表达“常量”语义的场景都使用 constexpr
“只读”和“不允许被修改”之间并没有必然的联系
int a = 10;
const int & con_b = a;
cout << con_b << endl; // 10
a = 20;
cout << con_b << endl; // 20
在某些场景中,必须明确使用 constexpr
#include <iostream>
#include <array>
using namespace std;
constexpr int sqr1(int arg){
return arg*arg;
}
const int sqr2(int arg){
return arg*arg;
}
int main()
{
array<int,sqr1(10)> mylist1;//可以,因为sqr1是constexpr函数
array<int,sqr2(10)> mylist1;//不可以,因为sqr2不是constexpr函数
return 0;
}
long long超长整形(8 个字节)
- 对于有符号 long long 整形,后缀用 “LL” 或者 “ll” 标识。例如,“10LL” 就表示有符号超长整数 10;
- 对于无符号 long long 整形,后缀用 “ULL”、“ull”、“Ull” 或者 “uLL” 标识。例如,“10ULL” 就表示无符号超长整数 10;
如果不添加任何标识,则所有的整数都会默认为 int 类型
如果想了解当前平台上 long long 整形的取值范围,可以使用头文件中与 long long 整形相关的 3 个宏
- LLONG_MIN:代表当前平台上最小的 long long 类型整数;
- LLONG_MAX:代表当前平台上最大的 long long 类型整数;
- ULLONG_MIN:代表当前平台上最大的 unsigned long long 类型整数(无符号超长整型的最小值为 0);
右值引用
左值和右值
左值的英文简写为“lvalue”,右值的英文简写为“rvalue”。
lvalue 是“loactor value”的缩写,可意为存储在内存中、有明确存储地址(可寻址)的数据
rvalue 译为 “read value”,指的是那些可以提供数据值的数据(不一定可以寻址,例如存储于寄存器中的数据)
判断方法:
- 可位于赋值号(=)左侧的表达式就是左值;反之,只能位于赋值号右侧的表达式就是右值
- 有名称的、可以获取到存储地址的表达式即为左值;反之则是右值
C++右值引用
int num = 10;
int &b = num; //正确
int &c = 10; //错误
const int &b = num;
const int &c = 10; // 允许使用常量左值引用操作右值
右值引用,用 “&&” 表示
int num = 10;
//int && a = num; //右值引用不能初始化为左值
int && a = 10;
a = 100;
cout << a << endl; // 100
1、右值引用主要用于移动语义和完美转发,其中前者需要有修改右值的权限
2、常量右值引用的作用就是引用一个不可修改的右值
引用类型 | 使用场景 |
---|---|
非常量右值引用 | 移动语义、完美转发 |
常量右值引用 | 无实际用途 |
移动构造函数的功能和用法
移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”
程序执行过程中产生的临时对象,只用于传递数据(没有其它的用处),并且会很快会被销毁。因此在使用临时对象初始化新对象时,可以将包含的指针成员指向的内存资源直接移给新对象所有,无需再新拷贝一份,这大大提高了初始化的执行效率。
#include <iostream>
using namespace std;
class demo{
public:
demo():num(new int(0)){
cout<<"construct!"<<endl;
}
demo(const demo &d):num(new int(*d.num)){
cout<<"copy construct!"<<endl;
}
//添加移动构造函数
demo(demo &&d):num(d.num){
d.num = NULL;
cout<<"move construct!"<<endl;
}
~demo(){
cout<<"class destruct!"<<endl;
}
private:
int *num;
};
demo get_demo(){
return demo();
}
int main(){
demo a = get_demo();
return 0;
}
程序执行结果中产生的临时对象(例如函数返回值、lambda 表达式等)既无名称也无法获取其存储地址,所以属于右值。
当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。
move()函数:将左值强制转换为右值
将某个左值强制转化为右值。
其常用于实现移动语义
#include <iostream>
using namespace std;
class first {
public:
first() :num(new int(0)) {
cout << "construct!" << endl;
}
//移动构造函数
first(first &&d) :num(d.num) {
d.num = NULL;
cout << "first move construct!" << endl;
}
public: //这里应该是 private,使用 public 是为了更方便说明问题
int *num;
};
class second {
public:
second() :fir() {}
//用 first 类的移动构造函数初始化 fir
second(second && sec) :fir(move(sec.fir)) {
cout << "second move construct" << endl;
}
public: //这里也应该是 private,使用 public 是为了更方便说明问题
first fir;
};
int main() {
second oth;
second oth2 = move(oth);
//cout << *oth.fir.num << endl; //程序报运行时错误
return 0;
}
引用限定符的用法
在成员函数的后面添加 “&” 或者 “&&”,从而限制调用者的类型(左值还是右值)
#include <iostream>
using namespace std;
class demo {
public:
demo(int num):num(num){}
int get_num()&{
return this->num;
}
private:
int num;
};
int main() {
demo a(10);
cout << a.get_num() << endl; // 正确
//cout << move(a).get_num() << endl; // 错误
return 0;
}
在 get_num() 成员函数的后面添加了 “&”,它可以限定调用该函数的对象必须是左值对象
引用限定符不适用于静态成员函数和友元函数
const和引用限定符
当引用限定符和 const 修饰同一个类的成员函数时,const 必须位于引用限定符前面。
当 const && 修饰类的成员函数时,调用它的对象只能是右值对象;
当 const & 修饰类的成员函数时,调用它的对象既可以是左值对象,也可以是右值对象。
无论是 const && 还是 const & 限定的成员函数,内部都不允许对当前对象做修改操作。
#include <iostream>
using namespace std;
class demo {
public:
demo(int num,int num2) :num(num),num2(num2) {}
//左值和右值对象都可以调用
int get_num() const &{
return this->num;
}
//仅供右值对象调用
int get_num2() const && {
return this->num2;
}
private:
int num;
int num2;
};
int main() {
demo a(10,20);
cout << a.get_num() << endl; // 正确
cout << move(a).get_num() << endl; // 正确
//cout << a.get_num2() << endl; // 错误
cout << move(a).get_num2() << endl; // 正确
return 0;
}
完美转发及其实现
函数模板可以将自己的参数“完美”地转发给内部调用的其它函数
不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变
template <typename T>
void function(T&& t) {
otherdef(t);
}
此模板函数的参数 t 既可以接收左值,也可以接收右值
- 当实参为左值或者左值引用(A&)时,函数模板中 T&& 将转变为 A&(A& && = A&)
- 当实参为右值或者右值引用(A&&)时,函数模板中 T&& 将转变为 A&&(A&& && = A&&)
只要函数模板的参数类型为 T&&,则 C++ 可以自行准确地判定出实际传入的实参是左值还是右值
#include <iostream>
using namespace std;
//重载被调用函数,查看完美转发的效果
void otherdef(int & t) {
cout << "lvalue\n";
}
void otherdef(const int & t) {
cout << "rvalue\n";
}
//实现完美转发的函数模板
template <typename T>
void function(T&& t) {
otherdef(forward<T>(t));
}
int main()
{
function(5); // rvalue
int x = 1;
function(x); // lvalue
return 0;
}
forword() 函数模板用于修饰被调用函数中需要维持参数左、右值属性的参数
shared_ptr智能指针
多个 shared_ptr 智能指针可以共同使用同一块堆内存
该类型智能指针在实现上采用的是引用计数机制,只有引用计数为 0 时,堆内存才会被自动释放
std::shared_ptr<int> p1; //不传入任何实参std::shared_ptr<int> p2(nullptr); //传入空指针 nullptr
std::shared_ptr<int> p3(new int(10));
std::shared_ptr<int> p3 = std::make_shared<int>(10);
//调用拷贝构造函数
std::shared_ptr<int> p4(p3);//或者 std::shared_ptr<int> p4 = p3;
//调用移动构造函数
std::shared_ptr<int> p5(std::move(p4)); //或者 std::shared_ptr<int> p5 = std::move(p4);
同一普通指针不能同时为多个 shared_ptr 对象赋值,否则会导致程序发生异常
int* ptr = new int;
std::shared_ptr<int> p1(ptr);
std::shared_ptr<int> p2(ptr);//错误
可以自定义所指堆内存的释放规则,这样当堆内存的引用计数为 0 时,会优先调用我们自定义的释放规则
//指定 default_delete 作为释放规则
std::shared_ptr<int> p6(new int[10], std::default_delete<int[]>());
//自定义释放规则
void deleteInt(int*p) {
delete []p;
}
//初始化智能指针,并自定义释放规则
std::shared_ptr<int> p7(new int[10], deleteInt);
使用:
#include <iostream>
#include <memory>
using namespace std;
int main()
{
//构建 2 个智能指针
std::shared_ptr<int> p1(new int(10));
std::shared_ptr<int> p2(p1);
//输出 p2 指向的数据
cout << *p2 << endl;
p1.reset();//引用计数减 1,p1为空指针
if (p1) {
cout << "p1 不为空" << endl;
}
else {
cout << "p1 为空" << endl;
}
//以上操作,并不会影响 p2
cout << *p2 << endl;
//判断当前和 p2 同指向的智能指针有多少个
cout << p2.use_count() << endl;
return 0;
}
// 10
// p1为空
// 10
// 1
unique_ptr智能指针
unique_ptr 指针指向的堆内存无法同其它 unique_ptr 共享,也就是说,每个 unique_ptr 指针都独自拥有对其所指堆内存空间的所有权
// 创建出空指针
std::unique_ptr<int> p1();
std::unique_ptr<int> p2(nullptr);
std::unique_ptr<int> p3(new int);
// unique_ptr没有提供拷贝构造函数,只提供了移动构造函数
std::unique_ptr<int> p4(new int);
std::unique_ptr<int> p5(p4);//错误,堆内存不共享
std::unique_ptr<int> p5(std::move(p4));//正确,调用移动构造函数
为 unique_ptr 自定义释放规则,只能采用函数对象的方式
//自定义的释放规则
struct myDel
{
void operator()(int *p) {
delete p;
}
};
std::unique_ptr<int, myDel> p6(new int);
//std::unique_ptr<int, myDel> p6(new int, myDel());
用法:
#include <iostream>
#include <memory>
using namespace std;
int main()
{
std::unique_ptr<int> p5(new int);
*p5 = 10;
int * p = p5.release(); // p 接收 p5 释放的堆内存
cout << *p << endl;
//判断 p5 是否为空指针
if (p5) {
cout << "p5 is not nullptr" << endl;
}
else {
cout << "p5 is nullptr" << endl;
}
std::unique_ptr<int> p6;
//p6 获取 p 的所有权
p6.reset(p);
cout << *p6 << endl;;
return 0;
}
// 10
// p5 is nullptr
// 10
weak_ptr智能指针
类型指针通常不单独使用(没有实际用处),只能和 shared_ptr 类型指针搭配使用
可以将 weak_ptr 类型指针视为 shared_ptr 指针的一种辅助工具,借助 weak_ptr 类型指针, 我们可以获取 shared_ptr 指针的一些状态信息,比如有多少指向相同的 shared_ptr 指针、shared_ptr 指针指向的堆内存是否已经被释放等等
- 当 weak_ptr 类型指针的指向和某一 shared_ptr 指针相同时,weak_ptr 指针并不会使所指堆内存的引用计数加 1
- 当 weak_ptr 指针被释放时,之前所指堆内存的引用计数也不会因此而减 1
- weak_ptr 模板类中没有重载 * 和 -> 运算符,这也就意味着,weak_ptr 类型指针只能访问所指的堆内存,而无法修改它
std::weak_ptr<int> wp1; // 可以创建一个空 weak_ptr 指针
std::weak_ptr<int> wp2 (wp1);
// weak_ptr 指针更常用于指向某一 shared_ptr 指针拥有的堆内存
std::shared_ptr<int> sp (new int);
std::weak_ptr<int> wp3 (sp);
使用:
#include <iostream>
#include <memory>
using namespace std;
int main()
{
std::shared_ptr<int> sp1(new int(10));
std::shared_ptr<int> sp2(sp1);
std::weak_ptr<int> wp(sp2);
//输出和 wp 同指向的 shared_ptr 类型指针的数量
cout << wp.use_count() << endl;
//释放 sp2
sp2.reset();
cout << wp.use_count() << endl;
//借助 lock() 函数,返回一个和 wp 同指向的 shared_ptr 类型指针,获取其存储的数据
cout << *(wp.lock()) << endl;
return 0;
}
// 2
// 1
// 10