“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++入门
c++作为面向对象的语言与c的简单区别:c语言作为面向过程的语言还是跟c++有很大的区别的,比如说一个简单的五子棋的实现对于c语言面向过程的设计思路是首先分析解决这个问题的步骤:(1)开始游戏(2)黑子先走(3)绘制画面(4)判断输赢(5)轮到白子(6)绘制画面(7)判断输赢(8)返回步骤(2) (9)输出最后结果。但对于c++就不一样了,在下五子棋的例子中,用面向对象的方法来解决的话,首先将整个五子棋游戏分为三个对象:(1)黑白双方,这两方的行为是一样的。(2)棋盘系统,负责绘制画面。
21 0
|
3月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
112 12
|
4月前
|
存储 算法 安全
企业员工数据泄露防范策略:基于 C++ 语言的布隆过滤器算法剖析[如何防止员工泄密]
企业运营过程中,防范员工泄密是信息安全领域的核心议题。员工泄密可能致使企业核心数据、商业机密等关键资产的流失,进而给企业造成严重损失。为应对这一挑战,借助恰当的数据结构与算法成为强化信息防护的有效路径。本文专注于 C++ 语言中的布隆过滤器算法,深入探究其在防范员工泄密场景中的应用。
75 8
|
4月前
|
存储 分布式计算 编译器
C++入门基础2
本内容主要讲解C++中的引用、inline函数和nullptr。引用是变量的别名,与原变量共享内存,定义时需初始化且不可更改指向对象,适用于传参和返回值以提高效率;const引用可增强代码灵活性。Inline函数通过展开提高效率,但是否展开由编译器决定,不建议分离声明与定义。Nullptr用于指针赋空,取代C语言中的NULL。最后鼓励持续学习,精进技能,提升竞争力。
|
4月前
|
存储 负载均衡 算法
基于 C++ 语言的迪杰斯特拉算法在局域网计算机管理中的应用剖析
在局域网计算机管理中,迪杰斯特拉算法用于优化网络路径、分配资源和定位故障节点,确保高效稳定的网络环境。该算法通过计算最短路径,提升数据传输速率与稳定性,实现负载均衡并快速排除故障。C++代码示例展示了其在网络模拟中的应用,为企业信息化建设提供有力支持。
115 15
|
5月前
|
存储 缓存 C++
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
C++ 标准模板库(STL)提供了一组功能强大的容器类,用于存储和操作数据集合。不同的容器具有独特的特性和应用场景,因此选择合适的容器对于程序的性能和代码的可读性至关重要。对于刚接触 C++ 的开发者来说,了解这些容器的基础知识以及它们的特点是迈向高效编程的重要一步。本文将详细介绍 C++ 常用的容器,包括序列容器(`std::vector`、`std::array`、`std::list`、`std::deque`)、关联容器(`std::set`、`std::map`)和无序容器(`std::unordered_set`、`std::unordered_map`),全面解析它们的特点、用法
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
|
8月前
|
安全 编译器 C++
【C++11】新特性
`C++11`是2011年发布的`C++`重要版本,引入了约140个新特性和600个缺陷修复。其中,列表初始化(List Initialization)提供了一种更统一、更灵活和更安全的初始化方式,支持内置类型和满足特定条件的自定义类型。此外,`C++11`还引入了`auto`关键字用于自动类型推导,简化了复杂类型的声明,提高了代码的可读性和可维护性。`decltype`则用于根据表达式推导类型,增强了编译时类型检查的能力,特别适用于模板和泛型编程。
80 2
|
9月前
|
C++
C++ 20新特性之结构化绑定
在C++ 20出现之前,当我们需要访问一个结构体或类的多个成员时,通常使用.或->操作符。对于复杂的数据结构,这种访问方式往往会显得冗长,也难以理解。C++ 20中引入的结构化绑定允许我们直接从一个聚合类型(比如:tuple、struct、class等)中提取出多个成员,并为它们分别命名。这一特性大大简化了对复杂数据结构的访问方式,使代码更加清晰、易读。
109 0
|
9月前
|
存储 安全 编译器
【C++打怪之路Lv1】-- 入门二级
【C++打怪之路Lv1】-- 入门二级
78 0