关于我们

质量为本、客户为根、勇于拼搏、务实创新

< 返回

【C++】C++入门

发布时间:2022-12-29 17:53:50

屏幕前的你,一起加油啊!!!
在这里插入图片描述

文章目录
  • 一、命名空间(namespace)
    • 1.命名空间的定义(::域作用限定符)
    • 2.命名空间的使用(三种使用方式)
  • 二、C++输入&输出(iostream+std)
  • 三、缺省参数
    • 1.全缺省参数
    • 2.半缺省参数
    • 3.(带有缺省参数)函数的定义和声明
  • 四、函数重载(一个名字多个函数)
    • 1.函数重载的类型
      • 1.1 形参的类型不同
      • 1.2 形参的个数不同
      • 1.3 形参的类型顺序不同
    • 2.函数重载+缺省参数(编译器懵逼的代码)
    • 3.C++支持函数重载的原理(汇编下的函数名修饰规则)
    • 4.返回值不同能否构成函数重载?
  • 五、引用(三姓家奴)
    • 1.引用概念(不就取别名么)
    • 2.引用特性
    • 3.引用的使用场景
      • 3.1 内存空间销毁意味着什么?& 访问销毁的内存空间会怎样?
      • 3.2 做返回值(减少拷贝提高效率,修改返回值)
      • 3.3 做参数(减少拷贝提高效率,可作为输出型参数)
    • 4.常引用(带有const的引用)
      • 4.1 指针/引用在赋值中,权限可以缩小,但是不能放大
      • 4.2 常引用做参数
      • 4.3 缺省参数如何引用?
      • 4.4 临时变量具有常性不能修改(传值返回,隐式/强制类型转换)
    • 5.引用和指针的区别
  • 六、内联函数(不建立函数栈帧的函数,已经不是正常的函数了)
    • 1.替代C语言中的宏
    • 2.编译器根据函数体大小来决定是否展开(代码膨胀)
    • 3.声明和定义分离(本质:内联函数无论是否被编译器当作内联处理,他的函数名和有效地址都不进符号表,与static修饰的全局函数一样,都不进符号表)
  • 七、auto用法
  • 八、基于范围的for循环
  • 九、指针空值nullptr ==> (void*)0



一、命名空间(namespace) 1.命名空间的定义(::域作用限定符)

a. 之前的C语言学习中我们就了解过全局和局部这部分的知识了,在C++里面他们有一个新的名词就是域,域就相当于一片领地,如果想定义两个一模一样的变量在同一个域中,这显然是不行的,会出现变量重命名的问题,但是这样的问题还是比较常见的,因为c++和C语言中都有很多的模板,函数库等等,难免我们定义的和库里面定义的,产生命名冲突和名字污染,namespace所创建的命名空间就是用来解决这样的问题的。

为了防止命名冲突的产生,C++规定了命名空间的出现,这可以很好的解决问题,我们可以把我们想定义的东西都放到我们自己定义的命名空间里面去,这样就不会产生命名冲突的问题了。

#include 
#include 
int rand = 10;
// C语言没办法解决类似这样的命名冲突问题,所以C++提出了namespace来解决
int main()
{
 printf("%d\n", rand);//stdlib.h文件里面有rand函数。
return 0;
}
// 编译后后报错:error C2365: “rand”: 重定义;以前的定义是“rand函数”

b. 编译器有一套自己的查找变量的规则:
1.默认先去局部域找变量,再到全局域里面去找变量
2.如果我们利用域作用限定符限定了编译器要查找的域,那编译器就会按照我们设定的查找规则来查找

#include 

int a = 0;//全局域中定义变量a
int main()
{
	int a = 1;//局部域中定义变量a,所以在不同的域中是可以定义同一个变量的。

	printf("%d\n", a);//先去局部域里面找a,再到全局域里面找a
	// ::域作用限定符
	printf("%d\n", ::a);//::的左面是空白,代表作用于全局域,指定编译器查找的位置是全局域

	return 0;
}

c. 我们现在利用命名空间wyn封装起来了rand,这时候就不会和stdlib.h文件中的rand()函数产生命名冲突了。定义的形式请看下面代码。

命名空间中的rand不是一个局部变量,而是一个全局变量,因为只有定义在函数里面,存放到栈上的变量才是局部变量。rand存放在静态区,并且现在的namespace根本就不是一个函数,自然也就说明rand不是局部变量,而是全局变量。

那么变量定义在命名空间中和定义在全局域中有什么区别呢?
其实区别就是编译器查找的规则不同,如果你指定查找的域,那编译器就去你定义的命名空间查找,如果你不指定查找的域,那编译器就先去局部域查找,再去全局域查找。

d. 命名空间也可嵌套定义,一个命名空间当中又细分多个命名空间,这样也是可以的,下面代码的wyn空间当中就嵌套定义了N1,N1中又嵌套定义了N2。

namespace wyn
{
	int rand = 10;
	
	int Add(int left, int right)
	{
		return left + right;
	}
	struct Node
	{
		struct Node* next;
		int val;
	};
	namespace N1
	{
		int a;
		int b;
		int Add(int left, int right)
		{
			return left + right;
		}
		namespace N2
		{
			int c;
			int d;
			int Sub(int left, int right)
			{
				return left - right;
			}
		}
	}
}

e.
同一个工程中的不同的文件允许存在多个相同名称的命名空间,编译器最后会合成到同一个命名空间当中去。
同一个文件里面的相同名称的命名空间也是会被编译器合并的。

2.命名空间的使用(三种使用方式)

C++官方封装好了一个命名空间叫做std,它和其他的一些命名空间都被封装到iostream头文件里面,C++所使用的cin和cout都被封装在iostream文件中的std命名空间。

这其实变相的帮助我们解决了一个问题,就是如果我们平常中的命名和官方库产生冲突时,我们也不害怕,因为两者所处的域是不同的,互不干扰。

a.利用域作用限定符

这种命名空间的使用方式堪称经典,使用语法:域名+域作用限定符+域中的成员名

b.展开整个命名空间

这种使用方式不是很推荐,因为一旦将命名空间全部展开,虽然我们在使用上可以直接使用,但是这会产生极大的命名冲突安全问题隐患,所以如果你只是写个小算法,小程序等等,可以这么使用。但如果在大型工程里面还是不要这么用了,因为出了问题,就麻烦了。

c.展开域中的部分成员

强烈推荐这样的使用方式,将我们常用的某些函数展开,我们在定义时,只要避免和部分展开的重名即可,这样的使用方式也较为安全,所以强烈推荐。

using namespace std;//这个东西存在的意义就是将命名空间里面的内容展开,用起来会方便很多
//当然反面的意义就是将命名空间的域拆开了,会产生命名冲突问题的隐患。

//日常练习,写个算法或小程序等等,这么使用可以,因为一般情况下不会产生命名冲突的问题,但项目里面最好不要这么用。

using std::cout;//将常用的展开,自己在定义的时候,尽量避免和常用的重名即可。
int main()
{
	//下面的所有访问都必须在iostream文件展开的基础上进行,只有展开后,那些大量的命名空间才会出现,下面的代码才可以访问
	//命名空间里面的变量、函数、结构体等等

	//第一种访问方式:指定域访问
	std::cout << "hello world" << std::endl;
	std::cout << "hello world" << std::endl;
	std::cout << "hello world" << std::endl;
	//第二种访问方式:将域在前面全部展开,编译器会先在局部找cout,然后去全局找cout,正好域全部展开了,全局自然存在cout
	cout << "hello world" << endl;
	cout << "hello world" << endl;
	cout << "hello world" << endl;
	//第三种访问方式:将域指定展开,只展开域中的某些常用成员,方便我们使用那些常用的函数或结构体。
	cout << "hello world" << std::endl;//endl没有被展开,需要指定访问的域
	cout << "hello world" << std::endl;
	cout << "hello world" << std::endl;
}
二、C++输入&输出(iostream+std)

a.

使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件以及按命名空间使用方法使用std。

b.

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

c.

<<是流插入运算符,>>是流提取运算符,endl是特殊的C++符号,表示换行输出,他也被包含在iostream头文件中

注意:

早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在std命名空间下,为了和C语言的头文件区分,也为了正确使用命名空间,规定C++头文件不带.h;所以我们可以看到iostream是不带.h的。

#include 
using namespace std;//我们平常写的时候全部展开std命名空间,以后写项目还是尽量不要这样写。

int main()
{
	//可以自动识别变量的类型,相比c的优势printf / scanf

	// <<  流插入运算符
	cout << "hello world!!!" << '\n';
	cout << "hello world!!!" << endl;//两种写法等价,C++喜欢下面这种写法

	int a;
	double b;
	char c;

	
	//  >>  流提取运算符
	cin >> a;
	cin >> b >> c;
	cout << a << endl;
	cout << b << ' ' << c << endl;
	
	//C++也有不方便的地方,如果你要输出小数点后几位,建议还是使用C语言来实现
	//还有一种场景是要求挨着打印出变量的类型,这时候用C语言也是较方便的。
	//C和C++哪个方便就用哪个
	printf("%.2f\n", b);
	cout << "int:" << a << ' ' << "double:" << b << ' ' << "char:" << (int)c << endl;//c前面加个强制类型转换,输出ascll
	printf("int:%d double:%.2f char:%d", a, b, c);

	return 0;
}
三、缺省参数

在声明或定义函数时,给函数指定一个缺省值。
调用该函数时,如果实参没有指定传什么,函数就使用该缺省值,如果指定了,那就使用实参的值。

#include 
using namespace std;

void Func(int a = 0)//缺省值
{
	cout << a << endl;
}
int main()
{
	Func();//函数拥有缺省值之后,可以给函数传参也可以不给他传参。
	Func(10);

	return 0;
}

1.全缺省参数

在给全缺省参数的函数传参时,我们必须从左向右依次传参,不可以中间空出来,跳跃的传参,这样的传参方式编译器是不支持的

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();
	Func(1);//默认传给了a
	Func(1, 2);//从左向右依次传参数
	//Func(1, , 2);//不可以中间空出来,跳过b,只给a和c传,这样编译器是不支持的。
	Func(1, 2, 3);
	return 0;
}
2.半缺省参数

半缺省参数的函数在设计时,缺省参数必须得是从右向左连续缺省。

void Func(int a, int b = 10, int c )//这样是不可以的
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl;
}
void Func(int a, int b , int c=10)//这样是可以的
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl;
}
int main()
{
	Func(1);
	Func(1, 2);
	Func(1, 2, 3);
	return 0;
}
3.(带有缺省参数)函数的定义和声明

a.
带有缺省参数的函数在定义和声明时,C++有特殊的规定,在函数的声明部分中写出缺省参数,在函数的定义部分中不写缺省参数,如下面代码所示。

在这里插入图片描述

b.
如果声明与定义中同时出现缺省值,而恰巧两个缺省值是不同的,这样的话,编译器是无法确定改用哪个缺省值。

c.
缺省值必须是常量(情况居多)或全局变量,C语言是不支持缺省参数这种概念的。

四、函数重载(一个名字多个函数) 1.函数重载的类型

C++允许在一个域中同时出现几个功能类似的同名函数,这些函数常用来处理实现功能类似数据类型不同的问题。
下面的两个函数在C++中是支持同时存在的,但在C语言中是不支持的。

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
void Swap(double* p1, double* p2)
{
	double tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
1.1 形参的类型不同
int add(int x, int y)//int类型
{
	return x + y;
}
double add(double x, double y)//double类型
{
	return x + y;
}
1.2 形参的个数不同
void f()//0个形参
{
	cout << "f()" << endl;
}
void f(int a)//一个形参
{
	cout << "f(int a)" << endl;
}
int main()
{
	f();
	f(1);
	return 0;
}
1.3 形参的类型顺序不同
void f(int a, char b)//int char
{
	cout << "f(int a,char b)" << endl;
}
void f(char a, int b)//char int
{
	cout << "f(char a,int b)" << endl;
}
//上面的两个函数算函数重载,因为参数的类型顺序不同
//下面的两个函数不算函数重载,因为编译器是按照参数类型识别的,并不是按照参数的名字来识别的。
void f(int a, int b)//int int 
{
	cout << "f(int a,int b)" << endl;
}
void f(int a, int b)//int int
{
	cout << "f(int a,int b)" << endl;
}
int main()
{
	f(106, 'A');
	f('A', 106);
	return 0;
}
2.函数重载+缺省参数(编译器懵逼的代码)

下面的两个函数确实构成函数重载,但在调用时,如果我们不指定实参的值,那就会产生二义性,编译器是不知道该调用哪个函数的,所以我们尽量不要写出这样的函数重载。

void f()//0个形参
{
	cout << "f()" << endl;
}
void f(int a = 0, char b = 1)//2个形参
{
	cout << "f(int a,char b)" << endl;
}
int main()
{
	f(10);
	f(10, 20);
	f();//这里会报错:对重载函数的调用不明确  --  产生二义性 -- 编译器蒙蔽


	return 0;
}
3.C++支持函数重载的原理(汇编下的函数名修饰规则)

C/C++程序运行原理:

稍微带大家复习一些程序运行原理部分的知识:
假设当前a.cpp中调用了b.cpp中定义的Add函数,那么在编译结束之后,a.cpp和b.cpp都会产生目标文件.obj,每个目标文件中都会有他们自己的符号表,汇总了全局域里面的函数名,变量名,结构体名等等。
编译器看到a.obj中调用了Add函数,但是没有Add的地址,这时链接器就会到b.obj中找到Add的地址并且把它链接到一起,进行符号表的合并。
那么在链接时遇到函数,编译器是依靠什么来找到函数的地址呢?依靠的其实就是函数名,每个函数名又都有自己的函数名修饰规则,我们接下来用gcc和g++编译器看一下汇编代码中的函数名是如何修饰的。

在这里插入图片描述

_z3Addii,
3代表3个字符,紧跟着函数名Add,然后是参数类型的缩写ii分别是int int
_z4funcidpi,
4代表4个字符,紧跟着函数名func,然后是参数类型的缩写idpi分别是int double int*
在这里插入图片描述
所以C语言没办法支持重载,因为同名函数在底层汇编中是无法区分的。

而C++可以通过函数名修饰规则,来区分同名函数。只要参数(个数、类型、类型顺序)不同,汇编底层中修饰出来的函数名就不一样,也就支持了函数重载。

4.返回值不同能否构成函数重载?

函数在调用时指定的是参数的类型,并没有指定返回值的类型。
所以在调用函数时,编译器只是通过参数来确定到底要调用哪个函数。
两个函数如果只有返回值类型不同的话,编译器是无法区分到底要调用哪个函数的,这会产生二义性。

int f(int a, int b)
{
	cout << "f(int a,int b)" << endl;
	return 0;
}
char f(int a, int b)
{
	cout << "f(int a,int b)" << endl;
	return 'A';
}
// 上面的两个函数不构成函数重载
int main()
{
	f(1, 1);
	f(2, 2);
	return 0;
}
五、引用(三姓家奴) 1.引用概念(不就取别名么)

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

ra和a的地址是相同的,说明ra和a共用同一块内存空间。

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

在这里插入图片描述
了解引用后在写链表时,就不需要传二级指针了,我们可以直接对一级指针进行引用,这样操作的时候引用参数也可以变成输出型参数。

void SlistPushBack(struct ListNode** pphead, int x)//以前C语言的用法
void  SlistPushBack(struct ListNode*& phead, int x)//给int*取别名,其实就是给指针变量取别名
{
	//有了别名之后,完全不需要二级指针了。
}

//有些教材会这样去写
typedef struct ListNode
{
	struct ListNode* next;
	int val;
}LTNode,*PLTNode;
void SlistPushBack(PLTNode& phead, int x)//这里其实就是一个结构体指针的引用
{

}
2.引用特性

a.引用在定义时必须初始化
b.一个变量可以有多个引用
c.一旦引用了某个实体,不可以在引用其他实体

void TestRef()
{
	int a = 10;
	int& ra = a;
	int& rrra=a;
	int& rrrra=a;//变量a可以有多个引用
	
	int& rra;//必须初始化引用,不能空引用	

	int b = 20;
	ra = b;
//这里是赋值操作,不是修改引用,引用一旦引用一个实体,就不能再引用其他实体,
//ra就是a,a就是ra,修改ra自然就是修改a了。

//C++里面引用无法完全替代指针,链表无法用引用实现,所以该用指针还得用指针。
//为什么实现不了捏?因为引用无法拿到下一个节点的地址呀!
}
3.引用的使用场景 3.1 内存空间销毁意味着什么?& 访问销毁的内存空间会怎样?

内存空间销毁并不是把这块内存空间撕烂了,永久性的毁灭这块儿空间,内存空间是不会消失的,他会原封不动的在那里,只不过当内存空间销毁之后,他的使用权不属于我们了,我们存到里面的数据不被保护了,有可能发生改变了。

销毁之后,我们依然可以访问到这块空间,只是访问的行为是不确定的,我们对空间的数据进行读写的行为是无法预料的。

销毁的空间可能发生的改变:
a.这块空间没有分配给别人,我们写的数据并未被清理掉,依旧可以正常访问到原来的数据
b.这块空间没有分配给别人,但是空间数据已经被销毁了,呈现出随机值
c.这块空间分配给别人,别人写了其他的数据,将我们的数据覆盖掉了。

上面的人是我们拟人化了,实际上他就是某些变量或结构体或函数栈帧等等……

3.2 做返回值(减少拷贝提高效率,修改返回值)

一、减少拷贝,提高效率

当我们要返回一棵树的时候,引用返回就可以帮我们大忙了,由于它不用拷贝,所以相比于传值返回,程序在运行上,效率提升的可不止一点。

下面图片所得结论:出了函数作用域,返回变量存在,可以使用引用返回,不存在,不可以使用引用返回。

在这里插入图片描述
汇编解释函数返回时利用临时变量
汇编解释函数返回时利用寄存器

寄存器其实也是需要拷贝的,先将局部变量的值拷贝到寄存器,然后再把寄存器的值拷到接收函数返回变量。

二、修改返回值

要知道,函数的返回值它是一个值,也就是一个临时变量,临时变量是具有常性的,是一个值,并不是一个变量。
所以如果不用引用返回的话,肯定是无法修改返回值的,编译器会报错:表达式必须是可修改的左值。

但是如果你用引用返回的话,我们就可以修改返回值了,因为引用变量是返回值的一个别名,首先引用变量就是这个返回值本身,并且引用还是一个变量,是可以修改的左值,所以我们可以利用引用做返回值来修改返回值,这一点在C语言中是无法做到的,因为C语言中返回值他只是一个值,并不是变量,无法修改,但C++有了引用之后便可做到这一点。

下面的两段代码给大家演示了C语言中,返回值无法修改的场景。

int* modify(int*pa)
{
	int b = 10;
	pa = &b;
	return pa;
}
int change(int* arr)
{
	for (int i = 0; i < 3; i++)
	{
		if (arr[i] == 2)
		{
			return arr[i];
		}
	}
}
int main()
{
	int a = 100;
	int arr[] = { 1,2,3 };
	change(arr) *= 2;//报错,表达式必须是可修改的左值
	modify(&a) *= 2;//报错,表达式必须是可修改的左值
}

下面这段代码给大家演示了C++中利用引用作为返回值来修改返回值的场景。
将数组中的偶数全部扩大二倍。

int& change(int* arr,int i)
{
	return arr[i];
}
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	for (int i = 0; i < 10; i++)
	{
		if (arr[i] % 2 == 0)
		{
			change(arr, i) *= 2;
		}
	}
	for (int i = 0; i < 10; i++)
	{
		cout << arr[i] << " ";
	}
}
3.3 做参数(减少拷贝提高效率,可作为输出型参数)

在调用函数时,形参是要做拷贝的,在它所在的函数栈帧里面,所以如果你要是传值调用,那必然在调用函数时,会做一份实参的临时拷贝,如果你是传址调用,指针变量也要开辟自己的空间,所以这些都是对程序性能的消耗。

但如果我们用引用做参数就不会有这些问题了,因为操作系统并不会给引用变量单独开辟一块空间,并且引用变量也不需要对实参进行拷贝,那就可以减少拷贝提高效率。
并且由于引用实质上就是实参本身,那么它也可以作为输出型参数,对函数外面的实参进行修改。

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;
}
4.常引用(带有const的引用) 4.1 指针/引用在赋值中,权限可以缩小,但是不能放大

权限的缩小和放大,针对的是从引用实体到引用变量的过程中,权限的变化

int main()
{
	int a = 0;
	int& ra = a;//ra既可以读到a,也可以修改a,权限的平移
	
	const int& rra = a;//rra只能读到a,并不可以修改a,这里是权限的缩小

	rra++;//rra没有修改a的权限,因为他是const引用
	a++;//a本身是int修饰,没有const,可以修改

	const int b = 1;//变量b只能被读取,不能被修改
	int& rb = b;//rb没有const修饰,可以读写b,这就是典型的权限放大,编译器会报错
	
	int& rd = 10;//常量不可以被修改,典型的权限放大。
	
	const int& rb = b;//rb有了const修饰,只能读b,不能写b,权限的平移
	
	return 0;
}
4.2 常引用做参数

a.一般引用做参数都是用常引用,也就是const+引用,如果不用const会有可能产生权限放大的问题,而常引用既可以接收只读的权限,又可以接收可读可写的权限。

b.常引用做参数并不是为了修改参数,而是为了减少拷贝提高效率。

4.3 缺省参数如何引用?

缺省参数如果想做为引用的话,必须用常引用,因为缺省参数是一个常量,是不允许被修改的,只可以读。

void func(const int& N = 10)
{

}
4.4 临时变量具有常性不能修改(传值返回,隐式/强制类型转换)

a.常引用接收传值返回

传值返回我们前面就提到过,他返回时需要依靠一个临时变量,而临时变量具有常性不能修改,所以如果想要用引用接收那就必须用常引用,必须带上const。

int Count()
{
	static int n = 0;
	n++;
	// ...
	return n;
}
int main()
{
	int& ret = Count();
	const int& ret = Count();
}

b.常引用接收临时变量

int main()
{
	const int& b = 10;

	double d = 12.34;

	cout << (int)d << endl;
	//强制类型转换,并不是改变了变量d,而是产生临时变量,输出的值也是临时变量的值。
	
	int i = d;
	//隐式类型转换,也是产生了临时变量。

	const int& ri = d;//这里引用的实体其实就是从double d 到int类型转换中间的临时变量
	
	cout << ri << endl;//这里输出的引用实际上就是double到int中间的临时变量的别名。
	return 0;
}
5.引用和指针的区别

a.语法概念上引用变量就是一个别名,不开空间,和引用实体共用一个空间。
底层实现上引用变量其实是要开空间的,因为引用在底层上是按照指针来实现的

在这里插入图片描述

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

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

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

e. 没有NULL引用,但有NULL指针

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

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

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

i. 访问实体方式不同,指针需要显式解引用,引用直接使用就好,具体细节编译器会自动处理

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

六、内联函数(不建立函数栈帧的函数,已经不是正常的函数了) 1.替代C语言中的宏

C语言中的宏在书写时,由于宏是单纯的替换,所以导致很容易出问题,例如下面,我们写一个实现两数之和的宏,大概能写出4种形式,可是这四种形式都是错的。
因为在不同的使用宏的场景下,对于宏的书写要求都是很高的。

a. 如果加分号,那么在分支语句的判断部分,会出语法错误。

b. 如果不加外层括号,可能由于运算符优先级的问题,无法得到我们想要的答案。

c. 如果内层不加括号,仅仅是加减这样的符号,都要比位操作符优先级高,这时候也无法得到我们想要的答案。

这时候,在C++中就提出了内联函数,内联函数在 ( 编译 ) 期间,编译器会用函数体来替换内联函数的调用,而不是宏那样的单纯替换

#define ADD(x,y) x+y
#define ADD(x,y) (x+y)
#define ADD(x,y) (x)+(y)
#define ADD(x,y) ((x)+(y));
int main()
{
	//不能加分号
	if (ADD(1, 2))
	{

	}

	//外层括号
	ADD(1, 2) * 3;

	//内层括号
	int a = 1, b = 2;
	ADD(a | b, a & b);//+运算符优先级高于|&
}
2.编译器根据函数体大小来决定是否展开(代码膨胀)

内联函数一般适用于频繁调用的小函数。
如果不是内联函数还频繁调用的话,就会频繁的开辟函数栈帧,这会对程序产生不小的开销,影响程序运行时的效率,内联函数不害怕这一点,因为它根本就不建立函数栈帧
同时如果内联函数体过大,编译器也会将主动权掌握在自己手里,他会决定是否在内联函数调用的地方展开函数体。
如果函数体过大,将不会展开,如果较小,就会展开,这个结论我们可以通过汇编指令来查看。

inline int Add(int x, int y)//频繁调用的小函数,推荐搞成内联函数。
{
	return x + y;
}
inline int func(int x, int y)//编译期间不会展开
{
	int ret = x + y;
	ret = x + y;
	ret += x + y;
	ret = x * y;
	ret = x + y;
	ret *= x - y;
	ret = x + y;
	ret = x / y;
	ret += x + y;
	ret /= x + y;
	ret *= x + y;
	ret = x + y;
	return ret;
}
int main()
{
	int ret = Add(1, 3);
	int ret2 = func(1, 2);

	return 0;
}

由于debug版本下我们要对代码进行调试,所以代码中不会展开内联函数体,我们需要先将工程属性设置成这样子,然后打开调试中的反汇编查看底层的汇编指令,看看编译器对于内联函数体展开的情况。
在这里插入图片描述
下面的汇编指令就可以验证我们之前的结论,内联函数体过大,编译器不展开内联函数调用的地方,函数体较小的时候编译器会在内联函数调用的地方展开。

在这里插入图片描述
函数体较长时,编译器不会展开是因为代码膨胀,假设函数体中的指令有30行,程序中内联函数调用的地方有10000处,一旦编译器展开函数体,程序就会瞬间出现30w行指令,这会疯狂增加可执行程序文件的大小,也就是安装包的大小,所以编译器不会让这样的事情发生,即使你对编译器发出了内联的请求,编译器也不会管你,说了句 ‘’ 莫挨劳资,走远点 ‘’

3.声明和定义分离(本质:内联函数无论是否被编译器当作内联处理,他的函数名和有效地址都不进符号表,与static修饰的全局函数一样,都不进符号表)

如果下面这部分知识不太清楚的话,可以看看下面这篇博文,补一下基础,因为接下来讲的东西需要用到下面的知识。

程序运行原理和预编译

如果内联函数的声明和定义分开的话,程序就会报链接错误,为什么呢?我们前面说过内联函数只是有可能将函数体展开,并不会建立函数栈帧,所以stack.obj文件的符号表就不会存放Add函数和它的地址,那在链接阶段,test.obj会根据Add的函数名字到stack.obj文件的符号表中寻找Add函数的有效地址,但可惜符号表中别说地址了,连函数名都没有,自然目标文件之间的链接就无法成功,编译器就无法识别test.cpp中的Add到底是什么,光有个函数声明,没有函数定义编译器也就会报错:无法解析的外部符号。
在这里插入图片描述

在这里插入图片描述
结论:内联函数在定义时不要搞到.c文件里定义了,直接在.h文件里面定义就好,不要把定义和声明分开,这样在展开.h文件之后,函数体就在那里,链接阶段就不会在去找函数的地址了,因为函数就在他自身的目标文件里面。

七、auto用法

1.补一下C语言芝士
第一行const直接修饰的是指针变量p1,所以指针变量p1本身不能修改,它指向的内容还是可以修改的,但p1现在被你搞成const修饰了,所以它必须被初始化,因为它只有一次赋值的机会,就是在初始化的那个地方,不能说你后面在去给p1赋值,这样不可以。
第二行const修饰的不是二级指针p2,修饰的是二级指针p2所指向的内容,那么指针变量p2是没有被const修饰的,所以p2可以不初始化,但p2所指向的内容是不可以发生改变的,因为const修饰的是p2指向的内容。

注意:语法检查的时候,是不会先替换typedef内容的,他会先直接分析你的代码是否在语法上存在问题,比如第一行代码,编译器是不会把pstring替换为char的,如果替换为char当然这句语句就没有问题了,不初始化也OK,但是编译器看的不是替换之后的,他在预编译之前就发现你这段代码语法有问题,所以编译器就直接会报错了,因为他认为p1就是个变量,你用const修饰了,那就必须给初始值,第二行代码编译器认为p2是个指针,因为它看到*的存在了,所以它认为const修饰的是p2指向的内容,不是p2本身

出现分析问题错误的原因,其实就是我们思考的是替换之后的结果,编译器在分析语法时,只会看到代码本身,根本不存在替换不替换这么一说。

typedef char* pstring;
int main()
{
	 const pstring p1;    // 编译成功还是失败?
	 const pstring* p2;   // 编译成功还是失败?
	 pstring* const p2;//如果这样写,const修饰的才是p2指针变量本身
	 return 0;
}

2. auto用于自动推导类型
在这里插入图片描述
3.auto利用逗号运算符,一行定义多个变量时,这些变量必须是相同的类型。

因为编译器实际上只对第一个类型进行推导,然后用推导出来的类型来定义其他变量,所以你定义的多个变量就必须是同一类型的。

void TestAuto()
{
    auto a = 1, b = 2; //必须是相同的类型
    auto c = 3, d = 4.0;  // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}

4.auto在推导类型时,如果想推导出引用类型则必须在auto后面加&,在推导指针类型时,auto后面加不加*都可以

int main()
{
    int x = 10;
    auto a = &x;
    auto* b = &x;//加不加*无所谓
    auto& c = x;//必须加&
    cout << typeid(a).name() << endl;//typeid().name()可以拿到类型的字符串
    cout << typeid(b).name() << endl;
    cout << typeid(c).name() << endl;
    return 0;
}

5. auto不能作为函数参数,因为无法事先确定需要开辟函数栈帧的大小

void TestAuto(auto a)//编译器无法推导a的类型,开辟栈帧时也就不知道开多大。
{}

6. auto不能用来声明数组

void TestAuto()
{
    int a[] = {1,2,3};
    auto b[] = {4,5,6};//这是错误的声明方式
}
八、基于范围的for循环

a. C++11中引入了基于范围的for循环,for后面的括号中有两部分组成,第一部分是在范围内用于迭代的变量,第二部分表示迭代的范围。

void TestFor()
{
	int array[] = { 1, 2, 3, 4, 5 };
	for(auto& e : array)//将迭代变量搞成引用,这样可以直接操作数组中的数据。
	     e *= 2;
	for(auto e : array)
	     cout << e << " ";
	return 0;
}

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

void func(int array[])//穿过来的array不是数组,而是指针。
{
    for(auto& e : array)
        cout<< e <
九、指针空值nullptr ==> (void*)0

在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。
下面是stddef.h头文件的部分源码,所以C++98对于指针空值是没有确定的值的。

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

C++11为了避免这样的情况发生,定义了关键字nullptr来表示指针空值,弥补C++98中有关NULL空指针的bug。

void f(int)
{
	cout << "1" << endl;
}
void f(int*)
{
	cout << "2" << endl;
}
int main()
{
	f(0);
	f(NULL);//这里原本想调用输出2的结果,但NULL被编译器默认为0,就调用了输出为1的函数,所以我们要想调用输出2的函数,就用nullptr关键字。
	
	f(nullptr);
	//nullptr就是(void*)0
	return 0;
}

下面是程序运行结果
在这里插入图片描述

  1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
  2. 在C++11中,sizeof(nullptr) 与 sizeof((void * )0)所占的字节数相同
  3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。

你是否还在寻找稳定的海外服务器提供商?风纳云www.fengnayun.com海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧


/template/Home/Dawn/PC/Static

选择风纳云,也许是您成就一番大事业的开端

注册账号