①友元

1.介绍

前边的章节中,我们将友元函数用作类的扩展接口中,我们也可以将类作为友元。在这种情况下,友元类的所有方法都可以访问原始类的私有成员和保护成员。也可以做更严格的定义,只让特定的成员函数指定为另一个类的友元。哪些函数、成员函数或类为友元是由类定义的。

2.友元类

什么时候希望一个类成为另一个类的友元呢?当二者可以相互影响或单方面影响的时候。例如,电视机和遥控(既不是Is-a,也不是has-a),而是遥控器可以影响电视(换台,调音等)。我们来实现这个例子。

#ifndef TV_H_
#define TV_H_class Tv
{
private:int state;     //on or offint volume;int maxchannel;int channel;    //current channel seetingint mode;      //broadcast or cableint input;   //TV or DVD
public:enum { Off, On };enum { Minval, Maxval = 20 };enum { Antenna, Cable };enum { TV, DvD };Tv(int s = Off, int mc = 125) :state(s), maxchannel(mc), volume(5), channel(2), mode(Cable), input(TV) {}void onoff() { state = (state == Off) ? On : Off; }  //切换状态bool ison() { return state == On; }bool volup();bool voldown();void chanup();void chandown();void set_mode() { mode = (mode == Antenna) ? Cable : Antenna; }void set_input() { input = (input == TV) ? DvD : TV; }void settings() const;friend class Remote;};
class Remote
{
private:int mode;
public:Remote(int m = Tv::TV):mode(m){}bool volup(Tv& t) { return t.volup(); }bool voldown(Tv& t) { return t.voldown(); }void onoff(Tv& t) { t.onoff(); }void channup(Tv& t) { t.chanup(); }void chandown(Tv& t) { t.chandown(); }void set_chan(Tv& t, int c) { t.channel = c; }void set_mode(Tv& t, int c) { t.set_mode(); }void set_input(Tv& t, int c) { t.set_input(); }};#endif 

友元类声明:

friend class Remote;
#include<iostream>
#include"TV.h"bool Tv::volup()
{if (volume < Maxval){volume++;return true;}elsereturn false;
}
bool Tv::voldown()
{if (volume < Minval)return false;volume--;return true;
}void Tv::chanup()
{if (channel < maxchannel){channel++;}elsechannel = 1;
}
void Tv::chandown()
{if (channel > 1){channel--;}elsechannel = maxchannel;
}void Tv::settings() const
{std::cout << "TV is " << (state == Off ? "Off" : "On") << std::endl;if (state == On){std::cout << "Volume setting = " << volume << std::endl;std::cout << "Channel setting = " << channel << std::endl;std::cout << "Mode = " << (mode == Antenna ? "Antenna" : "cabel") << std::endl;std::cout << "input = " << (input == TV ? "TV" : "DvD") << std::endl;}
}
#include<iostream>
#include"TV.h"int main()
{using std::cout; Tv s42;cout << "Initial settings for 42\"TV\"\n";s42.settings();s42.onoff();s42.chanup();cout<<"\nAdjusted settings for 42\"TV\"\n";s42.settings();Remote grey;grey.set_chan(s42, 10);grey.volup(s42);grey.volup(s42);cout << "\n Settings after using remote:\n";s42.settings();Tv s58(Tv::On);s58.set_mode();grey.set_chan(s58, 28);cout << "s58 setting:\n";s58.settings();return 0;}

说明:练习的主要目的表明,友元类是一种自然语言,用于表示一些关系。如果不这样,则必须将Tv类私有部分设置为公有,或建立包含两个声明的大型类,着都无法反应这样一个事实:同一个遥控器可用于多台电视机。

3.友元成员函数

上一个例子中,大多数Remote方法都是通过Tv类的公有接口实现的。唯一直接访问Tv成员的Remote方法是set::chan();因此我们可以将这个类的成员函数作为Tv类的友元函数。这样做的麻烦在于声明和定义的顺序。

1.在Tv类中将该成员函数声明为友元。编译器要处理,必须知道Remote定义,Remote在Tv前,但是Remote方法的参数又有Tv类,这意味着Tv类在前。解决该问题的方法是:使用前向说明,并且先声明,最后定义。

class Tv;

class Remote{...}

class Tv{...}

我们看到 remote()函数定义出现了调用Tv方法,Tv还没定义 ,所以解决办法就是将remote定义放在Tv后。(友元类不需要前向声明,因为 友元语句本身指出它就是个类)

#ifndef TV_H_
#define TV_H_class Tv;
class Remote
{
private:int mode;
public:Remote(int m = Tv::TV) :mode(m) {}bool volup(Tv& t);bool voldown(Tv& t);void onoff(Tv& t);void channup(Tv& t);void chandown(Tv& t);void set_chan(Tv& t, int c); void set_mode(Tv& t, int c); void set_input(Tv& t, int c); };
class Tv
{
private:int state;     //on or offint volume;int maxchannel;int channel;    //current channel seetingint mode;      //broadcast or cableint input;   //TV or DVD
public:enum { Off, On };enum { Minval, Maxval = 20 };enum { Antenna, Cable };enum { TV, DvD };Tv(int s = Off, int mc = 125) :state(s), maxchannel(mc), volume(5), channel(2), mode(Cable), input(TV) {}void onoff() { state = (state == Off) ? On : Off; }  //切换状态bool ison() { return state == On; }bool volup();bool voldown();void chanup();void chandown();void set_mode() { mode = (mode == Antenna) ? Cable : Antenna; }void set_input() { input = (input == TV) ? DvD : TV; }void settings() const;friend void Remote::set_chan(Tv& t, int c); ;};bool Remote::volup(Tv& t) { return t.volup(); }
bool Remote::voldown(Tv& t) { return t.voldown(); }
void Remote::onoff(Tv& t) { t.onoff(); }
void Remote::channup(Tv& t) { t.chanup(); }
void Remote::chandown(Tv& t) { t.chandown(); }
void Remote::set_chan(Tv& t, int c) { t.channel = c; }
void Remote::set_mode(Tv& t, int c) { t.set_mode(); }
void Remote::set_input(Tv& t, int c) { t.set_input(); }
#endif

4.其他友元关系

上边只介绍了遥控器影响电视状态,单方面影响,如果两个类互相影响,则互为对方的友元函数。在具体调用方法和顺序前,一定要记得先声明,后定义。

5.共同的友元

函数需要访问两个类的私有数据。从逻辑上看,这样的函数应该是每个类的成员函数,但这是不可能的。它可以是一个类的成员,同时是另一个类的友元,但有时将函数作为两个类的友元更合理。一个函数分别在一个类中声明两次。

class Analyzer;
class Probe
{friend void sync(Analyzer &a, const Probe &p);   // sync a to pfriend void sync(Probe &p, cosnt Analyzer &a);   // sync p to a
}class Analyzer
{friend void sync(Analyzer &a, const Probe &p);  // sync a to pfriend void sync(Probe &p, cosnt Analyzer &a);  // sync p to a
}inline void sync(Analyzer &a, const Probe &p)
{...
}
inline void sync(Probe &p, cosnt Analyzer &a)
{}

②嵌套类

1.嵌套类

在C++中,可以将类声明放在另一个类中,在另一个类中被声明的类称为嵌套类。包含类的成员函数可以创建和使用被嵌套类的对象,仅当声明位于公有部分,才能在包含类的外边使用嵌套类,而且必须使用作用域解析运算符。

对类进行嵌套与包含不同,包含是将类对象作为另一个类的成员,而对类进行嵌套不创建成员,而是定义了一种类型,为了更好的实现包含类。

对于队列的实现来说,我们包含了结构体Node。在队列中,只有enqueue()方法创建了Node对象,我们可以将创建新对象方法交给类的构造函数来完成。

class Queue
{
private:// 这里Node是一个嵌套的结构体定义// Item是一个别名, 详见之前的笔记struct Node {Item item; struct Node * next;}...
};
bool Queue::enqueue(const Item & item)
{if(isfull())return false;Node * add = new Node;add->item = item;add->next = NULL:...
}

采用嵌套类的构造函数来完成:

class Queue
{// 嵌套类class Node{public:            //全是公有Item item;Node * next;// 构造函数, 将item赋值为i, next指针设置为0, 也就是空值指针Node(const Item & i): item(i), next(0) {}};...
};bool Queue::enqueue(const Item & item)
{if(isfull())return false;Node * add = new Node(item);...
}

这个例子中是在类声明中定义了构造函数, 假设想在方法文件中定义构造函数, 则定义必须指出Node类是在Queue类中定义的, 我们可以通过两次作用域解析运算符来完成:

Queue::Node::Node(const Item & i): item(i), next(0) {}

2.嵌套类和访问权限

嵌套类的声明位置决定了嵌套类的作用域, 即它决定了程序的那些部分可以创建这种类的对象.

其次.嵌套类的公部分, 保护部分和私有部分控制了对类成员的访问。(哪些部分可以访问这些数据)

1.作用域

如果嵌套类是在另一个类的私有部分声明的, 则只有包含类知道它,派生类和外部均不知道嵌套类的存在,更别说使用。( 类的默认访问权限是private)

如果嵌套类是在另一个类的保护部分声明的, 则它对于后者和基于后者的派生类可见(可直接创建对象), 但是对于外部世界是不可见的。

如果嵌套类是在另一个类的共有部分声明的, 则允许后者, 后者的派生类以及外部世界使用它,然而, 由于嵌套类的作用域为包含它的类, 因此在外部世界使用它时, 必须使用类限定符.
2.访问控制

对嵌套类的访问控制规则与常规类相同. (私有不可见,只有公有部分可见,保护部分派生可见,外部不可见。这也是为什么Node均声明为公有的)

类声明的位置决定了类的作用域或可见性, 类可见后, 访问控制规则(共有, 保护, 私有, 友元)将决定程序对嵌套类成员的访问权限.

3.模板中嵌套类

// queuetp.h
#ifndef QUEUETP_H_
#define QUEUETP_H_// 模板类
template <class T>
class QueueTP
{
private:enum { Q_SIZE = 10 };// 定义一个嵌套类class Node{public:T item;    //用到了模板Node* next;Node(const T& i) :item(i), next(0) {}};Node* front;Node* rear;int items;const int qsize;QueueTP(const QueueTP& q) : qsize(0) {}QueueTP& operator=(const QueueTP& q) { return *this; }public:QueueTP(int qs = Q_SIZE);~QueueTP();bool isempty() const{return items == 0;}bool isfull() const{return items == qsize;}bool queuecount() const{return items;}bool enqueue(const T& item);bool dequeue(T& item);
};template<class T>
QueueTP<T>::QueueTP(int qs) :qsize(qs)
{// 置为空值指针front = rear = 0;items = 0;
}template<class T>
QueueTP<T>::~QueueTP()
{Node* temp;while (front != 0){temp = front;front = front->next;delete temp;}
}template <class T>
bool QueueTP<T>::enqueue(const T& item)
{if (isfull())return false;Node* add = new Node(item);items++;if (front == 0){front = add;}else {rear->next = add;}rear = add;return true;
}template<class T>
bool QueueTP<T>::dequeue(T& item)
{if (front == 0)return false;item = front->item;items--;Node* temp = front;front = front->next;delete temp;if (items == 0)rear = 0;return true;
}#endif

注意:

template <class T>
class QueueTP
{
private:enum { Q_SIZE = 10 };// 定义一个嵌套类class Node{public:T item;    //用到了模板Node* next;Node(const T& i) :item(i), next(0) {}};

嵌套类也使用了模板。

模板正常函数定义和之前没有区别:

template<class T>
QueueTP<T>::~QueueTP()
{Node* temp;while (front != 0){temp = front;front = front->next;delete temp;}
}

类型不一样,每个类型有一个定义,所以多个Node类不会发生名称冲突。

// 测试类
// nested.cpp
#include <iostream>
#include <string>
#include "queuetp.h"int main()
{using std::string;using std::cin;using std::cout;using std::endl;QueueTP<string> cs(5);string temp;while(!cs.isfull()){cout << "Please enter your name. You will be served in the order of arrival. name:" << endl;getline(cin, temp); cs.enqueue(temp);}cout << "The queue is full. Processing begins!" << endl;while(!cs.isempty()){cs.dequeue(temp);cout << "Now Processing " << temp << "..." << endl;}return 0;
}

③异常

1.介绍

程序有时会遇到运行阶段错误,导致程序无法正常的运行下去。例如我们要计算两个数的调和平均数,返回值分母是两个数之和,如果两个数互为相反数,很多新式编译器会生成Inf来表示无穷大,其他编译器在遇到这种情况可能会崩溃,所以最高编写在所有系统上都可以以相同的受控方式运行的代码。

2.调用abort()

对于这种问题,处理方式之一就是调用abort()函数,函数位于头文件cstdlib中,其典型实现就是向标准错误流发送程序异常终止消息,然后终止程序。他还返回一个随实现而异的值,告诉操作系统或父程序,处理失败。abort()函数是否刷新文件缓冲区取决于实现。也可以使用exit(),该函数刷新文件缓冲区,但不显式消息。

// error1.cpp
#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 << "Byte!" << std::endl;return 0;
}double hmean(double a, double b)
{if (a == -b){std::cout << "untenable arguments to hmean() " << std::endl;std::abort();}return 2.0 * a * b / (a + b);
}

注意:在hmean()中调用abort()函数将直接终止程序,而不是先返回到main()。为了避免这种异常终止,程序应该在调用hmean()函数之前检查x和y的值。

3.返回错误码

一种比异常终止更灵活的方法是:使用函数的返回值来指出问题。正常返回有效值,错误返回特殊错误的值。对于调和平均数来说,所有的数字都是正常值,我们可使用指针参数或引用参数来将值返回给调用程序,并使用函数的返回值来指出成功还是失败。

// error2.cpp
#include <iostream>
#include <cfloat>// 返回bool类型, 第三个参数是结果
bool hmean(double a, double b, double* ans);int main()
{double x, y, z;std::cout << "Enter two numbers: ";while (std::cin >> x >> y){if (hmean(x, y, &z))std::cout << "Harmonic mean of " << x << " and " << y << " is " << z << std::endl;elsestd::cout << "One value should not be the negative of the other - try again ." << std::endl;}std::cout << "Bye!" << std::endl;return 0;
}bool hmean(double a, double b, double* ans)
{if (a == -b){*ans = DBL_MAX;   //浮点数最大值return false;}else {*ans = 2.0 * a * b / (a + b);return true;}
}

除了用最大值或指针来表明验证错误,还可以返回条件方法一个全局变量。可能出现问题的函数可以在出现问题时将该全局变量设置为特定的值,而调用程序可以检查该变量。必须确保其他函数没有将该全局变量用于其他目的。

4.异常机制

C++异常是对程序运行过程中发生的异常情况的一种响应。异常提供了将控制权从程序的一个部分传递到另一个部分的途径。对异常的处理有三个组成部分:

1.引发异常——throw关键字表示引发异常,紧随其后的值(例如字符串或对象)指出了异常的特征。

2.使用处理程序捕获异常——处理程序以关键字catch开头,catch和异常类型用作标签,指出一场被引发时,程序跳到这个位置执行。

3.使用try块——try块表示块中的语句可能引发异常,他后边跟一个或多个catch块。

// error3.cpp
#include <iostream>
double hmean(double a, double b);int main()
{double x, y, z;std::cout << "Enter two numbers: ";while (std::cin >> x >> y){// 使用try来括起来可能出现异常的代码try {z = hmean(x, y);}catch (const char* s) {// 如果程序没有出现异常, 则不会进入catch的代码块std::cout << s << std::endl;std::cout << "Enter a new pair of numbers: ";continue;}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!" << std::endl;return 0;
}double hmean(double a, double b)
{if (a == -b){// 抛出异常throw "bad hmean() arguments: a = -b not allowed";}return 2.0 * a * b / (a + b);
}

说明:

1.try块中的语句发生异常,后面的catch块将对异常进行处理,如果程序在try块外边调用发生异常的函数,将无法处理异常。

2.被引发异常时字符串,也可以是其他类型,通常为类 类型。

3.执行throw语句类似于执行返回语句,因为它终止函数的执行,并将控制权交给包含try块的函数。

流程:

1.执行try块语句,没有发生异常,跳过catch语句,一直运行下去;

2. 执行try块语句,发生异常,throw执行,终于发生异常的函数,将控制权跳转到包含try块函数中与之匹配的catch函数,执行catch函数。

注:如果函数发生了异常,没有try块或匹配的处理程序,程序最后默认调用abort()函数。

5.将对象作为异常

通常,引发异常的函数传递一个对象,好处是对象可以携带信息,catch块可以根据这些信息来决定采取什么样的措施。

#ifndef ERROR_H_
#define ERROR_H_
// exc_mean.h
#include <iostream>// 第一个异常类
class Bad_hmean
{
private:double v1;double v2;public:Bad_hmean(double a = 0, double b = 0) : v1(a), v2(b) {}void mesg();
};inline void Bad_hmean::mesg()
{std::cout << "hmean (" << v1 << ", " << v2 << "): invalid arguments : a = -b " << std::endl;
}// 第二个异常类
class Bad_gmean
{
public:double v1;double v2;Bad_gmean(double a = 0, double b = 0) : v1(a), v2(b) {}const char* mesg();
};inline const char* Bad_gmean::mesg()
{return "gmean() arguments should be >= 0 \n";
}#endif
// 对应的测试程序
// error4.cpp
#include <iostream>
#include <cmath>
#include "error.h"// 函数原型
double hmean(double a, double b);
double gmean(double a, double b);int main()
{using std::cout;using std::cin;using std::endl;double x, y, z;cout << "Enter two numbers: ";while (cin >> x >> y){try {z = hmean(x, y);cout << "Harmonic mean of " << x << " and " << y << " is " << z << endl;cout << "Geometric mean of " << x << " and " << y << " is " << gmean(x, y) << endl;cout << "Enter next set of numbers <q to quit>: ";}catch (Bad_hmean& bg) {        //对象传递用引用传递// 匹配抓取的Bad_hmean异常bg.mesg();cout << "Try again." << endl;continue;}catch (Bad_gmean& hg) {cout << hg.mesg();cout << "Value used: " << hg.v1 << ", " << hg.v2 << endl;cout << "Sorry, you don't get to play any more." << endl;break;}}cout << "Bye !" << endl;return 0;
}
double hmean(double a, double b)
{if (a == -b)throw Bad_hmean(a, b);return 2.0 * a * b / (a + b);
}double gmean(double a, double b)
{if (a < 0 || b < 0)throw Bad_gmean(a, b);return std::sqrt(a * b);
}

注:对象传递都是用引用传递

6.异常规范和C++11

不建议使用异常规范,异常规范作用:1.告诉用户需要try块;2。让编译器添加执行运行阶段检查代码。

C++11支持一种特殊的异常规范:使用关键字noexcept指出函数不会引发异常(大多数情况不要用)

7.栈解退

假设try块没有直接调用引发异常的函数, 而是调用了对引发异常的函数进行调用的函数,则程序流程将从引发异常的函数跳到包含try块和处理程序的函数。在这个过程中,函数不是逐一返回到上一级调用函数处,而是一直释放栈, 直到找到一个位于try块中的返回地址。随后, 控制权将转到块尾的异常处理程序,而不是函数调用后面的第一条语句,这个过程被称为栈解退。

为什么要有栈解退这个该特性?

引发机制的一个非常重要的特性是,对于栈中的自动类对象, 类的析构函数将被调用,用于处理try块和throw之间函数调用被放在栈中的自动类对象。

函数返回也有这样的特性,然而函数返回仅仅处理该函数放在栈中的对象, 而throw语句则处理try块和throw之间整个函数调用序列放在栈中的对象。如果没有栈解退的特性,则引发异常后,处理try块和throw之间函数调用被放在栈中的自动类对象,其析构函数不会被调用。

#include <iostream>
#include <cmath>
#include <string>
#include "error.h"class demo
{
private:std::string word;
public:demo(const std::string& str){word = str;std::cout << "demo " << word << " created" << std::endl;}~demo(){std::cout << "demo " << word << " destroyed " << std::endl;}void show() const{std::cout << "demo " << word << " lives!" << std::endl;}
};double hmean(double a, double b);
double gmean(double a, double b);
double means(double a, double b);int main()
{using std::cout;using std::cin;using std::endl;double x, y, z;{demo d1("found in block in main()");cout << "Enter two numbers: ";while (cin >> x >> y){try {z = means(x, y);cout << "The mean mean of " << x << " and " << y << " is " << z << endl;cout << "Enter next pair: ";}catch (Bad_hmean& bh) {bh.mesg();cout << "Try again" << endl;continue;}catch (Bad_gmean& bg) {cout << bg.mesg();cout << "Values used: " << bg.v1 << ", " << bg.v2 << endl;cout << "Sorry you don't get to play any more. " << endl;break;}}d1.show();}cout << "Bye!" << endl;cin.get();cin.get();return 0;
}double hmean(double a, double b)
{if (a == -b)throw Bad_hmean(a, b);return 2.0 * a * b / (a + b);
}double gmean(double a, double b)
{if (a < 0 || b < 0)throw Bad_gmean(a, b);return std::sqrt(a * b);
}double means(double a, double b)
{double am, hm, gm;demo d2("found in means()");am = (a + b) / 2.0;try {hm = hmean(a, b);gm = gmean(a, b);}catch (Bad_hmean& bg) {bg.mesg();std::cout << "Caught in means()" << std::endl;throw;}d2.show();return (am + hm + gm) / 3.0;
}

说明:

1. 对于had_hmeans,有两个catch语句,程序发生异常,一步步线上寻找包含try的语句,首先找到位于means()中的try和catch语句,catch语句执行完,语句块又有throw语句,继续向上找,找到main函数中try语句后边的catch语句,异常处理结束。

2.对于had_gmeans,只有一个语句,程序发生异常,一直往上找,找到main函数中的try语句后边的catch语句,异常处理结束。

 try { z = means(x, y); ... }double means(double a, double b)
{double am, hm, gm;demo d2("found in means()");am = (a + b) / 2.0;try {hm = hmean(a, b);gm = gmean(a, b);}catch (Bad_hmean& bg) {bg.mesg();std::cout << "Caught in means()" << std::endl;throw;}d2.show();return (am + hm + gm) / 3.0;
}double hmean(double a, double b)
{if (a == -b)throw Bad_hmean(a, b);return 2.0 * a * b / (a + b);
}double gmean(double a, double b)
{if (a < 0 || b < 0)throw Bad_gmean(a, b);return std::sqrt(a * b);
}

3.思考,如果把means函数中catch语句后边的thorw删掉,会发生什么结果?

程序在hm那里直接结束,导致hm gm均没有被初始化,并不会去main函数中的catch语句。

4.思考,如果给初始化值呢?

程序运行正常,这意味着不会跳到main中catch函数块。

5.给z初始化,不给means函数初始化:

函数发生了异常:原因应该是catch语句执行完,会继续执行catch后边的语句,所以这里会报错。

6.means函数thorw后边什么都没有,前往main()函数中,catch还是捕获到了,异常应该是可以传递的。

8.其他异常特性

throw-catch机制类与函数参数和函数返回机制的不同之处。

其中之一是被调用函数中的返回语句将控制权返回到调用该函数的函数,throw语句将控制权向上返回到第一个这样的函数:包含能够捕获相应异常的try-catch组合。

另一个不同之处是引发异常时编译器总是创建一个临时拷贝,即使异常规范和catch块中指定的是引用。创建临时拷贝的原因在于临时创建的对象会被释放,不能指向一个被释放的对象。不直接使用值传递的原因在于,基类引用可以指向派生类对象。有一组通过继承关联起来的异常类型,则在异常规范中只需要列出一个基类引用,他将与任何派生类对象匹配。

注意:如果有一个异常类继承层次结构,应这样排列catch块;将捕获位于层次结构最下面的异常类的catch语句放在最前面,将捕获基类异常的catch语句放在最后面。

class bad_1 {...};
class bad_2 : public bad_1 {...};
class bad_3 : public bad_2 {...};
...
void duper()
{...if(oh_no)throw bad_1();if(rats)throw bad_2();if(drat)throw bad_3();
}
...
try {duper();
} catch(bad_3 & be){...
} catch(bad_2 & be){...
} catch(bad_1 & be){...
}

如果把bad_1处理放在程序最前面,他将捕获异常bad_1、bad_2、bad_3,通过相反的顺序排列,bad_3异常将被bad_3 & 处理程序所捕获。

有时候,我们不知道会发生什么异常,在这种情况下,依然可以捕获异常,即使不知道异常的类型。方法是使用省略号来表示异常类型,从而捕获任何异常:

catch(...){ statement}

9.exception类

异常使得在程序设计中包含错误处理功能更容易,异常类似于类,在头文件exception头文件定义了exception类,C++可以把它用作其他异常类的基类。该类有一个名为what()的虚拟成员函数,他返回一个字符串,该字符串的特征随实现而异。由于这是一个虚方法,因此可以在从exception派生而来的类中重新定义它:

// 注意需要包含头文件:
#include <exception>
class bad_hmean : public std::exception
{
public:const char * what() {return "bad arguments to hmean()";}...
};class bad_gmean : public std::exception
{
public:const char * what() {return "bad arguments to gmean()";}...
};

如果不想以不同的方式处理这些派生而来的异常,可以在同一个基类处理程序中捕获它们:

try{...
} catch(std::exception & e){cout << e.what() << endl;...
}

C++库定义了很多基于exception的异常类型:

1.stdexcept异常类

头文件stdexcept定义了其他几个异常类。

首先定义了logic_error,(接受字符串参数,what()返回接受的字符串)描述了典型的逻辑错误:(存在可以通过编程修复的问题)

domain_error——描述定义域错误;

invalid_argument——描述传入非法值(与输入要求的值类型不同)

length_error——没有足够空间执行所需要的操作

out_of_bounds——索引错误

runtime_error——(接受字符串参数)运行期间难以预计和防范的错误:(无法避免的问题)

underflow_error—— 下溢错误,发生在浮点数计算中;

overflow_error——上溢错误,整型和浮点型均会发生;这两个统称为range_error.

所有的这些错误有相同的常规特征,之间主要区别在于不同的类名让您能够分别处理每种异常,另一方面,继承院系也能够一起处理这些异常。

2.bad_alloc异常和new

对于使用new导致的内存分配问题,C++最新处理方式是让new引发bad_alloc异常,头文件new包含bad_alloc类的声明,是从exception类公有派生而来的。

#include<iostream>
#include<new>
#include<cstdlib>
using namespace std;struct Big
{double stuff[20000];
};int main()
{Big* pb;try{cout << "Trying to get a big block of memory:\n";pb = new Big[100000000000];cout << "Got past the new request:\n";}catch (bad_alloc& ba){cout << "Caught the exception\n";cout << ba.what() << endl;exit(EXIT_FAILURE);}cout << "Memory successfully allocated.\n";delete[] pb;return 0;
}

3.空指针和new

很多代码都是在new失败后返回空指针编写,C++提供了失败返回空指针的new:

int * p1 = new (std::nothrow) int;
int * p2 = new (std::nothrow) int[500];

使用这种new,可将上边程序核心代码改为:

Big * pb;
pb = new (std::nothrow) Big[10000];
if(pb == 0)
{cout << "Could not allocate memory Bye" << endl;exit(EXIT_FAILURE);
}

10.异常、类和继承

异常、类和继承三种方式相互关联。可以从一个异常类派生出另一个;可以在类定义中嵌套异常类声明来组合异常;嵌套声明本身可被继承,还可用作基类。

#ifndef SALES_H
#define SALES_H
#include<stdexcept>
#include<string>
using namespace std;
class Sales
{
public:enum { MONTHS = 12 };
private:double gross[MONTHS];//派生类能使用int year;
public:class bad_index :public logic_error//客户类的catch可以使用这个类作为类型{private:int bi;public:explicit bad_index(int ix, const string& s = "Index error in Sales object\n");int bi_val()const { return bi; }virtual ~bad_index()throw() {}};explicit Sales(int yy = 0);Sales(int yy, const double* gr, int n);virtual ~Sales() {}int Year()const { return year; }virtual double operator [](int i)const;virtual double& operator [](int i);
};class LabeledSalese :public Sales
{
public:class nbad_index :public Sales::bad_index//nbad_index从logic_error派生而来{private:string lbl;public:nbad_index(const string& lb, int ix,const string& s = "Index error in LabeledSales object\n");const string& label_val()const { return lbl; }virtual ~nbad_index()throw() {}};explicit LabeledSalese(const string& lb = "none", int yy = 0);LabeledSalese(const string& lb, int yy, const double* gr, int n);virtual ~LabeledSalese() {}const string& Label()const { return label; }virtual double operator [](int i)const;virtual double& operator [](int i);
private:string label;
};
#endif // SALES_H

说明:

virtual ~bad_index()throw() {}

表明这句话不会发生异常。

#include"sales.h"
#include<string>
using namespace std;
Sales::bad_index::bad_index(int ix, const string& s) :logic_error(s), bi(ix)
{}
Sales::Sales(int yy)
{year = yy;for (int i = 0; i < MONTHS; ++i)gross[i] = 0;
}
Sales::Sales(int yy, const double* gr, int n)
{year = yy;int lim = (n < MONTHS) ? n : MONTHS;int i = 0;for (i = 0; i < lim; ++i)gross[i] = gr[i];for (; i < MONTHS; ++i)gross[i] = 0;
}
double Sales::operator [](int i)const
{if (i < 0 || i >= MONTHS)throw bad_index(i);return gross[i];
}
double& Sales::operator [](int i)
{if (i < 0 || i >= MONTHS)throw bad_index(i);return gross[i];
}
LabeledSalese::nbad_index::nbad_index(const string& lb, int ix, const string& s) :Sales::bad_index(ix, s)
{lbl = lb;
}
LabeledSalese::LabeledSalese(const string& lb, int yy) :Sales(yy)
{label = lb;
}
LabeledSalese::LabeledSalese(const string& lb, int yy, const double* gr, int n) :Sales(yy, gr, n)
{label = lb;
}double LabeledSalese::operator [](int i)const
{if (i < 0 || i >= MONTHS)throw nbad_index(Label(), i);return Sales::operator [](i);
}double& LabeledSalese::operator [](int i)
{if (i < 0 || i >= MONTHS)throw nbad_index(Label(), i);return Sales::operator [](i);
}

说明:

throw bad_index(i),会跳到std::logic_error(s)里。

#include <iostream>
#include"sales.h"
using namespace std;
int main()
{double vals1[12] ={1220,1100,1122,2212,1232,2334,2884,2393,3302,2922,3002,3544};double vals2[12] ={12,11,22,21,32,34,28,29,33,29,32,35};Sales sales1(2011, vals1, 12);LabeledSalese sales2("Blogstar", 2012, vals2, 12);cout << "First try block:\n";try{int i;cout << "Year=" << sales1.Year() << endl;for (i = 0; i < 12; ++i){cout << sales1[i] << ' ';if (i % 6 == 5)cout << endl;}cout << "Year=" << sales2.Year() << endl;cout << "Label=" << sales2.Label() << endl;for (i = 0; i <= 12; ++i){cout << sales2[i] << ' ';if (i % 6 == 5)cout << endl;}cout << "End of try block 1.\n";}catch (LabeledSalese::nbad_index& bad){cout << bad.what();cout << "Company:" << bad.label_val() << endl;cout << "bad index:" << bad.bi_val() << endl;}catch (Sales::bad_index& bad){cout << bad.what();cout << "bad.index:" << bad.bi_val() << endl;}cout << "\nNext try block:\n";try{sales2[2] = 37.5;sales1[20] = 23345;cout << "End of try block 2.\n";}catch (LabeledSalese::nbad_index& bad){cout << bad.what();cout << "Company:" << bad.label_val() << endl;cout << "bad index:" << bad.bi_val() << endl;}catch (Sales::bad_index& bad){cout << bad.what();cout << "bad index:" << bad.bi_val() << endl;}cout << "done\n";return 0;
}

输出:

 cout << bad.what();

what成员函数返回接受的字符串。

11.异常何时会迷失方向

异常被引发后,在两种情况下,会导致问题:

1.若异常不是在函数中引发的(或者函数没有异常规范),必须捕获它。如果没有捕获(在没有try块或没有匹配的catch块时,将出现这种情况),则异常被称为未捕获异常。(在默认情况下,这将导致程序异常终止。然而可以修改程序对意外异常和未捕获异常的反应)

2.若在带异常规范的函数中引发的,必须与规范列表中的某种异常匹配(在继承乘次结构中,类类型与这个类及其派生类的对象匹配),否则称为意外异常。(在默认情况下,这将导致程序异常终止(c++11摒弃了异常规范,但仍支持以前的老代码里的异常规范))。
解决办法:

1.未捕获异常不会导致程序立刻异常终止。相反,程序将首先调用函数terminate()。
在默认情况下,terminate()调用abort()函数。

可以指定terminate()应调用的函数(而不是abort())来修改terminate()的这种行为,为此,可调用set_terminate()函数。

假设希望未捕获的异常导致程序打印一条消息,然后调用exit函数。

void myQuit()
{
cout<<"Terminating due to uncaught exception\n";
exit(5);
}

在程序开头,将终止操作指定为调用该函数。

set_terminate(myQuit);

2.意外异常。通过给函数指定异常规范,可以让函数的用户知道要捕获那些异常。假设函数的原型如下:

double Argh(double double) throw(out_of_bounds);

则可以这样使用该函数:

try{
x=Argh(a,b);
}
catch(out_of_bounds & ex)
{
...
}

若函数引发了其异常规范中没有的异常,这被称为意外异常,程序将调用unexpected()函数。
这个函数调用terminate(),也有修改unexpected()的行为的set_unexpected()函数。这些函数必须在头文件:exception中。

unexpected_handler函数作用:

1.通过调用terminate()(默认行为),abort()或exit()来终止程序;

2.引发异常;引发异常(第二种选择)的结果取决于unexpected_handler函数所引发的异常以及引发意外异常的函数的异常规范

(如果新引发的异常与原来的异常规范匹配,则程序将从那里开始进行正常处理,即寻找与新引发的异常匹配的catch块。基本上,这种方法将用预期的异常取代意外异常;

如果新引发的异常与原来的异常规范不匹配且原来的异常规范中没有包括bad_exception类型,则程序将调用terminate()。bad_exception是从exception派生而来的,其声明位于头文件exception中。

如果新引发的异常与原来的异常规范不匹配,且原来的异常规范中包含了bad_exception类型,则不匹配的异常将被bad_exception异常所取代。)

如果要捕获所有异常(意外或预期异常),可以这样做:

#include<exception>     //保证异常头文件声明可用
using namespace std;
//然后,设计一个替代函数,将意外异常转换为bad_exception异常,该函数的原型如下:
void myUnexpected()
{throw bad_exception();
}

仅使用throw,而不是指定异常将重新引发原来的异常。然而,如果异常规范中包含了这种类型,则该异常将被bad_exception对象取代。

接下来在程序的开始位置,将意外异常操作指定为调用该函数:

set_unexpected(myUnexpected);

最后,将bad_exception类型包括在异常规范中,并添加如下catch块序列:

double Argh(double double) throw(out_of_bounds,bad_exception);
...
try{
x=Argh(a,b);
}
catch(out_of_bounds & ex)
{
...
}
catch(bad_exception & ex)
{
...
}

12.有关异常的注意事项

异常和动态内存分配的注意事项:

1.

void test1(int n)
{
string mesg("I'm trapped in an endless loop");
...
if(oh_no)
throw exception();
...
return ;
}

即便因为异常退出,程序也会调用string析构函数(栈解退)。

2.

void test2(int n)
{
double * ar =new double[n];
...
if(oh_no)
throw exception();
...
delete [] ar;
return;
}

异常退出,ar因为栈解退被删除,内存释放不掉,这造成了内存泄漏。

如何解决:在引发一场函数中捕获该异常,在catch块中包含清理代码,然后重新引发异常:

void test3(int n)
{
double * ar=new double [n];
...
try{
if(oh_no)
throw exception();
}
catch(exception & ex)
{
delete [] ar;
throw;
}
...
delete [] ar;
return;

所以重新引发异常目的在于拿出来一个中间态,解决当前一些问腿(释放内存等),然后再次抛出异常,再解决该异常。

④RTTI

1.用途

由同一个基类派生出来的派生类,基类指针可以指向很多派生类,如何知道到底指的是哪一类呢?派生对象可能包含了不是继承而来的方法,想跟踪生成对象的类型,该怎么办呢?RTTI提供解决方案。

2.原理

C++有三个支持RTTI的元素。RTTI只适用于包含虚函数的类。

1.dynamic_cast

dynamic_cast运算符将使一个指向基类的指针来生成一个指向派生类的指针,否则,该运算符返回0——空指针,可以安全地将对象的地址赋给特定类型的指针。

什么叫安全?

class Grand{//虚函数};
class Superb:public Grand{...};
class Magnificent:public Superb{...};
Grand * pg =new Grand;
Grand * ps = new Superb;
Grand * pm = new Magnificent;
Magnificent * p1 = (Magnificent *) pm;//1
Magnificent *  p2=(Magnificent *) pg;//2
Superb * p3= (Magnificent *) pm;//3

1是安全的,相同类型;

2.不安全,基类赋给了派生类,派生类指的新对象基类就没有;

3.安全的,派生类指针赋给基类。

2.dynamic_cast用法:

Superb * pm = dynamic_cast<Superb *>(pg);

指针pg的类型是否可被安全地转换为Superb*?如果可以,运算符将返回对象的地址,否则返回一个空指针。

注:如果指向的对象(*pt)的类型为Type或者是从Type直接或间接派生而来的类型,则下面的表达式将指针pt转换为Type类型的指针:

dynamic_cast<Type *>(pt)
#include <iostream>
#include<cstdlib>
#include<ctime>
using namespace std;
class Grand
{
private:int hold;
public:Grand(int h = 0) :hold(h) {}virtual void Speak()const { cout << "I am a grand class!\n"; }virtual int Value()const { return hold; }
};class Superb :public Grand
{
public:Superb(int h = 0) :Grand(h) {}void Speak()const { cout << "I am a superb class!!\n"; }virtual void Say()const{{ cout << "I hold the superb value of" << Value() << "!\n"; }}
};class Magnificent :public Superb
{
private:char ch;
public:Magnificent(int h = 0, char c = 'A') :Superb(h), ch(c) {}void Speak()const { cout << "I am a magnificent class!!!\n"; }void Say()const {cout << "I hold the character" << ch <<"and the integer" << Value() << "!\n";}
};Grand* GetOne();int main()
{srand(time(0));Grand* pg;Superb* ps;for (int i = 0; i < 5; i++){pg = GetOne();pg->Speak();if (ps = dynamic_cast<Superb*>(pg))ps->Say();//转换成功,ps的值为true;失败,pg指向一个Grand对象,ps的值将为false.}return 0;
}Grand* GetOne()
{Grand* p = nullptr;switch (rand() % 3){case 0:p = new Grand(rand() % 100);break;case 1:p = new Superb(rand() % 100);break;case 2:p = new Magnificent(rand() % 100,'A' + rand() % 26);break;}return p;
}

注:应尽可能使用虚函数,而只在必要时使用RTTI。

也可以将dynamic_cast用于引用,语法有点不同:没有与空指针对应的引用值,因此无法使用特殊的引用值来指示错误。当请求不正确时,dynamic_cast将引发类型为bad_cast的异常,这种异常是从exception类派生而来,它是在头文件typeinfo中定义的。因此下面的rg是对Grand对象的引用:

#include<typeinfo>
...
try{
Superb & rs = dynamic_cast<Superb &>(rg);
...
}
catch(bad_cast &){
...
};

2.typeid运算符和type_info类

typeid运算符使得能够确定两个对象是否为同种类型。可以接受两种参数:类名或结果为对象的表达式;
typeid运算符返回一个对type_info对象的引用,其中,type_info是在头文件typeinfo中定义的一个类。
type_info类重载了==和!=运算符,用于对类型的比较。例:pg指向的是Magnificent对象。则下面的表达式结果为bool值true,否则false:

typeid(Magnificent)==typeid(*pg)

如果pg是空指针,程序将引发bad_typeid异常。该异常类型从exception类派生而来,是在头文件typeinfo中声明的。

type_info 类的实现随厂商而异,但包含一个name()成员名,该函数返回一个随实现而异的字符串:(通常但并非一定)是类的名称。例如:

cout<<"Now processing type "<<typeid(*pg).name()<<".\n";
#include<iostream>
#include<cstdlib>
#include<ctime>
#include<typeinfo>
using namespace std;
class Grand
{
private:int hold;
public:Grand(int h = 0) :hold(h) {}virtual void Speak()const { cout << "I am a grand class!\n"; }virtual int Value()const { return hold; }
};class Superb :public Grand
{
public:Superb(int h = 0) :Grand(h) {}void Speak()const { cout << "I am a magnificent class!!!\n"; }virtual void Say()const {cout << "I hold the superb value of" << Value() << "!\n";}
};class Magnificent :public Superb
{
private:char ch;
public:Magnificent(int h = 0, char cv = 'A') :Superb(h), ch(cv) {}void Speak()const { cout << "I am a magnificent class!!!\n"; }void Say()const {cout << "I hold the character" << ch <<"and the integer" << Value() << "!\n";}
};Grand* GetOne();
int main()
{srand(time(0));Grand* pg;Superb* ps;for (int i = 0; i < 5; i++){pg = GetOne();cout << "Now processing type" << typeid(*pg).name() << ".\n";//name()函数返回一个随实现而异的字符串:通常类的名称pg->Speak();if (ps = dynamic_cast<Superb*>(pg))ps->Say();if (typeid(Magnificent) == typeid(*pg))cout << "Yes,you're really magnificent.\n";}return 0;
}Grand* GetOne()
{Grand* p = nullptr;switch (rand() % 3){case 0:p = new Grand(rand() % 100);break;case 1:p = new Superb(rand() % 100);break;case 2:p = new Magnificent(rand() % 100, 'A' + rand() % 26);break;}return p;
}

3.误用RTTI例子

下面介绍应避免的编程方式。
例:

正确的:

Grand * pg;Superb * ps;for(int i=0;i<5;i++){pg=GetOne();pg->Speak();if(ps=dynamic_cast<Superb *>(pg))ps->Say();

要避免的:(通过放弃dynamic_cast和虚函数,而使用typeid,可以将上述代码重新编写)

Grand * pg;
Superb * ps;
Magnificent * pm;
for(int i=0;i<5;i++)
{pg=GetOne();if(typeid(Magnificent)==typeid(*pg)){pm=(Magnificent *)pg;pm=Speak();pm=Say();}else if(typeid(Superb)==typeid(*pg)){ps=(Superb *) pg;ps->Speak();ps->Say();}elsepg->Speak();}

上面的代码更长,而且显示地指定各个类存在严重的缺陷。

下面的语句适用于所有从Grand派生而来的类:

pg->Speak();

而下面得语句适用于所有从Superb派生而来的类:

if(ps=dynamic_cast<Superb *>(pg))
ps->Say();

注:如果发现在扩展的if else 语句系列中使用了typeid,则应考虑使用虚函数和dynamic_cast。

⑤类型转换运算符

c++提供了更严格地限制允许的类型转换,并添加4个类型转换运算符:

dynamic_cast:动态转换。                    const_cast:常量转换。

static_cast:静态类型转换。                  reinterpret_cast:重新解释转换。

可以根据目的的选择一个合适的运算符,并让编译器能够检查程序的行为是否与设计想法吻合。而不是使用通用的类型转换。

1.dynamic_cast

.dynamic_cast用法:

Superb * pm = dynamic_cast<Superb *>(pg);

指针pg的类型是否可被安全地转换为Superb*?如果可以,运算符将返回对象的地址,否则返回一个空指针。

注:如果指向的对象(*pt)的类型为Type或者是从Type直接或间接派生而来的类型,则下面的表达式将指针pt转换为Type类型的指针:

dynamic_cast<Type *>(pt)

2. const_cast

语法:const_cast<type-name>(expression),type-name和expression的类型必须相同

const_cast 用于删除 constvolatile 和 __unaligned 特性。

const_cast不是万能的,他可以修改指向一个值的指针,但不能修改const值的,可以间接通过修改指向const值的const指针来修改const值。

#include<iostream>
using std::cout;
using std::endl;
void change(const int* pt, int n);int main()
{int p1 = 1212;const int p2 = 500;cout << " p1 = " << p1 << ",p2 = " <<p2 << endl;change(&p1, -212);change(&p2, 300);cout << " p1 = " << p1 << ",p2 = " << p2 << endl;return 0;
}void change(const int* pt, int n)
{int* p = const_cast<int*> (pt);*p += n;
}

 p2的值没变。

#include<iostream>
using std::cout;
using std::endl;
void change(const int* pt, int n);int main()
{int p1 = 1212;const int p = 500;const int * p2 = &p;cout << " p1 = " << p1 << ",p2 = " <<*p2 << endl;change(&p1, -212);change(p2, 300);cout << " p1 = " << p1 << ",p2 = " << *p2 << endl;return 0;
}void change(const int* pt, int n)
{int* p = const_cast<int*> (pt);*p += n;
}

p2的值变了。

3.static_cast

static-cast<type-id>(expression)

仅根据表达式中存在的类型,将 expression 转换为 type-id 类型,当且仅当type_id可被隐式转换为expression所属的类型或expression可被隐式转换为type_name所属的类型时,上述转换才合法,否则将出错。(基类地址赋给派生类指针也是可以的。)

通常使用 static_cast 转换数值数据类型,例如将枚举型转换为整型或将整型转换为浮点型,而且你能确定参与转换的数据类型。 static_cast 转换安全性不如 dynamic_cast 转换,因为 static_cast 不执行运行时类型检查,而 dynamic_cast 执行该检查。 对不明确的指针的 dynamic_cast 将失败,而 static_cast 的返回结果看似没有问题;这是危险的。

static_cast 运算符还可用于执行任何隐式转换,包括标准转换和用户定义的转换。

4.  reinterpret_cast

reinterpret_cast<type_name>(expression);

允许将任何指针转换为任何其他指针类型。 也允许将任何整数类型转换为任何指针类型以及反向转换。

缺点:依赖于实现的底层编程技术,是不可移植的。并不支持所有的类型转换,不能将函数指针转换为数据指针,反之亦然。

【友元、异常和其他】——C++ Prime Plus CH15相关推荐

  1. 友元 异常 RTTI 类型转换符

    友元异常和其它 友元 友元类 友元成员函数 其他友元关系 共同友元 嵌套类 嵌套类和访问权限 模板中的嵌套 异常 调用abort() 返回错误码 异常机制 将对象用作异常类型 异常规范和C++11 栈 ...

  2. 《C++ Primer Plus》第15章 友元、异常和其他 学习笔记

    友元使得能够为类开发更灵活的接口.类可以将其他函数.其他类和其他类的成员函数作为友元.在某些情况下,可能需要前向声明,需要特别注意类和方法声明的顺序,以正确地组合友元. 潜逃类是在其他类中生命的类,它 ...

  3. C++ Primer Plus学习(十四)——友元、异常和其他

    友元.异常和其他 友元 嵌套类 异常 RTTI 类型转换运算符 友元 友元 在C++中,一个类可以有public.protected.private三种属性的成员,通过对象可以访问public成员,只 ...

  4. 重学C++笔记之(十三)友元、异常和其他

    本章先介绍C++语言最初就有的特性:友元类.友元成员函数和嵌套类,然后介绍C++语言新增的一些特性:异常.运行阶段类型识别(RTTI)和改进后的类型转换控制. 1. 友元 前面我们介绍了友元函数,它用 ...

  5. 【C++ Primer】第十五章 友元、异常和其他 --之一---友元和嵌套类

    一,友元 1)可以将类作为友元,友元类的所有方法都可以访问原始类的私有成员和保护成员. 2)下面例子介绍了 电视类和遥控器类,其中遥控器类为电视类的友元类 3)注意:友元关系不具对称性.即 A 是 B ...

  6. 想 new 个对象过七夕,她却抛了异常

    作者 | 轩辕之风 责编 |王晓曼 来源 | 编程技术宇宙(ID:xuanyuancoding) 七夕又到了,单身汪们太难了,每年不仅要经历双十一,要经历2.14,还要经历七夕节,真是古今中外都不肯放 ...

  7. 思科安全:加密流量威胁检测、加密流量威胁和恶意软件检测、识别无线干扰或威胁、Talos 情报源可加强对已知和新型威胁的防御、分布式安全异常检测...

    思科DNA竞品比较工具 您的网络能够驱动数字化转型吗? 根据IDC调查,45%的受调研公司计划在未来两年内做好网络数字化的准备.查看数字化网络带来的结果和商业价值. 下载报告 思科 HPE 华为 Ar ...

  8. 类模板,多种类型的类模板,自定义类模板,类模板的默认类型,数组的模板实现,友元和类模板,友元函数,类模板与静态变量,类模板与普通类之间互相继承,类模板作为模板参数,类嵌套,类模板嵌套,类包装器

     1.第一个最简单的类模板案例 #include "mainwindow.h" #include <QApplication> #include <QPush ...

  9. C/C++学习之路: 模板和异常

    C/C++学习之路: 模板和异常 目录 模板 类型转换 异常 1. 模板 1. 模板概述 c++提供了函数模板(function template),函数模板实际上是建立一个通用函数,其函数类型和形参 ...

  10. java创建对象过七夕,想 new 个对象过七夕,她却抛了异常

    原标题:想 new 个对象过七夕,她却抛了异常 关注 "" 导读:单身之痛...... 作者 | 轩辕之风 来源 | 编程技术宇宙(ID:xuanyuancoding) 七夕又到了 ...

最新文章

  1. C++ primer 第七章之 友元函数与友元类
  2. objective-c 中代码块(blocks)
  3. bzoj 1753: [Usaco2005 qua]Who's in the Middle【排序】
  4. 面向 Visual Studio 开发者的 Git 内部源代码
  5. uni-app第三方登陆-微信
  6. kubernetes service 原理解析
  7. linux哪个模块允许保存文件,linux网络操作系统与实训 课后习题答案
  8. 编码、解码Html代码
  9. MyEclipse格式化代码设置
  10. windows .bat脚本检测文件更新,并复制
  11. 大量字段表单在PHP便捷处理分享
  12. 如何使用Matlab实现数据的拟合
  13. Vue源码学习1--获取源码和查找入口文件
  14. java 事务回滚失败
  15. 向量设计丨导师简介丨客户案例丨商业活动丨跨界合作
  16. (笔记)数据结构——顺序表
  17. 斐波那契数列的各种求法
  18. Springer出版社旗下投稿word模板
  19. linux /sys
  20. 超酷! Atlas给黑白视频“上色”

热门文章

  1. Python海龟库write方法中形参font用法的记录
  2. linux上apk免杀,kali 免杀工具shellter安装以及使用(示例代码)
  3. 机器学习之决策树(数据分析师学习必备)——糖潮丽子的博客
  4. 推荐几款php探针,PHP探针推荐:检测服务器环境好帮手
  5. linux怎样解压bin文件,linux下解压bin文件
  6. 2020年 显卡天梯图 / Top Graphics Ranking
  7. Minidump方式保留程序崩溃现场
  8. linux vi下划线,如何将我的Vim高亮线更改为不是下划线?
  9. 如何用O2OA公文编辑器制作标准的红头文件?
  10. 如何在百度收录平台注册账号获取Token