智能指针,文件IO
二、智能指针&动态内存
1. 指针潜在问题
c++ 把内存的控制权对程序员开放,让程序显式的控制内存,这样能够快速的定位到占用的内存,完成释放的工作。但是此举经常会引发一些问题,比如忘记释放内存。由于内存没有得到及时的回收、重复利用,所以在一些c++程序中,常会遇到程序突然退出、占用内存越来越多,最后不得不选择重启来恢复。造成这些现象的原因可以归纳为下面几种情况:
- 野指针: 内存已经被释放、但是指针仍然指向它。这时内存有可能被系统重新分配给程序使用,从而会导致无法估计的错误
- 重复释放:程序试图释放已经释放过的内存,或者释放已经被重新分配过的内存,就会导致重复释放错误.
- 内存泄漏: 不再使用的内存,并没有释放,或者忘记释放,导致内存没有得到回收利用。 忘记调用delete
随着多线程程序的广泛使用,为了避免出现上述问题,c++提供了智能指针,并且c++11对c++98版本的智能指针进行了修改,以应对实际的应用需求。
2. 智能指针
在98版本提供的
auto_ptr
在 c++11得到删除,原因是拷贝是返回左值、不能调用delete[] 等。 c++11标准改用unique_ptr
|shared_ptr
|weak_ptr
等指针来自动回收堆中分配的内存。智能指针的用法和原始指针用法一样,只是它多了些释放回收的机制罢了。智能指针位于 头文件中,所以要想使用智能指针,还需要导入这个头文件
#include<memory>
a. unique_ptr
unique_ptr
是一个独享所有权的智能指针,它提供了严格意义上的所有权。也就是只有这个指针能够访问这片空间,不允许拷贝,但是允许移动(转让所有权)。
unique_ptr<int> p(new int(10)); //指针p无法被复制
unique_ptr<int> p2 = p ; //错误
cout << *p << endl; //依然可以使用 解引用获取数据。
unique_ptr<int> p3 = move(p) ; // 正确,至此,p将不再拥有控制权。
cout << *p3 << endl; // p3 现在是唯一指针
cout << *p << endl; // p 现在已经无法取值了。
p3.reset(); // 可以使用reset 显式释放内存。
p3.reset(new int(6));
p3.get() ; // 可以获取到指针存放的地址值。
b. shared_ptr
shared_ptr
: 允许多个智能指针共享同一块内存,由于并不是唯一指针,所以为了保证最后的释放回收,采用了计数处理,每一次的指向计数 + 1 , 每一次的reset会导致计数 -1 ,直到最终为0 ,内存才会最终被释放掉。 可以使用use_cout
来查看目前的指针个数
shared_ptr<int> s1(new int(3));
shared_ptr<int> s2 = s1;
s1.reset();
s2.reset(); // 至此全部解除指向 计数为0 。
cout << *s1 << endl; //无法取到值
- 共享指针的问题
对于引用计数法实现的计数,总是避免不了循环引用(或环形引用)的问题,即我中有你,你中有我,
shared_ptr
也不例外。 下面的例子就是,这是因为f和s内部的智能指针互相指向了对方,导致自己的引用计数一直为1,所以没有进行析构,这就造成了内存泄漏。
class father {
public:
father(){cout <<"father 构造" << endl;}
~father(){cout <<"father 析构" << endl;}
void setSon(shared_ptr<son> s) {
son = s;
}
private:
shared_ptr<son> son;
};
class son {
public:
son(){cout <<"son 构造" << endl;}
~son(){cout <<"son 析构" << endl;}
void setFather(shared_ptr<father> f) {
father = f;
}
private:
shared_ptr<father> father;
};
int main(){
shared_ptr<father> f(new father());
shared_ptr<son> s(new son());
f->setSon(s);
s->setFather(f);
}
c. weak_ptr
为了避免
shared_ptr
的环形引用问题,需要引入一个弱指针weak_ptr,它指向一个由
shared_ptr管理的对象而不影响所指对象的生命周期,也就是将一个
weak_ptr绑定到一个
shared_ptr不会改变
shared_ptr的引用计数。不论是否有
weak_ptr指向,一旦最后一个指向对象的
shared_ptr被销毁,对象就会被释放。从这个角度看,
weak_ptr更像是
shared_ptr`的一个助手而不是智能指针。
class father {
public:
father(){cout <<"father 构造" << endl;}
~father(){cout <<"father 析构" << endl;}
void setSon(shared_ptr<son> s) {
son = s;
}
private:
shared_ptr<son> son;
};
class son {
public:
son(){cout <<"son 构造" << endl;}
~son(){cout <<"son 析构" << endl;}
void setFather(shared_ptr<father> f) {
father = f;
}
private:
//shared_ptr<father> father;
weak_ptr<father> father; //替换成weak_ptr 即可。
};
int main(){
shared_ptr<father> f(new father());
shared_ptr<son> s(new son());
f->setSon(s);
s->setFather(f);
}
三、动态内存
1. 内存分区
在C++中内存分为5个区,分别是
堆
、栈
、全局/静态存储区
和代码|常量存储区
|共享内存区
。栈区:又叫堆栈,存储非静态局部变量、函数参数、 返回值等,栈是可以向下生长的
共享内存区:是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内 存,做进程间通讯
堆区:用于程序运行时动态内存分配,堆是可以向上增长的
静态区:存储全局数据和静态数据
代码区:存储可执行的代码、只读常量
- c/c++内存分配
- 从栈上分配:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是栈本身的容量有限
- 从堆上分配:动态成员存储位置,程序在运行的时候用
malloc
或new
申请任意多少的内存空间,程序员自己负责在何时用free
或delete
释放内存.动态内存的生存期由用户决定,使用非常灵活,但问题也最多。- 从静态区上分配:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在.例如全局变量、static变量
2. new 和 delete
在 c++ 中 , 如果要在堆内存中申请空间,那么需要借助
new
操作符,释放申请的空间,使用delete
操作 。而c语言使用的是malloc
和free
,实际上new
和delete
的底层实际上就是malloc
和free
。
- new
在c++中, new是一个关键字,同时也是一个操作符,用于在堆区申请开辟内存 , new的操作还具备以下几个特征:
- 内存申请成功后,会返回一个指向该内存的地址。
- 若内存申请失败,则抛出异常,
- 申请成功后,如果是程序员定义的类型,会执行相应的构造函数
int *a = new int();
stu *s = new stu();
//new的背后先创建
- delete
在c++中,
delete
和new
是成对出现的,所以就有了no new no delete
的说法。delete
用于释放new
申请的内存空间。delete
的操作具备以下几个特征:
- 如果指针的值是0 ,delete不会执行任何操作,有检测机制
- delete只是释放内存,不会修改指针,指针仍然会指向原来的地址
- 重复delete,有可能出现异常
- 如果是自定义类型,会执行析构函数
int *p = new int(6);
delete p ; // 回收数据
*p = 18 ; //依然可以往里面存值,但是不建议这么做。
3. malloc 和 free
malloc
和 free 实际上是C语言 申请内存的语法,在C++ 也得到了保存。只是与 new 和 delete 不同的是, 它们 是函数,而 new 和 delete是作为关键字使用。 若想使用,需要导入#include<stdlib.h>
- malloc
- malloc 申请成功之后,返回的是void类型的指针。需要将void*指针转换成我们需要的类型。
- malloc 要求制定申请的内存大小 , 而new由编译器自行计算。
- 申请失败,返回的是NULL , 比如: 内存不足。
- 不会执行自定义类型的构造函数
int *p=(int *)malloc(int); //如果申请失败,返回的是NULL
- free
free 和 malloc是成堆出现的,所以也有了 no malloc no free的说法。 free 用于释放 mallo申请的内存空间。
- 如果是空指针,多次释放没有问题,非空指针,重复释放有问题
- 不会执行对应的析构
- delete的底层执行的是free
free (p);
4. 动态数组
在申请动态内存时,new有一些灵活上的缺陷,其中一方面是它将内存分配和对象构造绑定在一起,delete将对象的析构和内存释放绑定到一起。如果是单个对象,那么这无可厚非。 但是如果分配一大片内存时,通常都想按需去构造对象,而不是把整个空间都构造了。避免了内存的浪费。
如下:开辟了10个空间,但是只使用了3个位置。
//动态数组:
stu *s = new stu[5]; //分配5个位置,打算存储5个学生对象
s[0] = son()
1. allocator类
allocator 是定义在 头文件 中的一个类, 它可以做到将内存分配和对象构造分离出来。它分配的内存是原始的,未构造的。与vector相似,它也是一个模板类,所以在使用的时候,需要制定分配内存的类型是什么类型。它会根据给定对象的类型来确定恰当的内存大小。分配成功后,返回一个指向内存区域的一个指针。
- allocator 常用函数
allocator | 定义一个名为a的allocator对象,它可以为类型为T的对象分配内存 |
---|---|
a.allocate(n) | 分配一段连续的为构造的内存,能容纳n个类型为T的对象 |
a.deallocate(p, n) | 释放从指针p中地址开始的内存,这块内存保存了n个类型为T的对象。p必须是以个先前有allocate返回的指针,而且n必须是创建p时所要求的大小。在调用deallocate以前,用户必须对每个在这块内存中创建的对象调用destroy |
a.construct(p, args) | p即分配内存返回的指针,可以通过指针运算进行偏移,args 即构造对象使用的参数,用来在p指向的内存块中构造一个对象。 |
a.destroy§ | p为类型为T的指针,对p指向的对象执行析构函数。 |
- 示例
//创建allocator对象
allocator<son> sa;
//申请分配一块5个连续的内存地址
son *p = sa.allocate(5);
//往第一个位置构造一个son
sa.construct(p , "张三",18); //第一个位置
sa.construct(p+1 , "李四",19); //第二个位置
sa.construct(p+4 , "王五",19); //第5个位置
//销毁对象,执行析构函数
sa.destroy(p);
sa.destroy(p+1);
sa.destroy(p+4);
//释放内存,标记这些内存可用
sa.deallocate(p , 5);
一、宏
宏替换是C/C++系列语言的技术特色,C/C++语言提供了强大的宏替换功能,源代码在进入编译器之前,要先经过一个称为“预处理器”的模块,这个模块将宏根据编译参数和实际编码进行展开,展开后的代码才正式进入编译器,进行词法分析、语法分析等等
1. 宏变量
宏变量和const 修饰的在定义语义上没有什么不同,都是可以用来定义常量,但在与const的定义进行对比时,没有任何优势可言,所以建议使用const来定义常量。
#define MAX 30
int scores[MAX]; //表示一个班30个人的成绩数组。
2. 条件宏
条件宏最常出现在头文件中,用于防止头文件被反复包含。
- 头文件的条件宏
#ifndef STUDENT_H
#define STUDENT_H
……
……
#endif
- 用于判断编译的条件宏
通过DEBUG宏,我们可以在代码调试的过程中输出辅助调试的信息。当DEBUG宏被删除时,这些输出的语句就不会被编译。更重要的是,这个宏可以通过编译参数来定义。因此通过改变编译参数,就可以方便的添加和取消这个宏的定义,从而改变代码条件编译的结果.
#define DEBUG
#define REALEASE
int main() {
#ifdef DEBUG
cout <<"debug模式下打印" << endl;
#elif REALEASE
cout <<"release模式下打印" << endl;
#else
cout <<"普通模式下打印" << endl;
#endif
//下面可言继续编写原有的逻辑
cout << "继续执行逻辑代码~~~"<<endl;
}
二、枚举
在C++里面定义常量,可以使用
#define和const
创建常量, 除此之外,还提供了枚举这种方式,它除了能定义常量之外,还表示了一种新的类型,但是必须按照一定的规则来定义。在枚举中定义的成员可以称之为枚举量
,每个枚举量都能对应一个数字,默认是他们的出现顺序,从0开始。
1. 两种方式
C++的枚举定义有两种方式,
限定作用域
和不限定作用域
,根据方式的不同,定义的结构也不同。
- 限定作用域
使用
enum class
或者enum struct
关键字定义枚举,枚举值位于enum
的局部作用域内,枚举值不会隐式的转化成其他类型
enum class Week{MON,TUS,WEN,THU,FRI,STU,SUN};
int val =(int)Week::TUS ; //打印会是1
- 不限定作用域
使用
enum
关键字定义,省略class | struct
, 枚举值域枚举类型位于同一个作用域,枚举值会隐式的转化成整数, 默认是从0开始,依次类推。 不允许有重复枚举值,因为他们属于同一个作用域。
enum traffic_light{red, yellow , green};
//匿名的未限定作用域
enum{red, yellow , green}; //重复定义会报错,因为red\yellow\green 已经定义过了。
//手动给枚举量 设置对应的数值
enum{red = 10, yellow =20 , green =30};
//使用 域操作符来获取对应的枚举量
int a= traffic_light::red;
int b = ::red;
2. 枚举的使用
枚举的目的:增加程序的可读性。枚举类型最常见也最有意义的用处之一就是用来描述状态量。
teacher t1("张三" , Gender::MALE);
teacher t2("李丽丽" , Gender::FEMALE);
teacher t3("李四" , Gender::MALE);
vector<teacher> v;
v.push_back(t1);
v.push_back(t2);
v.push_back(t3);
for(teacher t : v){
switch (t.gender){
case Gender::MALE: //男
cout <<"男老师" << endl;
break;
case Gender::FEMALE:
cout <<"女老师" << endl;
break;
default:
cout <<"性别错误" << endl;
break;
}
}
三、异常处理
异常时指存在于运行时的反常行为,这些行为超出了函数的正常功能的范围。和python一样,c++ 也有自己的异常处理机制。 在c++中,异常的处理包括
throw表达式
和try 语句块
以及异常类
。如果函数无法处理某个问题,则抛出异常,并且希望函数的调用者能够处理,否则继续向上抛出。如果希望处理异常,则可以选择捕获。
void test(){
try{
autoresult = do_something();
}catch(Some_error){
//处理该异常
}
}
int dosomething(){
if(条件满足){
return result;
}else{
throw Some_error(); //抛出异常
}
}
1. 不使用异常机制
- 终止程序
可以使用
abort
|exit
来终止程序的执行
int getDiv( int a , int b){
if(int b == 0 ){
abort(); // 或者是 exit(4) //括号内为错误的代码,可以使用常量定义
}
return a / b;
}
- 显示错误代码
与直接终止程序的突兀对比,错误代码显得更为友好些,同时也可以根据错误代码给出相应的提示。
int getDiv( int a , int b){
if(int b == 0 ){
//abort(); // 或者是 exit(4) //括号内为错误的代码,可以使用常量定义
return -1000001;// 外部可以对此代码进行处理
}
return a / b;
}
2. 使用异常机制
exception | 最常见的问题 |
---|---|
runtime_error | 只有在运行时才能检测出的问题 |
range_error | 运行时错误:生成的结果超出了有意义的值域范围 |
overflow_error | 运行时错误:计算上溢 |
underflow_error | 运行时错误:计算下溢 |
logic_error | 程序逻辑错误 |
domain_error | 逻辑错误:参数对应的结果值不存在 |
invalid_argument | 逻辑错误:参数无效 |
length_error | 逻辑错误:试图创建一个超过该类型最大长度的对象 |
out_of_range | 逻辑错误:使用一个超出有效范围的值 |
1. 捕获异常
若程序想对异常进行处理,以达到让程序继续友好的执行下去,可以使用捕获异常。
exception
是所有异常的父类 ,runtime_error
可以作为运行时出现的异常捕获。一旦发生异常,那么后面的语句将不会执行。一般捕获异常,采用try{} catch(){}的结构来捕获异常,catch可以有多个。
可以使用catch(...) 来表示捕获所有异常
, 但它必须出现在所有catch的最后。
try{
//执行的代码逻辑
}catch(runtime_error err ){ //捕获的异常类型。
//捕获到异常,执行的逻辑
cout << err.what() << endl; //打印错误信息
}
2. 抛出异常
函数内部如果不想处理异常,可以选择抛出异常(throw) , 进而由调用的它函数处理,若该函数仍未处理,则继续往上抛出。注意: 若抛出的语句位于try语句块内,则优先匹配try 语句匹配的异常,若没有匹配上,才选择向上抛出。 throw可以抛出任意类型的异常,要求是这些类型必须是这些类的对象可复制和移动。同样抛出异常时,后面的语句不会执行。
int calcDiv(int a, int b){
if(b == 0){
throw runtime_error("除数不能为0 ");
}
return a / b;
}
3. noexcept
如果预先知道某个函数不会抛出异常,那么可以在函数定义的参数列表后面跟上关键字
noexcept
, 通常会存在于移动构造函数 和 移动赋值函数中。即便某个函数声明不会抛出异常,但是在内部真的抛出异常,编译器仍然允许编译通过,但是在运行时为了确保不抛出异常的承诺,会调用terminate 终止程序的执行,甚至不会向上传递异常。
stu(stu && s) noexcept { //移动赋值函数
}
void operator=(stu && s) noexcept{ //表示不会抛出异常。
}
来源:CSDN
作者:hongge_smile
链接:https://blog.csdn.net/hongge_smile/article/details/104247857