一、自动类型推导:
1.1auto初始化
- 在C++11之前,auto关键字用于指明变量的存储类型,它和static是相对的。但是一般都省略不写了。
- 在C++11以后,auto关键字被赋予了新的含义,可以使用它来做自动的类型推导。也就是说,在使用了auto
- 关键字以后,编译器会在编译时自动推导出 变量的类型,这样我们就不用手动指定变量的类型了。 语法如下:
auto 变量名name=变量的值value;
注意:使用auto以后,必须给变量马上进行初始化,这样编译器才可以根据value来推导auto的类型。
//auto初始化,auto的单独用法和混合用法
void test01()
{
//单独用法
//auto s;//auto使用的时候必须立刻初始化,才能推导auto的类型
auto a = 3;//auto是int类型
auto b = 4.45;//auto是double
auto c = 'c';//auto是char
cout << typeid(a).name() << "," << typeid(b).name() << "," << typeid(c).name() << endl;
cout << sizeof(a) << "," << sizeof(b) << "," << sizeof(c) << endl;
1.2auto的混合用法
- auto除了可以独立使用,还可以跟某些类型混合使用,此时auto表示的就是部分类型,或者叫半个类型。
示例:
//混合用法
int x = 10;
auto* p1 = &x;//p1是int*类型,auto推导为int
cout << "p1类型:" << typeid(p1).name() << endl;;
auto p2 = &x;//p2是int*类型,auto推导为int*
cout << "p2类型:" << typeid(p2).name() << endl;
auto& r1 = x;//r1是int&类型,打印出来显式int类型,auto推导为int
cout << "r1类型:" << typeid(r1).name() << endl;
auto r2 = r1;//r2是int类型,auto推导为int,当右边的赋值是一个引用类型时,auto会直接推导出引用类型的原始类型。
cout << "r2类型:" << typeid(r2).name() << endl;
1.3auto的使用限制
-
除了上面说的auto必须马上初始化之外,还有一些使用限制: (
-
1)auto不能用于定义数组。
-
(2)auto不能用于类的成员变量(属性)。
-
(3)auto不能在函数参数中使用。因为形参只是一种声明,并没有赋值,只有在函数调用的时候才会给参数赋值。
-
(4)auto作为函数返回值时,可以用于函数的定义,但不能用于函数的声明。
-
(5)auto不能用于模板参数。
-
示例:
-
//auto的使用限制 //auto不能用于类的成员变量(属性)。 class TestAuto { public: //auto m_Age=20;//即使马上赋值也不行 int m_nAge; }; //auto作为函数返回值时,可以用于函数的定义,但不能用于函数的声明。 //auto mySum(int num);//auto作为返回值,不能用于函数的声明中 auto mySum(int num) { int sum = 0; for (int i = 1; i <= num; i++) { sum += i; } return sum; } //auto不能用于模板参数。 template<class T> class A { T m_Age; }; //auto不能在函数参数中使用。 int add(auto a=2, auto b=4)//即使赋值也不行 { return a + b; }
1.4auto的使用场合
- (1)定义函数的局部变量并且马上初始化
- (2)用于循环语句中的循环控制变量并且马上初始化
- (3)在遍历容器的时候,使用auto来定义迭代器类型
//auto的使用场合
void test02()
{
//用于循环语句中的循环控制变量并且马上初始化
int array[10] = { 1,2,3,4,5,6,7,8,9,10 };
for (auto i = 0; i < 10; i++)
{
cout << array[i] << " ";
}
cout << endl;
//接下来的循环中使用auto,体会它的好处
//在遍历容器的时候,使用auto来定义迭代器类型
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
//之前的写法,做容器遍历
for (vector<int>::iterator it = v.begin(); it != v.end(); it++)
{
cout << *it << " ";
}
cout << endl;
//使用auto之后,做容器遍历
for (auto it = v.begin(); it != v.end(); it++)
{
cout << *it << " ";
}
cout << endl;
//auto用在map的迭代器定义
map<int, int> m;
map<int, int>::iterator it = m.begin();//原来的写法
auto newit = m.begin();//使用auto后的写法
}
二、空指针nullptr:
-
在C语言里,常量0有着int常量和空指针的双重角色,NULL被定义为(void)0
-
在C++语言中,空指针NULL被定义为0
-
以上这样的定义,会导致一些问题,如下:
void fun(char* p); void fun(int n);
-
在C++中,在使用上面这两个重载的函数时,如果传入的参数是NULL,那么会调用哪个函数?
-
我们发现,会调用 void fun(int n);这个函数,这显然不是我们期望的,我们期望调用第一个函数。 所以,在C++11以后,引入了一个新的关键字nullptr,来充当单独的空指针常量,来纠正上面的问题它的类型是nullptr_t类型, 它可以隐式转换为任意类型的指针,并且可以和这些指针进行比较。
-
但是,nullptr不能隐式转换为整型,也不能跟整型进行比较。
//空指针nullptr
//如下两个函数重载,当我们传入NULL时,希望调用第一个函数,但结果会调用第二个函数。
void fun(const char* p) { const char* c = "你好啊"; p = c; cout << p <<",调用第一个" << endl; }
void fun(int n) { cout << n << "调用第二个" << endl;}
fun(NULL);//这里使用NULL存在问题,执行了第二个函数
fun(nullptr);//这次执行的是第一个函数,这是我们期望的
//nullptr可以隐式转换成任意类型的指针,如下:
char* p_char = nullptr;
int* p_int = nullptr;
int i = nullptr;//nullptr不能跟整型int进行隐式转换
int n = 0;
if (n==nullptr){}//nullptr不能跟整型进行比较
三、lambda表达式:
lambda表达式也成为lambda函数,或者叫匿名函数,就是没有名字的函数。匿名函数结尾要有分号结束符。
3.1语法结构:
捕捉列表参数列表mutable->返回值类型{函数体代码;
- capture(捕获列表): 指定了在lambda函数体中可以访问的外部变量。可以通过值捕获([x])、引用捕获([&x])或混合捕获([&, x]或[=, &x])。
- parameter_list(参数列表): 与普通函数的参数列表相似,用于接收传递给lambda的参数。
- return_type(返回类型): 指定lambda表达式的返回类型。可以省略,如果省略则返回类型由编译器推导。
- 函数体: 包含了实际的代码块,定义lambda函数的主体。
[capture](parameter_list) -> return_type
{
// 函数体
}
解释:
-
[捕捉列表]:它总是出现在起始位置,编译器根据它来判断是否为匿名函数。它的作用是捕捉当前作用域中的变量,给匿名函数使用。
-
捕捉列表的具体用法:[]:匿名函数不使用任何外部变量(但是仍然可以使用全局变量和静态变量)
-
[变量a,b,c…] 以传值的方式使用多个外部变量,用逗号分开。(传值捕捉来的变量具有常性,因为匿名函数默认是常函数,不能在匿名函数体中修改变量的值)
-
[&a,&b,&c…] 以引用的方式使用外部变量。(引用捕捉的变量,不具有常性,可以修改它们的值)
-
[=] 捕捉所有变量,并且都是用值传递的方式。
-
[&] 捕捉所有变量,并且都是用引用传递的方式。
-
(参数列表):它跟普通函数的参数列表用法相同,参数是不具有常性,可以修改它们的值。同时如果没有参数,()是可以省略的。当指定返回值时,参数列表()不能省略
-
mutable:它的作用就是取消值捕捉来的变量的常性,让它们可以在匿名函数的函数体内被修改。跟使用引用捕捉不一样,引用捕捉是直接改变原值。 (使用mutable时,参数列表不能省略,即使参数为空)
-
->返回值类型:它代表匿名函数的返回值类型,如果没有返回值或者有明确的返回值类型也是可以省略。
-
**{函数体代码;}😗*匿名函数执行的具体代码段,这里面可以使用捕捉来的变量,也可以使用参数。 最简单的匿名函数是:[]{}
3.2匿名函数的使用
(见下面的代码)
//lambda表达式,匿名函数
void test03()
{
//最简单的匿名函数,啥都做不了
[] {};
//下面演示可用的匿名函数
int a = 1;
int b = 2;
[=] {return a + b; };//捕捉当前作用域所有的变量,值捕捉,没有参数,省略了参数列表,有返回值,但是省略了返回值的书写,让它自动推导
//调用匿名函数,采用()符号来调用
cout << "a+b=" << [=] {return a + b; }() << endl;
//定义一个带参数的匿名函数
[](int num1, int num2) {return num1 + num2; };
cout << [](int num1, int num2) {return num1 + num2; }(5, 10) << endl;
//定义指定返回值的匿名函数
cout << []()->int {return 100; }() << endl;
//结合auto使用,将匿名函数当作一个对象来使用
auto f = []() {return 100; };
cout << f() << endl;
//引用捕捉可以去掉捕捉变量的常性,就可以修改它们的值
auto f1 = [&](int c) {b = a + c; };
f1(10);
cout << "引用捕捉,修改了b的值,b=" << b << endl;
//mutable的用法,它可以取消值捕捉变量的常性
int x = 1;
int y = 2;
//auto add1 = [x, y]()->int {x *= 2; };//报错,值捕捉不能修改捕捉变量的值,具有常性
auto add1 = [x, y]()mutable->int {x *= 2; return x; };//加上mutable关键字,可以取消常性,值可以被修改
cout << "添加mutable之后,x=" << add1() << endl;
//匿名函数可以在函数体内部调用其他普通函数
auto fun_mysum = []()->int {return mySum(10); };
cout << fun_mysum() << endl;
//匿名函数用于函数的参数,给容器的排序算法指定排序规则
vector<int> vec;
vec.push_back(4);
vec.push_back(6);
vec.push_back(2);
vec.push_back(8);
//使用排序算法进行默认的升序排序
sort(vec.begin(), vec.end());
for (auto it = vec.begin(); it != vec.end(); it++)
{
cout << *it << " ";
}
cout << endl;
//使用匿名函数进行降序排序
sort(vec.begin(), vec.end(), [](int x, int y) {return x > y; });
cout << "降序排序后" << endl;
for (auto it = vec.begin(); it != vec.end(); it++)
{
cout << *it << " ";
}
}
3.3匿名函数实现的原理:
- 匿名函数是在执行时,由编译器在全局生成一个类,类里面重载了operator()函数,函数体的内容就是匿名函数的内容。
四、for范围遍历:
- 这是一种新的遍历方式,可以对一个可遍历的数据进行逐个遍历。但是这种遍历方式无法指定遍历的范围,如果要制定范围,还是用传统for循环的形式。
4.1语法:
for(变量类型 变量名:可遍历的序列数据){ 循环体代码; }
- **解释:**这里的变量类型要跟可遍历数据中的元素类型一致,通常为了方便,使用auto来定义。
注意:变量参数默认是值传递的变量,也可以定义成引用传递的方式,如果用引用方式取值,就可以改变可便利序列数据中的元素值。
for(变量类型& 变量名:可遍历的序列数据)
{
循环体代码;
}
//for范围遍历
void test04()
{
int arr_int[10] = { 0,1,2,3,4,5,6,7,8,9 };
//原来的遍历方式
for (int i = 0; i < sizeof(arr_int)/sizeof(arr_int[0]); i++)
{
cout << arr_int[i] << " ";
}
cout << endl;
//遍历字符数组
char arr_char[] = "hello world";
for (auto c : arr_char)
{
cout << c;
}
cout << endl;
//遍历容器
vector<int> int_vec = { 1,2,3,4,5 };
for (auto i : int_vec)
{
cout << i << " ";
}
cout << endl;
//引用遍历
//默认的是值遍历,不能修改原值
for (auto i : int_vec)
{
cout << ++i << " ";
}
cout << endl;
for (auto i : int_vec)
{
cout << i << " ";
}
cout << endl;
//使用引用的方式遍历,改变容器中的原值
cout << "下面是引用遍历:" << endl;
for (auto& i : int_vec)
{
cout << ++i << " ";
}
cout << endl;
for (auto i : int_vec)
{
cout << i << " ";
}
cout << endl;
}
Map容器遍历:
//写一个打印map容器的函数模板
template<class T1,class T2>
void printMap(map<T1, T2>& m)
{
for (auto ele : m)
{
cout << ele.first << ":" << ele.second << endl;
}
cout << endl;
}
//使用函数模板打印map
map<int, int> int_map = { {1,10},{2,20},{3,30} };
map<int, string> int_str_map = { {9293,"张三"},{9494,"李四"},{8473,"王五"} };
printMap(int_map);
printMap(int_str_map);
4.2注意事项:
- 1)for范围遍历不能实现指定范围的遍历。
- 2)遍历范围可确定的,STL中的各类容器都可以正常使用。
- 3)for范围遍历在遍历字符串时,会连通字符串的结束符’\0’一起遍历。
//strlen函数不会统计结束符
int count = 0;
char arr_char_hello[] = "hello";
for (int i = 0; i < strlen(arr_char_hello); i++)
{
count++;
}
cout << count << endl;//5
//for范围遍历会统计结束符
count = 0;
for (auto c : arr_char_hello) { count++; }
cout << count << endl;//6
五、初始化列表:
使用{}来初始化 C++11以前,允许使用{}对数组元素进行统一的初始化。 C++11以后,扩大了{}的适用范围,可以用于所有的内置基本类型和用户自定义类型的初始化。 使用初始化列表的时候,等号=可以加也可以不加。
//初始化列表{}
//自定义类型也可以使用初始化列表
class Date
{
public:
int m_Year;
int m_Month;
int m_Day;
Date(int y,int m,int d):m_Year(y),m_Month(m),m_Day(d){}
};
void test05()
{
//数组使用初始化列表
int arr1[] = { 4,2,42,5 };
int arr2[5] = { 8 };
//对类使用初始化表
Date d1 = { 2024,1,25 };
Date d2{ 2023,12,25 };
cout << d1.m_Year << "-" << d1.m_Month << "-" << d1.m_Day << endl;
cout << d2.m_Year << "-" << d2.m_Month << "-" << d2.m_Day << endl;
//在堆内存申请内存的时候直接使用初始化列表
int* p1 = new int(100);//直接初始化成100
cout << "*p1=" << *p1 << endl;
int* p2 = new int[5] {1, 2, 3, 4, 5};
for (int i = 0; i < 5; i++)
{
cout << *(p2 + i) << " ";
}
cout << endl;
Date* p3 = new Date[3]{ {2024,2,4},{2024,2,14},{2024,2,10} };
cout << p3->m_Year << "-" << p3->m_Month << "-" << p3->m_Day << endl;
//初始化列表的类型:class std::initializer_list<T>
auto ls = { 10,20,30,40 };
cout << typeid(ls).name() << endl;
}
六、类型别名:
-
类型别名和类名等价,我们可以给某些类型重新起别名,这样就能用别名代替原来的类型名。
-
类型别名主要有两种语法:
- 1)typedef关键字 语法:typedef 类型名 类型别名;
- 2)using关键 语法:using 别名=类型原名;
-
以上两种别名定义方式的区别: 在语义上是等价,只有一个区别,就是typedef不能给模板类起别名,而using可以。
//类型别名
void test06()
{
//typedef的方式
//给Date类类型起别名
typedef Date D;
D d1 = { 2024,2,2 };
typedef double dou;
dou d = 23.55;
//using的方式
using test = TestAuto;
test t1;
//两种方式的区别:typedef不能给模板定义别名,应该使用using的方式
using a = A<int>;
}
七、智能指针
7.1常见内存泄露
什么是内存泄漏?
指的是由于人为疏忽或者异常错误等原因,造成内存使用完没有被释放,导致这些内存无法继续被使用。内存泄露不是指物理上的消失。
内存泄露的危害:长期运行内存泄露的程序,会导致程序越来越慢,最终卡死。
内存泄露的分类:
1)堆内存泄露(heap leak)
指的是通过malloc或者new等语句从堆内存分配的空间,用完后没有通过free或者delete语句释放,这些内存就无法再被使用,导致泄露。
栈内存由编译器负责管理,会自动释放,不需要我们管理。
2)系统资源泄露
指的是操作系统分配的一些资源,比如说套接字、文件描述符、管道等等,使用完之后没有通过对应的函数释放掉,导致系统资源变少,也会导致一些问题。
如何避免内存泄露?
首先遵守设计规范,养成好的编码习惯,记得去释放不用的内存。
可以采用一些第三方的检测工具,来检查你的代码有没有泄露。比如visual leak detector工具。
但是即使做到以上,仍然无法避免内存泄露,因为有时候程序会遇到异常,异常后就会中断执行。如果释放内存的语句是在异常代码之后,那就无法执行了。
7.2智能指针概念
- 以上分析我们得知,内存泄露很麻烦而且无法百分百避免,所以C++11之后,推出了智能指针,来解决这个问题,帮我们管理内存,自动释放内存。
智能指针是采用了RAII(资源获取即初始化)技术来实现的。就是利用对象的生命周期来管理资源的技术。在对象构造的时候获取资源,在对象生命周期内,保持对资源的访问,在对象析构的时候,释放资源,把资源管理任务委托给了对象。
- 智能指针就是根据RAII思想创建的一个类模板,并且重载了指针的运算符,使它具有指针一样的行为。
- 智能指针是封装了指向堆内存的原始指针(裸指针)的一个类模板,能够确保在离开智能指针对象作用域的时候,通过调用析构函数自动释放堆内存,防止内存泄露。
//模拟一个智能指针
template<class T>
class SmartPtr
{
public:
T* ptr;//裸指针,指向堆内存
SmartPtr(T* p):ptr(p){}
~SmartPtr()//析构函数负责回收内存
{
if (ptr!=nullptr)
{
cout << "智能指针帮我们回收了内存" << endl;
delete ptr;
ptr = nullptr;
}
}
};
//模拟智能指针
SmartPtr<int> ptr_int(new int(1));
SmartPtr<string> ptr_string(new string("abc"));
7.3智能指针分类
-
使用系统提供的智能指针,需要引入头文件**#include **
-
指针指针根据对资源的所有权来分类,分成三类:
1)共享智能指针shared_ptr
2)独占智能指针unique_ptr
3)弱共享智能指针weak_ptr -
下面分别介绍这三种类型:
7.3.1 共享智能指针shared_ptr:
共享智能指针是指多个智能指针同时管理一块内存,它们共享一个内存,同时内部共享一个引用计数。每增加一个智能指针就增加1,每减少一个指针指针,
就减少1,当计数为0的时候,就要释放这块内存,这样保证这块内存智能被释放一次。
共享指针的初始化:
构造函数:shared_ptr sp(要管理的堆内存地址); 如:shared_ptr sp1(new int(100));
//构造共享智能指针
shared_ptr<int> ptr(new int(1));
cout << ptr << endl;
cout << *ptr << endl;//智能指针重载了指针的运算符,可以像指针一样使用
*ptr = 30;//修改
cout << *ptr << endl;
拷贝构造:shared_ptr sp(已存在的智能指针对象);拷贝构造之后它们就共同管理了一块内存。
shared_ptr<int> ptr1(ptr);//拷贝构造,此时ptr1和ptr都共同管理同一块内存
cout << *ptr1 << endl;//30
辅助函数:[make_shared 如:shared_ptr sp1=make_shared(100);]
//赋值函数初始化
shared_ptr<int> ptr2 = make_shared<int>(10);
cout << *ptr2 << endl;
shared_ptr<int> ptr3 = make_shared<int>();//无参,管理一个没有初始化的空间
- 共享指针的成员函数:
- **use_count();**查看当前这块内存有多少共享指针在同时管理。也就是统计引用计数。
- **reset();**重置方法,不带参数的情况下,如果当前共享指针是唯一一个管理该内存的,那就释放内存并且置空该指针。如果它不是唯一管理者,则不需要释放内存,只需要引用计数-1,然后再将自己置空
- reset(要管理的新地址);重置方法,带参数的情况下,如果当前共享指针是唯一一个管理该内存的,那就释放内存并且管理新内存地址。
如果它不是唯一管理者,则不需要释放内存,只需要引用计数-1,然后再管理新内存地址。 - get();返回共享指针封装的原始指针,也叫裸指针。
- operator=():赋值运算符=的重载方法,完成共享指针之间的赋值操作,赋值之后它们就共同管理了一块内存。
//查看引用计数
cout << ptr.use_count() << endl;//2
cout << ptr1.use_count() << endl;//2
cout << ptr2.use_count() << endl;//1
cout << ptr3.use_count() << endl;//1
ptr3 = ptr2;//赋值函数,ptr3和ptr2共同管理一块内存
cout << *ptr3 << endl;//10
cout << ptr2.use_count() << endl;//2
cout << ptr3.use_count() << endl;//2
//重置方法
ptr3.reset();//ptr3,不是唯一管理者,它不需要释放所管理的空间,只需要引用计数-1,然后被重置
cout << ptr3.use_count() << endl;//0,ptr3被重置了,不管理任何空间了
cout << ptr2.use_count() << endl;//1,ptr3被重置了,只剩下ptr2管理原来的空间,所以计数为1
ptr2.reset();
cout << ptr2.use_count() << endl;//0,ptr2是唯一的管理着,被重置,引用计数变成0,原来空间被回收
ptr1.reset(new int(5));//ptr1被重置并且马上派去管理新空间5
cout << ptr1.use_count() << endl;//1
cout << ptr.use_count() << endl;//1
cout << *ptr1 << endl;//5
删除器:
删除器其实是用于回收内存的一个函数,作为参数使用,通过函数来调用它。
为什么要使用删除器?是因为对于自定义类型的连续多个内存空间,是无法被默认的析构函数回收的。需要我们手写删除器来完成回收工作。
删除器往往使用匿名函数来实现。
//删除器
void test02()
{
//shared_ptr<Test> ptr5(new Test[5]);//如果是多个连续的对象空间,默认的智能指针无法完成全部空间的释放
shared_ptr<Test> ptr5(new Test[5], [](Test* t) {delete[]t; });//通过一个匿名函数实现连续空间的释放,这个函数叫做删除器
cout << ptr5.use_count() << endl;
shared_ptr<Test> ptr6(new Test(100), [](Test* t) {delete t; cout << "Test对象100被删除器释放了"; });
ptr6->print();
//删除器的其他用法,使用系统提供的默认删除器
shared_ptr<Test> ptr7(new Test[3], default_delete<Test[]>());//第一种写法
shared_ptr<Test[]> ptr8(new Test[4]);//第二种写法
}
class Test
{
int m_num;
public:
Test() { cout << "Test类的无参构造" << endl;; }
Test(int x) { m_num = x; cout << "Test类的有参构造" << endl;; }
~Test() { cout << "Test类的析构" << endl;; }
void setValue(int v) { m_num = v; }
void print() { cout << "m_num=" << m_num << endl; }
};
-
使用共享指针的注意事项:
1)不能直接使用new方法来返回一个空间指针给共享指针,而是需要使用构造函数或者辅助函数来完成空间的管理。
2)一个普通指针指向的内存交给智能指针管理,此时就不能手动去回收普通指针的内存了,这样会造成重复释放同一块内存。
3)不要用同一个原始指针初始化多个共享指针,原因是这样会造成重复释放同一块内存。我们让多个共享指针共同管理同一块内存的时候,
应该使用赋值运算符或者拷贝构造的方式,这样共享指针之间才会共享引用计数,就不会造成多次释放了。
4)不要再函数参数中创建共享指针,应该在函数外部先把智能指针创建好,再传入函数参数中。原因是不同的编译器堆函数参数的处理顺序是不一样的。
比如:function(第一个参数,shared_ptr(new int(10)));//不要这么做
应该这样:shared_ptr p(new int(10));先构造好指针对象,然后再给函数传参function(第一个参数,p);
5)不要再类的成员函数中返回一个管理当前对象指针(即this指针)的智能指针对象。
6)避免循环引用的问题:即两个类互相含有对方类对象的智能指针,并且相互赋值。这会导致内存泄露。这个问题可以通过后面要讲的弱指针来解决。//注意事项 void testShared_Attenton() { //1)不能直接使用new方法来返回一个空间指针给共享指针管理,而是需要使用构造函数或者辅助函数来完成空间的管理。 //shared ptr<Test> ptest=new Test(2); shared_ptr<Test> pTemp(new Test(10)); shared_ptr<Test> pTemp1 = make_shared<Test>(100); //2)一个普通指针指向的内存交给智能指针管理,此时就不能手动去回收普通指针的内存了,这样会造成重复释放同一块内存 Test* pTest = new Test(100); //普通指针指向一个test对象 //再交给智能指针管理 shared_ptr<Test>pShare(pTest); //如果我们忘了这个对象已经被智能指针管理了,我们有自己释放了,就会导致重复释放 delete pTest; //我们释放了这块内存,但是智能指针还会释放一次,就报错了 //3)不要用同一个原始指针初始化多个共享指针,原因是这样会造成重复释放同一块内存。 int* p = new int(100); shared_ptr<int> p_sharel(p); shared_ptr<int> p_share2(p); cout << "p_sharel的指针计数:" << p_sharel.use_count() << endl;//1 cout << "p_share2的指针计数:" << p_share2.use_count() << endl;//1 //这样做是错误的,虽然p_ shar下1和p_ share2都管理了同一块内存,但是它们没有共享引用计数,它们都会回收这个内存,导致错误 //应该使用拷贝构造或者赋值的方式,让它们共享引用计数,如下: p_share2 = p_sharel;//这样才不会报错 cout << "p_sharel的指针计数:" << p_sharel.use_count() << endl;//2 cout << "p_share2的指针计数:" << p_share2.use_count() << endl;//2 //4)不要再函数参数中创建共享指针,应该在函数外部先把智能指针创建好,再传入函数参数中。 //应该先把智能指针构造好,起好名字,然后再给函数传参 //5)不要再类的成员函数中返回一个管理当前对象指针(即this指针)的智能指针对象。 shared_ptr<C> c(new C()); c->getP();// 报错了,因为被释放了两次,C析构了一次,调用getP()函数是被析构了一次 //6)避免循环引用的问题:即两个类互相含有对方类对象的智能指针,并且相互赋值。这会导致内存泄露。 shared_ptr<A>a(new A()); cout << a.use_count() << endl;//1 shared_ptr<B>b(new B()); cout << b.use_count() << endl;//1 //下面进行相互赋值 a->bsp = b; b->asp = a; cout << a.use_count() << endl;//2 cout << b.use_count() << endl;//2 //引用计数会变成2,所以导致内存泄漏,因为a离开释放了一次,b离开释放了一次 }
class C { public: C() { cout << "C的无参构造" << endl; } ~C() { cout << "C的析构" << endl; } shared_ptr<C> getP() { return shared_ptr<C>(this); } }; //(6)循环引用问题,会导致内存泄漏 class B;//这是前向声明,因为A类中马上用B类 class A { public: shared_ptr<B>bsp; //A类中含有管理B类的智能指针 ~A() { cout << "A在析构" << endl; } }; class B { public: shared_ptr<A>asp; //B类中含有管理A类的智能指针 ~B(){ cout << "B在析构" << endl; } };
7.3.2 独占智能指针unique_ptr:
-
独占智能指针不允许其他智能指针跟它共享,它独自管理一块内存。
-
独占智能指针禁用了赋值函数和拷贝构造函数。
-
独占指针的常用函数:
-
构造函数:
unique_ptr<T> up(管理的内存地址);
-
辅助函数:
make_unique 如:unique_ptr<int> up=make_unique<int>(100);
-
重置:
reset();解除对内存的管理,释放内存,指针置空。
-
重置:
reset(新地址);解除对原来内存的管理,释放内存,管理新地址。 get();获取智能指针管理的原始指针
-
void test04()
{
unique_ptr<Test> u_p1 = make_unique<Test>(8);
unique_ptr<Test> u_p2(new Test(9));
u_p1->print();
u_p2->print();
//独占指针拷贝构造和赋值
unique_ptr<Test> u_p3 = make_unique<Test>();
//u_p3 = u_p2; 不允许赋值
//unique_ptr<Test> u_p4(u_p3); //不允许拷贝构造
}
7.3.3 弱共享智能指针weak_ptr:
它可以看成共享智能指针的助手,它不管理内存,主要作用就是当共享指针的助手,像一个旁观者,用于监控共享指针管理的内存是否存在是否有效。
它没有重载指针的运算符,不能像指针一样使用。同时它的增加或减少也不会造成引用计数的改变。
弱共享智能指针的常用函数:
构造函数:weak_ptr wp(监视的地址);
拷贝构造:weak_ptr wp(另一个弱指针对象);
operator=():支持赋值函数,实现两个弱指针的赋值
use_count();统计所监视内存的引用计数
expired();判断所监视的内存是否被释放
lock();获取所监视内存的共享指针对象
reset();不再监视任何内存
void test05()
{
shared_ptr<int>sp(new int(10));
cout << "sp_count: " << sp.use_count() << endl;
weak_ptr<int> wp1;
cout << "wp1_count: " << wp1.use_count() << endl;
weak_ptr<int> wp2(sp);
cout << "wp2_count: " << wp2.use_count() << endl;
weak_ptr<int> wp3(sp);
cout << "wp3_count: " << wp3.use_count() << endl;
//增加管理者
shared_ptr<int>sp1(sp);
cout << "sp_count: " << sp.use_count() << endl;
cout << "wp3_count: " << wp3.use_count() << endl;
cout << wp3.expired() << endl; //返回0;
shared_ptr<int> ptr=wp3.lock();
cout << *ptr << endl;
wp3.reset();
cout << "wp2_count: " << wp2.use_count() << endl;
cout << "wp3_count: " << wp3.use_count() << endl;
}
弱指针解决循环引用问题:
由于弱指针不会导致引用计数的改变,所以只需要将A或者B类任意一个的共享指针成员改成弱指针,就可以打破循环引用。
// 通过弱共享引用打破上面的循环引用问题
class B_break;//这是前向声明,因为A_break类中马上用B_break类
class A_break {
public:
shared_ptr<B_break>bsp; //A_break类中含有管理B_break类的智能指针
~A_break() { cout << "A_break在析构" << endl; }
};
class B_break {
public:
weak_ptr<A_break>asp; //B_break类中属性换成弱指针,引用计数不会加一
//这样A_break类的对象就可以正常析构了。当他析构的时候,就会把他的属性对象也会析构了,此时bsp的析构就会导致他管理的B_break
//应用计数-1,从2变成1,
~B_break() { cout << "B_break在析构" << endl; }
};
void test06()
{
shared_ptr<A_break> a(new A_break());
shared_ptr<B_break> b(new B_break());
//下面进行相互赋值
a->bsp = b;
b->asp = a;
cout << a.use_count() << endl;//1
cout << b.use_count() << endl;//2
}
智能指针总结:
1)shared_ptr通常使用在所有权不明确的场景,会有多个智能指针同时管理一个内存。
2)unique_ptr使用在独享内存的情况下,只允许一个智能指针来管理内存。
3)避免智能指针和原始指针的混合交叉使用。
4)智能指针解决内存泄露的问题,但系统资源仍然需要自己回收。
5)使用智能指针的时候,每个指针对象都应该先初始化再使用。
6)使用弱指针打破循环引用。
最后,对于智能指针的使用,还是要谨慎,因为智能指针会导致性能的下降,所以,对于性能要求较高的程序,尽量不要用智能指针。对于性能要求不高的,可以用,比如QT里面用到的智能指针。还有一种情况,如果事先不能确定某个内存最终有事来释放,也可以使用智能指针。