上篇文章中说到,为了解决C语言会出现人为定义的函数和库函数出现重定义的错误,C++引入了一个新的概念,即命名空间,通过认为定义命名空间,来解决上述问题。
在本篇文章中,将继续介绍C++相对于C语言不足来进行的补充,例如:缺省参数,重载函数等等。
1. 缺省参数:
1.1 缺省参数的定义以及单个缺省参数简单应用:
缺省参数的定义大致如下:缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。例如下面的函数:
void PRINT(int a = 0)
{
cout << a << endl;
}
int main()
{
PRINT(0);
PRINT(10);
return 0;
}
不难发现,相比C语言中的函数书写方法,C++中,对于函数的形式参数给定了一个初始值,该值就是上面定义中所说的缺省值。对于这种带有缺省值的函数,其调用结果如下:
这也证实了定义中的一句话,如果没有对函数的实参进行指定赋值,则在函数执行时默认采用形参,反之采用实参。
1.2 多个缺省参数的应用:
在上面的情况中,函数只有单一的缺省值。当函数的缺省值数量大于时,函数的调用方式会更加多样,例如:
void PRINT(int a = 10, int b = 20, int c = 30)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
int main()
{
PRINT();
PRINT(1);
PRINT(1, 2);
PRINT(1, 2, 3);
return 0;
}
运行结果为:
通过上面的例子可以看到,针对这种缺省值数量大于的情况下,函数的调用会因为实参的给定而给出不用的效果。
但是对于实参数值的给定,需要注意,实参的给定必须是连续的,中间不能有跳跃的情况,例如:
PRINT(, 2, );
1.3 全缺省与半缺省:
全缺省参数就是上一部分的代码所展示,即所有的形参都给定了缺省值。对于半缺省,则是对于形式参数,不全部给定缺省值,例如:
void PRINT(int a, int b = 20, int c = 30)
但是对于半缺省参数的给定需要注意以下两点:
1. 缺省值必须从右向左给定,且必须是连续的。
2. 当函数的定义和声明分离时,缺省参数不能同时在声明和定义中出现。一般是将缺省参数给定到函数声明中
2. 函数重载:
2.1 函数重载的定义及类型:
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。例如下面给出的例子:
int ADD(int num1, int num2)
{
cout << "int ADD(int num1, int num2) = ";
return num1 + num2;
}
double ADD(double num1, double num2)
{
cout << " double ADD(double num1, double num2) = ";
return num1 + num2;
}
int main()
{
cout << ADD(1, 2) << endl;
cout << ADD(1.1, 2.2) << endl;
}
在上述代码中,建立两个函数,且这两个函数函数名相同,都是,但是这两个函数的返回值类型,以及参数类型都不同,此时符合上方函数重载的定义,构成了函数重载。
对于上述代码,运行结果如下:
上面的例子展示了函数的类型不同,以及其参数类型不同的情况下可以构成重载。当函数的参数数量不同时,即:
void fun()
{
cout << "fun()" << endl;
}
void fun(int a)
{
cout << "fun(a)" << endl;
}
int main()
{
fun();
fun(1);
}
此时符合重载定义中参数数量不同这一特性,程序依旧可以正常运行,运行结果如下:
但是上述函数有一种特殊情况需要注意,即:将函数的参数给定一个缺省值,例如:
void fun()
{
cout << "fun()" << endl;
}
void fun(int a = 0)
{
cout << "fun(a)" << endl;
}
int main()
{
fun();
fun(1);
}
运行程序,编译器会报错。这是因为在存在一个不含参数的函数与另一个有参数,且参数有缺省值的情况下,同时运行两个函数会造成二义性,即:编译器不能判断在函数中,调用函数时,调用的函数是上面两个函数哪一个函数。
对于函数重载,还有一种方式,即类型的顺序不同,具体情况如下方代码所示:
void fun(int a, char b)
{
cout << "void fun(int a, char b)" << endl;
}
void fun(char a, int b)
{
cout << "void fun(char a, int b)" << endl;
}
int main()
{
fun(2, 1.1);
fun(2.2, 1);
return 0;
}
3. 引用:
3.1 引用的概念以及基本使用:
在C++中,引用是基于C语言中指针的不足,为解决这些不足创建的。引用的概念如下:引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
对应代码如下:
int main()
{
int a = 1;
int& c = a;
}
函数中,第一行代码创建了一个名为的整型变量,第二行则是引用,即给已有的整型变量取了一个别名叫做。
在上面的概念中提到,编译器不会为引用变量开辟内存空间,引用与其引用的变量共用一块内存空间。因此,加入对引用进行更改,相应的变量也会更改,例如对于下面代码:
int main()
{
int a = 1;
int& c = a;
cout << "更改前 a=" << a << endl;
cout << "更改前 c=" << c << endl;
c++;
cout << "更改后 a=" << a << endl;
cout << "更改后 c=" << c << endl;
return 0;
}
运行结果为:
引用不光可以用在已有的变量上,也可以对引用进行引用,即:
int main()
{
int a = 1;
int& c = a;
int& e = c;
cout << "a = " << a << endl;
cout << "c=" << c << endl;
cout << "e=" << e << endl;
return 0;
}
运行结果为:
对于引用而言,其最大的价值适用于做参数,例如在之前文章中提到到交换函数:
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
int main()
{
int x = 5;
int y = 1;
Swap(&x, &y);
printf("%d %d", x, y);
return 0;
}
由于函数形参只是实参的一份临时拷贝,形参的改变并不会影响实参,所以在传递参数时,需要传递变量的地址。
如果利用引用的特性,即:引用的改变会引起变量的改变,来实现交换函数,则可以一定程度上化简上述函数,即:
void Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int x = 5;
int y = 1;
Swap(x, y);
printf("%d %d", x, y);
return 0;
}
运行结果如下:
3.1 使用引用的注意事项:
3.1.1 引用与初始化:
在指针这部分中,如果一个指针只是被创建但是不被初始化,则这个指针称为野指针。不过指针的初始化并不是必然的,但是对于引用来说,必须被初始化,例如:
int main()
{
int a = 0;
int& b;
return 0;
}
由于引用没有被初始化,因此程序错误。
3.1.2 引用能不能被改变指向:
在数据结构关于的链表的文章中,如果需要在链表中插入一个新的结点,需要改变链表中指针的指向,但是对于引用而言,不能被改变指向,例如:
int main()
{
int a = 0;
int& b = a;
int c = 1;
printf("%p %p %p", &a, &b, &c);
printf("\n");
b = c;
cout << "a = " << a << "b = " << b << "c = " << c << endl;
printf("%p %p %p", &a, &b, &c);
return 0;
}
运行结果如下:
通过结果不难发现,在执行了这一条代码后,的地址依旧与的地址相同,说明没有改变其指向。只是改变了的值。
因此,引用虽然在一定程度上弥补了指针的不足,但是由于引用并不能改变指向,因此引用并不能完全替代指针。
3.2 引用返回:
3.2.1 引用返回的概念:
在解释本节标题的内容之前,首先需要了解一个概念:
int fun()
{
int n = 0;
n++;
return n;
}
int main()
{
int ret = fun();
return 0;
}
图中给定的程序的函数的返回值返回,在函数外部创建了一个名为的变量来接收函数返回值,但是,由于在函数运行结束后,该函数对应的栈帧全部销毁,因此,并不是直接接收函数的返回值,而是返回值先被存到寄存器中,通过寄存器来间接接收这个值。
此时,如果将函数返回的返回值改为下方代码所示,即:
int& fun()
{
int n = 0;
n++;
return n;
}
int main()
{
int ret = fun();
return 0;
}
此时再去运行程序,编译器会给出警告。这是因为在这种返回类型下,返回值并不能确定,如果编译器在函数结束后直接清理掉栈帧,则返回空,如果编译器在函数结束后,并不会立即清理掉栈帧,则返回还能有返回值。
(注:由于VS并不会立刻清理掉栈帧,因此运行该程序,返回值会返回)
通过上面的例子,可以得出一个结论,即:在函数运行结束后,即离开函数的作用域后,此时的返回对象被销毁,因此不能采用引用返回。
但是,对于静态变量和利用类似方式创建的变量,在函数结束后不会销毁,可以采用引用返回。
3.2.2 引用返回的价值:
由于用返回的价值主要体现在C++的后续内容,因此,本文在此部分指给出结论,原理会在后续的文章中进行阐述。
1. 提高返回速度
2. 可以修改返回对象
3.3 常引用:
对于常引用,具体可以有下面代码反应:
int main()
{
const int a = 1;
int& b = a;
return 0;
}
从上面代码可以看到,变量是一个被修饰的常变量,下面对变量进行了一次引用。当去运行上述代码时,编译器会显示错误。
但是如果将上述代码进行修改,即:
int main()
{
const int a = 1;
const int& b = a;
return 0;
}
运行此代码时,并没有错误,不难观察到,上下的给的代码唯一的差距就是第二次给出的代码的引用前加了。因此,在进行常引用的过程中,一定不能涉及变量权限的放大。例如上面运行失败的代码中,原本变量的权限是不能修改,但是在引用后,由于引用的修改会引起原变量的修改,因此,引用的权限超过了原变量的权限,造成了权限放大。所以导致运行错误。这里需要注意的时,只是不能放大变量的权限,对于变量权限的平移的缩小均无影响,例如:
int main()
{
int c = 2;
const int& d = c;
return 0;
}
上述代码在引用后,缩小了变量的权限,可以正常运行。
另外,对于一个常量进行常饮用也是允许的,即:
const int& e = 10;
令外,当引用与被引用的对象存在类型转换的情况时,即:
int j = 10;
double h = 15;
const double& k = j;
由于类型转化的过程中,会额外生成一个常量作用于该过程,引用时接收的值并不是,而是通过一个常量完成类型转换间接传给,由于常量的常性,因此需要加上。
4. 指针与引用的练习与区别:
1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
2. 引用在定义时必须初始化,指针没有要求
3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何
一个同类型实体
4. 没有NULL引用,但有NULL指针
5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32
位平台下占4个字节)
6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
7. 有多级指针,但是没有多级引用
8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
9. 引用比指针使用起来相对更安全
10.在引用这一小节开头的时候说到,引用是针对一个对象起的别名,与被引用对象共用一块空间。在汇编层上,引用是利用指针来实现的,因此也需要开辟空间。不过,在日常运用时,一般认为引用是不会开辟新空间的。
5. 内联函数:
对于一般的函数,在函数作用时需要开辟栈帧,而内联函数不需要开辟栈帧,而是在运行中原地展开。由于内联函数不需要开辟栈帧,因此提高了程序的运行效率。
内敛函数的书写,只需要在函数声明的开头加上即可,例如:
inline int Add(int x, int y)
{
int z = x + y;
return x + y;
}
int main()
{
int x = 5;
int y = 1;
int ret = Add(x, y);
return 0;
}
但是需要注意,内联函数并不能完全替代普通函数,并且,当一个函数的代码函数10行时,不建议采用内联函数。这是因为,内联函数每次调用时都会原地展开,当调用的次数较大时,例如调用次,则一个代码行数为行的内联函数完全展开后为行,延缓程序的运行速度。