【C++】C++入门

news2024/11/26 22:45:16

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

文章目录

  • 一、命名空间(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.声明和定义分离(本质:内联函数不会建立函数栈帧==>符号表没有内联函数名)
  • 七、auto用法
  • 八、基于范围的for循环
  • 九、指针空值nullptr ==> (void*)0



一、命名空间(namespace)

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

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

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

#include <stdio.h>
#include <stdlib.h>
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 <stdio.h>

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 <iostream>
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 <iostream>
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.声明和定义分离(本质:内联函数不会建立函数栈帧==>符号表没有内联函数名)

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

程序运行原理和预编译

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

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

七、auto用法

1. auto用于自动推导类型
在这里插入图片描述
2. auto不能作为函数参数,因为无法事先确定需要开辟函数栈帧的大小

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

3. auto不能用来声明数组

void TestAuto()
{
    int a[] = {1,2,3};
    auto b[] = {456};//这是错误的声明方式
}

八、基于范围的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 <<endl; 
}

九、指针空值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。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/88776.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Maven pom.xm javafx-maven-plugin打包javafx 应用及调试

1、添加 javafx-maven-plugin到 plugin内 <plugin><groupId>com.zenjava</groupId><artifactId>javafx-maven-plugin</artifactId><version>8.8.3</version><configuration><!-- 启动类 --><mainClass>com.test.d…

【春招必备】Java面试题,面试加分项,从jvm层面了解线程的启动和停止

前言 Spring 作为一个轻量级的 Java 开发框架&#xff0c;将面向接口的编程思想贯穿整个 Java 系统应用&#xff0c;因此在 Java 面试中常被提。本次介绍的主要是解析面试过程中如果从源码角度分析常见的问题&#xff0c;为了方便大家阅读&#xff0c;小编这里还整理了一份微服…

零拷贝技术面试题

文章目录1 零拷贝的介绍2 传统的IO的执行流程3 零拷贝涉及的技术点3.1 内核空间和用户空间3.2 用户态和内核态3.3 DMA技术4 零拷贝实现的几种方式4.1 mmapwrite4.2 sendfile4.3 sendfileDMA scatter/gather实现的零拷贝5 java提供的零拷贝方式5.1 Java NIO对mmap的支持5.2 Java…

抖音快手如何轻松接入虚拟人直播

在上一篇文章零基础开启元宇宙——创建虚拟形象中&#xff0c;我们实现了创建虚拟形象&#xff0c;接下来我们可以利用虚拟形象“为所欲为”。今天我们利用虚拟形象在短视频平台如快手、抖音中直播&#xff0c;对于不希望露脸的主播们这是可是一大利器呀&#xff01;话不多说&a…

【Unity项目实战】手把手教学:飞翔的小鸟(5)背景滚动

承接上一篇&#xff1a;【Unity项目实战】手把手教学&#xff1a;飞翔的小鸟&#xff08;4&#xff09;文本添加&#xff0c;我们已经使得主角小鸟接触到地面后跳转到Game Over状态&#xff0c;接下来我们将继续往下&#xff0c;讲解得分机制。 一、重新进入游戏 根据上篇最后…

网络流量回溯分析助力企业实现高效率运维(一)

背景 汽车配件电子图册系统是某汽车集团的重要业务系统。业务部门反映&#xff0c;汽车配件电子图册调用图纸时&#xff0c;出现访问慢现象。 某汽车总部已部署NetInside流量分析系统&#xff0c;使用流量分析系统提供实时和历史原始流量。本次分析重点针对汽车配件电子图册系…

[附源码]Node.js计算机毕业设计二手书交易软件设计与实现Express

项目运行 环境配置&#xff1a; Node.js最新版 Vscode Mysql5.7 HBuilderXNavicat11Vue。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分离等等。 环境需要 1.运行环境&#xff1a;最好是Nodejs最新版&#xff0c;我…

数据技术之数据挖掘

第7章 数据挖掘 1.什么是数据挖掘 数据挖掘(Data Mining)就是从大量的数据中&#xff0c;提取隐藏在其中的&#xff0c;事先不知道的、但潜在有用的信息的过程。数据挖掘的目标是建立一个决策模型&#xff0c;根据过去的行动数据来预测未来的行为。 2.阿里数据挖掘平台 阿里…

Java-1213

JVM历程 Sun Classic VM 1996年发布&#xff0c;世界上第一款商用Java虚拟机&#xff0c;JDK1.4时被淘汰&#xff0c;现在hotspot内置了此虚拟机 这款虚拟机只提供了解释器&#xff08;现在主流的虚拟机还会提供即时编译器JIT&#xff09;解释器和JIT两者用一个就可以让程序执…

分享一种 ConstraintLayout 让TextView 自适应的同时,还不超出限制范围的方式

分享一种 ConstraintLayout 让TextView 自适应的同时&#xff0c;还不超出限制范围的方式 不知道大家有没有遇到这种布局需求&#xff1a; 上图布局很简单&#xff0c;ImageView 中间的TextView View ImageView&#xff0c;需求是中间的 TextView 宽度需要根据内容来展示&…

OpenMLDB 实时引擎性能测试报告

OpenMLDB 提供了一个线上线下一致性的特征平台。其中&#xff0c;为了支持低延迟高并发的在线实时特征计算&#xff0c;OpenMLDB 设计实现了一个高性能的实时 SQL 引擎。本报告覆盖了 OpenMLDB 实时 SQL 引擎的性能测试&#xff0c;包含了在较为复杂的负载、典型配置下的各种性…

多线程知识笔记(四)-----volatile、wait方法、notify方法

文章目录1、volatile关键字2、volatile和synchronized对比3、wait和notify方法1、volatile关键字 先看例子&#xff1a; class Counter {public int flag 0; }public class Test4{public static void main(String[] args) {Counter counter new Counter();Thread t1 new Th…

如何使用Footrace 钱包监控功能和设置自定义的交易警报

2022-06-12 本文将介绍如何使用 Footrace 监控 CEX 的钱包地址并设置自定义警报。 什么是 Footrace&#xff1f; Footrace (Foot Trace) 是一个多链的钱包追踪监控平台&#xff0c;可以监控CEX、DEX、鲸鱼、聪明钱、或任何你想关注的地址的钱包。 Footrace 帮助投资者保护他…

有了这几个软件安全测试工具,编写安全测试报告再也不愁

软件的安全是开发人员、测试人员、企业以及用户共同关心的话题&#xff0c;尤其是软件产品的使用者&#xff0c;因为系统中承载着用户的个人信息、人际互动、管理权限等各类隐私海量关键数据。软件安全测试工作不仅是为了用户&#xff0c;更牵扯到许多的利益共同体。因此软件安…

2854-40-2,环二肽cyclo(Pro-Val)

Component of coffee flavor 咖啡香精成分 在烘焙咖啡中检测到Cyclo(Pro-Val)和其他含pro的二酮哌嗪类化合物。这些化合物的含量随着烘焙强度的增加而增加&#xff0c;它们增加了苦味。这种味道苦涩的环二肽也在可可、巧克力、牛肉和奶酪等其他食品中被检测到。Cyclo(Pro-Val)显…

Linux物理内存管理——会议室管理员如何分配会议室

之前学习了站在内存的角度去看内存&#xff0c;看到的都是虚拟内存&#xff0c;这些虚拟内存总是要映射到物理页面的&#xff0c;这一篇文章来学习物理内存是如何管理的。 物理内存的组织方式 之前学习虚拟内存的时候&#xff0c;当涉及物理内存的映射的时候&#xff0c;总是…

不会还有人不知道如何搭建【关键字驱动自动化测试框架】吧 ?

前言 这篇文章我们将了解关键字驱动测试又是如何驱动自动化测试完成整个测试过程的。关键字驱动框架是一种功能自动化测试框架&#xff0c;它也被称为表格驱动测试或者基于动作字的测试。关键字驱动的框架的基本工作是将测试用例分成四个不同的部分。首先是测试步骤&#xff0…

MobileNet网络模型(V1,V2,V3)

MobileNet网络中的亮点&#xff1a;DW卷积&#xff0c;增加了两个超参数&#xff0c;控制卷积层卷积核个数的α&#xff0c;控制输入图像大小的β&#xff0c;这两个超参数是我们人为设定的&#xff0c;并不是学习到的。BN batch normal批规范化&#xff0c;为了加快训练收敛速…

大数据MapReduce学习案例:数据去重

文章目录一&#xff0c;案例分析&#xff08;一&#xff09;数据去重介绍&#xff08;二&#xff09;案例需求二&#xff0c;案例实施&#xff08;一&#xff09;准备数据文件&#xff08;1&#xff09;启动hadoop服务&#xff08;2&#xff09;在虚拟机上创建文本文件&#xf…

大数据MapReduce学习案例:TopN

文章目录一&#xff0c;案例分析&#xff08;一&#xff09;TopN分析法介绍&#xff08;二&#xff09;案例需求二&#xff0c;案例实施&#xff08;一&#xff09;准备数据文件&#xff08;1&#xff09;启动hadoop服务&#xff08;2&#xff09;在虚拟机上创建文本文件&#…