C++除了增加了面向对象机制外, 还做了很多细微的改进.
引用和传址调用
对一个数据可以建立一个引用,它的作用是为变量起一个别名,这是C++对C的一个重要扩充.
在声明引用时采用形如int val = 1; int &ref=val;
格式,类型名必须与被引用的变量相同.
引用在定义时必须进行初始化(形参在虚实结合的时候完成初始化),在使用引用时和使用原变量完全相同.
引用不是一种数据类型(连构造类型都不是),不能建立引用数组,引用没有地址也就没有引用的指针也不能建立引用的引用。
C++增加引用机制主要是作为函数形参以实现传址调用的功能:
#include <iostream>
using namespace std;
int ref_swap (int &x, int &y) {
int t;
t=x;
x=y;
y=t;
return 0;
}
int main()
{
int a,b;
int &x=a, &y=b;
cin>>a>>b;
ref_swap (a,b);
cout << a<<' '<<b << endl;
cout<<x/y<<endl;
return 0;
}
形参声明为引用int swap (int &a, int &b);
, 实参是变量名(变量原名或引用均可)在参数传递过程中传递的是地址,被调用函数中形参的变化同样影响实参.
使用引用传址时,实参是变量传递的是变量的地址;使用指针传址时, 实参是指针对象传递的是指针的值, 仍是传值调用。
默认参数
为函数设定默认参数时只需要在函数声明时对形参进行赋值,若实参表中没有给出该参数的值则自动使用默认值.
#include <iostream>
using namespace std;
template <typename t_name>
double aver(t_name a,t_name b){
return (a+b)/2;
}
int fun (int a, int b) {
return a%b;
}
double fun (double a, double b,double c=3) {
return (a+b)/c;
}
int main()
{
int a,b;
double x,y;
cin>>a>>b;
cin>>x>>y;
cout << fun(a,b)<<'\n'<<fun(x,y) << endl;
cout<<aver(a,b)<<'\n'<<aver(x,y)<<endl;
return 0;
}
一个参数指定了默认值那么它后面的参数也一定要指定默认值, 否则将出现语法上的二义性.
new和delete运算符
new
和delete
运算符代替了malloc
和free
进行动态内存分配和回收:
int *ptr = new int;
在堆区申请存储一个整数的空间,并返回int型指针
int *ptr = new int(arg)
堆区申请一个整数空间,并初始化为arg,返回指针。
char *str = new char[size];
堆区申请长度为size的数组,返回首指针。
可以申请高维数组,但所有数组都无法初始化。
delete ptr;
释放指针ptr指向的空间,free(ptr)
delete [] ptr;
释放指针ptr指向的数组。delete和[]之间有没有空格均可。
try-catch 异常处理
程序运行时中经常出现意想不到的情况,比如错误的输入,要打开的文件不存在等。程序要对这些情况进行检测和处理,避免在出现异常情况时程序崩溃,这就是异常处理机制。
异常处理与调试是两个概念,调试在程序代码最终完成提交前,为了确保程序正确进行的测试和修改。完成的代码中,调试用的代码(如assert宏或输出中间数据的语句)不应再发挥作用,但应该包含异常处理的代码。
C++提供了try-catch结构进行异常处理。try-catch结构允许检测到异常的函数或语句不处理异常,而是将异常抛出(throw)由它的上一级主调函数进行处理。
异常可以逐级向上抛出,直到第一个被调用的main函数也无法处理时程序才会崩溃.
class MathError {} error;
double div(int x, int y) {
if (y == 0) {
throw error;
}
return x / y;
}
double foo() {
try {
div(1, 0);
}
catch (MathError e) {
cout << "error: cannot divide 0" <<endl;
return 0;
}
catch(...) {
throw;
}
}
try-catch中间不允许加入其它语句,一个try可以后接多个catch.
在运行时首先执行try后的语句块,如果未检测到异常则执行最后一个catch后的语句即不进行异常处理.
若检测到异常程序员可以使用throw来跳出try块,throw可以写在try后的语句或者try的语句块所调用的函数中。无论throw在什么位置都将立刻终止try语句块的执行.
根据抛出的表达式的类型(如int,runtime_error等)选择接收对应类型的catch异常处理器(异常处理块)进行异常处理,执行完一个catch异常处理器后将退出try-catch结构.
上述匹配是从前到后进行的,也就是说可以有多个catch语句捕捉同一类型的异常信息但只有第一个可能收到异常信息并处理异常.
catch表达式中可以只填写数据类型但也可以在后面声明一个参数,这参数将存储接收的throw表达式的值, 从这一点看catch像是一个收到相应类型错误信息就会执行的函数.
catch后的表达式可以填写“...”用于接收任意类型的数据,通常写在最后类似switch结构中的default.
catch中也可以使用throw,这个throw可以(不是必须)不包含表达式表示将异常原样向上一级抛出.
<stdexcept>
和<exception>
头文件定义了一组标准异常类用于传递异常信息.
namespace
C++中提供了namespace提供命名空间管理. namespace与文件作用域类似, 可以在其中定义类型函数以及变量, 作为一个作用域其中的标识符不允许重复.
namespace可以进行嵌套, 同一个namespace可以分布在多个文件中.
name.h:
#ifndef NAME_H_INCLUDED
#define NAME_H_INCLUDED
namespace My {
int num;
void func();
}
void My::func() {
std::cout << "sth" << std::endl;
}
#endif // NAME_H_INCLUDED
main.cpp
namespace My {
class A {};
namespace Nested {
double d;
}
}
int main()
{
My::A a;
My::func();
return 0;
}
::
是C++中的作用域运算符, 用于指定标识符所属的命名空间或类.形如::main()
这样不指定左值的用法, 表示当前文件的全局作用域.
除了上述示例中使用My::
访问命名空间外还可以使用using
关键字进行访问.
`using namespace std;`
`using std::cout;`
std
是C++标准库定义的命名空间, 其中定义了C++标准库的标识符.
使用using语句后就可以不指定namespace而直接使用整个命名空间或者某个标识符.
overload
函数重载
C++允许对函数进行重载, 即同一个函数名根据不同的参数类型调用不同的实现.
函数重载允许函数名相同, 但不允许函数名与参数类型完全相同.
含有默认参数的函数作为重载函数时要注意,无论是否省略参数都不能使函数调用出现二义性
void print(int val) {
cout << "print int: " << val << endl;
}
void print(double val) {
cout << "print double: " << val << endl;
}
int main()
{
print(1);
print(1.2);
return 0;
}
运算符重载
C++也允许对运算符进行重载,比如<<
既是位左移运算符又是流插入运算符。
C++中通过定义一个运算符重载函数实现运算符的重载。函数的参数作为运算符的操作数,函数返回值作为表达式的值.
定义格式如icomplex operator+(icomplex, icomplex);
, operator是C++中定义运算符重载函数使用的关键字.
C++不允许用户自定义新的运算符,只能对已有的运算符进行重载。除成员运算符.
,解引用运算符::
、取长运算符sizeof()
、条件运算符外?:
的运算符均可重载.包括下标[]
,取成员->
,逗号运算符,
,new
和delete
等其它所有运算符.
运算符重载不改变优先级和结合性,不改变操作数的个数(重载函数也就不能有默认参数).
重载运算符的操作数至少有一个是类对象或类对象的引用,以避免程序员对基本类型运算的任意修改.
C++会为赋值运算符=
提供默认实现(浅拷贝),若程序员自定义了复制操作应对赋值运算符进行重载.复制构造函数的默认实现会调用=
, 无需再次重载.
重载某些特殊类型的运算符需要注意:
重载强制类型转换运算符时注意有个空格
operator int(icomplex)
.++
和--
运算符因为前置-和后置不同而参数表相同,无法直接区分。所以C++规定在形参表中多一个int形参数的是后置运算符的重载函数,否则为前置运算符重载函数。若对逻辑运算符进行了重载, 重载后的运算符不再具有短路特性.
因为重载函数需要访问类成员,所以一般把重载函数定义为类成员函数或友元函数。
类成员函数因为有this指针,所以在参数表列中省略第一个参数, 将运算符重载为友元函数时则没有此限制,但是必须将所有操作数写入参数表.
以operator+为例,编译器处理c1+c2
实际是c1.operator+(c2)
。因此,作为成员函数重载时运算符的第一个操作数必须是类对象且与函数类型相同.
将运算符重载为成员函数还是友元函数,有一些惯例:
C++规定赋值运算符
=
,变址运算符[]
,函数调用运算符()
,指针取成员运算符->
,必须作为成员函数.因为C++为它们提供了默认实现, 若将其重载为全局友元函数将在调用默认实现还是重载函数上产生二义性.单目运算符和复合赋值运算符一般作为成员函数;为避免重载为成员函数对操作数类型的限制, 双目运算符一般作为友元函数.
流插入
<<
和流提取>>
运算符只能作为友元函数.
两者重载格式为:
friend istream& operator>>(istream&,MyType);
friend ostream& operator<<(ostream&,myType);
.重载函数需要返回定义时的第一个参数,也就是输入或输出流对象(引用)以便于连续的输入输出(cin>>a>>b;
cout<<a<<b<<endl;
).
template
泛型编程即编写独立于具体类型的代码, C++泛型编程主要通过模板函数以及模板类来实现.
模板形参声明是一类特殊语句,用于声明模板参数代替具体数据类型:
`template<typename type1, typename type2 ...>
`template<class type1, class type2 ...>`
typename / class是C++关键字,用于声明其后的标识符type1 、type2可以代表任意数据类型,它们称为模板类型参数.
相同的类型参数在实例化后表示相同的数据类型, 如果像std::map一样需要多个模板类型, 则需要定义多个模板类型参数.
`template<int N = 16, ...>`
可以使用具体的类型声明模板参数,此时的模板参数可以作为模板声明作用域内的常量使用.
模板声明对紧跟它的代码块有效,如紧跟模板声明的类定义或函数定义。
与其它形参一样,编译器只关心虚拟类型参数的个数和使用它们定义的变量类型之间的关系,不关心参数的具体名称。
当函数模板声明与定义分开时,两处均需声明虚拟类型参数,只需要虚拟类型参数的个数一致名称可以不同.
模板函数可以是全局函数也可以是类的成员函数,只要声明了模板类型参数就可以使用这些参数表示任意类型,其它与普通函数相同。
在类定义前使用模板声明可以使这个类成为类模板,也可以在类定义内部使用模板声明使得紧跟的一个成员函数变为函数模板,而其它成员函数仍然为普通函数。
使用类模板声明具体对象称为类模板的实例化,不同的虚拟类型参数可以被实例化为不同的具体类型.若在另一个类模板B中使用模板类A,则实例化的实参可以是B的类型参数.
template<typename val_t>
struct node {
val_t val;
node<val_t> *prior;
node<val_t> *next;
node():prior(NULL),next(NULL) {};
node(val_t val_argu): val(val_argu),prior(NULL),next(NULL)
{};
};
template<typename val_t>
class dlist {
private:
node<val_t> *head;
node<val_t> *tail;
public:
dlist(void);
bool empty();
};
template<typename type>
bool dlist<type>::empty() {
if (head->next == tail) {
return true;
}
else {
return false;
}
}
关于运算符重载和模板的更多示例, 可以参见作者初学C++时写的两段代码: