【C++篇】C++新增的一些基础特性

本文详细介绍了C++中的名称空间,包括其定义、使用、命名空间与头文件的关系以及内联和嵌套命名空间。接着讨论了引用的概念,包括引用变量的创建、本质、作为函数参数和返回值的使用。然后,阐述了函数重载的基本原理和异常处理机制,包括传统的错误处理方式和C++的异常处理机制,以及异常的生命周期和安全性问题。最后,提到了C++的异常安全问题和标准库异常。
摘要由CSDN通过智能技术生成

友情链接:C/C++系列系统学习目录

知识总结顺序参考C Primer Plus(第六版)和谭浩强老师的C程序设计(第五版)等,内容以书中为标准,同时参考其它各类书籍以及优质文章,以至减少知识点上的错误,同时方便本人的基础复习,也希望能帮助到大家
 
最好的好人,都是犯过错误的过来人;一个人往往因为有一点小小的缺点,将来会变得更好。如有错漏之处,敬请指正,有更好的方法,也希望不吝提出。最好的生活方式就是和努力的大家,一起奔跑在路上



程序示例

说明:本篇是C++篇的的第一部分,由于C++是继承C而来,大部分基础知识就不再总结,一些细节上的改动总结在最后,大的变动,例如iostream库,命名空间等单独总结

#include <iostream>                           //a PREPROCEssoR directive,


int main()                                    //function header
{                                             //start of function body
    std::cout << "Come up and C++ me some time." ;   //message
    std::cout << std::endl;                              //start a new line
    std::cout << "You won't regret it" << std::endl;    //more: output
    
    return 0;
}

和C一样,某些窗口环境会在单独的窗口运行程序,然后在程序运行结束后自动关闭窗口。如果遇到这种情况,可以在程序中添加额外的代码,让窗口等待用户按下一个键后才关闭。一种方法是, 在程序的return语句前添加一行代码:

C:getchar();
C++:cin.get();

🚀一、名称空间

⛳(一)什么是名称空间

两个术语:

**声明区域:**声明区域是可以在其中进行声明的区域。例如,可以在函数外面声明全局变量,对于这种变量,其声明区域为其声明所在的文件。对于在函数中声明的变量,其声明区域为其声明所在的代码块。

**潜在作用域:**变量的潜在作用域从声明点开始,到其声明区域的结尾。因此潜在作用域比声明区域小,这是由于变量必须定义后才能使用。

然而,变量并非在其潜在作用域内的任何位置都是可见的。例如,它可能被另一个在嵌套声明区域中声明的同名变量隐藏。例如,在函数中声明的局部变量(对于这种变量,声明区域为整个函数)将隐藏在同一个文件中声明的全局变量(对于这种变量,声明区域为整个文件)。

在C++中,名称可以是变量、函数、结构、枚举、类以及类和结构的成员。当随着项目的增大,名称相互冲突的可能性也将增加。使用多个厂商的类库时,可能导致名称冲突。例如,两个库可能都定义了名为List、Tree和Node的类,但定义的方式不兼容。用户可能希望使用一个库的List类,而使用另一个库的Tree类。这种冲突被称为名称空间问题。

C++新增了这样一种功能,即通过定义一种新的声明区域来创建命名的名称空间,这样做的目的之一是提供一个声明名称的区域。一个名称空间中的名称不会与另外一个名称空间的相同名称发生冲突,同时允许程序的其他部分使用该名称空间中声明的东西。

⛳(二)命名空间的定义

例如,下面的代码使用新的关键字namespace创建了两个名称空间:Jack和Jill。

namespace Jack {
    double pail;                      //variable declaration
    void fetch();                     //function prototype
    int pal;                          // variable declaration
    struct well {...};                // structure declaration
}

namespace Jill {
    double bucket (double n){...}     // function definitiondouble fetch;
    double fetch;                     // variable declaration
    int pal;                          // variable declaration
    struct Hil1{...};                 // structure declaration
}
  1. 名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。因此,在默认情况下,在名称空间中声明的名称的链接性为外部的(除非它引用了常量)。

  2. 除了用户定义的名称空间外,还存在另一个名称空间——全局名称空间(global namespace)。它对应于文件级声明区域,因此前面所说的全局变量现在被描述为位于全局名称空间中。

  3. 任何名称空间中的名称都不会与其他名称空间中的名称发生冲突。因此,Jack 中的 fetch可以与Sill中的fetch共存,Jill中的 Hill 可以与外部Hill 共存。名称空间中的声明和定义规则同全局声明和定义规则相同。

  4. 名称空间是开放的 (open) ,即可以把名称加入到已有的名称空间中。例如,下面这条语句将名称goose添加到Jill中已有的名称列表中:

    namespace Jill {
    	char * goose(const char *);
    }
    

    同样,原来的Jack名称空间为fetch()函数提供了原型。可以在该文件后面(或另外一个文件中〉再次使用Jack名称空间来提供该函数的代码:

    namespace Jack {
    	void fetch()
        {
            ...
        }
    }
    

拓展:

传统上,程序员通过将其定义的全局实体名字设得很长来避免命名空间污染问题,这样的名字中通常包含表示名字所属库的前缀部分:

class cplusplus_primer_Query { ... };
string cplusplus_primer_make_plural(size_t, string&);

⛳(三)使用名称空间

当然,需要有一种方法来访问给定名称空间中的名称。最简单的方法是,通过作用域解析运算符::,使用名称空间来限定该名称:

Jack::pai = 32.34;   //use a variable
Jill::Hil1 mole;     // create a type Hill structure
Jack::fetch{};       //use a function

未被装饰的名称(如 pail)称为未限定的名称(unqualified name);包含名称空间的名称(如 Jack::pair)称为限定的名称(qualified name)。

在iostream中定义的用于输出的cout变量实际上是std::cout,而endl实际上是std::endl

🎈1.using 声明和using 编译指令

我们并不希望每次使用名称时都对它进行限定,因此C++提供了两种机制(using声明和using编指令)来简化对名称空间中名称的使用。using声明使特定的标识符可用,using编译指令使整个名称空间可用。

using声明由被限定的名称和它前面的关键字using组成:

using Jill::fetch;    //a using declaration
  • using声明将特定的名称添加到它所属的声明区域中。例如 main()中的using声明Jill::fetch将fetch添加到main()定义的声明区域中。完成该声明后,便可以使用名称fetch代替Jill::fetch

    namespace Jill{
        double bucket (double n){...}
        double fetch;
        struct Hill{ ... };
    }
    char fetch;
    int main(){
        using Jill::fetch;  //put fetch into local namespace
        double fetch;       //Error! Already have a local fetch
        cin >> fetch;       //read a value into Jill::fetch
        cin >> ::fetch;     //read avalue into global fetch
    }
    

    由于using声明将名称添加到局部声明区域中,因此这个示例避免了将另一个局部变量也命名为fetch,如果先定义了局部变量fetch,则不允许再用另外using声明将相同名称添加到局部声明区域中,和其他局部变量一样,fetch也将覆盖同名的全局变量

    如果要使用全局变量fetch,使用::a,全局变量 a 表达为 ::a,用于当有同名的局部变量时来区别两者。

  • 在函数的外面使用using声明时,将把名称添加到全局名称空间中:

    void other();
    namespace Jill{
        double bucket (double n) {...} 
        double fetch;
        struct Hill{...};
    }
    using Jill::fetch;    //put fetch into global namespace
    int main()
    {
        cin >> fetch;     //read a value into Jill::fetch
        other()
        ...
    }
    
    void other()
    {
        cout << fetch;    //display Jill::fetch
        ...
    }
    

using编译指令由名称空间名和前面的关键字using namespace组成:

using namespace Jack;  // make all the names in Jack available
  • 它使名称空间中的所有名称都可用,而不需要使用作用域解析运算符,同样,在函数中使用using编译指令,将使其中的名称在该函数中可用:

    int main()
    {
        using namespace Jack;    //make names available in vorn()
    }
    
  • 在全局声明区域中使用using编译指令,将使该名称空间的名称全局可用:

    #include <iostream>  //places names in namespace std
    using namespace std; // make names avairable globally
    

变量Jack::pal和Jill:pal是不同的标识符,表示不同的内存单元。但是:

  1. 如果使用using声明,情况将发生变化:

    using jack::pal;
    using jill::pal;
    

    事实上,编译器不允许您同时使用上述两个using声明,因为这将导致二义性。

  2. 使用的using编译指令和使用的using声明冲突,同样会导致二义性:

    namespace China {
        float population = 14.1; //单位: 亿
        std::string capital = "北京";
    }
    
    namespace Japan {
        float population = 1.27; //单位: 亿
        std::string capital = "东京";
    }
    
    using namespace China;      
    using Japan::population;
    

    China命名空间中有population又单独声明了Japan中的population,会造成population不明确的报错,需要在前面指定完整的域名(Japn::population)

🎈2.using 声明和using 编译指令之比较

namespace Jill {
    double bucket(double n) {...}
    double fetch;
    struct Hill{...};
}
  
char fetch;				//global namespace
int main()
{
    using namespace Jill;      //import all namespace names
    Hill Thrill;               //create a type Jill::Hill structure
    double water = bucket (2); //use Jill::bucket ();
    double fetch;              //not an error; hides Jili::fetch
    cin >> fetch;              //read a value  into the local fetch
    cin >> ::fetch;            //read a value  into global fetch
    cin >> Jill::fetch         //read a value  into Jill::fetch
}

int foom() {
    Hill top;                  //error
    Jill::Hill crest;          //valid
}
  • 使用using编译指令导入一个名称空间中所有的名称与使用多个using声明是不一样的,而更像是大量使用作用域解析运算符。使用using声明时,就好像声明了相应的名称一样。
  • 注意:假设名称空间和声明区域定义了相同的名称。如果试图使用using声明将名称空间的名称导该声明区域,则这两个名称会发生冲突,从而出错。如果使用using编译指令将该名称空间的名称导入该声明区域,则局部版本将隐藏名称空间版本。
  • 一般说来,使用using声明比使用using编译指令更安全,这是由于它只导入指定的名称。如果该名称与局部名称发生冲突,编译器将发出指示。using编译指令导入所有名称,包括可能并不需要的名称。如果与局部名称发生冲突,则局部名称将覆盖名称空间版本,而编译器并不会发出警告。另外,名称空间的开放性意味着名称空间的名称可能分散在多个地方,这使得难以准确知道添加了哪些名称。

建议使用:

int x;
std::cin >> X;
std::cout << x << std::endl;

//或者

using std::cin;
using std::cout;
using std::endl;
int x;
cin >> X;
cout << x << endl;

而不是:

using namespace std;  // avoid as too indiscriminate

⛳(四)名称空间和头文件

C++是在C语言的基础上开发的,早期的 C++ 还不完善,不支持命名空间,没有自己的编译器,而是将 C++ 代码翻译成C代码,再通过C编译器完成编译。这个时候的 C++ 仍然在使用C语言的库,stdio.h、stdlib.h、string.h 等头文件依然有效;此外 C++ 也开发了一些新的库,增加了自己的头文件,例如:

  • iostream.h:用于控制台输入输出头文件。
  • fstream.h:用于文件操作的头文件。
  • complex.h:用于复数计算的头文件。

和C语言一样,C++ 头文件仍然以.h为后缀,它们所包含的类、函数、宏等都是全局范围的。

后来 C++ 引入了命名空间的概念,计划重新编写库,将类、函数、宏等都统一纳入一个命名空间,这个命名空间的名字就是std。std 是 standard的缩写,意思是“标准命名空间”。

但是这时已经有很多用老式 C++ 开发的程序了,它们的代码中并没有使用命名空间,直接修改原来的库会带来一个很严重的后果:程序员会因为不愿花费大量时间修改老式代码而极力反抗,拒绝使用新标准的 C++ 代码。

C++ 开发人员想了一个好办法,保留原来的库和头文件,它们在 C++ 中可以继续使用,然后再把原来的库复制一份,在此基础上稍加修改,把类、函数、宏等纳入命名空间 std 下,就成了新版 C++ 标准库。这样共存在了两份功能相似的库,使用了老式 C++ 的程序可以继续使用原来的库,新开发的程序可以使用新版的 C++ 库。

为了避免头文件重名,新版 C++ 库也对头文件的命名做了调整,去掉了后缀.h,所以老式 C++ 的iostream.h变成了iostreamfstream.h变成了fstream。而对于原来C语言的头文件,也采用同样的方法,但在每个名字前还要添加一个c字母,所以C语言的stdio.h变成了cstdiostdlib.h变成了cstdlib

需要注意的是,旧的 C++ 头文件是官方所反对使用的,已明确提出不再支持,但旧的C头文件仍然可以使用,以保持对C的兼容性。实际上,编译器开发商不会停止对客户现有软件提供支持,可以预计,旧的 C++ 头文件在未来数年内还是会被支持。

在这里插入图片描述

  1. 旧的 C++ 头文件,如 iostream.h、fstream.h 等将会继续被支持,尽管它们不在官方标准中。这些头文件的内容不在命名空间 std 中。
  2. 新的 C++ 头文件,如 iostream、fstream 等包含的基本功能和对应的旧版头文件相似,但头文件的内容在命名空间 std 中。
  3. 标准C头文件如 stdio.h、stdlib.h 等继续被支持。头文件的内容不在 std 中。
  4. 具有C库功能的新C++头文件具有如 cstdio、cstdlib 这样的名字。它们提供的内容和相应的旧的C头文件相同,只是内容在 std 中。

可以发现,对于不带.h的头文件,所有的符号都位于命名空间 std 中,使用时需要声明命名空间 std;对于带.h的头文件,没有使用任何命名空间,所有符号都位于全局作用域。这也是 C++ 标准所规定的。

不过现实情况和 C++ 标准所期望的有些不同,对于原来C语言的头文件,即使按照 C++ 的方式来使用,即#include <cstdio>这种形式,那么符号可以位于命名空间 std 中,也可以位于全局范围中,请看下面的两段代码。

(1)使用命名空间 std:

#include <cstdio>
int main(){    
     std::printf("http://c.biancheng.net\n");    
     return 0;
}

(2)不使用命名空间 std:

#include <cstdio>
int main(){    
	printf("http://c.biancheng.net\n");    
 	return 0;
}

这两种形式在 Microsoft Visual C++ 和 GCC下都能够编译通过,也就是说,大部分编译器在实现时并没有严格遵循C++标准,它们对两种写法都支持,程序员可以使用 std 也可以不使用。

第 (1) 种写法是标准的,第 (2) 种不标准,虽然它们在目前的编译器中都没有错误,但我依然推荐使用第 1) 种写法,因为标准写法会一直被编译器支持,非标准写法可能会在以后的升级版本中不再支持。

同时,虽然 C++ 几乎完全兼容C语言,C语言的头文件在 C++ 中依然被支持,但 C++ 新增的库更加强大和灵活,请读者尽量使用这些 C++ 新增的头文件,例如 iostream、fstream、string 等。

⛳(五)名称空间的其他的特性

🎈1.名称空间的嵌套

namespace elements
{
    namespace fire
    {
        int flame;
        ...
    }
        float water;
}
  • flame指的是 elements::fIre::flame 。

  • 同样可以使用using编译指令:using namespace elements::fire;使内部名称的名称可用

  • using编译指令是可传递的,using namespace myth;等价于using namespace myth;加上using namespace elements;

  • 另外,也可以在名称空间中使用using编译指令和 using声明,如下所示:

    namespace myth
    {
        using Jill::fetch;
        using namespace elements;
        using std::cout;
        using std::cin;
    }
    

    假设要访问Jill::fetch。由于Jill::fetch 现在位于名称空间myth(在这里,它被叫做fetch)中,因此可以这样访问它:

    std::cin >> myth::fetch;
    

    当然,由于它也位于Jill名称空间中,因此仍然可以称作Jill::fetch

    std::cout << Jill::fetch;      // display value read into myth::fetch
    

🎈2.内联命名空间

C++11新标准引入了一种新的嵌套命名空间,称为内联命名空间(inline namespace)。和普通的嵌套命名空间不同,内联命名空间中的名字可以被外层命名空间直接使用。也就是说,我们无须在内联命名空间的名字前添加表示该命名空间的前缀,通过外层命名空间的名字就可以直接访问它。

定义内联命名空间的方式是在关键字namespace前添加关键字inline:

namespace elements
{
    inline namespace fire
    {
        int flame;
        ...
    }
        float water;
}

访问方式:elements::flame,但是如果在elements中还有单独的另外一个flame,则必须要使用作用域解析运算符::

🎈3.给名称空间起别名

可以给名称空间创建别名。例如,假设有下面的名称空间:

namespace my_very_favorite_things{...};

则可以使用下面的语句让mvft成为my_very_favorite_things 的别名:

namespace mvft = my_very_favorite_things;

可以使用这种技术来简化对嵌套名称空间的使用:

namespace MEF = myth::elements::fire;
using MEF::f1ame;

🎈4.未命名的名称空间

可以通过省略名称空间的名称来创建未命名的名称空间:

namespace   //unnamed namespace
{
    int ice;
    int bandycoot;
}
  • 未命名的命名空间中定义的变量拥有静态生命周期:它们在第一次使用前创建,并且直到程序结束才销毁
  • 定义在未命名的命名空间中的名字可以直接使用,毕竟我们找不到什么命名空间的名字来限定它们;同样的,我们也不能对未命名的命名空间的成员使用作用域运算符。
  • 未命名的命名空间中定义的名字的作用域与该命名空间所在的作用域相同。

在标准C++引入命名空间的概念之前,程序需要将名字声明成static的以使其对于整个文件有效。在文件中进行静态声明的做法是从C语言继承而来的。在C语言中声明为static的全局实体在其所在的文件外不可见

在文件中进行静态声明的做法已经被C++标准取消了,现在的做法是使用未命名的命名空间

🚀二、新增复合类型:引用

在C++中新增加了引用的概念,引用可以看作一个已定义变量的别名

⛳(一)引用变量的创建

语法:Type& name = var;

C和C++使用&符号来指示变量的地址。C++给&符号赋予了另一个含义,将其用来声明引用。例如,要将rodents作为rats变量的别名,可以这样做:

int rats;
int & rodents = rats;  //makes rodents an alias for rats
  • 引用不是一种独立的数据类型,对引用只有声明没有定义

  • 声明一个引用后,不能再使之作为另一个变量的引用

  • 声明一个引用时,必须进行初始化(实际上应该为:引用分配内存空间时,必须进行初始化!)例如:

    struct Test
    {
        int a[5];
        int &b;
    }
    //没有创建结构体变量时并未分配空间
    
  • 不能建立引用数组:

    int a[3];
    int &b[3]=a; //错误,不能建立引用数组
    int &b=a[0]; //错误,不能作为数组元素的别名
    
  • 引用作为其它变量的别名而存在,因此在一些场合可以代替指针,引用相对于指针来说具有更好的可读性和实用性

  • 不能建立引用的引用,不能建立引用的指针

  • 对同一内存空间可以取好几个名字

⛳(二)引用的本质

引用在C++中的内部实现是一个指针常量(常指针),即:

Type& name --> Type* const name
    
int &b = a;   -->  int * const b = &a;
  • 引用的实现实际上是占用内存空间的,但程序把它按照不占用内存空间来处理:

    从使用的角度,引用会让人误会其只是一个别名,没有自己的存储空间。这是C++为了实用性而做出的细节隐藏,实际上引用有自己的空间,大小与指针相同,只是经过编译器处理后,访问这块内存时将直接转而访问其指向的内存。因此在程序中无法读取到这块内存本身。

  • C++编译器在编译过程中使用常指针作为引用的内部实现,因此引用所占用的空间大小与指针相同。

  • 当实参传给形参引用的时候,只不过是c++编译器帮我们程序员手工取了一个实参地址,传给了形参引用(指针常量)

⛳(三)引用作为函数参数

引用经常被用作函数参数,使得函数中的变量名成为调用程序中的变量的别名。这种传递参数的方法称为按引用传递。按引用传递允许被调用的函数能够访问调用函数中的变量。C++新增的这项特性是对C语言的超越,C语言只能按值传递。按值传递导致被调用函数使用调用程序的值的拷贝。当然,C语言也允许避开按值传递的限制,采用按指针传递的方式。

// demo 8-25.c
#include <stdio.h>
#include <stdlib.h>
void swap(int a, int b)
{
    int tmp = a;
    a = b;
    b = tmp;
}
//方式一, 使用指针
void swap1(int *a, int *b)
{
    int tmp = *a;
    *a = *b;
    *b = tmp;
}
//方式二, 使用引用
void swap2(int &a, int &b)
{
    //int &c; //不可以
    int tmp = a;
    a = b;
    b = tmp;
}
int main(void){
    int x = 10, y = 100;
    //swap1(&x, &y);
    swap2(x, y);
    printf("x: %d, y: %d\n", x, y);
    system("pause");
    return 0;
}

但是,如果实参与引用参数不匹配,C++将生成临时变量。当前,仅当参数为const引用时,C++才允许这样做,但以前不是这样。

如果引用参数是const,则编译器将在下面两种情况下生成临时变量:

  • 实参的类型正确,但不是左值;
  • 实参的类型不正确,但可以转换为正确的类型。
double refcube (const double &ra){
	return ra * ra * ra;
}

double side = 3.0 ;
double * pd = &side;
double & rd = side;
long edge = 5L;
double lens [4] = {2.0,5.0,10.0, 12.o} ;
double cl = refcube (side) ;              // ra is side
double c2 = refcube (lens[2]);           // ra is lens [2]
double c3 = refcube (rd);                 // ra is rd is side
double c4 = refcube ( *pd) ;              // ra is *pd is side
double c5 = refcube (edge) ;              // ra is temporary variable
double c6 = refcube ( 7.0);               // ra is temporary variable
double c7 = refcube (side + 10.0);        // ra is temporary variable
  • 参数side、lens[2]、rd和*pd都是有名称的、double类型的数据对象,因此可以为其创建引用,而不需要临时变量(还记得吗,数组元素的行为与同类型的变量类似)
  • edge虽然是变量,类型却不正确,double引用不能指向long。另一方面,参数7.0和side + 10.0的类型都正确,但没有名称,在这些情况下,编译器都将生成一个临时匿名变量,并让ra指向它。这些临时变量只在函数调用期间存在,此后编译器便可以随意将其删除。
  • 现在的C++标准是禁止创建临时变量的,如果接受引用参数的函数的意图是修改作为参数传递的变量,则创建临时变量将阻止这种意图的实现。但如果声明将引用指定为const,C++将在必要时生成临时变量。实际上,对于形参为const引用的C++函数,如果实参不匹配,则其行为类似于按值传递,为确保原始数据不被修改,将使用临时变量来存储值。(如果函数调用的参数不是左值或与相应的const引用参数的类型不匹配,则C++将创建类型正确的匿名变量,将函数调用的参数的值传递给该匿名变量,并让参数来引用该变量。)

C++11新增了另一种引用——右值引用(rvalue reference)。这种引用可指向右值,是使用&&声明的:

double && rref = std::sqrt(36.00);// not allowed for double &
double j = 15.0 ;
double && jref = 2.0* j + 18.5 ;   // not allowed for double &
std::cout << rref << '\n ';     // display 6.0
std::cout << jref << '\n ' ;    // display 48.5;

新增右值引用的主要目的是,让库设计人员能够提供有些操作的更有效实现,以前的引用(使用&声明的引用)现在称为左值引用。

在这里插入图片描述

⛳(四)引用作为函数返回值

当返回类型为引用时,我们可以用引用来接收,也可以用普通变量来接收。用引用作函数的返回值的最大的好处是在内存中不产生返回值的副本,但需要注意几个地方:

#include <iostream>
#include <stdlib.h>
#include <stdio.h>

using namespace std;

//返回局部变量
int demo1() {
    int i = 1;
    printf("demo1()i 的地址: %p, i=%d\n", &i, i);
    
    return i;
}

//返回局部变量的引用(这里做测试,实际上不要返回局部变量的引用,即使有的编译器会正确输出局部变量的值)
int &demo() {
    int i = 666;
    printf("demo()i 的地址: %p, i=%d\n", &i, i);
    
    return i;
}

//返回静态变量的引用
int &demo_static() {
    static int i = 666;
    printf("demo_static()i 的地址: %p, i=%d\n", &i, i);
    
    return i;
}

// 函数返回形参(普通参数)当引用
int &demo3(int var) {
    var = 666;
    return var;
}

// 函数返回形参(引用)当引用
int &demo4(int &var) {
    var = 666;
    return var;
}

int main(void) {
    int ret = demo(); //虽然demo()的返回值是引用,但使用普通变量去接是可以的
    
    cout << endl;
    
    
    //第一种情况 函数返回局部变量引用不能成为其它引用的初始值,函数把地址复制后返回,但地址指向的内容已经被释放了,这样其内容将不可预料
    cout << "第一种情况,返回局部变量的引用:" << endl << endl;
    //例如: 
    int &i1 = demo();
    printf("i1的地址=%p,i1的值=%d\n",&i1,i1);   
	cout << endl;
    
    //此时只要去访问栈区,就会导致错误:
    printf("访问栈区:i1的地址=%p,i1的值=%d\n",&i1,i1);   //访问栈区
    cout << endl;
    
    //或者访问栈区,去修改i的值:
    demo1(); //会发现demo1()的i和demo()的i地址相同,修改i为1,自然i1也变成了1
    printf("访问栈区:i1的地址=%p,i1的值=%d\n",&i1,i1); 
    cout << endl << endl << endl;
    
    
    //第二种情况 函数返回局部变量不能做为左值,如果使用会出现和第一种一样的情况
    cout << "第二种情况,局部变量做为左值:" << endl << endl;
    demo() = 888; 
    printf("i1的地址=%p,i1的值=%d\n",&i1,i1);  
    cout << endl;
    demo1();
    printf("i1的地址=%p,i1的值=%d\n",&i1,i1);  
    
    cout << endl << endl << endl;

   
      
    //第三种情况 返回静态变量或全局变量的引用可以成为左值或是其它引用的初始值:demo_static(&addr) = 888;
    cout << "第三种情况,返回静态变量的引用:" << endl << endl;
    int& i2 = demo_static();
    printf("i2的地址 = %p value: %d\n", &i2, i2);
    cout << endl;
    demo1();   //这种就不会修改i2的值
    printf("i2的地址 = %p value: %d\n", &i2, i2);
    
   
    system("pause");
    return 0;
}

在这里插入图片描述

  1. demo1()返回普通类型:

    返回普通类型对象其实是返回这个对象的拷贝,c++其实会创建一个临时变量,这个临时变量被隐藏了

    demo()返回引用:

    返回引用实际返回的是一个指向返回值的隐式指针,在内存中不会产生副本,是直接将i拷贝给i1,这样就避免产生临时变量,相比返回普通类型的执行效率更高,而且这个返回引用的函数也可以作为赋值运算符的左操作数,但要注意以上提到的问题

  2. 若返回静态变量或全局变量可以成为其他引用的初始值即可作为右值使用,也可作为左值使用

  3. 返回形参当引用(注:C++链式编程中,经常用到引用,运算符重载专题)

⛳(五)指针引用和常引用

指针引用:

// demo 8-29.c
#include <stdio.h>
#include <stdlib.h>

void boy_home(int **meipo){
    static int boy = 23;
    *meipo = &boy;
}

void boy_home1(int* &meipo){
    static int boy = 23;
    meipo = &boy;
}

int main(void){
    /*int x = 666;
    int *p = &x;
    
    int* &q = p; //定义指针变量的引用
    printf("*p: %d *q: %d\n", *p, *q);
    */
    
    int *meipo = NULL;
    //boy_home(&meipo);
    boy_home1(meipo);
    printf("boy: %d\n", *meipo);
    
    system("pause");
    return 0;
}

常引用:

如果程序员的意图是让函数使用传递给它的信息,而不对这些信息进行修改,同时又想使用引用,则应使用常量引用

语法:

const Type& name = var;

分两种情况:

  1. 用变量初始化常引用

  2. 用字面量初始化常量引用

#include <stdio.h>
#include <stdlib.h>

int main(void){
    int a = 10;
    //int &b = a;
    //1.用变量初始化常引用
    const int &b = a;
    
    //b = 100; //常引用是让变量引用变成只读,不能通过引用对变量进行修改
    printf("a: %d\n", a);
    
    //2.用字面量初始化常量引用
    const int c1 = 10;
    const int &c2 = 10; //这个是在 C++中,编译器会对这样的定义的引用分配内存,这算是一个特例
    int c3 = c2;
    
    //c2 = 100;//不能修改
    
    system("pause");
    return 0;
}	
  • const & int e 相当于 const int * const e
  • 普通引用 相当于 int *const e1
  • 当使用常量(字面量)对const引用进行初始化时,C++编译器会为常量值分配空间,并将引用名作为这段空间的别名
  • 使用字面量对const引用初始化后,将生成一个只读变量

引用非常适合用于结构和类(C++的用户定义类型)。确实,引入引用主要是为了用于这些类型的,而不是基本的内置类型。与复制原始结构的拷贝相比,使用引用可节省时间和内存

如果accumulate()返回一个结构,而不是指向结构的引用,将把整个结构复制到一个临时位置,再将这个拷贝复制给dup。但在返回值为引用时,将直接把team复制到dup,其效率更高。

返回引用的函数实际上是被引用的变量的别名。

在这里插入图片描述

🚀三、函数重载

函数重载是一种静态的多态,c语言不支持多态特性

oop的三大特性:

  • 封装:将客观事物封装成抽象的类,而类可以把自己的数据和方法暴露给可信的类或者对象,对不可信的类或对象则进行信息隐藏
  • 继承:可以使用现有类的所有功能,并且无需重新编写原来的类即可对功能进行拓展;
  • 多态:一个类实例的相同方法在不同情形下有不同的表现形式,使不同内部结构的对象可以共享相同的外部接口。

函数重载属于多态的一小部分:

int add(int a, int b) {
    cout << "调用 add 版本 1" << endl;
    return a +b;
}

float add(float a, float b) {
    cout << "调用 add 版本 2" << endl;
    return a+b;
}	
  1. 构成重载的条件:

    • 函数名重载即函数名相同,但是, 函数的参数(形参)绝不相同:包括参数个数不同或参数个数相同, 但是参数的类型不同
    • 只有返回类型不同,不能构成函数重载、只有形参变量名不同, 不能构成函数重载.

    即:函数重载的关键是函数的参数列表——也称为函数特征标如果两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则它们的特征标相同,而变量名是无关紧要的。C++允许定义名称相同的函数,条件是它们的特征标不同。如果参数数目和/或参数类型不同,则特征标也不同。

  2. 类设计和STL经常使用引用参数,因此知道不同引用类型的重载很有用。请看下面三个原型:

    void sink ( double & r1) ;  //matches modifiable lvalue
    void sank (const double & r2); // matches modifiable or const lvalue, rvalue
    void sunk ( double && r3) ;   // matches rvalue
    

    r1参数可以与double类型变量匹配,r2参数可以与double类型变量,也可与const类型的double匹配,而r3与double类型右值匹配;注意到与r1或r3匹配的参数都与r2匹配这就带来了一个问题:如果重载使用这三种参数的函数,结果将如何?答案是将调用最匹配的版本:

    double x = 55.5 ;
    const double y = 32.0 ;
    stove(x) ;   // calls stove ( double &)
    stove(y) ;   // calls stove (const double &)
    stove (x+y); // calls stove (double &&)
    

    没有匹配的原型并不会自动停止使用其中的某个函数,因为C++将尝试使用标准类型转换强制进行匹配。但是有3个将数字作为第一个参数的原型,因此有3种转换year的方式。在这种情况下,C++将拒绝这种函数调用,并将其视为错误。

  3. 一些看起来彼此不同的特征标是不能共存的。例如,请看下面的两个原型

    double cube ( double x);
    double cube ( double & x);
    

    参数x与double x原型和double &x原型都匹配,因此编译器无法确定究竟应使用哪个原型。为避免这种混乱,编译器在检查函数特征标时,将把类型引用和类型本身视为同一个特征标。

  4. 匹配函数时,并不区分const和非const变量。

    例如,如果dribble( )函数有两个原型,一个用于const指针,另一个用于常规指针,编译器将根据实参是否为const来决定使用哪个原型。dribble( )函数只与带非const参数的调用匹配,而drivel( )函数可以与带const或非const参数的调用匹配。drivel( )和dabble( )之所以在行为上有这种差别,主要是由于将非const值赋给const变量是合法的,但反之则是非法的。

    double dribble(char * bits);
    double dribble(const char *cbits);
    

🚀四、异常处理

异常无处不在,程序随时可能误入歧途!C++ 提出了新的异常处理机制!

异常是一种程序控制机制,与函数机制互补,函数是一种以栈结构展开的上下函数衔接的程序控制系统,异常是另一种控制结构,它可以在出现“意外”时中断当前函数,并以某种机制(类型匹配)回馈给隔代的调用者相关的信息

⛳(一)传统错误处理机制

🎈1.通过函数返回值来处理错误

#include <stdio.h>
#include <stdlib.h>

#define BUFSIZE 1024

//实现文件的二进制拷贝
int copyfile(const char *dest,const char *src){
	FILE *fp1 = NULL, *fp2 = NULL;

    //rb 只读方式打开一个二进制文件,只允许读取数据
    fopen_s(&fp1, src, "rb");
    
    if(fp1 == NULL){
    	return -1;
	}
	
    //wb 以只写的方式打开或新建一个二进制文件,只允许写数据。
    fopen_s(&fp2, dest, "wb");
    if(fp2 == NULL){
    	return -2;
    }
    
    char buffer[BUFSIZE];
    int readlen, writelen;
    	
    //如果读到数据,则大于 0
    while( (readlen = fread(buffer, 1, BUFSIZE, fp1)) > 0 ){
    	writelen = fwrite(buffer, 1, readlen, fp2);
    	if(readlen != writelen){
    		return -3 ;
    }
}
    fclose(fp1);
    fclose(fp2);
    return 0;
}

void main(){
    int ret = 0;
    ret = copyfile("c:/test/dest.txt", "c:/test/src.txt");

    //传统错误处理机制:
    if(ret != 0){
        switch(ret){
        case -1:
            printf("打开源文件失败!\n");
            break;
        case -2:
            printf("打开目标文件失败!\n");
            break;
        case -3:
            printf("拷贝文件时失败!\n");
            break;
        default:
            printf("出现未知的情况!\n");
            break;
    	}
    }
    system("pause");
}

🎈2.C语言中统的错误处理机制

  • 使用assert断言直接终止程序,缺陷:用户难以接受,如发生内存错误,除0错误时就会直接终止程序
  • 返回错误码,缺陷:需要程序员自己去查找对应的错误,比如系统中很多库的接口函数都是通过把错误码放在errno中,来表示错误
int main()
{
	int* p = (int*)malloc(sizeof(int));
    
    //使用断言处理错误
	assert(p != NULL);
	
    //使用返回错误码的方式处理错误,单纯的将错误标号转为字符串描述,方便用户查找错误
	printf("%s", strerror(errno));

	return 0;
}

🎈3.调用abort()

abort( )函数的原型位于头文件cstdlib(或stdlib.h)中,其典型实现是向标准错误流(即cerr使用的错误流)发送消息abnormal program termination(程序异常终止),然后终止程序。它还返回一个随实现而异的值,告诉操作系统(如果程序是由另一个程序调用的,则告诉父进程),处理失败。abort( )是否刷新文件缓冲区(用于存储读写到文件中的数据的内存区域)取决于实现。如果愿意,也可以使用exit( ),该函数刷新文件缓冲区,但不显示消息。

#include <iostream>
#include <cstdlib>

double hmean ( double a,double b) ;

int main ()
{
	double x, y,Z;
	std::cout << "Enter two numbers: ";
	while (std::cin >> x >>y)
	{
		z = hmean (x, y);
		std::cout << "Harmonic mean of " << x << " and " << Y << " is " << z << std ::endl;
		std::cout << "Enter next set of numbers <q to quit>: ";
	}
	std::cout << "Bye ! n " ;
	return 0 ;
}

double hmean ( double a, double b)
{
    if (a == -b){
        std::cout << "untenable arguments to hmean ()\n" ;
        std::abort () ;
    }
    return 2.0*a *b / (a + b);
}

⛳(二)C++异常处理机制

#include <stdio.h>
#include <stdlib.h>
#include <string>

using namespace std;

#define BUFSIZE 1024

//实现文件的二进制拷贝
int copyfile2(char *dest, char *src){
	FILE *fp1 = NULL, *fp2 = NULL;
    fopen_s(&fp1, src, "rb");

    if(fp1 == NULL){
    	throw new string("文件不存在");
    }
    
    fopen_s(&fp2, dest, "wb");
    if(fp2 == NULL){
    	throw -2;                //抛出异常,程序到这里就会终止,直接进到对应的main函数的捕捉代码下运行
	}
	char buffer[BUFSIZE];
	int readlen, writelen;

    while( (readlen = fread(buffer, 1, BUFSIZE, fp1)) > 0 ){
    	writelen = fwrite(buffer, 1, readlen, fp2);
    	if(readlen != writelen){
    		throw -3 ;
    	}
	}
	
    fclose(fp1);
    fclose(fp2);
    return 0;
}
    
int copyfile1(char *dest, char *src){
	return copyfile2(dest, src);			//嵌套调用也不会成功返回copyfile2(dest, src)
}

void main(){
    int ret = 0;
    
    try{
   		ret = copyfile1("c:/test/dest.txt", "c:/test/src.txt");
    }catch(int error){                          //捕捉对应异常,可以打印出对应的错误代号,error = -2
    	printf("出现异常啦!%d\n", error);
    }catch(string *error){
    	printf("捕捉到字符串异常:%s\n", error->c_str());
    	delete error;
    }
    system("pause");
}

🎈1.异常处理基本语法

throw语句(实际上是一个跳转语句,即命令程序跳到另一条语句):异常发生第一现场,抛出异常:

void function( ){
    //... ... 
    throw 表达式;
    //... ... 
}

try…catch语句:在需要关注异常的地方,捕捉异常:

try{
	// 可能抛出异常的代码
}catch(异常类型声明){
	//... 异常处理代码 ... 
}catch(异常类型 形参){
	//... 异常处理代码 ... 
}catch(...){ //其它异常类型     
	//
}
  • …表示通配符,可以捕获到所有类型的异常

  • 当执行一个throw时,跟在throw后面的语句将不再被执行。有点类似于return语句:它通常作为条件语句的一部分或者作为某个函数的最后(或者唯一)一条语句。

  • catch语句匹配被抛出的异常对象。如果 catch 语句的参数是引用类型,则该参数可直接作用于异常对象,即参数的改变也会改变异常对象,而且在 catch 中重新抛出异常时会继续传递这种改变。如果 catch 参数是传值的,则复制构函数将依据异常对象来构造catch 参数对象。在该 catch 语句结束的时候,先析构 catch 参数对象,然后再析构异常对象。

  • throw 语句必须包含在 try 块中,也可以是被包含在调用栈的外层函数的 try 块中,如上面的例子

  • 具体过程:

    ①通过 throw 操作创建一个对象并抛掷

    ②由于该对象作用域在函数内部,会创建一个临时匿名对象,称为异常对象继续抛掷,异常对象放在内存的特殊位置,该位置既不是栈也不是堆,在 window 上是放在线程信息块 TIB 中,之后的语句不再执行

    ②在需要捕捉异常的地方,将可能抛出异常的程序段嵌在 try 块之中

    ③按正常的程序顺序执行到达 try 语句,然后执行 try 块{}内的保护段

    ④如果在保护段执行期间没有引起异常,那么跟在 try 块后的 catch 子句就不执行,程序从 try 块后跟随的最后一个 catch 子句后面的语句继续执行下去

    ⑤catch 子句按其在 try 块后出现的顺序被检查,匹配的 catch 子句将捕获并按catch 子句中的代码处理异常(或继续抛掷异常)

    ⑥如果没有找到匹配,则缺省功能是调用 abort 终止程序。

一个简单的例子:

int main()
{
    int score=0;
    while (cin >> score)
    {
        try
        {
            if (score > 100 || score < 0)
            {
                throw score;
            }
            //将分数写入文件或进行其他操作
        }
        catch (int score)
        {
            cerr << "你输入的分数数值有问题,请重新输入!";
            continue;
        }
    }
}

🎈2.异常处理的基本思想

(1)栈展开:

当抛出一个异常后,程序暂停当前函数的执行过程并立即开始寻找与异常匹配的catch子句。函数中出现错误时,可能是直接被调用,也可能是间接被调用。

直接调用:

  • 当throw出现在一个try语句块内时,检查与该try块关联的catch子句。如果找到了匹配的catch,就使用该catch处理异常。
  • 如果这一步没找到匹配的catch且该try语句嵌套在其他try块中,则继续检查与外层try匹配的catch子句。如果还是找不到匹配的catch,则退出当前的函数,在调用当前函数的外层函数中继续寻找。

间接调用:

  • 如果对抛出异常的函数的调用语句位于一个try语句块内,则检查与该try块关联的catch子句。如果找到了匹配的catch,就使用该catch处理异常。
  • 否则,如果该try语句嵌套在其他try块中,则继续检查与外层try匹配的catch子句。如果仍然没有找到匹配的catch,则退出当前这个主调函数,继续在调用了刚刚退出的这个函数的其他函数中寻找,以此类推。
#include <iostream>

using namespace std;

void func1()
{
    double a;
    try{
        throw a;
    }catch(double) {
        cout<<"catch func1()"<<endl; //throw
    }
    cout<<"end func1()"<<endl;
    return ;
}

void func2()
{
    try{
        func1();
    }catch(int){
        cout<<"catch func2()"<<endl;
    }
    cout<<"end func2()"<<endl;
}

void func3()
{
    try{
        func2();
    }catch(char){
        cout<<"catch func3()"<<endl;
    }
    cout<<"end func3()"<<endl;
}

int main()
{
    try{
        func3();
    }catch(double){
        cout<<"catch main"<<endl;
    }
    cout<<"end main"<<endl;
    return 0;
}

结果为:

//异常传递路线为 func1()->func2()->func3(),先在func1()找是否有对应catch块,无就跳转到上一级func2(),在 func1 中找到对应的 catch 块,然后执行对应 catch 块中的语句
catch func1()
end func1()
end func2()
end func3()
end main    

处理不了的异常,我们可以在 catch 的最后一个分支,使用 throw 语法,继续向调用者 throw:抛出的异常类型是一样的

void func1()
{
    double a;
    try{
        throw a;
    }catch(double) {
        throw;
        cout<<"catch func1()"<<endl; //throw
    }
    cout<<"end func1()"<<endl;
    return ;
}
//结果为:
catch main
end main

即沿着嵌套函数的调用链不断后退查找,直到找到匹配的catch 子句为止,或者一直没有找到匹配的catch,则退出主函数终止查找过程(调用标准库函数terminate)。上述过程被称为“栈展开”(stack unwinding)过程。

(2)栈自旋:

就是指如果在栈展开过程中退出了某个块,编译器将负责确保在这个块中创建的对象能被正确地销毁。如果某个局部对象的类型是类类型,则该对象的析构函数将被自动调用。

注意:创建的对象,在拷贝出来的临时对象找到对应的catch块时,创建的对象就会被析构,临时匿名对象会在catch快结束后自动析构

在这里插入图片描述

🎈3.异常类型和生命周期

(1)throw基本类型

//第一种情况,throw 普通类型,和函数返回传值是一样的
int copyfile2(char *dest, char *src){
	...
	if(fp1 == NULL){
        //int ret = -1;
        char ret = 'a';
        throw ret;
	}	
	
	if(fp2 == NULL){
	throw -2;
	}
	...
}

void main(){
	int ret = 0;

	try{//保护段
    //printf("开始执行 copyfile1...\n");
    ret = copyfile1("c:/test/dest.txt", "c:/test/src.txt");
    //printf("执行 copyfile1 完毕\n");
    
    }catch(int error){
    	printf("出现异常啦!%d\n", error);
    }catch(char error){
    	printf("出现异常啦!%c\n", error);
    }
    system("pause");
}

(2)throw字符串类型

//第二种情况,throw 字符串类型,实际抛出的指针,而且,修饰指针的 const也要严格进行类型匹配,即常量指针,指针常量不需要

//第一种情况,throw 普通类型,和函数返回传值是一样的
int copyfile3(char *dest, char *src){
	...
    if(fp1 == NULL){
    	const char * error = "大佬,你的源文件打开有问题";
    	printf("throw 前,error 的地址:%p\n", error);
    	throw error;
	}
	...
}

void main(){
	int ret = 0;

	try{//保护段
    //printf("开始执行 copyfile1...\n");
    ret = copyfile1("c:/test/dest.txt", "c:/test/src.txt");
    //printf("执行 copyfile1 完毕\n");
    
    }catch(string error){
    	printf("出现异常啦!%s\n", error.c_str());
    }catch(const char *error){
   		printf("出现异常啦(char *)!%s(地址:%p)\n", error, error);
    }catch(...){
    	printf("没捉到具体的异常类型\n");
    }

    system("pause");
}

(3)throw类类型

class ErrorException{
public:
    ErrorException(){
    	id = 0;
    	printf("ErrorException 构造!\n");
    }

    ~ErrorException(){
    	printf("ErrorException ~析构!(id: %d)\n", id);
    }

	ErrorException(const ErrorException &e){
    	id = 1;
    	printf("ErrorException 拷贝构造函数!\n");
    }
	int id;
};

//第三种情况,throw 类类型,最佳的方式是使用引用类型捕捉,抛出匿名对象
//当然,如果是动态分配的对象,直接抛出其指针
//注意:引用和普通的形参传值不能共存

int copyfile4(char *dest, char *src){
    ...
    if(fp1 == NULL){
    	//ErrorException error1;
        //throw error1
    	throw ErrorException(); //throw ErrorException();直接抛出匿名对象
    ...
}

void main(){
	int ret = 0;

	try{//保护段
    //printf("开始执行 copyfile1...\n");
    ret = copyfile1("c:/test/dest.txt", "c:/test/src.txt");
    //printf("执行 copyfile1 完毕\n");
    
    }catch(ErrorException error){
        printf("出现异常啦!捕捉到 ErrorException 类型 id: %d\n",error.id);
    }catch(ErrorException &error){
    	//error.id = 2;
        printf("出现异常啦!捕捉到 ErrorException &类型 id: %d\n",error.id);
    }catch(ErrorException *error){
		printf("出现异常啦!捕捉到 ErrorException *类型 id: %d\n",error->id);
		delete error;
	}catch(...){
    	printf("没捉到具体的异常类型\n");
    }

    system("pause");
}
    

  • 分析:

    ①如果 error 没有引用,运行结果:

    ErrorException 构造
    ErrorException 拷贝构造函数
    捕捉到异常!ErrorException 类型
    ErrorException ~析构 (ID:1)
    ErrorException ~析构 (ID:0)
    

    说明:调用了两次析构函数,一次是在创建了匿名对象抛出,一次是因为使用了形参值传递,会进行对象的拷贝。从ID 的变化可以看出,先析构拷贝构造函数,后析构构造函数。

    ②如果 error 使用了引用,运行结果

    ErrorException 构造
    捕捉到异常!ErrorException 类型
    ErrorException ~析构 (ID:0)
    

    说明:使用引用的话,将会少创建一个对象,是“临时匿名对象”的地址进行抛出,捕捉时直接捕捉引用,不用再次创建,这种效率最高。

    ③如果先创建对象,然后 throw 对象,并且用形参接受,则会变得更加低效:

    int copyfile4(char *dest, char *src){
        ...
        if(fp1 == NULL){
        	ErrorException error1;
            throw error1
        ...
    }
        
    	try
         {
             ret = test(1);
         }
         catch (ErrorException error)        //使用形参接收
         {
             printf("捕捉到异常!ErrorException 类型\n");
         }    
    

    运行结果:

    ErrorException 构造         ------------> 创建对象
    ErrorException 拷贝构造函数      ------------> 由于创建的对象作用域在函数内部,但异常要继续往外抛,编译器会创建一个“临时的匿名对象”。
    ErrorException 拷贝构造函数     ------------> error,用上边生成的“临时的匿名对象”进行拷贝。
    ErrorException ~析构 (ID:0)    ------------> 由于 test 函数结束,创建的对象被析构。
    捕捉到异常!ErrorException 类型   ------------> 进入catch块打印。
    ErrorException ~析构 (ID:1)    ------------> catch块结束,创建的error对象,走出作用域销毁。
    ErrorException ~析构 (ID:1)    ------------> 离开catch的作用域时 “临时的匿名对象”会被销毁。
    
  • 通常,引发异常的函数将传递一个对象。这样做的重要优点之一是,可以使用不同的异常类型来区分不同的函数在不同情况下引发的异常。另外,对象可以携带信息,程序员可以根据这些信息来确定引发异常的原因。

  • 异常对象(指编译器依据异常抛出表达式拷贝的临时匿名对象)的生存周期:局部对象在函数调用结束后就被自动销毁,而异常对象将驻留在所有可能被激活的 catch 语句都能访问到的内存空间中,当异常对象与 catch 语句成功匹配上后,在该 catch 语句的结束处被自动析构。

  • catch 的参数相当于函数的形参,而被抛出的异常对象相当于函数调用时的实参。catch 后面的参数只能采用传值、传引用和传指针三种方式,如果采用传值方式,则会生成实参的一个副本,如果实参是一个对象,就会导致拷贝构造函数被调用。

  • 注意:

    ①函数到底什么时候结束:当找到对应的catch,函数就会结束,因此,在 throw 语句中抛出局部变量的指针或引用也几乎是错误的行为。该指针所指向的变量在执行 catch 语句时已经被销毁(函数已经结束)对指针进行解引用将发生意想不到的后果。

    ②在throw语句中使用动态分配堆空间需要特别注意,在throw抛出之前必须delet掉内存,否则,等到找到对应catch语句块,该指针变量会被销毁,无法再回收堆内存

    ②同一种数据类型的传值 catch 分支和传引用 catch 分支不能同时出现。

    ③实际异常的抛出和捕获的匹配原则有个例外,捕获和抛出的异常类型并不一定要完全匹配,可以抛出派生类对象,使用基类进行捕获,这个在实际中非常有用

🎈4.继承和异常

上面讲过,类也可以作为异常,并且一般来说都是这样做,我们可以创建自己的异常类,在异常中可以使用(虚函数,派生,引用传递和数据成员等)

案例:设计一个数组类容器 Vector,重载[]操作,数组初始化时,对数组的个数进行有效检查

index < 0 抛出异常 errNegativeException

index = 0 抛出异常 errZeroException

index > 1000 抛出异常 errTooBigException

index < 10 抛出异常 errTooSmallException

errSizeException 类是以上类的父类,实现有参数构造、并定义 virtual void printError()输出错误。

#include <iostream>

using namespace std;

class errSizeException{
public:
    errSizeException(int size){
    	m_size = size;
    }
    
    virtual void printError(){
    	cout<<"size: "<< m_size <<endl;
    }
protected:
	int m_size;
};

class errNegativeException : public errSizeException{
public:
	errNegativeException(int size):errSizeException(size){
    }
    
    virtual void printError(){
    	cout<<"errNegativeException size: "<<m_size<<endl;
    }
};

class errZeroException : public errSizeException{
public:
    errZeroException(int size):errSizeException(size){
    }

    virtual void printError(){
		cout<<"errZeroException size: "<<m_size<<endl;
	}
};

class errTooBigException : public errSizeException{
public:
	errTooBigException(int size):errSizeException(size){
	}

    virtual void printError(){
		cout<<"errTooBigException size: "<<m_size<<endl;
	}
};

class errTooSmallException : public errSizeException{
public:
	errTooSmallException(int size):errSizeException(size){
	}

    virtual void printError(){
		cout<<"errTooSmallException size: "<<m_size<<endl;
	}
};



class Vector{
public:
	Vector(int size = 128); //构造函数
    int& operator[](int index);
    ~Vector();
    
    int getLength();//获取内部储存的元素个数
private:
    int *m_base;
    int m_len;
};

//在构造函数中抛出异常,可能导致对象不完整或没有完全初始化。但是这里并不影响,因为只有一个len
Vector::Vector(int len){
    if(len < 0){
    	throw errNegativeException(len);
    }else if(len == 0){
    	throw errZeroException(len);
    }else if(len > 1000){
    	throw errTooBigException(len);
    }else if(len < 10){
    	throw errTooSmallException(len);
    }
    
    m_len = len;
    m_base = new int[len];
}

Vector::~Vector(){
    if(m_base) delete[] m_base;
    m_len = 0;
}

int Vector::getLength(){
	return m_len;
}

int &Vector::operator[](int index){
	return m_base[index];
}

void main(){
	try{
        Vector v(10000);
        for(int i=0; i<v.getLength(); i++){
            v[i] = i+10;
            printf("v[i]: %d\n", v[i]);
        }
	}catch(errSizeException &err){   
		err.printError();
	}
    //上述描述为,抛出派生类对象,使用基类进行捕获,这个在实际中非常有用,也可以替换为如下使用
    
    /*catch(errNegativeException &err){
    	cout<<"errNegativeException..."<<endl;
    }catch(errZeroException &err){
    	cout<<"errZeroException..."<<endl;
    }catch(errTooBigException &err){
    	cout<<"errTooBigException..."<<endl;
    }catch(errTooSmallException &err){
    	cout<<"errTooSmallException..."<<endl;
    }*/
    
	system("pause");
	return ;
}

🎈5.异常安全问题与异常规范

(1)异常安全问题

将抛异常导致的安全问题叫做异常安全问题,对于异常安全问题下面给出几点建议:

  • 构造函数完成对象的构造和初始化,构造函数中抛出异常,可能导致对象不完整或没有完全初始化。
  • 析构函数主要完成对象资源的清理,最好不要在析构函数中抛出异常,否则可能导致资源泄露(内存泄露、句柄未关闭等)。
  • C++中异常经常会导致资源泄露的问题,比如在new和delete中抛出异常,导致内存泄露,在lock和unlock之间抛出异常导致死锁,

有两种解决办法:

  • 将异常捕获,释放资源后,将锁重新抛出。
  • 使用RAII的思想解决。定义一个类封装,管理资源。当要使用时实例化一个类对象,将资源传入,当退出函数,调用对象析构函数,释放资源。

(2)异常接口声明(异常规范)

可以在函数声明中列出可能抛出的所有异常类型,加强程序的可读性。如:

int copyfile2(char *dest, char *src) throw (float, string *, int)
    
void show();                                //该函数可能抛出任何异常
void show()throw();                         //该函数不抛出任何异常
void show()throw(char,int);                 //该函数可能抛出char和int型异常  
  • 对于异常接口的声明,在函数声明中列出可能抛出的所有异常类型
  • 如果没有包含异常接口声明,此函数可以抛出任何类型的异常
  • 如果函数声明中有列出可能抛出的所有异常类型,那么抛出其它类型的异常将可能导致程序终止
  • 如果一个函数不想抛出任何异常,可以使用 throw () 声明

异常规范是C++98新增的一项功能,但C++11却将其摒弃了,由于常规范的作用之一是,告诉用户可能需要使用try块。然而,这项工作也可使用注释轻松地完成。异常规范的另一个作用是,让编译器添加执行运行阶段检查的代码,检查是否违反了异常规范。这很难检查。例如,marm( )可能不会引发异常,但它可能调用一个函数,而这个函数调用的另一个函数引发了异常。另外,您给函数编写代码时它不会引发异常,但库更新后它却会引发异常。

C++11支持一种特殊的异常规范:可使用新增的关键字noexcept指出函数不会引发异常:

void func() noexcept { ... } // noexcept声明该函数不会产生异常(C++11)
void func() noexcept(表达式) //接受一个可选的实参,该实参必须能转换为bool类型:如果实参是true,则函数不会抛出异常;如果实参是false,则函数可能抛出异常:

🎈6.标准程序库异常

C++标准提供了一组标准异常类,这些类以基类Exception开始,它定义在<exception>头文件中,标准程序库抛出的所有异常,都派生于该基类,这些类构成如图12-2所示的异常类的派生继承关系。该基类提供一个成员函数 what (),用于返回错误信息(返回类型为const char * )。在Exception类中,what()函数的声明如下:

virtual const char * what() const throw(); //该函数可以在派生类中重定义。

在这里插入图片描述

下表列出了各个具体异常类的含义及定义它们的头文件。runtime_error 和 logic_error是一些具体的异常类的基类,它们分别表示两大类异常。logic_error 表示那些可以在程序中被预先检测到的异常,也就是说如果小心地编写程序,这类异常能够避免而runtime_error则表示那些难以被预先检测的异常。

在这里插入图片描述

在这里插入图片描述

一些编程语言规定只能抛掷某个类的派生类(例如Java中允许抛掷的类必须派生自Exception类),C++虽然没有这项强制的要求,但仍然可以这样实践。例如,在程序中可以使得所有抛出的异常皆派生自Exception(或者直接抛出标准程序库提供的异常类型,或者从标准程序库提供的异常类派生出新的类),这样会带来很多方便。

logic_error 和runtime_error两个类及其派生类,都有一个接收const string&.型参数的构造函数。在构造异常对象时需要将具体的错误信息传递给该函数,如果调用该对象的 what函数,就可以得到构造时提供的错误信息。

#include <iostream>
#include <exception>
#include <stdexcept>

using namespace std;

class Student{
public:
	Student(int age){
        if(age > 249){
        	throw out_of_range("年龄太大,你是外星人嘛?");
    }
    m_age = age;	
    m_space = new int[1024*1024*100];
}
private :
    int m_age;
    int *m_space;
};

void main(){

    try{
        for(int i=1; i<1024; i++){
        	Student * xiao6lang = new Student(18);
    	}
    }catch(out_of_range &e){
    	cout<<"捕捉到一只异常:"<<e.what()<<endl;
    }catch(bad_alloc &e){
    	cout<<"捕捉到动态内存分配的异常:"<<e.what()<<endl;
    }
    
    system("pause");
}

在实际中,我们可以去继承exception类实现自己的异常类


行文至此,落笔为终。文末搁笔,思绪驳杂。只道谢不道别。早晚复相逢,且祝诸君平安喜乐,万事顺意。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陈七.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值