C++函数重载、引用、内联函数、auto关键字和C++11范围for

1、函数重载

函数重载: 是函数的一种特殊情况, C++ 允许在 同一作用域中 声明几个功能类似 的同名函数 ,这
些同名函数的 形参列表 ( 参数个数 或 类型 或 类型顺序 ) 不同 ,常用来处理实现功能类似数据类型
不同的问题。
返回值不同,无法区分,返回值不是重载的条件。
// 下面两个构造函数重载
// f()但是这样调用存在歧义
void f()
{
	cout << "f()" << endl;
}

void f(int a = 10)
{
	cout << "f(int a)" << endl;
}
//不同作用域 可以同名
namespace bit1
{
	void Swap(int* pa, int* pb)
	{
		cout << "void Swap(int* pa, int* pb)" << endl;
	}
}

namespace bit2
{
	void Swap(int* px, int* py)
	{
		cout << "void Swap(int* pa, int* pb)" << endl;
	}
}


// 同一作用域 可以同名,满足重载规则
void Swap(double* pa, double* pb)
{
	cout << "void Swap(double* pa, double* pb)" << endl;
}

using namespace bit1;
using namespace bit2;
// 他们两依旧是ok,不是重载关系

int main()
{
	int a = 0, b = 1;
	double c = 0.1, d = 1.1;
	// 调用歧义
	//Swap(&a, &b);

	Swap(&c, &d);

	return 0;
}

2.为什么c语言不支持重载,c++支持?c++是怎么支持的

C++ 支持函数重载的原理 -- 名字修饰 (name Mangling)
①在 C/C++ 中,一个程序要运行起来,需要经历以下几个阶段: 预处理、编译、汇编、链接
实际项目通常是由多个头文件和多个源文件构成,而通过 C 语言阶段学习的编译链接,我们
可以知道,【当前 a.cpp 中调用了 b.cpp 中定义的 Add 函数时】,编译后链接前, a.o 的目标
文件中没有 Add 的函数地址,因为 Add 是在 b.cpp 中定义的,所以 Add 的地址在 b.o 中。那么
怎么办呢?
所以链接阶段就是专门处理这种问题, 链接器看到 a.o 调用 Add ,但是没有 Add 的地址,就
会到 b.o 的符号表中找 Add 的地址,然后链接到一起。
②c语言链接时,直接用修饰后的函数名字去查找,所以不支持重载
c++链接时,直接用修饰后的函数名字去查找,就可以支持重载
③通过这里就理解了 C 语言没办法支持重载,因为同名函数没办法区分。而 C++ 是通过函数修
饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载
如果两个函数函数名和参数是一样的,返回值不同是不构成重载的,因为调用时编译器没办
法区分。
3、引用
①引用概念: 引用 不是新定义一个变量,而 是给已存在变量取了一个别名 ,编译器不会为引用变量开辟内存空 间,它和它引用的变量 共用同一块内存空间。
类型 & 引用变量名 ( 对象名 ) = 引用实体;
void TestRef()
{
    int a = 10;
    int& ra = a;//<====定义引用类型
    printf("%p\n", &a);
    printf("%p\n", &ra);
}
注意: 引用类型 必须和引用 实体 同种类型

②引用特性

  引用在 定义时必须初始化
一个变量可以有多个引用
引用一旦引用一个实体,再不能引用其他实体
void TestRef()
{
   int a = 10;
   // int& ra;   // 该条语句编译时会出错,没有对引用进行初始化
   int& ra = a;
   int& rra = a;
   printf("%p %p %p\n", &a, &ra, &rra);  
}

一个引用的举例:

typedef struct SeqList
{
	int* a;
	// ...
}SLT;

//void SeqPushBack(struct SeqList& sl, int x) 
void SeqPushBack(SLT& sl, int x)
{}

typedef struct ListNode
{
	int val;
	struct ListNode* next;
}LTNode, *PNode;
// typedef struct ListNode* PNode;

//void ListPushBack(LTNode** phead, int x)//列表头插
//void ListPushBack(LTNode*& phead, int x)//采用引用方式的列表头插
void ListPushBack(PNode& phead, int x)
{
	if (phead == NULL)
	{
		// phead = newnode;
	}
}//采用引用方式的列表尾插

int main()
{
	struct SeqList s;
	SeqPushBack(s, 1);
	int* p1 = &x;
	// 指针变量取别名
	int*& pr = p1;
	pr = NULL;
	LTNode* plist = NULL;
	ListPushBack(plist, 1);
	ListPushBack(plist, 2);
	ListPushBack(plist, 2);
	return 0;
}

③常引用

void TestConstRef()
{
    const int a = 10;
    //int& ra = a;   // 该语句编译时会出错,a为常量,相当于权限的放大
    const int& ra = a;
    // int& b = 10; // 该语句编译时会出错,b为常量
    const int& b = 10;
    double d = 12.34;
    //int& rd = d; // 该语句编译时会出错,类型不同
    const int& rd = d;//d首先赋值给一个临时变量,临时变量具有常量性

	// 权限可以平移/缩小 不能放大
	double d = 12.34;

	// (强制)类型转换都会产生临时变量
	int i = d;

	const int& r1 = d;//临时变量具有常性,常性就是不能被修改,相当被const修饰一样

	int x = 0, y = 1;
	const int& r2 = x + y;//表达式运算也都会产生临时变量,临时变量具有常性

	return 0;
}

int main()
{
	// 权限的平移
	int x = 0;
	int& y = x;

	// 权限的缩小,可以
	const int& z = x;
	//z++; // 不可以
	y++;//会影响z

	// 权限的方法
	// m只读
	// n变成我的别名,n的权限是可读可写
	// 权限的放大,不可以
	const int m = 0;
	// int& n = m;

	const int& n = m;

	// 可以,不是权限的放大
	// m拷贝的给p,p的修改不影响m
	int p = m;

	// 权限的放大
	// p1可以修改 *p1不可以,const修饰是的*p1
	const int* p1 = &m;
	// p1++;//可以
	// int* p2 = p1;//由于*p1不能修改,这样写后*p2可以修改,权限的放大,不可以
	const int* p2 = p1;

	// 权限的缩小
	int* p3 = &x;
	const int* p4 = p3;

	return 0;
}

④传值、传引用效率比较

以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直
接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效
率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
#include <time.h>
struct A{ int a[10000]; };
void TestFunc1(A a){}
void TestFunc2(A& 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;
}

通过上述代码的比较,发现 传值和指针在作为传参以及返回值类型上效率相差很大

⑤引用和指针的区别

语法概念上 引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
底层实现上 实际是有空间的,因为 引用是按照指针方式来实现 的。
引用和指针的汇编代码对比:

引用和指针的不同点:

1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
2. 引用 在定义时 必须初始化 ,指针没有要求
3. 引用 在初始化时引用一个实体后,就 不能再引用其他实体 ,而指针可以在任何时候指向任何
一个同类型实体
4. 没有 NULL 引用 ,但有 NULL 指针
5. sizeof 中含义不同 引用 结果为 引用类型的大小 ,但 指针 始终是 地址空间所占字节个数 (32
位平台下占 4 个字节 )
6. 引用自加即引用的实体增加 1 ,指针自加即指针向后偏移一个类型的大小
7. 有多级指针,但是没有多级引用
8. 访问实体方式不同, 指针需要显式解引用,引用编译器自己处理
9. 引用比指针使用起来相对更安全
#include<iostream>
using namespace std;
int main()
{
	int a = 0;
	int& b = a;  // 语法上不开空间
	int* p = &a; // 语法上要开空间

	int* ptr = NULL;
	int& r = *ptr;//不报错,只是用一个指针把ptr(空指针NULL)的地址存起来
	//cout << r << endl;//报错,会发生访问和解引用

	//cout << sizeof(b) << endl;//指针始终是地址空间所占字节个数(32位平台下占4个字节)
	//cout << sizeof(int&) << endl;//引用结果为引用类型的大小
	//b++;//指针自加即指针向后偏移一个类型的大小
	//p++;//指针自加即指针向后偏移一个类型的大小

	return 0;
}

4、内联函数

①inline 是一种 以空间换时间 的做法,如果编译器将函数当成内联函数处理,在 编译阶段,会 用函数体替换函数调用 ,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运 行效率。
②inline 对于编译器而言只是一个建议,不同编译器关于 inline 实现机制可能不同 ,一般建 议:将函数规模较小 ( 即函数不是很长,具体没有准确的说法,取决于编译器内部实现 ) 是递归、且频繁调用 的函数采用 inline 修饰,否则编译器会忽略 inline特性。
③一般来说,内联机制用于规模较小、流程直接、频繁调用的函数。大多数编译器都不支持内联递归函数。
例如:一个100行指令的函数,有10000个位置调用。
展开后程序大小:10000*100

不展开程序大小:10000+100

所以展开后,编译出来的可执行程序会变大。

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

宏的优缺点:

优点:

        1.增强代码的复用性。
        2.提高性能。
        缺点:
        1.不方便调试宏。(因为预编译阶段进行了替换)
        2.导致代码可读性差,可维护性差,容易误用。
        3.没有类型安全的检查 。
        C++有哪些技术替代宏
        1. 常量定义 换用 const enum
        2. 短小函数定义 换用内联函数
5.auto关键字
C++11 中,标准委员会赋予了 auto 全新的含义即: auto 不再是一个存储类型指示符,而是作为一 个新的类型指示符来指示编译器, auto 声明的变量必须由编译器在编译时期推导而得
下面是使用auto的一个例子:
#include<map>
#include<string>

int main()
{
	int j = 0;

	// 右边初始化自动推导类型
	auto i = 0;
	std::map<std::string, std::string> dict;

	// 等价的,auto可以替代写起来比较长的类型的定义,简化代码
	//std::map<std::string, std::string>::iterator it = dict.begin();
	auto it = dict.begin();

	return 0;
}

也可以使用typedef给类型取别名,但是typedef会遇到新的难题:

typedef char* pstring;

int main()
{
	const pstring p1; 
       //编译成功还是失败?失败 相当于char* const p1;const的指针必须被初始化
	const pstring* p2; // 编译成功还是失败?
	return 0;
}

auto使用注意事项:

int TestAuto()
{
	return 10;
}

int main()
{
	//int a = 10;
	//auto b = a;
	//auto c = 'a';
	//auto d = TestAuto();
    //typeid可以帮我们去看一个对象的类型
	//cout << typeid(b).name() << endl;//int
	//cout << typeid(c).name() << endl;//char
	//cout << typeid(d).name() << endl;//int 

	int x = 10;

	auto a1 = x;
	auto a2 = &x;//auto声明指针类型时,必须加&

	// 指定必须是指针
	auto* b = &x;

	// 引用
	auto& c = x;

   //auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
   //使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto
   //的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编
   //译期会将auto替换为变量实际的类型。
}
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译 器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量
void TestAuto()
{
    auto a = 1, b = 2; 
    auto c = 3, d = 4.0;  // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}

不支持使用auto的场景:

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

//2.auto不能直接用来声明数组
void TestAuto()
{
    int a[] = {1,2,3};
    auto b[] = {4,5,6};
}
5. 基于范围的 for 循环 (C++11)
for 循环后的括号由冒号 分为两部分:第一部分是范 围内用于迭代的变量,第二部分则表示被迭代的范围
// 不支持
void TestFor(int array[])
{
	for (auto& e : array)
		cout << e << endl;
//这里array是指针不是数组了,因为for的范围不确定,就是数组中第一个元素和最后一个元素的范围不确定
}//数组不支持传参,传过去后变成首元素的地址,是一个指针,不是数组
 

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

	for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
	{
		array[i] *= 2;
	}
	
	for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
	{
		cout << array[i] << " ";
	}
	cout << endl;

	// C++11 范围for
	// 自动取数组array中,赋值给e
	// 自动++,自动判断结束
 //	for (auto e : array)
	{
		cout<< e << " ";
	}//遍历
	for (auto& e : array)
	{
		e /= 2;
	}//改变数组的值

	for (int x : array)
	{
		cout << x << " ";
	}
	cout << endl;

	return 0;
}
//注意:与普通循环类似,可以用 continue 来结束本次循环,也可以用 break 来跳出整个循环
6.最后介绍一下指针空值nullptr(C++11)
NULL 实际是一个宏,在传统的 C 头文件 (stddef.h) 中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL   0
#else
#define NULL   ((void *)0)
#endif
#endif
NULL 可能被定义为字面常量 0 ,或者被定义为无类型指针 (void*) 的常量 。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:
void f(int i)
{
	cout << "f(int)" << endl;
}

void f(int* p)
{
	cout << "f(int*)" << endl;
}

int main()
{
	f(0);
	f(NULL);//调用f(int i)
	f((int*)NULL);//调用f(int* p)
	f(nullptr);//调用f(int* p)

	return 0;
}
注意:
1. 在使用 nullptr 表示指针空值时,不需要包含头文件,因为 nullptr C++11 作为新关键字引入
2. C++11 中, sizeof(nullptr) sizeof((void*)0) 所占的字节数相同。
3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用 nullptr
  • 21
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值