1、什么是 C+11
-
C++11 标准为 C++编程语言的第三个官方标准, 正式名叫 ISO/IEC 14882:2011 - Information technology -Programming languages – C++。 在正式标准发布前, 原名 C++0x。 它将取代 C++标准第二版 ISO/IEC 14882:2003 - Programming languages – C++成为 C++语言新标准。
-
C++11 是对目前 C++语言的扩展和修正, C++11 不仅包含核心语言的新机能, 而且扩展了
C++的标准程序库(STL) , 并入了大部分的 C++ Technical Report 1(TR1) 程序库(数学
的特殊函数除外)。 -
C++11 包括大量的新特性: 包括 lambda 表达式, 类型推导关键字 auto、 decltype, 和模板
的大量改进。
1、Qt Creator 新建一个纯C++的项目,在项目文件配置:CONFIG += c++11
2、终端,g++ xxx.cpp -std=c++11
2、类型推导
2.1 auto的使用
- auto的自动类型推导,用于初始化表达式中推断出变量的数据类型。从这个意义上讲,auto并非一种"类型"声明,而是一个类型声明时的"占位符",编译器在编译时期会将auto替换为变量实际的类型。
- 通过auto的自动类型推导,可以大大简化我们的编程工作。
在C中,auto修饰局部变量,局部变量也叫auto变量,自动变量
{
int a;
auto int a;
}
C++11, auto根据用户初始化内容自动推导类型
auto使用代码
#include <iostream>
#include<vector>
double foo(){
//...
}
//自定义类型:结构体、枚举、联合体
//struct是声明结构体类型时所必须使用的关键字
//Test是结构体名字
struct Test{
int a;
};
void func(std::vector<std::string>& tmp){
//自动推导i的类型
for(auto i=tmp.begin();i<tmp.end();i++){
//...
}
}
int main(){
/*
在C中,auto修饰局部变量,局部变量也叫auto变量,自动变量
{
int a; 等价于auto int a;
auto int a;
}
C++ 11, auto根据用户初始化内容自动推导类型
*/
//内置类型
int a=1;
auto b=1; //的类型就是int
auto x = 1; // x的类型为int
auto y = foo(); // y的类型是double
//自定义类型
struct Test str={0}; //结构体 = {0}; 这种初始化方式来对结构体进行初始化,是可行的。
auto d=str; //d的类型就是struct Test类型
return 0;
}
2.2 auto的易错点
#include<iostream>
#include<vector>
//vs2013不支持,函数形参是auto变量,qt可以
void func(auto a){
//.....
}
//3.auto 变量不能作为自定义类型的成员变量
struct test{
int a;
auto b=10;
};
int main(){
//1.定义变量时,必须初始化
auto a;
a=10;
//4.不能是auto数组
auto b[3]={1,2,3};
//5.模板实例化类型不能是auto类型
std::vector<int> a;
std::vector<auto>b={1};
system("pause");
return 0;
}
2.3 decltype
decltype 实际上有点像 auto 的反函数, auto 可以让你声明一个变量, 而 decltype 则可以从一个变量或表达式中得到其类型, 如下:
#include<iostream>
#include<typeinfo> //typeid
#include<vector>
int main(){
//decltype 实际上有点像 auto 的反函数, auto 可以让你声明一个变量, 而 decltype 则可以从一个变量或表达式中得到其类型, 如下:
int i;
decltype(i) j=0;
std::cout<<typeid(j).name()<<std::endl;
float a;
decltype(a) b;
std::cout<<typeid(b).name()<<std::endl;
double c;
decltype(a+c) d;
std::cout<<typeid(d).name()<<std::endl;
std::vector<int> vec;
decltype(vec.begin()) tmp;
for(tmp=vec.begin();tmp!=vec.end();tmp++){
//...
}
//匿名类型的枚举变量
enum {
OK,
ERROR
} flag;
decltype(flag) flag2;
return 0;
}
2.4 追踪返回类型
返回类型后置: 在函数名和参数列表后面指定返回类型。
#include<iostream>
int Add(int a,int b){
return a+b;
}
auto Sub(int a,int b) -> int{
return a-b;
}
auto Div(int a,double b) -> decltype(a/b){
return a/b;
}
template<class T1,class T2>
auto Mul(const T1& t1,const T2& t2) -> decltype(t1*t2){
return t1*t2;
}
int main(){
auto a=10;
auto b=20;
std::cout<<Add(a,b)<<std::endl;
std::cout<<Sub(a,b)<<std::endl;
auto c=30;
auto d=5.5;
std::cout<<Div(c,d)<<std::endl;
std::cout<<Mul(c,d)<<std::endl;
return 0;
}
3、易用性的改进
3.1 初始化
3.1.1 类中内成员变量初始化
#include<iostream>
#include<string>
class A{
public:
A(int a)
:a_(a) //初始化列表给a_初始化
{
//a_=a
}
int a_;
};
class B{
public:
int data1{1}; // 使用"="初始化非静态普通成员, 也可以 int data{1};
int data2=2;
A tmp{10}; // 对象成员, 创建对象时, 可以使用{}来调用构造函数
std::string name{"c++"};
};
int main(){
B b;
std::cout<<b.data1<<std::endl;
std::cout<<b.data2<<std::endl;
std::cout<<b.tmp.a_<<std::endl;
std::cout<<b.name<<std::endl;
return 0;
}
3.1.2 列表初始化
C++11 引入了一个新的初始化方式, 称为初始化列表(List Initialize), 具体的初始化方式如下:
初始化列表可以用于初始化结构体类型, 例如:
#include<iostream>
using namespace std;
struct Test{
int a;
int b;
char name[50];
};
int main(void){
struct Test test={1,2,"mike"}; //列表初始化
int a=1;
int b={1}; //ok,列表初始化
int c{2}; //ok
int arr1[]={1,2,3};
int arr2[]{1,2,3};
return 0;
}
其他一些不方便初始化的地方使用, 比如 std的初始化, 如果不使用这种方式, 只能
用构造函数来初始化, 难以达到效果 。
std::vector<int> vec1(3,5);
std::vector<int> vec2={5,5,5};
std::vector<int> vec3={1,2,3,4,5}; //不使用列表初始化用构造函数难以实现
3.1.3 防止类型收缩
类型收窄指的是导致数据内容发生变化或者精度丢失的隐式类型转换。 使用列表初始化可以防止类型收窄。
#include<iostream>
int main(){
int a=512;
char b=a; //char 256位
std::cout<<"b= "<<std::endl;
return 0;
}
输出结果:
b=
#include<iostream>
int main(){
int a=512;
char b={a};
std::cout<<"b= "<<std::endl;
return 0;
}
编译不通过。
PS D:\C++11代码\build> cmake --build .
用于 .NET Framework 的 Microsoft (R) 生成引擎版本 16.7.0+b89cb5fde
版权所有(C) Microsoft Corporation。保留所有权利。
列表初始化防止类型收缩.cpp
D:\C++11代码\易用性的改进\初始化\类中成员变量直接初始化\列表初始化防止类型收缩.cpp(5,14): error C2397: 从“int”转换到“char”需要收缩转换 [D:\C++11代码\build\易用性的改进\初始化\类中成员变量直接初始化\main.vcxproj]
PS D:\C++11代码\build>
#include <iostream>
int main(void)
{
int a = 512;
char b = {a};
std::cout << "b= " << std::endl;
const int x = 1024;
const int y = 10;
char a = x; // 收窄, 但可以通过编译
char *b = new char(1024); // 收窄, 但可以通过编译
char c = {x}; // err, 收窄, 无法通过编译
char d = {y}; // 可以通过编译
unsigned char e{-1}; // err,收窄, 无法通过编译
float f{7}; // 可以通过编译
int g{2.0f}; // err,收窄, 无法通过编译
float *h = new float{1e48}; // err,收窄, 无法通过编译
float i = 1.2l; // 可以通过编译
return 0;
}
3.2 基于范围的for循环
在 C++中 for 循环可以使用基于范围的 for 循环, 示例代码如下:
#include<iostream>
int main(){
int arr[]={1,2,3,4,5,6,7,8,9,10};
int n=sizeof(arr)/sizeof(*arr); //元素个数
std::cout<<n<<std::endl;
for(int i=0;i<n;i++){
int tmp=arr[i];
std::cout<<tmp<<" ";
}
std::cout<<std::endl;
for(int tmp: arr){
std::cout<<tmp<<" ";
}
std::cout<<std::endl;
for(int i=0;i<n;i++){
int& tmp=arr[i];
tmp=tmp*2;
std::cout<<tmp<<" ";
}
std::cout<<std::endl;
for(int& tmp:arr){
tmp=tmp*2;
std::cout<<tmp<<" ";
}
std::cout<<std::endl;
return 0;
}
使用基于范围的 for 循环, 其 for 循环迭代的范围必须是可确定的:
void func(int arr[]) //形参中的数组不是数组,而是指针变量,存放数组的地址,无法确定元素个数
{
//使用基于范围的 for 循环, 其 for 循环迭代的范围必须是可确定的:
for(int& tmp:arr){
std::cout<<tmp<<" ";
}
}
int arr[]={1,2,3,4,5,6,7,8,9,10};
func(arr);//传过去的是arr数组的地址
3.3 静态断言
C/C++提供了调试工具 assert, 这是一个宏, 用于在运行阶段对断言进行检查, 如果条件为真, 执行程序, 否则调用 abort()。
#include<iostream>
#include<assert.h>
int main(){
//C/C++提供了调试工具 assert, 这是一个宏, 用于在运行阶段对断言进行检查, 如果条件为真, 执行程序, 否则调用 abort()。
bool flag=false;
int a=10;
//运行时检查条件,如果为真,往下执行,如果为假,中断提示错误
//assert(flag==true); //条件为假,中断
assert(flag==false); //条件为真,执行
std::cout<<a<<std::endl;
std::cout << "1" << std::endl;
std::cout <<"c++11"<<std::endl;
return 0;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bsJ9a7wj-1664897582183)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20220917193143153.png)]
C++ 11 新增了关键字 static_assert, 可用于在编译阶段对断言进行测试。静态断言的好处:
- 更早的报告错误, 我们知道构建是早于运行的, 更早的错误报告意味着开发成本的降低。
- 减少运行时调用堆栈开销, 静态断言是编译期检测的, 减少了运行时开销 。
语法如下:
- static_assert(常量表达式, 提示字符串)。注意: 只能是常量表达式, 不能是变量 。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rxSqsF9n-1664897582185)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20220917195154977.png)]
int main(void){
//static_asset(常量表达式条件,"提示的字符串")
//static_assert(sizeof(void* )==4,"64位系统不支持");
static_assert(sizeof(void* )==8,"64位系统支持");
std::cout<<sizeof(void*)<<std::endl;
return 0;
}
3.4 noexcept 修饰符(vs2013 不支持)
代表此函数不能抛出异常, 如果抛出, 就会异常C++11 使用 noexcept 替代 throw() 。
#include<iostream>
void func01(){
throw 1;
}
//这个函数不能抛出任何异常
void func02() throw(){
throw 1;
}
//这个函数只能抛出int、char类型异常
//C++11 已经弃用这个声明
void func03() throw(int,char){
}
//这个函数不能抛出任何异常
void func04() noexcept{
throw 1;
}
int main(){
func01();
func02();
func03();
func04();
return 0;
}
说明代码:
PS D:\C++11代码\build> cmake --build .
用于 .NET Framework 的 Microsoft (R) 生成引擎版本 16.7.0+b89cb5fde
版权所有(C) Microsoft Corporation。保留所有权利。
noexcept修饰符.cpp
D:\C++11代码\易用性的改进\noexcept修饰符\noexcept修饰符.cpp(9,1): warning C4297: “func02”: 假定函数不引发异常,
但确实发生了 [D:\C++11代码\build
\易用性的改进\noexcept修饰符\main.vcxproj]
D:\C++11代码\易用性的改进\noexcept修饰符\noexcept修饰符.cpp(9,1): message : 在函数上指定了 __declspec(nothrow)、
throw()、noexcept(true
)或 noexcept [D:\C++11代码\build\易用性的改进\noexcept修饰符\main.vcxproj]
D:\C++11代码\易用性的改进\noexcept修饰符\noexcept修饰符.cpp(14,29): warning C4290: 忽略 C++ 异常规范,但指示函数
不是 __declspec(nothrow) [D
:\C++11代码\build\易用性的改进\noexcept修饰符\main.vcxproj]
D:\C++11代码\易用性的改进\noexcept修饰符\noexcept修饰符.cpp(20,1): warning C4297: “func04”: 假定函数不引发异常,
但确实发生了 [D:\C++11代码\buil
d\易用性的改进\noexcept修饰符\main.vcxproj]
D:\C++11代码\易用性的改进\noexcept修饰符\noexcept修饰符.cpp(20,1): message : 在函数上指定了 __declspec(nothrow)
、throw()、noexcept(tru
e)或 noexcept [D:\C++11代码\build\易用性的改进\noexcept修饰符\main.vcxproj]
main.vcxproj -> D:\C++11代码\build\易用性的改进\noexcept修饰符\Debug\main.exe
PS D:\C++11代码\build>
3.5 nullptr
nullptr 是为了解决原来 C++中 NULL 的二义性问题而引进的一种新的类型, 因为 NULL 实际上代表的是 0。
#include<iostream>
void func(int p){
std::cout<<__LINE__<<std::endl;
}
void func(int* p){
std::cout<<__LINE__<<std::endl;
}
int main(){
int *a=NULL;
int *b=0;
//下面func都是调用第一个参数是整形的func,而不是指针的func
func(0); //调用 func(int), 就算写 NULL, 也是调用这个
func(NULL);
int* c=nullptr; //nullptr只能给指针赋值
if(a==c){
std::cout<<"equal"<<std::endl;
}
func(nullptr); //不能赋值给常量,这里报错
return 0;
}
输出结果:
PS D:\C++11代码\build\易用性的改进\nullptr\Debug> .\main.exe
4
4
equal
8
PS D:\C++11代码\build\易用性的改进\nullptr\Debug>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n90VhsG6-1664897582185)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20220917211018068.png)]
nullptr只能赋值给指针,不能赋值给常量。
int tmp=nullptr;
输出结果:
D:\C++11代码\易用性的改进\nullptr\nullptr.cpp(22,20): error C2440: “初始化”: 无法从“nullptr”转换为“int” [D:\C++11代码\build\易用性的改进
\nullptr\main.vcxproj]
D:\C++11代码\易用性的改进\nullptr\nullptr.cpp(22,12): message : 本机 nullptr 只能转换为 bool 或使用 reinterpret_cast 转换为整型 [D:\C+
3.6 强类型枚举
C++ 11 引入了一种新的枚举类型, 即“枚举类”, 又称“强类型枚举”。 声明请类型枚举非常简单, 只需要在 enum 后加上使用 class 或 struct。 如:
enum Old{Yes, No}; // old style
enum class New{Yes, No}; // new style
enum struct New2{Yes, No}; // new style
“传统”的 C++枚举类型有一些缺点: 它会在一个代码区间中抛出枚举类型成员(如果在相同的代码域中的两个枚举类型具有相同名字的枚举成员, 这会导致命名冲突) , 它们会被隐式转换为整型, 并且不可以指定枚举的底层数据类型。
#include <iostream>
enum Flag1
{
OK,
ERROR
};
enum Flag2
{
OK,
ERROR
};
int main()
{
Flag1 flag=OK;
return 0;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V07RYZ8L-1664897582186)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20220917213511329.png)]
输出结果:
PS D:\C++11代码\build> cmake --build .
用于 .NET Framework 的 Microsoft (R) 生成引擎版本 16.7.0+b89cb5fde
版权所有(C) Microsoft Corporation。保留所有权利。
强类型枚举.cpp
D:\C++11代码\易用性的改进\强类型枚举\强类型枚举.cpp(12,9): error C2365: “main::OK”: 重定义;以前的定义是“枚举数” [D:\C++11代码\build\易用性的改进\强类型枚
举\main.vcxproj]
D:\C++11代码\易用性的改进\强类型枚举\强类型枚举.cpp(7): message : 参见“main::OK”的声明 [D:\C++11代码\build\易用
性的改进\强类型枚举\main.vcxproj]
D:\C++11代码\易用性的改进\强类型枚举\强类型枚举.cpp(13,9): error C2365: “main::ERROR”: 重定义;以前的定义是“枚举
数” [D:\C++11代码\build\易用性的改进\强
类型枚举\main.vcxproj]
D:\C++11代码\易用性的改进\强类型枚举\强类型枚举.cpp(8): message : 参见“main::ERROR”的声明 [D:\C++11代码\build\易
用性的改进\强类型枚举\main.vcxproj]
PS D:\C++11代码\build>
普通枚举的类型大小为4。
#include <iostream>
enum Flag1
{
OK,
ERROR
};
// enum Flag2
// {
// OK,
// ERROR
// };
int main()
{
Flag1 flag=OK;
std::cout<<sizeof(OK)<<std::endl;
std::cout<<sizeof(Flag1)<<std::endl;
return 0;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NmJzA1ef-1664897582187)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20220917214224829.png)]
强类型枚举:强类型枚举,enum后面加上class或者struct修饰。
#include <iostream>
enum Flag1
{
OK,
ERROR
};
// enum Flag2
// {
// OK,
// ERROR
// };
//强类型枚举,enum后面加上class或者struct修饰
enum class Grade1{
Success,
Failed,
Equal
};
//强类型枚举,enum后面加上class或者struct修饰
enum struct Grade2{
Success,
Failed,
Equal
};
//强枚举类型可以指定枚举常量的类型
enum class Grade3:char{
Success,
Failed,
Equal
};
//强枚举类型可以指定枚举常量的类型
enum class Grade4:int{
Success,
Failed,
Equal
};
int main()
{
Flag1 flag=OK;
std::cout<<sizeof(OK)<<std::endl;
std::cout<<sizeof(Flag1)<<std::endl;
//Grade1 grade=Success; err 必须指定枚举常量的作用域
Grade1 grade=Grade1::Success;
std::cout<<sizeof(Grade3::Success)<<std::endl;
std::cout<<sizeof(Grade4::Success)<<std::endl;
return 0;
}
输出结果:
PS D:\C++11代码\build\易用性的改进\强类型枚举\Debug> .\main.exe
4
4
1
4
PS D:\C++11代码\build\易用性的改进\强类型枚举\Debug>
3.6 常量表达式(vs2013 不支持)
3.6.1
常量表达式主要是允许一些计算发生在编译时, 即发生在代码编译而不是运行的时候这是很大的优化: 假如有些事情可以在编译时做, 它将只做一次, 而不是每次程序运行时都计算 。
#include<iostream>>
enum Grade{
Ok,
Error
};
int getNum1(){
return Grade::Ok;
}
int main(){
//枚举常量初始化,必须是整形常量
enum {e1=getNum1(),e2};
return 0;
}
输出结果:
PS D:\C++11代码\build> cmake --build .
用于 .NET Framework 的 Microsoft (R) 生成引擎版本 16.7.0+b89cb5fde
版权所有(C) Microsoft Corporation。保留所有权利。
Checking Build System
Building Custom Rule D:/C++11????/?????????/????????/CMakeLists.txt
常量表达式.cpp
D:\C++11代码\易用性的改进\常量表达式\常量表达式.cpp(1,1): warning C4819: 该文件包含不能在当前代码页(936)中表示的
字符。请将该文件保存为 Unicode 格式以防止数据丢失 [D:\
C++11代码\build\易用性的改进\常量表达式\main.vcxproj]
D:\C++11代码\易用性的改进\常量表达式\常量表达式.cpp(1,9): warning C4067: 预处理器指令后有意外标记 - 应输入换行符
[D:\C++11代码\build\易用性的改进\常量表达式\mai
n.vcxproj]
D:\C++11代码\易用性的改进\常量表达式\常量表达式.cpp(14,21): error C2131: 表达式的计算结果不是常数 [D:\C++11代码\build\易用性的改进\常量表达式\main.vcxproj
]
D:\C++11代码\易用性的改进\常量表达式\常量表达式.cpp(14,21): message : 对未定义的函数或为未声明为“constexpr”的函
数的调用导致了故障 [D:\C++11代码\build\易用性的改
进\常量表达式\main.vcxproj]
D:\C++11代码\易用性的改进\常量表达式\常量表达式.cpp(14,21): message : 请参见“getNum1”的用法 [D:\C++11代码\build\易用性的改进\常量表达式\main.vcxproj
]
PS D:\C++11代码\build>
另一个例子:
#include<iostream>>
enum Grade{
Ok,
Error
};
int getNum1(){
return Grade::Ok;
}
const int getNum2(){
return Grade::Error;
}
int main(){
//枚举常量初始化,必须是整形常量
//enum {e1=getNum1(),e2};
enum{e1=getNum2(),e2}; //加了const也不行
return 0;
}
输出结果:
PS D:\C++11代码\build> cmake --build .
用于 .NET Framework 的 Microsoft (R) 生成引擎版本 16.7.0+b89cb5fde
版权所有(C) Microsoft Corporation。保留所有权利。
常量表达式.cpp
D:\C++11代码\易用性的改进\常量表达式\常量表达式.cpp(1,1): warning C4819: 该文件包含不能在当前代码页(936)中表示的
字符。请将该文件保存为 Unicode 格式以防止数据丢失 [D:\
C++11代码\build\易用性的改进\常量表达式\main.vcxproj]
D:\C++11代码\易用性的改进\常量表达式\常量表达式.cpp(1,9): warning C4067: 预处理器指令后有意外标记 - 应输入换行符
[D:\C++11代码\build\易用性的改进\常量表达式\mai
n.vcxproj]
D:\C++11代码\易用性的改进\常量表达式\常量表达式.cpp(18,20): error C2131: 表达式的计算结果不是常数 [D:\C++11代码\build\易用性的改进\常量表达式\main.vcxproj
]
D:\C++11代码\易用性的改进\常量表达式\常量表达式.cpp(18,20): message : 对未定义的函数或为未声明为“constexpr”的函
数的调用导致了故障 [D:\C++11代码\build\易用性的改
进\常量表达式\main.vcxproj]
D:\C++11代码\易用性的改进\常量表达式\常量表达式.cpp(18,20): message : 请参见“getNum2”的用法 [D:\C++11代码\build\易用性的改进\常量表达式\main.vcxproj
]
PS D:\C++11代码\build>
使用 constexpr, 你可以创建一个编译时的函数:
#include<iostream>>
enum Grade{
Ok,
Error
};
int getNum1(){
return Grade::Ok;
}
const int getNum2(){
return Grade::Error;
}
//常量表达式,发生在编译阶段
constexpr int getNum3(){
return Grade::Ok;
}
int main(){
//枚举常量初始化,必须是整形常量
//enum {e1=getNum1(),e2}; //枚举类型,err
//enum{e1=getNum2(),e2}; //err
enum flag{e1=getNum3(),e2};
constexpr int tmp=getNum3(); //ok,发生在编译阶段,而不是函数运行阶段
enum {a1=tmp,a}; //ok
std::cout<<flag::e1<<std::endl;
std::cout<<tmp<<std::endl;
return 0;
}
输出结果:
PS D:\C++11代码\build\易用性的改进\常量表达式\Debug> .\main.exe
0
0
PS D:\C++11代码\build\易用性的改进\常量表达式\Debug>
3.6.2 constexpr 函数的限制
- 函数中只能有一个 return 语句(有极少特例)。
- 函数必须返回值(不能是 void 函数)。
- 在使用前必须已有定义。
- return 返回语句表达式中不能使用非常量表达式的函数、 全局数据, 且必须是一个常量
表达式 。
函数中只能有一个 return 语句。
/*
constexpr函数的限制:
函数中只能有一个return语句(有极少特例)
函数必须返回值(不能是void函数)
在使用前必须已有定义
return返回语句表达式中不能使用非常量表达式的函数、全局数据,且必须是一个常量表达式
*/
int add(int x,int y){
return x+y;
}
constexpr int func1(){
//err 函数中只能有一个return语句
int b=add(1,2);
constexpr int a=1;
return a+b;
}
输出结果:
PS D:\C++11代码\build> cmake ..
CMake Deprecation Warning at CMakeLists.txt:1 (cmake_minimum_required):
Compatibility with CMake < 2.8.12 will be removed from a future version of
Update the VERSION argument <min> value or use a ...<max> suffix to tell
CMake that the project does not need compatibility with older versions.
-- Selecting Windows SDK version 10.0.18362.0 to target Windows 10.0.19042.
-- Configuring done
-- Generating done
-- Build files have been written to: D:/C++11代码/build
PS D:\C++11代码\build> cmake --build .
用于 .NET Framework 的 Microsoft (R) 生成引擎版本 16.7.0+b89cb5fde
版权所有(C) Microsoft Corporation。保留所有权利。
constexpr函数的限制.cpp
D:\C++11代码\易用性的改进\常量表达式\constexpr函数的限制.cpp(1,1): warning C4819: 该文件包含不能在当前代码页(936)中表示的字符。请将该文件保存为 Unicode 格式以防止
数据丢失 [D:\C++11代码\build\易用性的改进\常量表达式\main.vcxproj]
D:\C++11代码\易用性的改进\常量表达式\constexpr函数的限制.cpp(14,15): error C3615: constexpr 函数 "func1" 不会生
成常数表达式 [D:\C++11代码\buil
d\易用性的改进\常量表达式\main.vcxproj]
D:\C++11代码\易用性的改进\常量表达式\constexpr函数的限制.cpp(15,14): message : 对未定义的函数或为未声明为“constexpr”的函数的调用导致了故障 [D:\C++11代码\bu
ild\易用性的改进\常量表达式\main.vcxproj]
D:\C++11代码\易用性的改进\常量表达式\constexpr函数的限制.cpp(15,14): message : 请参见“add”的用法 [D:\C++11代码\build\易用性的改进\常量表达式\main.vc
xproj]
PS D:\C++11代码\build>
函数中只能有一个 return 语句(有极少特例)。
#include<iostream>
/*
constexpr函数的限制:
函数中只能有一个return语句(有极少特例)
函数必须返回值(不能是void函数)
在使用前必须已有定义
return返回语句表达式中不能使用非常量表达式的函数、全局数据,且必须是一个常量表达式
*/
int add(int x,int y){
return x+y;
}
constexpr int func1(){
//err 函数中只能有一个return语句
//int b=add(1,2);
constexpr int a=1;
return a;
}
constexpr int func2(){
//函数中只能有一个return语句(有极少特例)
//允许包含typedef,using指令,静态断言
static_assert(1,"fail");
constexpr int a=1;
return a;
}
int main(){
//std::cout<<func1()<<std::endl;
std::cout<<func1()<<std::endl;
return 0;
}
输出结果:
PS D:\C++11代码\build\易用性的改进\常量表达式\Debug> .\main.exe
1
PS D:\C++11代码\build\易用性的改进\常量表达式\Debug>
函数必须返回值(不能是 void 函数)。constexpr常量表达式必须有返回值。
return 返回语句表达式中不能使用非常量表达式的函数,且必须是一个常量
表达式 。
#include<iostream>
/*
constexpr函数的限制:
函数中只能有一个return语句(有极少特例)
函数必须返回值(不能是void函数)
在使用前必须已有定义
return返回语句表达式中不能使用非常量表达式的函数、全局数据,且必须是一个常量表达式
*/
int add(int x,int y){
return x+y;
}
constexpr int func1(){
//err 函数中只能有一个return语句
//int b=add(1,2);
constexpr int a=1;
return a;
}
constexpr int func2(){
//函数中只能有一个return语句(有极少特例)
//允许包含typedef,using指令,静态断言
static_assert(1,"fail");
constexpr int a=1;
return a;
}
int x=3;
constexpr int func3(){
return x; //返回全局变量
}
constexpr int func4()
{
return add(1,2);
}
int main(){
//std::cout<<func1()<<std::endl;
//std::cout<<func1()<<std::endl;
std::cout<<func3()<<std::endl;
return 0;
}
输出结果:
PS D:\C++11代码\build> cmake --build .
用于 .NET Framework 的 Microsoft (R) 生成引擎版本 16.7.0+b89cb5fde
版权所有(C) Microsoft Corporation。保留所有权利。
constexpr函数的限制.cpp
D:\C++11代码\易用性的改进\常量表达式\constexpr函数的限制.cpp(1,1): warning C4819: 该文件包含不能在当前代码页(936)中表示的字符。请将该文件保存为 Unicode 格式以防止
数据丢失 [D:\C++11代码\build\易用性的改进\常量表达式\main.vcxproj]
PS D:\C++11代码\build> cmake --build .
用于 .NET Framework 的 Microsoft (R) 生成引擎版本 16.7.0+b89cb5fde
版权所有(C) Microsoft Corporation。保留所有权利。
constexpr函数的限制.cpp
D:\C++11代码\易用性的改进\常量表达式\constexpr函数的限制.cpp(1,1): warning C4819: 该文件包含不能在当前代码页(936)中表示的字符。请将该文件保存为 Unicode 格式以防止
数据丢失 [D:\C++11代码\build\易用性的改进\常量表达式\main.vcxproj]
main.vcxproj -> D:\C++11代码\build\易用性的改进\常量表达式\Debug\main.exe
PS D:\C++11代码\build> cmake --build .
用于 .NET Framework 的 Microsoft (R) 生成引擎版本 16.7.0+b89cb5fde
版权所有(C) Microsoft Corporation。保留所有权利。
constexpr函数的限制.cpp
D:\C++11代码\易用性的改进\常量表达式\constexpr函数的限制.cpp(1,1): warning C4819: 该文件包含不能在当前代码页(936)中表示的字符。请将该文件保存为 Unicode 格式以防止
数据丢失 [D:\C++11代码\build\易用性的改进\常量表达式\main.vcxproj]
D:\C++11代码\易用性的改进\常量表达式\constexpr函数的限制.cpp(35,15): error C3615: constexpr 函数 "func4" 不会生
成常数表达式 [D:\C++11代码\buil
d\易用性的改进\常量表达式\main.vcxproj]
D:\C++11代码\易用性的改进\常量表达式\constexpr函数的限制.cpp(37,15): message : 对未定义的函数或为未声明为“constexpr”的函数的调用导致了故障 [D:\C++11代码\bu
ild\易用性的改进\常量表达式\main.vcxproj]
D:\C++11代码\易用性的改进\常量表达式\constexpr函数的限制.cpp(37,15): message : 请参见“add”的用法 [D:\C++11代码\build\易用性的改进\常量表达式\main.vc
xproj]
PS D:\C++11代码\build>
在使用前必须已有定义。
int main()
{
constexpr int func(); //函数声明, 定义放在 main 函数后面
constexpr int c = func(); //err, 无法通过编译, 在使用前必须已有定义
return 0;
}
constexpr int func()
{
return 1;
}
3.6.3 类中成员函数是常量表达式
常量表达式的构造函数有以下限制:
- 构造函数体必须为空。
- 初始化列表只能由常量表达式来赋值 。
#include<iostream>
class Date{
public:
//constexpr修饰构造函数,构造函数体必须为空
constexpr Date(int year,int month,int day)
:year_(year)
,month_(month)
,day_(day){
//构造函数体必须为空,那么不能使用赋值初始化,而是使用参数列表初始化
}
~Date(){}
constexpr int getYear(){
return year_;
}
constexpr int getMonth(){
return month_;
}
constexpr int getDay(){
return day_;
}
private:
int year_;
int month_;
int day_;
};
int main(){
Date date(2022,9,18); //必须使用常量给构造函数传参
std::cout<<date.getYear()<<std::endl;
std::cout<<date.getMonth()<<std::endl;
std::cout<<date.getDay()<<std::endl;
return 0;
}
输出结果:
PS D:\C++11代码\build\易用性的改进\常量表达式\Debug> .\main.exe
2022
9
18
PS D:\C++11代码\build\易用性的改进\常量表达式\Debug>
3.7 用户定义字面量(vs2013 不支持)
用户自定义字面值, 或者叫“自定义后缀”更直观些, 主要作用是简化代码的读写。
#include <iostream>
using namespace std;
//用户自定义字面值, 或者叫“自定义后缀”更直观些, 主要作用是简化代码的读写。
//自定义变量,名字要求operator"" xxx
long double operator"" _mm(long double x){
return x / 1000;
}
long double operator"" _m(long double x){
return x;
}
long double operator"" _km(long double x){
return x * 1000;
}
int main()
{
cout << 1.0_mm << endl; // 0.001
cout << 1.0_m << endl; // 1
cout << 1.0_km << endl; // 1000
return 0;
}
输出结果:
PS D:\C++11代码\build\易用性的改进\常量表达式\Debug> .\main.exe
0.001
1
1000
PS D:\C++11代码\build\易用性的改进\常量表达式\Debug>
根据 C++ 11 标准, 只有下面参数列表才是合法的:
//根据 C++ 11 标准, 只有下面参数列表才是合法的:
/*
char const *
unsigned long long
long double
char const *, size_t
wchar_t const *, size_t
char16_t const *, size_t
char32_t const *, size_t
*/
最后四个对于字符串相当有用, 因为第二个参数会自动推断为字符串的长度。 例如:
#include <iostream>
using namespace std;
//用户自定义字面值, 或者叫“自定义后缀”更直观些, 主要作用是简化代码的读写。
//自定义变量,名字要求operator"" xxx
long double operator"" _mm(long double x){
return x / 1000;
}
long double operator"" _m(long double x){
return x;
}
long double operator"" _km(long double x){
return x * 1000;
}
//根据 C++ 11 标准, 只有下面参数列表才是合法的:
/*
char const *
unsigned long long
long double
char const *, size_t
wchar_t const *, size_t
char16_t const *, size_t
char32_t const *, size_t
*/
//自定义变量,名字要求operato"" xxx
//只需要给第一个参数传参,第二个参数自动推算,测第一个参数的长度,给第二个参数赋值
size_t operator"" _len(char const* str,size_t n){
return n;
}
char const* operator"" _str(char const* buf,size_t n){
return buf;
}
char const* operator"" _test(char const* tmp){
return tmp;
}
int main()
{
cout << 1.0_mm << endl; // 0.001
cout << 1.0_m << endl; // 1
cout << 1.0_km << endl; // 1000
cout<<"abc "_len<<endl;
cout<<"abc d"_str<<endl;
cout<<1_test<<endl; //注意这个
return 0;
}
输出结果:
PS D:\C++11代码\build\易用性的改进\常量表达式\Debug> .\main.exe
0.001
1
1000
4
abc d
1
PS D:\C++11代码\build\易用性的改进\常量表达式\Debug>
*注意这个,对于参数 char const , 应该被称为原始字面量 raw literal 操作符。 例如 :
char const* operator"" _test(char const* tmp){
return tmp;
}
cout<<1_test<<endl; //注意这个
输出结果:
1
3.8 原生字符串字面值 (原始字符串字面值 )
原生字符串字面值(raw string literal)使用户书写的字符串“所见即所得”。 C++11 中原生字符串的声明相当简单, 只需在字符串前加入前缀, 即字母 R, 并在引号中使用括号左右标识,就可以声明该字符串字面量为原生字符串了 。
#include<iostream>
#include<string>
int main(){
std::cout<<R"(c++11 \n
fighting \n)";
std::string str=R"(love is beautiful,are you love me?
yes,i can not!\n)";
std::cout<<str<<std::endl;
return 0;
}
输出结果:
PS D:\C++11代码\build\易用性的改进\常量表达式\Debug> .\main.exe
c++11 \n
fighting \nlove is beautiful,are you love me?
yes,i can not!\n
PS D:\C++11代码\build\易用性的改进\常量表达式\Debug>
4、类的改进
4.1 继承构造(vs2013 不支持)
C++ 11 允许派生类继承基类的构造函数(默认构造函数、 复制构造函数、 移动构造函数除外) 。
#include<iostream>
class A{
public:
A(int a,int b)
:a_(a)
,b_(b)
{}
protected:
int a_;
int b_;
};
class B : public A{
public:
//通过参数列表给构造函数传参
B(int x,int y)
:A(x,y)
{}
void Display(){
std::cout<<"a= "<<a_<<" b= "<<b_<<std::endl;
}
};
int main(){
B b(10,20);
b.Display();
return 0;
}
输出结果:
PS D:\C++11代码\build\类的改进\Debug> .\main.exe
a= 10 b= 20
注意:
- 继承的构造函数只能初始化基类中的成员变量, 不能初始化派生类的成员变量。
- 如果基类的构造函数被声明为私有, 或者派生类是从基类中虚继承, 那么不能继承构造
函数。 - 一旦使用继承构造函数, 编译器不会再为派生类生成默认构造函数 。
#include<iostream>
class A{
public:
A(int a,int b)
:a_(a)
,b_(b)
{}
protected:
int a_;
int b_;
};
class B : public A{
public:
#if 0
//通过参数列表给构造函数传参
B(int x,int y)
:A(x,y)
{}
#endif
using A::A; // 继承构造函数
void Display(){
std::cout<<"a= "<<a_<<" b= "<<b_<<std::endl;
}
};
int main(){
B b(10,20);
b.Display();
return 0;
}
输出结果:
PS D:\C++11代码\build\类的改进\Debug> .\main.exe
a= 10 b= 20
4.2 委托构造
和继承构造函数类似, 委托构造函数也是 C++11 中对 C++的构造函数的一项改进, 其目的也是为了减少程序员书写构造函数的时间。
如果一个类包含多个构造函数, C++ 11 允许在一个构造函数中的定义中使用另一个构造函数, 但这必须通过初始化列表进行操作, 如下:
#include<iostream>
//继承构造函数类似, 委托构造函数也是 C++11 中对 C++的构造函数的一项改进, 其目的也是为了减少程序员书写构造函数的时间。
//如果一个类包含多个构造函数, C++ 11 允许在一个构造函数中的定义中使用另一个构造函数, 但这必须通过初始化列表进行操作, 如下:
class Test{
public:
Test()
: Test(1,'a'){
}
// Test(int x)
// : x_(x){
// }
Test(int x)
: Test(x,'b'){
}
// Test(char c)
// :c_(c){
// }
Test(char c)
:Test(100,c){
}
Test(int x,char c)
:x_(x)
,c_(c){
}
int x_;
char c_;
};
int main(){
Test test1; //Test():Test(1,'a'){}
Test test2(10); //Test(int x):Test(x,'b'){}
Test test3('c'); //Test(char c):Test(100,c){}
std::cout<<test1.x_<<std::endl;
std::cout<<test1.c_<<std::endl;
std::cout<<test2.x_<<std::endl;
std::cout<<test2.c_<<std::endl;
std::cout<<test3.x_<<std::endl;
std::cout<<test3.c_<<std::endl;
return 0;
}
输出结果:
PS D:\C++11代码\build\类的改进\Debug> .\main.exe
1
a
10
b
100
c
PS D:\C++11代码\build\类的改进\Debug>
4.3 继承控制: final 和 override
C++11 之前, 一直没有继承控制关键字, 禁用一个类的进一步衍生比较麻烦。C++ 11 添加了两个继承控制关键字: final 和 override。
- final 阻止类的进一步派生和虚函数的进一步重写。
- override 确保在派生类中声明的函数跟基类的虚函数有相同的签名 。
4.3.1 final 阻止类的进一步派生和虚函数的进一步重写
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Nie1xFFl-1664897582188)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20220918191555928.png)]
#include<iostream>
//final修饰类防止类进一步继承,修饰虚函数防止虚函数被重写
class A final{ //加上final,表明A类不能被继承,也就是不能派生出子类
public:
int a;
};
class B :public A{ //err,基类不能在派生
public:
};
class C{
public:
virtual void func() final; //加上final,修饰虚函数表示虚函数不能被重写
};
class D:public C{
public:
virtual void func(); //err,基类中的虚函数用final修饰,不能被重写
};
int main(){
return 0;
}
4.3.2 override 确保在派生类中声明的函数跟基类的虚函数有相同的签名
override 确保在派生类中声明的函数跟基类的虚函数有相同的签名 ,检查子类是否重写了基类的虚函数。
#include<iostream>
class A{
public:
//这是第一个虚函数,没有重写,不能用override修饰
virtual void func(int a,char c)=0;
};
class B :public A{
public:
//在重写虚函数地方,加上override,要求重写的虚函数与基类一样,就是检查子类虚函数有没有重写
virtual void func(int a,char c) override{
}
};
int main(){
return 0;
}
4.4 类默认函数的控制: “=default” 和 "=delete"函数
4.4.1 "=default"函数
C++ 的类有四类特殊成员函数, 它们分别是: 默认构造函数、 析构函数、 拷贝构造函数以及拷贝赋值运算符。
这些类的特殊成员函数负责创建、 初始化、 销毁, 或者拷贝类的对象。如果程序员没有显式地为一个类定义某个特殊成员函数, 而又需要用到该特殊成员函数时,则编译器会隐式的为这个类生成一个默认的特殊成员函数。
但是, 如果程序员为类显式的自定义了非默认构造函数, 编译器将不再会为它隐式地生成默认无参构造函数。
#include<iostream>
class X{
public:
X(); //只声明 外部定义
X()=default; //让编译器提供一个默认的构造函数,效率比用户高
X(int a) //写了带参的构造函数,系统不会提供默认构造函数
:a_(a)
{}
void Display(){
std::cout<<X::a_<<std::endl;
}
protected:
int a_;
};
//default只能修饰类中默认提供的成员函数:无参构造函数、拷贝构造函数、赋值运算符重载函数、析构函数
#if 0
class Y{
public:
int y()=default;
int z(int a,int b)=default;
protected:
int a_;
int b_;
};
#endif
X::X()=default; //外部定义也可以
int main(){
X obj;
X x(1);
x.Display();
return 0;
}
原本期望编译器自动生成的默认构造函数却需要程序员手动编写, 即程序员的工作量加大了。 此外, 手动编写的默认构造函数的代码执行效率比编译器自动生成的默认构造函数低。类的其它几类特殊成员函数也和默认构造函数一样, 当存在用户自定义的特殊成员函数时,编译器将不会隐式的自动生成默认特殊成员函数, 而需要程序员手动编写, 加大了程序员的工作量。 类似的, 手动编写的特殊成员函数的代码执行效率比编译器自动生成的特殊成员函数低。
C++11 标准引入了一个新特性: "=default"函数。 程序员只需在函数声明后加上“=default;”,就可将该函数声明为 "=default"函数, 编译器将为显式声明的 "=default"函数自动生成函数体。
"=default"函数特性仅适用于类的特殊成员函数, 且该特殊成员函数没有默认参数。 例如:
class X
{
public:
int f() = default; // err , 函数 f() 非类 X 的特殊成员函数
X(int, int) = default; // err , 构造函数 X(int, int) 非 X 的特殊成员函数
X(int = 1) = default; // err , 默认构造函数 X(int=1) 含有默认参数
};
"=default"函数既可以在类体里(inline) 定义, 也可以在类体外(out-of-line) 定义。 例如:
class X
{
public:
X() = default; // Inline defaulted 默认构造函数
X(const X &);
X &operator=(const X &);
~X() = default; // Inline defaulted 析构函数
};
X::X(const X &) = default; // Out-of-line defaulted 拷贝构造函数
X &X::operator=(const X &) = default; // Out-of-line defaulted 拷贝赋值操作符
4.4.2 "=delete"函数
为了能够让程序员显式的禁用某个函数, C++11 标准引入了一个新特性: "=delete"函数。 程序员只需在函数声明后上“=delete;”, 就可将该函数禁用。
#include<iostream>
//delete不同于deafult,delete适用于任何函数,deafult只适用于系统提供的默认成员函数
class X{
public:
X() {}; //无参构造函数
//X(const X & x){} //拷贝构造函数
X(const X & x)=delete; //拷贝构造函数,此函数被禁用
//X& operator=(const X& x){} //赋值运算符重载函数
X& operator=(const X& x)=delete; //赋值运算符重载函数,此函数被禁用
~X(){} //析构函数
//void* operator new(size_t size){} //重载new运算符
void* operator new(size_t size)=delete; //重载new运算符,此函数被禁用
//void* operator new[](size_t size){} //重载new[]运算符
void* operator new[](size_t size)=delete; //重载new[]运算符,此函数被禁用
};
int main(){
X x1;
X x2(x1);
X x3=x1;
X* x1=new X;
X* x2=new X[10];
return 0;
}
"=delete"函数特性还可用于禁用类的某些转换构造函数, 从而避免不期望的类型转换。
"=delete"函数特性还可以用来禁用某些用户自定义的类的 new 操作符, 从而避免在自由存储区创建类的对象。
5、模板的改进
5.1 右尖括号 > 的改进
在 C++98/03 的泛型编程中, 模板实例化有一个很繁琐的地方, 就是连续两个右尖括号(>>)会被编译解释成右移操作符, 而不是模板参数表的形式, 需要一个空格进行分割, 以避免发生编译时的错误。
#include <iostream>
template <int i>
class X{
};
template <class T>
class Y{
};
int main(){
Y<X<1>> x1; // ok, 编译成功
//template <int i>
Y<X<2>> x2; // err, 编译失败 c++98编译失败
return 0;
};
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L8cMJ5cS-1664897582189)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20220919221026454.png)]
class X
{
//...
};
template <class T>
class Y
{
//...
};
int main()
{
Y<X<10>> obj; // c++11之前,2个尖括号之间必须有空格
return 0;
}
在实例化模板时会出现连续两个右尖括号, 同样 static_cast、 dynamic_cast、 reinterpret_cast、const_cast 表达式转换时也会遇到相同的情况。 C++98 标准是让程序员在>>之间填上一个空格, 在 C++11 中, 这种限制被取消了。 在 C++11 标准中, 要求编译器对模板的右尖括号做单独处理, 使编译器能够正确判断出">>"是一个右移操作符还是模板参数表的结束标记。
5.2 模板的别名
c++11中可以用using给类型起别名。
#include<iostream>
#include<type_traits>
//通过typedef 给一个类型起别名,不是新建类型
typedef int int32;
using my_int=int ; //c++11的方式
int main(){
//is_same是判断两个类型是否一致,如果是则返回真,否则返回假
std::cout<< std::is_same<int32,my_int>::value<<std::endl;
return 0;
}
输出结果:
1
5.3 函数模板的默认模板参数
C++11 之前, 类模板是支持默认的模板参数, 却不支持函数模板的默认模板参数:
#include<iostream>
//1、 普通函数带默认参数, c++98 编译通过, c++11 编译通过
void DefParm(int m = 3) {}
//1、普通函数的默认参数
void func(int a=3){}
//2、 类模板是支持默认的模板参数, c++98 编译通过, c++11 编译通过
template <typename T = int>
class DefClass {};
//2、类模板支持默认的模板参数
template<class T,class T2=int> //类模板的模板参数必须是从右往左
class A{
};
//3、 函数模板的默认模板参数, c++98 - 编译失败, c++11 - 编译通过
template <typename T = int> void DefTempParm() {}
//3、C++11才支持,函数模板带默认的模板参数
// 函数模板的参数可以从左往右,也可以从右往左
template<class T=int,class T2> void func2(T a,T2 b){}
int main(){
return 0;
}
类模板的默认模板参数必须从右往左定义,函数模板的默认模板参数则没这个限定:
template<class T1, class T2 = int> class DefClass1;
template<class T1 = int, class T2> class DefClass2; // 无法通过编译
template<class T, int i = 0> class DefClass3;
template<int i = 0, class T> class DefClass4; // 无法通过编译
template<class T1 = int, class T2> void DefFunc1(T1 a, T2 b);
template<int i = 0, class T> void DefFunc2(T a);
6、可变参数的模板
在 C++11 之前, 类模板和函数模板只能含有固定数量的模板参数。 C++11 增强了模板功能,允许模板定义中包含 0 到任意个模板参数, 这就是可变参数模板。
可变参数模板和普通模板的语义是一样的, 只是写法上稍有区别, 声明可变参数模板时需要在 typename 或 class 后面带上省略号“…”:
#include<iostream>
#include<string>
//可变参数的函数模板
template<class...T> //T叫模板参数包
void func(T...args){ //args叫函数参数包
}
int main(){
func();
func<int>(10);
func<int,char>(10,'a');
func<int,char,std::string,double>(10,'a',"abc",1.0);
return 0;
}
省略号“…”的作用有两个:
- 声明一个参数包, 这个参数包中可以包含 0 到任意个模板参数。
- 在模板定义的右边, 可以将参数包展开成一个一个独立的参数 。
template <class... T>
void func(T... args) // T 叫模板参数包, args 叫函数参数包
{ //可变参数模板函数
}
func(); // OK: args 不含有任何实参
func(1); // OK: args 含有一个实参: int
func(2, 1.0); // OK: args 含有两个实参 int 和 double
6.1 可变参数模板函数
6.1.1 可变参数模板函数的定义
一个可变参数模板函数定义如下:sizeof…(sizeof 后面有 3 个小点) 计算变参个数 。
#include<iostream>
#include<string>
template<class...T>
void func(T...args){
//获取可变参数个数
std::cout<<"num= "<<sizeof...(args)<<std::endl;
}
int main(){
func();
func<int,char>(1,'a');
func<int ,double,std::string>(1,1.0,"girlfriend and work");
return 0;
}
输出结果:
PS D:\C++11代码> cd .\build\可变参数的模板\可变参数函数模板\计算可变参数个数\Debug\
PS D:\C++11代码\build\可变参数的模板\可变参数函数模板\计算可变参数个数\Debug> .\main.exe
num= 0
num= 2
num= 3
PS D:\C++11代码\build\可变参数的模板\可变参数函数模板\计算可变参数个数\Debug>
6.1.2参数包的展开
6.1.2.1 递归方式展开
通过递归方式展开参数包,需要提供一个参数包展开的函数和一个递归终止函数。
#include<iostream>
//递归终止函数 ,没有参数的时候就终止
void func(){
std::cout<<"empty"<<std::endl;
}
//可变参数模板
//参数包展开函数
template<class T1,class...T2>
void func(T1 first,T2...last){
std::cout<<first<<std::endl;
//递归调用函数本身
func(last...);
}
int main(){
func(1,2,3,4);
return 0;
}
输出结果:
PS D:\C++11代码\build\可变参数的模板\可变参数函数模板\参数包的展开\递归方式展开\Debug> .\main.exe
1
2
3
4
empty
PS D:\C++11代码\build\可变参数的模板\可变参数函数模板\参数包的展开\递归方式展开\Debug>
递归调用过程如下:
func(1, 2, 3, 4);
func(2, 3, 4);
func(3, 4);
func(4);
func();
6.1.2.2 非递归方式展开
#include<iostream>
template<class T>
void print(T tmp){
std::cout<<tmp<<std::endl;
}
//可变参数函数模板
template<class...T>
void expand(T...args){
//逗号运算符
//初始化列表
int a[]={(print(args),0)...};
}
int main(){
expand(1,2,3,4);
return 0;
}
输出结果:
PS D:\C++11代码\build\可变参数的模板\可变参数函数模板\参数包的展开\非递归方式展开\Debug> .\main.exe
1
2
3
4
PS D:\C++11代码\build\可变参数的模板\可变参数函数模板\参数包的展开\非递归方式展开\Debug>
expand 函数的逗号表达式: (print(args), 0), 也是按照这个执行顺序, 先执行 print(args), 再得到逗号表达式的结果 0。
同时, 通过初始化列表来初始化一个变长数组, { (print(args), 0)… }将会展开成( (print(args1),0), (print(args2), 0), (print(args3), 0), etc…), 最终会创建一个元素只都为 0 的数组 inta[sizeof…(args)] 。
6.2 可变参数模板类
6.2.1继承方式展开参数包
可变参数模板类的展开一般需要定义 2 ~ 3 个类, 包含类声明和特化的模板类:
#include<iostream>
#include<typeinfo>
//继承方式展开可变参数模板类
//1、可变参数模板声明
//2、递归继承模板类
//3、边界条件
//1、可变参数模板声明
template<class...T>
class Car{};
//2、递归继承模板类
template<class Head,class...Tail>
class Car<Head,Tail...>:public Car<Tail...>{
//递归继承本身
public:
Car(){
std::cout<<"type= "<<typeid(Head).name()<<std::endl;
}
};
//3、边界条件
template<>class Car<>{};
int main(){
Car<int,char*,double> bmw;
return 0;
}
输出结果:
PS D:\C++11代码\build\可变参数的模板\可变参数模板类\继承方式展开参数包\Debug> .\main.exe
type= double
type= char * __ptr64
type= int
PS D:\C++11代码\build\可变参数的模板\可变参数模板类\继承方式展开参数包\Debug>
6.2.2 模板递归和特化方式展开参数包
#include<iostream>
//模板递归和特化方式展开可变参数模板类
//1、可变参数模板声明
//2、递归继承模板类
//3、边界条件
//1、变长模板声明
template<int ... last>
class Test{
};
//2、变长模板类定义
template<int first,int ... last>
class Test<first,last...>{
public:
static const int val=first* Test<last...>::val;
};
//3、边界条件
template<>
class Test<>{
public:
static const int val=1;
};
int main(){
int sum=Test<2,3,4,5>::val;
std::cout<<sum<<std::endl;
return 0;
}
输出结果:
PS D:\C++11代码\build\可变参数的模板\可变参数模板类\模板递归和特化方式展开参数包\Debug> .\main.exe
120
PS D:\C++11代码\build\可变参数的模板\可变参数模板类\模板递归和特化方式展开参数包\Debug>
7 、 右值引用
7.1 左值引用、 右值引用
7.1.1 左值、 右值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XRz2sc73-1664897582190)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20220924171239938.png)]
在 C 语言中, 我们常常会提起左值(lvalue) 、 右值( rvalue) 这样的称呼。 一个最为典型的判别方法就是, 在赋值表达式中, 出现在等号左边的就是“左值”, 而在等号右边的, 则称为“右值”。 **左值与右值是C语言中的概念,但C标准并没有给出严格的区分方式,一般认为:可以放在=左边的,或者能够取地址的称为左值,只能放在=右边的,或者不能取地址的称为右值,但是也不一定完全正确。**如:
#include<iostream>
int func(){
static int tmp=10;
return tmp;
}
int main(){
//左值放左边、右值放右边 不一定正确
//左值可以去取地址、右值不可以取地址
int a=10;
int &c=a;
int b=2;
int &d=b;
std::cout<<"a= "<<a<<std::endl;
std::cout<<"c= "<<c<<std::endl;
std::cout<<"b= "<<b<<std::endl;
std::cout<<"d= "<<d<<std::endl;
int e=a+b;
std::cout<<"e= "<<e<<std::endl;
//int f=&(a+b);
int tmp=func();
std::cout<<"tmp= "<<tmp<<std::endl;
//右值 :字面常量、函数返回的非引用类型
return 0;
}
输出结果:
PS D:\C++11代码\build\7、右值引用\左值、右值\Debug> .\main.exe
a= 10
c= 10
b= 2
d= 2
e= 12
tmp= 10
PS D:\C++11代码\build\7、右值引用\左值、右值\Debug>
在这个赋值表达式中, a 就是一个左值, 而 a+b 则是一个右值。
不过 C++中还有一个被广泛认同的说法, 那就是可以取地址的、 有名字的就是左值, 反之,不能取地址的、 没有名字的就是右值。 那么这个加法赋值表达式中, &a 是允许的操作, 但&(a + b)这样的操作则不会通过编译。 因此 a 是一个左值, (a + b)是一个右值。
相对于左值, 右值表示字面常量、 表达式的运行结果是一个临时变量或者对象、 函数的非引用返回值等。
左值与右值是C语言中的概念,但C标准并没有给出严格的区分方式,一般认为:可以放在=左边的,或者能够取地址的称为左值,只能放在=右边的,或者不能取地址的称为右值,但是也不一定完全正确。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
int main()
{
int x = 1;
int y = 2;
//左值引用的定义
int a = 0;
int &b = a;
//左值引用不能引用右值,但const左值引用可以
//int& e = 10;
//int& f = x + y;
const int& e = 10;
const int& f = x + y;
//右值引用的定义
int&& c = 10;
int&& d = x + y;
//右值引用不能引用左值,但是可以引用move后左值
//int&& m = a;
int&& m = move(a);
return 0;
}
-
因此关于左值与右值的区分不是很好区分,一般认为:
-
普通类型的变量,因为有名字,可以取地址,都认为是左值。
-
const修饰的常量,不可修改,只读类型的,理论应该按照右值对待,但因为其可以取地址(如果只是const类型常量的定义,编译器不给其开辟空间,如果对该常量取地址时,编译器才为其开辟空间),C++11认为其是左值。
-
如果表达式的运行结果是一个临时变量或者对象,认为是右值。
-
如果表达式运行结果或单个变量是一个引用则认为是左值。
-
总结:
-
不能简单地通过能否放在=左侧右侧或者取地址来判断左值或者右值,要根据表达式结果或变量的性质判断,比如上述:c常量。
-
能得到引用的表达式一定能够作为引用,否则就用常引用。
-
C++11对右值进行了严格的区分::
-
C语言中的纯右值,比如:a+b, 100。
-
将亡值。比如:表达式的中间结果、函数按照值的方式进行返回。
7.1.2 右值引用概念
C++98中提出了引用的概念,引用即别名,引用变量与其引用实体公共同一块内存空间,而引用的底层是通过指针来实现的,因此使用引用,可以提高程序的可读性。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
void Swap(int& left, int & right)
{
int tmp = left;
left = right;
right = tmp;
}
为了提高程序运行效率,C++11中引入了右值引用,右值引用也是别名,但其只能对右值引用。
int add(int a, int b)
{
return a + b;
}
int main()
{
int a = 10;
int b = 20;
Swap(a, b);
const int&& ra = 10;
// 引用函数返回值,返回值是一个临时变量,为右值
int&& rRet = add(10, 20);
return 0;
}
为了与C++98中的引用进行区分,C++11将该种方式称之为右值引用。
7.1.3 左值引用、 右值引用
左值引用是对一个左值进行引用的类型, 右值引用则是对一个右值进行引用的类型。
左值引用和右值引用都是属于引用类型。 无论是声明一个左值引用还是右值引用, 都必须立即进行初始化。 而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存, 只是该对象的一个别名。
左值引用是具名变量值的别名, 而右值引用则是不具名(匿名) 变量的别名。左值引用
左值引用 :
int &a = 2; // 左值引用绑定到右值, 编译失败, err
int b = 2; // 非常量左值
const int &c = b; // 常量左值引用绑定到非常量左值, 编译通过, ok
const int d = 2; // 常量左值
const int &e = c; // 常量左值引用绑定到常量左值, 编译通过, ok
const int &b = 2; // 常量左值引用绑定到右值, 编程通过, ok
“const 类型 &”为 “万能”的引用类型, 它可以接受非常量左值、 常量左值、 右值对其进行初始化;
右值引用, 使用&&表示:
int && r1 = 22;
int x = 5;
int y = 8;
int && r2 = x + y;
T && a = ReturnRvalue();
通常情况下, 右值引用是不能够绑定到任何的左值的。
int c;
int && d = c; //err
测试示例:
void process_value(int &i) //参数为左值引用
{
cout << "LValue processed: " << i << endl;
}
void process_value(int &&i) //参数为右值引用
{
cout << "RValue processed: " << i << endl;
}
int main()
{
int a = 0;
process_value(a); // LValue processed: 0
process_value(1); // RValue processed: 1
return 0;
}
完整用例:
#include<iostream>
int& func(){
static int tmp=10;
return tmp;
}
int func2(){
return 10;
}
void test(int& tmp){
std::cout<<"左值tmp: "<<tmp<<std::endl;
}
void test(int&& tmp){
std::cout<<"右值tmp: "<<tmp<<std::endl;
}
int main(){
//非常量左值,把1000这个值存入ll变量
int ll=1000;
int& lll=ll;
//引用:给一个内存、对象起别名,定义时必须初始化
//左值引用
int a;
int &b=a; //ok
//int &c=1; //err
const int& d=a; //ok
const int& f=1; //ok
const int g =10; //g是10的别名
const int& h=g; //给g起别名
int& tmp=func(); //ok
std::cout<<"tmp= "<<tmp<<std::endl;
//const int& 万能引用
//右值引用
int&& i=10; //10是字面常量
int&& j=func2();
std::cout<<"i= "<<i<<std::endl;
std::cout<<"j= "<<j<<std::endl;
int x=10;
int y=20;
int&& z=x+y; //x+y是表达式
std::cout<<"z= "<<z<<std::endl;
int mm=20;
int* mmm=&mm;
int m=20;
//int&& n=m; //err, 不能把m左值赋值给右值
int l=100;
test(l); //左值引用
test(200); //右值引用
return 0;
}
输出结果:
PS D:\C++11代码\build\7、右值引用\左值引用、右值引用\左值引用、右值引用\Debug> .\main.exe
tmp= 10
i= 10
j= 10
z= 30
左值tmp: 100
右值tmp: 200
PS D:\C++11代码\build\7、右值引用\左值引用、右值引用\左值引用、右值引用\Debug>
7.1.4 引用与右值引用比较
在C++98中的普通引用与const引用在引用实体上的区别:
int main()
{
// 普通类型引用只能引用左值,不能引用右值
int a = 10;
int& ra1 = a; // ra为a的别名
//int& ra2 = 10; // 编译失败,因为10是右值
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
注意: 普通引用只能引用左值,不能引用右值,const引用既可引用左值,也可引用右值。
C++11中右值引用:只能引用右值,一般情况不能直接引用左值。
int main()
{
// 10纯右值,本来只是一个符号,没有具体的空间,
// 右值引用变量r1在定义过程中,编译器产生了一个临时变量,r1实际引用的是临时变量
int&& r1 = 10;
r1 = 100;
int a = 10;
int&& r2 = a; // 编译失败:右值引用不能引用左值
return 0;
}
问题:既然C++98中的const类型引用左值和右值都可以引用,那为什么C++11还要复杂的提出右值引用呢?
7.1.5 值的形式返回对象的缺陷
如果一个类中涉及到资源管理,用户必须显式提供拷贝构造、赋值运算符重载以及析构函数,否则编译器将会自动生成一个默认的,如果遇到拷贝对象或者对象之间相互赋值,就会出错,比如:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>
namespace yyw
{
class string
{
string(const char* str = "")
:_str(new char[strlen(str)+1])
{
strcpy(_str, str);
}
//s2(s1)
string(const string& s)
:_str(new char[strlen(s._str)+1])
{
strcpy(_str, s._str);
}
//s2=s1
string& operator=(const string& s)
{
if (this != &s)
{
char* _Pstr = new char[strlen(s._str) + 1];
strcpy(_Pstr, s._str);
delete[] _str;
_str = _Pstr;
}
return *this;
}
~string()
{
delete[]_str;
_str=nullptr;
}
private:
char* _str;
};
}
上述代码看起来没有什么问题,但是有一个不太尽人意的地方:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LY5jyljm-1664897582191)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20220924182007939.png)]
在operator+中:strRet在按照值返回时,必须创建一个临时对象,临时对象创建好之后,strRet就被销毁了,最后使用返回的临时对象构造s3,s3构造好之后,临时对象就被销毁了。仔细观察会发现:strRet、临时对象、s3每个对象创建后,都有自己独立的空间,而空间中存放内容也都相同,相当于创建了三个内容完全相同的对象,对于空间是一种浪费,程序的效率也会降低,而且临时对象确实作用不是很大,那能否对该种情况进行优化呢。
7.1.6 返回值优化技术
#define _CRT_SECURE_NO_WARNINGS 0
#include<iostream>
#include<string.h>
namespace my_string{
class string{
public:
//s1()
string(const char* tmp="")
:str(new char[strlen(tmp)+1]){ //普通构造函数
strcpy(str,tmp);
std::cout<<"普通构造函数 str= "<<str<<std::endl;
}
//s2(s1)
string(const string& tmp)
:str(new char[strlen(tmp.str)+1]){ //拷贝构造函数
strcpy(str,tmp.str);
std::cout<<"拷贝构造函数 tmp.str= "<<tmp.str<<std::endl;
}
//s2=s1
string& operator=(const string& tmp){ //赋值运算符重载函数
if(this!=&tmp){
char* p_str=new char[strlen(tmp.str)+1];
strcpy(p_str,tmp.str); //下次注意这里的调试
delete[] str;
str=p_str;
std::cout<<"赋值运算符重载函数 tmp.str"<<tmp.str<<std::endl;
}
return *this;
}
//~s1
~string(){ //析构函数
std::cout<<"析构函数: "<<std::endl;
if(nullptr!=str){
std::cout<<"操作delete[] str: "<<str<<std::endl;
delete[] str;
str=nullptr;
}
}
private:
char* str;
};
string func(){ //返回的普通的string对象不是引用,所以是个右值
string obj("dingding");
std::cout<<"&obj= "<<(void*)&obj<<std::endl;
return obj;
// qt, 返回值优化技术
// vs2013, debug模式,没有做返回值优化
}
}
int main(){
my_string::string tmp=my_string::func();
std::cout<<"&tmp= "<<(void*)&tmp<<std::endl;
return 0;
}
输出结果:
PS D:\C++11代码\build\7、右值引用\关于string返回值优化技术\Debug> .\main.exe
普通构造函数 str= dingding
&obj= 000000525A99F7E8
拷贝构造函数 tmp.str= dingding
析构函数:
操作delete[] str: dingding
&tmp= 000000525A99F838
析构函数:
操作delete[] str: dingding
PS D:\C++11代码\build\7、右值引用\关于string返回值优化技术\Debug>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RyWqBl9A-1664897582191)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20220924191801908.png)]
防止返回值优化选项:g++ xxx.cpp -fno-elide-constructors -std=c++11
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Izhhvpcc-1664897582192)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20220924194126430.png)]
7.2移动语义
C++11提出了移动语义概念,即:将一个对象中资源移动到另一个对象中的方式,可以有效缓解该问题。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wwuuhJ5J-1664897582193)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20220924221308844.png)]在C++11中如果需要实现移动语义,必须使用右值引用。上述String类增加移动构造:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ehe8niBT-1664897582193)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20220924221352744.png)]
因为strRet对象的生命周期在创建好临时对象后就结束了,即将亡值,C++11认为其为右值,在用strRet构造临时对象时,就会采用移动构造,即将strRet中资源转移到临时对象中。而临时对象也是右值,因此在用临时对象构造s3时,也采用移动构造,将临时对象中资源转移到s3中,整个过程,只需要创建一块堆内存即可,既省了空间,又大大提高程序运行的效率。
注意:
- 移动构造函数的参数千万不能设置成const类型的右值引用,因为资源无法转移而导致移动语义失效。
- 在C++11中,编译器会为类默认生成一个移动构造,该移动构造为浅拷贝,因此当类中涉及到资源管理
时,用户必须显式定义自己的移动构造。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TfBwz44H-1664897582194)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20220924221531169.png)]
7.2.1 为什么需要移动语义
-
右值引用是用来支持转移语义的。
-
转移语义可以将资源 ( 堆, 系统对象等 ) 从一个对象转移到另一个对象, 这样能够减少不必要的临时对象的创建、 拷贝以及销毁, 能够大幅度提高 C++ 应用程序的性能。
-
临时对象的维护 ( 创建和销毁 ) 对性能有严重影响。
-
转移语义是和拷贝语义相对的, 可以类比文件的剪切与拷贝, 当我们将文件从一个目录拷贝到另一个目录时, 速度比剪切慢很多。
-
通过转移语义, 临时对象中的资源能够转移其它的对象里。
7.2.2 移动语义定义
-
在现有的 C++ 机制中, 我们可以定义拷贝构造函数和赋值函数。 要实现转移语义, 需要定义转移构造函数, 还可以定义转移赋值操作符。 对于右值的拷贝和赋值会调用转移构造函数和转移赋值操作符。
-
如果转移构造函数和转移拷贝操作符没有定义, 那么就遵循现有的机制, 拷贝构造函数和赋值操作符会被调用。
-
普通的函数和操作符也可以利用右值引用操作符实现转移语义。
7.2.3 移动构造函数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RwSHSgwP-1664897582195)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20220924213037513.png)]
#define _CRT_SECURE_NO_WARNINGS 0
#include<iostream>
#include<string.h>
namespace my_string{
class string{
public:
//s1()
string(const char* tmp="")
:str(new char[strlen(tmp)+1]){ //普通构造函数
strcpy(str,tmp);
std::cout<<"普通构造函数: "<<str<<std::endl;
}
//s2(s1)
string(const string& tmp)
:str(new char[strlen(tmp.str)+1]){ //拷贝构造函数
strcpy(str,tmp.str);
std::cout<<"拷贝构造函数: "<<tmp.str<<std::endl;
}
//s2=s1
string& operator=(const string& tmp){ //赋值运算符重载函数
if(&tmp!=this){
char* p_str=new char[strlen(tmp.str)+1];
strcpy(p_str,tmp.str); //下次注意这里的调试
//先释放原来的内存
delete[] str;
str=p_str;
std::cout<<"赋值运算符重载函数: "<<tmp.str<<std::endl;
}
return *this;
}
//~s1
~string(){
std::cout<<"析构函数: "<<std::endl;
if(nullptr!=str){
std::cout<<"操作析构函数operator delete[]: "<<str<<std::endl;
delete[] str;
str=nullptr;
}
}
//移动构造函数
//参数是非const的右值引用,因为要改变右值
string(string&& tmp){
str=tmp.str; //拷贝地址,没有重新申请内存,类似于浅拷贝
//原来指针设置为空
tmp.str=nullptr;
std::cout<<"移动构造函数: "<<std::endl;
}
private:
char* str;
};
string func(){ //返回的是普通的string对象,不是&的对象,所以是右值
string obj("dingding");
return obj;
}
}
int main(){
my_string::string&& str=my_string::func(); //右值引用来接受
return 0;
}
输出结果:
PS D:\C++11代码\build\7、右值引用\移动语义\Debug> .\main.exe
普通构造函数: dingding
移动构造函数:
析构函数:
析构函数:
操作析构函数operator delete[]: dingding
PS D:\C++11代码\build\7、右值引用\移动语义\Debug>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3ZW2HPnT-1664897582195)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20220924204731202.png)]
和拷贝构造函数类似, 有几点需要注意:
- 参数( 右值) 的符号必须是右值引用符号, 即“&&”。
- 参数( 右值) 不可以是const常量, 因为我们需要修改右值。
- 参数( 右值) 的资源链接和标记必须修改, 否则, 右值的析构函数就会释放资源, 转移
到新对象的资源也就无效了。
有了右值引用和转移语义, 我们在设计和实现类时, 对于需要动态申请大量资源的类, 应该
设计转移构造函数和转移赋值函数, 以提高应用程序的效率。
7.2.4 移动赋值运算符重载函数
普通的赋值运算符重载函数:
#define _CRT_SECURE_NO_WARNINGS 0
#include<iostream>
#include<string.h>
namespace my_string{
class string{
public:
//s1()
string(const char* tmp="")
:str(new char[strlen(tmp)+1]){ //普通构造函数
strcpy(str,tmp);
std::cout<<"普通构造函数: "<<str<<std::endl;
}
//s2(s1)
string(const string& tmp)
:str(new char[strlen(tmp.str)+1]){ //拷贝构造函数
strcpy(str,tmp.str);
std::cout<<"拷贝构造函数: "<<tmp.str<<std::endl;
}
//s2=s1
// const 是因为对 s 不需要修改,安全性更高
// 参数 & 是因为不需要传值拷贝、效率高
// 返回值 & 是为了连续赋值(效率高)
string& operator=(const string& tmp){ //赋值运算符重载函数
//检查是否自己给自己赋值
if(this!=&tmp){
char* p_str=new char[strlen(tmp.str)+1];
strcpy(p_str,tmp.str); //下次注意这里的调试 //把tmp的数据拷贝到新开辟的空间
//先释放原来的内存
delete[] str; //把this指向的空间释放掉
str=p_str; //指向新空间
std::cout<<"赋值运算符重载函数: "<<tmp.str<<std::endl;
}
return *this;
}
//~s1
~string(){
std::cout<<"析构函数: "<<std::endl;
if(nullptr!=str){
std::cout<<"操作析构函数operator delete[]: "<<str<<std::endl;
delete[] str;
str=nullptr;
}
}
//移动构造函数
//参数是非const的右值引用,因为要改变右值
string(string&& tmp){
str=tmp.str; //拷贝地址,没有重新申请内存,类似于浅拷贝
//原来指针设置为空
tmp.str=nullptr;
std::cout<<"移动构造函数: "<<std::endl;
}
private:
char* str;
};
string func(){
string obj("dingding");
return obj;
}
}
int main(){
my_string::string tmp("abc"); //实例化一个对象
tmp=my_string::func();
return 0;
}
输出结果:
PS D:\C++11代码\build\7、右值引用\移动语义\Debug> .\main.exe
普通构造函数: abc
普通构造函数: dingding
移动构造函数:
析构函数
赋值运算符重载函数: dingding
析构函数
操作析构函数operator delete[]: dingding
析构函数
操作析构函数operator delete[]: dingding
PS D:\C++11代码\build\7、右值引用\移动语义\Debug>
移动赋值运算符重载函数:
#define _CRT_SECURE_NO_WARNINGS 0
#include<iostream>
#include<string.h>
namespace my_string{
class string{
public:
//s1()
string(const char* tmp="")
:str(new char[strlen(tmp)+1]){ //普通构造函数
strcpy(str,tmp);
std::cout<<"普通构造函数: "<<str<<std::endl;
}
//s2(s1)
string(const string& tmp)
:str(new char[strlen(tmp.str)+1]){ //拷贝构造函数
strcpy(str,tmp.str);
std::cout<<"拷贝构造函数: "<<tmp.str<<std::endl;
}
//s2=s1
// const 是因为对 s 不需要修改,安全性更高
// 参数 & 是因为不需要传值拷贝、效率高
// 返回值 & 是为了连续赋值(效率高)
string& operator=(const string& tmp){ //赋值运算符重载函数
//检查是否自己给自己赋值
if(&tmp!=this){
char* p_str=new char[strlen(tmp.str)+1];
strcpy(p_str,tmp.str); //下次注意这里的调试 //把tmp的数据拷贝到新开辟的空间
//先释放原来的内存
delete[] str; //把this指向的空间释放掉
str=p_str; //指向新空间
std::cout<<"赋值运算符重载函数: "<<tmp.str<<std::endl;
}
return *this;
}
//~s1
~string(){
std::cout<<"析构函数"<<std::endl;
if(nullptr!=str){
std::cout<<"操作析构函数operator delete[]: "<<str<<std::endl;
delete[] str;
str=nullptr;
}
}
//移动构造函数
//参数是非const的右值引用,因为要改变右值
string(string&& tmp){
str=tmp.str; //拷贝地址,没有重新申请内存,类似于浅拷贝
//原来指针设置为空
tmp.str=nullptr;
std::cout<<"移动构造函数: "<<std::endl;
}
//移动赋值运算符重载函数
string& operator=(string&& tmp){
if(&tmp!=this){
//先释放原来的内存
delete[] str;
//无需申请堆内存空间
str=tmp.str; //地址赋值
tmp.str=nullptr;
std::cout<<"移动赋值运算符重载函数"<<std::endl;
return *this;
}
}
private:
char* str;
};
string func(){
string obj("dingding");
return obj;
}
}
int main(){
my_string::string tmp("abc"); //实例化一个对象
tmp=my_string::func();
return 0;
}
输出结果:
PS D:\C++11代码\build\7、右值引用\移动语义\Debug> .\main.exe
普通构造函数: abc
普通构造函数: dingding
移动构造函数:
析构函数
移动赋值运算符重载函数
析构函数
析构函数
操作析构函数operator delete[]: dingding
PS D:\C++11代码\build\7、右值引用\移动语义\Debug>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EAHuB2C6-1664897582196)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20220924220434741.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IUwNazeP-1664897582197)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20220924220515911.png)]
7.3 标准库函数move—右值引用左值
既然编译器只对右值引用才能调用转移构造函数和转移赋值函数, 而所有命名对象都只能是左值引用, 如果已知一个命名对象不再被使用而想对它调用转移构造函数和转移赋值函数,也就是把一个左值引用当做右值引用来使用, 怎么做呢? 标准库提供了函数 std::move, 这个函数以非常简单的方式将左值引用转换为右值引用。
#include<iostream>
int main(){
int a=10;
//int&& b=a; //err,右值不能引用左值
int&&c =std::move(a); //ok,使用std::move可以右值引用左值
std::cout<<"a= "<<a<<std::endl;
std::cout<<"c= "<<c<<std::endl;
return 0;
}
输出结果:
PS D:\C++11代码\build\7、右值引用\标准库函数move\Debug> .\main.exe
a= 10
c= 10
PS D:\C++11代码\build\7、右值引用\标准库函数move\Debug>
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DfkeD17k-1664897582197)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20220924222154738.png)]
int main()
{
String s1("hello world");
String s2(move(s1));
String s3(s2);
return 0;
}
注意:以上代码是move函数的经典的误用,因为move将s1转化为右值后,在实现s2的拷贝时就会使用移动构造,此时s1的资源就被转移到s2中,s1就成为了无效的字符串,这里最后s1和s2都成为无效字符串。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kfwiU6ON-1664897582198)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20220924222231795.png)]
7.4 完美转发 std::forward
完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。
完美转发适用于这样的场景: 需要将一组参数原封不动的传递给另一个函数。“原封不动”不仅仅是参数的值不变, 在 C++ 中, 除了参数值之外, 还有一下两组属性: 左值/ 右值和 const/non-const。 完美转发就是在参数传递过程中, 所有这些属性和参数值都不能改变, 同时, 而不产生额外的开销, 就好像转发者不存在一样。 在泛型函数中, 这样的需求非常普遍。
#include<iostream>
template<class T>
void func(const T& tmp){
std::cout<<"const T& tmp"<<std::endl;
}
template<class T>
void func(T& tmp){
std::cout<<"T& tmp"<<std::endl;
}
//函数 forward_value 是一个泛型函数, 它将一个参数传递给另一个函数 process_value
template<class T>
void forward_val(const T& tmp){
func(tmp);
}
template<class T>
void forward_val(T& tmp){
func(tmp);
}
int main(){
int a=0;
const int& b=a;
//需要给forward_val重载两个版本,const T& ,T&
forward_val(a); //T&
forward_val(b); //const T&
forward_val(100); //const T&
return 0;
}
输出结果:
PS D:\C++11代码\build\7、右值引用\完美转发forward\Debug> .\main.exe
T& tmp
const T& tmp
const T& tmp
PS D:\C++11代码\build\7、右值引用\完美转发forward\Debug>
对于一个参数就要重载两次, 也就是函数重载的次数和参数的个数是一个正比的关系。 这个函数的定义次数对于程序员来说, 是非常低效的。
那 C++11 是如何解决完美转发的问题的呢? 实际上, C++11 是通过引入一条所谓“引用折叠”(reference collapsing) 的新语言规则, 并结合新的模板推导规则来完成完美转发。
typedef const int T;
typedef T & TR;
TR &v = 1; //在 C++11 中, 一旦出现了这样的表达式, 就会发生引用折叠, 即将复杂的未知
表达式折叠为已知的简单表达式
C++11 中的引用折叠规则:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZS6gKpo0-1664897582198)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20220924225110927.png)]
一旦定义中出现了左值引用, 引用折叠总是优先将其折叠为左值引用。C++11 中, std::forward 可以保存参数的左值或右值特性:
#include<iostream>
template<class T>
void func(const T& tmp){
std::cout<<"const T& tmp"<<std::endl;
}
template<class T>
void func(T& tmp){
std::cout<<"T& tmp"<<std::endl;
}
//函数 forward_value 是一个泛型函数, 它将一个参数传递给另一个函数 process_value
// template<class T>
// void forward_val(const T& tmp){
// func(tmp);
// }
// template<class T>
// void forward_val(T& tmp){
// func(tmp);
// }
template<class T>
void func(T&& ){
std::cout<<"T&& "<<std::endl;
}
template<class T>
void forward_val(T&& tmp){ //参数为右值引用
//std::forward保存参数的左值、右值属性
func(std::forward<T>(tmp)); //定义
}
int main(){
int a=0;
const int& b=a;
//需要给forward_val重载两个版本,const T& ,T&
forward_val(a); //T&
forward_val(b); //const T&
forward_val(100); //const T&
return 0;
}
输出结果:
PS D:\C++11代码\build\7、右值引用\完美转发forward\Debug> .\main.exe
T& tmp
const T& tmp
T&&
PS D:\C++11代码\build\7、右值引用\完美转发forward\Debug>
7.5 右值引用作用
C++98中引用作用:因为引用是一个别名,需要用指针操作的地方,可以使用指针来代替,可以提高代码的可读性以及安全性。
- C++11中右值引用主要有以下作用:
- 实现移动语义(移动构造与移动赋值)。
- 给中间临时变量取别名。
- 实现完美转发。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fwfJLWJb-1664897582199)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20220924222359169.png)]
Microsoft Windows [版本 10.0.19042.1706]
(c) Microsoft Corporation。保留所有权利。
D:\C++11代码\7、右值引用\移动语义>g++ 移动构造函数.cpp -fno-elide-constructors -std=c++11
'g++' 不是内部或外部命令,也不是可运行的程序
或批处理文件。
D:\C++11代码\7、右值引用\移动语义>yum install g++
'yum' 不是内部或外部命令,也不是可运行的程序
或批处理文件。
D:\C++11代码\7、右值引用\移动语义>-fno-elide-constructors -std=c++11^Z
D:\C++11代码\7、右值引用\移动语义>g++ 移动构造函数.cpp -fno-elide-constructors -std=c++11
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OmAXvoGh-1664897582199)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20220924225751915.png)]
7.6 explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有类型转换的作用。
代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
class Date
{
public:
Date(int year)
:_year(year)
{
}
void Display()
{
std::cout << _year <<std::endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2018);
d1.Display();
return 0;
}
Date d1(2018)调用构造函数
d1 = 2019;
d1.Display();
在这里同样打印出了d1,而且d1的值被改成了2019,这就是单参构造函数的隐式转换。语法上的d1=2019是先构造,在拷贝构造:
Date tmp(2019);
Date d1(tmp);
所以我们明白,早期的编译器遇到Date d1 = 2019,那么先构造一个临时对象tmp,然后用临时对象拷贝构造d1;然而现在的编译器已经做了优化,当遇到Date d1 = 2019,会根据Date d1(2019)来,这就是隐式类型转换。
所以加了explicit关键字后将会禁止单参构造函数的隐式转换。
在C语言中我们早就讲到了隐式类型转换。
int c=10;
double d=1.11;
d=c
我们可以知道这里是double类型创建出一个临时变量接收c,把临时变量的值在给d。
8、 智能指针
智能指针的类型
- auto_ptr //管理权转移, C++98。
- unique_ptr //防拷贝, C++11。
- shared_ptr //共享拷贝, C++11。
注意:shared_ptr有时会配合weak_ptr一起使用,但weak_ptr不是智能指针。
8.1 RAII
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W8LhXqny-1664897582200)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20220925174653821.png)]
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。
智能指针的底层实现重载了部分符号,使得智能指针具有指针访问数据的行为。
// 具有指针类似的行为
T &operator*()
{
return *_ptr;
}
T *operator->()
{
return _ptr;
}
T *Get()
{
return _ptr;
}
// 让该类对象具有指针类似的操作就可以了
T &operator*()
{
return *_ptr;
}
// -> 只能指针指向对象或者结构体的这些场景中
T *operator->()
{
return _ptr;
}
上述的Ptr还能将其称为智能指针,因为它还具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此:AutoPtr模板类中还得需要将 * 、->重载下,才可让其像指针一样去使用。
- 总结一下智能指针的原理:
- RAII特性。
- 重载operator*和opertaor->,具有像指针一样的行为。
C++11 中有 unique_ptr、 shared_ptr 与 weak_ptr 等智能指针(smart pointer), 定义在中。 可以对动态资源进行管理, 保证任何情况下, 已构造的对象最终会销毁, 即它的析构函数最终会被调用。
8.2 C++98中的auto_ptr
auto_ptr文档
C++98版本的库中就提供了auto_ptr的智能指针。下面演示的auto_ptr的使用及问题。
// C++库中的智能指针都定义在memory这个头文件中
#include <memory>
class Date
{
public:
Date() {
cout << "Date()" << endl; }
~Date(){
cout << "~Date()" << endl; }
int _year;
int _month;
int _day;
};
int main()
{
auto_ptr<Date> ap(new Date);
auto_ptr<Date> copy(ap);
// auto_ptr的问题:当对象拷贝或者赋值后,前面的对象就悬空了
// C++98中设计的auto_ptr问题是非常明显的,所以实际中很多公司明确规定了不能使用auto_ptr
ap->_year = 2018;
return 0;
}
8.3 unique_ptr
C++11中开始提供更靠谱的unique_ptr。unique_ptr的实现原理:简单粗暴的防拷贝,防赋值运算符重载。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
#include<memory>
int main()
{
unique_ptr<int> up1(new int);
// unique_ptr的设计思路非常的粗暴-防拷贝,也就是不让拷贝和赋值
unique_ptr<int> up2(up1);
unique_ptr<int> up3(new int);
up3 = up1;
return 0;
}
unique_ptr 持有对对象的独有权, 同一时刻只能有一个 unique_ptr 指向给定对象(通过禁止拷贝语义、 只有移动语义来实现) 。
unique_ptr 指针本身的生命周期: 从 unique_ptr 指针创建时开始, 直到离开作用域。
离开作用域时, 若其指向对象, 则将其所指对象销毁(默认使用 delete 操作符, 用户可指定其他操作)。
#include<iostream>
#include<memory.h>
class Test{
public:
~Test(){
std::cout<<"~Test()"<<std::endl;
}
};
int main(){
std::unique_ptr<int> up1(new int(10)); //创建智能指针对象
std::cout<<*up1<<std::endl;
{
std::unique_ptr<Test> up2(new Test); //无需释放,自动释放
//人为指定释放,多次释放也没有关系
up2=nullptr;
up2=NULL;
up2.reset();
up2.reset();
}
std::cout<<"1111"<<std::endl;
return 0;
}
输出结果:
PS D:\C++11代码\build\8、智能指针\unique_ptr\Debug> .\main.exe
10
~Test()
1111
PS D:\C++11代码\build\8、智能指针\unique_ptr\Debug>
#include<iostream>
#include<memory>
int main(){
std::unique_ptr<int> up1(new int(10)); //创建智能指针对象,up1是唯一使用者
//std::unique_ptr<int>up2(up1); //unique_ptr智能指针禁止拷贝构造
//std::unique_ptr<int>up1=up1; //unique_ptr智能指针禁止赋值运算符重载
//可以使用移动构造或者移动赋值运算符重载转移up1的使用权,然后up1不能在使用
//这里up2使用移动赋值运算符重载之后不能在使用up1操作测堆内存空间
std::unique_ptr<int>up2(std::move(up1));
std::cout<<"*up2= "<<*up2<<std::endl;
//这里up2使用移动赋值运算符重载之后不能在使用up2操作测堆内存空间
std::unique_ptr<int>up3=std::move(up2);
std::cout<<"*up3= "<<*up3<<std::endl;
return 0;
}
输出结果:
PS D:\C++11代码\build\8、智能指针\unique_ptr\Debug> .\main.exe
*up2= 10
*up3= 10
PS D:\C++11代码\build\8、智能指针\unique_ptr\Debug>
#include<iostream>
#include<memory>
int main(){
std::unique_ptr<int> up1(new int(10)); ///创建智能指针对象, up1是唯一使用者
std::cout<<"*up1= "<<*up1<<std::endl;
up1.reset(); //reset函数如果是无参,作用是显式释放堆区内容
//std::cout<<"*up1= "<<*up1<<std::endl;
up1.reset(new int(20)); //reset函数如果有参数,先释放原来堆区内容,重新给up1绑定一个新的堆区内容
std::cout<<"*up1= "<<*up1<<std::endl;
return 0;
}
输出结果:
PS D:\C++11代码\build\8、智能指针\unique_ptr\Debug> .\main.exe
*up1= 10
*up1= 20
#include<iostream>
#include<memory>
int main(){
std::unique_ptr<int> up1(new int(10));
//释放控制权,不释放堆区内存
int*p =up1.release();
std::cout<<"*p= "<<*p<<std::endl;
//std::cout<<"*up1= "<<*up1<<std::endl;
//最后要释放新的控制权
delete p;
return 0;
}
输出结果:
PS D:\C++11代码\build\8、智能指针\unique_ptr\Debug> .\main.exe
*p= 10
PS D:\C++11代码\build\8、智能指针\unique_ptr\Debug>
8.4 shared_ptr
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
#include<memory>
int main()
{
// shared_ptr通过引用计数支持智能指针对象的拷贝
shared_ptr<int> sp1(new int);
shared_ptr<int> sp2(sp1);
sp2 = sp1;
return 0;
}
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。例如:老师晚上在下班之前都会通知,让最后走的学生记得把门锁下。
- shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
- 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
- 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
shared_ptr 允许多个该智能指针共享第“拥有”同一堆分配对象的内存, 这通过引用计数(reference counting) 实现, 会记录有多少个 shared_ptr 共同指向一个对象, 一旦最后一个这样的指针被销毁, 也就是一旦某个对象的引用计数变为 0, 这个对象会被自动删除。
#include<iostream>
#include<memory>
int main(){
std::shared_ptr<int> sp1(new int(10));
std::shared_ptr<int> sp2(sp1); //拷贝构造, 有2个对象和堆区空间绑定
std::shared_ptr<int> sp3(sp2); //赋值运算符重载, 有2个对象和堆区空间绑定
std::cout<<"*sp1= "<<*sp1<<std::endl;
std::cout<<"num1= "<<sp1.use_count()<<std::endl; //打印计数器
std::cout<<"*sp2= "<<*sp2<<std::endl;
sp2.reset(); //释放sp2, 只是计数器减去1, 堆区空间没有释放
std::cout<<"num2= "<<sp1.use_count()<<std::endl; //打印计数器
std::cout<<"*sp3= "<<*sp3<<std::endl;
sp3.reset(); //释放sp2, 只是计数器减去1, 堆区空间没有释放
std::cout<<"num3= "<<sp1.use_count()<<std::endl; //打印计数器
//std::cout<<"*sp3"<<*sp3<<std::endl;
return 0;
}
输出结果:
PS D:\C++11代码\build\8、智能指针\shared_ptr\Debug> .\main.exe
*sp1= 10
num1= 3
*sp2= 10
num2= 2
*sp3= 10
num3= 1
PS D:\C++11代码\build\8、智能指针\shared_ptr\Debug>
8.5 weak_prt
struct ListNode
{
~ListNode()
{
std::cout << "~ListNode()" << std::endl;
}
//在使用智能指针的时候,为了让智能指针能够使用_prev/_next指向某一个智能指针的对象,
//这里需要把它们定义为2个智能指针类型的对象
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
int _data;
};
int main()
{
shared_ptr<ListNode> sp1(new ListNode);
shared_ptr<ListNode> sp2(new ListNode);
sp1->_next = sp2; //sp2的引用计数++
sp2->_prev = sp1; //sp1的引用计数++
std::cout << "~~~开始调用析构函数~~~" << std::endl;
return 0;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ULdS922c-1664897582201)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20220925185443415.png)]
std::shared_ptr的线程安全问题,通过下面的程序我们来测试shared_ptr的线程安全问题。需要注意的是shared_ptr的线程安全分为两方面:
- 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、–是需要加锁的,也就是说引用计数的操作是线程安全的。
- 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。
循环引用分析:
- node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
- node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
- node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
- 也就是说_next析构了,node2就释放了。
- 也就是说_prev析构了,node1就释放了。
- 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放。
#include <iostream>
#include <memory>
int main()
{
std::shared_ptr<int> sp1(new int(10));
std::shared_ptr<int> sp2 = sp1;
std::shared_ptr<int> sp3(sp2); //有3个对象绑定堆区内容
std::weak_ptr<int> wp1 = sp1;
std::cout << "num= " << sp1.use_count() << std::endl; //打印计数器
std::cout << "num= " << sp2.use_count() << std::endl; //打印计数器
std::cout << "num= " << sp3.use_count() << std::endl; //打印计数器
std::cout << "num= " << wp1.use_count() << std::endl; //打印计数器
// weak_ptr虽然不和堆区空间绑定,可以通过lock函数获取shared_ptr<int>对象
std::shared_ptr<int> wp2 = wp1.lock(); //有4个对象绑定堆区内容
std::cout << "num= " << sp1.use_count() << std::endl; //打印计数器
std::cout << "num= " << sp2.use_count() << std::endl; //打印计数器
std::cout << "num= " << sp3.use_count() << std::endl; //打印计数器
std::cout << "num= " << wp1.use_count() << std::endl; //打印计数器
std::cout<<"*sp1= "<<*sp1<<" ,"<<"*sp2= "<<*sp2<<" ,"<<"*sp3= "<<*sp3<<" ,"<<"*wp2= "<<*wp2<<std::endl;
sp1.reset(); //这里要把其他引用计数的也释放掉才可以调用delete释放堆区空间
std::cout << "num= " << sp1.use_count() << std::endl; //打印计数器
sp2.reset();
sp3.reset();
wp2.reset();
std::shared_ptr<int> tmp=wp1.lock();
if(nullptr==tmp){
std::cout<<"堆区空间已释放\n"<<std::endl;
}
return 0;
}
输出结果:
PS D:\C++11代码\build\8、智能指针\shared_ptr\Debug> .\main.exe
num= 3
num= 3
num= 3
num= 3
num= 4
num= 4
num= 4
num= 4
*sp1= 10 ,*sp2= 10 ,*sp3= 10 ,*wp2= 10
num= 0
堆区空间已释放
PS D:\C++11代码\build\8、智能指针\shared_ptr\Debug>
9、 闭包的实现
9.1什么是闭包
闭包有很多种定义, 一种说法是, 闭包是带有上下文的函数。 说白了, 就是有状态的函数,就是有自己的变量。更直接一些, 不就是个类吗? 换了个名字而已。
一个函数, 带上了一个状态, 就变成了闭包了。 那什么叫 “带上状态” 呢? 意思是这个闭包有属于自己的变量, 这些个变量的值是创建闭包的时候设置的, 并在调用闭包的时候, 可以访问这些变量。
函数是代码, 状态是一组变量, 将代码和一组变量捆绑 (bind) , 就形成了闭包。
闭包的状态捆绑, 必须发生在运行时。
9.2 闭包的实现
9.2.1 仿函数:重载 operator()
#include<iostream>
//仿函数,重载operator()
class MyFunctor{
public:
MyFunctor(int tmp)
:round_(tmp)
{}
int operator()(int tmp){
return round_+tmp;
}
private:
int round_; //round_就是这个闭包的状态
};
int main(){
int round=2;
MyFunctor mf(round); //调用构造函数
//调用仿函数
std::cout<<"result= "<<mf(1)<<std::endl; //operator()(int tmp)
return 0;
}
输出结果:
PS C:\Users\Administrator\Desktop\闭包的实现\build\仿函数重载operator括号\Debug> .\main.exe
result= 3
PS C:\Users\Administrator\Desktop\闭包的实现\build\仿函数重载operator括号\Debug>
std::function 对象最大的用处就是在实现函数回调, 使用者需要注意, 它不能被用来检查相等或者不相等, 但是可以与 NULL 或者 nullptr 进行比较。
9.2.2 std::bind 绑定器
9.2.2.1 std::function
在 C++中, 可调用实体主要包括: 函数、 函数指针、 函数引用、 可以隐式转换为函数指定的对象, 或者实现了 opetator()的对象。
C++11 中, 新增加了一个 std::function 类模板, 它是对 C++中现有的可调用实体的一种类型安全的包裹。 通过指定它的模板参数, 它可以用统一的方式处理函数、 函数对象、 函数指针,并允许保存和延迟执行它们。
#include<iostream>
#include<functional>
//1、普通函数
void Func(){
std::cout<<__LINE__<<":"<<__func__<<std::endl;
}
//2、类中静态函数
class Test{
public:
static int Func(int tmp){
std::cout<<__LINE__<<":"<<__func__<<"("<<tmp<<")->:";
return tmp;
}
};
//3、类中仿函数
class MyFunctor{
public:
MyFunctor(){}
MyFunctor(int tmp)
:round_(tmp)
{}
int operator()(int tmp){
std::cout<<__LINE__<<":"<<__func__<<"("<<tmp<<")->:";
return tmp+round_;
}
private:
int round_;
};
int main(){
//1、绑定普通函数
std::function<void(void)> f1=Func;
f1(); //等价于 func()
//2、绑定类中的静态函数
std::function<int(int)> f2=Test::Func;
std::cout<<f2(10)<<std::endl; // Test::Func(10)
//3、绑定类中的仿函数,绑定对象,仿函数调用obj
MyFunctor obj;
std::function<int(int)> f3=obj;
std::cout<<f3(20)<<std::endl;
return 0;
}
输出结果:
PS D:\C++11代码\build\9、闭包的实现\bind绑定器\function函数\Debug> .\main.exe
5:Func
13:Func(10)->:10
26:operator ()(20)->:-858993440
PS D:\C++11代码\build\9、闭包的实现\bind绑定器\function函数\Debug>
9.2.2.2 std::bind
std::bind 是这样一种机制, 它可以预先把指定可调用实体的某些参数绑定到已有的变量, 产生一个新的可调用实体, 这种机制在回调函数的使用过程中也颇为有用。
C++98 中, 有两个函数 bind1st 和 bind2nd, 它们分别可以用来绑定 functor 的第一个和第二个参数, 它们都是只可以绑定一个参数, 各种限制, 使得 bind1st 和 bind2nd 的可用性大大降低。
在 C++11 中, 提供了 std::bind, 它绑定的参数的个数不受限制, 绑定的具体哪些参数也不受限制, 由用户指定, 这个 bind 才是真正意义上的绑定。
std::bind 的基本语法:
#include<iostream>
#include<functional>
void Func(int x,int y)
{
std::cout<<x<<" "<<y<<std::endl;
}
int main(){
std::bind(Func,10,20)(); //输出10,20
std::bind(Func,std::placeholders::_1,std::placeholders::_2)(11,21); //输出11,21
using namespace std::placeholders;
std::bind(Func,_1,11)(10,20,30); //输出10,11
std::bind(Func,_1,_2)(11,21); //输出11,21
std::bind(Func,_2,_1)(12,22); //先输出_2就是22,在输出_1就是12
//std::bind(Func,_2,22)(11); //err,参数不匹配,没有第二个参数
std::bind(Func,_2,22)(11,0); //输出0,22
std::bind(Func,_3,22)(0,1,2); //输出3,22
return 0;
}
输出结果:
PS D:\C++11代码\build\9、闭包的实现\bind绑定器\bind函数\Debug> .\main.exe
10 20
11 21
10 11
11 21
22 12
0 22
2 22
PS D:\C++11代码\build\9、闭包的实现\bind绑定器\bind函数\Debug>
std::placeholders::_1 是一个占位符, 代表这个位置将在函数调用时, 被传入的第一个参数所替代。
9.2.2.3 std::bind 和 std::function的配合使用
#include<iostream>
#include<functional>
using namespace std::placeholders;
class Func{
public:
void func(int x,int y){
//成员函数
std::cout<<x<<" "<<y<<std::endl;
}
int a_; //成员变量
};
int main(){
Func obj; //创建对象
//绑定成员函数
std::function<void(int,int)> f1=std::bind(&Func::func,&obj,_1,_2);
f1(11,22); //obj.func(11,22)
std::function<int&()> f2=std::bind(&Func::a_,&obj);
f2()=11; //obj.a_=11
std::cout<<f2()<<std::endl;
return 0;
}
输出结果:
PS D:\C++11代码\build\9、闭包的实现\bind绑定器\function与bind配合使用\Debug> .\main.exe
11 22
11
PS D:\C++11代码\build\9、闭包的实现\bind绑定器\function与bind配合使用\Debug>
通过 std::bind 和 std::function 配合使用, 所有的可调用对象均有了统一的操作方法。
9.2.3 lambda表达式
9.2.3.1 lambad 基础使用
lambda 表达式(lambda expression)是一个匿名函数, lambda 表达式基于数学中的 λ 演算得名。
C++11 中的 lambda 表达式用于定义并创建匿名的函数对象, 以简化编程工作。
lambda 表达式的基本构成:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vEG8JWf5-1664897582202)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20220927211855789.png)]
① 函数对象参数
[], 标识一个 lambda 的开始, 这部分必须存在, 不能省略。 函数对象参数是传递给编
译器自动生成的函数对象类的构造函数的。 函数对象参数只能使用那些到定义 lambda
为止时 lambda 所在作用范围内可见的局部变量(包括 lambda 所在类的 this) 。 函数对
象参数有以下形式:
- 空。 没有使用任何函数对象参数。
- =。 函数体内可以使用 lambda 所在作用范围内所有可见的局部变量( 包括 lambda
所在类的 this) , 并且是值传递方式(相当于编译器自动为我们按值传递了所有局
部变量) 。 - &。 函数体内可以使用 lambda 所在作用范围内所有可见的局部变量(包括 lambda
所在类的 this) , 并且是引用传递方式(相当于编译器自动为我们按引用传递了所
有局部变量) 。 - this。 函数体内可以使用 lambda 所在类中的成员变量。
- a。 将 a 按值进行传递。 按值进行传递时, 函数体内不能修改传递进来的 a 的拷贝,
因为默认情况下函数是 const 的。 要修改传递进来的 a 的拷贝, 可以添加 mutable
修饰符。 - &a。 将 a 按引用进行传递。
- a, &b。 将 a 按值进行传递, b 按引用进行传递。
- =, &a, &b。 除 a 和 b 按引用进行传递外, 其他参数都按值进行传递。
- &, a, b。 除 a 和 b 按值进行传递外, 其他参数都按引用进行传递。
② 操作符重载函数参数
标识重载的()操作符的参数, 没有参数时, 这部分可以省略。 参数可以通过按值(如:
(a,b)) 和按引用(如: (&a,&b)) 两种方式进行传递。
③ 可修改标示符
mutable 声明, 这部分可以省略。 按值传递函数对象参数时, 加上 mutable 修饰符后,
可以修改按值传递进来的拷贝(注意是能修改拷贝, 而不是值本身) 。
④ 错误抛出标示符
exception 声明, 这部分也可以省略。 exception 声明用于指定函数抛出的异常, 如抛出
整数类型的异常, 可以使用 throw(int)
⑤ 函数返回值
->返回值类型, 标识函数返回值的类型, 当返回值为 void, 或者函数体中只有一处 return
的地方(此时编译器可以自动推断出返回值类型) 时, 这部分可以省略。
⑥ 是函数体
{}, 标识函数的实现, 这部分不能省略, 但函数体可以为空。
#include<iostream>
int tmp = 10;
class Test
{
public:
Test(int a=100)
:a_(a){}
void Func(){
int aaa;
// auto f1=[](){ //err, []为空,没有捕获任何变量
// std::cout<<"a_= "<<a_<<std::endl;
// };
auto f1 = [=]() { // err, []为空,没有捕获任何变量
std::cout << "a_= " << a_ << std::endl;
std::cout << "tmp= " <<tmp << std::endl;
};
f1();
auto f2 = [&]() { // err, []为空,没有捕获任何变量
std::cout << "a_= " << a_ << std::endl;
std::cout << "tmp= " <<tmp << std::endl;
};
f2();
//this指针只能捕获类成员变量、全局变量,不能捕获局部变量
auto f3=[this](){
std::cout << "a_= " << a_ << std::endl;
std::cout << "tmp= " <<tmp << std::endl;
//std::cout<<"aaa= "<<aaa<<std::endl;
};
f3();
}
private:
int a_;
};
int main(){
int a=0;
int b=0;
int c=1;
auto f1=[](){};
//a、b、c以值传递方式
auto f2=[a,b,c](){
std::cout<<"a= "<<a<<" b= "<<b<<" c= "<<c<<std::endl;
};
f2();
auto f3=[a,b,c](int x,int y,int z){
std::cout<<"a= "<<a<<" b= "<<b<<" c= "<<c<<std::endl;
std::cout<<"x= "<<x<<" y= "<<y<<" z= "<<z<<std::endl;
};
f3(10,20,30);
//以值传递方式传给lambda表达式
auto f5=[=](){
std::cout<<"a= "<<a<<" b= "<<b<<" c= "<<c<<std::endl;
};
f5();
//以引用方式传给lambda表达式
auto f6=[&](){
std::cout<<"a= "<<a<<" b= "<<b<<" c= "<<c<<std::endl;
};
f6();
//a值传递,b和c引用传递
auto f7=[&,b,c](){
std::cout<<"a= "<<a<<" b= "<<b<<" c= "<<c<<std::endl;
};
f7();
//a以引用传递,其他值传递
auto f8=[=,&a](){
std::cout<<"a= "<<a<<" b= "<<b<<" c= "<<c<<std::endl;
};
f8();
//默认情况下,lambd表达式,以const修饰的函数体值传递无法修改,引用传递可以修改
// auto f9=[=](){
// a++;
// b++;
// c++;
// std::cout<<"a= "<<a<<" b= "<<b<<" c= "<<c<<std::endl;
// };
auto f9=[&](){
a=a+1;
b=b+1;
c++;
std::cout<<"a= "<<a<<" b= "<<b<<" c= "<<c<<std::endl;
};
f9();
//默认情况下,lambd表达式,以const修饰的函数体值传递无法修改,引用传递可以修改,但值传递mutable可以修改
auto f10=[=]() mutable{
a=a+1;
b=b+1;
c++;
std::cout<<"a= "<<a<<" b= "<<b<<" c= "<<c<<std::endl;
};
f10();
Test test;
test.Func();
return 0;
}
输出结果:
PS D:\C++11代码\build\9、闭包的实现\lambda表达式\lambda的基本使用\Debug> .\main.exe
a= 0 b= 0 c= 1
a= 0 b= 0 c= 1
x= 10 y= 20 z= 30
a= 0 b= 0 c= 1
a= 0 b= 0 c= 1
a= 0 b= 0 c= 1
a= 0 b= 0 c= 1
a= 1 b= 1 c= 2
a= 2 b= 2 c= 3
a_= 100
tmp= 10
a_= 100
tmp= 10
a_= 100
tmp= 10
PS D:\C++11代码\build\9、闭包的实现\lambda表达式\lambda的基本使用\Debug>
值传递和引用传递区别:
#include<iostream>
int main(){
int x=10;
//lambda表达式,新建一个变量,外部变量给这个变赋值一份, 值传递,里面修改不会影响外面,外面修改也不会影响里面
auto f1=[=]() mutable{
x++;
std::cout<<"x= "<<x<<std::endl;
};
f1();
//++;
std::cout<<"x= "<<x<<std::endl;
int y=100;
//lambda表达式,新建一个变量,外部变量给这个变赋值一份,引用传递,里面修改会影响外面,外面修改也会影响里面
auto f2=[&](){
y++;
std::cout<<"y= "<<y<<std::endl;
};
f2();
std::cout<<"y= "<<y<<std::endl;
return 0;
}
输出结果:
PS D:\C++11代码\build\9、闭包的实现\lambda表达式\lambda的基本使用\Debug> .\main.exe
x= 11
x= 10
y= 101
y= 101
PS D:\C++11代码\build\9、闭包的实现\lambda表达式\lambda的基本使用\Debug>
9.2.3.2 lambad与仿函数
#include<iostream>
//仿函数重载operator()
class MyFunctor{
public:
MyFunctor(int round)
:round_(round)
{}
int operator()(int tmp){
return round_+tmp;
}
private:
int round_;
};
int main(){
int round=10;
MyFunctor mf(round);
std::cout<<mf(2)<<std::endl;
auto f1=[&](int tmp){
return round+tmp;
};
f1(2);
std::cout<<f1(2)<<std::endl;
return 0;
}
输出结果:
PS D:\C++11代码\build\9、闭包的实现\lambda表达式\lambda与仿函数\Debug> .\main.exe
12
12
PS D:\C++11代码\build\9、闭包的实现\lambda表达式\lambda与仿函数\Debug>
通过上面的例子, 我们看到, 仿函数以 round 初始化类, 而 lambda 函数也捕获了 round 变 量, 其它的, 果在参数传递上, 两者保持一致。
除去在语法层面上的不同, lambda 和仿函数有着相同的内涵——都可以捕获一些变量作为初始化状态, 并接受参数进行运行。
而事实上, 仿函数是编译器实现 lambda 的一种方式, 通过编译器都是把 lambda 表达式转化为一个仿函数对象。 因此, 在 C++11 中, lambda 可以视为仿函数的一种等价形式。
9.2.3.3 lambda类型
lambda 表达式的类型在 C++11 中被称为“闭包类型”, 每一个 lambda 表达式则会产生一个临时对象(右值)。 因此, 严格地将, lambda 函数并非函数指针。
不过 C++11 标准却允许 lambda 表达式向函数指针的转换, 但提前是 lambda 函数没有捕获任何变量, 且函数指针所示的函数原型, 必须跟 lambda 函数函数有着相同的调用方式。
#include<iostream>
#include<functional>
int main(){
int a=10;
std::function<int(int)> f1=[](int a){
return a+1;
};
std::cout<<f1(10)<<std::endl;
std::function<int(int)> f2=bind([](int a){return a+1;},std::placeholders::_1);
std::cout<<f2(11)<<std::endl;
// auto f3=[=](int x,int y) ->int{
// std::cout<<a<<std::endl;
// return x+y;
// };
auto f3 = [](int x, int y) -> int{
//std::cout << a << std::endl;
return x + y;
};
std::cout<<f3(10,10)<<std::endl;
decltype(f3) tmp=f3;
std::cout<<tmp(5,5)<<std::endl;
//定义一个函数指针类型
typedef int (*func)(int,int);
func p=f3; //lambda表达式转换为函数指针 ok,但是这时候不能捕获外部变量
std::cout<< p(20,20)<<std::endl;
//decltype(f3)=p1; //函数指针不可以转换为lambda表达式
return 0;
}
输出结果:
PS D:\C++11代码\build\9、闭包的实现\lambda表达式\lambda类型\Debug> .\main.exe
11
12
20
10
40
PS D:\C++11代码\build\9、闭包的实现\lambda表达式\lambda类型\Debug>
9.2.3.4 lambad优势
传入写法的输出:
#include<iostream>
#include<vector>
int tmp=5;
int main(){
std::vector<int> vec1;
std::vector<int> vec2;
for(int i=0;i<10;i++){
vec1.push_back(i);
}
for(auto it=vec1.begin();it!=vec1.end();it++){
if(*it>tmp){
vec2.push_back(*it);
}
}
for(auto it=vec2.begin();it!=vec2.end();it++){
std::cout<<*it<<" ";
}
std::cout<<std::endl;
return 0;
}
输出结果:
PS D:\C++11代码\build\9、闭包的实现\lambda表达式\lambda优势\Debug> .\main.exe
6 7 8 9
PS D:\C++11代码\build\9、闭包的实现\lambda表达式\lambda优势\Debug>
std::for_each函数的使用:
#include<iostream>
#include<algorithm>
#include<vector>
int tmp=5;
void Func1(int& n){
std::cout<<n<<" ";
}
void Func2(int& n){
std::cout<<n<<" ";
}
void ForEach(){
std::vector<int> vec1;
std::vector<int> vec2;
for (int i = 0; i < 10; i++)
{
vec1.push_back(i);
}
for (auto it = vec1.begin(); it != vec1.end(); it++){
if (*it > tmp)
{
vec2.push_back(*it);
}
}
std::for_each(vec1.begin(), vec1.end(), Func1);
std::cout<<std::endl;
std::for_each(vec2.begin(), vec2.end(), Func2);
}
int main(){
std::vector<int> vec1;
std::vector<int> vec2;
for(int i=0;i<10;i++){
vec1.push_back(i);
}
for(auto it=vec1.begin();it!=vec1.end();it++){
if(*it>tmp){
vec2.push_back(*it);
}
}
for(auto it=vec2.begin();it!=vec2.end();it++){
std::cout<<*it<<" ";
}
std::cout<<std::endl;
ForEach();
std::cout<<std::endl;
return 0;
}
输出结果:
PS D:\C++11代码\build\9、闭包的实现\lambda表达式\lambda优势\Debug> .\main.exe
6 7 8 9
0 1 2 3 4 5 6 7 8 9
6 7 8 9
PS D:\C++11代码\build\9、闭包的实现\lambda表达式\lambda优势\Debug>
lambda表达式作为for_each的回调函数:
#include<iostream>
#include<algorithm>
#include<vector>
int tmp=5;
void Lambda(){
std::vector<int> vec1;
std::vector<int> vec2;
for (int i = 0; i < 10; i++){
vec1.push_back(i);
}
// for (auto it = vec1.begin(); it != vec1.end(); it++){
// if (*it > tmp)
// {
// vec2.push_back(*it);
// }
// }
std::for_each(vec1.begin(),vec1.end(),
[&](int& n){
if(tmp<n){
vec2.push_back(n);
}
}
);
std::for_each(vec2.begin(),vec2.end(),
[](int& n){
std::cout<<n<<" ";
}
);
}
void Func1(int& n){
std::cout<<n<<" ";
}
void Func2(int& n){
std::cout<<n<<" ";
}
void ForEach(){
std::vector<int> vec1;
std::vector<int> vec2;
for (int i = 0; i < 10; i++)
{
vec1.push_back(i);
}
for (auto it = vec1.begin(); it != vec1.end(); it++){
if (*it > tmp)
{
vec2.push_back(*it);
}
}
std::for_each(vec1.begin(), vec1.end(), Func1);
std::cout<<std::endl;
std::for_each(vec2.begin(), vec2.end(), Func2);
}
int main(){
std::vector<int> vec1;
std::vector<int> vec2;
for(int i=0;i<10;i++){
vec1.push_back(i);
}
for(auto it=vec1.begin();it!=vec1.end();it++){
if(*it>tmp){
vec2.push_back(*it);
}
}
for(auto it=vec2.begin();it!=vec2.end();it++){
std::cout<<*it<<" ";
}
std::cout<<std::endl;
ForEach();
std::cout<<std::endl;
Lambda();
return 0;
}
输出结果:
PS D:\C++11代码\build\9、闭包的实现\lambda表达式\lambda优势\Debug> .\main.exe
6 7 8 9
0 1 2 3 4 5 6 7 8 9
6 7 8 9
6 7 8 9
PS D:\C++11代码\build\9、闭包的实现\lambda表达式\lambda优势\Debug>
lambda 表达式的价值在于, 就地封装短小的功能闭包, 可以及其方便地表达出我们希望执行的具体操作, 并让上下文结合更加紧密。