C++初阶 2:C++入门(2)

函数重载

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

函数重载的概念

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

但是如果是下面这种情况是不能算作函数重载的:
在这里插入图片描述

在这里插入图片描述

返回值不同无法区分,所以返回值不是重载的条件

1.参数类型不同

#include<iostream>
using namespace std;

int Add(int left , int right)
{
	cout<<"int Add(int left,int right)"<<endl;
}

int Add(float left , float right)
{
	cout<<"int Add(float left,float right)"<<endl;
}

2.参数个数不同

void f()
{
	cout << "f()" << endl;
}

void f(int a=1)
{
	cout << "f(int a)" << endl;
}

但是如果是下面这种情况,编译器将会打印什么呢?

int main()
{
	f();
	return 0;
}

而最后是无法完成运行,因为这里出现了调用歧义,不知道该调用谁,所以我们在写代码时,需要必要由缺省参数带来的调用歧义。
在这里插入图片描述

3.参数类型顺序不同

void f(int a, float b)
{
	cout << "f(int a,float b)" << endl;
}

void f(float b, int a)
{
	cout << "f(float ,int a)" << endl;
}

输入输出的结果:
在这里插入图片描述

C++支持函数重载的原理-名字修饰

上面已经介绍了函数重载,那么为什么C++支持函数重载,而C语言不支持呢?这里解释相对复杂,我将在以后单独写一篇博客来介绍它的原理,那么我们现在只需要记住它的结论就行。

C语言之所以没有函数重载是因为同名的函数无法区分,函数名字的修饰没有发生变化。

C++之所以存在函数重载是因为C++可以通过函数修饰规则来进行区分,只要参数不同,修饰出来的名字就不一样。这里也更好的解释了为什么返回值不同,却不算函数重载,而是会报重定义的错误。

引用

引用的概念

引用不是新定义一个变量,而是给已存在变量取一个别名,编译器不会为引用定义的变量开辟内存空间,它和它引用的变量共用同一块空间。就好像每一个都会有自己的小名和外号,但究其都还是指向这个人,而不是其他人。

表达形式为:
类型& 引用变量名(对象名)=引用实体

void test()
{
	int a=0;
	int& ra=a;//<===定义引用类
	printf("%p",&a);
	printf("%p",&ra);
	//两者最终会打印出相同的地址
}

引用特性

  1. 引用在定义时必须初始化
  2. 一个变量可以有多个引用
  3. 引用一旦引用一个实体,就不可以再引用其他实体
void test()
{
	int a=10;
	int& ra;//该句语句编译时会报错
	int& ra = a;
	int& rra = a;
	printf("%p %p %p",&a,&ra,&rra);
	//三者最后会打印出相同的地址
}

常引用

常引用的格式为const 类型& 引用变量名(对象名)=引用实体,常引用的作用是不希望对所引用的内容进行修改,但是在常引用中会经常出现权限的问题,那我们通过下面的代码,对常引用权限问题进行一下讲解:

void test()
{
	int x = 0;
	int& y = x;//权限的平移
	const int& z = x;//不会报错,属于权限的缩小
	//z++;//由于z是常引用,所以不能对值进行修改
	//y++;//可以进行

	const int m = 0;
	int& n = m;//会出现报错,属于权限的放大
	//权限的放大:
	//m是只读不可写
	//n只是变成了我的别名
	//n的权限是可读可写
	const int& n1 = m;//权限的平移
	int p = m;//不会报错,只是单纯将m拷贝给了p,p不会影响m
	
}

上面的代码对常引用权限问题解释的较为详尽,这里稍微进行一下总结:

  1. 常引用可以进行权限的平移和缩小,不可以进行权限的放大;
  2. 如果变量为常量,那么在使用引用的时候,务必使用常引用,不然编译时会报错,造成权限的放大

其实,指针也存在权限的问题,下面我们一起来分析一下:

void test()
{
	const int* p1=&m;//p1可以改,*p1不可以改
	int* p2 =p1;//会报错,存在了权限的放大,*p2可以改了

	int* p3=&x;
	const int*p4=&p3;//不会报错,是权限的缩小
}

使用场景

1.做参数

#include<isotream>
using namespace std;

void test(int b)
{
	......
}

int main()
{
	int a = 0;
	int& b = a;
	b++;
	void test(b);
}

2.做返回值

观察下面的代码,输出的结果是什么?为什么?

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

传值,传引用效率比较

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

综上所述,传值和指针在作为传参以及返回值类型上效率相差很大

引用和指针的区别

  1. 语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块土地。
int main()
{
	int a = 10;
	int& ra = a;

	cout << &a << endl;
	cout << &ra << endl;
	
	return 0;
}

在这里插入图片描述

  1. 底层实现上实际是有空间的,因为引用是按照指针方式来实现
int main()
{
	int a = 10;

	int& ra = a;
	ra = 20;

	int* pa = &a;
	*pa = 20;

	return 0;
}

下面观察下引用和指针的汇编代码对比:
在这里插入图片描述

引用和指针的不同点:

①引用概念上定义一个变量的别名,指针存储一个变量地址。但是在语法上,引用不开辟空间,指针要开辟空间;

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

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

没有NULL引用,但有NULL指针(相对而言,不容易出现);

int* p1=NULL;
int& a= *p1;//没有解引用的行为

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

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

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

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

引用比指针使用起来更加安全,指针存在非法访问的问题

内联函数

对于频繁调用小函数,在C语言中,常常采用宏函数来实现,但是宏函数的使用过程中存在许多需要注意的小细节,很容易出现错误,比如一个简单的加法函数,宏函数写出来需要写成:

#define Add(a,b) ((a)+(b))

所以在c++中,提出了内联函数的概念来处理频繁调用的小函数。

概念

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

在这里插入图片描述
如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体来替代函数调用,也就是在调用的地方进行展开,没有函数栈帧的建立

查看方式:

  1. 在release模式下,查看编译器生成的汇编代码中是否存在call Add (有call,则没有展开)
  2. 在debug模式下,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认不会对代码进行优化)
    在这里插入图片描述
    设置完后,如果使用inline关键字,汇编的结果则是,没有call:
    在这里插入图片描述

特性

  1. inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体代替函数调用
    缺陷:可能会使目标文件变大;优势:少了调用开销,提高程序运行效率。
    这里举出一个例子:有10000个位置调用100行指令,如果展开(使用inline)— 需要开辟10000*100的空间,如果不展开 — 10000+100(1万个call+100行函数体指令)。展开后,编译出来的可执行程序变大,也就是下载的内容变多的,也就是它的缺陷。

  2. inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同
    一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现),不是递归,且频繁调用的函数采用inline关键字,否则编译器会忽略inline特性。下面是某本书关于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;
}

//test.cpp
#include"F.h"
int main()
{
	f(10);//call+地址
	return 0;
}

在这里插入图片描述
//链接错误:test.obj : error LNK2019: 无法解析的外部符号 “void _cdecl f(int)” (?f@@YAXH@Z),该符号在函数 _main 中被引用 ——有声明没有找到定义

auto关键字(C++11)

类型别名思考

随着代码的数量增多,程序越来越复杂,程序中遇到的类型也越来越多,经常表现在:
1.类型难以拼写
2.含义不明确,导致容易出错

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

int main()
{
	std::map<std::string,std::string>m{{"apple","苹果"},{"orange","橙子"},{"pear","梨子"}},

	std::map<std::string,std::string>::iterator it=m.begin();
	while(it!=end())
	{
		//....
	}
	return 0;
}

其中std::map< std::string,std::string >::iterator是一个类型,但是该类型特别长,容易写错,且可读性不高。那么,根据之前的内容,很容易想到用typedef该类型自定义命名,比如:

#include<string>
#include<map>

typedef std::map<std::string,std::string> Map

int main()
{
	Map m{{"apple","苹果"},{"orange","橙子"},{"pear","梨子"}};
	Map::iterator it=m.begin();
	while(it!=end())
	{
		//....
	}
	return 0;
}

使用typedef给类型取名确实可以简化代码,但是会带来新的问题:

typedef char* pstring

int main()
{
	const pstring p1;
	const pstring* p2;
	return 0;
}

在编译时,通常需要把表达式的值赋值给变量,这就要求在声明变量的时候清楚知道表达式的类型,然而有时候要做到这一点是不容易的,所以在C++11中给auto赋予了新的含义

auto简介

在早期C和C++中auto的含义是:使用auto修饰的变量,是具有自动储存器的局部变量,但这个时候并没有什么人使用它,原因在于:

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

int TestAuto()
{
return 10;
}

int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = TestAuto();
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;

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

【注意】

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

auto的使用细则

  1. auto与指针和引用结合起来使用
    用auto声明指针类型时,用auto和auto* 没有任何区别,但是auto声明引用类型时则必须加&
int main()
{
	int x=10;
	auto a=&x;
	auto* b=&x;
	auto& c=x;
	
	*a=20;
	*b=30;
	c=40;

	return 0;
}
  1. 在同一行定义多个变量
    当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
void TestAuto()
{
	auto a=1,b=3;
	auto c=3,d=4.0;//该行代码会编译错误,因为c和d的初始化表达式类型不同
}

auto不能推导的场景

  1. auto不能作为函数的参数
//此处代码编译失败,auto不能作为形参类型,因为
void Testauto(auto a)
{ }
  1. auto不能用来声明数组

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

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

基于范围的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};
	for(auto& e : array)//修改array里面的值得用auto&(引用)
		e*2for(auto e:array)
		cout<<e<<" ";//自动取数组array的值,赋值给e;自动++,自动判断结束

	return 0}

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

范围for的使用条件

  1. for循环迭代的范围必须是确定的
    对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
  2. 迭代的对象要实现++和==的操作

指针空值nullptr(C++11)

** NULL可能被定义为字母常量0,或者被定义为无类型指针的常量**

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值