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;
}

需要注意以下两点

  1. 默认参数可以有多个,但必须位于参数列表末尾
  2. 在函数的多次声明中,只能有一次含默认参数的声明

异常处理

最简单的一段异常处理代码如下

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 类的继承层次以及它们所对应的头文件

其中的基类 exception 定义如下

class exception {
public:
    exception() throw();
    exception(const exception&) throw();
    exception& operator=(const exception&) throw();
    virtual ~exception() throw();
    virtual const char* what() const throw();
}

这里有两点需要说明

  1. 函数声明后的 throw() 是异常说明,会在后文中说明
  2. 定义了 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++;
} 

参考

C++ Template 基础篇

C++模板template用法总结

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

LutingWang

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

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

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

打赏作者

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

抵扣说明:

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

余额充值