c++之智能指针,异常处理,枚举

拈花ヽ惹草 提交于 2020-02-13 11:15:32

智能指针,文件IO

二、智能指针&动态内存

1. 指针潜在问题

c++ 把内存的控制权对程序员开放,让程序显式的控制内存,这样能够快速的定位到占用的内存,完成释放的工作。但是此举经常会引发一些问题,比如忘记释放内存。由于内存没有得到及时的回收、重复利用,所以在一些c++程序中,常会遇到程序突然退出、占用内存越来越多,最后不得不选择重启来恢复。造成这些现象的原因可以归纳为下面几种情况:

  1. 野指针: 内存已经被释放、但是指针仍然指向它。这时内存有可能被系统重新分配给程序使用,从而会导致无法估计的错误
  2. 重复释放:程序试图释放已经释放过的内存,或者释放已经被重新分配过的内存,就会导致重复释放错误.
  3. 内存泄漏: 不再使用的内存,并没有释放,或者忘记释放,导致内存没有得到回收利用。 忘记调用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++内存分配
  1. 从栈上分配:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是栈本身的容量有限
  2. 从堆上分配:动态成员存储位置,程序在运行的时候用 mallocnew申请任意多少的内存空间,程序员自己负责在何时用 freedelete释放内存.动态内存的生存期由用户决定,使用非常灵活,但问题也最多。
  3. 从静态区上分配:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在.例如全局变量、static变量

2. new 和 delete

在 c++ 中 , 如果要在堆内存中申请空间,那么需要借助 new 操作符,释放申请的空间,使用 delete操作 。而c语言使用的是mallocfree ,实际上newdelete 的底层实际上就是 mallocfree

  • new

在c++中, new是一个关键字,同时也是一个操作符,用于在堆区申请开辟内存 , new的操作还具备以下几个特征:

  1. 内存申请成功后,会返回一个指向该内存的地址。
  2. 若内存申请失败,则抛出异常,
  3. 申请成功后,如果是程序员定义的类型,会执行相应的构造函数
int *a = new int();
stu *s = new stu();

//new的背后先创建
  • delete

在c++中,deletenew 是成对出现的,所以就有了no new no delete的说法。delete用于释放new 申请的内存空间。 delete 的操作具备以下几个特征:

  1. 如果指针的值是0 ,delete不会执行任何操作,有检测机制
  2. delete只是释放内存,不会修改指针,指针仍然会指向原来的地址
  3. 重复delete,有可能出现异常
  4. 如果是自定义类型,会执行析构函数
int *p = new int(6);
delete p ; // 回收数据

*p = 18 ; //依然可以往里面存值,但是不建议这么做。

3. malloc 和 free

malloc 和 free 实际上是C语言 申请内存的语法,在C++ 也得到了保存。只是与 new 和 delete 不同的是, 它们 是函数,而 new 和 delete是作为关键字使用。 若想使用,需要导入#include<stdlib.h>

  • malloc
  1. malloc 申请成功之后,返回的是void类型的指针。需要将void*指针转换成我们需要的类型。
  2. malloc 要求制定申请的内存大小 , 而new由编译器自行计算。
  3. 申请失败,返回的是NULL , 比如: 内存不足。
  4. 不会执行自定义类型的构造函数
int *p=(int *)malloc(int); //如果申请失败,返回的是NULL

  • free

free 和 malloc是成堆出现的,所以也有了 no malloc no free的说法。 free 用于释放 mallo申请的内存空间。

  1. 如果是空指针,多次释放没有问题,非空指针,重复释放有问题
  2. 不会执行对应的析构
  3. 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{ //表示不会抛出异常。
    
}


易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!