“C++基础入门指南:了解语言特性和基本语法”(下)

简介: “C++基础入门指南:了解语言特性和基本语法”(下)

传值、传引用效率比较


以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。


  • 做参数比较


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


e25e2f24aa7a41f8b79b16620876bbc4.png


  • 做返回值比较


#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();
  system("pause");
}


bb24b39180104c37b11839c933cff6b2.png


通过上述代码的比较,发现传值和指针在作为传参以及返回值类型上效率相差很大。


引用和指针的区别


在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。


int main()
{
  int a = 10;
  int& ra = a;
  cout<<"&a = "<<&a<<endl;
  cout<<"&ra = "<<&ra<<endl;
  return 0;
}


1efe8b34948e4f6fb54de1ee76417839.png


在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。那就看下以下操作吧


int main()
{
  int a = 10;
  int* p1 = &a;
  int& ref = a;
  return 0;
}


我们来看下引用和指针的汇编代码对比


8a12611f50b44a71abec576ef8f317f4.png

可能有的同学不太看的懂汇编,但是我们会发现他们的汇编指令一模一样.

那我简单解释一下这几个指令:

  • lea是取地址的意思,先把把a的地址放到eax寄存器中。
  • dword ptr:表示操作数的大小,这里表示一个双字(32位)。
  • mov:表示将一个值从源操作数复制到目标操作数。将寄存器 eax 中的值复制到内存地址

p1 指向的位置,由于 eax 是一个32位寄存器,因此会将32位的值存储到该地址。下面的两行同样意思.

那有的的同学还有疑惑,那对他们进行 +±-的操作还会一样吗,我们继续来看看


int main()
{
  int a = 0;
  int* p1 = &a;
  int& ref = a;
  ++(*p1);
  ++ref;
  return 0;
}

68290a8399ab4574af786d0b2a2de5d1.png

我们发现他们的汇编代码也都是一样的,我在带大家解释一下这几个指令。

  • mov eax, dowrd ptr [p1] :: p1存的是一个地址,把这个地址放在eax寄存器上。
  • mov ecx, dword ptr [eax]:: 寄存器加个[] 就是解引用的意思,把这个解引用的值放在ecx

上,

  • add ecx,1 :: 对ecx的值加1
  • mov edx ,dword ptr [p1] :: 在将p1的地址放在edx寄存器里面
  • mov dword ptr [edx],ecx:: 把ecx的值放回 edx里面

下面的ref的操作也是一模一样的

底层的汇编语言级别,确实没有直接对应引用的概念。底层汇编语言通常只提供了指针的概念和相关指令,而没有引用的语法糖。- 所有我们从底层角度里面看,我们只有指针没有引用,


  • 但引用和指针还是有点区别的:


1.引用概念上定义一个变量的别名,指针存储一个变量地址。


2.初始化:引用必须在创建时进行初始化,并绑定到某个对象。指针可以在任何时候被初始化,包括创建后再指向一个对象。


3.空值:引用不能为null,它必须始终引用一个有效的对象。指针可以为null,表示它当前没有指向任何对象。


4.重新赋值:引用一旦初始化后,无法更改其绑定的对象。指针可以通过重新赋值来指向不同的对象。


5.空引用和空指针:引用不能表示不存在的对象,它始终引用一个有效的对象。指针可以为空,表示它目前没有指向任何对象。


6.访问对象:通过引用可以直接访问和操作对象,而指针需要使用解引用操作符(*)来访问指向的对象。


7..引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小


8.有多级指针,但是没有多级引用


9.在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32

位平台下占4个字节)


10.引用比指针使用起来相对更安全


总的来说,引用提供了方便的别名机制,用于直接访问对象,而指针则提供了更灵活的内存操作,可以指向不同的对象,并允许对对象的位置进行更多的控制。


C++函数重载



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


函数重载的规则如下:

  1. 函数名称必须相同。
  2. 参数列表必须不同,要么是参数类型不同,要么是参数个数不同,要么是参数顺序不同。
  3. 返回类型可以相同也可以不同,返回类型不参与函数重载的决策。


  • 参数类型不同


#include<iostream>
using namespace std;
// 参数类型不同
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;
}


  • 参数个数不同


void f()
{
 cout << "f()" << endl;
}
void f(int a)
{
 cout << "f(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++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。具体的讲解我已经写在该文章了【C语言】 程序员的自我修养之(程序编译过程)

这里我们直接总结一下:

7248eef4a65b4235b80479a22dc5c74a.png

  • C++与C找不到函数地址的报错信息

15f27ddb37c74d7191b9df9f5c93189e.png


为什么他们报错信息都不一样呢?接下来让我们看看吧!


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.由于Windows下vs的修饰规则过于复杂,而Linux下g++的修饰规则简单易懂,下面我们使

用了g++演示了这个修饰后的名字。

5.通过下面我们可以看出gcc的函数修饰后名字不变。而g++的函数修饰后变成【_Z+函数长度+函数名+类型首字母】


  • 采用C语言编译器编译后结果


ef2fc90c82f648848eeb0e13d736fd9f.png


结论:在linux下,采用gcc编译完成后,函数名字的修饰没有发生改变。C语言是静态链接的语言,函数的调用是通过函数名进行匹配的。由于函数名在编译阶段必须唯一,因此在C语言中,同一个作用域内不能存在同名的函数。


  • 采用C++编译器编译后


d00e5daf0eed4c97a834cbcc15de2f64.png


结论:在linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中。


1c534296627c424d9f176e42f05743f3.png


对比Linux会发现,windows下vs编译器对函数名字修饰规则相对复杂难懂,但道理都是类似的.


6.通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数修
饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。

7.如果两个函数函数名和参数是一样的,返回值不同是不构成重载的,因为调用时编译器没办
法区分。


C++内联函数



C++内联函数(inline function)是一种特殊类型的函数,以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率




6aa4ee4b711c476f9a0dca8d077e3ebc.png


如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用。


查看方式:

1.在release模式下,查看编译器生成的汇编代码中是否存在call Add

2.在debug模式下,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认不
会对代码进行优化,以下给出vs2013的设置方式)


6a3bfba9fa414e6183c68576e8ba3741.png


dbd8354d42064545bea3353af5f15d43.png


1.inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会

用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运

行效率。

2.inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建

议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不

是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。下图为

《C++prime》第五版关于inline的建议:


26574bf58d594ab6bb7dcbc59909fc29.png


3.inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址
了,链接就会找不到


// F.h
#include <iostream>
using namespace std;
inline void f(int i);
// F.cpp
#include "F.h"
void f(int i)
{
  cout << i << endl;
}
// main.cpp
#include "F.h"
int main()
{
  f(10);
  return 0;
}
// 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl 
f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用


在通常情况下,将内联函数的声明和定义放在同一个头文件中是比较推荐的做法。这是因为内联函数在编译时被展开,没有实际的函数地址,所以链接器无法正确地解析和连接分离的内联函数的定义。


如果将内联函数的声明放在头文件中,而将定义放在源文件中,那么在链接时,其他源文件无法找到该函数的定义,从而导致链接错误。


为了避免链接错误,通常的做法是在头文件中同时包含内联函数的声明和定义,并且将这些函数声明为inline,以确保函数被正确地展开和内联。

目录
相关文章
|
1月前
|
编译器 程序员 定位技术
C++ 20新特性之Concepts
在C++ 20之前,我们在编写泛型代码时,模板参数的约束往往通过复杂的SFINAE(Substitution Failure Is Not An Error)策略或繁琐的Traits类来实现。这不仅难以阅读,也非常容易出错,导致很多程序员在提及泛型编程时,总是心有余悸、脊背发凉。 在没有引入Concepts之前,我们只能依靠经验和技巧来解读编译器给出的错误信息,很容易陷入“类型迷路”。这就好比在没有GPS导航的年代,我们依靠复杂的地图和模糊的方向指示去一个陌生的地点,很容易迷路。而Concepts的引入,就像是给C++的模板系统安装了一个GPS导航仪
105 59
|
1月前
|
算法 C++
2022年第十三届蓝桥杯大赛C/C++语言B组省赛题解
2022年第十三届蓝桥杯大赛C/C++语言B组省赛题解
37 5
|
30天前
|
C++
C++ 20新特性之结构化绑定
在C++ 20出现之前,当我们需要访问一个结构体或类的多个成员时,通常使用.或->操作符。对于复杂的数据结构,这种访问方式往往会显得冗长,也难以理解。C++ 20中引入的结构化绑定允许我们直接从一个聚合类型(比如:tuple、struct、class等)中提取出多个成员,并为它们分别命名。这一特性大大简化了对复杂数据结构的访问方式,使代码更加清晰、易读。
32 0
|
1月前
|
存储 安全 编译器
【C++打怪之路Lv1】-- 入门二级
【C++打怪之路Lv1】-- 入门二级
23 0
|
1月前
|
自然语言处理 编译器 C语言
【C++打怪之路Lv1】-- C++开篇(入门)
【C++打怪之路Lv1】-- C++开篇(入门)
26 0
|
1月前
|
存储 编译器 C语言
深入计算机语言之C++:类与对象(上)
深入计算机语言之C++:类与对象(上)
|
1月前
|
存储 分布式计算 编译器
深入计算机语言之C++:C到C++的过度-2
深入计算机语言之C++:C到C++的过度-2
|
1月前
|
编译器 Linux C语言
深入计算机语言之C++:C到C++的过度-1
深入计算机语言之C++:C到C++的过度-1
|
1月前
|
分布式计算 Java 编译器
【C++入门(下)】—— 我与C++的不解之缘(二)
【C++入门(下)】—— 我与C++的不解之缘(二)
|
10天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
39 4
下一篇
无影云桌面