【C++】C++初阶入门(二)

本文介绍了C++中引用的定义、特性、使用,包括传值与传引用的区别,内联函数的概念及其效率提升,以及auto关键字的用法。还讨论了范围for循环和指针空值nullptr在现代C++中的应用。
摘要由CSDN通过智能技术生成

一、前言

    在上一期的博客中,提到了C++较C语言新增的许多语法。在本次博客中,将会继续介绍更多有趣实用的C++语法,为之后的学习打下坚实基础。

二、引用

    引用不是新定义一个变量,而是给一个已经存在的变量取别名。并且编译器不会给这个别名额外开辟新的内存空间,它和它所引用的变量将共享同一块内存空间

    打个比方,梁山泊一百单八个好汉,都有着各种各样的外号,比如宋江,江湖人称“及时雨”,有时又被叫做“呼保义”,但我们都知道这两个外号都指的是宋江这一个人,不会因为多出两个外号而额外多出两个人。

(一)引用的定义

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

    需要特别注意的是,引用实体和引用类型必须是同种类型!

    从上图中就看出来类型不匹配,编译器就会报错。

void Test()
{
	int a = 10;
	int& a1 = a;   //引用类型和引用实体是同一类型
	printf("%p\n", &a);
	printf("%p\n", &a1);
}

    通过上面的代码和演示结果可以明显看出,变量a和引用变量a1共享同一块内存空间。

(二)引用的特性
1.引用在定义时必须初始化

    引用必须要要有可以引用的对象。给一个变量起别名,那么就必须有这个变量的存在。

void Test()
{
	int a = 10;
	int& a1;    //编译时会报错
}

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

    正如前面所讲的,一个人可以有多个外号,一个变量也可以有多个引用。

void Test()
{
	int a = 10;
	int& a1 = a;
	int& a2 = a;
	printf("%p\n", &a);
	printf("%p\n", &a1);
	printf("%p\n", &a2);

    //a、a1和a2的地址都相同,证明a1和a2都是a的引用

}

3.引用一旦引用一个实体,就再也不能引用其它实体 

 从上图中很容易看出,a1作为a的引用变量和a共享内存空间,a2则是另一个变量,开辟的是新的内存空间。即使将a2赋值给a1,也只是进行了单纯的赋值操作,a1和a2的内存空间并没有共享。

(三)常引用

    用const修饰的引用,我们称之为常引用。

    在代码中,如果需要给带const修饰的变量进行引用的话,那么常引用就可以派上用场了。

常引用也可以帮我们解决前面提到过的类型转换问题。

 那么这是为什么呢?主要是因为在变量类型不同时赋值,编译器会进行隐式类型转换,在这个过程中会生成临时拷贝的变量。以上面的代码为例,a在这个过程中被拷贝到一个临时的变量中,而这个临时拷贝生成的变量具有常性。如果直接引用的话,会导致权限的放大,编译器就会报错。若加上const修饰的话,就是权限的平移,可以实现类型转换。

常引用可以引用普通变量和const修饰的变量,被称之为权限的缩小和平移。普通引用不能引用const修饰的变量,说明权限不能放大。

切记:权限不能放大,只能平移或缩小!

(四)引用的使用
1.做参数

引用可以作为函数的参数来使用。在函数中,函数的参数都是形参,而形参的改变是无法影响实参的。但使用引用的话,就可以直接改变实参,因为引用直接作用于引用实体(实参),形参在函数内部的修改,出了函数作用域仍然可以影响到实参。

void Swap(int& left, int& right)
{
	int temp = left;
	left = right;
	right = temp;
}


int main()
{

	int a = 1;
	int b = 2;

	cout << "a = " << a << endl;
	cout << "b = " << b << endl;

	cout << endl;
	Swap(a, b);

	cout << "a = " << a << endl;
	cout << "b = " << b << endl;

	return 0;
}

2.做返回值

    引用也可以应用在函数返回值中。

     但下面这种情况的结果却令人出乎意料。

在上面的代码中我们可以看到,明明在对引用变量ret赋值后并未做任何修改,打印处理的结果却大相径庭。那么造成这种情况出现的原因是什么?

在程序运行中,内存中会开辟一块空间,用于建立相关的函数栈帧。Add函数第一次调用时,返回值c的结果保存在栈帧之中。在调用结束后,函数栈帧被销毁,内存空间被释放,即失去了访问该内存空间的权限。所以main函数中引用变量ret所引用的是一块已经释放的内存空间,而第一次调用的返回值仍然在内存空间中,所以打印出来的结果是3。

在第二次调用中,再次建立栈帧,访问该空间,将函数运算的结果修改为7。在空间再次被释放后,第二次打印中ret所引用的空间存放的是7,所以打印出来的结果是7。

注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用
引用返回,如果已经还给系统了,则必须使用传值返回!

(五)传值和传引用的效率比较

 传值有传值传参和传值返回。传值的特点是在传参和返回的过程中,不会直接作用于实参或返回值,而是在这个过程中生成一份临时拷贝来完成传参和返回的过程。因此传值的效率显而易见的低下,特别是在数据类型非常大的时候。

传引用就是引用的使用。由于使用引用就相当直接对引用实体进行操作,所以传引用的效率高。

1.传参
#include <time.h>
struct A { int a[10000]; };
void Test1(A a) {}
void Test2(A& a) {}
void Testtime()
{
	A a;
	// 以值作为函数参数
	size_t begin1 = clock();
	for (size_t i = 0; i < 10000; ++i)
		Test1(a);
	size_t end1 = clock();
	// 以引用作为函数参数
	size_t begin2 = clock();
	for (size_t i = 0; i < 10000; ++i)
		Test2(a);
	size_t end2 = clock();
	// 分别计算两个函数运行结束后的时间
	cout << "Test1(A):" << end1 - begin1 << endl;
	cout << "Test2(A&):" << end2 - begin2 << endl;
}

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

2.返回
#include <time.h>
struct A { int a[10000]; };
A a;
// 值返回
A Test1() 
{
	return a;
}
// 引用返回
A& Test2() 
{ 
	return a;
}
void Test()
{
	// 以值作为函数的返回值类型
	size_t begin1 = clock();
	for (size_t i = 0; i < 100000; ++i)
		Test1();
	size_t end1 = clock();
	// 以引用作为函数的返回值类型
	size_t begin2 = clock();
	for (size_t i = 0; i < 100000; ++i)
		Test2();
	size_t end2 = clock();
	// 计算两个函数运算完成之后的时间
	cout << "Test1:" << end1 - begin1 << endl;
	cout << "Test2:" << end2 - begin2 << endl;
}

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

(六)引用和指针的区别

 引用在很多地方的作用与指针相同,比如说函数传值,指针用法是在形参列表中传实参的地址,来达到影响实参的效果。而引用则是直接作用于引用实体来进行操作。在语法层面上,二者并无交集,但在底层实现中,引用是用指针来实现,它是有一块空间的。

通过反汇编可以看出二者的底层实现方式是一样的。 

引用和指针的不同点:

1. 引用概念上定义一个变量的别名,指针存储一个变量地址

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

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

4. 没有NULL引用,但有NULL指针。

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

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

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

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

9. 引用比指针使用起来相对更安全

三、内联函数

(一)内联函数的定义

在C++中。以inline修饰的函数被称为内联函数,编译器在编译时会在调用内联函数的地方展开,减少栈帧建立的开销,提高程序运行的效率。

在功能上与C语言中的宏相同,但效果更好,可以替代宏。在C语言阶段,宏可以增强代码的复用性,提高性能,这些是宏的优点。然而宏的缺点也相当明显:因为在预编译阶段进行了宏替换,所以不方便调试宏,而且代码的可读性差,容易误用,并且宏没有类型安全检查,还容易犯语法错误,所以在C++中,我们常用内联函数来替代宏。

 图中的汇编代码可以看出,程序运行中有调用call指令建立函数栈帧。

加上inline修饰后,无需call指令建立栈帧,函数直接在调用的地方展开,进行相加的操作。

(二)内联函数的特性

1.使用内联函数是一种以空间换时间的方法,在程序的编译阶段,会直接用函数体替换函数调用。好处是减少调用开销,提高运行效率;坏处是整个函数展开使得目标文件变大

2.不同的编译器对inline的处理方式可能不同。内联函数对于编译器而言只是一个建议,编译器也完全可以忽视这个建议。一般建议用inline修饰函数规模小、非递归且频繁调用的函数

3.内联函数不建议声明和定义分离,容易导致链接错误。当内联函数展开时,是没有函数地址的,链接就会找不到。以下是简单示例。

四、 auto关键字

 在之后的深入学习中,程序会越来越复杂,类型也会越来越复杂,主要体现为两个方面:1.类型难于拼写。2.含义不明确导致容易出错。

简单举个例子,std::map<std::string, std::string>::iterator 是一个类型。这个类型的长度就完美符合以上两个特点,难写又难看。在C语言中,也许可以用typedef来给类型取给别名,但也容易遇得到以下困难。

 在编写代码时,常常需要将表达式的值赋给变量,这就要求在声明变量时要明确知道表达式类型,因此auto的作用就体现出来了。

(一)auto的简介

在早期的C++中,auto修饰的变量是具有自动存储器的局部变量。现在的C++11新标准中,auto是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时推导得到。

注意:auoto定义变量时必须初始化!

由上面的代码可得,auto可以自动帮我们推导出类型。

(二)auto的使用
1.auto与指针与引用的结合

auto在声明指针类型时,auto和auto*并没有多大区别,但auto在声明引用时必须使用auto&

2.在同一行定义多个变量

在同一行声明多个变量时,这些变量必须是同类型的变量,否则编译器就会报错。因为编译器实际上只对第一个变量进行推导,然后再通过推导结果定义其它变量。

void test()
{
    auto a = 1, b = 2;
    auto c = 3, d = 4.0; // c和d类型不同,会导致编译报错
}
(三)auto不能推导的场景

1.无法作为函数的参数使用。

2.不能直接声明数组。

3.为了避免和C++98的auto关键字混淆,C++11只保留了auto作为类型指示符的用法。

五、基于范围的for循环

当我们需要遍历一个数组时,可以采用以下方法:

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( 范围内用于迭代的变量 : 被迭代的范围 ) 

 注意:与寻常的for循环类似,可以用continue结束本次循环,也可以用break跳出循环。

范围for循环的使用条件:

1.for循环迭代的范围必须是可以确定的。对于数组来说,就是数组的第一个元素和最后一个元素的范围;对于类来说,需要提供begin和end来确定循环迭代的范围

2.迭代对象要实现++和==的操作。

六、指针空值nullptr

在C语言程序编写中,如果一个指针没有明确的指明对象,一般是赋予NULL值。

int* p = NULL;

NULL在传统的C头文件中,可能被定义为字面常量0,也可能被定义为无类型指针(void*)的常量。这种含糊的定义很容易导致出现以下问题 。

 程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但因为NULL被定义为常量0,导致最终结果与预期不符。如果引入nullptr则将解决这个问题。

注意:

1.在使用nullptr表示指针空值时,无需包含头文件。

2.在C++11中,sizeof(nullptr)和sizeof((void*)0)所占字节数相同。

3.为了提高代码的健壮性,在之后表示指针空值时最后使用nullptr。

七、结语

到此为止,C++的基本入门知识已经全部讲解完毕,如果对您有任何帮助的话,这将是我莫大的荣幸 。感谢观看,下期再见!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值