【C++】打开C++的大门

前言

C++是在C语言的基础上容纳进去了面向对象编程的思想并增加了许多有用的库以及编程范式等所以C++兼容了C的绝大部分特性(约99%)像指针、数组等等东西在C++中都是可以用的熟悉C语言之后对C++学习有一定的帮助本博客主要目标:

  1. 补充C语言语法的不足以及C++是如何对C语言设计不合理的地方进行优化的比如:作用域方面IO方面、函数方面、指针方面、宏方面等等。
  2. 为后续类和对象学习打基础。

1.什么是C++

C语言十结构化和模块化的语言适合处理较小规模的程序。对于复杂的问题规模加大的程序需要高度的抽象和建模时C语言则不合适为了解决软件危机计算机界提出了OPP:面向对象思想支持面向对象的程序设计语言应用而生。

C++是在C语言的基础上引入并扩充了面向对象的概念发明的一种新的程序设计语言。为了表达该语言与C语言的渊源命名为C++。

因此:C++既可以进行C语言的过程化程序设计又可以进行以抽象数据类型为特点的基于对象的程序设计还可以进行面向对象的程序设计

2.C++的发展史

1979年贝尔实验室试图分析unix内核的时候试图将内核模块化于是在C语言的基础上进行扩展增加了类的机制完成了一个可以运行的预处理程序称之为c with classes

语言的发展和打怪升级一样也是逐步递减由浅入深的过程下面是C++的历史版本。

阶段内容
C with classes类及派生类、公有和私有成员、类的构造和析构、友元、内联函数、赋值运算符 重载等
C++1.0添加虚函数概念函数和运算符重载引用、常量等
C++2.0更加完善支持面向对象新增保护成员、多重继承、对象的初始化、抽象类、静 态成员以及const成员函数
C++3.0进一步完善引入模板解决多重继承产生的二义性问题和相应构造和析构的处 理
C++98C++标准第一个版本绝大多数编译器都支持得到了国际标准化组织(ISO)和美 国标准化协会认可以模板方式重写C++标准库引入了STL(标准模板库)
C++03C++标准第二个版本语言特性无大改变主要:修订错误、减少多异性
C++05C++标准委员会发布了一份计数报告(Technical ReportTR1)正式更名 C++0x即:计划在本世纪第一个10年的某个时间发布
C++11增加了许多特性使得C++更像一种新语言比如:正则表达式、基于范围for循 环、auto关键字、新容器、列表初始化、标准线程库等
C++14对C++11的扩展主要是修复C++11中漏洞以及改进比如:泛型的lambda表 达式auto的返回值类型推导二进制字面常量等
C++17在C++11上做了一些小幅改进增加了19个新特性比如:static_assert()的文 本信息可选Fold表达式用于可变的模板if和switch语句中的初始化器等
C++20自C++11以来最大的发行版引入了许多新的特性比如:模块(Modules)、协 程(Coroutines)、范围(Ranges)、概念(Constraints)等重大特性还有对已有 特性的更新:比如Lambda支持模板、范围for支持初始化等
C++23制定ing

C++还在不断的向后发展。但是:现在公司主流使用还是C++98和C++11所有大家不用追求最新重点将C++98和C++11掌握好等工作后随着对C++理解不断加深有时间可以去琢磨下更新的特性。


小知识:C++11是大佬在使用C的时候觉的不顺手于是他顺手一改就成了C++。

3.C++关键字(C++98)

1998年确定的C++的第一个标准版本绝大多数编译器都支持得到了国际标准化组织(ISO)和美 国标准化协会认可以模板方式重写C++标准库引入了STL(标准模板库)

C++总计63个关键字C语言32个关键字

(下面我们只是看一下这些关键字不对关键字进行具体的讲解。这些关键字需要对应具体的知识点讲解该篇博客会讲解部分剩余的会在其他博客中讲解)

asmdoifreturntrycontinue
autodoubleinlineshorttypedeffor
booldynamic_castintsignedtypeidpublic
breakelselongsizeoftypenamethrow
caseenummutablestaticunionwchar_t
catchexplicitnamespacestatic_castunsigneddefault
charexportnewstructusingfriend
classexternoperatorswitchvirtualregister
constfalseprivatetemplatevoidtrue
const_castfloatprotectedthisvolatilewhile
deletegotoreinterpret_cast

4.命名空间

4.1命名冲突

在C/C++中变量、函数和后面要学到的类都是大量存在的这些变量、函数和类的名称都存在于全局作用域中可能会导致很多名字上的冲突。

  1. 我们自己定义的变量、函数可能跟库里面重名冲突

    #include<stdio.h>
    #include<stdlib.h>
    
    int rand = 0;
    
    int main()
    {
    	printf("%d\n", rand);
    
    	return 0;
    }
    

    如上代码在stdlib.h库中包含了rand函数而我们又定义了一个rand全局变量编译器在运行这段代码时无法判断我们要输出的是库中定义的函数的地址还是我们定义全局变量造成了命名冲突出现如下错误提示。

    error C2365: “rand”: 重定义;以前的定义是“函数”

  2. 进入公司项目组以后做的项目比较大。多人协作两个同事写的代码中命名冲突。

    在公司做大型的项目时是多人协作各写各的代码到最后整合有很大机率出现两个人使用相同的名字去命名一个变量或函数。编译器也无法判断到底调用的是那个。

C语言是没有很好的办法解决这个问题。

而C++提出一个新语法命名空间很好的解决了这个问题。

4.2命名空间定义

定义命名空间需要使用到namespace关键字后面跟命名空间的名字然后**接一对{}**即可{}中即为命名空间的成员。

#include<stdio.h>
#include<stdlib.h>

//YPrivate是命名空间的名字可以根据自己的要求随意起名
//不过在一般开发中一般是用项目名字做命名空间名。
namespace YPrivate   
{
	int rand = 0;
}

int main()
{
	printf("%d\n", rand);

	return 0;
}

在这里插入图片描述

这样我们将我们定义的全局变量放到自己定义的命名空间中在运行程序编译器默认查找是先去局部查找局部没有rand这时就去全局查找此时的头文件在预处理时就会被展开展开后就会找到rand函数编译器就会将输出的rand看作是库中的函数从而输出它的函数地址。(至于如何使用命名空间中的变量或函数在下文会讲到)

这里我们还要注意命名空间的几个特性:

  1. 命名空间中可以定义变量/函数/类型(结构体)

    namespace YPrivate
    {
    	int rand = 0;          //全局变量
    
    	int Add(int x, int y)  //函数
    	{
    		return x + y;
    	}
    	struct A               //结构体自定义类型
    	{
    		struct A* next;
    		int x;
    		int y;
    	};
    }
    
    int main()
    {
    	printf("%d\n", rand);
    
    	return 0;
    }
    
  2. 命名空间可以嵌套使用可以在一个命名空间a内嵌套另一个命名空间b在命名空间b中嵌套其他命名空间这个根据我们实际的需求判断。

    namespace YPrivate1
    {
    	int c;
    	int Sub(int x, int y)
    	{
    		return x + y;
    	}
    	namespace YPrivate2
    	{
    		int d;
    		int sub(int x, int y)
    		{
    			return x - y;
    		}
    	}
    }
    
    int main()
    {
    	printf("%d\n", rand);
    
    	return 0;
    }
    
  3. 同一个工程中允许存在多个相同名称的命名空间编译器最后会合成同一个命名空间中。
    在这里插入图片描述
    如图编译器在执行时会将同一个项目下两个名字相同的命名空间合为一个。

  4. 一个命名空间就定义了一个新的作用域命名空间中的所有内容都局限于该命名空间中想要使用需用到特殊的方法(下文会讲)。

4.3命名空间使用

我们该如何使用命名空间呢?

像下面这样直接使用命名空间中的变量和函数明显是错误的若是可以直接调用那编译器也可以产看命名空间内的变量和函数是否命名正确命名空间也就失去了价值。

namespace YPrivate
{
	int a = 0;
    int b = 2;
    int Add(int x,int y)
    {
        return x + y;
    }
    struct A
	{
		struct A* next;
		int x;
		int y;
	};
}

namespace YPrivate2
{
	int c = 0;
}

int main()
{
	printf("%d\n", a);

	return 0;
}

error C2065: “a”: 未声明的标识符

命名空间的三种使用方法:

  1. 加命名空间名称及作用域限定符

    指定作用域最好做到命名隔离但是它使用起来不方便。

    • 下午所有代码中的命名空间同一使用上午定义的YPrivate。

    • 使用::符号表示变量a属于命名空间YPrivate(注:::符号将伴随着C++的学习在很多地方都会出现主要功能为:符号右边的属于符号左边

    int main()
    {
        printf("%d\n", YPrivate::a);
        printf("%d\n", YPrivate::Add(1,2));
        struct YPrivate::A node;
        return 0;
    }
    

    在这里插入图片描述

    这也就体现了命名空间的好处在需要的时候指定使用命名空间中的数据不需要时正常使用其他数据(不同的空间不同的指定不会产生冲突)

    int main()
    {
    	printf("%d\n", YPrivate::a);
    	printf("%d\n", YPrivate2::c);
    	struct YPrivate::A node;
    	return 0;
    }
    

    在这里插入图片描述

    命名空间中的变量还是全局变量可以在如何函数内使用。只是在命名空间中定义使没有使用 :: 符号调用前编译器无法找到防止发生命名冲突。

    相当于哈利波特披着隐身斗篷哈利波特就在哪里什么都没有变就是看不到。

    void test()
    {
    	YPrivate::a = 10;
    }
    
    int main()
    {
    	test();
    	printf("%d\n", YPrivate::a);
    	printf("%d\n", YPrivate::Add(1,2));
    
    	return 0;
    }
    

    在这里插入图片描述

    拓展::: 符号在C语言中同样使用过。

    int a = 0;
    
    int main()
    {
    	int a = 10;
    	printf("%d\n", a);
    
    	return 0;
    }
    

    根据就近原则此时输出的a为局部变量。

    在这里插入图片描述

    要想在这种情况下使用全局变量则需要用到 :: 符号

    int a = 0;
    
    int main()
    {
    	int a = 10;
    	printf("%d\n", a);
    	printf("%d\n", ::a);  //此时::符号前为空白表示调用全局域
    
    	return 0;
    }
    

    在这里插入图片描述

  2. 使用using将命名空间中某个成员引入

    用于声明命名空间中经常用到的变量、函数或结构体

    using YPrivate::b;  //相当于声明告诉编译器变量b是哪里来的
    int main()
    {
        printf("%d\n", YPrivate::a);
        printf("%d\n", YPrivate::b);
        return 0;
    }
    

    在这里插入图片描述

  3. 使用using namespace 命名空间名称引入

    将命名空间中的数据全部释放用起来方便但隔离就失效了。

    using namespace YPrivate;
    int main()
    {
        printf("%d\n", a);
    	printf("%d\n", b);
    	printf("%d\n", Add(1, 2));
    
    	return 0;
    }
    

    在这里插入图片描述

    • 这种方法是不好的在工作编写项目不建议大家这样使用全部释放使用用的时候要非常小心。

    那它存在的意义是什么体现在下午。

    在我们日常学习中大概率见过C++程序的开头这样写。

    #include<iostream>
    using namespace std;
    

    这是因为C++把官方库的实现定义到了命名空间——std

    将库放在std中防止了命名冲突。

    一般在我们平常学习的时候我们是直接将std释放直接using namespace std即可这样就很方便这样我们就可以简单的操作官方库中的函数而不用在使用前增加std::

    std::cout << "helloc world!" << std::endl;
    
    • 平常学习我们可以这样写但在编写项目的时候不建议这样写项目中代码较多规模大防止std全部释放造成命名冲突应该使用上面的两种方法。

    至于iostream这是C++库里的头文件包含C++的输入输出

    C语言使用stdio.h头文件包含输入输出。

接下来我们就来看一下C++的输入输出

5.输入输出

新生婴儿会以自己独特的方式向这个世界打招呼C++刚出来后也算一个新事物那C++是如何向这个美好的世界来问候的?
在这里插入图片描述

#include<iostream>
using namespace std;

int main()
{
	cout << "hello world!!!" << endl;

	return 0;
}

说明:

  1. 使用cout标准输出对象(控制台黑框框就是)和cin标准输入对象(键盘)时必须包含头文件以及按命名空间使用方法使用std。

  2. cout和cin是全局的流对象endl是特殊的C++符号表示换行输出他们都包含在头文件中。

  3. <<是流插入运算符>>是流提取运算符。

  4. 使用C++输入输出更方便不需要像printf/scanf输入输出那样需要手动控制格式。C++的输入输出可以自动识别变量类型。

    #include<iostream>
    using namespace std;
    
    int main()
    {
    	cout << "hello world!!!" << endl;
    	int a = 10;
    	double b = 1.1;
    	char c = 'e';
    	cout << a << " " << b  << " " << c << endl;
        cin >> a >> b >> c;
    	cout << a << " " << b << " " << c << endl;
    
    	printf("%d %lf %c", a, b, c);
    	//我使用的是VS2019有些平台下printf所对应的头文件以及间接包含了不要在使用#include<stdio.h>
    	return 0;
    }
    

    在这里插入图片描述

  5. 实际上cout和cin分别是ostream和istream类型的对象>>和<<也涉及运算符重载等知识我们这里只是简单学习他们的使用。

  6. 关于cout和cin还有很多更复杂的用法比如控制浮点数输出精度控制整形输出格式等等但这些使用C++实现有点麻烦因为C++兼容C语法我们可以直接使用printf和scanf来完成这些操作这里就不展开学习了。

小技巧:

在这里插入图片描述
注意:

早期标准库将所有功能在全局域中实现声明在.h后缀的头文件中使用时只需包含对应头文件即可后来将其实现在std命名空间下。

为了和C头文件区分也为了正确使用命名空间规定C++头文件不带.h;旧编译器(vs 6.0)中还支持<iostream.h>格式后续编译器已不支持因此推荐使用+std的方式。

6.缺省参数

6.1缺省参数的概念

缺省参数是声明或定义函数时为函数的参数指点一个缺省值。在调用该函数时如果没有指定实参则采用该形参的缺省值否则使用指定的实参。

void test(int a = 0)
{
	cout << a << endl;
}

int main()
{
	test(10); // 传参使用传递的值
	test();   // 没有传参使用指定的实参

	return 0;
}

在这里插入图片描述

6.2缺省参数分类

  1. 全缺省参数

    void test(int a = 10, int b = 20, int c = 30)
    {
    	cout << "a=" << a << ' ' << "b=" << b << ' ' << "c=" << c << endl;
    }
    
    • 所有参数都被赋值
  2. 半缺省参数

    void test(int a, int b, int c = 30)
    {
    	cout << "a=" << a << ' ' << "b=" << b << ' ' << "c=" << c << endl;
    }
    
    • 半缺省参数必须从左到右依次给出不能间隔着给。

注意:

  1. 缺省参数不能在函数声明和定义中同时出现

    void test(int a = 10, int b = 20, int c = 30);//声明
    
    void test(int a = 10, int b = 20, int c = 30)//定义
    {}
    
    • 如上如果声明与定义同时缺省参数恰巧两位置提供的值不同或两位置缺省的参数不同那编译器就无法确定到底该用那个缺省值。
  2. 缺省参数必须是常量或全局变量

  3. C语言不支持(编译器不支持)

扩展:

  • C++11增加包装器可以支持( 2);太过复杂现在不用管。

    void test(int a = 10, int b, int c)
    {}
    
  • 缺省在后面的构造函数那非常有用。

7.函数重载

自然语言中一个词可以有多重含义人们可以通过上下文来判断该词真实的含义即该词被重载了。
比如:以前有一个笑话国有两个体育项目大家根本不用看也不用担心。一个是乒乓球一个是男足。前者是“谁也赢不了”后者是"谁也赢不了"。

7.1函数重载概念

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

  1. 参数类型不同

    //1.参数类型不同
    int Add(int left, int right)
    {
    	cout << "int Add(int left,int right)" << endl;
    }
    
    double Add(double left, double right)
    {
    	cout << "double Add(double left, double right)" << endl;
    }
    
    int main()
    {
    	Add(10, 20);
    	Add(10.1, 20.2);
    	
        return 0;
    }
    
  2. 参数个数不同

    //2.参数个数不同
    void f()
    {
    	cout << "void f()" << endl;
    }
    
    void f(int a)
    {
    	cout << "void f(int a, int b)" << endl;
    }
    
    int main()
    {
        f();
    	f(10);
        
        return 0;
    }
    
  3. 参数类型顺序不同

    //3.参数类型顺序不同
    void f(int a, char b)
    {
    	cout << "void f(int a, char b)" << endl;
    }
    
    void f(char b, int a)
    {
    	cout << "void f(char b, int a)" << endl;
    }
    
    int main()
    {
        f(10, 'a');
    	f('a', 10);
    }
    

注意:

  • 函数名相同一个无参数一个有一个缺省参数可以构成重载如下但编译器无法判断调用的是那个函数存在歧义编译器会报错
void f()
{
	cout << "void f()" << endl;
}

void f(int a=4)
{
	cout << "void f(int a)" << endl;
}
  • 缺省值不同不能构成重载

在函数缺省的情况下使用函数重载需注意放置歧义发生。

7.2C++函数重载的原理——名字修改

为什么C++支持重载而C语言不支持重载?

想要解释这个问题我们就需要在Linux系统下看一看C和C++的翻译环境。

C和C++的编译过程是相同的只是语法规则不同所以:

C/C++中一个程序要运行起来都需要经历以下几个阶段:预处理、编译、汇编、链接。

在这里插入图片描述

  • 最开始文件:func.h、func.c、test.c

预处理:展开头文件、宏替换、条件编译、去注释

  • 形成文件:func.i、test.i

编译:语法分析、词法分析、语义分析、符号汇总(检查语法生产汇编代码)

  • 形成文件:func.s、test.s

汇编:将汇编指令变为二进制机器码、形成符号表

  • 形成文件:func.o、test.o

链接:合成段表、符号表的合并和重定位(C++支持重载的关键点就在链接)

  • 形成可执行文件:a.out/xxxx.exe
  1. 实际项目中通常采用上述多个头文件和多个源文件构成通过C语言阶段学习的编译链接我们可以知道【test.c调用f()和f(10)函数时】编译后链接前test.o中没有这两个函数的地址。

  2. 编译器通常在汇编阶段形成每个源文件的符号表符号表中有源文件内对应函数的函数名和地址若是该源中用函数的定义使用有效地址否则使用无效地址。

  3. 在链接阶段符号表合并和重定位根据对应的函数名来识别和查找函数。

    在这里插入图片描述

  4. C语言不支持函数重载因为编译的时候两个重载函数函数名相同如上图在func.o符号表中存在歧义和冲突其次链接的时候也存在歧义和冲突因为他们都是直接使用函数名去标识和查找而重载函数函数名相同。

    • 采用C语言编译器在Linux下查看符号表中函数名

      在这里插入图片描述

    • 结论:在Linux下采用gcc编译完成后函数名字的修饰没有发生改变。

  5. C++的目标文件符号表中不是直接用函数名来标识和查找函数

    1. 是通过函数名修饰规则——【Linux为:_Z+函数长度+函数名+类型首字母】后来标识和查找函数的但是这个修饰规则不同的编译器下面不同
    2. 有了函数名修饰规则只要满足函数重载语法func.o 符号表里面重载的函数就不存在二义性和冲突了。
    3. 链接的时候test.o的main的函数里面去调用两个重载的函数通过函数修改规则名查找地址的时候也是明确的。
    • 采用C++编译器编译后的结果

      在这里插入图片描述

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

      注意:

      • Vs是根据文件后缀是去调用对应的编译器。.c就是c编译器.cpp就是C++编译器

      • Linux不用文件后缀区分gcc编译就是cg++就是cpp(最好自己使用好对应的后缀方便查看)

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

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

拓展:Windows下名字修饰规则

函数签名修饰后名称
int func(int)?func@@YAHH@Z
float func(float)?func@@YAMM@Z
int C::func(int)?func@C@@AAEHH@Z
int C::C2::func(int)?func@C2@C@@AAEHH@Z
int N::func(int)?func@N@@YAHH@Z
int N::C::func(int)?dunc@C@N@@AAEHH@Z

我们以int N::C::func(int)这个函数签名来猜测Visual C++的名称修饰规则(只需了解)。

修饰后名字由“?”开头接着是函数名由“@”符号结尾的函数名:后面跟着由“@”结尾的类名“C”和名称空间“N”再一个“@”表示函数的名称空间结束:第一个“A”表示函数调用类型为“__cdecl”(函数调用类型是其他知识点感兴趣可以去搜一下)接着是函数的参数类型及返回值由“@”结束最后由“Z”结尾。可以看到函数名、函数参数的类型和名称空间都被加入了修饰后名称这样编译器和链接器就可以区别同名但不同参数类型或名字空间的函数而不会导致link的时候函数多重定义。

对比Linux发现windows下vs编译器对函数名字修饰规则相对复杂难懂但道理都是类似的我们就不做细致研究了。

8.引用

8.1引用的概念

引用不是新定义一个变量而是给已存在变量取一个别名编译器不会为引用变量开辟内存空间它和它引用的变量共用同一块内存空间。

引用的出现为了解决指针的不足:复杂。

比如:邻居王叔我们可以叫它王先生、王哥或王**它就是那么一个人可以有多种不同的称呼放在代码中同样是一个变量我们也可以为它起不同的名称。

格式: 类型& 引用变量名(对象名) = 引用实体;

void Test()
{
    int a = 10;
    int& ra = a;//定义引用类型
    
    printf("%p\n", &a);
    printf("%p\n", &ra);
}

在这里插入图片描述

  • 引用类型必须和引用实体的类型相同。

注意:

int a = 10int& ra = a;    //只是引用
int* pra = &a;  //这是取地址

8.2引用特性

  1. 引用在定义时必须初始化
  2. 一个变量可以有多个引用
  3. 引用一旦引用一个实体再不能引用其他实体。
int main()
{
	int a = 10;
	//int& ra;   //没有初始化该条语句编译时会报错
	int& ra = a;
	int& rra = a;
	printf("%p %p %p %d\n", &a, &ra, &rra, ra);

	int b = 20;
	ra = b;//b的值赋值给a引用后无法在修改引用的实体
	printf("%p %p %p %p %d %d\n", &a, &ra, &rra,&b,ra,a);

	return 0;
}

在这里插入图片描述

8.3常引用

常引用即为在变量类型前修饰const使其不能修改这就使得被修饰的变量只能读没办法写。造成了被const修饰的变量无法被未被修饰的引用接收因为未修饰const的引用可以修改自身的值而被修饰的不能看如下例子:

void TestConstRef()
{
    //权限的放大
	const int a = 10;
	int& ra = a;  //该语句编译时会出错a为常量不能修改ra的类型为int可以修改此时无法通过
    
    int& b = 10;  //该语句编译时会出错10为常量不能修改b为int类型可以修改产生矛盾
	const int& b = 10;
    
    //权限不变
    const int& ra = a;//权限不变是可以的
    
    //权限缩小
    int e = 30;
    const int& re = e;//权限缩小也是可以的
}

对于常引用它的权限就是读、写权限只能被缩小不能被放大

a是常量无法被ra接收如果可以接收ra就可以修改a将原本只能写的权限放大即可读也可写。这产生本质上的错误故权限不能放大


b易是同理在没有被const修饰前既能读也能写而常量10只能读不能接收10。
b被const修饰后和常量的权限相同可以接收常量


e的没有被修饰可读可写当它被re接收后re可以读出e这对e没有影响re接收的只是e的部分权限故权限可以缩小。

1.应用:

void f(const int& x)
{
	cout << x << endl;
}

观察如上代码当我们学习的更多要去使用x接收一个大的对象时我们就要尽量使用引用减少拷贝如果要求在函数f()中不改变该对象就建议尽量用const引用传参从而不会改变x的值。

2.引用对于缺省参数

void Add(const int& a=10)
{}

像这样的缺省参数或是类似传递常量的函数在使用引用时必须使用const否则无法无法接收编译器报错。

3.不同类型的引用

int main()
{
	double d = 12.34;
    int i = d;
	//int& rd = d;  //该语句编译时会出错
	const int& rd = d;
	
    return 0;
}

为什么int& rd = d;会出错而int i = d;和const int& rd = d;却可以

在这里插入图片描述

  • 一个是被产生的临时变量赋值一个是引用产生的临时变量(共用同一块地址)
	printf("double:%p\n", &d);
	printf("const int:%p\n", &rd);

在这里插入图片描述

8.4使用场景

  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;
    }
    
    int main()
    {
    	int a = 10;
    	int b = 20;
    	Swap(a,b);
    	cout << "a=" << a << "" << "b=" << b << endl;
        //Swap1(&a,&b);
        
    	return 0;
    }
    

    在这里插入图片描述

    • 两个Swap参数不同构成函数重载。

    • 如果再加一个传值的Swap函数(如下)也可形成函数重载但在调用时会产生歧义编译器报错

      void Swap(int a,int b);
      
  2. 做返回值

    我们先来看下面图片中的代码:

    在这里插入图片描述

    根据上述代码可知编译器不会直接将Add函数内c的值直接返回而是通过临时变量。

    临时变量在哪呢?

    1. 返回值小(4~8个字节):一般是寄存器充当临时变量
    2. 返沪值较大:临时变量放在调用Add函数的栈帧中

    总之:所有的传值返回都会生产一个拷贝。

    我们再来看看下面使用引用返回:

    int& Add(int a, int b)
    {
    	int c = a + b;
    	return c;
    }
    int main()
    {
    	int& ret = Add(1, 2);
    	cout << "Add(1, 2) is :" << ret << endl;
    	Add(3, 4);
    	cout << "Add(3, 4) is :" << ret << endl;
    	return 0;
    }
    

    当前代码的问题:

    1. 存在非法访问因为Add(1,2)的返回值是c的引用所以Add栈帧销毁了以后回去访问c位置空间
    2. 如果Add函数栈帧销毁那么取c值得时候取到就是随机值给ret就是随机值取决于编译器是否清理Add函数被销毁的空间。(vs下销毁栈帧不会清理数据所以可以得到结果)

    原因:

    • 之所以得到这样的结果因为返回的是c这一地址的引用由ret接收此时ret的地址就是c的地址而c所在函数是被销毁并没有被清理数据依然存在。调用两次Add函数函数栈帧没有改变c的地址也没有改变每次都是c变量这一地址所存储的值发生变化所以ret随之发生变化。
    • 所以我们不要随意用引用返回可能会造成非法的访问。

    总结:

    如果函数返回时出了函数作用域如果返回对象还在(还没还给系统)则可以使用引用返回如果已经还给系统了则必须使用传值返回。

8.5传值、传引用效率比较

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

struct A {
	int a[10000];
};

void TestFunc1(A a){}

void TestFunc2(A& a){}

void TestRefAndValue()
{
	A a;
	//以值作为函数参数
	size_t begin1 = clock();
	for (int i = 0; i < 10000; i++)
	{
		TestFunc1(a);
	}
	size_t end1 = clock();

	//引用作为函数参数
	size_t begin2 = clock();
	for (int 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;
}

在这里插入图片描述

  • 传值是拷贝一份传递
  • 传引用是时形参变为实参的别名实参与形参的地址相同
  • 相比之下直接使用原有地址的数据要比重新拷贝更快

值和引用的作为返回值类型的性能比较

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

在这里插入图片描述

  • 值返回返回的是拷贝的一份零时变量的值
  • 引用返回返回的是所存数据地址的别名
  • 相比之下引用返回更有优势

总结:

引用的作用主要体现在传参和传返回值:

  1. 引用传参和传返回值有些场景下面可以提高性能。(大对象、深拷贝对象)
  2. 引用传参和传返回值输出型参数和输出型返回值。通俗点讲有些场景下面形参的改变可以改变实参。有些场景下面引用返回可以改变返回对象。

8.6引用的作用

  1. 提高效率

  2. 修改返回变量

    #define N 10
    
    int& At(int i)
    {
    	static int a[N];//保证出了作用域对象还在
    	return a[i];//返回a[i]的引用
    }
    
    int main()
    {
    	for (int i = 0; i < N; i++)
    	{
    		At(i) = 10 + i;//返回值是一个变量可以直接修改
    	}
    	for (int i = 0; i < N; i++)
    	{
    		cout << At(i) << " ";
    	}
    
    	return 0;
    }
    

    在这里插入图片描述

8.7引用和指针的区别

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

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. 引用比指针使用起来相对安全。

总结: 指针使用起来更复杂更容易出错。

  • 指针和引用的区别面试经常考察按自己的理解就可以

9.内联函数

9.1问题

在我们学习和工作中总要频繁的去调用一些小函数如下面的swap函数

int Swap(int& a1, int& a2)
{
	int temp = a1;
	a1 = a2;
	a2 = temp;
}

每次函数的调用都伴随着建立栈帧栈帧中要保留一些寄存器结束后又要恢复可以看到这些都是对时间和空间的消耗。

那么对于这些功能简单、调用次数多的小函数有什么办法可以优化一下吗?

C语言中提供了宏来解决这个问题

//方法1
#define SWAP(a,b) \
a= a^b;\
b= a^b;\
a= a^b;

//方法2
#define SWAP(a,b) \
a = a + b; \
b = a - b; \
a = a - b;
  • 不在使用函数而是在需要的位置直接替换为宏的表达式。
  • 但宏的实现有时又很复杂所以C++提供了内联函数来解决这个问题

9.2概念:

以inline修饰的函数叫做内联函数编译时C++编译器会在调用内联函数的地方展开(用函数体替换函数的调用)没有函数调用建立栈帧的开销内联函数提升程序运行的效率。

如下图的Swap即在开辟函数栈帧。
在这里插入图片描述

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

我们在debug

查看方法:

  1. 在release模式下查看编译器生成的汇编代码中是否存在call Swap
  2. 在debug模式下需要对编译器进行设置否则不会展开(因为debug模式下编译器默认不会对代码进行优化以下给出vs2019的设置方式)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
依次点击应用确认后

设置好后在来看一下函数是否展开是否有call Swap

在这里插入图片描述

9.3特性

  1. inline是一种以空间换时间(在调用位置替换不进行函数的栈帧)的做法如果编译器将函数当成内联函数处理在编译阶段会用函数体替换函数调用(将函数的实现换到了call的位置)缺陷:可能使目标文件变大优势:少了调用开销提高程序运行效率。

  2. inline对于编译器而言只是一个建议不同编译器关于inline实现机制可能不同一般建议:将函数规模小(即函数不是很长具体没有准确的说法取决于编译器内部实现)不使递归、且频繁调用的函数采用inline修饰否则编译器会忽略inline特性。下面为《C++prime》第五版关于inline的建议:

    • 内联说明只是向编译器发出的一个请求编译器可以选择忽略这个请求。

    一般来说内联机制用于优化规模较小、流程直接、频繁调用的函数。很多编译器都不支持内联递归函数而且一个75行的函数也不大可能在调用点内联地展开。

  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 __cdeclf(int)" (?f@@YAXH@Z)该符号在函数 _main 中被引用
    
    • F.cpp符号表中不会生成f函数的地址因为内联函数不需要地址它在调用的地方就展开了。

相关【面试题】

宏的优缺点?

优点:

  1. 增强代码的复用性。
  2. 提高性能。

缺点:

  1. 不方便调试宏。(因为预编译阶段进行了替换)
  2. 导致代码可读性查可维护性差容易误用。
  3. 没有类型安全的检查。

C++有那些技术替代宏?

  1. 常量定义 换用 const enum
  2. 短小函数定义 换用内联函数

10.auto关键字(C++11)

10.1类型别名思考

随着程序越来越复杂程序中用到的类型也越来越复杂经常体现在:

  1. 类型难于拼写。
  2. 含义不明确导致容易出错。

如下:

#include<iostream>
#include<map>
#include<string>
using namespace std;

int main()
{
	map<string, string> m = { {"lisi","李四"},{"zhangsan","张三"} };
	map<string, string>::iterator it = m.begin();

	return 0;
}

map<string, string>::iterator是一个类型但该类型太长了我们在写的时候并不方便。

在C语言中我们可以通过typedef来给类型取别名如下:

#include<map>
#include<string>
using namespace std;

typedef map<string, string> Map;

int main()
{
	Map m = { {"lisi","李四"},{"zhangsan","张三"} };
	Map::iterator it = m.begin();

	return 0;
}

使用typedef给类型取别名确实可以简化代码但是typedef也会自己的不足:

typedef char* pst;
int main()
{
	const pst p1; // 编译失败
	const pst* p2; // 编译成功
	return 0;
}

在这里插入图片描述

改为extern const pst p1;即可上面的外部的是指声明为extern的常量。

  • 在编译时常常需要把表达式的值赋值给变量这就要求在声明变量的时候清楚的知道表达式的类型。而有时候想要知道表达式的类型不是那么容易。

因此C++11给auto赋予了新的含义

10.2auto简介

在早期C/C++中auto的含义是:使用auto修饰的变量是具有自动存储器的局部变量但是一直没有人去使用它。像如下代码:

int main()
{
    auto int a = 0;
    
    return 0;
}

在早期使用auto关键字修饰变量a表示a是一个局部变量声明周期在main函数内可是不适应auto关键字修饰变量a还是一个局部变量它的生命周期没有改变所以auto关键字就失去了意义。

C++11中标准委员会赋予了auto全新的含义即:auto不再是一个存储类型的知识符而是作为一个新的类型指示符来指示编译器auto声明的变量必须由编译器在编译时期由等号右边的值推导而得

int main()
{
	int a = 0;

	//自动推导变量的类型
	auto c = &a;
	auto d = 'a';
	auto e = 10.11;
	map<string, string> m = { {"lisi","李四"},{"zhangsan","张三"} };
	auto m = m.begin();

	//typeid可以打印变量的类型
	cout << typeid(c).name() << endl;
	cout << typeid(d).name() << endl;
	cout << typeid(e).name() << endl;
	cout << typeid(m).name() << endl;

	//auto e;无法通过编译使用auto定义变量时必须对其进行初始化

	return 0;
}

在这里插入图片描述

注意: 使用auto定义变量时必须对其进行初始化在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明而是一个类型声明时的“占位符”编译器在编译期会将auto替换为变量实际的类型。

10.3auto的使用细则

  1. auto与指针和引用结合起来使用

    用auto声明指针类型时用auto和auto*没有任何区别但用auto声明引用类型时则必须加&

    int main()
    {
    	int x = 10;
    	auto a = &x;
    	auto* b = &x;
    	auto& c = x;
    
    	cout << typeid(a).name() << endl;
    	cout << typeid(b).name() << endl;
    	cout << typeid(c).name() << endl;
    
    	*a = 20;
    	cout << x << endl;
    	*b = 30;
    	cout << x << endl;
    	c = 40;
    	cout << x << endl;
    
    	return 0;
    }
    

    在这里插入图片描述

  2. 在同一行定义多个变量

    当在同一行声明多个变量时这些变量必须是相同的类型否则编译器将会报错因为编译器实际只对第一个类型进行推导然后用推导出来的类型定义其他变量。

    在这里插入图片描述

10.4auto不能推导的场景

  1. auto不能作为函数的参数

    //auto不能作为形参类型因为编译器无法对a的实际类型进行推导此处代码编译失败
    void Test(auto a)
    {}
    
  2. auto不能直接用来声明数组数组需要根据元素类型及个数来开辟空间而数组名代表指针因此auto无法推导

    void Test()
    {
        int a[] = {1,2,3};
        auto b[] = {4,5,6};//不能这样使用
    }
    

    在这里插入图片描述

  3. 为了避免与C++98中的auto发生混淆C++11只保留了auto作为类型指示符的用法。

  4. auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环还有lambda表达式等进行配合使用。

11.基于范围的for循环(C++11)

11.1范围for的语法

在C++98当中如果要遍历一个数组可以按照以下方式进行:

void Test()
{
	int arr[] = { 1,2,3,4,5,6,7 };
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	for (int* ptr = arr; ptr < arr + sizeof(arr)/sizeof(arr[0]); ptr++)
	{
		cout << *ptr << " ";
	}
	cout << endl;
}

在这里插入图片描述

对于一个有范围的集合而言由程序员来说明循环的范围式多余的有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ : ”分为两部分:第一部分是范围内用于迭代的变量第二部分则表示被迭代的范围。

void Test()
{
	int arr[] = { 1,2,3,4,5,6,7 };
	for (auto& e : arr)
	{
		cout << e << " ";
		e *= 2;
	}
	cout << endl;
	for (auto e : arr) // 自动依次取数组arr中的每个元素赋值给e
	{
		e *= 2;
		cout << e << " ";
	}
	cout << endl;
	for (auto e : arr)
	{
		cout << e << " ";
	}
	cout << endl;
}

注意: 与普通循环类似可以用continue来结束本次循环也可以用break来跳出整个循环

两种写法

  1. 不使用引用

    	for (auto e : arr) 
    	{
    		cout << e << " ";
    	}
    
    • 自动依次取数组arr中的每个元素赋值给ee得到的只是arr中的每个元素值无法数组进行修改

      auto e = arr;(arr依次循环一圈)

  2. 使用引用

    	for (auto& e : arr) 
    	{
    		e++;
    	}
    
    • 自动依次使数组arr中的每个元素的别名为e这样我们修改e就是修改数组中对应的值

      auto& e = arr;(arr依次循环一圈)

11.2范围for的使用条件

  1. for循环迭代的范围必须是确定的

    对于数组而言就是数组中第一个元素和最后一个元素的范围;对于类而言应该提供begin和end的方法begin和end就是for循环迭代的范围。

注意: 下面的代码就要问题因为for的范围不确定

void Test(int arr[])
{
    for(auto& e : arr)
    	cout << e << endl
}
  • 范围for中的arr必须是数组名数组名传参给函数是会变为指针。
  1. 迭代的对象要实现++和==的操作

12.指针空值nullptr(C++11)

12.1C++98中的指针空值

在良好的C/C++编程习惯中声明一个变量时最好给该变量一个合适的初始值否则可能会出现不可预料的错误比如未初始化的指针(造成野指针)。如果一个指针没有合法的指向我们基本都会按照如下方式对其进行初始化。

void TestPtr()
{
	int* ptr1 = NULL;
	int* ptr2 = 0;
}

NULL实际是一个在传统的C头文件<stddef.h>中可以看到如下代码:

#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif

可以看到对于c语言NULL可能被定义为字面常量0或者被定义为无类型指针(void*)的常量 。 但在C++中NULL代表的就是0。虽然0和((void*)0)在数值上表示相同但它们的类型不同一个是整形一个是指针。在C++中使用指针时需要的时指针类型这就导致NULL在C++中出现了如下问题NULL指针无法发挥它的作用。

void f(int)//当函数中不使用形参时可以直接写一个参数类型
{
	cout<<"f(int)"<<endl;
}
void f(int*)
{
	cout<<"f(int*)"<<endl;
}
int main()
{
    f(0);
    f(NULL);//此处NULL本意调用指针类型
    f((int*)NULL);
    return 0;
}

在这里插入图片描述

程序本意是想通过f(NULL)调用指针版本的f(int*)函数但是由于NULL被定义成0因此与程序的初衷相悖。

在C98中字面常量0既可以是一个整形数字也可以是无类型的指针(void*)常量但编译器默认情况下将其看成是一个整形常量如果要将其按照指针方式来使用必须对其进行强转(void*)0。

为了解决这个问题C++11提出了指针空值nullptr同时认为nullptr就是(void*)0

12.2C++11中的指针空值

指向如下代码:

void f(int)
{
    cout << "f(int)" << endl;
}
void f(int*)
{
    cout << "f(int*)" << endl;
}
int main()
{
    f(0);
    f(NULL);
    f(nullptr);
    return 0;
}

在这里插入图片描述

  • 使用C++11中的指针空值nullptr默认表示指针而非0.

注意:

  1. 在使用nullptr表示指针空值时不要包含头文件因为nullptr是C++11作为新关键字引入的。
  2. 在C++11中sizeof(nullptr)与sizeof((void*)0)所占的字节数相同。
  3. 为了提高代码的健壮性在后续表示指针空值时建议最好使用nullptr。
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: c++

“【C++】打开C++的大门” 的相关文章