【C++基础】一、C++基础入门(20000字掌握C++基础知识)


请添加图片描述

1.C++关键字

C++兼容C绝大多数的语法

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

asm do if return try continue
auto double inline short typedef for
bool dynamic_cast int signed typeid public
break else long sizeof typename throw
case enum mutable static union wchar_t
catch explicit namespace static_cast unsigned default
char export new struct using friend
class extern operator switch virtual register
const false private template void true
const_cast float protected this volatile while
delete goto reinterpret_cast

以上列出来的关键字不用刻意去记,只需要看一下,在后面的内容中会详细学习到的。

2.命名空间

再学习命名空间之前,我们来看一下以下C++代码,从这样一个小代码入手方便后面的学习。

#include<iostream>//io流,input/output
using namespace std;
int main()
{
    
    
	cout << "hello world" << endl;
	return 0;
}

关于上面的这段小代码,有以下几点需要我们注意

1)头文件:

C语言中我们使用头文件是用

#include<stdio.h>//头文件一般是以xx.h结尾的,h表示head(头)

而在C++中,使用头文件一般不再需要.h

#include<iostream>
#include<string>
#include<list>

此外,iostream实际上是input/output stream(输入/输出流)的缩写,一般我们需要使用到和输入输出相关的操作时,就需要包含这个头文件,这点和C语言中的stdio.h(standard input/output,标准输入输出)是一样的。

2)使用了命名空间,将名称为std这个命名空间全部展开了,关于命名空间的内容,后面内容会细讲。

3)使用了关键字cout,读作 see-out,是一个输出控制台,作用类似于printf,但实际又有很大区别,后面也会细讲。

4)流式符号 <<,表示将cout右边的内容流向cout(输出控制台)中,打印出来。

在遇到C语言和C++语法不同的情况下,我们要想弄明白为什么C++要这样设计时,需要知道这样一个原则:

C++每增加一个语法,都是为了解决在C语言中无法做到,或者是C虽然能做到但是很复杂,诸如此类的问题。

下面开始进入我们学习的正题。

2.1 什么是命名空间?

命名空间的关键字是namespace,含义就是这个关键字表面含义,用于管理我们的命名,比如说变量的名称,函数的名称,结构体或类的名称。

2.2 为什么需要命名空间?

在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。

简单来讲就是为了在名称相同时可以正常使用这些名称,才出现命名空间的。

2.3 如何定义命名空间?

命名空间的定义方式有三种,下面会详细介绍这三种方式。

总原则:定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对花括号{}即可,花括号{}内部即为命名空间的成员。

方式一:普通定义

//普通的命名空间
namespace point //point是命名空间的名称
{
    
    
	//用花括号{ }将命名空间的内容括起来
	//可以在花括号内部定义变量,函数和结构体
	int x;
	int y;
	int z;
	int Add(int x, int y)
	{
    
    
		return x + y;
	}
	struct line 
	{
    
    
		int begin_pos;
		int end_pos;
		int length;
	};
}

方式二:嵌套定义

//命名空间的嵌套定义
namespace Name1
{
    
    
	int a;
	int b;
	int Add(int x, int y)
	{
    
    
		return x + y;
	}
	namespace Name2
	{
    
    
		int a;
		int b;
		int Sub(int x, int y)
		{
    
    
			return x - y;
		}
	}
}

方式三:同工程下多次定义

我们可以在同一个工程文件下多次定义命名空间用来扩展命名空间中的成员内容,编译器最后会自动将这些定义在不同地方的命名空间合成到一个命名空间当中的。

比如说下面这个例子:

//同一个工程文件下多次定义,最后由编译器自动合成为一个命名空间
namespace Name1
{
    
    
	double c;
	double d;
	int Add(int x, int y)
	{
    
    
		return x + y;
	}
}
namespace Name1
{
    
    
	int a;
	int b;
	void Swap(int* px, int* py)
	{
    
    
		int temp = *px;
		*px = *py;
		*py = temp;
	}
}

虽然我们对命名空间Name1定义了两次,而且每次定义花括号里面的内容都是不同的,但是编译器最终会将这两个定义合成一个,效果和下面这种定义方式是一样的。

//最终编译器合成的效果
namespace Name1
{
    
    
	int a;
	int b;
	double c;
	double d;
	int Add(int x, int y)
	{
    
    
		return x + y;
	}
	void Swap(int* px, int* py)
	{
    
    
		int temp = *px;
		*px = *py;
		*py = temp;
	}
}

2.4 如何使用命名空间?

定义命名空间是为了方面我们之后对命名空间的使用,其使用的方式也有三种,下面会详细介绍这三种方式。

方式一:全部展开

我们可以使用using namespace 加命名空间的名称,将该命名空间中的内容全部展开,方便我们后面的使用

#include<iostream>
namespace Name1
{
    
    
	int a;
	int b;
	double c;
	double d;
	int Add(int x, int y)
	{
    
    
		return x + y;
	}
	void Swap(int* px, int* py)
	{
    
    
		int temp = *px;
		*px = *py;
		*py = temp;
	}
}
using namespace Name1;//将自己定义的Name1全部展开
using namespace std;//将C++标准库standard全部展开
int main()
{
    
    
	int num1 = 10;
	int num2 = 20;
	int sum = Add(num1, num2);
	cout << sum << endl;
	return 0;
}

image-20220310213344569

优缺点分析

**优点:**全部展开后,我们使用起来会很方便,不用去考虑其命名空间是什么,

比如说我们将标准库 std 展开后,再去使用cout进行输出操作,就不用去考虑它原来在哪个命名空间中。

using namespace std;

**缺点:**可能会出现命名污染。如果我们也定义了一个和全部展开的命名空间中成员名称一样的变量或函数,就会出现命名污染的情况。

总结:日常练习中,我们可以使用全部展开的方式,这样比较方便。但是在工程项目中最好不要全部展开,不然bug满天飞~

方式二:不展开

我们也可以不用展开命名空间,当需要使用到命名空间中的成员时,通过命名空间名称后面跟域作用符来使用

std::cout<<"hello world!";//::就是域作用符
//不展开,通过命名空间名称后面跟域作用符来使用
#include<iostream>
namespace Name1
{
    
    
	int a;
	int b;
	double c;
	double d;
	int Add(int x, int y)
	{
    
    
		return x + y;
	}
	void Swap(int* px, int* py)
	{
    
    
		int temp = *px;
		*px = *py;
		*py = temp;
	}
}
int main()
{
    
    
	int num1 = 10;
	int num2 = 20;
	int sum = Name1::Add(num1, num2);//::就是域作用符
	std::cout << sum << std::endl;
	return 0;
}

优缺点分析:

**优点:**不存在命名污染的问题

**缺点:**用起来很麻烦,每次使用都需要前面加上 命名::(简直烦死了~)

总结:日常练习不推荐这样使用。项目工程中推荐这样使用。

方式三:部分展开

这种方式是全部展开和不展开的一种折中方案,既想保留全部展开时使用的方便性,又想拥有不展开时不存在命名污染的特性。(聪明的小机灵鬼们总会找到解决问题的方法~)

#include<iostream>
namespace Name1
{
    
    
	int a;
	int b;
	double c;
	double d;
	int Add(int x, int y)
	{
    
    
		return x + y;
	}
	void Swap(int* px, int* py)
	{
    
    
		int temp = *px;
		*px = *py;
		*py = temp;
	}
}
using std::cout;//使用
using Name1::Add;
int main()
{
    
    
	int num1 = 10;
	int num2 = 20;
	int sum = Add(num1, num2);
	cout << sum << std::endl;
	return 0;
}

2.5 深入理解命名空间

要想真正弄懂命名空间的含义,我们需要理解作用域和生命周期这两个概念

**作用域:**开始生效到失效的程序范围段

**生命周期:**指的是程序执行过程中该对象存在的一段时间。

两者比较:作用域与生命周期是两个完全不同的概念。 在英文中,作用域用“scope”表示,生命周期则用“duration”表示。 作用域是一个静态概念,只在编译源程序的时候用到。 一个标识符的作用域指在源文件中该标识符能够独立地合法出现的区域。 生命周期则是一个运行时(Runtime)概念,它是指一个变量在整个程序从载入到结束运行的过程中存在的时间周期。 由于函数和数据类型是静态的概念,它们没有生命周期的说法,它们从编译、程序的运行到结束整个过程是一直存在的。

定义了一个命名空间,实际上就是定义了命名空间内部成员的作用域和生命周期,也就是这个成员起作用的范围和和运行时间范围。

3.C++输入&输出

在C语言当中,我们经常会使用scanf( )和prinf( )这两个函数来进行输入输出。因为C++是兼容C语言的,所以C语言当中的绝大多数语法和函数在C++当中仍然可以正常使用。但是C++当中的内容在C语言中不一样能够使用。此外C++作为一门新的语言,自然有其特有的方式来进行输入输出。在此之前我们还需要补充一些容易忽视的知识。

3.1 如何区分C和C++文件

在VS2019中,C源文件是以.c为后缀的,C++源文件是以.cpp为后缀的。

image-20220310220656015

3.2 cin和cout详解

#include<iostream>
using namespace std;//为了后面使用方便,这里直接展开
int main()
{
    
    
	int a = 0;
	int b = 0;
	cin >> a >> b;//从键盘中输入两个整数到a,b中,输入时用空格隔开
	cout << "a = " << a << "\nb = " << b << endl;//将a,b的值输出打印显示出来
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kzy2AzqZ-1647359255281)(https://s2.loli.net/2022/03/10/SfXA1GL9p56B8Nb.png)]

	cin >> a >> b;//从键盘中输入两个整数到a,b中,输入时用空格隔开

为了方便理解,我们可以将cin(读作see-in)想象成一个输入控制台,>> 是输入运算符/流提取运算符。cin>>a表示用户通过键盘输入内容放到输入控制台中,然后将输入控制台中的内容提取流向变量a的存储空间中。

同样,我们也可以将cout(读作see-out)想象成一个输出控制台,<< 是输出运算符/流插入运算符。cout<<a表示将变量a存储空间的内容插入到输出控制台中,然后通过输出控制台将内容输出显示打印在屏幕上。

3.3 说明:

1)使用cout标准输出(控制台)和cin标准输入(键盘)时,必须包含头文件以及std标准命名空间。

注意:早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名全间,规定C++不带.h;旧编译器(vc 6.0)中还支持<iostream.h>格式,后续编译器已不支持,因此推荐使用+std的方式。

2)使用C++输入输出更方便,不需增加数据格式控制,比如:整型–%d,字符–%c

cout << "a = " << a << "\nb = " << b << endl;//将a,b的值输出打印显示出来

上面这一句输出语句中,我们并没有指定输出格式(比如说用%d输出整数),但仍可以正常输出整数。这就是因为C++中的cout在输出时可以自动检测识别数据的类型。

3.4 建议:

我们在刚开始学C++时,很容易犯迷糊的以一个点就是:“是不是我都在C++了,那么C语言中的内容我就不能再用了,所有东西都要用C++的!”(C语言说:哼,人类可真是喜新厌旧的生物!)

这种想法实际上错的非常离谱,C++虽然是从C语言中发展而来的,但是这并不意味着C++所有的方面都要比C语言好。至少对于我们而言,刚学C++时,C语言比起C++,我们更熟悉。而且C++当中还有很多用起来比较困难,C语言使用比较方便的情况,比如:指定输出数据的格式,double类型指定输出小数点后2位,用C语言可以

double d = 1.22667;
printf("%.2f\n",d);//C语言当中的这种方式很简单

而在C++当中就非常非常非常麻烦!

#include <iostream>
#include <csdtio>
#include <iomanip>
using namespace std;
int main()
{
    
    
double d = 1.22667;
cout.setf(ios::fixed);//为了防止出现小数点后仅有0,但不显示的情况
cout<<setprecision(2)<<d<<endl;
}

总结:不管是C还是C++,哪个好用用哪个!

4.缺省参数

4.1 什么是缺省参数?

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

换一种说法:我们在使用函数的时候,某些参数可以传参,也可以不传参。如果传参了,就会使用传递的参数值;如果不传参数(也就是缺少参数,缺席/缺省),这时候就会使用默认的缺省参数值。

#include<iostream>
using namespace std;
void Print(int x = 10)
{
    
    
	cout << x << endl;
}
int main()
{
    
    
	Print();//不传参数,使用缺省参数值10
	Print(5);
	Print(20);
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MOcIzb2M-1647359255281)(https://s2.loli.net/2022/03/10/KFuN6extOIZ582k.png)]

4.2 深入理解缺省参数

理解:缺省参数实际上就相当于我们生活中的备胎,当实际参数不为空的时候,就不使用缺省参数,当实际参数为空的时候才使用缺省参数。类似的道理,汽车的备胎在汽车的主轮胎正常工作的时候就一直搁在后备箱或车尾处,不使用,当主轮胎坏了,不能正常工作时,比如说轮胎爆胎或漏气了,就需要换备胎上路!

以生活中情感的备胎为例的话,如果一个女生和男朋友的关系好好的,那么这个女生的备胎男朋友1号,备胎男朋友2号…3号…都没啥用。当这个女生和男朋友关系闹僵,分手时,备胎1号闪亮登场,迅速成为女生的新任男朋友,备胎2号就变成备胎1号……最后就是

image-20220310225314924

一些奇特的想法:为什么不叫备胎参数?或者舔狗参数呢?

我的想法:可能跟时代的局限性有关系,可能当初提出缺省参数的C++程序员没怎么坐过汽车,然后那个时代也还没有备胎、舔狗这个词,舔狗这个词是互联网新词~

4.3 如何定义缺省参数?

缺省参数是指在函数的声明或定义时为函数的参数指定一个默认值。注意是“或”不是且,也就是说要么在函数声明的时候定义缺省参数,要么在函数定义的时候定义缺省参数,但是不能既在函数声明中定义又在函数定义中定义!

void Print(int x = 10);//在函数声明时定义缺省参数
void Print(int x = 10)//在函数定义时定义缺省参数
{
    
    
	cout << x << endl;
}

上面两种方式均可,但是建议在声明的时候给缺省参数,定义的时候就不要给缺省参数。主要是为了防止声明中和定义中出现的缺省参数值不一样,导致函数执行的时候出现歧义或冲突。

4.4 缺省参数的分类

缺省参数分为两种

第一种:全缺省参数,也就是缺省全部参数,或者说全部参数都是缺省参数。

#include<iostream>
using namespace std;
void Func(int a = 10, int b = 20, int c = 30)
{
    
    
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl;
}
int main()
{
    
    
	Func();
	cout << "----------------------" << endl;
	Func(6);
	cout << "----------------------" << endl;
	Func(6, 8);
	cout << "----------------------" << endl;
	Func(6, 8, 11);
	cout << "----------------------" << endl;
	return 0;
}

image-20220310230820062

注意:全缺省参数只能从左到右依次缺省,不能跳过某个参数值来缺省。

	//以下这些方式都是错误的
	Func(, 2);//只传第二个,跳过第一个
	Func(, , 3);//只传第三个,跳过第一个和第二个
	Func(, 2, 3);//传第二个和第三个,跳过第一个
	Func(1, , 3);//传第一个和第三个,跳过第二个

在这里插入图片描述

代码中的逗号 , 是对传的参数进行分割。假如我们要将3传递给第三个缺省参数

Func(3);//这种方式会默认将参数传给第一个,传不到第三个
Func(,,3);//这种方式才会将参数传给第三个,虽然语法规定了这样的方式是编译不通过的

这个地方不太好理解,因为这是C++语法中规定的,学语法要记忆+理解。我个人的理解方式是:这样规定是为了避免缺省参数出现歧义性,将缺省顺序确定为从左到右依次缺省后,就不会产生传递缺省参数到底是给第一个还是第二个或第三个这个的歧义性问题。

第二种:半缺省参数,不是缺省一半的参数,而是缺省部分参数。也就是一部分参数是缺省参数,另外一部分不是缺省参数。

注意:这个‘半’不是指一半,对半,想想如果有3个参数,难道半缺省参数是1.5个吗?显然这个理解是不对的,这个‘半’的正确理解是相对于‘全’,‘全’是指全部,‘半’则是指部分!

规定:半缺省参数必须从右到左依次来给出,不能间隔着给出

#include<iostream>
using namespace std;
void Func(int a , int b = 20, int c = 30)
{
    
    
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl;
}
int main()
{
    
    
	Func(1);
	cout << "----------------------" << endl;
	Func(1, 2);
	cout << "----------------------" << endl;
	Func(1, 2, 3);
	return 0;
}

image-20220310232547286

对比:缺省的参数只能从右往左连续缺省,调用的时候只能从左往右连续传参(语法规定)

语法的学习需要半记忆半理解,有些东西就是规定死的,没法理解,需要我们直接记忆,有些东西可以理解,理解后有利于强化我们的记忆。(记忆和理解并不是矛盾的,而是相辅相成的)

半缺省参数缺省错误的案例:
Case1:
在这里插入图片描述
Case2:
在这里插入图片描述
Case3:

半缺省参数缺省正确的案例:
Case1:
在这里插入图片描述
Case2:
image-20220310233420237

4.5 缺省参数的注意事项

1)缺省参数不能再函数声明和定义中同时出现(避免声明和定义的缺省值不同而产生冲突)

2)缺省值必须是常量或者全局变量

3)C++支持,而C语言不支持

5.函数重载

引言

在日常生活中,我们可能会经常碰到一个词表示多重含义,一句话可能也能表示多种含义,虽然写法都是一样的。

这个时候,如果我们想要理解这些词和句子表示的真正含义,就需要结合上下文(也就是语境信息)来判断其真正的含义。这些词和句子就被重载了。

有趣的小故事:自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。比如:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前者是“谁也赢不了! ”(谁也/赢不了),后者是"谁也赢不了!”(谁/也赢不了)(中华文化真是博大精深,这里断句也不一定对,小伙伴们可以自己试试)

再比如说初高中学的文言文,经常包含一词多义,比如文言文中的安:①安(怎么)得广厦千万间,大辟天下寒士俱欢颜。②风雨不动安(安稳)如山。③然后生于忧患,死于安(安逸)乐。

此外还有我们学过英语单词,也经常出现同一个单词有多种含义,比如 look 表示 看,寻找,搜索,表情等意思

5.1 什么是函数重载?

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

总结:所谓的函数重载,指的是函数名相同,但是函数的形参个数,类型或类型顺序不同

5.2 函数重载的定义和使用

方式一:形参个数不同

#include<iostream>
int Add(int x, int y)
{
    
    
	return x + y;
}
int Add(int x, int y, int z)
{
    
    
	return x + y + z;
}
int main()
{
    
    
	int a = 10, b = 20, c = 30;
	std::cout << Add(a, b) << std::endl;//不展开
	std::cout << Add(a, b, c) << std::endl;
	return 0;
}

方式二:参数类型不同

#include<iostream>
using namespace std;
int Sub(int x, int y)
{
    
    
	return x - y;
}
double Sub(double x, double y)
{
    
    
	return x - y;
}
int main()
{
    
    
	int a = 66, b = 88;
	double c = 3.14, d = 6.88;
	cout << Sub(a, b) << endl;
	cout << Sub(c, d) << endl;
	return 0;
}

方式三:参数类型的顺序不同

#include<iostream>
#include<math.h>
using namespace std;
double Pow(int x, double y)
{
    
    
	return pow(x, y);
}
double Pow(double x, int y)
{
    
    
	return pow(x, y);
}
int main()
{
    
    
	int a = 2;
	double b = 3.14;
	cout << pow(a, b) << endl;
	cout << pow(b, a) << endl;
	return 0;
}

注意:函数重载跟函数的返回值类型无关,两个同名函数仅返回值不同,不能构成函数重载

#include<iostream>
using namespace std;
int Mul(short x, short y)
{
    
    
	return x * y;
}
int Mul(short x, short y)
{
    
    
	return x * y;
}
int main()
{
    
    
	short a = 2, b = 3;
	cout << Mul(a,b) << endl;
	return 0;
}

image-20220312151124984

5.3 深入理解函数重载

关于函数重载这个地方,面试官们最喜欢问的,同时也是最能体现我们对C++语言掌握程度的一个问题就是

“为什么C++支持函数重载,而C语言不支持函数重载呢?“

要回答这个问题,我们需要深入学习和理解函数重载!

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

阶段 主要工作内容 生成文件类型
预处理 头文件的展开、宏替换、条件编译、去掉注释 xxx**.i** 文件
编译 语法检查、生成汇编代码 xxx**.s** 文件
汇编 将汇编代码转化为二进制机器码 xxx**.o** 文件
链接 将生成的 xxx**.o** 文件和其调用的库函数文件链接到一起,生成可执行文件 xxx.exe(windows)
Linux下可以自己命名

由于Windows下不好进行相应的演示,以下演示均在Linux环境下进行

在这里插入图片描述

list.h 头文件

#include<stdio.h>
void list_push_back(int x);
void add(int i,int j);
int add(double i,double* j);

list.c 源文件

#include"list.h"
void list_push_back(int x)
{
    
    
        printf("%d\n",x);
}
void add(int i,int j)
{
    
    }
int add(double i,double* j)
{
    
    
        return 0;
}

test.c 测试文件

#include"list.h"
int main()
{
    
    
        list_push_back(1);
        return 0;
}

上面的代码运行经历的几个阶段如下:

c语言中源文件:test.c(以.c结尾,英文:source)

c++源文件:test.cpp(以.cpp结尾,英文:c-plus-plus)

预处理 -> 头文件展开、宏替换、条件编译、去掉注释 生成 list.i test.i

编译 -> 检查语法,生成汇编代码 生成 list.s test.s

汇编 -> 将汇编代码转换成二进制的机器码 生成 list.o test.o

链接 -> 将两个目标文件链接到一起 生成可执行文件(Windows下是test.exe)

上面的汇编和链接两个阶段就是我们要理解函数重载的关键,尤其是链接阶段

image-20220312154049993
如果我们直接用gcc编译器(c语言编译器)来编译以上代码,肯定是编译不通过的,因为这里有函数重载的问题,而C语言是不支持函数重载的。

image-20220312153614776

如果将函数重载的部分先注释掉


再进行编译,应该是没有问题的,同时我们用c++的编译器g++编译一下,得到:

在这里插入图片描述

利用xshell同时开多个窗口,可以快速进行修改
在这里插入图片描述

如果找到了,就正常链接,生成可执行程序.exe文件,如果找不到,就会出现链接时错误。

什么时候会出现找不到的情况呢?

当我们声明了一个函数,然后对其进行正常调用,但是没有定义,那么这个函数的地址就找不到

在C语言中,汇编生成的符号表中,可以发现对应的函数名字修饰没有发生改变,如果出现两个名称相同的函数,那么在链接的时候调用函数时应该找到哪一个函数的地址进行使用呢?是不是无法区分出来,这也是为什么C语言不支持函数重载的原因(汇编后函数名字的修饰未变化)

image-20220312154310600

那么C++呢?又是什么情况?为什么会支持函数重载?难道是函数的修饰名称发生了变化?

在这里插入图片描述

将之前生成的cpp先去掉,然后将之前注释的代码段去除注释,重新生成cpp文件

image-20220312154338591

使用objdump-S 命令来查看符号表相关信息
在这里插入图片描述

找到list_push_back,add,add三个函数的符号表

在这里插入图片描述

可以看到加了一些修饰内容,比如前面的Z14/_Z3,后面的ii,dPd),因此即便函数名相同,但是函数参数不同,生成的符号表也会不同,在调用的时候,根据生成的符号表,依然可以找到对应的函数地址,不会发生冲突,可以进行区分,所以支持重载!

在这里插入图片描述

<_Z3Addii> _Z3是前缀 Add是函数名,ii(i是类型的首字母)指两个int类型的参数

【_Z+函数长度+函数名+类型首字母】

另外我们也理解了,为什么函数重载要求参数不同,而跟返回值没关系。

延申问题:

编译器能不能实现,函数名相同,函数参数相同,但是函数返回值不同,构成重载呢?(通过修改编译器生成符号表规则)

答:不能,虽然编译器底层可以实现,也可以区分,但是在调用函数的时候,无法区分该调用哪一个函数,带有严重的歧义性。

总结:C++与C函数名修饰规则不一样,C++的函数名修饰规则中会引入参数相关的信息,所以在函数名相同,参数不同的情况下,经过函数名修饰规则修饰后生成的名称也不同,可以做到相同函数名称之间的区分!而C语言的函数名修饰规则是直接使用函数名,跟函数参数无关,如果出现名称相同的函数,那么在链接的时候去调用函数,应该调用哪一个呢?两个名称完全相同,就无法确定该调用哪一个了!

补充:对比Linux会发现,windows下C++编译器对函数名字修饰非常诡异,但道理都是一样的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e36ZHjSy-1647362584965)(https://s2.loli.net/2022/03/12/IhJ5wyocB4NjO2Y.png)]

其实,类似于C++中在生成符号表时加上函数名修饰规则的方式在python中也存在,这种方式的名称叫做name Mangling,翻译成中文就是名称修饰。

最后,让我们回到面试问题本身“为什么C++支持函数重载,而C语言不支持函数重载呢?“

这个问题实际上包含两个子问题。

1)为什么C++支持函数重载?或者说C++是如何支持函数重载的?

在生成符号表的时候,C++引入了函数名修饰规则,不能直接用函数名来当作符号表的名称,而是要对函数名进行修饰,带入函数参数的特点来进行修饰。这样,即便函数名相同,只要参数不同,比如说参数的个数,参数的类型或者是参数的类型顺序 ,修饰出来的符号表名称就会不同,那么就能进行区分了,所以可以支持重载。

2)为什么C语言不支持重载?

在生成符号表的时候,C语言编译器直接用函数名进行关联,当函数名相同的时候,修饰出来的符号表也会相同,这样就无法进行区分,所以不支持重载。

5.4 extern ”C“

有时候在C++工程中可能需要将某些函数按照C的风格来编译,在函数前加extern “C”,意思是告诉编译器,将该函数按照C语言规则来编译。比如: ttmlloc是google用C++实现的一个项目,他提供tcmallc()和tcfree两个接口来使用,但如果是C项国就没办法使用,那么他就使用extern“C"来解决。

C++编译器能够识别C++函数名修饰规则,也能识别C的修饰规则,因为C++是兼容C的

一些C++项目中,可能会用到一些用C实现的库(静态库和动态库——可以暂时理解为汇编形成的xxx.o文件)

比如说:glibc/网络库/数据库API库,可能是纯C写的,里面的函数名是按C语言的方式,C++可以使用。

C项目中使用C编译器,对于C++实现的库(动态库/静态库),比如说tcmalloc提供更高兴的malloc和free。这些库是用C++实现的,所以C++编译器需要将其按照C语言的规则来编译,否则C编译就无法调用

我们可以在github上面搜索tcmalloc,查看对应的tcmalloc.h

1)在github搜索栏输入tcmalloc,选择搜索出来的第一个Google的项目

image-20220312170353810

2)进入项目内部,选择tcmalloc文件路径

image-20220312170424171

3)找到 tcmalloc.h头文件

image-20220312170529760

4)直接点击文件,进行查看,可以看到由 extern “C” 包含一堆函数声明接口,这是告诉C++编译器这些函数要用C语言的规则去编译。

image-20220312170318620

总结:为了让C编译器能够识别C++编译器生成的库,让C++编译器以C规则生成库函数文件

静态库和动态库

image-20220312164451137

为了让C和C++程序都能用到这个C++的静态库/动态库,可以用extern “C”

在这里插入图片描述

C++兼容c语言的规则,用了extern “C”后就不支持函数重载了,因为此时符号表是按照C语言的规则去生成的

总结:C++项目可以直接调用C++的库,也可以直接调用C的库

C项目可以直接调用C的库,如果要调用C++的库,可以通过extern “C”来实现

5.5 面试题补充

1、下面两个函数能形成函数重载吗?有问题吗或者什么情况下会出问题?

void TestFunc(int a = 10)
{
    
    
	cout << "void TestFunc (int) " << endl;
}
void TestFunc(int a)
{
    
    
	cout << "void TestFunc(int) " << endl;
}

答:不构成函数重载,仅有缺省参数值不同,函数参数是相同的。

2、C++中能否将─个函数按照C的风格来编译?

可以,通过extern “C”,可以让C++将一个函数按照C的风格来编译。

如何控制程序编译后生成的内容,比如说可执行文件 .exe,动态库或静态库?

在这里插入图片描述

image-20220312172356284

6.引用

6.1 什么是引用?

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

比如:李逵,在家称为"铁牛",江湖上人称"黑旋风""。

在这里插入图片描述

再比如说:讲普法的罗翔老师经常谈到的张三,网友们送其外号“法外狂徒”,这也是起别名的方式。

6.2 引用的使用方式

类型& 引用变量/对象名 = 引用实体;

#include<iostream>
using namespace std;
int main()
{
    
    
	int a = 3;
	int& ra = a;
	printf("%p\n", &a);
	printf("%p\n", &ra);
	return 0;
}

image-20220314214749072

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

6.3 区分引用和取地址

**&**在这个地方表示引用,而不是取地址,一个操作符有多种含义,也就是操作符的重载。两者没有什么必然的关系。

如何区分引用和取地址呢?

在变量的面前使用&就是取地址 ,如:int* pa = &a;

在类型的后面使用&就是引用,如:int& ra = a;

image-20220314214833040

6.4 引用有哪些特性?

1)引用不但可以给变量起别名,还可以给别名再起别名

int a = 3;
int& ra = a;
int& rra = ra;

2)引用在定义时必须初始化

int a = 1;
int* pa;//指针定义时可以不初始化
pa = &a;
int& ra = a;//引用定义时必须初始化

image-20220314220436120

3)—个变量可以有多个引用,也就是说起别名没有数量限制。

4)一旦引用—个实体,再不能引用其他实体。

这个地方也跟指针是不一样的,一旦确定了要引用哪个变量,后面就不能更改引用的对象了。而指针是可以变更指向的变量的,所以我们可以说引用非常的专一。

	int a = 1;
	int b = 2;
	int& rm = a;//给a起了个别名叫做rm
	rm = b;//这个地方并不是更改引用的对象,这里是将b的值赋给rm,也就是赋给a

对于指针变量,也是可以取别名的

int* p1 = &x;
int* p2 = &y;
int*& p3 = p1;//给p1取别名p3,p3中存放x的地址
*p3 = 10;//将10赋给x
p3 = p2;//让p1不再指向x,而是指向y

6.5 什么是常引用?

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;l/该语句编译时会出错,类型不同
	const int& rd = d;
}

当a时const int类型时,表示a是一个常变量,常变量具有常属性—值不可改变,这时候如果用int& 来引用a,就会得到一个int类型的别名,这个别名是普通的变量,可以修改,就有可能会改变a的值,造成a常变量被修改的冲突!所以为了避免这个冲突,在引用const int 类型的a变量时,应该用 const int& b = a;

总结:

引用取别名时,变量访问的权限可以不变或缩小(如 int ->const int ),不能放大(如 const int -> int )

不能放大权限:不能将const类型的变量给非const类型的别名

可以权限缩小:即可以将非const类型的变量给非const类型的别名,也可以给const类型的别名。

提示:权限的缩小和放大,仅适用于引用和指针

const int a = 10;
int* p = &a;//这种不行,权限的放大
const int* pa = &a;//需要这种形式

int c = 1;
const int* pc = &c;//可以,属于权限的缩小

注意区分:

const int a = 10;
int& b = a;//这种是不行的,b是a的别名

const int x = 10;
int y = x;//这种是可以的,y和x没什么关系
//这种是不受影响的
int i = 1;
double d = i;//隐式类型转换
double& ri = i;//可以这样取别名吗? error
const double& rri = i;//这样呢?ok

double d = I;这句话是先产生了一个double的临时变量

double& ri = I;也是一个会先产生一个double类型的临时变量,然后真正引用的是这个临时变量,而这个临时变量具有常属性,所以直接用double&不可以,加上const又ok了。

类型转化包括隐式类型转换和强制类型转换

image-20220314221725303

6.6 引用的使用场景

1.做参数 (①输出型参数 ②提高效率)

#include<iostream>
using namespace std;
void Swap(int& left, int& right)
{
    
    
	int temp = left;
	left = right;
	right = temp;
}

int main()
{
    
    
	int a = 1;
	int b = 2;
	Swap(a, b);
	cout << a << " " << b << endl;
	return 0;
}

在这里插入图片描述

问:不是说引用定义的时候需要初始化吗?这里并没有初始化?

答:这个地方的引用并不是定义,什么时候才是定义呢?传参的时候才是定义,传参过来就会进行初始化操作。

image-20220314222733976

当然这里也可以用指针的方式来实现:

#include<iostream>
using namespace std;
void Swap(int* pa, int* pb)
{
    
    
	int tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}
int main()
{
    
    
	int a = 1;
	int b = 2;
	Swap(&a, &b);
	cout << a << " " << b << endl;
	return 0;
}

在这里插入图片描述

2.做返回值 (①提高效率 ②以后再讲)

凡是传值的方式(参数传值、传值返回),都会产生一个拷贝的临时变量。传引用的方式不会。

static 可以改变变量的生命周期,不会修改变量的访问权限

下面代码输出什么结果?为什么?

#include<iostream>
using namespace std;
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;
}

image-20220314223104793

注意:如果函数返回时,出了函数作用域,如果返回对象还未还给系统,则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。(说明引用返回说不安全的!)

那么使用引用返回有什么好处呢?

可以少创建一个临时变量,提高程序(传递)的效率

其实还有一个作用,以后会将(很多库函数的返回也会用引用返回)

6.7 传值和传引用效率的比较

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

#include<iostream>
using namespace std;
#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 < 1000000; ++i)
		TestFunc1(a);
	size_t end1 = clock();

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

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

#include<iostream>
using namespace std;
#include <time.h>
struct A
{
	int a[10000];
};
A a;
A TestFunc1()
{
	return a;
}
A& TestFunc2()
{
	return a;
}
void TestRefAndValue()
{
	A a;
	//以值作为函数的返回值类型
	size_t begin1 = clock();
	for (size_t i = 0; i < 1000000; ++i)
		TestFunc1();
	size_t end1 = clock();

	//以引用作为函数的返回值类型
	size_t begin2 = clock();
	for (size_t i = 0; i < 1000000; ++i)
		TestFunc2();
	size_t end2 = clock();

	//分别计算两个函数运行结束后的时间
	cout << "TestFunc1()-time : " << end1 - begin1 << endl;
	cout << "TestFunc2()-time: " << end2 - begin2 << endl;
}
int main()
{
	TestRefAndValue();
	return 0;
}

image-20220314223419327

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

6.8 引用和指针的区别和联系

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

#include<iostream>
using namespace std;
int main()
{
    
    
	int a = 10; int& ra = a;
	cout << "&a = " << &a << endl;
	cout << "&ra = " << &ra << endl;
	return 0;
}

image-20220314223633030

在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。

#include<iostream>
using namespace std;
int main()
{
    
    
	int a = 10;
	int& b = a;
	int* p = &a;
	return 0;
}

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

image-20220314223653433

引用和指针都是存放地址

引用和指针的不同点:

1.引用在定义时必须初始化,指针没有要求

2引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体

3.没有NULL引用,但有NUL指针

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

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

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

7.访问实体方式不同,指针需要显式解引用,引用编译器自己处理

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

7.内联函数

7.1 什么是内联函数?

以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。

image-20220314223812213

频繁调用Swap函数是有消耗的,比如我们在排序的时候,就会经常调用Swap函数

如何解决呢?

1)C语言中使用define定义宏来代替

2)C++中使用inline内联函数

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

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

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

在这里插入图片描述
在这里插入图片描述

内联函数没有地址,在调用的地方直接展开了

7.2 内联函数的特性

1)inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。

2)inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联。

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

7.3 面试题

宏的优缺点?

优点:

1)增强代码的复用性

2)提高性能。

缺点:

1)不方便调试宏。(因为预编译阶段进行了替换)

2)导致代码可读性差,可维护性差,容易误用。

3)没有类型安全的检查。

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

1)常量定义换用const

2) 函数定义换用内联函数

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

8.1 auto简介

在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它,大家可思考下为什么?

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

#include<iostream>
using namespace std;
int main()
{
    
    
	int a = 10;
	auto b = a;//b的类型是根据a的类型自动推导出来的
    auto c = 2;
    auto d = 3.14;
	cout << typeid(a).name() << endl;
	cout << typeid(b).name() << endl;
	cout << typeid(c).name() << endl;
	cout << typeid(d).name() << endl;
	return 0;
}

image-20220314230227613

注意:引用变量的类型和被引用类型一样,而不是类型+&,比如int& b = a; b的类型是int,而不是int&。引用变量是被引用变量的一个别名,如果被引用变量是int类型,那么引用变量也是int类型(别名也是int类型)。另外,没有int& 这种类型,因为引用是不占用空间的。

image-20220314230305545

【注意】

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

auto类型 – 自动推导类型

aoto真正的使用场景:STL中

map<string,string> dict;
//map<string,string>::iterator it = dict.begin();
auto it = dict.begin();//编译器自动推导出类型,简化代码

缺点:一定程度牺牲了代码的可读性

8.2 auto的使用细则

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

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

int x = 10;
auto a = &x;//&x后编译器可以推导出a的类型是int*
auto* b = &x;
auto& c = x;//如果auto后面没有&,编译器会认为auto应该是int类型

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

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

auto c = 1, e = 2;
auto d = 3.14,g = 3;//这行代码编译失败,因为g被认为是int类型,而d是double类型

8.3 auto不能推导的场景

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

函数调用建立栈帧的时候,不确定形参类型,无法确定开辟多大的空间

//此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{
    
    
	//...
}

2.auto不能直接用来声明数组

void TestAuto()
{
    
    
	int a[] = {
    
     1,2,3 };
	auto b[] = {
    
     4,5,6 };//这行无法编译
}

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

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

lambda:λ(拉姆达)是一个匿名函数

#include<iostream>
using namespace std;
int main()
{
    
    
	int array[] = {
    
     1,2,3,4,5,6,7,8,9,10 };
	for (int i = 0; i < sizeof(array) / sizeof(array[0]); i++)
		cout << array[i] << " ";
	//使用范围for循环 + auto
	for (auto& e : array)
		cout << e << " ";
	return 0;
}

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

#include<iostream>
using std::cout;
using std::endl;
int main()
{
    
    
	int arr[] = {
    
     1,2,3,4,5 };
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
		arr[i] *= 2;
	for (auto& j : arr)
		cout << j << " ";
	return 0;
}

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

注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。C++中的语法糖,Java、python中也有,写起来非常简洁。

for(auto e : arr)//这里只是将arr中的元素拷贝到临时变量e当中,e改变,arr中元素不会改变
for(auto& e : arr)//这里是给arr中的元素起别名,e改变就相当于arr中的元素改变

9.2 范围for的使用条件

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

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

注意:以下代码就有问题,因为for的范围不确定(数组传参后就变成了指针,或者说退化成了指针)

void Test(int array[])
{
    
    
	for (auto e : array)
		cout << e << " ";
	cout << endl;
}

2.迭代的对象要实现++和==的操作。(关于迭代器这个问题,以后会讲,现在大家了解一下就可以了)

10.指针空值—nullptr(C++11)

10.1 C++98中的指针空值

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

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

image-20220315232249496

可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:

#include<iostream>
using namespace std;
void fun(int n)
{
    
    
	cout << "整型" << endl;
}
void fun(int* p)
{
    
    
	cout << "整型指针" << endl;
}
int main()
{
    
    
	int* p1 = NULL;//C语言中
	int* p2 = nullptr;//C++中,推荐这种方式
	fun(0);
	fun(NULL);
	fun(nullptr);
	return 0;
}

image-20220315232436148

程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。

注意:

1.在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。

2.在C++11中,sizeof(nullptr)与sizeof((void*)O)所占的字节数相同。

3.为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。


思维导图总结

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/QIYICat/article/details/123515428