从c语言到c++快速入门(下)

目录

1. 函数重载

1.1. 函数重载的定义

1.2. 函数重载的分类

1.2.1. 参数类型不同

1.2.2. 参数个数不同

1.2.3. 参数类型顺序不同

1.3. 注意

1.返回值类型不同无法构成函数重载

2.缺省值不同也不能构成函数重载

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

2. 引用

2.1. 引用的概念

2.2. 注意

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

 2.一个变量可以有多个引用

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

2.3. 常引用

2.4. 引用的使用场景 

 2.4.1. 作为函数的参数

2.5. 传值与传引用

2.6. 引用与指针的区别 

3. 内联函数

3.1. 内联函数的定义

3.2. 注意

4. auto关键字

4.1. auto的简介

4.2. 注意

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

auto不能作为函数的参数或者声明数组

 5. 范围for

6. nullptr空指针

6.1注意


1. 函数重载

1.1. 函数重载的定义

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

1.2. 函数重载的分类
1.2.1. 参数类型不同
int Add(int a, int b)
{
	return a + b;
}
double Add(double a, double b)
{
	return a + b;
}
1.2.2. 参数个数不同
int Add(int a, int b)
{
	return a + b;
}
int Add(int a, int b, int c)
{
	return a + b;
}
1.2.3. 参数类型顺序不同
int Add(char a, int c)
{
	return a + c;
}
int Add(int a, char c)
{
	return a + c;
}
1.3. 注意
1.返回值类型不同无法构成函数重载
int Add(int a, int b)
{
	return a + b;
}
double Add(int a,int b)//error
{
	return a + b;
}
2.缺省值不同也不能构成函数重载
int Add(int a=1, int b=20)
{
	return a + b;
}
int Add(int a=1, int b=2)//error
{
	return a + b;
}
1.4.C++支持函数重载的原理--名字修饰 

为什么返回值不同,缺省值不同就不能构成函数重载呢?这就要涉及C++的函数名修饰规则。

我们在C语言当中学习编译与链接时就知道C/C++程序运行起来要经历的四个阶段:

预处理:头文件展开、宏替换、条件编译、去掉注释,生成 .i 的文件。.h的文件直接被展开。
编译: 语法检查(语法分析、语义分析、词法分析)、符号汇总、生成汇编代码,生成.s文件。
汇编: 把汇编代码转换为二进制机器码,形成符号表,生成.o文件。符号表里存放定义函数的地址信息。
链接: 合并目标文件、段表,符号表的合并和符号表的重定位,.o格式的目标文件合并到一起,生成.out/.exe文件。

1. 实际项目通常是由多个头文件和多个源文件构成,而通过C语言阶段学习的编译链接,我们
可以知道,【当前test.c中调用了add.c中定义的Add函数时】,编译后链接前,tset.o的目标
文件中没有Add的函数正确地址,因为Add是在Add.c中定义的,所以Add的地址在Add.o中。那么
怎么办呢?
2. 所以链接阶段就是专门处理这种问题,链接器看到test.o调用Add,但是没有Add的地址,就
会到Add.o的符号表中找Add的地址,然后链接到一起。
3. 那么链接时,面对Add函数,链接接器会使用哪个名字去找呢?这里每个编译器都有自己的
函数名修饰规则。
4. 由于Windows下vs的修饰规则过于复杂,而Linux下g++的修饰规则简单易懂,下面我们使
用了g++演示了这个修饰后的名字。 

5. 通过下面我们可以看出gcc的函数修饰后名字不变。而g++的函数修饰后变成【_Z+函数长度
+函数名+类型首字母】。

采用C语言编译器编译后结果

采用C++编译器编译后结果 

结论:1.在linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参
数类型信息添加到修改后的名字中。 

2.windows下vs编译器对函数名字修饰规则相对复杂难懂,但道理都
是类似的,我们就不做细致的研究了。

3.. 通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数修
饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。


 4.如果两个函数函数名和参数是一样的,返回值不同是不构成重载的,因为调用时编译器没办
 

2. 引用


2.1. 引用的概念


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

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

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

引用类似于指针,因为指向同一块空间,所以改变引用变量引用实体也会改变。

#include<iostream>
using namespace std;
int main()
{
	int a = 1;
	int& b = a;//引用
	cout << &a << endl;
	cout << &b << endl;
	b++;
	cout << a << endl;
	cout << b << endl;
	return 0;
}

2.2. 注意
1.引用在定义时必须初始化
int&b;//必须初始化
 2.一个变量可以有多个引用
int a=1;
int&b=a;
int&c=a;//多个引用
3.引用一旦引用一个实体,再不能引用其他实体
int a=1;
int b=2;
int& c=a;
int& c=b;//error
2.3. 常引用

我们可以通过const修饰引用来让其变为常引用。这时引用变量是不能被修改的,并且只能将常变量复杂给常引用,不能将常变量赋值给引用。也可以将变量赋值给常引用。

#include<iostream>
using namespace std;
int main()
{
	const int a = 1;//常变量
	const int& b = a;//right
	int& c = a;//error 权限放大不可以
	int c = 2;
	const int& d = c;//right 权限缩小可以
	double pi = 3.14;
	int& e = pi;//error
	//pi是浮点型,赋值给整型类型会发生隐式类型
	//这个隐式类型转换的值是个常变量
	const int& f = pi;
	return 0;
}
2.4. 引用的使用场景 
 2.4.1. 作为函数的参数
int swap(int& a, int& b)
{
	int tmp = 0;
	tmp = a;
	a = b;
	b = tmp;
}

做参数就可以解决C语言中形参的改变无法影响实参的问题。 

2.5. 传值与传引用

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

我们可以通过下列代码观察一下:

#include<iostream>
using namespace std;
#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 < 100000; ++i)
		TestFunc1(a);
	size_t end1 = clock();
	// 以引用作为函数参数
	size_t begin2 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc2(a);
	size_t end2 = clock();
	// 分别计算两个函数运行结束后的时间
	cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
	cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main()
{
	TestRefAndValue();
	return 0;
}

2.6. 引用与指针的区别 

引用的底层实现与指针其实并没有什么区别。

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

但是引用与指针还是有些区别。

3. 内联函数


在C语言中,无论宏常量还是宏函数都有易出错,无法调试等缺陷。而C++为了弥补这一缺陷,一般用const,enum代替宏常量,引入了内联函数的概念代替宏函数。

3.1. 内联函数的定义


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

#include<iostream>
using namespace std;
inline int Add(int x, int y)
{
	return x + y;
}
int main()
{
	Add(1, 2);
	return 0;
}
3.2. 注意


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


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

 因为内联函数会在调用时直接展开,如果声明与定义分离内联函数的地址根本不会进入符号表,链接时就无法找到定义的函数,就会发生链接错误。

4. auto关键字

4.1. auto的简介

在C++中,随着程序越来越复杂,程序所用的类型也越来越复杂。为了简化代码,增加代码的可读性,C++11引入了自动类型推断auto。所以C++11中,标准委员会赋予了auto全新的含义即是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。

int a = 1;
auto b = a;//自动推断b的类型
4.2. 注意
  1. 用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加& 。
int x = 1;
auto a = &x;
auto* b = &x;
auto& c = x;

 

  1. 当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
auto a = 1, b = 2;
auto c = 2, d = 3.14;//error

  1. auto不能作为函数的参数或者声明数组
void TestAuto(auto a)
{
    //auto不能推断形参的类型
}

 5. 范围for

在C++98之前,我们遍历一个数组,需要按照以下的形式:

#include<iostream>
using namespace std;
int main()
{
	int arr[] = { 1,2,3,4,5 };
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		cout << arr[i] << endl;
	}
	return 0;
}

但是在C++11,又引入了一种新的遍历方法——范围for。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围 

#include<iostream>
using namespace std;
int main()
{
	int arr[] = { 1,2,3,4,5 };
	for (auto e : arr)
	{
		cout << e << endl;
	}
	//取数组arr的值依次赋值给e
	//自动递增,自动判断结束
	return 0;
}

由于e是临时变量,所以要想改变数组的值,需要引用。 

#include<iostream>
using namespace std;
int main()
{
	int arr[] = { 1,2,3,4,5 };
	for (auto&e : arr)
	{
		e *= 2;
	}
	return 0;
}

6. nullptr空指针


 在C语言中,定义了一个宏NULL,在传统的C头文件(stddef.h)中,可以看到如下代码 :

#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif

由此我们知道NULL既可以代表数字0,也可以代表空指针。这种模棱两可的定义就可能引出一些问题,比如下面这段代码: 

#include<iostream>
using namespace std;
void func(int a)
{
	cout << "func(int)" << endl;
}
void func(int*p)
{
	cout << "func(int*)" << endl;
}
//函数重载
int main()
{
	func(0);
	func(NULL);
	func((int*)NULL);
	return 0;//输出??
}

我们的本意可能是将NULL当成一个指针,但是在默认情况下NULL被编译器当做数字0。这种问题是我们并不想看见的,所以C++11引入了nullptr来代替NULL。

6.1注意

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

2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。 

 

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值