目录
1.函数重载
1.1 函数重载概念
1.2C++支持函数重载的原理--名字修饰(name Mangling)
2.引用
2.1 引用概念
2.2 引用特性
2.3 常引用
2.4 使用场景
2.4.1 做参数
2.4.2 做返回值
2.5 传值、传引用效率比较
2.5.1 传值、传引用效率比较--做参数返回
2.5.2 值和引用的作为返回值类型的性能
2.6 引用和指针的区别
3 inline(内联)
3.1 概念
3.2 特性
4 缺省参数
4.1概念
4.2 分类
4.2.1 全缺省参数
4.2.2 半缺省参数
4.3 注意事项
5 namespace(命名空间)
5.1 命名空间定义
5.2命名空间的使用
5.2.1加命名空间名称及作用域限定符
5.2.2 使用using将命名空间中某个成员引入
5.2.3 使用using namespace 命名空间名称 引入
1.函数重载
1.1 函数重载概念
函数重载: 是函数的一种特殊情况, C++ 允许在 同一作用域中 声明几个功能类似 的同名函数 ,这些同名函数的 形参列表(参数个数 或 类型 或 类型顺序)不同 ,常用来处理实现功能类似数据类型不同的问题。
代码实例:
#include<iostream>
using namespace std;
// 1、参数类型不同
int Add(int left, int right) {
cout << "int Add(int left, int right)" << endl;
return left + right;
}
double Add(double left, double right) {
cout << "double Add(double left, double right)" << endl;
return left + right;
}
// 2、参数个数不同
void f() {
cout << "f()" << endl;
}
void f(int a) {
cout << "f(int a)" << endl;
}
// 3、参数类型顺序不同
void f(int a, char b) {
cout << "f(int a,char b)" << endl;
}
void f(char b, int a) {
cout << "f(char b, int a)" << endl;
}
int main() {
Add(10, 20);
Add(10.1, 20.2);
f();
f(10);
f(10, 'a');
f('a', 10);
return 0;
}
1.2C++支持函数重载的原理--名字修饰(name Mangling)
为什么C++支持函数重载,而C语言不支持函数重载呢?在 C/C++ 中,一个程序要运行起来,需要经历以下几个阶段: 预处理、编译、汇编、链接
1.
实际项目通常是由多个头文件和多个源文件构成,而通过
C
语言阶段学习的编译链接,我们可以知道,
【当前
a.cpp
中调用了
b.cpp
中定义的
Add
函数时】,编译后链接前,
a.o
的目标文件中没有
Add
的函数地
址,因为
Add
是在
b.cpp
中定义的,所以
Add
的地址在
b.o
中。那么怎么办呢?
2.
所以链接阶段就是专门处理这种问题,
链接器看到
a.o
调用
Add
,但是没有
Add
的地址,就会到
b.o
的符
号表中找
Add
的地址,然后链接到一起
。
3.
那么链接时,面对
Add
函数,链接接器会使用哪个名字去找呢?这里每个编译器都有自己的
函数名修饰
规则。
4.c
的函数修饰后名字不变。c++的
函数修饰后变成【
_Z+
函数长度
+
函数名
+
类
型首字母】。
可以通过下面的图示观察到结果
2.引用
2.1 引用概念
引用 不是新定义一个变量,而 是给已存在变量取了一个别名 ,编译器不会为引用变量开辟内存空间,它和它引用的变量 共用同一块内存空间。
类型
&
引用变量名
(
对象名
) =
引用实体;
void TestRef()
{
int a = 10;
int& ra = a;//<====定义引用类型
printf("%p\n", &a);
printf("%p\n", &ra);
}
//注意:应用类型必须和实体类型一致
2.2 引用特性
1. 引用在定义时必须初始化
2. 一个变量可以有多个引用
3. 引用一旦引用一个实体,再不能引用其他实体
void TestRef()
{
int a = 10;
// int& ra; // 该条语句编译时会出错(引用没有初始化)
int& ra = a;
int& rra = a;
printf("%p %p %p\n", &a, &ra, &rra);
}
2.3 常引用
void TestConstRef()
{
const int a = 10;
//int& ra = a; // 该语句编译时会出错,a为常量
const int& ra = a;
// int& b = 10; // 该语句编译时会出错,b为常量
const int& b = 10;
double d = 12.34;
//int& rd = d; // 该语句编译时会出错,类型不同
const int& rd = d;
}
2.4 使用场景
2.4.1 做参数
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
2.4.2 做返回值
int& Count()
{
static int n = 0;
n++;
// ...
return n;
}
思考一下:下面代码输出什么结果?为什么?
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is :"<< ret <<endl;
return 0;
}
将引用当作别名来理解,这个代码就像,李逵吃了一碗饭,黑旋风又吃了两碗,李逵到底吃了多少嘞。其实不论是李逵还是黑旋风,说的都是一个人,这里的ret也是。
注意:
如果函数返回时,出了函数作用域,如果返回对象还在
(
还没还给系统
)
,则可以使用引用返回,
如果已经还给系统了,则必须使用传值返回。
2.5 传值、传引用效率比较
2.5.1 传值、传引用效率比较--做参数返回
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是 传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是 当参数或者返回值类型非常大时,效率就更低
#include <time.h>
struct A { int a[10000]; };
void TestFunc1(A a) {}
void TestFunc2(A& a) {}
void TestRefAndValue()
{
A a;
// 以值作为函数参数
size_t begin1 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc2(a);
size_t end2 = clock();
// 分别计算两个函数运行结束后的时间
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main()
{
TestRefAndValue()
return 0;
}
2.5.2 值和引用的作为返回值类型的性能
#include <time.h>
struct A { int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a; }
// 引用返回
A& TestFunc2() { return a; }
void TestReturnByRefOrValue()
{
// 以值作为函数的返回值类型
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1();
size_t end1 = clock();
// 以引用作为函数的返回值类型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2();
size_t end2 = clock();
// 计算两个函数运算完成之后的时间
cout << "TestFunc1 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main()
{
TestReturnByRefOrValue();
return 0;
}
通过上述代码的比较,发现传值和指针在作为传参以及返回值类型上效率相差很大
2.6 引用和指针的区别
在
语法概念上
引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
int main()
{
int a = 10;
int& ra = a;
cout<<"&a = "<<&a<<endl;
cout<<"&ra = "<<&ra<<endl;
return 0;
}
在
底层实现上
实际是有空间的,因为
引用是按照指针方式来实现
的
int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}
通过上面的汇编分析,我们可以简单总结出一下几个引用和指针的不同点:
1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
2. 引用在定义时必须初始化,指针没有要求
3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型
实体
4. 没有NULL引用,但有NULL指针
5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占
4个字节)
6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
7. 有多级指针,但是没有多级引用
8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
9. 引用比指针使用起来相对更安全(指针又野指针问题)
3 inline(内联)
3.1 概念
以
inline
修饰
的函数叫做内联函数,
编译时
C++
编译器会在
调用内联函数的地方展开
,没有函数调用建立栈帧
的开销,内联函数提升程序运行的效率
如果在上述函数前增加
inline
关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用。
查看方式:
1.
在
release
模式下,查看编译器生成的汇编代码中是否存在
call Add
2.
在
debug
模式下,需要对编译器进行设置,否则不会展开
(
因为
debug
模式下,编译器默认不会对代码进行优化
3.2 特性
1. inline
是一种
以空间换时间
的做法,如果编译器将函数当成内联函数处理,在
编译阶段,会用函数体替
换函数调用
,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
2. inline对于编译器而言只是一个建议,不同编译器关于
inline
实现机制可能不同
,一般建议:将
函数规
模较小
(
即函数不是很长,具体没有准确的说法,取决于编译器内部实现
)
、
不是递归、频繁调用
的函数采用inline
修饰,否则编译器会忽略
inline
特性。下图为《
C++prime
》第五版关于
inline
的建议:
3. inline
不建议声明和定义分离,分离会导致链接错误。因为
inline
被展开,就没有函数地址了,链接就会找不到。
拓展:宏的优缺点?优点:1. 增强代码的复用性。2. 提高性能。缺点:1. 不方便调试宏。(因为预编译阶段进行了替换)2. 导致代码可读性差,可维护性差,容易误用。3. 没有类型安全的检查 。C++ 有哪些技术替代宏 ?1. 常量定义 换用 const enum2. 短小函数定义 换用内联函数
4 缺省参数
4.1概念
缺省参数是 声明或定义函数时 为函数的 参数指定一个缺省值 。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
void Func(int a = 0)
{
cout<<a<<endl;
}
int main()
{
Func(); // 没有传参时,使用参数的默认值
Func(10); // 传参时,使用指定的实参
return 0;
}
4.2 分类
4.2.1 全缺省参数
void Func(int a = 10, int b = 20, int c = 30)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
4.2.2 半缺省参数
void Func(int a, int b = 10, int c = 20)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
4.3 注意事项
1. 半缺省参数必须从右往左依次来给出,不能间隔着给
2. 缺省参数不能在函数声明和定义中同时出现
3. 缺省值必须是常量或者全局变量
4. C语言不支持(编译器不支持)
5 namespace(命名空间)
在 C/C++ 中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化 ,以 避免命名冲突或名字 污染 , namespace 关键字的出现就是针对这种问题
5.1 命名空间定义
定义命名空间,需要使用到
namespace
关键字
,后面跟
命名空间的名字
,然
后接一对
{}
即可,
{}
中即为命名空间的成员。
namespace HTY
{
//注意:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中
// 命名空间使用
// 命名空间中可以定义变量/函数/类型
int rand = 10;
int Add(int left, int right)
{
return left + right;
}
struct Node
{
struct Node* next;
int val;
};
}
//2. 命名空间可以嵌套
// test.cpp
namespace N1
{
int a;
int b;
int Add(int left, int right)
{
return left + right;
}
namespace N2
{
int c;
int d;
int Sub(int left, int right)
{
return left - right;
}
}
}
//3. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
// ps:一个工程中的test.h和上面test.cpp中两个N1会被合并成一个
// test.h
namespace N1
{
int Mul(int left, int right)
{
return left * right;
}
}
5.2命名空间的使用
有三种方式:
5.2.1加命名空间名称及作用域限定符
int main()
{
printf("%d\n", N::a);
return 0;
}
5.2.2 使用using将命名空间中某个成员引入
using N::b;
int main()
{
printf("%d\n", N::a);
printf("%d\n", b);
return 0;
}
5.2.3 使用using namespace 命名空间名称 引入
using namespce N;
int main()
{
printf("%d\n", N::a);
printf("%d\n", b);
Add(10, 20);
return 0;
}