【C++初阶:入门总结】命名空间 | 缺省参数 | 函数重载 | 引用 | 内联函数

举报
跳动的bit 发表于 2022/04/02 10:41:54 2022/04/02
2.1k+ 0 2
【摘要】 点到为止,并不深入。其次建工程这里就不说了,在之前的基础上 —— 文件名.cpp 就可以了,C 语言有 32 个关键字,而 C++ 有 63 个关键字,C 语言的关键字在 C++ 中继续可以使用

【写在前面】

点到为止,并不深入。其次建工程这里就不说了,在之前的基础上 —— 文件名.cpp 就可以了

一、C++关键字 (C++98)

💨 C 语言有 32 个关键字,而 C++ 有 63 个关键字,C 语言的关键字在 C++ 中继续可以使用

    ps:在本章中不对关键字进行详讲

在这里插入图片描述

❗ I/O ❕

//C++兼容C绝大多数语法 
#include<stdio.h>
int main01()
{
	printf("Hello CPLUSPLUS\n");
	return 0;
}
//但C++通常会这样写
#include<iostream>
using namespace std;
int main()
{
	cout << "Hello CPLUSPLUS" << endl;
	return 0;
}

二、命名空间

对比 C 语言,一般 C++ 每增加一个语法都是为了解决一些 C 语言做不到的事或者是 C 语言做的不好的地方

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

❗ C语言命名冲突示例 ❕

在不同的作用域中,可以定义同名的变量;在同一作用域下,不能定义同名的变量

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

📝 分析:

可以看到我们定义了一个 rand 变量输出是没有问题的,但如果包含了 stdlib 头时就会产生命名冲突,此时我们的 rand 变量就和库里的产生冲突;
实际除此之外在大型项目开发时,还有可能和同事之间发生冲突。C 语言面对这种问题是无法解决的,而对于这种场景 C++ 使用了命名空间

💦 命名空间定义

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

⚠ 注意一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中

//1. 普通的命名空间
namespace N1 // N1为命名空间的名称
{
 	//命名空间中的内容,既可以定义变量,也可以定义函数
 	int a;
 	int Add(int left, int right)
 	{
 		return left + right;
 	}
}
//2. 命名空间可以嵌套
namespace N2
{
	 int a;
	 int b;
	 int Add(int left, int right)
	 {
		 return left + right;
	 }
 
 	namespace N3
 	{
		 int c;
		 int d;
 		int Sub(int left, int right)
 		{
			 return left - right;
		}
	}
}
//3. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中
namespace N1
{
 	int a = 10;
}
namespace N1
{
 	int b = 20;
}

❗ 用命名空间解决变量 rand 和 stdlib 库里的命名冲突 ❕

#include<stdio.h>
#include<stdlib.h>
namespace WD//定义了一个命名空间域
{
	int rand = 10;//定义变量
	int Add(int x, int y)//定义函数
	{
		return x + y;	
	}
	struct Node//定义结构体类型
	{
		struct Node* next;
		int val;	
	};
}
int main()
{
	printf("%p\n", rand);//函数指针
	printf("%d\n", WD::rand);//rand变量;‘::’叫做域作用限定符
	WD::Add(3, 5);//调用函数
	struct WD::Node node1;//结构体
	
	return 0;
}

❗ 嵌套命名空间 ❕

#include<stdio.h>
#include<stdlib.h>
namespace WD
{
	int w = 20;
	int h = 10;
	namespace WH//嵌套命名空间域
	{
		int w = 10;
		int h = 20;
	}
}
int main()
{
	printf("%d\n", WD::WH::h);//访问嵌套命名空间
	return 0;
}

❗ 相同名称的命名空间 ❕

namespace WD
{
	int a = 10;
	int b = 20;
	namespace WH
	{
		int a = 20;
		int b = 10;
	}
}
namespace WD//相同名称的命名空间
{
	int rand = 50;
	//int a = 10;//err,在合并的时候冲突了
}

💦 命名空间使用

❓ 如何使用命名空间里的东西 ❔

1️⃣ 全部直接展开到全局

using namespace WD;
//using namespace std;//std是包含C++标准库的命名空间

  💨优点:用起来方便

  💨缺点:自己定义的东西也会暴露,导致命名污染

2️⃣ 访问每个命名空间中的东西时,指定命名空间

std::rand;

  💨优点:不存在命名污染

  💨缺点:如果要去访问多个命名空间里的东西时,需要一一指定

3️⃣ 仅展开常用的内容

using WD::Node;
using WD::Add;

  💨优点:不会造成大面积的污染;把常用的展开后,也不需要一一指定


namespace WD
{
	int a = 10;
	int b = 20;
	//...
}
int main()
{
	//using namespace WD;//1.展开WD空间所有内容
	//printf("%d\n", WD::a);//2.指定命名空间
	//using WD::a;//3.仅展开a
}

❓ 上面说了 C++ 把库里的东西都放到 std 这个域里了,那直接展开 std 不就行了或者包头 ❔

  注意

   1️⃣ #include <iostream>:展开定义

   2️⃣ using namespace std;:允许用

  所以两者缺一不可

三、C++中的I/O

❗ 说明 ❕

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

#include<iostream>
using namespace std;
int main()
{
	cout<<"Hello world!!!"<<endl;
	return 0;
}

❗ 上面这种写法其实有一定的规范缺陷 ❕

C++ 为什么用一个库去包它的命名空间,就是怕我们定义的与库冲突。但是一句 using namespace std; 就把库展开了,那么定义 std 的优势就无了。所以在项目中比较规范的写法如下:

#include<iostream>
//展开常用————工程项目中常见的对命名空间的用法
using std::cout;
using std::endl;
int main()
{
	//只要是库里的都得指定std
	//std::cout << "Hello world!!!" << std::endl
	//但如果cout经常要用的话,就在上面单独展开
	cout << "Hello world!!!" << endl;
	return 0;
}

⚠ 注意,在日常做题时不需要像项目那样规范,可以直接全部展开 std

❗ cout && cin ❕

cout 和 cin 类似 C 语言的 printf 和 scanf,这里只是先了解下,因为对于 C 语言中的 I/O 是函数,而 C++ 是对象

#include<iostream>
using namespace std;
int main()
{
	int n;
	cin >> n;//cin可以理解为键盘;>>可以理解为输入运算符/流提取运算符(官方)
	int* a = (int*)malloc(sizeof(int) * n);
	for(int i = 0; i < n; ++i)
	{
		cin >> a[i];
	}
	for(int i = 0; i < n; ++i)
	{
		cout << a[i] << " ";//cout可以理解为控制台;<<可以理解为输出运算符/流插入运算符(官方)
	}
	cout << endl;//等价于count << '\n';
	return 0;
}

❗ 对于I/O,C++比C便捷 ❕

C++ 不需增加数据格式控制,比如:整形 – %d,字符 – %c,它会自动实别

#include<iostream>
using namespace std;
int main01()
{
	int n;
	cin >> n;
	double* a = (double*)malloc(sizeof(int) * n);
	for(int i = 0; i < n; ++i)
	{
		cin >> a[i];//它会自动实别
	}
	for(int i = 0; i < n; ++i)
	{
		cout << a[i] << " ";//它会自动实别
	}
	count << endl;
	return 0;
}
//挺爽的吧!!!
int main()
{
	char ch = 'C';
	int i = 10;
	int* p = &i;
	double d = 3.14;
	double b = 3.1415926;
	cout << ch << endl;
	cout << i << endl;
	cout << p << endl;
	cout << d << endl;
	cout << b << endl;//注意对于浮点数,C++最多输出5位,当然也可以指定输出多少位,但是相对指定的方式(比较麻烦)可以配合printf使用
	return 0;
}

❗ 当然C++也并不完美 ❕

#include<iostream>
using namespace std;
struct Student
{
	char name[10];
	int age;
};
int main()
{
	struct Student s = { "小三", 18 };
	//输出结构体信息。类似下面场景printf更好
	//对于这种情况C++中不用纠结用啥,用习惯自己的就好
	cout << "名字:" << s.name << " " << "年龄:" << s.age << endl;
	printf("名字:%s 年龄:%d\n", s.name, s.age);
	return 0;
}

四、缺省参数

💦 缺省参数概念

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

#include<iostream>
using namespace std;
void TestFunc(int a = 0)//参数缺省值
{
	cout << a << endl;
}
int main()
{
	TestFunc();//没有指定实参,使用缺省值
	TestFunc(10);//指定实参,使用实参
	return 0;
}

❗ 有什么用 ❕

比如在 C 语言中有个很苦恼的问题是写栈时,不知道要开多大的空间,之前我们是如果栈为空就先开 4 块空间,之后再以 2 倍走,如果我们明确知道要很大的空间,那么这样就只能一点一点的接近这块空间,就太 low 了。但如果我们使用缺省,明确知道不需要太大时就使用默认的空间大小,明确知道要很大时再传参

#include<iostream>
using namespace std;
namespace WD
{
	struct Stack
	{
		int* a;
		int size;
		int capacity;	
	};
}
using namespace WD;
void StackInit(struct Stack* ps)
{
	ps->a = NULL; 
	ps->capacity = 0;
	ps->size = 0;
}
void StackPush(struct Stack* ps, int x)
{
	if(ps->size == ps->capacity)
	{
		//ps->capacity *= 2;//err
		ps->capacity == 0 ? 4 : ps->capacity * 2;//这里就必须写一个三目
	}
}

void StackInitCpp1(struct Stack* ps, int defaultCP)
{
	ps->a = (int*)malloc(sizeof(int) * defaultCP);
	ps->capacity = 0;
	ps->size = defaultCP;
}
void StackInitCpp2(struct Stack* ps, int defaultCP = 4)//ok
{
	ps->a = (int*)malloc(sizeof(int) * defaultCP);
	ps->capacity = 0;
	ps->size = defaultCP;
}
int main()
{
	//假设明确知道这里至少需要100个数据到st1
	struct Stack st1; 
	StackInitCpp1(&st1, 100);
	//假设不知道st2里需要多少个数据 ———— 希望开小点
	struct Stack st2;  
	StackInitCpp2(&st1);//缺省
	return 0;
}

💦 缺省参数分类

❗ 全缺省参数 ❕

void TestFunc(int a = 10, int b = 20, int c = 30)
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl;
	cout << endl;
}
int main()
{
	//非常灵活,
	TestFunc();
	TestFunc(1);
	TestFunc(1, 2);
	TestFunc(1, 2, 3);	
	//TestFunc(1, , 3);//err,注意它没办法实现b不传,只传a和b,也就是说编译器只能按照顺序传
	return 0;
}

⚠ 注意:

  1️⃣ 全缺省参数只支持顺序传参

❗ 半缺省参数 ❕

//void TestFunc(int a, int b = 10, /*int f, - err*/ int c = 20);//err,

void TestFunc(int a, int b = 10, /*int f, int x = y, -> err*/ int c = 20)
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl;
	cout << endl;
}
int main()
{
	//TestFunc();//err,至少得传一个,这是根据形参有几个非半缺省参数确定的
	TestFunc(1);
	TestFunc(1, 2);
	TestFunc(1, 2, 3);	
	return 0;
}
//a.h
void TestFunc(int a = 10);
//a.cpp
void TestFunc(int a = 20)
{}

⚠ 注意:

  1️⃣ 半缺省参数必须从右往左依次来给出,且不能间隔着给

  2️⃣ 缺省参数不能在函数声明和定义中同时出现

  3️⃣ 缺省值必须是常量或者全局变量

  4️⃣ C 语言不支持缺省

五、函数重载

自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。

比如:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前者是 “谁也赢不了!”,后者是 “谁也赢不了!”
在这里插入图片描述

💦 函数重载概念

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

//1.参数个数
int Add(int x)
{}
int Add(int x, int y)
{}
//2.参数类型
int Add(int x, int y)
{}
int Add(int x, double y)
{}
//3.参数顺序
int Add(int x, float y)
{}
int Add(float y, int x)
{}

❗ 怎么调用 ❕

int Add(int left, int right)
{
	return left + right;
}
double Add(double left, double right)
{
	return left + right;
}
long Add(long left, long right)
{
	return left + right;
}
int main()
{
	//这里分别调用三种不同的函数
	Add(10, 20);//默认整型
	Add(10.0, 20.0);//浮点型
	Add(10L, 20L);//长整型
	return 0;
}

⚠ 注意

对于函数重载,如果你想调用某一函数,那么在传参的时候就必须写好对应参数的类型、个数、顺序,比如 float 数据,就要写 3f,因为这样它才能找到对应的函数调用

💦 名字修饰 (name Mangling)

❓ 为什么C++支持函数重载,而C语言不支持函数重载呢,以 Linux 环境下演示 ❔

注意这里就不细讲 Linux 的一些指令了,具体的我都写在或者将写在 《Linux专栏》里了

先在 Linux 下以两种编译方式编译函数重载的程序,这里有三个文件 :f.h、f.c、test.c

  ▶ 可以看到以 C 语言去编译函数重载报错了

  ▶ 可以看到以 C++ 去编译函数重载是可以的

在这里插入图片描述

在正式探究前我们先回忆下,注意 C/C++ 都有类似的过程,但肯定是有区别的,现在我们要找的就是那个区别
在这里插入图片描述

  这里我们对照着程序走一遍过程

在这里插入图片描述

然后再回到我们的问题

  ❓ C语言不支持函数重载 ❔

   C 编译器,直接用函数名关联,函数名相同时,无法区分

  ❗ 验证 ❕

   通过命令 objdump -S 去查看 C 编译生成的符号表

在这里插入图片描述

   通过命令 objdump -S 去查看 C++ 编译生成的符号表

在这里插入图片描述

  ❓ C++ 支持函数重载 ❔

   从上就可以看出对于函数重载 C++ 相对于 C 语言引入了某种规则

   C++ 引用了《函数名修饰规则 (Linux下)》不能直接用函数名,要对函数名进行修饰 (带入参数的特点修饰)

在这里插入图片描述

    📝 说明:

     1️⃣ _Z 是前缀

     2️⃣ 3 是函数名的长度

     3️⃣ Add 是函数名

     4️⃣ ii / dd 是函数参数类型的首字母,如果是 int* i,那么就是 pi

💨小结:

  C++ 是支持函数重载的,函数名相同,只要参数不同,修饰在符号表中的名字也不同,那么就能区分了

❗ Windows下函数名修饰规则 ❕

在这里插入图片描述
🍳【扩展学习:C/C++函数调用约定和名字修饰规则】

C++函数重载

C/C++ 函数调用约定

❓ 下面两个函数属于函数重载吗 (编译器能不能只实现返回值不同,就能构成重载) ❔

short Add(short left, short right)
{
	return left+right;
}
int Add(short left, short right)
{
	return left+right;
}

显然《函数名修饰规则》并不能让它们支持重载。

 ❓ 其次如果想自己定义《函数名修饰规则》让只有返回值不同的函数支持重载可以吗 ❔

  💨 编译器层面是可以区分的

   short Add; -> _Z3sAdd

   int Add; -> _Z3iAdd

  💨 语法调用层面有严重歧义

   Add(3, 5); ???

❓ 下面两个函数能形成函数重载吗?❔

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

虽然两个函数的参数是缺省值和非缺省值,但是依旧不影响修饰出来的函数名,所以不能构成函数重载

💦 extern"C"

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

extern "C" int Add(int left, int right);
int main()
{
	Add(1,2);
	return 0; 
}

💨总结

  1️⃣ C++ 项目可以调用 C++ 库,也可以调用 C 的库,C++ 是直接兼容 C 的

  2️⃣ C 项目可以调用 C 库,也可以使用 extern"C" 调用 C++ 库 (C++ 提供的函数加上 extern"C")

六、引用

💦 引用概念

引用不是新定义一个变量,而是给已存在变量取了一个别名,语法理解上程序不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间

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

在这里插入图片描述

❗ 类型& 引用变量名(对象名) = 引用实体 ❕

int main()
{
	//有一块空间a,后面给a取了三个别名b、c、d
	int a = 10;
	int& b = a;
	int& c = a;
	int& d = b;
	
	//char& d = a;//err,引用类型和引用实体不是同类型(这里有争议————char a = b[int类型],留个悬念,下面会解答)

	//会被修改
	c = 20;
	d = 30;
	
	return 0;
}

⚠ 注意

1️⃣ 引用类型必须和引用实体是同种类型

2️⃣ 注意区分 ‘&’ 取地址符号

💦 引用特性

1️⃣ 引用在定义时必须初始化

2️⃣ 一个变量可以有多个引用

3️⃣ 引用一旦引用一个实体,再不能引用其他实体

int main()
{
	//int& e;//err
	
	int a = 10;
	int& b = a;
	//这里指的是把c的值赋值于b
	int c = 20;
	b = c;
	
	return 0;
}

💦 常引用

void TestConstRef()
{
	const int a = 10;
	//int& ra = a; //该语句编译时会出错,a为常量;由const int到int
	const int& ra = a;//ok
	
	int b = 20;
	const int& c = b; //ok,由int到const int
	//b可以改,c只能读不能写
	b = 30;
	//c = 30;//err
	//b、c分别起的别名的权限可以是不变或缩小
	int& d = b;//ok
	//int& e = c//err
	const int& e = c;//ok
	
	//int& f = 10; // 该语句编译时会出错,b为常量
	const int& g = 10;//ok

	int h = 10;
	double i = h;//ok
	//double& j = h;//err
	const double& j = h;//ok
	//?为啥h能赋值给i了(隐式类型转换),而给h起一个double类型的别名却不行————如果是仅仅是类型的问题那为啥加上const就行了?
	//double i = h;并不是直接把h给i,而是在它们中间产生了一个临时变量(double类型、常量),并利用这个临时变量赋值
	//也就是说const double& j = h;就意味着j不是直接变成h的别名,而是变成临时变量(doublde类型)的别名,但是这个临时变量是一个常量,这也解释了为啥需要加上const
}

💨小结

1️⃣ 我能否满足你变成别名的条件:可以不变或者缩小你读写的权限 (const int -> const int 或 int -> const int),而不能放大你读写的权限 (const int -> int)

2️⃣ 别名的意义可以改变,并不是每个别名都跟原名有一样的权限

3️⃣ 不能给类型不同的变量起别名的真正原因不是类型不同,而是隐式类型转换后具有常性了

❗ 常引用的意义 (举例栈) ❕

typedef struct Stack
{
	int* a;
	int top;
	int capacity;
}ST;
void InitStack(ST& s)//传引用是为了形参的改变影响实参
{//...}
void PrintStack(const ST& s)//1.传引用是为了减少拷贝 2. 同时保护实参不会被修改
{//...}
void Test(const int& n)//即可以接收变量,也可以接收常量
{//...}
int main()
{
	ST st;
	InitStack(st);
	//...
	PrintStack(st);

	int i = 10;
	Test(i);
	Test(20);
	
	return 0;
}

💨小结

1️⃣ 函数传参如果想减少拷贝使用引用传参,如果函数中不改变这个参数最好使用 const 引用传参

2️⃣ const 引用的好处是保护实参,避免被误改,且它可以传普通对象也可以传 const 对象

💦 使用场景

❗ 1、做参数 ❕

void Swap1(int* p1, int* p2)
{
	int temp = *p1;
	*p1 = *p2;
	*p2 = temp;
}
void Swap2(int& rx, int& ry)
{
	int temp = rx;
	rx = ry;
	ry = temp;
}
int main()
{
	int x = 3, y = 5;
	Swap1(&x, &y);//C传参
	Swap2(x, y);//C++传参
	return 0;
}

💨在 C++ 中形参变量的改变,要影响实参,可以用指针或者引用解决

意义:指针实现单链表尾插 || 引用实现单链表尾插

指针
在这里插入图片描述

引用

void SListPushBack(SLTNode*& phead, int x)
{
	//这里phead的改变就是plist的改变
}
void TestSList2()
{
	SLTNode* plist = NULL;
	SListPushBack(plist, 1);
	SListPushBack(plist, 2);
}

有些书上喜欢这样写 (不推荐)

typedef int SLTDataType;
typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode, *PSLTNode;
void SListPushBack(PSLTNode& phead, int x)
{
	//...
}

❗ 2、做返回值 ❕

2.1、传值返回

//传值返回
int Add(int a, int b)
{
	int c = a + b;
	return c;//需要拷贝
}
int main()
{
	int ret = Add(1, 2);//ok, 3
	Add(3, 4);
	cout << "Add(1, 2) is :"<< ret <<endl;
	return 0;
}

💨Add 函数里的 return c; —— 传值返回,临时变量作返回值。如果比较小,通常是寄存器;如果比较大,会在 main 函数里开辟一块临时空间

怎么证明呢

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

💨从上面就可以验证 Add 函数的返回值是先存储在临时空间里的

2.2、传引用返回

//传引用返回
int& Add(int a, int b)
{
	int c = a + b;
	return c;//不需要拷贝
}
int main()
{
	int ret = Add(1, 2);//err, 3
	Add(3, 4);
	cout << "Add(1, 2) is :"<< ret <<endl;
	return 0;
}

💨结果是不确定的,因为 Add 函数的返回值是 c 的别名,所以在赋给 ret 前,c 的值到底是 3 还是随机值,跟平台有关系 (具体是平台销毁栈帧时是否会清理栈帧空间),所以这里的这种写法本身就是越界的 (越界抽查不一定报错)、错误的

发现这样也能跑,但诡异的是为啥 ret 是 7

//传引用返回
int& Add(int a, int b)
{
	int c = a + b;
	return c;
}
int main()
{
	int& ret = Add(1, 2);//err, 7
	Add(3, 4);
	cout << "Add(1, 2) is :"<< ret <<endl;
	return 0;
}

💨在上面我们在 VS 下运行,可以得出编译器并没有清理栈帧,那么这里进一步验证引用返回的危害

虽然能正常运行,但是它是有问题的
在这里插入图片描述
小结引用做返回值

  1️⃣ 出了 TEST 函数的作用域,ret 变量会销毁,就不能引用返回

  2️⃣ 出了 TEST 函数的作用域,ret 变量不会销毁,就可以引用返回

  3️⃣ 引用返回的价值是减少拷贝

❓ 观察并剖析以下代码 ❔

int main()
{
	int x = 3, y = 5;
	int* p1 = &x;
	int* p2 = &y;
	int*& p3 = p1;
	*p3 = 10;
	p3 = p2;
	return 0;
}

在这里插入图片描述

💦 函数参数及返回值 ———— 传值、传引用效率比较

#include <time.h>
#include<iostream>
using namespace std;
struct A { int a[10000]; };
A a;
void TestFunc1(A a) {}
void TestFunc2(A& a) {}
A TestFunc3() { return a; }
A& TestFunc4() { return 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;
}
void TestReturnByRefOrValue()
{
	// 以值作为函数的返回值类型
	size_t begin1 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc3();
	size_t end1 = clock();
	// 以引用作为函数的返回值类型
	size_t begin2 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc4();
	size_t end2 = clock();
	// 计算两个函数运算完成之后的时间
	cout << "TestFunc1 time:" << end1 - begin1 << endl;
	cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main()
{
	//传值、传引用效率比较
	TestRefAndValue();
	cout << "----------cut----------" << endl;
	//值和引用作为返回值类型的性能比较
	TestReturnByRefOrValue();
	return 0;
}

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

💦 引用和指针的区别

1️⃣ 语法概念 1️⃣

引用就是一个别名,没有独立空间,和其引用实体共用同一块空间

指针变量是开辟一块空间,存储变量的地址

int main()
{
	int a = 10;
	int& ra = a;
	cout<<"&a = "<<&a<<endl;
	cout<<"&ra = "<<&ra<<endl;

	int b = 20;
	int* pb = &b;
	cout<<"&b = "<<&b<<endl;
	cout<<"&pb = "<<&pb<<endl;
	return 0;
}

2️⃣ 底层实现 2️⃣

引用和指针是一样的,因为引用是按照指针方式来实现的

int main()
{
	int a = 10;
 
	int& ra = a;
	ra = 20;
 
	int* pa = &a;
	*pa = 20;
 
	return 0;  
}

这里我们对比一下 VS 下引用和指针的汇编代码可以看出来他俩是同根同源
在这里插入图片描述

引用和指针的不同点:

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

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

3 、没有 NULL 引用,但有 NULL 指针

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

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

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

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

8、引用比指针使用起来相对更安全,指针容易出现野指针、空指针等非法访问问题

七、内联函数

💦 什么是内联函数

在程序中大量重复的建立函数栈帧 (如 swap 等函数) 会造成很大的性能开销,当然 C 语言可以用宏来代替函数,使之不会开辟栈帧,但是宏优点多,但也有不少的劣势,这时内联函数就可以针对这种场景解决问题 (内联函数对标宏函数)。

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

#include<iostream>
using namespace std;
//Add就会在调用的地方展开
inline int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int ret = Add(10, 20);
	cout << ret << endl;
	return 0;
}

❓ 验证 ❔

1、在 release 模式下,查看编译器生成的汇编是否存在 call Add
在这里插入图片描述

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

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

💦 内联函数的特性

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

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

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 中被引用

八、auto关键字 (C++11)

注意 C++11 一般要标准之后的编译器才支持的比较好 (最少 2013)

💦 auto简介

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

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

int main()
{
	int a = 3;
	char b = 'A';

	//通过右边的赋值对象,自动推导变量类型
	auto c = a;
	auto d = b;

	//typeid可以去看变量的实际类型
	cout << typeid(c).name() << endl;
	cout << typeid(d).name() << endl;
	
	return 0;
}

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

❓ auto的价值 ❔

auto 在前期并不能很好的体现它的价值,在后面学了 STL 时就能体现了,这里我们可以先看看。

map<string, string> dict;
//map<string, string> :: iterator it = dict.begin();
auto it = dict.begin;//同上

从上就可以看出 auto 的价值就是简化代码

  💨优点:auto 可以自动推导类型简化代码

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

💦 auto的使用细则

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

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

int main()
{
	int x = 10;
	auto a = &x;
	auto* b = &x;
	auto& c = x;

	cout << typeid(a).name() << endl;//int*
	cout << typeid(b).name() << endl;//int*
	cout << typeid(c).name() << endl;//int

	*a = 20;
	*b = 30;
	 c = 40;
	
	return 0;
}

2️⃣ 在同一行定义多个变量 2️⃣

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

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

💦 auto不能推导的场景

1️⃣ auto 不能作为函数的参数 1️⃣

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

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

void TestAuto()
{
	int a[] = {1,2,3};
	auto b[] = {456};
}

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

❗ C++98 auto 和 C++11 auto❕

  早期 C++98 标准中就存在了 auto 关键字,那时的 auto 用于声明变量为自动变量,拥有自动的生命周期;但是这个作用是多余的,因为变量默认拥有自动的生命周期

  在 C++11 中,已经删除了这种用法,取而代之的用处是自动推断变量的类型

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

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

💦 范围for的语法

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

void TestFor()
{
	int array[] = { 1, 2, 3, 4, 5 };
	for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
		array[i] *= 2;
 
	for (int* p = array; p < array + sizeof(array)/ sizeof(array[0]); ++p)
		cout << *p << endl;
}

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

void TestFor()
{
	int array[] = { 1, 2, 3, 4, 5 };

 	//自动依次取数组中的值给e并自动判断结束
	for(auto e : array)
		cout << e << " ";
 
 	//这里要对数组里的内容进行改变,就要使用&,因为这里的e只是一份拷贝
 	for(auto& e : array)
		e *= 2;
	return 0;
}

⚠ 注意:

范围 for 与普通循环类似,可以用 continue 来结束本次循环,也可以用 break 来跳出整个循环。这个东西后面到了容器会详细介绍

💦 范围for的使用条件

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

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

  以下代码就有问题,因为 for 的范围不确定

void TestFor(int array[])
{
	//注意这里的array已经不是数组了,已经退化为指针了
	for(auto& e : array)
		cout<< e <<endl;
}

2️⃣ 迭代的对象要实现++和==的操作 2️⃣

  关于迭代器这个问题,以后会讲,在这篇文章只做为了解

十、指针空值 —— nullptr (C++11)

💦 C++98中的指针空值

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

void TestPtr()
{
	//C++98
	int* p1 = NULL;
	int* p2 = 0;	
 
 	//C++11
 	int* p3 = nullptr;
	// ……
}

⚠ 注意:

  指针本质是内存按字节为单位的编号,空指针并不是不存在的指针,而是第一个字节的编号,一般我们不使用这个空间存有效数据。空指针一般用来初始化,表示指针没有指向一块有效数据的空间。

❓ 为啥C++11后推荐用nullptr来初始化空指针 ❔

NULL 实际是一个宏,在传统的 C 头文件 (stddef.h) 中,可以看到如下代码:
在这里插入图片描述
场景:这里本意上是想让 0 匹配 int、NULL 匹配 int*

void f(int)
{
	cout<<"f(int)"<<endl;
}
void f(int*)
{
	cout<<"f(int*)"<<endl;
}
int main()
{
	f(0);
	f(NULL);
	//f((int*)NULL);
	//f(nullptr);//使用C++11中的nullptr更准确
	return 0;
}

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

⚠ 注意:

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

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

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

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

作者其他文章

评论(0

抱歉,系统识别当前为高风险访问,暂不支持该操作

    全部回复

    上滑加载中

    设置昵称

    在此一键设置昵称,即可参与社区互动!

    *长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

    *长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。