C++ 学习笔记4
Overloading
函数签名包含了描述一个函数所必须的信息
- 函数名
- 参数类型、个数、顺序等
- 所在类和命名空间
当函数名相同时,编译器还是可以通过函数签名来决定究竟调用哪个函数。这里需要注意形参名称不是函数签名的一部分。函数声明时可以只给出参数类型的列表,因为形参名在声明时并不重要。有时可能见到函数的参数列表只有一个 void
关键字,代表不能传入参数
void main(void) {}
函数定义时也可以省略形参名
void fun(int) { cout << "fun called" << endl; }
重载运算符
重载的运算符可以是类中的一个成员函数
class Entry {
public:
int value;
Entry(int value) : value(value) {}
Entry operator+(const Entry& e) { return value + e.value; }
};
void main() {
Entry a(1), b(2);
a + b; // a.+(b)
}
也可以作为一个全局函数
class Entry {
public:
int value;
Entry(int value) : value(value) {}
};
Entry operator+(const Entry& lhs, const Entry& rhs) {
return lhs.value + rhs.value;
}
void main() {
Entry a(1), b(2);
a + b; // +(a, b)
}
如果成员运算符和全局运算符同时可用,C++ 会优先选择成员运算符。如果类的定义出现在命名空间中,很显然成员运算符不会受到影响,但是全局运算符就有可能定义在命名空间的内外。为了解决这个问题,C++ 会将类所在的命名空间全部导入,然后搜索运算符。因此,在命名空间内外同时定义运算符是非法的
namespace anon {
class Entry {
public:
int value;
Entry(int value) : value(value) {}
};
Entry operator+(const Entry& lhs, const Entry& rhs) {
return lhs.value + rhs.value;
}
}
anon::Entry operator+(const anon::Entry& lhs, const anon::Entry& rhs) {
return lhs.value + rhs.value;
}
void main() {
Entry a(1), b(2);
a + b; // error: use of overloaded operator '+' is ambiguous
}
这就导致了一个很隐蔽的问题,比如
namespace anon {
class Entry {
friend Entry operator+(const Entry&, const Entry&);
public:
int value;
Entry(int value) : value(value) {}
};
}
anon::Entry operator+(const anon::Entry& lhs, const anon::Entry& rhs) {
return lhs.value + rhs.value;
}
由于友元函数的声明本身就可以作为函数声明,因此此时还是相当于上面那种重定义的情景,即使友元函数没有被定义。
自定义输出
全局的运算符重载可以用于输出自定义类型
class Entry {
friend ostream& operator<<(ostream&, const Entry&);
bool isInt;
int value;
};
ostream& operator<<(ostream& out, const Entry& e) {
out << e.isInt << ' ' << e.value;
return out;
}
如果将 <<
定义为类的成员函数,调用时就需要写成 e << cout
。这样虽然可以通过编译,但是却违背了编程的直观性,因此不推荐这样写。
自增运算符重载
i++
与 ++i
是 C 语言中常用的两个操作。对于自定义类型,可以这样对自增运算符进行重载
class Entry {
public:
int value;
Entry(int value) : value(value) {}
Entry& operator++(void) { // ++i
value++;
return *this;
}
Entry operator++(int) { // i++
Entry oldValue = *this;
value++;
return oldValue;
}
};
可以看出,后置自增运算符需要首先把当前状态保存起来,因此会比前置自增运算符慢。但是如果自增的返回值被忽略时,如
void main() {
Entry a(1);
a++;
++a;
}
此时编译器会进行优化,前置后置运算符效果相同。
默认参数
在函数声明时,可以在形参名后跟一个默认值。用户调用函数时,如果没有显式的传递参数,则这一参数会被赋予默认值参与运算。例如
void fun(int a, int b = 1) {
// ...
}
int main(int argc, char *argv[]) {
fun(10);
fun(10, 11);
return 0;
}
需要注意以下两点
- 默认参数可以有多个,但必须位于参数列表末尾
- 在函数的多次声明中,只能有一次含默认参数的声明
异常处理
最简单的一段异常处理代码如下
void func() {
throw "error";
}
int main() {
try {
func();
} catch (char const* s) {
cout << "error: " << s << endl;
}
return 0;
}
输出
error: error
可以看出,C++ 的异常处理语法与 Java 十分相似。与之不同的是,C++ 可以抛出几乎所有类型的异常,而不必限定于异常类的体系结构中。这意味着我们可以抛出任意的自定义类型异常
class Error {};
void func() {
throw Error();
}
int main() {
try {
func();
} catch (Error& e) {
cout << "caught" << endl;
}
return 0;
}
但在实际应用中,还是应该将自定义异常类加入到标准异常类体系中(图片来源:C++ exception类:C++标准异常的基类)
其中的基类 exception
定义如下
class exception {
public:
exception() throw();
exception(const exception&) throw();
exception& operator=(const exception&) throw();
virtual ~exception() throw();
virtual const char* what() const throw();
}
这里有两点需要说明
- 函数声明后的
throw()
是异常说明,会在后文中说明 - 定义了
what()
函数
许多在异常类体系结构中的类都支持由字符串初始化,而这个字符串就可以被 what
函数取出
void fun() {
throw runtime_error("error occurred");
}
int main() {
try {
fun();
} catch (exception& e) {
cout << e.what() << endl; // error occurred
}
return 0;
}
catch 子句
在使用 catch
子句捕获异常时,其后的异常说明符类似于函数形参。也就是说,要在 catch
块内使用异常说明符,程序需要对异常对象进行复制,这显然是低效的。因此一般来说,应该使用引用来声明异常说明符
catch (exception& e) { ... }
如果想要捕获全部异常
class Error {};
int main() {
try {
throw Error();
} catch (...) {
cout << "error caught" << endl;
}
return 0;
}
有时,一段代码可能无法完全处理遇到的异常,因而需要将异常重新抛出
try {
throw Error();
} catch (...) {
cout << "error caught" << endl;
throw;
}
在 catch
块中使用 throw
可以重新抛出其捕获的异常。
函数 try 块
可以用 try-catch
结构将整个函数包裹起来
int main() try {
throw Error();
return 0;
} catch (...) {
cout << "caught" << endl;
}
这样的结构在处理构造函数所导致的异常时特别有用
class Super {
public:
Super(int) {
throw runtime_error("runtime error");
}
};
class Son {
Super father;
public:
Son() : father(1) {
try {
cout << "inside try block" << endl;
} catch (...) {
cout << "error caught" << endl;
}
}
};
int main() {
Son s;
return 0;
}
输出
libc++abi.dylib: terminating with uncaught exception of type std::runtime_error: runtime error
Command terminated
可见,Son
的构造函数内的 try
块并没有捕获到 Super
构造函数抛出的异常。为了捕获初始化列表中的异常,可以使用函数测试块
Son() try : father(1) {
cout << "inside try block" << endl;
} catch (...) {
cout << "error" << endl;
}
输出
error
libc++abi.dylib: terminating with uncaught exception of type std::runtime_error: runtime error
Command terminated
可以看到,Son
的构造函数成功捕获了来自 Super
的异常。然而在这个例子中,异常的传播并没有在 Son
处停止,而是继续传播到了主函数。
异常声明
在旧版本 C++ 中,使用 throw
来标注一个函数可能抛出的异常。但这一特性在 C++11 中被 noexcept
关键字取代
void func(void) noexcept;
也可以在某些条件为真时声明函数不抛出异常
template<class T> T f() noexcept(sizeof(T) < 4);
noexcept
说明符的参数必须是常量表达式。
需要注意,析构函数默认不抛出异常。
命名空间
现代工程规模越来越大,难免出现两个函数或变量重名的情况。为了解决这个问题,C++ 中引入了命名空间这一概念。本质上,命名空间就是变量名解析的一个范围。定义一个命名空间需要使用 namespace
关键字
namespace name {
void fun(void) {
// ...
}
}
命名空间的定义可以是不连续的或嵌套的。使用命名空间名解析变量名时需要使用域区分符 ::
void main(void) {
name::fun();
}
引用全局数据或函数时,可以不跟名称
int a;
void Set(int a) {
::a = a;
}
匿名命名空间
为了限制标志符的作用域,C 中提出了 static
关键字。在 C++ 中相同的功能由匿名命名空间完成
namespace {
// ...
}
本质上匿名命名空间的定义等价于以下代码
namespace __some_name {
// ...
}
using namespace __some_name;
由于这一名称由编译器生成,因此在当前编译单元以外无法访问。这样就保证了匿名命名空间中定义的标志符具有文件作用域。
Using指令
使用 using
指令可以将一个命名空间内的所有变量及函数导入当前文件。最常见的例子是
using namespace std;
不使用这一指令实际上也可以完成所有操作
#include <iostream>
void main(void)
{
std::cout << "..." << std::endl;
}
只是由于std
这一命名空间中的变量较为常用,因此通常在程序开头将其导入。另外,using
指令还可以针对性导入某个名称
#include <iostream>
using std::cout;
void main(void) {
cout << "..." << std::endl;
}
模板
模板是一种代码重用的方法,属于泛型编程。在没有模板的时候,如果我们要完成两个数的交换操作,我们可能需要定义一组 swap
函数
void swap(int &a, int &b) {
int temp = a;
a = b;
b = temp;
}
void swap(double &a, double &b) {
double temp = a;
a = b;
b = temp;
}
// ...
然而这些函数中的核心代码都是一样的,只有参数类型发生了改变。因此我们可以定义一个模板来完成任意类型的元素交换。
函数模板
template <typename T> // 等价于 <class T>
void swap(T &a, T &b) {
T temp = a;
a = b;
b = temp;
}
在调用时,可以显式的指定模板类型,也可以缺省交给编译器进行类型推断
void main(void) {
int a = 1;
int b = 2;
swap<int>(a, b);
swap(a, b);
}
类模板
类似的,我们可以定义类模板
template <typename T>
class Swap {
public:
static void swap(T &a, T &b);
};
template <typename T>
void Swap<T>::swap(T &a, T &b) {
T temp = a;
a = b;
b = temp;
}
但与函数模板不同,类模板无法类型推断,必须手动指定类型
void main(void) {
int a = 1;
int b = 2;
Swap<int>::swap(a, b);
cout << a << endl;
cout << b << endl;
}
输出
2
1
STL
Standard template library 是一套标准模板类。这里以 vector
为例做一个简单的说明
#include <vector>
using namespace std;
void main(void) {
vector<int> v;
for (int i = 0; i < 1000; i++) {
v.push_back(i); // 加入队列
}
}
访问元素时,可以将 vector
对象类比于数组使用,例如 vi[900]
。可以看出 vector
类中重载了访问下标这一运算符。另一种访问方式运用了迭代器设计模式
vector<int>::iterator it = vi.begin();
while(it != vi.end()) {
cout << *it << endl;
it++;
}