Cpp快速入门语法(下)(2)


前言

承前启后,正文开始!


一、函数重载

概念与使用

  函数重载是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,常用来处理实现功能类似数据类型不同的问题,而C语言不允许同名函数
  但是需要满足的条件是:函数的形参列表不同,即参数个数,类型,类型顺序不同

  在C语言中,我们如果要实现两数之和 Add 函数,如果需要int、double两种各一个,我们可能会命名为Addi、Addd,这很麻烦,而函数重载就可以解决这个问题,下面让我们来看具体实现代码:

#include<iostream>
using namespace std;

// 1、参数类型不同
int Add(int x, int y)
{
    return x + y;
}
double Add(double x, double y)
{
    return x + y;
}

// 2、参数个数不同
void f()
{
    cout << "f()" << endl;
}
void f(int a)
{
    cout << "f(a)" << endl;
}

// 3、参数类型顺序不同(本质还是参数类型不同)
void f(int a, char b)
{
	cout << "f(int a,char b)" << endl;
}
void f(char b, int a)
{
	cout << "f(char b, int a)" << endl;
}

int main()
{
	// 都可以对应到正确的函数
    Add(10, 20); 
	Add(10.1, 20.2); 
    
    f();
	f(10);
    
    f(10, 'a');
	f('a', 10);
    return 0;
}

另外你需要注意,只有返回类型不同不构成重载,原因会产生歧义,具体看以下代码

#include <iostream>
using namespace std;

void f()
{
	cout << "void f()" << endl;
}
int f()
{
	cout << "int f()" << endl;
	return 0;
}

int main()
{
	f(); // 调用哪一个不确定
	return 0;
}

C++为何支持函数重载?

  这里我们就需要回想前面学习C的时候有关预处理和编译的内容了
  在C/C++,程序运行之前,需要进行以下几个阶段: 预处理、编译、汇编、链接

关于链接,你可以尝试回想以下:

我们知道,在编译阶段会将程序中的每个源文件的全局范围的变量符号分别进行汇总。在汇编阶段会给每个源文件汇总出来的符号分配一个地址(若符号只是一个声明,则给其分配一个无意义的地址),然后分别生成一个符号表。最后在链接期间会将每个源文件的符号表进行合并,若不同源文件的符号表中出现了相同的符号,则取合法的地址为合并后的地址(重定位)

举个例子,我们观看下面两个同根.c文件内容:

	// sum.c
	int sum(int num1, int num2)
	{
		return num1 + num2;
	}
	
	// main.c
	extern int sum(int num1, int num2);
	int main()
	{
		sum(1,2);
		return 0;
	}

  注意,在链接前两个.c文件都是单线不交互的,这时候,sum.c里面的sum函数有定义,而main.c里面的sum函数没有定义,等到两个.c文件经过汇编后,main.o形成如下符号表:

main 0x100
sum 0x000 (无意义的地址)

sum.o形成以下符号表

sum 0x800 (有意义的地址)

  接着,两个文件合成一个文件,错误的sum地址被改为正确的地址,而你想,假如有两个sum函数被定义,即有地址,那么它们单独来看都是有意义的地址,可是这时候要重定位哪个?哪怕只有一个文件,两个重名函数,那么你call的是哪个函数,这很明显有歧义

来验证一下吧,首先我们在Linux环境下采用gcc编译器
在这里插入图片描述
在这里插入图片描述
可以看到,Add就是Add,func就是func,没有半点修饰

接着我们再在Linux环境下采用g++编译器来编译
在这里插入图片描述

多试几个函数,其实你会发现修饰函数名字在此环境下的规律为 { _Z + 函数名长度 + 函数名 + 类型首字母 }

  也就是说,C++在进行符号汇总时,对函数的名字修饰做了改动,函数汇总出的符号不再单单是函数的函数名,而是通过其参数的类型和个数以及顺序等信息汇总出一个名字,这样一来,就算是函数名相同的函数,只要其参数的类型或参数的个数或参数的顺序不同,那么汇总出来的符号也就不同了,其实也从侧面说明了函数重载跟返回类型没关系

这可能很抽象,毕竟有关编译甚至在大学还有专门的一门专业课《编译原理》,大家如有困惑可以自行查阅其他相关资料

二、引用

概念

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

“李逵”、“铁牛”、“黑旋风”本质上都是一个人

语法

  类型说明符& 引用对象名 =引用实体(引用类型必须和引用实体是同种类型)

来个具体例子:

void TestPef()
{
    int a = 10;
    int& pa = a; // pa是a的别名
   
    // 从地址上,可以得出它和它引用的变量共用同一块内存空间
    printf("&a == %p\n", &a);
    printf("&pa == %p\n", &pa);
}

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

特性

  1. 引用在定义时必须初始化
int a = 10;
int& b = a; // right
  1. 一个变量可以有多个引用
int a = 10;
int& b = a; // right
int& c = a; // right
int& d = a; // right
  1. 引用一旦引用了一个实体,就不能再引用其他实体
	int a = 10;
	int& b = a;
	int c = 20;
	b = c; //你的想法:让b转而引用c,其实是c赋值给b

权限(常引用)

  我们知道,权限可以缩小或者平移,但是绝对不能放大

void TestConstRef()
{
	int a=0;
    int& b=a;
    
    const int& c=a; //支持->权限缩小
     
    const int x=10;
    int& y=x;//不支持-权限放大(此时的x只有读权限,没有写权限)
    const int& y=x;//支持权限相等
    
    //表达式的返回值是临时对象,而临时对象具有常性!!
    int& n = a+x = 临时对象 //这里是属于权限放大
    const int& n = a+x = 临时对象; //支持权限相等

使用场景

  1. 用作形参,因为是同一块内存空间,所以在一定程度上可以替代指针
//交换函数
void Swap(int& a, int& b)
{
	int tmp = a;
	a = b;
	b = tmp;
}
  1. 不用创建临时变量,提高效率
#include <ctime>
#include <iostream>
using namespace std;

struct A { int a[10000]; };

void TestFunc1(struct A& a) {}
void TestFunc2(struct A a) {}

int main()
{
	A a;

	size_t begin1 = clock();
	for (int i = 0; i < 10000; i++)
		TestFunc1(a);
	size_t end1 = clock();

	size_t begin2 = clock();
	for (int i = 0; i < 10000; i++)
		TestFunc2(a);
	size_t end2 = clock();

	// 在某次错误时
	cout << "TestFunc1(struct A& a):" << end1 - begin1 << endl; // 0
	cout << "TestFunc1(struct A a):" << end2 - begin2 << endl; // 5

	return 0;
}

与指针的区别

  其实,引用不可像指针那样更改,注定了无法完全替代指针,像链表我们就必须用到指针

  在语法概念上,引用是一个别名,没有独立空间,同其引用实体共用同一块空间,但是在底层实现上,实际引用是有开辟空间的,由于引用是按照指针方式实现
在这里插入图片描述

总而言之,你需要记住以下几点:

1、引用在定义时必须初始化,指针没有要求。
2、引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。
3、没有NULL引用,但有NULL指针。
4、在sizeof中的含义不同:引用的结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)。
5、引用进行自增操作就相当于实体增加1,而指针进行自增操作是指针向后偏移一个类型的大小。
6、有多级指针,但是没有多级引用。
7、访问实体的方式不同,指针需要显示解引用,而引用是编译器自己处理。
8、引用比指针使用起来相对更安全。

三、内联函数

  在C语言中,假设有一些小而频繁使用的函数如交换函数Swap,大量使用会建立栈帧,消耗时间,宏是C语言给出的解决方式,可这样太麻烦且易错

比如来个Add函数,宏的正确写法是 #define Add(x, y) ((x) + (y))

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

事实上,C++相当不鼓励使用宏,理由有代码可读性差(导致调试不方便)、与函数相比没有类型检查(宏做的仅仅是替换),在有些场景下比较复杂(需要谨慎替换后运算符的优先级)等

而C++给出的方案是:
i, 用const和enum替代宏常量;
ii,用inline(内联函数)替代宏函数

还是来个具体例子吧,我们现在来观察调用普通函数和内联函数的汇编代码来进一步查看其优势:

int Add(int a, int b)
{
	return a + b;
}
int main()
{
	int ret = Add(1, 2);

	return 0;
}

在这里插入图片描述
  如果内联函数语句较多且多次不同地方调用,可能会使编译后的文件(可执行程序)变大,其实,这本质上就是一种以空间换时间的做法,但优点是减少了调用开销,提高了程序运行效率

  内联函数是对编译器的一个建议,对于我们实现的内联函数,编译器不一定执行,不同编译器关于inline函数得实现机制可能不同;一般情况下,建议将函数规模较小,不是递归且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性;

  inline函数不要让声明和定义分离,分离会导致链接错误;因为inline被展开,就不再调用函数,没有函数地址了,链接就会找不到

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

  随着学习的深入,我们会发现1. 类型难于拼写 2. 含义不明确导致容易出错
  auto在C11就因此被赋予了新的含义:作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得

  1. 用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时必须加&
#include <iostream>
using namespace std;

int main()
{
	int a = 10;
	auto b = &a;   // 自动推导出b的类型为int*
	auto* c = &a;  // 自动推导出c的类型为int*
	auto& d = a;   // 自动推导出d的类型为int
	
	// 打印变量b,c,d的类型
	cout << typeid(b).name() << endl;// 打印结果为int*
	cout << typeid(c).name() << endl;// 打印结果为int*
	cout << typeid(d).name() << endl;// 打印结果为int
	return 0;
}
  1. 在同一行定义多个变量必须是同一类型
int main()
{
	auto a = 1, b = 2; // right
	auto c = 3, d = 4.0; // err: “auto”必须始终推导为同一类型
	return 0;
}
  1. auto不能作为函数的参数
void TestAuto(auto x) {} // err
  1. auto不能直接用来声明数组
int main()
{
	int a[] = { 1, 2, 3 };
	auto b[] = { 4, 5, 6 };// err
	return 0;
}

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

  C++11中引入了基于范围的for循环。for循环后的括号由冒号分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。

其实是抄的Python的作业

	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	//将数组元素值全部乘以2
	for (auto& e : arr) // 运用了引用
	{
		e *= 2;
	}
	//打印数组中的所有元素
	for (auto e : arr)
	{
		cout << e << " ";
	}
	cout << endl;

范围for的使用是有条件的:

一、for循环迭代的范围必须是确定的
 对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
二、迭代的对象要实现++和==操作
 这是关于迭代器的问题,大家先了解一下。

六、指针空值nullptr(C++11)

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

/* Define NULL pointer value */
#ifndef NULL
#ifdef __cplusplus
#define NULL    0 // NULL 直接被替换为0
#else  /* __cplusplus */
#define NULL    ((void *)0)
#endif  /* __cplusplus */
#endif  /* NULL */

  我们之前都拿NULL当指针空值,而上述错误就可能导致以下BUG:

#include <iostream>
using namespace std;

void f(int)
{
	cout << "f(int)" << endl;
}
void f(int*)
{
	cout << "f(int*)" << endl;
}
int main()
{
	f(0);
	f(NULL); // 我们想的是匹配第二个,结果是第一个,这就是错误的宏替换带来的后果
	f((int*)NULL);
	return 0;
}

所以,对于C++98中的问题,C++11引入了关键字nullptr

请注意:

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

总结

  本节干货好多,函数重载原理的那一部分可能有些困难,加油!

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值