> 文章列表 > C++入门(中篇)

C++入门(中篇)

C++入门(中篇)

🔥🔥本章重内容
C++入门(中篇)

C++入门

  • 1. 函数重载
    • C++是怎么支持函数名重载的呢?
  • 2.引用
    • 2.1引用特性
    • 2.2常引用
    • 2.3使用场景
      • 1. 做参数
      • 2. 做返回值
    • 2.4引用和指针的区别
  • 3.内联函数

1. 函数重载

函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表==(参数个数 或 类型 或 类型顺序)==不同,常用来处理实现功能类似数据类型不同的问题。
那函数重载是上面样呢?
列如:

#include <iostream>
using namespace std;void Add(int a, int b)
{cout << a + b << endl;
}void Add(double a, double b)
{cout << a + b << endl;
}int main()
{Add(1, 2);Add(1.1, 2.2);return 0;
}

上面这段代码在C语言中肯定是编不过去的,C语言会报错说,函数重命名
但是C++支持这样写,只要函数的**(参数个数 或 类型 或 类型顺序)**不同就可以。

//参数类型不同
void Add(int a, int b)
{cout << a + b << endl;
}void Add(double a, double b)
{cout << a + b << endl;
}//参数个数不同
void fun()
{cout << "fun()" << endl;
}void fun(int a)
{cout << "fun(int a)" << endl;
}//参数顺序不同
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;
}

C++是怎么支持函数名重载的呢?

C++中有函数名修饰规则,但是这个规则不是C++创始人给的,是由写编译器的人来给的,所以Linux与Windows的函数名修饰规则是不同的。
函数名修饰规则会给函数另起一个名字,简单的了解一下汇编代码。
C++入门(中篇)
图中箭头是main函数中对应的指令,call的意思是跳转到被调用的函数那里,可以看到两次call他们的地址是不同的,所以C++允许函数名相同,但(参数个数 或 类型 或 类型顺序)不能相同。
看一下输出结果:
C++入门(中篇)
注意:如果两个函数函数名和参数是一样的,返回值不同是不构成重载的,因为调用时编译器没办法区分。

2.引用

引用就相当于是起别名,相当于你的大名和小名,它都指的是你。

//类型& 引用变量名(对象名) = 引用实体;
void fun()
{int a = 10;int& ra = a;//<====定义引用类型printf("%p\\n", &a);printf("%p\\n", &ra);
}

代码中的int& ra = a;就是对a的引用
既然是对a起别名那他们的地址相同吗?
如下图所示:
C++入门(中篇)
所以如果我们改变别名,原本的值也会发生改变。

注意引用类型必须和引用实体是同种类型的

2.1引用特性

  1. 引用在定义时必须初始化
  2. 一个变量可以有多个引用
  3. 引用一旦引用一个实体,再不能引用其他实体
int main()
{//这样的代码是不行的int& b;//一个变量可以有多个引用int a = 0;int& c = a;int& d = a;//引用一旦引用一个实体,再不能引用其他实体int x = 5;int y = 6;int& z = x;int& z = y;//错误写法return 0;
}

前面两种好理解,讲一下后面这种为什么不行。

解释为什么引用一旦引用一个实体,再不能引用其他实体
当我们写了两个
int& z = ;
int& z = ;
编译器会认为 z 是重定义,进行了多次初始化。
就像我们写了两个 int a = ; int a = ;这样肯定是不行的。
如果我们写成
int& z = x;
z = y;
这相当于我们把y的值赋给了z
所以说:引用一旦引用一个实体,再不能引用其他实体

2.2常引用

什么是常引用呢,就是我们引用的是常量。
先来说结论,大家来判断下面的代码是否正确。
结论:引用过程中权限可以平移或缩小,但不能扩大。

int main()
{//问题一const int a = 10;int& ra = a;//问题二int b = 10;const int& rb = b;//问题三double d = 1.1;int& rd = d;return 0;
}

前两个问题大家都可以根据结论判断它是否正确。
问题一是错误的,因为它将a的权限扩大了,原本a是const的常量,我们在起别名的时候只能是常量别名。
问题二是正确的,我们说权限可以缩小或平移,所以别名可以加上const。
问题三呢?它是给一个double类型的变量d起别名,但别名的类型是int类型。
有的同学可能会说,给double类型的数据起别名只能用double类型。
那我们先来看结果。
C++入门(中篇)
为什么会有警告说是从“double”转换到“const int”呢?
C++入门(中篇)
所以会有上面那样的警告。
我们说权限可以缩小和平移,如果我们给int前加上const那么程序是不是就可以正常运行了?
C++入门(中篇)
答案是正确的。

2.3使用场景

1. 做参数

用引用参数,可以代替我们之前使用的指针。
列如我们要交换两个数的值

//指针写法
void Swap(int* left, int* right)
{int temp = *left;*left = *right;*right = temp;
}//引用写法
void Swap(int& left, int& right)
{int temp = left;left = right;right = temp;
}

我们发现引用使用起来更方便,使用指针有时候我们可能还会传值错误,忘记传地址过去。
但我们使用引用的话,根本不需要去考虑传地址的问题。
引用直接给变量起别名,使用起来更方便。

2. 做返回值

那我们先来回忆一下之前C语言用类型做返回值。
C++入门(中篇)
如果返回值的内存占用大,我们直接返回引用值。
如果是返回引用的话,可以直接跳过拷贝,赋值给a。减少时间和空间的使用。
那我们以后是所有带返回值的函数都要用引用返回吗?
当然不是。
就比如上面那段代码如果用引用值返回的话,会带来后果呢?
C++入门(中篇)
图片中的输出结果看似没有问题,但实际是有问题的。
我们返回c的别名,但函数调用结束后,栈帧会被销毁,空间会被释放掉。

这里打印ret的值是不确定的
如果Count函数结束,栈帧销毁,没有清理栈帧,那么ret的结果侥幸是正确的
如果Count函数结束,栈帧销毁,清理栈帧,那么ret的结果是随机值

当我们再调用依次其他函数,或再多写几行代码,之前的函数的数据会被覆盖。
那怎么证明呢?
我们用引用来接收返回值。相当于是返回值的别名。
C++入门(中篇)
因为a是返回值的别名,所以后面再调函数,会改变a的值。
C++入门(中篇)
当我们调用其它函数后,它会覆盖掉当前的函数。所以a会变为随机值。
C++入门(中篇)
注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用
引用返回,如果已经还给系统了,则必须使用传值返回。

2.4引用和指针的区别

在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。(他们的地址相同我们有证明过)。
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。

int main()
{int a = 10;int& ra = a;ra = 20;int* pa = &a;*pa = 20;return 0;
}

C++入门(中篇)
可以看到引用的汇编代码与指针的汇编代码是相同的,所以引用也会开辟一个指针的空间
32位平台下开4个字节,64位平台下开8个字节

  1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
  2. 引用在定义时必须初始化,指针没有要求
  3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
  4. 没有NULL引用,但有NULL指针
  5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
  6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
  7. 有多级指针,但是没有多级引用
  8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
  9. 引用比指针使用起来相对更安全

3.内联函数

内联函数是怎么来的呢?
我们先看一段代码。

void Print(int i)
{cout << i << endl;
}int main()
{for (int i = 0; i < 100; i++){Print(i);}return 0;
}

这个函数会被调用100次,函数调用建立栈帧的开销会比较大。
而内联函数是在调用函数时直接在被调用的位置上展开的,不会开辟栈帧。
只需要在函数前加一个inline。这个函数就可能会成为内联函数。

//内联函数写法
inline void Print(int i)
{cout << i << endl;
}

C++入门(中篇)
在我们看汇编代码时,它为什么还是会call(调用函数)呢?
因为我们在debug版本下,debug模式下,编译器默认不会对代码进行优化。
但我们也可以对其进行修改,使inline可以在debug版本下进行优化。
C++入门(中篇)

C++入门(中篇)
C++入门(中篇)
修改完成之后便可以观察汇编代码,看其是否以优化。
C++入门(中篇)
所以这就是内联函数的优点,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
既然内联函数没有函数调用建立栈帧的开销,又可以提升运行效率那我们是不是应该全去使用内联函数呢?
C++入门(中篇)
如果我们都去使用内敛函数的话,上图会产生1000 * 50行代码,
不用内联函数的话就只会有1000行调用代码和20行Add的代码。
所以如果函数内容代码量大的话,我们就不能使用内联函数。

如果函数内代码行数大于10,我们就不能使用内联函数。

使用内联函数还需要注意一个点,就是内联函数的声明和定义必须放在一起。
C++入门(中篇)
C++入门(中篇)
C++入门(中篇)
如果像我们这样写程序是过不去的。
因为内联函数是没有地址的,当我们链接时发现找不到fun函数,内联函数不会函数名修饰。
C++入门(中篇)

所以我们要把内联函数的定义与声明放在一起写。
当我们把声明和定义都写在头文件里时,程序就可以链接上了。
C++入门(中篇)

特性

  1. inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
  2. inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
  3. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到

在这里插入图片描述